diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index ae7e5b89085088eb106768f52849e0645a418ae0..27b4d705ba36dd3eb77eca41c2f32185bb0e6347 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -2,8 +2,8 @@ name: Upload translation sources to Crowdin translate.keyman.com on: schedule: - # At 06:00 every two weeks - - cron: '0 6 1,15 * *' + # At 06:00 every day. https://crontab.cronhub.io/ + - cron: '0 6 * * *' jobs: upload-sources-to-crowdin: @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: crowdin action - uses: crowdin/github-action@1.0.4 + uses: crowdin/github-action@1.3.2 with: upload_sources: true @@ -28,7 +28,7 @@ jobs: # TODO if we want action to auto create PRs #GITHUB_TOKEN: $ - # See https://translate.keyman.com/project/keyman/integrations/api + # See https://translate.keyman.com/project/keyman/tools/api project_id: ${{ secrets.CROWDIN_PROJECT_ID }} # A personal access token diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index 7b12bf88809a1ba9766554f89408696050a83ffe..7106ed49b3fa3bb61877cf4a0488b3571a076a93 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -1,4 +1,5 @@ name: "Ubuntu packaging" +run-name: "Ubuntu packaging - ${{ github.event.client_payload.branch }} (branch ${{ github.head_ref }}), by @${{ github.actor }}" on: repository_dispatch: types: ['deb-release-packaging:*', 'deb-pr-packaging:*'] @@ -17,6 +18,8 @@ jobs: VERSION: ${{ steps.version_step.outputs.VERSION }} PRERELEASE_TAG: ${{ steps.prerelease_tag.outputs.PRERELEASE_TAG }} GIT_SHA: ${{ steps.set_status.outputs.GIT_SHA }} + GHA_TEST_BUILD: ${{ github.event.client_payload.isTestBuild }} + GHA_BRANCH: ${{ github.event.client_payload.branch }} steps: - name: Checkout uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c #v3.3.0 @@ -102,7 +105,10 @@ jobs: strategy: fail-fast: true matrix: - dist: [focal, jammy, kinetic, lunar] + # Currently not building mantic until ibus version on mantic stabilizied + # and we can provide a patched version + # dist: [focal, jammy, lunar, mantic] + dist: [focal, jammy, lunar] arch: [amd64] runs-on: ubuntu-latest @@ -113,14 +119,14 @@ jobs: path: artifacts - name: Build - uses: sillsdev/gha-ubuntu-packaging@5b56c86ee78598308d864edc55a1d06b4ba36f92 # v0.3 + uses: sillsdev/gha-ubuntu-packaging@4f3a013ec28f4defc2b3d6ecb04c98815cd9de25 # v0.9 with: dist: "${{ matrix.dist }}" platform: "${{ matrix.arch }}" source_dir: "artifacts/keyman-srcpkg" sourcepackage: "keyman_${{ needs.sourcepackage.outputs.VERSION }}-1.dsc" - deb_fullname: $DEBFULLNAME - deb_email: $DEBEMAIL + deb_fullname: ${{env.DEBFULLNAME}} + deb_email: ${{env.DEBEMAIL}} prerelease_tag: ${{ needs.sourcepackage.outputs.PRERELEASE_TAG }} - name: Output resulting .deb files @@ -159,7 +165,7 @@ jobs: upload-to-llso: name: Upload packages to llso - needs: deb_signing + needs: [sourcepackage, deb_signing] runs-on: self-hosted environment: "deploy (linux)" if: github.event.client_payload.isTestBuild == 'false' diff --git a/HISTORY.md b/HISTORY.md index 9841e4b8388f69fb2eceb57addac3786ad6669b8..3da35e3d94cdb80679856203c1b4f1534ec34489 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,271 @@ # Keyman Version History +## 17.0.153 alpha 2023-08-03 + +* docs(windows): Update OS requirement to Windows 10 (#9381) +* fix(web): maintenance of focus when changing keyboard via Toolbar UI (#9397) +* chore(linux): Remove Kinetic from GHA (#9399) +* chore(linux): Properly treat test builds with packaging GHA (#9400) + +## 17.0.152 alpha 2023-08-02 + +* fix(developer): more wasm uset fixes (#9382) +* docs(windows): corrected nmake cmd for certificates (#9376) +* chore: add run-name to deb-packaging (#9386) +* chore: try another variable for reporting (#9388) +* chore(linux): Remove package build on Jenkins for Keyman 17 (#9380) +* docs(linux): Add build doc for Keyman Web and Android (#9383) + +## 17.0.151 alpha 2023-08-01 + +* feat(developer) marker steps (#9364) +* feat(common): marker processing (#9365) +* chore(linux): Don't fail on parallel builds (#9368) +* fix(developer): fix breakage from emscripten 3.1.44 (#9375) +* docs(core): Document how to build Core on Linux (#9328) + +## 17.0.150 alpha 2023-07-31 + +* chore(linux): Update debian changelog (#9358) +* chore(linux): Fix creation of PRs after uploading to Debian (#9360) + +## 17.0.149 alpha 2023-07-30 + +* fix(core): Better range check for Uni_IsValid() (#9346) +* chore(core): update documentation in transform logic and processor (#9352) + +## 17.0.148 alpha 2023-07-27 + +* feat(core): merge transform/reorder processing w/ u32 (#9293) +* chore(developer): make unknown vkey a hint, not error (#9344) +* chore(linux): Update supported Ubuntu versions (#9341) + +## 17.0.147 alpha 2023-07-25 + +* chore(linux): Update debian changelog (#9327) + +## 17.0.146 alpha 2023-07-24 + +* chore(deps-dev): bump word-wrap from 1.2.3 to 1.2.4 (#9314) + +## 17.0.145 alpha 2023-07-21 + +* fix(linux): Fix logging (#9310) +* fix(windows): open pdf in an external browser (#9295) +* fix(linux): Fix installation of keyboards with lang tag `mul` (#9027) +* fix(web): allows registering precached keyboards (#9304) + +## 17.0.144 alpha 2023-07-20 + +* refactor(linux): Use better way to get username (#9313) + +## 17.0.143 alpha 2023-07-19 + +* (#9292) +* docs(linux): Update man page and remove do-nothing option (#9291) +* docs(linux): Update documentation (#9277) + +## 17.0.142 alpha 2023-07-17 + +* chore(windows): disable wix compression for debug builds (#9074) + +## 17.0.141 alpha 2023-07-15 + +* feat(core): implement reorder (#9223) +* chore(common): add BUILDER_STR_REF (#9248) +* feat(core): load reorder from kmx+, test case (#9260) + +## 17.0.140 alpha 2023-07-14 + +* (#9008) + +## 17.0.139 alpha 2023-07-13 + +* (#9259) +* chore(linux): Add unit tests for dconf_util.py (#9215) +* refactor(linux): Refactor image loading (#9245) + +## 17.0.138 alpha 2023-07-11 + +* chore: Update standards data (#9193) +* chore(linux): Trigger Linux integration tests (#9221) + +## 17.0.137 alpha 2023-07-10 + +* chore: authenticate github request when triggering test builds (#9225) +* feat(web): logs enhanced debug info for error reports (#9217) +* fix(web): silences low-priority warning that shows up in iOS kbd init (#9206) + +## 17.0.136 alpha 2023-07-07 + +* chore(common): Update crowdin strings for Kibaku (#9214) +* feat(core): kmxplus uset implementation in core (#9197) +* chore(linux): Add support for `--no-integration` flag to build files (#9212) +* (#9196) + +## 17.0.135 alpha 2023-07-06 + +* (#9195) +* chore(ios): minor build-script cleanup (#9201) +* chore(linux): Allow to run tests without integration tests (#9192) +* fix(web): sentry sourcemapping round 2 - yesterday's 'fix' unfortunately doesn't upload the maps (#9199) +* chore(web): normalizes sourcemap paths for cleaner Sentry reports (#9200) +* feat(linux): Allow to install keyboard for multiple languages (#9017) + +## 17.0.134 alpha 2023-07-05 + +* fix(linux): Fix packaging GHA (#9172) +* chore(ios): standardizes iOS build scripts (#9092) +* fix(web): fixes up Web's InlinedOSK manual test page (#9175) +* fix(web): re-establishes web srcmap uploading (#9179) +* refactor(linux): Code refactoring (#9129) +* refactor(linux): Extract methods (#9168) +* fix(linux): Fix vertical alignment of labels in About dialog (#9167) +* fix(linux): Add scrollbar to About dialog (#9166) +* fix(linux): Remove tags from package description (#9165) +* docs(linux): Update readme (#9190) +* feat(developer): uset in element string (#9144) +* fix(developer): improve coverage, add todos (#9170) +* chore(developer): refactor so KmnCompiler only created once within kmc (#9171) +* fix(common): support multichar escapes in element sets (#9173) +* chore(linux): Update docker builds (#9188) + +## 17.0.133 alpha 2023-07-04 + +* feat(common): add segmenter for element strings (#9141) +* fix(windows): adds an extra row for Change Hotkey text label (#9149) +* fix(linux): Another attempt to fix packaging GHA (#9155) +* fix(linux): Fix packaging GHA (#9164) +* fix(linux): Fix packaging GHA (#9169) + +## 17.0.132 alpha 2023-06-30 + +* docs(mac): Update requirements.md (#9137) +* fix(linux): Fix GHA package builds for non-Jammy dists (#9123) +* fix(linux): Fix packaging GHA (#9142) + +## 17.0.131 alpha 2023-06-27 + +* feat(developer,common,core): ldml UTF-32 literals in binary (#9084) +* fix(web): fixes Web build zip artifact construction (#9096) + +## 17.0.130 alpha 2023-06-26 + +* chore(deps): bump semver from 7.3.8 to 7.5.2 (#9085) +* epic: conversion of Keyman Engine for Web to ES modules (#8560) + +## 17.0.129 alpha 2023-06-23 + +* chore(common): Update crowdin strings for Spanish (es-ES and es-419) (#9040) +* chore(android/app): Address some pre-launch accessibility report warnings (#9070) +* fix(windows): support kmxplus in mcompile (#9071) +* fix(core): fix ldml transforms (#9072) +* feat(common,developer,core): add uset section (#9049) + +## 17.0.128 alpha 2023-06-22 + +* (#9061) +* docs(windows): add more steps for clarity (#9063) + +## 17.0.127 alpha 2023-06-21 + +* feat(core): implementation of non-regex components of tran (#9019) +* chore: enable common ci for web (#9047) + +## 17.0.126 alpha 2023-06-19 + +* chore(mac): move from altool to notarytool (#9010) + +## 17.0.125 alpha 2023-06-17 + +* fix(android/samples): Cleanup Sample/Tests apk's (#9030) +* chore(common): Bump crowdin github action to 1.3.2 (#9029) + +## 17.0.124 alpha 2023-06-14 + +* fix(windows): qr code id uses keyboard name and not package name (#9002) +* refactor(linux): Extract widgets to separate classes and files (#9007) + +## 17.0.123 alpha 2023-06-13 + +* chore(developer): support cp1252 'ansi' keyboards (#8988) +* fix(developer): keep kmc-kmn messages within namespace (#8999) +* fix(linux): Fix display of installed keyboard version (#8948) +* fix(linux): Fix creation of markdown help files for Linux (#9001) + +## 17.0.122 alpha 2023-06-12 + +* refactor(developer): import kmcmplib errors into kmc-kmn (#8962) + +## 17.0.121 alpha 2023-06-09 + +* fix(linux): Fix disabling of buttons (#8946) +* chore(developer): check for nodejs deps in kmc-kmn (#8961) +* spec(core): spec/impl for transform and vars (#8695) +* (#8967) + +## 17.0.120 alpha 2023-06-07 + +* chore(core): look for emcc.py, not emcc (#8934) +* chore(linux): Move some files to keyman-config (#8917) +* fix(linux): Fix title of reference page and links (#8939) +* fix(android/engine): Re-enable KMManager tests (#8940) + +## 17.0.119 alpha 2023-06-06 + +* chore(developer): ensure kmcmplib messages are all UTF-8 (#8927) +* chore(linux): Fix lintian warnings (#8931) + +## 17.0.118 alpha 2023-06-05 + +* chore(ios): replace fv cert (#8900) +* fix(core): Fix compilation if hotdoc is installed (#8912) +* feat(linux): Rename column and add tooltip (#8918) + +## 17.0.117 alpha 2023-06-04 + +* chore(developer): verify long lines compile correctly (#8915) + +## 17.0.116 alpha 2023-06-02 + +* feat(linux): Add column for installation location (#8897) +* chore(developer): verify kvks files and report errors (#8892) +* refactor(developer): rearrange kmcmplib interface source (#8899) +* refactor(developer): move filename consistency check to kmc (#8907) +* chore(developer): loadFile callback error check and optimization (#8908) +* chore(common): remove url module ref from common/web/types (#8914) + +## 17.0.115 alpha 2023-06-01 + +* refactor(developer): complete fs move out of kmcmplib (#8882) +* fix(common): tweak pack/publish support for npm 9.5.1 and node 18.16.0 (#8894) +* feat(linux): Implement Options page and option to disable Sentry error reporting (#7989) + +## 17.0.114 alpha 2023-05-31 + +* chore(developer): replace cwrap wasm bindings (#8857) +* chore(developer): refactor kmcmplib interfaces (#8870) +* chore(developer): move keyboard repo fixtures (#8874) +* chore(linux): Move build steps to build.sh (#8864) + +## 17.0.113 alpha 2023-05-26 + +* docs(android): Update documentation for building Android on Linux (#8860) +* fix(android): Fix typo in build-utils.sh (#8859) +* refactor(developer): compiler interface part 1 for wasm (#8850) +* chore(developer): remove SetError macro (#8856) + +## 17.0.112 alpha 2023-05-25 + +* refactor(developer): move file write out of WriteCompiledKeyboard and CompileKeyboard merge (#8796) + +## 17.0.111 alpha 2023-05-24 + +* fix(android/app): Pathing for publish script (#8845) +* chore(developer): add test for missing .js file to kmc-package (#8846) +* feat(linux): Add dbus system service (#8509) +* chore(linux): Add missing dependency to GHA (#8849) + ## 17.0.110 alpha 2023-05-22 * fix(common): ensure child builds don't rebuild dependencies unnecessarily (#8834) diff --git a/VERSION.md b/VERSION.md index 15aebb389af96f341e927bdc0adb2baf31917f60..0f0cc2cceb620afffa1e0fbde25d44641e24949c 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.111 \ No newline at end of file +17.0.154 \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore index 09e84019e45cb0f92efbbb0e099af0ca4bf1ce69..9504f1f1b4d8892b7296c8c402dcfb8d0c495777 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -13,6 +13,7 @@ sentry.properties # User-specific stuff: **/.idea/libraries/ **/.idea/dictionaries +**/.idea/shelf/ **/.idea/**/*.xml **/*.iml @@ -34,6 +35,9 @@ gradle_models.ser KMEA/**/assets/ajax-loader.gif KMEA/**/assets/keymanandroid.js KMEA/**/assets/keyman.js.map +KMEA/**/assets/keymanweb-webview.js +KMEA/**/assets/keymanweb-webview.js.map +KMEA/**/assets/sentry.min.js KMEA/**/assets/keyman-sentry.js KMEA/**/assets/es6-shim.min.js KMEA/**/assets/kmwosk.css diff --git a/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml b/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml index 979422b9805855dca392126cc7067bd54bcf8c11..2245ef557a33f9ff3d7e63e992827be324a746d1 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/text_size_controller.xml @@ -21,6 +21,7 @@ android:id="@+id/seekBar" android:layout_width="180dp" android:layout_height="wrap_content" + android:contentDescription="@string/ic_text_size_slider" android:layout_gravity="center" /> + Compartir @@ -25,6 +26,7 @@ Comienza a escribir aquí… + Tamaño del texto: %1$d @@ -82,7 +84,7 @@ No mostrar leyenda en la barra espaciadora - Vibrate when typing + Vibrar al escribir Mostrar siempre el banner @@ -156,5 +158,5 @@ El teclado requiere una versión más reciente de Keyman - Unable to launch web browser + No se puede iniciar el navegador web diff --git a/android/KMAPro/kMAPro/src/main/res/values-ckl-rNG/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-ckl-rNG/strings.xml index b6c15b2315dceb4546f748c10f9c6a0cae0bc23e..30ce93650e0516f0c20bf9cf9be69e671428d9b9 100644 --- a/android/KMAPro/kMAPro/src/main/res/values-ckl-rNG/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values-ckl-rNG/strings.xml @@ -34,6 +34,8 @@ Dzakti ngakurir text Kǝnta ngakurir text + + Sǝr mbǝyati ka su rubwa rubwa ʻya nga ta waci \nShanga text ata fwavuntadzi\n diff --git a/android/KMAPro/kMAPro/src/main/res/values-es-rES/strings.xml b/android/KMAPro/kMAPro/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..836362fadb575016f3daf9c19c858a6061c8a21e --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,162 @@ + + + + + Compartir + + Navegador web + + Tamaño del texto + + Más + + Borrar texto + + Información + + Configuración + + Instalar actualizaciones + + Versión: %1$s + + Keyman requiere Chrome versión 57 o posterior. + + Actualiza Chrome + + Escriba aquí… + + + + + Tamaño del texto: %1$d + + Agrandar texto + + Achicar texto + + \nTodo el texto será borrado\n + + Empezar + + Añadir un teclado para su idioma + + Habilitar Keyman como teclado de sistema + + Establecer teclado como teclado predeterminado + + Más información + + Mostrar \"%1$s\" al inicio + + Para instalar los paquetes de teclado, permita que Keyman acceda al almacenamiento externo. + + Se ha denegado la solicitud de permiso de almacenamiento. La instalación del paquete de teclado podría fallar + + Configuración + + + Idiomas instalados (%1$d) + Idiomas instalados (%1$d) + + + Instalar teclado o diccionario + + Mostrar idioma + + Ajustar altura del teclado + + Leyenda del teclado + + Teclado + + Idioma + + Idioma + Teclado + + Vacío + + Mostrar nombre del teclado en la barra espaciadora + + Mostrar el nombre del idioma en la barra espaciadora + + Idioma y teclado en la barra espaciadora + + No mostrar leyenda en la barra espaciadora + + Vibrar al escribir + + Mostrar siempre el banner + + Próximamente implementado + + Cuando esté desactivado, sólo mostrado cuando el texto predictivo esté habilitado + + Permitir el envío de informes de fallos a través de la red + + Cuando esté activado, se enviarán los informes de fallos + + Cuando esté desactivado, los informes de fallos no se enviarán + + Instalar desde keyman.com + + Instalar desde archivo local + + Instalar desde otro dispositivo + + Añadir idiomas al teclado instalado + + (desde el paquete de teclados) + + Seleccione módulo de teclado + + Seleccionar idiomas para %1$s + + Añadido idioma %1$s a %2$s + + Todos los idiomas ya están instalados + + Mueve el teclado para reajustar la altura + + Rotar el dispositivo para ajustar a modo vertical u horizontal + + Restablecer valores predeterminados + + Buscar o escribir una dirección web + + Favoritos + + No hay favoritos + + Añadir a Favoritos + + Título + + Dirección web + + El módulo %1$s no se instaló + + Descargando el módulo del teclado\n%1$s… + + Error al extraer + + Instalar teclado + + Instalar diccionario + + %1$s no es un archivo de paquete Keyman válido.\n%2$s\" + + El paquete del teclado no tiene teclados optimizados para el tacto para instalar + + No hay nuevo texto predictivo para instalar + + No hay teclados ni texto predictivo para instalar + + El paquete del teclado no tiene idiomas asociados para instalar + + Metadata inválida/ausente en paquete + + Flipboard requires a newer version of Keyman + + No se puede iniciar el navegador web + diff --git a/android/KMAPro/kMAPro/src/main/res/values/strings.xml b/android/KMAPro/kMAPro/src/main/res/values/strings.xml index acb93e3a834e5b6787f6a536b8dae8734f2ab03f..5524b2b6647cab6c65f7a2fa4a8f916be024d305 100644 --- a/android/KMAPro/kMAPro/src/main/res/values/strings.xml +++ b/android/KMAPro/kMAPro/src/main/res/values/strings.xml @@ -57,6 +57,9 @@ Text size down + + Text size slider + \nAll text will be cleared\n diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 1bfcb0b1d660f1581e5914ed7f1876073cca076d..25e4223032e861c401d1775a1e2a4e31c41c6989 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -10,7 +10,7 @@ var oskHeight = Math.ceil(window.jsInterface.getKeyboardHeight() / window.device var oskWidth = 0; var fragmentToggle = 0; -var sentryManager = new com.keyman.KeymanSentryManager({ +var sentryManager = new KeymanSentryManager({ hostPlatform: "android" }); sentryManager.init(); @@ -25,24 +25,24 @@ function init() { //document.body.style.backgroundColor="transparent"; //window.console.log('Device type = '+device); //window.console.log('Keyboard height = '+oskHeight); - keyman.init({'app':device,'fonts':'packages/',root:'./'}); - keyman.util.setOption('attachType','manual'); - keyman.oninserttext = insertText; keyman.showKeyboardList = showMenu; - keyman.menuKeyUp = menuKeyUp; keyman.hideKeyboard = hideKeyboard; + keyman.menuKeyUp = menuKeyUp; keyman.getOskHeight = getOskHeight; keyman.getOskWidth = getOskWidth; keyman.beepKeyboard = beepKeyboard; - var ta = document.getElementById('ta'); - keyman.setActiveElement(ta); - - ta.readOnly = false; - checkTextArea(); - - // Tell KMW the default banner height to use - com.keyman.osk.Banner.DEFAULT_HEIGHT = - Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio); + keyman.init({ + 'embeddingApp':device, + 'fonts':'packages/', + oninserttext: insertText, + root:'./' + }).then(function () { // Note: For non-upgraded API 21, arrow functions will break the keyboard! + const bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio); + + // The OSK is not available until initialization is complete. + keyman.osk.bannerView.activeBannerHeight = bannerHeight; + keyman.refreshOskLayout(); + }); keyman.addEventListener('keyboardloaded', setIsChiral); keyman.addEventListener('keyboardchange', setIsChiral); @@ -71,7 +71,7 @@ function setBannerHeight(h) { osk.banner.height = Math.ceil(h / window.devicePixelRatio); } // Refresh KMW OSK - keyman.correctOSKTextSize(); + keyman.refreshOskLayout(); } function setOskHeight(h) { @@ -81,7 +81,7 @@ function setOskHeight(h) { if(keyman && keyman.core && keyman.core.activeKeyboard) { keyman.core.activeKeyboard.refreshLayouts(); } - keyman.correctOSKTextSize(); + keyman.refreshOskLayout(); } function setOskWidth(w) { @@ -111,7 +111,7 @@ function onStateChange(change) { //window.console.log('onStateChange change: ' + change); // Refresh KMW OSK - keyman.correctOSKTextSize(); + keyman.refreshOskLayout(); fragmentToggle = (fragmentToggle + 1) % 100; if(change != 'configured') { // doesn't change the display; only initiates suggestions. @@ -129,16 +129,11 @@ function setIsChiral(keyboardProperties) { function setKeymanLanguage(k) { KeymanWeb.registerStub(k); - keyman.setActiveKeyboard(k.KP + '::'+k.KI, k.KLC); - keyman.osk.show(true); + keyman.setActiveKeyboard(k.KI, k.KLC); } function setSpacebarText(mode) { - keyman.options['spacebarText'] = mode; - keyman.osk.show(true); - - // Refresh KMW OSK - keyman.correctOSKTextSize(); + keyman.config.spacebarText = mode; } // #6665: we need to know when the user has pressed a hardware key so we don't @@ -164,29 +159,25 @@ function insertText(dn, s, dr) { } function deregisterModel(modelID) { - keyman.modelManager.deregister(modelID); + keyman.removeModel(modelID); } function enableSuggestions(model, mayPredict, mayCorrect) { // Set the options first so that KMW's ModelManager can properly handle model enablement states // the moment we actually register the new model. - keyman.osk.banner.setOptions({ - 'mayPredict': mayPredict, - 'mayCorrect': mayCorrect - }); + keyman.core.languageProcessor.mayPredict = mayPredict; + keyman.core.languageProcessor.mayCorrect = mayCorrect; registerModel(model); } function setBannerOptions(mayPredict) { - keyman.osk.banner.setOptions({ - 'mayPredict': mayPredict - }); + keyman.core.languageProcessor.mayPredict = mayPredict; } function registerModel(model) { //window.console.log('registerModel: ' + model); - keyman.registerModel(model); + keyman.addModel(model); } function resetContext() { @@ -201,16 +192,15 @@ function setNumericLayer() { } function updateKMText(text) { - var ta = document.getElementById('ta'); - console_debug('updateKMText(text='+text+') ta.value='+ta.value); - if(text == undefined) { text = ''; } - if(ta.value != text) { - ta.value = text; - window.resetContext(); + console_debug('updateKMText(text='+text+') context.value='+keyman.context.getText()); + + if(text != keyman.context.getText()) { + keyman.context.setText(text); + keyman.resetContext(); } } @@ -221,22 +211,19 @@ function console_debug(s) { } function updateKMSelectionRange(start, end) { - var ta = document.getElementById('ta'); - console_debug('updateKMSelectionRange('+start+','+end+'): ta.selectionStart='+ta.selectionStart+' '+ - '['+ta._KeymanWebSelectionStart+'] ta.selectionEnd='+ta.selectionEnd+' '+ta._KeymanWebSelectionEnd); + var context = keyman.context; + + // console_debug('updateKMSelectionRange('+start+','+end+'): context.selStart='+ta.selectionStart+' '+ + // '['+ta._KeymanWebSelectionStart+'] context.selEnd='+ta.selectionEnd+' '+ta._KeymanWebSelectionEnd); - var selDirection = 'forward'; if(start > end) { var e0 = end; end = start; start = e0; - selDirection = 'backward'; } - if(ta.selectionStart != start || ta.selectionEnd != end || ta.selectionDirection != selDirection) { - ta.selectionStart = ta._KeymanWebSelectionStart = start; - ta.selectionEnd = ta._KeymanWebSelectionEnd = end; - ta.selectionDirection = selDirection; + if(context.selStart != start || context.selEnd != end) { + keyman.context.setSelection(start, end); keyman.resetContext(); } } @@ -330,7 +317,7 @@ function hideKeyboard() { function showKeyboard() { // Refresh KMW OSK - keyman.correctOSKTextSize(); + keyman.refreshOskLayout(); } function executePopupKey(keyID, keyText) { @@ -339,19 +326,24 @@ function executePopupKey(keyID, keyText) { keyman.executePopupKey(keyID); } +// Cannot make it explicitly async / await on API 21. function executeHardwareKeystroke(code, shift, lstates, eventModifiers) { console_debug('executeHardwareKeystroke(code='+code+',shift='+shift+',lstates='+lstates+',eventModifiers='+eventModifiers+')'); - try { - executingHardwareKeystroke = true; - if (keyman.executeHardwareKeystroke(code, shift, lstates)) { // false if matched, true if not + + executingHardwareKeystroke = true; + + // Would be cleaner if we could async / await here, which would give us a simple try-catch implementation. + var promise = keyman.hardKeyboard.raiseKeyEvent(code, shift, lstates); + promise.then(function (result) { + if(result) { // false if matched, true if not // KMW didn't process the key, so have the Android app dispatch the key with the original event modifiers window.jsInterface.dispatchKey(code, eventModifiers); + executingHardwareKeystroke = false; } - executingHardwareKeystroke = false; - } catch(e) { + }).catch(function (e) { window.console.log('executeHardwareKeystroke exception: '+e); executingHardwareKeystroke = false; - } + }); } function popupVisible(value) { @@ -373,24 +365,3 @@ function toHex(theString) { } return hexString; } - -/** - * Check the WebView version and determine if the textarea that KeymanWeb uses needs to be "visible". - * Normally, this textarea is not displayed to avoid redundant layout calculations. - * In older WebViews on Android 5.0 though, selectionStart and selectionEnd positions fail to - * update unless the textarea is visible. - * Reference: Issue #5376 - */ -function checkTextArea() { - var uaRe = /Chrome\/([0-9]*)\./g; - var chromeMajorVersion = uaRe.exec(navigator.userAgent); - if (chromeMajorVersion && parseInt(chromeMajorVersion[1]) <= 37) { - var ta = document.getElementById('ta'); - if (ta != null) { - ta.style.display = ''; - ta.style.position = 'absolute'; - ta.style.left = '-500px'; - ta.style.top = '0px'; - } - } -} diff --git a/android/KMEA/app/src/main/assets/keyboard.html b/android/KMEA/app/src/main/assets/keyboard.html index e677ba4c46ec95cec1d0dbbafb670b6951b25482..b11a21124eca991f2417bb409e3d11cf11182c32 100644 --- a/android/KMEA/app/src/main/assets/keyboard.html +++ b/android/KMEA/app/src/main/assets/keyboard.html @@ -15,7 +15,8 @@ polyfill. --> - + + - diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java b/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java index a84282d85f20a17c52468bcb2778123e37eb1471..f437e563dbf7665d514f9685110537fa3ac123ba 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/DisplayLanguages.java @@ -45,6 +45,7 @@ public class DisplayLanguages { new DisplayLanguageType("cs-CZ", "čeština (Czech)"), new DisplayLanguageType("ff-NG", "Ɗemngal Fulfulde"), new DisplayLanguageType("en", "English"), + new DisplayLanguageType("es-ES", "Español (Spanish)"), new DisplayLanguageType("es-419", "Español (Spanish - Latin America)"), new DisplayLanguageType("fr-FR", "French"), new DisplayLanguageType("ha-HG", "Hausa"), diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 88dc290322cb9ba588934c855f3ee79afa13e229..bc0aed1d04965b9b138d354e2ae26f42c277915e 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -29,6 +29,7 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Color; @@ -223,8 +224,11 @@ final class KMKeyboard extends WebView { getSettings().setUseWideViewPort(true); getSettings().setLoadWithOverviewMode(true); - setWebContentsDebuggingEnabled(true); - + if (0 != (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE)) { + // Enable debugging of WebView via adb. Not used during unit tests + // Refer: https://developer.chrome.com/docs/devtools/remote-debugging/webviews/#configure_webviews_for_debugging + setWebContentsDebuggingEnabled(true); + } setWebChromeClient(new WebChromeClient() { public boolean onConsoleMessage(ConsoleMessage cm) { String msg = KMString.format("KMW JS Log: Line %d, %s:%s", cm.lineNumber(), cm.sourceId(), cm.message()); diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index ebb8727bef53e5c39457f7f5bfeb1fcad89d7e82..6777de8133961cce45df728e8ba23abcd49f45cb 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -272,9 +272,10 @@ public final class KMManager { // Keyman files protected static final String KMFilename_KeyboardHtml = "keyboard.html"; - protected static final String KMFilename_JSEngine = "keymanandroid.js"; - protected static final String KMFilename_JSEngine_Sourcemap = "keyman.js.map"; - protected static final String KMFilename_JSSentry = "keyman-sentry.js"; + protected static final String KMFilename_JSEngine = "keymanweb-webview.js"; + protected static final String KMFilename_JSEngine_Sourcemap = "keymanweb-webview.js.map"; + protected static final String KMFilename_JSSentry = "sentry.min.js"; + protected static final String KMFilename_JSSentryInit = "keyman-sentry.js"; protected static final String KMFilename_AndroidHost = "android-host.js"; protected static final String KMFilename_KmwCss = "kmwosk.css"; protected static final String KMFilename_KmwGlobeHintCss = "globe-hint.css"; @@ -624,8 +625,11 @@ public final class KMManager { return; } - RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); - keyboard.setLayoutParams(params); + if (!isTestMode()) { + // Keyboard layout not needed in unit tests. #5125 + RelativeLayout.LayoutParams params = getKeyboardLayoutParams(); + keyboard.setLayoutParams(params); + } keyboard.setVerticalScrollBarEnabled(false); keyboard.setHorizontalScrollBarEnabled(false); keyboard.setWebViewClient(webViewClient); @@ -796,6 +800,7 @@ public final class KMManager { copyAsset(context, KMFilename_KeyboardHtml, "", true); copyAsset(context, KMFilename_JSEngine, "", true); copyAsset(context, KMFilename_JSSentry, "", true); + copyAsset(context, KMFilename_JSSentryInit, "", true); copyAsset(context, KMFilename_AndroidHost, "", true); if(KMManager.isDebugMode()) { copyAsset(context, KMFilename_JSEngine_Sourcemap, "", true); diff --git a/android/KMEA/app/src/main/res/values-b+es+419/strings.xml b/android/KMEA/app/src/main/res/values-b+es+419/strings.xml index 1bd3df0568fc62cd658794fbb2b16ca7510b662d..8cefeeeefd2547ff86268455e69b72453d94216f 100644 --- a/android/KMEA/app/src/main/res/values-b+es+419/strings.xml +++ b/android/KMEA/app/src/main/res/values-b+es+419/strings.xml @@ -168,5 +168,5 @@ Pulsa aquí para cambiar el teclado - Unable to launch web browser + No se puede iniciar el navegador web diff --git a/android/KMEA/app/src/main/res/values-es-rES/strings.xml b/android/KMEA/app/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..11b94984ae65c86af7328fe680ac75da58d24ddd --- /dev/null +++ b/android/KMEA/app/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,172 @@ + + + + + + + Teclado + Teclados + + + + Otro método de entrada + Otros métodos de entrada + + + Añadir nuevo teclado + + Idiomas instalados + + %1$s Ajustes + + Añadir + + Atrás + + Cancelar + + Cerrar + + Cerrar Keyman + + Adelante + + Siguiente método de entrada + + Descargar + + Instalar + + Después + + Siguiente + + Aceptar + + Actualizar + + Sin conexión a internet + + ¡No se puede conectar con el servidor Keyman! + + ¿Desea eliminar este teclado? + + ¿Desea descargar la última versión de este teclado? + + ¿Desea actualizar teclados y diccionarios ahora? + + Actualización de recursos + + Actualizaciones de recursos disponibles + + %1$s (Actualización disponible) + + Actualizaciones disponibles para el teclado %1$s: %2$s + + Actualizaciones disponibles para el diccionario %1$s: %2$s + + Versión de teclado + + Enlace de ayuda + + Desinstalar teclado + + [new] %1$s + + Escanee este código para cargar este \nteclado en otro dispositivo + + Bienvenido a %1$s + + Biblioteca de FileProvider necesaria para ver el archivo de ayuda: %1$s + + Error de teclado fatal con %1$s:%2$s para %3$s idioma. Cargando teclado predeterminado. + + Error en el teclado %1$s:%2$s para %3$s idioma. + + Comprobando si hay un diccionario asociado para descargar + No se puede conectar al servidor Keyman para comprobar si hay un diccionario asociado para descargar + + ¿Desea descargar la última versión de este diccionario? + + Ningún diccionario para descargar + + El catálogo de recursos no está disponible + + Actualización del catálogo iniciada en segundo plano + + El catálogo todavía está descargando; ¡Inténtalo de nuevo en un momento! + + Buscando recurso + + La descarga del teclado inició en segundo plano + + El teclado seleccionado ya se está descargando; ¡por favor, inténtalo de nuevo en un momento! + + ¡La descarga del teclado ha finalizado! + + La descarga del diccionario inició en segundo plano + + El diccionario seleccionado ya se está descargando; ¡por favor, inténtalo de nuevo en un momento! + + La descarga del diccionario ha finalizado. + + Descargar falló + + Falló al recuperar el archivo descargado + + ¡Falló al acceder al servidor! + + "¡Todos los recursos están actualizados!" + + ¡Uno o más recursos no se han actualizado! + + ¡Recursos actualizados con éxito! + + Versión del diccionario + + Desinstalar diccionario + + ¿Desea eliminar este teclado? + + Diccionario eliminado + + Teclado %1$s instalado + + Teclado eliminado + + Habilitar correcciones + + Habilitar predicciones + + Diccionario + + Diccionario + Diccionarios + + + Comprobar diccionario disponible + Buscar diccionarios en línea + + Diccionario: %1$s + + %1$s diccionarios + + + Diccionario instalado + + + (%1$d teclado) + (%1$d teclados) + + + Configuración regional predeterminada + + + + Eliminar + + + Pulsa aquí para cambiar el teclado + + No se puede iniciar el navegador web + diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 50f52c5ef34dbb68dd4f69ead9e2dd93ad21fcd3..eceebd47be11a259860e5857e6b28392b4896f95 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -251,7 +251,7 @@ Default Locale - Image Button + Image Image View diff --git a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java index 97515b719086d625bfd00ea213b3231cc77f2c1c..59a0237b377f44a0c226af1c11f69627c8eb66ae 100644 --- a/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java +++ b/android/KMEA/app/src/test/java/com/keyman/engine/KMManagerTest.java @@ -29,13 +29,14 @@ public class KMManagerTest { private static final String OLD_KEYBOARDS_LIST = "old_keyboards_list.dat"; ArrayList> dat_list; - @Before - public void loadOldKeyboardsList() throws FileNotFoundException { + // For some keyboard list tests, load an existing keyboard list. + // Can't use @Before because context is null before running tests. + public void loadOldKeyboardsList() { KMManager.initialize(ApplicationProvider.getApplicationContext(), KMManager.KeyboardType.KEYBOARD_TYPE_INAPP); File keyboards_dat = new File(TEST_RESOURCE_ROOT, OLD_KEYBOARDS_LIST); if (keyboards_dat == null || !keyboards_dat.exists()) { - throw new FileNotFoundException(); + Assert.fail(TAG + ": keyboards list not found"); } try { ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(keyboards_dat)); @@ -46,7 +47,6 @@ public class KMManagerTest { } } - @Ignore("Investigate ResourcesNotFoundException") @Test public void test_getTier() { String versionName = "14.0.248-alpha"; @@ -109,9 +109,9 @@ public class KMManagerTest { /* * This test is manually run to edit/regenerate the old keyboards list */ - @Ignore @Test public void create_newKeyboardsList() { + loadOldKeyboardsList(); dat_list = new ArrayList>(); HashMap usInfo = new HashMap(); @@ -181,9 +181,9 @@ public class KMManagerTest { } } - @Ignore("Investigate ResourcesNotFoundException") @Test public void test_updateOldKeyboardsList() { + loadOldKeyboardsList(); Assert.assertNotNull(dat_list); // Verify old keyboards list contains deprecated keyboards diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index c479d9a13375a226ef115a6820c5c3efecb14855..ff78403c03ef0992d44d3e96fe22e26a6fe24a3e 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -29,7 +29,8 @@ TEST_FLAGS="-x aR lintRelease testRelease" # Gradle test w/o build JUNIT_RESULTS="##teamcity[importData type='junit' path='keyman\android\KMEA\app\build\test-results\testReleaseUnitTest\']" builder_describe "Builds Keyman Engine for Android." \ - "@/web" \ + "@/web/src/app/webview" \ + "@/common/web/sentry-manager" \ "clean" \ "configure" \ "build" \ @@ -49,7 +50,6 @@ if builder_is_debug_build; then fi builder_describe_outputs \ - configure:engine /android/KMEA/app/src/main/assets/keymanandroid.js \ build:engine /android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar #### Build @@ -82,14 +82,15 @@ if builder_start_action build:engine; then # Copy KeymanWeb artifacts echo "Copying Keyman Web artifacts" - cp "$KEYMAN_WEB_ROOT/build/app/embed/$CONFIG/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif" - cp "$KEYMAN_WEB_ROOT/build/app/embed/$CONFIG/keyman.js" "$ENGINE_ASSETS/keymanandroid.js" - cp "$KEYMAN_WEB_ROOT/build/app/embed/$CONFIG/keyman.js.map" "$ENGINE_ASSETS/keyman.js.map" - cp "$KEYMAN_WEB_ROOT/build/app/embed/$CONFIG/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css" - cp "$KEYMAN_WEB_ROOT/build/app/embed/$CONFIG/osk/globe-hint.css" "$ENGINE_ASSETS/globe-hint.css" - cp "$KEYMAN_WEB_ROOT/build/app/embed/$CONFIG/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf" - - cp "$KEYMAN_ROOT/common/web/sentry-manager/build/index.js" "$ENGINE_ASSETS/keyman-sentry.js" + cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js" "$ENGINE_ASSETS/keymanweb-webview.js" + cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js.map" "$ENGINE_ASSETS/keymanweb-webview.js.map" + cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif" + cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css" + cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/globe-hint.css" "$ENGINE_ASSETS/globe-hint.css" + cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/keymanweb-osk.ttf" "$ENGINE_ASSETS/keymanweb-osk.ttf" + + cp "$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" "$ENGINE_ASSETS/sentry.min.js" + cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$ENGINE_ASSETS/keyman-sentry.js" echo "Copying es6-shim polyfill" cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$ENGINE_ASSETS/es6-shim.min.js" diff --git a/android/README.md b/android/README.md index 2c776873a396f948e8df1a16c58d27042f0e5b20..9b138ccfceae6e50d134894959c4ddc57a17a925 100644 --- a/android/README.md +++ b/android/README.md @@ -1,33 +1,48 @@ # Keyman for Android & Keyman Engine for Android ## Prerequisites -See [build configuration](../docs/build/index.md) for details on how to configure your build environment. + +See [build configuration](../docs/build/index.md) for details on how to configure +your build environment. ## Keyman Minimum Android Requirements + Keyman for Android has a minSdkVersion of 21 for [Android 5.0 Lollipop](https://developer.android.com/about/versions/lollipop) ## Keyman for Android Development -Keyman for Android (formerly named KMAPro) can be built from a command line (preferred) or Android Studio. -Building Keyman Web is a precursor for compiling KMEA, so verify your system has all the [Minimum Web Compilation Requirements](../web/README.md#minimum-web-compilation-requirements) +Keyman for Android (formerly named KMAPro) can be built from a command line +(preferred) or Android Studio. + +Building Keyman Web is a precursor for compiling KMEA, so verify your system has +all the [Minimum Web Compilation Requirements](../web/README.md#minimum-web-compilation-requirements) + +When attempting to build via Android Studio the first time, you'll also need to +accept the SDK licenses. Depending on your OS, you may need to run a command +line similar to: -When attempting to build via Android Studio the first time, you'll also need to accept the SDK licenses. Depending on your OS, you may need to run a command line similar to: ```bash yes | ~/Library/Android/sdk/tools/bin/sdkmanager --licenses ``` ### Crash Reporting -Keyman for Android uses [Sentry](https://sentry.io) for crash reporting at a server https://sentry.keyman.com. The analytics for Debug are associated with an App Bundle ID `com.tavultesoft.kmapro.debug`. - + +Keyman for Android uses [Sentry](https://sentry.io) for crash reporting. The +analytics for Debug are associated with an App Bundle ID +`com.tavultesoft.kmapro.debug`. + ### Compiling From Command Line + 1. Launch a command prompt and cd to the directory **keyman/android** 2. Run the top level build script `./build.sh configure build --debug` which will: * Compile KMEA (and its KMW dependency) * Download default keyboard and dictionary resources as needed * Compile KMAPro -3. The APK will be found in **keyman/android/KMAPro/kMAPro/build/outputs/apk/debug/keyman-${version}.apk** where `${version}` is the current version number. +3. The APK will be found in **keyman/android/KMAPro/kMAPro/build/outputs/apk/debug/keyman-\${version}.apk** + where `${version}` is the current version number. ### Compiling From Android Studio + 1. Ensure that [Keyman Engine for Android](#how-to-build-keyman-engine-for-android) is built. 2. Launch Android Studio and import the Gradle project **keyman/android/KMAPro/build.gradle** 3. From the project view, *configure* anything that Gradle reports. @@ -37,24 +52,31 @@ Keyman for Android uses [Sentry](https://sentry.io) for crash reporting at a ser 3. Change Module to kMAPro 4. Name your run configuration kMAPro (or as desired) 5. Run your new configuration: Select Run --> Run 'kMAPro' -6. Select a physical device or create a new virtual device to match your target API version - For Ubuntu 18.04, you will need to add your user to the `kvm` group for permission accessing the emulator. -``` +6. Select a physical device or create a new virtual device to match your target + API version. + + For Ubuntu 18.04, you will need to add your user to the `kvm` group + for permission accessing the emulator. + + ```bash sudo apt install qemu-kvm sudo adduser kvm -``` + ``` ### Running From Command Line + 1. Launch a command prompt 2. Ensure that a physical device is connected or an emulator is running. 1. To list physical devices: `$ANDROID_HOME/platform-tools/adb.exe devices` 2. To list created emulator devices: `$ANDROID_HOME/tools/emulator.exe -list-avds` - 3. [Create a new virtual device](https://developer.android.com/studio/run/managing-avds.html) if there are no - existing devices. + 3. [Create a new virtual device](https://developer.android.com/studio/run/managing-avds.html) + if there are no existing devices. 4. Start an emulator: `$ANDROID_HOME/tools/emulator.exe @nameofemulator` 3. Load the APK: `$ANDROID_HOME/platform-tools/adb.exe install -r path/to/apk.apk` - * If multiple devices are connected you may need `$ANDROID_HOME/platform-tools/adb.exe install -r -s SERIAL path/to/apk.apk`. Replace `SERIAL` with - the device serial number listed in step 2. + + * If multiple devices are connected you may need + `$ANDROID_HOME/platform-tools/adb.exe install -r -s SERIAL path/to/apk.apk`. + Replace `SERIAL` with the device serial number listed in step 2. ### Compiling the app's offline help Keyman for Android help is maintained in the Markdown files in android/help/. diff --git a/android/Samples/KMSample1/build.sh b/android/Samples/KMSample1/build.sh index 12c72c78b9dfac285001ebd8ada0b3e72ceeb712..30cf4d7d66a112f19fa5edd71578d3eac5572eb5 100755 --- a/android/Samples/KMSample1/build.sh +++ b/android/Samples/KMSample1/build.sh @@ -36,14 +36,15 @@ builder_describe "Build KMSample1 app for Android." \ # parse before describe_outputs to check debug flags builder_parse "$@" +ARTIFACT="app-release-unsigned.apk" + if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" SAMPLE_FLAGS="assembleDebug" + ARTIFACT="app-$CONFIG.apk" fi -ARTIFACT="app-$CONFIG.apk" - builder_describe_outputs \ configure /android/Samples/KMSample1/app/libs/keyman-engine.aar \ diff --git a/android/Samples/KMSample2/build.gradle b/android/Samples/KMSample2/build.gradle index 2210ec42b7d0f15ea0dd0c6e2f2342eab8e50b1a..d764f927ded6a435fd5d35c4ffbc98814fb84e60 100644 --- a/android/Samples/KMSample2/build.gradle +++ b/android/Samples/KMSample2/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.4.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/Samples/KMSample2/build.sh b/android/Samples/KMSample2/build.sh index 100669c251197c6233bbabb966e7102081375607..af70979d4a41dfa8d3eacf3b57b70b733e2c1bbd 100755 --- a/android/Samples/KMSample2/build.sh +++ b/android/Samples/KMSample2/build.sh @@ -36,13 +36,15 @@ builder_describe "Build KMSample2 app for Android." \ # parse before describe_outputs to check debug flags builder_parse "$@" +ARTIFACT="app-release-unsigned.apk" + if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" SAMPLE_FLAGS="assembleDebug" + ARTIFACT="app-$CONFIG.apk" fi -ARTIFACT="app-$CONFIG.apk" builder_describe_outputs \ diff --git a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp index 9db5e29cc24510d3493b9d38ad6e812e45dd006a..3a8f1cb624cd78f89c8104361f9287581c4dde49 100644 Binary files a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp and b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp differ diff --git a/android/Tests/KeyboardHarness/build.sh b/android/Tests/KeyboardHarness/build.sh index f1b3f5c0979ba041cfb8a070002e1c4d40b0ed0e..413b1e08da4ae45a0e094f948513d942a04745be 100755 --- a/android/Tests/KeyboardHarness/build.sh +++ b/android/Tests/KeyboardHarness/build.sh @@ -35,14 +35,16 @@ builder_describe "Build KeyboardHarness test app for Android." \ # parse before describe outputs to check debug flags builder_parse "$@" +ARTIFACT="app-release-unsigned.apk" + if builder_is_debug_build; then builder_heading "### Debug config ####" CONFIG="debug" BUILD_FLAGS="assembleDebug -x lint -x test" TEST_FLAGS="-x assembleDebug lintDebug testDebug" + ARTIFACT="app-$CONFIG.apk" fi -ARTIFACT="app-$CONFIG.apk" builder_describe_outputs \ configure /android/Tests/KeyboardHarness/app/libs/keyman-engine.aar \ diff --git a/common/models/templates/.c8rc.json b/common/models/templates/.c8rc.json new file mode 100644 index 0000000000000000000000000000000000000000..deaa1318eb28ff4b2ea2e4be41504cfb1a7a362e --- /dev/null +++ b/common/models/templates/.c8rc.json @@ -0,0 +1,14 @@ +{ + "check-coverage": false, + "clean": true, + "exclude": [ + "node_modules/**", + "test/**" + ], + "exclude-after-remap": true, + "reporter": ["text", "text-summary"], + "reports-dir": "build/coverage", + "src": [ + "src/" + ] +} \ No newline at end of file diff --git a/common/models/templates/build.sh b/common/models/templates/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..592b47178360181c5e829d63c11a5c7ad7000c8f --- /dev/null +++ b/common/models/templates/build.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Compile our sourcemap-path remapping module for use by Web builds, releases, etc. +# +set -eu + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")" +. "$(dirname "$THIS_SCRIPT")/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$(dirname "$THIS_SCRIPT")" + +################################ Main script ################################ + +builder_describe "Builds the predictive-text model template implementation module" \ + "@/common/web/keyman-version" \ + "@/common/web/es-bundling" \ + "@/common/models/wordbreakers" \ + "clean" \ + "configure" \ + "build" \ + "test" \ + "--ci" + +builder_describe_outputs \ + configure /node_modules \ + build build/obj/index.js + +builder_parse "$@" + + +function do_build() { + tsc -b + + # Declaration bundling. + tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts +} + +function do_test() { + local FLAGS= + if builder_has_option --ci; then + FLAGS="-reporter mocha-teamcity-reporter" + fi + + c8 mocha $FLAGS --require test/helpers.js --recursive test +} + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/models/templates/package.json b/common/models/templates/package.json index 67699e342a93ecf2ab13046640bc8472f87d5351..0bec0fc88fa189d31d3743bd8dbe6c96ed23eb5e 100644 --- a/common/models/templates/package.json +++ b/common/models/templates/package.json @@ -16,8 +16,19 @@ ], "homepage": "https://github.com/keymanapp/keyman#readme", "license": "MIT", - "main": "index.js", - "types": "index.d.ts", + "main": "./build/obj/index.js", + "types": "./build/obj/index.d.ts", + "exports": { + ".": "./build/obj/index.js", + "./lib": { + "types": "./build/lib/index.d.ts", + "import": "./build/lib/index.mjs", + "require": "./build/lib/index.cjs" + }, + "./obj/*.js": "./build/obj/*.js" + }, + + "type": "module", "directories": { "test": "test" }, @@ -32,9 +43,8 @@ "url": "git+https://github.com/keymanapp/keyman.git" }, "scripts": { - "build": "tsc", - "pretest": "tsc -b tsconfig.bundled.json", - "test": "mocha -r test/helpers.js" + "build": "gosh ./build.sh", + "test": "gosh ./build.sh test" }, "bugs": { "url": "https://github.com/keymanapp/keyman/issues" @@ -45,11 +55,16 @@ "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", "@types/node": "^14.0.4", + "c8": "^7.12.0", "chai": "^4.3.4", "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", "typescript": "^4.9.5" }, "dependencies": { - "@keymanapp/models-wordbreakers": "*" + "@keymanapp/keyman-version": "*", + "@keymanapp/models-types": "*", + "@keymanapp/models-wordbreakers": "*", + "@keymanapp/resources-gosh": "*" } } diff --git a/common/models/templates/src/common.ts b/common/models/templates/src/common.ts index 9117ec7d6f4c40df3b29133926e005ee2b021119..9f3eae1dc1a8c23db83682073d7dac795dd03c76 100644 --- a/common/models/templates/src/common.ts +++ b/common/models/templates/src/common.ts @@ -1,148 +1,151 @@ -namespace models { - export const SENTINEL_CODE_UNIT = '\uFDD0'; - - export function applyTransform(transform: Transform, context: Context): Context { - // First, get the current context - let fullLeftContext = context.left || ''; - let lLen = fullLeftContext.kmwLength(); - let lDel = lLen < transform.deleteLeft ? lLen : transform.deleteLeft; - - let leftContext = fullLeftContext.kmwSubstr(0, lLen - lDel) + (transform.insert || ''); - - let fullRightContext = context.right || ''; - let rLen = fullRightContext.kmwLength(); - let rDel = rLen < transform.deleteRight ? rLen : transform.deleteRight; - - let rightContext = fullRightContext.kmwSubstr(rDel); - - return { - left: leftContext, - right: rightContext, - startOfBuffer: context.startOfBuffer, - endOfBuffer: context.endOfBuffer, - casingForm: context.casingForm - }; - } +// Allows the kmwstring bindings to resolve. +import { extendString } from "@keymanapp/web-utils"; - /** - * Merges two Transforms as if they were applied to a `Context` successively. - * @param first - * @param second - */ - export function buildMergedTransform(first: Transform, second: Transform): Transform { - // These exist to avoid parameter mutation. - let mergedFirstInsert: string = first.insert; - let mergedSecondDelete: number = second.deleteLeft; - - // The 'fun' case: the second Transform wants to delete something from the first. - if(second.deleteLeft) { - let firstLength = first.insert.kmwLength(); - if(firstLength <= second.deleteLeft) { - mergedFirstInsert = ''; - mergedSecondDelete = second.deleteLeft - firstLength; - } else { - mergedFirstInsert = first.insert.kmwSubstr(0, firstLength - second.deleteLeft); - mergedSecondDelete = 0; - } - } +extendString(); - return { - insert: mergedFirstInsert + second.insert, - deleteLeft: first.deleteLeft + mergedSecondDelete, - // As `first` would affect the context before `second` could take effect, - // this is the correct way to merge `deleteRight`. - deleteRight: (first.deleteRight || 0) + (second.deleteRight || 0) - } - } +export const SENTINEL_CODE_UNIT = '\uFDD0'; - /** - * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 high surrogate. - * - * @param char A single JavaScript (UCS-2) char corresponding to a single code unit. - */ - export function isHighSurrogate(char: string): boolean; - /** - * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 high surrogate. - * - * @param codeUnit A code unit corresponding to a single UCS-2 char. - */ - export function isHighSurrogate(codeUnit: number): boolean; - export function isHighSurrogate(codeUnit: string|number): boolean { - if(typeof codeUnit == 'string') { - codeUnit = codeUnit.charCodeAt(0); - } +export function applyTransform(transform: Transform, context: Context): Context { + // First, get the current context + let fullLeftContext = context.left || ''; + let lLen = fullLeftContext.kmwLength(); + let lDel = lLen < transform.deleteLeft ? lLen : transform.deleteLeft; - return codeUnit >= 0xD800 && codeUnit <= 0xDBFF; - } + let leftContext = fullLeftContext.kmwSubstr(0, lLen - lDel) + (transform.insert || ''); - /** - * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 low surrogate. - * - * @param char A single JavaScript (UCS-2) char corresponding to a single code unit. - */ - export function isLowSurrogate(char: string): boolean; - /** - * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 low surrogate. - * - * @param codeUnit A code unit corresponding to a single UCS-2 char. - */ - export function isLowSurrogate(codeUnit: number): boolean; - export function isLowSurrogate(codeUnit: string|number): boolean { - if(typeof codeUnit == 'string') { - codeUnit = codeUnit.charCodeAt(0); + let fullRightContext = context.right || ''; + let rLen = fullRightContext.kmwLength(); + let rDel = (rLen < (transform.deleteRight ?? 0)) ? rLen : (transform.deleteRight ?? 0); + + let rightContext = fullRightContext.kmwSubstr(rDel); + + return { + left: leftContext, + right: rightContext, + startOfBuffer: context.startOfBuffer, + endOfBuffer: context.endOfBuffer, + casingForm: context.casingForm + }; +} + +/** + * Merges two Transforms as if they were applied to a `Context` successively. + * @param first + * @param second + */ +export function buildMergedTransform(first: Transform, second: Transform): Transform { + // These exist to avoid parameter mutation. + let mergedFirstInsert: string = first.insert; + let mergedSecondDelete: number = second.deleteLeft; + + // The 'fun' case: the second Transform wants to delete something from the first. + if(second.deleteLeft) { + let firstLength = first.insert.kmwLength(); + if(firstLength <= second.deleteLeft) { + mergedFirstInsert = ''; + mergedSecondDelete = second.deleteLeft - firstLength; + } else { + mergedFirstInsert = first.insert.kmwSubstr(0, firstLength - second.deleteLeft); + mergedSecondDelete = 0; } + } - return codeUnit >= 0xDC00 && codeUnit <= 0xDFFF; + return { + insert: mergedFirstInsert + second.insert, + deleteLeft: first.deleteLeft + mergedSecondDelete, + // As `first` would affect the context before `second` could take effect, + // this is the correct way to merge `deleteRight`. + deleteRight: (first.deleteRight || 0) + (second.deleteRight || 0) } +} - export function isSentinel(char: string): boolean { - return char == models.SENTINEL_CODE_UNIT; +/** + * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 high surrogate. + * + * @param char A single JavaScript (UCS-2) char corresponding to a single code unit. + */ +export function isHighSurrogate(char: string): boolean; +/** + * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 high surrogate. + * + * @param codeUnit A code unit corresponding to a single UCS-2 char. + */ +export function isHighSurrogate(codeUnit: number): boolean; +export function isHighSurrogate(codeUnit: string|number): boolean { + if(typeof codeUnit == 'string') { + codeUnit = codeUnit.charCodeAt(0); } + return codeUnit >= 0xD800 && codeUnit <= 0xDBFF; +} + /** - * Builds a Suggestion based on a Transform corresponding to a predictive-text op. - * - * Assumes that the Transform's `insert` property represents a completed word, - * as models generally delete the whole prefix, replacing it with the full lexical entry. - * @param transform - */ - export function transformToSuggestion(transform: Transform): Suggestion; - export function transformToSuggestion(transform: Transform, p: number): WithOutcome; - export function transformToSuggestion(transform: Transform, p?: number): Outcome { - let suggestion: Outcome = { - transform: transform, - transformId: transform.id, - displayAs: transform.insert - }; - - if(p === 0 || p) { - suggestion.p = p; - } - return suggestion; + * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 low surrogate. + * + * @param char A single JavaScript (UCS-2) char corresponding to a single code unit. + */ +export function isLowSurrogate(char: string): boolean; +/** + * Checks whether or not the specified UCS-2 character corresponds to a UTF-16 low surrogate. + * + * @param codeUnit A code unit corresponding to a single UCS-2 char. + */ +export function isLowSurrogate(codeUnit: number): boolean; +export function isLowSurrogate(codeUnit: string|number): boolean { + if(typeof codeUnit == 'string') { + codeUnit = codeUnit.charCodeAt(0); + } + + return codeUnit >= 0xDC00 && codeUnit <= 0xDFFF; +} + +export function isSentinel(char: string): boolean { + return char == SENTINEL_CODE_UNIT; +} + +/** + * Builds a Suggestion based on a Transform corresponding to a predictive-text op. + * + * Assumes that the Transform's `insert` property represents a completed word, + * as models generally delete the whole prefix, replacing it with the full lexical entry. + * @param transform + */ +export function transformToSuggestion(transform: Transform): Suggestion; +export function transformToSuggestion(transform: Transform, p: number): WithOutcome; +export function transformToSuggestion(transform: Transform, p?: number): Outcome { + let suggestion: Outcome = { + transform: transform, + transformId: transform.id, + displayAs: transform.insert + }; + + if(p === 0 || p) { + suggestion.p = p; } + return suggestion; +} - export function defaultApplyCasing(casing: CasingForm, text: string): string { - switch(casing) { - case 'lower': - return text.toLowerCase(); - case 'upper': - return text.toUpperCase(); - case 'initial': - // The length of the first code unit, as measured in code points. - let headUnitLength = 1; - - // Is the first character a high surrogate, indicating possible use of UTF-16 - // surrogate pairs? Also, is the string long enough for there to BE a pair? - if(text.length > 1 && isHighSurrogate(text.charAt(0))) { - // It's possible, so now we check for low surrogates. - if(isLowSurrogate(text.charCodeAt(1))) { - // We have a surrogate pair; this pair is the 'first' character. - headUnitLength = 2; - } +export function defaultApplyCasing(casing: CasingForm, text: string): string { + switch(casing) { + case 'lower': + return text.toLowerCase(); + case 'upper': + return text.toUpperCase(); + case 'initial': + // The length of the first code unit, as measured in code points. + let headUnitLength = 1; + + // Is the first character a high surrogate, indicating possible use of UTF-16 + // surrogate pairs? Also, is the string long enough for there to BE a pair? + if(text.length > 1 && isHighSurrogate(text.charAt(0))) { + // It's possible, so now we check for low surrogates. + if(isLowSurrogate(text.charCodeAt(1))) { + // We have a surrogate pair; this pair is the 'first' character. + headUnitLength = 2; } + } - // Capitalizes the first code unit of the string, leaving the rest intact. - return text.substring(0, headUnitLength).toUpperCase() .concat(text.substring(headUnitLength)); - } + // Capitalizes the first code unit of the string, leaving the rest intact. + return text.substring(0, headUnitLength).toUpperCase() .concat(text.substring(headUnitLength)); } } diff --git a/common/models/templates/src/index.ts b/common/models/templates/src/index.ts index a20b43f60284806d2f8c14dbc7e685b4949c48dc..f828066846b72d8ef14a4482598935759cb744d5 100644 --- a/common/models/templates/src/index.ts +++ b/common/models/templates/src/index.ts @@ -1,7 +1,22 @@ -/// -/// +import { + SENTINEL_CODE_UNIT, applyTransform, buildMergedTransform, isHighSurrogate, isLowSurrogate, isSentinel, + transformToSuggestion, defaultApplyCasing +} from "./common.js"; +import PriorityQueue, { Comparator } from "./priority-queue.js"; +import QuoteBehavior from "./quote-behavior.js"; +import { Tokenization, tokenize, getLastPreCaretToken, wordbreak } from "./tokenization.js"; +import TrieModel, { TrieModelOptions } from "./trie-model.js"; -// Add all namespaces defined here to the global scope: -if (typeof module != 'undefined' && typeof module.exports != 'undefined') { - module.exports['models'] = models; -} +import { extendString } from "@keymanapp/web-utils"; + +// This package requires our string-extension functions. +extendString(); + +export { + SENTINEL_CODE_UNIT, applyTransform, buildMergedTransform, isHighSurrogate, isLowSurrogate, isSentinel, + transformToSuggestion, defaultApplyCasing, // "common.ts" + PriorityQueue, Comparator, // "priority-queue.ts" + QuoteBehavior, // "quote-behavior.ts", + Tokenization, tokenize, getLastPreCaretToken, wordbreak, // "tokenization.ts" + TrieModel, TrieModelOptions // "trie-model.ts" +}; diff --git a/common/models/templates/src/priority-queue.ts b/common/models/templates/src/priority-queue.ts index c372d881c58074ad3caf1e502066d163244a697c..8ecae0533f37b808886f946d2f5f74ffe1fe728a 100644 --- a/common/models/templates/src/priority-queue.ts +++ b/common/models/templates/src/priority-queue.ts @@ -4,216 +4,215 @@ * Defines a mildly abstracted priority queue implementation. */ -namespace models { +/** + * Used to compare two instances of a type. + * @returns + * - value < 0 if `a` should come before `b` + * - value > 0 if `b` should come before `a` + * - 0 if they should be treated equally. + */ +export type Comparator = (a: Type, b: Type) => number; + +export default class PriorityQueue { + + private comparator: Comparator; + private heap: Type[]; + /** - * Used to compare two instances of a type. - * @returns - * - value < 0 if `a` should come before `b` - * - value > 0 if `b` should come before `a` - * - 0 if they should be treated equally. + * Constructs an empty priority queue. + * @param comparator A `Comparator` returning negative values when and only when + * the first parameter should precede the second parameter. */ - export type Comparator = (a: Type, b: Type) => number; - - export class PriorityQueue { - - private comparator: Comparator; - private heap: Type[]; - - /** - * Constructs an empty priority queue. - * @param comparator A `Comparator` returning negative values when and only when - * the first parameter should precede the second parameter. - */ - constructor(comparator: Comparator, initialEntries: Type[] = []) { - // TODO: We may wish to allow options specifying a limit or threshold for adding - // items to the priority queue. Possibly both. - // - // When that time comes, consider a min-max heap. - // https://en.wikipedia.org/wiki/Min-max_heap - this.comparator = comparator; - - this.heap = Array.from(initialEntries); - this.heapify(); - } + constructor(comparator: Comparator, initialEntries: Type[] = []) { + // TODO: We may wish to allow options specifying a limit or threshold for adding + // items to the priority queue. Possibly both. + // + // When that time comes, consider a min-max heap. + // https://en.wikipedia.org/wiki/Min-max_heap + this.comparator = comparator; + + this.heap = Array.from(initialEntries); + this.heapify(); + } - private static leftChildIndex(index: number): number { - return index * 2 + 1; - } + private static leftChildIndex(index: number): number { + return index * 2 + 1; + } - private static rightChildIndex(index: number): number { - return index * 2 + 2; - } + private static rightChildIndex(index: number): number { + return index * 2 + 2; + } - private static parentIndex(index: number): number { - return Math.floor((index-1)/2); - } + private static parentIndex(index: number): number { + return Math.floor((index-1)/2); + } - /** - * Maintains internal state, rearranging the internal state until all heap constraints - * are properly satisfied. - * - O(N) when 'heapifying' the whole heap - * - O(N) worst-case for partial heap operations (as part of an enqueueAll) - *

- */ - private heapify(): void; - private heapify(start: number, end: number): void; - private heapify(start?: number, end?: number): void { - if(start == undefined || end == undefined) { - this.heapify(0, this.count - 1); - } + /** + * Maintains internal state, rearranging the internal state until all heap constraints + * are properly satisfied. + * - O(N) when 'heapifying' the whole heap + * - O(N) worst-case for partial heap operations (as part of an enqueueAll) + *

+ */ + private heapify(): void; + private heapify(start: number, end: number): void; + private heapify(start?: number, end?: number): void { + if(start == undefined || end == undefined) { + this.heapify(0, this.count - 1); + return; + } - // Use of 'indices' here is a bit of a customization. - // At the cost of (temporary) extra storage space, we can more efficiently enqueue - // multiple elements simultaneously. - let queuedIndices: number[] = []; - let lastParent = -1; - - for(let i = end; i >= start; i--) { - let parent = PriorityQueue.parentIndex(i); - if(this.siftDown(i) && parent < start && lastParent != parent) { - // We only need to queue examination for a heap node if its children have changed - // and it isn't already being examined. - queuedIndices.push(parent); - lastParent = parent; - } + // Use of 'indices' here is a bit of a customization. + // At the cost of (temporary) extra storage space, we can more efficiently enqueue + // multiple elements simultaneously. + let queuedIndices: number[] = []; + let lastParent = -1; + + for(let i = end; i >= start; i--) { + let parent = PriorityQueue.parentIndex(i); + if(this.siftDown(i) && parent < start && lastParent != parent) { + // We only need to queue examination for a heap node if its children have changed + // and it isn't already being examined. + queuedIndices.push(parent); + lastParent = parent; } + } - lastParent = -1; - while(queuedIndices.length > 0) { - let index = queuedIndices.shift(); - let parent = PriorityQueue.parentIndex(index); - if(this.siftDown(index) && parent >= 0 && lastParent != parent) { - // We only need to queue examination for a heap node if its children have changed. - queuedIndices.push(parent); - lastParent = parent; - } + lastParent = -1; + while(queuedIndices.length > 0) { + let index = queuedIndices.shift() as number; + let parent = PriorityQueue.parentIndex(index); + if(this.siftDown(index) && parent >= 0 && lastParent != parent) { + // We only need to queue examination for a heap node if its children have changed. + queuedIndices.push(parent); + lastParent = parent; } } + } - /** - * Returns the number of elements currently held by the priority queue. - */ - get count(): number { - return this.heap.length; - } + /** + * Returns the number of elements currently held by the priority queue. + */ + get count(): number { + return this.heap.length; + } - /** - * Returns the highest-priority item within the priority queue. - *

- * Is O(1). - */ - peek() { - return this.heap[0]; // undefined if it doesn't exist... which is completely correct. - } + /** + * Returns the highest-priority item within the priority queue. + *

+ * Is O(1). + */ + peek() { + return this.heap[0]; // undefined if it doesn't exist... which is completely correct. + } - /** - * Inserts a new element into the priority queue, placing it in order. - *

- * Is O(log N), where N = # of items in the priority queue. - * @param element - */ - enqueue(element: Type) { - let index = this.heap.length; - this.heap.push(element); - - let parent = PriorityQueue.parentIndex; - let parentIndex = parent(index); - while(index !== 0 && this.comparator(this.heap[index], this.heap[parentIndex]) < 0) { - let a = this.heap[index]; - this.heap[index] = this.heap[parentIndex]; - this.heap[parentIndex] = a; - - index = parentIndex; - parentIndex = parent(index); - } + /** + * Inserts a new element into the priority queue, placing it in order. + *

+ * Is O(log N), where N = # of items in the priority queue. + * @param element + */ + enqueue(element: Type) { + let index = this.heap.length; + this.heap.push(element); + + let parent = PriorityQueue.parentIndex; + let parentIndex = parent(index); + while(index !== 0 && this.comparator(this.heap[index], this.heap[parentIndex]) < 0) { + let a = this.heap[index]; + this.heap[index] = this.heap[parentIndex]; + this.heap[parentIndex] = a; + + index = parentIndex; + parentIndex = parent(index); } + } - /** - * Efficiently batch-enqueues multiple elements. - * Worst-case is the _better_ of the following: - * - O(`elements.count` + `heap.count`) - large element counts will trigger in-place - * heap reconstruction. - * - O(`elements.count` * log(`heap.count`)) - logarithmic when elements.count << heap.count - * @param elements A group of elements to enqueue simultaneously. - */ - enqueueAll(elements: Type[]) { - if(elements.length == 0) { - return; - } - - let firstIndex = this.count - this.heap = this.heap.concat(elements); - let firstParent = PriorityQueue.parentIndex(firstIndex); - - // The 'parent' of index 0 will return -1, which is illegal. - this.heapify(firstParent >= 0 ? firstParent : 0, PriorityQueue.parentIndex(this.count-1)); + /** + * Efficiently batch-enqueues multiple elements. + * Worst-case is the _better_ of the following: + * - O(`elements.count` + `heap.count`) - large element counts will trigger in-place + * heap reconstruction. + * - O(`elements.count` * log(`heap.count`)) - logarithmic when elements.count << heap.count + * @param elements A group of elements to enqueue simultaneously. + */ + enqueueAll(elements: Type[]) { + if(elements.length == 0) { + return; } - /** - * Removes the highest-priority element from the queue, returning it. - *

- * Is O(log N), where N = number of items in the priority queue. - */ - dequeue(): Type { - if(this.count == 0) { - return undefined; - } + let firstIndex = this.count + this.heap = this.heap.concat(elements); + let firstParent = PriorityQueue.parentIndex(firstIndex); - const root = this.heap[0]; - let tail = this.heap.pop(); - if(this.heap.length > 0) { - this.heap[0] = tail; - this.siftDown(0); - } + // The 'parent' of index 0 will return -1, which is illegal. + this.heapify(firstParent >= 0 ? firstParent : 0, PriorityQueue.parentIndex(this.count-1)); + } - return root; + /** + * Removes the highest-priority element from the queue, returning it. + *

+ * Is O(log N), where N = number of items in the priority queue. + */ + dequeue(): Type | undefined { + if(this.count == 0) { + return undefined; } - /** - * Compares the entry at the specified index against its children, - * propagating it downward within the heap until heap requirements are specified. - *

- * Is O(log N), where N = number of items in the priority queue. - * - * @param index The index of the top-most node that must be examined - * for repositioning. - * @returns `true` if a swap occurred, `false` otherwise. - */ - private siftDown(index: number): boolean { - let leftIndex = PriorityQueue.leftChildIndex(index); - let rightIndex = PriorityQueue.rightChildIndex(index); - let topMostIndex = index; - - if(leftIndex < this.heap.length && this.comparator(this.heap[leftIndex], this.heap[topMostIndex]) < 0) { - topMostIndex = leftIndex; - } + const root = this.heap[0]; + let tail = this.heap.pop() as Type; + if(this.heap.length > 0) { + this.heap[0] = tail; + this.siftDown(0); + } - if(rightIndex < this.heap.length && this.comparator(this.heap[rightIndex], this.heap[topMostIndex]) < 0) { - topMostIndex = rightIndex; - } + return root; + } - if(topMostIndex != index) { - let a = this.heap[index]; - this.heap[index] = this.heap[topMostIndex]; - this.heap[topMostIndex] = a; + /** + * Compares the entry at the specified index against its children, + * propagating it downward within the heap until heap requirements are specified. + *

+ * Is O(log N), where N = number of items in the priority queue. + * + * @param index The index of the top-most node that must be examined + * for repositioning. + * @returns `true` if a swap occurred, `false` otherwise. + */ + private siftDown(index: number): boolean { + let leftIndex = PriorityQueue.leftChildIndex(index); + let rightIndex = PriorityQueue.rightChildIndex(index); + let topMostIndex = index; - this.siftDown(topMostIndex); - return true; - } else { - return false; - } + if(leftIndex < this.heap.length && this.comparator(this.heap[leftIndex], this.heap[topMostIndex]) < 0) { + topMostIndex = leftIndex; } - /** - * Returns an array containing all entries of the priority queue. - * Altering the returned array will not affect the queue, but mutating - * the array's elements can cause unintended side effects. - * - * This function makes no guarantees on the ordering of the returned elements; - * they will almost certainly be unsorted. - */ - toArray(): Type[] { - return Array.from(this.heap); + if(rightIndex < this.heap.length && this.comparator(this.heap[rightIndex], this.heap[topMostIndex]) < 0) { + topMostIndex = rightIndex; } + + if(topMostIndex != index) { + let a = this.heap[index]; + this.heap[index] = this.heap[topMostIndex]; + this.heap[topMostIndex] = a; + + this.siftDown(topMostIndex); + return true; + } else { + return false; + } + } + + /** + * Returns an array containing all entries of the priority queue. + * Altering the returned array will not affect the queue, but mutating + * the array's elements can cause unintended side effects. + * + * This function makes no guarantees on the ordering of the returned elements; + * they will almost certainly be unsorted. + */ + toArray(): Type[] { + return Array.from(this.heap); } -} +} \ No newline at end of file diff --git a/common/models/templates/src/quote-behavior.ts b/common/models/templates/src/quote-behavior.ts index 7a95d04c49617afa382caf89f500cc736c38833e..2ca7bbd04c413e4d8d35d44d97ea414fb7421b33 100644 --- a/common/models/templates/src/quote-behavior.ts +++ b/common/models/templates/src/quote-behavior.ts @@ -1,38 +1,38 @@ -namespace models { - export enum QuoteBehavior { - noQuotes = "no-quotes", - useQuotes = "use-quotes", - default = "default-quotes" - } - - // For an "enum/namespace merge". See https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-namespaces-with-classes - export namespace QuoteBehavior { - /** - * Applies the specified quote behavior to the provided `Transform`, mutating it as appropriate. - * @param transform The Transform to be mutated - * @param punctuation The active `LexicalModelPunctuation` settings - * @param defaultTo The default quote behavior to use (in case the current value is `.default`) - */ - export function apply(behavior: QuoteBehavior, text: string, punctuation: LexicalModelPunctuation, defaultTo: QuoteBehavior): string { - if(defaultTo == QuoteBehavior.default || !defaultTo) { - throw "Specified quote behavior may be ambiguous - default behavior not specified (may not be .default)"; - } - - if(behavior == QuoteBehavior.default) { - behavior = defaultTo; - } - - switch(behavior) { - case QuoteBehavior.noQuotes: - return text; - case QuoteBehavior.useQuotes: - let {open, close} = punctuation.quotesForKeepSuggestion; - - // This part's simple enough, at least. - return open + text + close; - default: - throw "Unsupported quote behavior state detected; implementation missing!"; - } +export enum QuoteBehavior { + noQuotes = "no-quotes", + useQuotes = "use-quotes", + default = "default-quotes" +} + +// For an "enum/namespace merge". See https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-namespaces-with-classes +export namespace QuoteBehavior { + /** + * Applies the specified quote behavior to the provided `Transform`, mutating it as appropriate. + * @param transform The Transform to be mutated + * @param punctuation The active `LexicalModelPunctuation` settings + * @param defaultTo The default quote behavior to use (in case the current value is `.default`) + */ + export function apply(behavior: QuoteBehavior, text: string, punctuation: LexicalModelPunctuation, defaultTo: QuoteBehavior): string { + if(defaultTo == QuoteBehavior.default || !defaultTo) { + throw "Specified quote behavior may be ambiguous - default behavior not specified (may not be .default)"; + } + + if(behavior == QuoteBehavior.default) { + behavior = defaultTo; + } + + switch(behavior) { + case QuoteBehavior.noQuotes: + return text; + case QuoteBehavior.useQuotes: + let {open, close} = punctuation.quotesForKeepSuggestion; + + // This part's simple enough, at least. + return open + text + close; + default: + throw "Unsupported quote behavior state detected; implementation missing!"; } } -} \ No newline at end of file +} + +export default QuoteBehavior; \ No newline at end of file diff --git a/common/models/templates/src/tokenization.ts b/common/models/templates/src/tokenization.ts index 9993050eab3dc86bbc8f1ed92f8a74ec97ae4b2e..2aa5c392e6d923df44954b0cf6bef3cf7453cbdb 100644 --- a/common/models/templates/src/tokenization.ts +++ b/common/models/templates/src/tokenization.ts @@ -1,129 +1,127 @@ // While we _could_ define this within @keymanapp/models-wordbreakers instead, it's probably -// better to leave that package as _just_ the wordbreakers. - -namespace models { - export interface Tokenization { - /** - * An array of tokens to the left of the caret. If the caret is in the middle of a token, - * only the part to the left of the caret is included. - */ - left: USVString[], - - /** - * An array of tokens to the right of the caret. If the caret is in the middle of a token, - * only the part to the right of the caret is included. - */ - right: USVString[], - - /** - * A flag indicating whether or not the caret's position in the context caused a token - * to be split. If `true`, the last entry of `left` is from the same token as the first - * entry of `right`. - */ - caretSplitsToken: boolean - } +// better to leave that package as _just_ the wordbreakers. - export function tokenize(wordBreaker: WordBreakingFunction, context?: Partial): Tokenization { - context = context || { - left: undefined, - startOfBuffer: undefined, - endOfBuffer: undefined - }; +export interface Tokenization { + /** + * An array of tokens to the left of the caret. If the caret is in the middle of a token, + * only the part to the left of the caret is included. + */ + left: USVString[], - let leftSpans = wordBreaker(context.left || '') || []; - let rightSpans = wordBreaker(context.right || '') || []; + /** + * An array of tokens to the right of the caret. If the caret is in the middle of a token, + * only the part to the right of the caret is included. + */ + right: USVString[], - let leftTail: Span; - if(leftSpans.length > 0) { - leftTail = leftSpans[leftSpans.length - 1]; - } + /** + * A flag indicating whether or not the caret's position in the context caused a token + * to be split. If `true`, the last entry of `left` is from the same token as the first + * entry of `right`. + */ + caretSplitsToken: boolean +} + +export function tokenize(wordBreaker: WordBreakingFunction, context?: Partial): Tokenization { + context = context || { + left: undefined, + startOfBuffer: undefined, + endOfBuffer: undefined + }; + + let leftSpans = wordBreaker(context.left || '') || []; + let rightSpans = wordBreaker(context.right || '') || []; + + let leftTail: Span; + if(leftSpans.length > 0) { + leftTail = leftSpans[leftSpans.length - 1]; + } - // Handle any desired special handling for directly-pre-caret scenarios - where for this - // _specific_ context, we should not make a token division where one normally would exist otherwise. - // - // One notable example: word-final apostrophe is tokenized separate from preceding text, but - // word-internal apostrophe is treated as part of the same word (i.e, English contractions). - // But, if the user is editing text and the caret is directly after a caret, there's a notable - // chance they may in the middle of typing a contraction. Refer to - // https://github.com/keymanapp/keyman/issues/6572. - if(leftSpans.length > 1) { - const leftTailBase = leftSpans[leftSpans.length - 2]; - - // If the final two pre-caret spans are adjacent - without intervening whitespace... - if(leftTailBase.end == leftTail.start) { - // Ideal: if(leftTailBase is standard-char-class && leftTail is single-quote-class) - // But we don't have character class access here; it's all wordbreaker-function internal. - // Upon inspection of the wordbreaker data definitions... the single-quote-class is ONLY "'". - // So... we'll just be lazy for now and append the `'`. - if(leftTail.text == "'") { - let mergedSpan: Span = { - text: leftTailBase.text + leftTail.text, - start: leftTailBase.start, - end: leftTail.end, - length: leftTailBase.length + leftTail.length - }; - - leftSpans.pop(); // leftTail - leftSpans.pop(); // leftTailBase - leftSpans.push(mergedSpan); - leftTail = mergedSpan; // don't forget to update the `leftTail` Span! - } + // Handle any desired special handling for directly-pre-caret scenarios - where for this + // _specific_ context, we should not make a token division where one normally would exist otherwise. + // + // One notable example: word-final apostrophe is tokenized separate from preceding text, but + // word-internal apostrophe is treated as part of the same word (i.e, English contractions). + // But, if the user is editing text and the caret is directly after a caret, there's a notable + // chance they may in the middle of typing a contraction. Refer to + // https://github.com/keymanapp/keyman/issues/6572. + if(leftSpans.length > 1) { + const leftTailBase = leftSpans[leftSpans.length - 2]; + + // If the final two pre-caret spans are adjacent - without intervening whitespace... + if(leftTailBase.end == leftTail!.start) { + // Ideal: if(leftTailBase is standard-char-class && leftTail is single-quote-class) + // But we don't have character class access here; it's all wordbreaker-function internal. + // Upon inspection of the wordbreaker data definitions... the single-quote-class is ONLY "'". + // So... we'll just be lazy for now and append the `'`. + if(leftTail!.text == "'") { + let mergedSpan: Span = { + text: leftTailBase.text + leftTail!.text, + start: leftTailBase.start, + end: leftTail!.end, + length: leftTailBase.length + leftTail!.length + }; + + leftSpans.pop(); // leftTail + leftSpans.pop(); // leftTailBase + leftSpans.push(mergedSpan); + leftTail = mergedSpan; // don't forget to update the `leftTail` Span! } } + } - // With any 'detokenization' cases already handled, we may now begin to build the return object. - let tokenization: Tokenization = { - left: leftSpans.map(span => span.text), - right: rightSpans.map(span => span.text), - - // A default initialization of the value. - caretSplitsToken: false - }; - - // Now the hard part - determining whether or not the caret caused a token split. - if(leftSpans.length > 0 && rightSpans.length > 0) { - let rightHead = rightSpans[0]; - - // If tokenization includes all characters on each side of the caret, - // we have a good candidate for a caret-splitting scenario. - let leftSuffixWordbreak = leftTail.end != context.left.length; - let rightPrefixWordbreak = rightHead.start != 0; - - if(leftSuffixWordbreak || rightPrefixWordbreak) { - // Bypass the final test, as we already know the caret didn't split a token. - // (The tokenization process already removed characters between the two.) - return tokenization; - } + // With any 'detokenization' cases already handled, we may now begin to build the return object. + let tokenization: Tokenization = { + left: leftSpans.map(span => span.text), + right: rightSpans.map(span => span.text), - // Worth note - some languages don't use wordbreaking characters. So, a final check: - // - // Does the wordbreaker split a merge of the 'two center' tokens? - // If not, then the caret is responsible for the split. - if(wordBreaker(leftTail.text + rightHead.text).length == 1) { - tokenization.caretSplitsToken = true; - } - } + // A default initialization of the value. + caretSplitsToken: false + }; - return tokenization; - } + // Now the hard part - determining whether or not the caret caused a token split. + if(leftSpans.length > 0 && rightSpans.length > 0) { + let rightHead = rightSpans[0]; - /** - * Get the last word of the phrase before the caret or nothing. - * @param fullLeftContext the entire left context of the string. - */ - export function getLastPreCaretToken(wordBreaker: WordBreakingFunction, context: Context): string { - let tokenization = tokenize(wordBreaker, context); - if (tokenization.left.length > 0) { - return tokenization.left.pop(); + // If tokenization includes all characters on each side of the caret, + // we have a good candidate for a caret-splitting scenario. + let leftSuffixWordbreak = leftTail!.end != context.left!.length; + let rightPrefixWordbreak = rightHead.start != 0; + + if(leftSuffixWordbreak || rightPrefixWordbreak) { + // Bypass the final test, as we already know the caret didn't split a token. + // (The tokenization process already removed characters between the two.) + return tokenization; } - return ''; + // Worth note - some languages don't use wordbreaking characters. So, a final check: + // + // Does the wordbreaker split a merge of the 'two center' tokens? + // If not, then the caret is responsible for the split. + if(wordBreaker(leftTail!.text + rightHead.text).length == 1) { + tokenization.caretSplitsToken = true; + } } - // While it is currently identical to getLastWord, this may change in the future. - // It's best not to write ourselves into a corner on this one, as disambiguating later - // would likely be pretty painful. - export function wordbreak(wordBreaker: WordBreakingFunction, context: Context): USVString { - return getLastPreCaretToken(wordBreaker, context); + return tokenization; +} + +/** + * Get the last word of the phrase before the caret or nothing. + * @param fullLeftContext the entire left context of the string. + */ +export function getLastPreCaretToken(wordBreaker: WordBreakingFunction, context: Context): string { + let tokenization = tokenize(wordBreaker, context); + if (tokenization.left.length > 0) { + return tokenization.left.pop() as string; } + + return ''; +} + +// While it is currently identical to getLastWord, this may change in the future. +// It's best not to write ourselves into a corner on this one, as disambiguating later +// would likely be pretty painful. +export function wordbreak(wordBreaker: WordBreakingFunction, context: Context): USVString { + return getLastPreCaretToken(wordBreaker, context); } \ No newline at end of file diff --git a/common/models/templates/src/trie-model.ts b/common/models/templates/src/trie-model.ts index 4eb6b80afedfb9be959197202f9a458b74ee5d3b..990ab61dc6e5d1f950378c0930fc81924222b4f5 100644 --- a/common/models/templates/src/trie-model.ts +++ b/common/models/templates/src/trie-model.ts @@ -24,10 +24,14 @@ // Worth noting: we're starting to get quite a 'library' of common model/LMLayer functionality. // Should probably make a 'lm-utils' submodule. -/// -/// -/// -/// + +// Allows the kmwstring bindings to resolve. +import { extendString } from "@keymanapp/web-utils"; +import { default as defaultWordBreaker } from "@keymanapp/models-wordbreakers"; + +import { applyTransform, isHighSurrogate, isSentinel, SENTINEL_CODE_UNIT, transformToSuggestion } from "./common.js"; +import { getLastPreCaretToken } from "./tokenization.js"; +import PriorityQueue from "./priority-queue.js"; /** * @file trie-model.ts @@ -35,524 +39,510 @@ * Defines a simple word list (unigram) model. */ - namespace models { - /** Upper bound on the amount of suggestions to generate. */ - const MAX_SUGGESTIONS = 12; +/** Upper bound on the amount of suggestions to generate. */ +const MAX_SUGGESTIONS = 12; +/** + * Additional arguments to pass into the model, in addition to the model + * parameters themselves. + */ +export interface TrieModelOptions { /** - * Additional arguments to pass into the model, in addition to the model - * parameters themselves. + * How to break words in a phrase. */ - interface TrieModelOptions { - /** - * How to break words in a phrase. - */ - wordBreaker?: WordBreakingFunction; - /** - * This should simplify a search term into a key. - */ - searchTermToKey?: (searchTerm: string) => string; - - /** - * Indicates that the model's written form has 'casing' behavior. - */ - languageUsesCasing?: boolean; - - /** - * Specifies a function used to apply the language's casing rules to a word. - */ - applyCasing?: CasingFunction; - - /** - * Any punctuation to expose to the user. - */ - punctuation?: LexicalModelPunctuation; - } - + wordBreaker?: WordBreakingFunction; /** - * Used to determine the probability of an entry from the trie. + * This should simplify a search term into a key. */ - type TextWithProbability = { - text: string; - // TODO: use negative-log scaling instead? - p: number; // real-number weight, from 0 to 1 - } + searchTermToKey?: (searchTerm: string) => string; /** - * @class TrieModel - * - * Defines a trie-backed word list model, or the unigram model. - * Unigram models throw away all preceding words, and search - * for the next word exclusively. As such, they can perform simple - * prefix searches within words, however they are not very good - * at predicting the next word. + * Indicates that the model's written form has 'casing' behavior. */ - export class TrieModel implements LexicalModel { - configuration: Configuration; - private _trie: Trie; - readonly breakWords: WordBreakingFunction; - readonly punctuation?: LexicalModelPunctuation; - readonly languageUsesCasing?: boolean; - - readonly applyCasing?: CasingFunction; - - constructor(trieData: object, options: TrieModelOptions = {}) { - this.languageUsesCasing = options.languageUsesCasing; - this.applyCasing = options.applyCasing; - - this._trie = new Trie( - trieData['root'], - trieData['totalWeight'], - options.searchTermToKey as Wordform2Key || defaultSearchTermToKey - ); - this.breakWords = options.wordBreaker || getDefaultWordBreaker(); - this.punctuation = options.punctuation; - } + languageUsesCasing?: boolean; - configure(capabilities: Capabilities): Configuration { - return this.configuration = { - leftContextCodePoints: capabilities.maxLeftContextCodePoints, - rightContextCodePoints: capabilities.maxRightContextCodePoints - }; - } + /** + * Specifies a function used to apply the language's casing rules to a word. + */ + applyCasing?: CasingFunction; - toKey(text: USVString): USVString { - return this._trie.toKey(text); - } + /** + * Any punctuation to expose to the user. + */ + punctuation?: LexicalModelPunctuation; +} - predict(transform: Transform, context: Context): Distribution { - // Special-case the empty buffer/transform: return the top suggestions. - if (!transform.insert && !context.left && !context.right && context.startOfBuffer && context.endOfBuffer) { - return makeDistribution(this._trie.firstN(MAX_SUGGESTIONS).map(({text, p}) => ({ - transform: { - insert: text, - deleteLeft: 0 - }, - displayAs: text, - p: p - }))); - } +/** + * Used to determine the probability of an entry from the trie. + */ +type TextWithProbability = { + text: string; + // TODO: use negative-log scaling instead? + p: number; // real-number weight, from 0 to 1 +} - // Compute the results of the keystroke: - let newContext = models.applyTransform(transform, context); +/** + * @class TrieModel + * + * Defines a trie-backed word list model, or the unigram model. + * Unigram models throw away all preceding words, and search + * for the next word exclusively. As such, they can perform simple + * prefix searches within words, however they are not very good + * at predicting the next word. + */ +export default class TrieModel implements LexicalModel { + configuration?: Configuration; + private _trie: Trie; + readonly breakWords: WordBreakingFunction; + readonly punctuation?: LexicalModelPunctuation; + readonly languageUsesCasing?: boolean; + + readonly applyCasing?: CasingFunction; + + constructor(trieData: {root: Node, totalWeight: number}, options: TrieModelOptions = {}) { + this.languageUsesCasing = options.languageUsesCasing; + this.applyCasing = options.applyCasing; + + this._trie = new Trie( + trieData['root'], + trieData['totalWeight'], + options.searchTermToKey as Wordform2Key || defaultSearchTermToKey + ); + this.breakWords = options.wordBreaker || defaultWordBreaker; + this.punctuation = options.punctuation; + } - // Computes the different in word length after applying the transform above. - let leftDelOffset = transform.deleteLeft - transform.insert.kmwLength(); + configure(capabilities: Capabilities): Configuration { + return this.configuration = { + leftContextCodePoints: capabilities.maxLeftContextCodePoints, + rightContextCodePoints: capabilities.maxRightContextCodePoints ?? 0 + }; + } - // All text to the left of the cursor INCLUDING anything that has - // just been typed. - let prefix = models.getLastPreCaretToken(this.breakWords, newContext); + toKey(text: USVString): USVString { + return this._trie.toKey(text); + } - // Return suggestions from the trie. - return makeDistribution(this._trie.lookup(prefix).map(({text, p}) => - models.transformToSuggestion({ + predict(transform: Transform, context: Context): Distribution { + // Special-case the empty buffer/transform: return the top suggestions. + if (!transform.insert && !context.left && !context.right && context.startOfBuffer && context.endOfBuffer) { + return makeDistribution(this._trie.firstN(MAX_SUGGESTIONS).map(({text, p}) => ({ + transform: { insert: text, - // Delete whatever the prefix that the user wrote. - deleteLeft: leftDelOffset + prefix.kmwLength() - // Note: a separate capitalization/orthography engine can take this - // result and transform it as needed. + deleteLeft: 0 }, - p - ))); + displayAs: text, + p: p + }))); + } - /* Helper */ + // Compute the results of the keystroke: + let newContext = applyTransform(transform, context); - function makeDistribution(suggestions: WithOutcome[]): Distribution { - let distribution: Distribution = []; + // Computes the different in word length after applying the transform above. + let leftDelOffset = transform.deleteLeft - transform.insert.kmwLength(); - for(let s of suggestions) { - distribution.push({sample: s, p: s.p}); - } + // All text to the left of the cursor INCLUDING anything that has + // just been typed. + let prefix = getLastPreCaretToken(this.breakWords, newContext); - return distribution; + // Return suggestions from the trie. + return makeDistribution(this._trie.lookup(prefix).map(({text, p}) => + transformToSuggestion({ + insert: text, + // Delete whatever the prefix that the user wrote. + deleteLeft: leftDelOffset + prefix.kmwLength() + // Note: a separate capitalization/orthography engine can take this + // result and transform it as needed. + }, + p + ))); + + /* Helper */ + + function makeDistribution(suggestions: WithOutcome[]): Distribution { + let distribution: Distribution = []; + + for(let s of suggestions) { + distribution.push({sample: s, p: s.p}); } - } - get wordbreaker(): WordBreakingFunction { - return this.breakWords; + return distribution; } + } - public traverseFromRoot(): LexiconTraversal { - return new TrieModel.Traversal(this._trie['root'], ''); - } + get wordbreaker(): WordBreakingFunction { + return this.breakWords; + } - private static Traversal = class implements LexiconTraversal { - /** - * The lexical prefix corresponding to the current traversal state. - */ - prefix: String; - - /** - * The current traversal node. Serves as the 'root' of its own sub-Trie, - * and we cannot navigate back to its parent. - */ - root: Node; - - constructor(root: Node, prefix: string) { - this.root = root; - this.prefix = prefix; - } + public traverseFromRoot(): LexiconTraversal { + return new TrieModel.Traversal(this._trie['root'], ''); + } - *children(): Generator<{char: string, traversal: () => LexiconTraversal}> { - let root = this.root; - - if(root.type == 'internal') { - for(let entry of root.values) { - let entryNode = root.children[entry]; - - // UTF-16 astral plane check. - if(models.isHighSurrogate(entry)) { - // First code unit of a UTF-16 code point. - // For now, we'll just assume the second always completes such a char. - // - // Note: Things get nasty here if this is only sometimes true; in the future, - // we should compile-time enforce that this assumption is always true if possible. - if(entryNode.type == 'internal') { - let internalNode = entryNode; - for(let lowSurrogate of internalNode.values) { - let prefix = this.prefix + entry + lowSurrogate; - yield { - char: entry + lowSurrogate, - traversal: function() { return new TrieModel.Traversal(internalNode.children[lowSurrogate], prefix) } - } - } - } else { - // Determine how much of the 'leaf' entry has no Trie nodes, emulate them. - let fullText = entryNode.entries[0].key; - entry = entry + fullText[this.prefix.length + 1]; // The other half of the non-BMP char. - let prefix = this.prefix + entry; + private static Traversal = class implements LexiconTraversal { + /** + * The lexical prefix corresponding to the current traversal state. + */ + prefix: String; + + /** + * The current traversal node. Serves as the 'root' of its own sub-Trie, + * and we cannot navigate back to its parent. + */ + root: Node; + + constructor(root: Node, prefix: string) { + this.root = root; + this.prefix = prefix; + } + *children(): Generator<{char: string, traversal: () => LexiconTraversal}> { + let root = this.root; + + if(root.type == 'internal') { + for(let entry of root.values) { + let entryNode = root.children[entry]; + + // UTF-16 astral plane check. + if(isHighSurrogate(entry)) { + // First code unit of a UTF-16 code point. + // For now, we'll just assume the second always completes such a char. + // + // Note: Things get nasty here if this is only sometimes true; in the future, + // we should compile-time enforce that this assumption is always true if possible. + if(entryNode.type == 'internal') { + let internalNode = entryNode; + for(let lowSurrogate of internalNode.values) { + let prefix = this.prefix + entry + lowSurrogate; yield { - char: entry, - traversal: function () {return new TrieModel.Traversal(entryNode, prefix)} + char: entry + lowSurrogate, + traversal: function() { return new TrieModel.Traversal(internalNode.children[lowSurrogate], prefix) } } } - } else if(models.isSentinel(entry)) { - continue; - } else if(!entry) { - // Prevent any accidental 'null' or 'undefined' entries from having an effect. - continue; } else { + // Determine how much of the 'leaf' entry has no Trie nodes, emulate them. + let fullText = entryNode.entries[0].key; + entry = entry + fullText[this.prefix.length + 1]; // The other half of the non-BMP char. let prefix = this.prefix + entry; + yield { char: entry, - traversal: function() { return new TrieModel.Traversal(entryNode, prefix)} + traversal: function () {return new TrieModel.Traversal(entryNode, prefix)} } } + } else if(isSentinel(entry)) { + continue; + } else if(!entry) { + // Prevent any accidental 'null' or 'undefined' entries from having an effect. + continue; + } else { + let prefix = this.prefix + entry; + yield { + char: entry, + traversal: function() { return new TrieModel.Traversal(entryNode, prefix)} + } } + } - return; - } else { // type == 'leaf' - let prefix = this.prefix; + return; + } else { // type == 'leaf' + let prefix = this.prefix; - let children = root.entries.filter(function(entry) { - return entry.key != prefix && prefix.length < entry.key.length; - }) + let children = root.entries.filter(function(entry) { + return entry.key != prefix && prefix.length < entry.key.length; + }) - for(let {key} of children) { - let nodeKey = key[prefix.length]; + for(let {key} of children) { + let nodeKey = key[prefix.length]; - if(models.isHighSurrogate(nodeKey)) { - // Merge the other half of an SMP char in! - nodeKey = nodeKey + key[prefix.length+1]; - } - yield { - char: nodeKey, - traversal: function() { return new TrieModel.Traversal(root, prefix + nodeKey)} - } - }; - return; - } + if(isHighSurrogate(nodeKey)) { + // Merge the other half of an SMP char in! + nodeKey = nodeKey + key[prefix.length+1]; + } + yield { + char: nodeKey, + traversal: function() { return new TrieModel.Traversal(root, prefix + nodeKey)} + } + }; + return; } + } - get entries(): string[] { - if(this.root.type == 'leaf') { - let prefix = this.prefix; - let matches = this.root.entries.filter(function(entry) { - return entry.key == prefix; - }); - - return matches.map(function(value) { return value.content }); + get entries(): string[] { + if(this.root.type == 'leaf') { + let prefix = this.prefix; + let matches = this.root.entries.filter(function(entry) { + return entry.key == prefix; + }); + + return matches.map(function(value) { return value.content }); + } else { + let matchingLeaf = this.root.children[SENTINEL_CODE_UNIT]; + if(matchingLeaf && matchingLeaf.type == 'leaf') { + return matchingLeaf.entries.map(function(value) { return value.content }); } else { - let matchingLeaf = this.root.children[models.SENTINEL_CODE_UNIT]; - if(matchingLeaf && matchingLeaf.type == 'leaf') { - return matchingLeaf.entries.map(function(value) { return value.content }); - } else { - return []; - } + return []; } } } - }; + } +}; - ///////////////////////////////////////////////////////////////////////////////// - // What remains in this file is the trie implementation proper. Note: to // - // reduce bundle size, any functions/methods related to creating the trie have // - // been removed. // - ///////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// +// What remains in this file is the trie implementation proper. Note: to // +// reduce bundle size, any functions/methods related to creating the trie have // +// been removed. // +///////////////////////////////////////////////////////////////////////////////// - /** - * An **opaque** type for a string that is exclusively used as a search key in - * the trie. There should be a function that converts arbitrary strings - * (queries) and converts them into a standard search key for a given language - * model. - * - * Fun fact: This opaque type has ALREADY saved my bacon and found a bug! - */ - type SearchKey = string & { _: 'SearchKey'}; +/** + * An **opaque** type for a string that is exclusively used as a search key in + * the trie. There should be a function that converts arbitrary strings + * (queries) and converts them into a standard search key for a given language + * model. + * + * Fun fact: This opaque type has ALREADY saved my bacon and found a bug! + */ +type SearchKey = string & { _: 'SearchKey'}; - /** - * The priority queue will always pop the most weighted item. There can only - * be two kinds of items right now: nodes, and entries; both having a weight - * attribute. - */ - type Weighted = Node | Entry; +/** + * The priority queue will always pop the most weighted item. There can only + * be two kinds of items right now: nodes, and entries; both having a weight + * attribute. + */ +type Weighted = Node | Entry; - /** - * A function that converts a string (word form or query) into a search key - * (secretly, this is also a string). - */ - interface Wordform2Key { - (wordform: string): SearchKey; - } +/** + * A function that converts a string (word form or query) into a search key + * (secretly, this is also a string). + */ +interface Wordform2Key { + (wordform: string): SearchKey; +} - // The following trie implementation has been (heavily) derived from trie-ing - // by Conrad Irwin. - // trie-ing is copyright (C) 2015–2017 Conrad Irwin. - // Distributed under the terms of the MIT license: - // https://github.com/ConradIrwin/trie-ing/blob/df55d7af7068d357829db9e0a7faa8a38add1d1d/LICENSE +// The following trie implementation has been (heavily) derived from trie-ing +// by Conrad Irwin. +// trie-ing is copyright (C) 2015–2017 Conrad Irwin. +// Distributed under the terms of the MIT license: +// https://github.com/ConradIrwin/trie-ing/blob/df55d7af7068d357829db9e0a7faa8a38add1d1d/LICENSE - type Node = InternalNode | Leaf; +type Node = InternalNode | Leaf; +/** + * An internal node in the trie. Internal nodes NEVER contain entries; if an + * internal node should contain an entry, then it has a dummy leaf node (see + * below), that can be accessed by node.children["\uFDD0"]. + */ +interface InternalNode { + type: 'internal'; + weight: number; + /** Maintains the keys of children in descending order of weight. */ + values: string[]; // TODO: As an optimization, "values" can be a single string! /** - * An internal node in the trie. Internal nodes NEVER contain entries; if an - * internal node should contain an entry, then it has a dummy leaf node (see - * below), that can be accessed by node.children["\uFDD0"]. + * Maps a single UTF-16 code unit to a child node in the trie. This child + * node may be a leaf or an internal node. The keys of this object are kept + * in sorted order in the .values array. */ - interface InternalNode { - type: 'internal'; - weight: number; - /** Maintains the keys of children in descending order of weight. */ - values: string[]; // TODO: As an optimization, "values" can be a single string! - /** - * Maps a single UTF-16 code unit to a child node in the trie. This child - * node may be a leaf or an internal node. The keys of this object are kept - * in sorted order in the .values array. - */ - children: { [codeunit: string]: Node }; - } - /** Only leaf nodes actually contain entries (i.e., the words proper). */ - interface Leaf { - type: 'leaf'; - weight: number; - entries: Entry[]; - } + children: { [codeunit: string]: Node }; +} +/** Only leaf nodes actually contain entries (i.e., the words proper). */ +interface Leaf { + type: 'leaf'; + weight: number; + entries: Entry[]; +} + +/** + * An entry in the prefix trie (stored in leaf nodes exclusively!) + */ +interface Entry { + /** The actual word form, stored in the trie. */ + content: string; + /** A search key that usually simplifies the word form, for ease of search. */ + key: SearchKey; + weight: number; +} +/** + * Wrapper class for the trie and its nodes. + */ +class Trie { + private root: Node; + /** The total weight of the entire trie. */ + private totalWeight: number; /** - * An entry in the prefix trie (stored in leaf nodes exclusively!) + * Converts arbitrary strings to a search key. The trie is built up of + * search keys; not each entry's word form! */ - interface Entry { - /** The actual word form, stored in the trie. */ - content: string; - /** A search key that usually simplifies the word form, for ease of search. */ - key: SearchKey; - weight: number; + toKey: Wordform2Key; + + constructor(root: Node, totalWeight: number, wordform2key: Wordform2Key) { + this.root = root; + this.toKey = wordform2key; + this.totalWeight = totalWeight; } /** - * Wrapper class for the trie and its nodes. + * Lookups an arbitrary prefix (a query) in the trie. Returns the top 3 + * results in sorted order. + * + * @param prefix */ - class Trie { - private root: Node; - /** The total weight of the entire trie. */ - private totalWeight: number; - /** - * Converts arbitrary strings to a search key. The trie is built up of - * search keys; not each entry's word form! - */ - toKey: Wordform2Key; - - constructor(root: Node, totalWeight: number, wordform2key: Wordform2Key) { - this.root = root; - this.toKey = wordform2key; - this.totalWeight = totalWeight; - } - - /** - * Lookups an arbitrary prefix (a query) in the trie. Returns the top 3 - * results in sorted order. - * - * @param prefix - */ - lookup(prefix: string): TextWithProbability[] { - let searchKey = this.toKey(prefix); - let lowestCommonNode = findPrefix(this.root, searchKey); - if (lowestCommonNode === null) { - return []; - } - - return getSortedResults(lowestCommonNode, searchKey, this.totalWeight); + lookup(prefix: string): TextWithProbability[] { + let searchKey = this.toKey(prefix); + let lowestCommonNode = findPrefix(this.root, searchKey); + if (lowestCommonNode === null) { + return []; } - /** - * Returns the top N suggestions from the trie. - * @param n How many suggestions, maximum, to return. - */ - firstN(n: number): TextWithProbability[] { - return getSortedResults(this.root, '' as SearchKey, this.totalWeight, n); - } + return getSortedResults(lowestCommonNode, searchKey, this.totalWeight); } /** - * Finds the deepest descendent in the trie with the given prefix key. - * - * This means that a search in the trie for a given prefix has a best-case - * complexity of O(m) where m is the length of the prefix. - * - * @param key The prefix to search for. - * @param index The index in the prefix. Initially 0. + * Returns the top N suggestions from the trie. + * @param n How many suggestions, maximum, to return. */ - function findPrefix(node: Node, key: SearchKey, index: number = 0): Node | null { - // An important note - the Trie itself is built on a per-JS-character basis, - // not on a UTF-8 character-code basis. - if (node.type === 'leaf' || index === key.length) { - return node; - } + firstN(n: number): TextWithProbability[] { + return getSortedResults(this.root, '' as SearchKey, this.totalWeight, n); + } +} - // So, for SMP models, we need to match each char of the supplementary pair - // in sequence. Each has its own node in the Trie. - let char = key[index]; - if (node.children[char]) { - return findPrefix(node.children[char], key, index + 1); - } +/** + * Finds the deepest descendent in the trie with the given prefix key. + * + * This means that a search in the trie for a given prefix has a best-case + * complexity of O(m) where m is the length of the prefix. + * + * @param key The prefix to search for. + * @param index The index in the prefix. Initially 0. + */ +function findPrefix(node: Node, key: SearchKey, index: number = 0): Node | null { + // An important note - the Trie itself is built on a per-JS-character basis, + // not on a UTF-8 character-code basis. + if (node.type === 'leaf' || index === key.length) { + return node; + } - return null; + // So, for SMP models, we need to match each char of the supplementary pair + // in sequence. Each has its own node in the Trie. + let char = key[index]; + if (node.children[char]) { + return findPrefix(node.children[char], key, index + 1); } - /** - * Returns all entries matching the given prefix, in descending order of - * weight. - * - * @param prefix the prefix to match. - * @param results the current results - * @param queue - */ - function getSortedResults(node: Node, prefix: SearchKey, N: number, limit = MAX_SUGGESTIONS): TextWithProbability[] { - let queue = new PriorityQueue(function(a: Weighted, b: Weighted) { - // In case of Trie compilation issues that emit `null` or `undefined` - return (b ? b.weight : 0) - (a ? a.weight : 0); - }); - let results: TextWithProbability[] = []; - - if (node.type === 'leaf') { - // Assuming the values are sorted, we can just add all of the values in the - // leaf, until we reach the limit. - for (let item of node.entries) { - // String.startsWith is not supported on certain Android (5.0) devices we wish to support. - // Requires a minimum of Chrome 36, as opposed to 5.0's default of 35. - if (item.key.indexOf(prefix) == 0) { - let { content, weight } = item; - results.push({ - text: content, - p: weight / N - }); - - if (results.length >= limit) { - return results; - } + return null; +} + +/** + * Returns all entries matching the given prefix, in descending order of + * weight. + * + * @param prefix the prefix to match. + * @param results the current results + * @param queue + */ +function getSortedResults(node: Node, prefix: SearchKey, N: number, limit = MAX_SUGGESTIONS): TextWithProbability[] { + let queue = new PriorityQueue(function(a: Weighted, b: Weighted) { + // In case of Trie compilation issues that emit `null` or `undefined` + return (b ? b.weight : 0) - (a ? a.weight : 0); + }); + let results: TextWithProbability[] = []; + + if (node.type === 'leaf') { + // Assuming the values are sorted, we can just add all of the values in the + // leaf, until we reach the limit. + for (let item of node.entries) { + // String.startsWith is not supported on certain Android (5.0) devices we wish to support. + // Requires a minimum of Chrome 36, as opposed to 5.0's default of 35. + if (item.key.indexOf(prefix) == 0) { + let { content, weight } = item; + results.push({ + text: content, + p: weight / N + }); + + if (results.length >= limit) { + return results; } } - } else { - queue.enqueue(node); - let next: Weighted; - - while (next = queue.dequeue()) { - if (isNode(next)) { - // When a node is next up in the queue, that means that next least - // likely suggestion is among its decsendants. - // So we search all of its descendants! - if (next.type === 'leaf') { - queue.enqueueAll(next.entries); - } else { - // XXX: alias `next` so that TypeScript can be SURE that internal is - // in fact an internal node. Because of the callback binding to the - // original definition of node (i.e., a Node | Entry), this will not - // type-check otherwise. - let internal = next; - queue.enqueueAll(next.values.map(char => { - return internal.children[char]; - })); - } + } + } else { + queue.enqueue(node); + let next: Weighted | undefined; + + while (next = queue.dequeue()) { + if (isNode(next)) { + // When a node is next up in the queue, that means that next least + // likely suggestion is among its decsendants. + // So we search all of its descendants! + if (next.type === 'leaf') { + queue.enqueueAll(next.entries); } else { - // When an entry is up next in the queue, we just add its contents to - // the results! - results.push({ - text: next.content, - p: next.weight / N - }); - if (results.length >= limit) { - return results; - } + // XXX: alias `next` so that TypeScript can be SURE that internal is + // in fact an internal node. Because of the callback binding to the + // original definition of node (i.e., a Node | Entry), this will not + // type-check otherwise. + let internal = next; + queue.enqueueAll(next.values.map(char => { + return internal.children[char]; + })); + } + } else { + // When an entry is up next in the queue, we just add its contents to + // the results! + results.push({ + text: next.content, + p: next.weight / N + }); + if (results.length >= limit) { + return results; } } } - return results; - } + return results; - /** TypeScript type guard that returns whether the thing is a Node. */ - function isNode(x: Entry | Node): x is Node { - return 'type' in x; - } +} +/** TypeScript type guard that returns whether the thing is a Node. */ +function isNode(x: Entry | Node): x is Node { + return 'type' in x; +} + +/** + * Converts wordforms into an indexable form. It does this by + * normalizing into NFD, removing diacritics, and then converting + * the result to lowercase. + * + * This is a very naïve implementation, that I only think will work on + * some languages that use the Latin script. As of 2020-04-08, only + * 4 out of 11 (36%) of published language models use the Latin script, + * so this might not actually be a great default. + * + * This uses String.prototype.normalize() to convert normalize into NFD. + * NFD is an easy way to separate a Latin character from its diacritics; + * Even then, some Latin-based orthographies use code points that, + * under NFD normalization, do NOT decompose into an ASCII letter and a + * combining diacritical mark (e.g., SENĆOŦEN). + * + * Use this only in early iterations of the model. For a production lexical + * model, you SHOULD write/generate your own key function, tailored to your + * language. + */ +function defaultSearchTermToKey(wordform: string): SearchKey { /** - * Converts wordforms into an indexable form. It does this by - * normalizing into NFD, removing diacritics, and then converting - * the result to lowercase. - * - * This is a very naïve implementation, that I only think will work on - * some languages that use the Latin script. As of 2020-04-08, only - * 4 out of 11 (36%) of published language models use the Latin script, - * so this might not actually be a great default. + * N.B.: this is (slightly) DIFFERENT than the version in + * keymanapp/lexical-model-compiler/build-trie + * as this is for compatibility for models built + * BEFORE the searchTermToKey function was bundled with + * all models. * - * This uses String.prototype.normalize() to convert normalize into NFD. - * NFD is an easy way to separate a Latin character from its diacritics; - * Even then, some Latin-based orthographies use code points that, - * under NFD normalization, do NOT decompose into an ASCII letter and a - * combining diacritical mark (e.g., SENĆOŦEN). - * - * Use this only in early iterations of the model. For a production lexical - * model, you SHOULD write/generate your own key function, tailored to your - * language. + * This compatibility version lowercases AFTER removing diacritics; + * the new version (bundled in future models) lowercases, + * NFD normalizes, THEN removes diacritics. */ - function defaultSearchTermToKey(wordform: string): SearchKey { - /** - * N.B.: this is (slightly) DIFFERENT than the version in - * keymanapp/lexical-model-compiler/build-trie - * as this is for compatibility for models built - * BEFORE the searchTermToKey function was bundled with - * all models. - * - * This compatibility version lowercases AFTER removing diacritics; - * the new version (bundled in future models) lowercases, - * NFD normalizes, THEN removes diacritics. - */ - return wordform - .normalize('NFD') - // Remove all combining diacritics (if input is in NFD) - // common to Latin-orthographies. - .replace(/[\u0300-\u036f]/g, '') - .toLowerCase() as SearchKey; - } - - function getDefaultWordBreaker() { - let namespace: {}; - // @ts-ignore - if (typeof wordBreakers !== 'undefined') { - // @ts-ignore - namespace = wordBreakers; - } else { - namespace = require('@keymanapp/models-wordBreakers').wordBreakers; - } - return namespace['default']; - } + return wordform + .normalize('NFD') + // Remove all combining diacritics (if input is in NFD) + // common to Latin-orthographies. + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase() as SearchKey; } diff --git a/common/models/templates/test/helpers.js b/common/models/templates/test/helpers.js index 268e24b7ddc15b2fe3b3575735134f0cd5b3afac..c4376b613fd9effc0c6b7e3f1f6ce3008bd6769f 100644 --- a/common/models/templates/test/helpers.js +++ b/common/models/templates/test/helpers.js @@ -1,5 +1,5 @@ -const path = require('path'); -const assert = require('assert'); +import path from 'path'; +import { assert } from 'chai'; var _ = global; @@ -7,16 +7,25 @@ var _ = global; // TODO: then mocha invocation is as follows: // TODO: mocha -r @keymanapp/models-test-helpers test/ -// Ensure that we can successfully load the module & apply kmwLength, as it's -// needed for some of the unit tests. -require('../build/index.bundled.js'); +import { extendString } from '@keymanapp/web-utils'; +import { createRequire } from "module"; +import { fileURLToPath } from 'url'; + +extendString(); assert.ok('💩'.kmwLength); /** * Load JSON fixtures from a well-known place. */ _.jsonFixture = function (name) { + // The most straight-forward way... is to use CommonJS-style require to load JSON. + // Fortunately, Node provides the tools needed to recreate it. + const require = createRequire(import.meta.url); + + // ES-module mode also leaves out `__dirname`, so we rebuild that too. + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); return require(path.join(__dirname, 'fixtures', `${name}.json`)); } diff --git a/common/models/templates/test/test-common.js b/common/models/templates/test/test-common.js index 7dfc565369ae9a66e721dbbb560e16c78a742953..dfd5c875cb4f701e5a833ebc552be1a42ee5f8b4 100644 --- a/common/models/templates/test/test-common.js +++ b/common/models/templates/test/test-common.js @@ -2,8 +2,8 @@ * Unit tests for common utility functions/methods. */ -var assert = require('chai').assert; -var models = require('../build/index.bundled.js').models; +import { assert } from 'chai'; +import * as models from '@keymanapp/models-templates'; describe('Common utility functions', function() { // TODO: unit tests for other common utility functions diff --git a/common/models/templates/test/test-priority-queue.js b/common/models/templates/test/test-priority-queue.js index b004027d84a08c3701d13b626e2f669418801f4a..5fdd0caa3d4f1dab3044192845f97d563b474371 100644 --- a/common/models/templates/test/test-priority-queue.js +++ b/common/models/templates/test/test-priority-queue.js @@ -2,8 +2,8 @@ * Unit tests for the priority queue. */ -var assert = require('chai').assert; -var PriorityQueue = require('../build/index.bundled.js').models.PriorityQueue; +import { assert } from 'chai'; +import { PriorityQueue } from '@keymanapp/models-templates'; describe('Priority queue', function() { it('can act as a min-heap', function () { diff --git a/common/models/templates/test/test-quote-behavior.js b/common/models/templates/test/test-quote-behavior.js index dd5408f6a96f9e4b32a3c328d3494d3f1b4e7149..c6348d651d9de16d8075b13f2d1c4019448c7812 100644 --- a/common/models/templates/test/test-quote-behavior.js +++ b/common/models/templates/test/test-quote-behavior.js @@ -2,8 +2,8 @@ * Unit tests for quote behaviors. */ -var assert = require('chai').assert; -var QuoteBehavior = require('../build/index.bundled.js').models.QuoteBehavior; +import { assert } from 'chai'; +import { QuoteBehavior } from '@keymanapp/models-templates'; describe('Quote behaviors', function() { describe('Script directionality', function() { diff --git a/common/models/templates/test/test-tokenization.js b/common/models/templates/test/test-tokenization.js index a7ae0835e05daa19851ae3f7db4aafbed479066c..6c1ff746e58bf2e10ed1a3aaccb6be37bcbf217f 100644 --- a/common/models/templates/test/test-tokenization.js +++ b/common/models/templates/test/test-tokenization.js @@ -2,9 +2,9 @@ * Unit tests for common utility functions/methods. */ -var assert = require('chai').assert; -var models = require('../build/index.bundled.js').models; -var wordBreakers = require('@keymanapp/models-wordbreakers').wordBreakers; +import { assert } from 'chai'; +import * as models from "@keymanapp/models-templates"; +import * as wordBreakers from "@keymanapp/models-wordbreakers"; describe('Tokenization functions', function() { describe('tokenize', function() { @@ -210,7 +210,7 @@ describe('Tokenization functions', function() { // Step 2: Allow shifting a defined 'constant' span without mutating the definition. let shiftSpan = function(span, delta) { // Avoid mutating the parameter! - shiftedSpan = { + let shiftedSpan = { text: span.text, start: span.start + delta, end: span.end + delta, diff --git a/common/models/templates/test/test-trie-model.js b/common/models/templates/test/test-trie-model.js index b48dc254ecc52740edc6c4d605df498bca1a393b..e63c66148b391f2e6cff2718d1f5dc1ca5cfdc20 100644 --- a/common/models/templates/test/test-trie-model.js +++ b/common/models/templates/test/test-trie-model.js @@ -2,8 +2,8 @@ * Unit tests for the Trie prediction model. */ -var assert = require('chai').assert; -var TrieModel = require('../build/index.bundled.js').models.TrieModel; +import { assert } from 'chai'; +import { TrieModel } from '@keymanapp/models-templates'; describe('LMLayerWorker trie model for word lists', function() { describe('instantiation', function () { diff --git a/common/models/templates/test/test-trie-traversal.js b/common/models/templates/test/test-trie-traversal.js index 31f52f854a3dff84439edb108f24108ae76b4463..d3d04963165280562116aac8f1487ed8e2267561 100644 --- a/common/models/templates/test/test-trie-traversal.js +++ b/common/models/templates/test/test-trie-traversal.js @@ -2,8 +2,8 @@ * Unit tests for the Trie prediction model. */ -var assert = require('chai').assert; -var TrieModel = require('../build/index.bundled.js').models.TrieModel; +import { assert } from 'chai'; +import { TrieModel } from '@keymanapp/models-templates'; // Useful for tests related to strings with supplementary pairs. var smpForUnicode = function(code){ @@ -23,7 +23,7 @@ describe('Trie traversal abstractions', function() { let rootKeys = ['t', 'o', 'a', 'i', 'w', 'h', 'f', 'b', 'n', 'y', 's', 'm', 'u', 'c', 'd', 'l', 'e', 'j', 'p', 'g', 'v', 'k', 'r', 'q'] - for(child of rootTraversal.children()) { + for(let child of rootTraversal.children()) { let keyIndex = rootKeys.indexOf(child.char); assert.notEqual(keyIndex, -1); rootKeys.splice(keyIndex, 1); @@ -43,7 +43,7 @@ describe('Trie traversal abstractions', function() { let tSuccess = false; let hSuccess = false; let eSuccess = false; - for(child of rootTraversal.children()) { + for(let child of rootTraversal.children()) { if(child.char == 't') { tSuccess = true; let traversalInner1 = child.traversal(); @@ -51,7 +51,7 @@ describe('Trie traversal abstractions', function() { assert.isArray(child.traversal().entries); assert.isEmpty(child.traversal().entries); - for(tChild of traversalInner1.children()) { + for(let tChild of traversalInner1.children()) { if(tChild.char == 'h') { hSuccess = true; let traversalInner2 = tChild.traversal(); @@ -59,7 +59,7 @@ describe('Trie traversal abstractions', function() { assert.isEmpty(tChild.traversal().entries); assert.isArray(tChild.traversal().entries); - for(hChild of traversalInner2.children()) { + for(let hChild of traversalInner2.children()) { if(hChild.char == 'e') { eSuccess = true; let traversalInner3 = hChild.traversal(); @@ -68,7 +68,7 @@ describe('Trie traversal abstractions', function() { assert.isDefined(traversalInner3.entries); assert.equal(traversalInner3.entries[0], "the"); - for(eChild of traversalInner3.children()) { + for(let eChild of traversalInner3.children()) { let keyIndex = eKeys.indexOf(eChild.char); assert.notEqual(keyIndex, -1, "Did not find char '" + eChild.char + "' in array!"); eKeys.splice(keyIndex, 1); @@ -96,21 +96,21 @@ describe('Trie traversal abstractions', function() { // 't', 'r', 'o' have siblings, but these don't. let leafChildSequence = ['u', 'b', 'l', 'e']; let eSuccess = false; - for(child of rootTraversal.children()) { + for(let child of rootTraversal.children()) { if(child.char == 't') { let traversalInner1 = child.traversal(); assert.isDefined(traversalInner1); assert.isArray(child.traversal().entries); assert.isEmpty(child.traversal().entries); - for(tChild of traversalInner1.children()) { + for(let tChild of traversalInner1.children()) { if(tChild.char == 'r') { let traversalInner2 = tChild.traversal(); assert.isDefined(traversalInner2); assert.isArray(tChild.traversal().entries); assert.isEmpty(tChild.traversal().entries); - for(rChild of traversalInner2.children()) { + for(let rChild of traversalInner2.children()) { if(rChild.char == 'o') { let curChild = rChild; @@ -176,7 +176,7 @@ describe('Trie traversal abstractions', function() { let aSuccess = false; let pSuccess = false; let eSuccess = false; - for(child of rootTraversal.children()) { + for(let child of rootTraversal.children()) { if(child.char == smpA) { aSuccess = true; let traversalInner1 = child.traversal(); @@ -184,7 +184,7 @@ describe('Trie traversal abstractions', function() { assert.isArray(child.traversal().entries); assert.isEmpty(child.traversal().entries); - for(aChild of traversalInner1.children()) { + for(let aChild of traversalInner1.children()) { if(aChild.char == smpP) { pSuccess = true; let traversalInner2 = aChild.traversal(); @@ -192,7 +192,7 @@ describe('Trie traversal abstractions', function() { assert.isArray(aChild.traversal().entries); assert.isEmpty(aChild.traversal().entries); - for(pChild of traversalInner2.children()) { + for(let pChild of traversalInner2.children()) { let keyIndex = pKeys.indexOf(pChild.char); assert.notEqual(keyIndex, -1, "Did not find char '" + pChild.char + "' in array!"); pKeys.splice(keyIndex, 1); diff --git a/common/models/templates/tsconfig.bundled.json b/common/models/templates/tsconfig.bundled.json deleted file mode 100644 index fd8dc5f65a532eba7b4c41dce246bc9d21ae15c6..0000000000000000000000000000000000000000 --- a/common/models/templates/tsconfig.bundled.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - // This variant of the tsconfig.json exists to create a 'leaf', 'bundled' - // version of the models/templates build product. The same reference - // cannot be prepended twice in a composite tsc build, posing problems - // for certain down-line builds if the two tsconfigs are not differentiated. - "extends": "./tsconfig.json", - "compilerOptions": { - "outFile": "build/index.bundled.js", - }, - "references": [ - { "path": "../../web/keyman-version", "prepend": true}, - { "path": "../../web/utils", "prepend": true }, - { "path": "../types" } - ], - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "test" - ] -} diff --git a/common/models/templates/tsconfig.json b/common/models/templates/tsconfig.json index 8cc9c6af1c6e0d46233c99508ac359f7b121e965..34b528d7cfda923793f9d536167976ef66384796 100644 --- a/common/models/templates/tsconfig.json +++ b/common/models/templates/tsconfig.json @@ -1,16 +1,16 @@ { - "extends": "../../../tsconfig-base.json", + "extends": "../tsconfig.kmw-worker-base.json", "compilerOptions": { - "declaration": true, - "module": "none", - "outFile": "./build/index.js", - "sourceMap": true, - "lib": ["es6"], - "target": "es5", + "strict": true, + "baseUrl": "./", + "outDir": "build/obj", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", + "rootDir": "./src" }, "references": [ + { "path": "../types" }, { "path": "../../web/utils" }, - { "path": "../types" } + { "path": "../../models/wordbreakers" } ], "include": [ "src/**/*.ts" diff --git a/common/models/tsconfig.kmw-worker-base.json b/common/models/tsconfig.kmw-worker-base.json new file mode 100644 index 0000000000000000000000000000000000000000..0b581af1be3ddf29d746b65a74404df43a0a4028 --- /dev/null +++ b/common/models/tsconfig.kmw-worker-base.json @@ -0,0 +1,12 @@ +{ + "extends": "../../web/tsconfig.base.json", + + "compilerOptions": { + // To help better support legacy Android devices + "downlevelIteration": true, + // Facilitates & simplifies stitching together the worker sourcemaps during the polyfill-concatenation step. + "inlineSourceMap": true, + // May not be set at the same time as the prior setting. + "sourceMap": false + } +} diff --git a/common/models/types/tsconfig.json b/common/models/types/tsconfig.json index 124def350d24da9afdb48c5fbc7249736d86e55b..5d6a84afcbdb2fdb26dc8a29246c97c85c92f753 100644 --- a/common/models/types/tsconfig.json +++ b/common/models/types/tsconfig.json @@ -1,12 +1,9 @@ { - "extends": "../../../tsconfig-base.json", + "extends": "../tsconfig.kmw-worker-base.json", "compilerOptions": { "declaration": true, "module": "none", "outDir": "build/", - "sourceMap": true, - "lib": ["es6"], - "target": "es5" }, "include": ["./*.ts"], "exclude": ["test.ts"] diff --git a/common/models/wordbreakers/.c8rc.json b/common/models/wordbreakers/.c8rc.json new file mode 100644 index 0000000000000000000000000000000000000000..e1de5a59991b15bd675da84a6fcc98ae682c26e0 --- /dev/null +++ b/common/models/wordbreakers/.c8rc.json @@ -0,0 +1,14 @@ +{ + "check-coverage": false, + "clean": true, + "exclude": [ + "node_modules/*", + "test" + ], + "exclude-after-remap": true, + "reporter": ["text", "text-summary"], + "reports-dir": "build/coverage", + "src": [ + "src/" + ] +} \ No newline at end of file diff --git a/common/models/wordbreakers/build.sh b/common/models/wordbreakers/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..fadd3501184ea5171deba5dd119896940d4b420d --- /dev/null +++ b/common/models/wordbreakers/build.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# Compile our sourcemap-path remapping module for use by Web builds, releases, etc. +# +set -eu + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(greadlink -f "${BASH_SOURCE[0]}" 2>/dev/null || readlink -f "${BASH_SOURCE[0]}")" +. "$(dirname "$THIS_SCRIPT")/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$(dirname "$THIS_SCRIPT")" + +################################ Main script ################################ + +builder_describe "Builds the predictive-text wordbreaker implementation module" \ + "clean" \ + "configure" \ + "build" \ + "test" \ + "--ci" + +builder_describe_outputs \ + configure /node_modules \ + build build/obj/index.js + +builder_parse "$@" + +function do_build() { + tsc -b + + # Declaration bundling. + tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts +} + +function do_test() { + if builder_has_option --ci; then + c8 mocha -reporter mocha-teamcity-reporter + else + c8 mocha + fi +} + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/models/wordbreakers/package.json b/common/models/wordbreakers/package.json index 3c2eaa1028daaba061a82048b7d33d741770dc15..bd8d0157fbd7164a1e36f1c10388ae10fd386d57 100644 --- a/common/models/wordbreakers/package.json +++ b/common/models/wordbreakers/package.json @@ -14,7 +14,16 @@ ], "homepage": "https://github.com/keymanapp/keyman", "license": "MIT", - "main": "build/index.js", + "main": "build/obj/index.js", + "types": "build/obj/index.d.ts", + "exports": { + ".": "./build/obj/index.js", + "./lib": { + "import": "./build/lib/index.mjs", + "require": "./build/lib/index.cjs" + }, + "./obj/*.js": "./build/obj/*.js" + }, "directories": { "lib": "lib", "test": "test" @@ -30,23 +39,25 @@ "url": "git+https://github.com/keymanapp/keyman.git" }, "scripts": { - "build": "tsc", - "prepublish": "npm run build", - "pretest": "npm run build", - "test": "mocha" + "build": "gosh ./build.sh", + "test": "gosh ./build.sh test" }, "bugs": { "url": "https://github.com/keymanapp/keyman/issues" }, "devDependencies": { "@keymanapp/models-types": "*", + "@keymanapp/resources-gosh": "*", "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", + "c8": "^7.12.0", "chai": "^4.3.4", "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", "ts-node": "^10.9.1", "typescript": "^4.9.5" }, + "type": "module", "dependencies": { "@types/node": "^14.0.3" } diff --git a/common/models/wordbreakers/src/ascii.ts b/common/models/wordbreakers/src/ascii.ts index e321f387d4414824eb44c1f6592a7d8908ebcfd1..902a6e70583af7917de4667c6414d83eee51432e 100644 --- a/common/models/wordbreakers/src/ascii.ts +++ b/common/models/wordbreakers/src/ascii.ts @@ -1,40 +1,39 @@ /// -namespace wordBreakers { - /** - * Splits ASCII words. - * - * @param phrase - */ - export function ascii(phrase: string): Span[] { - let matchWord = /[A-Za-z0-9']+/g; - let words: Span[] = []; - let match: RegExpExecArray | null; - while ((match = matchWord.exec(phrase)) !== null) { - words.push(new RegExpDerivedSpan(match[0], match.index)); - } - return words; +/** + * Splits ASCII words. + * + * @param phrase + */ +export default function ascii(phrase: string): Span[] { + let matchWord = /[A-Za-z0-9']+/g; + let words: Span[] = []; + let match: RegExpExecArray | null; + while ((match = matchWord.exec(phrase)) !== null) { + words.push(new RegExpDerivedSpan(match[0], match.index)); } - /** - * A concrete span class that derives its properties from the result of - * RegExp.exec() array. - */ - class RegExpDerivedSpan implements Span { - readonly text: string; - readonly start: number; + return words; +} + +/** + * A concrete span class that derives its properties from the result of + * RegExp.exec() array. + */ +class RegExpDerivedSpan implements Span { + readonly text: string; + readonly start: number; - constructor(text: string, start: number) { - this.text = text; - this.start = start; - } + constructor(text: string, start: number) { + this.text = text; + this.start = start; + } - get length(): number { - return this.text.length; - } + get length(): number { + return this.text.length; + } - get end(): number { - return this.start + this.text.length; - } + get end(): number { + return this.start + this.text.length; } } diff --git a/common/models/wordbreakers/src/default/data.ts b/common/models/wordbreakers/src/default/data.ts index 16a4d9beb38b3c058e90cc1f225d7806521fd733..abd128f020ee41ce2f723c4def7fa6835c4046ef 100644 --- a/common/models/wordbreakers/src/default/data.ts +++ b/common/models/wordbreakers/src/default/data.ts @@ -1,6 +1,5 @@ -namespace wordBreakers { -export namespace data { // Automatically generated file. DO NOT MODIFY. + /** * Valid values for a word break property. */ @@ -1801,5 +1800,3 @@ export const WORD_BREAK_PROPERTY: [number, WordBreakProperty][] = [ [/*start*/ 0xE0100, WordBreakProperty.Extend], [/*start*/ 0xE01F0, WordBreakProperty.Other], ]; -} -} diff --git a/common/models/wordbreakers/src/default/index.ts b/common/models/wordbreakers/src/default/index.ts index 715b722932c48f93d3e599f9b6f30a3cf1a4df50..20e159ed1988e81ae47cd07300ee56307c38f364 100644 --- a/common/models/wordbreakers/src/default/index.ts +++ b/common/models/wordbreakers/src/default/index.ts @@ -1,625 +1,608 @@ -// Include the word-breaking data here: -/// -namespace wordBreakers { +import { WordBreakProperty, WORD_BREAK_PROPERTY, I, propertyMap } from "./data.js"; + +/** + * A set of options used to customize and extend the behavior of the default + * Unicode wordbreaker. + */ +export interface DefaultWordBreakerOptions { /** - * A set of options used to customize and extend the behavior of the default - * Unicode wordbreaker. + * Allows addition of custom wordbreaking rules, which will be applied + * after WB1-WB4 and before all other default wordbreaking rules. + * + * @see `WordbreakerRule` */ - export interface DefaultWordBreakerOptions { - /** - * Allows addition of custom wordbreaking rules, which will be applied - * after WB1-WB4 and before all other default wordbreaking rules. - * - * @see `WordbreakerRule` - */ - rules?: WordbreakerRule[]; - - /** - * Allows assignment of characters to different word-breaking properties than - * their standard word-breaking assignment, including to custom properties - * specified within `customProperties`. - * @param char - */ - propertyMapping?(char: string): string; - - /** - * Allows definition of extra word-breaking properties for use with custom - * rules. - */ - customProperties?: string[]; - } + rules?: WordbreakerRule[]; /** - * Word breaker based on Unicode Standard Annex #29, Section 4.1: - * Default Word Boundary Specification. - * - * @see http://unicode.org/reports/tr29/#Word_Boundaries - * @see https://github.com/eddieantonio/unicode-default-word-boundary/tree/v12.0.0 + * Allows assignment of characters to different word-breaking properties than + * their standard word-breaking assignment, including to custom properties + * specified within `customProperties`. + * @param char */ - export function default_(text: string, options?: DefaultWordBreakerOptions): Span[] { - let boundaries = findBoundaries(text, options); - if (boundaries.length == 0) { - return []; - } - - // All non-empty strings have at least TWO boundaries: at the start and at the end of - // the string. - let spans = []; - for (let i = 0; i < boundaries.length - 1; i++) { - let start = boundaries[i]; - let end = boundaries[i + 1]; - let span = new LazySpan(text, start, end); - - if (isNonSpace(span.text, options)) { - spans.push(span); - // Preserve a sequence-final space if it exists. Needed to signal "end of word". - } else if (i == boundaries.length - 2) { // if "we just checked the final boundary"... - // We don't want to return the whitespace itself; the correct token is simply ''. - span = new LazySpan(text, end, end); - spans.push(span); - } + propertyMapping?(char: string): string; + + /** + * Allows definition of extra word-breaking properties for use with custom + * rules. + */ + customProperties?: string[]; +} + +/** + * Word breaker based on Unicode Standard Annex #29, Section 4.1: + * Default Word Boundary Specification. + * + * @see http://unicode.org/reports/tr29/#Word_Boundaries + * @see https://github.com/eddieantonio/unicode-default-word-boundary/tree/v12.0.0 + */ +export default function default_(text: string, options?: DefaultWordBreakerOptions): Span[] { + let boundaries = findBoundaries(text, options); + if (boundaries.length == 0) { + return []; + } + + // All non-empty strings have at least TWO boundaries: at the start and at the end of + // the string. + let spans = []; + for (let i = 0; i < boundaries.length - 1; i++) { + let start = boundaries[i]; + let end = boundaries[i + 1]; + let span = new LazySpan(text, start, end); + + if (isNonSpace(span.text, options)) { + spans.push(span); + // Preserve a sequence-final space if it exists. Needed to signal "end of word". + } else if (i == boundaries.length - 2) { // if "we just checked the final boundary"... + // We don't want to return the whitespace itself; the correct token is simply ''. + span = new LazySpan(text, end, end); + spans.push(span); } - return spans; + } + return spans; +} + +/** + * A span that does not cut out the substring until it absolutely has to! + */ +class LazySpan implements Span { + private _source: string; + readonly start: number; + readonly end: number; + constructor(source: string, start: number, end: number) { + this._source = source; + this.start = start; + this.end = end; + } + + get text(): string { + return this._source.substring(this.start, this.end); } - // Utilities // - import WordBreakProperty = wordBreakers.data.WordBreakProperty; - import WORD_BREAK_PROPERTY = wordBreakers.data.WORD_BREAK_PROPERTY; - import I = wordBreakers.data.I; + get length(): number { + return this.end - this.start; + } +} +/** + * An abstraction supporting custom wordbreaker boundary rules. While this doesn't provide + * support for more complex rules like WB4, WB15, or WB16, this is sufficient for all other + * default word-breaking rules and can be used to define custom rules of similar structure. + * + * @see https://unicode.org/reports/tr29/#WB_Rule_Macros + */ +export interface WordbreakerRule { /** - * A span that does not cut out the substring until it absolutely has to! + * Indicates whether or not the rule applies in the specified context. + * @param context */ - class LazySpan implements Span { - private _source: string; - readonly start: number; - readonly end: number; - constructor(source: string, start: number, end: number) { - this._source = source; - this.start = start; - this.end = end; - } + match(context: BreakerContext): boolean; - get text(): string { - return this._source.substring(this.start, this.end); - } + /** + * Indicates whether or not the rule indicates a word boundary at the context's site when it matches. + */ + breakIfMatch: boolean; +} - get length(): number { - return this.end - this.start; +/** + * Provides a useful presentation for wordbreaker's context for use in word-breaking rules. + * + * @see https://unicode.org/reports/tr29/#Word_Boundary_Rules + */ +export class BreakerContext { + // Referenced by this object in order to facilitate `lookahead` maintenance. + private readonly text: string; + readonly options?: DefaultWordBreakerOptions; + + /** + * Represents the property of character immediately preceding `left`'s character. + */ + readonly lookbehind: WordBreakProperty = WordBreakProperty.sot; + + /** + * Represents the property of the character immediately preceding the potential word boundary. + */ + readonly left: WordBreakProperty = WordBreakProperty.sot; + + /** + * Represents the property of the character immediately following the potential word boundary. + */ + readonly right: WordBreakProperty = WordBreakProperty.sot; + + /** + * Represents the property of the character immediately following `right`'s character. + */ + readonly lookahead: WordBreakProperty; // Always initialized by constructor. + + /** + * Initializes the word-breaking context at the start of the word-breaker's boundary-detection + * algorithm. + * @param text The text to be word-broken + * @param lookaheadPos The position corresponding to `lookahead`. + */ + constructor(text: string, options: DefaultWordBreakerOptions | undefined, lookaheadPos: number); + /** + * Used internally by the boundary-detection algorithm during context-shifting operations. + * @param text + * @param lookbehind + * @param left + * @param right + * @param lookahead + */ + constructor(text: string, + options: DefaultWordBreakerOptions | undefined, + lookbehind: WordBreakProperty, + left: WordBreakProperty, + right: WordBreakProperty, + lookahead: WordBreakProperty); + constructor(text: string, + options: DefaultWordBreakerOptions | undefined, + prop1: WordBreakProperty | number, + prop2?: WordBreakProperty, + prop3?: WordBreakProperty, + prop4?: WordBreakProperty) { + this.text = text; + this.options = options; + + if(arguments.length == 3) { + this.lookahead = this.wordbreakPropertyAt(prop1);// prop1; + } else /*if(arguments.length == 6)*/ { + this.lookbehind = prop1 as WordBreakProperty; + this.left = prop2 as WordBreakProperty; + this.right = prop3 as WordBreakProperty; + this.lookahead = prop4 as WordBreakProperty; } } /** - * An abstraction supporting custom wordbreaker boundary rules. While this doesn't provide - * support for more complex rules like WB4, WB15, or WB16, this is sufficient for all other - * default word-breaking rules and can be used to define custom rules of similar structure. - * - * @see https://unicode.org/reports/tr29/#WB_Rule_Macros + * The general use-case when shifting boundary-check position if WB4 is not active. + * @param lookahead The WordBreakProperty for the character to become `lookahead`. + * @returns */ - export interface WordbreakerRule { - /** - * Indicates whether or not the rule applies in the specified context. - * @param context - */ - match(context: BreakerContext): boolean; - - /** - * Indicates whether or not the rule indicates a word boundary at the context's site when it matches. - */ - breakIfMatch: boolean; + public next(lookaheadPos: number): BreakerContext { + let newLookahead = this.wordbreakPropertyAt(lookaheadPos); + return new BreakerContext(this.text, this.options, this.left, this.right, this.lookahead, newLookahead); } /** - * Provides a useful presentation for wordbreaker's context for use in word-breaking rules. - * - * @see https://unicode.org/reports/tr29/#Word_Boundary_Rules + * Used for WB4: when ignoring characters before an intervening linebreak, we + * replace `right` with the current `lookahead`, without affecting `lookbehind` + * or `left`. A new `lookahead` is then needed. + * @param lookahead + * @returns */ - export class BreakerContext { - // Referenced by this object in order to facilitate `lookahead` maintenance. - private readonly text: string; - readonly options?: DefaultWordBreakerOptions; - - /** - * Represents the property of character immediately preceding `left`'s character. - */ - readonly lookbehind: WordBreakProperty = WordBreakProperty.sot; - - /** - * Represents the property of the character immediately preceding the potential word boundary. - */ - readonly left: WordBreakProperty = WordBreakProperty.sot; - - /** - * Represents the property of the character immediately following the potential word boundary. - */ - readonly right: WordBreakProperty = WordBreakProperty.sot; - - /** - * Represents the property of the character immediately following `right`'s character. - */ - readonly lookahead: WordBreakProperty; // Always initialized by constructor. - - /** - * Initializes the word-breaking context at the start of the word-breaker's boundary-detection - * algorithm. - * @param text The text to be word-broken - * @param lookaheadPos The position corresponding to `lookahead`. - */ - constructor(text: string, options: DefaultWordBreakerOptions | undefined, lookaheadPos: number); - /** - * Used internally by the boundary-detection algorithm during context-shifting operations. - * @param text - * @param lookbehind - * @param left - * @param right - * @param lookahead - */ - constructor(text: string, - options: DefaultWordBreakerOptions | undefined, - lookbehind: WordBreakProperty, - left: WordBreakProperty, - right: WordBreakProperty, - lookahead: WordBreakProperty); - constructor(text: string, - options: DefaultWordBreakerOptions | undefined, - prop1: WordBreakProperty | number, - prop2?: WordBreakProperty, - prop3?: WordBreakProperty, - prop4?: WordBreakProperty) { - this.text = text; - this.options = options; - - if(arguments.length == 3) { - this.lookahead = this.wordbreakPropertyAt(prop1);// prop1; - } else /*if(arguments.length == 6)*/ { - this.lookbehind = prop1 as WordBreakProperty; - this.left = prop2 as WordBreakProperty; - this.right = prop3 as WordBreakProperty; - this.lookahead = prop4 as WordBreakProperty; - } - } + public ignoringRight(lookaheadPos: number) { + let newLookahead = this.wordbreakPropertyAt(lookaheadPos); + return new BreakerContext(this.text, this.options, this.lookbehind, this.left, this.lookahead, newLookahead); + } - /** - * The general use-case when shifting boundary-check position if WB4 is not active. - * @param lookahead The WordBreakProperty for the character to become `lookahead`. - * @returns - */ - public next(lookaheadPos: number): BreakerContext { - let newLookahead = this.wordbreakPropertyAt(lookaheadPos); - return new BreakerContext(this.text, this.options, this.left, this.right, this.lookahead, newLookahead); - } - - /** - * Used for WB4: when ignoring characters before an intervening linebreak, we - * replace `right` with the current `lookahead`, without affecting `lookbehind` - * or `left`. A new `lookahead` is then needed. - * @param lookahead - * @returns - */ - public ignoringRight(lookaheadPos: number) { - let newLookahead = this.wordbreakPropertyAt(lookaheadPos); - return new BreakerContext(this.text, this.options, this.lookbehind, this.left, this.lookahead, newLookahead); - } - - /** - * Used for WB4: when ignoring characters after an intervening linebreak, it's - * `lookahead` that gets replaced without shifting the other tracked properties. - * @param lookahead - * @returns - */ - public ignoringLookahead(lookaheadPos: number) { - let newLookahead = this.wordbreakPropertyAt(lookaheadPos); - return new BreakerContext(this.text, this.options, this.lookbehind, this.left, this.right, newLookahead); - } - - /** - * Return the value of the Word_Break property at the given string index. - * @param pos position in the text. - */ - private wordbreakPropertyAt(pos: number) { - if (pos < 0) { - return WordBreakProperty.sot; // Always "start of string" before the string starts! - } else if (pos >= this.text.length) { - return WordBreakProperty.eot; // Always "end of string" after the string ends! - } else if (isStartOfSurrogatePair(this.text[pos])) { - // Surrogate pairs the next TWO items from the string! - return property(this.text[pos] + this.text[pos + 1]); - } - return property(this.text[pos], this.options); - } - - /** - * Returns `true` if and only if each member of the context has a property included within - * its corresponding set (when specified). Any set may be replaced with null to disable - * a check against its corresponding property. - * @param lookbehindSet - * @param leftSet - * @param rightSet - * @param lookaheadSet - */ - public match(lookbehindSet: WordBreakProperty[] | null, - leftSet: WordBreakProperty[] | null, - rightSet: WordBreakProperty[] | null, - lookaheadSet: WordBreakProperty[] | null) : boolean { - let result: boolean = lookbehindSet?.includes(this.lookbehind) ?? true; - result = result && (leftSet?.includes(this.left) ?? true); - result = result && (rightSet?.includes(this.right) ?? true); - return result && (lookaheadSet?.includes(this.lookahead) ?? true); - } - - /** - * Returns `true` if and only if each member of the context has a property included within - * its corresponding set (when specified). Any set may be replaced with null to disable - * a check against its corresponding property. - * - * Names should match those found at https://unicode.org/reports/tr29/#Word_Boundary_Rules - * or defined in the word-breaker customization options; matching is case-insensitive. - * Also includes two extra properties: - * - `sot` - start of text - * - `eot` - end of text - * @param lookbehindSet - * @param leftSet - * @param rightSet - * @param lookaheadSet - */ - public propertyMatch(lookbehindSet: string[] | null, - leftSet: string[] | null, - rightSet: string[] | null, - lookaheadSet: string[] | null) : boolean { - const propMapper = (name: string) => propertyVal(name, this.options); - return this.match(lookbehindSet?.map(propMapper) as WordBreakProperty[] | null, - leftSet?.map(propMapper) as WordBreakProperty[] | null, - rightSet?.map(propMapper) as WordBreakProperty[] | null, - lookaheadSet?.map(propMapper) as WordBreakProperty[] | null); - } + /** + * Used for WB4: when ignoring characters after an intervening linebreak, it's + * `lookahead` that gets replaced without shifting the other tracked properties. + * @param lookahead + * @returns + */ + public ignoringLookahead(lookaheadPos: number) { + let newLookahead = this.wordbreakPropertyAt(lookaheadPos); + return new BreakerContext(this.text, this.options, this.lookbehind, this.left, this.right, newLookahead); + } + + /** + * Return the value of the Word_Break property at the given string index. + * @param pos position in the text. + */ + private wordbreakPropertyAt(pos: number) { + if (pos < 0) { + return WordBreakProperty.sot; // Always "start of string" before the string starts! + } else if (pos >= this.text.length) { + return WordBreakProperty.eot; // Always "end of string" after the string ends! + } else if (isStartOfSurrogatePair(this.text[pos])) { + // Surrogate pairs the next TWO items from the string! + return property(this.text[pos] + this.text[pos + 1]); } + return property(this.text[pos], this.options); + } /** - * Returns true when the chunk does not solely consist of whitespace. - * - * @param chunk a chunk of text. Starts and ends at word boundaries. + * Returns `true` if and only if each member of the context has a property included within + * its corresponding set (when specified). Any set may be replaced with null to disable + * a check against its corresponding property. + * @param lookbehindSet + * @param leftSet + * @param rightSet + * @param lookaheadSet */ - function isNonSpace(chunk: string, options?: DefaultWordBreakerOptions): boolean { - return !Array.from(chunk).map((char) => property(char, options)).every(wb => ( - wb === WordBreakProperty.CR || - wb === WordBreakProperty.LF || - wb === WordBreakProperty.Newline || - wb === WordBreakProperty.WSegSpace - )); + public match(lookbehindSet: WordBreakProperty[] | null, + leftSet: WordBreakProperty[] | null, + rightSet: WordBreakProperty[] | null, + lookaheadSet: WordBreakProperty[] | null) : boolean { + let result: boolean = lookbehindSet?.includes(this.lookbehind) ?? true; + result = result && (leftSet?.includes(this.left) ?? true); + result = result && (rightSet?.includes(this.right) ?? true); + return result && (lookaheadSet?.includes(this.lookahead) ?? true); } /** - * Yields a series of string indices where a word break should - * occur. That is, there should be a break BEFORE each string - * index yielded by this generator. + * Returns `true` if and only if each member of the context has a property included within + * its corresponding set (when specified). Any set may be replaced with null to disable + * a check against its corresponding property. * - * @param text Text to find word boundaries in. + * Names should match those found at https://unicode.org/reports/tr29/#Word_Boundary_Rules + * or defined in the word-breaker customization options; matching is case-insensitive. + * Also includes two extra properties: + * - `sot` - start of text + * - `eot` - end of text + * @param lookbehindSet + * @param leftSet + * @param rightSet + * @param lookaheadSet */ - function findBoundaries(text: string, options?: DefaultWordBreakerOptions): number[] { - // WB1 and WB2: no boundaries if given an empty string. - if (text.length === 0) { - // There are no boundaries in an empty string! - return []; - } - - if(options && !options.rules) { - options.rules = []; - } - - // This algorithm works by maintaining a sliding window of four SCALAR VALUES. - // - // - Scalar values? JavaScript strings are NOT actually a string of - // Unicode code points; some characters are made up of TWO - // JavaScript indices. e.g., - // "💩".length === 2; - // "💩"[0] === '\uD83D'; - // "💩"[1] === '\uDCA9'; - // - // These characters that are represented by TWO indices are - // called "surrogate pairs". Since we don't want to be in the - // "middle" of a character, make sure we're always advancing - // by scalar values, and NOT indices. That means, we sometimes - // need to advance by TWO indices, not just one. - // - Four values? Some rules look at what's to the left of - // left, and some look at what's to the right of right. So - // keep track of this! - - let boundaries = []; - - let rightPos: number; - let lookaheadPos = 0; // lookahead, one scalar value to the right of right. - // Before the start of the string is also the start of the string. - let state = new BreakerContext(text, options, lookaheadPos); - // Count RIs to make sure we're not splitting emoji flags: - let nConsecutiveRegionalIndicators = 0; - - do { - // Shift all positions, one scalar value to the right. - rightPos = lookaheadPos; - lookaheadPos = positionAfter(lookaheadPos); - // Shift all properties, one scalar value to the right. - state = state.next(lookaheadPos); + public propertyMatch(lookbehindSet: string[] | null, + leftSet: string[] | null, + rightSet: string[] | null, + lookaheadSet: string[] | null) : boolean { + const propMapper = (name: string) => propertyVal(name, this.options); + return this.match(lookbehindSet?.map(propMapper) as WordBreakProperty[] | null, + leftSet?.map(propMapper) as WordBreakProperty[] | null, + rightSet?.map(propMapper) as WordBreakProperty[] | null, + lookaheadSet?.map(propMapper) as WordBreakProperty[] | null); + } + } - // Break at the start and end of text, unless the text is empty. - // WB1: Break at start of text... - if (state.match(null, [WordBreakProperty.sot], null, null)) { - boundaries.push(rightPos); - continue; - } - // WB2: Break at the end of text... - if (state.match(null, null, [WordBreakProperty.eot], null)) { - boundaries.push(rightPos); - break; // Reached the end of the string. We're done! - } - // WB3: Do not break within CRLF: - if (state.match(null, [WordBreakProperty.CR], [WordBreakProperty.LF], null)) { - continue; - } +/** + * Returns true when the chunk does not solely consist of whitespace. + * + * @param chunk a chunk of text. Starts and ends at word boundaries. + */ +function isNonSpace(chunk: string, options?: DefaultWordBreakerOptions): boolean { + return !Array.from(chunk).map((char) => property(char, options)).every(wb => ( + wb === WordBreakProperty.CR || + wb === WordBreakProperty.LF || + wb === WordBreakProperty.Newline || + wb === WordBreakProperty.WSegSpace + )); +} - // WB3b: Otherwise, break after... - const NEWLINE_SET = [WordBreakProperty.Newline, WordBreakProperty.CR, WordBreakProperty.LF]; - if(state.match(null, NEWLINE_SET, null, null)) { - boundaries.push(rightPos); - continue; - } - // WB3a: ...and before newlines - if (state.match(null, null, NEWLINE_SET, null)) { - boundaries.push(rightPos); - continue; - } +/** + * Yields a series of string indices where a word break should + * occur. That is, there should be a break BEFORE each string + * index yielded by this generator. + * + * @param text Text to find word boundaries in. + */ +function findBoundaries(text: string, options?: DefaultWordBreakerOptions): number[] { + // WB1 and WB2: no boundaries if given an empty string. + if (text.length === 0) { + // There are no boundaries in an empty string! + return []; + } - // TODO: WB3c is not implemented, due to its complex, error-prone - // implementation, requiring a ginormous regexp, and the fact that - // the only thing it does is prevent big emoji sequences from being - // split up, like 🧚🏼‍♂️ - // https://www.unicode.org/Public/emoji/12.0/emoji-zwj-sequences.txt + if(options && !options.rules) { + options.rules = []; + } - // WB3d: Keep horizontal whitespace together - if (state.match(null, [WordBreakProperty.WSegSpace], [WordBreakProperty.WSegSpace], null)) { - continue; - } + // This algorithm works by maintaining a sliding window of four SCALAR VALUES. + // + // - Scalar values? JavaScript strings are NOT actually a string of + // Unicode code points; some characters are made up of TWO + // JavaScript indices. e.g., + // "💩".length === 2; + // "💩"[0] === '\uD83D'; + // "💩"[1] === '\uDCA9'; + // + // These characters that are represented by TWO indices are + // called "surrogate pairs". Since we don't want to be in the + // "middle" of a character, make sure we're always advancing + // by scalar values, and NOT indices. That means, we sometimes + // need to advance by TWO indices, not just one. + // - Four values? Some rules look at what's to the left of + // left, and some look at what's to the right of right. So + // keep track of this! + + let boundaries = []; + + let rightPos: number; + let lookaheadPos = 0; // lookahead, one scalar value to the right of right. + // Before the start of the string is also the start of the string. + let state = new BreakerContext(text, options, lookaheadPos); + // Count RIs to make sure we're not splitting emoji flags: + let nConsecutiveRegionalIndicators = 0; + + do { + // Shift all positions, one scalar value to the right. + rightPos = lookaheadPos; + lookaheadPos = positionAfter(lookaheadPos); + // Shift all properties, one scalar value to the right. + state = state.next(lookaheadPos); + + // Break at the start and end of text, unless the text is empty. + // WB1: Break at start of text... + if (state.match(null, [WordBreakProperty.sot], null, null)) { + boundaries.push(rightPos); + continue; + } + // WB2: Break at the end of text... + if (state.match(null, null, [WordBreakProperty.eot], null)) { + boundaries.push(rightPos); + break; // Reached the end of the string. We're done! + } + // WB3: Do not break within CRLF: + if (state.match(null, [WordBreakProperty.CR], [WordBreakProperty.LF], null)) { + continue; + } - // WB4: Ignore format and extend characters - // This is to keep grapheme clusters together! - // See: Section 6.2: https://unicode.org/reports/tr29/#Grapheme_Cluster_and_Format_Rules - // N.B.: The rule about "except after sot, CR, LF, and - // Newline" already been by WB1, WB2, WB3a, and WB3b above. - const SET_WB4_IGNORE = [WordBreakProperty.Format, WordBreakProperty.Extend, WordBreakProperty.ZWJ]; - while (state.match(null, null, SET_WB4_IGNORE, null)) { - // Continue advancing in the string, as if these - // characters do not exist. DO NOT update left and - // lookbehind however! - [rightPos, lookaheadPos] = [lookaheadPos, positionAfter(lookaheadPos)]; - state = state.ignoringRight(lookaheadPos); - } - // In ignoring the characters in the previous loop, we could - // have fallen off the end of the string, so end the loop - // prematurely if that happens! - if (state.right === WordBreakProperty.eot) { - boundaries.push(rightPos); - break; - } - // WB4 (continued): Lookahead must ALSO ignore these format, - // extend, ZWJ characters! - while (state.match(null, null, null, SET_WB4_IGNORE)) { - // Continue advancing in the string, as if these - // characters do not exist. DO NOT update left and right, - // however! - lookaheadPos = positionAfter(lookaheadPos); - state = state.ignoringLookahead(lookaheadPos); - } + // WB3b: Otherwise, break after... + const NEWLINE_SET = [WordBreakProperty.Newline, WordBreakProperty.CR, WordBreakProperty.LF]; + if(state.match(null, NEWLINE_SET, null, null)) { + boundaries.push(rightPos); + continue; + } + // WB3a: ...and before newlines + if (state.match(null, null, NEWLINE_SET, null)) { + boundaries.push(rightPos); + continue; + } - // See: https://unicode.org/reports/tr29/#WB_Rule_Macros - const SET_AHLETTER = [WordBreakProperty.ALetter, WordBreakProperty.Hebrew_Letter]; - const SET_MIDNUMLETQ = [WordBreakProperty.MidNumLet, WordBreakProperty.Single_Quote]; - - // Custom rules may override the base ruleset aside from the first few fundamental ones. - if(options?.rules) { - let customMatch: boolean = false; - for(const rule of options.rules) { - customMatch = rule.match(state); - if(customMatch) { - if(rule.breakIfMatch) { - boundaries.push(rightPos); - } - break; // as customMatch == true here, this will trigger the `continue` that follows. - } - } + // TODO: WB3c is not implemented, due to its complex, error-prone + // implementation, requiring a ginormous regexp, and the fact that + // the only thing it does is prevent big emoji sequences from being + // split up, like 🧚🏼‍♂️ + // https://www.unicode.org/Public/emoji/12.0/emoji-zwj-sequences.txt + + // WB3d: Keep horizontal whitespace together + if (state.match(null, [WordBreakProperty.WSegSpace], [WordBreakProperty.WSegSpace], null)) { + continue; + } + + // WB4: Ignore format and extend characters + // This is to keep grapheme clusters together! + // See: Section 6.2: https://unicode.org/reports/tr29/#Grapheme_Cluster_and_Format_Rules + // N.B.: The rule about "except after sot, CR, LF, and + // Newline" already been by WB1, WB2, WB3a, and WB3b above. + const SET_WB4_IGNORE = [WordBreakProperty.Format, WordBreakProperty.Extend, WordBreakProperty.ZWJ]; + while (state.match(null, null, SET_WB4_IGNORE, null)) { + // Continue advancing in the string, as if these + // characters do not exist. DO NOT update left and + // lookbehind however! + [rightPos, lookaheadPos] = [lookaheadPos, positionAfter(lookaheadPos)]; + state = state.ignoringRight(lookaheadPos); + } + // In ignoring the characters in the previous loop, we could + // have fallen off the end of the string, so end the loop + // prematurely if that happens! + if (state.right === WordBreakProperty.eot) { + boundaries.push(rightPos); + break; + } + // WB4 (continued): Lookahead must ALSO ignore these format, + // extend, ZWJ characters! + while (state.match(null, null, null, SET_WB4_IGNORE)) { + // Continue advancing in the string, as if these + // characters do not exist. DO NOT update left and right, + // however! + lookaheadPos = positionAfter(lookaheadPos); + state = state.ignoringLookahead(lookaheadPos); + } + + // See: https://unicode.org/reports/tr29/#WB_Rule_Macros + const SET_AHLETTER = [WordBreakProperty.ALetter, WordBreakProperty.Hebrew_Letter]; + const SET_MIDNUMLETQ = [WordBreakProperty.MidNumLet, WordBreakProperty.Single_Quote]; + + // Custom rules may override the base ruleset aside from the first few fundamental ones. + if(options?.rules) { + let customMatch: boolean = false; + for(const rule of options.rules) { + customMatch = rule.match(state); if(customMatch) { - continue; + if(rule.breakIfMatch) { + boundaries.push(rightPos); + } + break; // as customMatch == true here, this will trigger the `continue` that follows. } } - - // WB5: Do not break between most letters. - // if (isAHLetter(state.left) && isAHLetter(state.right)) - if(state.match(null, SET_AHLETTER, SET_AHLETTER, null)) { - continue; - } - // Do not break across certain punctuation - // WB6: (Don't break before apostrophes in contractions) - const SET_ALL_MIDLETTER = [WordBreakProperty.MidLetter, ...SET_MIDNUMLETQ]; - if(state.match(null, SET_AHLETTER, SET_ALL_MIDLETTER, SET_AHLETTER)) { - continue; - } - // WB7: (Don't break after apostrophes in contractions) - if(state.match(SET_AHLETTER, SET_ALL_MIDLETTER, SET_AHLETTER, null)) { + if(customMatch) { continue; } + } - // WB7a - if(state.match(null, [WordBreakProperty.Hebrew_Letter], [WordBreakProperty.Single_Quote], null)) { - continue; - } - // WB7b - if(state.match(null, - [WordBreakProperty.Hebrew_Letter], - [WordBreakProperty.Double_Quote], - [WordBreakProperty.Hebrew_Letter])) { - continue; - } - // WB7c - if(state.match([WordBreakProperty.Hebrew_Letter], - [WordBreakProperty.Double_Quote], - [WordBreakProperty.Hebrew_Letter], - null)) { - continue; - } - // Do not break within sequences of digits, or digits adjacent to letters. - // e.g., "3a" or "A3" - // WB8 - if(state.match(null, [WordBreakProperty.Numeric], [WordBreakProperty.Numeric], null)) { - continue; - } - // WB9 - if(state.match(null, SET_AHLETTER, [WordBreakProperty.Numeric], null)) { - continue; - } - // WB10 - if(state.match(null, [WordBreakProperty.Numeric], SET_AHLETTER, null)) { - continue; - } - // Do not break within sequences, such as 3.2, 3,456.789 - // WB11 - const SET_ALL_MIDNUM = [WordBreakProperty.MidNum, ...SET_MIDNUMLETQ]; - if(state.match([WordBreakProperty.Numeric], SET_ALL_MIDNUM, [WordBreakProperty.Numeric], null)) { - continue; - } - // WB12 - if(state.match(null, [WordBreakProperty.Numeric], SET_ALL_MIDNUM, [WordBreakProperty.Numeric])) { - continue; - } - // WB13: Do not break between Katakana - if(state.match(null, [WordBreakProperty.Katakana], [WordBreakProperty.Katakana], null)) { - continue; - } - // Do not break from extenders (e.g., U+202F NARROW NO-BREAK SPACE) - // WB13a - const SET_NUM_KAT_LET = [WordBreakProperty.Katakana, - WordBreakProperty.Numeric, - ...SET_AHLETTER]; - if(state.match(null, SET_NUM_KAT_LET, [WordBreakProperty.ExtendNumLet], null)) { - continue; - } - if(state.match(null, [WordBreakProperty.ExtendNumLet], [WordBreakProperty.ExtendNumLet], null)) { - continue; - } - // WB13b - if(state.match(null, [WordBreakProperty.ExtendNumLet], SET_NUM_KAT_LET, null)) { - continue; - } + // WB5: Do not break between most letters. + // if (isAHLetter(state.left) && isAHLetter(state.right)) + if(state.match(null, SET_AHLETTER, SET_AHLETTER, null)) { + continue; + } + // Do not break across certain punctuation + // WB6: (Don't break before apostrophes in contractions) + const SET_ALL_MIDLETTER = [WordBreakProperty.MidLetter].concat(SET_MIDNUMLETQ); + if(state.match(null, SET_AHLETTER, SET_ALL_MIDLETTER, SET_AHLETTER)) { + continue; + } + // WB7: (Don't break after apostrophes in contractions) + if(state.match(SET_AHLETTER, SET_ALL_MIDLETTER, SET_AHLETTER, null)) { + continue; + } - // WB15 & WB16: - // Do not break within emoji flag sequences. That is, do not break between - // regional indicator (RI) symbols if there is an odd number of RI - // characters before the break point. - if (state.right === WordBreakProperty.Regional_Indicator) { - // Emoji flags are actually composed of TWO scalar values, each being a - // "regional indicator". These indicators correspond to Latin letters. Put - // two of them together, and they spell out an ISO 3166-1-alpha-2 country - // code. Since these always come in pairs, NEVER split the pairs! So, if - // we happen to be inside the middle of an odd numbered of - // Regional_Indicators, DON'T SPLIT IT! - nConsecutiveRegionalIndicators += 1; - if ((nConsecutiveRegionalIndicators % 2) == 1) { - continue; - } - } else { - nConsecutiveRegionalIndicators = 0; - } - // WB999: Otherwise, break EVERYWHERE (including around ideographs) - boundaries.push(rightPos); - } while (rightPos < text.length); - - return boundaries; - - ///// Internal utility functions ///// - - /** - * Returns the position of the start of the next scalar value. This jumps - * over surrogate pairs. - * - * If asked for the character AFTER the end of the string, this always - * returns the length of the string. - */ - function positionAfter(pos: number): number { - if (pos >= text.length) { - return text.length; - } else if (isStartOfSurrogatePair(text[pos])) { - return pos + 2; + // WB7a + if(state.match(null, [WordBreakProperty.Hebrew_Letter], [WordBreakProperty.Single_Quote], null)) { + continue; + } + // WB7b + if(state.match(null, + [WordBreakProperty.Hebrew_Letter], + [WordBreakProperty.Double_Quote], + [WordBreakProperty.Hebrew_Letter])) { + continue; + } + // WB7c + if(state.match([WordBreakProperty.Hebrew_Letter], + [WordBreakProperty.Double_Quote], + [WordBreakProperty.Hebrew_Letter], + null)) { + continue; + } + // Do not break within sequences of digits, or digits adjacent to letters. + // e.g., "3a" or "A3" + // WB8 + if(state.match(null, [WordBreakProperty.Numeric], [WordBreakProperty.Numeric], null)) { + continue; + } + // WB9 + if(state.match(null, SET_AHLETTER, [WordBreakProperty.Numeric], null)) { + continue; + } + // WB10 + if(state.match(null, [WordBreakProperty.Numeric], SET_AHLETTER, null)) { + continue; + } + // Do not break within sequences, such as 3.2, 3,456.789 + // WB11 + const SET_ALL_MIDNUM = [WordBreakProperty.MidNum].concat(SET_MIDNUMLETQ); + if(state.match([WordBreakProperty.Numeric], SET_ALL_MIDNUM, [WordBreakProperty.Numeric], null)) { + continue; + } + // WB12 + if(state.match(null, [WordBreakProperty.Numeric], SET_ALL_MIDNUM, [WordBreakProperty.Numeric])) { + continue; + } + // WB13: Do not break between Katakana + if(state.match(null, [WordBreakProperty.Katakana], [WordBreakProperty.Katakana], null)) { + continue; + } + // Do not break from extenders (e.g., U+202F NARROW NO-BREAK SPACE) + // WB13a + const SET_NUM_KAT_LET = [WordBreakProperty.Katakana, WordBreakProperty.Numeric].concat(SET_AHLETTER); + if(state.match(null, SET_NUM_KAT_LET, [WordBreakProperty.ExtendNumLet], null)) { + continue; + } + if(state.match(null, [WordBreakProperty.ExtendNumLet], [WordBreakProperty.ExtendNumLet], null)) { + continue; + } + // WB13b + if(state.match(null, [WordBreakProperty.ExtendNumLet], SET_NUM_KAT_LET, null)) { + continue; + } + + // WB15 & WB16: + // Do not break within emoji flag sequences. That is, do not break between + // regional indicator (RI) symbols if there is an odd number of RI + // characters before the break point. + if (state.right === WordBreakProperty.Regional_Indicator) { + // Emoji flags are actually composed of TWO scalar values, each being a + // "regional indicator". These indicators correspond to Latin letters. Put + // two of them together, and they spell out an ISO 3166-1-alpha-2 country + // code. Since these always come in pairs, NEVER split the pairs! So, if + // we happen to be inside the middle of an odd numbered of + // Regional_Indicators, DON'T SPLIT IT! + nConsecutiveRegionalIndicators += 1; + if ((nConsecutiveRegionalIndicators % 2) == 1) { + continue; } - return pos + 1; + } else { + nConsecutiveRegionalIndicators = 0; } - } + // WB999: Otherwise, break EVERYWHERE (including around ideographs) + boundaries.push(rightPos); + } while (rightPos < text.length); - function isStartOfSurrogatePair(character: string) { - let codeUnit = character.charCodeAt(0); - return codeUnit >= 0xD800 && codeUnit <= 0xDBFF; - } + return boundaries; + + ///// Internal utility functions ///// /** - * Return the Word_Break property value for a character. - * Note that - * @param character a scalar value + * Returns the position of the start of the next scalar value. This jumps + * over surrogate pairs. + * + * If asked for the character AFTER the end of the string, this always + * returns the length of the string. */ - function property(character: string, options?: DefaultWordBreakerOptions): WordBreakProperty { - // If there is a customized mapping for the character, prioritize that. - if(options?.propertyMapping) { - let propName = options.propertyMapping(character); - if(propName) { - return propertyVal(propName, options); - } + function positionAfter(pos: number): number { + if (pos >= text.length) { + return text.length; + } else if (isStartOfSurrogatePair(text[pos])) { + return pos + 2; } + return pos + 1; + } +} - // This MUST be a scalar value. - // TODO: remove dependence on character.codepointAt()? - let codepoint = character.codePointAt(0) as number; +function isStartOfSurrogatePair(character: string) { + let codeUnit = character.charCodeAt(0); + return codeUnit >= 0xD800 && codeUnit <= 0xDBFF; +} - return searchForProperty(codepoint, 0, WORD_BREAK_PROPERTY.length - 1); +/** + * Return the Word_Break property value for a character. + * Note that + * @param character a scalar value + */ +function property(character: string, options?: DefaultWordBreakerOptions): WordBreakProperty { + // If there is a customized mapping for the character, prioritize that. + if(options?.propertyMapping) { + let propName = options.propertyMapping(character); + if(propName) { + return propertyVal(propName, options); + } } - function propertyVal(propName: string, options?: DefaultWordBreakerOptions) { - const matcher = (name: string) => name.toLowerCase() == propName.toLowerCase() + // This MUST be a scalar value. + // TODO: remove dependence on character.codepointAt()? + let codepoint = character.codePointAt(0) as number; - const customIndex = options?.customProperties?.findIndex(matcher) ?? -1; - return customIndex != -1 ? -customIndex - 1 : data.propertyMap.findIndex(matcher); - } + return searchForProperty(codepoint, 0, WORD_BREAK_PROPERTY.length - 1); +} - /** - * Binary search for the word break property of a given CODE POINT. - * - * The auto-generated data.ts master array defines a **character range** - * lookup table. If a character's codepoint is equal to or greater than - * the I.Start value for an entry and exclusively less than the next entry, - * it falls in the first entry's range bucket and is classified accordingly - * by this method. - */ - function searchForProperty(codePoint: number, left: number, right: number): WordBreakProperty { - // All items that are not found in the array are assigned the 'Other' property. - if (right < left) { - return WordBreakProperty.Other; - } +function propertyVal(propName: string, options?: DefaultWordBreakerOptions) { + const matcher = (name: string) => name.toLowerCase() == propName.toLowerCase() - let midpoint = left + ~~((right - left) / 2); - let candidate = WORD_BREAK_PROPERTY[midpoint]; + const customIndex = options?.customProperties?.findIndex(matcher) ?? -1; + return customIndex != -1 ? -customIndex - 1 : propertyMap.findIndex(matcher); +} - let nextRange = WORD_BREAK_PROPERTY[midpoint + 1]; - let startOfNextRange = nextRange ? nextRange[I.Start] : Infinity; +/** + * Binary search for the word break property of a given CODE POINT. + * + * The auto-generated data.ts master array defines a **character range** + * lookup table. If a character's codepoint is equal to or greater than + * the I.Start value for an entry and exclusively less than the next entry, + * it falls in the first entry's range bucket and is classified accordingly + * by this method. + */ +function searchForProperty(codePoint: number, left: number, right: number): WordBreakProperty { + // All items that are not found in the array are assigned the 'Other' property. + if (right < left) { + return WordBreakProperty.Other; + } - if (codePoint < candidate[I.Start]) { - return searchForProperty(codePoint, left, midpoint - 1); - } else if (codePoint >= startOfNextRange) { - return searchForProperty(codePoint, midpoint + 1, right); - } + let midpoint = left + ~~((right - left) / 2); + let candidate = WORD_BREAK_PROPERTY[midpoint]; - // We found it! - return candidate[I.Value]; + let nextRange = WORD_BREAK_PROPERTY[midpoint + 1]; + let startOfNextRange = nextRange ? nextRange[I.Start] : Infinity; + + if (codePoint < candidate[I.Start]) { + return searchForProperty(codePoint, left, midpoint - 1); + } else if (codePoint >= startOfNextRange) { + return searchForProperty(codePoint, midpoint + 1, right); } -} -// We cannot export a member whose name is a reserved word when -// implementing a namespace, BUT we can manually make the -// assignment and **declare** it as part of the namespace. -wordBreakers['default'] = wordBreakers.default_; -declare namespace wordBreakers { - export { default_ as default }; + // We found it! + return candidate[I.Value]; } diff --git a/common/models/wordbreakers/src/index.ts b/common/models/wordbreakers/src/index.ts index 2140ae6bf4cf0157cdb93dff26d5dabcc4cb2a46..7f1682fca27c7599385e983f6d5eda72ca6688c3 100644 --- a/common/models/wordbreakers/src/index.ts +++ b/common/models/wordbreakers/src/index.ts @@ -1,8 +1,5 @@ -/// -/// -/// +import placeholder from "./placeholder.js"; +import ascii from "./ascii.js"; +import default_ from "./default/index.js"; -// Let the word breakers be available both in the browser and in Node. -if (typeof module !== "undefined" && (module as any).exports) { - (module as any).exports['wordBreakers'] = wordBreakers; -} +export { placeholder, ascii, default_ as default, default_ as defaultWordbreaker }; \ No newline at end of file diff --git a/common/models/wordbreakers/src/placeholder.ts b/common/models/wordbreakers/src/placeholder.ts index 1105b89fb99b56e4a6182d5237aa9ac8b88c561c..87832505ed046761bda5301d3b4a63ceb2dac1ad 100644 --- a/common/models/wordbreakers/src/placeholder.ts +++ b/common/models/wordbreakers/src/placeholder.ts @@ -1,24 +1,21 @@ -namespace wordBreakers { - - /** - * A **VERY** dumb word breaker that simply splits at words. Do not use this - * word breaker! - * - * @param phrase The phrase in which to break words. - * @deprecated Use a word breaker tailored to your language instead! - */ - export function placeholder(phrase: string): Span[] { - let nextStart = 0; - return phrase.split(/\s+/).map(utterance => { - // XXX: The indices are NOT accurate to the original phrase! - let span = { - start: nextStart, - end: nextStart + utterance.length, - text: utterance, - length: utterance.length - }; - nextStart = span.end; - return span; - }); - } +/** + * A **VERY** dumb word breaker that simply splits at words. Do not use this + * word breaker! + * + * @param phrase The phrase in which to break words. + * @deprecated Use a word breaker tailored to your language instead! + */ +export default function placeholder(phrase: string): Span[] { + let nextStart = 0; + return phrase.split(/\s+/).map(utterance => { + // XXX: The indices are NOT accurate to the original phrase! + let span = { + start: nextStart, + end: nextStart + utterance.length, + text: utterance, + length: utterance.length + }; + nextStart = span.end; + return span; + }); } diff --git a/common/models/wordbreakers/test/test-ascii-word-breaker.js b/common/models/wordbreakers/test/test-ascii-word-breaker.js index 55b9c3b8ba0f5f440e266955eee39b04d1f214cd..843df8c002aeb050814bfded663c2b796d6be285 100644 --- a/common/models/wordbreakers/test/test-ascii-word-breaker.js +++ b/common/models/wordbreakers/test/test-ascii-word-breaker.js @@ -1,5 +1,5 @@ -var assert = require('chai').assert; -var breakASCIIWords = require('../build').wordBreakers['ascii']; +import { assert } from 'chai'; +import { ascii as breakASCIIWords } from '@keymanapp/models-wordbreakers'; describe('The ASCII word breaker', function () { it('should break simple English sentences', function () { diff --git a/common/models/wordbreakers/test/test-default-word-breaker.js b/common/models/wordbreakers/test/test-default-word-breaker.js index 85129b836ccd3673c69cd7f8efb62c37d5934e7d..17c03cc4b8a0259161923893d7c0b29b6b4b4d49 100644 --- a/common/models/wordbreakers/test/test-default-word-breaker.js +++ b/common/models/wordbreakers/test/test-default-word-breaker.js @@ -2,8 +2,8 @@ * Smoke-test the default */ -const assert = require('chai').assert; -const breakWords = require('../build').wordBreakers['default']; +import { assert } from 'chai'; +import { default as breakWords } from '@keymanapp/models-wordbreakers'; const SHY = '\u00AD'; // Other, Format. The "Soft HYphen" - usually invisible unless needed for word-wrapping. diff --git a/common/models/wordbreakers/test/test-placeholder-word-breaker.js b/common/models/wordbreakers/test/test-placeholder-word-breaker.js index dbc1aef34e61714e9d127d85d6f0519b52902a28..0991315cd25805ee8f754c03059241c15d193fbc 100644 --- a/common/models/wordbreakers/test/test-placeholder-word-breaker.js +++ b/common/models/wordbreakers/test/test-placeholder-word-breaker.js @@ -1,6 +1,6 @@ -const assert = require('chai').assert; -const breakWords = require('../build').wordBreakers['placeholder']; +import { assert } from 'chai'; +import { placeholder as breakWords } from '@keymanapp/models-wordbreakers'; describe('The placeholder word breaker', function () { it('should break simple English sentences', function () { diff --git a/common/models/wordbreakers/tsconfig.json b/common/models/wordbreakers/tsconfig.json index 6575d9e7c568e7880dd0594afb78d80a303de226..54d12d5d8c78bcda471741a60aec4e9db6bf604b 100644 --- a/common/models/wordbreakers/tsconfig.json +++ b/common/models/wordbreakers/tsconfig.json @@ -1,15 +1,11 @@ { - "extends": "../../../tsconfig-base.json", + "extends": "../tsconfig.kmw-worker-base.json", "compilerOptions": { - "module": "none", - "lib": ["es6"], - "target": "es5", - "declaration": true, - "sourceMap": true, - "outFile": "./build/index.js", - "strict": true, - "allowJs": false, + "baseUrl": "./", + "outDir": "build/obj", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", + "rootDir": "./src" }, "references": [ { "path": "../types" } diff --git a/common/predictive-text/.gitignore b/common/predictive-text/.gitignore index 924ca9595eb0d21772177fe976896e9e3f57abbe..b4aef947b4ad67df7cebb579736918eb28bcd390 100644 --- a/common/predictive-text/.gitignore +++ b/common/predictive-text/.gitignore @@ -68,6 +68,7 @@ typings/ **/*.js # Intentional JavaScript files. +!build-bundler.js !testing/**/*.js !unit_tests/**/*.js !polyfills/**/*.js diff --git a/common/predictive-text/browser.tsconfig.json b/common/predictive-text/browser.tsconfig.json deleted file mode 100644 index 929924f9f16ea7b81c496526f03c658068b6ff7f..0000000000000000000000000000000000000000 --- a/common/predictive-text/browser.tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "extends": "../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": true, - "module": "none", - "outFile": "./build/index.js", - "inlineSources": true, - "sourceMap": true, - "target": "es5", - "lib": ["es6", "dom"] - }, - "include" : ["./*.ts"], - "references": [ - { "path": "../models/types" }, - { "path": "../models/wordbreakers" }, - { "path": "../models/templates" }, - { "path": "../web/lm-message-types" } - ], - "exclude" : [ - "node-defaultWorker.ts", - "virtualizedWorker.ts", - "index.d.ts", - "worker-interface.d.ts", - "node_modules", - "testing" - ], - "files": [ - "../web/lm-worker/build/index.wrapped-for-bundle.ts" - ] -} diff --git a/common/predictive-text/build-bundler.js b/common/predictive-text/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..12eac857a5017ea04e9a1898a7f367c95ad97fdc --- /dev/null +++ b/common/predictive-text/build-bundler.js @@ -0,0 +1,10 @@ +import esbuild from 'esbuild'; +import { esmConfiguration } from '../web/es-bundling/build/index.mjs'; + +await esbuild.build({ + ...esmConfiguration, + entryPoints: { + 'index': 'build/obj/web/index.js', + }, + outdir: 'build/lib/web' // We want to preserve the subdirectory for this one. +}); \ No newline at end of file diff --git a/common/predictive-text/build.sh b/common/predictive-text/build.sh index 617eaa379b7707b07e138f7fe402c6c57a51902e..cd304c0b348a303e08d2cd02a6dd4c48e345a31f 100755 --- a/common/predictive-text/build.sh +++ b/common/predictive-text/build.sh @@ -22,80 +22,46 @@ cd "$(dirname "$THIS_SCRIPT")" ################################ Main script ################################ -# TODO: once these modules are builder-based, reference here too: -# "@../models/templates" \ -# "@../models/types" \ -# "@../models/wordbreakers" +# "@../models/types" \ # is just a .d.ts, so there's nothing to actually BUILD. builder_describe "Builds the lm-layer module" \ "@/common/web/keyman-version" \ + "@/common/web/es-bundling" \ "@/common/web/lm-worker" \ "clean" \ "configure" \ "build" \ "test" \ - ":headless A headless, Node-oriented version of the module useful for unit tests" \ - ":browser The standard version of the module for in-browser use" \ "--ci Sets $(builder_term test) action to use CI-based test configurations & reporting" builder_describe_outputs \ - configure /node_modules \ - configure:headless /node_modules \ - configure:browser /node_modules \ - build:headless /common/predictive-text/build/headless.js \ - build:browser /common/predictive-text/build/index.js + configure /node_modules \ + build /common/predictive-text/build/lib/web/index.mjs # is built by the final step. builder_parse "$@" -### CONFIGURE ACTIONS +function do_build() { + # Builds the top-level JavaScript file for use on Node + tsc -b ./tsconfig.all.json -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi + # esbuild-bundled products at this level are not intended to be used for anything but testing. + node build-bundler.js +} -### CLEAN ACTIONS - -if builder_start_action clean; then - rm -rf build/ - builder_finish_action success clean -fi - -### BUILD ACTIONS - -# Builds the top-level JavaScript file for use in browsers -if builder_start_action build:browser; then - npm run tsc -- -b ./browser.tsconfig.json - - builder_finish_action success build:browser -fi - -# Builds the top-level JavaScript file for use on Node -if builder_start_action build:headless; then - npm run tsc -- -b ./tsconfig.json - - builder_finish_action success build:headless -fi - -### TEST ACTIONS # Note - the actual test setup is done in a separate test script, but it's easy # enough to route the calls through. - -TEST_OPTIONS= -if builder_has_option --ci; then - TEST_OPTIONS=--ci -fi - -if builder_start_action test:headless; then - # We'll test the included libraries here for now, at least until we have - # converted their builds to builder scripts - ./unit_tests/test.sh test:libraries test:headless $TEST_OPTIONS - - builder_finish_action success test:headless -fi - -if builder_start_action test:browser; then - ./unit_tests/test.sh test:browser $TEST_OPTIONS - - builder_finish_action success test:browser -fi +function do_test() { + local TEST_OPTIONS= + if builder_has_option --ci; then + TEST_OPTIONS=--ci + fi + + # We'll test the included libraries here for now. At some point, we may wish + # to establish a ci.sh script for predictive-text to handle this instead. + ./unit_tests/test.sh test:libraries test:headless test:browser $TEST_OPTIONS +} + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/predictive-text/index.d.ts b/common/predictive-text/index.d.ts deleted file mode 100644 index 3dd8c4b1f47d3bc625a73426ff2b20ab0266a618..0000000000000000000000000000000000000000 --- a/common/predictive-text/index.d.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Defines TS method typing for the LMLayer's public API. - * - * Unfortunately, it is not possible to do this programmatically at this time, as TS won't output declarations - * if any JS files are allowed in compilation. We can't convert the embedded worker's intermediate file to TS - * safely due to DedicatedWorkerGlobalScope and a few other typing issues. - */ - -/// -/// - -declare namespace com.keyman.text.prediction { - class LMLayer { - - /** - * Construct the top-level LMLayer interface. This also starts the underlying Worker. - * Make sure to call .load() when using the default Worker. - * - * @param uri URI of the underlying LMLayer worker code. This will usually be a blob: - * or file: URI. If uri is not provided, this will start the default Worker. - */ - constructor(capabilities: Capabilities, worker?: Worker); - - /** - * Initializes the LMLayer worker with the keyboard/platform's capabilities, - * as well as a description of the model required. - */ - loadModel(model: string): Promise; - - /** - * Prepares the LMLayer for reinitialization with a different model/capability set. - */ - unloadModel(); - - predict(transform: Transform, context: Context): Promise; - - // TODO: asynchronous close() method. - // Worker code must recognize message and call self.close(). - - private onMessage(event: MessageEvent): void; - - /** - * Given a function, this utility returns the source code within it, as a string. - * This is intended to unwrap the "wrapped" source code created in the LMLayerWorker - * build process. - * - * @param fn The function whose body will be returned. - */ - static unwrap(fn: Function): string; - - /** - * Converts the INSIDE of a function into a blob URI that can - * be passed as a valid URI for a Worker. - * @param fn Function whose body will be referenced by a URI. - * - * This function makes the following possible: - * - * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () { - * postMessage('inside Web Worker') - * function onmessage(event) { - * // handle message inside Web Worker. - * } - * })); - */ - static asBlobURI(fn: Function): string; - - /** - * Clears out any computational resources in use by the LMLayer, including shutting - * down any internal WebWorkers. - */ - public shutdown(): void; - } -} \ No newline at end of file diff --git a/common/predictive-text/index.ts b/common/predictive-text/index.ts deleted file mode 100644 index 887f71c2ce9f60f80c83b0ff11b23cf544896afc..0000000000000000000000000000000000000000 --- a/common/predictive-text/index.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos) - * Copyright (c) 2018 SIL International - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/// -/// -/// - -/** - * Top-level interface to the Language Modelling layer, or "LMLayer" for short. - * - * The Language Modelling layer provides a way for keyboards to offer prediction and - * correction functionalities. The LMLayer proper runs within a Web Worker, however, - * this class is intended to run in the main thread, and automatically spawn a Web - * Worker, capable of offering predictions. - * - * Since the Worker runs in a different thread, the public methods of this class are - * asynchronous. Methods of note include: - * - * - #loadModel() -- loads a specified model file - * - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for - * the input event - * - #unloadModel() -- unloads the LMLayer's currently loaded model, preparing it to - * receive (load) a new model - * - * The top-level LMLayer will automatically starts up its own Web Worker. - */ - -namespace com.keyman.text.prediction { - export class LMLayer { - /** - * The underlying worker instance. By default, this is the LMLayerWorker. - */ - private _worker: Worker; - /** Call this when the LMLayer has sent us the 'ready' message! */ - private _declareLMLayerReady: (conf: Configuration) => void; - private _predictPromises: PromiseStore; - private _wordbreakPromises: PromiseStore; - private _acceptPromises: PromiseStore; - private _revertPromises: PromiseStore; - private _nextToken: number; - private capabilities: Capabilities; - - /** - * Construct the top-level LMLayer interface. This also starts the underlying Worker. - * - * @param uri URI of the underlying LMLayer worker code. This will usually be a blob: - * or file: URI. If uri is not provided, this will start the default Worker. - */ - constructor(capabilities: Capabilities, worker?: Worker, testMode?: boolean) { - // Either use the given worker, or instantiate the default worker. - this._worker = worker || DefaultWorker.constructInstance(); - this._worker.onmessage = this.onMessage.bind(this) - this._declareLMLayerReady = null; - this._predictPromises = new PromiseStore(); - this._wordbreakPromises = new PromiseStore(); - this._acceptPromises = new PromiseStore(); - this._revertPromises = new PromiseStore(); - this._nextToken = Number.MIN_SAFE_INTEGER; - - this.sendConfig(capabilities, !!testMode); - } - - /** - * Initializes the LMLayer worker with the host platform's capability set. - * - * @param capabilities The host platform's capability spec - a model cannot assume access to more context - * than specified by this parameter. - */ - private sendConfig(capabilities: Capabilities, testMode: boolean) { - this._worker.postMessage({ - message: 'config', - capabilities: capabilities, - testMode: testMode - }); - } - - /** - * Initializes the LMLayer worker with a path to the desired model file. - */ - loadModel(modelSource: string, loadType: 'file' | 'raw' = 'file'): Promise { - return new Promise((resolve, _reject) => { - // Sets up so the promise is resolved in the onMessage() callback, when it receives - // the 'ready' message. - this._declareLMLayerReady = resolve; - - let modelSourceSpec: any = { - type: loadType - }; - - if(loadType == 'file') { - modelSourceSpec.file = modelSource; - } else { - modelSourceSpec.code = modelSource; - } - - this._worker.postMessage({ - message: 'load', - source: modelSourceSpec - }); - }); - } - - /** - * Unloads the previously-active model from memory, resetting the LMLayer to prep - * for transition to use of a new model. - */ - public unloadModel() { - this._worker.postMessage({ - message: 'unload' - }); - } - - predict(transform: Transform | Distribution, context: Context): Promise { - let token = this._nextToken++; - return new Promise((resolve, reject) => { - this._predictPromises.make(token, resolve, reject); - this._worker.postMessage({ - message: 'predict', - token: token, - transform: transform, - context: context, - }); - }); - } - - wordbreak(context: Context): Promise { - let token = this._nextToken++; - return new Promise((resolve, reject) => { - this._wordbreakPromises.make(token, resolve, reject); - this._worker.postMessage({ - message: 'wordbreak', - token: token, - context: context - }) - }); - } - - acceptSuggestion(suggestion: Suggestion, context: Context, postTransform: Transform): Promise { - let token = this._nextToken++; - return new Promise((resolve, reject) => { - this._acceptPromises.make(token, resolve, reject); - this._worker.postMessage({ - message: 'accept', - token: token, - suggestion: suggestion, - context: context, - postTransform: postTransform - }); - }); - } - - revertSuggestion(reversion: Reversion, context: Context): Promise { - let token = this._nextToken++; - return new Promise((resolve, reject) => { - this._revertPromises.make(token, resolve, reject); - this._worker.postMessage({ - message: 'revert', - token: token, - reversion: reversion, - context: context - }) - }); - } - - resetContext(context: Context) { - this._worker.postMessage({ - message: 'reset-context', - context: context - }); - } - - // TODO: asynchronous close() method. - // Worker code must recognize message and call self.close(). - - private onMessage(event: MessageEvent): void { - let payload: OutgoingMessage = event.data; - if (payload.message === 'error') { - console.error(payload.log); - if(payload.error) { - console.error(payload.error); - } - } - else if (payload.message === 'ready') { - this._declareLMLayerReady(event.data.configuration); - } else if (payload.message === 'suggestions') { - this._predictPromises.keep(payload.token, payload.suggestions); - } else if (payload.message === 'currentword') { - this._wordbreakPromises.keep(payload.token, payload.word); - } else if (payload.message === 'postaccept') { - this._acceptPromises.keep(payload.token, payload.reversion); - } else if (payload.message === 'postrevert') { - this._revertPromises.keep(payload.token, payload.suggestions); - } else { - // This branch should never execute, but just in case... - //@ts-ignore - throw new Error(`Message not implemented: ${payload.message}`); - } - } - - /** - * Clears out any computational resources in use by the LMLayer, including shutting - * down any internal WebWorkers. - */ - public shutdown() { - this._worker.terminate(); - } - - /** - * Given a function, this utility returns the source code within it, as a string. - * This is intended to unwrap the "wrapped" source code created in the LMLayerWorker - * build process. - * - * @param fn The function whose body will be returned. - */ - static unwrap(fn: Function): string { - let wrapper = fn.toString(); - let match = wrapper.match(/function[^{]+{((?:.|\r|\n)+)}[^}]*$/); - return match[1]; - } - } -} - -(function () { - let ns = com.keyman.text.prediction; - - // Let LMLayer be available both in the browser and in Node. - if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = ns.LMLayer; - //@ts-ignore - ns.LMLayer.PromiseStore = ns.PromiseStore; - } else { - //@ts-ignore - window.LMLayer = ns.LMLayer; - } -}()); \ No newline at end of file diff --git a/common/predictive-text/node-defaultWorker.ts b/common/predictive-text/node-defaultWorker.ts deleted file mode 100644 index 3cf24dcf53714808e99f097176cafe201386dee9..0000000000000000000000000000000000000000 --- a/common/predictive-text/node-defaultWorker.ts +++ /dev/null @@ -1,12 +0,0 @@ -/// - -namespace com.keyman.text.prediction { - export class DefaultWorker { - static constructInstance(): Worker { - let scriptStr = LMLayer.unwrap(LMLayerWorkerCode); - let worker = new VirtualizedWorker(scriptStr); - - return worker; - } - } -} \ No newline at end of file diff --git a/common/predictive-text/package.json b/common/predictive-text/package.json index beb7c46615f64318676c8a980549ce0124a9c122..bd37e490c44c8f999363961581a448c040cd5a3c 100644 --- a/common/predictive-text/package.json +++ b/common/predictive-text/package.json @@ -1,12 +1,31 @@ { "name": "@keymanapp/lexical-model-layer", "description": "Keyman/Predictive Text integration layer", - "main": "build/index.js", + "main": "build/obj/node/index.js", + "exports": { + ".": { + "node": { + "types": "./build/obj/node/index.d.ts", + "import": "./build/obj/index.js" + }, + "default": { + "types": "./build/obj/web/index.d.ts", + "import": "./build/obj/web/index.js" + } + }, + "./node": "./build/obj/node/index.js", + "./web": "./build/obj/web/index.js", + "./build/obj/node/index.js": "./build/obj/node/index.js", + "./build/obj/web/index.js": "./build/obj/web/index.js" + }, + "imports": { + "#./*.js": "./build/obj/*.js", + "#./node/*.js": "./build/obj/node/*.js", + "#./web/*.js": "./build/obj/web/*.js" + }, + "type": "module", "scripts": { - "karma": "karma", - "mocha": "mocha", - "test": "gosh ./unit_tests/test.sh", - "tsc": "tsc" + "test": "gosh ./unit_tests/test.sh" }, "repository": { "type": "git", diff --git a/common/predictive-text/promise-store.ts b/common/predictive-text/promise-store.ts deleted file mode 100644 index 4ef29ad7c013a827be3f9c56a23a88e6455a26b9..0000000000000000000000000000000000000000 --- a/common/predictive-text/promise-store.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos) - * Copyright (c) 2018 SIL International - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - - -type Resolve = (value?: T | PromiseLike) => void; -type Reject = (reason?: any) => void; -interface PromiseCallbacks { - resolve: Resolve; - reject: Reject; -} - -namespace com.keyman.text.prediction { - /** - * Associate tokens with promises. - * - * First, .make() a promise -- associate a token with resolve/reject callbacks. - * - * You can either .keep() a promise -- resolve() and forget it; - * Or you may also .break() a promise -- reject() and forget it. - * - * is the type of resolved value (value yielded successfully by promise). - */ - export class PromiseStore { - // IE11 offers partial support for new Map(). - // Assume only .get(), .set(), .has(), .delete(), and .size work. - // See: http://kangax.github.io/compat-table/es6/#test-Map - private _promises: Map>; - constructor() { - this._promises = new Map(); - } - /** - * How many promises are currently being tracked? - */ - get length(): number { - return this._promises.size; - } - /** - * Associate a token with its respective resolve and reject callbacks. - */ - make(token: Token, resolve: Resolve, reject: Reject): void { - if (this._promises.has(token)) { - return reject(`Existing request with token ${token}`); - } - this._promises.set(token, { reject, resolve }); - } - /** - * Resolve the promise associated with a token (with a value!). - * Once the promise is resolved, the token is removed.. - */ - keep(token: Token, value: T) { - let callbacks = this._promises.get(token); - if (!callbacks) { - throw new Error(`No promise associated with token: ${token}`); - } - let accept = callbacks.resolve; - this._promises.delete(token); - return accept(value); - } - /** - * Instantly reject and forget a promise associated with the token. - */ - break(token: Token, reason?: any): void { - let callbacks = this._promises.get(token); - if (!callbacks) { - throw new Error(`No promise associated with token: ${token}`); - } - this._promises.delete(token); - callbacks.reject(reason); - } - } -} \ No newline at end of file diff --git a/common/predictive-text/src/index.ts b/common/predictive-text/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..e087e6957dc1e9cb0b35bfe66d1cf9ff836658f8 --- /dev/null +++ b/common/predictive-text/src/index.ts @@ -0,0 +1,3 @@ +export { default as LMLayer } from './lmlayer.js'; +export { default as NodeWorker } from './node/worker.js'; +export { default as WebWorker } from './web/worker.js'; \ No newline at end of file diff --git a/common/predictive-text/src/lmlayer.ts b/common/predictive-text/src/lmlayer.ts new file mode 100644 index 0000000000000000000000000000000000000000..f331273e657202f768a9c831bc5c6389c96fa702 --- /dev/null +++ b/common/predictive-text/src/lmlayer.ts @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos) + * Copyright (c) 2018 SIL International + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import PromiseStore from "./promise-store.js"; + +/// +/// + +/** + * Top-level interface to the Language Modelling layer, or "LMLayer" for short. + * + * The Language Modelling layer provides a way for keyboards to offer prediction and + * correction functionalities. The LMLayer proper runs within a Web Worker, however, + * this class is intended to run in the main thread, and automatically spawn a Web + * Worker, capable of offering predictions. + * + * Since the Worker runs in a different thread, the public methods of this class are + * asynchronous. Methods of note include: + * + * - #loadModel() -- loads a specified model file + * - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for + * the input event + * - #unloadModel() -- unloads the LMLayer's currently loaded model, preparing it to + * receive (load) a new model + * + * The top-level LMLayer will automatically starts up its own Web Worker. + */ + +export default class LMLayer { + /** + * The underlying worker instance. By default, this is the LMLayerWorker. + */ + private _worker: Worker; + /** Call this when the LMLayer has sent us the 'ready' message! */ + private _declareLMLayerReady: (conf: Configuration) => void; + private _predictPromises: PromiseStore; + private _wordbreakPromises: PromiseStore; + private _acceptPromises: PromiseStore; + private _revertPromises: PromiseStore; + private _nextToken: number; + private capabilities: Capabilities; + + /** + * Construct the top-level LMLayer interface. This also starts the underlying Worker. + * + * @param uri URI of the underlying LMLayer worker code. This will usually be a blob: + * or file: URI. If uri is not provided, this will start the default Worker. + */ + constructor(capabilities: Capabilities, worker: Worker, testMode?: boolean) { + // Either use the given worker, or instantiate the default worker. + this._worker = worker; + this._worker.onmessage = this.onMessage.bind(this) + this._declareLMLayerReady = null; + this._predictPromises = new PromiseStore(); + this._wordbreakPromises = new PromiseStore(); + this._acceptPromises = new PromiseStore(); + this._revertPromises = new PromiseStore(); + this._nextToken = Number.MIN_SAFE_INTEGER; + + this.sendConfig(capabilities, !!testMode); + } + + /** + * Initializes the LMLayer worker with the host platform's capability set. + * + * @param capabilities The host platform's capability spec - a model cannot assume access to more context + * than specified by this parameter. + */ + private sendConfig(capabilities: Capabilities, testMode: boolean) { + this._worker.postMessage({ + message: 'config', + capabilities: capabilities, + testMode: testMode + }); + } + + /** + * Initializes the LMLayer worker with a path to the desired model file. + */ + loadModel(modelSource: string, loadType: 'file' | 'raw' = 'file'): Promise { + return new Promise((resolve, _reject) => { + // Sets up so the promise is resolved in the onMessage() callback, when it receives + // the 'ready' message. + this._declareLMLayerReady = resolve; + + let modelSourceSpec: any = { + type: loadType + }; + + if(loadType == 'file') { + modelSourceSpec.file = modelSource; + } else { + modelSourceSpec.code = modelSource; + } + + this._worker.postMessage({ + message: 'load', + source: modelSourceSpec + }); + }); + } + + /** + * Unloads the previously-active model from memory, resetting the LMLayer to prep + * for transition to use of a new model. + */ + public unloadModel() { + this._worker.postMessage({ + message: 'unload' + }); + } + + predict(transform: Transform | Distribution, context: Context): Promise { + let token = this._nextToken++; + return new Promise((resolve, reject) => { + this._predictPromises.make(token, resolve, reject); + this._worker.postMessage({ + message: 'predict', + token: token, + transform: transform, + context: context, + }); + }); + } + + wordbreak(context: Context): Promise { + let token = this._nextToken++; + return new Promise((resolve, reject) => { + this._wordbreakPromises.make(token, resolve, reject); + this._worker.postMessage({ + message: 'wordbreak', + token: token, + context: context + }) + }); + } + + acceptSuggestion(suggestion: Suggestion, context: Context, postTransform: Transform): Promise { + let token = this._nextToken++; + return new Promise((resolve, reject) => { + this._acceptPromises.make(token, resolve, reject); + this._worker.postMessage({ + message: 'accept', + token: token, + suggestion: suggestion, + context: context, + postTransform: postTransform + }); + }); + } + + revertSuggestion(reversion: Reversion, context: Context): Promise { + let token = this._nextToken++; + return new Promise((resolve, reject) => { + this._revertPromises.make(token, resolve, reject); + this._worker.postMessage({ + message: 'revert', + token: token, + reversion: reversion, + context: context + }) + }); + } + + resetContext(context: Context) { + this._worker.postMessage({ + message: 'reset-context', + context: context + }); + } + + // TODO: asynchronous close() method. + // Worker code must recognize message and call self.close(). + + private onMessage(event: MessageEvent): void { + let payload: OutgoingMessage = event.data; + if (payload.message === 'error') { + console.error(payload.log); + if(payload.error) { + console.error(payload.error); + } + } + else if (payload.message === 'ready') { + this._declareLMLayerReady(event.data.configuration); + } else if (payload.message === 'suggestions') { + this._predictPromises.keep(payload.token, payload.suggestions); + } else if (payload.message === 'currentword') { + this._wordbreakPromises.keep(payload.token, payload.word); + } else if (payload.message === 'postaccept') { + this._acceptPromises.keep(payload.token, payload.reversion); + } else if (payload.message === 'postrevert') { + this._revertPromises.keep(payload.token, payload.suggestions); + } else { + // This branch should never execute, but just in case... + //@ts-ignore + throw new Error(`Message not implemented: ${payload.message}`); + } + } + + /** + * Clears out any computational resources in use by the LMLayer, including shutting + * down any internal WebWorkers. + */ + public shutdown() { + this._worker.terminate(); + } +} diff --git a/common/predictive-text/src/node/index.ts b/common/predictive-text/src/node/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a70a48bf30393310e4a59815ced628fd69baa36 --- /dev/null +++ b/common/predictive-text/src/node/index.ts @@ -0,0 +1,3 @@ +export { default as LMLayer } from '../lmlayer.js'; +export { default as Worker } from './worker.js'; +export { default as SourcemappedWorker } from './sourcemappedWorker.js'; \ No newline at end of file diff --git a/common/predictive-text/src/node/mappedWorker.ts b/common/predictive-text/src/node/mappedWorker.ts new file mode 100644 index 0000000000000000000000000000000000000000..aafbba52323d6f516f40d867b449cd5e07377aac --- /dev/null +++ b/common/predictive-text/src/node/mappedWorker.ts @@ -0,0 +1,88 @@ +// We use a subset of the Worker interface here; compiling directly against the true +// WebWorker type definitions would require us to implement more methods than we do. +/// + +// Defines types related to Node workers. +import * as worker from 'worker_threads'; +import { Buffer } from 'buffer'; +import { URL } from 'url'; + +/** + * Defines mappings from Node Worker signatures to WebWorker signatures + */ +const nodeWorkerToWebWorkerMappingSource = ` +import { parentPort } from 'worker_threads'; +import fs from 'fs'; +import vm from 'vm'; + +function postMessage(...args) { + parentPort.postMessage.call(parentPort, args); +} + +parentPort.on('message', (ev) => { + onmessage({data: ev}); +}); + +function importScripts(...args) { + function loadScriptInContext(scriptPath) { + let scriptStr = fs.readFileSync(scriptPath); + var script = new vm.Script(scriptStr, { filename: scriptPath }); + script.runInThisContext(); + } + + for(let arg of args) { + loadScriptInContext(arg); + } +} + +/* + * You'd think the method signature mapping would be implied from the first line, + * but all three lines must be explicitly specified or the emulation will fail. + */ +const self = globalThis; +self.postMessage = postMessage; +self.importScripts = importScripts; +`; + +/** + * Uses the Node version of Workers to provide proper, authentic separate-thread + * 'sandboxing'. Also intercepts and interprets certain WebWorker method signatures + * necessary to run the WebWorker-oriented worker code. + * + * Alternatively, only after writing this did I discover this package: + * https://github.com/developit/web-worker. They also ran one notable issue I did: + * Node 18.x, at least, does not support use of Node Blobs for construction of a + * Worker: https://github.com/developit/web-worker/pull/32... unlike Web Workers. + * + * So... Base64-encoded Data URLs it is. + * + * What we have here is perfectly fine for now, but if we need more complicated + * cross-platform Worker support in the future, it may be wise to swap to use of + * that package. + */ +export default class MappedWorker extends worker.Worker implements Worker { + constructor(scriptStr: string) { + const concatenatedScript = ` + ${nodeWorkerToWebWorkerMappingSource} + + ${scriptStr} + `; + const buffer = Buffer.from(concatenatedScript); + const dataSrc = "data:text/javascript;base64," + buffer.toString('base64'); + //@ts-ignore + super(new URL(dataSrc)); + + // WebWorkers have a defined `onmessage` function, rather than this.on('message', ...) + this.on('message', (ev) => { + if(this.onmessage) { + this.onmessage({data: ev[0]}); + } + }); + } + + /** + * Accepts a callback function that will receive messages sent from the `VirtualizedWorker`'s `postMessage` function, + * much like the standard `Worker.onmessage`. + */ + onmessage: (this: Worker, ev: MessageEvent) => any; +} \ No newline at end of file diff --git a/common/predictive-text/src/node/sourcemappedWorker.ts b/common/predictive-text/src/node/sourcemappedWorker.ts new file mode 100644 index 0000000000000000000000000000000000000000..aaaf79e0f6fcc1f1d42207dd767b4663f4f09e95 --- /dev/null +++ b/common/predictive-text/src/node/sourcemappedWorker.ts @@ -0,0 +1,19 @@ +import MappedWorker from "./mappedWorker.js"; +import unwrap from '../unwrap.js'; + +import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.js"; + +export default class SourcemappedWorker { + static constructInstance(): Worker { + let scriptStr = unwrap(LMLayerWorkerCode); + + // If this is definitively set to either true or false, tree-shaking can take effect. + // An imported const variable doesn't seem to do it, though. + // if(false) { + scriptStr += '\n' + LMLayerWorkerSourcemapComment; + // } + let worker = new MappedWorker(scriptStr); + + return worker as any as Worker; + } +} \ No newline at end of file diff --git a/common/predictive-text/src/node/tsconfig.json b/common/predictive-text/src/node/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..3f847a1c30db002817573893e6a325ec0e8e46c2 --- /dev/null +++ b/common/predictive-text/src/node/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../web/tsconfig.kmw-main-base.json", + "compilerOptions": { + // The Worker class needs access to the `new` keyword, else Node will throw an + // error when constructing the Worker for unit tests. ES6 is sufficient. + "target": "ES6", + "types": ["node"], + "baseUrl": "../", + "outDir": "../../build/obj", + "tsBuildInfoFile": "../../build/obj/node/tsconfig.tsbuildinfo", + "rootDir": "../" + }, + "include": [ + "*.ts" + ], + "references": [ + { "path": "../tsconfig.json" }, + { "path": "../../../web/lm-worker" } + ] +} \ No newline at end of file diff --git a/common/predictive-text/src/node/worker.ts b/common/predictive-text/src/node/worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ad04e55d0be08b11b34073ef60d72b1e25981b7 --- /dev/null +++ b/common/predictive-text/src/node/worker.ts @@ -0,0 +1,15 @@ +import MappedWorker from "./mappedWorker.js"; +import unwrap from '../unwrap.js'; + +import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.min.js"; + +export default class Worker { + static constructInstance(): Worker { + let scriptStr = unwrap(LMLayerWorkerCode); + + scriptStr += '\n' + LMLayerWorkerSourcemapComment; + let worker = new MappedWorker(scriptStr); + + return worker as any as Worker; + } +} \ No newline at end of file diff --git a/common/predictive-text/src/promise-store.ts b/common/predictive-text/src/promise-store.ts new file mode 100644 index 0000000000000000000000000000000000000000..890ba5923929a57ffa6fdc0ffe223527b0f590e2 --- /dev/null +++ b/common/predictive-text/src/promise-store.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos) + * Copyright (c) 2018 SIL International + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/// + +type Resolve = (value?: T | PromiseLike) => void; +type Reject = (reason?: any) => void; +interface PromiseCallbacks { + resolve: Resolve; + reject: Reject; +} + + +/** + * Associate tokens with promises. + * + * First, .make() a promise -- associate a token with resolve/reject callbacks. + * + * You can either .keep() a promise -- resolve() and forget it; + * Or you may also .break() a promise -- reject() and forget it. + * + * is the type of resolved value (value yielded successfully by promise). + */ +export default class PromiseStore { + // IE11 offers partial support for new Map(). + // Assume only .get(), .set(), .has(), .delete(), and .size work. + // See: http://kangax.github.io/compat-table/es6/#test-Map + private _promises: Map>; + constructor() { + this._promises = new Map(); + } + /** + * How many promises are currently being tracked? + */ + get length(): number { + return this._promises.size; + } + /** + * Associate a token with its respective resolve and reject callbacks. + */ + make(token: Token, resolve: Resolve, reject: Reject): void { + if (this._promises.has(token)) { + return reject(`Existing request with token ${token}`); + } + this._promises.set(token, { reject, resolve }); + } + /** + * Resolve the promise associated with a token (with a value!). + * Once the promise is resolved, the token is removed.. + */ + keep(token: Token, value: T) { + let callbacks = this._promises.get(token); + if (!callbacks) { + throw new Error(`No promise associated with token: ${token}`); + } + let accept = callbacks.resolve; + this._promises.delete(token); + return accept(value); + } + /** + * Instantly reject and forget a promise associated with the token. + */ + break(token: Token, reason?: any): void { + let callbacks = this._promises.get(token); + if (!callbacks) { + throw new Error(`No promise associated with token: ${token}`); + } + this._promises.delete(token); + callbacks.reject(reason); + } +} \ No newline at end of file diff --git a/common/predictive-text/src/tsconfig.json b/common/predictive-text/src/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..675bf7cd537b1a154c32c6ae847d57917179ed6a --- /dev/null +++ b/common/predictive-text/src/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../web/tsconfig.kmw-main-base.json", + + "compilerOptions": { + "baseUrl": "./", + "outDir": "../build/obj", + "tsBuildInfoFile": "../build/obj/tsconfig.tsbuildinfo", + "rootDir": "./" + }, + "references": [ + { "path": "../../web/utils" }, + { "path": "../../models/types"}, + { "path": "../../models/wordbreakers"}, + { "path": "../../web/lm-message-types"}, + { "path": "../../models/templates"} + ], + "include" : [ "./*.ts" ], + "exclude" : [ + "node", + "web", + "index.ts" // It's handled by the final layer. + ] +} diff --git a/common/predictive-text/src/unwrap.ts b/common/predictive-text/src/unwrap.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc0769e5bd112e3738169f0cc9de63bcf8b920af --- /dev/null +++ b/common/predictive-text/src/unwrap.ts @@ -0,0 +1,11 @@ +/** + * Given a function, this utility returns the source code within it, as a string. + * This is intended to unwrap the "wrapped" source code created in the LMLayerWorker + * build process. + * + * @param fn The function whose body will be returned. + */ +export default function unwrap(encodedSrc: string): string { + // There used to be more to this, but now it's a pretty simple passthrough! + return encodedSrc; +} \ No newline at end of file diff --git a/common/predictive-text/src/web/index.ts b/common/predictive-text/src/web/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a70a48bf30393310e4a59815ced628fd69baa36 --- /dev/null +++ b/common/predictive-text/src/web/index.ts @@ -0,0 +1,3 @@ +export { default as LMLayer } from '../lmlayer.js'; +export { default as Worker } from './worker.js'; +export { default as SourcemappedWorker } from './sourcemappedWorker.js'; \ No newline at end of file diff --git a/common/predictive-text/src/web/sourcemappedWorker.ts b/common/predictive-text/src/web/sourcemappedWorker.ts new file mode 100644 index 0000000000000000000000000000000000000000..da7ed77eb96393f01e5737404a6a26c8256e98e5 --- /dev/null +++ b/common/predictive-text/src/web/sourcemappedWorker.ts @@ -0,0 +1,34 @@ +import unwrap from '../unwrap.js'; +import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.js"; + +export default class SourcemappedWorker { + static constructInstance(): Worker { + return new Worker(this.asBlobURI(LMLayerWorkerCode)); + } + + /** + * Converts the INSIDE of a function into a blob URI that can + * be passed as a valid URI for a Worker. + * @param fn Function whose body will be referenced by a URI. + * + * This function makes the following possible: + * + * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () { + * postMessage('inside Web Worker') + * function onmessage(event) { + * // handle message inside Web Worker. + * } + * })); + */ + static asBlobURI(encodedSrc: string): string { + let code = unwrap(encodedSrc); + + // If this is definitively set to either true or false, tree-shaking can take effect. + // An imported const variable doesn't seem to do it, though. + // if(false) { + code += '\n' + LMLayerWorkerSourcemapComment; + // } + let blob = new Blob([code], { type: 'text/javascript' }); + return URL.createObjectURL(blob); + } +} \ No newline at end of file diff --git a/common/predictive-text/src/web/tsconfig.json b/common/predictive-text/src/web/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..c16936db12da3b1aba586b7c2c041c0609b2dab0 --- /dev/null +++ b/common/predictive-text/src/web/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../web/tsconfig.kmw-main-base.json", + "compilerOptions": { + "lib": ["es6", "DOM"], + "baseUrl": "../", + "outDir": "../../build/obj", + "tsBuildInfoFile": "../../build/obj/web/tsconfig.tsbuildinfo", + "rootDir": "../" + }, + "include": [ + "*.ts" + ], + "references": [ + { "path": "../tsconfig.json" } + ] +} \ No newline at end of file diff --git a/common/predictive-text/src/web/worker.ts b/common/predictive-text/src/web/worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..372887fcddf22c499ef7bc54b356acbd668a5c87 --- /dev/null +++ b/common/predictive-text/src/web/worker.ts @@ -0,0 +1,34 @@ +import unwrap from '../unwrap.js'; +import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.min.js"; + +export default class DefaultWorker { + static constructInstance(): Worker { + return new Worker(this.asBlobURI(LMLayerWorkerCode)); + } + + /** + * Converts the INSIDE of a function into a blob URI that can + * be passed as a valid URI for a Worker. + * @param fn Function whose body will be referenced by a URI. + * + * This function makes the following possible: + * + * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () { + * postMessage('inside Web Worker') + * function onmessage(event) { + * // handle message inside Web Worker. + * } + * })); + */ + static asBlobURI(encodedSrc: string): string { + let code = unwrap(encodedSrc); + + // If this is definitively set to either true or false, tree-shaking can take effect. + // An imported const variable doesn't seem to do it, though. + // if(false) { + code += '\n' + LMLayerWorkerSourcemapComment; + // } + let blob = new Blob([code], { type: 'text/javascript' }); + return URL.createObjectURL(blob); + } +} \ No newline at end of file diff --git a/common/predictive-text/worker-interface.d.ts b/common/predictive-text/src/worker-interface.d.ts similarity index 100% rename from common/predictive-text/worker-interface.d.ts rename to common/predictive-text/src/worker-interface.d.ts diff --git a/common/predictive-text/tsconfig.all.json b/common/predictive-text/tsconfig.all.json new file mode 100644 index 0000000000000000000000000000000000000000..f47b208edafe3255baeae8e47d53d22a8caaf03a --- /dev/null +++ b/common/predictive-text/tsconfig.all.json @@ -0,0 +1,24 @@ +{ + // Due to the heterogenous target mix supported by the LMLayer, the two different "worker" types + // require separate sub-tsconfigs, both of which require common references. The multi-config + // setup in this project and its subfolders ensure all modules can compile cleanly and in the + // necessary compilation order. + "extends": "../web/tsconfig.kmw-main-base.json", + + "compilerOptions": { + "baseUrl": "./", + "outDir": "build/obj", + "tsBuildInfoFile": "build/obj/tsconfig.all.tsbuildinfo", + "rootDir": "./src" + }, + "files": [], + "references": [ + { "path": "../web/utils" }, + { "path": "../models/types"}, + { "path": "../models/wordbreakers"}, + { "path": "../web/lm-message-types"}, + { "path": "../models/templates"}, + { "path": "src/node" }, + { "path": "src/web" } + ], +} diff --git a/common/predictive-text/tsconfig.json b/common/predictive-text/tsconfig.json deleted file mode 100644 index c364f699a12ef53fcf06a33ed92b6ca28554a255..0000000000000000000000000000000000000000 --- a/common/predictive-text/tsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "extends": "../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": true, - "outFile": "build/headless.js", - "module": "none", - "inlineSources": true, - "sourceMap": true, - "target": "es5", - "lib": ["es6"] - }, - "references": [ - { "path": "../web/utils" }, - { "path": "../models/types"}, - { "path": "../models/wordbreakers"}, - { "path": "../web/lm-message-types"}, - { "path": "../models/templates"}, - ], - "include" : [ "./*.ts" ], - "exclude" : [ - "web-defaultWorker.ts", - "node_modules", - "testing" - ], - "files": [ - // By renaming the output JS as TS, and including the @ts-nocheck directive, we can - // bypass wrapped-worker typing issues while still bundling the file as part of the - // tsc build! - "../web/lm-worker/build/index.wrapped-for-bundle.ts" - ] -} diff --git a/common/predictive-text/unit_tests/headless/promise-store.js b/common/predictive-text/unit_tests/headless/promise-store.js index f14b9c8555626377f7aa4eb57fdb7bb96d59f256..0809b215ce71386e0b8002d5d5a1212570afc529 100644 --- a/common/predictive-text/unit_tests/headless/promise-store.js +++ b/common/predictive-text/unit_tests/headless/promise-store.js @@ -1,7 +1,8 @@ -var assert = require('chai').assert; -var sinon = require('sinon'); +import { assert } from 'chai'; +import sinon from 'sinon'; -let PromiseStore = require('../../build/headless').PromiseStore; +import PromiseStore from '#./promise-store.js'; +import { randomToken } from '@keymanapp/common-test-resources/model-helpers.mjs'; describe('PromiseStore', function () { describe('.make()', function () { diff --git a/common/predictive-text/unit_tests/headless/top-level-lmlayer.js b/common/predictive-text/unit_tests/headless/top-level-lmlayer.js index f4b891c8e088049888b113b57a690fc6dc0009ba..78b071e3c427ddaa07bdfeb1559ddd790d363c01 100644 --- a/common/predictive-text/unit_tests/headless/top-level-lmlayer.js +++ b/common/predictive-text/unit_tests/headless/top-level-lmlayer.js @@ -1,11 +1,29 @@ -var assert = require('chai').assert; -var sinon = require('sinon'); +import { assert } from 'chai'; +import sinon from 'sinon'; -let LMLayer = require('../../build/headless'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { LMLayer } from '#./node/index.js'; +import { capabilities } from '@keymanapp/common-test-resources/model-helpers.mjs'; // Test the top-level LMLayer interface. // Note: these tests can only be run after BOTH stages of compilation are completed. describe('LMLayer', function() { + /** + * Returns an object implementing *enough* of the Worker + * interface to fool the LMLayer into thinking it's + * communicating with a bona fide Web Worker. + * + * @returns {Worker} an object with sinon.fake() instances. + */ + function createFakeWorker(postMessage) { + return { + postMessage: postMessage ? sinon.fake(postMessage) : sinon.fake(), + onmessage: null + }; + } + describe('[[constructor]]', function () { it('should accept a Worker to instantiate', function () { new LMLayer(capabilities(), createFakeWorker()); @@ -29,7 +47,7 @@ describe('LMLayer', function() { let fakeWorker = createFakeWorker(); let lmLayer = new LMLayer(capabilities(), fakeWorker); - lmLayer.loadModel("./unit_tests/in_browser/resources/models/simple-dummy.js"); + lmLayer.loadModel(require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js")); assert.isFunction(fakeWorker.onmessage, 'LMLayer failed to set a callback!'); }); @@ -37,7 +55,7 @@ describe('LMLayer', function() { it('should send the `load` message to the LMLayer', async function () { let fakeWorker = createFakeWorker(fakePostMessage); let lmLayer = new LMLayer(capabilities(), fakeWorker); - let configuration = await lmLayer.loadModel("./unit_tests/in_browser/resources/models/simple-dummy.js"); + let configuration = await lmLayer.loadModel(require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js")); assert.propertyVal(fakeWorker.postMessage, 'callCount', 2); // In the "Worker", assert the message looks right and @@ -95,34 +113,6 @@ describe('LMLayer', function() { }) }); - // Since the Blob API is limited to browsers, look for those - // tests for .asBlobURI() in the in_browser tests. - describe('.unwrap', function () { - it('should return the inner code of a function', function () { - // Create a multi-line function body we can match in a RegExp. - let text = LMLayer.unwrap(function hello() { - var hello; - var world; - }); - // Unwrap should give us back ONLY the body. Whitespace isn't really important. - assert.match(text, /^\s*var\s+hello;\s*var\s+world;\s*$/); - }); - }); - - /** - * Returns an object implementing *enough* of the Worker - * interface to fool the LMLayer into thinking it's - * communicating with a bona fide Web Worker. - * - * @returns {Worker} an object with sinon.fake() instances. - */ - function createFakeWorker(postMessage) { - return { - postMessage: postMessage ? sinon.fake(postMessage) : sinon.fake(), - onmessage: null - }; - } - /** * Call a function in the future, i.e., later in the event loop. * The call does NOT block the current execution. diff --git a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js index f1d8c375d40e3dc07bcd80f8335c808c34ae8ea0..d546edb70403a4e4e6e1a11f126f3de3d41a2197 100644 --- a/common/predictive-text/unit_tests/headless/worker-dummy-integration.js +++ b/common/predictive-text/unit_tests/headless/worker-dummy-integration.js @@ -1,9 +1,11 @@ -var assert = require('chai').assert; -var fs = require('fs'); -let LMLayer = require('../../build/headless'); +import { assert } from 'chai'; +import fs from 'fs'; -// Load the LMLayerWorkerCode function into this context -require('node:vm').runInThisContext(fs.readFileSync(__dirname + '/../../../web/lm-worker/build/index.js', 'utf-8')); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { LMLayer, SourcemappedWorker as Worker } from '#./node/index.js'; +import { capabilities, iGotDistractedByHazel } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* * Shows off the LMLayer API, using the full prediction interface. @@ -14,10 +16,23 @@ require('node:vm').runInThisContext(fs.readFileSync(__dirname + '/../../../web/l * of suggestions when loaded and return them sequentially. */ describe('LMLayer using dummy model', function () { + let lmLayer; + let worker; + + beforeEach(function() { + worker = Worker.constructInstance(); + lmLayer = new LMLayer(capabilities(), worker); + }); + + afterEach(function () { + // As we're using Node worker threads here, failure to terminate them will cause the + // headless test run to hang after completion. + lmLayer.shutdown(); + worker.terminate(); // should be covered by the former, but just in case... for CI stability. + }); + describe('Prediction', function () { it('will predict future suggestions (loaded from file)', function () { - var lmLayer = new LMLayer(capabilities()); - var stripIDs = function(suggestions) { suggestions.forEach(function(suggestion) { delete suggestion.id; @@ -29,7 +44,7 @@ describe('LMLayer using dummy model', function () { // Not done yet, as this test case is a slightly-edited copy of the in-browser version. return lmLayer.loadModel( // We're running headlessly, so the path can be relative to the npm root directory. - "unit_tests/in_browser/resources/models/simple-dummy.js" + require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js") ).then(function (actualConfiguration) { return Promise.resolve(); }).then(function () { @@ -49,14 +64,11 @@ describe('LMLayer using dummy model', function () { }).then(function (suggestions) { stripIDs(suggestions); assert.deepEqual(suggestions, iGotDistractedByHazel()[3]); - lmLayer.shutdown(); return Promise.resolve(); }); }); it('will predict future suggestions (loaded from raw source)', function () { - var lmLayer = new LMLayer(capabilities()); - var stripIDs = function(suggestions) { suggestions.forEach(function(suggestion) { delete suggestion.id; @@ -64,7 +76,7 @@ describe('LMLayer using dummy model', function () { }; // We're running headlessly, so the path can be relative to the npm root directory. - let modelCode = fs.readFileSync("./unit_tests/in_browser/resources/models/simple-dummy.js").toString(); + let modelCode = fs.readFileSync(require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js")).toString(); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax. @@ -90,7 +102,6 @@ describe('LMLayer using dummy model', function () { }).then(function (suggestions) { stripIDs(suggestions); assert.deepEqual(suggestions, iGotDistractedByHazel()[3]); - lmLayer.shutdown(); return Promise.resolve(); }); }); @@ -98,14 +109,12 @@ describe('LMLayer using dummy model', function () { describe('Wordbreaking', function () { it('will perform (default) wordbreaking and return word at caret', function () { - var lmLayer = new LMLayer(capabilities()); - // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax. // Not done yet, as this test case is a slightly-edited copy of the in-browser version. return lmLayer.loadModel( // We're running headlessly, so the path can be relative to the npm root directory. - "unit_tests/in_browser/resources/models/simple-dummy.js" + require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js") ).then(function (actualConfiguration) { return Promise.resolve(); }).then(function () { diff --git a/common/predictive-text/unit_tests/headless/worker-trie-integration.js b/common/predictive-text/unit_tests/headless/worker-trie-integration.js index ca543301a20e9337d8b209fcfe0ceb491c21f727..8cd257878d1491e56937ba5f01bbcb41267343a9 100644 --- a/common/predictive-text/unit_tests/headless/worker-trie-integration.js +++ b/common/predictive-text/unit_tests/headless/worker-trie-integration.js @@ -1,21 +1,39 @@ -var assert = require('chai').assert; -let LMLayer = require('../../build/headless'); +import { assert } from 'chai'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { LMLayer, SourcemappedWorker as Worker } from '#./node/index.js'; +import { capabilities } from '@keymanapp/common-test-resources/model-helpers.mjs'; /* * How to run the worlist */ describe('LMLayer using the trie model', function () { + let lmLayer; + let worker; + + beforeEach(function() { + worker = Worker.constructInstance(); + lmLayer = new LMLayer(capabilities(), worker); + }); + + afterEach(function () { + // As we're using Node worker threads here, failure to terminate them will cause the + // headless test run to hang after completion. + lmLayer.shutdown(); + worker.terminate(); // should be covered by the former, but just in case... for CI stability. + }); + describe('Prediction', function () { var EXPECTED_SUGGESTIONS = 3; it('will predict an empty buffer', function () { - var lmLayer = new LMLayer(capabilities()); - // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax. // Not done yet, as this test case is a slightly-edited copy of the in-browser version. return lmLayer.loadModel( // We're running headlessly, so the path can be relative to the npm root directory. - "unit_tests/in_browser/resources/models/simple-trie.js" + require.resolve("@keymanapp/common-test-resources/models/simple-trie.js") ).then(function (_actualConfiguration) { return Promise.resolve(); }).then(function () { @@ -50,11 +68,9 @@ describe('LMLayer using the trie model', function () { // // https://community.software.sil.org/t/search-term-to-key-in-lexical-model-not-working-both-ways-by-default/3133 it('should use the default searchTermToKey()', function () { - var lmLayer = new LMLayer(capabilities()); - return lmLayer.loadModel( // We're running headlessly, so the path can be relative to the npm root directory. - "unit_tests/in_browser/resources/models/naive-trie.js" + require.resolve("@keymanapp/common-test-resources/models/naive-trie.js") ).then(function (_actualConfiguration) { return Promise.resolve(); }).then(function () { diff --git a/common/predictive-text/unit_tests/helpers.js b/common/predictive-text/unit_tests/helpers.js deleted file mode 100644 index 1ece33428bca1d7372bbc1e8f723d54bec11b7ca..0000000000000000000000000000000000000000 --- a/common/predictive-text/unit_tests/helpers.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * @file helpers - * - * Globally-defined helper functions for use in in Mocha tests. - */ - -var fs = require("fs"); -var vm = require("vm"); - -// Choose the appropriate global object. Either `global` in -// Node, or `window` in browsers. -var _ = global || window; - -/** - * Creates a MessageEvent (for inter-worker communication), with the given data payload. - * - * @param {*} data - */ -_.createMessageEventWithData = function createMessageEventWithData(data) { - return { data }; -} - -/** - * Creates a simple, default capabilities object for standard-case LMLayer init. - */ -_.capabilities = function capabilities() { - return { - maxLeftContextCodePoints: 64 - } -} - -/** - * Mimics a message from the outer LMLayer shell with a simple, default config object. - * Used for Worker tests. - */ -_.configWorker = function configWorker(worker) { - worker.onMessage(createMessageEventWithData({ - message: 'config', - capabilities: _.capabilities() - })); -} - -/** - * A valid model that suggests exactly what you want it to suggest. - * - * @returns {ModelDescription} - */ -_.dummyModel = function dummyModel(futureSuggestions) { - return { - type: 'dummy', - futureSuggestions: futureSuggestions || [] - }; -} -/** - * Capabilities of a keyboard that will ONLY send left-sided capabilities. - * The keyboard does not support deleting to the right. - * - * @returns {Capabilities} - */ -_.defaultCapabilities = function defaultCapabilities() { - return { - maxLeftContextCodeUnits: 64 - }; -} - -/** - * Returns the Context of an empty buffer; no text, at both the start and - * end of the buffer. - * - * @returns {Context} - */ -_.emptyContext = function emptyContext() { - return { - left: '', - startOfBuffer: true, - endOfBuffer: true - }; -} - -/** - * Returns a Transform that, when applied, makes no changes to the buffer. - * - * @returns {Transform} - */ -_.zeroTransform = function zeroTransform() { - return { - insert: '', - deleteLeft: 0, - }; -} - -/** - * Returns a random token. NOT guaranteed to be unique. - * - * @returns {Token} - */ -_.randomToken = function randomToken() { - var range = Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER; - return Math.random() * range + Number.MIN_SAFE_INTEGER; -} - -// Use fixtures used in browser tests IN NODE! -if (typeof require === 'function') { - _.iGotDistractedByHazel = function () { - return jsonFixture('future_suggestions/i_got_distracted_by_hazel'); - } - - /** - * Return a JSON fixture - */ - _.jsonFixture = function (name) { - // Assuming this file structure: - // . - // ├── helpers.js - // └── in_browser - // └── json - // ├── future_suggestions - // │ └── ... - // └── wordlists - // └── ... - return require('./in_browser/json/' + name); - } - - // This worker-global function does not exist by default in Node! - _.importScriptsWith = function(context) { - return function() { // the constructed context's importScripts method. - - /* Use of vm.createContext and script.runInContext allow us to avoid - * polluting the global scope with imports. When we throw away the - * context object, imported scripts will be automatically GC'd. - */ - for(var i=0; i < arguments.length; i++) { - context = vm.createContext(context); - var script = new vm.Script(fs.readFileSync(arguments[i])); - script.runInContext(context); - } - } - } -} diff --git a/common/predictive-text/unit_tests/in_browser/CI.conf.cjs b/common/predictive-text/unit_tests/in_browser/CI.conf.cjs new file mode 100644 index 0000000000000000000000000000000000000000..eaedb4a1ca6d9190c49f0ef4b1347593ec46773d --- /dev/null +++ b/common/predictive-text/unit_tests/in_browser/CI.conf.cjs @@ -0,0 +1,4 @@ +var BASE_CONFIG = require("./base.conf.cjs"); +var ci_config_adapter = require("../../../test/resources/karma-browserstack-config.cjs"); + +module.exports = ci_config_adapter(BASE_CONFIG, "full"); \ No newline at end of file diff --git a/common/predictive-text/unit_tests/in_browser/CI.conf.js b/common/predictive-text/unit_tests/in_browser/CI.conf.js deleted file mode 100644 index d3d2b02e400591819170741716f02af73dce9277..0000000000000000000000000000000000000000 --- a/common/predictive-text/unit_tests/in_browser/CI.conf.js +++ /dev/null @@ -1,4 +0,0 @@ -var BASE_CONFIG = require("./base.conf.js"); -var ci_config_adapter = require("../../../../common/test/resources/karma-browserstack-config.js"); - -module.exports = ci_config_adapter(BASE_CONFIG, "full"); \ No newline at end of file diff --git a/common/predictive-text/unit_tests/in_browser/base.conf.js b/common/predictive-text/unit_tests/in_browser/base.conf.cjs similarity index 55% rename from common/predictive-text/unit_tests/in_browser/base.conf.js rename to common/predictive-text/unit_tests/in_browser/base.conf.cjs index 2c62af18549764d6fb37f87ac5aa838cd1abc103..6fa9de9f22ddf4045dd12506d91b5bac618751ed 100644 --- a/common/predictive-text/unit_tests/in_browser/base.conf.js +++ b/common/predictive-text/unit_tests/in_browser/base.conf.cjs @@ -4,7 +4,7 @@ module.exports = { // base path that will be used to resolve all patterns (eg. files, exclude) - basePath: './', + basePath: '../../../../', // Root directory of the repository(!) client: { /* `client.args` here is passed to the test runner page as `__karma__.config.args`. @@ -31,16 +31,27 @@ module.exports = { // list of files / patterns to load in the browser files: [ - // Include the generated worker code. Make sure it's linked before any of the test cases. - '../../../web/lm-worker/build/index.js', - '../../build/index.js', - '../../../../common/test/resources/timeout-adapter.js', - 'helpers.js', // Provides utility helpers and objects for tests. - 'cases/**/*.js', // Where the tests actually reside. + // { pattern: 'node_modules/**/*.js', served: true, watched: false, included: false }, + // { pattern: 'node_modules/**/*.mjs', served: true, watched: false, included: false, type: "module" }, + // { pattern: 'node_modules/**/*.cjs', served: true, watched: false, included: false }, + + // Provides utility helpers and objects for tests. + { pattern: 'common/predictive-text/unit_tests/in_browser/helpers.mjs', served: true, watched: true, included: false}, + + // Where the tests actually reside. + { pattern: 'common/predictive-text/unit_tests/in_browser/cases/**/*.js', type: 'module' }, + + 'common/test/resources/*.js', + 'common/test/resources/json/models/**/*.json', + { pattern: 'common/web/lm-worker/build/lib/*.js', watched: true, served: true, included: false}, + { pattern: 'common/web/lm-worker/build/lib/*.js.map', watched: true, served: true, included: false}, + { pattern: 'common/predictive-text/build/obj/**/*.*', watched: true, served: true, included: false }, + { pattern: 'common/predictive-text/build/obj/**/*.js.map', watched: true, served: true, included: false }, + { pattern: 'common/predictive-text/build/lib/**/*.*', watched: true, served: true, included: false }, + { pattern: 'common/predictive-text/build/lib/**/*.js.map', watched: true, served: true, included: false }, // We don't have anything in these locations... yet. But they'll be useful for test resources. - 'json/**/*.json', // Where pre-loaded JSON resides. - {pattern: 'resources/**/*.*', watched: true, served: true, included: false}, // General testing resources. + {pattern: 'common/test/resources/**/*.*', watched: true, served: true, included: false}, // General testing resources. ], // list of files / patterns to exclude @@ -50,17 +61,19 @@ module.exports = { // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { - 'json/**/*.json' : ['json_fixtures'] + 'common/test/resources/json/models/**/*.json' : ['json_fixtures'] }, // Settings to properly configure how JSON fixtures are automatically loaded by Karma. jsonFixturesPreprocessor: { - stripPrefix: 'json/', + stripPrefix: 'common/test/resources/json/', variableName: '__json__' }, proxies: { - "/resources/": "/base/resources/" + "/resources/": "/base/common/test/resources/", + // "/node_modules/": "/base/node_modules/", + // "/@keymanapp/lm-worker/": "/base/node_modules/@keymanapp/lm-worker/" }, // web server port diff --git a/common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js b/common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js index 12c0c2aff136aa65677fd7eca621ce253874b1c4..d2481e522cac955c4f60a10684938dd286bf4eb7 100644 --- a/common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js +++ b/common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js @@ -1,22 +1,23 @@ var assert = chai.assert; -var LMLayer = com.keyman.text.prediction.LMLayer; -var DefaultWorker = com.keyman.text.prediction.DefaultWorker; + +import { LMLayer, Worker as WorkerBuilder } from "../../../build/lib/web/index.mjs"; +import * as helpers from "../helpers.mjs"; describe('LMLayer', function () { this.timeout(testconfig.timeouts.standard); describe('[[constructor]]', function () { it('should construct with a single argument', function () { - let lmLayer = new LMLayer(helpers.defaultCapabilities, null, true); + let lmLayer = new LMLayer(helpers.defaultCapabilities, WorkerBuilder.constructInstance(), true); assert.instanceOf(lmLayer, LMLayer); lmLayer.shutdown(); }); }); - describe('#asBlobURI()', function () { + describe.skip('#asBlobURI()', function () { // #asBlobURI() requires browser APIs, hence why it cannot be tested headless in Node. it('should take a function and convert it into a blob function', function (done) { - let uri = DefaultWorker.asBlobURI(function dummyHandler() { + let uri = WorkerBuilder.asBlobURI(function dummyHandler() { // Post something weird, so we can be reasonably certain the Web Worker is... // well, working. // WARNING: Do NOT refactor this string as a variable. It **MUST** remain a string diff --git a/common/predictive-text/unit_tests/in_browser/cases/worker-dummy-integration.js b/common/predictive-text/unit_tests/in_browser/cases/worker-dummy-integration.js index 993975dbfeac9ec27f9a28e98ba878cd3b37c29b..b3c8ec3e9c164c05c4c1eeeef354f2614518bebc 100644 --- a/common/predictive-text/unit_tests/in_browser/cases/worker-dummy-integration.js +++ b/common/predictive-text/unit_tests/in_browser/cases/worker-dummy-integration.js @@ -1,5 +1,7 @@ var assert = chai.assert; -var LMLayer = com.keyman.text.prediction.LMLayer; + +import { LMLayer, Worker } from "../../../build/lib/web/index.mjs"; +import * as helpers from "../helpers.mjs"; /* * Shows off the LMLayer API, using the full prediction interface. @@ -17,7 +19,7 @@ describe('LMLayer using dummy model', function () { this.timeout(testconfig.timeouts.standard * 3); // This one makes multiple subsequent calls across // the WebWorker boundary, so we should be generous here. - var lmLayer = new LMLayer(helpers.defaultCapabilities, null, true); + var lmLayer = new LMLayer(helpers.defaultCapabilities, Worker.constructInstance(), true); var stripIDs = function(suggestions) { suggestions.forEach(function(suggestion) { @@ -60,7 +62,7 @@ describe('LMLayer using dummy model', function () { it('will perform (default) wordbreaking and return word at caret', function () { this.timeout(testconfig.timeouts.standard * 3); // This one makes multiple subsequent calls across // the WebWorker boundary, so we should be generous here. - var lmLayer = new LMLayer(helpers.defaultCapabilities); + var lmLayer = new LMLayer(helpers.defaultCapabilities, Worker.constructInstance()); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax, but @@ -94,6 +96,6 @@ describe('LMLayer using dummy model', function () { } function iGotDistractedByHazel() { - return __json__['future_suggestions/i_got_distracted_by_hazel']; + return __json__['models/future_suggestions/i_got_distracted_by_hazel']; } }); diff --git a/common/predictive-text/unit_tests/in_browser/cases/worker-trie-integration.js b/common/predictive-text/unit_tests/in_browser/cases/worker-trie-integration.js index 833cda2aeae3d49c36bae0a83d13766675253c79..c068daf8f1a919dbc14f6f97d9a9bedcb597f814 100644 --- a/common/predictive-text/unit_tests/in_browser/cases/worker-trie-integration.js +++ b/common/predictive-text/unit_tests/in_browser/cases/worker-trie-integration.js @@ -1,5 +1,7 @@ var assert = chai.assert; -var LMLayer = com.keyman.text.prediction.LMLayer; + +import { LMLayer, Worker } from "../../../build/lib/web/index.mjs"; +import * as helpers from "../helpers.mjs"; /* * How to run the worlist @@ -16,7 +18,7 @@ describe('LMLayer using the trie model', function () { // Parameter 3 = true: enables 'test mode', disables correction-search timeout. // This helps prevent the correction-search timeout from flaking out periodically during unit tests in // CI, since remote servers / devices are involved. - var lmLayer = new LMLayer(helpers.defaultCapabilities, null, true); + var lmLayer = new LMLayer(helpers.defaultCapabilities, Worker.constructInstance(), true); // We're testing many as asynchronous messages in a row. // this would be cleaner using async/await syntax, but @@ -58,7 +60,7 @@ describe('LMLayer using the trie model', function () { // // https://community.software.sil.org/t/search-term-to-key-in-lexical-model-not-working-both-ways-by-default/3133 it('should use the default searchTermToKey()', function () { - var lmLayer = new LMLayer(helpers.defaultCapabilities); + var lmLayer = new LMLayer(helpers.defaultCapabilities, Worker.constructInstance()); return lmLayer.loadModel( // We need to provide an absolute path since the worker is based within a blob. diff --git a/common/predictive-text/unit_tests/in_browser/cases/worker.js b/common/predictive-text/unit_tests/in_browser/cases/worker.js index fdf85dfcf16493d708138cb42a7ae77ff8fe2392..4bcb5bdc1f344311de48f614d889383fa09d6fa2 100644 --- a/common/predictive-text/unit_tests/in_browser/cases/worker.js +++ b/common/predictive-text/unit_tests/in_browser/cases/worker.js @@ -1,6 +1,8 @@ var assert = chai.assert; -var LMLayer = com.keyman.text.prediction.LMLayer; -var DefaultWorker = com.keyman.text.prediction.DefaultWorker; + +import { Worker as WorkerBuilder } from "../../../build/lib/web/index.mjs"; +import { LMLayerWorkerCode } from "/base/common/web/lm-worker/build/lib/worker-main.wrapped.min.js"; +import * as helpers from "../helpers.mjs"; describe('LMLayerWorker', function () { // This one makes multiple subsequent calls across the WebWorker boundary, so we should be generous here. @@ -8,15 +10,16 @@ describe('LMLayerWorker', function () { describe('LMLayerWorkerCode', function() { it('should exist!', function() { - assert.isFunction(LMLayerWorkerCode, - 'Could not find LMLayerWorkerCode! Does embedded_worker.js exist?' - ); + // assert.isFunction(LMLayerWorkerCode, + // 'Could not find LMLayerWorkerCode! Does embedded_worker.js exist?' + // ); + assert.isString(LMLayerWorkerCode); }); }); describe('Usage within a Web Worker', function () { it('should install itself in the worker context', function (done) { - let uri = DefaultWorker.asBlobURI(LMLayerWorkerCode); + let uri = WorkerBuilder.asBlobURI(LMLayerWorkerCode); let worker = new Worker(uri); worker.onmessage = function thisShouldBeCalled(message) { done(); diff --git a/common/predictive-text/unit_tests/in_browser/helpers.js b/common/predictive-text/unit_tests/in_browser/helpers.js deleted file mode 100644 index a2d8ab127ed4c9c11d3c60d7fd07fcdbaa0e41ef..0000000000000000000000000000000000000000 --- a/common/predictive-text/unit_tests/in_browser/helpers.js +++ /dev/null @@ -1,8 +0,0 @@ -var helpers; - -// Establishes the equivalent of a TS namespace. -(function(helpers){ - helpers.defaultCapabilities = { - maxLeftContextCodeUnits: 64 - }; -})(helpers || (helpers = {})); \ No newline at end of file diff --git a/common/predictive-text/unit_tests/in_browser/helpers.mjs b/common/predictive-text/unit_tests/in_browser/helpers.mjs new file mode 100644 index 0000000000000000000000000000000000000000..385379bde64fadf5d3cde19cf820cc73eb205273 --- /dev/null +++ b/common/predictive-text/unit_tests/in_browser/helpers.mjs @@ -0,0 +1,3 @@ +export let defaultCapabilities = { + maxLeftContextCodeUnits: 64 +}; \ No newline at end of file diff --git a/common/predictive-text/unit_tests/in_browser/manual.conf.js b/common/predictive-text/unit_tests/in_browser/manual.conf.cjs similarity index 89% rename from common/predictive-text/unit_tests/in_browser/manual.conf.js rename to common/predictive-text/unit_tests/in_browser/manual.conf.cjs index f1e3ed48652bad04529b3854401feda4e047d6e1..738f2b7713d597b9cf047fc53af14c9aba5d3d36 100644 --- a/common/predictive-text/unit_tests/in_browser/manual.conf.js +++ b/common/predictive-text/unit_tests/in_browser/manual.conf.cjs @@ -1,5 +1,5 @@ module.exports = function(config) { - var base = require("./base.conf.js"); + var base = require("./base.conf.cjs"); var specifics = { // test results reporter to use @@ -19,7 +19,7 @@ module.exports = function(config) { // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - // Can't set in base.conf.js b/c of constant's definition style. + // Can't set in base.conf.cjs b/c of constant's definition style. logLevel: config.LOG_INFO }; diff --git a/common/predictive-text/unit_tests/test.sh b/common/predictive-text/unit_tests/test.sh index f1b49635f62081e6b61958dad39cfebed4ed809e..c35c22aa0329a36e27d55a38efa81865b932f085 100755 --- a/common/predictive-text/unit_tests/test.sh +++ b/common/predictive-text/unit_tests/test.sh @@ -15,7 +15,7 @@ cd "$THIS_SCRIPT_PATH" ################################ Main script ################################ # Defaults -FLAGS="--require ./unit_tests/helpers" +FLAGS="" builder_describe "Runs all tests for the language-modeling / predictive-text layer module" \ "configure" \ @@ -29,8 +29,11 @@ builder_describe "Runs all tests for the language-modeling / predictive-text lay builder_parse "$@" +TEST_OPTS= if builder_has_option --ci && builder_is_debug_build; then builder_die "Options --ci and --debug are incompatible." +elif builder_has_option --ci; then + TEST_OPTS=--ci fi if builder_start_action configure; then @@ -44,26 +47,14 @@ if builder_start_action test:libraries; then # They do not have builder-based scripts, being run directly via npm package script. # So, for now, we add a text header to clarify what is running at each stage, in # addition to fair bit of `pushd` and `popd`. - pushd "$KEYMAN_ROOT/common/models/wordbreakers" echo echo "### Running $(builder_term common/models/wordbreakers) tests" - # NPM doesn't seem to parse the post `--` part if specified via script variable. - # So... a simple if-else will do the job for now. - if builder_has_option --ci; then - npm run test -- -reporter mocha-teamcity-reporter - else - npm run test - fi - popd + "$KEYMAN_ROOT/common/models/wordbreakers/build.sh" test $TEST_OPTS pushd "$KEYMAN_ROOT/common/models/templates" echo echo "### Running $(builder_term common/models/templates) tests" - if builder_has_option --ci; then - npm run test -- -reporter mocha-teamcity-reporter - else - npm run test - fi + "$KEYMAN_ROOT/common/models/templates/build.sh" test $TEST_OPTS popd pushd "$KEYMAN_ROOT/common/models/types" @@ -74,6 +65,12 @@ if builder_start_action test:libraries; then npm run test popd + pushd "$KEYMAN_ROOT/common/web/lm-worker" + echo + echo "### Running ${BUILDER_TERM_START}common/web/lm-worker${BUILDER_TERM_END} tests" + ./build.sh test $TEST_OPTS + popd + builder_finish_action success test:libraries fi @@ -84,7 +81,7 @@ if builder_start_action test:headless; then MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter" fi - npm run mocha -- --recursive $MOCHA_FLAGS ./unit_tests/headless/*.js ./unit_tests/headless/**/*.js + mocha --recursive $MOCHA_FLAGS ./headless/*.js ./headless/**/*.js builder_finish_action success test:headless fi @@ -122,13 +119,13 @@ if builder_start_action test:browser; then if builder_has_option --ci; then KARMA_FLAGS="$KARMA_FLAGS --reporters teamcity,BrowserStack" - KARMA_CONFIG="CI.conf.js" + KARMA_CONFIG="CI.conf.cjs" KARMA_INFO_LEVEL="--log-level=debug" else - KARMA_CONFIG="manual.conf.js" + KARMA_CONFIG="manual.conf.cjs" if builder_is_debug_build; then KARMA_FLAGS="$KARMA_FLAGS --no-single-run" - KARMA_CONFIG="manual.conf.js" + KARMA_CONFIG="manual.conf.cjs" KARMA_INFO_LEVEL="--log-level=debug" echo @@ -137,12 +134,12 @@ if builder_start_action test:browser; then fi fi - if [[ KARMA_CONFIG == "manual.conf.js" ]]; then + if [[ KARMA_CONFIG == "manual.conf.cjs" ]]; then get_browser_set_for_OS else BROWSERS= fi - npm run karma -- start $KARMA_INFO_LEVEL $KARMA_FLAGS $BROWSERS unit_tests/in_browser/$KARMA_CONFIG + karma start $KARMA_INFO_LEVEL $KARMA_FLAGS $BROWSERS ./in_browser/$KARMA_CONFIG builder_finish_action success test:browser fi diff --git a/common/predictive-text/virtualizedWorker.ts b/common/predictive-text/virtualizedWorker.ts deleted file mode 100644 index ea2233d1ab768b8fffae4f7a19f3ec6cf64fbb52..0000000000000000000000000000000000000000 --- a/common/predictive-text/virtualizedWorker.ts +++ /dev/null @@ -1,88 +0,0 @@ -/// -/// - -namespace com.keyman.text.prediction { - // Unfortunately, importing like this loses all type information for the modules. - // It's that, or massively overhaul lm-layer to be module-based instead of - // namespace-based. - var fs = require("fs"); - var vm = require("vm"); - - class VirtualizedWorkerContext { - // The LMLayerWorker installs itself to 'self', the expected Worker global, so we provide an alias. - self: VirtualizedWorkerContext; - - constructor() { - this.self = this; - } - - postMessage: (message: any) => void; - - importScripts(...scriptNames: string[]) { - /* Use of vm.createContext and script.runInContext allow us to avoid - * polluting the global scope with imports. When we throw away the - * context object, imported scripts will be automatically GC'd. - */ - for(let script of scriptNames) { - this.__importScriptString(fs.readFileSync(script, "UTF-8")); - } - } - - __importScriptString(scriptStr: string) { - let context = vm.createContext(this); - var script = new vm.Script(scriptStr); - script.runInContext(context); - } - } - - /** - * Note: this does not create an actual Worker, separate process, or thread. Everything will - * be executed in-line on a virtualized context. - * - * In the future, it might be nice to use Node's Worker Threads implementation. - */ - export class VirtualizedWorker implements Worker { - private _workerContext: VirtualizedWorkerContext; - - constructor(scriptStr: string) { - this._workerContext = new VirtualizedWorkerContext(); - // Needs to exist before setting up the worker; must exist by `.install()`. - this._workerContext.postMessage = this.workerPostMessage.bind(this); - - // Initialize the "worker". - this._workerContext.__importScriptString(scriptStr); - } - - // Sends the worker's postMessage messages to the appropriate `onmessage` handler. - private workerPostMessage(message: unknown) { - if(this.onmessage) { - this.onmessage({data: message} as any as MessageEvent); - } - } - - /** - * Accepts a callback function that will receive messages sent from the `VirtualizedWorker`'s `postMessage` function, - * much like the standard `Worker.onmessage`. - */ - onmessage: (this: Worker, ev: MessageEvent) => any; - - postMessage(message: any) { - let msgObj = {data: message}; - let msgJSON = JSON.stringify(msgObj); - - /* - * Execute the command within the virtualized worker's scope. The worker's returned - * `postMessage` calls will still reach outside, as they have a reference to `this` via - * `postMessage` (which we've set to a bound `this.workerPostMessage`). - * - * Among other things, this will allow the worker to use its internal namespaces without issue. - */ - let msgCommand = "onmessage(" + msgJSON + ")"; - this._workerContext.__importScriptString(msgCommand); - } - - terminate(): void { - this._workerContext = null; - } - } -} \ No newline at end of file diff --git a/common/predictive-text/web-defaultWorker.ts b/common/predictive-text/web-defaultWorker.ts deleted file mode 100644 index 6a4f527c138e286edbf245b8d7e2dc7e047ad723..0000000000000000000000000000000000000000 --- a/common/predictive-text/web-defaultWorker.ts +++ /dev/null @@ -1,27 +0,0 @@ -namespace com.keyman.text.prediction { - export class DefaultWorker { - static constructInstance(): Worker { - return new Worker(this.asBlobURI(LMLayerWorkerCode)); - } - - /** - * Converts the INSIDE of a function into a blob URI that can - * be passed as a valid URI for a Worker. - * @param fn Function whose body will be referenced by a URI. - * - * This function makes the following possible: - * - * let worker = new Worker(LMLayer.asBlobURI(function myWorkerCode () { - * postMessage('inside Web Worker') - * function onmessage(event) { - * // handle message inside Web Worker. - * } - * })); - */ - static asBlobURI(fn: Function): string { - let code = LMLayer.unwrap(fn); - let blob = new Blob([code], { type: 'text/javascript' }); - return URL.createObjectURL(blob); - } - } -} \ No newline at end of file diff --git a/common/test/keyboards/caps_lock_layer_3620/binary/caps_lock_layer_3620.kmx b/common/test/keyboards/caps_lock_layer_3620/binary/caps_lock_layer_3620.kmx new file mode 100644 index 0000000000000000000000000000000000000000..563a139c19f7c6168fb49117503263b82ac5b616 Binary files /dev/null and b/common/test/keyboards/caps_lock_layer_3620/binary/caps_lock_layer_3620.kmx differ diff --git a/common/test/keyboards/caps_lock_layer_3620/binary/caps_lock_layer_3620.kvk b/common/test/keyboards/caps_lock_layer_3620/binary/caps_lock_layer_3620.kvk new file mode 100644 index 0000000000000000000000000000000000000000..79190ea1a00669de6aa6580f99793b43f565020a Binary files /dev/null and b/common/test/keyboards/caps_lock_layer_3620/binary/caps_lock_layer_3620.kvk differ diff --git a/common/test/resources/index.mjs b/common/test/resources/index.mjs new file mode 100644 index 0000000000000000000000000000000000000000..3152cd4584d6746ebfffff38a4abea6809068e4f --- /dev/null +++ b/common/test/resources/index.mjs @@ -0,0 +1,3 @@ +// Exists to facilitate relative path resolution via Node's require.resolve function. + +export { }; \ No newline at end of file diff --git a/common/test/resources/json/engine_tests/chirality.json b/common/test/resources/json/engine_tests/chirality.json index f46ab5399ddb4f94005f7245f063d929fe907a3b..4f3153c2758fc2fc484e405068397e1dba6a8972 100644 --- a/common/test/resources/json/engine_tests/chirality.json +++ b/common/test/resources/json/engine_tests/chirality.json @@ -4,7 +4,13 @@ "id": "test_chirality", "name": "KeymanWeb Chirality Tester", "filename": "resources/keyboards/test_chirality.js", - "languages": {} + "languages": [ + { + "id": "en", + "name": "English", + "region": "World" + } + ] }, "inputTestSets": [ { diff --git a/common/test/resources/json/engine_tests/deadkeys.json b/common/test/resources/json/engine_tests/deadkeys.json index 1764915605bda0c7fe2b229d4dc992d69265f233..aec422f0cc6e23a6b91760a68ec43b696ce3e849 100644 --- a/common/test/resources/json/engine_tests/deadkeys.json +++ b/common/test/resources/json/engine_tests/deadkeys.json @@ -4,7 +4,13 @@ "id": "test_deadkeys", "name": "Keyman Deadkey Stress-Tester", "filename": "resources/keyboards/test_deadkeys.js", - "languages": {} + "languages": [ + { + "id": "en", + "name": "English", + "region": "World" + } + ] }, "inputTestSets": [ { diff --git a/common/predictive-text/unit_tests/in_browser/json/future_suggestions/i_got_distracted_by_hazel.json b/common/test/resources/json/models/future_suggestions/i_got_distracted_by_hazel.json similarity index 100% rename from common/predictive-text/unit_tests/in_browser/json/future_suggestions/i_got_distracted_by_hazel.json rename to common/test/resources/json/models/future_suggestions/i_got_distracted_by_hazel.json diff --git a/common/predictive-text/unit_tests/in_browser/json/tries/accented.json b/common/test/resources/json/models/tries/accented.json similarity index 100% rename from common/predictive-text/unit_tests/in_browser/json/tries/accented.json rename to common/test/resources/json/models/tries/accented.json diff --git a/common/predictive-text/unit_tests/in_browser/json/tries/english-1000.json b/common/test/resources/json/models/tries/english-1000.json similarity index 100% rename from common/predictive-text/unit_tests/in_browser/json/tries/english-1000.json rename to common/test/resources/json/models/tries/english-1000.json diff --git a/common/test/resources/karma-browserstack-config.js b/common/test/resources/karma-browserstack-config.cjs similarity index 100% rename from common/test/resources/karma-browserstack-config.js rename to common/test/resources/karma-browserstack-config.cjs diff --git a/common/test/resources/keyboards/khmer_angkor.js b/common/test/resources/keyboards/khmer_angkor.js index 7b797e7e6922b7e8ca8d5a6e7fd502f9de58b29a..93cdec4df5e081088bd2bc64aea27923565fd430 100644 --- a/common/test/resources/keyboards/khmer_angkor.js +++ b/common/test/resources/keyboards/khmer_angkor.js @@ -6,8 +6,8 @@ KeymanWeb.KR(new Keyboard_khmer_angkor()); } function Keyboard_khmer_angkor() { - var modCodes = com.keyman.text.Codes.modifierCodes; - var keyCodes = com.keyman.text.Codes.keyCodes; + var modCodes = keyman.osk.modifierCodes; + var keyCodes = keyman.osk.keyCodes; this.KI="Keyboard_khmer_angkor"; this.KN="Khmer Angkor"; @@ -2804,7 +2804,7 @@ function Keyboard_khmer_angkor() k.KO(-1,t,"»"); } if(m) { - + k.KDC(-1,t); r=this.g_normalise(t,e); } diff --git a/common/test/resources/keyboards/test_deadkeys.js b/common/test/resources/keyboards/test_deadkeys.js index c55165b8f60b27cec70da4043142e9c06b25ca22..697fb65b5461e96be6b4ef42d81d63b014849e29 100644 --- a/common/test/resources/keyboards/test_deadkeys.js +++ b/common/test/resources/keyboards/test_deadkeys.js @@ -6,8 +6,8 @@ KeymanWeb.KR(new Keyboard_test_deadkeys()); } function Keyboard_test_deadkeys() { - var modCodes = com.keyman.text.Codes.modifierCodes; - var keyCodes = com.keyman.text.Codes.keyCodes; + var modCodes = keyman.osk.modifierCodes; + var keyCodes = keyman.osk.keyCodes; this.KI="Keyboard_test_deadkeys"; this.KN="Keyman Deadkey Stress-Tester"; @@ -325,7 +325,7 @@ function Keyboard_test_deadkeys() k.KDO(-1,t,17); } if(m) { - + k.KDC(-1,t); r=this.g_dead_reorder(t,e); } diff --git a/common/test/resources/model-helpers.mjs b/common/test/resources/model-helpers.mjs new file mode 100644 index 0000000000000000000000000000000000000000..1de10bdb15e48e5b6826c2cb40864f508d477c3c --- /dev/null +++ b/common/test/resources/model-helpers.mjs @@ -0,0 +1,160 @@ +import path from 'path'; +import { assert } from 'chai'; +import fs from 'fs'; +import vm from 'vm'; + +/** + * @file model-helpers.mjs + * + * Defines a common set of helper functions for use with predictive-text-related + * unit tests. + */ + +// Ensure that we can successfully load the module & apply kmwLength, as it's +// needed for some of the unit tests. + +// // Verify that the KMW string extensions are loaded via side-effect. + +import { extendString } from '@keymanapp/web-utils'; + +import { createRequire } from "module"; +import { fileURLToPath } from 'url'; + +// Ensure that our KMW string-extensions activate. +extendString(); +assert.ok('💩'.kmwLength); + +/** + * Creates a MessageEvent (for inter-worker communication), with the given data payload. + * + * @param {*} data + */ +export function createMessageEventWithData(data) { + return { data }; +} + +/** + * Creates a simple, default capabilities object for standard-case LMLayer init. + */ +export function capabilities() { + return { + maxLeftContextCodePoints: 64 + } +} + +/** + * Mimics a message from the outer LMLayer shell with a simple, default config object. + * Used for Worker tests. + */ +export function configWorker(worker) { + worker.onMessage(createMessageEventWithData({ + message: 'config', + capabilities: capabilities() + })); +} + +/** + * A valid model that suggests exactly what you want it to suggest. + * + * @returns {ModelDescription} + */ +export function dummyModel(futureSuggestions) { + return { + type: 'dummy', + futureSuggestions: futureSuggestions || [] + }; +} +/** + * Capabilities of a keyboard that will ONLY send left-sided capabilities. + * The keyboard does not support deleting to the right. + * + * @returns {Capabilities} + */ +export function defaultCapabilities() { + return { + maxLeftContextCodeUnits: 64 + }; +} + +/** + * Returns the Context of an empty buffer; no text, at both the start and + * end of the buffer. + * + * @returns {Context} + */ +export function emptyContext() { + return { + left: '', + startOfBuffer: true, + endOfBuffer: true + }; +} + +/** + * Returns a Transform that, when applied, makes no changes to the buffer. + * + * @returns {Transform} + */ +export function zeroTransform() { + return { + insert: '', + deleteLeft: 0, + }; +} + +/** + * Returns a random token. NOT guaranteed to be unique. + * + * @returns {Token} + */ +export function randomToken() { + var range = Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER; + return Math.random() * range + Number.MIN_SAFE_INTEGER; +} + +export function iGotDistractedByHazel() { + return jsonFixture('models/future_suggestions/i_got_distracted_by_hazel'); +} + +export function jsonFixture(name, root, import_root) { + // Assuming this file structure: + // root + // └── json + // ├── future_suggestions + // │ └── ... + // └── wordlists + // └── ... + + // Default root: this folder. + if(!root) { + root = 'json'; + } + + if(!import_root) { + import_root = import.meta.url; + } + + // The most straight-forward way... is to use CommonJS-style require to load JSON. + // Fortunately, Node provides the tools needed to recreate it. + const require = createRequire(import_root); + + // ES-module mode also leaves out `__dirname`, so we rebuild that too. + const __filename = fileURLToPath(import_root); + const __dirname = path.dirname(__filename); + return require(path.join(__dirname, root, `${name}.json`)); +} + +export function importScriptsWith(context) { + return function() { // the constructed context's importScripts method. + + /* Use of vm.createContext and script.runInContext allow us to avoid + * polluting the global scope with imports. When we throw away the + * context object, imported scripts will be automatically GC'd. + */ + for(var i=0; i < arguments.length; i++) { + context = vm.createContext(context); + var script = new vm.Script(fs.readFileSync(arguments[i])); + script.runInContext(context); + } + } +} diff --git a/common/predictive-text/unit_tests/in_browser/resources/models/naive-trie.js b/common/test/resources/models/naive-trie.js similarity index 100% rename from common/predictive-text/unit_tests/in_browser/resources/models/naive-trie.js rename to common/test/resources/models/naive-trie.js diff --git a/common/predictive-text/unit_tests/in_browser/resources/models/simple-dummy.js b/common/test/resources/models/simple-dummy.js similarity index 100% rename from common/predictive-text/unit_tests/in_browser/resources/models/simple-dummy.js rename to common/test/resources/models/simple-dummy.js diff --git a/common/predictive-text/unit_tests/in_browser/resources/models/simple-trie.js b/common/test/resources/models/simple-trie.js similarity index 100% rename from common/predictive-text/unit_tests/in_browser/resources/models/simple-trie.js rename to common/test/resources/models/simple-trie.js diff --git a/common/test/resources/package.json b/common/test/resources/package.json new file mode 100644 index 0000000000000000000000000000000000000000..03f8de3cb4f63223fb0b043d2650ab238bf6f9aa --- /dev/null +++ b/common/test/resources/package.json @@ -0,0 +1,12 @@ +{ + "name": "@keymanapp/common-test-resources", + "description": "Common test resources for Keyman's JS & TS-based components & products", + "type": "module", + "license": "MIT", + "devDependencies": { + "@keymanapp/resources-gosh": "*", + "@types/node": "^10.17.21", + "chai": "^4.3.4", + "typescript": "^4.9.5" + } +} diff --git a/common/test/resources/timeout-adapter.js b/common/test/resources/timeout-adapter.js index b8141a6a418858330d1eec338c2bed95a5a58957..0e93ec94a9558a46789cb672178ce15ac8029bd3 100644 --- a/common/test/resources/timeout-adapter.js +++ b/common/test/resources/timeout-adapter.js @@ -1,6 +1,6 @@ // Preprocessing of the Karma configuration's client.args parameter. -com = com || {}; +var com = com || {}; com.keyman = com.keyman || {}; com.keyman.karma = com.keyman.karma || {}; diff --git a/common/tools/hextobin/hextobin.ts b/common/tools/hextobin/hextobin.ts index 7a184f95fb71edf660752ac709dc006566d5f8d3..503b18d4b1a9e46f76446681e83a5ff9fc95e10d 100644 --- a/common/tools/hextobin/hextobin.ts +++ b/common/tools/hextobin/hextobin.ts @@ -19,10 +19,11 @@ Format: 11 22 33 44 55 aa bb # inserts hex bytes offset(block) # inserts 4 byte offset of block from BOF sizeof(block[,divisor]) # inserts 4 byte size of block, optionally divided by divisor - diff(block1,block2) # inserts 4 byte offset diff between start of block1 and block2 + diff(block1,block2[,divisor]) # inserts 4 byte offset diff between start of block1 and block2, ÷divisor (default: ÷1) index(block1,block2[,divisor]) # inserts 4 byte index diff between block1 and block2, # optionally divided by divisor # block names are required, but if not referenced can be reused (e.g. block(x)) + # Spaces after commas aren't allowed: 'sizeof(block,3)' not 'sizeof(block, 3)' ` ) .arguments('') diff --git a/common/tools/hextobin/index.ts b/common/tools/hextobin/index.ts index db2094538398306ed24bbd8b53a4f81327a0f7c7..b53b2d734a624a260846b10ce3a81148c7463399 100644 --- a/common/tools/hextobin/index.ts +++ b/common/tools/hextobin/index.ts @@ -100,8 +100,12 @@ export default async function hextobin(inputFilename: string, outputFilename?: s } break; case 'diff': - b.refs.push({type: 'diff', blockName: t.parameters[0], blockName2: t.parameters[1], offset: b.hex.length / 2}); - b.hex += "_".repeat(8); // always 4 bytes, placeholder will be filled in during reconciliation phase + // diff can also take a divisor, defaults to 1 + { + const divisor = t.parameters.length > 2 ? parseInt(t.parameters[2],10) : 1; + b.refs.push({type: 'diff', blockName: t.parameters[0], blockName2: t.parameters[1], divisor: divisor, offset: b.hex.length / 2}); + b.hex += "_".repeat(8); // always 4 bytes, placeholder will be filled in during reconciliation phase + } break; case 'index': // index can take a third parameter, divisor @@ -181,7 +185,7 @@ export default async function hextobin(inputFilename: string, outputFilename?: s reportError(`Could not find block ${r.blockName2} when reconciling ${b.name}`); return false; } - fillBlockPlaceholder(b, r.offset, v2.offset - v.offset); + fillBlockPlaceholder(b, r.offset, (v2.offset - v.offset) / r.divisor); } break; case 'offset': @@ -226,4 +230,4 @@ export default async function hextobin(inputFilename: string, outputFilename?: s } return buffer; } -} \ No newline at end of file +} diff --git a/common/tools/sourcemap-path-remapper/build.sh b/common/tools/sourcemap-path-remapper/build.sh index 16c5c76a673b9c23167cc51a63984cab1a87b6c1..d917bca89bc6557b73a1430c588cc8b9296d3633 100755 --- a/common/tools/sourcemap-path-remapper/build.sh +++ b/common/tools/sourcemap-path-remapper/build.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash # -# Compile our sourcemap-path remapping module for use by Web builds, releases, etc. +# Compiles the JS sourcemap remapper tool used by some of our TS projects when complications +# arise with sourcemap handling. # + +# Exit on command failure and when using unset variables: set -eu ## START STANDARD BUILD SCRIPT INCLUDE @@ -17,9 +20,6 @@ cd "$THIS_SCRIPT_PATH" ################################ Main script ################################ -# TODO: for predictive-text, we only need :headless, perhaps we should be splitting modules? -# TODO: remove :tools once kmlmc is a dependency for test:module - builder_describe "Builds a sourcemap manipulation ES module for use in Web-related builds" \ "clean" \ "configure" \ @@ -31,13 +31,6 @@ builder_describe_outputs \ builder_parse "$@" -### CONFIGURE ACTIONS - -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi - ### CLEAN ACTIONS if builder_start_action clean; then @@ -45,6 +38,13 @@ if builder_start_action clean; then builder_finish_action success clean fi +### CONFIGURE ACTIONS + +if builder_start_action configure; then + verify_npm_setup + builder_finish_action success configure +fi + ### BUILD ACTIONS if builder_start_action build; then diff --git a/common/tools/sourcemap-path-remapper/package.json b/common/tools/sourcemap-path-remapper/package.json index 552d60989b39bfd14b02e345a13b68e650eee9d6..2a468977393aa9caf51b62de1753aa9780d90fe8 100644 --- a/common/tools/sourcemap-path-remapper/package.json +++ b/common/tools/sourcemap-path-remapper/package.json @@ -10,7 +10,7 @@ "type": "git", "url": "git+https://github.com/keymanapp/keyman.git" }, - "author": "Joshua A. Horton ", + "author": "Joshua A. Horton ", "license": "MIT", "bugs": { "url": "https://github.com/keymanapp/keyman/issues" diff --git a/common/tools/sourcemap-path-remapper/src/index.ts b/common/tools/sourcemap-path-remapper/src/index.ts index de54a3499b4a428b0e60e85b4ba8681c23e1145f..fca96c3b14ea14c31fb02cf650039d5aa50fbf69 100644 --- a/common/tools/sourcemap-path-remapper/src/index.ts +++ b/common/tools/sourcemap-path-remapper/src/index.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; -import convertSourcemap from 'convert-source-map'; // Transforms sourcemaps among various common formats. +import path from 'path'; +import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats. // Base64, stringified-JSON, end-of-file comment... // Because we're compiling the main project based on its TS build outputs, and our cross-module references @@ -16,33 +17,33 @@ export interface Mapping { */ class RemappingState { constructor(srcmap: Object) { - this.sourcemap = srcmap; + this.sourceMap = srcmap; this.resetChangedSourcepathTracking(); } - public readonly sourcemap: any; - private _unchangedSourcepaths: string[] = []; + public readonly sourceMap: any; + private _unchangedSourcePaths: string[] = []; public get unchangedSourcepaths() { - return this._unchangedSourcepaths; + return this._unchangedSourcePaths; } public resetChangedSourcepathTracking() { - this._unchangedSourcepaths = [...this.sourcemap.sources]; + this._unchangedSourcePaths = [...this.sourceMap.sources]; } /** * The sourcemap's `sourceRoot` property. */ public get sourceRoot() { - return this.sourcemap.sourceRoot; + return this.sourceMap.sourceRoot; } /** * The sourcemap's `sourceRoot` property. */ public set sourceRoot(path: string) { - this.sourcemap.sourceRoot = path; + this.sourceMap.sourceRoot = path; } /** @@ -59,7 +60,7 @@ class RemappingState { const unusedTransforms = [...transforms]; - const sourcePaths = this.sourcemap.sources; + const sourcePaths = this.sourceMap.sources; for(let i = 0; i < sourcePaths.length; i++) { for(const transform of transforms) { if(sourcePaths[i].match(transform.from)) { @@ -96,12 +97,32 @@ class RemappingState { return this; } + /** + * Prepends all `sources` paths with the current sourceRoot (or just the part after a + * specified index). + * @param index + */ + public normalizeWithSourceRoot(index?: number) { + const sources = this.sourceMap.sources; + const originalSrcRoot = this.sourceRoot; + + index = index || 0; + const prefix = originalSrcRoot.substring(index); + this.sourceRoot = originalSrcRoot.substring(0, index); + + for(let i=0; i < sources.length; i++) { + // Normalizes, then swaps Windows '\\' entries with '/' if they exist; + // path.normalize respects Windows too much for our taste here. + sources[i] = path.normalize(`${prefix}/${sources[i]}`).replace(/\\/g, '/'); + } + } + /** * Writes the current state of the remapped sourcemap out to the specified file. * @param file */ public toFile(file: fs.PathLike) { - fs.writeFileSync(file, convertSourcemap.fromObject(this.sourcemap).toJSON()); + fs.writeFileSync(file, convertSourceMap.fromObject(this.sourceMap).toJSON(2)); // 2 = space count } } @@ -132,6 +153,6 @@ export default class SourcemapRemapper { * @returns */ public static fromBuffer(buffer: Buffer) { - return this.fromObject(convertSourcemap.fromJSON(buffer).toObject()); + return this.fromObject(convertSourceMap.fromJSON(buffer).toObject()); } } \ No newline at end of file diff --git a/common/web/es-bundling/README.md b/common/web/es-bundling/README.md new file mode 100644 index 0000000000000000000000000000000000000000..dd0b553bfa74d2a1449113b3ee066cf552562530 --- /dev/null +++ b/common/web/es-bundling/README.md @@ -0,0 +1,6 @@ +This subproject provides definitions for the common configuration settings used for bundling +web/ and common/web projects via `esbuild`. This includes: + +- Standard settings for bundling to IIFE and ESM module formats +- An `esbuild` plugin that facilitate tree-shaking of classes downcompiled to ES5 by TypeScript +- Two `esbuild` plugins that facilitate tree-shaking of unused `tslib` helper functions \ No newline at end of file diff --git a/common/web/es-bundling/build.sh b/common/web/es-bundling/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..3fbcd01133e10def7796b1cfad8da098e6a9b342 --- /dev/null +++ b/common/web/es-bundling/build.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +################################ Main script ################################ + +builder_describe "Builds KMW's esbuild-oriented common configuration & tooling" \ + "@/common/web/tslib" \ + "clean" \ + "configure" \ + "build" + +builder_describe_outputs \ + configure /node_modules \ + build /common/web/es-bundling/build/index.mjs + +builder_parse "$@" + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build tsc -b tsconfig.json \ No newline at end of file diff --git a/common/web/es-bundling/src/classTreeshaker.mts b/common/web/es-bundling/src/classTreeshaker.mts new file mode 100644 index 0000000000000000000000000000000000000000..4f0e1d8dad19493249f6cd816a16ea5b5d619212 --- /dev/null +++ b/common/web/es-bundling/src/classTreeshaker.mts @@ -0,0 +1,23 @@ +import type * as esbuild from 'esbuild'; +import * as fs from 'fs'; + +/** + * Refer to https://github.com/microsoft/TypeScript/issues/13721#issuecomment-307259227 - + * the `@class` emit comment-annotation is designed to facilitate tree-shaking for ES5-targeted + * down-level emits. `esbuild` doesn't look for it by default... but we can override that with + * this plugin. + */ +export const pluginForDowncompiledClassTreeshaking: esbuild.Plugin = { + name: '@class -> __PURE__', + setup(build) { + build.onLoad({filter: /\.js$/ }, async (args) => { + let source = await fs.promises.readFile(args.path, 'utf8'); + return { + // Marks any classes compiled by TS (as per the /** @class */ annotation) + // as __PURE__ in order to facilitate tree-shaking. + contents: source.replace('/** @class */', '/* @__PURE__ */ /** @class */'), + loader: 'js' + } + }); + } +} \ No newline at end of file diff --git a/common/web/es-bundling/src/configuration.mts b/common/web/es-bundling/src/configuration.mts new file mode 100644 index 0000000000000000000000000000000000000000..01e0cc32f1a358abbf7f1c257f12f166208293f1 --- /dev/null +++ b/common/web/es-bundling/src/configuration.mts @@ -0,0 +1,60 @@ +import type * as esbuild from 'esbuild'; +import { pluginForDowncompiledClassTreeshaking } from './classTreeshaker.mjs'; + +export const esmConfiguration: esbuild.BuildOptions = { + alias: { + 'tslib': '@keymanapp/tslib' + }, + bundle: true, + format: "esm", + outExtension: { '.js': '.mjs'}, + plugins: [ pluginForDowncompiledClassTreeshaking ], + sourcemap: true, + sourcesContent: true, + target: "es5" +}; + +export const iifeConfiguration: esbuild.BuildOptions = { + ...esmConfiguration, + format: 'iife', + outExtension: { '.js': '.js'}, + treeShaking: true +}; + +export function objRoot(entryPath: string) { + const OBJ_REGEX = /^(.+)\/obj\//; + const match = entryPath.match(OBJ_REGEX); + + return `${match[1]}`; +} + +/** + * Given the KMW build output convention of raw TS output within a subfolder of `build` named `obj`, + * this configures esbuild to use the specified entry points for bundling and to have their output + * bundle locations rooted within a specified sibling folder to their "source" `obj` subfolder. + * + * Examples: + * 1. 'lib', 'build/obj/index.js' => 'build/lib/index.js' + * 2. 'debug', 'build/app/ui/obj/kmwuitoggle.js' => 'build/app/browser/debug/kmwuitoggle.js' + * + * @param path + * @configFolder + * @returns + */ +export function bundleObjEntryPoints(configFolder: 'lib' | 'debug' | 'release', ...path: string[]): esbuild.BuildOptions { + if(!path.length) { + throw new Error("Must specify at least one entry point."); + } + + let mappedRoot = objRoot(path[0]); + if(path.map((entry) => objRoot(entry)).find((mappedEntry) => mappedEntry != mappedRoot)) { + throw new Error("The specified entry points map to different roots, therefore mapping is not 'standard'."); + } + + mappedRoot = `${mappedRoot}/${configFolder}/` + + return { + entryPoints: path, + outdir: mappedRoot + }; +} \ No newline at end of file diff --git a/common/web/es-bundling/src/index.mts b/common/web/es-bundling/src/index.mts new file mode 100644 index 0000000000000000000000000000000000000000..a7fd0a330f6605b9652c6f74db5b974e60306aab --- /dev/null +++ b/common/web/es-bundling/src/index.mts @@ -0,0 +1,3 @@ +export * from './classTreeshaker.mjs'; +export * from './configuration.mjs'; +export * from './tslibTreeshaking.mjs'; \ No newline at end of file diff --git a/common/web/es-bundling/src/tslibTreeshaking.mts b/common/web/es-bundling/src/tslibTreeshaking.mts new file mode 100644 index 0000000000000000000000000000000000000000..9091a6f910635171c0195e2352a516c1194aa345 --- /dev/null +++ b/common/web/es-bundling/src/tslibTreeshaking.mts @@ -0,0 +1,351 @@ +import esbuild from 'esbuild'; +import * as fs from 'fs'; + +// Note: this package is intended 100% as a dev-tool, hence why esbuild is just a dev-dependency. + +// Component #1: Detect all `tslib` helpers we actually want to use. +export async function determineNeededDowncompileHelpers(config: esbuild.BuildOptions, ignoreFilePattern?: RegExp, log?: boolean) { + const tslibHelperNames = [ + "__extends", + "__assign", + "__rest", + "__decorate", + "__param", + "__metadata", + "__awaiter", + "__generator", + "__exportStar", // uses __createBinding + "__createBinding", + "__values", + "__read", + "__spread", + "__spreadArray", + "__spreadArrays", + "__await", + "__asyncGenerator", + "__asyncDelegator", + "__asyncValues", + "__makeTemplateObject", + "__importStar", + "__setModuleDefault", // only used by the previous entry! + "__importDefault", + "__classPrivateFieldGet", + "__classPrivateFieldSet", + "__classPrivateFieldIn", + "__runInitializers", + "__setFunctionName", + "__propKey", + "__esDecorate" + ]; + + const detectedHelpers: string[] = []; + + const pluginName = 'es-bundling: tslib helper use detection' + let tslibHelperDetectionPlugin = { + name: pluginName, + setup(build: esbuild.PluginBuild) { + build.onLoad({filter: /\.js$/}, async (args) => { + let source = await fs.promises.readFile(args.path, 'utf8'); + + let warnings: esbuild.Message[] = []; + + if(/tslib.js$/.test(args.path)) { + let declarationRegex = /var (__[a-zA-Z0-9]+)/g; + let results = source.match(declarationRegex); + + if(!results) { + return buildMessage( + pluginName, + "tslib's definition pattern has shifted dramatically! tslib helper-definition detector maintenance is needed.", + indexToSourceLocation('', 0, 0, args.path) + ); + } + + for(let result of results) { + let capture = result.substring(4); + + if(!tslibHelperNames.find((entry) => entry == capture)) { + // Match: `result` itself. Grab index, build location, build message. + let index = source.indexOf(result); + + warnings.push(buildMessage( + pluginName, + 'Probable `tslib` helper `' + capture + '` has not been validated for tree-shaking potential', + indexToSourceLocation(source, index, result.length, args.path) + )); + } + } + + // Returning `undefined` for `content` makes this 'pass-through' - it doesn't prevent other + // configured plugins from working. + return { + warnings: warnings + }; + } + + if(ignoreFilePattern?.test(args.path)) { + return {}; + } + + for(let helper of tslibHelperNames) { + if(source.indexOf(helper) > -1 && !detectedHelpers.find((entry) => entry == helper)) { + detectedHelpers.push(helper); + + if(helper == '__importStar') { + detectedHelpers.push('__setModuleDefault'); + detectedHelpers.push('__createBinding'); + } else if(helper == '__exportStar') { + detectedHelpers.push('__createBinding'); + } + } + } + + // Returning an empty object makes this 'pass-through' - it doesn't prevent other + // configured plugins from working. + return {}; + }); + } + } + + // tslib tree-shake phase 1 - detecting which helpers are safe to remove. + await esbuild.build({ + ...config, + // `tslibHelperDetectionPlugin` is pass-through, and so has no net effect on + // the build. We just use this run to scan for any utilized `tslib` helper funcs, + // not to manipulate the actual source in any way. + plugins: [ tslibHelperDetectionPlugin, ...(config.plugins ?? []) ], + write: false + }); + + // At this point, we can determine what's unused. + detectedHelpers.sort(); + + const unusedHelpers: string[] = []; + tslibHelperNames.forEach((entry) => { + if(!detectedHelpers.find((detected) => detected == entry)) { + unusedHelpers.push(entry); + } + }); + + if(log) { + // Logs on the tree-shaking decisions + console.log("Detected helpers from tslib: "); + console.log(); + + console.log(); + console.log("Unused helpers: "); + console.log(unusedHelpers); + } + + return unusedHelpers; +} + +function indexToSourceLocation(source: string, index: number, matchLength: number, file: string): esbuild.Location { + let priorText = source.substring(0, index); + let lineNum = priorText.split('\n').length; + let lastLineBreakIndex = priorText.lastIndexOf('\n'); + let colNum = index - lastLineBreakIndex; + let nextLineBreakIndex = source.indexOf('\n', lastLineBreakIndex+1); + + return { + file: file, + line: lineNum, + column: colNum, + lineText: source.substring(lastLineBreakIndex+1, nextLineBreakIndex), + namespace: '', + suggestion: '', + length: matchLength + } +} + +function buildMessage(pluginName: string, message: string, location: esbuild.Location): esbuild.Message { + return { + id: '', + notes: [], + detail: '', + pluginName: pluginName, + text: message, + location: location + } +} + +// Ugh. https://github.com/evanw/esbuild/issues/2656#issuecomment-1304013941 +function wrapClassPlugin(instancePlugin: esbuild.Plugin) { + return { + name: instancePlugin.name, + setup: instancePlugin.setup.bind(instancePlugin) + }; +} + +// Component #2: when we've actually determined which ones are safe to remove, this plugin +// can remove their code. +class TslibTreeshaker implements esbuild.Plugin { + public readonly name = 'tslib forced treeshaking'; + + private unusedHelpers: string[]; + + constructor(unusedHelpers: string[]) { + this.unusedHelpers = unusedHelpers; + } + + treeshakeDefinition(source: string, start: number, expectTernary: boolean, name: string, location: esbuild.Location) { + let scopeDepth = 0; + let parenDepth = 0; + let topLevelTernaryActive = 0; + let i = start; + let char = source.charAt(i); + while(char != '}' || --scopeDepth != 0 || topLevelTernaryActive) { + if(char == '{') { + scopeDepth++; + } else if(char == '(') { + parenDepth++; + } else if(char == ')') { + parenDepth--; + } else if(char == '?' && scopeDepth == 0) { + if(!expectTernary) { + return { + source: source, + error: buildMessage(this.name, `Unexpected conditional for definition of ${name}`, location) + } + } + topLevelTernaryActive++; + } else if(char == ':' && topLevelTernaryActive && scopeDepth == 0) { + topLevelTernaryActive--; + } + i++; + + if(i > source.length) { + return { + source: source, + error: buildMessage(this.name, `Failed to determine end of definition for ${name}`, location) + }; + } + + char = source.charAt(i); + } + i++; // we want to erase the final '}', too. + + // The functions may be wrapped with parens. + if(parenDepth == 1) { + let nextOpen = source.indexOf('(', i); + let nextClosed = source.indexOf(')', i); + + if(nextOpen < nextClosed) { + return { + source: source, + error: buildMessage(this.name, `Failed to determine end of definition for ${name}`, location) + }; + } else { + i = nextClosed + 1; + } + } + + if(source.charAt(i) == ';') { + i++; + } + + return { + source: source.replace(source.substring(start, i), '') + }; + } + + setup(build: esbuild.PluginBuild) { + build.onLoad({filter: /tslib.js$/}, async (args) => { + const trueSource = await fs.promises.readFile(args.path, 'utf8'); + let source = trueSource; + + let warnings: esbuild.Message[] = []; + let errors: esbuild.Message[] = []; + + for(let unusedHelper of this.unusedHelpers) { + // Removes the 'exporter' line used to actually export it from the tslib source. + source = source.replace(`exporter\(\"${unusedHelper}\", ${unusedHelper}\);`, ''); + + // Removes the actual helper function definition - obviously, the biggest filesize savings to be had here. + let matchString = `${unusedHelper} = function`; + let expectTernary = false; + + // A special case - it has two different versions depending on if Object.create exists or not. + // This is established via a ternary conditional. Just adds a bit to the parsing. + if(unusedHelper == '__createBinding') { + matchString = `${unusedHelper} = `; + expectTernary = true; + } else if(unusedHelper == '__setModuleDefault') { + matchString = `var ${unusedHelper} = `; // is inlined, not declared at top! + expectTernary = true; + } + let definitionStart = source.indexOf(matchString); + + if(definitionStart > -1) { + const result = this.treeshakeDefinition( + source, + definitionStart, + expectTernary, + unusedHelper, + indexToSourceLocation(trueSource, trueSource.indexOf(matchString), matchString.length, args.path) + ); + + if(result.error) { + errors.push(result.error); + } else { + source = result.source; + } + continue; + } + + // If we reached this point, we couldn't treeshake the unused helper appropriately. + if(definitionStart == -1) { + // Matches the standard definition pattern's left-hand assignment component. + matchString = `${unusedHelper} =` + let bestGuessIndex = trueSource.indexOf(matchString); + + if(bestGuessIndex == -1) { + // Failing the above, we match the declaration higher up within the file. + matchString = `var ${unusedHelper}` + bestGuessIndex = trueSource.indexOf(matchString); + } + + // Failing THAT, we just give up and go start-of-file. + if(bestGuessIndex == -1) { + matchString = ''; + bestGuessIndex = 0; + } + + warnings.push( + buildMessage( + this.name, + "tslib has likely been updated recently; could not force-treeshake tslib helper " + unusedHelper, + indexToSourceLocation(trueSource, bestGuessIndex, matchString.length, args.path) + ) + ); + } + } + + return { + contents: source, + loader: 'js', + warnings: warnings, + errors: errors + }; + }); + } +}; + +export function buildTslibTreeshaker(unusedHelpers: string[]) { + return wrapClassPlugin(new TslibTreeshaker(unusedHelpers)); +}; + +/** + * Runs a non-output build of the build configuration to prepare the ability to treeshake unused + * `tslib` downcompile helper functions and adds a fully-configured plugin to the configuration + * once completed. + * @param esbuildConfig + * @returns The same build-configuration instance passed in + */ +export async function prepareTslibTreeshaking(esbuildConfig: esbuild.BuildOptions, ignoreFilePattern?: RegExp, log?: boolean) { + // Prepare the needed setup for `tslib` treeshaking. + const unusedHelpers = await determineNeededDowncompileHelpers(esbuildConfig, ignoreFilePattern, log); + esbuildConfig.plugins = [buildTslibTreeshaker(unusedHelpers), ...esbuildConfig.plugins]; + + return esbuildConfig; +} \ No newline at end of file diff --git a/common/web/es-bundling/tsconfig.json b/common/web/es-bundling/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..d2ab738acfbd1518b9cb3e9d63709134c434dbd5 --- /dev/null +++ b/common/web/es-bundling/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.esm-base.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "build/", + "tsBuildInfoFile": "build/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "include": ["./src/**/*.mts"] +} diff --git a/common/web/input-processor/build-bundler.js b/common/web/input-processor/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..11277c46206cae64f9be8fad1119c16026ed9d18 --- /dev/null +++ b/common/web/input-processor/build-bundler.js @@ -0,0 +1,8 @@ +import esbuild from 'esbuild'; +import { esmConfiguration, bundleObjEntryPoints } from '../es-bundling/build/index.mjs'; + +// Bundled ES module version +await esbuild.build({ + ...esmConfiguration, + ...bundleObjEntryPoints('lib', 'build/obj/index.js') +}); \ No newline at end of file diff --git a/common/web/input-processor/build.sh b/common/web/input-processor/build.sh index 11efb33fbaa300f0329f019a0e54732efc7f4dbc..f1d62d12fe087480ba68e3608a288cdc8608f9ad 100755 --- a/common/web/input-processor/build.sh +++ b/common/web/input-processor/build.sh @@ -30,43 +30,28 @@ builder_describe "Builds the standalone, headless form of Keyman Engine for Web' builder_describe_outputs \ configure /node_modules \ - build /common/web/input-processor/build/index.js + build /common/web/input-processor/build/lib/index.mjs \ builder_parse "$@" -### CONFIGURE ACTIONS +function do_build() { + tsc -b ./tsconfig.json + node build-bundler.js -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi + # Declaration bundling. + tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts +} -### CLEAN ACTIONS - -if builder_start_action clean; then - rm -rf build/ - builder_finish_action success clean -fi - -### BUILD ACTIONS - -if builder_start_action build; then - tsc --build src/tsconfig.json - builder_finish_action success build -fi - -# TEST ACTIONS - -if builder_start_action test; then - FLAGS= +function do_test() { + local FLAGS= if builder_has_option --ci; then FLAGS="--reporter mocha-teamcity-reporter" fi - # Build the leaf-style, bundled version of input-processor for use in testing. - tsc -b src/tsconfig.bundled.json - mocha --recursive $FLAGS ./tests/cases/ +} - builder_finish_action success test -fi \ No newline at end of file +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/web/input-processor/package.json b/common/web/input-processor/package.json index 6675574334dfd432dd20912a1eda3eb222a27849..e64581d23199a821f53ee48c5e50b3c31ce828ae 100644 --- a/common/web/input-processor/package.json +++ b/common/web/input-processor/package.json @@ -27,9 +27,7 @@ "typescript": "^4.9.5" }, "scripts": { - "tsc": "tsc", - "test": "gosh ./test.sh", - "mocha": "mocha" + "test": "gosh ./test.sh" }, "dependencies": { "@keymanapp/keyboard-processor": "*", @@ -38,5 +36,16 @@ "@keymanapp/keyman-version": "*", "@keymanapp/web-utils": "*", "eventemitter3": "^4.0.0" + }, + "type": "module", + "main": "./build/obj/index.js", + "types": "./build/obj/index.d.ts", + "exports": { + ".": "./build/obj/index.js", + "./lib": { + "types": "./build/lib/index.d.ts", + "import": "./build/lib/index.mjs" + }, + "./obj/*.js": "./build/obj/*.js" } } diff --git a/common/web/input-processor/src/includes/events.ts b/common/web/input-processor/src/includes/events.ts deleted file mode 100644 index 2893c0ccee9f181956ecc0dfa6695a37cce0d6c7..0000000000000000000000000000000000000000 --- a/common/web/input-processor/src/includes/events.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Implements Node's EventEmitter class and related module components in a near -// browser-compatible way. (Just requires a blank 'module' object on the window.) -/// - -// Unfortunately, I can't get it to recognize type information properly -// because we can't use require statements. So, a small-scale manual definition. -declare class EventEmitter { - /** Add a listener for a given event */ - on(event: string, func: (...args: any[]) => boolean, context?: any); - /** Add a one-time listener for a given event */ - once(event: string, func: (...args: any[]) => boolean, context?: any); - removeListener(event: string, func: (...args: any[]) => boolean, context?: any, once?: boolean); - - // Defines their alternately-themed aliases. - addListener: typeof EventEmitter.prototype.on; - off: typeof EventEmitter.prototype.removeListener; - - // Defines the actual event-raising function. - emit(eventName: string, ...args: any[]); -} \ No newline at end of file diff --git a/common/web/input-processor/src/index.ts b/common/web/input-processor/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..360a66010834b73c5ccf54c2cdaee26144607063 --- /dev/null +++ b/common/web/input-processor/src/index.ts @@ -0,0 +1,7 @@ +/// + +export { default as InputProcessor } from './text/inputProcessor.js'; +export { default as ContextWindow } from './text/contextWindow.js'; +export { default as ModelSpec } from './text/prediction/modelSpec.js'; +export { default as LanguageProcessor, StateChangeEnum } from './text/prediction/languageProcessor.js'; +export { default as PredictionContext } from './text/prediction/predictionContext.js'; \ No newline at end of file diff --git a/common/web/input-processor/src/text/contextWindow.ts b/common/web/input-processor/src/text/contextWindow.ts index 69b83a58dbb96a1bb63d7e41f60b84aed8459168..2adb4f0b6a6a0b11c5bc05b8edf3a1b71937f740 100644 --- a/common/web/input-processor/src/text/contextWindow.ts +++ b/common/web/input-processor/src/text/contextWindow.ts @@ -1,44 +1,44 @@ -namespace com.keyman.text { - export class ContextWindow implements Context { - // Used to limit the range of context replicated for use of keyboard rules within - // the engine, as used for fat-finger prep / `Alternate` generation. - public static readonly ENGINE_RULE_WINDOW: Configuration = { - leftContextCodePoints: 64, - rightContextCodePoints: 32 - }; - - left: string; - right?: string; - - startOfBuffer: boolean; - endOfBuffer: boolean; - - casingForm?: CasingForm; - - constructor(mock: Mock, config: Configuration, layerId: string) { - this.left = mock.getTextBeforeCaret(); - this.startOfBuffer = this.left._kmwLength() <= config.leftContextCodePoints; - if(!this.startOfBuffer) { - // Our custom substring version will return the last n characters if param #1 is given -n. - this.left = this.left._kmwSubstr(-config.leftContextCodePoints); - } - - this.right = mock.getTextAfterCaret(); - this.endOfBuffer = this.right._kmwLength() <= config.rightContextCodePoints; - if(!this.endOfBuffer) { - this.right = this.right._kmwSubstr(0, config.rightContextCodePoints); - } - - this.casingForm = - layerId == 'shift' ? 'initial' : - layerId == 'caps' ? 'upper' : - null; +import { Mock } from "@keymanapp/keyboard-processor"; + +export default class ContextWindow implements Context { + // Used to limit the range of context replicated for use of keyboard rules within + // the engine, as used for fat-finger prep / `Alternate` generation. + public static readonly ENGINE_RULE_WINDOW: Configuration = { + leftContextCodePoints: 64, + rightContextCodePoints: 32 + }; + + left: string; + right?: string; + + startOfBuffer: boolean; + endOfBuffer: boolean; + + casingForm?: CasingForm; + + constructor(mock: Mock, config: Configuration, layerId: string) { + this.left = mock.getTextBeforeCaret(); + this.startOfBuffer = this.left._kmwLength() <= config.leftContextCodePoints; + if(!this.startOfBuffer) { + // Our custom substring version will return the last n characters if param #1 is given -n. + this.left = this.left._kmwSubstr(-config.leftContextCodePoints); } - public toMock(): Mock { - let caretPos = this.left._kmwLength(); - - return new Mock(this.left + (this.right || ""), caretPos); + this.right = mock.getTextAfterCaret(); + this.endOfBuffer = this.right._kmwLength() <= config.rightContextCodePoints; + if(!this.endOfBuffer) { + this.right = this.right._kmwSubstr(0, config.rightContextCodePoints); } + + this.casingForm = + layerId == 'shift' ? 'initial' : + layerId == 'caps' ? 'upper' : + null; + } + + public toMock(): Mock { + let caretPos = this.left._kmwLength(); + + return new Mock(this.left + (this.right || ""), caretPos); } } \ No newline at end of file diff --git a/common/web/input-processor/src/text/inputProcessor.ts b/common/web/input-processor/src/text/inputProcessor.ts index c2cf2aeed866753262c1db3865bc0319aa613afe..51516882a4e34b01d8a6fdf258f12a105f288e0e 100644 --- a/common/web/input-processor/src/text/inputProcessor.ts +++ b/common/web/input-processor/src/text/inputProcessor.ts @@ -1,373 +1,359 @@ // Defines a 'polyfill' of sorts for NPM's events module -/// -/// -/// -/// - -namespace com.keyman.text { - export class InputProcessor { - public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { - baseLayout: 'us' - } - /** - * Indicates the device (platform) to be used for non-keystroke events, - * such as those sent to `begin postkeystroke` and `begin newcontext` - * entry points. - */ - private contextDevice: utils.DeviceSpec; - private kbdProcessor: KeyboardProcessor; - private lngProcessor: prediction.LanguageProcessor; - - constructor(device: utils.DeviceSpec, options?: ProcessorInitOptions) { - if(!device) { - throw new Error('device must be defined'); - } +/// - if(!options) { - options = InputProcessor.DEFAULT_OPTIONS; - } +import ContextWindow from "./contextWindow.js"; +import LanguageProcessor from "./prediction/languageProcessor.js"; +import type ModelSpec from "./prediction/modelSpec.js"; +import { globalObject, DeviceSpec } from "@keymanapp/web-utils"; + +import { + type Alternate, + Codes, + isEmptyTransform, + type Keyboard, + KeyboardInterface, + KeyboardProcessor, + type KeyEvent, + Mock, + type OutputTarget, + type ProcessorInitOptions, + RuleBehavior, + SystemStoreIDs, + type TextTransform +} from "@keymanapp/keyboard-processor"; + +export default class InputProcessor { + public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { + baseLayout: 'us' + } - this.contextDevice = device; - this.kbdProcessor = new KeyboardProcessor(device, options); - this.lngProcessor = new prediction.LanguageProcessor(); + /** + * Indicates the device (platform) to be used for non-keystroke events, + * such as those sent to `begin postkeystroke` and `begin newcontext` + * entry points. + */ + private contextDevice: DeviceSpec; + private kbdProcessor: KeyboardProcessor; + private lngProcessor: LanguageProcessor; + + constructor(device: DeviceSpec, predictiveTextWorker: Worker, options?: ProcessorInitOptions) { + if(!device) { + throw new Error('device must be defined'); } - public get languageProcessor(): prediction.LanguageProcessor { - return this.lngProcessor; + if(!options) { + options = InputProcessor.DEFAULT_OPTIONS; } - public get keyboardProcessor(): KeyboardProcessor { - return this.kbdProcessor; - } + this.contextDevice = device; + this.kbdProcessor = new KeyboardProcessor(device, options); + this.lngProcessor = new LanguageProcessor(predictiveTextWorker); + } - public get keyboardInterface(): text.KeyboardInterface { - return this.keyboardProcessor.keyboardInterface; - } + public get languageProcessor(): LanguageProcessor { + return this.lngProcessor; + } - public get activeKeyboard(): keyboards.Keyboard { - return this.keyboardInterface.activeKeyboard; - } + public get keyboardProcessor(): KeyboardProcessor { + return this.kbdProcessor; + } + + public get keyboardInterface(): KeyboardInterface { + return this.keyboardProcessor.keyboardInterface; + } - public set activeKeyboard(keyboard: keyboards.Keyboard) { - this.keyboardInterface.activeKeyboard = keyboard; + public get activeKeyboard(): Keyboard { + return this.keyboardInterface.activeKeyboard; + } - // All old deadkeys and keyboard-specific cache should immediately be invalidated - // on a keyboard change. - this.resetContext(); - } + public set activeKeyboard(keyboard: Keyboard) { + this.keyboardInterface.activeKeyboard = keyboard; - public get activeModel(): prediction.ModelSpec { - return this.languageProcessor.activeModel; - } + // All old deadkeys and keyboard-specific cache should immediately be invalidated + // on a keyboard change. + this.resetContext(); + } - /** - * Tell the currently active keyboard that a new context has been selected, - * e.g. by focus change, selection change, keyboard change, etc. - * - * @param {Object} outputTarget The OutputTarget that has focus - * @returns {Object} A RuleBehavior object describing the cumulative effects of - * all matched keyboard rules - */ - processNewContextEvent(outputTarget: OutputTarget): RuleBehavior { - const ruleBehavior = this.keyboardProcessor.processNewContextEvent(this.contextDevice, outputTarget); - - if(ruleBehavior) { - ruleBehavior.finalize(this.keyboardProcessor, outputTarget, true); - } - return ruleBehavior; - } + public get activeModel(): ModelSpec { + return this.languageProcessor.activeModel; + } - /** - * Simulate a keystroke according to the touched keyboard button element - * - * Handles default output and keyboard processing for both OSK and physical keystrokes. - * - * @param {Object} keyEvent The abstracted KeyEvent to use for keystroke processing - * @param {Object} outputTarget The OutputTarget receiving the KeyEvent - * @returns {Object} A RuleBehavior object describing the cumulative effects of - * all matched keyboard rules. - */ - processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { - const kbdMismatch = keyEvent.srcKeyboard && this.activeKeyboard != keyEvent.srcKeyboard; - const trueActiveKeyboard = this.activeKeyboard; - - try { - if(kbdMismatch) { - // Avoid force-reset of context per our setter above. - this.keyboardInterface.activeKeyboard = keyEvent.srcKeyboard; - } + /** + * Simulate a keystroke according to the touched keyboard button element + * + * Handles default output and keyboard processing for both OSK and physical keystrokes. + * + * @param {Object} keyEvent The abstracted KeyEvent to use for keystroke processing + * @param {Object} outputTarget The OutputTarget receiving the KeyEvent + * @returns {Object} A RuleBehavior object describing the cumulative effects of + * all matched keyboard rules. + */ + processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { + const kbdMismatch = keyEvent.srcKeyboard && this.activeKeyboard != keyEvent.srcKeyboard; + const trueActiveKeyboard = this.activeKeyboard; + + try { + if(kbdMismatch) { + // Avoid force-reset of context per our setter above. + this.keyboardInterface.activeKeyboard = keyEvent.srcKeyboard; + } - return this._processKeyEvent(keyEvent, outputTarget); - } finally { - if(kbdMismatch) { - // Restore our "current" activeKeyboard to its setting before the mismatching KeyEvent. - this.keyboardInterface.activeKeyboard = trueActiveKeyboard; - } + return this._processKeyEvent(keyEvent, outputTarget); + } finally { + if(kbdMismatch) { + // Restore our "current" activeKeyboard to its setting before the mismatching KeyEvent. + this.keyboardInterface.activeKeyboard = trueActiveKeyboard; } } + } - /** - * Acts as the core of `processKeyEvent` once we're comfortable asserting that the incoming - * keystroke matches the current `activeKeyboard`. - * @param keyEvent - * @param outputTarget - * @returns - */ - private _processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { - let formFactor = keyEvent.device.formFactor; - let fromOSK = keyEvent.isSynthetic; - - // The default OSK layout for desktop devices does not include nextlayer info, relying on modifier detection here. - // It's the OSK equivalent to doModifierPress on 'desktop' form factors. - if((formFactor == utils.FormFactor.Desktop || !this.activeKeyboard || this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device)) && fromOSK) { - // If it's a desktop OSK style and this triggers a layer change, - // a modifier key was clicked. No output expected, so it's safe to instantly exit. - if(this.keyboardProcessor.selectLayer(keyEvent)) { - return new RuleBehavior(); - } + /** + * Acts as the core of `processKeyEvent` once we're comfortable asserting that the incoming + * keystroke matches the current `activeKeyboard`. + * @param keyEvent + * @param outputTarget + * @returns + */ + private _processKeyEvent(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { + let formFactor = keyEvent.device.formFactor; + let fromOSK = keyEvent.isSynthetic; + + // The default OSK layout for desktop devices does not include nextlayer info, relying on modifier detection here. + // It's the OSK equivalent to doModifierPress on 'desktop' form factors. + if((formFactor == DeviceSpec.FormFactor.Desktop || !this.activeKeyboard || this.activeKeyboard.usesDesktopLayoutOnDevice(keyEvent.device)) && fromOSK) { + // If it's a desktop OSK style and this triggers a layer change, + // a modifier key was clicked. No output expected, so it's safe to instantly exit. + if(this.keyboardProcessor.selectLayer(keyEvent)) { + return new RuleBehavior(); } + } - // Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version - // of state management. `doModifierPress` must always run. - if(this.keyboardProcessor.doModifierPress(keyEvent, outputTarget, !fromOSK)) { - // If run on a desktop platform, we know that modifier & state key presses may not - // produce output, so we may make an immediate return safely. - if(!fromOSK) { - return new RuleBehavior(); - } + // Will handle keystroke-based non-layer change modifier & state keys, mapping them through the physical keyboard's version + // of state management. `doModifierPress` must always run. + if(this.keyboardProcessor.doModifierPress(keyEvent, outputTarget, !fromOSK)) { + // If run on a desktop platform, we know that modifier & state key presses may not + // produce output, so we may make an immediate return safely. + if(!fromOSK) { + return new RuleBehavior(); } + } - // If suggestions exist AND space is pressed, accept the suggestion and do not process the keystroke. - // If a suggestion was just accepted AND backspace is pressed, revert the change and do not process the backspace. - // We check the first condition here, while the prediction UI handles the second through the try__() methods below. - if(this.languageProcessor.isActive) { - // The following code relies on JS's logical operator "short-circuit" properties to prevent unwanted triggering of the second condition. - - // Can the suggestion UI revert a recent suggestion? If so, do that and swallow the backspace. - if((keyEvent.kName == "K_BKSP" || keyEvent.Lcode == Codes.keyCodes["K_BKSP"]) && this.languageProcessor.tryRevertSuggestion()) { - return new RuleBehavior(); - // Can the suggestion UI accept an existing suggestion? If so, do that and swallow the space character. - } else if((keyEvent.kName == "K_SPACE" || keyEvent.Lcode == Codes.keyCodes["K_SPACE"]) && this.languageProcessor.tryAcceptSuggestion('space')) { - return new RuleBehavior(); - } + // If suggestions exist AND space is pressed, accept the suggestion and do not process the keystroke. + // If a suggestion was just accepted AND backspace is pressed, revert the change and do not process the backspace. + // We check the first condition here, while the prediction UI handles the second through the try__() methods below. + if(this.languageProcessor.isActive) { + // The following code relies on JS's logical operator "short-circuit" properties to prevent unwanted triggering of the second condition. + + // Can the suggestion UI revert a recent suggestion? If so, do that and swallow the backspace. + if((keyEvent.kName == "K_BKSP" || keyEvent.Lcode == Codes.keyCodes["K_BKSP"]) && this.languageProcessor.tryRevertSuggestion()) { + return new RuleBehavior(); + // Can the suggestion UI accept an existing suggestion? If so, do that and swallow the space character. + } else if((keyEvent.kName == "K_SPACE" || keyEvent.Lcode == Codes.keyCodes["K_SPACE"]) && this.languageProcessor.tryAcceptSuggestion('space')) { + return new RuleBehavior(); } + } - // // ...end I3363 (Build 301) + // // ...end I3363 (Build 301) - // Create a "mock" backup of the current outputTarget in its pre-input state. - // Current, long-existing assumption - it's DOM-backed. - let preInputMock = Mock.from(outputTarget, true); + // Create a "mock" backup of the current outputTarget in its pre-input state. + // Current, long-existing assumption - it's DOM-backed. + let preInputMock = Mock.from(outputTarget, true); - const startingLayerId = this.keyboardProcessor.layerId; + const startingLayerId = this.keyboardProcessor.layerId; - // We presently need the true keystroke to run on the FULL context. That index is still - // needed for some indexing operations when comparing two different output targets. - let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, outputTarget); + // We presently need the true keystroke to run on the FULL context. That index is still + // needed for some indexing operations when comparing two different output targets. + let ruleBehavior = this.keyboardProcessor.processKeystroke(keyEvent, outputTarget); - // Swap layer as appropriate. - if(keyEvent.kNextLayer) { - this.keyboardProcessor.selectLayer(keyEvent); - } + // Swap layer as appropriate. + if(keyEvent.kNextLayer) { + this.keyboardProcessor.selectLayer(keyEvent); + } - // If it's a key that we 'optimize out' of our fat-finger correction algorithm, - // we MUST NOT trigger it for this keystroke. - let isOnlyLayerSwitchKey = text.Codes.isKnownOSKModifierKey(keyEvent.kName); - - // Best-guess stopgap for possible custom modifier keys. - // If a key (1) does not affect the context and (2) shifts the active layer, - // we assume it's a modifier key. (Touch keyboards may define custom modifier keys.) - // - // Note: this will mean we won't generate alternates in the niche scenario where: - // 1. Keypress does not alter the actual context - // 2. It DOES emit a deadkey with an earlier processing rule. - // 3. The FINAL processing rule does not match. - // 4. The key ALSO signals a layer shift. - // If any of the four above conditions aren't met - no problem! - // So it's a pretty niche scenario. - if((ruleBehavior?.transcription?.transform as TextTransform)?.isNoOp() && keyEvent.kNextLayer) { - isOnlyLayerSwitchKey = true; - } + // If it's a key that we 'optimize out' of our fat-finger correction algorithm, + // we MUST NOT trigger it for this keystroke. + let isOnlyLayerSwitchKey = Codes.isKnownOSKModifierKey(keyEvent.kName); + + // Best-guess stopgap for possible custom modifier keys. + // If a key (1) does not affect the context and (2) shifts the active layer, + // we assume it's a modifier key. (Touch keyboards may define custom modifier keys.) + // + // Note: this will mean we won't generate alternates in the niche scenario where: + // 1. Keypress does not alter the actual context + // 2. It DOES emit a deadkey with an earlier processing rule. + // 3. The FINAL processing rule does not match. + // 4. The key ALSO signals a layer shift. + // If any of the four above conditions aren't met - no problem! + // So it's a pretty niche scenario. + + if(isEmptyTransform(ruleBehavior?.transcription?.transform) && keyEvent.kNextLayer) { + isOnlyLayerSwitchKey = true; + } - const keepRuleBehavior = ruleBehavior != null; - // Should we swallow any further processing of keystroke events for this keydown-keypress sequence? - if(keepRuleBehavior) { - // alternates are our fat-finger alternate outputs. We don't build these for keys we detect as - // layer switch keys - let alternates = isOnlyLayerSwitchKey ? null : this.buildAlternates(ruleBehavior, keyEvent, preInputMock); + const keepRuleBehavior = ruleBehavior != null; + // Should we swallow any further processing of keystroke events for this keydown-keypress sequence? + if(keepRuleBehavior) { + // alternates are our fat-finger alternate outputs. We don't build these for keys we detect as + // layer switch keys + let alternates = isOnlyLayerSwitchKey ? null : this.buildAlternates(ruleBehavior, keyEvent, preInputMock); - // Now that we've done all the keystroke processing needed, ensure any extra effects triggered - // by the actual keystroke occur. - ruleBehavior.finalize(this.keyboardProcessor, outputTarget, false); + // Now that we've done all the keystroke processing needed, ensure any extra effects triggered + // by the actual keystroke occur. + ruleBehavior.finalize(this.keyboardProcessor, outputTarget, false); - // -- All keystroke (and 'alternate') processing is now complete. Time to finalize everything! -- + // -- All keystroke (and 'alternate') processing is now complete. Time to finalize everything! -- - // Notify the ModelManager of new input - it's predictive text time! - if(alternates && alternates.length > 0) { - ruleBehavior.transcription.alternates = alternates; - } - } else { - // We need a dummy RuleBehavior for keys which have no output (e.g. Shift) - ruleBehavior = new RuleBehavior(); - ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false); - ruleBehavior.triggersDefaultCommand = true; + // Notify the ModelManager of new input - it's predictive text time! + if(alternates && alternates.length > 0) { + ruleBehavior.transcription.alternates = alternates; } + } else { + // We need a dummy RuleBehavior for keys which have no output (e.g. Shift) + ruleBehavior = new RuleBehavior(); + ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false); + ruleBehavior.triggersDefaultCommand = true; + } - // The keyboard may want to take an action after all other keystroke processing is - // finished, for example to switch layers. This action may not have any output - // but may change system store or variable store values. Given this, we don't need to - // save anything about the post behavior, after finalizing it + // The keyboard may want to take an action after all other keystroke processing is + // finished, for example to switch layers. This action may not have any output + // but may change system store or variable store values. Given this, we don't need to + // save anything about the post behavior, after finalizing it - // We need to tell the keyboard if the layer has been changed, either by a keyboard rule itself, - // or by the touch layout 'nextlayer' control. - const hasLayerChanged = ruleBehavior.setStore[KeyboardInterface.TSS_LAYER] || keyEvent.kNextLayer; - this.keyboardProcessor.newLayerStore.set(hasLayerChanged ? this.keyboardProcessor.layerId : ''); - this.keyboardProcessor.oldLayerStore.set(hasLayerChanged ? startingLayerId : ''); + // We need to tell the keyboard if the layer has been changed, either by a keyboard rule itself, + // or by the touch layout 'nextlayer' control. + const hasLayerChanged = ruleBehavior.setStore[SystemStoreIDs.TSS_LAYER] || keyEvent.kNextLayer; + this.keyboardProcessor.newLayerStore.set(hasLayerChanged ? this.keyboardProcessor.layerId : ''); + this.keyboardProcessor.oldLayerStore.set(hasLayerChanged ? startingLayerId : ''); - let postRuleBehavior = this.keyboardProcessor.processPostKeystroke(this.contextDevice, outputTarget); - if(postRuleBehavior) { - postRuleBehavior.finalize(this.keyboardProcessor, outputTarget, true); - } - - // Yes, even for ruleBehavior.triggersDefaultCommand. Those tend to change the context. - ruleBehavior.predictionPromise = this.languageProcessor.predict(ruleBehavior.transcription, this.keyboardProcessor.layerId); + let postRuleBehavior = this.keyboardProcessor.processPostKeystroke(this.contextDevice, outputTarget); + if(postRuleBehavior) { + postRuleBehavior.finalize(this.keyboardProcessor, outputTarget, true); + } - // Text did not change (thus, no text "input") if we tabbed or merely moved the caret. - if(!ruleBehavior.triggersDefaultCommand) { - // For DOM-aware targets, this will trigger a DOM event page designers may listen for. - outputTarget.doInputEvent(); - } + // Yes, even for ruleBehavior.triggersDefaultCommand. Those tend to change the context. + ruleBehavior.predictionPromise = this.languageProcessor.predict(ruleBehavior.transcription, this.keyboardProcessor.layerId); - return keepRuleBehavior ? ruleBehavior : null; + // Text did not change (thus, no text "input") if we tabbed or merely moved the caret. + if(!ruleBehavior.triggersDefaultCommand) { + // For DOM-aware targets, this will trigger a DOM event page designers may listen for. + outputTarget.doInputEvent(); } - private buildAlternates(ruleBehavior: RuleBehavior, keyEvent: KeyEvent, preInputMock: Mock): Alternate[] { - let alternates: Alternate[]; - - // If we're performing a 'default command', it's not a standard 'typing' event - don't do fat-finger stuff. - // Also, don't do fat-finger stuff if predictive text isn't enabled. - if(this.languageProcessor.isActive && !ruleBehavior.triggersDefaultCommand) { - let keyDistribution = keyEvent.keyDistribution; - - // We don't need to track absolute indexing during alternate-generation; - // only position-relative, so it's better to use a sliding window for context - // when making alternates. (Slightly worse for short text, matters greatly - // for long text.) - let contextWindow = new ContextWindow(preInputMock, ContextWindow.ENGINE_RULE_WINDOW, this.keyboardProcessor.layerId); - let windowedMock = contextWindow.toMock(); - - // Note - we don't yet do fat-fingering with longpress keys. - if(keyDistribution && keyEvent.kbdLayer) { - // Tracks a 'deadline' for fat-finger ops, just in case both context is long enough - // and device is slow enough that the calculation takes too long. - // - // Consider use of https://developer.mozilla.org/en-US/docs/Web/API/Performance/now instead? - // Would allow finer-tuned control. - let TIMEOUT_THRESHOLD: number = Number.MAX_VALUE; - let _globalThis = com.keyman.utils.getGlobalObject(); - let timer: () => number; - - // Available by default on `window` in browsers, but _not_ on `global` in Node, - // surprisingly. Since we can't use code dependent on `require` statements - // at present, we have to condition upon it actually existing. - if(_globalThis['performance'] && _globalThis['performance']['now']) { - timer = function() { - return _globalThis['performance']['now'](); - }; - - TIMEOUT_THRESHOLD = timer() + 16; // + 16ms. - } // else { - // We _could_ just use Date.now() as a backup... but that (probably) only matters - // when unit testing. So... we actually don't _need_ time thresholding when in - // a Node environment. - // } - - // Tracks a minimum probability for keystroke probability. Anything less will not be - // included in alternate calculations. - // - // Seek to match SearchSpace.EDIT_DISTANCE_COST_SCALE from the predictive-text engine. - // Reasoning for the selected value may be seen there. Short version - keystrokes - // that _appear_ very precise may otherwise not even consider directly-neighboring keys. - let KEYSTROKE_EPSILON = Math.exp(-5); - - // Sort the distribution into probability-descending order. - keyDistribution.sort((a, b) => b.p - a.p); - - let activeLayout = this.activeKeyboard.layout(keyEvent.device.formFactor); - alternates = []; - - let totalMass = 0; // Tracks sum of non-error probabilities. - for(let pair of keyDistribution) { - if(pair.p < KEYSTROKE_EPSILON) { - totalMass += pair.p; - break; - } else if(timer && timer() >= TIMEOUT_THRESHOLD) { - // Note: it's always possible that the thread _executing_ our JS - // got paused by the OS, even if JS itself is single-threaded. - // - // The case where `alternates` is initialized (line 167) but empty - // (because of net-zero loop iterations) MUST be handled. - break; - } - - let mock = Mock.from(windowedMock, false); - - let altKey = activeLayout.getLayer(keyEvent.kbdLayer).getKey(pair.keyId); - if(!altKey) { - console.warn("Potential fat-finger key could not be found in layer!"); - continue; - } - - let altEvent = altKey.constructKeyEvent(this.keyboardProcessor, keyEvent.device); - let alternateBehavior = this.keyboardProcessor.processKeystroke(altEvent, mock); - - // If alternateBehavior.beep == true, ignore it. It's a disallowed key sequence, - // so we expect users to never intend their use. + return keepRuleBehavior ? ruleBehavior : null; + } + + private buildAlternates(ruleBehavior: RuleBehavior, keyEvent: KeyEvent, preInputMock: Mock): Alternate[] { + let alternates: Alternate[]; + + // If we're performing a 'default command', it's not a standard 'typing' event - don't do fat-finger stuff. + // Also, don't do fat-finger stuff if predictive text isn't enabled. + if(this.languageProcessor.isActive && !ruleBehavior.triggersDefaultCommand) { + let keyDistribution = keyEvent.keyDistribution; + + // We don't need to track absolute indexing during alternate-generation; + // only position-relative, so it's better to use a sliding window for context + // when making alternates. (Slightly worse for short text, matters greatly + // for long text.) + let contextWindow = new ContextWindow(preInputMock, ContextWindow.ENGINE_RULE_WINDOW, this.keyboardProcessor.layerId); + let windowedMock = contextWindow.toMock(); + + // Note - we don't yet do fat-fingering with longpress keys. + if(this.languageProcessor.isActive && keyDistribution && keyEvent.kbdLayer) { + // Tracks a 'deadline' for fat-finger ops, just in case both context is long enough + // and device is slow enough that the calculation takes too long. + // + // Consider use of https://developer.mozilla.org/en-US/docs/Web/API/Performance/now instead? + // Would allow finer-tuned control. + let TIMEOUT_THRESHOLD: number = Number.MAX_VALUE; + let _globalThis = globalObject(); + let timer: () => number; + + // Available by default on `window` in browsers, but _not_ on `global` in Node, + // surprisingly. Since we can't use code dependent on `require` statements + // at present, we have to condition upon it actually existing. + if(_globalThis['performance'] && _globalThis['performance']['now']) { + timer = function() { + return _globalThis['performance']['now'](); + }; + + TIMEOUT_THRESHOLD = timer() + 16; // + 16ms. + } // else { + // We _could_ just use Date.now() as a backup... but that (probably) only matters + // when unit testing. So... we actually don't _need_ time thresholding when in + // a Node environment. + // } + + // Tracks a minimum probability for keystroke probability. Anything less will not be + // included in alternate calculations. + // + // Seek to match SearchSpace.EDIT_DISTANCE_COST_SCALE from the predictive-text engine. + // Reasoning for the selected value may be seen there. Short version - keystrokes + // that _appear_ very precise may otherwise not even consider directly-neighboring keys. + let KEYSTROKE_EPSILON = Math.exp(-5); + + // Sort the distribution into probability-descending order. + keyDistribution.sort((a, b) => b.p - a.p); + + let activeLayout = this.activeKeyboard.layout(keyEvent.device.formFactor); + alternates = []; + + let totalMass = 0; // Tracks sum of non-error probabilities. + for(let pair of keyDistribution) { + if(pair.p < KEYSTROKE_EPSILON) { + totalMass += pair.p; + break; + } else if(timer && timer() >= TIMEOUT_THRESHOLD) { + // Note: it's always possible that the thread _executing_ our JS + // got paused by the OS, even if JS itself is single-threaded. // - // Also possible that this set of conditions fail for all evaluated alternates. - if(alternateBehavior && !alternateBehavior.beep && pair.p > 0) { - let transform: Transform = alternateBehavior.transcription.transform; - - // Ensure that the alternate's token id matches that of the current keystroke, as we only - // record the matched rule's context (since they match) - transform.id = ruleBehavior.transcription.token; - alternates.push({sample: transform, 'p': pair.p}); - totalMass += pair.p; - } + // The case where `alternates` is initialized (line 167) but empty + // (because of net-zero loop iterations) MUST be handled. + break; } - // Renormalizes the distribution, as any error (beep) results - // will result in a distribution that doesn't sum to 1 otherwise. - // All `.p` values are strictly positive, so totalMass is - // guaranteed to be > 0 if the array has entries. - alternates.forEach(function(alt) { - alt.p /= totalMass; - }); - } - } - return alternates; - } + let mock = Mock.from(windowedMock, false); - public resetContext(outputTarget?: OutputTarget) { - this.keyboardProcessor.resetContext(); - this.languageProcessor.invalidateContext(outputTarget, this.keyboardProcessor.layerId); + let altKey = activeLayout.getLayer(keyEvent.kbdLayer).getKey(pair.keyId); + if(!altKey) { + console.warn("Potential fat-finger key could not be found in layer!"); + continue; + } + + let altEvent = this.keyboardProcessor.activeKeyboard.constructKeyEvent(altKey, keyEvent.device, this.keyboardProcessor.stateKeys); + let alternateBehavior = this.keyboardProcessor.processKeystroke(altEvent, mock); - // Let the keyboard do its initial group processing - //console.log('processNewContextEvent called from resetContext'); - if(outputTarget) { - this.processNewContextEvent(outputTarget); + // If alternateBehavior.beep == true, ignore it. It's a disallowed key sequence, + // so we expect users to never intend their use. + // + // Also possible that this set of conditions fail for all evaluated alternates. + if(alternateBehavior && !alternateBehavior.beep && pair.p > 0) { + let transform: Transform = alternateBehavior.transcription.transform; + + // Ensure that the alternate's token id matches that of the current keystroke, as we only + // record the matched rule's context (since they match) + transform.id = ruleBehavior.transcription.token; + alternates.push({sample: transform, 'p': pair.p}); + totalMass += pair.p; + } + } + + // Renormalizes the distribution, as any error (beep) results + // will result in a distribution that doesn't sum to 1 otherwise. + // All `.p` values are strictly positive, so totalMass is + // guaranteed to be > 0 if the array has entries. + alternates.forEach(function(alt) { + alt.p /= totalMass; + }); } } + return alternates; } -} - -(function () { - let ns = com.keyman.text; - // Let the InputProcessor be available both in the browser and in Node. - if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = ns.InputProcessor; - //@ts-ignore - ns.InputProcessor.com = com; // Export the root namespace so that all InputProcessor classes are accessible by unit tests. + public resetContext(outputTarget?: OutputTarget) { + // Also handles new-context events, which may modify the layer + this.keyboardProcessor.resetContext(outputTarget); + // With the layer now set, we trigger new predictions. + this.languageProcessor.invalidateContext(outputTarget, this.keyboardProcessor.layerId); } -}()); \ No newline at end of file +} \ No newline at end of file diff --git a/common/web/input-processor/src/text/prediction/languageProcessor.ts b/common/web/input-processor/src/text/prediction/languageProcessor.ts index 12511cfb9f75f245b387ce1eb63b6a7bfd9a52f3..58c6e4299327f3d7f1217c15b83c000993097261 100644 --- a/common/web/input-processor/src/text/prediction/languageProcessor.ts +++ b/common/web/input-processor/src/text/prediction/languageProcessor.ts @@ -1,288 +1,225 @@ -/// -/// - -namespace com.keyman.text.prediction { - export interface ModelSpec { - /** - * The model's unique identifier. - */ - id: string; - - /** - * The list of supported BCP-47 language codes. Only one language should be supported, - * although multiple variants based on region code (at min) may be specified. - */ - languages: string[]; - - /** - * The path/URL to the file that defines the model. If both `path` and `raw` are specified, - * `path` takes precedence. - */ - path: string; - - /** - * The raw JS script defining the model. Only used if `path` is not specified. - */ - code: string; +import EventEmitter from "eventemitter3"; +import { LMLayer } from "@keymanapp/lexical-model-layer/web"; +import { OutputTarget, Transcription, Mock } from "@keymanapp/keyboard-processor"; +import ContextWindow from "../contextWindow.js"; +import ModelSpec from "./modelSpec.js" + +/** + * Corresponds to the 'suggestionsready' LanguageProcessor event. + */ +export type ReadySuggestionsHandler = (prediction: ReadySuggestions) => boolean; + +export type StateChangeEnum = 'active'|'configured'|'inactive'; +/** + * Corresponds to the 'statechange' LanguageProcessor event. + */ +export type StateChangeHandler = (state: StateChangeEnum) => any; + +/** + * Covers 'tryaccept' events. + */ +export type TryUIHandler = (source: string) => boolean; + +export type InvalidateSourceEnum = 'new'|'context'; + +/** + * Corresponds to the 'invalidatesuggestions' LanguageProcessor event. + */ +export type InvalidateSuggestionsHandler = (source: InvalidateSourceEnum) => boolean; + +export class ReadySuggestions { + suggestions: Suggestion[]; + transcriptionID: number; + + constructor(suggestions: Suggestion[], id: number) { + this.suggestions = suggestions; + this.transcriptionID = id; } +} - /** - * Corresponds to the 'suggestionsready' LanguageProcessor event. - */ - export type ReadySuggestionsHandler = (prediction: ReadySuggestions) => boolean; +interface LanguageProcessorEventMap { + 'suggestionsready': ReadySuggestionsHandler, + 'invalidatesuggestions': InvalidateSuggestionsHandler, + 'statechange': StateChangeHandler, + 'tryaccept': TryUIHandler, + 'tryrevert': () => void, - export type StateChangeEnum = 'active'|'configured'|'inactive'; /** - * Corresponds to the 'statechange' LanguageProcessor event. + * Is called synchronously once suggestion application is successful and the context has been updated. + * + * @param outputTarget The `OutputTarget` representation of the context the suggestion was applied to. + * @returns */ - export type StateChangeHandler = (state: StateChangeEnum) => boolean; + 'suggestionapplied': (outputTarget: OutputTarget) => boolean +} - /** - * Covers both 'tryaccept' and 'tryrevert' events. - */ - export type TryUIHandler = (source: string) => boolean; +/* Is more like the model configuration engine */ +export default class LanguageProcessor extends EventEmitter { + private lmEngine: LMLayer; + private currentModel?: ModelSpec; + private configuration?: Configuration; + private currentPromise?: Promise; - export type InvalidateSourceEnum = 'new'|'context'; + private recentTranscriptions: Transcription[] = []; - /** - * Corresponds to the 'invalidatesuggestions' LanguageProcessor event. - */ - export type InvalidateSuggestionsHandler = (source: InvalidateSourceEnum) => boolean; + private _mayPredict: boolean = true; + private _mayCorrect: boolean = true; - export class ReadySuggestions { - suggestions: Suggestion[]; - transcriptionID: number; + private _state: StateChangeEnum = 'inactive'; - constructor(suggestions: Suggestion[], id: number) { - this.suggestions = suggestions; - this.transcriptionID = id; - } - } + private static readonly TRANSCRIPTION_BUFFER: 10 = 10; - type SupportedEventNames = "suggestionsready" | "invalidatesuggestions" | "statechange" | "tryaccept" | "tryrevert"; - type SupportedEventHandler = InvalidateSuggestionsHandler | ReadySuggestionsHandler | StateChangeHandler | TryUIHandler; + public constructor(predictiveTextWorker: Worker, supportsRightDeletions: boolean = false) { + super(); - export class LanguageProcessor extends EventEmitter { - private lmEngine: LMLayer; - private currentModel?: ModelSpec; - private configuration?: Configuration; - private currentPromise?: Promise; + // Establishes KMW's platform 'capabilities', which limit the range of context a LMLayer + // model may expect. + let capabilities: Capabilities = { + maxLeftContextCodePoints: 64, + // Since the apps don't yet support right-deletions. + maxRightContextCodePoints: supportsRightDeletions ? 0 : 64 + } - private recentTranscriptions: Transcription[] = []; + if(!predictiveTextWorker) { + return; + } - private _mayPredict: boolean = true; - private _mayCorrect: boolean = true; + this.lmEngine = new LMLayer(capabilities, predictiveTextWorker); + } - private static readonly TRANSCRIPTION_BUFFER: 10 = 10; + public get activeModel(): ModelSpec { + return this.currentModel; + } - public init(supportsRightDeletions: boolean = false) { - // Establishes KMW's platform 'capabilities', which limit the range of context a LMLayer - // model may expect. - let capabilities: Capabilities = { - maxLeftContextCodePoints: 64, - // Since the apps don't yet support right-deletions. - maxRightContextCodePoints: supportsRightDeletions ? 0 : 64 - } + public get isConfigured(): boolean { + return !!this.configuration; + } - if(!this.canEnable()) { - return; - } + public get state(): StateChangeEnum { + return this._state; + } - this.lmEngine = new LMLayer(capabilities); - } + public unloadModel() { + this.lmEngine.unloadModel(); + delete this.currentModel; + delete this.configuration; - public get activeModel(): ModelSpec { - return this.currentModel; + this._state = 'inactive'; + this.emit('statechange', 'inactive'); + } + + loadModel(model: ModelSpec): Promise { + if(!model) { + throw new Error("Null reference not allowed."); } - public unloadModel() { - this.lmEngine.unloadModel(); - delete this.currentModel; - delete this.configuration; + let specType: 'file'|'raw' = model.path ? 'file' : 'raw'; + let source = specType == 'file' ? model.path : model.code; - this.emit('statechange', 'inactive'); + // We pre-emptively emit so that the banner's DOM elements may update synchronously. + // Prevents an ugly "flash of unstyled content" layout issue during keyboard load + // on our mobile platforms when embedded. + this.currentModel = model; + if(this.mayPredict) { + this._state = 'active'; + this.emit('statechange', 'active'); } - loadModel(model: ModelSpec): Promise { - if(!model) { - throw new Error("Null reference not allowed."); + return this.lmEngine.loadModel(source, specType).then((config: Configuration) => { + this.configuration = config; + this._state = 'configured'; + this.emit('statechange', 'configured'); + }).catch((error) => { + // Does this provide enough logging information? + let message: string; + if(error instanceof Error) { + message = error.message; + } else { + message = String(error); } + console.error("Could not load model '" + model.id + "': " + message); - let specType: 'file'|'raw' = model.path ? 'file' : 'raw'; - let source = specType == 'file' ? model.path : model.code; - let lp = this; - - // We pre-emptively emit so that the banner's DOM elements may update synchronously. - // Prevents an ugly "flash of unstyled content" layout issue during keyboard load - // on our mobile platforms when embedded. - lp.currentModel = model; - if(this.mayPredict) { - lp.emit('statechange', 'active'); - } + // Since the model couldn't load, immediately deactivate. Visually, it'll look + // like the banner crashed shortly after load. + this.currentModel = null; + this._state = 'inactive'; + this.emit('statechange', 'inactive'); + }); + } - return this.lmEngine.loadModel(source, specType).then(function(config: Configuration) { - lp.configuration = config; - lp.emit('statechange', 'configured'); - }).catch(function(error) { - // Does this provide enough logging information? - let message: string; - if(error instanceof Error) { - message = error.message; - } else { - message = String(error); - } - console.error("Could not load model '" + model.id + "': " + message); + public invalidateContext(outputTarget: OutputTarget, layerId: string): Promise { + // Signal to any predictive text UI that the context has changed, invalidating recent predictions. + this.emit('invalidatesuggestions', 'context'); - // Since the model couldn't load, immediately deactivate. Visually, it'll look - // like the banner crashed shortly after load. - lp.currentModel = null; - lp.emit('statechange', 'inactive'); - }); + // If there's no active model, there can be no predictions. + // We'll also be missing important data needed to even properly REQUEST the predictions. + if(!this.currentModel || !this.configuration) { + return Promise.resolve([]); } - public invalidateContext(outputTarget: OutputTarget, layerId: string) { - // Signal to any predictive text UI that the context has changed, invalidating recent predictions. - this.emit('invalidatesuggestions', 'context'); - - // If there's no active model, there can be no predictions. - // We'll also be missing important data needed to even properly REQUEST the predictions. - if(!this.currentModel || !this.configuration) { - return; - } - - // Don't attempt predictions when disabled! - // invalidateContext otherwise bypasses .predict()'s check against this. - if(!this.isActive) { - return; - } else if(outputTarget) { - let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false); - this.predict_internal(transcription, true, layerId); - } + // Don't attempt predictions when disabled! + // invalidateContext otherwise bypasses .predict()'s check against this. + if(!this.isActive) { + return Promise.resolve([]); + } else if(outputTarget) { + let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false); + return this.predict_internal(transcription, true, layerId); } + } - public wordbreak(target: OutputTarget, layerId: string): Promise { - if(!this.isActive) { - return null; - } - - let context = new ContextWindow(Mock.from(target, false), this.configuration, layerId); - return this.lmEngine.wordbreak(context); + public wordbreak(target: OutputTarget, layerId: string): Promise { + if(!this.isActive) { + return null; } - public predict(transcription: Transcription, layerId: string): Promise { - if(!this.isActive) { - return null; - } - - // If there's no active model, there can be no predictions. - // We'll also be missing important data needed to even properly REQUEST the predictions. - if(!this.currentModel || !this.configuration) { - return null; - } - - // We've already invalidated any suggestions resulting from any previously-existing Promise - - // may as well officially invalidate them via event. - this.emit("invalidatesuggestions", 'new'); + let context = new ContextWindow(Mock.from(target, false), this.configuration, layerId); + return this.lmEngine.wordbreak(context); + } - return this.predict_internal(transcription, false, layerId); + public predict(transcription: Transcription, layerId: string): Promise { + if(!this.isActive) { + return null; } - /** - * - * @param suggestion - * @param outputTarget - * @param getLayerId a function that returns the current layerId, - * required because layerid can be changed by PostKeystroke - * @returns - */ - public applySuggestion(suggestion: Suggestion, outputTarget: OutputTarget, getLayerId: ()=>string): Promise { - if(!outputTarget) { - throw "Accepting suggestions requires a destination OutputTarget instance." - } - - // Find the state of the context at the time the suggestion was generated. - // This may refer to the context before an input keystroke or before application - // of a predictive suggestion. - let original = this.getPredictionState(suggestion.transformId); - if(!original) { - console.warn("Could not apply the Suggestion!"); - return null; - } else { - // Apply the Suggestion! - - // Step 1: determine the final output text - let final = text.Mock.from(original.preInput, false); - final.apply(suggestion.transform); - - // Step 2: build a final, master Transform that will produce the desired results from the CURRENT state. - // In embedded mode, both Android and iOS are best served by calculating this transform and applying its - // values as needed for use with their IME interfaces. - let transform = final.buildTransformFrom(outputTarget); - outputTarget.apply(transform); - - // Tell the banner that a suggestion was applied, so it can call the - // keyboard's PostKeystroke entry point as needed - this.emit('suggestionapplied', outputTarget); - - // Build a 'reversion' Transcription that can be used to undo this apply() if needed, - // replacing the suggestion transform with the original input text. - let preApply = text.Mock.from(original.preInput, false); - preApply.apply(original.transform); - - // Builds the reversion option according to the loaded lexical model's known - // syntactic properties. - let suggestionContext = new ContextWindow(original.preInput, this.configuration, getLayerId()); - - // We must accept the Suggestion from its original context, which was before - // `original.transform` was applied. - let reversionPromise: Promise = this.lmEngine.acceptSuggestion(suggestion, suggestionContext, original.transform); - - // Also, request new prediction set based on the resulting context. - let lp = this; - reversionPromise = reversionPromise.then(function(reversion) { - let mappedReversion: Reversion = { - // By mapping back to the original Transcription that generated the Suggestion, - // the input will be automatically rewound to the preInput state. - transform: original.transform, - // The ID part is critical; the reversion can't be applied without it. - transformId: -original.token, // reversions use the additive inverse. - displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer. - id: reversion.id, - tag: reversion.tag - } - // // If using the version from lm-layer: - // let mappedReversion = reversion; - // mappedReversion.transformId = reversionTranscription.token; - lp.predictFromTarget(outputTarget, getLayerId()); - return mappedReversion; - }); - - return reversionPromise; - } + // If there's no active model, there can be no predictions. + // We'll also be missing important data needed to even properly REQUEST the predictions. + if(!this.currentModel || !this.configuration) { + return null; } - public applyReversion(reversion: Reversion, outputTarget: OutputTarget) { - if(!outputTarget) { - throw "Accepting suggestions requires a destination OutputTarget instance." - } + // We've already invalidated any suggestions resulting from any previously-existing Promise - + // may as well officially invalidate them via event. + this.emit("invalidatesuggestions", 'new'); - // Find the state of the context at the time the suggestion was generated. - // This may refer to the context before an input keystroke or before application - // of a predictive suggestion. - // - // Reversions use the additive inverse of the id token of the Transcription being - // reverted to. - let original = this.getPredictionState(-reversion.transformId); - if(!original) { - console.warn("Could not apply the Suggestion!"); - return; - } + return this.predict_internal(transcription, false, layerId); + } + + /** + * + * @param suggestion + * @param outputTarget + * @param getLayerId a function that returns the current layerId, + * required because layerid can be changed by PostKeystroke + * @returns + */ + public applySuggestion(suggestion: Suggestion, outputTarget: OutputTarget, getLayerId: ()=>string): Promise { + if(!outputTarget) { + throw "Accepting suggestions requires a destination OutputTarget instance." + } - // Apply the Reversion! + // Find the state of the context at the time the suggestion was generated. + // This may refer to the context before an input keystroke or before application + // of a predictive suggestion. + let original = this.getPredictionState(suggestion.transformId); + if(!original) { + console.warn("Could not apply the Suggestion!"); + return null; + } else { + // Apply the Suggestion! // Step 1: determine the final output text - let final = text.Mock.from(original.preInput, false); - final.apply(reversion.transform); // Should match original.transform, actually. (See applySuggestion) + let final = Mock.from(original.preInput, false); + final.apply(suggestion.transform); // Step 2: build a final, master Transform that will produce the desired results from the CURRENT state. // In embedded mode, both Android and iOS are best served by calculating this transform and applying its @@ -290,155 +227,237 @@ namespace com.keyman.text.prediction { let transform = final.buildTransformFrom(outputTarget); outputTarget.apply(transform); - // The reason we need to preserve the additive-inverse 'transformId' property on Reversions. - let promise = this.lmEngine.revertSuggestion(reversion, new ContextWindow(original.preInput, this.configuration, null)) - - let lp = this; - return promise.then(function(suggestions: Suggestion[]) { - let result = new ReadySuggestions(suggestions, transform.id); - lp.emit("suggestionsready", result); - lp.currentPromise = null; - - return suggestions; + // Tell the banner that a suggestion was applied, so it can call the + // keyboard's PostKeystroke entry point as needed + this.emit('suggestionapplied', outputTarget); + + // Build a 'reversion' Transcription that can be used to undo this apply() if needed, + // replacing the suggestion transform with the original input text. + let preApply = Mock.from(original.preInput, false); + preApply.apply(original.transform); + + // Builds the reversion option according to the loaded lexical model's known + // syntactic properties. + let suggestionContext = new ContextWindow(original.preInput, this.configuration, getLayerId()); + + // We must accept the Suggestion from its original context, which was before + // `original.transform` was applied. + let reversionPromise: Promise = this.lmEngine.acceptSuggestion(suggestion, suggestionContext, original.transform); + + // Also, request new prediction set based on the resulting context. + reversionPromise = reversionPromise.then((reversion) => { + let mappedReversion: Reversion = { + // By mapping back to the original Transcription that generated the Suggestion, + // the input will be automatically rewound to the preInput state. + transform: original.transform, + // The ID part is critical; the reversion can't be applied without it. + transformId: -original.token, // reversions use the additive inverse. + displayAs: reversion.displayAs, // The real reason we needed to call the LMLayer. + id: reversion.id, + tag: reversion.tag + } + // // If using the version from lm-layer: + // let mappedReversion = reversion; + // mappedReversion.transformId = reversionTranscription.token; + this.predictFromTarget(outputTarget, getLayerId()); + return mappedReversion; }); + + return reversionPromise; } + } - public predictFromTarget(outputTarget: OutputTarget, layerId: string): Promise { - if(!outputTarget) { - return null; - } + public applyReversion(reversion: Reversion, outputTarget: OutputTarget) { + if(!outputTarget) { + throw "Accepting suggestions requires a destination OutputTarget instance." + } - let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false); - return this.predict(transcription, layerId); + // Find the state of the context at the time the suggestion was generated. + // This may refer to the context before an input keystroke or before application + // of a predictive suggestion. + // + // Reversions use the additive inverse of the id token of the Transcription being + // reverted to. + let original = this.getPredictionState(-reversion.transformId); + if(!original) { + console.warn("Could not apply the Suggestion!"); + return; } - /** - * Called internally to do actual predictions after any relevant "invalidatesuggestions" events - * have been raised. - * @param transcription The triggering transcription (if it exists) - */ - private predict_internal(transcription: Transcription, resetContext: boolean, layerId: string): Promise { - if(!transcription) { - return null; - } + // Apply the Reversion! - let context = new ContextWindow(transcription.preInput, this.configuration, layerId); - this.recordTranscription(transcription); + // Step 1: determine the final output text + let final = Mock.from(original.preInput, false); + final.apply(reversion.transform); // Should match original.transform, actually. (See applySuggestion) - if(resetContext) { - this.lmEngine.resetContext(context); - } + // Step 2: build a final, master Transform that will produce the desired results from the CURRENT state. + // In embedded mode, both Android and iOS are best served by calculating this transform and applying its + // values as needed for use with their IME interfaces. + let transform = final.buildTransformFrom(outputTarget); + outputTarget.apply(transform); - let alternates = transcription.alternates; - if(!alternates || alternates.length == 0) { - alternates = [{ - sample: transcription.transform, - p: 1.0 - }]; - } + // The reason we need to preserve the additive-inverse 'transformId' property on Reversions. + let promise = this.lmEngine.revertSuggestion(reversion, new ContextWindow(original.preInput, this.configuration, null)) - let transform = transcription.transform; - var promise = this.currentPromise = this.lmEngine.predict(alternates, context); + return promise.then((suggestions: Suggestion[]) => { + let result = new ReadySuggestions(suggestions, transform.id); + this.emit("suggestionsready", result); + this.currentPromise = null; - let lp = this; - return promise.then(function(suggestions: Suggestion[]) { - if(promise == lp.currentPromise) { - let result = new ReadySuggestions(suggestions, transform.id); - lp.emit("suggestionsready", result); - lp.currentPromise = null; - } + return suggestions; + }); + } - return suggestions; - }); + public predictFromTarget(outputTarget: OutputTarget, layerId: string): Promise { + if(!outputTarget) { + return null; } - private recordTranscription(transcription: Transcription) { - this.recentTranscriptions.push(transcription); + let transcription = outputTarget.buildTranscriptionFrom(outputTarget, null, false); + return this.predict(transcription, layerId); + } - if(this.recentTranscriptions.length > LanguageProcessor.TRANSCRIPTION_BUFFER) { - this.recentTranscriptions.splice(0, 1); - } + /** + * Called internally to do actual predictions after any relevant "invalidatesuggestions" events + * have been raised. + * @param transcription The triggering transcription (if it exists) + */ + private predict_internal(transcription: Transcription, resetContext: boolean, layerId: string): Promise { + if(!transcription) { + return null; } - /** - * Retrieves the context and output state of KMW immediately before the prediction with - * token `id` was generated. Must correspond to a 'recent' one, as only so many are stored - * in `ModelManager`'s history buffer. - * @param id A unique identifier corresponding to a recent `Transcription`. - * @returns The matching `Transcription`, or `null` none is found. - */ - public getPredictionState(id: number): Transcription { - let match = this.recentTranscriptions.filter(function(t: Transcription) { - return t.token == id; - }) - - return match.length == 0 ? null : match[0]; + let context = new ContextWindow(transcription.preInput, this.configuration, layerId); + this.recordTranscription(transcription); + + if(resetContext) { + this.lmEngine.resetContext(context); } - public shutdown() { - this.lmEngine.shutdown(); + let alternates = transcription.alternates; + if(!alternates || alternates.length == 0) { + alternates = [{ + sample: transcription.transform, + p: 1.0 + }]; } - public get isActive(): boolean { - if(!this.canEnable()) { - this._mayPredict = false; - return false; + let transform = transcription.transform; + var promise = this.currentPromise = this.lmEngine.predict(alternates, context); + + return promise.then((suggestions: Suggestion[]) => { + if(promise == this.currentPromise) { + let result = new ReadySuggestions(suggestions, transform.id); + this.emit("suggestionsready", result); + this.currentPromise = null; } - return (this.activeModel || false) && this._mayPredict; - } - public canEnable(): boolean { - // Is overridden for dom-aware KMW in case of old IE versions. - return true; + return suggestions; + }); + } + + private recordTranscription(transcription: Transcription) { + this.recentTranscriptions.push(transcription); + + if(this.recentTranscriptions.length > LanguageProcessor.TRANSCRIPTION_BUFFER) { + this.recentTranscriptions.splice(0, 1); } + } + + /** + * Retrieves the context and output state of KMW immediately before the prediction with + * token `id` was generated. Must correspond to a 'recent' one, as only so many are stored + * in `ModelManager`'s history buffer. + * @param id A unique identifier corresponding to a recent `Transcription`. + * @returns The matching `Transcription`, or `null` none is found. + */ + public getPredictionState(id: number): Transcription { + let match = this.recentTranscriptions.filter((t: Transcription) => { + return t.token == id; + }) + + return match.length == 0 ? null : match[0]; + } - public get mayPredict() { - return this._mayPredict; + public shutdown() { + this.lmEngine.shutdown(); + } + + public get isActive(): boolean { + if(!this.canEnable()) { + this._mayPredict = false; + return false; } + return (this.activeModel || false) && this._mayPredict; + } - public set mayPredict(flag: boolean) { - if(!this.canEnable()) { - return; - } + canEnable(): boolean { + // Is not initialized if there is no worker. + return !!this.lmEngine; + } - let oldVal = this._mayPredict; - this._mayPredict = flag; + public get mayPredict() { + return this._mayPredict; + } - if(oldVal != flag) { - // If there's no model to be activated and we've reached this point, - // the banner should remain inactive, as it already was. - // If it there was one and we've reached this point, we're globally - // deactivating, so we're fine. - if(this.activeModel) { - this.emit('statechange', flag ? 'active' : 'inactive'); + public set mayPredict(flag: boolean) { + if(!this.canEnable()) { + return; + } + + let oldVal = this._mayPredict; + this._mayPredict = flag; + + if(oldVal != flag) { + // If there's no model to be activated and we've reached this point, + // the banner should remain inactive, as it already was. + // If it there was one and we've reached this point, we're globally + // deactivating, so we're fine. + if(this.activeModel) { + // If someone toggles predictions on and off without changing the model, it is possible + // that the model is already configured! + let state: StateChangeEnum = flag ? 'active' : 'inactive'; + + // We always signal the 'active' state here, even if 'configured', b/c of an + // anti-banner-flicker optimization in the Android app. + this._state = state; + this.emit('statechange', state); + + if(this.isConfigured) { + this._state = 'configured'; + this.emit('statechange', 'configured'); } } } + } - public get mayCorrect() { - return this._mayCorrect; - } + public get mayCorrect() { + return this._mayCorrect; + } - public set mayCorrect(flag: boolean) { - this._mayCorrect = flag; - } + public set mayCorrect(flag: boolean) { + this._mayCorrect = flag; + } - public get wordbreaksAfterSuggestions() { - return this.configuration.wordbreaksAfterSuggestions; - } + public get wordbreaksAfterSuggestions() { + return this.configuration.wordbreaksAfterSuggestions; + } - public tryAcceptSuggestion(source: string): boolean { - let returnObj = {shouldSwallow: false}; - this.emit('tryaccept', source, returnObj); + public tryAcceptSuggestion(source: string): boolean { + // If and when we do auto-correct, the suggestion is to pass this object to the event and + // denote any mutations to the contained value. + //let returnObj = {shouldSwallow: false}; + this.emit('tryaccept', source); - return returnObj.shouldSwallow; - } + return false; + } - public tryRevertSuggestion(): boolean { - let returnObj = {shouldSwallow: false}; - this.emit('tryrevert', returnObj); + public tryRevertSuggestion(): boolean { + // If and when we do auto-revert, the suggestion is to pass this object to the event and + // denote any mutations to the contained value. + //let returnObj = {shouldSwallow: false}; + this.emit('tryrevert'); - return returnObj.shouldSwallow; - } + return false; } } \ No newline at end of file diff --git a/common/web/input-processor/src/text/prediction/modelSpec.ts b/common/web/input-processor/src/text/prediction/modelSpec.ts new file mode 100644 index 0000000000000000000000000000000000000000..986d94631160d2853b9c9540dca07dae2e11b3ec --- /dev/null +++ b/common/web/input-processor/src/text/prediction/modelSpec.ts @@ -0,0 +1,27 @@ +/** + * Defines the metadata needed to support and load lexical models compatible with + * Keyman Engine for Web. + */ +export default interface ModelSpec { + /** + * The model's unique identifier. + */ + id: string; + + /** + * The list of supported BCP-47 language codes. Only one language should be supported, + * although multiple variants based on region code (at min) may be specified. + */ + languages: string[]; + + /** + * The path/URL to the file that defines the model. If both `path` and `code` are specified, + * `path` takes precedence. + */ + path: string; + + /** + * The raw JS script defining the model. Only used if `path` is not specified. + */ + code: string; +} \ No newline at end of file diff --git a/common/web/input-processor/src/text/prediction/predictionContext.ts b/common/web/input-processor/src/text/prediction/predictionContext.ts new file mode 100644 index 0000000000000000000000000000000000000000..7578545b98507986157d2094e51ce6bdbe78514e --- /dev/null +++ b/common/web/input-processor/src/text/prediction/predictionContext.ts @@ -0,0 +1,360 @@ +import EventEmitter from "eventemitter3"; +import type LanguageProcessor from "./languageProcessor.js"; +import { type ReadySuggestions, type InvalidateSourceEnum, StateChangeEnum, StateChangeHandler } from './languageProcessor.js'; +import { type KeyboardProcessor, type OutputTarget } from "@keymanapp/keyboard-processor"; + +interface PredictionContextEventMap { + update: (suggestions: Suggestion[]) => void; +} + +/** + * Maintains predictive-text state information corresponding to the current context. + */ +export default class PredictionContext extends EventEmitter { + // Historical note: before 17.0, this code was intertwined with /web/source/osk/banner.ts's + // SuggestionBanner class. This class serves as the main implementation of the banner's core logic. + + // Designed for use with auto-correct behavior + private selected: Suggestion; + + private initNewContext: boolean = true; + + private _currentSuggestions: Suggestion[] = []; + private keepSuggestion: Keep; + private revertSuggestion: Reversion; + + private recentAccept: boolean = false; + private revertAcceptancePromise: Promise; + + private swallowPrediction: boolean = false; + + private doRevert: boolean = false; + private recentRevert: boolean = false; + + private langProcessor: LanguageProcessor; + private kbdProcessor: KeyboardProcessor; + + /** + * Represents the active context used when requesting and applying predictive-text operations. + */ + private _currentTarget: OutputTarget; + + public get currentTarget(): OutputTarget { + return this._currentTarget; + } + + public setCurrentTarget(target: OutputTarget): Promise { + const originalTarget = this._currentTarget; + this._currentTarget = target; + + if(originalTarget != target) { + // Note: should be triggered after the corresponding new-context event rule has been processed, + // as that may affect the value of layerId here. + return this.resetContext(); + } else { + return Promise.resolve([]); + } + } + + private readonly suggestionApplier: (suggestion: Suggestion) => Promise; + private readonly suggestionReverter: (reversion: Reversion) => void; + + /** + * Handler for post-processing once a suggestion has been applied: calls + * into the active keyboard's `begin postKeystroke` entry point. + * + * Called after the suggestion is applied but _before_ new predictions are + * requested based on the resulting context. + */ + private readonly postApplicationHandler: () => void; + + public constructor(langProcessor: LanguageProcessor, kbdProcessor: KeyboardProcessor) { + super(); + + this.langProcessor = langProcessor; + this.kbdProcessor = kbdProcessor; + + const validSuggestionState: () => boolean = () => + this.currentTarget && langProcessor.state == 'configured'; + + this.suggestionApplier = (suggestion) => { + if(validSuggestionState()) { + return langProcessor.applySuggestion(suggestion, this.currentTarget, () => kbdProcessor.layerId); + } + } + + this.suggestionReverter = (reversion) => { + if(validSuggestionState()) { + langProcessor.applyReversion(reversion, this.currentTarget); + } + } + + // As it's called synchronously via event-callback during `this.suggestionApplier`, + // `this.currentTarget` is guaranteed to remain unchanged. + this.postApplicationHandler = () => { + // Tell the keyboard that the current layer has not changed + kbdProcessor.newLayerStore.set(''); + kbdProcessor.oldLayerStore.set(''); + // Call the keyboard's entry point. + kbdProcessor.processPostKeystroke(kbdProcessor.contextDevice, this.currentTarget) + // If we have a RuleBehavior as a result, run it on the target. This should + // only change system store and variable store values. + ?.finalize(kbdProcessor, this.currentTarget, true); + }; + + this.connect(); + } + + private connect() { + this.langProcessor.addListener('invalidatesuggestions', this.invalidateSuggestions); + this.langProcessor.addListener('suggestionsready', this.updateSuggestions); + this.langProcessor.addListener('tryaccept', this.doTryAccept); + this.langProcessor.addListener('tryrevert', this.doTryRevert); + this.langProcessor.addListener('statechange', this.onModelStateChange); + + this.langProcessor.addListener('suggestionapplied', this.postApplicationHandler); + } + + public disconnect() { + this.langProcessor.removeListener('invalidatesuggestions', this.invalidateSuggestions); + this.langProcessor.removeListener('suggestionsready', this.updateSuggestions); + this.langProcessor.removeListener('tryaccept', this.doTryAccept); + this.langProcessor.removeListener('tryrevert', this.doTryRevert); + this.langProcessor.removeListener('statechange', this.onModelStateChange); + + this.langProcessor.removeListener('suggestionapplied', this.postApplicationHandler); + this.clearSuggestions(); + } + + public get currentSuggestions(): Suggestion[] { + let suggestions = []; + // Insert 'current text' if/when valid as the leading option. + // Since we don't yet do auto-corrections, we only show 'keep' whenever it's + // a valid word (according to the model). + + if(this.activateKeep() && this.keepSuggestion && this.keepSuggestion.matchesModel) { + suggestions.push(this.keepSuggestion); + } else if(this.doRevert) { + suggestions.push(this.revertSuggestion); + } + + return suggestions.concat(this._currentSuggestions); + } + + /** + * Function apply + * Description Applies the predictive `Suggestion` represented by this `BannerSuggestion`. + */ + private acceptInternal(suggestion: Suggestion): Promise { + if(!suggestion) { + return null; + } + + // Should be safe to convert into an event handled externally. + // layerID can be obtained by whoever/whatever holds the InputProcessor instance. + if(suggestion.tag == 'revert') { + this.suggestionReverter(suggestion as Reversion); + return null; + } else { + return this.suggestionApplier(suggestion); + } + } + + /** + * Applies predictive-text suggestions and post-acceptance reversions to the current + * prediction context. + * + * Note that both cases will additionally trigger a new asynchronous `predict` operation, + * though no corresponding Promise is returned by this function. As such, the current + * suggestions should be considered outdated after calling this method, pending replacement + * upon the completed async `predict`. + * + * @param suggestion Either a `Suggestion` or `Reversion`. + * @returns if `suggestion` is a `Suggestion`, will return a `Promise`; else, `null`. + */ + public accept(suggestion: Suggestion): Promise | null { + let _this = this; + + // Selecting a suggestion or a reversion should both clear selection + // and clear the reversion-displaying state of the banner. + this.selected = null; + this.doRevert = false; + + this.revertAcceptancePromise = this.acceptInternal(suggestion); + if(!this.revertAcceptancePromise) { + // We get here either if suggestion acceptance fails or if it was a reversion. + if(suggestion && suggestion.tag == 'revert') { + // Reversion state management + this.recentAccept = false; + this.recentRevert = true; + } + return null; + } + + this.revertAcceptancePromise.then(function(suggestion) { + // Always null-check! + if(suggestion) { + _this.revertSuggestion = suggestion; + } + }); + + this.recentAccept = true; + this.recentRevert = false; + + this.swallowPrediction = true; + + return this.revertAcceptancePromise; + } + + private showRevert() { + // Construct a 'revert suggestion' to facilitate a reversion UI component. + this.doRevert = true; + this.sendUpdateEvent(); + } + + /** + * Receives messages from the keyboard that the 'accept' keystroke has been entered. + * Should return 'false' if the current state allows accepting a suggestion and act accordingly. + * Otherwise, return true. + */ + private doTryAccept = (source: string /*, returnObj: {shouldSwallow: boolean}*/): void => { + //let keyman = com.keyman.singleton; + + if(!this.recentAccept && this.selected) { + this.accept(this.selected); + // returnObj.shouldSwallow = true; + } else if(this.recentAccept && source == 'space') { + this.recentAccept = false; + // // If the model doesn't insert wordbreaks, don't swallow the space. If it does, + // // we consider that insertion to be the results of the first post-accept space. + // returnObj.shouldSwallow = !!keyman.core.languageProcessor.wordbreaksAfterSuggestions; // can be handed outside + } else { + // returnObj.shouldSwallow = false; + } + } + + /** + * Receives messages from the keyboard that the 'revert' keystroke has been entered. + * Should return 'false' if the current state allows reverting a recently-applied suggestion and act accordingly. + * Otherwise, return true. + */ + private doTryRevert = (/*returnObj: {shouldSwallow: boolean}*/): boolean => { + // Has the revert keystroke (BKSP) already been sent once since the last accept? + if(this.doRevert) { + // If so, clear the 'revert' option and start doing normal predictions again. + this.doRevert = false; + this.recentAccept = false; + // Otherwise, did we just accept something before the revert signal was received? + } else if(this.recentAccept) { + this.showRevert(); + this.swallowPrediction = true; + } + + // // We don't yet actually do key-based reversions. + // returnObj.shouldSwallow = false; + return; + } + + /** + * Function invalidateSuggestions + * Scope Public + * Description Clears the suggestions in the suggestion banner + */ + private invalidateSuggestions = (source: InvalidateSourceEnum): void => { + // By default, we assume that the context is the same until we notice otherwise. + this.initNewContext = false; + + if(!this.swallowPrediction || source == 'context') { + this.recentAccept = false; + this.doRevert = false; + this.recentRevert = false; + + if(source == 'context') { + this.swallowPrediction = false; + this.initNewContext = true; + } + } + + // Not checking this can result in a perceptible 'flash' of sorts due to the suggestion-update delay. + if(source != 'new') { + this.clearSuggestions(); + // this.options.forEach((option: BannerSuggestion) => { + // option.update(null); + // }); + } + } + + private clearSuggestions() { + this.updateSuggestions({ + suggestions: [], + transcriptionID: 0 + }); + } + + private activateKeep(): boolean { + return !this.recentAccept && !this.recentRevert && !this.initNewContext; + } + + /** + * Function updateSuggestions + * Scope Public + * @param {Suggestion[]} suggestions Array of suggestions from the lexical model. + * Description Update the displayed suggestions in the SuggestionBanner + */ + private updateSuggestions = (prediction: ReadySuggestions): void => { + let suggestions = prediction.suggestions; + + this._currentSuggestions = suggestions; + + // Do we have a keep suggestion? If so, remove it from the list so that we can control its display position + // and prevent it from being hidden after reversion operations. + this.keepSuggestion = null; + for(let s of suggestions) { + if(s.tag == 'keep') { + this.keepSuggestion = s as Keep; + } + } + + if(this.keepSuggestion) { + this._currentSuggestions.splice(this._currentSuggestions.indexOf(this.keepSuggestion), 1); + } + + // If we've gotten an update request like this, it's almost always user-triggered and means the context has shifted. + if(!this.swallowPrediction) { + this.recentAccept = false; + this.doRevert = false; + this.recentRevert = false; + } else { // This prediction was triggered by a recent 'accept.' Now that it's fulfilled, we clear the flag. + this.swallowPrediction = false; + } + + // The rest is the same, whether from input or from "self-updating" after a reversion to provide new suggestions. + this.sendUpdateEvent(); + } + + public sendUpdateEvent() { + this.emit('update', this.currentSuggestions); + } + + public resetContext(): Promise { + const target = this.currentTarget; + + if(target) { + // Note: should be triggered after the corresponding new-context event rule has been processed, + // as that may affect the value of layerId here. + return this.langProcessor.invalidateContext(target, this.kbdProcessor.layerId); + } else { + return Promise.resolve([]); + } + } + + private onModelStateChange: StateChangeHandler = (state) => { + // Either way, the model has changed; either state marks the completion of such a transition. + // The 'active' state displays the banner while a model loads... but its predictions are + // only possible once fully 'configured'. They may appear to 'blink on' after a small delay + // as a result. + if(state == 'configured' || state == 'inactive') { + this.resetContext(); + } + } +} \ No newline at end of file diff --git a/common/web/input-processor/src/tsconfig.bundled.json b/common/web/input-processor/src/tsconfig.bundled.json deleted file mode 100644 index d16fe709fb25f7e6598db3ff3d58962eaee96dd0..0000000000000000000000000000000000000000 --- a/common/web/input-processor/src/tsconfig.bundled.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // This variant of the tsconfig.json exists to create a 'leaf', 'bundled' - // version of the keyboard-processor build product. The same reference - // cannot be prepended twice in a composite tsc build, posing problems - // for certain down-line builds if the two tsconfigs are not differentiated. - "extends": "./tsconfig.json", - "compilerOptions": { - "outFile": "../build/index.bundled.js", - }, - "references": [ - // Prepended by lm-worker. - { "path": "../../keyman-version", "prepend": true }, - { "path": "../../utils", "prepend": true }, - { "path": "../../keyboard-processor/src", "prepend": true }, - // For headless tests, we need the headless version of the compile. - { "path": "../../../predictive-text/tsconfig.json", "prepend": true } - ], - "files": [ - "text/inputProcessor.ts", - "../../../../node_modules/eventemitter3/index.js", - ] -} diff --git a/common/web/input-processor/src/tsconfig.json b/common/web/input-processor/src/tsconfig.json deleted file mode 100644 index 230233a2d1d95c9e6a2d10e64ed5bd3769556c8a..0000000000000000000000000000000000000000 --- a/common/web/input-processor/src/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "../../../../tsconfig-base.json", - "compilerOptions": { - "allowJs": true, - "module": "none", - "outDir": "../build/", - "declaration": true, - "inlineSources": true, - "sourceMap": true, - "sourceRoot": "keyman/", - "target": "es5", - "types": ["node"], - "lib": ["es6"], - "outFile": "../build/index.js" - }, - "references": [ - { "path": "../../keyman-version" }, - { "path": "../../utils" }, - { "path": "../../keyboard-processor/src" }, - { "path": "../../../predictive-text/browser.tsconfig.json" }, - ], - "include": ["./**/*.ts"], - "files": [ - "text/inputProcessor.ts", - "../../../../node_modules/eventemitter3/index.js" - ] -} diff --git a/common/web/input-processor/tests/cases/inputProcessor.js b/common/web/input-processor/tests/cases/inputProcessor.js index d69ccd5af92f4601035ea3561d33bee3f41130f2..00fc130a200160355de164ffef8afb0bb2ca30fd 100644 --- a/common/web/input-processor/tests/cases/inputProcessor.js +++ b/common/web/input-processor/tests/cases/inputProcessor.js @@ -1,11 +1,17 @@ -var assert = require('chai').assert; -var fs = require("fs"); -var vm = require("vm"); +import { assert } from 'chai'; +import fs from 'fs'; -let InputProcessor = require('../../build/index.bundled.js'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { InputProcessor } from '@keymanapp/input-processor'; +import { KeyboardInterface, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; + +import { Worker } from '@keymanapp/lexical-model-layer/node'; +import * as utils from '@keymanapp/web-utils'; // Required initialization setup. -global.com = InputProcessor.com; // exports all keyboard-processor namespacing. global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. @@ -27,35 +33,43 @@ describe('InputProcessor', function() { }); it('has expected default values after initialization', function () { - let core = new InputProcessor(device); - - assert.isOk(core.keyboardProcessor); - assert.isDefined(core.keyboardProcessor.contextDevice); - assert.isOk(core.languageProcessor); - assert.isOk(core.keyboardInterface); - assert.isUndefined(core.activeKeyboard); // No keyboard should be loaded yet. - assert.isUndefined(core.activeModel); // Same for the model. - - // These checks are lifted from the keyboard-processor init checks found in - // common/web/keyboard-processor/tests/cases/basic-init.js. - assert.equal('us', core.keyboardProcessor.baseLayout, 'KeyboardProcessor has unexpected base layout') - assert.isNotNull(global.KeymanWeb, 'KeymanWeb global was not automatically installed'); - assert.equal('default', core.keyboardProcessor.layerId, 'Default layer is not set to "default"'); - assert.isUndefined(core.keyboardProcessor.activeKeyboard, 'Initialized with already-active keyboard'); - - // Lifted from languageProcessor.js - the core should not be changing these with its init. - assert.isUndefined(core.languageProcessor.activeModel); - assert.isFalse(core.languageProcessor.isActive); - assert.isTrue(core.languageProcessor.mayPredict); + // Can construct without the second parameter; if so, the final assertion - .mayPredict + // will be invalidated. (No worker, no ability to predict.) + let worker = Worker.constructInstance(); + + try { + let core = new InputProcessor(device, worker); + + assert.isOk(core.keyboardProcessor); + assert.isDefined(core.keyboardProcessor.contextDevice); + assert.isOk(core.languageProcessor); + assert.isOk(core.keyboardInterface); + assert.isUndefined(core.activeKeyboard); // No keyboard should be loaded yet. + assert.isUndefined(core.activeModel); // Same for the model. + + // These checks are lifted from the keyboard-processor init checks found in + // common/web/keyboard-processor/tests/cases/basic-init.js. + assert.equal('us', core.keyboardProcessor.baseLayout, 'KeyboardProcessor has unexpected base layout') + assert.isNotNull(global.KeymanWeb, 'KeymanWeb global was not automatically installed'); + assert.equal('default', core.keyboardProcessor.layerId, 'Default layer is not set to "default"'); + assert.isUndefined(core.keyboardProcessor.activeKeyboard, 'Initialized with already-active keyboard'); + + // Lifted from languageProcessor.js - the core should not be changing these with its init. + assert.isUndefined(core.languageProcessor.activeModel); + assert.isFalse(core.languageProcessor.isActive); + assert.isTrue(core.languageProcessor.mayPredict); + } finally { + worker.terminate(); + } }); }); describe('efficiency tests', function() { let testDistribution = []; - var keyboard; + let keyboardWithHarness; // Easy peasy long context: use the input processor's full source! - let coreSourceCode = fs.readFileSync('build/index.bundled.js', 'utf-8'); + let coreSourceCode = fs.readFileSync('build/lib/index.mjs', 'utf-8'); // At the time this test block was written... 810485 chars. // Let's force it to the same order of magnitude, even if the codebase grows. @@ -63,7 +77,7 @@ describe('InputProcessor', function() { coreSourceCode = coreSourceCode.substring(0, 1000000); } - this.beforeAll(function() { + this.beforeAll(async function() { testDistribution = []; for(let c = 'A'.charCodeAt(0); c <= 'Z'.charCodeAt(0); c++) { @@ -75,27 +89,24 @@ describe('InputProcessor', function() { }); } - // Load the keyboard. We'll need an InputProcessor instance as an intermediary. - let core = new InputProcessor(device); - - // These two lines will load a keyboard from its file; headless-mode `registerKeyboard` will - // automatically set the keyboard as active. - let kbdScript = new vm.Script(fs.readFileSync('../../test/resources/keyboards/test_chirality.js')); - kbdScript.runInThisContext(); - - keyboard = core.activeKeyboard; + // Load the keyboard. + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_chirality.js')); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; }); describe('without fat-fingering', function() { it('with minimal context (no fat-fingers)', function() { this.timeout(32); // ms let core = new InputProcessor(device); - let context = new com.keyman.text.Mock("", 0); + let context = new Mock("", 0); - core.activeKeyboard = keyboard; - let layout = keyboard.layout(com.keyman.utils.FormFactor.Phone); + core.keyboardProcessor.keyboardInterface = keyboardWithHarness; + let keyboard = keyboardWithHarness.activeKeyboard; + let layout = keyboard.layout(utils.DeviceSpec.FormFactor.Phone); let key = layout.getLayer('default').getKey('K_A'); - let event = key.constructKeyEvent(core.keyboardProcessor, device); + let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys); let behavior = core.processKeyEvent(event, context); assert.isNotNull(behavior); @@ -103,7 +114,7 @@ describe('InputProcessor', function() { it('with extremely long context (' + coreSourceCode._kmwLength() + ' chars, no fat-fingers)', function() { // Assumes no SMP chars in the source, which is fine. - let context = new com.keyman.text.Mock(coreSourceCode, coreSourceCode._kmwLength()); + let context = new Mock(coreSourceCode, coreSourceCode._kmwLength()); this.timeout(500); // 500 ms, excluding text import. // These often run on VMs, so we'll be a bit generous. @@ -111,10 +122,11 @@ describe('InputProcessor', function() { let core = new InputProcessor(device); // I mean, it IS long context, and time // thresholding is disabled within Node. - core.activeKeyboard = keyboard; - let layout = keyboard.layout(com.keyman.utils.FormFactor.Phone); + core.keyboardProcessor.keyboardInterface = keyboardWithHarness; + let keyboard = keyboardWithHarness.activeKeyboard; + let layout = keyboard.layout(utils.DeviceSpec.FormFactor.Phone); let key = layout.getLayer('default').getKey('K_A'); - let event = key.constructKeyEvent(core.keyboardProcessor, device); + let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys); let behavior = core.processKeyEvent(event, context); assert.isNotNull(behavior); @@ -125,13 +137,14 @@ describe('InputProcessor', function() { it('with minimal context (with fat-fingers)', function() { this.timeout(32); // ms let core = new InputProcessor(device); - let context = new com.keyman.text.Mock("", 0); + let context = new Mock("", 0); - core.activeKeyboard = keyboard; - let layout = keyboard.layout(com.keyman.utils.FormFactor.Phone); + core.keyboardProcessor.keyboardInterface = keyboardWithHarness; + let keyboard = keyboardWithHarness.activeKeyboard; + let layout = keyboard.layout(utils.DeviceSpec.FormFactor.Phone); let key = layout.getLayer('default').getKey('K_A'); key.keyDistribution = testDistribution; - let event = key.constructKeyEvent(core.keyboardProcessor, device); + let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys); let behavior = core.processKeyEvent(event, context); assert.isNotNull(behavior); @@ -139,7 +152,7 @@ describe('InputProcessor', function() { it('with extremely long context (' + coreSourceCode._kmwLength() + ' chars, with fat-fingers)', function() { // Assumes no SMP chars in the source, which is fine. - let context = new com.keyman.text.Mock(coreSourceCode, coreSourceCode._kmwLength()); + let context = new Mock(coreSourceCode, coreSourceCode._kmwLength()); this.timeout(500); // 500 ms, excluding text import. // These often run on VMs, so we'll be a bit generous. @@ -150,11 +163,12 @@ describe('InputProcessor', function() { let core = new InputProcessor(device); // It IS long context, and time // thresholding is disabled within Node. - core.activeKeyboard = keyboard; - let layout = keyboard.layout(com.keyman.utils.FormFactor.Phone); + core.keyboardProcessor.keyboardInterface = keyboardWithHarness; + let keyboard = keyboardWithHarness.activeKeyboard; + let layout = keyboard.layout(utils.DeviceSpec.FormFactor.Phone); let key = layout.getLayer('default').getKey('K_A'); key.keyDistribution = testDistribution; - let event = key.constructKeyEvent(core.keyboardProcessor, device); + let event = keyboard.constructKeyEvent(key, device, core.keyboardProcessor.stateKeys); let behavior = core.processKeyEvent(event, context); assert.isNotNull(behavior); diff --git a/common/web/input-processor/tests/cases/languageProcessor.js b/common/web/input-processor/tests/cases/languageProcessor.js index c3e02a97621023e13bc95d4d02f6750f8fdc6412..e50fcc4b5edb68d05a152e64a6267e329139ed48 100644 --- a/common/web/input-processor/tests/cases/languageProcessor.js +++ b/common/web/input-processor/tests/cases/languageProcessor.js @@ -1,48 +1,54 @@ -var assert = require('chai').assert; -var fs = require("fs"); -var vm = require("vm"); +import { assert } from 'chai'; + +import { LanguageProcessor } from '@keymanapp/input-processor'; +import { SourcemappedWorker as LMWorker } from "@keymanapp/lexical-model-layer/node"; +import { Mock } from '@keymanapp/keyboard-processor'; /* * Unit tests for the Dummy prediction model. */ -// TODO: this relies on esbuild output for lexical-model-compiler; later should use import -var LexicalModelCompiler = require('../../../../../developer/src/kmc-model/build/cjs-src/lexical-model-compiler.cjs').default; -var path = require('path'); - -let InputProcessor = require('../../build/index.bundled.js'); +import { LexicalModelCompiler } from '@keymanapp/kmc-model'; +import { fileURLToPath } from 'url'; +import path from 'path'; // Required initialization setup. -global.com = InputProcessor.com; // exports all keyboard-processor namespacing. global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. // Initialize supplementary plane string extensions String.kmwEnableSupplementaryPlane(false); -let LanguageProcessor = com.keyman.text.prediction.LanguageProcessor; - // Test the KeyboardProcessor interface. describe('LanguageProcessor', function() { + let worker; + + beforeEach(function() { + worker = LMWorker.constructInstance(); + }); + + afterEach(function() { + worker.terminate(); + }); + describe('[[constructor]]', function () { it('should initialize without errors', function () { - let lp = new LanguageProcessor(); + let lp = new LanguageProcessor(worker); assert.isNotNull(lp); }); it('has expected default values after initialization', function () { - let languageProcessor = new LanguageProcessor(); + let languageProcessor = new LanguageProcessor(worker); // These checks are lifted from the keyboard-processor init checks found in // common/web/keyboard-processor/tests/cases/basic-init.js. - assert.isUndefined(languageProcessor.lmEngine); + assert.isDefined(languageProcessor.lmEngine); assert.isUndefined(languageProcessor.activeModel); assert.isFalse(languageProcessor.isActive); assert.isTrue(languageProcessor.mayPredict); // Some aspects of initialization must wait until after construction and overall // load of the core. See /web/source/kmwbase.ts, in the final IIFE. - languageProcessor.init(); assert.isOk(languageProcessor.lmEngine); }); }); @@ -50,6 +56,11 @@ describe('LanguageProcessor', function() { describe('.predict', function() { let compiler = new LexicalModelCompiler(); const MODEL_ID = 'example.qaa.trivial'; + + // ES-module mode leaves out `__dirname`, so we rebuild it using other components. + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const PATH = path.join(__dirname, '../../../../../developer/src/kmc-model/test/fixtures', MODEL_ID); describe('using angle brackets for quotes', function() { @@ -69,8 +80,7 @@ describe('LanguageProcessor', function() { }; it("successfully loads the model", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); languageProcessor.loadModel(modelSpec).then(function() { assert.isOk(languageProcessor.activeModel); // is only set after a successful load. @@ -81,10 +91,9 @@ describe('LanguageProcessor', function() { }); it("generates the expected prediction set", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("li", 2); + let contextSource = new Mock("li", 2); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -118,10 +127,9 @@ describe('LanguageProcessor', function() { describe("does not alter casing when input is lowercased", function() { it("when input is fully lowercased", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("li", 2); + let contextSource = new Mock("li", 2); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -138,10 +146,9 @@ describe('LanguageProcessor', function() { }); it("when input has non-initial uppercased letters", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("lI", 2); + let contextSource = new Mock("lI", 2); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -159,10 +166,9 @@ describe('LanguageProcessor', function() { }); it("unless the suggestion has uppercased letters", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("i", 1); + let contextSource = new Mock("i", 1); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -181,10 +187,9 @@ describe('LanguageProcessor', function() { describe("uppercases suggestions when input is fully capitalized ", function() { it("for suggestions with default casing (== 'lower')", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("LI", 2); + let contextSource = new Mock("LI", 2); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -202,10 +207,9 @@ describe('LanguageProcessor', function() { }); it("for precapitalized suggestions", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("I", 1); + let contextSource = new Mock("I", 1); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -225,10 +229,9 @@ describe('LanguageProcessor', function() { describe("initial-cases suggestions when input uses initial casing ", function() { describe("when input is a single capitalized letter", function() { it("for suggestions with default casing (== 'lower')", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("L", 1); + let contextSource = new Mock("L", 1); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { @@ -248,10 +251,9 @@ describe('LanguageProcessor', function() { describe("input length > 1", function() { it("for suggestions with default casing (== 'lower')", function(done) { - let languageProcessor = new LanguageProcessor(); - languageProcessor.init(); + let languageProcessor = new LanguageProcessor(worker); - let contextSource = new com.keyman.text.Mock("Li", 2); + let contextSource = new Mock("Li", 2); let transcription = contextSource.buildTranscriptionFrom(contextSource, null, null); languageProcessor.loadModel(modelSpec).then(function() { diff --git a/common/web/input-processor/tests/cases/predictionContext.js b/common/web/input-processor/tests/cases/predictionContext.js new file mode 100644 index 0000000000000000000000000000000000000000..fd03dda639e20e30b8f7dee40aed18c6aef1e11b --- /dev/null +++ b/common/web/input-processor/tests/cases/predictionContext.js @@ -0,0 +1,305 @@ +import { assert } from 'chai'; +import sinon from 'sinon'; + +import { LanguageProcessor, PredictionContext } from '@keymanapp/input-processor'; +import { Worker as LMWorker } from "@keymanapp/lexical-model-layer/node"; +import { DeviceSpec, KeyboardProcessor, Mock } from '@keymanapp/keyboard-processor'; + +function compileDummyModel(suggestionSets) { + return ` +LMLayerWorker.loadModel(new models.DummyModel({ + futureSuggestions: ${JSON.stringify(suggestionSets, null, 2)}, +})); +`; +} + +// Common spec used for each test's setup. It's actually irrelevant for the tests, +// but KeyboardProcessor needs an instance. +const deviceSpec = new DeviceSpec( + DeviceSpec.Browser.Chrome, + DeviceSpec.FormFactor.Desktop, + DeviceSpec.OperatingSystem.Windows +); + +const appleDummySuggestionSets = [[ + // Set 1: + { + transform: { insert: 'e', deleteLeft: 0}, + displayAs: 'apple', + }, { + transform: { insert: 'y', deleteLeft: 0}, + displayAs: 'apply' + }, { + transform: { insert: 'es', deleteLeft: 0}, + displayAs: 'apples' + } +], [ + // Set 2: + { + transform: { insert: '', deleteLeft: 0}, + displayAs: 'apple', + tag: 'keep' + }, { + transform: { insert: 'y', deleteLeft: 0}, + displayAs: 'apply' + }, { + transform: { insert: 's', deleteLeft: 1}, + displayAs: 'apps' + } +], [ + // Set 3: + { + transform: { insert: 'ied', deleteLeft: 2}, + displayAs: 'applied' + } +], [ + { + transform: { insert: ' reverted', deleteLeft: 5}, + displayAs: 'reverted' + } +]]; + +const appleDummyModel = { + id: 'dummy', + languages: ['en'], + code: compileDummyModel(appleDummySuggestionSets) +}; + +describe("PredictionContext", () => { + let worker; + + beforeEach(function() { + worker = LMWorker.constructInstance(); + }); + + afterEach(function() { + worker.terminate(); + }); + + it('receives predictions as they are generated', async function () { + const langProcessor = new LanguageProcessor(worker); + await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker. + + const kbdProcessor = new KeyboardProcessor(deviceSpec); + const predictiveContext = new PredictionContext(langProcessor, kbdProcessor); + + let updateFake = sinon.fake(); + predictiveContext.on('update', updateFake); + + let mock = new Mock("appl", 4); // "appl|", with '|' as the caret position. + const initialMock = Mock.from(mock); + const promise = predictiveContext.setCurrentTarget(mock); + + // Initial predictive state: no suggestions. context.initializeState() has not yet been called. + assert.equal(updateFake.callCount, 1); + assert.isEmpty(updateFake.firstCall.args[0]); // should have no suggestions. (if convenient for testing) + + await promise; + let suggestions; + + // Initialization results: our first set of dummy suggestions. + assert.equal(updateFake.callCount, 2); + suggestions = updateFake.secondCall.args[0]; + assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['apple', 'apply', 'apples']); + assert.isNotOk(suggestions.find((obj) => obj.tag == 'keep')); + assert.isNotOk(suggestions.find((obj) => obj.transform.deleteLeft != 0)); + + mock.insertTextBeforeCaret('e'); // appl| + e = apple + let transcription = mock.buildTranscriptionFrom(initialMock, null, true); + await langProcessor.predict(transcription, kbdProcessor.layerId); + + // First predict call results: our second set of dummy suggestions, the first of which includes + // a 'keep' of the original text. + assert.equal(updateFake.callCount, 3); + suggestions = updateFake.thirdCall.args[0]; + assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['apple', 'apply', 'apps']); + assert.equal(suggestions.find((obj) => obj.tag == 'keep').displayAs, 'apple'); + assert.equal(suggestions.find((obj) => obj.transform.deleteLeft != 0).displayAs, 'apps'); + }); + + it('sendUpdateState retrieves the most recent suggestion set', async function() { + const langProcessor = new LanguageProcessor(worker); + await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker. + + const kbdProcessor = new KeyboardProcessor(deviceSpec); + const predictiveContext = new PredictionContext(langProcessor, kbdProcessor); + + let mock = new Mock("appl", 4); // "appl|", with '|' as the caret position. + const initialSuggestions = await predictiveContext.setCurrentTarget(mock); + + let updateFake = sinon.fake(); + predictiveContext.on('update', updateFake); + predictiveContext.sendUpdateEvent(); // Allows external code to request a re-retrieval of current suggestion state. + + // Now, let's initialize the predictive state - this will load the initial suggestions. + let suggestions; + + // Initialization results: our first set of dummy suggestions. + assert.isTrue(updateFake.calledOnce); + suggestions = updateFake.firstCall.args[0]; + assert.deepEqual(suggestions, initialSuggestions); + + // The array instances may be different, but their contents should be the same instances and in the same order. + assert.sameOrderedMembers(suggestions, initialSuggestions); + }); + + it('suggestion application logic & triggered effects', async function () { + const langProcessor = new LanguageProcessor(worker); + await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker. + + const kbdProcessor = new KeyboardProcessor(deviceSpec); + const predictiveContext = new PredictionContext(langProcessor, kbdProcessor); + + let textState = new Mock("appl", 4); // "appl|", with '|' as the caret position. + + await predictiveContext.setCurrentTarget(textState); + + let updateFake = sinon.fake(); + predictiveContext.on('update', updateFake); + + let suggestions; + + let previousTextState = Mock.from(textState); + textState.insertTextBeforeCaret('e'); // appl| + e = apple + let transcription = textState.buildTranscriptionFrom(previousTextState, null, true); + await langProcessor.predict(transcription, kbdProcessor.layerId); + + // Verify setup. + assert.equal(updateFake.callCount, 1); + suggestions = updateFake.firstCall.args[0]; + assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['apple', 'apply', 'apps']); + assert.equal(suggestions.find((obj) => obj.tag == 'keep').displayAs, 'apple'); + assert.equal(suggestions.find((obj) => obj.transform.deleteLeft != 0).displayAs, 'apps'); + + // Now for the real test. + previousTextState = Mock.from(textState); // snapshot it! + + const suggestionApply = suggestions.find((obj) => obj.displayAs == 'apply'); + assert.isOk(suggestionApply); + + // For awaiting the suggestions generated upon applying our desired suggestion. + // We aren't given a direct Promise for that, but we can construct one this way. + let postApplySuggestions = new Promise((resolve) => { + predictiveContext.once('update', resolve); + }); + + // Apply the desired suggestion. Also passively generates new, post-acceptance + // suggestions, but this function itself don't provide a Promise for that... + // hence the previous block. + let promiseForApplyReversion = predictiveContext.accept(suggestionApply); + + assert.equal(updateFake.callCount, 1); // No new 'update' has been raised yet. + + // Check 1: did our active text context get changed? We DID just ask to apply + // a suggestion... + assert.notEqual(textState.getText(), previousTextState.getText()); + assert.equal(textState.getText(), 'apply '); + + let reversion = await promiseForApplyReversion; + await postApplySuggestions; + + // Check 2: a second 'update' - post-application predictions! + assert.equal(updateFake.callCount, 2); + suggestions = updateFake.secondCall.args[0]; + assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['applied']); + assert.isNotOk(suggestions.find((obj) => obj.tag == 'keep')); + assert.equal(suggestions[0].transform.deleteLeft, 2); + + // Check 4+: for the returned reversion object. + assert.isOk(reversion); + + // All other reversion details are tested in the 'reversion application logic...' section defined below. + }); + + it('reversion application logic & triggered effects', async function () { + const langProcessor = new LanguageProcessor(worker); + await langProcessor.loadModel(appleDummyModel); // await: must fully 'configure', load script into worker. + + const kbdProcessor = new KeyboardProcessor(deviceSpec); + const predictiveContext = new PredictionContext(langProcessor, kbdProcessor); + + let textState = new Mock("appl", 4); // "appl|", with '|' as the caret position. + + // Test setup - return to the state at the end of the prior-defined unit test ('suggestion application...') + + await predictiveContext.setCurrentTarget(textState); + + // This is the point in time that a reversion operation will rewind the context to. + const revertBaseTextState = Mock.from(textState); + textState.insertTextBeforeCaret('e'); // appl| + e = apple + let transcription = textState.buildTranscriptionFrom(revertBaseTextState, null, true); + + let suggestionCaptureFake = sinon.fake(); + predictiveContext.once('update', suggestionCaptureFake); + await langProcessor.predict(transcription, kbdProcessor.layerId); + + // We need to capture the suggestion we wish to apply. We could hardcode a forced + // value, but that might become brittle in the long-term. + const originalSuggestionSet = suggestionCaptureFake.firstCall.args[0]; + const suggestionApply = originalSuggestionSet.find((obj) => obj.displayAs == 'apply'); + assert.isOk(suggestionApply); + + let previousTextState = Mock.from(textState); + + // For awaiting the suggestions generated upon applying our desired suggestion. + // We aren't given a direct Promise for that, but we can construct one this way. + let postApplySuggestions = new Promise((resolve) => { + predictiveContext.once('update', resolve); + }); + + let reversion = await predictiveContext.accept(suggestionApply); + await postApplySuggestions; + + // Test setup complete. + + // Check the assertion object (from end of 'suggestion application' checks) - verify setup. + assert.isOk(reversion); + + // Reversion IDs are inverses of the applied suggestion. + // This is the important link used to rewind the context when reverting suggestions. + assert.equal(reversion.id, -suggestionApply.id); + assert.equal(reversion.transformId, -suggestionApply.transformId); + + // Revert display strings include quotes. + // + // We could check this more rigorously by importing from @keymanapp/models-wordbreakers for + // the default quotes. + assert.isTrue(reversion.displayAs.includes(previousTextState.getText())); + assert.equal(reversion.displayAs.length, previousTextState.getText().length + 2); // +2: opening + closing quotes. + + // Fire away! Time to apply the reversion. + previousTextState = Mock.from(textState); + + // Since the test uses a separate thread via Worker, make sure to set up any important event handlers + // before we request the reversion. + let updateFake = sinon.fake(); + predictiveContext.on('update', updateFake); + + // Now, in order to synchronize... we rely on a Promise. The callback is indeed + // called synchronously. + let postRevertSuggestions = new Promise((resolve) => { + predictiveContext.once('update', resolve); + }); + + // And now, apply the reversion itself. + let returnValue = predictiveContext.accept(reversion); + + // 'accepting' a reversion performs a rewind; there's no need for async ops here. + assert.isNull(returnValue); // as per the method's spec. + + // Verify that the rewind + application of reversion worked! + let rewoundTextStateWithInput = Mock.from(revertBaseTextState); // appl + rewoundTextStateWithInput.apply(reversion.transform); // + e + assert.equal(rewoundTextStateWithInput.getText(), 'apple'); // For visual clarity. + + // Note: no space appended. + assert.equal(textState.getText(), rewoundTextStateWithInput.getText()); + + // Re-synchronize once we've received word of the new post-reversion predictions. + await postRevertSuggestions; + + assert.equal(updateFake.callCount, 1); + const suggestionsPostReversion = updateFake.firstCall.args[0]; + assert.deepEqual(suggestionsPostReversion.map((obj) => obj.displayAs), ['reverted']); + }); +}); \ No newline at end of file diff --git a/common/web/input-processor/tsconfig.json b/common/web/input-processor/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..b6716d28cc6c8a2ec46f416cb933a550c7e752be --- /dev/null +++ b/common/web/input-processor/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.kmw-main-base.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "build/obj/", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "references": [ + { "path": "../utils" }, + { "path": "../keyboard-processor" }, + { "path": "../../predictive-text/tsconfig.all.json" }, + ], + // The extra .d.ts is necessary to avoid issues with lack of a Worker type definition when not including + // the standard DOM in the TS "lib" configuration. + "include": ["./src/**/*.ts", "../../predictive-text/src/worker-interface.d.ts"] +} diff --git a/common/web/keyboard-processor/.c8rc.json b/common/web/keyboard-processor/.c8rc.json new file mode 100644 index 0000000000000000000000000000000000000000..ceca963e1585086f04fc48a45e8e274c0d319227 --- /dev/null +++ b/common/web/keyboard-processor/.c8rc.json @@ -0,0 +1,15 @@ +{ + "check-coverage": false, + "clean": true, + "exclude": [ + "src/keyboards/loaders/dom*", + "node_modules/*", + "tests" + ], + "exclude-after-remap": true, + "reporter": ["text", "text-summary"], + "reports-dir": "build/coverage", + "src": [ + "src/" + ] +} \ No newline at end of file diff --git a/common/web/keyboard-processor/build-bundler.js b/common/web/keyboard-processor/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..25521c9409747e7dbae97f0dda83adbdc3cc6181 --- /dev/null +++ b/common/web/keyboard-processor/build-bundler.js @@ -0,0 +1,24 @@ +import esbuild from 'esbuild'; +import { esmConfiguration, bundleObjEntryPoints } from '../es-bundling/build/index.mjs'; +import * as fs from 'fs'; + +// Bundled ES module version +await esbuild.build({ + ...esmConfiguration, + ...bundleObjEntryPoints('lib', 'build/obj/index.js', 'build/obj/keyboards/loaders/dom-keyboard-loader.js') +}); + +// Sadly, esbuild aims to preserve the relative path between entry points... we want it directly +// in /build/lib, not in a buried subfolder. (This matters most for explicitly DOM libs like this +// one, since we can't do import path resolution in DOM tests like we can for headless tests.) +// +// Alternatively, we can just build it separately like the node-oriented one. +fs.renameSync('build/lib/keyboards/loaders/dom-keyboard-loader.mjs', 'build/lib/dom-keyboard-loader.mjs'); +fs.rmSync('build/lib/keyboards', { recursive: true, force: true }); + +// The node-based keyboard loader needs an extra parameter due to Node-built-in imports: +await esbuild.build({ + ...esmConfiguration, + ...bundleObjEntryPoints('lib', 'build/obj/keyboards/loaders/node-keyboard-loader.js'), + platform: "node" +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/build.sh b/common/web/keyboard-processor/build.sh index ebcab580ab5db576ff6face96206fcd2f95590ac..5bbca3140510de15c04e3d7fddb74e78e36253b8 100755 --- a/common/web/keyboard-processor/build.sh +++ b/common/web/keyboard-processor/build.sh @@ -21,6 +21,7 @@ builder_describe \ "Compiles the web-oriented utility function module." \ "@/common/web/recorder test" \ "@/common/web/keyman-version" \ + "@/common/web/es-bundling" \ "@/common/web/utils" \ configure \ clean \ @@ -30,37 +31,34 @@ builder_describe \ builder_describe_outputs \ configure /node_modules \ - build /common/web/keyboard-processor/build/index.js + build /common/web/keyboard-processor/build/lib/index.mjs builder_parse "$@" -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi +function do_build() { + tsc --build "$THIS_SCRIPT_PATH/tsconfig.all.json" + node ./build-bundler.js -if builder_start_action clean; then - npm run clean - builder_finish_action success clean -fi + # Declaration bundling. + tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts + tsc --emitDeclarationOnly --outFile ./build/lib/dom-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.dom.json + tsc --emitDeclarationOnly --outFile ./build/lib/node-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.node.json +} -if builder_start_action build; then - tsc --build "$THIS_SCRIPT_PATH/src/tsconfig.json" - builder_finish_action success build -fi - -if builder_start_action test; then - tsc --build "$THIS_SCRIPT_PATH/src/tsconfig.bundled.json" - - builder_heading "Running Keyboard Processor test suite" - - FLAGS= +function do_test() { + local MOCHA_FLAGS= + local KARMA_CONFIG=manual.conf.cjs if builder_has_option --ci; then echo "Replacing user-friendly test reports with CI-friendly versions." - FLAGS="$FLAGS --reporter mocha-teamcity-reporter" + MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter" + KARMA_CONFIG=CI.conf.cjs fi - mocha --recursive $FLAGS ./tests/cases/ + c8 mocha --recursive $MOCHA_FLAGS ./tests/node/ + karma start ./tests/dom/$KARMA_CONFIG +} - builder_finish_action success test -fi \ No newline at end of file +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf ./build +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/web/keyboard-processor/package.json b/common/web/keyboard-processor/package.json index c93d6f124d8de7285762bdd86c7d587b8fb3295f..2f57b7973271486e83578e139d9466b2d6399ce2 100644 --- a/common/web/keyboard-processor/package.json +++ b/common/web/keyboard-processor/package.json @@ -18,22 +18,55 @@ "homepage": "https://github.com/keymanapp/keyman#readme", "devDependencies": { "@keymanapp/resources-gosh": "*", + "c8": "^7.12.0", "chai": "^4.3.4", + "karma": "^6.4.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-teamcity-reporter": "^1.1.0", "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "ts-node": "^10.9.1", "typescript": "^4.9.5" }, "scripts": { - "tsc": "tsc", - "clean": "tsc -b --clean src/tsconfig.json && tsc -b --clean src/tsconfig.bundled.json", - "test": "gosh ./test.sh", - "mocha": "mocha" + "build": "gosh build.sh", + "clean": "gosh build.sh clean", + "test": "gosh build.sh test" }, "dependencies": { "@keymanapp/models-types": "*", "@keymanapp/keyman-version": "*", "@keymanapp/web-utils": "*", "@types/node": "^11.9.4" + }, + "type": "module", + "main": "./build/obj/index.js", + "types": "./build/obj/index.d.ts", + "exports": { + ".": "./build/obj/index.js", + "./node-keyboard-loader": { + "types": "./build/obj/keyboards/loaders/node-keyboard-loader.d.ts", + "import": "./build/obj/keyboards/loaders/node-keyboard-loader.js" + }, + "./dom-keyboard-loader": { + "types": "./build/obj/keyboards/loaders/dom-keyboard-loader.d.ts", + "import": "./build/obj/keyboards/loaders/dom-keyboard-loader.js" + }, + "./lib": { + "types": "./build/lib/index.d.ts", + "import": "./build/lib/index.mjs" + }, + "./lib/node-keyboard-loader": { + "types": "./build/lib/node-keyboard-loader.d.ts", + "import": "./build/lib/node-keyboard-loader.mjs" + }, + "./lib/dom-keyboard-loader": { + "types": "./build/lib/dom-keyboard-loader.d.ts", + "import": "./build/lib/dom-keyboard-loader.mjs" + }, + "./obj/*.js": "./build/obj/*.js" } } diff --git a/common/web/keyboard-processor/src/index.ts b/common/web/keyboard-processor/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad7aa495235b0a03f7085b590f771dd90df57ebb --- /dev/null +++ b/common/web/keyboard-processor/src/index.ts @@ -0,0 +1,50 @@ +export * from "./keyboards/activeLayout.js"; +export * from "./keyboards/defaultLayouts.js"; +export { default as Keyboard } from "./keyboards/keyboard.js"; +export * from "./keyboards/keyboard.js"; +export { KeyboardHarness, KeyboardKeymanGlobal, MinimalCodesInterface, MinimalKeymanGlobal } from "./keyboards/keyboardHarness.js"; +export { + default as KeyboardLoaderBase, + KeyboardLoadErrorBuilder, + KeyboardMissingError, + KeyboardScriptError +} from "./keyboards/keyboardLoaderBase.js"; +export { + CloudKeyboardFont, + internalizeFont, + InternalKeyboardFont, + KeyboardAPIPropertyMultilangSpec, + KeyboardAPIPropertySpec, + KeyboardInternalPropertySpec, + default as KeyboardProperties, + KeyboardFont, + MetadataObj as RawKeyboardMetadata, + LanguageAPIPropertySpec +} from "./keyboards/keyboardProperties.js"; +export { default as SpacebarText } from "./keyboards/spacebarText.js"; +export { default as StateKeyMap } from "./keyboards/stateKeyMap.js"; + +export { default as Codes } from "./text/codes.js"; +export * from "./text/codes.js"; +export * from "./text/deadkeys.js"; +export { default as DefaultRules } from "./text/defaultRules.js"; +export * from "./text/defaultRules.js"; +export { default as KeyboardInterface } from "./text/kbdInterface.js"; +export * from "./text/kbdInterface.js"; +export { default as KeyboardProcessor } from "./text/keyboardProcessor.js"; +export * from "./text/keyboardProcessor.js"; +export { default as KeyEvent } from "./text/keyEvent.js"; +export * from "./text/keyEvent.js"; +export { default as KeyMapping } from "./text/keyMapping.js"; +export { default as OutputTarget } from "./text/outputTarget.js"; +export * from "./text/outputTarget.js"; +export { default as RuleBehavior } from "./text/ruleBehavior.js"; +export * from "./text/systemStores.js"; + +export * from "@keymanapp/web-utils"; + +// At the top level, there should be no default export. + +// Without the line below... OutputTarget would likely be aliased there, as it's +// the last `export { default as _ }` => `export * from` pairing seen above. +export default undefined; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/activeLayout.ts b/common/web/keyboard-processor/src/keyboards/activeLayout.ts index 398682becd97877bbd8dadd1e62a820457275f3c..e6d2ab9f1578b82cc6ec97eeae3fc02db2e8200c 100644 --- a/common/web/keyboard-processor/src/keyboards/activeLayout.ts +++ b/common/web/keyboard-processor/src/keyboards/activeLayout.ts @@ -1,782 +1,765 @@ -namespace com.keyman.keyboards { - type KeyDistribution = text.KeyDistribution; - - // TS 3.9 changed behavior of getters to make them - // non-enumerable by default. This broke our 'polyfill' - // functions which depended on enumeration to copy the - // relevant props over. - // https://github.com/microsoft/TypeScript/pull/32264#issuecomment-677718191 - function Enumerable( - target: unknown, - propertyKey: string, - descriptor: PropertyDescriptor - ) { - descriptor.enumerable = true; +import Codes from "../text/codes.js"; +import KeyEvent, { KeyEventSpec } from "../text/keyEvent.js"; +import KeyMapping from "../text/keyMapping.js"; +import type { KeyDistribution } from "../text/keyEvent.js"; +import type { LayoutKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js"; +import type Keyboard from "./keyboard.js"; + +import { type DeviceSpec } from "@keymanapp/web-utils"; + +// TS 3.9 changed behavior of getters to make them +// non-enumerable by default. This broke our 'polyfill' +// functions which depended on enumeration to copy the +// relevant props over. +// https://github.com/microsoft/TypeScript/pull/32264#issuecomment-677718191 +function Enumerable( + target: unknown, + propertyKey: string, + descriptor: PropertyDescriptor +) { + descriptor.enumerable = true; +}; + +export class ActiveKey implements LayoutKey { + static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units + static readonly DEFAULT_RIGHT_MARGIN=15; // Padding to right of right-most key, in virtual units + static readonly DEFAULT_KEY_WIDTH=100; // Width of a key, if not specified, in virtual units + + // Defines key defaults + static readonly DEFAULT_KEY = { + text: '', + width: ActiveKey.DEFAULT_KEY_WIDTH, + sp: 0, + pad: ActiveKey.DEFAULT_PAD }; - export class ActiveKey implements LayoutKey { - static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units - static readonly DEFAULT_RIGHT_MARGIN=15; // Padding to right of right-most key, in virtual units - static readonly DEFAULT_KEY_WIDTH=100; // Width of a key, if not specified, in virtual units - - // Defines key defaults - static readonly DEFAULT_KEY = { - text: '', - width: ActiveKey.DEFAULT_KEY_WIDTH, - sp: 0, - pad: ActiveKey.DEFAULT_PAD - }; - - /** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */ - id?: string; - - // These are fine. - width?: number; - pad?: number; - - layer: string; - displayLayer: string; - nextlayer: string; - sp?: ButtonClass; - - private baseKeyEvent: text.KeyEvent; - isMnemonic: boolean = false; - - proportionalPad: number; - proportionalX: number; - proportionalWidth: number; - - sk?: ActiveKey[]; - - // Keeping things simple here, as this was added LATE in 14.0 beta. - // Could definitely extend in the future to instead return an object - // that denotes the 'nature' of the key. - // - isUnicode - // - isHardwareKey - // - etc. - - // Reference for the terminology in the comments below: - // https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty - - /** - * Matches the key code as set within Keyman Developer for the layout. - * For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output, - * with no additional metadata like layer or active modifiers. - * - * Is used to determine the keycode for input events, rule-matching, and keystroke processing. - */ - @Enumerable - public get baseKeyID(): string { - if(typeof this.id === 'undefined') { - return undefined; - } - - return this.id; - } - - @Enumerable - public get isPadding(): boolean { - // Does not include 9 (class: blank) as that may be an intentional 'catch' for misplaced - // keystrokes. - return this['sp'] == 10; // Button class: hidden. - } - - /** - * A unique identifier based on both the key ID & the 'desktop layer' to be used for the key. - * - * Allows diambiguation of scenarios where the same key ID is used twice within a layer, but - * with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617) - * The 'desktop layer' may be omitted if it matches the key's display layer. - * - * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: - * - * ``` - * "K_Q" - * + [K_Q] - * "K_Q+shift" - * + [K_Q SHIFT] - * ``` - * - * Useful when the active layer of an input-event is already known. - */ - @Enumerable - public get coreID(): string { - if(typeof this.id === 'undefined') { - return undefined; - } - - let baseID = this.id || ''; + /** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */ + id?: string; + + // These are fine. + width?: number; + pad?: number; + + layer: string; + displayLayer: string; + nextlayer: string; + sp?: ButtonClass; + + _baseKeyEvent: KeyEvent; + isMnemonic: boolean = false; + + proportionalPad: number; + proportionalX: number; + proportionalWidth: number; + + sk?: ActiveKey[]; + + // Keeping things simple here, as this was added LATE in 14.0 beta. + // Could definitely extend in the future to instead return an object + // that denotes the 'nature' of the key. + // - isUnicode + // - isHardwareKey + // - etc. + + // Reference for the terminology in the comments below: + // https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty + + /** + * Matches the key code as set within Keyman Developer for the layout. + * For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output, + * with no additional metadata like layer or active modifiers. + * + * Is used to determine the keycode for input events, rule-matching, and keystroke processing. + */ + @Enumerable + public get baseKeyID(): string { + if(typeof this.id === 'undefined') { + return undefined; + } - if(this.displayLayer != this.layer) { - baseID = baseID + '+' + this.layer; - } + return this.id; + } - return baseID; - } - - /** - * A keyboard-unique identifier to be used for any display elements representing this key - * in user interfaces and/or on-screen keyboards. - * - * Distinguishes between otherwise-identical keys on different layers of an OSK. - * Includes identifying information about the key's display layer. - * - * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: - * - * ``` - * "default-K_Q" - * + [K_Q] - * "default-K_Q+shift" - * + [K_Q SHIFT] - * ``` - * - * Useful when only the active keyboard is known about an input event. - */ - @Enumerable - public get elementID(): string { - if(typeof this.id === 'undefined') { - return undefined; - } + @Enumerable + public get isPadding(): boolean { + // Does not include 9 (class: blank) as that may be an intentional 'catch' for misplaced + // keystrokes. + return this['sp'] == 10; // Button class: hidden. + } - return this.displayLayer + '-' + this.coreID; + /** + * A unique identifier based on both the key ID & the 'desktop layer' to be used for the key. + * + * Allows diambiguation of scenarios where the same key ID is used twice within a layer, but + * with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617) + * The 'desktop layer' may be omitted if it matches the key's display layer. + * + * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: + * + * ``` + * "K_Q" + * + [K_Q] + * "K_Q+shift" + * + [K_Q SHIFT] + * ``` + * + * Useful when the active layer of an input-event is already known. + */ + @Enumerable + public get coreID(): string { + if(typeof this.id === 'undefined') { + return undefined; } - /** - * Converts key IDs of the U_* form to their corresponding UTF-16 text. - * If an ID not matching the pattern is received, returns null. - * @param id - * @returns - */ - static unicodeIDToText(id: string, errorCallback?: (codeAsString: string) => void) { - if(!id || id.substring(0,2) != 'U_') { - return null; - } + let baseID = this.id || ''; - let result = ''; - const codePoints = id.substring(2).split('_'); - for(let codePoint of codePoints) { - const codePointValue = parseInt(codePoint, 16); - if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || - ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) || - isNaN(codePointValue)) { - if(errorCallback) { - errorCallback(codePoint); - } - continue; - } else { - // String.fromCharCode() is inadequate to handle the entire range of Unicode - // Someday after upgrading to ES2015, can use String.fromCodePoint() - result += String.kmwFromCharCode(codePointValue); - } - } - return result ? result : null; + if(this.displayLayer != this.layer) { + baseID = baseID + '+' + this.layer; } - static sanitize(rawKey: LayoutKey) { - if(typeof rawKey.width == 'string') { - rawKey.width = parseInt(rawKey.width, 10); - } - // Handles NaN cases as well as 'set to 0' cases; both are intentional here. - rawKey.width ||= ActiveKey.DEFAULT_KEY_WIDTH; + return baseID; + } - if(typeof rawKey.pad == 'string') { - rawKey.pad = parseInt(rawKey.pad, 10); - } - rawKey.pad ||= ActiveKey.DEFAULT_PAD; + /** + * A keyboard-unique identifier to be used for any display elements representing this key + * in user interfaces and/or on-screen keyboards. + * + * Distinguishes between otherwise-identical keys on different layers of an OSK. + * Includes identifying information about the key's display layer. + * + * Examples, given a 'default' display layer, matching keys to Keyman keyboard language: + * + * ``` + * "default-K_Q" + * + [K_Q] + * "default-K_Q+shift" + * + [K_Q SHIFT] + * ``` + * + * Useful when only the active keyboard is known about an input event. + */ + @Enumerable + public get elementID(): string { + if(typeof this.id === 'undefined') { + return undefined; + } - if(typeof rawKey.sp == 'string') { - rawKey.sp = Number.parseInt(rawKey.sp, 10) as ButtonClass; - } - rawKey.sp ||= 0; // The default button class. - } - - static polyfill(key: LayoutKey, layout: ActiveLayout, displayLayer: string) { - // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. - let dummy = new ActiveKey(); - let proto = Object.getPrototypeOf(dummy); - - for(let prop in dummy) { - if(!key.hasOwnProperty(prop)) { - let descriptor = Object.getOwnPropertyDescriptor(proto, prop); - if(descriptor) { - // It's a computed property! Copy the descriptor onto the key's object. - Object.defineProperty(key, prop, descriptor); - } else { - key[prop] = dummy[prop]; - } - } - } + return this.displayLayer + '-' + this.coreID; + } - // Ensure subkeys are also properly extended. - if(key.sk) { - for(let subkey of key.sk) { - ActiveKey.polyfill(subkey, layout, displayLayer); - } - } + @Enumerable + public get baseKeyEvent(): KeyEvent { + return new KeyEvent(this._baseKeyEvent); + } - let aKey = key as ActiveKey; - aKey.displayLayer = displayLayer; - aKey.layer = aKey.layer || displayLayer; - - // Compute the key's base KeyEvent properties for use in future event generation - aKey.constructBaseKeyEvent(layout, displayLayer); - } - - private constructBaseKeyEvent(layout: ActiveLayout, displayLayer: string) { - // Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling) - // Note - virtual keys should be treated case-insensitive, so we force uppercasing here. - let layer = this.layer || displayLayer || ''; - let keyName= this.id ? this.id.toUpperCase() : null; - - // Start: mirrors _GetKeyEventProperties - - - // First check the virtual key, and process shift, control, alt or function keys - var Lkc: text.KeyEvent = { - // Override key shift state if specified for key in layout (corrected for popup keys KMEW-93) - Lmodifiers: text.KeyboardProcessor.getModifierState(layer), - Lstates: text.KeyboardProcessor.getStateFromLayer(layer), - Lcode: keyName ? text.Codes.keyCodes[keyName] : 0, - LisVirtualKey: true, - vkCode: 0, - kName: keyName, - kLayer: layer, - kbdLayer: displayLayer, - kNextLayer: this.nextlayer, - device: null, - isSynthetic: true - }; - - if(layout.keyboard) { - let keyboard = layout.keyboard; - - // Include *limited* support for mnemonic keyboards (Sept 2012) - // If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices. - if(keyboard.isMnemonic && !(layout.isDefault && layout.formFactor != 'desktop')) { - if(Lkc.Lcode != text.Codes.keyCodes['K_SPACE']) { // exception required, March 2013 - // Jan 2019 - interesting that 'K_SPACE' also affects the caps-state check... - Lkc.vkCode = Lkc.Lcode; - this.isMnemonic = true; - } - } else { - Lkc.vkCode=Lkc.Lcode; - } + /** + * Converts key IDs of the U_* form to their corresponding UTF-16 text. + * If an ID not matching the pattern is received, returns null. + * @param id + * @returns + */ + static unicodeIDToText(id: string, errorCallback?: (codeAsString: string) => void) { + if(!id || id.substring(0,2) != 'U_') { + return null; + } - // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic - if(!keyboard.definesPositionalOrMnemonic) { - // Not the best pattern, but currently safe - we don't look up any properties of any of the - // arguments in this use case, and the object's scope is extremely limited. - Lkc.Lcode = KeyMapping._USKeyCodeToCharCode(this.constructKeyEvent(null, null)); - Lkc.LisVirtualKey=false; + let result = ''; + const codePoints = id.substring(2).split('_'); + for(let codePoint of codePoints) { + const codePointValue = parseInt(codePoint, 16); + if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || + ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) || + isNaN(codePointValue)) { + if(errorCallback) { + errorCallback(codePoint); } + continue; + } else { + // String.fromCharCode() is inadequate to handle the entire range of Unicode + // Someday after upgrading to ES2015, can use String.fromCodePoint() + result += String.kmwFromCharCode(codePointValue); } + } + return result ? result : null; + } - this.baseKeyEvent = Lkc; + static sanitize(rawKey: LayoutKey) { + if(typeof rawKey.width == 'string') { + rawKey.width = parseInt(rawKey.width, 10); } + // Handles NaN cases as well as 'set to 0' cases; both are intentional here. + rawKey.width ||= ActiveKey.DEFAULT_KEY_WIDTH; - constructKeyEvent(keyboardProcessor: text.KeyboardProcessor, device: utils.DeviceSpec): text.KeyEvent { - // Make a deep copy of our preconstructed key event, filling it out from there. - let Lkc = utils.deepCopy(this.baseKeyEvent); - Lkc.device = device; + if(typeof rawKey.pad == 'string') { + rawKey.pad = parseInt(rawKey.pad, 10); + } + rawKey.pad ||= ActiveKey.DEFAULT_PAD; - if(this.isMnemonic) { - text.KeyboardProcessor.setMnemonicCode(Lkc, this.layer.indexOf('shift') != -1, keyboardProcessor ? keyboardProcessor.stateKeys['K_CAPS'] : false); - } + if(typeof rawKey.sp == 'string') { + rawKey.sp = Number.parseInt(rawKey.sp, 10) as ButtonClass; + } + rawKey.sp ||= 0; // The default button class. + } - // Performs common pre-analysis for both 'native' and 'embedded' OSK key & subkey input events. - // This part depends on the keyboard processor's active state. - if(keyboardProcessor) { - keyboardProcessor.setSyntheticEventDefaults(Lkc); - - // If it's a state key modifier, trigger its effects as part of the - // keystroke. - const bitmap = { - 'K_CAPS': text.Codes.stateBitmasks.CAPS, - 'K_NUMLOCK': text.Codes.stateBitmasks.NUM_LOCK, - 'K_SCROLL': text.Codes.stateBitmasks.SCROLL_LOCK - }; - const bitmask = bitmap[Lkc.kName]; - - if(bitmask) { - Lkc.Lstates ^= bitmask; - Lkc.LmodifierChange = true; + static polyfill(key: LayoutKey, keyboard: Keyboard, layout: ActiveLayout, displayLayer: string) { + // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. + let dummy = new ActiveKey(); + let proto = Object.getPrototypeOf(dummy); + + for(let prop in dummy) { + if(!key.hasOwnProperty(prop)) { + let descriptor = Object.getOwnPropertyDescriptor(proto, prop); + if(descriptor) { + // It's a computed property! Copy the descriptor onto the key's object. + Object.defineProperty(key, prop, descriptor); + } else { + key[prop] = dummy[prop]; } } - - return Lkc; } - public getSubkey(coreID: string): ActiveKey { - if(this.sk) { - for(let key of this.sk) { - if(key.coreID == coreID) { - return key; - } - } + // Ensure subkeys are also properly extended. + if(key.sk) { + for(let subkey of key.sk) { + ActiveKey.polyfill(subkey, keyboard, layout, displayLayer); } - - return null; } - } - export class ActiveRow implements LayoutRow { - // Identify key labels (e.g. *Shift*) that require the special OSK font - static readonly SPECIAL_LABEL=/\*\w+\*/; + let aKey = key as ActiveKey; + aKey.displayLayer = displayLayer; + aKey.layer = aKey.layer || displayLayer; - id: number; - key: ActiveKey[]; + // Compute the key's base KeyEvent properties for use in future event generation + aKey.constructBaseKeyEvent(keyboard, layout, displayLayer); + } - /** - * Used for calculating fat-fingering offsets. - */ - proportionalY: number; + private constructBaseKeyEvent(keyboard: Keyboard, layout: ActiveLayout, displayLayer: string) { + // Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling) + // Note - virtual keys should be treated case-insensitive, so we force uppercasing here. + let layer = this.layer || displayLayer || ''; + let keyName= this.id ? this.id.toUpperCase() : null; + + // Start: mirrors _GetKeyEventProperties + + // First check the virtual key, and process shift, control, alt or function keys + let props: KeyEventSpec = { + // Override key shift state if specified for key in layout (corrected for popup keys KMEW-93) + Lmodifiers: Codes.getModifierState(layer), + Lstates: Codes.getStateFromLayer(layer), + Lcode: keyName ? Codes.keyCodes[keyName] : 0, + LisVirtualKey: true, + vkCode: 0, + kName: keyName, + kLayer: layer, + kbdLayer: displayLayer, + kNextLayer: this.nextlayer, + device: null, + isSynthetic: true + }; - private constructor() { + let Lkc: KeyEvent = new KeyEvent(props); - } + if(layout.keyboard) { + let keyboard = layout.keyboard; - static sanitize(rawRow: LayoutRow) { - for(const key of rawRow.key) { - // Test for a trailing comma included in spec, added as null object by IE - // It has only ever appeared at the end of a row's spec. - if(key == null) { - rawRow.key.length = rawRow.key.length-1; - } else { - ActiveKey.sanitize(key); + // Include *limited* support for mnemonic keyboards (Sept 2012) + // If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices. + if(keyboard.isMnemonic && !(layout.isDefault && layout.formFactor != 'desktop')) { + if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { // exception required, March 2013 + // Jan 2019 - interesting that 'K_SPACE' also affects the caps-state check... + Lkc.vkCode = Lkc.Lcode; + this.isMnemonic = true; } + } else { + Lkc.vkCode=Lkc.Lcode; } - if(typeof rawRow.id == 'string') { - rawRow.id = Number.parseInt(rawRow.id, 10); + // Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic + if(!keyboard.definesPositionalOrMnemonic) { + // Not the best pattern, but currently safe - we don't look up any properties of any of the + // arguments in this use case, and the object's scope is extremely limited. + Lkc.Lcode = KeyMapping._USKeyCodeToCharCode(keyboard.constructKeyEvent(null, null, { + K_CAPS: false, + K_NUMLOCK: false, + K_SCROLL: false + })); + Lkc.LisVirtualKey=false; } } - static polyfill(row: LayoutRow, layout: ActiveLayout, displayLayer: string, totalWidth: number, proportionalY: number) { - // Apply defaults, setting the width and other undefined properties for each key - let keys=row['key']; - for(let j=0; j 0) { - const finalKey = keys[keys.length-1] as ActiveKey; + } - // If a single key, and padding is negative, add padding to right align the key - if(keys.length == 1 && finalKey.pad < 0) { - const keyPercent = finalKey.width/totalWidth; - const padPercent = 1-(totalPercent + keyPercent + rightMargin); + static sanitize(rawRow: LayoutRow) { + for(const key of rawRow.key) { + // Test for a trailing comma included in spec, added as null object by IE + // It has only ever appeared at the end of a row's spec. + if(key == null) { + rawRow.key.length = rawRow.key.length-1; + } else { + ActiveKey.sanitize(key); + } + } - // compute center's default x-coord (used in headless modes) - setProportions(finalKey, padPercent, keyPercent, totalPercent); - } else { - const padPercent = finalKey.pad/totalWidth; - const keyPercent = 1-(totalPercent + padPercent + rightMargin); + if(typeof rawRow.id == 'string') { + rawRow.id = Number.parseInt(rawRow.id, 10); + } + } - // compute center's default x-coord (used in headless modes) - setProportions(finalKey, padPercent, keyPercent, totalPercent); + static polyfill(row: LayoutRow, keyboard: Keyboard, layout: ActiveLayout, displayLayer: string, totalWidth: number, proportionalY: number) { + // Apply defaults, setting the width and other undefined properties for each key + let keys=row['key']; + for(let j=0; j 0) { + const finalKey = keys[keys.length-1] as ActiveKey; - constructor() { + // If a single key, and padding is negative, add padding to right align the key + if(keys.length == 1 && finalKey.pad < 0) { + const keyPercent = finalKey.width/totalWidth; + const padPercent = 1-(totalPercent + keyPercent + rightMargin); + // compute center's default x-coord (used in headless modes) + setProportions(finalKey, padPercent, keyPercent, totalPercent); + } else { + const padPercent = finalKey.pad/totalWidth; + const keyPercent = 1-(totalPercent + padPercent + rightMargin); + + // compute center's default x-coord (used in headless modes) + setProportions(finalKey, padPercent, keyPercent, totalPercent); + } } - static sanitize(rawLayer: LayoutLayer) { - for(const row of rawLayer.row) { - ActiveRow.sanitize(row); + // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. + let dummy = new ActiveRow(); + for(let key in dummy) { + if(!row.hasOwnProperty(key)) { + row[key] = dummy[key]; } } - static polyfill(layer: LayoutLayer, layout: ActiveLayout) { - layer.aligned=false; + let aRow = row as ActiveRow; + aRow.proportionalY = proportionalY; + } - // Create a DIV for each row of the group - let rows=layer['row']; + populateKeyMap(map: {[keyId: string]: ActiveKey}) { + this.key.forEach(function(key: ActiveKey) { + if(key.coreID) { + map[key.coreID] = key; + } + }); + } +} - // Calculate the maximum row width (in layout units) - let totalWidth=0; - for(const row of rows) { - let width=0; - const keys=row['key']; +export class ActiveLayer implements LayoutLayer { + row: ActiveRow[]; + id: string; - for(const key of keys) { - // So long as `sanitize` is called first, these coercions are safe. - width += (key.width as number) + (key.pad as number); - } + // These already exist on the objects, pre-polyfill... + // but they still need to be proactively declared on this type. + capsKey?: ActiveKey; + numKey?: ActiveKey; + scrollKey?: ActiveKey; - if(width > totalWidth) { - totalWidth = width; - } - } + totalWidth: number; - // Add default right margin - if(layout.formFactor == 'desktop') { - totalWidth += 5; // TODO: resolve difference between touch and desktop; why don't we use ActiveKey.DEFAULT_RIGHT_MARGIN? - } else { - totalWidth += ActiveKey.DEFAULT_RIGHT_MARGIN; - } + defaultKeyProportionalWidth: number; + rowProportionalHeight: number; - let rowCount = layer.row.length; - for(let i=0; i probability, use a function parameter in place - // of the formula in the loop below. - for(let key in keyDists) { - totalMass += keyProbs[key] = 1 / (Math.pow(keyDists[key], 2) + 1e-6); // Prevent div-by-0 errors. + if(width > totalWidth) { + totalWidth = width; } + } + + // Add default right margin + if(layout.formFactor == 'desktop') { + totalWidth += 5; // TODO: resolve difference between touch and desktop; why don't we use ActiveKey.DEFAULT_RIGHT_MARGIN? + } else { + totalWidth += ActiveKey.DEFAULT_RIGHT_MARGIN; + } - for(let key in keyProbs) { - keyProbs[key] /= totalMass; + let rowCount = layer.row.length; + for(let i=0; i probability, use a function parameter in place + // of the formula in the loop below. + for(let key in keyDists) { + totalMass += keyProbs[key] = 1 / (Math.pow(keyDists[key], 2) + 1e-6); // Prevent div-by-0 errors. + } + + for(let key in keyProbs) { + keyProbs[key] /= totalMass; + } - let keyDists: {[keyId: string]: number} = {}; + return keyProbs; + } - // This double-nested loop computes a pseudo-distance for the touch from each key. Quite useful for - // generating a probability distribution. - this.row.forEach(function(row: ActiveRow): void { - row.key.forEach(function(key: ActiveKey): void { - // If the key lacks an ID, just skip it. Sometimes used for padding. - if(!key.baseKeyID) { + /** + * Computes a squared 'pseudo-distance' for the touch from each key. (Not a proper metric.) + * Intended for use in generating a probability distribution over the keys based on the touch input. + * @param touchCoords A proportional (x, y) coordinate of the touch within the keyboard's geometry. + * Should be within [0, 0] to [1, 1]. + * @param kbdScaleRatio The ratio of the keyboard's horizontal scale to its vertical scale. + * For a 400 x 200 keyboard, should be 2. + */ + private keyTouchDistances(touchCoords: {x: number, y: number}, kbdScaleRatio: number): {[keyId: string]: number} { + let layer = this; + + let keyDists: {[keyId: string]: number} = {}; + + // This double-nested loop computes a pseudo-distance for the touch from each key. Quite useful for + // generating a probability distribution. + this.row.forEach(function(row: ActiveRow): void { + row.key.forEach(function(key: ActiveKey): void { + // If the key lacks an ID, just skip it. Sometimes used for padding. + if(!key.baseKeyID) { + return; + } else { + // Attempt to filter out known non-output keys. + // Results in a more optimized distribution. + if(Codes.isKnownOSKModifierKey(key.baseKeyID)) { + return; + } else if(key.isPadding) { // to the user, blank / padding keys do not exist. return; - } else { - // Attempt to filter out known non-output keys. - // Results in a more optimized distribution. - if(text.Codes.isKnownOSKModifierKey(key.baseKeyID)) { - return; - } else if(key.isPadding) { // to the user, blank / padding keys do not exist. - return; - } - } - // These represent the within-key distance of the touch from the key's center. - // Both should be on the interval [0, 0.5]. - let dx = Math.abs(touchCoords.x - key.proportionalX); - let dy = Math.abs(touchCoords.y - row.proportionalY); - - // If the touch isn't within the key, these store the out-of-key distance - // from the closest point on the key being checked. - let distX: number, distY: number; - - if(dx > 0.5 * key.proportionalWidth) { - distX = (dx - 0.5 * key.proportionalWidth); - dx = 0.5; - } else { - distX = 0; - dx /= key.proportionalWidth; } + } + // These represent the within-key distance of the touch from the key's center. + // Both should be on the interval [0, 0.5]. + let dx = Math.abs(touchCoords.x - key.proportionalX); + let dy = Math.abs(touchCoords.y - row.proportionalY); + + // If the touch isn't within the key, these store the out-of-key distance + // from the closest point on the key being checked. + let distX: number, distY: number; + + if(dx > 0.5 * key.proportionalWidth) { + distX = (dx - 0.5 * key.proportionalWidth); + dx = 0.5; + } else { + distX = 0; + dx /= key.proportionalWidth; + } - if(dy > 0.5 * layer.rowProportionalHeight) { - distY = (dy - 0.5 * layer.rowProportionalHeight); - dy = 0.5; - } else { - distY = 0; - dy /= layer.rowProportionalHeight; - } + if(dy > 0.5 * layer.rowProportionalHeight) { + distY = (dy - 0.5 * layer.rowProportionalHeight); + dy = 0.5; + } else { + distY = 0; + dy /= layer.rowProportionalHeight; + } - // Now that the differentials are computed, it's time to do distance scaling. - // - // For out-of-key distance, we scale the X component by the keyboard's aspect ratio - // to get the actual out-of-key distance rather than proportional. - distX *= kbdScaleRatio; - - // While the keys are rarely perfect squares, we map all within-key distance - // to a square shape. (ALT/CMD should seem as close to SPACE as a 'B'.) - // - // For that square, we take the rowHeight as its edge lengths. - distX += dx * layer.rowProportionalHeight; - distY += dy * layer.rowProportionalHeight; - - let distance = distX * distX + distY * distY; - keyDists[key.coreID] = distance; - }); + // Now that the differentials are computed, it's time to do distance scaling. + // + // For out-of-key distance, we scale the X component by the keyboard's aspect ratio + // to get the actual out-of-key distance rather than proportional. + distX *= kbdScaleRatio; + + // While the keys are rarely perfect squares, we map all within-key distance + // to a square shape. (ALT/CMD should seem as close to SPACE as a 'B'.) + // + // For that square, we take the rowHeight as its edge lengths. + distX += dx * layer.rowProportionalHeight; + distY += dy * layer.rowProportionalHeight; + + let distance = distX * distX + distY * distY; + keyDists[key.coreID] = distance; }); + }); - return keyDists; - } + return keyDists; + } - getKey(keyId: string) { - // Keys usually are specified in a "long form" prefixed with their layer's ID. - if(keyId.indexOf(this.id + '-') == 0) { - keyId = keyId.replace(this.id + '-', ''); - } + getKey(keyId: string) { + // Keys usually are specified in a "long form" prefixed with their layer's ID. + if(keyId.indexOf(this.id + '-') == 0) { + keyId = keyId.replace(this.id + '-', ''); + } - let idComponents = keyId.split('::'); - if(idComponents.length > 1) { - let baseKey = this.keyMap[idComponents[0]]; - return baseKey.getSubkey(idComponents[1]); - } else { - return this.keyMap[keyId]; - } + let idComponents = keyId.split('::'); + if(idComponents.length > 1) { + let baseKey = this.keyMap[idComponents[0]]; + return baseKey.getSubkey(idComponents[1]); + } else { + return this.keyMap[keyId]; } } +} - export class ActiveLayout implements LayoutFormFactor{ - layer: ActiveLayer[]; - font: string; - keyLabels: boolean; - isDefault?: boolean; - keyboard: Keyboard; - formFactor: utils.FormFactor; - - /** - * Facilitates mapping layer id strings to their specification objects. - */ - layerMap: {[layerId: string]: ActiveLayer}; - - private constructor() { - - } - - getLayer(layerId: string): ActiveLayer { - return this.layerMap[layerId]; - } - - /** - * Refer to https://github.com/keymanapp/keyman/issues/254, which mentions - * KD-11 from a prior issue-tracking system from the closed-source days that - * resulted in an unintended extra empty row. - * - * It'll be pretty rare to see a keyboard affected by the bug, but we don't - * 100% control all keyboards out there, so it's best we make sure the edge - * case is covered. - * - * @param layers The layer group to be loaded for the form factor. Will be - * mutated by this operation. - */ - static correctLayerEmptyRowBug(layers: LayoutLayer[]) { - for(let n=0; n=0; i--) { - if(!Array.isArray(rows[i]['key']) || rows[i]['key'].length == 0) { - rows.splice(i, 1) - } +export class ActiveLayout implements LayoutFormFactor{ + layer: ActiveLayer[]; + font: string; + keyLabels: boolean; + isDefault?: boolean; + keyboard: Keyboard; + formFactor: DeviceSpec.FormFactor; + + /** + * Facilitates mapping layer id strings to their specification objects. + */ + layerMap: {[layerId: string]: ActiveLayer}; + + private constructor() { + + } + + getLayer(layerId: string): ActiveLayer { + return this.layerMap[layerId]; + } + + /** + * Refer to https://github.com/keymanapp/keyman/issues/254, which mentions + * KD-11 from a prior issue-tracking system from the closed-source days that + * resulted in an unintended extra empty row. + * + * It'll be pretty rare to see a keyboard affected by the bug, but we don't + * 100% control all keyboards out there, so it's best we make sure the edge + * case is covered. + * + * @param layers The layer group to be loaded for the form factor. Will be + * mutated by this operation. + */ + static correctLayerEmptyRowBug(layers: LayoutLayer[]) { + for(let n=0; n=0; i--) { + if(!Array.isArray(rows[i]['key']) || rows[i]['key'].length == 0) { + rows.splice(i, 1) } } } + } - static sanitize(rawLayout: LayoutFormFactor) { - ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer); + static sanitize(rawLayout: LayoutFormFactor) { + ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer); - for(const layer of rawLayout.layer) { - ActiveLayer.sanitize(layer); - } + for(const layer of rawLayout.layer) { + ActiveLayer.sanitize(layer); } + } - /** - * - * @param layout - * @param formFactor - */ - static polyfill(layout: LayoutFormFactor, keyboard: Keyboard, formFactor: utils.FormFactor): ActiveLayout { - if(layout == null) { - throw new Error("Cannot build an ActiveLayout for a null specification."); - } + /** + * + * @param layout + * @param formFactor + */ + static polyfill(layout: LayoutFormFactor, keyboard: Keyboard, formFactor: DeviceSpec.FormFactor): ActiveLayout { + if(layout == null) { + throw new Error("Cannot build an ActiveLayout for a null specification."); + } - /* Standardize the layout object's data types. - * - * In older versions of KMW, some numeric properties were long represented as strings instead, - * and that lives on within a _lot_ of keyboards. The data should be sanitized before it - * is processed by this method. - */ - this.sanitize(layout); - - // Create a separate OSK div for each OSK layer, only one of which will ever be visible - var n: number; - let layerMap: {[layerId: string]: ActiveLayer} = {}; - - let layers=layout.layer; - - // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. - let dummy = new ActiveLayout(); - for(let key in dummy) { - if(!layout.hasOwnProperty(key)) { - layout[key] = dummy[key]; - } - } + /* Standardize the layout object's data types. + * + * In older versions of KMW, some numeric properties were long represented as strings instead, + * and that lives on within a _lot_ of keyboards. The data should be sanitized before it + * is processed by this method. + */ + this.sanitize(layout); - let aLayout = layout as ActiveLayout; - aLayout.keyboard = keyboard; - aLayout.formFactor = formFactor; + // Create a separate OSK div for each OSK layer, only one of which will ever be visible + var n: number; + let layerMap: {[layerId: string]: ActiveLayer} = {}; - for(n=0; n?~~~~~ '; + + static readonly DEFAULT_RAW_SPEC = {'F':'Tahoma', 'BK': Layouts.dfltText}; + + // Cross-reference with the ids in osk.setButtonClass. + static buttonClasses: {[name: string]: ButtonClass} = { + 'DEFAULT':0, + 'SHIFT':1, + 'SHIFT-ON':2, + 'SPECIAL':3, + 'SPECIAL-ON':4, + 'DEADKEY':8, + 'BLANK':9, + 'HIDDEN':10 + }; - export type LayoutSpec = { - "desktop"?: LayoutFormFactor, - "phone"?: LayoutFormFactor, - "tablet"?: LayoutFormFactor - } + static modifierSpecials = { + 'leftalt': '*LAlt*', + 'rightalt': '*RAlt*', + 'alt': '*Alt*', + 'leftctrl': '*LCtrl*', + 'rightctrl': '*RCtrl*', + 'ctrl': '*Ctrl*', + 'ctrl-alt': '*AltGr*', + 'leftctrl-leftalt': '*LAltCtrl*', + 'rightctrl-rightalt': '*RAltCtrl*', + 'leftctrl-leftalt-shift': '*LAltCtrlShift*', + 'rightctrl-rightalt-shift': '*RAltCtrlShift*', + 'shift': '*Shift*', + 'shift-alt': '*AltShift*', + 'shift-ctrl': '*CtrlShift*', + 'shift-ctrl-alt': '*AltCtrlShift*', + 'leftalt-shift': '*LAltShift*', + 'rightalt-shift': '*RAltShift*', + 'leftctrl-shift': '*LCtrlShift*', + 'rightctrl-shift': '*RCtrlShift*' + }; - // This class manages default layout construction for consumption by OSKs without a specified layout. - export class Layouts { - static dfltCodes=[ - "K_BKQUOTE","K_1","K_2","K_3","K_4","K_5","K_6","K_7","K_8","K_9","K_0", - "K_HYPHEN","K_EQUAL","K_*","K_*","K_*","K_Q","K_W","K_E","K_R","K_T", - "K_Y","K_U","K_I","K_O","K_P","K_LBRKT","K_RBRKT","K_BKSLASH","K_*", - "K_*","K_*","K_A","K_S","K_D","K_F","K_G","K_H","K_J","K_K","K_L", - "K_COLON","K_QUOTE","K_*","K_*","K_*","K_*","K_*","K_oE2", - "K_Z","K_X","K_C","K_V","K_B","K_N","K_M","K_COMMA","K_PERIOD", - "K_SLASH","K_*","K_*","K_*","K_*","K_*","K_SPACE" - ]; - - static dfltText='`1234567890-=\xA7~~qwertyuiop[]\\~~~asdfghjkl;\'~~~~~?zxcvbnm,./~~~~~ ' - +'~!@#$%^&*()_+\xA7~~QWERTYUIOP{}\\~~~ASDFGHJKL:"~~~~~?ZXCVBNM<>?~~~~~ '; - - static readonly DEFAULT_RAW_SPEC = {'F':'Tahoma', 'BK': Layouts.dfltText}; - - // Cross-reference with the ids in osk.setButtonClass. - static buttonClasses: {[name: string]: ButtonClass} = { - 'DEFAULT':0, - 'SHIFT':1, - 'SHIFT-ON':2, - 'SPECIAL':3, - 'SPECIAL-ON':4, - 'DEADKEY':8, - 'BLANK':9, - 'HIDDEN':10 - }; - - static modifierSpecials = { - 'leftalt': '*LAlt*', - 'rightalt': '*RAlt*', - 'alt': '*Alt*', - 'leftctrl': '*LCtrl*', - 'rightctrl': '*RCtrl*', - 'ctrl': '*Ctrl*', - 'ctrl-alt': '*AltGr*', - 'leftctrl-leftalt': '*LAltCtrl*', - 'rightctrl-rightalt': '*RAltCtrl*', - 'leftctrl-leftalt-shift': '*LAltCtrlShift*', - 'rightctrl-rightalt-shift': '*RAltCtrlShift*', - 'shift': '*Shift*', - 'shift-alt': '*AltShift*', - 'shift-ctrl': '*CtrlShift*', - 'shift-ctrl-alt': '*AltCtrlShift*', - 'leftalt-shift': '*LAltShift*', - 'rightalt-shift': '*RAltShift*', - 'leftctrl-shift': '*LCtrlShift*', - 'rightctrl-shift': '*RCtrlShift*' - }; - - /** - * Build a default layout for keyboards with no explicit layout - * - * @param {Object} PVK raw specifications - * @param {Keyboard} keyboard keyboard object (as loaded) - * @param {string} formFactor (really utils.FormFactor) - * @return {LayoutFormFactor} - */ - static buildDefaultLayout(PVK, keyboard: Keyboard, formFactor: string): LayoutFormFactor { - // Build a layout using the default for the device - var layoutType=formFactor; - - if(typeof Layouts.dfltLayout[layoutType] != 'object') { - layoutType = 'desktop'; - } + /** + * Build a default layout for keyboards with no explicit layout + * + * @param {Object} PVK raw specifications + * @param {Keyboard} keyboard keyboard object (as loaded) + * @param {string} formFactor (really utils.FormFactor) + * @return {LayoutFormFactor} + */ + static buildDefaultLayout(PVK, keyboard: Keyboard, formFactor: string): LayoutFormFactor { + // Build a layout using the default for the device + var layoutType=formFactor; + + if(typeof Layouts.dfltLayout[layoutType] != 'object') { + layoutType = 'desktop'; + } - let kbdBitmask = Codes.modifierBitmasks['NON_CHIRAL']; - // An unfortunate dependency there. Should probably also set a version within web-core for use. - let kbdDevVersion = utils.Version.CURRENT; - if(keyboard) { - kbdBitmask = keyboard.modifierBitmask; - kbdDevVersion = keyboard.compilerVersion; - } + let kbdBitmask = Codes.modifierBitmasks['NON_CHIRAL']; + // An unfortunate dependency there. Should probably also set a version within web-core for use. + let kbdDevVersion = Version.CURRENT; + if(keyboard) { + kbdBitmask = keyboard.modifierBitmask; + kbdDevVersion = keyboard.compilerVersion; + } - if(!PVK) { - PVK = this.DEFAULT_RAW_SPEC; - } + if(!PVK) { + PVK = this.DEFAULT_RAW_SPEC; + } - // Clone the default layout object for this device - var layout: LayoutFormFactor = utils.deepCopy(Layouts.dfltLayout[layoutType]); + // Clone the default layout object for this device + var layout: LayoutFormFactor = deepCopy(Layouts.dfltLayout[layoutType]); - var n,layers=layout['layer'], keyLabels: KLS=PVK['KLS'], key102=PVK['K102']; - var i, j, k, m, row, rows: LayoutRow[], key: LayoutKey, keys: LayoutKey[]; - var chiral: boolean = (kbdBitmask & Codes.modifierBitmasks.IS_CHIRAL) != 0; + var n,layers=layout['layer'], keyLabels: KLS=PVK['KLS'], key102=PVK['K102']; + var i, j, k, m, row, rows: LayoutRow[], key: LayoutKey, keys: LayoutKey[]; + var chiral: boolean = (kbdBitmask & Codes.modifierBitmasks.IS_CHIRAL) != 0; - if(PVK['F']) { - // The KeymanWeb compiler generates a string of the format `[italic ][bold ] 1em ""` - // We will ignore the bold, italic and font size spec - let legacyFontSpec = /^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) "(.+)"$/.exec(PVK['F']); - if(legacyFontSpec) { - layout.font = legacyFontSpec[1]; - } + if(PVK['F']) { + // The KeymanWeb compiler generates a string of the format `[italic ][bold ] 1em ""` + // We will ignore the bold, italic and font size spec + let legacyFontSpec = /^(?:(?:italic|bold) )* *[0-9.eE-]+(?:[a-z]+) "(.+)"$/.exec(PVK['F']); + if(legacyFontSpec) { + layout.font = legacyFontSpec[1]; } + } - var kmw10Plus = !(typeof keyLabels == 'undefined' || !keyLabels); - if(!kmw10Plus) { - // Save the processed key label information to the keyboard's general data. - // Makes things more efficient elsewhere and for reloading after keyboard swaps. - keyLabels = PVK['KLS'] = Layouts.processLegacyDefinitions(PVK['BK']); - } + var kmw10Plus = !(typeof keyLabels == 'undefined' || !keyLabels); + if(!kmw10Plus) { + // Save the processed key label information to the keyboard's general data. + // Makes things more efficient elsewhere and for reloading after keyboard swaps. + keyLabels = PVK['KLS'] = Layouts.processLegacyDefinitions(PVK['BK']); + } - // Identify key labels (e.g. *Shift*) that require the special OSK font - var specialLabel=/\*\w+\*/; + // Identify key labels (e.g. *Shift*) that require the special OSK font + var specialLabel=/\*\w+\*/; - // *** Step 1: instantiate the layer objects. *** + // *** Step 1: instantiate the layer objects. *** - // Get the list of valid layers, enforcing that the 'default' layer must be the first one processed. - var validIdList = Object.getOwnPropertyNames(keyLabels), invalidIdList = []; - validIdList.splice(validIdList.indexOf('default'), 1); - validIdList = [ 'default' ].concat(validIdList); + // Get the list of valid layers, enforcing that the 'default' layer must be the first one processed. + var validIdList = Object.getOwnPropertyNames(keyLabels), invalidIdList = []; + validIdList.splice(validIdList.indexOf('default'), 1); + validIdList = [ 'default' ].concat(validIdList); - // Automatic AltGr emulation if the 'leftctrl-leftalt' layer is otherwise undefined. - if(keyboard && keyboard.emulatesAltGr) { - // We insert only the layers that need to be emulated. - if((validIdList.indexOf('leftctrl-leftalt') == -1) && validIdList.indexOf('rightalt') != -1) { - validIdList.push('leftctrl-leftalt'); - keyLabels['leftctrl-leftalt'] = keyLabels['rightalt']; - } + // Automatic AltGr emulation if the 'leftctrl-leftalt' layer is otherwise undefined. + if(keyboard && keyboard.emulatesAltGr) { + // We insert only the layers that need to be emulated. + if((validIdList.indexOf('leftctrl-leftalt') == -1) && validIdList.indexOf('rightalt') != -1) { + validIdList.push('leftctrl-leftalt'); + keyLabels['leftctrl-leftalt'] = keyLabels['rightalt']; + } - if((validIdList.indexOf('leftctrl-leftalt-shift') == -1) && validIdList.indexOf('rightalt-shift') != -1) { - validIdList.push('leftctrl-leftalt-shift'); - keyLabels['leftctrl-leftalt-shift'] = keyLabels['rightalt-shift']; - } + if((validIdList.indexOf('leftctrl-leftalt-shift') == -1) && validIdList.indexOf('rightalt-shift') != -1) { + validIdList.push('leftctrl-leftalt-shift'); + keyLabels['leftctrl-leftalt-shift'] = keyLabels['rightalt-shift']; } + } - // If there is no predefined layout, even touch layouts will follow the desktop's - // setting for the displayUnderlying flag. As the desktop layout uses a different - // format for its layout spec, that's found at the field referenced below. - layout["displayUnderlying"] = keyboard ? !!keyboard.scriptObject['KDU'] : false; + // If there is no predefined layout, even touch layouts will follow the desktop's + // setting for the displayUnderlying flag. As the desktop layout uses a different + // format for its layout spec, that's found at the field referenced below. + layout["displayUnderlying"] = keyboard ? !!keyboard.scriptObject['KDU'] : false; - // For desktop devices, we must create all layers, even if invalid. - if(formFactor == 'desktop') { - invalidIdList = Layouts.generateLayerIds(chiral); + // For desktop devices, we must create all layers, even if invalid. + if(formFactor == 'desktop') { + invalidIdList = Layouts.generateLayerIds(chiral); - // Filter out all ids considered valid. (We also don't want duplicates in the following list...) - for(n=0; n 0) { - layers[n]=utils.deepCopy(layers[0]); - } - layers[n]['id']=idList[n]; - layers[n]['nextlayer']=idList[n]; // This would only be different for a dynamic keyboard - - // Extraced into a helper method to improve readability. - Layouts.formatDefaultLayer(layers[n], chiral, formFactor, !!key102); + for(n=0; n 0) { + layers[n]=deepCopy(layers[0]); } + layers[n]['id']=idList[n]; + layers[n]['nextlayer']=idList[n]; // This would only be different for a dynamic keyboard - // *** Step 2: Layer objects now exist; time to fill them with the appropriate key labels and key styles *** - for(n=0; n= 0 && kx < layerSpec.length) key['text']=layerSpec[kx]; - } + // Extraced into a helper method to improve readability. + Layouts.formatDefaultLayer(layers[n], chiral, formFactor, !!key102); + } - // Legacy (pre 12.0) behavior: fall back to US English keycap text as default for the base two layers - // if a key cap is not otherwise defined. (Any intentional 'ghost' keys must be explicitly defined.) - if(isDefault && kbdDevVersion.precedes(utils.Version.NO_DEFAULT_KEYCAPS)) { - if(key['id'] != 'K_SPACE' && kx+65 * isShift < Layouts.dfltText.length && key['text'] !== null) { - key['text'] = key['text'] || Layouts.dfltText[kx+65*isShift]; - } - } + // *** Step 2: Layer objects now exist; time to fill them with the appropriate key labels and key styles *** + for(n=0; n= 0 && kx < layerSpec.length) key['text']=layerSpec[kx]; } - // Leave any unmarked key caps as null strings - if(key['text'] !== null) { - key['text'] = key['text'] || ''; + // Legacy (pre 12.0) behavior: fall back to US English keycap text as default for the base two layers + // if a key cap is not otherwise defined. (Any intentional 'ghost' keys must be explicitly defined.) + if(isDefault && kbdDevVersion.precedes(Version.NO_DEFAULT_KEYCAPS)) { + if(key['id'] != 'K_SPACE' && kx+65 * isShift < Layouts.dfltText.length && key['text'] !== null) { + key['text'] = key['text'] || Layouts.dfltText[kx+65*isShift]; + } } + } - // Detect important tracking keys. - switch(key['id']) { - case "K_SHIFT": - shiftKey=key; - break; - case "K_TAB": - nextKey=key; - break; - case "K_CAPS": - capsKey=key; - break; - case "K_NUMLOCK": - numKey=key; - break; - case "K_SCROLL": - scrollKey=key; - break; - } + // Leave any unmarked key caps as null strings + if(key['text'] !== null) { + key['text'] = key['text'] || ''; + } - // Remove pop-up shift keys referencing invalid layers (Build 349) - if(key['sk'] != null) { - for(k=0; k 0 && shiftKey != null) { - shiftKey['sp']=Layouts.buttonClasses['SHIFT-ON']; - shiftKey['sk']=null; - shiftKey['text'] = Layouts.modifierSpecials[layers[n].id] ? Layouts.modifierSpecials[layers[n].id] : "*Shift*"; + if(key['sk'].length == 0) { + key['sk']=null; + } } } } - return layout; - } - - /** - * Function getLayerId - * Scope Private - * @param {number} m shift modifier code - * @return {string} layer string from shift modifier code (desktop keyboards) - * Description Get name of layer from code, where the modifer order is determined by ascending bit-flag value. - */ - static getLayerId(m: number): string { - let modifierCodes = Codes.modifierCodes; - - var s=''; - if(m == 0) { - return 'default'; - } else { - if(m & modifierCodes['LCTRL']) { - s = (s.length > 0 ? s + '-' : '') + 'leftctrl'; - } - if(m & modifierCodes['RCTRL']) { - s = (s.length > 0 ? s + '-' : '') + 'rightctrl'; + // We're done with the layer keys initialization pass. Time to do post-analysis layer-level init where necessary. + layer.shiftKey=shiftKey; + layer.capsKey=capsKey; + layer.numKey=numKey; + layer.scrollKey=scrollKey; + + // Set modifier key appearance and behaviour for non-desktop devices using the default layout + if(formFactor != 'desktop') { + if(n > 0 && shiftKey != null) { + shiftKey['sp']=Layouts.buttonClasses['SHIFT-ON']; + shiftKey['sk']=null; + shiftKey['text'] = Layouts.modifierSpecials[layers[n].id] ? Layouts.modifierSpecials[layers[n].id] : "*Shift*"; } - if(m & modifierCodes['LALT']) { - s = (s.length > 0 ? s + '-' : '') + 'leftalt'; - } - if(m & modifierCodes['RALT']) { - s = (s.length > 0 ? s + '-' : '') + 'rightalt'; - } - if(m & modifierCodes['SHIFT']) { - s = (s.length > 0 ? s + '-' : '') + 'shift'; - } - if(m & modifierCodes['CTRL']) { - s = (s.length > 0 ? s + '-' : '') + 'ctrl'; - } - if(m & modifierCodes['ALT']) { - s = (s.length > 0 ? s + '-' : '') + 'alt'; - } - return s; } } - /** - * Generates a list of potential layer ids for the specified chirality mode. - * - * @param {boolean} chiral // Does the keyboard use chiral modifiers or not? - */ - static generateLayerIds(chiral: boolean): string[] { - var layerCnt, offset; - - if(chiral) { - layerCnt=32; - offset=0x01; - } else { - layerCnt=8; - offset=0x10; + return layout; + } + + /** + * Function getLayerId + * Scope Private + * @param {number} m shift modifier code + * @return {string} layer string from shift modifier code (desktop keyboards) + * Description Get name of layer from code, where the modifer order is determined by ascending bit-flag value. + */ + static getLayerId(m: number): string { + let modifierCodes = Codes.modifierCodes; + + var s=''; + if(m == 0) { + return 'default'; + } else { + if(m & modifierCodes['LCTRL']) { + s = (s.length > 0 ? s + '-' : '') + 'leftctrl'; } + if(m & modifierCodes['RCTRL']) { + s = (s.length > 0 ? s + '-' : '') + 'rightctrl'; + } + if(m & modifierCodes['LALT']) { + s = (s.length > 0 ? s + '-' : '') + 'leftalt'; + } + if(m & modifierCodes['RALT']) { + s = (s.length > 0 ? s + '-' : '') + 'rightalt'; + } + if(m & modifierCodes['SHIFT']) { + s = (s.length > 0 ? s + '-' : '') + 'shift'; + } + if(m & modifierCodes['CTRL']) { + s = (s.length > 0 ? s + '-' : '') + 'ctrl'; + } + if(m & modifierCodes['ALT']) { + s = (s.length > 0 ? s + '-' : '') + 'alt'; + } + return s; + } + } - var layerIds = []; + /** + * Generates a list of potential layer ids for the specified chirality mode. + * + * @param {boolean} chiral // Does the keyboard use chiral modifiers or not? + */ + static generateLayerIds(chiral: boolean): string[] { + var layerCnt, offset; + + if(chiral) { + layerCnt=32; + offset=0x01; + } else { + layerCnt=8; + offset=0x10; + } - for(var i=0; i < layerCnt; i++) { - layerIds.push(Layouts.getLayerId(i * offset)); - } + var layerIds = []; - return layerIds; + for(var i=0; i < layerCnt; i++) { + layerIds.push(Layouts.getLayerId(i * offset)); } - /** - * Sets a formatting property for the modifier keys when constructing a default layout for a keyboard. - * - * @param {Object} layer // One layer specification - * @param {boolean} chiral // Whether or not the keyboard uses chiral modifier information. - * @param {string} formFactor // The form factor of the device the layout is being constructed for. - * @param {boolean} key102 // Whether or not the extended key 102 should be hidden. - */ - static formatDefaultLayer(layer: LayoutLayer, chiral: boolean, formFactor: string, key102: boolean) { - var layerId = layer['id']; - let buttonClasses = Layouts.buttonClasses; - - // Correct appearance of state-dependent modifier keys according to group - for(var i=0; i -/// -/// +import Codes from "../text/codes.js"; +import { Layouts, type LayoutFormFactor } from "./defaultLayouts.js"; +import { ActiveKey, ActiveLayout } from "./activeLayout.js"; +import KeyEvent from "../text/keyEvent.js"; +import type OutputTarget from "../text/outputTarget.js"; + +import type { ComplexKeyboardStore } from "../text/kbdInterface.js"; + +import { Version, DeviceSpec } from "@keymanapp/web-utils"; +import StateKeyMap from "./stateKeyMap.js"; + +/** + * Stores preprocessed properties of a keyboard for quick retrieval later. + */ +class CacheTag { + stores: {[storeName: string]: ComplexKeyboardStore}; + + constructor() { + this.stores = {}; + } +} + +export enum LayoutState { + NOT_LOADED = undefined, + POLYFILLED = 1, + CALIBRATED = 2 +} + +export interface VariableStoreDictionary { + [name: string]: string; +}; + + +/** + * Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information + * and keyboard-centered functionality in an object-oriented way without modifying the + * wrapped keyboard itself. + */ +export default class Keyboard { + public static DEFAULT_SCRIPT_OBJECT = { + 'gs': function(outputTarget, keystroke) { return false; }, // no matching rules; rely on defaultRuleOutput entirely + 'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this. + 'KN': '', + 'KV': Layouts.DEFAULT_RAW_SPEC, + 'KM': 0 // May not be the best default, but this matches current behavior when there is no activeKeyboard. + } -namespace com.keyman.keyboards { /** - * Stores preprocessed properties of a keyboard for quick retrieval later. + * This is the object provided to KeyboardInterface.registerKeyboard - that is, the keyboard + * being wrapped. + * + * TODO: Make this private instead. But there are a LOT of references that must be rooted out first. */ - class CacheTag { - stores: {[storeName: string]: text.ComplexKeyboardStore}; + public readonly scriptObject: any; + private layoutStates: {[layout: string]: LayoutState}; - constructor() { - this.stores = {}; + constructor(keyboardScript: any) { + if(keyboardScript) { + this.scriptObject = keyboardScript; + } else { + this.scriptObject = Keyboard.DEFAULT_SCRIPT_OBJECT; } + this.layoutStates = {}; + } + + /** + * Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group. + */ + process(outputTarget: OutputTarget, keystroke: KeyEvent): boolean { + return this.scriptObject['gs'](outputTarget, keystroke); + } + + /** + * Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group. + */ + processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): boolean { + return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false; + } + + /** + * Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group. + */ + processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): boolean { + return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false; } - export enum LayoutState { - NOT_LOADED = undefined, - POLYFILLED = 1, - CALIBRATED = 2 + get isHollow(): boolean { + return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT; } - export interface VariableStoreDictionary { - [name: string]: string; - }; + get id(): string { + return this.scriptObject['KI']; + } + get name(): string { + return this.scriptObject['KN']; + } /** - * Acts as a wrapper class for Keyman keyboards compiled to JS, providing type information - * and keyboard-centered functionality in an object-oriented way without modifying the - * wrapped keyboard itself. + * Cache variable store values + * + * Primarily used for predictive text to prevent variable store + * values from being changed in 'fat finger' processing. + * + * KVS is available in keyboards compiled with Keyman Developer 15 + * and later versions. See #2924. + * + * @returns an object with each property referencing a variable store */ - export class Keyboard { - public static DEFAULT_SCRIPT_OBJECT = { - 'gs': function(outputTarget, keystroke) { return false; }, // no matching rules; rely on defaultRuleOutput entirely - 'KI': '', // The currently-existing default keyboard ID; we already have checks that focus against this. - 'KN': '', - 'KV': Layouts.DEFAULT_RAW_SPEC, - 'KM': 0 // May not be the best default, but this matches current behavior when there is no activeKeyboard. + get variableStores(): VariableStoreDictionary { + const storeNames = this.scriptObject['KVS']; + let values = {}; + if(Array.isArray(storeNames)) { + for(let store of storeNames) { + values[store] = this.scriptObject[store]; + } } + return values; + } - /** - * This is the object provided to KeyboardInterface.registerKeyboard - that is, the keyboard - * being wrapped. - * - * TODO: Make this private instead. But there are a LOT of references that must be rooted out first. - */ - public readonly scriptObject: any; - private layoutStates: {[layout: string]: LayoutState}; - - constructor(keyboardScript: any) { - if(keyboardScript) { - this.scriptObject = keyboardScript; - } else { - this.scriptObject = Keyboard.DEFAULT_SCRIPT_OBJECT; + /** + * Restore variable store values from cache + * + * KVS is available in keyboards compiled with Keyman Developer 15 + * and later versions. See #2924. + * + * @param values name-value pairs for each store value + */ + set variableStores(values: VariableStoreDictionary) { + const storeNames = this.scriptObject['KVS']; + if(Array.isArray(storeNames)) { + for(let store of storeNames) { + // If the value is not present in the cache, don't overwrite it; + // while this is not used in initial implementation, we could use + // it in future to update a single variable store value rather than + // the whole cache. + if(typeof values[store] == 'string') { + this.scriptObject[store] = values[store]; + } } - this.layoutStates = {}; } + } - /** - * Calls the keyboard's `gs` function, which represents the keyboard source's begin Unicode group. - */ - process(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { - return this.scriptObject['gs'](outputTarget, keystroke); - } + // TODO: Better typing. + private get _legacyLayoutSpec(): any { + return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime. + } - /** - * Calls the keyboard's `gn` function, which represents the keyboard source's begin newContext group. - */ - processNewContextEvent(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { - return this.scriptObject['gn'] ? this.scriptObject['gn'](outputTarget, keystroke) : false; - } + // May return null if no layouts exist or have been initialized. + private get _layouts(): {[formFactor: string]: LayoutFormFactor} { + return this.scriptObject['KVKL']; // This one is compiled by Developer's visual keyboard layout editor. + } - /** - * Calls the keyboard's `gpk` function, which represents the keyboard source's begin postKeystroke group. - */ - processPostKeystroke(outputTarget: text.OutputTarget, keystroke: text.KeyEvent): boolean { - return this.scriptObject['gpk'] ? this.scriptObject['gpk'](outputTarget, keystroke) : false; - } + private set _layouts(value) { + this.scriptObject['KVKL'] = value; + } - get isHollow(): boolean { - return this.scriptObject == Keyboard.DEFAULT_SCRIPT_OBJECT; - } + get compilerVersion(): Version { + return new Version(this.scriptObject['KVER']); + } - get id(): string { - return this.scriptObject['KI']; - } + get isMnemonic(): boolean { + return !!this.scriptObject['KM']; + } - get name(): string { - return this.scriptObject['KN']; - } + get definesPositionalOrMnemonic(): boolean { + return typeof this.scriptObject['KM'] != 'undefined'; + } - /** - * Cache variable store values - * - * Primarily used for predictive text to prevent variable store - * values from being changed in 'fat finger' processing. - * - * KVS is available in keyboards compiled with Keyman Developer 15 - * and later versions. See #2924. - * - * @returns an object with each property referencing a variable store - */ - get variableStores(): VariableStoreDictionary { - const storeNames = this.scriptObject['KVS']; - let values = {}; - if(Array.isArray(storeNames)) { - for(let store of storeNames) { - values[store] = this.scriptObject[store]; - } - } - return values; - } + /** + * HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores. + * + * Reference: https://help.keyman.com/developer/language/reference/kmw_helptext, + * https://help.keyman.com/developer/language/reference/kmw_helpfile + */ + get helpText(): string { + return this.scriptObject['KH']; + } - /** - * Restore variable store values from cache - * - * KVS is available in keyboards compiled with Keyman Developer 15 - * and later versions. See #2924. - * - * @param values name-value pairs for each store value - */ - set variableStores(values: VariableStoreDictionary) { - const storeNames = this.scriptObject['KVS']; - if(Array.isArray(storeNames)) { - for(let store of storeNames) { - // If the value is not present in the cache, don't overwrite it; - // while this is not used in initial implementation, we could use - // it in future to update a single variable store value rather than - // the whole cache. - if(typeof values[store] == 'string') { - this.scriptObject[store] = values[store]; - } - } - } - } + /** + * Embedded JS script designed for use with a keyboard's HTML help text. Always defined + * within the file referenced by &kmw_embedjs in a keyboard's source, though that file + * may also contain _other_ script definitions as well. (`KHF` must be explicitly defined + * within that file.) + */ + get hasScript(): boolean { + return !!this.scriptObject['KHF']; + } - // TODO: Better typing. - private get _legacyLayoutSpec(): any { - return this.scriptObject['KV']; // used with buildDefaultLayout; layout must be constructed at runtime. - } + /** + * Embeds a custom script for use by the OSK, which may be interactive (like with sil_euro_latin). + * Note: this must be called AFTER any contents of `helpText` have been inserted into the DOM. + * (See sil_euro_latin's source -> sil_euro_latin_js.txt) + * + * Reference: https://help.keyman.com/developer/language/reference/kmw_embedjs + */ + embedScript(e: any) { + // e: Expects the OSKManager's _Box element. We don't add type info here b/c it would + // reference the DOM. + this.scriptObject['KHF'](e); + } - // May return null if no layouts exist or have been initialized. - private get _layouts(): {[formFactor: string]: LayoutFormFactor} { - return this.scriptObject['KVKL']; // This one is compiled by Developer's visual keyboard layout editor. - } + get oskStyling(): string { + return this.scriptObject['KCSS']; + } - private set _layouts(value) { - this.scriptObject['KVKL'] = value; + /** + * true if this keyboard uses a (legacy) pick list (Chinese, Japanese, Korean, etc.) + * + * TODO: Make a property on keyboards (say, `isPickList` / `KPL`) to signal this when we + * get around to better, generalized picker-list support. + */ + get isCJK(): boolean { // I3363 (Build 301) + var lg: string; + if(typeof(this.scriptObject['KLC']) != 'undefined') { + lg = this.scriptObject['KLC']; + } else if(typeof(this.scriptObject['LanguageCode']) != 'undefined') { + lg = this.scriptObject['LanguageCode']; } - get compilerVersion(): utils.Version { - return new utils.Version(this.scriptObject['KVER']); - } + // While some of these aren't proper BCP-47 language codes, the CJK keyboards predate our use of BCP-47. + // So, we preserve the old ISO 639-3 codes, as that's what the keyboards are matching against. + return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor')); + } + + get isRTL(): boolean { + return !!this.scriptObject['KRTL']; + } + + /** + * Obtains the currently-active modifier bitmask for the active keyboard. + */ + get modifierBitmask(): number { + // NON_CHIRAL is the default bitmask if KMBM is not defined. + // We always need a bitmask to compare against, as seen in `isChiral`. + return this.scriptObject['KMBM'] || Codes.modifierBitmasks['NON_CHIRAL']; + } + + get isChiral(): boolean { + return !!(this.modifierBitmask & Codes.modifierBitmasks['IS_CHIRAL']); + } - get isMnemonic(): boolean { - return !!this.scriptObject['KM']; + get desktopFont(): string { + if(this.scriptObject['KV']) { + return this.scriptObject['KV']['F']; + } else { + return null; } + } + + private get cacheTag(): CacheTag { + let tag = this.scriptObject['_kmw']; - get definesPositionalOrMnemonic(): boolean { - return typeof this.scriptObject['KM'] != 'undefined'; + if(!tag) { + tag = new CacheTag(); + this.scriptObject['_kmw'] = tag; } - /** - * HTML help text, as specified by either the &kmw_helptext or &kmw_helpfile system stores. - * - * Reference: https://help.keyman.com/developer/language/reference/kmw_helptext, - * https://help.keyman.com/developer/language/reference/kmw_helpfile - */ - get helpText(): string { - return this.scriptObject['KH']; + return tag; + } + + get explodedStores(): {[storeName: string]: ComplexKeyboardStore} { + return this.cacheTag.stores; + } + + /** + * Signifies whether or not a layout or OSK should include AltGr / Right-alt emulation for this keyboard. + * @param {Object=} keyLabels + * @return {boolean} + */ + get emulatesAltGr(): boolean { + let modifierCodes = Codes.modifierCodes; + + // If we're not chiral, we're not emulating. + if(!this.isChiral) { + return false; } - /** - * Embedded JS script designed for use with a keyboard's HTML help text. Always defined - * within the file referenced by &kmw_embedjs in a keyboard's source, though that file - * may also contain _other_ script definitions as well. (`KHF` must be explicitly defined - * within that file.) - */ - get hasScript(): boolean { - return !!this.scriptObject['KHF']; + if(this._legacyLayoutSpec == null) { + return false; } - /** - * Embeds a custom script for use by the OSK, which may be interactive (like with sil_euro_latin). - * Note: this must be called AFTER any contents of `helpText` have been inserted into the DOM. - * (See sil_euro_latin's source -> sil_euro_latin_js.txt) - * - * Reference: https://help.keyman.com/developer/language/reference/kmw_embedjs - */ - embedScript(e: any) { - // e: Expects the OSKManager's _Box element. We don't add type info here b/c it would - // reference the DOM. - this.scriptObject['KHF'](e); + // Only exists in KMW 10.0+, but before that Web had no chirality support, so... return false. + let layers = this._legacyLayoutSpec['KLS']; + if(!layers) { + return false; } - get oskStyling(): string { - return this.scriptObject['KCSS']; + var emulationMask = modifierCodes['LCTRL'] | modifierCodes['LALT']; + var unshiftedEmulationLayer = layers[Layouts.getLayerId(emulationMask)]; + var shiftedEmulationLayer = layers[Layouts.getLayerId(modifierCodes['SHIFT'] | emulationMask)]; + + // buildDefaultLayout ensures that these are aliased to the original modifier set being emulated. + // As a result, we can directly test for reference equality. + // + // This allows us to still return `true` after creating the layers for emulation; during keyboard + // construction, the two layers should be null for AltGr emulation to succeed. + if(unshiftedEmulationLayer != null && + unshiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'])]) { + return false; } - /** - * true if this keyboard uses a (legacy) pick list (Chinese, Japanese, Korean, etc.) - * - * TODO: Make a property on keyboards (say, `isPickList` / `KPL`) to signal this when we - * get around to better, generalized picker-list support. - */ - get isCJK(): boolean { // I3363 (Build 301) - var lg: string; - if(typeof(this.scriptObject['KLC']) != 'undefined') { - lg = this.scriptObject['KLC']; - } else if(typeof(this.scriptObject['LanguageCode']) != 'undefined') { - lg = this.scriptObject['LanguageCode']; - } + if(shiftedEmulationLayer != null && + shiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'] | modifierCodes['SHIFT'])]) { + return false; + } - // While some of these aren't proper BCP-47 language codes, the CJK keyboards predate our use of BCP-47. - // So, we preserve the old ISO 639-3 codes, as that's what the keyboards are matching against. - return ((lg == 'cmn') || (lg == 'jpn') || (lg == 'kor')); + // It's technically possible for the OSK to not specify anything while allowing chiral input. A last-ditch catch: + var bitmask = this.modifierBitmask; + if((bitmask & emulationMask) != emulationMask) { + // At least one of the emulation modifiers is never used by the keyboard! We can confirm everything's safe. + return true; } - get isRTL(): boolean { - return !!this.scriptObject['KRTL']; + if(unshiftedEmulationLayer == null && shiftedEmulationLayer == null) { + // We've run out of things to go on; we can't detect if chiral AltGr emulation is intended or not. + // TODO: handle this again! + // if(!osk.altGrWarning) { + // console.warn("Could not detect if AltGr emulation is safe, but defaulting to active emulation!") + // // Avoid spamming the console with warnings on every call of the method. + // osk.altGrWarning = true; + // } + return true; } + return true; + } + + get usesSupplementaryPlaneChars(): boolean { + let kbd = this.scriptObject; + // I3319 - SMP extension, I3363 (Build 301) + return kbd && ((kbd['KS'] && kbd['KS'] == 1) || kbd['KN'] == 'Hieroglyphic'); + } + + get version(): string { + return this.scriptObject['KBVER'] || ''; + } - /** - * Obtains the currently-active modifier bitmask for the active keyboard. - */ - get modifierBitmask(): number { - // NON_CHIRAL is the default bitmask if KMBM is not defined. - // We always need a bitmask to compare against, as seen in `isChiral`. - return this.scriptObject['KMBM'] || text.Codes.modifierBitmasks['NON_CHIRAL']; + usesDesktopLayoutOnDevice(device: DeviceSpec) { + if(this.scriptObject['KVKL']) { + // A custom mobile layout is defined... but are we using it? + return device.formFactor == DeviceSpec.FormFactor.Desktop; + } else { + return true; } + } - get isChiral(): boolean { - return !!(this.modifierBitmask & text.Codes.modifierBitmasks['IS_CHIRAL']); + /** + * @param {number} _PCommand event code (16,17,18) or 0 + * @param {Object} _PTarget target element + * @param {number} _PData 1 or 0 + * Notifies keyboard of keystroke or other event + */ + notify(_PCommand: number, _PTarget: OutputTarget, _PData: number) { // I2187 + // Good example use case - the Japanese CJK-picker keyboard + if(typeof(this.scriptObject['KNS']) == 'function') { + this.scriptObject['KNS'](_PCommand, _PTarget, _PData); } + } - get desktopFont(): string { - if(this.scriptObject['KV']) { - return this.scriptObject['KV']['F']; - } else { - return null; + private findOrConstructLayout(formFactor: DeviceSpec.FormFactor): LayoutFormFactor { + if(this._layouts) { + // Search for viable layouts. `null` is allowed for desktop form factors when help text is available, + // so we check explicitly against `undefined`. + if(this._layouts[formFactor] !== undefined) { + return this._layouts[formFactor]; + } else if(formFactor == DeviceSpec.FormFactor.Phone && this._layouts[DeviceSpec.FormFactor.Tablet]) { + return this._layouts[DeviceSpec.FormFactor.Phone] = this._layouts[DeviceSpec.FormFactor.Tablet]; + } else if(formFactor == DeviceSpec.FormFactor.Tablet && this._layouts[DeviceSpec.FormFactor.Phone]) { + return this._layouts[DeviceSpec.FormFactor.Tablet] = this._layouts[DeviceSpec.FormFactor.Phone]; } } - private get cacheTag(): CacheTag { - let tag = this.scriptObject['_kmw']; - - if(!tag) { - tag = new CacheTag(); - this.scriptObject['_kmw'] = tag; + // No pre-built layout available; time to start constructing it via defaults. + // First, if we have non-default keys specified by the ['BK'] array, we've got + // enough to work with to build a default layout. + let rawSpecifications: any = null; // TODO: better typing, same type as this._legacyLayoutSpec. + if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['KLS']) { // KLS is only specified whenever there are non-default keys. + rawSpecifications = this._legacyLayoutSpec; + } else if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['BK'] != null) { + var keyCaps=this._legacyLayoutSpec['BK']; + for(var i=0; i 0) { + rawSpecifications = this._legacyLayoutSpec; + break; + } } + } - return tag; + // If we don't have key definitions to use for a layout but also lack help text or are a touch-based layout, + // we make a default layout anyway. We have to show display something usable. + if(!rawSpecifications && (this.helpText == '' || formFactor != DeviceSpec.FormFactor.Desktop)) { + rawSpecifications = {'F':'Tahoma', 'BK': Layouts.dfltText}; } - get explodedStores(): {[storeName: string]: text.ComplexKeyboardStore} { - return this.cacheTag.stores; + // Regardless of success, we'll want to initialize the field that backs the property; + // may as well cache the default layout we just built, or a 'null' if it shouldn't exist.. + if(!this._layouts) { + this._layouts = {}; } - /** - * Signifies whether or not a layout or OSK should include AltGr / Right-alt emulation for this keyboard. - * @param {Object=} keyLabels - * @return {boolean} - */ - get emulatesAltGr(): boolean { - let modifierCodes = text.Codes.modifierCodes; - - // If we're not chiral, we're not emulating. - if(!this.isChiral) { - return false; - } + // Final check - do we construct a layout, or is this a case where helpText / insertHelpHTML should take over? + if(rawSpecifications) { + // Now to generate a layout from our raw specifications. + let layout = this._layouts[formFactor] = Layouts.buildDefaultLayout(rawSpecifications, this, formFactor); + layout.isDefault = true; + return layout; + } else { + // The fact that it doesn't exist will indicate that help text/HTML should be inserted instead. + this._layouts[formFactor] = null; // provides a cached value for the check at the top of this method. + return null; + } + } - if(this._legacyLayoutSpec == null) { - return false; + /** + * Returns an ActiveLayout object representing the keyboard's layout for this form factor. May return null if a custom desktop "help" OSK is defined, as with sil_euro_latin. + * + * In such cases, please use either `helpText` or `insertHelpHTML` instead. + * @param formFactor {string} The desired form factor for the layout. + */ + public layout(formFactor: DeviceSpec.FormFactor): ActiveLayout { + let rawLayout = this.findOrConstructLayout(formFactor); + + if(rawLayout) { + // Prevents accidentally reprocessing layouts; it's a simple enough check. + if(this.layoutStates[formFactor] == LayoutState.NOT_LOADED) { + rawLayout = ActiveLayout.polyfill(rawLayout, this, formFactor); + this.layoutStates[formFactor] = LayoutState.POLYFILLED; } - // Only exists in KMW 10.0+, but before that Web had no chirality support, so... return false. - let layers = this._legacyLayoutSpec['KLS']; - if(!layers) { - return false; - } + return rawLayout as ActiveLayout; + } else { + return null; + } + } - var emulationMask = modifierCodes['LCTRL'] | modifierCodes['LALT']; - var unshiftedEmulationLayer = layers[Layouts.getLayerId(emulationMask)]; - var shiftedEmulationLayer = layers[Layouts.getLayerId(modifierCodes['SHIFT'] | emulationMask)]; - - // buildDefaultLayout ensures that these are aliased to the original modifier set being emulated. - // As a result, we can directly test for reference equality. - // - // This allows us to still return `true` after creating the layers for emulation; during keyboard - // construction, the two layers should be null for AltGr emulation to succeed. - if(unshiftedEmulationLayer != null && - unshiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'])]) { - return false; - } + public refreshLayouts() { + let formFactors = [ DeviceSpec.FormFactor.Desktop, DeviceSpec.FormFactor.Phone, DeviceSpec.FormFactor.Tablet ]; - if(shiftedEmulationLayer != null && - shiftedEmulationLayer != layers[Layouts.getLayerId(modifierCodes['RALT'] | modifierCodes['SHIFT'])]) { - return false; - } + let _this = this; - // It's technically possible for the OSK to not specify anything while allowing chiral input. A last-ditch catch: - var bitmask = this.modifierBitmask; - if((bitmask & emulationMask) != emulationMask) { - // At least one of the emulation modifiers is never used by the keyboard! We can confirm everything's safe. - return true; - } + formFactors.forEach(function(form) { + // Currently doesn't work if we reset it to POLYFILLED, likely due to how 'calibration' + // currently works. + _this.layoutStates[form] = LayoutState.NOT_LOADED; + }); + } - if(unshiftedEmulationLayer == null && shiftedEmulationLayer == null) { - // We've run out of things to go on; we can't detect if chiral AltGr emulation is intended or not. - // TODO: handle this again! - // if(!osk.altGrWarning) { - // console.warn("Could not detect if AltGr emulation is safe, but defaulting to active emulation!") - // // Avoid spamming the console with warnings on every call of the method. - // osk.altGrWarning = true; - // } - return true; - } - return true; + public markLayoutCalibrated(formFactor: DeviceSpec.FormFactor) { + if(this.layoutStates[formFactor] != LayoutState.NOT_LOADED) { + this.layoutStates[formFactor] = LayoutState.CALIBRATED; } + } - get usesSupplementaryPlaneChars(): boolean { - let kbd = this.scriptObject; - // I3319 - SMP extension, I3363 (Build 301) - return kbd && ((kbd['KS'] && kbd['KS'] == 1) || kbd['KN'] == 'Hieroglyphic'); - } + public getLayoutState(formFactor: DeviceSpec.FormFactor) { + return this.layoutStates[formFactor]; + } - usesDesktopLayoutOnDevice(device: utils.DeviceSpec) { - if(this.scriptObject['KVKL']) { - // A custom mobile layout is defined... but are we using it? - return device.formFactor == utils.FormFactor.Desktop; - } else { - return true; - } - } - /** - * @param {number} _PCommand event code (16,17,18) or 0 - * @param {Object} _PTarget target element - * @param {number} _PData 1 or 0 - * Notifies keyboard of keystroke or other event - */ - notify(_PCommand: number, _PTarget: text.OutputTarget, _PData: number) { // I2187 - // Good example use case - the Japanese CJK-picker keyboard - if(typeof(this.scriptObject['KNS']) == 'function') { - this.scriptObject['KNS'](_PCommand, _PTarget, _PData); - } + constructNullKeyEvent(device: DeviceSpec, stateKeys?: StateKeyMap): KeyEvent { + stateKeys = stateKeys || { + K_CAPS: false, + K_NUMLOCK: false, + K_SCROLL: false } - private findOrConstructLayout(formFactor: utils.FormFactor): LayoutFormFactor { - if(this._layouts) { - // Search for viable layouts. `null` is allowed for desktop form factors when help text is available, - // so we check explicitly against `undefined`. - if(this._layouts[formFactor] !== undefined) { - return this._layouts[formFactor]; - } else if(formFactor == utils.FormFactor.Phone && this._layouts[utils.FormFactor.Tablet]) { - return this._layouts[utils.FormFactor.Phone] = this._layouts[utils.FormFactor.Tablet]; - } else if(formFactor == utils.FormFactor.Tablet && this._layouts[utils.FormFactor.Phone]) { - return this._layouts[utils.FormFactor.Tablet] = this._layouts[utils.FormFactor.Phone]; - } - } + const keyEvent = KeyEvent.constructNullKeyEvent(device); + this.setSyntheticEventDefaults(keyEvent, stateKeys); + return keyEvent; + } - // No pre-built layout available; time to start constructing it via defaults. - // First, if we have non-default keys specified by the ['BK'] array, we've got - // enough to work with to build a default layout. - let rawSpecifications: any = null; // TODO: better typing, same type as this._legacyLayoutSpec. - if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['KLS']) { // KLS is only specified whenever there are non-default keys. - rawSpecifications = this._legacyLayoutSpec; - } else if(this._legacyLayoutSpec != null && this._legacyLayoutSpec['BK'] != null) { - var keyCaps=this._legacyLayoutSpec['BK']; - for(var i=0; i 0) { - rawSpecifications = this._legacyLayoutSpec; - break; - } - } - } + constructKeyEvent(key: ActiveKey, device: DeviceSpec, stateKeys: StateKeyMap): KeyEvent { + // Make a deep copy of our preconstructed key event, filling it out from there. + const Lkc = key.baseKeyEvent; + Lkc.device = device; - // If we don't have key definitions to use for a layout but also lack help text or are a touch-based layout, - // we make a default layout anyway. We have to show display something usable. - if(!rawSpecifications && (this.helpText == '' || formFactor != utils.FormFactor.Desktop)) { - rawSpecifications = {'F':'Tahoma', 'BK': Layouts.dfltText}; - } + if(this.isMnemonic) { + Lkc.setMnemonicCode(key.layer.indexOf('shift') != -1, stateKeys['K_CAPS']); + } - // Regardless of success, we'll want to initialize the field that backs the property; - // may as well cache the default layout we just built, or a 'null' if it shouldn't exist.. - if(!this._layouts) { - this._layouts = {}; - } + // Performs common pre-analysis for both 'native' and 'embedded' OSK key & subkey input events. + // This part depends on the keyboard processor's active state. + this.setSyntheticEventDefaults(Lkc, stateKeys); - // Final check - do we construct a layout, or is this a case where helpText / insertHelpHTML should take over? - if(rawSpecifications) { - // Now to generate a layout from our raw specifications. - let layout = this._layouts[formFactor] = Layouts.buildDefaultLayout(rawSpecifications, this, formFactor); - layout.isDefault = true; - return layout; - } else { - // The fact that it doesn't exist will indicate that help text/HTML should be inserted instead. - this._layouts[formFactor] = null; // provides a cached value for the check at the top of this method. - return null; - } + // If it's a state key modifier, trigger its effects as part of the + // keystroke. + const bitmap = { + 'K_CAPS': Codes.stateBitmasks.CAPS, + 'K_NUMLOCK': Codes.stateBitmasks.NUM_LOCK, + 'K_SCROLL': Codes.stateBitmasks.SCROLL_LOCK + }; + const bitmask = bitmap[Lkc.kName]; + + if(bitmask) { + Lkc.Lstates ^= bitmask; + Lkc.LmodifierChange = true; } - /** - * Returns an ActiveLayout object representing the keyboard's layout for this form factor. May return null if a custom desktop "help" OSK is defined, as with sil_euro_latin. - * - * In such cases, please use either `helpText` or `insertHelpHTML` instead. - * @param formFactor {string} The desired form factor for the layout. - */ - public layout(formFactor: utils.FormFactor): ActiveLayout { - let rawLayout = this.findOrConstructLayout(formFactor); - - if(rawLayout) { - // Prevents accidentally reprocessing layouts; it's a simple enough check. - if(this.layoutStates[formFactor] == LayoutState.NOT_LOADED) { - rawLayout = ActiveLayout.polyfill(rawLayout, this, formFactor); - this.layoutStates[formFactor] = LayoutState.POLYFILLED; - } + return Lkc; + } - return rawLayout as ActiveLayout; - } else { - return null; + setSyntheticEventDefaults(Lkc: KeyEvent, stateKeys: StateKeyMap) { + // Set the flags for the state keys - for desktop devices. For touch + // devices, the only state key in use currently is Caps Lock, which is set + // when the 'caps' layer is active in ActiveKey::constructBaseKeyEvent. + if(!Lkc.device.touchable) { + /* + * For desktop-style keyboards, start from a blank slate. They have a 'default' + * (implicit 'NO_CAPS') layer but not a 'caps' layer. With caps set, it just + * highlights the key on the 'default' layer instead. + * + * We should never set both `CAPS` and `NO_CAPS` at the same time, and + * same for the other modifiers. + */ + Lkc.Lstates = 0; + Lkc.Lstates |= stateKeys['K_CAPS'] ? Codes.modifierCodes['CAPS'] : Codes.modifierCodes['NO_CAPS']; + Lkc.Lstates |= stateKeys['K_NUMLOCK'] ? Codes.modifierCodes['NUM_LOCK'] : Codes.modifierCodes['NO_NUM_LOCK']; + Lkc.Lstates |= stateKeys['K_SCROLL'] ? Codes.modifierCodes['SCROLL_LOCK'] : Codes.modifierCodes['NO_SCROLL_LOCK']; + } + + // Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys + if(Lkc.kName && Lkc.kName.substr(0,2) == 'U_') { + Lkc.LisVirtualKey=false; + } + + // Get code for non-physical keys (T_KOKAI, U_05AB etc) + if(typeof Lkc.Lcode == 'undefined') { + Lkc.Lcode = this.getVKDictionaryCode(Lkc.kName);// Updated for Build 347 + if(!Lkc.Lcode) { + // Special case for U_xxxx keys. This vk code will never be used + // in a keyboard, so we use this to ensure that keystroke processing + // occurs for the key. + Lkc.Lcode = 1; } } - public refreshLayouts() { - let formFactors = [ utils.FormFactor.Desktop, utils.FormFactor.Phone, utils.FormFactor.Tablet ]; - - let _this = this; - - formFactors.forEach(function(form) { - // Currently doesn't work if we reset it to POLYFILLED, likely due to how 'calibration' - // currently works. - _this.layoutStates[form] = LayoutState.NOT_LOADED; - }); + // Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer. + if((Lkc.Lmodifiers & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM'] && this.emulatesAltGr) { + Lkc.Lmodifiers &= ~Codes.modifierBitmasks['ALT_GR_SIM']; + Lkc.Lmodifiers |= Codes.modifierCodes['RALT']; } + } - public markLayoutCalibrated(formFactor: utils.FormFactor) { - if(this.layoutStates[formFactor] != LayoutState.NOT_LOADED) { - this.layoutStates[formFactor] = LayoutState.CALIBRATED; + /** + * @summary Look up a custom virtual key code in the virtual key code dictionary KVKD. + * On first run, will build the dictionary. + * + * `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed + * at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e. + * outside the range of standard virtual key codes). These additional codes are both + * `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However, + * `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that + * is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only + * referenced as the id of a key in the touch layout, then it does not get an entry in + * the `KVKD` property. + * + * @private + * @param {string} keyName custom virtual key code to lookup in the dictionary + * @return {number} key code > 255 on success, or 0 if not found + */ + getVKDictionaryCode(keyName: string) { + if(!this.scriptObject['VKDictionary']) { + const a=[]; + if(typeof this.scriptObject['KVKD'] == 'string') { + // Build the VK dictionary + // TODO: Move the dictionary build into the compiler -- so compiler generates code such as following. + // Makes the VKDictionary member unnecessary. + // this.KVKD={"K_ABC":256,"K_DEF":257,...}; + const s=this.scriptObject['KVKD'].split(' '); + for(var i=0; i { + this.harness.install(); + const promise = this.loadKeyboardInternal(uri, new UriBasedErrorBuilder(uri)); + + return promise; + } + + public loadKeyboardFromStub(stub: KeyboardStub) { + this.harness.install(); + let promise = this.loadKeyboardInternal(stub.filename, new StubBasedErrorBuilder(stub), stub.id); + + return promise; + } + + protected abstract loadKeyboardInternal( + uri: string, + errorBuilder: KeyboardLoadErrorBuilder, + id?: string + ): Promise; +} \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/keyboardProperties.ts b/common/web/keyboard-processor/src/keyboards/keyboardProperties.ts new file mode 100644 index 0000000000000000000000000000000000000000..e83d1a137f192aa5460dce6531a9103a5d22e4e1 --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/keyboardProperties.ts @@ -0,0 +1,264 @@ +import SpacebarText from './spacebarText.js'; + +export interface InternalKeyboardFont { + family: string; + filename?: never; + files: string | string[]; // internal + source?: never; + path: string; +} + +interface CloudKeyboardFont1 { + family: string; + filename: string | string[]; + files?: never; + source?: never; +} + +interface CloudKeyboardFont2 { + family: string; + filename?: never; + files?: never; + source: string | string[]; +} + +export type CloudKeyboardFont = CloudKeyboardFont1 | CloudKeyboardFont2; + +/** + * Converts one of three public-facing font-specification formats into a consistent structure + * used generally among the Keyman JS/TS modules. + * @param fontObj + * @param fontPath + * @returns + */ +export function internalizeFont(fontObj: CloudKeyboardFont, fontPath: string): InternalKeyboardFont { + if(!fontObj) { + return undefined; + } else { + return { + family: fontObj.family, + path: fontPath, + files: fontObj.filename || fontObj.source + } + } +} + +export type KeyboardFont = CloudKeyboardFont | InternalKeyboardFont; + +// Filename properties are deliberately omitted here; we can add that at higher-levels where it matters +// via 'mix-in'. +// +// For example, the OSK module doesn't care about the filename of a loaded keyboard. It doesn't do +// keyboard loading on its own whatsoever. + +// Corresponds to Keyman Engine for Web's internal "keyboard stub" format. +// Also referred to by KMW 2.0-era loaders: https://help.keyman.com/developer/8.0/docs/reference_kmw20_example +export interface KeyboardInternalPropertySpec { + KI: string, + KFont: InternalKeyboardFont, + KOskFont: InternalKeyboardFont, + displayName?: string, + KN?: string, + KL?: string, + KLC?: string +}; + +export type LanguageAPIPropertySpec = { + id: string, + name: string, + font: CloudKeyboardFont, + oskFont: CloudKeyboardFont, + region?: number|string +} + +/** + * Corresponds to the documented API for the Web engine's `addKeyboards` function + * when a single language object is specified - not an array. + * + * See https://help.keyman.com/DEVELOPER/ENGINE/WEB/15.0/reference/core/addKeyboards, + * "Using an `object`". + */ +export type KeyboardAPIPropertySpec = { + id: string, + name: string, + + /** + * @deprecated Replaced with `languages`. + */ + language?: LanguageAPIPropertySpec; + languages: LanguageAPIPropertySpec; +} + +/** + * Corresponds to the documented API for the Web engine's `addKeyboards` function + * when a language array is specified for the object. + * + * See https://help.keyman.com/DEVELOPER/ENGINE/WEB/15.0/reference/core/addKeyboards, + * "Using an `object`". + */ +export type KeyboardAPIPropertyMultilangSpec = { + id: string, + name: string, + + /** + * @deprecated Replaced with `languages`. + */ + language?: LanguageAPIPropertySpec[]; + languages: LanguageAPIPropertySpec[]; +} + +export type MetadataObj = KeyboardInternalPropertySpec | KeyboardAPIPropertySpec | KeyboardAPIPropertyMultilangSpec; + +export default class KeyboardProperties implements KeyboardInternalPropertySpec { + KI: string; + KN: string; + KL: string; + KLC: string; + KFont: InternalKeyboardFont; + KOskFont: InternalKeyboardFont; + _displayName?: string; + + private static spacebarTextModeSrc: SpacebarText | (() => SpacebarText) = SpacebarText.KEYBOARD; + + public static get spacebarTextMode(): SpacebarText { + if(typeof this.spacebarTextModeSrc == 'string') { + return this.spacebarTextModeSrc; + } else { + return this.spacebarTextModeSrc(); + } + } + + public static set spacebarTextMode(source: typeof KeyboardProperties.spacebarTextModeSrc) { + this.spacebarTextModeSrc = source; + } + + public constructor(metadataObj: MetadataObj, fontPath?: string); + public constructor(keyboardId: string, languageCode: string); + public constructor(arg1: MetadataObj | string, arg2?: string) { + if(!(typeof arg1 == 'string')) { + if(arg1['KI'] || arg1['KL'] || arg1['KLC'] || arg1['KFont'] || arg1['KOskFont']) { + const other = arg1 as KeyboardInternalPropertySpec; + this.KI = other.KI; + this.KN = other.KN; + this.KL = other.KL; + this.KLC = other.KLC; + // Do NOT apply fontPath here; the mobile apps will have font issues if you do! + this.KFont = other.KFont; + this.KOskFont = other.KOskFont; + this._displayName = (other instanceof KeyboardProperties) ? other._displayName : other.displayName; + } else { + let apiStub = arg1 as KeyboardAPIPropertySpec; // TODO: could be an array, as currently specified. :( + + apiStub.languages ||= apiStub.language; + + this.KI = apiStub.id, + this.KN = apiStub.name, + this.KL = apiStub.languages.name, + this.KLC = apiStub.languages.id, + this.KFont = internalizeFont(apiStub.languages.font, arg2), + this.KOskFont = internalizeFont(apiStub.languages.oskFont, arg2) + } + } else { + this.KI = arg1; + this.KLC = arg2; + } + } + + public static fromMultilanguageAPIStub(apiStub: KeyboardAPIPropertyMultilangSpec): KeyboardProperties[] { + let stubs: KeyboardProperties[] = []; + + apiStub.languages ||= apiStub.language; + + for(let langSpec of apiStub.languages) { + let stub: KeyboardAPIPropertySpec = { + id: apiStub.id, + name: apiStub.name, + languages: langSpec + }; + + stubs.push(new KeyboardProperties(stub)); + } + + return stubs; + } + + public get id(): string { + return this.KI; + } + + public get name(): string { + return this.KN; + } + + public get langId(): string { + return this.KLC; + } + + public get langName(): string { + return this.KL; + } + + public get displayName(): string { + if(this._displayName) { + return this._displayName; + } + + // else, construct it. + const kbdName = this.KN; + const lgName = this.KL; + + switch (KeyboardProperties.spacebarTextMode) { + case SpacebarText.KEYBOARD: + return kbdName; + case SpacebarText.LANGUAGE: + return lgName; + case SpacebarText.LANGUAGE_KEYBOARD: + return (kbdName == lgName) ? lgName : lgName + ' - ' + kbdName; + case SpacebarText.BLANK: + return ''; + default: + return kbdName; + } + } + + public set displayName(name: string) { + this._displayName = name; + } + + public get textFont() { + return this.KFont; + } + + public get oskFont() { + return this.KOskFont; + } + + /** + * Generates an error for objects with specification levels insufficient for use in the on-screen-keyboard + * module, complete with a message about one or more details in need of correction. + * @returns A preconstructed `Error` instance that may be thrown by the caller. + */ + public validateForOSK(): Error { + if(!this.KLC) { + if(this.KI || this.KN) { + return new Error(`No language code was specified for use with the ${this.KI || this.KN} keyboard`); + } else { + return new Error("No language code was specified for use with the corresponding keyboard") + } + } + + if(this.displayName === undefined || (KeyboardProperties.spacebarTextMode != SpacebarText.BLANK && !this.displayName)) { + return new Error("A display name is missing for this keyboard and cannot be generated under current settings.") + } + + return null; + } + + public validateForCustomKeyboard(): Error { + if(!this.KI || !this.KN || !this.KL || !this.KLC) { + return new Error("To use a custom keyboard, you must specify keyboard id, keyboard name, language and language code."); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/loaders/dom-keyboard-loader.ts b/common/web/keyboard-processor/src/keyboards/loaders/dom-keyboard-loader.ts new file mode 100644 index 0000000000000000000000000000000000000000..989d5b647774819f31926dccfbb23f72740c2c2c --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/loaders/dom-keyboard-loader.ts @@ -0,0 +1 @@ +export * from './domKeyboardLoader.js'; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts b/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts new file mode 100644 index 0000000000000000000000000000000000000000..84ee1f0fb19bc412684ae3f5bc5e912f11fc1187 --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/loaders/domKeyboardLoader.ts @@ -0,0 +1,87 @@ +// Enables DOM types, but just for this one module. + +/// + +import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from '@keymanapp/keyboard-processor'; + +import { ManagedPromise } from '@keymanapp/web-utils'; + +export class DOMKeyboardLoader extends KeyboardLoaderBase { + public readonly element: HTMLIFrameElement; + private readonly performCacheBusting: boolean; + + constructor() + constructor(harness: KeyboardHarness); + constructor(harness: KeyboardHarness, cacheBust?: boolean) + constructor(harness?: KeyboardHarness, cacheBust?: boolean) { + if(harness && harness._jsGlobal != window) { + // Copy the String typing over; preserve string extensions! + harness._jsGlobal['String'] = window['String']; + } + + if(!harness) { + super(new KeyboardHarness(window, MinimalKeymanGlobal)); + } else { + super(harness); + } + + this.performCacheBusting = cacheBust || false; + } + + protected loadKeyboardInternal( + uri: string, + errorBuilder: KeyboardLoadErrorBuilder, + id?: string + ): Promise { + const promise = new ManagedPromise(); + + if(this.performCacheBusting) { + uri = this.cacheBust(uri); + } + + try { + const document = this.harness._jsGlobal.document; + const script = document.createElement('script'); + if(id) { + script.id = id; + } + document.head.appendChild(script); + script.onerror = (err) => { + promise.reject(errorBuilder.missingError(err)); + } + script.onload = () => { + if(this.harness.loadedKeyboard) { + const keyboard = this.harness.loadedKeyboard; + this.harness.loadedKeyboard = null; + promise.resolve(keyboard); + } else { + promise.reject(errorBuilder.scriptError()); + } + } + + // On the oldest mobile devices we support, Promise.finally may not actually exist. + // Fortunately... it's not that hard of an issue to work around. + // Note: es6-shim doesn't polyfill Promise.finally! + promise.then(() => { + // It is safe to remove the script once it has been run (https://stackoverflow.com/a/37393041) + script.remove(); + }).catch(() => { + script.remove(); + }); + + // Now that EVERYTHING ELSE is ready, establish the link to the keyboard's script. + script.src = uri; + } catch (err) { + return Promise.reject(err); + } + + return promise.corePromise; + } + + private cacheBust(uri: string) { + // Our WebView version directly sets the keyboard path, and it may replace the file + // after KMW has loaded. We need cache-busting to prevent the new version from + // being ignored. + return uri + "?v=" + (new Date()).getTime(); /*cache buster*/ + } +} \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/loaders/node-keyboard-loader.ts b/common/web/keyboard-processor/src/keyboards/loaders/node-keyboard-loader.ts new file mode 100644 index 0000000000000000000000000000000000000000..9630e113b0144d4d60ad512369bc34c25c8ae835 --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/loaders/node-keyboard-loader.ts @@ -0,0 +1 @@ +export * from './nodeKeyboardLoader.js'; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/loaders/nodeKeyboardLoader.ts b/common/web/keyboard-processor/src/keyboards/loaders/nodeKeyboardLoader.ts new file mode 100644 index 0000000000000000000000000000000000000000..143e1c16c1121999b7b63fa1835cd901a853454a --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/loaders/nodeKeyboardLoader.ts @@ -0,0 +1,47 @@ +import { Keyboard, KeyboardHarness, KeyboardLoaderBase, KeyboardLoadErrorBuilder, MinimalKeymanGlobal } from '@keymanapp/keyboard-processor'; + +import vm from 'vm'; +import fs from 'fs'; +import { globalObject } from '@keymanapp/web-utils'; + +export class NodeKeyboardLoader extends KeyboardLoaderBase { + constructor() + constructor(harness: KeyboardHarness); + constructor(harness?: KeyboardHarness) { + if(!harness) { + super(new KeyboardHarness(vm.createContext(), MinimalKeymanGlobal)); + } else { + // If we're going to sandbox, make sure the sandbox is sufficient! + if(globalObject() != harness._jsGlobal) { + vm.createContext(harness._jsGlobal); + // Ensure any and all string extensions are available within the sandbox context! + harness._jsGlobal.String = globalObject().String; + } + + super(harness); + } + } + + protected loadKeyboardInternal(uri: string, errorBuilder: KeyboardLoadErrorBuilder): Promise { + // `fs` does not like 'file:///'; it IS "File System" oriented, after all, and wants a path, not a URI. + if(uri.indexOf('file:///') == 0) { + uri = uri.substring('file:///'.length); + } + + let script; + try { + script = new vm.Script(fs.readFileSync(uri).toString()); + } catch (err) { + return Promise.reject(errorBuilder.missingError(err)); + } + try { + script.runInContext(this.harness._jsGlobal); + } catch (err) { + return Promise.reject(errorBuilder.scriptError(err)); + } + + const keyboard = this.harness.loadedKeyboard; + this.harness.loadedKeyboard = null; + return Promise.resolve(keyboard); + } +} \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json new file mode 100644 index 0000000000000000000000000000000000000000..7ba2abe026aec53f58c727030092c021014266be --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.dom.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../../tsconfig.kmw-main-base.json", + "compilerOptions": { + "baseUrl": "../../../", + "outDir": "../../../build/obj/keyboards/loaders/", + "tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.dom.tsbuildinfo", + "rootDir": "." + }, + "references": [ + { "path": "../../../tsconfig.json" } + ], + "include": ["dom-keyboard-loader.ts", "domKeyboardLoader.ts"], +} diff --git a/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json new file mode 100644 index 0000000000000000000000000000000000000000..0be3f169ad3f47e57174e17aee142871625ba32f --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/loaders/tsconfig.node.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../../tsconfig.kmw-main-base.json", + "compilerOptions": { + "types": [ "node" ], + "baseUrl": "../../../", + "outDir": "../../../build/obj/keyboards/loaders/", + "tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.node.tsbuildinfo", + "rootDir": "." + }, + "references": [ + { "path": "../../../tsconfig.json" } + ], + "include": ["node-keyboard-loader.ts", "nodeKeyboardLoader.ts"], +} diff --git a/common/web/keyboard-processor/src/keyboards/spacebarText.ts b/common/web/keyboard-processor/src/keyboards/spacebarText.ts new file mode 100644 index 0000000000000000000000000000000000000000..d98e7b6462d4481302f678806418ecd9d9bfee1e --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/spacebarText.ts @@ -0,0 +1,9 @@ +// Compiles completely out if `const enum`, making it unavailable in JS-based unit tests. +enum SpacebarText { + KEYBOARD = 'keyboard', + LANGUAGE = 'language', + LANGUAGE_KEYBOARD = 'languageKeyboard', + BLANK = 'blank' +}; + +export default SpacebarText; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/keyboards/stateKeyMap.ts b/common/web/keyboard-processor/src/keyboards/stateKeyMap.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8e9ee8b7634d66e84fc113ca0ea5e7a66a52192 --- /dev/null +++ b/common/web/keyboard-processor/src/keyboards/stateKeyMap.ts @@ -0,0 +1,9 @@ +/** + * Provided by @keymanapp/keyboard-processor's `KeyboardProcessor` class and utilized by the OSK + * to provide state feedback on any corresponding keys visible in the OSK. + */ +export default interface StateKeyMap { + K_CAPS: boolean, + K_NUMLOCK: boolean, + K_SCROLL: boolean +} \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/codes.ts b/common/web/keyboard-processor/src/text/codes.ts index 48fde0d728ccac9eff3f8aac6c6c3192ee8aecd7..5bbd26f51276b9e5e38f9ebf9c94307721568d19 100644 --- a/common/web/keyboard-processor/src/text/codes.ts +++ b/common/web/keyboard-processor/src/text/codes.ts @@ -1,103 +1,169 @@ -namespace com.keyman.text { - export var Codes = { - // Define Keyman Developer modifier bit-flags (exposed for use by other modules) - // Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton. - modifierCodes: { - "LCTRL":0x0001, // LCTRLFLAG - "RCTRL":0x0002, // RCTRLFLAG - "LALT":0x0004, // LALTFLAG - "RALT":0x0008, // RALTFLAG - "SHIFT":0x0010, // K_SHIFTFLAG - "CTRL":0x0020, // K_CTRLFLAG - "ALT":0x0040, // K_ALTFLAG - // TENTATIVE: Represents command keys, which some OSes use for shortcuts we don't - // want to block. No rule will ever target a modifier set with this bit set to 1. - "META":0x0080, // K_METAFLAG - "CAPS":0x0100, // CAPITALFLAG - "NO_CAPS":0x0200, // NOTCAPITALFLAG - "NUM_LOCK":0x0400, // NUMLOCKFLAG - "NO_NUM_LOCK":0x0800, // NOTNUMLOCKFLAG - "SCROLL_LOCK":0x1000, // SCROLLFLAG - "NO_SCROLL_LOCK":0x2000, // NOTSCROLLFLAG - "VIRTUAL_KEY":0x4000, // ISVIRTUALKEY - "VIRTUAL_CHAR_KEY":0x8000 // VIRTUALCHARKEY // Unused by KMW, but reserved for use by other Keyman engines. - }, - - modifierBitmasks: { - "ALL":0x007F, - "ALT_GR_SIM": (0x0001 | 0x0004), - "CHIRAL":0x001F, // The base bitmask for chiral keyboards. Includes SHIFT, which is non-chiral. - "IS_CHIRAL":0x000F, // Used to test if a bitmask uses a chiral modifier. - "NON_CHIRAL":0x0070 // The default bitmask, for non-chiral keyboards - }, - - stateBitmasks: { - "ALL":0x3F00, - "CAPS":0x0300, - "NUM_LOCK":0x0C00, - "SCROLL_LOCK":0x3000 - }, - - // Define standard keycode numbers (exposed for use by other modules) - keyCodes: { - "K_BKSP":8,"K_TAB":9,"K_ENTER":13, - "K_SHIFT":16,"K_CONTROL":17,"K_ALT":18,"K_PAUSE":19,"K_CAPS":20, - "K_ESC":27,"K_SPACE":32,"K_PGUP":33, - "K_PGDN":34,"K_END":35,"K_HOME":36,"K_LEFT":37,"K_UP":38, - "K_RIGHT":39,"K_DOWN":40,"K_SEL":41,"K_PRINT":42,"K_EXEC":43, - "K_INS":45,"K_DEL":46,"K_HELP":47,"K_0":48, - "K_1":49,"K_2":50,"K_3":51,"K_4":52,"K_5":53,"K_6":54,"K_7":55, - "K_8":56,"K_9":57,"K_A":65,"K_B":66,"K_C":67,"K_D":68,"K_E":69, - "K_F":70,"K_G":71,"K_H":72,"K_I":73,"K_J":74,"K_K":75,"K_L":76, - "K_M":77,"K_N":78,"K_O":79,"K_P":80,"K_Q":81,"K_R":82,"K_S":83, - "K_T":84,"K_U":85,"K_V":86,"K_W":87,"K_X":88,"K_Y":89,"K_Z":90, - "K_NP0":96,"K_NP1":97,"K_NP2":98, - "K_NP3":99,"K_NP4":100,"K_NP5":101,"K_NP6":102, - "K_NP7":103,"K_NP8":104,"K_NP9":105,"K_NPSTAR":106, - "K_NPPLUS":107,"K_SEPARATOR":108,"K_NPMINUS":109,"K_NPDOT":110, - "K_NPSLASH":111,"K_F1":112,"K_F2":113,"K_F3":114,"K_F4":115, - "K_F5":116,"K_F6":117,"K_F7":118,"K_F8":119,"K_F9":120, - "K_F10":121,"K_F11":122,"K_F12":123,"K_NUMLOCK":144,"K_SCROLL":145, - "K_LSHIFT":160,"K_RSHIFT":161,"K_LCONTROL":162,"K_RCONTROL":163, - "K_LALT":164,"K_RALT":165, - "K_COLON":186,"K_EQUAL":187,"K_COMMA":188,"K_HYPHEN":189, - "K_PERIOD":190,"K_SLASH":191,"K_BKQUOTE":192, - "K_LBRKT":219,"K_BKSLASH":220,"K_RBRKT":221, - "K_QUOTE":222,"K_oE2":226,"K_OE2":226, - "K_LOPT":50001,"K_ROPT":50002, - "K_NUMERALS":50003,"K_SYMBOLS":50004,"K_CURRENCIES":50005, - "K_UPPER":50006,"K_LOWER":50007,"K_ALPHA":50008, - "K_SHIFTED":50009,"K_ALTGR":50010, - "K_TABBACK":50011,"K_TABFWD":50012 - }, - - codesUS: [ - ['0123456789',';=,-./`', '[\\]\''], - [')!@#$%^&*(',':+<_>?~', '{|}"'] - ], - - isKnownOSKModifierKey(keyID: string): boolean { - switch(keyID) { - case 'K_SHIFT': - case 'K_LOPT': - case 'K_ROPT': - case 'K_NUMLOCK': // Often used for numeric layers. - case 'K_CAPS': +// TODO: Move to separate folder: 'codes' +// We should start splitting off code needed by keyboards even without a KeyboardProcessor active. +// There's an upcoming `/common/web/types` package that 'codes' and 'keyboards' may fit well within. +// In fact, there's a file there (on its branch) that should be merged with this one! + +const Codes = { + // Define Keyman Developer modifier bit-flags (exposed for use by other modules) + // Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton. + modifierCodes: { + "LCTRL":0x0001, // LCTRLFLAG + "RCTRL":0x0002, // RCTRLFLAG + "LALT":0x0004, // LALTFLAG + "RALT":0x0008, // RALTFLAG + "SHIFT":0x0010, // K_SHIFTFLAG + "CTRL":0x0020, // K_CTRLFLAG + "ALT":0x0040, // K_ALTFLAG + // TENTATIVE: Represents command keys, which some OSes use for shortcuts we don't + // want to block. No rule will ever target a modifier set with this bit set to 1. + "META":0x0080, // K_METAFLAG + "CAPS":0x0100, // CAPITALFLAG + "NO_CAPS":0x0200, // NOTCAPITALFLAG + "NUM_LOCK":0x0400, // NUMLOCKFLAG + "NO_NUM_LOCK":0x0800, // NOTNUMLOCKFLAG + "SCROLL_LOCK":0x1000, // SCROLLFLAG + "NO_SCROLL_LOCK":0x2000, // NOTSCROLLFLAG + "VIRTUAL_KEY":0x4000, // ISVIRTUALKEY + "VIRTUAL_CHAR_KEY":0x8000 // VIRTUALCHARKEY // Unused by KMW, but reserved for use by other Keyman engines. + }, + + modifierBitmasks: { + "ALL":0x007F, + "ALT_GR_SIM": (0x0001 | 0x0004), + "CHIRAL":0x001F, // The base bitmask for chiral keyboards. Includes SHIFT, which is non-chiral. + "IS_CHIRAL":0x000F, // Used to test if a bitmask uses a chiral modifier. + "NON_CHIRAL":0x0070 // The default bitmask, for non-chiral keyboards + }, + + stateBitmasks: { + "ALL":0x3F00, + "CAPS":0x0300, + "NUM_LOCK":0x0C00, + "SCROLL_LOCK":0x3000 + }, + + // Define standard keycode numbers (exposed for use by other modules) + keyCodes: { + "K_BKSP":8,"K_TAB":9,"K_ENTER":13, + "K_SHIFT":16,"K_CONTROL":17,"K_ALT":18,"K_PAUSE":19,"K_CAPS":20, + "K_ESC":27,"K_SPACE":32,"K_PGUP":33, + "K_PGDN":34,"K_END":35,"K_HOME":36,"K_LEFT":37,"K_UP":38, + "K_RIGHT":39,"K_DOWN":40,"K_SEL":41,"K_PRINT":42,"K_EXEC":43, + "K_INS":45,"K_DEL":46,"K_HELP":47,"K_0":48, + "K_1":49,"K_2":50,"K_3":51,"K_4":52,"K_5":53,"K_6":54,"K_7":55, + "K_8":56,"K_9":57,"K_A":65,"K_B":66,"K_C":67,"K_D":68,"K_E":69, + "K_F":70,"K_G":71,"K_H":72,"K_I":73,"K_J":74,"K_K":75,"K_L":76, + "K_M":77,"K_N":78,"K_O":79,"K_P":80,"K_Q":81,"K_R":82,"K_S":83, + "K_T":84,"K_U":85,"K_V":86,"K_W":87,"K_X":88,"K_Y":89,"K_Z":90, + "K_NP0":96,"K_NP1":97,"K_NP2":98, + "K_NP3":99,"K_NP4":100,"K_NP5":101,"K_NP6":102, + "K_NP7":103,"K_NP8":104,"K_NP9":105,"K_NPSTAR":106, + "K_NPPLUS":107,"K_SEPARATOR":108,"K_NPMINUS":109,"K_NPDOT":110, + "K_NPSLASH":111,"K_F1":112,"K_F2":113,"K_F3":114,"K_F4":115, + "K_F5":116,"K_F6":117,"K_F7":118,"K_F8":119,"K_F9":120, + "K_F10":121,"K_F11":122,"K_F12":123,"K_NUMLOCK":144,"K_SCROLL":145, + "K_LSHIFT":160,"K_RSHIFT":161,"K_LCONTROL":162,"K_RCONTROL":163, + "K_LALT":164,"K_RALT":165, + "K_COLON":186,"K_EQUAL":187,"K_COMMA":188,"K_HYPHEN":189, + "K_PERIOD":190,"K_SLASH":191,"K_BKQUOTE":192, + "K_LBRKT":219,"K_BKSLASH":220,"K_RBRKT":221, + "K_QUOTE":222,"K_oE2":226,"K_OE2":226, + "K_LOPT":50001,"K_ROPT":50002, + "K_NUMERALS":50003,"K_SYMBOLS":50004,"K_CURRENCIES":50005, + "K_UPPER":50006,"K_LOWER":50007,"K_ALPHA":50008, + "K_SHIFTED":50009,"K_ALTGR":50010, + "K_TABBACK":50011,"K_TABFWD":50012 + }, + + codesUS: [ + ['0123456789',';=,-./`', '[\\]\''], + [')!@#$%^&*(',':+<_>?~', '{|}"'] + ], + + isKnownOSKModifierKey(keyID: string): boolean { + switch(keyID) { + case 'K_SHIFT': + case 'K_LOPT': + case 'K_ROPT': + case 'K_NUMLOCK': // Often used for numeric layers. + case 'K_CAPS': + return true; + default: + if(Codes.keyCodes[keyID] >= 50000) { // A few are used by `sil_euro_latin`. + return true; // is a 'K_' key defined for layer shifting or 'control' use. + } + // Refer to text/codes.ts - these are Keyman-custom "keycodes" used for + // layer shifting keys. To be safe, we currently let K_TABBACK and + // K_TABFWD through, though we might be able to drop them too. + const code = Codes[keyID]; + if(code > 50000 && code < 50011) { return true; - default: - if(Codes.keyCodes[keyID] >= 50000) { // A few are used by `sil_euro_latin`. - return true; // is a 'K_' key defined for layer shifting or 'control' use. - } - // Refer to text/codes.ts - these are Keyman-custom "keycodes" used for - // layer shifting keys. To be safe, we currently let K_TABBACK and - // K_TABFWD through, though we might be able to drop them too. - let code = com.keyman.text.Codes[keyID]; - if(code > 50000 && code < 50011) { - return true; - } - } - - return false; + } + } + + return false; + }, + + + /** + * Get modifier key state from layer id + * + * @param {string} layerId layer id (e.g. ctrlshift) + * @return {number} modifier key state (desktop keyboards) + */ + getModifierState(layerId: string): number { + var modifier=0; + if(layerId.indexOf('shift') >= 0) { + modifier |= Codes.modifierCodes['SHIFT']; + } + + // The chiral checks must not be directly exclusive due each other to visual OSK feedback. + var ctrlMatched=false; + if(layerId.indexOf('leftctrl') >= 0) { + modifier |= Codes.modifierCodes['LCTRL']; + ctrlMatched=true; } + if(layerId.indexOf('rightctrl') >= 0) { + modifier |= Codes.modifierCodes['RCTRL']; + ctrlMatched=true; + } + if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) { + modifier |= Codes.modifierCodes['CTRL']; + } + + var altMatched=false; + if(layerId.indexOf('leftalt') >= 0) { + modifier |= Codes.modifierCodes['LALT']; + altMatched=true; + } + if(layerId.indexOf('rightalt') >= 0) { + modifier |= Codes.modifierCodes['RALT']; + altMatched=true; + } + if(layerId.indexOf('alt') >= 0 && !altMatched) { + modifier |= Codes.modifierCodes['ALT']; + } + + return modifier; + }, + + /** + * Get state key state from layer id + * + * @param {string} layerId layer id (e.g. caps) + * @return {number} modifier key state (desktop keyboards) + */ + getStateFromLayer(layerId: string): number { + var modifier=0; + + if(layerId.indexOf('caps') >= 0) { + modifier |= Codes.modifierCodes['CAPS']; + } else { + modifier |= Codes.modifierCodes['NO_CAPS']; + } + + return modifier; } -} \ No newline at end of file +} + +export default Codes; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/deadkeys.ts b/common/web/keyboard-processor/src/text/deadkeys.ts index d0f775ae0405c40d0f69c305ead0eb68c7f53bbd..e5b32a788fb7773d84df8a5c99f426b26e86fb7c 100644 --- a/common/web/keyboard-processor/src/text/deadkeys.ts +++ b/common/web/keyboard-processor/src/text/deadkeys.ts @@ -1,160 +1,157 @@ -namespace com.keyman.text { - // Defines the base Deadkey-tracking object. - - export class Deadkey { - p: number; // Position of deadkey - d: number; // Numerical id of the deadkey - o: number; // Ordinal value of the deadkey (resolves same-place conflicts) - matched: number; - - static ordinalSeed: number = 0; - - constructor(pos: number, id: number) { - this.p = pos; - this.d = id; - this.o = Deadkey.ordinalSeed++; - } +// Defines the base Deadkey-tracking object. +export class Deadkey { + p: number; // Position of deadkey + d: number; // Numerical id of the deadkey + o: number; // Ordinal value of the deadkey (resolves same-place conflicts) + matched: number; + + static ordinalSeed: number = 0; + + constructor(pos: number, id: number) { + this.p = pos; + this.d = id; + this.o = Deadkey.ordinalSeed++; + } - match(p: number, d: number): boolean { - var result:boolean = (this.p == p && this.d == d); + match(p: number, d: number): boolean { + var result:boolean = (this.p == p && this.d == d); - return result; - } - - set(): void { - this.matched = 1; - } + return result; + } - reset(): void { - this.matched = 0; - } + set(): void { + this.matched = 1; + } - before(other: Deadkey): boolean { - return this.o < other.o; - } + reset(): void { + this.matched = 0; + } - clone(): Deadkey { - let dk = new Deadkey(this.p, this.d); - dk.o = this.o; + before(other: Deadkey): boolean { + return this.o < other.o; + } - return dk; - } + clone(): Deadkey { + let dk = new Deadkey(this.p, this.d); + dk.o = this.o; - /** - * Sorts the deadkeys in reverse order. - */ - static sortFunc = function(a: Deadkey, b: Deadkey) { - // We want descending order, so we want 'later' deadkeys first. - if(a.p != b.p) { - return b.p - a.p; - } else { - return b.o - a.o; - } - }; + return dk; } - // Object-orients deadkey management. - export class DeadkeyTracker { - dks: Deadkey[] = []; + /** + * Sorts the deadkeys in reverse order. + */ + static sortFunc = function(a: Deadkey, b: Deadkey) { + // We want descending order, so we want 'later' deadkeys first. + if(a.p != b.p) { + return b.p - a.p; + } else { + return b.o - a.o; + } + }; +} + +// Object-orients deadkey management. +export class DeadkeyTracker { + dks: Deadkey[] = []; + + toSortedArray(): Deadkey[] { + this.dks = this.dks.sort(Deadkey.sortFunc); + return [].concat(this.dks); + } - toSortedArray(): Deadkey[] { - this.dks = this.dks.sort(Deadkey.sortFunc); - return [].concat(this.dks); - } + clone(): DeadkeyTracker { + let dkt = new DeadkeyTracker(); + let dks = this.toSortedArray(); - clone(): DeadkeyTracker { - let dkt = new DeadkeyTracker(); - let dks = this.toSortedArray(); + // Make sure to clone the deadkeys themselves - the Deadkey object is mutable. + dkt.dks = []; + dks.forEach(function(value: Deadkey) { + dkt.dks.push(value.clone()); + }); - // Make sure to clone the deadkeys themselves - the Deadkey object is mutable. - dkt.dks = []; - dks.forEach(function(value: Deadkey) { - dkt.dks.push(value.clone()); - }); + return dkt; + } - return dkt; - } - - /** - * Function isMatch - * Scope Public - * @param {number} caretPos current cursor position - * @param {number} n expected offset of deadkey from cursor - * @param {number} d deadkey - * @return {boolean} True if deadkey found selected context matches val - * Description Match deadkey at current cursor position - */ - isMatch(caretPos: number, n: number, d: number): boolean { - if(this.dks.length == 0) { - return false; // I3318 + /** + * Function isMatch + * Scope Public + * @param {number} caretPos current cursor position + * @param {number} n expected offset of deadkey from cursor + * @param {number} d deadkey + * @return {boolean} True if deadkey found selected context matches val + * Description Match deadkey at current cursor position + */ + isMatch(caretPos: number, n: number, d: number): boolean { + if(this.dks.length == 0) { + return false; // I3318 + } + + var sp=caretPos; + n = sp - n; + for(var i = 0; i < this.dks.length; i++) { + // Don't re-match an already-matched deadkey. It's possible to have two identical + // entries, and they should be kept separately. + if(this.dks[i].match(n, d) && !this.dks[i].matched) { + this.dks[i].set(); + // Assumption: since we match the first possible entry in the array, we + // match the entry with the lower ordinal - the 'first' deadkey in the position. + return true; // I3318 } + } - var sp=caretPos; - n = sp - n; - for(var i = 0; i < this.dks.length; i++) { - // Don't re-match an already-matched deadkey. It's possible to have two identical - // entries, and they should be kept separately. - if(this.dks[i].match(n, d) && !this.dks[i].matched) { - this.dks[i].set(); - // Assumption: since we match the first possible entry in the array, we - // match the entry with the lower ordinal - the 'first' deadkey in the position. - return true; // I3318 - } - } + this.resetMatched(); // I3318 - this.resetMatched(); // I3318 + return false; + } - return false; - } + add(dk: Deadkey) { + this.dks = this.dks.concat(dk); + } - add(dk: Deadkey) { - this.dks = this.dks.concat(dk); - } + remove(dk: Deadkey) { + var index = this.dks.indexOf(dk); + this.dks.splice(index, 1); + } - remove(dk: Deadkey) { - var index = this.dks.indexOf(dk); - this.dks.splice(index, 1); - } + clear() { + this.dks = []; + } - clear() { - this.dks = []; + resetMatched() { + for(let dk of this.dks) { + dk.reset(); } + } - resetMatched() { - for(let dk of this.dks) { - dk.reset(); - } - } - - deleteMatched(): void { - for(var Li = 0; Li < this.dks.length; Li++) { - if(this.dks[Li].matched) { - this.dks.splice(Li--, 1); // Don't forget to decrement! - } + deleteMatched(): void { + for(var Li = 0; Li < this.dks.length; Li++) { + if(this.dks[Li].matched) { + this.dks.splice(Li--, 1); // Don't forget to decrement! } } + } - /** - * Function adjustPositions (formerly _DeadkeyAdjustPos) - * Scope Private - * @param {number} Lstart start position in context - * @param {number} Ldelta characters to adjust by - * Description Adjust saved positions of deadkeys in context - */ - adjustPositions(Lstart: number, Ldelta: number): void { - if(Ldelta == 0) { - return; - } - - for(let dk of this.dks) { - if(dk.p > Lstart) { - dk.p += Ldelta; - } + /** + * Function adjustPositions (formerly _DeadkeyAdjustPos) + * Scope Private + * @param {number} Lstart start position in context + * @param {number} Ldelta characters to adjust by + * Description Adjust saved positions of deadkeys in context + */ + adjustPositions(Lstart: number, Ldelta: number): void { + if(Ldelta == 0) { + return; + } + + for(let dk of this.dks) { + if(dk.p > Lstart) { + dk.p += Ldelta; } } + } - count(): number { - return this.dks.length; - } + count(): number { + return this.dks.length; } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/defaultOutput.ts b/common/web/keyboard-processor/src/text/defaultOutput.ts deleted file mode 100644 index 9d96f4bd1cc8aee6f1df20150e9a0d55fabf3ecc..0000000000000000000000000000000000000000 --- a/common/web/keyboard-processor/src/text/defaultOutput.ts +++ /dev/null @@ -1,191 +0,0 @@ -// Establishes key-code definitions. -/// -// Defines our generalized "KeyEvent" class. -/// - -namespace com.keyman.text { - export enum EmulationKeystrokes { - Enter = '\n', - Backspace = '\b' - } - - /** - * Defines a collection of static library functions that define KeymanWeb's default (implied) keyboard rule behaviors. - */ - export class DefaultOutput { - private constructor() { - } - - static codeForEvent(Lkc: KeyEvent) { - return Codes.keyCodes[Lkc.kName] || Lkc.Lcode;; - } - - /** - * Serves as a default keycode lookup table. This may be referenced safely by mnemonic handling without fear of side-effects. - * Also used by Processor.defaultRuleBehavior to generate output after filtering for special cases. - */ - public static forAny(Lkc: KeyEvent, isMnemonic: boolean, ruleBehavior?: RuleBehavior) { - var char = ''; - - // A pretty simple table of lookups, corresponding VERY closely to the original defaultKeyOutput. - if((char = DefaultOutput.forSpecialEmulation(Lkc, ruleBehavior)) != null) { - return char; - } else if(!isMnemonic && ((char = DefaultOutput.forNumpadKeys(Lkc, ruleBehavior)) != null)) { - return char; - } else if((char = DefaultOutput.forUnicodeKeynames(Lkc, ruleBehavior)) != null) { - return char; - } else if((char = DefaultOutput.forBaseKeys(Lkc, ruleBehavior)) != null) { - return char; - } else { - // // For headless and embeddded, we may well allow '\t'. It's DOM mode that has other uses. - // // Not originally defined for text output within defaultKeyOutput. - // // We can't enable it yet, as it'll cause hardware keystrokes in the DOM to output '\t' rather - // // than rely on the browser-default handling. - let code = DefaultOutput.codeForEvent(Lkc); - switch(code) { - // case Codes.keyCodes['K_TAB']: - // case Codes.keyCodes['K_TABBACK']: - // case Codes.keyCodes['K_TABFWD']: - // return '\t'; - default: - return null; - } - } - } - - /** - * isCommand - returns a boolean indicating if a non-text event should be triggered by the keystroke. - */ - public static isCommand(Lkc: KeyEvent): boolean { - let code = DefaultOutput.codeForEvent(Lkc); - - switch(code) { - // Should we ever implement them: - // case Codes.keyCodes['K_LEFT']: // would not output text, but would alter the caret's position in the context. - // case Codes.keyCodes['K_RIGHT']: - // return true; - default: - return false; - } - } - - /** - * Used when a RuleBehavior represents a non-text "command" within the Engine. This will generally - * trigger events that require context reset - often by moving the caret or by moving what OutputTarget - * the caret is in. However, we let those events perform the actual context reset. - * - * Note: is extended by DOM-aware KeymanWeb code. - */ - public static applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void { - // Notes for potential default-handling extensions: - // - // switch(code) { - // // Problem: clusters, and doing them right. - // // The commented-out code below should be a decent starting point, but clusters make it complex. - // // Mostly based on pre-12.0 code, but the general idea should be relatively clear. - // - // case Codes.keyCodes['K_LEFT']: - // if(touchAlias) { - // var caretPos = keymanweb.getTextCaret(Lelem); - // keymanweb.setTextCaret(Lelem, caretPos - 1 >= 0 ? caretPos - 1 : 0); - // } - // break; - // case Codes.keyCodes['K_RIGHT']: - // if(touchAlias) { - // var caretPos = keymanweb.getTextCaret(Lelem); - // keymanweb.setTextCaret(Lelem, caretPos + 1); - // } - // if(code == VisualKeyboard.keyCodes['K_RIGHT']) { - // break; - // } - // } - // - // Note that these would be useful even outside of a DOM context. - } - - /** - * Codes matched here generally have default implementations when in a browser but require emulation - * for 'synthetic' `OutputTarget`s like `Mock`s, which have no default text handling. - */ - public static forSpecialEmulation(Lkc: KeyEvent, ruleBehavior?: RuleBehavior): EmulationKeystrokes { - let code = DefaultOutput.codeForEvent(Lkc); - - switch(code) { - case Codes.keyCodes['K_BKSP']: - return EmulationKeystrokes.Backspace; - case Codes.keyCodes['K_ENTER']: - return EmulationKeystrokes.Enter; - // case Codes.keyCodes['K_DEL']: - // return '\u007f'; // 127, ASCII / Unicode control code for DEL. - default: - return null; - } - } - - // Should not be used for mnenomic keyboards. forAny()'s use of this method checks first. - public static forNumpadKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) { - // Translate numpad keystrokes into their non-numpad equivalents - if(Lkc.Lcode >= Codes.keyCodes["K_NP0"] && Lkc.Lcode <= Codes.keyCodes["K_NPSLASH"]) { - // Number pad, numlock on - if(Lkc.Lcode < 106) { - var Lch = Lkc.Lcode-48; - } else { - Lch = Lkc.Lcode-64; - } - let ch = String._kmwFromCharCode(Lch); //I3319 - return ch; - } else { - return null; - } - } - - // Test for fall back to U_xxxxxx key id - // For this first test, we ignore the keyCode and use the keyName - public static forUnicodeKeynames(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) { - const keyName = Lkc.kName; - - return keyboards.ActiveKey.unicodeIDToText(keyName, (codeWithError) => { - ruleBehavior.errorLog = ("Suppressing Unicode control code in " + keyName + ": " + codeWithError); - }); - } - - // Test for otherwise unimplemented keys on the the base default & shift layers. - // Those keys must be blocked by keyboard rules if intentionally unimplemented; otherwise, this function will trigger. - public static forBaseKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) { - let n = Lkc.Lcode; - let keyShiftState = Lkc.Lmodifiers; - - // check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs. - // TODO: Extend to allow AltGr as well - better mnemonic support. - if(keyShiftState == Codes.modifierCodes['SHIFT']) { - keyShiftState = 1; - } else if(keyShiftState != 0) { - if(ruleBehavior) { - ruleBehavior.warningLog = "KMW only defines default key output for the 'default' and 'shift' layers!"; - } - return null; - } - - // Now that keyShiftState is either 0 or 1, we can use the following structure to determine the default output. - try { - if(n == Codes.keyCodes['K_SPACE']) { - return ' '; - } else if(n >= Codes.keyCodes['K_0'] && n <= Codes.keyCodes['K_9']) { // The number keys. - return Codes.codesUS[keyShiftState][0][n-Codes.keyCodes['K_0']]; - } else if(n >= Codes.keyCodes['K_A'] && n <= Codes.keyCodes['K_Z']) { // The base letter keys - return String.fromCharCode(n+(keyShiftState?0:32)); // 32 is the offset from uppercase to lowercase. - } else if(n >= Codes.keyCodes['K_COLON'] && n <= Codes.keyCodes['K_BKQUOTE']) { - return Codes.codesUS[keyShiftState][1][n-Codes.keyCodes['K_COLON']]; - } else if(n >= Codes.keyCodes['K_LBRKT'] && n <= Codes.keyCodes['K_QUOTE']) { - return Codes.codesUS[keyShiftState][2][n-Codes.keyCodes['K_LBRKT']]; - } - } catch (e) { - if(ruleBehavior) { - ruleBehavior.errorLog = "Error detected with default mapping for key: code = " + n + ", shift state = " + (keyShiftState == 1 ? 'shift' : 'default'); - } - } - - return null; - } - } -} diff --git a/common/web/keyboard-processor/src/text/defaultRules.ts b/common/web/keyboard-processor/src/text/defaultRules.ts new file mode 100644 index 0000000000000000000000000000000000000000..5ad708bd67a1220e0da024f33e7279df3f9b3a58 --- /dev/null +++ b/common/web/keyboard-processor/src/text/defaultRules.ts @@ -0,0 +1,225 @@ +// TODO: Move to separate folder: 'codes' +// We should start splitting off code needed by keyboards even without a KeyboardProcessor active. +// There's an upcoming `/common/web/types` package that 'codes' and 'keyboards' may fit well within. + +import Codes from "./codes.js"; +import type KeyEvent from "./keyEvent.js"; +import type OutputTarget from "./outputTarget.js"; + +// The only members referenced are to produce warning and error logs. A little abstraction +// via an optional 'logger' interface can maintain it while facilitating a the split alluded +// to above. +// +// Alternatively, we could just... not take in the parameter at all, which'd also facilitate +// the future modularization effort. +import RuleBehavior from "./ruleBehavior.js"; + +export enum EmulationKeystrokes { + Enter = '\n', + Backspace = '\b' +} + +/** + * Defines a collection of static library functions that define KeymanWeb's default (implied) keyboard rule behaviors. + */ +export default class DefaultRules { + public constructor() { + } + + codeForEvent(Lkc: KeyEvent) { + return Codes.keyCodes[Lkc.kName] || Lkc.Lcode;; + } + + /** + * Serves as a default keycode lookup table. This may be referenced safely by mnemonic handling without fear of side-effects. + * Also used by Processor.defaultRuleBehavior to generate output after filtering for special cases. + */ + public forAny(Lkc: KeyEvent, isMnemonic: boolean, ruleBehavior?: RuleBehavior) { + var char = ''; + + // A pretty simple table of lookups, corresponding VERY closely to the original defaultKeyOutput. + if((char = this.forSpecialEmulation(Lkc)) != null) { + return char; + } else if(!isMnemonic && ((char = this.forNumpadKeys(Lkc)) != null)) { + return char; + } else if((char = this.forUnicodeKeynames(Lkc, ruleBehavior)) != null) { + return char; + } else if((char = this.forBaseKeys(Lkc, ruleBehavior)) != null) { + return char; + } else { + // // For headless and embeddded, we may well allow '\t'. It's DOM mode that has other uses. + // // Not originally defined for text output within defaultKeyOutput. + // // We can't enable it yet, as it'll cause hardware keystrokes in the DOM to output '\t' rather + // // than rely on the browser-default handling. + let code = this.codeForEvent(Lkc); + switch(code) { + // case Codes.keyCodes['K_TAB']: + // case Codes.keyCodes['K_TABBACK']: + // case Codes.keyCodes['K_TABFWD']: + // return '\t'; + default: + return null; + } + } + } + + /** + * isCommand - returns a boolean indicating if a non-text event should be triggered by the keystroke. + */ + public isCommand(Lkc: KeyEvent): boolean { + let code = this.codeForEvent(Lkc); + + switch(code) { + // Should we ever implement them: + // case Codes.keyCodes['K_LEFT']: // would not output text, but would alter the caret's position in the context. + // case Codes.keyCodes['K_RIGHT']: + // return true; + default: + return false; + } + } + + /** + * Used when a RuleBehavior represents a non-text "command" within the Engine. This will generally + * trigger events that require context reset - often by moving the caret or by moving what OutputTarget + * the caret is in. However, we let those events perform the actual context reset. + * + * Note: is extended by DOM-aware KeymanWeb code. + */ + public applyCommand(Lkc: KeyEvent, outputTarget: OutputTarget): void { + // Notes for potential default-handling extensions: + // + // switch(code) { + // // Problem: clusters, and doing them right. + // // The commented-out code below should be a decent starting point, but clusters make it complex. + // // Mostly based on pre-12.0 code, but the general idea should be relatively clear. + // + // case Codes.keyCodes['K_LEFT']: + // if(touchAlias) { + // var caretPos = keymanweb.getTextCaret(Lelem); + // keymanweb.setTextCaret(Lelem, caretPos - 1 >= 0 ? caretPos - 1 : 0); + // } + // break; + // case Codes.keyCodes['K_RIGHT']: + // if(touchAlias) { + // var caretPos = keymanweb.getTextCaret(Lelem); + // keymanweb.setTextCaret(Lelem, caretPos + 1); + // } + // if(code == VisualKeyboard.keyCodes['K_RIGHT']) { + // break; + // } + // } + // + // Note that these would be useful even outside of a DOM context. + } + + /** + * Codes matched here generally have default implementations when in a browser but require emulation + * for 'synthetic' `OutputTarget`s like `Mock`s, which have no default text handling. + */ + public forSpecialEmulation(Lkc: KeyEvent): EmulationKeystrokes { + let code = this.codeForEvent(Lkc); + + switch(code) { + case Codes.keyCodes['K_BKSP']: + return EmulationKeystrokes.Backspace; + case Codes.keyCodes['K_ENTER']: + return EmulationKeystrokes.Enter; + // case Codes.keyCodes['K_DEL']: + // return '\u007f'; // 127, ASCII / Unicode control code for DEL. + default: + return null; + } + } + + // Should not be used for mnenomic keyboards. forAny()'s use of this method checks first. + public forNumpadKeys(Lkc: KeyEvent) { + // Translate numpad keystrokes into their non-numpad equivalents + if(Lkc.Lcode >= Codes.keyCodes["K_NP0"] && Lkc.Lcode <= Codes.keyCodes["K_NPSLASH"]) { + // Number pad, numlock on + if(Lkc.Lcode < 106) { + var Lch = Lkc.Lcode-48; + } else { + Lch = Lkc.Lcode-64; + } + let ch = String._kmwFromCharCode(Lch); //I3319 + return ch; + } else { + return null; + } + } + + // Test for fall back to U_xxxxxx key id + // For this first test, we ignore the keyCode and use the keyName + public forUnicodeKeynames(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) { + const keyName = Lkc.kName; + + // Test for fall back to U_xxxxxx key id + // For this first test, we ignore the keyCode and use the keyName + if(!keyName || keyName.substr(0,2) != 'U_') { + return null; + } + + let result = ''; + const codePoints = keyName.substr(2).split('_'); + for(let codePoint of codePoints) { + const codePointValue = parseInt(codePoint, 16); + if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || ((0x80 <= codePointValue) && (codePointValue <= 0x9F)) || isNaN(codePointValue)) { + // Code points [U_0000 - U_001F] and [U_0080 - U_009F] refer to Unicode C0 and C1 control codes. + // Check the codePoint number and do not allow output of these codes via U_xxxxxx shortcuts. + // Also handles invalid identifiers (e.g. `U_ghij`) for which parseInt returns NaN + if(ruleBehavior) { + ruleBehavior.errorLog = ("Suppressing Unicode control code in " + keyName); + } + // We'll attempt to add valid chars + continue; + } else { + // String.fromCharCode() is inadequate to handle the entire range of Unicode + // Someday after upgrading to ES2015, can use String.fromCodePoint() + result += String.kmwFromCharCode(codePointValue); + } + } + return result ? result : null; + } + + // Test for otherwise unimplemented keys on the the base default & shift layers. + // Those keys must be blocked by keyboard rules if intentionally unimplemented; otherwise, this function will trigger. + public forBaseKeys(Lkc: KeyEvent, ruleBehavior?: RuleBehavior) { + let n = Lkc.Lcode; + let keyShiftState = Lkc.Lmodifiers; + + // check if exact match to SHIFT's code. Only the 'default' and 'shift' layers should have default key outputs. + // TODO: Extend to allow AltGr as well - better mnemonic support. + if(keyShiftState == Codes.modifierCodes['SHIFT']) { + keyShiftState = 1; + } else if(keyShiftState != 0) { + if(ruleBehavior) { + ruleBehavior.warningLog = "KMW only defines default key output for the 'default' and 'shift' layers!"; + } + return null; + } + + // Now that keyShiftState is either 0 or 1, we can use the following structure to determine the default output. + try { + if(n == Codes.keyCodes['K_SPACE']) { + return ' '; + } else if(n >= Codes.keyCodes['K_0'] && n <= Codes.keyCodes['K_9']) { // The number keys. + return Codes.codesUS[keyShiftState][0][n-Codes.keyCodes['K_0']]; + } else if(n >= Codes.keyCodes['K_A'] && n <= Codes.keyCodes['K_Z']) { // The base letter keys + return String.fromCharCode(n+(keyShiftState?0:32)); // 32 is the offset from uppercase to lowercase. + } else if(n >= Codes.keyCodes['K_COLON'] && n <= Codes.keyCodes['K_BKQUOTE']) { + return Codes.codesUS[keyShiftState][1][n-Codes.keyCodes['K_COLON']]; + } else if(n >= Codes.keyCodes['K_LBRKT'] && n <= Codes.keyCodes['K_QUOTE']) { + return Codes.codesUS[keyShiftState][2][n-Codes.keyCodes['K_LBRKT']]; + } else if(n == Codes.keyCodes['K_oE2']) { + return keyShiftState ? '|' : '\\'; + } + } catch (e) { + if(ruleBehavior) { + ruleBehavior.errorLog = "Error detected with default mapping for key: code = " + n + ", shift state = " + (keyShiftState == 1 ? 'shift' : 'default'); + } + } + + return null; + } +} diff --git a/common/web/keyboard-processor/src/text/kbdInterface.ts b/common/web/keyboard-processor/src/text/kbdInterface.ts index 672ecd4dedb266caf424476f266b0bdfde639af9..311a3a75e82c92abbe2658a7d4a86308a4123a0c 100644 --- a/common/web/keyboard-processor/src/text/kbdInterface.ts +++ b/common/web/keyboard-processor/src/text/kbdInterface.ts @@ -1,1164 +1,1178 @@ -/// -/// - -// Defines classes for handling system stores -/// - /*** KeymanWeb 11.0 Copyright 2019 SIL International ***/ -namespace com.keyman.text { - //#region Helper type definitions +//#region Imports - export class KeyInformation { - vk: boolean; - code: number; - modifiers: number; - } +import { type DeviceSpec } from "@keymanapp/web-utils"; - /* - * Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+). - * No constructors or methods since keyboards will not utilize the same backing prototype, and - * property names are shorthanded to promote minification. - */ - type PlainKeyboardStore = string; +import Codes from "./codes.js"; +import type KeyEvent from "./keyEvent.js"; +import type { Deadkey } from "./deadkeys.js"; +import KeyMapping from "./keyMapping.js"; +import { SystemStore, MutableSystemStore, PlatformSystemStore } from "./systemStores.js"; +import type { VariableStoreSerializer } from "./keyboardProcessor.js"; +import type OutputTarget from "./outputTarget.js"; +import { Mock } from "./outputTarget.js"; +import RuleBehavior from "./ruleBehavior.js"; +import Keyboard, { VariableStoreDictionary } from "../keyboards/keyboard.js"; +import { KeyboardHarness, KeyboardKeymanGlobal } from "../keyboards/keyboardHarness.js"; - export type KeyboardStoreElement = (string|StoreNonCharEntry); - export type ComplexKeyboardStore = KeyboardStoreElement[]; +//#endregion - type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore; +//#region Helper type definitions - export type VariableStore = {[name: string]: string}; +export class KeyInformation { + vk: boolean; + code: number; + modifiers: number; +} - type RuleChar = string; +/* +* Type alias definitions to reflect the parameters of the fullContextMatch() callback (KMW 10+). +* No constructors or methods since keyboards will not utilize the same backing prototype, and +* property names are shorthanded to promote minification. +*/ +type PlainKeyboardStore = string; - class RuleDeadkey { - /** Discriminant field - 'd' for Deadkey. - */ - ['t']: 'd'; +export type KeyboardStoreElement = (string|StoreNonCharEntry); +export type ComplexKeyboardStore = KeyboardStoreElement[]; - /** - * Value: the deadkey's ID. - */ - ['d']: number; // For 'd'eadkey; also reflects the Deadkey class's 'd' property. - } +type KeyboardStore = PlainKeyboardStore | ComplexKeyboardStore; - class ContextAny { - /** Discriminant field - 'a' for `any()`. - */ - ['t']: 'a'; +export type VariableStore = {[name: string]: string}; - /** - * Value: the store to search. - */ - ['a']: KeyboardStore; // For 'a'ny statement. +type RuleChar = string; - /** - * If set to true, negates the 'any'. - */ - ['n']: boolean|0|1; - } - - class RuleIndex { - /** Discriminant field - 'i' for `index()`. - */ - ['t']: 'i'; +class RuleDeadkey { + /** Discriminant field - 'd' for Deadkey. + */ + ['t']: 'd'; - /** - * Value: the Store from which to output - */ - ['i']: KeyboardStore; + /** + * Value: the deadkey's ID. + */ + ['d']: number; // For 'd'eadkey; also reflects the Deadkey class's 'd' property. +} - /** - * Offset: the offset in context for the corresponding `any()`. - */ - ['o']: number; - } +class ContextAny { + /** Discriminant field - 'a' for `any()`. + */ + ['t']: 'a'; - class ContextEx { - /** Discriminant field - 'c' for `context()`. - */ - ['t']: 'c'; + /** + * Value: the store to search. + */ + ['a']: KeyboardStore; // For 'a'ny statement. - /** - * Value: The offset into the current rule's context to be matched. - */ - ['c']: number; // For 'c'ontext statement. - } + /** + * If set to true, negates the 'any'. + */ + ['n']: boolean|0|1; +} - class ContextNul { - /** Discriminant field - 'n' for `nul` - */ - ['t']: 'n'; - } +class RuleIndex { + /** Discriminant field - 'i' for `index()`. + */ + ['t']: 'i'; - class StoreBeep { - /** Discriminant field - 'b' for `beep` - */ - ['t']: 'b'; - } + /** + * Value: the Store from which to output + */ + ['i']: KeyboardStore; - type ContextNonCharEntry = RuleDeadkey | ContextAny | RuleIndex | ContextEx | ContextNul; - type ContextEntry = RuleChar | ContextNonCharEntry; + /** + * Offset: the offset in context for the corresponding `any()`. + */ + ['o']: number; +} - type StoreNonCharEntry = RuleDeadkey | StoreBeep; +class ContextEx { + /** Discriminant field - 'c' for `context()`. + */ + ['t']: 'c'; /** - * Cache of context storing and retrieving return values from KC - * Must be reset prior to each keystroke and after any text changes - * MCD 3/1/14 - **/ - class CachedContext { - _cache: string[][]; - - reset(): void { - this._cache = []; - } - - get(n: number, ln: number): string { - // return null; // uncomment this line to disable context caching - if(typeof this._cache[n] == 'undefined') { - return null; - } else if(typeof this._cache[n][ln] == 'undefined') { - return null; - } - return this._cache[n][ln]; - } + * Value: The offset into the current rule's context to be matched. + */ + ['c']: number; // For 'c'ontext statement. +} - set(n: number, ln: number, val: string): void { - if(typeof this._cache[n] == 'undefined') { - this._cache[n] = []; - } - this._cache[n][ln] = val; - } - }; +class ContextNul { + /** Discriminant field - 'n' for `nul` + */ + ['t']: 'n'; +} - type CachedExEntry = {valContext: (string|number)[], deadContext: text.Deadkey[]}; - /** - * An extended version of cached context storing designed to work with - * `fullContextMatch` and its helper functions. +class StoreBeep { + /** Discriminant field - 'b' for `beep` */ - class CachedContextEx { - _cache: CachedExEntry[][]; + ['t']: 'b'; +} + +type ContextNonCharEntry = RuleDeadkey | ContextAny | RuleIndex | ContextEx | ContextNul; +type ContextEntry = RuleChar | ContextNonCharEntry; + +type StoreNonCharEntry = RuleDeadkey | StoreBeep; - reset(): void { - this._cache = []; +/** + * Cache of context storing and retrieving return values from KC + * Must be reset prior to each keystroke and after any text changes + * MCD 3/1/14 + **/ +class CachedContext { + _cache: string[][]; + + reset(): void { + this._cache = []; + } + + get(n: number, ln: number): string { + // return null; // uncomment this line to disable context caching + if(typeof this._cache[n] == 'undefined') { + return null; + } else if(typeof this._cache[n][ln] == 'undefined') { + return null; } + return this._cache[n][ln]; + } - get(n: number, ln: number): CachedExEntry { - // return null; // uncomment this line to disable context caching - if(typeof this._cache[n] == 'undefined') { - return null; - } else if(typeof this._cache[n][ln] == 'undefined') { - return null; - } - return this._cache[n][ln]; + set(n: number, ln: number, val: string): void { + if(typeof this._cache[n] == 'undefined') { + this._cache[n] = []; } + this._cache[n][ln] = val; + } +}; + +type CachedExEntry = {valContext: (string|number)[], deadContext: Deadkey[]}; +/** + * An extended version of cached context storing designed to work with + * `fullContextMatch` and its helper functions. + */ +class CachedContextEx { + _cache: CachedExEntry[][]; + + reset(): void { + this._cache = []; + } - set(n: number, ln: number, val: CachedExEntry): void { - if(typeof this._cache[n] == 'undefined') { - this._cache[n] = []; - } - this._cache[n][ln] = val; + get(n: number, ln: number): CachedExEntry { + // return null; // uncomment this line to disable context caching + if(typeof this._cache[n] == 'undefined') { + return null; + } else if(typeof this._cache[n][ln] == 'undefined') { + return null; } + return this._cache[n][ln]; + } - clone(): CachedContextEx { - let r = new CachedContextEx(); - r._cache = this._cache; - return r; + set(n: number, ln: number, val: CachedExEntry): void { + if(typeof this._cache[n] == 'undefined') { + this._cache[n] = []; } - }; + this._cache[n][ln] = val; + } - //#endregion + clone(): CachedContextEx { + let r = new CachedContextEx(); + r._cache = this._cache; + return r; + } +}; - export class KeyboardInterface { - static readonly GLOBAL_NAME = 'KeymanWeb'; +export enum SystemStoreIDs { + TSS_LAYER = 33, + TSS_PLATFORM = 31, + TSS_NEWLAYER = 42, + TSS_OLDLAYER = 43 +} - cachedContext: CachedContext = new CachedContext(); - cachedContextEx: CachedContextEx = new CachedContextEx(); - ruleContextEx: CachedContextEx; +//#endregion - activeTargetOutput: OutputTarget; - ruleBehavior: RuleBehavior; +export default class KeyboardInterface extends KeyboardHarness { + static readonly GLOBAL_NAME = 'KeymanWeb'; - static readonly TSS_LAYER: number = 33; - static readonly TSS_PLATFORM: number = 31; - static readonly TSS_NEWLAYER: number = 42; - static readonly TSS_OLDLAYER: number = 43; + cachedContext: CachedContext = new CachedContext(); + cachedContextEx: CachedContextEx = new CachedContextEx(); + ruleContextEx: CachedContextEx; - systemStores: {[storeID: number]: SystemStore}; + activeTargetOutput: OutputTarget; + ruleBehavior: RuleBehavior; - _AnyIndices: number[] = []; // AnyIndex - array of any/index match indices + systemStores: {[storeID: number]: SystemStore}; - // Must be accessible to some of the keyboard API methods. - activeKeyboard: keyboards.Keyboard; - activeDevice: utils.DeviceSpec; + _AnyIndices: number[] = []; // AnyIndex - array of any/index match indices - variableStoreSerializer?: VariableStoreSerializer; + // Must be accessible to some of the keyboard API methods. + activeKeyboard: Keyboard; + activeDevice: DeviceSpec; - constructor(variableStoreSerializer: VariableStoreSerializer = null) { - this.systemStores = {}; + variableStoreSerializer?: VariableStoreSerializer; - this.systemStores[KeyboardInterface.TSS_PLATFORM] = new PlatformSystemStore(this); - this.systemStores[KeyboardInterface.TSS_LAYER] = new MutableSystemStore(KeyboardInterface.TSS_LAYER, 'default'); - this.systemStores[KeyboardInterface.TSS_NEWLAYER] = new MutableSystemStore(KeyboardInterface.TSS_NEWLAYER, ''); - this.systemStores[KeyboardInterface.TSS_OLDLAYER] = new MutableSystemStore(KeyboardInterface.TSS_OLDLAYER, ''); + // A 'reference point' that debug keyboards may use to access KMW's code constants. + public get Codes(): typeof Codes { + return Codes; + } - this.variableStoreSerializer = variableStoreSerializer; - } + constructor(_jsGlobal: any, keymanGlobal: KeyboardKeymanGlobal, variableStoreSerializer: VariableStoreSerializer = null) { + super(_jsGlobal, keymanGlobal); - /** - * Function KSF - * Scope Public - * - * Saves the document's current focus settings on behalf of the keyboard. Often paired with insertText. - */ - saveFocus(): void { } - - /** - * A text-insertion method used by custom OSKs for helpHTML interaction, like with sil_euro_latin. - * - * This function currently bypasses web-core's standard text handling control path and all predictive text processing. - * It also has DOM-dependencies that help ensure KMW's active OutputTarget retains focus during use. - */ - insertText?: (Ptext: string, PdeadKey: number) => boolean; - - /** - * Function registerKeyboard KR - * Scope Public - * @param {Object} Pk Keyboard object - * Description Registers a keyboard with KeymanWeb once its script has fully loaded. - * - * In web-core, this also activates the keyboard; in other modules, this method - * may be replaced with other implementations. - */ - registerKeyboard(Pk): void { - // NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale, - // by DOM-aware code. - let keyboard = new keyboards.Keyboard(Pk); - this.activeKeyboard = keyboard; - } + this.systemStores = {}; - /** - * Used by DOM-aware KeymanWeb to add keyboard stubs, used by the `KeyboardManager` type - * to optimize resource use. - */ - registerStub?: (Pstub) => number; - - /** - * Get *cached or uncached* keyboard context for a specified range, relative to caret - * - * @param {number} n Number of characters to move back from caret - * @param {number} ln Number of characters to return - * @param {Object} Pelem Element to work with (must be currently focused element) - * @return {string} Context string - * - * Example [abcdef|ghi] as INPUT, with the caret position marked by |: - * KC(2,1,Pelem) == "e" - * KC(3,3,Pelem) == "def" - * KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string - */ - - context(n: number, ln: number, outputTarget: OutputTarget): string { - var v = this.cachedContext.get(n, ln); - if(v !== null) { - return v; - } + this.systemStores[SystemStoreIDs.TSS_PLATFORM] = new PlatformSystemStore(this); + this.systemStores[SystemStoreIDs.TSS_LAYER] = new MutableSystemStore(SystemStoreIDs.TSS_LAYER, 'default'); + this.systemStores[SystemStoreIDs.TSS_NEWLAYER] = new MutableSystemStore(SystemStoreIDs.TSS_NEWLAYER, ''); + this.systemStores[SystemStoreIDs.TSS_OLDLAYER] = new MutableSystemStore(SystemStoreIDs.TSS_OLDLAYER, ''); - var r = this.KC_(n, ln, outputTarget); - this.cachedContext.set(n, ln, r); - return r; - } + this.variableStoreSerializer = variableStoreSerializer; + } - /** - * Get (uncached) keyboard context for a specified range, relative to caret - * - * @param {number} n Number of characters to move back from caret - * @param {number} ln Number of characters to return - * @param {Object} Pelem Element to work with (must be currently focused element) - * @return {string} Context string - * - * Example [abcdef|ghi] as INPUT, with the caret position marked by |: - * KC(2,1,Pelem) == "e" - * KC(3,3,Pelem) == "def" - * KC(10,10,Pelem) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE - */ - private KC_(n: number, ln: number, outputTarget: OutputTarget): string { - var tempContext = ''; - - // If we have a selection, we have an empty context - tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : ""; - - if(tempContext._kmwLength() < n) { - tempContext = Array(n-tempContext._kmwLength()+1).join("\uFFFE") + tempContext; - } + /** + * Function KSF + * Scope Public + * + * Saves the document's current focus settings on behalf of the keyboard. Often paired with insertText. + */ + saveFocus(): void { } + + /** + * A text-insertion method used by custom OSKs for helpHTML interaction, like with sil_euro_latin. + * + * This function currently bypasses web-core's standard text handling control path and all predictive text processing. + * It also has DOM-dependencies that help ensure KMW's active OutputTarget retains focus during use. + */ + insertText?: (Ptext: string, PdeadKey: number) => void; + + /** + * Function registerKeyboard KR + * Scope Public + * @param {Object} Pk Keyboard object + * Description Registers a keyboard with KeymanWeb once its script has fully loaded. + * + * In web-core, this also activates the keyboard; in other modules, this method + * may be replaced with other implementations. + */ + registerKeyboard(Pk): void { + // NOTE: This implementation is web-core specific and is intentionally replaced, whole-sale, + // by DOM-aware code. + let keyboard = new Keyboard(Pk); + this.loadedKeyboard = keyboard; + } - return tempContext._kmwSubstr(-n)._kmwSubstr(0,ln); + /** + * Get *cached or uncached* keyboard context for a specified range, relative to caret + * + * @param {number} n Number of characters to move back from caret + * @param {number} ln Number of characters to return + * @param {Object} Pelem Element to work with (must be currently focused element) + * @return {string} Context string + * + * Example [abcdef|ghi] as INPUT, with the caret position marked by |: + * KC(2,1,Pelem) == "e" + * KC(3,3,Pelem) == "def" + * KC(10,10,Pelem) == "abcdef" i.e. return as much as possible of the requested string + */ + + context(n: number, ln: number, outputTarget: OutputTarget): string { + var v = this.cachedContext.get(n, ln); + if(v !== null) { + return v; } - /** - * Function nul KN - * Scope Public - * @param {number} n Length of context to check - * @param {Object} Ptarg Element to work with (must be currently focused element) - * @return {boolean} True if length of context is less than or equal to n - * Description Test length of context, return true if the length of the context is less than or equal to n - * - * Example [abc|def] as INPUT, with the caret position marked by |: - * KN(3,Pelem) == TRUE - * KN(2,Pelem) == FALSE - * KN(4,Pelem) == TRUE - */ - nul(n: number, outputTarget: OutputTarget): boolean { - var cx=this.context(n+1, 1, outputTarget); - - // With #31, the result will be a replacement character if context is empty. - return cx === "\uFFFE"; + var r = this.KC_(n, ln, outputTarget); + this.cachedContext.set(n, ln, r); + return r; + } + + /** + * Get (uncached) keyboard context for a specified range, relative to caret + * + * @param {number} n Number of characters to move back from caret + * @param {number} ln Number of characters to return + * @param {Object} Pelem Element to work with (must be currently focused element) + * @return {string} Context string + * + * Example [abcdef|ghi] as INPUT, with the caret position marked by |: + * KC(2,1,Pelem) == "e" + * KC(3,3,Pelem) == "def" + * KC(10,10,Pelem) == "XXXXabcdef" i.e. return as much as possible of the requested string, where X = \uFFFE + */ + private KC_(n: number, ln: number, outputTarget: OutputTarget): string { + var tempContext = ''; + + // If we have a selection, we have an empty context + tempContext = outputTarget.isSelectionEmpty() ? outputTarget.getTextBeforeCaret() : ""; + + if(tempContext._kmwLength() < n) { + tempContext = Array(n-tempContext._kmwLength()+1).join("\uFFFE") + tempContext; } - /** - * Function contextMatch KCM - * Scope Public - * @param {number} n Number of characters to move back from caret - * @param {Object} Ptarg Focused element - * @param {string} val String to match - * @param {number} ln Number of characters to return - * @return {boolean} True if selected context matches val - * Description Test keyboard context for match - */ - contextMatch(n: number, outputTarget: OutputTarget, val: string, ln: number): boolean { - var cx=this.context(n, ln, outputTarget); - if(cx === val) { - return true; // I3318 - } - outputTarget.deadkeys().resetMatched(); // I3318 - return false; + return tempContext._kmwSubstr(-n)._kmwSubstr(0,ln); + } + + /** + * Function nul KN + * Scope Public + * @param {number} n Length of context to check + * @param {Object} Ptarg Element to work with (must be currently focused element) + * @return {boolean} True if length of context is less than or equal to n + * Description Test length of context, return true if the length of the context is less than or equal to n + * + * Example [abc|def] as INPUT, with the caret position marked by |: + * KN(3,Pelem) == TRUE + * KN(2,Pelem) == FALSE + * KN(4,Pelem) == TRUE + */ + nul(n: number, outputTarget: OutputTarget): boolean { + var cx=this.context(n+1, 1, outputTarget); + + // With #31, the result will be a replacement character if context is empty. + return cx === "\uFFFE"; + } + + /** + * Function contextMatch KCM + * Scope Public + * @param {number} n Number of characters to move back from caret + * @param {Object} Ptarg Focused element + * @param {string} val String to match + * @param {number} ln Number of characters to return + * @return {boolean} True if selected context matches val + * Description Test keyboard context for match + */ + contextMatch(n: number, outputTarget: OutputTarget, val: string, ln: number): boolean { + var cx=this.context(n, ln, outputTarget); + if(cx === val) { + return true; // I3318 } + outputTarget.deadkeys().resetMatched(); // I3318 + return false; + } - /** - * Builds the *cached or uncached* keyboard context for a specified range, relative to caret - * - * @param {number} n Number of characters to move back from caret - * @param {number} ln Number of characters to return - * @param {Object} Pelem Element to work with (must be currently focused element) - * @return {Array} Context array (of strings and numbers) - */ - private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTarget): CachedExEntry { - var cache: CachedExEntry = this.cachedContextEx.get(n, ln); - if(cache !== null) { - return cache; - } else { - // By far the easiest way to correctly build what we want is to start from the right and work to what we need. - // We may have done it for a similar cursor position before. - cache = this.cachedContextEx.get(n, n); - if(cache === null) { - // First, let's make sure we have a cloned, sorted copy of the deadkey array. - let unmatchedDeadkeys = outputTarget.deadkeys().toSortedArray(); // Is reverse-order sorted for us already. - - // Time to build from scratch! - var index = 0; - cache = { valContext: [], deadContext: []}; - while(cache.valContext.length < n) { - // As adapted from `deadkeyMatch`. - var sp = outputTarget.getDeadkeyCaret(); - var deadPos = sp - index; - if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p > deadPos) { - // We have deadkeys at the right-hand side of the caret! They don't belong in the context, so pop 'em off. - unmatchedDeadkeys.splice(0, 1); - continue; - } else if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p == deadPos) { - // Take the deadkey. - cache.deadContext[n-cache.valContext.length-1] = unmatchedDeadkeys[0]; - cache.valContext = ([unmatchedDeadkeys[0].d] as (string|number)[]).concat(cache.valContext); - unmatchedDeadkeys.splice(0, 1); - } else { - // Take the character. We get "\ufffe" if it doesn't exist. - var kc = this.context(++index, 1, outputTarget); - cache.valContext = ([kc] as (string|number)[]).concat(cache.valContext); - } + /** + * Builds the *cached or uncached* keyboard context for a specified range, relative to caret + * + * @param {number} n Number of characters to move back from caret + * @param {number} ln Number of characters to return + * @param {Object} Pelem Element to work with (must be currently focused element) + * @return {Array} Context array (of strings and numbers) + */ + private _BuildExtendedContext(n: number, ln: number, outputTarget: OutputTarget): CachedExEntry { + var cache: CachedExEntry = this.cachedContextEx.get(n, ln); + if(cache !== null) { + return cache; + } else { + // By far the easiest way to correctly build what we want is to start from the right and work to what we need. + // We may have done it for a similar cursor position before. + cache = this.cachedContextEx.get(n, n); + if(cache === null) { + // First, let's make sure we have a cloned, sorted copy of the deadkey array. + let unmatchedDeadkeys = outputTarget.deadkeys().toSortedArray(); // Is reverse-order sorted for us already. + + // Time to build from scratch! + var index = 0; + cache = { valContext: [], deadContext: []}; + while(cache.valContext.length < n) { + // As adapted from `deadkeyMatch`. + var sp = outputTarget.getDeadkeyCaret(); + var deadPos = sp - index; + if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p > deadPos) { + // We have deadkeys at the right-hand side of the caret! They don't belong in the context, so pop 'em off. + unmatchedDeadkeys.splice(0, 1); + continue; + } else if(unmatchedDeadkeys.length > 0 && unmatchedDeadkeys[0].p == deadPos) { + // Take the deadkey. + cache.deadContext[n-cache.valContext.length-1] = unmatchedDeadkeys[0]; + cache.valContext = ([unmatchedDeadkeys[0].d] as (string|number)[]).concat(cache.valContext); + unmatchedDeadkeys.splice(0, 1); + } else { + // Take the character. We get "\ufffe" if it doesn't exist. + var kc = this.context(++index, 1, outputTarget); + cache.valContext = ([kc] as (string|number)[]).concat(cache.valContext); } - this.cachedContextEx.set(n, n, cache); } + this.cachedContextEx.set(n, n, cache); + } - // Now that we have the cache... - var subCache = cache; - subCache.valContext = subCache.valContext.slice(0, ln); - for(var i=0; i < subCache.valContext.length; i++) { - if(subCache[i] == '\ufffe') { - subCache.valContext.splice(0, 1); - subCache.deadContext.splice(0, 1); - } + // Now that we have the cache... + var subCache = cache; + subCache.valContext = subCache.valContext.slice(0, ln); + for(var i=0; i < subCache.valContext.length; i++) { + if(subCache[i] == '\ufffe') { + subCache.valContext.splice(0, 1); + subCache.deadContext.splice(0, 1); } + } - if(subCache.valContext.length == 0) { - subCache.valContext = ['\ufffe']; - subCache.deadContext = []; - } + if(subCache.valContext.length == 0) { + subCache.valContext = ['\ufffe']; + subCache.deadContext = []; + } - this.cachedContextEx.set(n, ln, subCache); + this.cachedContextEx.set(n, ln, subCache); - return subCache; - } + return subCache; } + } - /** - * Function fullContextMatch KFCM - * Scope Private - * @param {number} n Number of characters to move back from caret - * @param {Object} Ptarg Focused element - * @param {Array} rule An array of ContextEntries to match. - * @return {boolean} True if the fully-specified rule context matches the current KMW state. - * - * A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality, - * near-directly modeling (externally) the compiled form of Desktop rules' context section. - */ - fullContextMatch(n: number, outputTarget: OutputTarget, rule: ContextEntry[]): boolean { - // Stage one: build the context index map. - var fullContext = this._BuildExtendedContext(n, rule.length, outputTarget); - this.ruleContextEx = this.cachedContextEx.clone(); - var context = fullContext.valContext; - var deadContext = fullContext.deadContext; - - var mismatch = false; - - // This symbol internally indicates lack of context in a position. (See KC_) - const NUL_CONTEXT = "\uFFFE"; - - var assertNever = function(x: never): never { - // Could be accessed by improperly handwritten calls to `fullContextMatch`. - throw new Error("Unexpected object in fullContextMatch specification: " + x); - } + /** + * Function fullContextMatch KFCM + * Scope Private + * @param {number} n Number of characters to move back from caret + * @param {Object} Ptarg Focused element + * @param {Array} rule An array of ContextEntries to match. + * @return {boolean} True if the fully-specified rule context matches the current KMW state. + * + * A KMW 10+ function designed to bring KMW closer to Keyman Desktop functionality, + * near-directly modeling (externally) the compiled form of Desktop rules' context section. + */ + fullContextMatch(n: number, outputTarget: OutputTarget, rule: ContextEntry[]): boolean { + // Stage one: build the context index map. + var fullContext = this._BuildExtendedContext(n, rule.length, outputTarget); + this.ruleContextEx = this.cachedContextEx.clone(); + var context = fullContext.valContext; + var deadContext = fullContext.deadContext; + + var mismatch = false; + + // This symbol internally indicates lack of context in a position. (See KC_) + const NUL_CONTEXT = "\uFFFE"; + + var assertNever = function(x: never): never { + // Could be accessed by improperly handwritten calls to `fullContextMatch`. + throw new Error("Unexpected object in fullContextMatch specification: " + x); + } - // Stage two: time to match against the rule specified. - for(var i=0; i < rule.length; i++) { - if(typeof rule[i] == 'string') { - var str = rule[i] as string; - if(str !== context[i]) { - mismatch = true; + // Stage two: time to match against the rule specified. + for(var i=0; i < rule.length; i++) { + if(typeof rule[i] == 'string') { + var str = rule[i] as string; + if(str !== context[i]) { + mismatch = true; + break; + } + } else { + // TypeScript needs a cast to this intermediate type to do its discriminated union magic. + var r = rule[i] as ContextNonCharEntry; + switch(r.t) { + case 'd': + // We still need to set a flag here; + if(r['d'] !== context[i]) { + mismatch = true; + } else { + deadContext[i].set(); + } break; - } - } else { - // TypeScript needs a cast to this intermediate type to do its discriminated union magic. - var r = rule[i] as ContextNonCharEntry; - switch(r.t) { - case 'd': - // We still need to set a flag here; - if(r['d'] !== context[i]) { - mismatch = true; - } else { - deadContext[i].set(); - } - break; - case 'a': - var lookup: KeyboardStoreElement; - - if(typeof context[i] == 'string') { - lookup = context[i] as string; - } else { - lookup = {'t': 'd', 'd': context[i] as number}; - } + case 'a': + var lookup: KeyboardStoreElement; - var result = this.any(i, lookup, r.a); - - if(!r.n) { // If it's a standard 'any'... - if(!result) { - mismatch = true; - } else if(deadContext[i] !== undefined) { - // It's a deadkey match, so indicate that. - deadContext[i].set(); - } - // 'n' for 'notany'. - // - if `result === true`, `any` would match: this should thus fail. - // - if `context[i] === NUL_CONTEXT`, `notany` should not match. - } else if(r.n && (result || context[i] === NUL_CONTEXT)) { - mismatch = true; - } - break; - case 'i': - // The context will never hold a 'beep.' - var ch = this._Index(r.i, r.o) as string | RuleDeadkey; + if(typeof context[i] == 'string') { + lookup = context[i] as string; + } else { + lookup = {'t': 'd', 'd': context[i] as number}; + } - if(ch !== undefined && (typeof(ch) == 'string' ? ch : ch.d) !== context[i]) { - mismatch = true; - } else if(deadContext[i] !== undefined) { - deadContext[i].set(); - } - break; - case 'c': - if(context[r.c - 1] !== context[i]) { + var result = this.any(i, lookup, r.a); + + if(!r.n) { // If it's a standard 'any'... + if(!result) { mismatch = true; } else if(deadContext[i] !== undefined) { + // It's a deadkey match, so indicate that. deadContext[i].set(); } - break; - case 'n': - // \uFFFE is the internal 'no context here sentinel'. - if(context[i] != NUL_CONTEXT) { - mismatch = true; - } - break; - default: - assertNever(r); - } + // 'n' for 'notany'. + // - if `result === true`, `any` would match: this should thus fail. + // - if `context[i] === NUL_CONTEXT`, `notany` should not match. + } else if(r.n && (result || context[i] === NUL_CONTEXT)) { + mismatch = true; + } + break; + case 'i': + // The context will never hold a 'beep.' + var ch = this._Index(r.i, r.o) as string | RuleDeadkey; + + if(ch !== undefined && (typeof(ch) == 'string' ? ch : ch.d) !== context[i]) { + mismatch = true; + } else if(deadContext[i] !== undefined) { + deadContext[i].set(); + } + break; + case 'c': + if(context[r.c - 1] !== context[i]) { + mismatch = true; + } else if(deadContext[i] !== undefined) { + deadContext[i].set(); + } + break; + case 'n': + // \uFFFE is the internal 'no context here sentinel'. + if(context[i] != NUL_CONTEXT) { + mismatch = true; + } + break; + default: + assertNever(r); } } + } - if(mismatch) { - // Reset the matched 'any' indices, if any. - outputTarget.deadkeys().resetMatched(); - this._AnyIndices = []; - } - - return !mismatch; + if(mismatch) { + // Reset the matched 'any' indices, if any. + outputTarget.deadkeys().resetMatched(); + this._AnyIndices = []; } - /** - * Function KIK - * Scope Public - * @param {Object} e keystroke event - * @return {boolean} true if keypress event - * Description Test if event as a keypress event - */ - isKeypress(e: KeyEvent): boolean { - if(this.activeKeyboard.isMnemonic) { // I1380 - support KIK for positional layouts - return !e.LisVirtualKey; // will now return true for U_xxxx keys, but not for T_xxxx keys - } else { - return KeyMapping._USKeyCodeToCharCode(e) ? true : false; // I1380 - support KIK for positional layouts - } + return !mismatch; + } + + /** + * Function KIK + * Scope Public + * @param {Object} e keystroke event + * @return {boolean} true if keypress event + * Description Test if event as a keypress event + */ + isKeypress(e: KeyEvent): boolean { + if(this.activeKeyboard.isMnemonic) { // I1380 - support KIK for positional layouts + return !e.LisVirtualKey; // will now return true for U_xxxx keys, but not for T_xxxx keys + } else { + return KeyMapping._USKeyCodeToCharCode(e) ? true : false; // I1380 - support KIK for positional layouts } + } - /** - * Maps a KeyEvent's modifiers to their appropriate value for key-rule evaluation - * based on the rule's specified target modifier set. - * - * Mostly used to correct chiral OSK-keys targeting non-chiral rules. - * @param e The source KeyEvent - * @returns - */ - private static matchModifiersToRuleChirality(eventModifiers: number, targetModifierMask: number): number { - const CHIRAL_ALT = Codes.modifierCodes["LALT"] | Codes.modifierCodes["RALT"]; - const CHIRAL_CTRL = Codes.modifierCodes["LCTRL"] | Codes.modifierCodes["RCTRL"]; - - let modifiers = eventModifiers; - - // If the target rule does not use chiral alt... - if(!(targetModifierMask & CHIRAL_ALT)) { - const altIntersection = modifiers & CHIRAL_ALT; - - if(altIntersection) { - // Undo the chiral part and replace with non-chiral. - modifiers ^= altIntersection | Codes.modifierCodes["ALT"]; - } - } + /** + * Maps a KeyEvent's modifiers to their appropriate value for key-rule evaluation + * based on the rule's specified target modifier set. + * + * Mostly used to correct chiral OSK-keys targeting non-chiral rules. + * @param e The source KeyEvent + * @returns + */ + private static matchModifiersToRuleChirality(eventModifiers: number, targetModifierMask: number): number { + const CHIRAL_ALT = Codes.modifierCodes["LALT"] | Codes.modifierCodes["RALT"]; + const CHIRAL_CTRL = Codes.modifierCodes["LCTRL"] | Codes.modifierCodes["RCTRL"]; - // If the target rule does not use chiral ctrl... - if(!(targetModifierMask & CHIRAL_CTRL)) { - const ctrlIntersection = modifiers & CHIRAL_CTRL; + let modifiers = eventModifiers; - if(ctrlIntersection) { - // Undo the chiral part and replace with non-chiral. - modifiers ^= ctrlIntersection | Codes.modifierCodes["CTRL"]; - } - } + // If the target rule does not use chiral alt... + if(!(targetModifierMask & CHIRAL_ALT)) { + const altIntersection = modifiers & CHIRAL_ALT; - return modifiers; + if(altIntersection) { + // Undo the chiral part and replace with non-chiral. + modifiers ^= altIntersection | Codes.modifierCodes["ALT"]; + } } - /** - * Function keyMatch KKM - * Scope Public - * @param {Object} e keystroke event - * @param {number} Lruleshift - * @param {number} Lrulekey - * @return {boolean} True if key matches rule - * Description Test keystroke with modifiers against rule - */ - keyMatch(e: KeyEvent, Lruleshift:number, Lrulekey:number): boolean { - var retVal = false; // I3318 - var keyCode = (e.Lcode == 173 ? 189 : e.Lcode); //I3555 (Firefox hyphen issue) - - let bitmask = this.activeKeyboard.modifierBitmask; - let Codes = com.keyman.text.Codes; - var modifierBitmask = bitmask & Codes.modifierBitmasks["ALL"]; - var stateBitmask = bitmask & Codes.stateBitmasks["ALL"]; - - const eventModifiers = KeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift); - - if(e.vkCode > 255) { - keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts - } + // If the target rule does not use chiral ctrl... + if(!(targetModifierMask & CHIRAL_CTRL)) { + const ctrlIntersection = modifiers & CHIRAL_CTRL; - if(e.LisVirtualKey || keyCode > 255) { - if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) { // added keyCode test to support extended keys - retVal = ((Lrulekey == keyCode) && ((Lruleshift & modifierBitmask) == eventModifiers)); //I3318, I3555 - retVal = retVal && this.stateMatch(e, Lruleshift & stateBitmask); - } - } else if((Lruleshift & 0x4000) == 0) { - retVal = (keyCode == Lrulekey); // I3318, I3555 - } - if(!retVal) { - this.activeTargetOutput.deadkeys().resetMatched(); // I3318 + if(ctrlIntersection) { + // Undo the chiral part and replace with non-chiral. + modifiers ^= ctrlIntersection | Codes.modifierCodes["CTRL"]; } - return retVal; // I3318 - }; - - /** - * Function stateMatch KSM - * Scope Public - * @param {Object} e keystroke event - * @param {number} Lstate - * Description Test keystroke against state key rules - */ - stateMatch(e: KeyEvent, Lstate: number) { - return ((Lstate & e.Lstates) == Lstate); } - /** - * Function keyInformation KKI - * Scope Public - * @param {Object} e - * @return {Object} Object with event's virtual key flag, key code, and modifiers - * Description Get object with extended key event information - */ - keyInformation(e: KeyEvent): KeyInformation { - var ei = new KeyInformation(); - ei['vk'] = e.LisVirtualKey; - ei['code'] = e.Lcode; - ei['modifiers'] = e.Lmodifiers; - return ei; - }; - - /** - * Function deadkeyMatch KDM - * Scope Public - * @param {number} n offset from current cursor position - * @param {Object} Ptarg target element - * @param {number} d deadkey - * @return {boolean} True if deadkey found selected context matches val - * Description Match deadkey at current cursor position - */ - deadkeyMatch(n: number, outputTarget: OutputTarget, d: number): boolean { - return outputTarget.hasDeadkeyMatch(n, d); + return modifiers; + } + + /** + * Function keyMatch KKM + * Scope Public + * @param {Object} e keystroke event + * @param {number} Lruleshift + * @param {number} Lrulekey + * @return {boolean} True if key matches rule + * Description Test keystroke with modifiers against rule + */ + keyMatch(e: KeyEvent, Lruleshift:number, Lrulekey:number): boolean { + var retVal = false; // I3318 + var keyCode = (e.Lcode == 173 ? 189 : e.Lcode); //I3555 (Firefox hyphen issue) + + let bitmask = this.activeKeyboard.modifierBitmask; + var modifierBitmask = bitmask & Codes.modifierBitmasks["ALL"]; + var stateBitmask = bitmask & Codes.stateBitmasks["ALL"]; + + const eventModifiers = KeyboardInterface.matchModifiersToRuleChirality(e.Lmodifiers, Lruleshift); + + if(e.vkCode > 255) { + keyCode = e.vkCode; // added to support extended (touch-hold) keys for mnemonic layouts } - /** - * Function beep KB - * Scope Public - * @param {Object} Pelem element to flash - * Description Flash body as substitute for audible beep; notify embedded device to vibrate - */ - beep(outputTarget: OutputTarget): void { - this.resetContextCache(); - - // Denote as part of the matched rule's behavior. - this.ruleBehavior.beep = true; + if(e.LisVirtualKey || keyCode > 255) { + if((Lruleshift & 0x4000) == 0x4000 || (keyCode > 255)) { // added keyCode test to support extended keys + retVal = ((Lrulekey == keyCode) && ((Lruleshift & modifierBitmask) == eventModifiers)); //I3318, I3555 + retVal = retVal && this.stateMatch(e, Lruleshift & stateBitmask); + } + } else if((Lruleshift & 0x4000) == 0) { + retVal = (keyCode == Lrulekey); // I3318, I3555 + } + if(!retVal) { + this.activeTargetOutput.deadkeys().resetMatched(); // I3318 } + return retVal; // I3318 + }; - _ExplodeStore(store: KeyboardStore): ComplexKeyboardStore { - if(typeof(store) == 'string') { - let cachedStores = this.activeKeyboard.explodedStores; + /** + * Function stateMatch KSM + * Scope Public + * @param {Object} e keystroke event + * @param {number} Lstate + * Description Test keystroke against state key rules + */ + stateMatch(e: KeyEvent, Lstate: number) { + return ((Lstate & e.Lstates) == Lstate); + } - // Is the result cached? - if(cachedStores[store]) { - return cachedStores[store]; - } + /** + * Function keyInformation KKI + * Scope Public + * @param {Object} e + * @return {Object} Object with event's virtual key flag, key code, and modifiers + * Description Get object with extended key event information + */ + keyInformation(e: KeyEvent): KeyInformation { + var ei = new KeyInformation(); + ei['vk'] = e.LisVirtualKey; + ei['code'] = e.Lcode; + ei['modifiers'] = e.Lmodifiers; + return ei; + }; - // Nope, so let's build its cache. - var result: ComplexKeyboardStore = []; - for(var i=0; i < store._kmwLength(); i++) { - result.push(store._kmwCharAt(i)); - } + /** + * Function deadkeyMatch KDM + * Scope Public + * @param {number} n offset from current cursor position + * @param {Object} Ptarg target element + * @param {number} d deadkey + * @return {boolean} True if deadkey found selected context matches val + * Description Match deadkey at current cursor position + */ + deadkeyMatch(n: number, outputTarget: OutputTarget, d: number): boolean { + return outputTarget.hasDeadkeyMatch(n, d); + } - // Cache the result for later! - cachedStores[store] = result; - return result; - } else { - return store; + /** + * Function beep KB + * Scope Public + * @param {Object} Pelem element to flash + * Description Flash body as substitute for audible beep; notify embedded device to vibrate + */ + beep(outputTarget: OutputTarget): void { + this.resetContextCache(); + + // Denote as part of the matched rule's behavior. + this.ruleBehavior.beep = true; + } + + _ExplodeStore(store: KeyboardStore): ComplexKeyboardStore { + if(typeof(store) == 'string') { + let cachedStores = this.activeKeyboard.explodedStores; + + // Is the result cached? + if(cachedStores[store]) { + return cachedStores[store]; } - } - /** - * Function any KA - * Scope Public - * @param {number} n character position (index) - * @param {string} ch character to find in string - * @param {string} s 'any' string - * @return {boolean} True if character found in 'any' string, sets index accordingly - * Description Test for character matching - */ - any(n: number, ch: KeyboardStoreElement, s: KeyboardStore): boolean { - if(ch == '') { - return false; + // Nope, so let's build its cache. + var result: ComplexKeyboardStore = []; + for(var i=0; i < store._kmwLength(); i++) { + result.push(store._kmwCharAt(i)); } - s = this._ExplodeStore(s); - var Lix = -1; - for(var i=0; i < s.length; i++) { - if(typeof(s[i]) == 'string') { - if(s[i] == ch) { - Lix = i; - break; - } - } else if(s[i]['d'] === ch['d']) { + // Cache the result for later! + cachedStores[store] = result; + return result; + } else { + return store; + } + } + + /** + * Function any KA + * Scope Public + * @param {number} n character position (index) + * @param {string} ch character to find in string + * @param {string} s 'any' string + * @return {boolean} True if character found in 'any' string, sets index accordingly + * Description Test for character matching + */ + any(n: number, ch: KeyboardStoreElement, s: KeyboardStore): boolean { + if(ch == '') { + return false; + } + + s = this._ExplodeStore(s); + var Lix = -1; + for(var i=0; i < s.length; i++) { + if(typeof(s[i]) == 'string') { + if(s[i] == ch) { Lix = i; break; } + } else if(s[i]['d'] === ch['d']) { + Lix = i; + break; } - this._AnyIndices[n] = Lix; - return Lix >= 0; } + this._AnyIndices[n] = Lix; + return Lix >= 0; + } - /** - * Function _Index - * Scope Public - * @param {string} Ps string - * @param {number} Pn index - * Description Returns the character from a store string according to the offset in the index array - */ - _Index(Ps: KeyboardStore, Pn: number): KeyboardStoreElement { - Ps = this._ExplodeStore(Ps); - - if(this._AnyIndices[Pn-1] < Ps.length) { //I3319 - return Ps[this._AnyIndices[Pn-1]]; - } else { - /* Should not be possible for a compiled keyboard, but may arise - * during the development of handwritten keyboards. - */ - console.warn("Unmatched contextual index() statement detected in rule with index " + Pn + "!"); - return ""; - } + /** + * Function _Index + * Scope Public + * @param {string} Ps string + * @param {number} Pn index + * Description Returns the character from a store string according to the offset in the index array + */ + _Index(Ps: KeyboardStore, Pn: number): KeyboardStoreElement { + Ps = this._ExplodeStore(Ps); + + if(this._AnyIndices[Pn-1] < Ps.length) { //I3319 + return Ps[this._AnyIndices[Pn-1]]; + } else { + /* Should not be possible for a compiled keyboard, but may arise + * during the development of handwritten keyboards. + */ + console.warn("Unmatched contextual index() statement detected in rule with index " + Pn + "!"); + return ""; } + } - /** - * Function indexOutput KIO - * Scope Public - * @param {number} Pdn no of character to overwrite (delete) - * @param {string} Ps string - * @param {number} Pn index - * @param {Object} Pelem element to output to - * Description Output a character selected from the string according to the offset in the index array - */ - indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTarget): void { - this.resetContextCache(); - - var assertNever = function(x: never): never { - // Could be accessed by improperly handwritten calls to `fullContextMatch`. - throw new Error("Unexpected object in fullContextMatch specification: " + x); - } + /** + * Function indexOutput KIO + * Scope Public + * @param {number} Pdn no of character to overwrite (delete) + * @param {string} Ps string + * @param {number} Pn index + * @param {Object} Pelem element to output to + * Description Output a character selected from the string according to the offset in the index array + */ + indexOutput(Pdn: number, Ps: KeyboardStore, Pn: number, outputTarget: OutputTarget): void { + this.resetContextCache(); - var indexChar = this._Index(Ps, Pn); - if(indexChar !== "") { - if(typeof indexChar == 'string' ) { - this.output(Pdn, outputTarget, indexChar); //I3319 - } else if(indexChar['t']) { - var storeEntry = indexChar as StoreNonCharEntry; - - switch(storeEntry.t) { - case 'b': // Beep commands may appear within stores. - this.beep(outputTarget); - break; - case 'd': - this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); - break; - default: - assertNever(storeEntry); - } - } else { // For keyboards developed during 10.0's alpha phase - t:'d' was assumed. - this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); + var assertNever = function(x: never): never { + // Could be accessed by improperly handwritten calls to `fullContextMatch`. + throw new Error("Unexpected object in fullContextMatch specification: " + x); + } + + var indexChar = this._Index(Ps, Pn); + if(indexChar !== "") { + if(typeof indexChar == 'string' ) { + this.output(Pdn, outputTarget, indexChar); //I3319 + } else if(indexChar['t']) { + var storeEntry = indexChar as StoreNonCharEntry; + + switch(storeEntry.t) { + case 'b': // Beep commands may appear within stores. + this.beep(outputTarget); + break; + case 'd': + this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); + break; + default: + assertNever(storeEntry); } + } else { // For keyboards developed during 10.0's alpha phase - t:'d' was assumed. + this.deadkeyOutput(Pdn, outputTarget, indexChar['d']); } } + } - /** - * Function deleteContext KDC - * Scope Public - * @param {number} dn number of context entries to overwrite - * @param {Object} Pelem element to output to - * @param {string} s string to output - * Description Keyboard output - */ - deleteContext(dn: number, outputTarget: OutputTarget): void { - var context: CachedExEntry; - - // We want to control exactly which deadkeys get removed. - if(dn > 0) { - context = this._BuildExtendedContext(dn, dn, outputTarget); - let nulCount = 0; - - for(var i=0; i < context.valContext.length; i++) { - var dk = context.deadContext[i]; - - if(dk) { - // Remove deadkey in context. - outputTarget.deadkeys().remove(dk); - - // Reduce our reported context size. - dn--; - } else if(context.valContext[i] == "\uFFFE") { - // Count any `nul` sentinels that would contribute to our deletion count. - nulCount++; - } + /** + * Function deleteContext KDC + * Scope Public + * @param {number} dn number of context entries to overwrite + * @param {Object} Pelem element to output to + * @param {string} s string to output + * Description Keyboard output + */ + deleteContext(dn: number, outputTarget: OutputTarget): void { + var context: CachedExEntry; + + // We want to control exactly which deadkeys get removed. + if(dn > 0) { + context = this._BuildExtendedContext(dn, dn, outputTarget); + let nulCount = 0; + + for(var i=0; i < context.valContext.length; i++) { + var dk = context.deadContext[i]; + + if(dk) { + // Remove deadkey in context. + outputTarget.deadkeys().remove(dk); + + // Reduce our reported context size. + dn--; + } else if(context.valContext[i] == "\uFFFE") { + // Count any `nul` sentinels that would contribute to our deletion count. + nulCount++; } + } - // Prevent attempts to delete nul sentinels, as they don't exist in the actual context. - // (Addresses regression from KMW v 12.0 paired with Developer bug through same version) - let contextLength = context.valContext.length - nulCount; - if(dn > contextLength) { - dn = contextLength; - } + // Prevent attempts to delete nul sentinels, as they don't exist in the actual context. + // (Addresses regression from KMW v 12.0 paired with Developer bug through same version) + let contextLength = context.valContext.length - nulCount; + if(dn > contextLength) { + dn = contextLength; } + } - // If a matched deadkey hasn't been deleted, we don't WANT to delete it. - outputTarget.deadkeys().resetMatched(); + // If a matched deadkey hasn't been deleted, we don't WANT to delete it. + outputTarget.deadkeys().resetMatched(); - // Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'. - this.output(dn, outputTarget, ''); - } + // Why reinvent the wheel? Delete the remaining characters by 'inserting a blank string'. + this.output(dn, outputTarget, ''); + } - /** - * Function output KO - * Scope Public - * @param {number} dn number of characters to overwrite - * @param {Object} Pelem element to output to - * @param {string} s string to output - * Description Keyboard output - */ - output(dn: number, outputTarget: OutputTarget, s:string): void { - this.resetContextCache(); - - outputTarget.saveProperties(); - outputTarget.clearSelection(); - outputTarget.deadkeys().deleteMatched(); // I3318 - if(dn >= 0) { - // Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support. - outputTarget.deleteCharsBeforeCaret(dn); - } - // Automatically manages affected deadkey positions. - outputTarget.insertTextBeforeCaret(s); - outputTarget.restoreProperties(); + /** + * Function output KO + * Scope Public + * @param {number} dn number of characters to overwrite + * @param {Object} Pelem element to output to + * @param {string} s string to output + * Description Keyboard output + */ + output(dn: number, outputTarget: OutputTarget, s:string): void { + this.resetContextCache(); + + outputTarget.saveProperties(); + outputTarget.clearSelection(); + outputTarget.deadkeys().deleteMatched(); // I3318 + if(dn >= 0) { + // Automatically manages affected deadkey positions. Does not delete deadkeys b/c legacy behavior support. + outputTarget.deleteCharsBeforeCaret(dn); } + // Automatically manages affected deadkey positions. + outputTarget.insertTextBeforeCaret(s); + outputTarget.restoreProperties(); + } - /** - * `contextExOutput` function emits the character or object at `contextOffset` from the - * current matched rule's context. Introduced in Keyman 14.0, in order to resolve a - * gap between desktop and web core functionality for context(n) matching on notany(). - * See #917 for additional detail. - * @alias KCXO - * @public - * @param {number} Pdn number of characters to delete left of cursor - * @param {OutputTarget} outputTarget target to output to - * @param {number} contextLength length of current rule context to retrieve - * @param {number} contextOffset offset from start of current rule context, 1-based - */ - contextExOutput(Pdn: number, outputTarget: OutputTarget, contextLength: number, contextOffset: number): void { - this.resetContextCache(); - - if(Pdn >= 0) { - this.output(Pdn, outputTarget, ""); - } + /** + * `contextExOutput` function emits the character or object at `contextOffset` from the + * current matched rule's context. Introduced in Keyman 14.0, in order to resolve a + * gap between desktop and web core functionality for context(n) matching on notany(). + * See #917 for additional detail. + * @alias KCXO + * @public + * @param {number} Pdn number of characters to delete left of cursor + * @param {OutputTarget} outputTarget target to output to + * @param {number} contextLength length of current rule context to retrieve + * @param {number} contextOffset offset from start of current rule context, 1-based + */ + contextExOutput(Pdn: number, outputTarget: OutputTarget, contextLength: number, contextOffset: number): void { + this.resetContextCache(); - const context = this.ruleContextEx.get(contextLength, contextLength); - const dk = context.deadContext[contextOffset-1], vc = context.valContext[contextOffset-1]; - if(dk) { - outputTarget.insertDeadkeyBeforeCaret(dk.d); - } else if(typeof vc == 'string') { - this.output(-1, outputTarget, vc); - } else { - throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext"); - } + if(Pdn >= 0) { + this.output(Pdn, outputTarget, ""); } - /** - * Function deadkeyOutput KDO - * Scope Public - * @param {number} Pdn no of character to overwrite (delete) - * @param {Object} Pelem element to output to - * @param {number} Pd deadkey id - * Description Record a deadkey at current cursor position, deleting Pdn characters first - */ - deadkeyOutput(Pdn: number, outputTarget: OutputTarget, Pd: number): void { - this.resetContextCache(); - - if(Pdn >= 0) { - this.output(Pdn, outputTarget,""); //I3318 corrected to >= - } - - outputTarget.insertDeadkeyBeforeCaret(Pd); - // _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd); + const context = this.ruleContextEx.get(contextLength, contextLength); + const dk = context.deadContext[contextOffset-1], vc = context.valContext[contextOffset-1]; + if(dk) { + outputTarget.insertDeadkeyBeforeCaret(dk.d); + } else if(typeof vc == 'string') { + this.output(-1, outputTarget, vc); + } else { + throw new Error("contextExOutput: should never be a numeric valContext with no corresponding deadContext"); } + } - /** - * KIFS compares the content of a system store with a string value - * - * @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported) - * @param {string} strValue String value to compare to - * @param {Object} Pelem Currently active element (may be needed by future tests) - * @return {boolean} True if the test succeeds - */ - ifStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { - var result=true; - let store = this.systemStores[systemId]; - if(store) { - result = store.matches(strValue); - } - return result; //Moved from previous line, now supports layer selection, Build 350 + /** + * Function deadkeyOutput KDO + * Scope Public + * @param {number} Pdn no of character to overwrite (delete) + * @param {Object} Pelem element to output to + * @param {number} Pd deadkey id + * Description Record a deadkey at current cursor position, deleting Pdn characters first + */ + deadkeyOutput(Pdn: number, outputTarget: OutputTarget, Pd: number): void { + this.resetContextCache(); + + if(Pdn >= 0) { + this.output(Pdn, outputTarget,""); //I3318 corrected to >= } - /** - * KSETS sets the value of a system store to a string - * - * @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported) - * @param {string} strValue String to set as the system store content - * @param {Object} Pelem Currently active element (may be needed in future tests) - * @return {boolean} True if command succeeds - * (i.e. for TSS_LAYER, if the layer is successfully selected) - * - * Note that option/variable stores are instead set within keyboard script code, as they only - * affect keyboard behavior. - */ - setStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { - this.resetContextCache(); - // Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs. - if(systemId == KeyboardInterface.TSS_LAYER && this.activeDevice.touchable) { - // Denote the changed store as part of the matched rule's behavior. - this.ruleBehavior.setStore[systemId] = strValue; - } else { - return false; - } + outputTarget.insertDeadkeyBeforeCaret(Pd); + // _DebugDeadKeys(Pelem, 'KDeadKeyOutput: dn='+Pdn+'; deadKey='+Pd); + } + + /** + * KIFS compares the content of a system store with a string value + * + * @param {number} systemId ID of the system store to test (only TSS_LAYER currently supported) + * @param {string} strValue String value to compare to + * @param {Object} Pelem Currently active element (may be needed by future tests) + * @return {boolean} True if the test succeeds + */ + ifStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { + var result=true; + let store = this.systemStores[systemId]; + if(store) { + result = store.matches(strValue); } + return result; //Moved from previous line, now supports layer selection, Build 350 + } - /** - * Load an option store value from a cookie or default value - * - * @param {string} kbdName keyboard internal name - * @param {string} storeName store (option) name, embedded in cookie name - * @param {string} dfltValue default value - * @return {string} current or default option value - * - * This will only ever be called when the keyboard is loaded, as it is used by keyboards - * to initialize a store value on the keyboard's script object. - */ - loadStore(kbdName: string, storeName:string, dfltValue:string): string { - this.resetContextCache(); - if(this.variableStoreSerializer) { - let cValue = this.variableStoreSerializer.loadStore(kbdName, storeName); - return cValue[storeName] || dfltValue; - } else { - return dfltValue; - } + /** + * KSETS sets the value of a system store to a string + * + * @param {number} systemId ID of the system store to set (only TSS_LAYER currently supported) + * @param {string} strValue String to set as the system store content + * @param {Object} Pelem Currently active element (may be needed in future tests) + * @return {boolean} True if command succeeds + * (i.e. for TSS_LAYER, if the layer is successfully selected) + * + * Note that option/variable stores are instead set within keyboard script code, as they only + * affect keyboard behavior. + */ + setStore(systemId: number, strValue: string, outputTarget: OutputTarget): boolean { + this.resetContextCache(); + // Unique case: we only allow set(&layer) ops from keyboard rules triggered by touch OSKs. + if(systemId == SystemStoreIDs.TSS_LAYER && this.activeDevice.touchable) { + // Denote the changed store as part of the matched rule's behavior. + this.ruleBehavior.setStore[systemId] = strValue; + } else { + return false; } + } - /** - * Save an option store value to a cookie - * - * @param {string} storeName store (option) name, embedded in cookie name - * @param {string} optValue option value to save - * @return {boolean} true if save successful - * - * Note that a keyboard will freely manipulate the value of its variable stores on the - * script object within its own code. This function's use is merely to _persist_ that - * value across sessions, providing a custom user default for later uses of the keyboard. - */ - saveStore(storeName:string, optValue:string): boolean { - this.resetContextCache(); - var kbd=this.activeKeyboard; - if(!kbd || typeof kbd.id == 'undefined' || kbd.id == '') { - return false; - } + /** + * Load an option store value from a cookie or default value + * + * @param {string} kbdName keyboard internal name + * @param {string} storeName store (option) name, embedded in cookie name + * @param {string} dfltValue default value + * @return {string} current or default option value + * + * This will only ever be called when the keyboard is loaded, as it is used by keyboards + * to initialize a store value on the keyboard's script object. + */ + loadStore(kbdName: string, storeName:string, dfltValue:string): string { + this.resetContextCache(); + if(this.variableStoreSerializer) { + let cValue = this.variableStoreSerializer.loadStore(kbdName, storeName); + return cValue[storeName] || dfltValue; + } else { + return dfltValue; + } + } - // And the lookup under that entry looks for the value under the store name, again. - let valueObj: VariableStore = {}; - valueObj[storeName] = optValue; + /** + * Save an option store value to a cookie + * + * @param {string} storeName store (option) name, embedded in cookie name + * @param {string} optValue option value to save + * @return {boolean} true if save successful + * + * Note that a keyboard will freely manipulate the value of its variable stores on the + * script object within its own code. This function's use is merely to _persist_ that + * value across sessions, providing a custom user default for later uses of the keyboard. + */ + saveStore(storeName:string, optValue:string): boolean { + this.resetContextCache(); + var kbd=this.activeKeyboard; + if(!kbd || typeof kbd.id == 'undefined' || kbd.id == '') { + return false; + } - // Null-check in case of invocation during unit-test - if(this.ruleBehavior) { - this.ruleBehavior.saveStore[storeName] = valueObj; - } else { - // We're in a unit-test environment, directly invoking this method from outside of a keyboard. - // In this case, we should immediately commit the change. - this.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeName, valueObj); - } - return true; + // And the lookup under that entry looks for the value under the store name, again. + let valueObj: VariableStore = {}; + valueObj[storeName] = optValue; + + // Null-check in case of invocation during unit-test + if(this.ruleBehavior) { + this.ruleBehavior.saveStore[storeName] = valueObj; + } else { + // We're in a unit-test environment, directly invoking this method from outside of a keyboard. + // In this case, we should immediately commit the change. + this.variableStoreSerializer.saveStore(this.activeKeyboard.id, storeName, valueObj); } + return true; + } + + resetContextCache(): void { + this.cachedContext.reset(); + this.cachedContextEx.reset(); + } - resetContextCache(): void { - this.cachedContext.reset(); - this.cachedContextEx.reset(); + defaultBackspace(outputTarget: OutputTarget) { + if(outputTarget.isSelectionEmpty()) { + // Delete the character left of the caret + this.output(1, outputTarget, ""); + } else { + // Delete just the selection + this.output(0, outputTarget, ""); } + } - defaultBackspace(outputTarget: OutputTarget) { - if(outputTarget.isSelectionEmpty()) { - // Delete the character left of the caret - this.output(1, outputTarget, ""); - } else { - // Delete just the selection - this.output(0, outputTarget, ""); - } + /** + * Function processNewContextEvent + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. + * Description Calls the keyboard's `begin newContext` group + * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin NewContext` + */ + processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; } + return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke, true); + } - /** - * Function processNewContextEvent - * Scope Private - * @param {Object} outputTarget The target receiving input - * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. - * Description Calls the keyboard's `begin newContext` group - * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin NewContext` - */ - processNewContextEvent(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } - return this.process(this.activeKeyboard.processNewContextEvent.bind(this.activeKeyboard), outputTarget, keystroke, true); + /** + * Function processPostKeystroke + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard. + * Description Calls the keyboard's `begin postKeystroke` group + * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin PostKeystroke` + */ + processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; } + return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke, true); + } - /** - * Function processPostKeystroke - * Scope Private - * @param {Object} outputTarget The target receiving input - * @param {Object} keystroke The input keystroke with relevant properties to be mapped by the keyboard. - * Description Calls the keyboard's `begin postKeystroke` group - * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin PostKeystroke` - */ - processPostKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } - return this.process(this.activeKeyboard.processPostKeystroke.bind(this.activeKeyboard), outputTarget, keystroke, true); + /** + * Function processKeystroke + * Scope Private + * @param {Object} outputTarget The target receiving input + * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. + * Description Encapsulates calls to keyboard input processing. + * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin Unicode` + */ + processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { + if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; } + return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false); + } - /** - * Function processKeystroke - * Scope Private - * @param {Object} outputTarget The target receiving input - * @param {Object} keystroke The input keystroke (with its properties) to be mapped by the keyboard. - * Description Encapsulates calls to keyboard input processing. - * @returns {RuleBehavior} Record of commands and state changes that result from executing `begin Unicode` - */ - processKeystroke(outputTarget: OutputTarget, keystroke: KeyEvent): RuleBehavior { - if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } - return this.process(this.activeKeyboard.process.bind(this.activeKeyboard), outputTarget, keystroke, false); + private process(callee, outputTarget: OutputTarget, keystroke: KeyEvent, readonly: boolean): RuleBehavior { + // Clear internal state tracking data from prior keystrokes. + if(!outputTarget) { + throw "No target specified for keyboard output!"; + } else if(!this.activeKeyboard) { + throw "No active keyboard for keystroke processing!"; + } else if(!callee) { + throw "No callee for keystroke processing!"; } - private process(callee, outputTarget: OutputTarget, keystroke: KeyEvent, readonly: boolean): RuleBehavior { - // Clear internal state tracking data from prior keystrokes. - if(!outputTarget) { - throw "No target specified for keyboard output!"; - } else if(!this.activeKeyboard) { - throw "No active keyboard for keystroke processing!"; - } else if(!callee) { - throw "No callee for keystroke processing!"; - } + outputTarget.invalidateSelection(); - outputTarget.invalidateSelection(); + outputTarget.deadkeys().resetMatched(); // I3318 + this.resetContextCache(); - outputTarget.deadkeys().resetMatched(); // I3318 - this.resetContextCache(); + // Capture the initial state of the OutputTarget before any rules are matched. + let preInput = Mock.from(outputTarget, true); - // Capture the initial state of the OutputTarget before any rules are matched. - let preInput = Mock.from(outputTarget, true); + // Capture the initial state of any variable stores + const cachedVariableStores = this.activeKeyboard.variableStores; - // Capture the initial state of any variable stores - const cachedVariableStores = this.activeKeyboard.variableStores; + // Establishes the results object, allowing corresponding commands to set values here as appropriate. + this.ruleBehavior = new RuleBehavior(); - // Establishes the results object, allowing corresponding commands to set values here as appropriate. - this.ruleBehavior = new RuleBehavior(); + // Ensure the settings are in place so that KIFS/ifState activates and deactivates + // the appropriate rule(s) for the modeled device. + this.activeDevice = keystroke.device; - // Ensure the settings are in place so that KIFS/ifState activates and deactivates - // the appropriate rule(s) for the modeled device. - this.activeDevice = keystroke.device; + // Calls the start-group of the active keyboard. + this.activeTargetOutput = outputTarget; + var matched = callee(outputTarget, keystroke); + this.activeTargetOutput = null; - // Calls the start-group of the active keyboard. - this.activeTargetOutput = outputTarget; - var matched = callee(outputTarget, keystroke); - this.activeTargetOutput = null; + // Finalize the rule's results. + this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly); - // Finalize the rule's results. - this.ruleBehavior.transcription = outputTarget.buildTranscriptionFrom(preInput, keystroke, readonly); + // We always backup the changes to variable stores to the RuleBehavior, to + // be applied during finalization, then restore them to the cached initial + // values to avoid side-effects with predictive text mocks. + this.ruleBehavior.variableStores = this.activeKeyboard.variableStores; + this.activeKeyboard.variableStores = cachedVariableStores; - // We always backup the changes to variable stores to the RuleBehavior, to - // be applied during finalization, then restore them to the cached initial - // values to avoid side-effects with predictive text mocks. - this.ruleBehavior.variableStores = this.activeKeyboard.variableStores; - this.activeKeyboard.variableStores = cachedVariableStores; + // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than + // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's + // default behavior (if appropriate). + // + // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852 + this.ruleBehavior.triggerKeyDefault = !matched; - // `matched` refers to whether or not the FINAL rule (from any group) matched, rather than - // whether or not ANY rule matched. If the final rule doesn't match, we trigger the key's - // default behavior (if appropriate). - // - // See https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852 - this.ruleBehavior.triggerKeyDefault = !matched; + // Clear our result-tracking variable to prevent any possible pollution for future processing. + let behavior = this.ruleBehavior; + this.ruleBehavior = null; - // Clear our result-tracking variable to prevent any possible pollution for future processing. - let behavior = this.ruleBehavior; - this.ruleBehavior = null; + return behavior; + } - return behavior; - } + /** + * Applies the dictionary of variable store values to the active keyboard + * + * Has no effect on keyboards compiled with 14.0 or earlier; system store + * names are not exposed unless compiled with Developer 15.0 or later. + * + * @param stores A dictionary of stores which should be found in the + * keyboard + */ + applyVariableStores(stores: VariableStoreDictionary): void { + this.activeKeyboard.variableStores = stores; + } - /** - * Applies the dictionary of variable store values to the active keyboard - * - * Has no effect on keyboards compiled with 14.0 or earlier; system store - * names are not exposed unless compiled with Developer 15.0 or later. - * - * @param stores A dictionary of stores which should be found in the - * keyboard - */ - applyVariableStores(stores: com.keyman.keyboards.VariableStoreDictionary): void { - this.activeKeyboard.variableStores = stores; - } + /** + * Publishes the KeyboardInterface's shorthand API names. As this assigns the current functions + * held by the longform versions, note that this should be called after replacing any of them via + * JS method extension. + * + * DOM-aware KeymanWeb should call this after its domKbdInterface.ts code is loaded, as it replaces + * a few. (This is currently done within its kmwapi.ts.) + */ + static __publishShorthandAPI() { + // Keyboard callbacks + let prototype = this.prototype; - /** - * Publishes the KeyboardInterface's shorthand API names. As this assigns the current functions - * held by the longform versions, note that this should be called after replacing any of them via - * JS method extension. - * - * DOM-aware KeymanWeb should call this after its domKbdInterface.ts code is loaded, as it replaces - * a few. (This is currently done within its kmwapi.ts.) - */ - static __publishShorthandAPI() { - // Keyboard callbacks - let prototype = this.prototype; - - var exportKBCallback = function(miniName: string, longName: string) { + var exportKBCallback = function(miniName: string, longName: string) { + if(prototype[longName]) { prototype[miniName] = prototype[longName]; } - - exportKBCallback('KSF', 'saveFocus'); - exportKBCallback('KBR', 'beepReset'); - exportKBCallback('KT', 'insertText'); - exportKBCallback('KR', 'registerKeyboard'); - exportKBCallback('KRS', 'registerStub'); - exportKBCallback('KC', 'context'); - exportKBCallback('KN', 'nul'); - exportKBCallback('KCM', 'contextMatch'); - exportKBCallback('KFCM', 'fullContextMatch'); - exportKBCallback('KIK', 'isKeypress'); - exportKBCallback('KKM', 'keyMatch'); - exportKBCallback('KSM', 'stateMatch'); - exportKBCallback('KKI', 'keyInformation'); - exportKBCallback('KDM', 'deadkeyMatch'); - exportKBCallback('KB', 'beep'); - exportKBCallback('KA', 'any'); - exportKBCallback('KDC', 'deleteContext'); - exportKBCallback('KO', 'output'); - exportKBCallback('KDO', 'deadkeyOutput'); - exportKBCallback('KCXO', 'contextExOutput'); - exportKBCallback('KIO', 'indexOutput'); - exportKBCallback('KIFS', 'ifStore'); - exportKBCallback('KSETS', 'setStore'); - exportKBCallback('KLOAD', 'loadStore'); - exportKBCallback('KSAVE', 'saveStore'); } + + exportKBCallback('KSF', 'saveFocus'); + exportKBCallback('KBR', 'beepReset'); + exportKBCallback('KT', 'insertText'); + exportKBCallback('KR', 'registerKeyboard'); + exportKBCallback('KRS', 'registerStub'); + exportKBCallback('KC', 'context'); + exportKBCallback('KN', 'nul'); + exportKBCallback('KCM', 'contextMatch'); + exportKBCallback('KFCM', 'fullContextMatch'); + exportKBCallback('KIK', 'isKeypress'); + exportKBCallback('KKM', 'keyMatch'); + exportKBCallback('KSM', 'stateMatch'); + exportKBCallback('KKI', 'keyInformation'); + exportKBCallback('KDM', 'deadkeyMatch'); + exportKBCallback('KB', 'beep'); + exportKBCallback('KA', 'any'); + exportKBCallback('KDC', 'deleteContext'); + exportKBCallback('KO', 'output'); + exportKBCallback('KDO', 'deadkeyOutput'); + exportKBCallback('KCXO', 'contextExOutput'); + exportKBCallback('KIO', 'indexOutput'); + exportKBCallback('KIFS', 'ifStore'); + exportKBCallback('KSETS', 'setStore'); + exportKBCallback('KLOAD', 'loadStore'); + exportKBCallback('KSAVE', 'saveStore'); } +} - (function() { - // This will be the only call within the keyboard-processor module. - KeyboardInterface.__publishShorthandAPI(); - }()); -} \ No newline at end of file +(function() { + // This will be the only call within the keyboard-processor module. + KeyboardInterface.__publishShorthandAPI(); +}()); \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/keyEvent.ts b/common/web/keyboard-processor/src/text/keyEvent.ts index 1a16dc94bc7a51e068b0347092cf170c3be864fe..719de488b11b243b0ea9bf1362d02059fe6b59ae 100644 --- a/common/web/keyboard-processor/src/text/keyEvent.ts +++ b/common/web/keyboard-processor/src/text/keyEvent.ts @@ -1,57 +1,188 @@ -/// +// TODO: Move to separate folder: 'codes' +// We should start splitting off code needed by keyboards even without a KeyboardProcessor active. +// There's an upcoming `/common/web/types` package that 'codes' and 'keyboards' may fit well within. -namespace com.keyman.text { - // Represents a probability distribution over a keyboard's keys. - // Defined here to avoid compilation issues. - export type KeyDistribution = {keyId: string, p: number}[]; +// KeyEvent may be a _little_ bit of pollution, but this IS what the Web OSK currently generates to signal +// a key event. The most straightforward way to integrate Web OSK events on other platforms is to have +// other platforms recognize and utilize this type. + +import type Keyboard from "../keyboards/keyboard.js"; +import { type DeviceSpec } from "@keymanapp/web-utils"; + +import Codes from './codes.js'; +import DefaultRules from './defaultRules.js'; + +// Represents a probability distribution over a keyboard's keys. +// Defined here to avoid compilation issues. +export type KeyDistribution = {keyId: string, p: number}[]; + +/** + * A simple instance of the standard 'default rules' for keystroke processing from the + * DefaultRules base class. + */ +const BASE_DEFAULT_RULES = new DefaultRules(); + +export interface KeyEventSpec { + + Lcode: number; + Lstates: number; + LmodifierChange?: boolean; + Lmodifiers: number; + LisVirtualKey?: boolean; + vkCode?: number; + kName: string; + kLayer?: string; // The key's layer property + kbdLayer?: string; // The virtual keyboard's active layer + kNextLayer?: string; + + /** + * Marks the active keyboard at the time that this KeyEvent was generated by the user. + * + * Note: this is NOT equivalent to the active keyboard at the time that the event handler begins + * processing! It should be set via closure (or similar) on the event handler that can 100% + * guarantee that the keyboard instance known to the handler has not changed during JS execution + * since the user's interaction that raised the event. + */ + srcKeyboard?: Keyboard; + + // Holds relevant event properties leading to construction of this KeyEvent. + source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode. + // Holds a generated fat-finger distribution (when appropriate) + keyDistribution?: KeyDistribution; + + /** + * The device model for web-core to follow when processing the keystroke. + */ + device: DeviceSpec; + + /** + * `true` if this event was produced by sources other than a DOM-based KeyboardEvent. + */ + isSynthetic?: boolean; +} + +/** + * This class is defined within its own file so that it can be loaded by code outside of KMW without + * having to actually load the entirety of KMW. + */ +export default class KeyEvent implements KeyEventSpec { + Lcode: number; + Lstates: number; + LmodifierChange?: boolean; + Lmodifiers: number; + LisVirtualKey?: boolean; + vkCode?: number; + kName: string; + kLayer?: string; // The key's layer property + kbdLayer?: string; // The virtual keyboard's active layer + kNextLayer?: string; /** - * This class is defined within its own file so that it can be loaded by code outside of KMW without - * having to actually load the entirety of KMW. + * Marks the active keyboard at the time that this KeyEvent was generated by the user. + * + * Note: this is NOT equivalent to the active keyboard at the time that the event handler begins + * processing! It should be set via closure (or similar) on the event handler that can 100% + * guarantee that the keyboard instance known to the handler has not changed during JS execution + * since the user's interaction that raised the event. */ - export class KeyEvent { - Lcode: number; - Lstates: number; - LmodifierChange?: boolean; - Lmodifiers: number; - LisVirtualKey: boolean; - vkCode: number; - kName: string; - kLayer?: string; // The key's layer property - kbdLayer?: string; // The virtual keyboard's active layer - kNextLayer?: string; - - /** - * Marks the active keyboard at the time that this KeyEvent was generated by the user. - * - * Note: this is NOT equivalent to the active keyboard at the time that the event handler begins - * processing! It should be set via closure (or similar) on the event handler that can 100% - * guarantee that the keyboard instance known to the handler has not changed during JS execution - * since the user's interaction that raised the event. - */ - srcKeyboard?: keyboards.Keyboard; - - // Holds relevant event properties leading to construction of this KeyEvent. - source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode. - // Holds a generated fat-finger distribution (when appropriate) - keyDistribution?: KeyDistribution; - - /** - * The device model for web-core to follow when processing the keystroke. - */ - device: utils.DeviceSpec; - - /** - * `true` if this event was produced by sources other than a DOM-based KeyboardEvent. - */ - isSynthetic: boolean = true; - - public static constructNullKeyEvent(device: utils.DeviceSpec): KeyEvent { - const keyEvent = new KeyEvent(); - keyEvent.Lcode = 0; - keyEvent.kName = ''; - keyEvent.device = device; - return keyEvent; + srcKeyboard?: Keyboard; + + // Holds relevant event properties leading to construction of this KeyEvent. + source?: any; // Technically, KeyEvent|MouseEvent|Touch - but those are DOM types that must be kept out of headless mode. + // Holds a generated fat-finger distribution (when appropriate) + keyDistribution?: KeyDistribution; + + /** + * The device model for web-core to follow when processing the keystroke. + */ + device: DeviceSpec; + + /** + * `true` if this event was produced by sources other than a DOM-based KeyboardEvent. + */ + isSynthetic: boolean = true; + + public constructor(keyEventSpec: KeyEventSpec) { + for(let key in keyEventSpec) { + if(keyEventSpec[key] !== undefined) { + this[key] = keyEventSpec[key]; + } + } + } + + public static constructNullKeyEvent(device: DeviceSpec): KeyEvent { + const keyEvent = new KeyEvent({ + Lcode: 0, + kName: '', + device: device, + Lstates: undefined, + Lmodifiers: undefined, + vkCode: undefined, + LisVirtualKey: undefined + }); + return keyEvent; + } + + get isModifier(): boolean { + switch(this.Lcode) { + case 16: //"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18 + case 17: + case 18: + case 20: //"K_CAPS":20, "K_NUMLOCK":144,"K_SCROLL":145 + case 144: + case 145: + return true; + default: + return false; + } + } + + // FIXME: makes some bad assumptions. + setMnemonicCode(shifted: boolean, capsActive: boolean) { + // K_SPACE is not handled by defaultKeyOutput for physical keystrokes unless using touch-aliased elements. + // It's also a "exception required, March 2013" for clickKey, so at least they both have this requirement. + if(this.Lcode != Codes.keyCodes['K_SPACE']) { + // So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value. + // We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'. + let mappingEvent: KeyEvent = new KeyEvent(this); + for(let key in (this as KeyEvent)) { + mappingEvent[key] = this[key]; + } + + // To facilitate storing relevant commands, we should probably reverse-lookup + // the actual keyname instead. + mappingEvent.kName = 'K_xxxx'; + mappingEvent.Lmodifiers = (shifted ? 0x10 : 0); // mnemonic lookups only exist for default & shift layers. + var mappedChar: string = BASE_DEFAULT_RULES.forAny(mappingEvent, true); + + /* First, save a backup of the original code. This one won't needlessly trigger keyboard + * rules, but allows us to replicate/emulate commands after rule processing if needed. + * (Like backspaces) + */ + this.vkCode = this.Lcode; + if(mappedChar) { + // Will return 96 for 'a', which is a keycode corresponding to Codes.keyCodes('K_NP1') - a numpad key. + // That stated, we're in mnemonic mode - this keyboard's rules are based on the char codes. + this.Lcode = mappedChar.charCodeAt(0); + } else { + // Don't let command-type keys (like K_DEL, which will output '.' otherwise!) + // trigger keyboard rules. + // + // However, DO make sure modifier keys pass through safely. + // (https://github.com/keymanapp/keyman/issues/3744) + if(!this.isModifier) { + delete this.Lcode; + } + } + } + + if(capsActive) { + // TODO: Needs fixing - does not properly mirror physical keystrokes, as Lcode range 96-111 corresponds + // to numpad keys! (Physical keyboard section has its own issues here.) + if((this.Lcode >= 65 && this.Lcode <= 90) /* 'A' - 'Z' */ || (this.Lcode >= 97 && this.Lcode <= 122) /* 'a' - 'z' */) { + this.Lmodifiers ^= 0x10; // Flip the 'shifted' bit, so it'll act as the opposite key. + this.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics. + } } - }; -} \ No newline at end of file + } +}; \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/keyMapping.ts b/common/web/keyboard-processor/src/text/keyMapping.ts index ba7cc09868802a61e6dc09141553aea1099169f3..663eb5457f7a7bb3c0221008923f1aa11da930a4 100644 --- a/common/web/keyboard-processor/src/text/keyMapping.ts +++ b/common/web/keyboard-processor/src/text/keyMapping.ts @@ -2,185 +2,186 @@ KeymanWeb 11.0 Copyright 2019 SIL International ***/ -namespace com.keyman { - class KeyMap { - [keycode: string]: number; +import type KeyEvent from "./keyEvent.js"; +import { KeyEventSpec } from "./keyEvent.js"; + +class KeyMap { + [keycode: string]: number; +} + +class BrowserKeyMaps { + FF: KeyMap = new KeyMap(); + Safari: KeyMap = new KeyMap(); + Opera: KeyMap = new KeyMap(); + + constructor() { + // All three have been around since at least May 2014 / FF 29. + // It'd hard to find precise history, but at least that much has been confirmed. + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021. + this.FF['k61'] = 187; // = // FF 2.0 + this.FF['k59'] = 186; // ; + this.FF['k173'] = 189; // -/_ } - - class BrowserKeyMaps { - FF: KeyMap = new KeyMap(); - Safari: KeyMap = new KeyMap(); - Opera: KeyMap = new KeyMap(); - - constructor() { - // All three have been around since at least May 2014 / FF 29. - // It'd hard to find precise history, but at least that much has been confirmed. - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode, on Feb 26 2021. - this.FF['k61'] = 187; // = // FF 2.0 - this.FF['k59'] = 186; // ; - this.FF['k173'] = 189; // -/_ - } +} + +class LanguageKeyMaps { + [languageCode: string]: KeyMap; + + // // Here are some old legacy definitions that were no longer referenced but are likely related: + // static _BaseLayoutEuro: {[code: string]: string} = { + // 'se': '\u00a71234567890+´~~~QWERTYUIOP\u00c5\u00a8\'~~~ASDFGHJKL\u00d6\u00c4~~~~~ ` ~ + this['uk']['k192'] = 222; // ' @ => ' " + this['uk']['k222'] = 226; // # ~ => K_oE2 // I1504 - UK keyboard mixup #, \ + this['uk']['k220'] = 220; // \ | => \ | // I1504 - UK keyboard mixup #, \ } +} - class LanguageKeyMaps { - [languageCode: string]: KeyMap; - - // // Here are some old legacy definitions that were no longer referenced but are likely related: - // static _BaseLayoutEuro: {[code: string]: string} = { - // 'se': '\u00a71234567890+´~~~QWERTYUIOP\u00c5\u00a8\'~~~ASDFGHJKL\u00d6\u00c4~~~~~ ` ~ - this['uk']['k192'] = 222; // ' @ => ' " - this['uk']['k222'] = 226; // # ~ => K_oE2 // I1504 - UK keyboard mixup #, \ - this['uk']['k220'] = 220; // \ | => \ | // I1504 - UK keyboard mixup #, \ - } - } +export default class KeyMapping { + static readonly browserMap: BrowserKeyMaps = new BrowserKeyMaps(); + static readonly languageMap: LanguageKeyMaps = new LanguageKeyMaps(); - export class KeyMapping { - static readonly browserMap: BrowserKeyMaps = new BrowserKeyMaps(); - static readonly languageMap: LanguageKeyMaps = new LanguageKeyMaps(); + private static _usCharCodes: KeyMap[]; - private static _usCharCodes: KeyMap[]; + private constructor() { + // Do not construct this class. + } - private constructor() { - // Do not construct this class. - } + private static _usCodeInit() { + var s0=new KeyMap(),s1=new KeyMap(); + + s0['k192'] = 96; + s0['k49'] = 49; + s0['k50'] = 50; + s0['k51'] = 51; + s0['k52'] = 52; + s0['k53'] = 53; + s0['k54'] = 54; + s0['k55'] = 55; + s0['k56'] = 56; + s0['k57'] = 57; + s0['k48'] = 48; + s0['k189'] = 45; + s0['k187'] = 61; + s0['k81'] = 113; + s0['k87'] = 119; + s0['k69'] = 101; + s0['k82'] = 114; + s0['k84'] = 116; + s0['k89'] = 121; + s0['k85'] = 117; + s0['k73'] = 105; + s0['k79'] = 111; + s0['k80'] = 112; + s0['k219'] = 91; + s0['k221'] = 93; + s0['k220'] = 92; + s0['k65'] = 97; + s0['k83'] = 115; + s0['k68'] = 100; + s0['k70'] = 102; + s0['k71'] = 103; + s0['k72'] = 104; + s0['k74'] = 106; + s0['k75'] = 107; + s0['k76'] = 108; + s0['k186'] = 59; + s0['k222'] = 39; + s0['k90'] = 122; + s0['k88'] = 120; + s0['k67'] = 99; + s0['k86'] = 118; + s0['k66'] = 98; + s0['k78'] = 110; + s0['k77'] = 109; + s0['k188'] = 44; + s0['k190'] = 46; + s0['k191'] = 47; + + s1['k192'] = 126; + s1['k49'] = 33; + s1['k50'] = 64; + s1['k51'] = 35; + s1['k52'] = 36; + s1['k53'] = 37; + s1['k54'] = 94; + s1['k55'] = 38; + s1['k56'] = 42; + s1['k57'] = 40; + s1['k48'] = 41; + s1['k189'] = 95; + s1['k187'] = 43; + s1['k81'] = 81; + s1['k87'] = 87; + s1['k69'] = 69; + s1['k82'] = 82; + s1['k84'] = 84; + s1['k89'] = 89; + s1['k85'] = 85; + s1['k73'] = 73; + s1['k79'] = 79; + s1['k80'] = 80; + s1['k219'] = 123; + s1['k221'] = 125; + s1['k220'] = 124; + s1['k65'] = 65; + s1['k83'] = 83; + s1['k68'] = 68; + s1['k70'] = 70; + s1['k71'] = 71; + s1['k72'] = 72; + s1['k74'] = 74; + s1['k75'] = 75; + s1['k76'] = 76; + s1['k186'] = 58; + s1['k222'] = 34; + s1['k90'] = 90; + s1['k88'] = 88; + s1['k67'] = 67; + s1['k86'] = 86; + s1['k66'] = 66; + s1['k78'] = 78; + s1['k77'] = 77; + s1['k188'] = 60; + s1['k190'] = 62; + s1['k191'] = 63; + + KeyMapping._usCharCodes = [s0,s1]; + } - private static _usCodeInit() { - var s0=new KeyMap(),s1=new KeyMap(); - - s0['k192'] = 96; - s0['k49'] = 49; - s0['k50'] = 50; - s0['k51'] = 51; - s0['k52'] = 52; - s0['k53'] = 53; - s0['k54'] = 54; - s0['k55'] = 55; - s0['k56'] = 56; - s0['k57'] = 57; - s0['k48'] = 48; - s0['k189'] = 45; - s0['k187'] = 61; - s0['k81'] = 113; - s0['k87'] = 119; - s0['k69'] = 101; - s0['k82'] = 114; - s0['k84'] = 116; - s0['k89'] = 121; - s0['k85'] = 117; - s0['k73'] = 105; - s0['k79'] = 111; - s0['k80'] = 112; - s0['k219'] = 91; - s0['k221'] = 93; - s0['k220'] = 92; - s0['k65'] = 97; - s0['k83'] = 115; - s0['k68'] = 100; - s0['k70'] = 102; - s0['k71'] = 103; - s0['k72'] = 104; - s0['k74'] = 106; - s0['k75'] = 107; - s0['k76'] = 108; - s0['k186'] = 59; - s0['k222'] = 39; - s0['k90'] = 122; - s0['k88'] = 120; - s0['k67'] = 99; - s0['k86'] = 118; - s0['k66'] = 98; - s0['k78'] = 110; - s0['k77'] = 109; - s0['k188'] = 44; - s0['k190'] = 46; - s0['k191'] = 47; - - s1['k192'] = 126; - s1['k49'] = 33; - s1['k50'] = 64; - s1['k51'] = 35; - s1['k52'] = 36; - s1['k53'] = 37; - s1['k54'] = 94; - s1['k55'] = 38; - s1['k56'] = 42; - s1['k57'] = 40; - s1['k48'] = 41; - s1['k189'] = 95; - s1['k187'] = 43; - s1['k81'] = 81; - s1['k87'] = 87; - s1['k69'] = 69; - s1['k82'] = 82; - s1['k84'] = 84; - s1['k89'] = 89; - s1['k85'] = 85; - s1['k73'] = 73; - s1['k79'] = 79; - s1['k80'] = 80; - s1['k219'] = 123; - s1['k221'] = 125; - s1['k220'] = 124; - s1['k65'] = 65; - s1['k83'] = 83; - s1['k68'] = 68; - s1['k70'] = 70; - s1['k71'] = 71; - s1['k72'] = 72; - s1['k74'] = 74; - s1['k75'] = 75; - s1['k76'] = 76; - s1['k186'] = 58; - s1['k222'] = 34; - s1['k90'] = 90; - s1['k88'] = 88; - s1['k67'] = 67; - s1['k86'] = 86; - s1['k66'] = 66; - s1['k78'] = 78; - s1['k77'] = 77; - s1['k188'] = 60; - s1['k190'] = 62; - s1['k191'] = 63; - - KeyMapping._usCharCodes = [s0,s1]; + /** + * Function _USKeyCodeToCharCode + * Scope Private + * @param {Event} Levent KMW event object + * @return {number} Character code + * Description Translate keyboard codes to standard US layout codes + */ + static _USKeyCodeToCharCode(Levent: KeyEvent | KeyEventSpec) { + return KeyMapping.usCharCodes[Levent.Lmodifiers & 0x10 ? 1 : 0]['k'+Levent.Lcode]; + }; + + public static get usCharCodes() { + if(!KeyMapping._usCharCodes) { + KeyMapping._usCodeInit(); } - /** - * Function _USKeyCodeToCharCode - * Scope Private - * @param {Event} Levent KMW event object - * @return {number} Character code - * Description Translate keyboard codes to standard US layout codes - */ - static _USKeyCodeToCharCode(Levent: com.keyman.text.KeyEvent) { - return KeyMapping.usCharCodes[Levent.Lmodifiers & 0x10 ? 1 : 0]['k'+Levent.Lcode]; - }; - - public static get usCharCodes() { - if(!KeyMapping._usCharCodes) { - KeyMapping._usCodeInit(); - } - - return KeyMapping._usCharCodes; - } + return KeyMapping._usCharCodes; } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/keyboardProcessor.ts b/common/web/keyboard-processor/src/text/keyboardProcessor.ts index a1ad0699f03a73e4f3d633a1807895e8212654fb..3726c67a170ac211e73f271ded8aa08b858fd888 100644 --- a/common/web/keyboard-processor/src/text/keyboardProcessor.ts +++ b/common/web/keyboard-processor/src/text/keyboardProcessor.ts @@ -1,780 +1,598 @@ -// Establishes key-code definitions. -/// -// Defines our generalized "KeyEvent" class. -/// -// Defines the RuleBehavior keyboard-processing return object. -/// -// Defines default key handling behaviors. -/// -// Defines the keyboard wrapper object. -/// -// Defines built-in keymapping. -/// +// #region Big ol' list of imports -// Also relies on @keymanapp/web-utils, which is included via tsconfig.json. - -namespace com.keyman.text { - export type BeepHandler = (outputTarget: OutputTarget) => void; - export type LogMessageHandler = (str: string) => void; +import EventEmitter from 'eventemitter3'; - export interface VariableStoreSerializer { - loadStore(keyboardID: string, storeName: string): VariableStore; - saveStore(keyboardID: string, storeName: string, storeMap: VariableStore); - } +import Codes from "./codes.js"; +import type Keyboard from "../keyboards/keyboard.js"; +import { MinimalKeymanGlobal } from '../keyboards/keyboardHarness.js'; +import KeyEvent from "./keyEvent.js"; +import { Layouts } from "../keyboards/defaultLayouts.js"; +import type { MutableSystemStore } from "./systemStores.js"; - export interface ProcessorInitOptions { - baseLayout?: string; - variableStoreSerializer?: VariableStoreSerializer; - } +import DefaultRules, { EmulationKeystrokes } from "./defaultRules.js"; +import type OutputTarget from "./outputTarget.js"; +import { Mock } from "./outputTarget.js"; - export class KeyboardProcessor { - public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { - baseLayout: 'us' - } +import KeyboardInterface, { SystemStoreIDs, VariableStore } from "./kbdInterface.js"; +import RuleBehavior from "./ruleBehavior.js"; - // Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them. - // Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code. - stateKeys = { - "K_CAPS":false, - "K_NUMLOCK":false, - "K_SCROLL":false - }; - - // Tracks the most recent modifier state information in order to quickly detect changes - // in keyboard state not otherwise captured by the hosting page in the browser. - // Needed for AltGr simulation. - modStateFlags: number = 0; - - keyboardInterface: KeyboardInterface; - - /** - * Indicates the device (platform) to be used for non-keystroke events, - * such as those sent to `begin postkeystroke` and `begin newcontext` - * entry points. - */ - contextDevice: utils.DeviceSpec; - - baseLayout: string; - - // Callbacks for various feedback types - beepHandler?: BeepHandler; - warningLogger?: LogMessageHandler; - errorLogger?: LogMessageHandler; - - constructor(device: utils.DeviceSpec, options?: ProcessorInitOptions) { - if(!options) { - options = KeyboardProcessor.DEFAULT_OPTIONS; - } +import { DeviceSpec, globalObject } from "@keymanapp/web-utils"; - this.contextDevice = device; +// #endregion - this.baseLayout = options.baseLayout || KeyboardProcessor.DEFAULT_OPTIONS.baseLayout; - this.keyboardInterface = new KeyboardInterface(options.variableStoreSerializer); - this.installInterface(); - } - - private installInterface() { - // We must ensure that the keyboard can find the API functions at the expected place. - let globalThis = utils.getGlobalObject(); - globalThis[KeyboardInterface.GLOBAL_NAME] = this.keyboardInterface; - - // Ensure that the active keyboard is set on the keyboard interface object. - if(this.activeKeyboard) { - this.keyboardInterface.activeKeyboard = this.activeKeyboard; - } - } +// Also relies on @keymanapp/web-utils, which is included via tsconfig.json. - public get activeKeyboard(): keyboards.Keyboard { - return this.keyboardInterface.activeKeyboard; - } +export type BeepHandler = (outputTarget: OutputTarget) => void; +export type LogMessageHandler = (str: string) => void; - public set activeKeyboard(keyboard: keyboards.Keyboard) { - this.keyboardInterface.activeKeyboard = keyboard; +export interface VariableStoreSerializer { + loadStore(keyboardID: string, storeName: string): VariableStore; + saveStore(keyboardID: string, storeName: string, storeMap: VariableStore); +} - // All old deadkeys and keyboard-specific cache should immediately be invalidated - // on a keyboard change. - this.resetContext(); - } +export interface ProcessorInitOptions { + baseLayout?: string; + keyboardInterface?: KeyboardInterface; + defaultOutputRules?: DefaultRules; // Takes the class def object, not an instance thereof. +} - get layerStore(): MutableSystemStore { - return this.keyboardInterface.systemStores[KeyboardInterface.TSS_LAYER] as MutableSystemStore; - } +interface EventMap { + statekeychange: (stateKeys: typeof KeyboardProcessor.prototype.stateKeys) => void; +} - public get newLayerStore(): MutableSystemStore { - return this.keyboardInterface.systemStores[KeyboardInterface.TSS_NEWLAYER] as MutableSystemStore; - } +export default class KeyboardProcessor extends EventEmitter { + public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { + baseLayout: 'us', + defaultOutputRules: new DefaultRules() + }; - public get oldLayerStore(): MutableSystemStore { - return this.keyboardInterface.systemStores[KeyboardInterface.TSS_OLDLAYER] as MutableSystemStore; - } + // Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them. + // Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code. + stateKeys = { + "K_CAPS":false, + "K_NUMLOCK":false, + "K_SCROLL":false + }; - public get layerId(): string { - return this.layerStore.value; - } + // Tracks the most recent modifier state information in order to quickly detect changes + // in keyboard state not otherwise captured by the hosting page in the browser. + // Needed for AltGr simulation. + modStateFlags: number = 0; - // Note: will trigger an 'event' callback designed to notify the OSK of layer changes. - public set layerId(value: string) { - this.layerStore.set(value); - } + keyboardInterface: KeyboardInterface; - /** - * Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults - * where and when appropriate. - * - * @param {object} Lkc The pre-analyzed KeyEvent object - * @param {boolean} outputTarget The OutputTarget receiving the KeyEvent - * @return {string} - */ - defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTarget, readonly: boolean): RuleBehavior { - let preInput = Mock.from(outputTarget, readonly); - let ruleBehavior = new RuleBehavior(); - - let matched = false; - var char = ''; - var special: EmulationKeystrokes; - if(Lkc.isSynthetic || outputTarget.isSynthetic) { - matched = true; // All the conditions below result in matches until the final else, which restores the expected default - // if no match occurs. - - if(DefaultOutput.isCommand(Lkc)) { - // Note this in the rule behavior, return successfully. We'll consider applying it later. - ruleBehavior.triggersDefaultCommand = true; - - // We'd rather let the browser handle these keys, but we're using emulated keystrokes, forcing KMW - // to emulate default behavior here. - } else if((special = DefaultOutput.forSpecialEmulation(Lkc)) != null) { - switch(special) { - case EmulationKeystrokes.Backspace: - this.keyboardInterface.defaultBackspace(outputTarget); - break; - case EmulationKeystrokes.Enter: - outputTarget.handleNewlineAtCaret(); - break; - // case '\u007f': // K_DEL - // // For (possible) future implementation. - // // Would recommend (conceptually) equaling K_RIGHT + K_BKSP, the former of which would technically be a 'command'. - default: - // In case we extend the allowed set, but forget to implement its handling case above. - ruleBehavior.errorLog = "Unexpected 'special emulation' character (\\u" + (special as String).kmwCharCodeAt(0).toString(16) + ") went unhandled!"; - } - } else { - // Back to the standard default, pending normal matching. - matched = false; - } - } + /** + * Indicates the device (platform) to be used for non-keystroke events, + * such as those sent to `begin postkeystroke` and `begin newcontext` + * entry points. + */ + contextDevice: DeviceSpec; - let isMnemonic = this.activeKeyboard && this.activeKeyboard.isMnemonic; + baseLayout: string; - if(!matched) { - if((char = DefaultOutput.forAny(Lkc, isMnemonic)) != null) { - special = DefaultOutput.forSpecialEmulation(Lkc) - if(special == EmulationKeystrokes.Backspace) { - // A browser's default backspace may fail to delete both parts of an SMP character. - this.keyboardInterface.defaultBackspace(outputTarget); - } else if(special || DefaultOutput.isCommand(Lkc)) { // Filters out 'commands' like TAB. - // We only do the "for special emulation" cases under the condition above... aside from backspace - // Let the browser handle those. - return null; - } else { - this.keyboardInterface.output(0, outputTarget, char); - } - } else { - // No match, no default RuleBehavior. - return null; - } - } + defaultRules: DefaultRules; - // Shortcut things immediately if there were issues generating this rule behavior. - if(ruleBehavior.errorLog) { - return ruleBehavior; - } + // Callbacks for various feedback types + beepHandler?: BeepHandler; + warningLogger?: LogMessageHandler; + errorLogger?: LogMessageHandler; - let transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc, readonly); - ruleBehavior.transcription = transcription; + constructor(device: DeviceSpec, options?: ProcessorInitOptions) { + super(); - return ruleBehavior; + if(!options) { + options = KeyboardProcessor.DEFAULT_OPTIONS; } - setSyntheticEventDefaults(Lkc: text.KeyEvent) { - // Set the flags for the state keys - for desktop devices. For touch - // devices, the only state key in use currently is Caps Lock, which is set - // when the 'caps' layer is active in ActiveKey::constructBaseKeyEvent. - if(!Lkc.device.touchable) { - Lkc.Lstates |= this.stateKeys['K_CAPS'] ? Codes.modifierCodes['CAPS'] : Codes.modifierCodes['NO_CAPS']; - Lkc.Lstates |= this.stateKeys['K_NUMLOCK'] ? Codes.modifierCodes['NUM_LOCK'] : Codes.modifierCodes['NO_NUM_LOCK']; - Lkc.Lstates |= this.stateKeys['K_SCROLL'] ? Codes.modifierCodes['SCROLL_LOCK'] : Codes.modifierCodes['NO_SCROLL_LOCK']; - } - - // Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys - if(Lkc.kName && Lkc.kName.substr(0,2) == 'U_') { - Lkc.LisVirtualKey=false; - } + this.contextDevice = device; - // Get code for non-physical keys (T_KOKAI, U_05AB etc) - if(typeof Lkc.Lcode == 'undefined') { - Lkc.Lcode = this.getVKDictionaryCode(Lkc.kName);// Updated for Build 347 - if(!Lkc.Lcode) { - // Special case for U_xxxx keys. This vk code will never be used - // in a keyboard, so we use this to ensure that keystroke processing - // occurs for the key. - Lkc.Lcode = 1; - } - } + this.baseLayout = options.baseLayout || KeyboardProcessor.DEFAULT_OPTIONS.baseLayout; + this.keyboardInterface = options.keyboardInterface || new KeyboardInterface(globalObject(), MinimalKeymanGlobal); + this.defaultRules = options.defaultOutputRules || KeyboardProcessor.DEFAULT_OPTIONS.defaultOutputRules; + } - // Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer. - if((Lkc.Lmodifiers & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM'] && this.activeKeyboard.emulatesAltGr) { - Lkc.Lmodifiers &= ~Codes.modifierBitmasks['ALT_GR_SIM']; - Lkc.Lmodifiers |= Codes.modifierCodes['RALT']; - } - } + public get activeKeyboard(): Keyboard { + return this.keyboardInterface.activeKeyboard; + } - constructNullKeyEvent(device: utils.DeviceSpec): KeyEvent { - const keyEvent = KeyEvent.constructNullKeyEvent(device); - this.setSyntheticEventDefaults(keyEvent); - return keyEvent; - } + public set activeKeyboard(keyboard: Keyboard) { + this.keyboardInterface.activeKeyboard = keyboard; - processNewContextEvent(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { - return this.activeKeyboard ? - this.keyboardInterface.processNewContextEvent(outputTarget, this.constructNullKeyEvent(device)) : - null; - } + // All old deadkeys and keyboard-specific cache should immediately be invalidated + // on a keyboard change. + this.resetContext(); + } - processPostKeystroke(device: utils.DeviceSpec, outputTarget: OutputTarget): RuleBehavior { - return this.activeKeyboard ? - this.keyboardInterface.processPostKeystroke(outputTarget, this.constructNullKeyEvent(device)) : - null; - } + get layerStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_LAYER] as MutableSystemStore; + } - processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { - var matchBehavior: RuleBehavior; - - // Pass this key code and state to the keyboard program - if(this.activeKeyboard && keyEvent.Lcode != 0) { - /* - * The `this.installInterface()` call is insurance against something I've seen in unit tests when things break a bit. - * - * Currently, when a KMW shutdown doesn't go through properly or completely, sometimes we end up with parallel - * versions of KMW running, and an old, partially-shutdown one will "snipe" a command meant for the most-recent - * one's test. So, installing here ensures that the active Processor has its matching KeyboardInterface ready, - * even should that occur. - */ - this.installInterface(); - matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent); - } + public get newLayerStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_NEWLAYER] as MutableSystemStore; + } - if(!matchBehavior || matchBehavior.triggerKeyDefault) { - // Restore the virtual key code if a mnemonic keyboard is being used - // If no vkCode value was stored, maintain the original Lcode value. - keyEvent.Lcode=keyEvent.vkCode || keyEvent.Lcode; - - // Handle unmapped keys, including special keys - // The following is physical layout dependent, so should be avoided if possible. All keys should be mapped. - this.keyboardInterface.activeTargetOutput = outputTarget; - - // Match against the 'default keyboard' - rules to mimic the default string output when typing in a browser. - // Many keyboards rely upon these 'implied rules'. - let defaultBehavior = this.defaultRuleBehavior(keyEvent, outputTarget, false); - if(defaultBehavior) { - if(!matchBehavior) { - matchBehavior = defaultBehavior; - } else { - matchBehavior.mergeInDefaults(defaultBehavior); - } - matchBehavior.triggerKeyDefault = false; // We've triggered it successfully. - } // If null, we must rely on something else (like the browser, in DOM-aware code) to fulfill the default. + public get oldLayerStore(): MutableSystemStore { + return this.keyboardInterface.systemStores[SystemStoreIDs.TSS_OLDLAYER] as MutableSystemStore; + } - this.keyboardInterface.activeTargetOutput = null; - } + public get layerId(): string { + return this.layerStore.value; + } - return matchBehavior; - } + // Note: will trigger an 'event' callback designed to notify the OSK of layer changes. + public set layerId(value: string) { + this.layerStore.set(value); + } - // FIXME: makes some bad assumptions. - static setMnemonicCode(Lkc: KeyEvent, shifted: boolean, capsActive: boolean) { - // K_SPACE is not handled by defaultKeyOutput for physical keystrokes unless using touch-aliased elements. - // It's also a "exception required, March 2013" for clickKey, so at least they both have this requirement. - if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { - // So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value. - // We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'. - let mappingEvent: KeyEvent = new KeyEvent(); - for(var key in Lkc) { - mappingEvent[key] = Lkc[key]; + /** + * Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults + * where and when appropriate. + * + * @param {object} Lkc The pre-analyzed KeyEvent object + * @param {boolean} outputTarget The OutputTarget receiving the KeyEvent + * @return {string} + */ + defaultRuleBehavior(Lkc: KeyEvent, outputTarget: OutputTarget, readonly: boolean): RuleBehavior { + let preInput = Mock.from(outputTarget, readonly); + let ruleBehavior = new RuleBehavior(); + + let matched = false; + var char = ''; + var special: EmulationKeystrokes; + if(Lkc.isSynthetic || outputTarget.isSynthetic) { + matched = true; // All the conditions below result in matches until the final else, which restores the expected default + // if no match occurs. + + if(this.defaultRules.isCommand(Lkc)) { + // Note this in the rule behavior, return successfully. We'll consider applying it later. + ruleBehavior.triggersDefaultCommand = true; + + // We'd rather let the browser handle these keys, but we're using emulated keystrokes, forcing KMW + // to emulate default behavior here. + } else if((special = this.defaultRules.forSpecialEmulation(Lkc)) != null) { + switch(special) { + case EmulationKeystrokes.Backspace: + this.keyboardInterface.defaultBackspace(outputTarget); + break; + case EmulationKeystrokes.Enter: + outputTarget.handleNewlineAtCaret(); + break; + // case '\u007f': // K_DEL + // // For (possible) future implementation. + // // Would recommend (conceptually) equaling K_RIGHT + K_BKSP, the former of which would technically be a 'command'. + default: + // In case we extend the allowed set, but forget to implement its handling case above. + ruleBehavior.errorLog = "Unexpected 'special emulation' character (\\u" + (special as String).kmwCharCodeAt(0).toString(16) + ") went unhandled!"; } + } else { + // Back to the standard default, pending normal matching. + matched = false; + } + } - // To facilitate storing relevant commands, we should probably reverse-lookup - // the actual keyname instead. - mappingEvent.kName = 'K_xxxx'; - mappingEvent.Lmodifiers = (shifted ? 0x10 : 0); // mnemonic lookups only exist for default & shift layers. - var mappedChar: string = DefaultOutput.forAny(mappingEvent, true); - - /* First, save a backup of the original code. This one won't needlessly trigger keyboard - * rules, but allows us to replicate/emulate commands after rule processing if needed. - * (Like backspaces) - */ - Lkc.vkCode = Lkc.Lcode; - if(mappedChar) { - // Will return 96 for 'a', which is a keycode corresponding to Codes.keyCodes('K_NP1') - a numpad key. - // That stated, we're in mnemonic mode - this keyboard's rules are based on the char codes. - Lkc.Lcode = mappedChar.charCodeAt(0); + let isMnemonic = this.activeKeyboard && this.activeKeyboard.isMnemonic; + + if(!matched) { + if((char = this.defaultRules.forAny(Lkc, isMnemonic)) != null) { + special = this.defaultRules.forSpecialEmulation(Lkc) + if(special == EmulationKeystrokes.Backspace) { + // A browser's default backspace may fail to delete both parts of an SMP character. + this.keyboardInterface.defaultBackspace(outputTarget); + } else if(special || this.defaultRules.isCommand(Lkc)) { // Filters out 'commands' like TAB. + // We only do the "for special emulation" cases under the condition above... aside from backspace + // Let the browser handle those. + return null; } else { - // Don't let command-type keys (like K_DEL, which will output '.' otherwise!) - // trigger keyboard rules. - // - // However, DO make sure modifier keys pass through safely. - // (https://github.com/keymanapp/keyman/issues/3744) - if(!KeyboardProcessor.isModifier(Lkc)) { - delete Lkc.Lcode; - } + this.keyboardInterface.output(0, outputTarget, char); } + } else { + // No match, no default RuleBehavior. + return null; } + } - if(capsActive) { - // TODO: Needs fixing - does not properly mirror physical keystrokes, as Lcode range 96-111 corresponds - // to numpad keys! (Physical keyboard section has its own issues here.) - if((Lkc.Lcode >= 65 && Lkc.Lcode <= 90) /* 'A' - 'Z' */ || (Lkc.Lcode >= 97 && Lkc.Lcode <= 122) /* 'a' - 'z' */) { - Lkc.Lmodifiers ^= 0x10; // Flip the 'shifted' bit, so it'll act as the opposite key. - Lkc.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics. - } - } + // Shortcut things immediately if there were issues generating this rule behavior. + if(ruleBehavior.errorLog) { + return ruleBehavior; } - /** - * Get modifier key state from layer id - * - * @param {string} layerId layer id (e.g. ctrlshift) - * @return {number} modifier key state (desktop keyboards) - */ - static getModifierState(layerId: string): number { - var modifier=0; - if(layerId.indexOf('shift') >= 0) { - modifier |= Codes.modifierCodes['SHIFT']; - } + let transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc, readonly); + ruleBehavior.transcription = transcription; - // The chiral checks must not be directly exclusive due each other to visual OSK feedback. - var ctrlMatched=false; - if(layerId.indexOf('leftctrl') >= 0) { - modifier |= Codes.modifierCodes['LCTRL']; - ctrlMatched=true; - } - if(layerId.indexOf('rightctrl') >= 0) { - modifier |= Codes.modifierCodes['RCTRL']; - ctrlMatched=true; - } - if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) { - modifier |= Codes.modifierCodes['CTRL']; - } + return ruleBehavior; + } - var altMatched=false; - if(layerId.indexOf('leftalt') >= 0) { - modifier |= Codes.modifierCodes['LALT']; - altMatched=true; - } - if(layerId.indexOf('rightalt') >= 0) { - modifier |= Codes.modifierCodes['RALT']; - altMatched=true; - } - if(layerId.indexOf('alt') >= 0 && !altMatched) { - modifier |= Codes.modifierCodes['ALT']; - } + processNewContextEvent(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior { + return this.activeKeyboard ? + this.keyboardInterface.processNewContextEvent(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) : + null; + } - return modifier; - } + processPostKeystroke(device: DeviceSpec, outputTarget: OutputTarget): RuleBehavior { + return this.activeKeyboard ? + this.keyboardInterface.processPostKeystroke(outputTarget, this.activeKeyboard.constructNullKeyEvent(device, this.stateKeys)) : + null; + } - /** - * Get state key state from layer id - * - * @param {string} layerId layer id (e.g. caps) - * @return {number} modifier key state (desktop keyboards) - */ - static getStateFromLayer(layerId: string): number { - var modifier=0; - - if(layerId.indexOf('caps') >= 0) { - modifier |= Codes.modifierCodes['CAPS']; - } else { - modifier |= Codes.modifierCodes['NO_CAPS']; - } + processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { + var matchBehavior: RuleBehavior; - return modifier; + // Pass this key code and state to the keyboard program + if(this.activeKeyboard && keyEvent.Lcode != 0) { + matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent); } - /** - * @summary Look up a custom virtual key code in the virtual key code dictionary KVKD. On first run, will build the dictionary. - * - * `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed - * at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e. - * outside the range of standard virtual key codes). These additional codes are both - * `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However, - * `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that - * is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only - * referenced as the id of a key in the touch layout, then it does not get an entry in - * the `KVKD` property. - * - * @private - * @param {string} keyName custom virtual key code to lookup in the dictionary - * @return {number} key code > 255 on success, or 0 if not found - */ - getVKDictionaryCode(keyName: string) { - var activeKeyboard = this.activeKeyboard; - if(!activeKeyboard.scriptObject['VKDictionary']) { - var a=[]; - if(typeof activeKeyboard.scriptObject['KVKD'] == 'string') { - // Build the VK dictionary - // TODO: Move the dictionary build into the compiler -- so compiler generates code such as following. - // Makes the VKDictionary member unnecessary. - // this.KVKD={"K_ABC":256,"K_DEF":257,...}; - var s=activeKeyboard.scriptObject['KVKD'].split(' '); - for(var i=0; i -// Defines the KeyEvent type. -/// /// +import { extendString } from "@keymanapp/web-utils"; -// Also relies on string-extensions provided by the web-utils package. - -namespace com.keyman.text { - export class TextTransform implements Transform { - readonly insert: string; - readonly deleteLeft: number; - readonly deleteRight?: number; +extendString(); - constructor(insert: string, deleteLeft: number, deleteRight?: number) { - this.insert = insert; - this.deleteLeft = deleteLeft; - this.deleteRight = deleteRight || 0; - } +// Defines deadkey management in a manner attachable to each element interface. +import type KeyEvent from "./keyEvent.js"; +import { Deadkey, DeadkeyTracker } from "./deadkeys.js"; - public static readonly nil = new TextTransform('', 0, 0); +// Also relies on string-extensions provided by the web-utils package. - public isNoOp(): boolean { - return this.insert === '' && this.deleteLeft === 0 && this.deleteRight === 0; - } +export function isEmptyTransform(transform: Transform) { + if(!transform) { + return false; + } + return transform.insert === '' && transform.deleteLeft === 0 && (transform.deleteRight ?? 0) === 0; +} + +export class TextTransform implements Transform { + readonly insert: string; + readonly deleteLeft: number; + readonly deleteRight?: number; + + constructor(insert: string, deleteLeft: number, deleteRight?: number) { + this.insert = insert; + this.deleteLeft = deleteLeft; + this.deleteRight = deleteRight || 0; } - export class Transcription { - readonly token: number; - readonly keystroke: KeyEvent; - readonly transform: Transform; - alternates: Alternate[]; // constructed after the rest of the transcription. - readonly preInput: Mock; + public static readonly nil = new TextTransform('', 0, 0); +} - private static tokenSeed: number = 0; +export class Transcription { + readonly token: number; + readonly keystroke: KeyEvent; + readonly transform: Transform; + alternates: Alternate[]; // constructed after the rest of the transcription. + readonly preInput: Mock; - constructor(keystroke: KeyEvent, transform: Transform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) { - let token = this.token = Transcription.tokenSeed++; + private static tokenSeed: number = 0; - this.keystroke = keystroke; - this.transform = transform; - this.alternates = alternates; - this.preInput = preInput; + constructor(keystroke: KeyEvent, transform: Transform, preInput: Mock, alternates?: Alternate[]/*, removedDks: Deadkey[], insertedDks: Deadkey[]*/) { + let token = this.token = Transcription.tokenSeed++; - this.transform.id = this.token; + this.keystroke = keystroke; + this.transform = transform; + this.alternates = alternates; + this.preInput = preInput; - // Assign the ID to each alternate, as well. - if(alternates) { - alternates.forEach(function(alt) { - alt.sample.id = token; - }); - } + this.transform.id = this.token; + + // Assign the ID to each alternate, as well. + if(alternates) { + alternates.forEach(function(alt) { + alt.sample.id = token; + }); } } +} - export type Alternate = ProbabilityMass; +export type Alternate = ProbabilityMass; - export abstract class OutputTarget { - private _dks: text.DeadkeyTracker; +export default abstract class OutputTarget { + private _dks: DeadkeyTracker; - constructor() { - this._dks = new text.DeadkeyTracker(); - } + constructor() { + this._dks = new DeadkeyTracker(); + } - /** - * Signifies that this OutputTarget has no default key processing behaviors. This should be false - * for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement. - */ - get isSynthetic(): boolean { - return true; - } + /** + * Signifies that this OutputTarget has no default key processing behaviors. This should be false + * for OutputTargets backed by web elements like HTMLInputElement or HTMLTextAreaElement. + */ + get isSynthetic(): boolean { + return true; + } - resetContext(): void { - this.deadkeys().clear(); - } + resetContext(): void { + this.deadkeys().clear(); + } - deadkeys(): text.DeadkeyTracker { - return this._dks; - } + deadkeys(): DeadkeyTracker { + return this._dks; + } - hasDeadkeyMatch(n: number, d: number): boolean { - return this.deadkeys().isMatch(this.getDeadkeyCaret(), n, d); - } + hasDeadkeyMatch(n: number, d: number): boolean { + return this.deadkeys().isMatch(this.getDeadkeyCaret(), n, d); + } - insertDeadkeyBeforeCaret(d: number) { - var dk: Deadkey = new Deadkey(this.getDeadkeyCaret(), d); - this.deadkeys().add(dk); - } + insertDeadkeyBeforeCaret(d: number) { + var dk: Deadkey = new Deadkey(this.getDeadkeyCaret(), d); + this.deadkeys().add(dk); + } - /** - * Should be called by each output target immediately before text mutation operations occur. - * - * Maintains solutions to old issues: I3318,I3319 - * @param {number} delta Use negative values if characters were deleted, positive if characters were added. - */ - protected adjustDeadkeys(delta: number) { - this.deadkeys().adjustPositions(this.getDeadkeyCaret(), delta); - } + /** + * Should be called by each output target immediately before text mutation operations occur. + * + * Maintains solutions to old issues: I3318,I3319 + * @param {number} delta Use negative values if characters were deleted, positive if characters were added. + */ + protected adjustDeadkeys(delta: number) { + this.deadkeys().adjustPositions(this.getDeadkeyCaret(), delta); + } - /** - * Needed to properly clone deadkeys for use with Mock element interfaces toward predictive text purposes. - * @param {object} dks An existing set of deadkeys to deep-copy for use by this element interface. - */ - protected setDeadkeys(dks: text.DeadkeyTracker) { - this._dks = dks.clone(); - } + /** + * Needed to properly clone deadkeys for use with Mock element interfaces toward predictive text purposes. + * @param {object} dks An existing set of deadkeys to deep-copy for use by this element interface. + */ + protected setDeadkeys(dks: DeadkeyTracker) { + this._dks = dks.clone(); + } - /** - * Determines the basic operations needed to reconstruct the current OutputTarget's text from the prior state specified - * by another OutputTarget based on their text and caret positions. - * - * This is designed for use as a "before and after" comparison to determine the effect of a single keyboard rule at a time. - * As such, it assumes that the caret is immediately after any inserted text. - * @param from An output target (preferably a Mock) representing the prior state of the input/output system. - */ - buildTransformFrom(original: OutputTarget): Transform { - let to = this.getText(); - let from = original.getText(); - - let fromCaret = original.getDeadkeyCaret(); - let toCaret = this.getDeadkeyCaret(); - - // Step 1: Determine the number of left-deletions. - let maxSMPLeftMatch = fromCaret < toCaret ? fromCaret : toCaret; - - // We need the corresponding non-SMP caret location in order to binary-search efficiently. - // (Examining code units is much more computationally efficient.) - let maxLeftMatch = to._kmwCodePointToCodeUnit(maxSMPLeftMatch); - - // 1.1: use a non-SMP-aware binary search to determine the divergence point. - let start = 0; - let end = maxLeftMatch; // the index AFTER the last possible matching char. - - // This search is O(maxLeftMatch). 1/2 + 1/4 + 1/8 + ... converges to = 1. - while(start < end) { - let mid = Math.floor((end+start+1) / 2); // round up (compare more) - let fromLeft = from.substr(start, mid-start); - let toLeft = to.substr(start, mid-start); - - if(fromLeft == toLeft) { - start = mid; - } else { - end = mid - 1; - } + /** + * Determines the basic operations needed to reconstruct the current OutputTarget's text from the prior state specified + * by another OutputTarget based on their text and caret positions. + * + * This is designed for use as a "before and after" comparison to determine the effect of a single keyboard rule at a time. + * As such, it assumes that the caret is immediately after any inserted text. + * @param from An output target (preferably a Mock) representing the prior state of the input/output system. + */ + buildTransformFrom(original: OutputTarget): Transform { + let to = this.getText(); + let from = original.getText(); + + let fromCaret = original.getDeadkeyCaret(); + let toCaret = this.getDeadkeyCaret(); + + // Step 1: Determine the number of left-deletions. + let maxSMPLeftMatch = fromCaret < toCaret ? fromCaret : toCaret; + + // We need the corresponding non-SMP caret location in order to binary-search efficiently. + // (Examining code units is much more computationally efficient.) + let maxLeftMatch = to._kmwCodePointToCodeUnit(maxSMPLeftMatch); + + // 1.1: use a non-SMP-aware binary search to determine the divergence point. + let start = 0; + let end = maxLeftMatch; // the index AFTER the last possible matching char. + + // This search is O(maxLeftMatch). 1/2 + 1/4 + 1/8 + ... converges to = 1. + while(start < end) { + let mid = Math.floor((end+start+1) / 2); // round up (compare more) + let fromLeft = from.substr(start, mid-start); + let toLeft = to.substr(start, mid-start); + + if(fromLeft == toLeft) { + start = mid; + } else { + end = mid - 1; } + } - // At the loop's end: `end` now holds the non-SMP-aware divergence point. - // The 'caret' is after the last matching code unit. - - // 1.2: detect a possible surrogate-pair split scenario, correcting for it - // (by moving the split before the high-surrogate) if detected. - - // If the split location is precisely on either end of the context, we can't - // have split a surrogate pair. - if(end > 0 && end < maxLeftMatch) { - let potentialHigh = from.charCodeAt(end-1); - let potentialFromLow = from.charCodeAt(end); - let potentialToLow = to.charCodeAt(end); - - // if potentialHigh is a possible high surrogate... - if(potentialHigh >= 0xD800 && potentialHigh <= 0xDBFF) { - // and at least one potential 'low' is a possible low surrogate... - let flag = potentialFromLow >= 0xDC00 && potentialFromLow <= 0xDFFF; - flag = flag || (potentialToLow >= 0XDC00 && potentialToLow <= 0xDFFF); - - // Correct the split location, moving it 'before' the high surrogate. - if(flag) { - end = end - 1; - } - } - } + // At the loop's end: `end` now holds the non-SMP-aware divergence point. + // The 'caret' is after the last matching code unit. - // 1.3: take substring from start to the split point; determine SMP-aware length. - // This yields the SMP-aware divergence index, which gives the number of left-deletes. - let newCaret = from._kmwCodeUnitToCodePoint(end); - let deletedLeft = fromCaret - newCaret; - - // Step 2: Determine the other properties. - // Since the 'after' OutputTarget's caret indicates the end of any inserted text, we - // can easily calculate the rest. - let insertedLength = toCaret - newCaret; - let delta = to._kmwSubstr(newCaret, insertedLength); - - let undeletedRight = to._kmwLength() - toCaret; - let originalRight = from._kmwLength() - fromCaret; - let deletedRight = originalRight - undeletedRight; - - // May occur when reverting a suggestion that had been applied mid-word. - if(deletedRight < 0) { - // Restores deleteRight characters. - delta = delta + to._kmwSubstr(toCaret, -deletedRight); - deletedRight = 0; - } + // 1.2: detect a possible surrogate-pair split scenario, correcting for it + // (by moving the split before the high-surrogate) if detected. + + // If the split location is precisely on either end of the context, we can't + // have split a surrogate pair. + if(end > 0 && end < maxLeftMatch) { + let potentialHigh = from.charCodeAt(end-1); + let potentialFromLow = from.charCodeAt(end); + let potentialToLow = to.charCodeAt(end); - return new TextTransform(delta, deletedLeft, deletedRight); + // if potentialHigh is a possible high surrogate... + if(potentialHigh >= 0xD800 && potentialHigh <= 0xDBFF) { + // and at least one potential 'low' is a possible low surrogate... + let flag = potentialFromLow >= 0xDC00 && potentialFromLow <= 0xDFFF; + flag = flag || (potentialToLow >= 0XDC00 && potentialToLow <= 0xDFFF); + + // Correct the split location, moving it 'before' the high surrogate. + if(flag) { + end = end - 1; + } + } } - buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription { - let transform = this.buildTransformFrom(original); + // 1.3: take substring from start to the split point; determine SMP-aware length. + // This yields the SMP-aware divergence index, which gives the number of left-deletes. + let newCaret = from._kmwCodeUnitToCodePoint(end); + let deletedLeft = fromCaret - newCaret; - // If we ever decide to re-add deadkey tracking, this is the place for it. + // Step 2: Determine the other properties. + // Since the 'after' OutputTarget's caret indicates the end of any inserted text, we + // can easily calculate the rest. + let insertedLength = toCaret - newCaret; + let delta = to._kmwSubstr(newCaret, insertedLength); - return new Transcription(keyEvent, transform, Mock.from(original, readonly), alternates); - } + let undeletedRight = to._kmwLength() - toCaret; + let originalRight = from._kmwLength() - fromCaret; + let deletedRight = originalRight - undeletedRight; - /** - * Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`. - * @param original An `OutputTarget` (usually a `Mock`). - */ - restoreTo(original: OutputTarget) { - // - this.setTextBeforeCaret(original.getTextBeforeCaret()); - this.setTextAfterCaret(original.getTextAfterCaret()); - - // Also, restore the deadkeys! - this._dks = original._dks.clone(); + // May occur when reverting a suggestion that had been applied mid-word. + if(deletedRight < 0) { + // Restores deleteRight characters. + delta = delta + to._kmwSubstr(toCaret, -deletedRight); + deletedRight = 0; } - apply(transform: Transform) { - if(transform.deleteRight) { - this.setTextAfterCaret(this.getTextAfterCaret()._kmwSubstr(transform.deleteRight)); - } + return new TextTransform(delta, deletedLeft, deletedRight); + } - if(transform.deleteLeft) { - this.deleteCharsBeforeCaret(transform.deleteLeft); - } + buildTranscriptionFrom(original: OutputTarget, keyEvent: KeyEvent, readonly: boolean, alternates?: Alternate[]): Transcription { + let transform = this.buildTransformFrom(original); - if(transform.insert) { - this.insertTextBeforeCaret(transform.insert); - } + // If we ever decide to re-add deadkey tracking, this is the place for it. - // We assume that all deadkeys are invalidated after applying a Transform, since - // prediction implies we'll be completing a word, post-deadkeys. - this._dks.clear(); - } + return new Transcription(keyEvent, transform, Mock.from(original, readonly), alternates); + } + + /** + * Restores the `OutputTarget` to the indicated state. Designed for use with `Transcription.preInput`. + * @param original An `OutputTarget` (usually a `Mock`). + */ + restoreTo(original: OutputTarget) { + // + this.setTextBeforeCaret(original.getTextBeforeCaret()); + this.setTextAfterCaret(original.getTextAfterCaret()); + + // Also, restore the deadkeys! + this._dks = original._dks.clone(); + } - /** - * Helper to `restoreTo` - allows directly setting the 'before' context to that of another - * `OutputTarget`. - * @param s - */ - protected setTextBeforeCaret(s: string): void { - // This one's easy enough to provide a default implementation for. - this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength()); - this.insertTextBeforeCaret(s); + apply(transform: Transform) { + if(transform.deleteRight) { + this.setTextAfterCaret(this.getTextAfterCaret()._kmwSubstr(transform.deleteRight)); } - /** - * Helper to `restoreTo` - allows directly setting the 'after' context to that of another - * `OutputTarget`. - * @param s - */ - protected abstract setTextAfterCaret(s: string): void; - - /** - * Clears any selected text within the wrapper's element(s). - * Silently does nothing if no such text exists. - */ - abstract clearSelection(): void; - - /** - * Clears any cached selection-related state values. - */ - abstract invalidateSelection(): void; - - /** - * Indicates whether or not the underlying element has its own selection (input, textarea) - * or is part of (or possesses) the DOM's active selection. Don't confuse with isSelectionEmpty(). - * - * TODO: rename to supportsOwnSelection - */ - abstract hasSelection(): boolean; - - /** - * Returns true if there is no current selection -- that is, the selection range is empty - */ - abstract isSelectionEmpty(): boolean; - - /** - * Returns an index corresponding to the caret's position for use with deadkeys. - */ - abstract getDeadkeyCaret(): number; - - /** - * Relative to the caret, gets the current context within the wrapper's element. - */ - abstract getTextBeforeCaret(): string; - - /** - * Relative to the caret (and/or active selection), gets the element's text after the caret, - * excluding any actively selected text that would be immediately replaced upon text entry. - */ - abstract getTextAfterCaret(): string; - - /** - * Gets the element's full text, including any text that is actively selected. - */ - abstract getText(): string; - - /** - * Performs context deletions (from the left of the caret) as needed by the KeymanWeb engine and - * corrects the location of any affected deadkeys. - * - * Does not delete deadkeys (b/c KMW 1 & 2 behavior maintenance). - * @param dn The number of characters to delete. If negative, context will be left unchanged. - */ - abstract deleteCharsBeforeCaret(dn: number): void; - - /** - * Inserts text immediately before the caret's current position, moving the caret after the - * newly inserted text in the process along with any affected deadkeys. - * - * @param s Text to insert before the caret's current position. - */ - abstract insertTextBeforeCaret(s: string): void; - - /** - * Allows element-specific handling for ENTER key inputs. Conceptually, this should usually - * correspond to `insertTextBeforeCaret('\n'), but actual implementation will vary greatly among - * elements. - */ - abstract handleNewlineAtCaret(): void; - - /** - * Saves element-specific state properties prone to mutation, enabling restoration after - * text-output operations. - */ - saveProperties() { - // Most element interfaces won't need anything here. + if(transform.deleteLeft) { + this.deleteCharsBeforeCaret(transform.deleteLeft); } - /** - * Restores previously-saved element-specific state properties. Designed for use after text-output - * ops to facilitate more-seamless web-dev and user interactions. - */ - restoreProperties(){ - // Most element interfaces won't need anything here. + if(transform.insert) { + this.insertTextBeforeCaret(transform.insert); } - /** - * Generates a synthetic event on the underlying element, signalling that its value has changed. - */ - abstract doInputEvent(): void; + // We assume that all deadkeys are invalidated after applying a Transform, since + // prediction implies we'll be completing a word, post-deadkeys. + this._dks.clear(); } - // Due to some interesting requirements on compile ordering in TS, - // this needs to be in the same file as OutputTarget now. - export class Mock extends OutputTarget { - text: string; - caretIndex: number; - - constructor(text?: string, caretPos?: number) { - super(); + /** + * Helper to `restoreTo` - allows directly setting the 'before' context to that of another + * `OutputTarget`. + * @param s + */ + protected setTextBeforeCaret(s: string): void { + // This one's easy enough to provide a default implementation for. + this.deleteCharsBeforeCaret(this.getTextBeforeCaret()._kmwLength()); + this.insertTextBeforeCaret(s); + } - this.text = text ? text : ""; - var defaultLength = this.text._kmwLength(); - // Ensures that `caretPos == 0` is handled correctly. - this.caretIndex = typeof caretPos == "number" ? caretPos : defaultLength; - } + /** + * Helper to `restoreTo` - allows directly setting the 'after' context to that of another + * `OutputTarget`. + * @param s + */ + protected abstract setTextAfterCaret(s: string): void; + + /** + * Clears any selected text within the wrapper's element(s). + * Silently does nothing if no such text exists. + */ + abstract clearSelection(): void; + + /** + * Clears any cached selection-related state values. + */ + abstract invalidateSelection(): void; + + /** + * Indicates whether or not the underlying element has its own selection (input, textarea) + * or is part of (or possesses) the DOM's active selection. Don't confuse with isSelectionEmpty(). + * + * TODO: rename to supportsOwnSelection + */ + abstract hasSelection(): boolean; + + /** + * Returns true if there is no current selection -- that is, the selection range is empty + */ + abstract isSelectionEmpty(): boolean; + + /** + * Returns an index corresponding to the caret's position for use with deadkeys. + */ + abstract getDeadkeyCaret(): number; + + /** + * Relative to the caret, gets the current context within the wrapper's element. + */ + abstract getTextBeforeCaret(): string; + + /** + * Gets the element's-currently selected text. + */ + abstract getSelectedText(): string; + + /** + * Relative to the caret (and/or active selection), gets the element's text after the caret, + * excluding any actively selected text that would be immediately replaced upon text entry. + */ + abstract getTextAfterCaret(): string; + + /** + * Gets the element's full text, including any text that is actively selected. + */ + abstract getText(): string; + + /** + * Performs context deletions (from the left of the caret) as needed by the KeymanWeb engine and + * corrects the location of any affected deadkeys. + * + * Does not delete deadkeys (b/c KMW 1 & 2 behavior maintenance). + * @param dn The number of characters to delete. If negative, context will be left unchanged. + */ + abstract deleteCharsBeforeCaret(dn: number): void; + + /** + * Inserts text immediately before the caret's current position, moving the caret after the + * newly inserted text in the process along with any affected deadkeys. + * + * @param s Text to insert before the caret's current position. + */ + abstract insertTextBeforeCaret(s: string): void; + + /** + * Allows element-specific handling for ENTER key inputs. Conceptually, this should usually + * correspond to `insertTextBeforeCaret('\n'), but actual implementation will vary greatly among + * elements. + */ + abstract handleNewlineAtCaret(): void; + + /** + * Saves element-specific state properties prone to mutation, enabling restoration after + * text-output operations. + */ + saveProperties() { + // Most element interfaces won't need anything here. + } - // Clones the state of an existing EditableElement, creating a Mock version of its state. - static from(outputTarget: OutputTarget, readonly: boolean) { - let clone: Mock; + /** + * Restores previously-saved element-specific state properties. Designed for use after text-output + * ops to facilitate more-seamless web-dev and user interactions. + */ + restoreProperties(){ + // Most element interfaces won't need anything here. + } - if(outputTarget instanceof Mock) { - // Avoids the need to run expensive kmwstring.ts / `_kmwLength()` - // calculations when deep-copying Mock instances. - let priorMock = outputTarget as Mock; - clone = new Mock(priorMock.text, priorMock.caretIndex); - } else { - // If we're 'cloning' a different OutputTarget type, we don't have a - // guaranteed way to more efficiently get these values; these are the - // best methods specified by the abstraction. - - if(readonly) { - // for NewContext and PostOutput, we want the whole text - let text = outputTarget.getText(); - let afterText = outputTarget.getTextAfterCaret(); - let caretIndex = text._kmwLength() - afterText._kmwLength(); - clone = new Mock(text, caretIndex); - } else { - // We choose to ignore (rather, pre-emptively remove) any actively-selected text, - // as since it's always removed instantly during any text mutation operations. - let preText = outputTarget.getTextBeforeCaret(); - let caretIndex = preText._kmwLength(); - clone = new Mock(preText + outputTarget.getTextAfterCaret(), caretIndex); - } - } + /** + * Generates a synthetic event on the underlying element, signalling that its value has changed. + */ + abstract doInputEvent(): void; +} - // Also duplicate deadkey state! (Needed for fat-finger ops.) - clone.setDeadkeys(outputTarget.deadkeys()); +// Due to some interesting requirements on compile ordering in TS, +// this needs to be in the same file as OutputTarget now. +export class Mock extends OutputTarget { + text: string; - return clone; - } + selStart: number; + selEnd: number; + selForward: boolean = true; - clearSelection(): void { - return; - } + constructor(text?: string, caretPos?: number); + constructor(text?: string, selStart?: number, selEnd?: number); + constructor(text?: string, selStart?: number, selEnd?: number) { + super(); - invalidateSelection(): void { - return; - } + this.text = text ? text : ""; + var defaultLength = this.text._kmwLength(); - isSelectionEmpty(): boolean { - // TODO: consider if we need to maintain selection information in Mocks - return true; - } + // Ensures that `caretPos == 0` is handled correctly. + this.selStart = typeof selStart == "number" ? selStart : defaultLength; - hasSelection(): boolean { - return true; - } + // If no selection-end is set, selection length is implied to be 0. + this.selEnd = typeof selEnd == "number" ? selEnd : this.selStart; - getDeadkeyCaret(): number { - return this.caretIndex; - } + this.selForward = this.selEnd >= this.selStart; + } - setDeadkeyCaret(index: number) { - if(index < 0 || index > this.text._kmwLength()) { - throw new Error("Provided caret index is out of range."); + // Clones the state of an existing EditableElement, creating a Mock version of its state. + static from(outputTarget: OutputTarget, readonly?: boolean) { + let clone: Mock; + + if(outputTarget instanceof Mock) { + // Avoids the need to run expensive kmwstring.ts / `_kmwLength()` + // calculations when deep-copying Mock instances. + let priorMock = outputTarget as Mock; + clone = new Mock(priorMock.text, priorMock.selStart, priorMock.selEnd); + } else { + const text = outputTarget.getText(); + const textLen = text._kmwLength(); + + // If !hasSelection() + let selectionStart: number = textLen; + let selectionEnd: number = 0; + + if(outputTarget.hasSelection()) { + let beforeText = outputTarget.getTextBeforeCaret(); + let afterText = outputTarget.getTextAfterCaret(); + selectionStart = beforeText._kmwLength(); + selectionEnd = textLen - afterText._kmwLength(); } - this.caretIndex = index; - } - getTextBeforeCaret(): string { - return this.text.kmwSubstr(0, this.caretIndex); + // readonly group or not, the returned Mock remains the same. + // New-context events should act as if the caret were at the earlier-in-context + // side of the selection, same as standard keyboard rules. + clone = new Mock(text, selectionStart, selectionEnd); } - getTextAfterCaret(): string { - return this.text.kmwSubstr(this.caretIndex); - } + // Also duplicate deadkey state! (Needed for fat-finger ops.) + clone.setDeadkeys(outputTarget.deadkeys()); + + return clone; + } + + clearSelection(): void { + this.text = this.getTextBeforeCaret() + this.getTextAfterCaret(); + this.selEnd = this.selStart; + this.selForward = true; + } + + invalidateSelection(): void { + return; + } + + isSelectionEmpty(): boolean { + return this.selStart == this.selEnd; + } - getText(): string { - return this.text; + hasSelection(): boolean { + return true; + } + + getDeadkeyCaret(): number { + return this.selStart; + } + + setSelection(start: number, end?: number) { + this.selStart = start; + this.selEnd = typeof end == 'number' ? end : start; + + this.selForward = end >= start; + if(!this.selForward) { + let temp = this.selStart; + this.selStart = this.selEnd; + this.selEnd = temp; } + } - deleteCharsBeforeCaret(dn: number): void { - if(dn >= 0) { - if(dn > this.caretIndex) { - dn = this.caretIndex; - } - this.adjustDeadkeys(-dn); - this.text = this.text.kmwSubstr(0, this.caretIndex - dn) + this.getTextAfterCaret(); - this.caretIndex -= dn; + getTextBeforeCaret(): string { + return this.text.kmwSubstr(0, this.selStart); + } + + getSelectedText(): string { + return this.text.kmwSubstr(this.selStart, this.selEnd - this.selStart); + } + + getTextAfterCaret(): string { + return this.text.kmwSubstr(this.selEnd); + } + + getText(): string { + return this.text; + } + + deleteCharsBeforeCaret(dn: number): void { + if(dn >= 0) { + if(dn > this.selStart) { + dn = this.selStart; } + this.adjustDeadkeys(-dn); + this.text = this.text.kmwSubstr(0, this.selStart - dn) + this.text.kmwSubstr(this.selStart); + this.selStart -= dn; + this.selEnd -= dn; } + } - insertTextBeforeCaret(s: string): void { - this.adjustDeadkeys(s._kmwLength()); - this.text = this.getTextBeforeCaret() + s + this.getTextAfterCaret(); - this.caretIndex += s.kmwLength(); - } + insertTextBeforeCaret(s: string): void { + this.adjustDeadkeys(s._kmwLength()); + this.text = this.getTextBeforeCaret() + s + this.text.kmwSubstr(this.selStart); + this.selStart += s.kmwLength(); + this.selEnd += s.kmwLength(); + } - handleNewlineAtCaret(): void { - this.insertTextBeforeCaret('\n'); - } + handleNewlineAtCaret(): void { + this.insertTextBeforeCaret('\n'); + } - protected setTextAfterCaret(s: string): void { - this.text = this.getTextBeforeCaret() + s; - } + protected setTextAfterCaret(s: string): void { + this.text = this.getTextBeforeCaret() + s; + } - doInputEvent() { - // Mock isn't backed by an element, so it won't have any event listeners. - } + doInputEvent() { + // Mock isn't backed by an element, so it won't have any event listeners. } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/ruleBehavior.ts b/common/web/keyboard-processor/src/text/ruleBehavior.ts index e1bc700cf1bec75185142591a875dfa1d289fb8f..e5d669e9092dd862154524ba00baee3ae59d5682 100644 --- a/common/web/keyboard-processor/src/text/ruleBehavior.ts +++ b/common/web/keyboard-processor/src/text/ruleBehavior.ts @@ -1,133 +1,138 @@ -namespace com.keyman.text { +/// + +import KeyboardProcessor from "./keyboardProcessor.js"; +import OutputTarget, { Mock, type Transcription } from "./outputTarget.js"; +import { VariableStoreDictionary } from "../keyboards/keyboard.js"; +import type { VariableStore } from "./kbdInterface.js"; + +/** + * Represents the commands and state changes that result from a matched keyboard rule. + */ +export default class RuleBehavior { /** - * Represents the commands and state changes that result from a matched keyboard rule. + * The before-and-after Transform from matching a keyboard rule. May be `null` + * if no keyboard rules were matched for the keystroke. */ - export class RuleBehavior { - /** - * The before-and-after Transform from matching a keyboard rule. May be `null` - * if no keyboard rules were matched for the keystroke. - */ - transcription: Transcription = null; - - /** - * Indicates whether or not a BEEP command was issued by the matched keyboard rule. - */ - beep?: boolean; - - /** - * A set of changed store values triggered by the matched keyboard rule. - */ - setStore: {[id: number]: string} = {}; - - /** - * A set of variable stores with save requests triggered by the matched keyboard rule - */ - saveStore: {[name: string]: VariableStore} = {}; - - /** - * A set of variable stores with possible changes to be applied during finalization. - */ - variableStores: keyboards.VariableStoreDictionary = {}; - - /** - * Denotes a non-output default behavior; this should be evaluated later, against the true keystroke. - */ - triggersDefaultCommand: boolean = false; - - /** - * Denotes error log messages generated when attempting to generate this behavior. - */ - errorLog?: string; - - /** - * Denotes warning log messages generated when attempting to generate this behavior. - */ - warningLog?: string; - - /** - * If predictive text is active, contains a Promise returning predictive Suggestions. - */ - predictionPromise?: Promise; - - /** - * In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852: - * - * If the final group processed is a context and keystroke group (using keys), - * and there is no nomatch rule, and the keystroke is not matched in the group, - * the keystroke's default behavior should trigger, regardless of whether or not any - * rules in prior groups matched. - */ - triggerKeyDefault?: boolean; - - finalize(processor: KeyboardProcessor, outputTarget: OutputTarget, readonly: boolean) { - if(!this.transcription) { - throw "Cannot finalize a RuleBehavior with no transcription."; - } + transcription: Transcription = null; - if(processor.beepHandler && this.beep) { - processor.beepHandler(outputTarget); - } + /** + * Indicates whether or not a BEEP command was issued by the matched keyboard rule. + */ + beep?: boolean; + + /** + * A set of changed store values triggered by the matched keyboard rule. + */ + setStore: {[id: number]: string} = {}; + + /** + * A set of variable stores with save requests triggered by the matched keyboard rule + */ + saveStore: {[name: string]: VariableStore} = {}; + + /** + * A set of variable stores with possible changes to be applied during finalization. + */ + variableStores: VariableStoreDictionary = {}; + + /** + * Denotes a non-output default behavior; this should be evaluated later, against the true keystroke. + */ + triggersDefaultCommand: boolean = false; + + /** + * Denotes error log messages generated when attempting to generate this behavior. + */ + errorLog?: string; + + /** + * Denotes warning log messages generated when attempting to generate this behavior. + */ + warningLog?: string; + + /** + * If predictive text is active, contains a Promise returning predictive Suggestions. + */ + predictionPromise?: Promise; + + /** + * In reference to https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852: + * + * If the final group processed is a context and keystroke group (using keys), + * and there is no nomatch rule, and the keystroke is not matched in the group, + * the keystroke's default behavior should trigger, regardless of whether or not any + * rules in prior groups matched. + */ + triggerKeyDefault?: boolean; + + finalize(processor: KeyboardProcessor, outputTarget: OutputTarget, readonly: boolean) { + if(!this.transcription) { + throw "Cannot finalize a RuleBehavior with no transcription."; + } - for(let storeID in this.setStore) { - let sysStore = processor.keyboardInterface.systemStores[storeID]; - if(sysStore) { - try { - sysStore.set(this.setStore[storeID]); - } catch (error) { - if(processor.errorLogger) { - processor.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed."); - } + if(processor.beepHandler && this.beep) { + processor.beepHandler(outputTarget); + } + + for(let storeID in this.setStore) { + let sysStore = processor.keyboardInterface.systemStores[storeID]; + if(sysStore) { + try { + sysStore.set(this.setStore[storeID]); + } catch (error) { + if(processor.errorLogger) { + processor.errorLogger("Rule attempted to perform illegal operation - 'platform' may not be changed."); } - } else if(processor.warningLogger) { - processor.warningLogger("Unknown store affected by keyboard rule: " + storeID); } + } else if(processor.warningLogger) { + processor.warningLogger("Unknown store affected by keyboard rule: " + storeID); } + } - processor.keyboardInterface.applyVariableStores(this.variableStores); + processor.keyboardInterface.applyVariableStores(this.variableStores); - if(processor.keyboardInterface.variableStoreSerializer) { - for(let storeID in this.saveStore) { - processor.keyboardInterface.variableStoreSerializer.saveStore(processor.activeKeyboard.id, storeID, this.saveStore[storeID]); - } + if(processor.keyboardInterface.variableStoreSerializer) { + for(let storeID in this.saveStore) { + processor.keyboardInterface.variableStoreSerializer.saveStore(processor.activeKeyboard.id, storeID, this.saveStore[storeID]); } + } - if(this.triggersDefaultCommand) { - let keyEvent = this.transcription.keystroke; - DefaultOutput.applyCommand(keyEvent, outputTarget); - } + if(this.triggersDefaultCommand) { + let keyEvent = this.transcription.keystroke; + processor.defaultRules.applyCommand(keyEvent, outputTarget); + } - if(processor.warningLogger && this.warningLog) { - processor.warningLogger(this.warningLog); - } else if(processor.errorLogger && this.errorLog) { - processor.errorLogger(this.errorLog); - } + if(processor.warningLogger && this.warningLog) { + processor.warningLogger(this.warningLog); + } else if(processor.errorLogger && this.errorLog) { + processor.errorLogger(this.errorLog); } + } - /** - * Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance - * "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke. - * - * Intended use: merging rule-based behavior with default key behavior during scenarios like those described - * at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852. - * - * This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things - * WILL break for unintended uses. - * @param other - */ - mergeInDefaults(other: RuleBehavior) { - let keystroke = this.transcription.keystroke; - let keyFromOther = other.transcription.keystroke; - if(keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) { - throw "RuleBehavior default-merge not supported unless keystrokes are identical!"; - } + /** + * Merges default-related behaviors from another RuleBehavior into this one. Assumes that the current instance + * "came first" chronologically. Both RuleBehaviors must be sourced from the same keystroke. + * + * Intended use: merging rule-based behavior with default key behavior during scenarios like those described + * at https://github.com/keymanapp/keyman/pull/4350#issuecomment-768753852. + * + * This function does not attempt a "complete" merge for two fully-constructed RuleBehaviors! Things + * WILL break for unintended uses. + * @param other + */ + mergeInDefaults(other: RuleBehavior) { + let keystroke = this.transcription.keystroke; + let keyFromOther = other.transcription.keystroke; + if(keystroke.Lcode != keyFromOther.Lcode || keystroke.Lmodifiers != keyFromOther.Lmodifiers) { + throw "RuleBehavior default-merge not supported unless keystrokes are identical!"; + } - this.triggersDefaultCommand = this.triggersDefaultCommand || other.triggersDefaultCommand; + this.triggersDefaultCommand = this.triggersDefaultCommand || other.triggersDefaultCommand; - let mergingMock = Mock.from(this.transcription.preInput, false); - mergingMock.apply(this.transcription.transform); - mergingMock.apply(other.transcription.transform); + let mergingMock = Mock.from(this.transcription.preInput, false); + mergingMock.apply(this.transcription.transform); + mergingMock.apply(other.transcription.transform); - this.transcription = mergingMock.buildTranscriptionFrom(this.transcription.preInput, keystroke, false, this.transcription.alternates); - } + this.transcription = mergingMock.buildTranscriptionFrom(this.transcription.preInput, keystroke, false, this.transcription.alternates); } } \ No newline at end of file diff --git a/common/web/keyboard-processor/src/text/systemStores.ts b/common/web/keyboard-processor/src/text/systemStores.ts index 00be331b72bc341a6c96d593b50aa4723abffb97..13184baafec2f65df3fec1525631bb42d660547c 100644 --- a/common/web/keyboard-processor/src/text/systemStores.ts +++ b/common/web/keyboard-processor/src/text/systemStores.ts @@ -1,133 +1,134 @@ -namespace com.keyman.text { - /** - * Defines common behaviors associated with system stores. - */ - export abstract class SystemStore { - public readonly id: number; - - constructor(id: number) { - this.id = id; - } +import type KeyboardInterface from "./kbdInterface.js"; +import { SystemStoreIDs } from "./kbdInterface.js"; - abstract matches(value: string): boolean; +/** + * Defines common behaviors associated with system stores. + */ +export abstract class SystemStore { + public readonly id: number; - set(value: string): void { - throw new Error("System store with ID " + this.id + " may not be directly set."); - } + constructor(id: number) { + this.id = id; } - /** - * A handler designed to receive feedback whenever a system store's value is changed. - * @param source The system store being mutated, before the value change occurs. - * @param newValue The new value being set - * @returns `false` / `undefined` to allow the change, `true` to block the change. - */ - export type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean; - - export class MutableSystemStore extends SystemStore { - private _value: string; - handler?: SystemStoreMutationHandler = null; - - constructor(id: number, defaultValue: string) { - super(id); - this._value = defaultValue; - } + abstract matches(value: string): boolean; - get value() { - return this._value; - } + set(value: string): void { + throw new Error("System store with ID " + this.id + " may not be directly set."); + } +} + +/** + * A handler designed to receive feedback whenever a system store's value is changed. + * @param source The system store being mutated, before the value change occurs. + * @param newValue The new value being set + * @returns `false` / `undefined` to allow the change, `true` to block the change. + */ +export type SystemStoreMutationHandler = (source: MutableSystemStore, newValue: string) => boolean; + +export class MutableSystemStore extends SystemStore { + private _value: string; + handler?: SystemStoreMutationHandler = null; + + constructor(id: number, defaultValue: string) { + super(id); + this._value = defaultValue; + } - matches(value: string) { - return this._value == value; - } + get value() { + return this._value; + } - set(value: string) { - // Even if things stay the same, we should still signal this. - // It's important for tracking if a rule directly set the layer - // versus if it passively remained. - if(this.handler) { - if(this.handler(this, value)) { - return; - } - } + matches(value: string) { + return this._value == value; + } - this._value = value; + set(value: string) { + // Even if things stay the same, we should still signal this. + // It's important for tracking if a rule directly set the layer + // versus if it passively remained. + if(this.handler) { + if(this.handler(this, value)) { + return; + } } + + this._value = value; } +} - /** - * Handles checks against the current platform. - */ - export class PlatformSystemStore extends SystemStore { - private readonly kbdInterface: KeyboardInterface; +/** + * Handles checks against the current platform. + */ +export class PlatformSystemStore extends SystemStore { + private readonly kbdInterface: KeyboardInterface; - constructor(keyboardInterface: KeyboardInterface) { - super(KeyboardInterface.TSS_PLATFORM); + constructor(keyboardInterface: KeyboardInterface) { + super(SystemStoreIDs.TSS_PLATFORM); - this.kbdInterface = keyboardInterface; - } + this.kbdInterface = keyboardInterface; + } - matches(value: string) { - var i,constraint,constraints=value.split(' '); - let device = this.kbdInterface.activeDevice; - - for(i=0; i { + if(window.KeymanWeb) { + window.KeymanWeb.uninstall(); + } + }) + + it('`window`, disabled rule processing', async () => { + const harness = new KeyboardHarness(window, MinimalKeymanGlobal); + let keyboardLoader = new DOMKeyboardLoader(harness); + let keyboard = await keyboardLoader.loadKeyboardFromPath('/resources/keyboards/khmer_angkor.js'); + + assert.isOk(keyboard); + assert.equal(keyboard.id, 'Keyboard_khmer_angkor'); + assert.isTrue(keyboard.isChiral); + assert.isFalse(keyboard.isCJK); + assert.isOk(window.KeymanWeb); + assert.isOk(window.keyman); + + // Should be not be modified by the keyboard load; it is not activated by default. + assert.isNotOk(harness.activeKeyboard); + + // Should be cleared post-keyboard-load. + assert.isNotOk(harness.loadedKeyboard); + }); + + it('`window`, enabled rule processing', async () => { + const harness = new KeyboardInterface(window, MinimalKeymanGlobal); + const keyboardLoader = new DOMKeyboardLoader(harness); + const keyboard = await keyboardLoader.loadKeyboardFromPath('/resources/keyboards/khmer_angkor.js'); + harness.activeKeyboard = keyboard; + + assert.isOk(keyboard); + assert.equal(keyboard.id, 'Keyboard_khmer_angkor'); + assert.isTrue(keyboard.isChiral); + assert.isFalse(keyboard.isCJK); + assert.isOk(window.KeymanWeb); + assert.isOk(window.keyman); + + // TODO: verify actual rule processing. + const nullKeyEvent = keyboard.constructNullKeyEvent(device); + const mock = new Mock(); + const result = harness.processKeystroke(mock, nullKeyEvent); + + assert.isOk(result); + assert.isOk(window.KeymanWeb); + assert.isOk(window.keyman); + + // Should be cleared post-keyboard-load. + assert.isNotOk(harness.loadedKeyboard); + }); + + it('load keyboards successfully in parallel without side effects', async () => { + let harness = new KeyboardInterface(window, MinimalKeymanGlobal); + let keyboardLoader = new DOMKeyboardLoader(harness); + + // Preload a keyboard and make it active. + const test_kbd = await keyboardLoader.loadKeyboardFromPath('/resources/keyboards/test_917.js'); + harness.activeKeyboard = test_kbd; + assert.isNotOk(harness.loadedKeyboard); + + // With an active keyboard, load three keyboards but activate none of them. + const lao_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/resources/keyboards/lao_2008_basic.js'); + const khmer_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/resources/keyboards/khmer_angkor.js'); + const chiral_keyboard_promise = keyboardLoader.loadKeyboardFromPath('/resources/keyboards/test_chirality.js'); + + // Sure, why not `await` out of order? + const chiral_keyboard = await chiral_keyboard_promise; + const lao_keyboard = await lao_keyboard_promise; + const khmer_keyboard = await khmer_keyboard_promise; + + assert.strictEqual(test_kbd, harness.activeKeyboard); + assert.isNotOk(harness.loadedKeyboard); + + assert.isOk(lao_keyboard); + assert.isOk(chiral_keyboard); + assert.isOk(khmer_keyboard); + + // This part provides assurance that the keyboard properly loaded. + assert.equal(lao_keyboard.id, "Keyboard_lao_2008_basic"); + assert.equal(khmer_keyboard.id, "Keyboard_khmer_angkor"); + assert.equal(chiral_keyboard.id, "Keyboard_test_chirality"); + + harness.activeKeyboard = lao_keyboard; + }); +}); \ No newline at end of file diff --git a/web/src/test/auto/manual.conf.js b/common/web/keyboard-processor/tests/dom/manual.conf.cjs similarity index 79% rename from web/src/test/auto/manual.conf.js rename to common/web/keyboard-processor/tests/dom/manual.conf.cjs index e9ba5c97a9bf851a57fdc06f0551a75792245d17..37d982e9b5a70c983c7b64cb766adb1f00b9adfd 100644 --- a/web/src/test/auto/manual.conf.js +++ b/common/web/keyboard-processor/tests/dom/manual.conf.cjs @@ -1,5 +1,5 @@ module.exports = function(config) { - var base = require("./base.conf.js"); + var base = require("./base.conf.cjs"); var specifics = { // test results reporter to use @@ -9,7 +9,7 @@ module.exports = function(config) { // start these browsers // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher - browsers: ['Firefox', 'Chrome', 'Edge'], // Can be specified at run-time instead! + browsers: ['Chrome'], // Can be specified at run-time instead! // Future note for us: https://www.npmjs.com/package/karma-browserstack-launcher // Concurrency level @@ -18,7 +18,7 @@ module.exports = function(config) { // level of logging // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG - // Can't set in base.conf.js b/c of constant's definition style. + // Can't set in base.conf.cjs b/c of constant's definition style. logLevel: config.LOG_INFO }; diff --git a/common/web/keyboard-processor/tests/dom/readme.md b/common/web/keyboard-processor/tests/dom/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..153b7f4a1e8bb278a6b3c74ec94740fca518a157 --- /dev/null +++ b/common/web/keyboard-processor/tests/dom/readme.md @@ -0,0 +1,5 @@ +Automated tests in this subfolder and its children are designed to facilitate simple, browser-independent +unit tests that are DOM-reliant. + +Tests for anything that may reasonably vary depending upon the browser used to run the code should go under +the "integrated" folder instead. \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/node/basic-engine.js b/common/web/keyboard-processor/tests/node/basic-engine.js new file mode 100644 index 0000000000000000000000000000000000000000..15fd1efc5d8449cd4e9101e19a8984674299ae72 --- /dev/null +++ b/common/web/keyboard-processor/tests/node/basic-engine.js @@ -0,0 +1,52 @@ +import { assert } from 'chai'; +import fs from 'fs'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { KeyboardInterface, MinimalKeymanGlobal } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; +import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core'; + +describe('Engine - Basic Simulation', function() { + let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/basic_lao_simulation.json')); + // Common test suite setup. + let testSuite = new KeyboardTest(JSON.parse(testJSONtext)); + + let keyboardWithHarness; + + let device = { + formFactor: 'desktop', + OS: 'windows', + browser: 'native' + } + + before(async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; + + assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id); + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // This part provides extra assurance that the keyboard properly loaded. + assert.equal(keyboard.id, "Keyboard_lao_2008_basic"); + }); + + // Converts each test set into its own Mocha-level test. + for(let set of testSuite.inputTestSets) { + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); + + if(!proctor.compatibleWithSuite(testSuite)) { + it.skip(set.toTestName() + " - Cannot run this test suite on Node."); + } else { + it(set.toTestName(), function() { + // Refresh the proctor instance at runtime. + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); + set.test(proctor); + }); + } + } +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/cases/basic-init.js b/common/web/keyboard-processor/tests/node/basic-init.js similarity index 63% rename from common/web/keyboard-processor/tests/cases/basic-init.js rename to common/web/keyboard-processor/tests/node/basic-init.js index 9fb576cd9058f651a7120ed6d74ff2f6ac88a55d..8f04a6a6e8b9a70ca8ddf6f40f0be87649a66206 100644 --- a/common/web/keyboard-processor/tests/cases/basic-init.js +++ b/common/web/keyboard-processor/tests/node/basic-init.js @@ -1,11 +1,11 @@ -var assert = require('chai').assert; -var fs = require("fs"); -var vm = require("vm"); +import { assert } from 'chai'; -let KeyboardProcessor = require('../../build/index.bundled.js'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { KeyboardProcessor } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. @@ -30,16 +30,16 @@ describe('KeyboardProcessor', function() { }); describe('activeKeyboard', function() { - it('is automatically set (in headless) on keyboard load', function () { + it('is automatically set (in headless) on keyboard load', async function () { let kp = new KeyboardProcessor(); // These two lines will load a keyboard from its file; headless-mode `registerKeyboard` will // automatically set the keyboard as active. - var script = new vm.Script(fs.readFileSync('../../test/resources/keyboards/khmer_angkor.js')); - script.runInThisContext(); + let keyboardLoader = new NodeKeyboardLoader(); + let keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js')); - assert.isDefined(kp.activeKeyboard, 'Keyboard failed to register on script load'); - assert.equal('Keyboard_khmer_angkor', kp.activeKeyboard.id, 'Unexpected keyboard id found after script load'); + assert.isDefined(keyboard, 'Keyboard failed to register on script load'); + assert.equal('Keyboard_khmer_angkor', keyboard.id, 'Unexpected keyboard id found after script load'); }); }); }); diff --git a/common/web/keyboard-processor/tests/node/bundled-module.js b/common/web/keyboard-processor/tests/node/bundled-module.js new file mode 100644 index 0000000000000000000000000000000000000000..bfff937b626bfbbc99b9e4332042f55671137aac --- /dev/null +++ b/common/web/keyboard-processor/tests/node/bundled-module.js @@ -0,0 +1,50 @@ +import { assert } from "chai"; +import * as Package from "../../build/lib/index.mjs"; + +// A few small tests to ensure that the ES Module bundle was successfully constructed and is usable. + +var toSupplementaryPairString = function(code){ + var H = Math.floor((code - 0x10000) / 0x400) + 0xD800; + var L = (code - 0x10000) % 0x400 + 0xDC00; + + return String.fromCharCode(H, L); +} + +let u = toSupplementaryPairString; + +describe('Bundled ES Module', function() { + describe('KeyboardProcessor', function () { + it('should initialize without errors', function () { + let kp = new Package.KeyboardProcessor(); + assert.isNotNull(kp); + }); + }); + + describe('Mock', () => { + it('basic functionality test', () => { + let target = new Package.Mock("aple", 2); // ap | le + target.insertTextBeforeCaret('p'); + assert.equal(target.getText(), "apple"); + }); + + it('smp test', () => { + // Is installed as a _side effect_ from importing the module. + // We could disable that and require a call of `extendString()` instead. + String.kmwEnableSupplementaryPlane(true); // Declared & defined in web-utils. + try { + let target = new Package.Mock(u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be), 2); // ap | le + target.insertTextBeforeCaret(u(0x1d5c9)); + assert.equal(target.getText(), u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be)); + } finally { + String.kmwEnableSupplementaryPlane(false); + } + }); + }); + + describe("Imported `utils`", function() { + it("should include `utils` package's Version class", () => { + let v16 = new Package.Version([16, 1]); + assert.equal(v16.toString(), "16.1"); + }); + }) +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/cases/chirality.js b/common/web/keyboard-processor/tests/node/chirality.js similarity index 87% rename from common/web/keyboard-processor/tests/cases/chirality.js rename to common/web/keyboard-processor/tests/node/chirality.js index 7d7436cf2c25a07f3007148a3d3d2bcdbc2ffee4..b7f13214d48fba09237a058440d3d6abde55f021 100644 --- a/common/web/keyboard-processor/tests/cases/chirality.js +++ b/common/web/keyboard-processor/tests/node/chirality.js @@ -1,38 +1,33 @@ -var assert = require('chai').assert; -let fs = require('fs'); -let vm = require('vm'); +import { assert } from 'chai'; +import fs from 'fs'; -let KeyboardProcessor = require('../../build/index.bundled.js'); -let KMWRecorder = require('../../../recorder/build/nodeProctor'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. -let KeyboardInterface = com.keyman.text.KeyboardInterface; -let Codes = com.keyman.text.Codes; +import { Codes, KeyboardInterface, MinimalKeymanGlobal } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; +import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core'; describe('Engine - Chirality', function() { - let testJSONtext = fs.readFileSync('../../test/resources/json/engine_tests/chirality.json'); + let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/chirality.json')); // Common test suite setup. - let testSuite = new KMWRecorder.KeyboardTest(JSON.parse(testJSONtext)); + let testSuite = new KeyboardTest(JSON.parse(testJSONtext)); + + let keyboardWithHarness; - var keyboard; let device = { formFactor: 'desktop', OS: 'windows', browser: 'native' } - before(function() { + before(async function() { // -- START: Standard Recorder-based unit test loading boilerplate -- - // Load the keyboard. We'll need a KeyboardProcessor instance as an intermediary. - let kp = new KeyboardProcessor(); - - // These two lines will load a keyboard from its file; headless-mode `registerKeyboard` will - // automatically set the keyboard as active. - var script = new vm.Script(fs.readFileSync('../../test/' + testSuite.keyboard.filename)); - script.runInThisContext(); + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; - keyboard = kp.activeKeyboard; assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id); // -- END: Standard Recorder-based unit test loading boilerplate -- @@ -42,14 +37,14 @@ describe('Engine - Chirality', function() { // Converts each test set into its own Mocha-level test. for(let set of testSuite.inputTestSets) { - let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal); + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); if(!proctor.compatibleWithSuite(testSuite)) { it.skip(set.toTestName() + " - Cannot run this test suite on Node."); } else if(set.constraint.target == 'hardware') { it(set.toTestName(), function() { // Refresh the proctor instance at runtime. - let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal); + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); set.test(proctor); }); } else { @@ -92,7 +87,7 @@ describe('Engine - Chirality', function() { // We should get the same results whether or not there actually is a corresponding modifier // expected by the rule we're examining. - mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE); + let mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE); assert.equal(targetModifiers, mappedModifiers); mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE | ALT_CODE); @@ -111,7 +106,7 @@ describe('Engine - Chirality', function() { // We should get the same results whether or not there actually is a corresponding modifier // expected by the rule we're examining. - mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE); + let mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE); assert.equal(targetModifiers, mappedModifiers); let ctrlPlusAlt = ALT_CODE | CTRL_CODE; @@ -250,7 +245,7 @@ describe('Engine - Chirality', function() { let modifierTarget = VIRTUAL_KEY_CODE | ALT_CODE | LCTRL_CODE | SHIFT_CODE; - mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE | LALT_CODE | RCTRL_CODE); + let mappedModifiers = KeyboardInterface.matchModifiersToRuleChirality(initialModifiers, VIRTUAL_KEY_CODE | LALT_CODE | RCTRL_CODE); assert.equal(modifierTarget, mappedModifiers); }); }); diff --git a/common/web/keyboard-processor/tests/node/deadkeys.js b/common/web/keyboard-processor/tests/node/deadkeys.js new file mode 100644 index 0000000000000000000000000000000000000000..c7e195bc2f7170fbf9709bbbe353c7e9641c862d --- /dev/null +++ b/common/web/keyboard-processor/tests/node/deadkeys.js @@ -0,0 +1,52 @@ +import { assert } from 'chai'; +import fs from 'fs'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { KeyboardInterface, MinimalKeymanGlobal } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; +import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core'; + +describe('Engine - Deadkeys', function() { + let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/deadkeys.json')); + // Common test suite setup. + let testSuite = new KeyboardTest(JSON.parse(testJSONtext)); + + let keyboardWithHarness; + + let device = { + formFactor: 'desktop', + OS: 'windows', + browser: 'native' + } + + before(async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + let keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; + + assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id); + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // This part provides extra assurance that the keyboard properly loaded. + assert.equal(keyboard.id, "Keyboard_test_deadkeys"); + }); + + // Converts each test set into its own Mocha-level test. + for(let set of testSuite.inputTestSets) { + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); + + if(!proctor.compatibleWithSuite(testSuite)) { + it.skip(set.toTestName() + " - Cannot run this test suite on Node."); + } else { + it(set.toTestName(), function() { + // Refresh the proctor instance at runtime. + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); + set.test(proctor); + }); + } + } +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/cases/engine/context.js b/common/web/keyboard-processor/tests/node/engine/context.js similarity index 97% rename from common/web/keyboard-processor/tests/cases/engine/context.js rename to common/web/keyboard-processor/tests/node/engine/context.js index 8b8fb34e9ee579633312321a077eeade82e5f42e..a1fe5863df99630d00bb13cb2236ce8b78bf8c41 100644 --- a/common/web/keyboard-processor/tests/cases/engine/context.js +++ b/common/web/keyboard-processor/tests/node/engine/context.js @@ -1,13 +1,12 @@ -var assert = require('chai').assert; -let fs = require('fs'); -let vm = require('vm'); +import { assert } from 'chai'; -let KeyboardProcessor = require('../../../build/index.bundled.js'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. +import { KeyboardInterface, KeyboardProcessor, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; -let KMWRecorder = require('../../../../recorder/build/nodeProctor'); +import { NodeProctor, RecordedKeystrokeSequence } from '@keymanapp/recorder-core'; /* * ABOUT THIS TEST SUITE @@ -36,7 +35,7 @@ let device = { browser: 'native' } -var keyboard; +let keyboardWithHarness; var toSupplementaryPairString = function(code){ var H = Math.floor((code - 0x10000) / 0x400) + 0xD800; @@ -60,16 +59,16 @@ function runEngineRuleSet(ruleSet, defaultNoun) { for(var j = 0; j < matchDefs.length; j++) { // Prepare the context! var matchTest = matchDefs[j]; - var ruleSeq = new KMWRecorder.RecordedKeystrokeSequence(matchTest.sequence); - let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal); + var ruleSeq = new RecordedKeystrokeSequence(matchTest.sequence); + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); // We want to specify the OutputTarget for this test; our actual concern is the resulting context. - var target = new com.keyman.text.Mock(); + var target = new Mock(); ruleSeq.test(proctor, target); // Now for the real test! let processor = new KeyboardProcessor(device); - processor.activeKeyboard = keyboard; + processor.keyboardInterface = keyboardWithHarness; var res = processor.keyboardInterface.fullContextMatch(ruleDef.n, target, ruleDef.rule); var msg = matchTest.msg; @@ -81,6 +80,8 @@ function runEngineRuleSet(ruleSet, defaultNoun) { } } +//#region Test Spec Definitions + // Unfortunately, at present, this is all handwritten stuff crafted from partial Recorder use. Might should make // a page that writes this format instead. Note that I've omitted the eventSpec part here, since these tests are // Node-only. @@ -980,16 +981,14 @@ var NOTANY_NUL_RULE_SET = [ NOTANY_NUL_TEST_1, NOTANY_NUL_TEST_2, NOTANY_NUL_TES // ----------- -describe('Engine - Context Matching', function() { - before(function() { - let kp = new KeyboardProcessor(device); - - // These two lines will load a keyboard from its file; headless-mode `registerKeyboard` will - // automatically set the keyboard as active. - var script = new vm.Script(fs.readFileSync('../../test/resources/keyboards/test_simple_deadkeys.js')); - script.runInThisContext(); +//#endregion - keyboard = kp.activeKeyboard; +describe('Engine - Context Matching', function() { + before(async function() { + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_simple_deadkeys.js')); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; }); // Tests "stage 1" of fullContextMatch - ensuring that a proper context index map is built. @@ -999,16 +998,16 @@ describe('Engine - Context Matching', function() { for(var j = 0; j < matchDefs.length; j++) { // Prepare the context! var ruleDef = matchDefs[j]; - var ruleSeq = new KMWRecorder.RecordedKeystrokeSequence(ruleDef.baseSequence); - let proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal); + var ruleSeq = new RecordedKeystrokeSequence(ruleDef.baseSequence); + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); // We want to specify the OutputTarget for this test; our actual concern is the resulting context. - var target = new com.keyman.text.Mock(); + var target = new Mock(); ruleSeq.test(proctor, target); // Now for the real test! let processor = new KeyboardProcessor(device); - processor.activeKeyboard = keyboard; + processor.keyboardInterface = keyboardWithHarness; var res = processor.keyboardInterface._BuildExtendedContext(ruleDef.n, ruleDef.ln, target); assert.sameOrderedMembers(res.valContext, ruleDef.contextCache); diff --git a/common/web/keyboard-processor/tests/cases/engine/notany_context.js b/common/web/keyboard-processor/tests/node/engine/notany_context.js similarity index 70% rename from common/web/keyboard-processor/tests/cases/engine/notany_context.js rename to common/web/keyboard-processor/tests/node/engine/notany_context.js index 2c0ba3585dd4f42c4b5f604c64035f92415968fc..2329463d465f26b137e3fd8ad3f6e08b6b00d5a9 100644 --- a/common/web/keyboard-processor/tests/cases/engine/notany_context.js +++ b/common/web/keyboard-processor/tests/node/engine/notany_context.js @@ -1,15 +1,14 @@ -const assert = require('chai').assert; -const fs = require('fs'); -const vm = require('vm'); +import { assert } from 'chai'; -let KeyboardProcessor = require('../../../build/index.bundled.js'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. -global.keyman = {}; // So that keyboard-based checks against the global `keyman` succeed. - // 10.0+ dependent keyboards, like khmer_angkor, will otherwise fail to load. +import { KeyboardInterface, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; +import { NodeProctor, RecordedKeystrokeSequence } from '@keymanapp/recorder-core'; +import { extendString } from '@keymanapp/web-utils'; -let KMWRecorder = require('../../../../recorder/build/nodeProctor'); +extendString(); // Ensure KMW's string-extension functionality is available. // Initialize supplementary plane string extensions String.kmwEnableSupplementaryPlane(false); @@ -20,14 +19,14 @@ const device = { browser: 'native' } -let keyboard; +let keyboardWithHarness; function runEngineRuleSet(ruleSet) { for(let ruleDef of ruleSet) { // Prepare the context! - const ruleSeq = new KMWRecorder.RecordedKeystrokeSequence(ruleDef); - const proctor = new KMWRecorder.NodeProctor(keyboard, device, assert.equal); - const target = new com.keyman.text.Mock(); + const ruleSeq = new RecordedKeystrokeSequence(ruleDef); + const proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); + const target = new Mock(); ruleSeq.test(proctor, target); } } @@ -55,15 +54,11 @@ function runStringRuleSet(input, output) { // ----------- describe('Engine - notany() and context()', function() { - before(function() { - const kp = new KeyboardProcessor(); - - // These two lines will load a keyboard from its file; headless-mode `registerKeyboard` will - // automatically set the keyboard as active. - const script = new vm.Script(fs.readFileSync('../../test/resources/keyboards/test_917.js')); - script.runInThisContext(); - - keyboard = kp.activeKeyboard; + before(async function() { + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + const keyboard = await keyboardLoader.loadKeyboardFromPath(require.resolve('@keymanapp/common-test-resources/keyboards/test_917.js')); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; }); /* diff --git a/common/web/keyboard-processor/tests/cases/engine/stores.js b/common/web/keyboard-processor/tests/node/engine/stores.js similarity index 84% rename from common/web/keyboard-processor/tests/cases/engine/stores.js rename to common/web/keyboard-processor/tests/node/engine/stores.js index 530ed0c9319a89d2874b8fa69558aaae3e628c98..ade1c2bd47112b9f8d04da30e5a4b693324791ea 100644 --- a/common/web/keyboard-processor/tests/cases/engine/stores.js +++ b/common/web/keyboard-processor/tests/node/engine/stores.js @@ -1,11 +1,9 @@ -var assert = require('chai').assert; -let fs = require('fs'); -let vm = require('vm'); +import { assert } from 'chai'; -let KeyboardProcessor = require('../../../build/index.bundled.js'); +import { Keyboard, KeyboardProcessor } from '@keymanapp/keyboard-processor'; +import { extendString } from '@keymanapp/web-utils'; -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. +extendString(); let device = { formFactor: 'desktop', @@ -25,7 +23,7 @@ describe('Engine - Stores', function() { let processor = new KeyboardProcessor(device); // A 'hollow' Keyboard that only follows default rules. That said, we need a Keyboard // instance to host cache data for our exploded store tests. - processor.activeKeyboard = new com.keyman.keyboards.Keyboard(); + processor.activeKeyboard = new Keyboard(); // Function defined at top of file; creates supplementary pairs for extended Unicode codepoints. var u = toSupplementaryPairString; diff --git a/common/web/keyboard-processor/tests/node/engine/unmatched_final_group.js b/common/web/keyboard-processor/tests/node/engine/unmatched_final_group.js new file mode 100644 index 0000000000000000000000000000000000000000..81f9765da4c38668f1271e0f7f652715ca4b75f1 --- /dev/null +++ b/common/web/keyboard-processor/tests/node/engine/unmatched_final_group.js @@ -0,0 +1,41 @@ +import { assert } from 'chai'; +import fs from 'fs'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { KeyboardInterface, MinimalKeymanGlobal } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; +import { KeyboardTest, NodeProctor } from '@keymanapp/recorder-core'; + +describe('Engine - Unmatched Final Groups', function() { + let testJSONtext = fs.readFileSync(require.resolve('@keymanapp/common-test-resources/json/engine_tests/ghp_enter.json')); + // Common test suite setup. + let testSuite = new KeyboardTest(JSON.parse(testJSONtext)); + + var keyboardWithHarness; + let device = { + formFactor: 'desktop', + OS: 'windows', + browser: 'native' + } + + before(async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let keyboardLoader = new NodeKeyboardLoader(new KeyboardInterface({}, MinimalKeymanGlobal)); + const keyboard = await keyboardLoader.loadKeyboardFromPath('../../test/' + testSuite.keyboard.filename); + keyboardWithHarness = keyboardLoader.harness; + keyboardWithHarness.activeKeyboard = keyboard; + + assert.equal(keyboard.id, "Keyboard_" + testSuite.keyboard.id); + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // This part provides extra assurance that the keyboard properly loaded. + assert.equal(keyboard.id, "Keyboard_galaxie_hebrew_positional"); + }); + + it('Emits default enter AND matches rule from early group', function() { + let proctor = new NodeProctor(keyboardWithHarness, device, assert.equal); + testSuite.test(proctor); + }); +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/node/keyboard-loading.js b/common/web/keyboard-processor/tests/node/keyboard-loading.js new file mode 100644 index 0000000000000000000000000000000000000000..4ae04351c0aacc88bb094bb7bd4dfc9bc4cb0447 --- /dev/null +++ b/common/web/keyboard-processor/tests/node/keyboard-loading.js @@ -0,0 +1,131 @@ +import { assert } from 'chai'; +import fs from 'fs'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { KeyboardHarness, KeyboardInterface, KeyboardLoaderBase, MinimalKeymanGlobal, Mock } from '@keymanapp/keyboard-processor'; +import { NodeKeyboardLoader } from '@keymanapp/keyboard-processor/node-keyboard-loader'; + +describe('Headless keyboard loading', function() { + const laoPath = require.resolve('@keymanapp/common-test-resources/keyboards/lao_2008_basic.js'); + const khmerPath = require.resolve('@keymanapp/common-test-resources/keyboards/khmer_angkor.js'); + const nonKeyboardPath = require.resolve('@keymanapp/common-test-resources/index.mjs'); + // Common test suite setup. + + let device = { + formFactor: 'desktop', + OS: 'windows', + browser: 'native' + } + + describe('Minimal harness loading', () => { + it('successfully loads', async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let harness = new KeyboardHarness({}, MinimalKeymanGlobal); + let keyboardLoader = new NodeKeyboardLoader(harness); + let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // Asserts that the harness's loading field is cleared once the load is complete. + assert.isNotOk(harness.loadedKeyboard); + + // Asserts that the `activeKeyboard` field was not set by the operation. + assert.isNotOk(harness.activeKeyboard); + + // This part provides assurance that the keyboard properly loaded. + assert.equal(keyboard.id, "Keyboard_lao_2008_basic"); + }); + + it('cannot evaluate rules', async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let harness = new KeyboardHarness({}, MinimalKeymanGlobal); + let keyboardLoader = new NodeKeyboardLoader(harness); + let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // Runs a blank KeyEvent through the keyboard's rule processing... + // but via separate harness configured with a different captured global. + let ruleHarness = new KeyboardInterface({}, MinimalKeymanGlobal); + ruleHarness.activeKeyboard = keyboard; + try { + ruleHarness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device)); + assert.fail(); + } catch (err) { + // Drives home an important detail: the 'global' object is effectively + // closure-captured. (Similar constraints may occur when experimenting with + // 'sandboxed' keyboard loading in the DOM!) + assert.equal(err.message, 'k.KKM is not a function'); + } + }); + }); + + describe('Full harness loading', () => { + it('successfully loads', async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let harness = new KeyboardInterface({}, MinimalKeymanGlobal); + let keyboardLoader = new NodeKeyboardLoader(harness); + let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); + harness.activeKeyboard = keyboard; + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // This part provides assurance that the keyboard properly loaded. + assert.equal(keyboard.id, "Keyboard_lao_2008_basic"); + }); + + it('can evaluate rules', async function() { + // -- START: Standard Recorder-based unit test loading boilerplate -- + let harness = new KeyboardInterface({}, MinimalKeymanGlobal); + let keyboardLoader = new NodeKeyboardLoader(harness); + let keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); + harness.activeKeyboard = keyboard; + // -- END: Standard Recorder-based unit test loading boilerplate -- + + // Runs a blank KeyEvent through the keyboard's rule processing. + harness.processKeystroke(new Mock(), keyboard.constructNullKeyEvent(device)); + }); + + it('does not change the active kehboard', async function() { + let harness = new KeyboardInterface({}, MinimalKeymanGlobal); + let keyboardLoader = new NodeKeyboardLoader(harness); + const lao_keyboard = await keyboardLoader.loadKeyboardFromPath(laoPath); + assert.isNotOk(harness.activeKeyboard); + assert.isOk(lao_keyboard); + + // This part provides assurance that the keyboard properly loaded. + assert.equal(lao_keyboard.id, "Keyboard_lao_2008_basic"); + + harness.activeKeyboard = lao_keyboard; + + const khmer_keyboard = await keyboardLoader.loadKeyboardFromPath(khmerPath); + assert.strictEqual(lao_keyboard, harness.activeKeyboard); + + assert.equal(khmer_keyboard.id, "Keyboard_khmer_angkor"); + }); + + it('throws distinct errors', async function() { + const invalidPath = 'totally_invalid_path.js'; + + let harness = new KeyboardInterface({}, MinimalKeymanGlobal); + let keyboardLoader = new NodeKeyboardLoader(harness); + let missingError; + try { + await keyboardLoader.loadKeyboardFromPath(invalidPath); + } catch (err) { + missingError = err; + } + assert.isOk(missingError); + + let scriptLoadError; + try { + await keyboardLoader.loadKeyboardFromPath(nonKeyboardPath); + } catch (err) { + scriptLoadError = err; + } + assert.isOk(scriptLoadError); + + // The main test: do the errors match? + assert.notEqual(scriptLoadError.message, missingError.message); + }); + }) +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/node/keyboard-properties.js b/common/web/keyboard-processor/tests/node/keyboard-properties.js new file mode 100644 index 0000000000000000000000000000000000000000..b6ec2dbf8d2b7fe07f8cb376bc59d68cdf3bbbb4 --- /dev/null +++ b/common/web/keyboard-processor/tests/node/keyboard-properties.js @@ -0,0 +1,98 @@ +import { assert } from 'chai'; +import fs from 'fs'; +import path from 'path'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +import { KeyboardProperties, SpacebarText } from '@keymanapp/keyboard-processor'; + +describe('Keyboard Properties', function() { + let commonResourcesPackage = '@keymanapp/common-test-resources'; + let commonStubsSubpath = 'json/keyboards'; + let rootCommonStubPath = `${commonResourcesPackage}/${commonStubsSubpath}`; + + it('initialization from KMW\'s addKeyboards() API spec', () => { + // require.resolve cannot resolve a directory directly, unfortunately. + // Needs a file to 'root' the resolution mechanism. See https://github.com/nodejs/node/issues/42219. + const resolvedResourcesPath = path.dirname(require.resolve(`${commonResourcesPackage}/index.mjs`)); + const resolvedCommonStubPath = `${resolvedResourcesPath}/${commonStubsSubpath}`; + let files = fs.readdirSync(resolvedCommonStubPath); + + for(let file of files) { + let stub = JSON.parse(fs.readFileSync(`${resolvedCommonStubPath}/${file}`)); + + let dataset = []; + if(stub.languages instanceof Array) { + dataset = KeyboardProperties.fromMultilanguageAPIStub(stub); + } else { + dataset = [new KeyboardProperties(stub)]; + } + + // do verification + for(let data of dataset) { + assert.isOk(data.id); + assert.isOk(data.name); + assert.isOk(data.langId); + assert.isOk(data.langName); + // Do not make assertions on font, oskFont - those may be undefined for some keyboards. + + // Generated dynamically from the backing source data. + assert.isOk(data.displayName); + } + } + }); + + it('generates display-name text if not directly-specified', () => { + // Could convert to run on all stubs, but... this should be fine as-is. + let stub = JSON.parse(fs.readFileSync(require.resolve(`${rootCommonStubPath}/khmer_angkor.json`))); + let propObject = new KeyboardProperties(stub); + + // Without a configured SpacebarText value, will display the keyboard name. + assert.equal(propObject.displayName, propObject.name); + + KeyboardProperties.spacebarTextMode = SpacebarText.BLANK; + assert.equal(propObject.displayName, ''); + + KeyboardProperties.spacebarTextMode = SpacebarText.KEYBOARD; + assert.equal(propObject.displayName, propObject.name); + + KeyboardProperties.spacebarTextMode = SpacebarText.LANGUAGE; + assert.equal(propObject.displayName, propObject.langName); + + KeyboardProperties.spacebarTextMode = SpacebarText.LANGUAGE_KEYBOARD; + assert.isTrue(propObject.displayName.includes(propObject.langName) && propObject.displayName.includes(propObject.name)); + }); + + it('does not override directly-specified display-name text', () => { + // Could convert to run on all stubs, but... this should be fine as-is. + let stub = JSON.parse(fs.readFileSync(require.resolve(`${rootCommonStubPath}/khmer_angkor.json`))); + + const customDisplayName = "(custom)"; + let propObject = new KeyboardProperties(stub); + + propObject.displayName = customDisplayName; + + // Without a configured SpacebarText value, will display the keyboard name. + assert.equal(propObject.displayName, customDisplayName); + + propObject.spacebarTextMode = SpacebarText.BLANK; + assert.equal(propObject.displayName, customDisplayName); + + propObject.spacebarTextMode = SpacebarText.KEYBOARD; + assert.equal(propObject.displayName, customDisplayName); + + propObject.spacebarTextMode = SpacebarText.LANGUAGE; + assert.equal(propObject.displayName, customDisplayName); + + propObject.spacebarTextMode = SpacebarText.LANGUAGE_KEYBOARD; + assert.equal(propObject.displayName, customDisplayName); + + propObject.displayName = null; // clear the value + assert.notEqual(propObject.displayName, customDisplayName); + assert.isTrue(propObject.displayName.includes(propObject.langName) && propObject.displayName.includes(propObject.name)); + + propObject.displayName = customDisplayName; + assert.equal(propObject.displayName, customDisplayName); + }); +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/node/mocks.js b/common/web/keyboard-processor/tests/node/mocks.js new file mode 100644 index 0000000000000000000000000000000000000000..9a84e2a8cc0f126409c9f3364dab534e5108abe9 --- /dev/null +++ b/common/web/keyboard-processor/tests/node/mocks.js @@ -0,0 +1,92 @@ +import { assert } from 'chai'; +import { Mock } from '@keymanapp/keyboard-processor'; + +describe('Mocks', function() { + describe('app|les', () => { + const testMock = new Mock('apples', 3); + + it('Cloning with .from()', () => { + assert.deepEqual(Mock.from(testMock), testMock); + assert.notStrictEqual(Mock.from(testMock), testMock); + }); + + it('getText', () => { + assert.equal(testMock.getText(), 'apples'); + }); + + it('getTextBeforeCaret', () => { + assert.equal(testMock.getTextBeforeCaret(), 'app'); + }); + + it('getTextAfterCaret', () => { + assert.equal(testMock.getTextAfterCaret(), 'les'); + }); + + it('getSelectedText', () => { + assert.equal(testMock.getSelectedText(), ''); + }); + + it('isSelectionEmpty', () => { + assert.isTrue(testMock.isSelectionEmpty()); + }); + + it('clearSelection', () => { + let editMock = Mock.from(testMock); + editMock.clearSelection(); + + assert.equal(editMock.getText(), testMock.getTextBeforeCaret() + testMock.getTextAfterCaret()); + assert.equal(editMock.getTextBeforeCaret(), testMock.getTextBeforeCaret()); + assert.equal(editMock.getTextAfterCaret(), testMock.getTextAfterCaret()); + assert.isTrue(editMock.isSelectionEmpty()); + + let postClear = Mock.from(editMock); + editMock.clearSelection(); // on same object; make sure its internal selection stuff updates correctly! + assert.notStrictEqual(postClear, editMock); + assert.deepEqual(postClear, editMock); + }); + }); + + describe('app|les and ba|nanas', () => { // selection = 'les and ba' + const testMock = new Mock('apples and bananas', 3, 13); + + it('Cloning with from()', () => { + assert.deepEqual(Mock.from(testMock), testMock); + assert.notStrictEqual(Mock.from(testMock), testMock); + }); + + it('getText', () => { + assert.equal(testMock.getText(), 'apples and bananas'); + }); + + it('getTextBeforeCaret', () => { + assert.equal(testMock.getTextBeforeCaret(), 'app'); + }); + + it('getTextAfterCaret', () => { + assert.equal(testMock.getTextAfterCaret(), 'nanas'); + }); + + it('getSelectedText', () => { + assert.equal(testMock.getSelectedText(), 'les and ba'); + }); + + it('isSelectionEmpty', () => { + assert.isFalse(testMock.isSelectionEmpty()); + }); + + it('clearSelection', () => { + let editMock = Mock.from(testMock); + editMock.clearSelection(); + + assert.equal(editMock.getText(), testMock.getTextBeforeCaret() + testMock.getTextAfterCaret()); + assert.equal(editMock.getTextBeforeCaret(), testMock.getTextBeforeCaret()); + assert.equal(editMock.getTextAfterCaret(), testMock.getTextAfterCaret()); + assert.isTrue(editMock.isSelectionEmpty()); + + let postClear = Mock.from(editMock); + editMock.clearSelection(); // on same object; make sure its internal selection stuff updates correctly! + assert.notStrictEqual(postClear, editMock); + assert.deepEqual(postClear, editMock); + }); + }); +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/cases/transcriptions.js b/common/web/keyboard-processor/tests/node/transcriptions.js similarity index 93% rename from common/web/keyboard-processor/tests/cases/transcriptions.js rename to common/web/keyboard-processor/tests/node/transcriptions.js index 11619557206280b5fd08bbb65a90d1319cfa9df4..073e0dc6b324dc0b04d679e8fdfd4c8faa0508ec 100644 --- a/common/web/keyboard-processor/tests/cases/transcriptions.js +++ b/common/web/keyboard-processor/tests/node/transcriptions.js @@ -1,8 +1,9 @@ -var assert = require('chai').assert; -let KeyboardProcessor = require('../../build/index.bundled.js'); +import { assert } from 'chai'; -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. +import { Mock } from '@keymanapp/keyboard-processor'; +import { extendString } from '@keymanapp/web-utils'; + +extendString(); // Ensure KMW's string-extension functionality is available. String.kmwEnableSupplementaryPlane(false); @@ -19,8 +20,6 @@ describe("Transcriptions and Transforms", function() { let smpApple = u(0x1d5ba)+u(0x1d5c9)+u(0x1d5c9)+u(0x1d5c5)+u(0x1d5be); it("does not store an alias for related OutputTargets", function() { - var Mock = com.keyman.text.Mock; - // We have other texts validating Mocks; by using them as our base 'element', this unit test file // could eventually run in 'headless' mode. var target = new Mock("apple"); @@ -38,8 +37,6 @@ describe("Transcriptions and Transforms", function() { describe("Plain text operations", function() { it("handles context-free single-char output rules", function() { - var Mock = com.keyman.text.Mock; - // We have other texts validating Mocks; by using them as our base 'element', this unit test file // could eventually run in 'headless' mode. var target = new Mock("apple"); @@ -67,11 +64,9 @@ describe("Transcriptions and Transforms", function() { }); it("handles operations with moderately long text", function() { - var Mock = com.keyman.text.Mock; - var target = new Mock("The quick brown cat jumped onto the lazy dog.", 19); var original = Mock.from(target); - target.setDeadkeyCaret(30); // 19 + 11: moves it to after "onto". + target.setSelection(30); // 19 + 11: moves it to after "onto". target.deleteCharsBeforeCaret(14); // delete: "cat jumped onto" target.insertTextBeforeCaret("fox jumped over"); @@ -86,8 +81,6 @@ describe("Transcriptions and Transforms", function() { }); it("handles operations with long text", function() { - var Mock = com.keyman.text.Mock; - // Eh... had to pick SOMETHING. let text = `Did you ever hear the Tragedy of Darth Plagueis the wise? I thought not. It's not a story the Jedi would tell you. It's a Sith legend. Darth Plagueis was a @@ -116,11 +109,9 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. }); it("handles deletions around the caret without text insertion", function() { - var Mock = com.keyman.text.Mock; - var target = new Mock("apple", 2); var original = Mock.from(target); - target.setDeadkeyCaret(3); + target.setSelection(3); target.deleteCharsBeforeCaret(2); // "ale" /* It's not exactly black box, but presently we don't NEED the keyEvent object for the method to work. @@ -136,11 +127,9 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. it("handles deletions around the caret without text insertion (SMP text)", function() { try { String.kmwEnableSupplementaryPlane(true); - var Mock = com.keyman.text.Mock; - var target = new Mock(smpApple, 2); var original = Mock.from(target); - target.setDeadkeyCaret(3); + target.setSelection(3); target.deleteCharsBeforeCaret(2); // "ale" /* It's not exactly black box, but presently we don't NEED the keyEvent object for the method to work. @@ -157,13 +146,11 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. }); it("handles deletions around the caret with text insertion", function() { - var Mock = com.keyman.text.Mock; - // We have other texts validating Mocks; by using them as our base 'element', this unit test file // could eventually run in 'headless' mode. var target = new Mock("apple", 2); var original = Mock.from(target); - target.setDeadkeyCaret(3); + target.setSelection(3); target.deleteCharsBeforeCaret(2); target.insertTextBeforeCaret("PP"); // "aPPle" @@ -180,7 +167,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. var target = new Mock("apple", 2); var original = Mock.from(target); - target.setDeadkeyCaret(4); + target.setSelection(4); target.deleteCharsBeforeCaret(3); target.insertTextBeforeCaret("P"); // "aPe" @@ -197,7 +184,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. var target = new Mock("apple", 2); var original = Mock.from(target); - target.setDeadkeyCaret(4); + target.setSelection(4); target.deleteCharsBeforeCaret(3); target.insertTextBeforeCaret("aaaaaaaaaaaaaa"); // "aaaaaaaaaaaaaaae" @@ -214,7 +201,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. var target = new Mock("apple", 2); var original = Mock.from(target); - target.setDeadkeyCaret(5); + target.setSelection(5); target.deleteCharsBeforeCaret(4); target.insertTextBeforeCaret("les"); // "ales" - since we've appended a letter at the very end, the whole right-hand is indeed an insertion. @@ -232,14 +219,13 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. it("handles deletions around the caret with text insertion (SMP text)", function() { try { String.kmwEnableSupplementaryPlane(true); - var Mock = com.keyman.text.Mock; // We have other texts validating Mocks; by using them as our base 'element', this unit test file // could eventually run in 'headless' mode. var target = new Mock(smpApple, 2); let smpLE = u(0x1d5c5)+u(0x1d5be); var original = Mock.from(target); - target.setDeadkeyCaret(3); + target.setSelection(3); target.deleteCharsBeforeCaret(2); target.insertTextBeforeCaret(smpLE); // "alele" @@ -259,7 +245,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. var target = new Mock(smpApple, 2); let smpB = u(0x1d5bb); var original = Mock.from(target); - target.setDeadkeyCaret(4); + target.setSelection(4); target.deleteCharsBeforeCaret(3); target.insertTextBeforeCaret(smpB); // "aPe" @@ -276,7 +262,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. var target = new Mock(smpApple, 2); var original = Mock.from(target); - target.setDeadkeyCaret(4); + target.setSelection(4); target.deleteCharsBeforeCaret(3); target.insertTextBeforeCaret("aaaaaaaaaaaaaa"); // "aaaaaaaaaaaaaaae" @@ -294,7 +280,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. var target = new Mock(smpApple, 2); let smpLES = u(0x1d5c5)+u(0x1d5be)+u(0x1d5cb); var original = Mock.from(target); - target.setDeadkeyCaret(5); + target.setSelection(5); target.deleteCharsBeforeCaret(4); target.insertTextBeforeCaret(smpLES); // "ales" - since we've appended a letter at the very end, the whole right-hand is indeed an insertion. @@ -316,18 +302,17 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. /*describe("Operations with deadkeys", function() { // Just one, less nuanced/subdivided; it's not a present priority for our work, but it should provide a decent basis if/when it's needed. it("Correctly recognizes deadkey set mutations", function() { - var Mock = com.keyman.text.Mock; // We have other texts validating Mocks; by using them as our base 'element', this unit test file // could eventually run in 'headless' mode. var target = new Mock("apple"); var original = Mock.from(target); - target.setDeadkeyCaret(4); + target.setSelection(4); target.insertDeadkeyBeforeCaret(0); - target.setDeadkeyCaret(1); + target.setSelection(1); target.insertDeadkeyBeforeCaret(1); - target.setDeadkeyCaret(2); + target.setSelection(2); target.insertDeadkeyBeforeCaret(2); // 'a' dk(1) 'p' dk(2) | 'p' 'l' dk(0) 'e' var original = Mock.from(target); @@ -335,7 +320,7 @@ but not himself.`; // Sheev Palpatine, in the Star Wars prequels. target.hasDeadkeyMatch(0, 2); target.deadkeys().deleteMatched(); - target.setDeadkeyCaret(3); + target.setSelection(3); target.deleteCharsBeforeCaret(2); target.insertTextBeforeCaret("b"); target.insertDeadkeyBeforeCaret(3); // In effect: 'a' dk(1) 'b' dk(3) | 'l' dk(0) 'e' diff --git a/common/web/keyboard-processor/tsconfig.all.json b/common/web/keyboard-processor/tsconfig.all.json new file mode 100644 index 0000000000000000000000000000000000000000..3325d965e054fe348fcfd6475bd1a7c545adc584 --- /dev/null +++ b/common/web/keyboard-processor/tsconfig.all.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.kmw-main-base.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "build/obj/", + "tsBuildInfoFile": "build/obj/tsconfig.all.tsbuildinfo", + "rootDir": "./src" + }, + "references": [ + { "path": "./src/keyboards/loaders/tsconfig.dom.json" }, + { "path": "./src/keyboards/loaders/tsconfig.node.json" } + ], + // Actual main-body compilation is in tsconfig.json. This config is just a wrapper + // to trigger all three components at once. + "include": [] +} diff --git a/common/web/keyboard-processor/tsconfig.json b/common/web/keyboard-processor/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..7f261c8021f8a6e452673beb25c895e31f92a37a --- /dev/null +++ b/common/web/keyboard-processor/tsconfig.json @@ -0,0 +1,17 @@ +// Is used by the keyboard-loader submodules. +{ + "extends": "../tsconfig.kmw-main-base.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "build/obj/", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + "references": [ + { "path": "../../models/types" }, + { "path": "../keyman-version/" }, + { "path": "../utils/" } + ], + "include": ["./src/**/*.ts"], + "exclude": ["./src/keyboards/loaders/**/*.ts"] +} diff --git a/common/web/keyman-version/build-bundler.js b/common/web/keyman-version/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..a5f26a44df9368b0fc17e5d83b36f48ff9c29c91 --- /dev/null +++ b/common/web/keyman-version/build-bundler.js @@ -0,0 +1,22 @@ +/* + * Bundles @keymanapp/keyman-version as a single-file CommonJS module for components + * still in need of it. + */ + +import esbuild from 'esbuild'; + +// Bundles to a compact ESModule +esbuild.buildSync({ + entryPoints: ['build/version.inc.js'], + bundle: true, + sourcemap: true, + //minify: true, // No need to minify a module. + //keepNames: true, + format: "cjs", + // Sets 'common/web' as a root folder for module resolution; + // this allows the keyman-version import to resolve. + nodePaths: ['..'], + outfile: "build/version.inc.cjs", + tsconfig: 'tsconfig.json', + target: "es5" +}); \ No newline at end of file diff --git a/common/web/keyman-version/build.sh b/common/web/keyman-version/build.sh index 07fb6220601af5e32214e0bc8db292bbee3052f8..939709732c2f27066bfffaa0e3c3dbc865a2e0ba 100755 --- a/common/web/keyman-version/build.sh +++ b/common/web/keyman-version/build.sh @@ -30,7 +30,7 @@ builder_describe "Build the include script for current Keyman version" \ builder_describe_outputs \ configure "/node_modules" \ - build "/common/web/keyman-version/build/keyman-version.mjs" + build "/common/web/keyman-version/build/version.inc.js" builder_parse "$@" @@ -43,53 +43,41 @@ fi if builder_start_action clean; then npm run clean rm -f ./version.inc.ts + rm -f ./keyman-version.mts + rm -rf build builder_finish_action success clean fi if builder_start_action build; then - # Generate keyman-version.mts + # Generate version.inc.ts echo " - // Generated by common/web/keyman-version/build.sh - export default class KEYMAN_VERSION { - static readonly VERSION = \"$VERSION\"; - static readonly VERSION_RELEASE =\"$VERSION_RELEASE\"; - static readonly VERSION_MAJOR = \"$VERSION_MAJOR\"; - static readonly VERSION_MINOR = \"$VERSION_MINOR\"; - static readonly VERSION_PATCH = \"$VERSION_PATCH\"; - static readonly TIER =\"$TIER\"; - static readonly VERSION_TAG = \"$VERSION_TAG\"; - static readonly VERSION_WITH_TAG = \"$VERSION_WITH_TAG\"; - static readonly VERSION_ENVIRONMENT = \"$VERSION_ENVIRONMENT\"; - static readonly SENTRY_RELEASE = \"release-$VERSION_WITH_TAG\"; - } - " > ./keyman-version.mts - - # Generate version.inc.ts -- used by TypeScript code that isn't yet modular - echo " - // Generated by common/web/keyman-version/build.sh - namespace com.keyman { - export class KEYMAN_VERSION { - static readonly VERSION = \"$VERSION\"; - static readonly VERSION_RELEASE =\"$VERSION_RELEASE\"; - static readonly VERSION_MAJOR = \"$VERSION_MAJOR\"; - static readonly VERSION_MINOR = \"$VERSION_MINOR\"; - static readonly VERSION_PATCH = \"$VERSION_PATCH\"; - static readonly TIER =\"$TIER\"; - static readonly VERSION_TAG = \"$VERSION_TAG\"; - static readonly VERSION_WITH_TAG = \"$VERSION_WITH_TAG\"; - static readonly VERSION_ENVIRONMENT = \"$VERSION_ENVIRONMENT\"; - static readonly VERSION_GIT_TAG = \"$VERSION_GIT_TAG\"; - } - } +// Generated by common/web/keyman-version/build.sh +// +// Note: does not use the 'default' keyword so that the export name is +// correct when converted to a CommonJS module with \`esbuild\`. +export class KEYMAN_VERSION { + static readonly VERSION = \"$VERSION\"; + static readonly VERSION_RELEASE =\"$VERSION_RELEASE\"; + static readonly VERSION_MAJOR = \"$VERSION_MAJOR\"; + static readonly VERSION_MINOR = \"$VERSION_MINOR\"; + static readonly VERSION_PATCH = \"$VERSION_PATCH\"; + static readonly TIER =\"$TIER\"; + static readonly VERSION_TAG = \"$VERSION_TAG\"; + static readonly VERSION_WITH_TAG = \"$VERSION_WITH_TAG\"; + static readonly VERSION_ENVIRONMENT = \"$VERSION_ENVIRONMENT\"; + static readonly VERSION_GIT_TAG = \"$VERSION_GIT_TAG\"; +} + +// Also provides it as a 'default' export. +export default KEYMAN_VERSION; " > ./version.inc.ts - # Note: in a dependency build, we'll expect keyman-version to be built by tsc -b - if builder_is_dep_build; then - builder_echo "skipping tsc -b; will be completed by $builder_dep_parent" - else - echo 'Building @keymanapp/keyman-version' - tsc -b $builder_verbose - fi + tsc -b $builder_verbose + # kmlmc (the lexical model compiler) relies on a Node-based import, but after some of the earlier + # ES-modularization work, our main output's an ES module. Fortunately, esbuild can provide an easy stopgap. + + # Generates a CommonJS variant (in case other modules still need it). + node ./build-bundler.js builder_finish_action success build fi diff --git a/common/web/keyman-version/index.ts b/common/web/keyman-version/index.ts deleted file mode 100644 index 64d0ff3da8c4d70998ffcce1aa4bfd60ed53de1f..0000000000000000000000000000000000000000 --- a/common/web/keyman-version/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// - -// Aliases the namespaced type with the non-namespaced module export. -declare let KEYMAN_VERSION: typeof com.keyman.KEYMAN_VERSION; - -// Add all namespaces defined here to the global scope: -if (typeof module != 'undefined' && typeof module.exports != 'undefined') { - module.exports['KEYMAN_VERSION'] = com.keyman.KEYMAN_VERSION; -} diff --git a/common/web/keyman-version/package.json b/common/web/keyman-version/package.json index ef94e58750ad4187925ff01f19b54b702d56e426..b6f5cfd0cf3a3702feaeac21a8ab717ad6b4653f 100644 --- a/common/web/keyman-version/package.json +++ b/common/web/keyman-version/package.json @@ -1,21 +1,24 @@ { "name": "@keymanapp/keyman-version", "description": "Keyman global version data", - "main": "./build/index.js", + "main": "./build/version.inc.cjs", "exports": { ".": { - "import": "./build/keyman-version.mjs", - "require": "./build/index.js" - } + "import": "./build/version.inc.js", + "require": "./build/version.inc.cjs" + }, + "./build/version.inc.cjs": "./build/version.inc.cjs" }, "files": [ "/build/" ], "scripts": { "build": "echo 'Building @keymanapp/keyman-version' && tsc -b", - "clean": "tsc -b --clean" + "clean": "tsc -b --clean", + "tsc": "tsc" }, "license": "MIT", + "type": "module", "devDependencies": { "@types/node": "^18.7.13", "typescript": "^4.9.5" diff --git a/common/web/keyman-version/tsconfig.esm.json b/common/web/keyman-version/tsconfig.esm.json deleted file mode 100644 index 1e72269873081357583fa68b418e2b4a7f674b3f..0000000000000000000000000000000000000000 --- a/common/web/keyman-version/tsconfig.esm.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "../../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": false, - "declaration": true, - "module": "es2020", - "outDir": "./build/", - "baseUrl": ".", - "rootDir": ".", - "sourceMap": true, - "lib": ["es6"], - "target": "es6", - }, - "include": [ - "keyman-version.mts" - ] -} diff --git a/common/web/keyman-version/tsconfig.json b/common/web/keyman-version/tsconfig.json index 18ed6ce938f0472eb873df27a472e57a4acaa718..8618c6dd9223f5ae597061a2feff2f81d510bbbc 100644 --- a/common/web/keyman-version/tsconfig.json +++ b/common/web/keyman-version/tsconfig.json @@ -4,15 +4,15 @@ "compilerOptions": { "allowJs": false, "declaration": true, - "module": "none", - "outFile": "./build/index.js", + "module": "es6", "sourceMap": true, + "outDir": "./build", "lib": ["es6"], "target": "es5", "downlevelIteration": true, + "rootDir": "." }, "include": [ - "index.ts", "version.inc.ts" ] } diff --git a/common/web/lm-worker/.c8rc.json b/common/web/lm-worker/.c8rc.json new file mode 100644 index 0000000000000000000000000000000000000000..56bc01f350e24172663e60c1cb57d265ac156783 --- /dev/null +++ b/common/web/lm-worker/.c8rc.json @@ -0,0 +1,14 @@ +{ + "check-coverage": false, + "clean": true, + "exclude": [ + "src/test/**", + "node_modules/*" + ], + "exclude-after-remap": true, + "reporter": ["text", "text-summary"], + "reports-dir": "build/coverage", + "src": [ + "src/main" + ] +} \ No newline at end of file diff --git a/common/web/lm-worker/build-bundler.js b/common/web/lm-worker/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..1c4045d5e296eb59ac60fb6cbdc932f9a32af180 --- /dev/null +++ b/common/web/lm-worker/build-bundler.js @@ -0,0 +1,69 @@ +/* + * Note: while this file is not meant to exist long-term, it provides a nice + * low-level proof-of-concept for esbuild bundling of the various Web submodules. + * + * Add some extra code at the end of src/index.ts and run it to verify successful bundling! + */ + +import esbuild from 'esbuild'; +import { bundleObjEntryPoints, esmConfiguration, prepareTslibTreeshaking } from '../es-bundling/build/index.mjs'; + +import fs from 'fs'; + +let EMIT_FILESIZE_PROFILE = false; + +if(process.argv.length > 2) { + for(let i = 2; i < process.argv.length; i++) { + const arg = process.argv[i]; + + switch(arg) { + case '': + break; + case '--ci': + EMIT_FILESIZE_PROFILE=true + break; + // May add other options if desired in the future. + default: + console.error("Invalid command-line option set for script; only --ci is permitted."); + process.exit(1); + } + } +} + +const embeddedWorkerBuildOptions = await prepareTslibTreeshaking({ + ...esmConfiguration, + ...bundleObjEntryPoints('lib', 'build/obj/index.js', 'build/obj/worker-main.js'), + // To be explicit, in comparison to the other build below. Even if our other + // configs change their default, we should not minify for THIS build; we'll + // have a separate minify pass later, after concatenating our polyfills. + minify: false + // No treeshaking at the moment (in case it treeshakes out certain model templates!) +}); + +// Direct-use version +await esbuild.build(embeddedWorkerBuildOptions); + +// ------------------------ + +// Now to generate a filesize profile for the minified version of the worker. +// We want a specialized bundle build here instead; no output, but minified like +// the actual release worker. +const minifiedProfilingOptions = { + ...embeddedWorkerBuildOptions, + minify: true, + metafile: true, + write: false // don't actually write the file. +} + +let result = await esbuild.build(minifiedProfilingOptions); +let filesizeProfile = await esbuild.analyzeMetafile(result.metafile, { verbose: true }); +fs.writeFileSync('build/filesize-profile.log', ` +// Minified Worker filesize profile, before polyfilling +${filesizeProfile} +`); + +if(EMIT_FILESIZE_PROFILE) { + console.log("Minified, pre-polyfill worker filesize profile:"); + // Profiles the sourcecode! + console.log(filesizeProfile); +} diff --git a/common/web/lm-worker/build-polyfill-concatenator.js b/common/web/lm-worker/build-polyfill-concatenator.js new file mode 100644 index 0000000000000000000000000000000000000000..3b350eb55ea2c9303d04f60b5ef0ce626d5c0509 --- /dev/null +++ b/common/web/lm-worker/build-polyfill-concatenator.js @@ -0,0 +1,117 @@ +import fs from 'fs'; + +import SourceMapCombiner from 'combine-source-map'; +import convertSourceMap from 'convert-source-map'; // Transforms sourcemaps among various common formats. + // Base64, stringified-JSON, end-of-file comment... + +let loadPolyfill = function(scriptFile, sourceMapFile) { + // May want to retool the pathing somewhat! + return { + source: fs.readFileSync(scriptFile).toString(), + sourceFile: sourceMapFile || scriptFile + }; +} + +let loadCompiledModuleFilePair = function(file, mapFilename) { + let module = fs.readFileSync(file); + let moduleSourcemapJSON = JSON.parse(fs.readFileSync(file + '.map').toString()); + + // May want to retool the pathing somewhat! + return { + plainSource: `${module}`, + sourceMapAsJSON: moduleSourcemapJSON, + sourceFile: mapFilename || file, + + /**The source + inlined sourcemap-as-comment used by `combine-source-map`. */ + get source() { + let jsonAsBuffer = Buffer.from(JSON.stringify(this.sourceMapAsJSON)); + return `${this.plainSource}\n${convertSourceMap.fromJSON(jsonAsBuffer).toComment()}`; + } + }; +} + +function concatScriptsAndSourcemaps(files, finalName, separatorFile) { + let combiner = SourceMapCombiner.create(finalName); + + let finalConcatenationArray = []; + let lineCountThusFar = 0; + + for(let filePairing of files) { + let offset = { + line: lineCountThusFar + }; + + console.log(`- ${filePairing.sourceFile}`); + combiner = combiner.addFile(filePairing, offset); + + let rawSourceToConcat = filePairing.plainSource || filePairing.source; + lineCountThusFar += rawSourceToConcat.split('\n').length + 1; // Not sure why it needs the fudge-factor, but it does. + + finalConcatenationArray.push(filePairing); + if(filePairing != files[files.length-1]) { + combiner = combiner.addFile(separatorFile); + finalConcatenationArray.push(separatorFile); + } + } + + let bundledSource = finalConcatenationArray.map((pair => pair.plainSource || pair.source)).join(''); + + return { + script: bundledSource, + sourcemapJSON: JSON.parse(convertSourceMap.fromBase64(combiner.base64()).toJSON()), + scriptFilename: finalName + } +} + +// Centralized? + +console.log("Pass 1: worker + polyfill concatenation"); + +let separatorFile = { + source: ` + + `, + sourceFile: '' +} + +let sourceFileSet = [ + // Needed for Android / Chromium browser pre-41. + loadPolyfill('../../../node_modules/string.prototype.codepointat/codepointat.js', 'src/polyfills/string.codepointat.js'), + // Needed for Android / Chromium browser pre-45. + loadPolyfill('src/polyfills/array.fill.js', 'src/polyfills/array.fill.js'), + // Needed for Android / Chromium browser pre-45. + loadPolyfill('src/polyfills/array.findIndex.js', 'src/polyfills/array.findIndex.js'), + // Needed for Android / Chromium browser pre-45. + loadPolyfill('src/polyfills/array.from.js', 'src/polyfills/array.from.js'), + // Needed for Android / Chromium browser pre-47. + loadPolyfill('src/polyfills/array.includes.js', 'src/polyfills/array.includes.js'), + // For Object.values, for iteration over object-based associate arrays. + // Needed for Android / Chromium browser pre-54. + loadPolyfill('src/polyfills/object.values.js', 'src/polyfills/object.values.js'), + // Needed to support Symbol.iterator, as used by the correction algorithm. + // Needed for Android / Chromium browser pre-43. + loadPolyfill('src/polyfills/symbol-es6.min.js', 'src/polyfills/symbol-es6.min.js'), + loadCompiledModuleFilePair('build/lib/worker-main.mjs', 'build/lib/worker-main.mjs') +]; + +let fullWorkerConcatenation = concatScriptsAndSourcemaps(sourceFileSet, "worker-main.polyfilled.js", separatorFile); + +// New stage: cleaning the sourcemaps + +// Sources are being passed into the sourcemap concatenator via our working directory. +let sourceRoot = '@keymanapp/keyman/common/web/lm-worker/'; +fullWorkerConcatenation.sourcemapJSON.sourceRoot = sourceRoot; + +// End "cleaning the sourcemaps" + +console.log(); +console.log("Pass 2: Output intermediate state and perform minification"); + +// IMPORTANT: Remove file-end sourcemap ref comment and replace it! +fullWorkerConcatenation.script = fullWorkerConcatenation.script.substring(0, fullWorkerConcatenation.script.lastIndexOf('//# sourceMappingURL')); +fullWorkerConcatenation.script += `//# sourceMappingURL=${fullWorkerConcatenation.scriptFilename}.map`; + +fs.writeFileSync(`build/lib/${fullWorkerConcatenation.scriptFilename}`, fullWorkerConcatenation.script); +if(fullWorkerConcatenation.sourcemapJSON) { + fs.writeFileSync(`build/lib/${fullWorkerConcatenation.scriptFilename}.map`, convertSourceMap.fromObject(fullWorkerConcatenation.sourcemapJSON).toJSON()); +} \ No newline at end of file diff --git a/common/web/lm-worker/build-wrap-and-minify.js b/common/web/lm-worker/build-wrap-and-minify.js new file mode 100644 index 0000000000000000000000000000000000000000..1572238b7a875309973641dca899a97b08716900 --- /dev/null +++ b/common/web/lm-worker/build-wrap-and-minify.js @@ -0,0 +1,86 @@ +import fs from 'fs'; +import esbuild from 'esbuild'; + +import convertSourcemap from 'convert-source-map'; // Transforms sourcemaps among various common formats. + // Base64, stringified-JSON, end-of-file comment... + +let DEBUG = false; +let MINIFY = false; + +if(process.argv.length > 2) { + for(let i = 2; i < process.argv.length; i++) { + const arg = process.argv[i]; + + switch(arg) { + case '--debug': + DEBUG = true; + break; + case '--minify': + MINIFY = true; + break; + // May add other options if desired in the future. + default: + console.error("Invalid command-line option set for script; only --debug and --minify are permitted."); + process.exit(1); + } + } +} + +let sourcemapJSON = ''; + +if(MINIFY) { + await esbuild.build({ + entryPoints: [`build/lib/worker-main.polyfilled.js`], + sourcemap: 'external', + sourcesContent: DEBUG, + minify: true, + keepNames: true, + target: 'es5', + outfile: `build/lib/worker-main.polyfilled.min.js` + }); +} + +sourcemapJSON = convertSourcemap.fromJSON(fs.readFileSync(`build/lib/worker-main.polyfilled${MINIFY ? '.min' : ''}.js.map`)).toObject(); + +const workerConcatenation = { + script: fs.readFileSync(`build/lib/worker-main.polyfilled${MINIFY ? '.min' : ''}.js`), + sourcemapJSON: sourcemapJSON +} + +// While it IS possible to do partial sourcemaps (without the sources, but with everything else) within the worker... +// the resulting sourcemaps are -surprisingly- large - larger than the code itself! +// +// So... we don't use that strategy here. + +console.log(); +console.log(`Wrapping + generating final output: ${MINIFY ? 'minified' : 'unminified'} + ${DEBUG ? 'full sourcemaps' : 'reduced sourcemaps'}`); + +// Now, to build the wrapper... + +// First, let's build the encoded sourcemap. +const encodedSrcMap = convertSourcemap.fromObject(workerConcatenation.sourcemapJSON).toBase64(); +const srcMapString = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${encodedSrcMap}`; + +/* + * It'd be nice to do a 'partial' encodeURIComponent that only gets the important bits... + * but my attempts to do so end up triggering errors when loading. + */ + +let rawScript = workerConcatenation.script.toString(); +// Two layers of encoding: one for the raw source (parsed by the JS engine), +// one to 'unwrap' it from a string _within_ that source. +let jsonEncoded = JSON.stringify(rawScript); + +let wrapper = ` +// Autogenerated code. Do not modify! +// --START:LMLayerWorkerCode-- + +export var LMLayerWorkerCode = ${jsonEncoded}; + +${MINIFY && "// Sourcemaps have been omitted for this release build."} +export var LMLayerWorkerSourcemapComment = "${DEBUG ? srcMapString : ''}"; + +// --END:LMLayerWorkerCode +`; + +fs.writeFileSync(`build/lib/worker-main.wrapped${MINIFY ? '.min' : ''}.js`, wrapper); \ No newline at end of file diff --git a/common/web/lm-worker/build.sh b/common/web/lm-worker/build.sh index 59472b0f93351bd78aed7a3becb320ce359ba61b..fdb4db75f45ac8d3455149cd9c1b3adcc89b447f 100755 --- a/common/web/lm-worker/build.sh +++ b/common/web/lm-worker/build.sh @@ -18,115 +18,59 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" # This script runs from its own folder -cd "$(dirname "$THIS_SCRIPT")" - -WORKER_OUTPUT=build -WORKER_OUTPUT_FILENAME=$WORKER_OUTPUT/index.js -WORKER_TEST_BUNDLE_TARGET_FILENAME=$WORKER_OUTPUT/index.wrapped-for-bundle.ts - -# Wraps JavaScript code in a way that can be embedded in a worker. -# To get the inner source code, include the file generated by this function, -# then use name.toString() where `name` is the name passed into this -# function. -wrap-worker-code ( ) { - name="$1" - js="$2" - # Reference: https://stackoverflow.com/a/59046041 - # Use of this annotation allows us to actually build against the file, rather - # than hackily insert it later when/where needed. - echo "// @ts-nocheck" - echo "// Autogenerated code. Do not modify!" - echo "// --START:LMLayerWorkerCode--" - printf "function %s () {\n" "${name}" - - # Since the worker is compiled with "allowJS=false" so that we can make - # declaration files, we have to insert polyfills here. - - ### NOTE ### - # Android API 21 (our current minimum) released with Chrome for Android 37. - # It's also updatable as of this version, but we can't guarantee that the user - # actually updated it, especially on first launch of the Android app/keyboard. - - # This one's a minimal, targeted polyfill. es6-shim could do the same, - # but also adds a lot more code the worker doesn't need to use. - # Recommended by MDN while keeping the worker lean and efficient. - # Needed for Android / Chromium browser pre-41. - cat "../../../node_modules/string.prototype.codepointat/codepointat.js" || builder_die - - # These two are straight from MDN - I didn't find any NPM ones that don't - # use the node `require` statement for the second. They're also relatively - # short and simple, which is good. - cat "src/polyfills/array.fill.js" || builder_die # Needed for Android / Chromium browser pre-45. - cat "src/polyfills/array.findIndex.js" || builder_die # Needed for Android / Chromium browser pre-45. - cat "src/polyfills/array.from.js" || builder_die # Needed for Android / Chromium browser pre-45. - cat "src/polyfills/array.includes.js" || builder_die # Needed for Android / Chromium browser pre-47. - - # For Object.values, for iteration over object-based associate arrays. - cat "src/polyfills/object.values.js" || builder_die # Needed for Android / Chromium browser pre-54. - - # Needed to support Symbol.iterator, as used by the correction algorithm. - cat "src/polyfills/symbol-es6.min.js" || builder_die # Needed for Android / Chromium browser pre-43. - - echo "" - - cat "${js}" || builder_die - printf "\n}\n" - echo "// --END:LMLlayerWorkerCode" -} +cd "$THIS_SCRIPT_PATH" + +WORKER_OUTPUT=build/obj +WORKER_OUTPUT_FILENAME=build/lib/worker-main.js ################################ Main script ################################ builder_describe \ "Compiles the Language Modeling Layer for common use in predictive text and autocorrective applications." \ "@/common/web/keyman-version" \ - configure clean build test + "@/common/web/es-bundling" \ + "@/common/models/wordbreakers" \ + "@/common/models/templates" \ + configure clean build test --ci builder_describe_outputs \ configure /node_modules \ - build /common/web/lm-worker/build/index.js + build /common/web/lm-worker/build/lib/worker-main.wrapped.min.js builder_parse "$@" -# TODO: build if out-of-date if test is specified -# TODO: configure if npm has not been run, and build is specified - -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi - -# We always need to clean first because the wrapping function -# breaks the typescript build, as it makes the index.tsbuildinfo file -# inconsistent with the actual index.js and causes the file to be -# corrupted on subsequent builds. In order to use the --build parameter -# of typescript, we need to avoid this! -# TODO: we should try and rework this to avoid the need to manually wrap - -if builder_start_action clean; then - npm run clean - builder_finish_action success clean -fi - -if builder_start_action build; then - if ! builder_has_action clean; then - npm run clean - fi - +function do_build() { # Build worker with tsc first tsc -b $builder_verbose || builder_die "Could not build worker." + node build-bundler.js + + EXT_FLAGS= + if builder_has_option --ci; then + EXT_FLAGS=--ci + fi + + # Declaration bundling. + tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts + tsc --emitDeclarationOnly --outFile ./build/lib/worker-main.d.ts + + echo "Preparing the polyfills + worker for script-embedding" + node build-polyfill-concatenator.js + + node build-wrap-and-minify.js --debug + node build-wrap-and-minify.js --minify +} + +function do_test() { + local MOCHA_FLAGS= + + if builder_has_option --ci; then + MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter" + fi + + c8 mocha --recursive $MOCHA_FLAGS ./src/test/cases/ +} - # Wrap the worker code and create embedded index.js. Must be run after the - # worker is built - echo "Wrapping worker in function LMLayerWorkerCode ${WORKER_OUTPUT_FILENAME}" - # Note: We use intermediate.js for unit tests in predictive-text - cp "${WORKER_OUTPUT_FILENAME}" "${WORKER_OUTPUT}/intermediate.js" - wrap-worker-code LMLayerWorkerCode "${WORKER_OUTPUT}/intermediate.js" > "${WORKER_OUTPUT_FILENAME}" || builder_die - cp "${WORKER_OUTPUT_FILENAME}" "${WORKER_TEST_BUNDLE_TARGET_FILENAME}" || builder_die - - builder_finish_action success build -fi - -if builder_start_action test; then - npm test || builder_die "Tests failed" - builder_finish_action success test -fi +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/web/lm-worker/package.json b/common/web/lm-worker/package.json index a25290d5d3a3b42bbbd9b4cb0cecb25c9a283587..9ca87147fefc2d1bd5becaa539c3ef711600caf8 100644 --- a/common/web/lm-worker/package.json +++ b/common/web/lm-worker/package.json @@ -1,20 +1,31 @@ { "name": "@keymanapp/lm-worker", "description": "Keyman/Predictive Text worker", - "main": "build/index.js", + "main": "./build/lib/worker-main.wrapped.js", + "exports": { + "./worker-main.wrapped.js": "./build/lib/worker-main.wrapped.js", + "./worker-main.wrapped.min.js": "./build/lib/worker-main.wrapped.min.js" + }, + "imports": { + "#./*.js": "./build/obj/*.js", + "#./correction/*.js": "./build/obj/correction/*.js", + "#./models/*.js": "./build/obj/models/*.js" + }, + "type": "module", "scripts": { - "karma": "karma", - "mocha": "mocha", - "test": "gosh ./test/test.sh", - "build": "tsc -b", - "clean": "tsc -b --clean" + "test": "gosh ./build.sh test", + "build": "gosh ./build.sh", + "clean": "gosh ./build.sh clean" }, "license": "MIT", "devDependencies": { + "@keymanapp/common-test-resources": "*", "@keymanapp/models-types": "*", "@keymanapp/resources-gosh": "*", "@types/node": "^10.17.21", + "c8": "^7.12.0", "chai": "^4.3.4", + "combine-source-map": "^0.8.0", "karma": "^6.4.1", "karma-browserstack-launcher": "^1.6.0", "karma-chai": "^0.1.0", @@ -36,9 +47,9 @@ "typescript": "^4.9.5" }, "dependencies": { + "@keymanapp/keyman-version": "*", "@keymanapp/models-templates": "*", "@keymanapp/models-wordbreakers": "*", - "@keymanapp/keyman-version": "*", "@keymanapp/web-utils": "*", "es6-shim": "^0.35.5", "string.prototype.codepointat": "^0.2.1", diff --git a/common/web/lm-worker/src/correction/classical-calculation.ts b/common/web/lm-worker/src/correction/classical-calculation.ts deleted file mode 100644 index 3ac31ecd54b05eb6cf61f010b6d8b93585733848..0000000000000000000000000000000000000000 --- a/common/web/lm-worker/src/correction/classical-calculation.ts +++ /dev/null @@ -1,572 +0,0 @@ -namespace correction { - type EditOperation = 'insert' | 'delete' | 'match' | 'substitute' | 'transpose-start' | 'transpose-end' | 'transpose-insert' | 'transpose-delete'; - - /** - * Represents the lowest-level unit for comparison during edit-distance calculations. - */ - export interface EditToken { - key: TUnit; - } - - // A semi-optimized 'online'/iterative Damerau-Levenshtein calculator with the following features: - // - may add new character to the 'input' string or to the 'match' string, reusing all old calculations efficiently. - // - allows a 'focused' evaluation that seeks if the edit distance is within a specific range. Designed for use in match-searching, - // where we want to find the 'closest' matching strings in a lexicon. - // - towards such a match-searching algorithm/heuristic: should nothing be found within that range, all prior calculations may be reused - // to search across the lexicon with an incremented edit distance. - // - minimized memory footprint: O(m) memory footprint (where m = length of 'input' string), rather than O(mn) (where n = length of 'match' string) - // - guaranteed to use a smaller footprint than DiagonalizedIterativeDamerauLevenshteinCalculation. - // - // In short: Used to optimize calculations for low edit-distance checks, then expanded if/as necessary - // if a greater edit distance is requested. - // - // Reference: https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm#Possible_modifications - // - Motivating statement: "if we are only interested in the distance if it is smaller than a threshold..." - export class ClassicalDistanceCalculation = EditToken, TMatch extends EditToken = EditToken> { - /** - * Stores ONLY the computed diagonal elements, nothing else. - * - * Mapped as seen in the example below (with a diagonal of width 1): - * ``` - * MAX | MAX | MAX | MAX | MAX | ... - * MAX | 0 | 1 | 2 | 3 | ... > - * MAX | 1 | a | b | - | ... ====>> | - | a | b | - * MAX | 2 | c | d | e | ... > | c | d | e | - * MAX | 3 | - | f | g | ... | f | g | ... | - * ... | ... | ... | ... | ... | ... | ... | ... | ... | - * ``` - * - * Any "`-`" entries are undefined, as they lie outside of the diagonal under consideration. - * - * Things of note: - * - The entry where row index = col index will always lie at the center of the row's array. - * - For each +1 increase in row index, the row's entries are (logically) shifted by -1 in order to make this happen. - * - As all of the MAX entries and numerical entries above are fixed, known values, they are not represented here. - */ - resolvedDistances: number[][]; - /** - * Specifies how far off-diagonal calculations should be performed. A value of 0 only evaluates cells with matching - * row and column indicies. - * - * The resulting value from .getFinalCost() is only guaranteed correct if it is less than or equal to this value. - * Otherwise, this object represents a heuristic that _may_ overestimate the true edit distance. Note that it will - * never underestimate. - */ - diagonalWidth: number = 2; // TODO: Ideally, should start at 1... but we'll start at 2 for now - // as a naive workaround for multi-char transform limitations. - - // The sequence of characters input so far. - inputSequence: TInput[] = []; - matchSequence: TMatch[] = []; - - constructor(); - constructor(other: ClassicalDistanceCalculation); - constructor(other?: ClassicalDistanceCalculation) { - if(other) { - // Clone class properties. - let rowCount = other.resolvedDistances.length; - this.resolvedDistances = Array(rowCount); - - for(let r = 0; r < rowCount; r++) { - this.resolvedDistances[r] = Array.from(other.resolvedDistances[r]); - } - - this.inputSequence = Array.from(other.inputSequence); - this.matchSequence = Array.from(other.matchSequence); - this.diagonalWidth = other.diagonalWidth; - } else { - this.resolvedDistances = []; - } - } - - private getTrueIndex(r: number, c: number, width: number): {row: number, col: number, sparse: boolean} { - let retVal = { - row: r, - col: c - r + width, - sparse: false - } - - if(retVal.col < 0 || retVal.col > 2 * width) { - retVal.sparse = true; - } - - return retVal; - } - - private getCostAt(i: number, j: number, width: number = this.diagonalWidth): number { - // Check for and handle the set of fixed-value virtualized indices. - if(i < 0 || j < 0) { - if(i == -1 && j >= -1) { - return j+1; - } else if(j == -1 && i >= -1) { - return i+1; - } - - return Number.MAX_VALUE; - } - - let index = this.getTrueIndex(i, j, width); - return index.sparse ? Number.MAX_VALUE : this.resolvedDistances[index.row][index.col]; - } - - /** - * Noting the above link's statement prefixed "By examining diagonals instead of rows, and by using lazy evaluation...", - * this function will return the actual edit distance between the strings, temporarily increasing the computed - * diagonal's size if necessary. - * - * Does not actually mutate the instance. - */ - getFinalCost(): number { - let buffer = this as ClassicalDistanceCalculation; - let val = buffer.getHeuristicFinalCost(); - - while(val > buffer.diagonalWidth) { - // A consequence of treating this class as immutable. - buffer = buffer.increaseMaxDistance(); - val = buffer.getHeuristicFinalCost(); - } - - return val; - } - - /** - * Returns this instance's computed edit distance. If greater than the diagonal's width value, note that it may be an overestimate. - */ - getHeuristicFinalCost(): number { - return this.getCostAt(this.inputSequence.length-1, this.matchSequence.length-1); - } - - /** - * Returns `true` if the represented edit distance is less than or equal to the specified threshold, minimizing the amount of calculations - * needed to meet the specified limit. - * - * Does not mutate the instance. - * @param threshold - */ - hasFinalCostWithin(threshold: number): boolean { - let buffer = this as ClassicalDistanceCalculation; - let val = buffer.getHeuristicFinalCost(); - let guaranteedBound = this.diagonalWidth; - - do { - // val will never exceed the length of the longer string, no matter how large the threshold. - if(val <= threshold) { - return true; - } else if(guaranteedBound < threshold) { - buffer = buffer.increaseMaxDistance(); - guaranteedBound++; - val = buffer.getHeuristicFinalCost(); - } else { - break; - } - } while(true); - - return false; - } - - /** - * Determines the edit path used to obtain the optimal cost, distinguishing between zero-cost - * substitutions ('match' operations) and actual substitutions. - * @param row - * @param col - */ - public editPath(row: number = this.inputSequence.length - 1, col: number = this.matchSequence.length - 1): EditOperation[] { - let currentCost = this.getCostAt(row, col); - let ops: EditOperation[] = null; - let parent: [number, number] = null; - - let insertParentCost = this.getCostAt(row, col-1); - let deleteParentCost = this.getCostAt(row-1, col); - let substitutionParentCost = this.getCostAt(row-1, col-1); - let [lastInputIndex, lastMatchIndex] = ClassicalDistanceCalculation.getTransposeParent(this, row, col); - if(lastInputIndex >= 0 && lastMatchIndex >= 0) { - // OK, a transposition source is quite possible. Still need to do more vetting, to be sure. - let expectedCost = 1; - - // This transposition includes either 'transpose-insert' or 'transpose-delete' operations. - ops = ['transpose-start']; // always needs a 'start'. - if(lastInputIndex != row-1) { - let count = row - lastInputIndex - 1; - ops = ops.concat( Array(count).fill('transpose-delete') ); - expectedCost += count; - } else { - let count = col - lastMatchIndex - 1; - ops = ops.concat( Array(count).fill('transpose-insert') ); - expectedCost += count; - } - ops.push('transpose-end'); - - // Double-check our expectations. - if(this.getCostAt(lastInputIndex-1, lastMatchIndex-1) != currentCost - expectedCost) { - ops = null; - } - parent = [lastInputIndex-1, lastMatchIndex-1]; - } - - if(ops) { - // bypass the ladder. - } else if(substitutionParentCost == currentCost - 1) { - ops = ['substitute']; - parent = [row-1, col-1]; - } else if(insertParentCost == currentCost - 1) { - ops = ['insert']; - parent = [row, col-1]; - } else if(deleteParentCost == currentCost - 1) { - ops = ['delete']; - parent = [row-1, col]; - } else { //if(substitutionParentCost == currentCost) { - ops = ['match']; - parent = [row-1, col-1]; - } - - // Recursively build the edit path. - if(parent[0] >= 0 && parent[1] >= 0) { - return this.editPath(parent[0], parent[1]).concat(ops); - } else { - if(parent[0] > -1) { - // There are initial deletions. - return Array(parent[0]+1).fill('delete').concat(ops); - } else if(parent[1] > -1) { - // There are initial insertions. - return Array(parent[1]+1).fill('insert').concat(ops); - } else { - return ops; - } - } - } - - private static getTransposeParent, TMatch extends EditToken>( - buffer: ClassicalDistanceCalculation, - r: number, - c: number - ): [number, number] { - // Block any transpositions where the tokens are identical. - // Other operations will be cheaper. Also, block cases where 'parents' are impossible. - if(r < 0 || c < 0 || buffer.inputSequence[r].key == buffer.matchSequence[c].key) { - return [-1, -1]; - } - - // Transposition checks - let lastInputIndex = -1; - for(let i = r-1; i >= 0; i--) { - if(buffer.inputSequence[i].key == buffer.matchSequence[c].key) { - lastInputIndex = i; - break; - } - } - - let lastMatchIndex = -1; - for(let i = c-1; i >= 0; i--) { - if(buffer.matchSequence[i].key == buffer.inputSequence[r].key) { - lastMatchIndex = i; - break; - } - } - - return [lastInputIndex, lastMatchIndex]; - } - - private static initialCostAt, TMatch extends EditToken>( - buffer: ClassicalDistanceCalculation, - r: number, - c: number, - insertCost?: number, - deleteCost?: number) { - var baseSubstitutionCost = buffer.inputSequence[r].key == buffer.matchSequence[c].key ? 0 : 1; - var substitutionCost: number = buffer.getCostAt(r-1, c-1) + baseSubstitutionCost; - var insertionCost: number = insertCost || buffer.getCostAt(r, c-1) + 1; // If set meaningfully, will never equal zero. - var deletionCost: number = deleteCost || buffer.getCostAt(r-1, c) + 1; // If set meaningfully, will never equal zero. - var transpositionCost: number = Number.MAX_VALUE - - if(r > 0 && c > 0) { // bypass when transpositions are known to be impossible. - let [lastInputIndex, lastMatchIndex] = ClassicalDistanceCalculation.getTransposeParent(buffer, r, c); - transpositionCost = buffer.getCostAt(lastInputIndex-1, lastMatchIndex-1) + (r - lastInputIndex - 1) + 1 + (c - lastMatchIndex - 1); - } - - return Math.min(substitutionCost, deletionCost, insertionCost, transpositionCost); - } - - getSubset(inputLength: number, matchLength: number): ClassicalDistanceCalculation { - let trimmedInstance = new ClassicalDistanceCalculation(this); - - if(inputLength > this.inputSequence.length || matchLength > this.matchSequence.length) { - throw "Invalid dimensions specified for trim operation"; - } - // Trim our tracked input & match sequences. - trimmedInstance.inputSequence.splice(inputLength); - trimmedInstance.matchSequence.splice(matchLength); - - // Major index corresponds to input length. - trimmedInstance.resolvedDistances.splice(inputLength); - - // The real fun: trimming off columns. (Minor index, corresponds to match length) - let finalTrueIndex = this.getTrueIndex(inputLength-1, matchLength-1, this.diagonalWidth); - // The diagonal index increases as the row index decreases. - for(let diagonalIndex = finalTrueIndex.col; diagonalIndex <= 2 * this.diagonalWidth; diagonalIndex++) { - let row = finalTrueIndex.row - (diagonalIndex - finalTrueIndex.col); - if(row < 0) { - break; - } - - if(diagonalIndex < 0) { - trimmedInstance.resolvedDistances[row] = Array(2 * trimmedInstance.diagonalWidth + 1).fill(Number.MAX_VALUE); - } else { - let newCount = 2 * this.diagonalWidth - diagonalIndex; - let keptEntries = trimmedInstance.resolvedDistances[row].splice(0, diagonalIndex+1); - let newEntries = Array(newCount).fill(Number.MAX_VALUE); - trimmedInstance.resolvedDistances[row] = keptEntries.concat(newEntries); - } - } - - return trimmedInstance; - } - - private static forDiagonalOfAxis(diagonalWidth: number, centerIndex: number, axisCap: number, closure: (axisIndex: number, diagIndex: number) => void) { - let diagonalCap = axisCap - centerIndex < diagonalWidth ? axisCap - centerIndex + diagonalWidth : 2 * diagonalWidth; - let startOffset = centerIndex - diagonalWidth; // The axis's index for diagonal entry 0. May be negative. - let diagonalStart = startOffset < 0 ? 0 : startOffset; - - for(let diagonalIndex = diagonalStart - startOffset; diagonalIndex <= diagonalCap; diagonalIndex++) { - closure(startOffset + diagonalIndex, diagonalIndex); - } - } - - // Inputs add an extra row / first index entry. - addInputChar(token: TInput): ClassicalDistanceCalculation { - let returnBuffer = new ClassicalDistanceCalculation(this); - - let r = returnBuffer.inputSequence.length; - returnBuffer.inputSequence.push(token); - - // Insert a row, even if we don't actually do anything with it yet. - // Initialize all entries with Number.MAX_VALUE, as `undefined` use leads to JS math issues. - let row = Array(2 * returnBuffer.diagonalWidth + 1).fill(Number.MAX_VALUE); - returnBuffer.resolvedDistances[r] = row; - - // If there isn't a 'match' entry yet, there are no values to compute. Exit immediately. - if(returnBuffer.matchSequence.length == 0) { - return returnBuffer; - } - - ClassicalDistanceCalculation.forDiagonalOfAxis(returnBuffer.diagonalWidth, r, returnBuffer.matchSequence.length - 1, function(c, diagIndex) { - row[diagIndex] = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c); - }); - - return returnBuffer; - } - - addMatchChar(token: TMatch): ClassicalDistanceCalculation { - let returnBuffer = new ClassicalDistanceCalculation(this); - - let c = returnBuffer.matchSequence.length; - returnBuffer.matchSequence.push(token); - - // If there isn't a 'match' entry yet, there are no values to compute. Exit immediately. - if(returnBuffer.inputSequence.length == 0) { - return returnBuffer; - } - - ClassicalDistanceCalculation.forDiagonalOfAxis(returnBuffer.diagonalWidth, c, returnBuffer.inputSequence.length - 1, function(r, diagIndex) { - var row = returnBuffer.resolvedDistances[r]; - // Since diagIndex is from the perspective of the row, it must be inverted to properly index the column. - row[2 * returnBuffer.diagonalWidth - diagIndex] = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c); - }); - - return returnBuffer; - } - - public increaseMaxDistance(): ClassicalDistanceCalculation { - let returnBuffer = new ClassicalDistanceCalculation(this); - returnBuffer.diagonalWidth++; - - if(returnBuffer.inputSequence.length < 1 || returnBuffer.matchSequence.length < 1) { - return returnBuffer; - } - - // An abstraction of the common aspects of transposition handling during diagonal extensions. - function forPossibleTranspositionsInDiagonal(startPos: number, fixedChar: TUnit, lookupString: EditToken[], closure: (axisIndex: number, diagIndex: number) => void) { - let diagonalCap = 2 * (returnBuffer.diagonalWidth - 1); // The maximum diagonal index permitted - let axisCap = lookupString.length - 1; // The maximum index supported by the axis of iteration - - // Ensures that diagonal iteration only occurs within the axis's supported range - diagonalCap = diagonalCap < axisCap - startPos ? diagonalCap : axisCap - startPos; - - // Iterate within the diagonal and call our closure for any potential transpositions. - for(let diagonalIndex = 0; diagonalIndex <= diagonalCap; diagonalIndex++) { - if(fixedChar == lookupString[startPos + diagonalIndex].key) { - closure(startPos + diagonalIndex, diagonalIndex); - } - } - } - - for(let r = 0; r < returnBuffer.inputSequence.length; r++) { - let leftCell = Number.MAX_VALUE; - let c = r - returnBuffer.diagonalWidth // External index of the left-most entry, which we will now calculate. - if(c >= 0) { - // If c == 0, cell is at edge, thus a known value for insertions exists. - // Base cost: r+1, +1 for inserting. - let insertionCost = c == 0 ? r + 2 : Number.MAX_VALUE; - // compute new left cell - leftCell = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c, insertionCost, undefined); - let addedCost = leftCell; - - // daisy-chain possible updates - - // cell (r, c+1): new insertion source - if(c < returnBuffer.matchSequence.length-1) { - // We propagate the new added cost (via insertion) to the old left-most cell, which is one to our right. - ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, r, c+1, addedCost+1, 0); - - // Only possible if insertions are also possible AND more conditions are met. - // cells (r+2, * > c+2): new transposition source - let transposeRow = r+2; - if(r+2 < this.inputSequence.length) { // Row to check for transposes must exist. - let rowChar = returnBuffer.inputSequence[r+1].key; - // First possible match in input could be at index c + 2, which adjusts col c+2's cost. Except that entry in r+2 - // doesn't exist yet - so we start with c+3 instead. - forPossibleTranspositionsInDiagonal(c + 3, rowChar, returnBuffer.matchSequence, function(axisIndex, diagIndex) { - // Because (r+2, c+3) is root, not (r+2, c+2). Min cost of 2. - ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, transposeRow, axisIndex, addedCost + diagIndex + 2, diagIndex); - }); - } - } - } - - let rightCell = Number.MAX_VALUE; - c = r + returnBuffer.diagonalWidth; - if(c < returnBuffer.matchSequence.length) { - // If r == 0, cell is at edge, thus a known value for insertions exists. - // Base cost: c+1, +1 for inserting. - let deletionCost = r == 0 ? c + 2 : Number.MAX_VALUE; - - // the current row wants to use adjusted diagonal width; we must specify use of the old width & its mapping instead. - var insertionCost: number = returnBuffer.getCostAt(r, c-1, this.diagonalWidth) + 1; - - // compute new right cell - rightCell = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c, insertionCost, deletionCost); - let addedCost = rightCell; - - // daisy-chain possible updates - - // cell (r+1, c): new deletion source - if(r < returnBuffer.inputSequence.length - 1) { - // We propagate the new added cost (via deletion) to the old right-most cell, which is one to our right. - ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, r+1, c, addedCost + 1, 2 * this.diagonalWidth); - - // Only possible if deletions are also possible AND more conditions are met. - // cells(* > r+2, c+2): new transposition source - let transposeCol = c+2; - if(c+2 < this.matchSequence.length) { // Row to check for transposes must exist. - let colChar = returnBuffer.matchSequence[r+1].key; - // First possible match in input could be at index r + 2, which adjusts row r+2's cost. Except that entry in c+2 - // doesn't exist yet - so we start with r+3 instead. - forPossibleTranspositionsInDiagonal(r+3, colChar, returnBuffer.inputSequence, function(axisIndex, diagIndex) { - let diagColIndex = 2 * (returnBuffer.diagonalWidth - 1) - diagIndex; - // Because (r+3, c+2) is root, not (r+2, c+2). Min cost of 2. - ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, axisIndex, transposeCol, addedCost + diagIndex + 2, diagColIndex); - }); - } - } - } - - // Constructs the final expanded diagonal for the row. - returnBuffer.resolvedDistances[r] = [leftCell].concat(returnBuffer.resolvedDistances[r], rightCell); - } - - return returnBuffer; - } - - private static propagateUpdateFrom, TMatch extends EditToken>( - buffer: ClassicalDistanceCalculation, - r: number, - c: number, - value: number, - diagonalIndex: number) { - // Note: this function does not actually need the `c` parameter! - // That said, it's very useful when tracing stack traces & debugging. - if(value < buffer.resolvedDistances[r][diagonalIndex]) { - buffer.resolvedDistances[r][diagonalIndex] = value; - } else { - return - } - - let internalRow = r < buffer.inputSequence.length - 1; - let internalCol = c < buffer.matchSequence.length - 1; - - // We have to compensate for the current & following rows not having been expanded yet. - if(diagonalIndex < 2 * (buffer.diagonalWidth - 1) && internalCol) { - // We've inserted to the left of an existing calculation - check for propagation via insertion. - let updateCost = value + 1; - this.propagateUpdateFrom(buffer, r, c+1, updateCost, diagonalIndex+1); - } - - if(diagonalIndex > 0 && internalRow) { - // We've inserted above an existing calculation - check for propagation via deletion - let updateCost = value + 1 - this.propagateUpdateFrom(buffer, r+1, c, updateCost, diagonalIndex-1); - } - - // If both, check for propagation via substitution and possible transpositions - if(internalRow && internalCol) { - let updateCost = value + (buffer.inputSequence[r+1].key == buffer.matchSequence[c+1].key ? 0 : 1); - this.propagateUpdateFrom(buffer, r+1, c+1, updateCost, diagonalIndex); - - // Propagating transpositions (only possible if 'internal'.) - let nextInputIndex = -1; - for(let i = r+2; i < buffer.inputSequence.length; i++) { - if(buffer.inputSequence[i].key == buffer.matchSequence[c+1].key) { - nextInputIndex = i; - break; - } - } - - let nextMatchIndex = -1; - for(let i = c+2; i < buffer.matchSequence.length; i++) { - if(buffer.matchSequence[i].key == buffer.inputSequence[r+1].key) { - nextMatchIndex = i; - break; - } - } - - if(nextInputIndex > 0 && nextMatchIndex > 0) { - let transpositionCost = value + (nextInputIndex - r - 2) + 1 + (nextMatchIndex - c - 2); - this.propagateUpdateFrom(buffer, nextInputIndex, nextMatchIndex, transpositionCost, (buffer.diagonalWidth - 1) + nextMatchIndex - nextInputIndex); - } - } - } - - get mapKey(): string { - let inputString = this.inputSequence.map((value) => value.key).join(''); - let matchString = this.matchSequence.map((value) => value.key).join(''); - return inputString + models.SENTINEL_CODE_UNIT + matchString + models.SENTINEL_CODE_UNIT + this.diagonalWidth; - } - - get lastInputEntry(): TInput { - return this.inputSequence[this.inputSequence.length-1]; - } - - get lastMatchEntry(): TMatch { - return this.matchSequence[this.matchSequence.length-1]; - } - - static computeDistance, TMatch extends EditToken>( - input: TInput[], - match: TMatch[], - bandSize: number = 1) { - // Initialize the calculation buffer, setting the diagonal width (as appropriate) in advance. - let buffer = new ClassicalDistanceCalculation(); - bandSize = bandSize || 1; - buffer.diagonalWidth = bandSize; - - for(let i = 0; i < input.length; i++) { - buffer = buffer.addInputChar(input[i]); - } - - for(let j = 0; j < match.length; j++) { - buffer = buffer.addMatchChar(match[j]); - } - - return buffer; - } - } -} diff --git a/common/web/lm-worker/src/correction/context-tracker.ts b/common/web/lm-worker/src/correction/context-tracker.ts deleted file mode 100644 index e17e201d785441b51fed3e75ea27779a935d987c..0000000000000000000000000000000000000000 --- a/common/web/lm-worker/src/correction/context-tracker.ts +++ /dev/null @@ -1,539 +0,0 @@ -/// - -namespace correction { - - function textToCharTransforms(text: string, transformId?: number) { - let perCharTransforms: Transform[] = []; - - for(let i=0; i < text.kmwLength(); i++) { - let char = text.kmwCharAt(i); // is SMP-aware - - let transform: Transform = { - insert: char, - deleteLeft: 0, - id: transformId - }; - - perCharTransforms.push(transform); - } - - return perCharTransforms; - } - export class TrackedContextSuggestion { - suggestion: Suggestion; - tokenWidth: number; - } - - export class TrackedContextToken { - raw: string; - replacementText: string; - - transformDistributions: Distribution[] = []; - replacements: TrackedContextSuggestion[]; - activeReplacementId: number = -1; - - get currentText(): string { - if(this.replacementText === undefined || this.replacementText === null) { - return this.raw; - } else { - return this.replacementText; - } - } - - get replacement(): TrackedContextSuggestion { - let replacementId = this.activeReplacementId; - return this.replacements.find(function(replacement) { - return replacement.suggestion.id == replacementId; - }); - } - - revert() { - delete this.activeReplacementId; - } - } - - export class TrackedContextState { - // Stores the source Context (as a debugging reference). Not currently utilized. - taggedContext: Context; - model: LexicalModel; - - tokens: TrackedContextToken[]; - /** - * How many tokens were removed from the start of the best-matching ancestor. - * Useful for restoring older states, e.g., when the user moves the caret backwards, we can recover the context at that position. - */ - indexOffset: number; - - // Tracks all search spaces starting at the current token. - // In the lm-layer's current form, this should only ever have one entry. - // Leaves 'design space' for if/when we add support for phrase-level corrections/predictions. - searchSpace: SearchSpace[] = []; - - constructor(source: TrackedContextState); - constructor(model: LexicalModel); - constructor(obj: TrackedContextState | LexicalModel) { - if(obj instanceof TrackedContextState) { - let source = obj; - // Be sure to deep-copy the tokens! Pointer-aliasing is bad here. - this.tokens = source.tokens.map(function(token) { - let copy = new TrackedContextToken(); - copy.raw = token.raw; - copy.replacements = token.replacements - copy.activeReplacementId = token.activeReplacementId; - copy.transformDistributions = token.transformDistributions; - - if(token.replacementText) { - copy.replacementText = token.replacementText; - } - - return copy; - }); - this.searchSpace = obj.searchSpace; - this.indexOffset = 0; - this.model = obj.model; - } else { - let lexicalModel = obj; - this.tokens = []; - this.indexOffset = Number.MIN_SAFE_INTEGER; - this.model = lexicalModel; - - if(lexicalModel && lexicalModel.traverseFromRoot) { - this.searchSpace = [new SearchSpace(lexicalModel)]; - } - } - } - - get head(): TrackedContextToken { - return this.tokens[0]; - } - - get tail(): TrackedContextToken { - return this.tokens[this.tokens.length - 1]; - } - - popHead() { - this.tokens.splice(0, 2); - this.indexOffset -= 1; - } - - pushTail(token: TrackedContextToken) { - if(this.model && this.model.traverseFromRoot) { - this.searchSpace = [new SearchSpace(this.model)]; // yeah, need to update SearchSpace for compatibility - } else { - this.searchSpace = []; - } - this.tokens.push(token); - - let state = this; - if(state.searchSpace.length > 0) { - token.transformDistributions.forEach(distrib => state.searchSpace[0].addInput(distrib)); - } - } - - pushWhitespaceToTail(transformDistribution: Distribution = null) { - let whitespaceToken = new TrackedContextToken(); - - // Track the Transform that resulted in the whitespace 'token'. - // Will be needed for phrase-level correction/prediction. - whitespaceToken.transformDistributions = transformDistribution ? [transformDistribution] : []; - - whitespaceToken.raw = null; - this.tokens.push(whitespaceToken); - } - - /** - * Used for 14.0's backspace workaround, which flattens all previous Distribution - * entries because of limitations with direct use of backspace transforms. - * @param tokenText - * @param transformId - */ - replaceTailForBackspace(tokenText: USVString, transformId: number) { - this.tokens.pop(); - - // It's a backspace transform; time for special handling! - // - // For now, with 14.0, we simply compress all remaining Transforms for the token into - // multiple single-char transforms. Probabalistically modeling BKSP is quite complex, - // so we simplify by assuming everything remaining after a BKSP is 'true' and 'intended' text. - // - // Note that we cannot just use a single, monolithic transform at this point b/c - // of our current edit-distance optimization strategy; diagonalization is currently... - // not very compatible with that. - let backspacedTokenContext: Distribution[] = textToCharTransforms(tokenText, transformId).map(function(transform) { - return [{sample: transform, p: 1.0}]; - }); - - let compactedToken = new TrackedContextToken(); - compactedToken.raw = tokenText; - compactedToken.transformDistributions = backspacedTokenContext; - this.pushTail(compactedToken); - } - - updateTail(transformDistribution: Distribution, tokenText?: USVString) { - let editedToken = this.tail; - - // Preserve existing text if new text isn't specified. - tokenText = tokenText || (tokenText === '' ? '' : editedToken.raw); - - if(transformDistribution && transformDistribution.length > 0) { - editedToken.transformDistributions.push(transformDistribution); - if(this.searchSpace) { - this.searchSpace.forEach(space => space.addInput(transformDistribution)); - } - } - // Replace old token's raw-text with new token's raw-text. - editedToken.raw = tokenText; - } - - toRawTokenization() { - let sequence: USVString[] = []; - - for(let token of this.tokens) { - // Hide any tokens representing wordbreaks. (Thinking ahead to phrase-level possibilities) - if(token.currentText !== null) { - sequence.push(token.currentText); - } - } - - return sequence; - } - } - - class CircularArray { - static readonly DEFAULT_ARRAY_SIZE = 5; - private circle: Item[]; - private currentHead: number=0; - private currentTail: number=0; - - constructor(size: number = CircularArray.DEFAULT_ARRAY_SIZE) { - this.circle = Array(size); - } - - get count(): number { - let diff = this.currentHead - this.currentTail; - - if(diff < 0) { - diff = diff + this.circle.length; - } - - return diff; - } - - get maxCount(): number { - return this.circle.length; - } - - get oldest(): Item { - if(this.count == 0) { - return undefined; - } - - return this.item(0); - } - - get newest(): Item { - if(this.count == 0) { - return undefined; - } - - return this.item(this.count - 1); - } - - enqueue(item: Item): Item { - var prevItem = null; - let nextHead = (this.currentHead + 1) % this.maxCount; - - if(nextHead == this.currentTail) { - prevItem = this.circle[this.currentTail]; - this.currentTail = (this.currentTail + 1) % this.maxCount; - } - - this.circle[this.currentHead] = item; - this.currentHead = nextHead; - - return prevItem; - } - - dequeue(): Item { - if(this.currentTail == this.currentHead) { - return null; - } else { - let item = this.circle[this.currentTail]; - this.currentTail = (this.currentTail + 1) % this.maxCount; - return item; - } - } - - popNewest(): Item { - if(this.currentTail == this.currentHead) { - return null; - } else { - let item = this.circle[this.currentHead]; - this.currentHead = (this.currentHead - 1 + this.maxCount) % this.maxCount; - return item; - } - } - - /** - * Returns items contained within the circular array, ordered from 'oldest' to 'newest' - - * the same order in which the items will be dequeued. - * @param index - */ - item(index: number) { - if(index >= this.count) { - throw "Invalid array index"; - } - - let mappedIndex = (this.currentTail + index) % this.maxCount; - return this.circle[mappedIndex]; - } - } - - export class ContextTracker extends CircularArray { - static attemptMatchContext(tokenizedContext: USVString[], - matchState: TrackedContextState, - transformDistribution?: Distribution,): TrackedContextState { - // Map the previous tokenized state to an edit-distance friendly version. - let matchContext: USVString[] = matchState.toRawTokenization(); - - // Inverted order, since 'match' existed before our new context. - let mapping = ClassicalDistanceCalculation.computeDistance(matchContext.map(value => ({key: value})), - tokenizedContext.map(value => ({key: value})), - 1); - - let editPath = mapping.editPath(); - - let poppedHead = false; - let pushedTail = false; - - // Matters greatly when starting from a nil context. - if(editPath.length > 1) { - // First entry: may not be an 'insert' or a 'transpose' op. - // 'insert' allowed if the next token is 'substitute', as this may occur with an edit path of length 2. - if((editPath[0] == 'insert' && !(editPath[1] == 'substitute' && editPath.length == 2)) || editPath[0].indexOf('transpose') >= 0) { - return null; - } else if(editPath[0] == 'delete') { - poppedHead = true; // a token from the previous state has been wholly removed. - } - } - - // Last entry: may not be a 'delete' or a 'transpose' op. - let tailIndex = editPath.length -1; - let ignorePenultimateMatch = false; - if(editPath[tailIndex] == 'delete' || editPath[0].indexOf('transpose') >= 0) { - return null; - } else if(editPath[tailIndex] == 'insert') { - pushedTail = true; - } else if(tailIndex > 0 && editPath[tailIndex-1] == 'insert' && editPath[tailIndex] == 'substitute') { - // Tends to happen when accepting suggestions. - pushedTail = true; - ignorePenultimateMatch = true; - } - - // Can happen for the first text input after backspace deletes a wordbreaking character, - // thus the new input continues a previous word while dropping the empty word after - // that prior wordbreaking character. - // - // We can't handle it reliably from this match state, but a previous entry (without the empty token) - // should still be in the cache and will be reliable for this example case. - if(tailIndex > 0 && editPath[tailIndex-1] == 'delete' && editPath[tailIndex] == 'substitute') { - return null; - } - - // Now to check everything in-between: should be exclusively 'match'es. - for(let index = 1; index < editPath.length - (ignorePenultimateMatch ? 2 : 1); index++) { - if(editPath[index] != 'match') { - return null; - } - } - - // If we've made it here... success! We have a context match! - let state: TrackedContextState; - - if(pushedTail) { - // On suggestion acceptance, we should update the previous final token. - // We do it first so that the acceptance is replicated in the new TrackedContextState - // as well. - if(ignorePenultimateMatch) { - // For this case, we were likely called by ModelCompositor.acceptSuggestion(), which - // would have marked the accepted suggestion. - matchState.tail.replacementText = tokenizedContext[tokenizedContext.length-2]; - } - - state = new TrackedContextState(matchState); - } else { - // Since we're continuing a previously-cached context, we can reuse the same SearchSpace - // to continue making predictions. - state = matchState; - } - - const hasDistribution = transformDistribution && Array.isArray(transformDistribution); - let primaryInput = hasDistribution ? transformDistribution[0].sample : null; - if(primaryInput && primaryInput.insert == "" && primaryInput.deleteLeft == 0 && !primaryInput.deleteRight) { - primaryInput = null; - } - - const isWhitespace = primaryInput && TransformUtils.isWhitespace(primaryInput); - const isBackspace = primaryInput && TransformUtils.isBackspace(primaryInput); - const finalToken = tokenizedContext[tokenizedContext.length-1]; - - /* Assumption: This is an adequate check for its two sub-branches. - * - * Basis: - * - Assumption: one keystroke may only cause a single token to rotate out of context. - * - That is, no "reasonable" keystroke would emit enough code points to 'bump' two words simultaneously. - * - ... This one may need to be loosened a bit... but it should be enough for initial correction testing as-is. - * - Assumption: one keystroke may only cause a single token to be appended to the context - * - That is, no "reasonable" keystroke would emit a Transform adding two separate word tokens - * - For languages using whitespace to word-break, said keystroke would have to include said whitespace to break the assumption. - */ - - // If there is/was more than one context token available... - if(editPath.length > 1) { - // We're removing a context token, but at least one remains. - if(poppedHead) { - state.popHead(); - } - - // We're adding an additional context token. - if(pushedTail) { - const tokenizedTail = tokenizedContext[tokenizedContext.length - 1]; - /* - * Common-case: most transforms that trigger this case are from pure-whitespace Transforms. MOST. - * - * Less-common, but noteworthy: some wordbreaks may occur without whitespace. Example: - * `"o` => ['"', 'o']. Make sure to double-check against `tokenizedContext`! - */ - let pushedToken = new TrackedContextToken(); - pushedToken.raw = tokenizedTail; - - if(isWhitespace || !primaryInput) { - state.pushWhitespaceToTail(transformDistribution ?? []); - // Continuing the earlier assumption, that 'pure-whitespace Transform' does not emit any initial characters - // for the new word (token), so the input keystrokes do not correspond to the new text token. - pushedToken.transformDistributions = []; - } else { - state.pushWhitespaceToTail(); - // Assumption: Since we only allow one-transform-at-a-time changes between states, we shouldn't be missing - // any metadata used to construct the new context state token. - pushedToken.transformDistributions = transformDistribution ? [transformDistribution] : []; - } - - state.pushTail(pushedToken); - } else { // We're editing the final context token. - // TODO: Assumption: we didn't 'miss' any inputs somehow. - // As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's. - if(isBackspace) { - state.replaceTailForBackspace(finalToken, primaryInput.id); - } else { - state.updateTail(primaryInput ? transformDistribution : null, finalToken); - } - } - // There is only one word in the context. - } else { - // TODO: Assumption: we didn't 'miss' any inputs somehow. - // As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's. - - if(editPath[tailIndex] == 'insert') { - // Construct appropriate initial token. - let token = new TrackedContextToken(); - token.raw = tokenizedContext[0]; - token.transformDistributions = [transformDistribution]; - state.pushTail(token); - } else { // Edit the lone context token. - // Consider backspace entry for this case? - if(isBackspace) { - state.replaceTailForBackspace(finalToken, primaryInput.id); - } else { - state.updateTail(primaryInput ? transformDistribution : null, finalToken); - } - } - } - return state; - } - - static modelContextState(tokenizedContext: USVString[], - transformDistribution: Distribution, - lexicalModel: LexicalModel): TrackedContextState { - let baseTokens = tokenizedContext.map(function(entry) { - let token = new TrackedContextToken(); - token.raw = entry; - if(token.raw) { - token.transformDistributions = textToCharTransforms(token.raw).map(function(transform) { - return [{sample: transform, p: 1.0}]; - }); - } else { - // Helps model context-final wordbreaks. - token.transformDistributions = []; - } - return token; - }); - - // And now build the final context state object, which includes whitespace 'tokens'. - let state = new TrackedContextState(lexicalModel); - - if(baseTokens.length > 0) { - state.pushTail(baseTokens.splice(0, 1)[0]); - } - - while(baseTokens.length > 0) { - state.pushWhitespaceToTail(); - state.pushTail(baseTokens.splice(0, 1)[0]); - } - - if(state.tokens.length == 0) { - let token = new TrackedContextToken(); - token.raw = ''; - - state.pushTail(token); - } - - return state; - } - - /** - * Compares the current, post-input context against the most recently-seen contexts from previous prediction calls, returning - * the most information-rich `TrackedContextState` possible. If a match is found, the state will be annotated with the - * input information provided to previous prediction calls and persisted correction-search calculations for re-use. - * - * @param model - * @param context - * @param transformDistribution - */ - analyzeState(model: LexicalModel, - context: Context, - transformDistribution?: Distribution): TrackedContextState { - if(!model.traverseFromRoot) { - // Assumption: LexicalModel provides a valid traverseFromRoot function. (Is technically optional) - // Without it, no 'corrections' may be made; the model can only be used to predict, not correct. - throw "This lexical model does not provide adequate data for correction algorithms and context reuse"; - } - - let tokenizedContext = models.tokenize(model.wordbreaker || wordBreakers.default, context); - - if(tokenizedContext.left.length > 0) { - for(let i = this.count - 1; i >= 0; i--) { - let resultState = ContextTracker.attemptMatchContext(tokenizedContext.left, this.item(i), transformDistribution); - - if(resultState) { - resultState.taggedContext = context; - if(resultState != this.item(i)) { - this.enqueue(resultState); - } - return resultState; - } - } - } - - // Else: either empty OR we've detected a 'new context'. Initialize from scratch; no prior input information is - // available. Only the results of the prior inputs are known. - // - // Assumption: as a caret needs to move to context before any actual transform distributions occur, - // this state is only reached on caret moves; thus, transformDistribution is actually just a single null transform. - let state = ContextTracker.modelContextState(tokenizedContext.left, transformDistribution, model); - state.taggedContext = context; - this.enqueue(state); - return state; - } - } -} diff --git a/common/web/lm-worker/src/correction/distance-modeler.ts b/common/web/lm-worker/src/correction/distance-modeler.ts deleted file mode 100644 index 723cbc93ca045f299f1b140ec400a2a8a84f51ad..0000000000000000000000000000000000000000 --- a/common/web/lm-worker/src/correction/distance-modeler.ts +++ /dev/null @@ -1,805 +0,0 @@ -/// - -namespace correction { - type RealizedInput = ProbabilityMass[]; // NOT Distribution - they're masses from separate distributions. - - export type TraversableToken = { - key: TUnit, - traversal: LexiconTraversal - } - - export const QUEUE_NODE_COMPARATOR: models.Comparator = function(arg1, arg2) { - return arg1.currentCost - arg2.currentCost; - } - - // Represents a processed node for the correction-search's search-space's tree-like graph. May represent - // internal and 'leaf' nodes on said graph, as well as the overall root of the search. Also used to represent - // edges on the graph TO said nodes - there's a bit of overloading here. Either way, it stores the cost of the - // optimum path used to reach the ndoe. - // - // The stored path cost may be an overestimate when the edit distance is greater than the current search threshold. The - // first version of the node to be dequeued from SearchSpace's priority queue hierarchy 'wins' and is taken as the absolute - // minimum; subsequent versions are ignored as suboptimal. - // - // Provides functions usable to enumerate across the node's outward edges to new nodes for continued search. - // Most of the actual calculations occur as part of this process. - // - // For nodes with raw edit-distance cost within the current threshold for correction searches, we do have admissibility. - // If not enough nodes are available within that threshold, however, admissibility may be lost, leaving our search as a - // heuristic. - // - export class SearchNode { - calculation: ClassicalDistanceCalculation, TraversableToken>; - - currentTraversal: LexiconTraversal; - toKey: (wordform: USVString) => USVString = str => str; - priorInput: RealizedInput; - - // Internal lazy-cache for .inputSamplingCost, as it's a bit expensive to re-compute. - private _inputCost?: number; - - constructor(rootTraversal: LexiconTraversal, toKey?: (USVString) => USVString); - constructor(node: SearchNode); - constructor(rootTraversal: LexiconTraversal | SearchNode, toKey?: (USVString) => USVString) { - toKey = toKey || (x => x); - - if(rootTraversal instanceof SearchNode) { - let priorNode = rootTraversal; - this.calculation = priorNode.calculation; - this.currentTraversal = priorNode.currentTraversal; - this.priorInput = priorNode.priorInput; - this.toKey = priorNode.toKey; - } else { - this.calculation = new ClassicalDistanceCalculation(); - this.currentTraversal = rootTraversal; - this.priorInput = []; - this.toKey = toKey; - } - } - - get knownCost(): number { - return this.calculation.getHeuristicFinalCost(); - } - - get inputSamplingCost(): number { - if(this._inputCost !== undefined) { - return this._inputCost; - } else { - let MIN_P = SearchSpace.MIN_KEYSTROKE_PROBABILITY; - // Should technically re-normalize the sampling distribution. - // -ln(p) is smaller for larger probabilities, as ln(p) is always <= 0. Approaches infinity as p => 0. - - // TODO: probably more efficient to instead use actual 'probability space'... but that'll involve extra changes. - this._inputCost = this.priorInput.map(mass => mass.p > MIN_P ? mass.p : MIN_P).reduce((previous, current) => previous - Math.log(current), 0); - return this._inputCost; - } - } - - // The part used to prioritize our search. - get currentCost(): number { - // - We reintrepret 'known cost' as a psuedo-probability. - // - Noting that 1/e = 0.367879441, an edit-distance cost of 1 may be intepreted as -ln(1/e) - a log-space 'likelihood'. - // - Not exactly normalized, though. - // That's a really, really high likelihood, thoough. - // - // At any rate, we can linearly scale the known-cost to have about whatever probability we want. - // If we can state it as p = 1 / (c * e), note then that ln(c * e) = ln(c) + 1. So, scale * (ln(c) + 1). - // If we can state it as e^x, note that ln(e^x) = x * ln(e) = x - just scale by 'x'! - - // p = 1 / (e^4) = 0.01831563888. This still exceeds many neighboring keys! - // p = 1 / (e^5) = 0.00673794699. Strikes a good balance. - // Should easily give priority to neighboring keys before edit-distance kicks in (when keys are a bit ambiguous) - return SearchSpace.EDIT_DISTANCE_COST_SCALE * this.knownCost + this.inputSamplingCost; - } - - buildInsertionEdges(): SearchNode[] { - let edges: SearchNode[] = []; - - for(let lexicalChild of this.currentTraversal.children()) { - let traversal = lexicalChild.traversal(); - let matchToken = { - key: lexicalChild.char, - traversal: traversal - } - - let childCalc = this.calculation.addMatchChar(matchToken); - - let searchChild = new SearchNode(this); - searchChild.calculation = childCalc; - searchChild.priorInput = this.priorInput; - searchChild.currentTraversal = traversal; - - edges.push(searchChild); - } - - return edges; - } - - buildDeletionEdges(inputDistribution: Distribution): SearchNode[] { - let edges: SearchNode[] = []; - - /* - * If the probability of an input is less than the highest probability * the base edit-distance likelihood, - * don't build an edge for it; just rely on edits from the highest-probability edge. - * - * We may be able to be stricter, but this should be a decent start. - * - * Note: thanks to ModelCompositor.predict, we know the distribution is pre-sorted. - */ - for(let probMass of inputDistribution) { - if(probMass.p < inputDistribution[0].p * Math.exp(-SearchSpace.EDIT_DISTANCE_COST_SCALE)) { - // Again, we're pre-sorted. All further entries will be too low-cost to consider. - break; - } - - let edgeCalc = this.calculation; - let transform = probMass.sample; - if(transform.deleteLeft) { - edgeCalc = edgeCalc.getSubset(edgeCalc.inputSequence.length - transform.deleteLeft, edgeCalc.matchSequence.length); - } - - // TODO: transform.deleteRight currently not supported. - - let inputPath = Array.from(this.priorInput); - inputPath.push(probMass); - // Tokenize and iterate over input chars, adding them into the calc. - for(let i=0; i < transform.insert.length; i++) { - let char = transform.insert[i]; - if(models.isHighSurrogate(char)) { - i++; - char = char + transform.insert[i]; - } - - // In case of NFD input, filter out any empty-strings that may arise - // when 'keying' raw diacritics. - let keyedChar = this.toKey(char); - if(keyedChar) { - edgeCalc = edgeCalc.addInputChar({key: keyedChar}); - } - } - - let childEdge = new SearchNode(this); - childEdge.calculation = edgeCalc; - childEdge.priorInput = inputPath; - - edges.push(childEdge); - } - - return edges; - } - - // While this may SEEM to be unnecessary, note that sometimes substitutions (which are computed - // via insert + delete) may be lower cost than both just-insert and just-delete. - buildSubstitutionEdges(inputDistribution: Distribution): SearchNode[] { - // Handles the 'input' component. - let intermediateEdges = this.buildDeletionEdges(inputDistribution); - let edges: SearchNode[] = []; - - for(let lexicalChild of this.currentTraversal.children()) { - for(let edge of intermediateEdges) { - let traversal = lexicalChild.traversal(); - let matchToken = { - key: lexicalChild.char, - traversal: traversal - } - - let childCalc = edge.calculation.addMatchChar(matchToken); - - let searchChild = new SearchNode(this); - searchChild.calculation = childCalc; - searchChild.priorInput = edge.priorInput; - searchChild.currentTraversal = traversal; - - edges.push(searchChild); - } - } - - return edges; - } - - get mapKey(): string { - let inputString = this.priorInput.map((value) => '+' + value.sample.insert + '-' + value.sample.deleteLeft).join(''); - let matchString = this.calculation.matchSequence.map((value) => value.key).join(''); - - // TODO: might should also track diagonalWidth. - return inputString + models.SENTINEL_CODE_UNIT + matchString; - } - - get isFullReplacement(): boolean { - // If the known edit-distance cost is equal to the input length, this means - // that literally every input has been full-on replaced. Thus, this is - // likely not a good 'root' to use for predictions. - // - // Logic exception: 0 cost, 0 length != a "replacement". - return this.knownCost && this.knownCost == this.priorInput.length; - } - } - - class SearchSpaceTier { - correctionQueue: models.PriorityQueue; - processed: SearchNode[] = []; - index: number; - - constructor(index: number, initialEdges?: SearchNode[]) { - this.index = index; - this.correctionQueue = new models.PriorityQueue(QUEUE_NODE_COMPARATOR, initialEdges); - } - - increaseMaxEditDistance() { - // By extracting the entries from the priority queue and increasing distance outside of it as a batch job, - // we get an O(N) implementation, rather than the O(N log N) that would result from maintaining the original queue. - let entries = this.correctionQueue.toArray(); - - entries.forEach(function(edge) { edge.calculation = edge.calculation.increaseMaxDistance(); }); - - // Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify. - this.correctionQueue = new models.PriorityQueue(QUEUE_NODE_COMPARATOR, entries); - } - } - - export class SearchResult { - private resultNode: SearchNode; - - constructor(node: SearchNode) { - this.resultNode = node; - } - - get inputSequence(): ProbabilityMass[] { - return this.resultNode.priorInput; - } - - get matchSequence(): TraversableToken[] { - return this.resultNode.calculation.matchSequence; - }; - - get matchString(): USVString { - return this.matchSequence.map(value => value.key).join(''); - } - - get knownCost(): number { - return this.resultNode.knownCost; - } - - get inputSamplingCost(): number { - return this.resultNode.inputSamplingCost; - } - - get totalCost(): number { - return this.resultNode.currentCost; - } - - get finalTraversal(): LexiconTraversal { - return this.resultNode.currentTraversal; - } - } - - type NullPath = { - type: 'none' - } - - type IntermediateSearchPath = { - type: 'intermediate', - cost: number - } - - type CompleteSearchPath = { - type: 'complete', - cost: number, - finalNode: SearchNode - } - - type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath; - - // The set of search spaces corresponding to the same 'context' for search. - // Whenever a wordbreak boundary is crossed, a new instance should be made. - export class SearchSpace { - private QUEUE_SPACE_COMPARATOR: models.Comparator; - - // p = 1 / (e^4) = 0.01831563888. This still exceeds many neighboring keys! - // p = 1 / (e^5) = 0.00673794699. Strikes a good balance. - // Should easily give priority to neighboring keys before edit-distance kicks in (when keys are a bit ambiguous) - static readonly EDIT_DISTANCE_COST_SCALE = 5; - static readonly MIN_KEYSTROKE_PROBABILITY = 0.0001; - static readonly DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL = 33; // in milliseconds. - - private tierOrdering: SearchSpaceTier[] = []; - private selectionQueue: models.PriorityQueue; - private inputSequence: Distribution[] = []; - private minInputCost: number[] = []; - private rootNode: SearchNode; - - // We use an array and not a PriorityQueue b/c batch-heapifying at a single point in time - // is cheaper than iteratively building a priority queue. - private completedPaths: SearchNode[]; - - // Marks all results that have already been returned since the last input was received. - private returnedValues: {[mapKey: string]: SearchNode} = {}; - - // Signals that the edge has already been processed. - private processedEdgeSet: {[mapKey: string]: boolean} = {}; - - constructor(model: LexicalModel) { - if(!model) { - throw "The LexicalModel parameter must not be null / undefined."; - } else if(!model.traverseFromRoot) { - throw "The provided model does not implement the `traverseFromRoot` function, which is needed to support robust correction searching."; - } - - // Constructs the comparator needed for the following line. - this.buildQueueSpaceComparator(); - this.selectionQueue = new models.PriorityQueue(this.QUEUE_SPACE_COMPARATOR); - this.rootNode = new SearchNode(model.traverseFromRoot(), model.toKey ? model.toKey.bind(model) : null); - - this.completedPaths = [this.rootNode]; - - // Adds a base level queue to handle initial insertions. - // Start with _just_ the root node. Necessary for proper empty-token, empty-input handling! - let baseTier = new SearchSpaceTier(0, [this.rootNode]); - this.tierOrdering.push(baseTier); - this.selectionQueue.enqueue(baseTier); - } - - private buildQueueSpaceComparator() { - let searchSpace = this; - - this.QUEUE_SPACE_COMPARATOR = function(space1, space2) { - let node1 = space1.correctionQueue.peek(); - let node2 = space2.correctionQueue.peek(); - - let index1 = space1.index; - let index2 = space2.index; - - let tierMinCost: number = 0; - let sign = 1; - - if(index2 < index1) { - let temp = index2; - index2 = index1; - index1 = temp; - - sign = -1; - } - - // Boost the cost of the lower tier by the minimum cost possible for the missing inputs between them. - // In essence, compare the nodes as if the lower tier had the most likely input appended for each such - // input missing at the lower tier. - // - // A 100% admissible heuristic to favor a deeper search, since the added cost is guaranteed if the path - // is traversed further. - // - // Remember, tier index i's last used input was from input index i-1. - // As a result, i is the first needed input index, with index2 - 1 the last entry needed to match them. - for(let i=index1; i < index2; i++) { - tierMinCost = tierMinCost + searchSpace.minInputCost[i]; - } - - // Guards, just in case one of the search spaces ever has an empty node. - if(node1 && node2) { - // If node1 is lower-tier, node1 is the one in need of boosted cost. - // `sign` flips it when node2 is lower tier. - return node1.currentCost - node2.currentCost + sign * tierMinCost; - } else if(node2) { - return 1; - } else { - return -1; - } - } - } - - increaseMaxEditDistance() { - this.tierOrdering.forEach(function(tier) { tier.increaseMaxEditDistance() }); - } - - addInput(inputDistribution: Distribution) { - this.inputSequence.push(inputDistribution); - - // Assumes that `inputDistribution` is already sorted. - this.minInputCost.push(-Math.log(inputDistribution[0].p)); - - // With a newly-available input, we can extend new input-dependent paths from - // our previously-reached 'extractedResults' nodes. - let newlyAvailableEdges: SearchNode[] = []; - let batches = this.completedPaths.map(function(node) { - let deletions = node.buildDeletionEdges(inputDistribution); - let substitutions = node.buildSubstitutionEdges(inputDistribution); - - return deletions.concat(substitutions); - }); - - // Don't forget to reset the array; the contained nodes no longer reach the search's end. - this.completedPaths = []; - this.returnedValues = {}; - - batches.forEach(function(batch) { - newlyAvailableEdges = newlyAvailableEdges.concat(batch); - }); - - // Now that we've built the new edges, we can efficiently construct the new search tier. - let tier = new SearchSpaceTier(this.tierOrdering.length, newlyAvailableEdges); - this.tierOrdering.push(tier); - this.selectionQueue.enqueue(tier); - } - - // TODO: will want eventually for reversions and/or backspaces - removeLastInput() { - // 1. truncate all entries from that search tier; we need to 'restore' extractedResults to match - // the state that would have existed without the last search tier. - // 2. remove the last search tier. Which may necessitate reconstructing the tier queue, but oh well. - } - - private hasNextMatchEntry(): boolean { - let topQueue = this.selectionQueue.peek(); - if(topQueue) { - return topQueue.correctionQueue.count > 0; - } else { - return false; - } - } - - private handleNextNode(): PathResult { - if(!this.hasNextMatchEntry()) { - return { type: 'none' }; - } - - let bestTier = this.selectionQueue.dequeue(); - let currentNode = bestTier.correctionQueue.dequeue(); - - let unmatchedResult: IntermediateSearchPath = { - type: 'intermediate', - cost: currentNode.currentCost - } - - // Have we already processed a matching edge? If so, skip it. - // We already know the previous edge is of lower cost. - if(this.processedEdgeSet[currentNode.mapKey]) { - this.selectionQueue.enqueue(bestTier); - return unmatchedResult; - } else { - this.processedEdgeSet[currentNode.mapKey] = true; - } - - // Stage 1: filter out nodes/edges we want to prune - - // Forbid a raw edit-distance of greater than 2. - // Note: .knownCost is not scaled, while its contribution to .currentCost _is_ scaled. - let substitutionsOnly = false; - if(currentNode.knownCost > 2) { - return unmatchedResult; - } else if(currentNode.knownCost == 2) { - // Hard restriction: no further edits will be supported. This helps keep the search - // more narrowly focused. - substitutionsOnly = true; - } - - let tierMinCost = 0; - for(let i = 0; i <= bestTier.index; i++) { - tierMinCost += this.minInputCost[i]; - } - - // Thresholds _any_ path, partially based on currently-traversed distance. - // Allows a little 'wiggle room' + 2 "hard" edits. - // Can be important if needed characters don't actually exist on the keyboard - // ... or even just not the then-current layer of the keyboard. - if(currentNode.currentCost > tierMinCost + 2.5 * SearchSpace.EDIT_DISTANCE_COST_SCALE) { - return unmatchedResult; - } - - // Stage 2: build remaining edges - - // Always possible, as this does not require any new input. - if(!substitutionsOnly) { - let insertionEdges = currentNode.buildInsertionEdges(); - bestTier.correctionQueue.enqueueAll(insertionEdges); - } - - if(bestTier.index == this.tierOrdering.length - 1) { - // It was the final tier - store the node for future reference. - this.completedPaths.push(currentNode); - - // Since we don't modify any other tier, we may simply reinsert the removed tier. - this.selectionQueue.enqueue(bestTier); - - return { - type: 'complete', - cost: currentNode.currentCost, - finalNode: currentNode - }; - } else { - // Time to construct new edges for the next tier! - let nextTier = this.tierOrdering[bestTier.index+1]; - - let inputIndex = nextTier.index; - - let deletionEdges = []; - if(!substitutionsOnly) { - deletionEdges = currentNode.buildDeletionEdges(this.inputSequence[inputIndex-1]); - } - let substitutionEdges = currentNode.buildSubstitutionEdges(this.inputSequence[inputIndex-1]); - - // Note: we're live-modifying the tier's cost here! The priority queue loses its guarantees as a result. - nextTier.correctionQueue.enqueueAll(deletionEdges.concat(substitutionEdges)); - - // So, we simply rebuild the selection queue. - this.selectionQueue = new models.PriorityQueue(this.QUEUE_SPACE_COMPARATOR, this.tierOrdering); - - // We didn't reach an end-node, so we just end the iteration and continue the search. - } - - // If we've somehow fully exhausted all search options, indicate that none remain. - return unmatchedResult; - } - - // Current best guesstimate of how compositor will retrieve ideal corrections. - *getBestMatches(waitMillis?: number): Generator { - // might should also include a 'base cost' parameter of sorts? - let searchSpace = this; - let currentReturns: {[mapKey: string]: SearchNode} = {}; - - let maxTime: number; - if(waitMillis == 0) { - maxTime = Infinity; - } else if(waitMillis == undefined || Number.isNaN(waitMillis)) { // also covers null. - maxTime = SearchSpace.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL; - } else { - maxTime = waitMillis; - } - - /** - * This inner class is designed to help the algorithm detect its active execution time. - * While there's no official JS way to do this, we can approximate it by polling the - * current system time (in ms) after each iteration of a short-duration loop. Unusual - * spikes in system time for a single iteration is likely to indicate that an OS - * context switch occurred at some point during the iteration's execution. - */ - class ExecutionTimer { - /** - * The system time when this instance was created. - */ - private start: number; - - /** - * Marks the system time at the start of the currently-running loop, as noted - * by a call to the `startLoop` function. - */ - private loopStart: number; - - private maxExecutionTime: number; - private maxTrueTime: number; - - private executionTime: number; - - /** - * Used to track intervals in which potential context swaps by the OS may - * have occurred. Context switches generally seem to pause threads for - * at least 16 ms, while we expect each loop iteration to complete - * within just 1 ms. So, any possible context switch should have the - * longest observed change in system time. - * - * See `updateOutliers` for more details. - */ - private largestIntervals: number[] = [0]; - - constructor(maxExecutionTime: number, maxTrueTime: number) { - // JS measures time by the number of milliseconds since Jan 1, 1970. - this.loopStart = this.start = Date.now(); - this.maxExecutionTime = maxExecutionTime; - this.maxTrueTime = maxTrueTime; - } - - startLoop() { - this.loopStart = Date.now(); - } - - markIteration() { - const now = Date.now(); - const delta = now - this.loopStart; - this.executionTime += delta; - - /** - * Update the list of the three longest system-time intervals observed - * for execution of a single loop iteration. - * - * Ignore any zero-ms length intervals; they'd make the logic much - * messier than necessary otherwise. - */ - if(delta) { - // If the currently-observed interval is longer than the shortest of the 3 - // previously-observed longest intervals, replace it. - if(this.largestIntervals.length > 2 && delta > this.largestIntervals[0]) { - this.largestIntervals[0] = delta; - } else { - this.largestIntervals.push(delta); - } - // Puts the list in ascending order. Shortest of the list becomes the head, - // longest one the tail. - this.largestIntervals.sort(); - - // Then, determine if we need to update our outlier-based tweaks. - this.updateOutliers(); - } - } - - updateOutliers() { - /* Base assumption: since each loop of the search should evaluate within ~1ms, - * notably longer execution times are probably context switches. - * - * Base assumption: OS context switches generally last at least 16ms. (Based on - * a window.setTimeout() usually not evaluating for at least - * that long, even if set to 1ms.) - * - * To mitigate these assumptions: we'll track the execution time of every loop - * iteration. If the longest observation somehow matches or exceeds the length of - * the next two almost-longest observations twice over... we have a very strong - * 'context switch' candidate. - * - * Or, in near-formal math/stats: we expect a very low variance in execution - * time among the iterations of the search's loops. With a very low variance, - * ANY significant proportional spikes in execution time are outliers - outliers - * likely caused by an OS context switch. - * - * Rather than do intensive math, we use a somewhat lazy approach below that - * achieves the same net results given our assumptions, even when relaxed somewhat. - * - * The logic below relaxes the base assumptions a bit to be safe: - * - [2ms, 2ms, 8ms] will cause 8ms to be seen as an outlier. - * - [2ms, 3ms, 10ms] will cause 10ms to be seen as an outlier. - * - * Ideally: - * - [1ms, 1ms, 4ms] will view 4ms as an outlier. - * - * So we can safely handle slightly longer average intervals and slightly shorter - * OS context-switch time intervals. - */ - if(this.largestIntervals.length > 2) { - // Precondition: the `largestIntervals` array is sorted in ascending order. - // Shortest entry is at the head, longest at the tail. - if(this.largestIntervals[2] >= 2 * (this.largestIntervals[0] + this.largestIntervals[1])) { - this.executionTime -= this.largestIntervals[2]; - this.largestIntervals.pop(); - } - } - } - - shouldTimeout(): boolean { - const now = Date.now(); - if(now - this.start > this.maxTrueTime) { - return true; - } - - return this.executionTime > this.maxExecutionTime; - } - - resetOutlierCheck() { - this.largestIntervals = []; - } - } - - class BatchingAssistant { - currentCost = Number.MIN_SAFE_INTEGER; - entries: SearchResult[] = []; - - checkAndAdd(entry: SearchNode): SearchResult[] | null { - var result: SearchResult[] = null; - - if(entry.currentCost > this.currentCost) { - result = this.tryFinalize(); - - this.currentCost = entry.currentCost; - } - - // Filter out any duplicated match sequences. The same match sequence may be reached via - // different input sequences, after all. - let outputMapKey = entry.calculation.matchSequence.map(value => value.key).join(''); - - // First, ensure the edge has an existing 'shared' cache entry. - if(!searchSpace.returnedValues[outputMapKey]) { - searchSpace.returnedValues[outputMapKey] = entry; - } - - // Check the generator's local returned-value cache - this determines whether or not we - // need to add a new 'return' to the batch. - if(!currentReturns[outputMapKey]) { - this.entries.push(new SearchResult(entry)); - currentReturns[outputMapKey] = entry; - } - - return result; - } - - tryFinalize(): SearchResult[] | null { - var result: SearchResult[] = null; - if(this.entries.length > 0) { - result = this.entries; - this.entries = []; - } - - return result; - } - } - - let batcher = new BatchingAssistant(); - - const timer = new ExecutionTimer(maxTime*1.5, maxTime); - - // Stage 1 - if we already have extracted results, build a queue just for them and iterate over it first. - let returnedValues = Object.values(this.returnedValues); - if(returnedValues.length > 0) { - let preprocessedQueue = new models.PriorityQueue(QUEUE_NODE_COMPARATOR, returnedValues); - - // Build batches of same-cost entries. - timer.startLoop(); - while(preprocessedQueue.count > 0) { - let entry = preprocessedQueue.dequeue(); - - // Is the entry a reasonable result? - if(entry.isFullReplacement) { - // If the entry's 'match' fully replaces the input string, we consider it - // unreasonable and ignore it. - continue; - } - - let batch = batcher.checkAndAdd(entry); - timer.markIteration(); - - if(batch) { - // Do not track yielded time. - yield batch; - } - } - - // As we only return a batch once all entries of the same cost have been processed, we can safely - // finalize the last preprocessed group without issue. - let batch = batcher.tryFinalize(); - if(batch) { - // Do not track yielded time. - yield batch; - } - } - - // Stage 2: the fun part; actually searching! - timer.resetOutlierCheck(); - timer.startLoop(); - let timedOut = false; - do { - let newResult: PathResult; - - // Search for a 'complete' path, skipping all partial paths as long as time remains. - do { - newResult = this.handleNextNode(); - timer.markIteration(); - - if(timer.shouldTimeout()) { - timedOut = true; - } - } while(!timedOut && newResult.type == 'intermediate') - - // TODO: check 'cost' on intermediate, running it through batcher to early-detect cost changes. - let batch: SearchResult[]; - if(newResult.type == 'none') { - break; - } else if(newResult.type == 'complete') { - // Is the entry a reasonable result? - if(newResult.finalNode.isFullReplacement) { - // If the entry's 'match' fully replaces the input string, we consider it - // unreasonable and ignore it. Also, if we've reached this point... - // we can(?) assume that everything thereafter is as well. - break; - } - batch = batcher.checkAndAdd(newResult.finalNode); - } - - if(batch) { - yield batch; - } - } while(!timedOut && this.hasNextMatchEntry()); - - // If we _somehow_ exhaust all search options, make sure to return the final results. - let batch = batcher.tryFinalize(); - if(batch) { - yield batch; - } - - return null; - } - } -} diff --git a/common/web/lm-worker/src/main/correction/classical-calculation.ts b/common/web/lm-worker/src/main/correction/classical-calculation.ts new file mode 100644 index 0000000000000000000000000000000000000000..50ddf935513a654a115bc361d5e72dd874c0e823 --- /dev/null +++ b/common/web/lm-worker/src/main/correction/classical-calculation.ts @@ -0,0 +1,572 @@ +import { SENTINEL_CODE_UNIT } from '@keymanapp/models-templates'; + +type EditOperation = 'insert' | 'delete' | 'match' | 'substitute' | 'transpose-start' | 'transpose-end' | 'transpose-insert' | 'transpose-delete'; + +/** + * Represents the lowest-level unit for comparison during edit-distance calculations. + */ +export interface EditToken { + key: TUnit; +} + +// A semi-optimized 'online'/iterative Damerau-Levenshtein calculator with the following features: +// - may add new character to the 'input' string or to the 'match' string, reusing all old calculations efficiently. +// - allows a 'focused' evaluation that seeks if the edit distance is within a specific range. Designed for use in match-searching, +// where we want to find the 'closest' matching strings in a lexicon. +// - towards such a match-searching algorithm/heuristic: should nothing be found within that range, all prior calculations may be reused +// to search across the lexicon with an incremented edit distance. +// - minimized memory footprint: O(m) memory footprint (where m = length of 'input' string), rather than O(mn) (where n = length of 'match' string) +// - guaranteed to use a smaller footprint than DiagonalizedIterativeDamerauLevenshteinCalculation. +// +// In short: Used to optimize calculations for low edit-distance checks, then expanded if/as necessary +// if a greater edit distance is requested. +// +// Reference: https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm#Possible_modifications +// - Motivating statement: "if we are only interested in the distance if it is smaller than a threshold..." +export class ClassicalDistanceCalculation = EditToken, TMatch extends EditToken = EditToken> { + /** + * Stores ONLY the computed diagonal elements, nothing else. + * + * Mapped as seen in the example below (with a diagonal of width 1): + * ``` + * MAX | MAX | MAX | MAX | MAX | ... + * MAX | 0 | 1 | 2 | 3 | ... > + * MAX | 1 | a | b | - | ... ====>> | - | a | b | + * MAX | 2 | c | d | e | ... > | c | d | e | + * MAX | 3 | - | f | g | ... | f | g | ... | + * ... | ... | ... | ... | ... | ... | ... | ... | ... | + * ``` + * + * Any "`-`" entries are undefined, as they lie outside of the diagonal under consideration. + * + * Things of note: + * - The entry where row index = col index will always lie at the center of the row's array. + * - For each +1 increase in row index, the row's entries are (logically) shifted by -1 in order to make this happen. + * - As all of the MAX entries and numerical entries above are fixed, known values, they are not represented here. + */ + resolvedDistances: number[][]; + /** + * Specifies how far off-diagonal calculations should be performed. A value of 0 only evaluates cells with matching + * row and column indicies. + * + * The resulting value from .getFinalCost() is only guaranteed correct if it is less than or equal to this value. + * Otherwise, this object represents a heuristic that _may_ overestimate the true edit distance. Note that it will + * never underestimate. + */ + diagonalWidth: number = 2; // TODO: Ideally, should start at 1... but we'll start at 2 for now + // as a naive workaround for multi-char transform limitations. + + // The sequence of characters input so far. + inputSequence: TInput[] = []; + matchSequence: TMatch[] = []; + + constructor(); + constructor(other: ClassicalDistanceCalculation); + constructor(other?: ClassicalDistanceCalculation) { + if(other) { + // Clone class properties. + let rowCount = other.resolvedDistances.length; + this.resolvedDistances = Array(rowCount); + + for(let r = 0; r < rowCount; r++) { + this.resolvedDistances[r] = Array.from(other.resolvedDistances[r]); + } + + this.inputSequence = Array.from(other.inputSequence); + this.matchSequence = Array.from(other.matchSequence); + this.diagonalWidth = other.diagonalWidth; + } else { + this.resolvedDistances = []; + } + } + + private getTrueIndex(r: number, c: number, width: number): {row: number, col: number, sparse: boolean} { + let retVal = { + row: r, + col: c - r + width, + sparse: false + } + + if(retVal.col < 0 || retVal.col > 2 * width) { + retVal.sparse = true; + } + + return retVal; + } + + private getCostAt(i: number, j: number, width: number = this.diagonalWidth): number { + // Check for and handle the set of fixed-value virtualized indices. + if(i < 0 || j < 0) { + if(i == -1 && j >= -1) { + return j+1; + } else if(j == -1 && i >= -1) { + return i+1; + } + + return Number.MAX_VALUE; + } + + let index = this.getTrueIndex(i, j, width); + return index.sparse ? Number.MAX_VALUE : this.resolvedDistances[index.row][index.col]; + } + + /** + * Noting the above link's statement prefixed "By examining diagonals instead of rows, and by using lazy evaluation...", + * this function will return the actual edit distance between the strings, temporarily increasing the computed + * diagonal's size if necessary. + * + * Does not actually mutate the instance. + */ + getFinalCost(): number { + let buffer = this as ClassicalDistanceCalculation; + let val = buffer.getHeuristicFinalCost(); + + while(val > buffer.diagonalWidth) { + // A consequence of treating this class as immutable. + buffer = buffer.increaseMaxDistance(); + val = buffer.getHeuristicFinalCost(); + } + + return val; + } + + /** + * Returns this instance's computed edit distance. If greater than the diagonal's width value, note that it may be an overestimate. + */ + getHeuristicFinalCost(): number { + return this.getCostAt(this.inputSequence.length-1, this.matchSequence.length-1); + } + + /** + * Returns `true` if the represented edit distance is less than or equal to the specified threshold, minimizing the amount of calculations + * needed to meet the specified limit. + * + * Does not mutate the instance. + * @param threshold + */ + hasFinalCostWithin(threshold: number): boolean { + let buffer = this as ClassicalDistanceCalculation; + let val = buffer.getHeuristicFinalCost(); + let guaranteedBound = this.diagonalWidth; + + do { + // val will never exceed the length of the longer string, no matter how large the threshold. + if(val <= threshold) { + return true; + } else if(guaranteedBound < threshold) { + buffer = buffer.increaseMaxDistance(); + guaranteedBound++; + val = buffer.getHeuristicFinalCost(); + } else { + break; + } + } while(true); + + return false; + } + + /** + * Determines the edit path used to obtain the optimal cost, distinguishing between zero-cost + * substitutions ('match' operations) and actual substitutions. + * @param row + * @param col + */ + public editPath(row: number = this.inputSequence.length - 1, col: number = this.matchSequence.length - 1): EditOperation[] { + let currentCost = this.getCostAt(row, col); + let ops: EditOperation[] = null; + let parent: [number, number] = null; + + let insertParentCost = this.getCostAt(row, col-1); + let deleteParentCost = this.getCostAt(row-1, col); + let substitutionParentCost = this.getCostAt(row-1, col-1); + let [lastInputIndex, lastMatchIndex] = ClassicalDistanceCalculation.getTransposeParent(this, row, col); + if(lastInputIndex >= 0 && lastMatchIndex >= 0) { + // OK, a transposition source is quite possible. Still need to do more vetting, to be sure. + let expectedCost = 1; + + // This transposition includes either 'transpose-insert' or 'transpose-delete' operations. + ops = ['transpose-start']; // always needs a 'start'. + if(lastInputIndex != row-1) { + let count = row - lastInputIndex - 1; + ops = ops.concat( Array(count).fill('transpose-delete') ); + expectedCost += count; + } else { + let count = col - lastMatchIndex - 1; + ops = ops.concat( Array(count).fill('transpose-insert') ); + expectedCost += count; + } + ops.push('transpose-end'); + + // Double-check our expectations. + if(this.getCostAt(lastInputIndex-1, lastMatchIndex-1) != currentCost - expectedCost) { + ops = null; + } + parent = [lastInputIndex-1, lastMatchIndex-1]; + } + + if(ops) { + // bypass the ladder. + } else if(substitutionParentCost == currentCost - 1) { + ops = ['substitute']; + parent = [row-1, col-1]; + } else if(insertParentCost == currentCost - 1) { + ops = ['insert']; + parent = [row, col-1]; + } else if(deleteParentCost == currentCost - 1) { + ops = ['delete']; + parent = [row-1, col]; + } else { //if(substitutionParentCost == currentCost) { + ops = ['match']; + parent = [row-1, col-1]; + } + + // Recursively build the edit path. + if(parent[0] >= 0 && parent[1] >= 0) { + return this.editPath(parent[0], parent[1]).concat(ops); + } else { + if(parent[0] > -1) { + // There are initial deletions. + return Array(parent[0]+1).fill('delete').concat(ops); + } else if(parent[1] > -1) { + // There are initial insertions. + return Array(parent[1]+1).fill('insert').concat(ops); + } else { + return ops; + } + } + } + + private static getTransposeParent, TMatch extends EditToken>( + buffer: ClassicalDistanceCalculation, + r: number, + c: number + ): [number, number] { + // Block any transpositions where the tokens are identical. + // Other operations will be cheaper. Also, block cases where 'parents' are impossible. + if(r < 0 || c < 0 || buffer.inputSequence[r].key == buffer.matchSequence[c].key) { + return [-1, -1]; + } + + // Transposition checks + let lastInputIndex = -1; + for(let i = r-1; i >= 0; i--) { + if(buffer.inputSequence[i].key == buffer.matchSequence[c].key) { + lastInputIndex = i; + break; + } + } + + let lastMatchIndex = -1; + for(let i = c-1; i >= 0; i--) { + if(buffer.matchSequence[i].key == buffer.inputSequence[r].key) { + lastMatchIndex = i; + break; + } + } + + return [lastInputIndex, lastMatchIndex]; + } + + private static initialCostAt, TMatch extends EditToken>( + buffer: ClassicalDistanceCalculation, + r: number, + c: number, + insertCost?: number, + deleteCost?: number) { + var baseSubstitutionCost = buffer.inputSequence[r].key == buffer.matchSequence[c].key ? 0 : 1; + var substitutionCost: number = buffer.getCostAt(r-1, c-1) + baseSubstitutionCost; + var insertionCost: number = insertCost || buffer.getCostAt(r, c-1) + 1; // If set meaningfully, will never equal zero. + var deletionCost: number = deleteCost || buffer.getCostAt(r-1, c) + 1; // If set meaningfully, will never equal zero. + var transpositionCost: number = Number.MAX_VALUE + + if(r > 0 && c > 0) { // bypass when transpositions are known to be impossible. + let [lastInputIndex, lastMatchIndex] = ClassicalDistanceCalculation.getTransposeParent(buffer, r, c); + transpositionCost = buffer.getCostAt(lastInputIndex-1, lastMatchIndex-1) + (r - lastInputIndex - 1) + 1 + (c - lastMatchIndex - 1); + } + + return Math.min(substitutionCost, deletionCost, insertionCost, transpositionCost); + } + + getSubset(inputLength: number, matchLength: number): ClassicalDistanceCalculation { + let trimmedInstance = new ClassicalDistanceCalculation(this); + + if(inputLength > this.inputSequence.length || matchLength > this.matchSequence.length) { + throw "Invalid dimensions specified for trim operation"; + } + // Trim our tracked input & match sequences. + trimmedInstance.inputSequence.splice(inputLength); + trimmedInstance.matchSequence.splice(matchLength); + + // Major index corresponds to input length. + trimmedInstance.resolvedDistances.splice(inputLength); + + // The real fun: trimming off columns. (Minor index, corresponds to match length) + let finalTrueIndex = this.getTrueIndex(inputLength-1, matchLength-1, this.diagonalWidth); + // The diagonal index increases as the row index decreases. + for(let diagonalIndex = finalTrueIndex.col; diagonalIndex <= 2 * this.diagonalWidth; diagonalIndex++) { + let row = finalTrueIndex.row - (diagonalIndex - finalTrueIndex.col); + if(row < 0) { + break; + } + + if(diagonalIndex < 0) { + trimmedInstance.resolvedDistances[row] = Array(2 * trimmedInstance.diagonalWidth + 1).fill(Number.MAX_VALUE); + } else { + let newCount = 2 * this.diagonalWidth - diagonalIndex; + let keptEntries = trimmedInstance.resolvedDistances[row].splice(0, diagonalIndex+1); + let newEntries = Array(newCount).fill(Number.MAX_VALUE); + trimmedInstance.resolvedDistances[row] = keptEntries.concat(newEntries); + } + } + + return trimmedInstance; + } + + private static forDiagonalOfAxis(diagonalWidth: number, centerIndex: number, axisCap: number, closure: (axisIndex: number, diagIndex: number) => void) { + let diagonalCap = axisCap - centerIndex < diagonalWidth ? axisCap - centerIndex + diagonalWidth : 2 * diagonalWidth; + let startOffset = centerIndex - diagonalWidth; // The axis's index for diagonal entry 0. May be negative. + let diagonalStart = startOffset < 0 ? 0 : startOffset; + + for(let diagonalIndex = diagonalStart - startOffset; diagonalIndex <= diagonalCap; diagonalIndex++) { + closure(startOffset + diagonalIndex, diagonalIndex); + } + } + + // Inputs add an extra row / first index entry. + addInputChar(token: TInput): ClassicalDistanceCalculation { + let returnBuffer = new ClassicalDistanceCalculation(this); + + let r = returnBuffer.inputSequence.length; + returnBuffer.inputSequence.push(token); + + // Insert a row, even if we don't actually do anything with it yet. + // Initialize all entries with Number.MAX_VALUE, as `undefined` use leads to JS math issues. + let row = Array(2 * returnBuffer.diagonalWidth + 1).fill(Number.MAX_VALUE); + returnBuffer.resolvedDistances[r] = row; + + // If there isn't a 'match' entry yet, there are no values to compute. Exit immediately. + if(returnBuffer.matchSequence.length == 0) { + return returnBuffer; + } + + ClassicalDistanceCalculation.forDiagonalOfAxis(returnBuffer.diagonalWidth, r, returnBuffer.matchSequence.length - 1, function(c, diagIndex) { + row[diagIndex] = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c); + }); + + return returnBuffer; + } + + addMatchChar(token: TMatch): ClassicalDistanceCalculation { + let returnBuffer = new ClassicalDistanceCalculation(this); + + let c = returnBuffer.matchSequence.length; + returnBuffer.matchSequence.push(token); + + // If there isn't a 'match' entry yet, there are no values to compute. Exit immediately. + if(returnBuffer.inputSequence.length == 0) { + return returnBuffer; + } + + ClassicalDistanceCalculation.forDiagonalOfAxis(returnBuffer.diagonalWidth, c, returnBuffer.inputSequence.length - 1, function(r, diagIndex) { + var row = returnBuffer.resolvedDistances[r]; + // Since diagIndex is from the perspective of the row, it must be inverted to properly index the column. + row[2 * returnBuffer.diagonalWidth - diagIndex] = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c); + }); + + return returnBuffer; + } + + public increaseMaxDistance(): ClassicalDistanceCalculation { + let returnBuffer = new ClassicalDistanceCalculation(this); + returnBuffer.diagonalWidth++; + + if(returnBuffer.inputSequence.length < 1 || returnBuffer.matchSequence.length < 1) { + return returnBuffer; + } + + // An abstraction of the common aspects of transposition handling during diagonal extensions. + function forPossibleTranspositionsInDiagonal(startPos: number, fixedChar: TUnit, lookupString: EditToken[], closure: (axisIndex: number, diagIndex: number) => void) { + let diagonalCap = 2 * (returnBuffer.diagonalWidth - 1); // The maximum diagonal index permitted + let axisCap = lookupString.length - 1; // The maximum index supported by the axis of iteration + + // Ensures that diagonal iteration only occurs within the axis's supported range + diagonalCap = diagonalCap < axisCap - startPos ? diagonalCap : axisCap - startPos; + + // Iterate within the diagonal and call our closure for any potential transpositions. + for(let diagonalIndex = 0; diagonalIndex <= diagonalCap; diagonalIndex++) { + if(fixedChar == lookupString[startPos + diagonalIndex].key) { + closure(startPos + diagonalIndex, diagonalIndex); + } + } + } + + for(let r = 0; r < returnBuffer.inputSequence.length; r++) { + let leftCell = Number.MAX_VALUE; + let c = r - returnBuffer.diagonalWidth // External index of the left-most entry, which we will now calculate. + if(c >= 0) { + // If c == 0, cell is at edge, thus a known value for insertions exists. + // Base cost: r+1, +1 for inserting. + let insertionCost = c == 0 ? r + 2 : Number.MAX_VALUE; + // compute new left cell + leftCell = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c, insertionCost, undefined); + let addedCost = leftCell; + + // daisy-chain possible updates + + // cell (r, c+1): new insertion source + if(c < returnBuffer.matchSequence.length-1) { + // We propagate the new added cost (via insertion) to the old left-most cell, which is one to our right. + ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, r, c+1, addedCost+1, 0); + + // Only possible if insertions are also possible AND more conditions are met. + // cells (r+2, * > c+2): new transposition source + let transposeRow = r+2; + if(r+2 < this.inputSequence.length) { // Row to check for transposes must exist. + let rowChar = returnBuffer.inputSequence[r+1].key; + // First possible match in input could be at index c + 2, which adjusts col c+2's cost. Except that entry in r+2 + // doesn't exist yet - so we start with c+3 instead. + forPossibleTranspositionsInDiagonal(c + 3, rowChar, returnBuffer.matchSequence, function(axisIndex, diagIndex) { + // Because (r+2, c+3) is root, not (r+2, c+2). Min cost of 2. + ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, transposeRow, axisIndex, addedCost + diagIndex + 2, diagIndex); + }); + } + } + } + + let rightCell = Number.MAX_VALUE; + c = r + returnBuffer.diagonalWidth; + if(c < returnBuffer.matchSequence.length) { + // If r == 0, cell is at edge, thus a known value for insertions exists. + // Base cost: c+1, +1 for inserting. + let deletionCost = r == 0 ? c + 2 : Number.MAX_VALUE; + + // the current row wants to use adjusted diagonal width; we must specify use of the old width & its mapping instead. + var insertionCost: number = returnBuffer.getCostAt(r, c-1, this.diagonalWidth) + 1; + + // compute new right cell + rightCell = ClassicalDistanceCalculation.initialCostAt(returnBuffer, r, c, insertionCost, deletionCost); + let addedCost = rightCell; + + // daisy-chain possible updates + + // cell (r+1, c): new deletion source + if(r < returnBuffer.inputSequence.length - 1) { + // We propagate the new added cost (via deletion) to the old right-most cell, which is one to our right. + ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, r+1, c, addedCost + 1, 2 * this.diagonalWidth); + + // Only possible if deletions are also possible AND more conditions are met. + // cells(* > r+2, c+2): new transposition source + let transposeCol = c+2; + if(c+2 < this.matchSequence.length) { // Row to check for transposes must exist. + let colChar = returnBuffer.matchSequence[r+1].key; + // First possible match in input could be at index r + 2, which adjusts row r+2's cost. Except that entry in c+2 + // doesn't exist yet - so we start with r+3 instead. + forPossibleTranspositionsInDiagonal(r+3, colChar, returnBuffer.inputSequence, function(axisIndex, diagIndex) { + let diagColIndex = 2 * (returnBuffer.diagonalWidth - 1) - diagIndex; + // Because (r+3, c+2) is root, not (r+2, c+2). Min cost of 2. + ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, axisIndex, transposeCol, addedCost + diagIndex + 2, diagColIndex); + }); + } + } + } + + // Constructs the final expanded diagonal for the row. + returnBuffer.resolvedDistances[r] = [leftCell].concat(returnBuffer.resolvedDistances[r], rightCell); + } + + return returnBuffer; + } + + private static propagateUpdateFrom, TMatch extends EditToken>( + buffer: ClassicalDistanceCalculation, + r: number, + c: number, + value: number, + diagonalIndex: number) { + // Note: this function does not actually need the `c` parameter! + // That said, it's very useful when tracing stack traces & debugging. + if(value < buffer.resolvedDistances[r][diagonalIndex]) { + buffer.resolvedDistances[r][diagonalIndex] = value; + } else { + return + } + + let internalRow = r < buffer.inputSequence.length - 1; + let internalCol = c < buffer.matchSequence.length - 1; + + // We have to compensate for the current & following rows not having been expanded yet. + if(diagonalIndex < 2 * (buffer.diagonalWidth - 1) && internalCol) { + // We've inserted to the left of an existing calculation - check for propagation via insertion. + let updateCost = value + 1; + this.propagateUpdateFrom(buffer, r, c+1, updateCost, diagonalIndex+1); + } + + if(diagonalIndex > 0 && internalRow) { + // We've inserted above an existing calculation - check for propagation via deletion + let updateCost = value + 1 + this.propagateUpdateFrom(buffer, r+1, c, updateCost, diagonalIndex-1); + } + + // If both, check for propagation via substitution and possible transpositions + if(internalRow && internalCol) { + let updateCost = value + (buffer.inputSequence[r+1].key == buffer.matchSequence[c+1].key ? 0 : 1); + this.propagateUpdateFrom(buffer, r+1, c+1, updateCost, diagonalIndex); + + // Propagating transpositions (only possible if 'internal'.) + let nextInputIndex = -1; + for(let i = r+2; i < buffer.inputSequence.length; i++) { + if(buffer.inputSequence[i].key == buffer.matchSequence[c+1].key) { + nextInputIndex = i; + break; + } + } + + let nextMatchIndex = -1; + for(let i = c+2; i < buffer.matchSequence.length; i++) { + if(buffer.matchSequence[i].key == buffer.inputSequence[r+1].key) { + nextMatchIndex = i; + break; + } + } + + if(nextInputIndex > 0 && nextMatchIndex > 0) { + let transpositionCost = value + (nextInputIndex - r - 2) + 1 + (nextMatchIndex - c - 2); + this.propagateUpdateFrom(buffer, nextInputIndex, nextMatchIndex, transpositionCost, (buffer.diagonalWidth - 1) + nextMatchIndex - nextInputIndex); + } + } + } + + get mapKey(): string { + let inputString = this.inputSequence.map((value) => value.key).join(''); + let matchString = this.matchSequence.map((value) => value.key).join(''); + return inputString + SENTINEL_CODE_UNIT + matchString + SENTINEL_CODE_UNIT + this.diagonalWidth; + } + + get lastInputEntry(): TInput { + return this.inputSequence[this.inputSequence.length-1]; + } + + get lastMatchEntry(): TMatch { + return this.matchSequence[this.matchSequence.length-1]; + } + + static computeDistance, TMatch extends EditToken>( + input: TInput[], + match: TMatch[], + bandSize: number = 1) { + // Initialize the calculation buffer, setting the diagonal width (as appropriate) in advance. + let buffer = new ClassicalDistanceCalculation(); + bandSize = bandSize || 1; + buffer.diagonalWidth = bandSize; + + for(let i = 0; i < input.length; i++) { + buffer = buffer.addInputChar(input[i]); + } + + for(let j = 0; j < match.length; j++) { + buffer = buffer.addMatchChar(match[j]); + } + + return buffer; + } +} \ No newline at end of file diff --git a/common/web/lm-worker/src/main/correction/context-tracker.ts b/common/web/lm-worker/src/main/correction/context-tracker.ts new file mode 100644 index 0000000000000000000000000000000000000000..27783a4852823112886990139d7482f8bd062c54 --- /dev/null +++ b/common/web/lm-worker/src/main/correction/context-tracker.ts @@ -0,0 +1,542 @@ +import { tokenize } from '@keymanapp/models-templates'; +import { defaultWordbreaker } from '@keymanapp/models-wordbreakers'; + +import { ClassicalDistanceCalculation } from './classical-calculation.js'; +import { SearchSpace } from './distance-modeler.js'; +import TransformUtils from '../transformUtils.js'; + +function textToCharTransforms(text: string, transformId?: number) { + let perCharTransforms: Transform[] = []; + + for(let i=0; i < text.kmwLength(); i++) { + let char = text.kmwCharAt(i); // is SMP-aware + + let transform: Transform = { + insert: char, + deleteLeft: 0, + id: transformId + }; + + perCharTransforms.push(transform); + } + + return perCharTransforms; +} + +export class TrackedContextSuggestion { + suggestion: Suggestion; + tokenWidth: number; +} + +export class TrackedContextToken { + raw: string; + replacementText: string; + + transformDistributions: Distribution[] = []; + replacements: TrackedContextSuggestion[]; + activeReplacementId: number = -1; + + get currentText(): string { + if(this.replacementText === undefined || this.replacementText === null) { + return this.raw; + } else { + return this.replacementText; + } + } + + get replacement(): TrackedContextSuggestion { + let replacementId = this.activeReplacementId; + return this.replacements.find(function(replacement) { + return replacement.suggestion.id == replacementId; + }); + } + + revert() { + delete this.activeReplacementId; + } +} + +export class TrackedContextState { + // Stores the source Context (as a debugging reference). Not currently utilized. + taggedContext: Context; + model: LexicalModel; + + tokens: TrackedContextToken[]; + /** + * How many tokens were removed from the start of the best-matching ancestor. + * Useful for restoring older states, e.g., when the user moves the caret backwards, we can recover the context at that position. + */ + indexOffset: number; + + // Tracks all search spaces starting at the current token. + // In the lm-layer's current form, this should only ever have one entry. + // Leaves 'design space' for if/when we add support for phrase-level corrections/predictions. + searchSpace: SearchSpace[] = []; + + constructor(source: TrackedContextState); + constructor(model: LexicalModel); + constructor(obj: TrackedContextState | LexicalModel) { + if(obj instanceof TrackedContextState) { + let source = obj; + // Be sure to deep-copy the tokens! Pointer-aliasing is bad here. + this.tokens = source.tokens.map(function(token) { + let copy = new TrackedContextToken(); + copy.raw = token.raw; + copy.replacements = token.replacements + copy.activeReplacementId = token.activeReplacementId; + copy.transformDistributions = token.transformDistributions; + + if(token.replacementText) { + copy.replacementText = token.replacementText; + } + + return copy; + }); + this.searchSpace = obj.searchSpace; + this.indexOffset = 0; + this.model = obj.model; + } else { + let lexicalModel = obj; + this.tokens = []; + this.indexOffset = Number.MIN_SAFE_INTEGER; + this.model = lexicalModel; + + if(lexicalModel && lexicalModel.traverseFromRoot) { + this.searchSpace = [new SearchSpace(lexicalModel)]; + } + } + } + + get head(): TrackedContextToken { + return this.tokens[0]; + } + + get tail(): TrackedContextToken { + return this.tokens[this.tokens.length - 1]; + } + + popHead() { + this.tokens.splice(0, 2); + this.indexOffset -= 1; + } + + pushTail(token: TrackedContextToken) { + if(this.model && this.model.traverseFromRoot) { + this.searchSpace = [new SearchSpace(this.model)]; // yeah, need to update SearchSpace for compatibility + } else { + this.searchSpace = []; + } + this.tokens.push(token); + + let state = this; + if(state.searchSpace.length > 0) { + token.transformDistributions.forEach(distrib => state.searchSpace[0].addInput(distrib)); + } + } + + pushWhitespaceToTail(transformDistribution: Distribution = null) { + let whitespaceToken = new TrackedContextToken(); + + // Track the Transform that resulted in the whitespace 'token'. + // Will be needed for phrase-level correction/prediction. + whitespaceToken.transformDistributions = transformDistribution ? [transformDistribution] : []; + + whitespaceToken.raw = null; + this.tokens.push(whitespaceToken); + } + + /** + * Used for 14.0's backspace workaround, which flattens all previous Distribution + * entries because of limitations with direct use of backspace transforms. + * @param tokenText + * @param transformId + */ + replaceTailForBackspace(tokenText: USVString, transformId: number) { + this.tokens.pop(); + + // It's a backspace transform; time for special handling! + // + // For now, with 14.0, we simply compress all remaining Transforms for the token into + // multiple single-char transforms. Probabalistically modeling BKSP is quite complex, + // so we simplify by assuming everything remaining after a BKSP is 'true' and 'intended' text. + // + // Note that we cannot just use a single, monolithic transform at this point b/c + // of our current edit-distance optimization strategy; diagonalization is currently... + // not very compatible with that. + let backspacedTokenContext: Distribution[] = textToCharTransforms(tokenText, transformId).map(function(transform) { + return [{sample: transform, p: 1.0}]; + }); + + let compactedToken = new TrackedContextToken(); + compactedToken.raw = tokenText; + compactedToken.transformDistributions = backspacedTokenContext; + this.pushTail(compactedToken); + } + + updateTail(transformDistribution: Distribution, tokenText?: USVString) { + let editedToken = this.tail; + + // Preserve existing text if new text isn't specified. + tokenText = tokenText || (tokenText === '' ? '' : editedToken.raw); + + if(transformDistribution && transformDistribution.length > 0) { + editedToken.transformDistributions.push(transformDistribution); + if(this.searchSpace) { + this.searchSpace.forEach(space => space.addInput(transformDistribution)); + } + } + // Replace old token's raw-text with new token's raw-text. + editedToken.raw = tokenText; + } + + toRawTokenization() { + let sequence: USVString[] = []; + + for(let token of this.tokens) { + // Hide any tokens representing wordbreaks. (Thinking ahead to phrase-level possibilities) + if(token.currentText !== null) { + sequence.push(token.currentText); + } + } + + return sequence; + } +} + +class CircularArray { + static readonly DEFAULT_ARRAY_SIZE = 5; + private circle: Item[]; + private currentHead: number=0; + private currentTail: number=0; + + constructor(size: number = CircularArray.DEFAULT_ARRAY_SIZE) { + this.circle = Array(size); + } + + get count(): number { + let diff = this.currentHead - this.currentTail; + + if(diff < 0) { + diff = diff + this.circle.length; + } + + return diff; + } + + get maxCount(): number { + return this.circle.length; + } + + get oldest(): Item { + if(this.count == 0) { + return undefined; + } + + return this.item(0); + } + + get newest(): Item { + if(this.count == 0) { + return undefined; + } + + return this.item(this.count - 1); + } + + enqueue(item: Item): Item { + var prevItem = null; + let nextHead = (this.currentHead + 1) % this.maxCount; + + if(nextHead == this.currentTail) { + prevItem = this.circle[this.currentTail]; + this.currentTail = (this.currentTail + 1) % this.maxCount; + } + + this.circle[this.currentHead] = item; + this.currentHead = nextHead; + + return prevItem; + } + + dequeue(): Item { + if(this.currentTail == this.currentHead) { + return null; + } else { + let item = this.circle[this.currentTail]; + this.currentTail = (this.currentTail + 1) % this.maxCount; + return item; + } + } + + popNewest(): Item { + if(this.currentTail == this.currentHead) { + return null; + } else { + let item = this.circle[this.currentHead]; + this.currentHead = (this.currentHead - 1 + this.maxCount) % this.maxCount; + return item; + } + } + + /** + * Returns items contained within the circular array, ordered from 'oldest' to 'newest' - + * the same order in which the items will be dequeued. + * @param index + */ + item(index: number) { + if(index >= this.count) { + throw "Invalid array index"; + } + + let mappedIndex = (this.currentTail + index) % this.maxCount; + return this.circle[mappedIndex]; + } +} + +export class ContextTracker extends CircularArray { + static attemptMatchContext(tokenizedContext: USVString[], + matchState: TrackedContextState, + transformDistribution?: Distribution,): TrackedContextState { + // Map the previous tokenized state to an edit-distance friendly version. + let matchContext: USVString[] = matchState.toRawTokenization(); + + // Inverted order, since 'match' existed before our new context. + let mapping = ClassicalDistanceCalculation.computeDistance(matchContext.map(value => ({key: value})), + tokenizedContext.map(value => ({key: value})), + 1); + + let editPath = mapping.editPath(); + + let poppedHead = false; + let pushedTail = false; + + // Matters greatly when starting from a nil context. + if(editPath.length > 1) { + // First entry: may not be an 'insert' or a 'transpose' op. + // 'insert' allowed if the next token is 'substitute', as this may occur with an edit path of length 2. + if((editPath[0] == 'insert' && !(editPath[1] == 'substitute' && editPath.length == 2)) || editPath[0].indexOf('transpose') >= 0) { + return null; + } else if(editPath[0] == 'delete') { + poppedHead = true; // a token from the previous state has been wholly removed. + } + } + + // Last entry: may not be a 'delete' or a 'transpose' op. + let tailIndex = editPath.length -1; + let ignorePenultimateMatch = false; + if(editPath[tailIndex] == 'delete' || editPath[0].indexOf('transpose') >= 0) { + return null; + } else if(editPath[tailIndex] == 'insert') { + pushedTail = true; + } else if(tailIndex > 0 && editPath[tailIndex-1] == 'insert' && editPath[tailIndex] == 'substitute') { + // Tends to happen when accepting suggestions. + pushedTail = true; + ignorePenultimateMatch = true; + } + + // Can happen for the first text input after backspace deletes a wordbreaking character, + // thus the new input continues a previous word while dropping the empty word after + // that prior wordbreaking character. + // + // We can't handle it reliably from this match state, but a previous entry (without the empty token) + // should still be in the cache and will be reliable for this example case. + if(tailIndex > 0 && editPath[tailIndex-1] == 'delete' && editPath[tailIndex] == 'substitute') { + return null; + } + + // Now to check everything in-between: should be exclusively 'match'es. + for(let index = 1; index < editPath.length - (ignorePenultimateMatch ? 2 : 1); index++) { + if(editPath[index] != 'match') { + return null; + } + } + + // If we've made it here... success! We have a context match! + let state: TrackedContextState; + + if(pushedTail) { + // On suggestion acceptance, we should update the previous final token. + // We do it first so that the acceptance is replicated in the new TrackedContextState + // as well. + if(ignorePenultimateMatch) { + // For this case, we were likely called by ModelCompositor.acceptSuggestion(), which + // would have marked the accepted suggestion. + matchState.tail.replacementText = tokenizedContext[tokenizedContext.length-2]; + } + + state = new TrackedContextState(matchState); + } else { + // Since we're continuing a previously-cached context, we can reuse the same SearchSpace + // to continue making predictions. + state = matchState; + } + + const hasDistribution = transformDistribution && Array.isArray(transformDistribution); + let primaryInput = hasDistribution ? transformDistribution[0].sample : null; + if(primaryInput && primaryInput.insert == "" && primaryInput.deleteLeft == 0 && !primaryInput.deleteRight) { + primaryInput = null; + } + + const isWhitespace = primaryInput && TransformUtils.isWhitespace(primaryInput); + const isBackspace = primaryInput && TransformUtils.isBackspace(primaryInput); + const finalToken = tokenizedContext[tokenizedContext.length-1]; + + /* Assumption: This is an adequate check for its two sub-branches. + * + * Basis: + * - Assumption: one keystroke may only cause a single token to rotate out of context. + * - That is, no "reasonable" keystroke would emit enough code points to 'bump' two words simultaneously. + * - ... This one may need to be loosened a bit... but it should be enough for initial correction testing as-is. + * - Assumption: one keystroke may only cause a single token to be appended to the context + * - That is, no "reasonable" keystroke would emit a Transform adding two separate word tokens + * - For languages using whitespace to word-break, said keystroke would have to include said whitespace to break the assumption. + */ + + // If there is/was more than one context token available... + if(editPath.length > 1) { + // We're removing a context token, but at least one remains. + if(poppedHead) { + state.popHead(); + } + + // We're adding an additional context token. + if(pushedTail) { + const tokenizedTail = tokenizedContext[tokenizedContext.length - 1]; + /* + * Common-case: most transforms that trigger this case are from pure-whitespace Transforms. MOST. + * + * Less-common, but noteworthy: some wordbreaks may occur without whitespace. Example: + * `"o` => ['"', 'o']. Make sure to double-check against `tokenizedContext`! + */ + let pushedToken = new TrackedContextToken(); + pushedToken.raw = tokenizedTail; + + if(isWhitespace || !primaryInput) { + state.pushWhitespaceToTail(transformDistribution ?? []); + // Continuing the earlier assumption, that 'pure-whitespace Transform' does not emit any initial characters + // for the new word (token), so the input keystrokes do not correspond to the new text token. + pushedToken.transformDistributions = []; + } else { + state.pushWhitespaceToTail(); + // Assumption: Since we only allow one-transform-at-a-time changes between states, we shouldn't be missing + // any metadata used to construct the new context state token. + pushedToken.transformDistributions = transformDistribution ? [transformDistribution] : []; + } + + state.pushTail(pushedToken); + } else { // We're editing the final context token. + // TODO: Assumption: we didn't 'miss' any inputs somehow. + // As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's. + if(isBackspace) { + state.replaceTailForBackspace(finalToken, primaryInput.id); + } else { + state.updateTail(primaryInput ? transformDistribution : null, finalToken); + } + } + // There is only one word in the context. + } else { + // TODO: Assumption: we didn't 'miss' any inputs somehow. + // As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's. + + if(editPath[tailIndex] == 'insert') { + // Construct appropriate initial token. + let token = new TrackedContextToken(); + token.raw = tokenizedContext[0]; + token.transformDistributions = [transformDistribution]; + state.pushTail(token); + } else { // Edit the lone context token. + // Consider backspace entry for this case? + if(isBackspace) { + state.replaceTailForBackspace(finalToken, primaryInput.id); + } else { + state.updateTail(primaryInput ? transformDistribution : null, finalToken); + } + } + } + return state; + } + + static modelContextState(tokenizedContext: USVString[], + transformDistribution: Distribution, + lexicalModel: LexicalModel): TrackedContextState { + let baseTokens = tokenizedContext.map(function(entry) { + let token = new TrackedContextToken(); + token.raw = entry; + if(token.raw) { + token.transformDistributions = textToCharTransforms(token.raw).map(function(transform) { + return [{sample: transform, p: 1.0}]; + }); + } else { + // Helps model context-final wordbreaks. + token.transformDistributions = []; + } + return token; + }); + + // And now build the final context state object, which includes whitespace 'tokens'. + let state = new TrackedContextState(lexicalModel); + + if(baseTokens.length > 0) { + state.pushTail(baseTokens.splice(0, 1)[0]); + } + + while(baseTokens.length > 0) { + state.pushWhitespaceToTail(); + state.pushTail(baseTokens.splice(0, 1)[0]); + } + + if(state.tokens.length == 0) { + let token = new TrackedContextToken(); + token.raw = ''; + + state.pushTail(token); + } + + return state; + } + + /** + * Compares the current, post-input context against the most recently-seen contexts from previous prediction calls, returning + * the most information-rich `TrackedContextState` possible. If a match is found, the state will be annotated with the + * input information provided to previous prediction calls and persisted correction-search calculations for re-use. + * + * @param model + * @param context + * @param transformDistribution + */ + analyzeState(model: LexicalModel, + context: Context, + transformDistribution?: Distribution): TrackedContextState { + if(!model.traverseFromRoot) { + // Assumption: LexicalModel provides a valid traverseFromRoot function. (Is technically optional) + // Without it, no 'corrections' may be made; the model can only be used to predict, not correct. + throw "This lexical model does not provide adequate data for correction algorithms and context reuse"; + } + + let tokenizedContext = tokenize(model.wordbreaker || defaultWordbreaker, context); + + if(tokenizedContext.left.length > 0) { + for(let i = this.count - 1; i >= 0; i--) { + let resultState = ContextTracker.attemptMatchContext(tokenizedContext.left, this.item(i), transformDistribution); + + if(resultState) { + resultState.taggedContext = context; + if(resultState != this.item(i)) { + this.enqueue(resultState); + } + return resultState; + } + } + } + + // Else: either empty OR we've detected a 'new context'. Initialize from scratch; no prior input information is + // available. Only the results of the prior inputs are known. + // + // Assumption: as a caret needs to move to context before any actual transform distributions occur, + // this state is only reached on caret moves; thus, transformDistribution is actually just a single null transform. + let state = ContextTracker.modelContextState(tokenizedContext.left, transformDistribution, model); + state.taggedContext = context; + this.enqueue(state); + return state; + } +} diff --git a/common/web/lm-worker/src/main/correction/distance-modeler.ts b/common/web/lm-worker/src/main/correction/distance-modeler.ts new file mode 100644 index 0000000000000000000000000000000000000000..20d72c546890d2d0077065e21e312df1981929c2 --- /dev/null +++ b/common/web/lm-worker/src/main/correction/distance-modeler.ts @@ -0,0 +1,805 @@ +import { Comparator, isHighSurrogate, SENTINEL_CODE_UNIT, PriorityQueue } from '@keymanapp/models-templates'; + +import { ClassicalDistanceCalculation, EditToken } from './classical-calculation.js'; + +type RealizedInput = ProbabilityMass[]; // NOT Distribution - they're masses from separate distributions. + +export type TraversableToken = { + key: TUnit, + traversal: LexiconTraversal +} + +export const QUEUE_NODE_COMPARATOR: Comparator = function(arg1, arg2) { + return arg1.currentCost - arg2.currentCost; +} + +// Represents a processed node for the correction-search's search-space's tree-like graph. May represent +// internal and 'leaf' nodes on said graph, as well as the overall root of the search. Also used to represent +// edges on the graph TO said nodes - there's a bit of overloading here. Either way, it stores the cost of the +// optimum path used to reach the ndoe. +// +// The stored path cost may be an overestimate when the edit distance is greater than the current search threshold. The +// first version of the node to be dequeued from SearchSpace's priority queue hierarchy 'wins' and is taken as the absolute +// minimum; subsequent versions are ignored as suboptimal. +// +// Provides functions usable to enumerate across the node's outward edges to new nodes for continued search. +// Most of the actual calculations occur as part of this process. +// +// For nodes with raw edit-distance cost within the current threshold for correction searches, we do have admissibility. +// If not enough nodes are available within that threshold, however, admissibility may be lost, leaving our search as a +// heuristic. +// +export class SearchNode { + calculation: ClassicalDistanceCalculation, TraversableToken>; + + currentTraversal: LexiconTraversal; + toKey: (wordform: USVString) => USVString = str => str; + priorInput: RealizedInput; + + // Internal lazy-cache for .inputSamplingCost, as it's a bit expensive to re-compute. + private _inputCost?: number; + + constructor(rootTraversal: LexiconTraversal, toKey?: (USVString) => USVString); + constructor(node: SearchNode); + constructor(rootTraversal: LexiconTraversal | SearchNode, toKey?: (USVString) => USVString) { + toKey = toKey || (x => x); + + if(rootTraversal instanceof SearchNode) { + let priorNode = rootTraversal; + this.calculation = priorNode.calculation; + this.currentTraversal = priorNode.currentTraversal; + this.priorInput = priorNode.priorInput; + this.toKey = priorNode.toKey; + } else { + this.calculation = new ClassicalDistanceCalculation(); + this.currentTraversal = rootTraversal; + this.priorInput = []; + this.toKey = toKey; + } + } + + get knownCost(): number { + return this.calculation.getHeuristicFinalCost(); + } + + get inputSamplingCost(): number { + if(this._inputCost !== undefined) { + return this._inputCost; + } else { + let MIN_P = SearchSpace.MIN_KEYSTROKE_PROBABILITY; + // Should technically re-normalize the sampling distribution. + // -ln(p) is smaller for larger probabilities, as ln(p) is always <= 0. Approaches infinity as p => 0. + + // TODO: probably more efficient to instead use actual 'probability space'... but that'll involve extra changes. + this._inputCost = this.priorInput.map(mass => mass.p > MIN_P ? mass.p : MIN_P).reduce((previous, current) => previous - Math.log(current), 0); + return this._inputCost; + } + } + + // The part used to prioritize our search. + get currentCost(): number { + // - We reintrepret 'known cost' as a psuedo-probability. + // - Noting that 1/e = 0.367879441, an edit-distance cost of 1 may be intepreted as -ln(1/e) - a log-space 'likelihood'. + // - Not exactly normalized, though. + // That's a really, really high likelihood, thoough. + // + // At any rate, we can linearly scale the known-cost to have about whatever probability we want. + // If we can state it as p = 1 / (c * e), note then that ln(c * e) = ln(c) + 1. So, scale * (ln(c) + 1). + // If we can state it as e^x, note that ln(e^x) = x * ln(e) = x - just scale by 'x'! + + // p = 1 / (e^4) = 0.01831563888. This still exceeds many neighboring keys! + // p = 1 / (e^5) = 0.00673794699. Strikes a good balance. + // Should easily give priority to neighboring keys before edit-distance kicks in (when keys are a bit ambiguous) + return SearchSpace.EDIT_DISTANCE_COST_SCALE * this.knownCost + this.inputSamplingCost; + } + + buildInsertionEdges(): SearchNode[] { + let edges: SearchNode[] = []; + + for(let lexicalChild of this.currentTraversal.children()) { + let traversal = lexicalChild.traversal(); + let matchToken = { + key: lexicalChild.char, + traversal: traversal + } + + let childCalc = this.calculation.addMatchChar(matchToken); + + let searchChild = new SearchNode(this); + searchChild.calculation = childCalc; + searchChild.priorInput = this.priorInput; + searchChild.currentTraversal = traversal; + + edges.push(searchChild); + } + + return edges; + } + + buildDeletionEdges(inputDistribution: Distribution): SearchNode[] { + let edges: SearchNode[] = []; + + /* + * If the probability of an input is less than the highest probability * the base edit-distance likelihood, + * don't build an edge for it; just rely on edits from the highest-probability edge. + * + * We may be able to be stricter, but this should be a decent start. + * + * Note: thanks to ModelCompositor.predict, we know the distribution is pre-sorted. + */ + for(let probMass of inputDistribution) { + if(probMass.p < inputDistribution[0].p * Math.exp(-SearchSpace.EDIT_DISTANCE_COST_SCALE)) { + // Again, we're pre-sorted. All further entries will be too low-cost to consider. + break; + } + + let edgeCalc = this.calculation; + let transform = probMass.sample; + if(transform.deleteLeft) { + edgeCalc = edgeCalc.getSubset(edgeCalc.inputSequence.length - transform.deleteLeft, edgeCalc.matchSequence.length); + } + + // TODO: transform.deleteRight currently not supported. + + let inputPath = Array.from(this.priorInput); + inputPath.push(probMass); + // Tokenize and iterate over input chars, adding them into the calc. + for(let i=0; i < transform.insert.length; i++) { + let char = transform.insert[i]; + if(isHighSurrogate(char)) { + i++; + char = char + transform.insert[i]; + } + + // In case of NFD input, filter out any empty-strings that may arise + // when 'keying' raw diacritics. + let keyedChar = this.toKey(char); + if(keyedChar) { + edgeCalc = edgeCalc.addInputChar({key: keyedChar}); + } + } + + let childEdge = new SearchNode(this); + childEdge.calculation = edgeCalc; + childEdge.priorInput = inputPath; + + edges.push(childEdge); + } + + return edges; + } + + // While this may SEEM to be unnecessary, note that sometimes substitutions (which are computed + // via insert + delete) may be lower cost than both just-insert and just-delete. + buildSubstitutionEdges(inputDistribution: Distribution): SearchNode[] { + // Handles the 'input' component. + let intermediateEdges = this.buildDeletionEdges(inputDistribution); + let edges: SearchNode[] = []; + + for(let lexicalChild of this.currentTraversal.children()) { + for(let edge of intermediateEdges) { + let traversal = lexicalChild.traversal(); + let matchToken = { + key: lexicalChild.char, + traversal: traversal + } + + let childCalc = edge.calculation.addMatchChar(matchToken); + + let searchChild = new SearchNode(this); + searchChild.calculation = childCalc; + searchChild.priorInput = edge.priorInput; + searchChild.currentTraversal = traversal; + + edges.push(searchChild); + } + } + + return edges; + } + + get mapKey(): string { + let inputString = this.priorInput.map((value) => '+' + value.sample.insert + '-' + value.sample.deleteLeft).join(''); + let matchString = this.calculation.matchSequence.map((value) => value.key).join(''); + + // TODO: might should also track diagonalWidth. + return inputString + SENTINEL_CODE_UNIT + matchString; + } + + get isFullReplacement(): boolean { + // If the known edit-distance cost is equal to the input length, this means + // that literally every input has been full-on replaced. Thus, this is + // likely not a good 'root' to use for predictions. + // + // Logic exception: 0 cost, 0 length != a "replacement". + return this.knownCost && this.knownCost == this.priorInput.length; + } +} + +class SearchSpaceTier { + correctionQueue: PriorityQueue; + processed: SearchNode[] = []; + index: number; + + constructor(index: number, initialEdges?: SearchNode[]) { + this.index = index; + this.correctionQueue = new PriorityQueue(QUEUE_NODE_COMPARATOR, initialEdges); + } + + increaseMaxEditDistance() { + // By extracting the entries from the priority queue and increasing distance outside of it as a batch job, + // we get an O(N) implementation, rather than the O(N log N) that would result from maintaining the original queue. + let entries = this.correctionQueue.toArray(); + + entries.forEach(function(edge) { edge.calculation = edge.calculation.increaseMaxDistance(); }); + + // Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify. + this.correctionQueue = new PriorityQueue(QUEUE_NODE_COMPARATOR, entries); + } +} + +export class SearchResult { + private resultNode: SearchNode; + + constructor(node: SearchNode) { + this.resultNode = node; + } + + get inputSequence(): ProbabilityMass[] { + return this.resultNode.priorInput; + } + + get matchSequence(): TraversableToken[] { + return this.resultNode.calculation.matchSequence; + }; + + get matchString(): USVString { + return this.matchSequence.map(value => value.key).join(''); + } + + get knownCost(): number { + return this.resultNode.knownCost; + } + + get inputSamplingCost(): number { + return this.resultNode.inputSamplingCost; + } + + get totalCost(): number { + return this.resultNode.currentCost; + } + + get finalTraversal(): LexiconTraversal { + return this.resultNode.currentTraversal; + } +} + +type NullPath = { + type: 'none' +} + +type IntermediateSearchPath = { + type: 'intermediate', + cost: number +} + +type CompleteSearchPath = { + type: 'complete', + cost: number, + finalNode: SearchNode +} + +type PathResult = NullPath | IntermediateSearchPath | CompleteSearchPath; + +// The set of search spaces corresponding to the same 'context' for search. +// Whenever a wordbreak boundary is crossed, a new instance should be made. +export class SearchSpace { + private QUEUE_SPACE_COMPARATOR: Comparator; + + // p = 1 / (e^4) = 0.01831563888. This still exceeds many neighboring keys! + // p = 1 / (e^5) = 0.00673794699. Strikes a good balance. + // Should easily give priority to neighboring keys before edit-distance kicks in (when keys are a bit ambiguous) + static readonly EDIT_DISTANCE_COST_SCALE = 5; + static readonly MIN_KEYSTROKE_PROBABILITY = 0.0001; + static readonly DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL = 33; // in milliseconds. + + private tierOrdering: SearchSpaceTier[] = []; + private selectionQueue: PriorityQueue; + private inputSequence: Distribution[] = []; + private minInputCost: number[] = []; + private rootNode: SearchNode; + + // We use an array and not a PriorityQueue b/c batch-heapifying at a single point in time + // is cheaper than iteratively building a priority queue. + private completedPaths: SearchNode[]; + + // Marks all results that have already been returned since the last input was received. + private returnedValues: {[mapKey: string]: SearchNode} = {}; + + // Signals that the edge has already been processed. + private processedEdgeSet: {[mapKey: string]: boolean} = {}; + + constructor(model: LexicalModel) { + if(!model) { + throw "The LexicalModel parameter must not be null / undefined."; + } else if(!model.traverseFromRoot) { + throw "The provided model does not implement the `traverseFromRoot` function, which is needed to support robust correction searching."; + } + + // Constructs the comparator needed for the following line. + this.buildQueueSpaceComparator(); + this.selectionQueue = new PriorityQueue(this.QUEUE_SPACE_COMPARATOR); + this.rootNode = new SearchNode(model.traverseFromRoot(), model.toKey ? model.toKey.bind(model) : null); + + this.completedPaths = [this.rootNode]; + + // Adds a base level queue to handle initial insertions. + // Start with _just_ the root node. Necessary for proper empty-token, empty-input handling! + let baseTier = new SearchSpaceTier(0, [this.rootNode]); + this.tierOrdering.push(baseTier); + this.selectionQueue.enqueue(baseTier); + } + + private buildQueueSpaceComparator() { + let searchSpace = this; + + this.QUEUE_SPACE_COMPARATOR = function(space1, space2) { + let node1 = space1.correctionQueue.peek(); + let node2 = space2.correctionQueue.peek(); + + let index1 = space1.index; + let index2 = space2.index; + + let tierMinCost: number = 0; + let sign = 1; + + if(index2 < index1) { + let temp = index2; + index2 = index1; + index1 = temp; + + sign = -1; + } + + // Boost the cost of the lower tier by the minimum cost possible for the missing inputs between them. + // In essence, compare the nodes as if the lower tier had the most likely input appended for each such + // input missing at the lower tier. + // + // A 100% admissible heuristic to favor a deeper search, since the added cost is guaranteed if the path + // is traversed further. + // + // Remember, tier index i's last used input was from input index i-1. + // As a result, i is the first needed input index, with index2 - 1 the last entry needed to match them. + for(let i=index1; i < index2; i++) { + tierMinCost = tierMinCost + searchSpace.minInputCost[i]; + } + + // Guards, just in case one of the search spaces ever has an empty node. + if(node1 && node2) { + // If node1 is lower-tier, node1 is the one in need of boosted cost. + // `sign` flips it when node2 is lower tier. + return node1.currentCost - node2.currentCost + sign * tierMinCost; + } else if(node2) { + return 1; + } else { + return -1; + } + } + } + + increaseMaxEditDistance() { + this.tierOrdering.forEach(function(tier) { tier.increaseMaxEditDistance() }); + } + + addInput(inputDistribution: Distribution) { + this.inputSequence.push(inputDistribution); + + // Assumes that `inputDistribution` is already sorted. + this.minInputCost.push(-Math.log(inputDistribution[0].p)); + + // With a newly-available input, we can extend new input-dependent paths from + // our previously-reached 'extractedResults' nodes. + let newlyAvailableEdges: SearchNode[] = []; + let batches = this.completedPaths.map(function(node) { + let deletions = node.buildDeletionEdges(inputDistribution); + let substitutions = node.buildSubstitutionEdges(inputDistribution); + + return deletions.concat(substitutions); + }); + + // Don't forget to reset the array; the contained nodes no longer reach the search's end. + this.completedPaths = []; + this.returnedValues = {}; + + batches.forEach(function(batch) { + newlyAvailableEdges = newlyAvailableEdges.concat(batch); + }); + + // Now that we've built the new edges, we can efficiently construct the new search tier. + let tier = new SearchSpaceTier(this.tierOrdering.length, newlyAvailableEdges); + this.tierOrdering.push(tier); + this.selectionQueue.enqueue(tier); + } + + // TODO: will want eventually for reversions and/or backspaces + removeLastInput() { + // 1. truncate all entries from that search tier; we need to 'restore' extractedResults to match + // the state that would have existed without the last search tier. + // 2. remove the last search tier. Which may necessitate reconstructing the tier queue, but oh well. + } + + private hasNextMatchEntry(): boolean { + let topQueue = this.selectionQueue.peek(); + if(topQueue) { + return topQueue.correctionQueue.count > 0; + } else { + return false; + } + } + + private handleNextNode(): PathResult { + if(!this.hasNextMatchEntry()) { + return { type: 'none' }; + } + + let bestTier = this.selectionQueue.dequeue(); + let currentNode = bestTier.correctionQueue.dequeue(); + + let unmatchedResult: IntermediateSearchPath = { + type: 'intermediate', + cost: currentNode.currentCost + } + + // Have we already processed a matching edge? If so, skip it. + // We already know the previous edge is of lower cost. + if(this.processedEdgeSet[currentNode.mapKey]) { + this.selectionQueue.enqueue(bestTier); + return unmatchedResult; + } else { + this.processedEdgeSet[currentNode.mapKey] = true; + } + + // Stage 1: filter out nodes/edges we want to prune + + // Forbid a raw edit-distance of greater than 2. + // Note: .knownCost is not scaled, while its contribution to .currentCost _is_ scaled. + let substitutionsOnly = false; + if(currentNode.knownCost > 2) { + return unmatchedResult; + } else if(currentNode.knownCost == 2) { + // Hard restriction: no further edits will be supported. This helps keep the search + // more narrowly focused. + substitutionsOnly = true; + } + + let tierMinCost = 0; + for(let i = 0; i <= bestTier.index; i++) { + tierMinCost += this.minInputCost[i]; + } + + // Thresholds _any_ path, partially based on currently-traversed distance. + // Allows a little 'wiggle room' + 2 "hard" edits. + // Can be important if needed characters don't actually exist on the keyboard + // ... or even just not the then-current layer of the keyboard. + if(currentNode.currentCost > tierMinCost + 2.5 * SearchSpace.EDIT_DISTANCE_COST_SCALE) { + return unmatchedResult; + } + + // Stage 2: build remaining edges + + // Always possible, as this does not require any new input. + if(!substitutionsOnly) { + let insertionEdges = currentNode.buildInsertionEdges(); + bestTier.correctionQueue.enqueueAll(insertionEdges); + } + + if(bestTier.index == this.tierOrdering.length - 1) { + // It was the final tier - store the node for future reference. + this.completedPaths.push(currentNode); + + // Since we don't modify any other tier, we may simply reinsert the removed tier. + this.selectionQueue.enqueue(bestTier); + + return { + type: 'complete', + cost: currentNode.currentCost, + finalNode: currentNode + }; + } else { + // Time to construct new edges for the next tier! + let nextTier = this.tierOrdering[bestTier.index+1]; + + let inputIndex = nextTier.index; + + let deletionEdges = []; + if(!substitutionsOnly) { + deletionEdges = currentNode.buildDeletionEdges(this.inputSequence[inputIndex-1]); + } + let substitutionEdges = currentNode.buildSubstitutionEdges(this.inputSequence[inputIndex-1]); + + // Note: we're live-modifying the tier's cost here! The priority queue loses its guarantees as a result. + nextTier.correctionQueue.enqueueAll(deletionEdges.concat(substitutionEdges)); + + // So, we simply rebuild the selection queue. + this.selectionQueue = new PriorityQueue(this.QUEUE_SPACE_COMPARATOR, this.tierOrdering); + + // We didn't reach an end-node, so we just end the iteration and continue the search. + } + + // If we've somehow fully exhausted all search options, indicate that none remain. + return unmatchedResult; + } + + // Current best guesstimate of how compositor will retrieve ideal corrections. + *getBestMatches(waitMillis?: number): Generator { + // might should also include a 'base cost' parameter of sorts? + let searchSpace = this; + let currentReturns: {[mapKey: string]: SearchNode} = {}; + + let maxTime: number; + if(waitMillis == 0) { + maxTime = Infinity; + } else if(waitMillis == undefined || Number.isNaN(waitMillis)) { // also covers null. + maxTime = SearchSpace.DEFAULT_ALLOTTED_CORRECTION_TIME_INTERVAL; + } else { + maxTime = waitMillis; + } + + /** + * This inner class is designed to help the algorithm detect its active execution time. + * While there's no official JS way to do this, we can approximate it by polling the + * current system time (in ms) after each iteration of a short-duration loop. Unusual + * spikes in system time for a single iteration is likely to indicate that an OS + * context switch occurred at some point during the iteration's execution. + */ + class ExecutionTimer { + /** + * The system time when this instance was created. + */ + private start: number; + + /** + * Marks the system time at the start of the currently-running loop, as noted + * by a call to the `startLoop` function. + */ + private loopStart: number; + + private maxExecutionTime: number; + private maxTrueTime: number; + + private executionTime: number; + + /** + * Used to track intervals in which potential context swaps by the OS may + * have occurred. Context switches generally seem to pause threads for + * at least 16 ms, while we expect each loop iteration to complete + * within just 1 ms. So, any possible context switch should have the + * longest observed change in system time. + * + * See `updateOutliers` for more details. + */ + private largestIntervals: number[] = [0]; + + constructor(maxExecutionTime: number, maxTrueTime: number) { + // JS measures time by the number of milliseconds since Jan 1, 1970. + this.loopStart = this.start = Date.now(); + this.maxExecutionTime = maxExecutionTime; + this.maxTrueTime = maxTrueTime; + } + + startLoop() { + this.loopStart = Date.now(); + } + + markIteration() { + const now = Date.now(); + const delta = now - this.loopStart; + this.executionTime += delta; + + /** + * Update the list of the three longest system-time intervals observed + * for execution of a single loop iteration. + * + * Ignore any zero-ms length intervals; they'd make the logic much + * messier than necessary otherwise. + */ + if(delta) { + // If the currently-observed interval is longer than the shortest of the 3 + // previously-observed longest intervals, replace it. + if(this.largestIntervals.length > 2 && delta > this.largestIntervals[0]) { + this.largestIntervals[0] = delta; + } else { + this.largestIntervals.push(delta); + } + // Puts the list in ascending order. Shortest of the list becomes the head, + // longest one the tail. + this.largestIntervals.sort(); + + // Then, determine if we need to update our outlier-based tweaks. + this.updateOutliers(); + } + } + + updateOutliers() { + /* Base assumption: since each loop of the search should evaluate within ~1ms, + * notably longer execution times are probably context switches. + * + * Base assumption: OS context switches generally last at least 16ms. (Based on + * a window.setTimeout() usually not evaluating for at least + * that long, even if set to 1ms.) + * + * To mitigate these assumptions: we'll track the execution time of every loop + * iteration. If the longest observation somehow matches or exceeds the length of + * the next two almost-longest observations twice over... we have a very strong + * 'context switch' candidate. + * + * Or, in near-formal math/stats: we expect a very low variance in execution + * time among the iterations of the search's loops. With a very low variance, + * ANY significant proportional spikes in execution time are outliers - outliers + * likely caused by an OS context switch. + * + * Rather than do intensive math, we use a somewhat lazy approach below that + * achieves the same net results given our assumptions, even when relaxed somewhat. + * + * The logic below relaxes the base assumptions a bit to be safe: + * - [2ms, 2ms, 8ms] will cause 8ms to be seen as an outlier. + * - [2ms, 3ms, 10ms] will cause 10ms to be seen as an outlier. + * + * Ideally: + * - [1ms, 1ms, 4ms] will view 4ms as an outlier. + * + * So we can safely handle slightly longer average intervals and slightly shorter + * OS context-switch time intervals. + */ + if(this.largestIntervals.length > 2) { + // Precondition: the `largestIntervals` array is sorted in ascending order. + // Shortest entry is at the head, longest at the tail. + if(this.largestIntervals[2] >= 2 * (this.largestIntervals[0] + this.largestIntervals[1])) { + this.executionTime -= this.largestIntervals[2]; + this.largestIntervals.pop(); + } + } + } + + shouldTimeout(): boolean { + const now = Date.now(); + if(now - this.start > this.maxTrueTime) { + return true; + } + + return this.executionTime > this.maxExecutionTime; + } + + resetOutlierCheck() { + this.largestIntervals = []; + } + } + + class BatchingAssistant { + currentCost = Number.MIN_SAFE_INTEGER; + entries: SearchResult[] = []; + + checkAndAdd(entry: SearchNode): SearchResult[] | null { + var result: SearchResult[] = null; + + if(entry.currentCost > this.currentCost) { + result = this.tryFinalize(); + + this.currentCost = entry.currentCost; + } + + // Filter out any duplicated match sequences. The same match sequence may be reached via + // different input sequences, after all. + let outputMapKey = entry.calculation.matchSequence.map(value => value.key).join(''); + + // First, ensure the edge has an existing 'shared' cache entry. + if(!searchSpace.returnedValues[outputMapKey]) { + searchSpace.returnedValues[outputMapKey] = entry; + } + + // Check the generator's local returned-value cache - this determines whether or not we + // need to add a new 'return' to the batch. + if(!currentReturns[outputMapKey]) { + this.entries.push(new SearchResult(entry)); + currentReturns[outputMapKey] = entry; + } + + return result; + } + + tryFinalize(): SearchResult[] | null { + var result: SearchResult[] = null; + if(this.entries.length > 0) { + result = this.entries; + this.entries = []; + } + + return result; + } + } + + let batcher = new BatchingAssistant(); + + const timer = new ExecutionTimer(maxTime*1.5, maxTime); + + // Stage 1 - if we already have extracted results, build a queue just for them and iterate over it first. + let returnedValues = Object.values(this.returnedValues); + if(returnedValues.length > 0) { + let preprocessedQueue = new PriorityQueue(QUEUE_NODE_COMPARATOR, returnedValues); + + // Build batches of same-cost entries. + timer.startLoop(); + while(preprocessedQueue.count > 0) { + let entry = preprocessedQueue.dequeue(); + + // Is the entry a reasonable result? + if(entry.isFullReplacement) { + // If the entry's 'match' fully replaces the input string, we consider it + // unreasonable and ignore it. + continue; + } + + let batch = batcher.checkAndAdd(entry); + timer.markIteration(); + + if(batch) { + // Do not track yielded time. + yield batch; + } + } + + // As we only return a batch once all entries of the same cost have been processed, we can safely + // finalize the last preprocessed group without issue. + let batch = batcher.tryFinalize(); + if(batch) { + // Do not track yielded time. + yield batch; + } + } + + // Stage 2: the fun part; actually searching! + timer.resetOutlierCheck(); + timer.startLoop(); + let timedOut = false; + do { + let newResult: PathResult; + + // Search for a 'complete' path, skipping all partial paths as long as time remains. + do { + newResult = this.handleNextNode(); + timer.markIteration(); + + if(timer.shouldTimeout()) { + timedOut = true; + } + } while(!timedOut && newResult.type == 'intermediate') + + // TODO: check 'cost' on intermediate, running it through batcher to early-detect cost changes. + let batch: SearchResult[]; + if(newResult.type == 'none') { + break; + } else if(newResult.type == 'complete') { + // Is the entry a reasonable result? + if(newResult.finalNode.isFullReplacement) { + // If the entry's 'match' fully replaces the input string, we consider it + // unreasonable and ignore it. Also, if we've reached this point... + // we can(?) assume that everything thereafter is as well. + break; + } + batch = batcher.checkAndAdd(newResult.finalNode); + } + + if(batch) { + yield batch; + } + } while(!timedOut && this.hasNextMatchEntry()); + + // If we _somehow_ exhaust all search options, make sure to return the final results. + let batch = batcher.tryFinalize(); + if(batch) { + yield batch; + } + + return null; + } +} diff --git a/common/web/lm-worker/src/main/correction/index.ts b/common/web/lm-worker/src/main/correction/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..890c49ae3305f4c103b3649f45d269e1f1f5615b --- /dev/null +++ b/common/web/lm-worker/src/main/correction/index.ts @@ -0,0 +1,3 @@ +export * from './classical-calculation.js'; +export * from './context-tracker.js'; +export * from './distance-modeler.js'; \ No newline at end of file diff --git a/common/web/lm-worker/src/index.ts b/common/web/lm-worker/src/main/index.ts similarity index 90% rename from common/web/lm-worker/src/index.ts rename to common/web/lm-worker/src/main/index.ts index 6494c3f5d202eaf332d1cf836bee48dadf540599..a44d4e68bbe5a62e6e43465b23905a9953b3aaba 100644 --- a/common/web/lm-worker/src/index.ts +++ b/common/web/lm-worker/src/main/index.ts @@ -30,9 +30,16 @@ */ /// -/// -/// -/// +import { extendString } from "@keymanapp/web-utils"; + +extendString(); + +import * as models from './models/index.js'; +import * as correction from './correction/index.js'; +import * as wordBreakers from '@keymanapp/models-wordbreakers'; + +import ModelCompositor from './model-compositor.js'; +import { ImportScripts, IncomingMessage, LMLayerWorkerState, LoadMessage, ModelEval, ModelFile, ModelSourceSpec, PostMessage } from './worker-interfaces.js'; /** * Encapsulates all the state required for the LMLayer's worker thread. @@ -57,7 +64,7 @@ * The model and the configuration are ONLY relevant in the `ready` state; * as such, they are NOT direct properties of the LMLayerWorker. */ -class LMLayerWorker { +export default class LMLayerWorker { /** * State pattern. This object handles onMessage(). * handleMessage() can transition to a different state, if @@ -288,16 +295,16 @@ class LMLayerWorker { if(payload.source.type == 'file') { _this.loadModelFile(payload.source.file); } else { - // Creates a closure capturing all top-level names that the model must be able to reference. - // `eval` runs by scope rules; our virtualized worker needs a special scope for this to work. - // - // Reference: https://stackoverflow.com/a/40108685 - // Note that we don't need `this`, but we do need the namespaces seen below. let code = payload.source.code; - let evalInContext = function(LMLayerWorker, models, correction, wordBreakers) { - eval(code); - } - evalInContext(_this, models, correction, wordBreakers); + + // Limits the scope accessible by the code we're about to evaluate; the code may only access + // global scope and the arguments specified in the constructor below. + // + // This is far more encapsulated and likely more secure... and the former point means this is + // easier to bundle and more optimizable when bundling than direct eval. + // Reference: https://esbuild.github.io/link/direct-eval + const modelLoader = new Function('LMLayerWorker', 'models', 'correction', 'wordBreakers', code); + modelLoader(_this, models, correction, wordBreakers); } } }; @@ -401,21 +408,4 @@ class LMLayerWorker { return worker; } -} - -// Let LMLayerWorker be available both in the browser and in Node. -if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { - module.exports = LMLayerWorker; - module.exports['correction'] = correction; - module.exports['models'] = models; - module.exports['wordBreakers'] = wordBreakers; - /// XXX: export the ModelCompositor for testing. - module.exports['ModelCompositor'] = ModelCompositor; - module.exports['TransformUtils'] = TransformUtils; -} else if (typeof self !== 'undefined' && 'postMessage' in self && 'importScripts' in self) { - // Automatically install if we're in a Web Worker. - LMLayerWorker.install(self as any); // really, 'as typeof globalThis', but we're currently getting TS errors from use of that. -} else { - //@ts-ignore - window.LMLayerWorker = LMLayerWorker; -} +} \ No newline at end of file diff --git a/common/web/lm-worker/src/model-compositor.ts b/common/web/lm-worker/src/main/model-compositor.ts similarity index 99% rename from common/web/lm-worker/src/model-compositor.ts rename to common/web/lm-worker/src/main/model-compositor.ts index 2bb4fdbafaa187c84775ea1e11d5da125100e016..561d57a7189e816f4717f9ffcd486b8e6f4dafd8 100644 --- a/common/web/lm-worker/src/model-compositor.ts +++ b/common/web/lm-worker/src/main/model-compositor.ts @@ -1,6 +1,10 @@ -/// +import * as models from '@keymanapp/models-templates'; +import * as wordBreakers from '@keymanapp/models-wordbreakers'; +import * as correction from './correction/index.js' -class ModelCompositor { +import TransformUtils from './transformUtils.js'; + +export default class ModelCompositor { private lexicalModel: LexicalModel; private contextTracker?: correction.ContextTracker; private static readonly MAX_SUGGESTIONS = 12; diff --git a/common/web/lm-worker/src/main/models/dummy-model.ts b/common/web/lm-worker/src/main/models/dummy-model.ts new file mode 100644 index 0000000000000000000000000000000000000000..5312121fde976f0f684dd3ce16843d8e456b7b00 --- /dev/null +++ b/common/web/lm-worker/src/main/models/dummy-model.ts @@ -0,0 +1,86 @@ +/// + +/* + * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos) + * Copyright (c) 2018 SIL International + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + + +/** + * @file dummy-model.ts + * + * Defines the Dummy model, which is used for testing the + * prediction API exclusively. + */ + +/** + * The Dummy Model that returns nonsensical, but predictable results. + */ +export default class DummyModel implements LexicalModel { + configuration: Configuration; + punctuation?: LexicalModelPunctuation; + private _futureSuggestions: Suggestion[][]; + + constructor(options?: any) { + options = options || {}; + // Create a shallow copy of the suggestions; + // this class mutates the array. + this._futureSuggestions = options.futureSuggestions + ? options.futureSuggestions.slice() : []; + + if (options.punctuation) { + this.punctuation = options.punctuation; + } + } + + configure(capabilities: Capabilities): Configuration { + this.configuration = { + leftContextCodePoints: capabilities.maxLeftContextCodePoints, + rightContextCodePoints: capabilities.maxRightContextCodePoints + }; + + return this.configuration; + } + + predict(transform: Transform, context: Context, injectedSuggestions?: Suggestion[]): Distribution { + let makeUniformDistribution = function(suggestions: Suggestion[]): Distribution { + let distribution: Distribution = []; + let n = suggestions.length; + + for(let s of suggestions) { + distribution.push({sample: s, p: 1}); // For a dummy model, this is sufficient. The uniformness is all that matters. + } + + return distribution; + } + + if (injectedSuggestions) { + return makeUniformDistribution(injectedSuggestions); + } + + let currentSet = this._futureSuggestions.shift(); + + if(!currentSet) { + return []; + } else { + return makeUniformDistribution(currentSet); + } + } +}; \ No newline at end of file diff --git a/common/web/lm-worker/src/main/models/index.ts b/common/web/lm-worker/src/main/models/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..14f77edc493bb0d416779f851aa425c6c9021e5f --- /dev/null +++ b/common/web/lm-worker/src/main/models/index.ts @@ -0,0 +1,2 @@ +export * from '@keymanapp/models-templates'; +export { default as DummyModel } from './dummy-model.js'; diff --git a/common/web/lm-worker/src/transformUtils.ts b/common/web/lm-worker/src/main/transformUtils.ts similarity index 94% rename from common/web/lm-worker/src/transformUtils.ts rename to common/web/lm-worker/src/main/transformUtils.ts index cbb2c151fe7557c1a2688d398841de611ccf211b..f9d8d8a63f18324139dd3299b3ea23dc3a3d70f9 100644 --- a/common/web/lm-worker/src/transformUtils.ts +++ b/common/web/lm-worker/src/main/transformUtils.ts @@ -1,4 +1,4 @@ -class TransformUtils { +export default class TransformUtils { static isWhitespace(transform: Transform): boolean { // Matches a string that is entirely one or more characters with Unicode general property Z* or the following: CR, LF, and Tab. const whitespaceRemover = /^[\u0009\u000A\u000D\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u202f\u205f\u3000]+$/i; diff --git a/common/web/lm-worker/src/worker-interfaces.ts b/common/web/lm-worker/src/main/worker-interfaces.ts similarity index 82% rename from common/web/lm-worker/src/worker-interfaces.ts rename to common/web/lm-worker/src/main/worker-interfaces.ts index de8f20a1a23db712ad0f4c6f2d85085a4163247c..bcde9b56fd16f67f0d6473dfead7c2e5905ccebe 100644 --- a/common/web/lm-worker/src/worker-interfaces.ts +++ b/common/web/lm-worker/src/main/worker-interfaces.ts @@ -28,24 +28,26 @@ /// +import type ModelCompositor from './model-compositor.js'; + /** * The signature of self.postMessage(), so that unit tests can mock it. */ -type PostMessage = typeof DedicatedWorkerGlobalScope.prototype.postMessage; -type ImportScripts = typeof DedicatedWorkerGlobalScope.prototype.importScripts; +export type PostMessage = typeof DedicatedWorkerGlobalScope.prototype.postMessage; +export type ImportScripts = typeof DedicatedWorkerGlobalScope.prototype.importScripts; /** * The valid incoming message kinds. */ -type IncomingMessageKind = 'config' | 'load' | 'predict' | 'unload' | 'wordbreak' | 'accept' | 'revert' | 'reset-context'; -type IncomingMessage = ConfigMessage | LoadMessage | PredictMessage | UnloadMessage | WordbreakMessage | AcceptMessage | RevertMessage | ResetContextMessage; +export type IncomingMessageKind = 'config' | 'load' | 'predict' | 'unload' | 'wordbreak' | 'accept' | 'revert' | 'reset-context'; +export type IncomingMessage = ConfigMessage | LoadMessage | PredictMessage | UnloadMessage | WordbreakMessage | AcceptMessage | RevertMessage | ResetContextMessage; /** * The structure of a config message. It should include the platform's supported * capabilities. */ -interface ConfigMessage { +export interface ConfigMessage { message: 'config'; /** @@ -61,7 +63,7 @@ interface ConfigMessage { testMode?: boolean } -interface ModelFile { +export interface ModelFile { type: 'file'; /** @@ -70,7 +72,7 @@ interface ModelFile { file: string; } -interface ModelEval { +export interface ModelEval { type: 'raw'; /** @@ -80,13 +82,13 @@ interface ModelEval { code: string; } -type ModelSourceSpec = ModelFile | ModelEval; +export type ModelSourceSpec = ModelFile | ModelEval; /** * The structure of an initialization message. It should include the model (either in * source code or parameter form), as well as the keyboard's capabilities. */ -interface LoadMessage { +export interface LoadMessage { message: 'load'; /** @@ -98,7 +100,7 @@ interface LoadMessage { /** * Message to suggestion text. */ -interface PredictMessage { +export interface PredictMessage { message: 'predict'; /** @@ -124,14 +126,14 @@ interface PredictMessage { context: Context; } -interface UnloadMessage { +export interface UnloadMessage { message: 'unload' } /** * Message used to request the last pre-cursor word in the context. */ -interface WordbreakMessage { +export interface WordbreakMessage { message: 'wordbreak'; /** @@ -146,7 +148,7 @@ interface WordbreakMessage { context: Context; } -interface AcceptMessage { +export interface AcceptMessage { message: 'accept'; /** @@ -177,7 +179,7 @@ interface AcceptMessage { postTransform?: Transform; } -interface RevertMessage { +export interface RevertMessage { message: 'revert'; /** @@ -198,7 +200,7 @@ interface RevertMessage { context: Context; } -interface ResetContextMessage { +export interface ResetContextMessage { message: 'reset-context'; /** @@ -210,12 +212,12 @@ interface ResetContextMessage { /** * The LMLayer can be in one of the following states. The LMLayer can only produce predictions in the 'ready' state. */ -type LMLayerWorkerState = LMLayerWorkerUnconfiguredState | LMLayerWorkerModellessState | LMLayerWorkerReadyState; + export type LMLayerWorkerState = LMLayerWorkerUnconfiguredState | LMLayerWorkerModellessState | LMLayerWorkerReadyState; /** * Represents the unconfigured state of the LMLayer. */ -interface LMLayerWorkerUnconfiguredState { +export interface LMLayerWorkerUnconfiguredState { name: 'unconfigured'; handleMessage(payload: IncomingMessage): void; } @@ -223,7 +225,7 @@ interface LMLayerWorkerUnconfiguredState { /** * Represents the pre-model-load state of the LMLayer. */ -interface LMLayerWorkerModellessState { +export interface LMLayerWorkerModellessState { name: 'modelless'; handleMessage(payload: IncomingMessage): void; } @@ -231,7 +233,7 @@ interface LMLayerWorkerModellessState { /** * Represents the 'ready' state of the LMLayer. */ -interface LMLayerWorkerReadyState { +export interface LMLayerWorkerReadyState { name: 'ready'; handleMessage(payload: IncomingMessage): void; compositor: ModelCompositor; @@ -240,7 +242,7 @@ interface LMLayerWorkerReadyState { /** * Constructors that return worker internal models. */ -interface WorkerInternalModelConstructor { +export interface WorkerInternalModelConstructor { /** * LexicalModel instances are all given the keyboard's * capabilities, plus any parameters they require. diff --git a/common/predictive-text/embedded_worker.d.ts b/common/web/lm-worker/src/main/worker-main.ts similarity index 67% rename from common/predictive-text/embedded_worker.d.ts rename to common/web/lm-worker/src/main/worker-main.ts index 243339f134bdb736fb4420e379c185d49c8d4fac..665943f66fb35953c0717f325cf2f513be65ff3f 100644 --- a/common/predictive-text/embedded_worker.d.ts +++ b/common/web/lm-worker/src/main/worker-main.ts @@ -20,17 +20,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -// Include the code intended to run WITHIN the Web Worker. -// The worker code MUST be compiled before this file is compiled. -// If you see a 'File: embedded_worker.js not found.' error message, please -// compile the worker first (stage one). -/*/// */ +import LMLayerWorker from './index.js'; -/** - * The body of this function contains code that should run - * inside a Web Worker context. - * - * Convert this code into a blob URI with LMLayer.asBlobURI(), - * and pass the URI into the new Worker() constructor. - */ - declare function LMLayerWorkerCode(): void; +// Let LMLayerWorker be available both in the browser and in Node. +if (typeof self !== 'undefined' && 'postMessage' in self && 'importScripts' in self) { + // Automatically install if we're in a Web Worker. + LMLayerWorker.install(self as any); // really, 'as typeof globalThis', but we're currently getting TS errors from use of that. +} else { + //@ts-ignore + window.LMLayerWorker = LMLayerWorker; +} diff --git a/common/web/lm-worker/src/models/dummy-model.ts b/common/web/lm-worker/src/models/dummy-model.ts deleted file mode 100644 index e077aff5343a37d1d9292b2ebd40ec1faf706d74..0000000000000000000000000000000000000000 --- a/common/web/lm-worker/src/models/dummy-model.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos) - * Copyright (c) 2018 SIL International - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -namespace models { - /** - * @file dummy-model.ts - * - * Defines the Dummy model, which is used for testing the - * prediction API exclusively. - */ - - /** - * The Dummy Model that returns nonsensical, but predictable results. - */ - export class DummyModel implements LexicalModel { - configuration: Configuration; - punctuation?: LexicalModelPunctuation; - private _futureSuggestions: Suggestion[][]; - - constructor(options?: any) { - options = options || {}; - // Create a shallow copy of the suggestions; - // this class mutates the array. - this._futureSuggestions = options.futureSuggestions - ? options.futureSuggestions.slice() : []; - - if (options.punctuation) { - this.punctuation = options.punctuation; - } - } - - configure(capabilities: Capabilities): Configuration { - this.configuration = { - leftContextCodePoints: capabilities.maxLeftContextCodePoints, - rightContextCodePoints: capabilities.maxRightContextCodePoints - }; - - return this.configuration; - } - - predict(transform: Transform, context: Context, injectedSuggestions?: Suggestion[]): Distribution { - let makeUniformDistribution = function(suggestions: Suggestion[]): Distribution { - let distribution: Distribution = []; - let n = suggestions.length; - - for(let s of suggestions) { - distribution.push({sample: s, p: 1}); // For a dummy model, this is sufficient. The uniformness is all that matters. - } - - return distribution; - } - - if (injectedSuggestions) { - return makeUniformDistribution(injectedSuggestions); - } - - let currentSet = this._futureSuggestions.shift(); - - if(!currentSet) { - return []; - } else { - return makeUniformDistribution(currentSet); - } - } - }; -} \ No newline at end of file diff --git a/common/predictive-text/unit_tests/headless/edit-distance/classical-calculation.js b/common/web/lm-worker/src/test/cases/edit-distance/classical-calculation.js similarity index 98% rename from common/predictive-text/unit_tests/headless/edit-distance/classical-calculation.js rename to common/web/lm-worker/src/test/cases/edit-distance/classical-calculation.js index 12159d06b0e6d837c8886cb647530bf4c91baf59..1833e9f4d477dad07950a0a7aa7b05e8de2f9f6b 100644 --- a/common/predictive-text/unit_tests/headless/edit-distance/classical-calculation.js +++ b/common/web/lm-worker/src/test/cases/edit-distance/classical-calculation.js @@ -1,6 +1,5 @@ -var assert = require('chai').assert; -const LMLayerWorker = require('../../../../web/lm-worker/build/intermediate.js'); -var ClassicalDistanceCalculation = LMLayerWorker.correction.ClassicalDistanceCalculation; +import { assert } from 'chai'; +import { ClassicalDistanceCalculation } from '#./correction/classical-calculation.js'; function prettyPrintMatrix(matrix) { for(let r = 0; r < matrix.length; r++) { @@ -261,7 +260,7 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() { // Two transpositions: da <- abcd, wxyz -> zw and one deletion ('g') it("'daefghiwxyz' -> 'abcdefhizw' (width 2) = 7", function() { - buffer = compute("daefghiwxyz", "abcdefhizw", "InputThenMatch", 2); + let buffer = compute("daefghiwxyz", "abcdefhizw", "InputThenMatch", 2); assert.equal(buffer.getHeuristicFinalCost(), 7); }); }); diff --git a/common/predictive-text/unit_tests/headless/edit-distance/context-tracker.js b/common/web/lm-worker/src/test/cases/edit-distance/context-tracker.js similarity index 95% rename from common/predictive-text/unit_tests/headless/edit-distance/context-tracker.js rename to common/web/lm-worker/src/test/cases/edit-distance/context-tracker.js index fa09ca20decd8ec6d3e54dc85159e3b22b258d72..f8b715b43f9d048220b76d30c32bfa631db0f0ed 100644 --- a/common/predictive-text/unit_tests/headless/edit-distance/context-tracker.js +++ b/common/web/lm-worker/src/test/cases/edit-distance/context-tracker.js @@ -1,8 +1,10 @@ -var assert = require('chai').assert; -const LMLayerWorker = require('../../../../web/lm-worker/build/intermediate.js'); -var ContextTracker = LMLayerWorker.correction.ContextTracker; -var ModelCompositor = LMLayerWorker.ModelCompositor; -var models = LMLayerWorker.models; +import { assert } from 'chai'; + +import { ContextTracker } from '#./correction/context-tracker.js'; +import ModelCompositor from '#./model-compositor.js'; +import * as models from '#./models/index.js'; + +import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; describe('ContextTracker', function() { function toWrapperDistribution(transform) { @@ -158,7 +160,7 @@ describe('ContextTracker', function() { punctuation: englishPunctuation }; - let model = new models.TrieModel(jsonFixture('tries/english-1000'), options); + let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), options); let compositor = new ModelCompositor(model); let baseContextState = compositor.contextTracker.analyzeState(model, baseContext); diff --git a/common/predictive-text/unit_tests/headless/edit-distance/distance-modeler.js b/common/web/lm-worker/src/test/cases/edit-distance/distance-modeler.js similarity index 97% rename from common/predictive-text/unit_tests/headless/edit-distance/distance-modeler.js rename to common/web/lm-worker/src/test/cases/edit-distance/distance-modeler.js index b893df826cf63bd82a6b1f8a0d44cd5025d95dfc..c2ed18bbc676321cda00e1c6802df5ebb660f19b 100644 --- a/common/predictive-text/unit_tests/headless/edit-distance/distance-modeler.js +++ b/common/web/lm-worker/src/test/cases/edit-distance/distance-modeler.js @@ -1,7 +1,8 @@ -var assert = require('chai').assert; -const LMLayerWorker = require('../../../../web/lm-worker/build/intermediate.js'); -let models = LMLayerWorker.models; -let correction = LMLayerWorker.correction; +import { assert } from 'chai'; +import * as models from '#./models/index.js'; +import * as correction from '#./correction/index.js'; + +import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; function assertEdgeChars(edge, input, match) { assert.isTrue(edgeHasChars(edge, input, match)); @@ -29,7 +30,7 @@ describe('Correction Distance Modeler', function() { var testModel; before(function() { - testModel = new models.TrieModel(jsonFixture('tries/english-1000')); + testModel = new models.TrieModel(jsonFixture('models/tries/english-1000')); }); it('SearchNode.buildInsertionEdges() - from root', function() { @@ -244,7 +245,7 @@ describe('Correction Distance Modeler', function() { var testModel; before(function() { - testModel = new models.TrieModel(jsonFixture('tries/english-1000')); + testModel = new models.TrieModel(jsonFixture('models/tries/english-1000')); }); let checkResults_teh = function(iter) { diff --git a/common/predictive-text/unit_tests/headless/transform-utils.js b/common/web/lm-worker/src/test/cases/transform-utils.js similarity index 86% rename from common/predictive-text/unit_tests/headless/transform-utils.js rename to common/web/lm-worker/src/test/cases/transform-utils.js index b00a24a1230ebcc66da4f98f1ed63123158cbd1d..d4da4501f43b5c2bae913a9817fd1a77e94955b2 100644 --- a/common/predictive-text/unit_tests/headless/transform-utils.js +++ b/common/web/lm-worker/src/test/cases/transform-utils.js @@ -1,6 +1,6 @@ -var assert = require('chai').assert; +import { assert } from 'chai'; -let TransformUtils = require('../../../web/lm-worker/build/intermediate.js').TransformUtils; +import TransformUtils from '#./transformUtils.js'; describe('TransformUtils', function () { describe('isWhitespace', function () { @@ -20,7 +20,7 @@ describe('TransformUtils', function () { }); it("should match a simple ' ' transform", function() { - transform = { + let transform = { insert: " ", deleteLeft: 0 }; @@ -29,7 +29,7 @@ describe('TransformUtils', function () { }); it("should match a simple ' ' transform with delete-left", function() { - transform = { + let transform = { insert: " ", deleteLeft: 1 }; @@ -38,7 +38,7 @@ describe('TransformUtils', function () { }); it("should match a transform consisting of multiple characters of only whitespace", function() { - transform = { + let transform = { insert: " \n\r\u00a0\t\u2000 ", deleteLeft: 0 }; @@ -47,7 +47,7 @@ describe('TransformUtils', function () { }); it("stress tests", function() { - transform = { + let transform = { insert: " \n\r\u00a0\ta\u2000 ", // the 'a' should cause failure. deleteLeft: 0 }; diff --git a/common/predictive-text/unit_tests/headless/worker-custom-punctuation.js b/common/web/lm-worker/src/test/cases/worker-custom-punctuation.js similarity index 92% rename from common/predictive-text/unit_tests/headless/worker-custom-punctuation.js rename to common/web/lm-worker/src/test/cases/worker-custom-punctuation.js index 2e6e477a77ccbd9feb289db88b7d48034e82b34b..e0ff73d1c9e399509294e4b37135b7fef599e49c 100644 --- a/common/predictive-text/unit_tests/headless/worker-custom-punctuation.js +++ b/common/web/lm-worker/src/test/cases/worker-custom-punctuation.js @@ -2,10 +2,10 @@ * Unit tests for the Dummy prediction model. */ -var assert = require('chai').assert; -const LMLayerWorker = require('../../../web/lm-worker/build/intermediate.js'); -var DummyModel = LMLayerWorker.models.DummyModel; -var ModelCompositor = LMLayerWorker.ModelCompositor; +import { assert } from 'chai'; + +import DummyModel from '#./models/dummy-model.js'; +import ModelCompositor from '#./model-compositor.js'; describe('Custom Punctuation', function () { it('appears in the keep suggestion', function () { diff --git a/common/predictive-text/unit_tests/headless/worker-initialization.js b/common/web/lm-worker/src/test/cases/worker-initialization.js similarity index 89% rename from common/predictive-text/unit_tests/headless/worker-initialization.js rename to common/web/lm-worker/src/test/cases/worker-initialization.js index 49316ae273d501d9db227ef96b8157bb2f46ed8d..3c15543bcebc76f7b3e8fdb41954584f4cf10b99 100644 --- a/common/predictive-text/unit_tests/headless/worker-initialization.js +++ b/common/web/lm-worker/src/test/cases/worker-initialization.js @@ -1,12 +1,17 @@ -var assert = require('chai').assert; -var sinon = require('sinon'); -var fs = require('fs'); +import { assert } from 'chai'; +import sinon from 'sinon'; +import fs from 'fs'; -let LMLayerWorker = require('../../../web/lm-worker/build/intermediate.js'); +import LMLayerWorker from '#./index.js'; + +import { configWorker, createMessageEventWithData, importScriptsWith } from '@keymanapp/common-test-resources/model-helpers.mjs'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); // Unit tests for instantiating and initializing the LMLayer Worker in isolation. // -// Although the LMLayerWorker expected to be used inside a DedicatedWorkerGlobalScope, +// Although the LMLayerWorker expects to be used inside a DedicatedWorkerGlobalScope, // these unit tests DO NOT run inside a Worker, and instead use Sinon fakes to assert // behavior. describe('LMLayerWorker', function() { @@ -149,7 +154,7 @@ describe('LMLayerWorker', function() { message: 'load', source: { type: 'file', - file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + file: require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js") } })); @@ -168,7 +173,7 @@ describe('LMLayerWorker', function() { var worker = LMLayerWorker.install(context); configWorker(worker); - let modelCode = fs.readFileSync("./unit_tests/in_browser/resources/models/simple-dummy.js").toString(); + let modelCode = fs.readFileSync(require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js")).toString(); worker.onMessage(createMessageEventWithData({ message: 'load', @@ -199,7 +204,7 @@ describe('LMLayerWorker', function() { message: 'load', source: { type: 'file', - file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + file: require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js") } })); diff --git a/common/predictive-text/unit_tests/headless/worker-model-compositor.js b/common/web/lm-worker/src/test/cases/worker-model-compositor.js similarity index 97% rename from common/predictive-text/unit_tests/headless/worker-model-compositor.js rename to common/web/lm-worker/src/test/cases/worker-model-compositor.js index 8080b48f47369770ff231eb7f3398a0652ca7d0d..3c2622be1f7476a9372540062ff9edec2c1ece4c 100644 --- a/common/predictive-text/unit_tests/headless/worker-model-compositor.js +++ b/common/web/lm-worker/src/test/cases/worker-model-compositor.js @@ -2,15 +2,20 @@ * Integration tests for the model compositor with the trie model. */ -const { models, wordBreakers, ModelCompositor } = require('../../../web/lm-worker/build/intermediate.js'); +import ModelCompositor from '#./model-compositor.js'; +import * as models from '#./models/index.js'; +import * as wordBreakers from '@keymanapp/models-wordbreakers'; + +import { assert } from 'chai'; + +import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'; -var assert = require('chai').assert; var TrieModel = models.TrieModel; describe('ModelCompositor', function() { describe('Prediction with 14.0+ models', function() { describe('Basic suggestion generation', function() { - var plainModel = new TrieModel(jsonFixture('tries/english-1000'), + var plainModel = new TrieModel(jsonFixture('models/tries/english-1000'), {wordBreaker: wordBreakers.default} ); @@ -169,7 +174,7 @@ describe('ModelCompositor', function() { }; var plainCasedModel = new TrieModel( - jsonFixture('tries/english-1000'), { + jsonFixture('models/tries/english-1000'), { languageUsesCasing: true, applyCasing: plainApplyCasing, wordBreaker: wordBreakers.default, @@ -304,7 +309,7 @@ describe('ModelCompositor', function() { describe('Model uses default-style keying, no casing', function () { var uncasedModel = new TrieModel( - jsonFixture('tries/english-1000'), { + jsonFixture('models/tries/english-1000'), { languageUsesCasing: false, wordBreaker: wordBreakers.default, searchTermToKey: function(text) { @@ -379,7 +384,7 @@ describe('ModelCompositor', function() { }; var casedModel = new TrieModel( - jsonFixture('tries/english-1000'), { + jsonFixture('models/tries/english-1000'), { languageUsesCasing: true, applyCasing: applyCasing, wordBreaker: wordBreakers.default, @@ -505,7 +510,7 @@ describe('ModelCompositor', function() { describe('Prediction with legacy Models (12.0 / 13.0)', function() { it('should compose suggestions from a fat-fingered keypress (no keying needed)', function () { var model = new TrieModel( - jsonFixture('tries/english-1000') + jsonFixture('models/tries/english-1000') ); var composite = new ModelCompositor(model); @@ -535,7 +540,7 @@ describe('ModelCompositor', function() { it('should compose suggestions from a fat-fingered keypress (keying needed)', function () { var model = new TrieModel( - jsonFixture('tries/english-1000') + jsonFixture('models/tries/english-1000') ); var composite = new ModelCompositor(model); @@ -903,7 +908,7 @@ describe('ModelCompositor', function() { id: 13 } - let model = new models.TrieModel(jsonFixture('tries/english-1000'), {punctuation: englishPunctuation}); + let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), {punctuation: englishPunctuation}); let compositor = new ModelCompositor(model); let initialSuggestions = compositor.predict(postTransform, baseContext); @@ -938,7 +943,7 @@ describe('ModelCompositor', function() { id: 13 } - let model = new models.TrieModel(jsonFixture('tries/english-1000'), {punctuation: englishPunctuation}); + let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), {punctuation: englishPunctuation}); let compositor = new ModelCompositor(model); let initialSuggestions = compositor.predict(postTransform, baseContext); diff --git a/common/predictive-text/unit_tests/headless/worker-predict-dummy.js b/common/web/lm-worker/src/test/cases/worker-predict-dummy.js similarity index 93% rename from common/predictive-text/unit_tests/headless/worker-predict-dummy.js rename to common/web/lm-worker/src/test/cases/worker-predict-dummy.js index 7275eeef40d7d433246d715b0dda95ad7be0efed..f552f6e29d7e0671f8cd120a01da3bc02e850ea9 100644 --- a/common/predictive-text/unit_tests/headless/worker-predict-dummy.js +++ b/common/web/lm-worker/src/test/cases/worker-predict-dummy.js @@ -2,9 +2,10 @@ * Unit tests for the Dummy prediction model. */ -var assert = require('chai').assert; -const LMLayerWorker = require('../../../web/lm-worker/build/intermediate.js'); -var DummyModel = LMLayerWorker.models.DummyModel; +import { assert } from 'chai'; +import DummyModel from '#./models/dummy-model.js'; + +import { emptyContext, iGotDistractedByHazel, zeroTransform } from '@keymanapp/common-test-resources/model-helpers.mjs'; describe('LMLayerWorker dummy model', function() { describe('instantiation', function () { diff --git a/common/predictive-text/unit_tests/headless/worker-predict.js b/common/web/lm-worker/src/test/cases/worker-predict.js similarity index 79% rename from common/predictive-text/unit_tests/headless/worker-predict.js rename to common/web/lm-worker/src/test/cases/worker-predict.js index 2a2fd40e15ac20a3bcb42a4b6976ad446a72e0d3..f050998e878d627a40f90ec1b509df5efac0f519 100644 --- a/common/predictive-text/unit_tests/headless/worker-predict.js +++ b/common/web/lm-worker/src/test/cases/worker-predict.js @@ -1,8 +1,13 @@ -var assert = require('chai').assert; -var sinon = require('sinon'); +import { assert } from 'chai'; +import sinon from 'sinon'; -const LMLayerWorker = require('../../../web/lm-worker/build/intermediate.js'); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); +import LMLayerWorker from '#./index.js'; + +import { configWorker, createMessageEventWithData, emptyContext, iGotDistractedByHazel, + importScriptsWith, randomToken, zeroTransform } from '@keymanapp/common-test-resources/model-helpers.mjs'; describe('LMLayerWorker', function () { describe('#predict()', function () { @@ -41,7 +46,7 @@ describe('LMLayerWorker', function () { message: 'load', source: { type: 'file', - file: "./unit_tests/in_browser/resources/models/simple-dummy.js" + file: require.resolve("@keymanapp/common-test-resources/models/simple-dummy.js") } })); sinon.assert.calledWithMatch(fakePostMessage.lastCall, { diff --git a/common/web/lm-worker/tsconfig.json b/common/web/lm-worker/tsconfig.json index 43c77edf0c3e3000805824717b9b1452445e0eae..f439e969409ee6d779ef99ade8d633e96005253a 100644 --- a/common/web/lm-worker/tsconfig.json +++ b/common/web/lm-worker/tsconfig.json @@ -1,27 +1,26 @@ { - "extends": "../../../tsconfig-base.json", + "extends": "../../models/tsconfig.kmw-worker-base.json", "compilerOptions": { - "allowJs": false, - "declaration": true, - "module": "none", - "outFile": "./build/index.js", - "sourceMap": true, + "baseUrl": "./", + "outDir": "build/obj", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", + "rootDir": "./src/main", + + // As this one is the one that directly interfaces with the worker (from the inside) "lib": ["webworker", "es6"], - "target": "es5", - "downlevelIteration": true, }, "references": [ // types { "path": "../../models/types" }, { "path": "../lm-message-types" }, // modules - { "path": "../keyman-version", "prepend": true }, - { "path": "../utils", "prepend": true }, - { "path": "../../models/templates", "prepend": true }, - { "path": "../../models/wordbreakers", "prepend": true }, + { "path": "../keyman-version" }, + { "path": "../utils" }, + { "path": "../../models/templates" }, + { "path": "../../models/wordbreakers" }, ], "include": [ - "src/**/*.ts" + "src/main/**/*.ts" ] } diff --git a/common/web/recorder/build.sh b/common/web/recorder/build.sh index b9e578e05476a529c290f1d5bb43080b0357d35f..73951544b05d3db09bd6d5053c6f518253963eee 100755 --- a/common/web/recorder/build.sh +++ b/common/web/recorder/build.sh @@ -22,16 +22,11 @@ builder_describe \ "@/common/web/keyman-version" \ configure \ clean \ - build \ - ":module Builds recorder-core module" \ - ":proctor Builds headless-testing, node-oriented 'proctor' component" + build builder_describe_outputs \ - configure "/node_modules" \ - configure:module "/node_modules" \ - configure:proctor "/node_modules" \ - build:module "/common/web/recorder/build/index.js" \ - build:proctor "/common/web/recorder/build/nodeProctor/index.js" + configure "/node_modules" \ + build "/common/web/recorder/build/obj/index.js" builder_parse "$@" @@ -40,22 +35,12 @@ if builder_start_action configure; then builder_finish_action success configure fi -if builder_start_action clean:module; then - tsc -b --clean "$THIS_SCRIPT_PATH/src/tsconfig.json" - builder_finish_action success clean:module +if builder_start_action clean; then + tsc --build --clean "$THIS_SCRIPT_PATH/tsconfig.json" + builder_finish_action success clean fi -if builder_start_action clean:proctor; then - tsc -b --clean "$THIS_SCRIPT_PATH/src/nodeProctor.tsconfig.json" - builder_finish_action success clean:proctor -fi - -if builder_start_action build:module; then - tsc --build "$THIS_SCRIPT_PATH/src/tsconfig.json" - builder_finish_action success build:module -fi - -if builder_start_action build:proctor; then - tsc --build "$THIS_SCRIPT_PATH/src/nodeProctor.tsconfig.json" - builder_finish_action success build:proctor +if builder_start_action build; then + tsc --build "$THIS_SCRIPT_PATH/tsconfig.json" + builder_finish_action success build fi \ No newline at end of file diff --git a/common/web/recorder/package.json b/common/web/recorder/package.json index ecc8c438188c73208ebd5e4185bbc3ee48f1a5b6..20ff012f595f15bf378b8f4f7068405ca61cf83e 100644 --- a/common/web/recorder/package.json +++ b/common/web/recorder/package.json @@ -1,7 +1,9 @@ { "name": "@keymanapp/recorder-core", "description": "Core classes used to develop KeymanWeb test cases based on keystrokes", - "main": "index.js", + "main": "build/obj/index.js", + "types": "build/obj/index.d.ts", + "type": "module", "scripts": { "tsc": "tsc", "clean": "tsc -b --clean src/tsconfig.json && tsc -b --clean src/nodeProctor.tsconfig.json" diff --git a/common/web/recorder/src/index.ts b/common/web/recorder/src/index.ts index d01dcad71b1cdb278df2bfae6a5160f60f67d6d7..940f663b684c9d3d44569db22199b7c10b76d784 100644 --- a/common/web/recorder/src/index.ts +++ b/common/web/recorder/src/index.ts @@ -1,774 +1,780 @@ -/// - -namespace KMWRecorder { - //#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation - export abstract class InputEventSpec { - type: "key" | "osk"; - static fromJSONObject(obj: any): InputEventSpec { - if(obj && obj.type) { - if(obj.type == "key") { - return new PhysicalInputEventSpec(obj); - } else if(obj.type == "osk") { - return new OSKInputEventSpec(obj); - } - } else { - throw new SyntaxError("Error in JSON format corresponding to an InputEventSpec!"); - } - } +import { type OutputTarget } from "@keymanapp/keyboard-processor"; +import { KeyDistribution, KeyEvent, Mock } from "@keymanapp/keyboard-processor"; - toPrettyJSON(): string { - // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. - var str = JSON.stringify(this); - return str; - } - } - - export class PhysicalInputEventSpec extends InputEventSpec { - static readonly modifierCodes: { [mod:string]: number } = { - "Shift":0x0001, - "Control":0x0002, - "Alt":0x0004, - "Meta":0x0008, - "CapsLock":0x0010, - "NumLock":0x0020, - "ScrollLock":0x0040 - }; - - // KeyboardEvent properties - type: "key" = "key"; - key: string; - code: string; - keyCode: number; - modifierSet: number; - location: number; - - constructor(e?: PhysicalInputEventSpec) { // parameter is used to reconstruct from JSON. - super(); - - if(e) { - this.key = e.key; - this.code = e.code; - this.keyCode = e.keyCode; - this.modifierSet = e.modifierSet; - this.location = e.location; - } - } +import Proctor from "./proctor.js"; - getModifierState(key: string): boolean { - return (PhysicalInputEventSpec.modifierCodes[key] & this.modifierSet) != 0; - } +export { default as Proctor } from "./proctor.js"; +export { default as NodeProctor } from "./nodeProctor.js"; - generateModifierString(): string { - var list: string = ""; +import * as utils from "@keymanapp/web-utils"; - for(var key in PhysicalInputEventSpec.modifierCodes) { - if(this.getModifierState(key)) { - list += ((list != "" ? " " : "") + key); - } +//#region Defines the InputEventSpec set, used to reconstruct DOM-based events for browser-based simulation +export abstract class InputEventSpec { + type: "key" | "osk"; + static fromJSONObject(obj: any): InputEventSpec { + if(obj && obj.type) { + if(obj.type == "key") { + return new PhysicalInputEventSpec(obj); + } else if(obj.type == "osk") { + return new OSKInputEventSpec(obj); } - - return list; + } else { + throw new SyntaxError("Error in JSON format corresponding to an InputEventSpec!"); } } - export class OSKInputEventSpec extends InputEventSpec { - type: "osk" = "osk"; - keyID: string; - - // The parameter may be used to reconstruct the item from raw JSON. - constructor(e?: OSKInputEventSpec) { - super(); - if(e) { - this.keyID = e.keyID; - } + toPrettyJSON(): string { + // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. + var str = JSON.stringify(this); + return str; + } +} + +export class PhysicalInputEventSpec extends InputEventSpec { + static readonly modifierCodes: { [mod:string]: number } = { + "Shift":0x0001, + "Control":0x0002, + "Alt":0x0004, + "Meta":0x0008, + "CapsLock":0x0010, + "NumLock":0x0020, + "ScrollLock":0x0040 + }; + + // KeyboardEvent properties + type: "key" = "key"; + key: string; + code: string; + keyCode: number; + modifierSet: number; + location: number; + + constructor(e?: PhysicalInputEventSpec) { // parameter is used to reconstruct from JSON. + super(); + + if(e) { + this.key = e.key; + this.code = e.code; + this.keyCode = e.keyCode; + this.modifierSet = e.modifierSet; + this.location = e.location; } } - //#endregion - export abstract class RecordedKeystroke { - type: "key" | "osk"; + getModifierState(key: string): boolean { + return (PhysicalInputEventSpec.modifierCodes[key] & this.modifierSet) != 0; + } - static fromJSONObject(obj: any): RecordedKeystroke { - if(obj && obj.type) { - if(obj.type == "key") { - return new RecordedPhysicalKeystroke(obj as RecordedPhysicalKeystroke); - } else if(obj && obj.type) { - return new RecordedSyntheticKeystroke(obj as RecordedSyntheticKeystroke); - } - } else { - throw new SyntaxError("Error in JSON format corresponding to a RecordedKeystroke!"); - } - } + generateModifierString(): string { + var list: string = ""; - toPrettyJSON(): string { - // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. - var str = JSON.stringify(this); - return str; + for(var key in PhysicalInputEventSpec.modifierCodes) { + if(this.getModifierState(key)) { + list += ((list != "" ? " " : "") + key); + } } - /** - * Returns an InputEventSpec that may be used to simulate the keystroke within a browser-based environment. - */ - abstract get inputEventSpec(): InputEventSpec; + return list; } +} - export class RecordedPhysicalKeystroke extends RecordedKeystroke { - // KeyboardEvent properties - type: "key" = "key"; - - keyCode: number; // may be different from eventSpec's value b/c keymapping - states: number; - modifiers: number; - modifierChanged: boolean; - isVirtualKey: boolean; - vkCode: number; // may be possible to eliminate; differences arise from mnemonics. +export class OSKInputEventSpec extends InputEventSpec { + type: "osk" = "osk"; + keyID: string; - eventSpec: PhysicalInputEventSpec; - - constructor(keystroke: RecordedPhysicalKeystroke) - constructor(keystroke: com.keyman.text.KeyEvent, eventSpec: PhysicalInputEventSpec) - constructor(keystroke: RecordedPhysicalKeystroke|com.keyman.text.KeyEvent, eventSpec?: PhysicalInputEventSpec) { - super(); + // The parameter may be used to reconstruct the item from raw JSON. + constructor(e?: OSKInputEventSpec) { + super(); + if(e) { + this.keyID = e.keyID; + } + } +} +//#endregion - if(keystroke instanceof com.keyman.text.KeyEvent || typeof keystroke.type === 'undefined') { - // Store what is necessary for headless event reconstruction. - keystroke = keystroke as com.keyman.text.KeyEvent; - this.keyCode = keystroke.Lcode; - this.states = keystroke.Lstates; - this.modifiers = keystroke.Lmodifiers; - this.modifierChanged = !!keystroke.LmodifierChange; - this.isVirtualKey = keystroke.LisVirtualKey; - this.vkCode = keystroke.vkCode; +export abstract class RecordedKeystroke { + type: "key" | "osk"; - // Also store the DOM-based event spec for use in integrated testing. - this.eventSpec = eventSpec; - } else { - // It might be a raw object, from JSON. - this.keyCode = keystroke.keyCode; - this.states = keystroke.states; - this.modifiers = keystroke.modifiers; - this.modifierChanged = keystroke.modifierChanged; - this.isVirtualKey = keystroke.isVirtualKey; - this.vkCode = keystroke.vkCode; - - this.eventSpec = new PhysicalInputEventSpec(keystroke.eventSpec); // must also be reconstructed. + static fromJSONObject(obj: any): RecordedKeystroke { + if(obj && obj.type) { + if(obj.type == "key") { + return new RecordedPhysicalKeystroke(obj as RecordedPhysicalKeystroke); + } else if(obj && obj.type) { + return new RecordedSyntheticKeystroke(obj as RecordedSyntheticKeystroke); } + } else { + throw new SyntaxError("Error in JSON format corresponding to a RecordedKeystroke!"); } + } + + toPrettyJSON(): string { + // We want the default, non-spaced JSON for this class, even when otherwise adding whitespace. + var str = JSON.stringify(this); + return str; + } - get inputEventSpec(): InputEventSpec { - return this.eventSpec; + /** + * Returns an InputEventSpec that may be used to simulate the keystroke within a browser-based environment. + */ + abstract get inputEventSpec(): InputEventSpec; +} + +export class RecordedPhysicalKeystroke extends RecordedKeystroke { + // KeyboardEvent properties + type: "key" = "key"; + + keyCode: number; // may be different from eventSpec's value b/c keymapping + states: number; + modifiers: number; + modifierChanged: boolean; + isVirtualKey: boolean; + vkCode: number; // may be possible to eliminate; differences arise from mnemonics. + + eventSpec: PhysicalInputEventSpec; + + constructor(keystroke: RecordedPhysicalKeystroke) + constructor(keystroke: KeyEvent, eventSpec: PhysicalInputEventSpec) + constructor(keystroke: RecordedPhysicalKeystroke|KeyEvent, eventSpec?: PhysicalInputEventSpec) { + super(); + + if(keystroke instanceof KeyEvent || typeof keystroke.type === 'undefined') { + // Store what is necessary for headless event reconstruction. + keystroke = keystroke as KeyEvent; + this.keyCode = keystroke.Lcode; + this.states = keystroke.Lstates; + this.modifiers = keystroke.Lmodifiers; + this.modifierChanged = !!keystroke.LmodifierChange; + this.isVirtualKey = keystroke.LisVirtualKey; + this.vkCode = keystroke.vkCode; + + // Also store the DOM-based event spec for use in integrated testing. + this.eventSpec = eventSpec; + } else { + // It might be a raw object, from JSON. + this.keyCode = keystroke.keyCode; + this.states = keystroke.states; + this.modifiers = keystroke.modifiers; + this.modifierChanged = keystroke.modifierChanged; + this.isVirtualKey = keystroke.isVirtualKey; + this.vkCode = keystroke.vkCode; + + this.eventSpec = new PhysicalInputEventSpec(keystroke.eventSpec); // must also be reconstructed. } } - export class RecordedSyntheticKeystroke extends RecordedKeystroke { - // KeyboardEvent properties - type: "osk" = "osk"; + get inputEventSpec(): InputEventSpec { + return this.eventSpec; + } +} - keyName: string; - layer: string; +export class RecordedSyntheticKeystroke extends RecordedKeystroke { + // KeyboardEvent properties + type: "osk" = "osk"; - keyDistribution?: com.keyman.text.KeyDistribution; + keyName: string; + layer: string; - constructor(keystroke: RecordedSyntheticKeystroke) - constructor(keystroke: com.keyman.text.KeyEvent) - constructor(keystroke: RecordedSyntheticKeystroke|com.keyman.text.KeyEvent) { - super(); + keyDistribution?: KeyDistribution; - if(keystroke instanceof com.keyman.text.KeyEvent || typeof keystroke.type === 'undefined') { - keystroke = keystroke as com.keyman.text.KeyEvent; - // Store what is necessary for headless event reconstruction. + constructor(keystroke: RecordedSyntheticKeystroke) + constructor(keystroke: KeyEvent) + constructor(keystroke: RecordedSyntheticKeystroke|KeyEvent) { + super(); - // Also store the DOM-based event spec for use in integrated testing. - this.layer = keystroke.kbdLayer; - this.keyName = keystroke.kName; - this.keyDistribution = keystroke.keyDistribution; - } else { - // It might be a raw object, from JSON. - this.layer = keystroke.layer; - this.keyName = keystroke.keyName; - this.keyDistribution = keystroke.keyDistribution; - } + if(keystroke instanceof KeyEvent || typeof keystroke.type === 'undefined') { + keystroke = keystroke as KeyEvent; + // Store what is necessary for headless event reconstruction. + + // Also store the DOM-based event spec for use in integrated testing. + this.layer = keystroke.kbdLayer; + this.keyName = keystroke.kName; + this.keyDistribution = keystroke.keyDistribution; + } else { + // It might be a raw object, from JSON. + this.layer = keystroke.layer; + this.keyName = keystroke.keyName; + this.keyDistribution = keystroke.keyDistribution; } + } - get inputEventSpec(): InputEventSpec { - let eventSpec = new OSKInputEventSpec(); - eventSpec.keyID = this.layer + '-' + this.keyName; + get inputEventSpec(): InputEventSpec { + let eventSpec = new OSKInputEventSpec(); + eventSpec.keyID = this.layer + '-' + this.keyName; - return eventSpec; - } + return eventSpec; } +} - export abstract class TestSequence { - inputs: KeyRecord[]; - output: string; - msg?: string; +export abstract class TestSequence { + inputs: KeyRecord[]; + output: string; + msg?: string; - abstract hasOSKInteraction(): boolean; + abstract hasOSKInteraction(): boolean; - test(proctor: Proctor, target?: com.keyman.text.OutputTarget): {success: boolean, result: string} { - // Start with an empty OutputTarget and a fresh KeyboardProcessor. - if(!target) { - target = new com.keyman.text.Mock(); - } + test(proctor: Proctor, target?: OutputTarget): {success: boolean, result: string} { + // Start with an empty OutputTarget and a fresh KeyboardProcessor. + if(!target) { + target = new Mock(); + } - proctor.before(); + proctor.before(); - let result = proctor.simulateSequence(this, target); - proctor.assertEquals(result, this.output, this.msg); + let result = proctor.simulateSequence(this, target); + proctor.assertEquals(result, this.output, this.msg); - return {success: (result == this.output), result: result}; - } + return {success: (result == this.output), result: result}; + } - toPrettyJSON(): string { - var str = "{ "; - if(this.output) { - str += "\"output\": \"" + this.output + "\", "; - } - str += "\"inputs\": [\n"; - for(var i = 0; i < this.inputs.length; i++) { - str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); - } - if(this.msg) { - str += "], \"message\": \"" + this.msg + "\" }"; - } else { - str += "]}"; - } - return str; + toPrettyJSON(): string { + var str = "{ "; + if(this.output) { + str += "\"output\": \"" + this.output + "\", "; + } + str += "\"inputs\": [\n"; + for(var i = 0; i < this.inputs.length; i++) { + str += " " + this.inputs[i].toPrettyJSON() + ((i == this.inputs.length-1) ? "\n" : ",\n"); + } + if(this.msg) { + str += "], \"message\": \"" + this.msg + "\" }"; + } else { + str += "]}"; } + return str; } +} - export class InputEventSpecSequence extends TestSequence { - inputs: InputEventSpec[]; - output: string; - msg?: string; +export class InputEventSpecSequence extends TestSequence { + inputs: InputEventSpec[]; + output: string; + msg?: string; - constructor(ins?: InputEventSpec[] | InputEventSpecSequence, outs?: string, msg?: string) { - super(); + constructor(ins?: InputEventSpec[] | InputEventSpecSequence, outs?: string, msg?: string) { + super(); - if(ins) { - if(ins instanceof Array) { - this.inputs = [].concat(ins); - } else { - // We're constructing from existing JSON. - this.inputs = []; - - for(var ie=0; ie < ins.inputs.length; ie++) { - this.inputs.push(InputEventSpec.fromJSONObject(ins.inputs[ie])); - } - - this.output = ins.output; - this.msg = ins.msg; - return; - } + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); } else { + // We're constructing from existing JSON. this.inputs = []; - } - if(outs) { - this.output = outs; - } + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(InputEventSpec.fromJSONObject(ins.inputs[ie])); + } - if(msg) { - this.msg = msg; + this.output = ins.output; + this.msg = ins.msg; + return; } + } else { + this.inputs = []; } - addInput(event: InputEventSpec, output: string) { - this.inputs.push(event); - this.output = output; + if(outs) { + this.output = outs; } - hasOSKInteraction(): boolean { - for(var i=0; i < this.inputs.length; i++) { - if(this.inputs[i] instanceof OSKInputEventSpec) { - return true; - } - } - - return false; + if(msg) { + this.msg = msg; } } - export class RecordedKeystrokeSequence extends TestSequence { - inputs: RecordedKeystroke[]; - output: string; - msg?: string; + addInput(event: InputEventSpec, output: string) { + this.inputs.push(event); + this.output = output; + } - constructor(ins?: RecordedKeystroke[], outs?: string, msg?: string) - constructor(sequence: RecordedKeystrokeSequence) - constructor(ins?: RecordedKeystroke[] | RecordedKeystrokeSequence, outs?: string, msg?: string) { - super(); + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof OSKInputEventSpec) { + return true; + } + } - if(ins) { - if(ins instanceof Array) { - this.inputs = [].concat(ins); - } else { - // We're constructing from existing JSON. - this.inputs = []; + return false; + } +} - for(var ie=0; ie < ins.inputs.length; ie++) { - this.inputs.push(RecordedKeystroke.fromJSONObject(ins.inputs[ie])); - } +export class RecordedKeystrokeSequence extends TestSequence { + inputs: RecordedKeystroke[]; + output: string; + msg?: string; - this.output = ins.output; - this.msg = ins.msg; - return; - } + constructor(ins?: RecordedKeystroke[], outs?: string, msg?: string) + constructor(sequence: RecordedKeystrokeSequence) + constructor(ins?: RecordedKeystroke[] | RecordedKeystrokeSequence, outs?: string, msg?: string) { + super(); + + if(ins) { + if(ins instanceof Array) { + this.inputs = [].concat(ins); } else { + // We're constructing from existing JSON. this.inputs = []; - } - if(outs) { - this.output = outs; - } + for(var ie=0; ie < ins.inputs.length; ie++) { + this.inputs.push(RecordedKeystroke.fromJSONObject(ins.inputs[ie])); + } - if(msg) { - this.msg = msg; + this.output = ins.output; + this.msg = ins.msg; + return; } + } else { + this.inputs = []; } - addInput(event: RecordedKeystroke, output: string) { - this.inputs.push(event); - this.output = output; + if(outs) { + this.output = outs; } - hasOSKInteraction(): boolean { - for(var i=0; i < this.inputs.length; i++) { - if(this.inputs[i] instanceof RecordedSyntheticKeystroke) { - return true; - } - } - - return false; + if(msg) { + this.msg = msg; } } - class FontStubForLanguage { - family: string; - source: string[]; - - constructor(activeStubEntry: any) { - this.family = activeStubEntry.family; - - var src = activeStubEntry.files; - if(!(src instanceof Array)) { - src = [ src ]; - } + addInput(event: RecordedKeystroke, output: string) { + this.inputs.push(event); + this.output = output; + } - this.source = []; - for(var i=0; i < src.length; i++) { - this.source.push(activeStubEntry.path + src[i]); + hasOSKInteraction(): boolean { + for(var i=0; i < this.inputs.length; i++) { + if(this.inputs[i] instanceof RecordedSyntheticKeystroke) { + return true; } } + + return false; } +} - export class LanguageStubForKeyboard { - id: string; - name: string; - region: string; - font?: FontStubForLanguage; - oskFont?: FontStubForLanguage; +class FontStubForLanguage { + family: string; + source: string[]; - constructor(activeStub: any) { - if(activeStub.KLC) { - this.id = activeStub.KLC; - this.name = activeStub.KL; - this.region = activeStub.KR; + constructor(activeStubEntry: any) { + this.family = activeStubEntry.family; - // Fonts. - if(activeStub.KFont) { - this.font = new FontStubForLanguage(activeStub.KFont); - } - if(activeStub.KOskFont) { - this.oskFont = new FontStubForLanguage(activeStub.KOskFont); - } - } else { - this.id = activeStub.id; - this.name = activeStub.name; - this.region = activeStub.region; + var src = activeStubEntry.files; + if(!(src instanceof Array)) { + src = [ src ]; + } - // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. - this.font = activeStub.font; - this.oskFont = activeStub.oskFont; - } + this.source = []; + for(var i=0; i < src.length; i++) { + this.source.push(activeStubEntry.path + src[i]); } } +} - export class KeyboardStub { - id: string; - name: string; - filename: string; - languages: LanguageStubForKeyboard | LanguageStubForKeyboard[]; - - // Constructs a stub usable with KeymanWeb's addKeyboards() API function from - // the internally-tracked ActiveStub value for that keyboard. - constructor(json?: KeyboardStub) { - if(json) { - this.id = json.id; - this.name = json.name; - this.filename = json.filename; - - if(!Array.isArray(json.languages)) { - this.languages = new LanguageStubForKeyboard(json.languages); - } else { - this.languages = []; - for(var i=0; i < json.languages.length; i++) { - this.languages.push(new LanguageStubForKeyboard(json.languages[i])); - } - } +export class LanguageStubForKeyboard { + id: string; + name: string; + region: string; + font?: FontStubForLanguage; + oskFont?: FontStubForLanguage; + + constructor(activeStub: any) { + if(activeStub.KLC) { + this.id = activeStub.KLC; + this.name = activeStub.KL; + this.region = activeStub.KR; + + // Fonts. + if(activeStub.KFont) { + this.font = new FontStubForLanguage(activeStub.KFont); } - } + if(activeStub.KOskFont) { + this.oskFont = new FontStubForLanguage(activeStub.KOskFont); + } + } else { + this.id = activeStub.id; + this.name = activeStub.name; + this.region = activeStub.region; - getFirstLanguage() { - if(this.languages instanceof LanguageStubForKeyboard) { - return this.languages.id; + // If we end up adding functionality to FontStubForLanguage, we'll need to properly reconstruct these. + this.font = activeStub.font; + this.oskFont = activeStub.oskFont; + } + } +} + +export class KeyboardStub { + id: string; + name: string; + filename: string; + languages: LanguageStubForKeyboard | LanguageStubForKeyboard[]; + + // Constructs a stub usable with KeymanWeb's addKeyboards() API function from + // the internally-tracked ActiveStub value for that keyboard. + constructor(json?: KeyboardStub) { + if(json) { + this.id = json.id; + this.name = json.name; + this.filename = json.filename; + + if(!Array.isArray(json.languages)) { + this.languages = new LanguageStubForKeyboard(json.languages); } else { - return this.languages[0].id; + this.languages = []; + for(var i=0; i < json.languages.length; i++) { + this.languages.push(new LanguageStubForKeyboard(json.languages[i])); + } } } } - type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; - type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; - type BROWSER = 'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! - - export class Constraint { - target: TARGET; - validOSList?: OS[]; - validBrowsers?: BROWSER[]; - - constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { - if(typeof(target) == 'string') { - this.target = target; - this.validOSList = validOSList; - this.validBrowsers = validBrowsers; - } else { - var json = target; - this.target = json.target; - this.validOSList = json.validOSList; - this.validBrowsers = json.validBrowsers; - } + getFirstLanguage() { + if(this.languages instanceof LanguageStubForKeyboard) { + return this.languages.id; + } else { + return this.languages[0].id; + } + } +} + +type TARGET = 'hardware'|'desktop'|'phone'|'tablet'; +type OS = 'windows'|'android'|'ios'|'macosx'|'linux'; +type BROWSER = 'chrome'|'firefox'|'safari'|'opera'; // ! no 'edge' detection in KMW! + +export class Constraint { + target: TARGET; + validOSList?: OS[]; + validBrowsers?: BROWSER[]; + + constructor(target: TARGET|Constraint, validOSList?: OS[], validBrowsers?: BROWSER[]) { + if(typeof(target) == 'string') { + this.target = target; + this.validOSList = validOSList; + this.validBrowsers = validBrowsers; + } else { + var json = target; + this.target = json.target; + this.validOSList = json.validOSList; + this.validBrowsers = json.validBrowsers; } + } - matchesClient(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - // #1: Platform check. - if(usingOSK === true) { - if(this.target != device.formFactor) { - return false; - } - } else if(usingOSK === false) { - if(this.target != 'hardware') { - return false; - } - } else if(this.target != device.formFactor && this.target != 'hardware') { + matchesClient(device: utils.DeviceSpec, usingOSK?: boolean) { + // #1: Platform check. + if(usingOSK === true) { + if(this.target != device.formFactor) { return false; } - - if(this.validOSList) { - if(this.validOSList.indexOf(device.OS as OS) == -1) { - return false; - } - } - - if(this.validBrowsers) { - if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { - return false; - } + } else if(usingOSK === false) { + if(this.target != 'hardware') { + return false; } - - return true; + } else if(this.target != device.formFactor && this.target != 'hardware') { + return false; } - // Checks if another Constraint instance is functionally identical to this one. - equals(other: Constraint) { - if(this.target != other.target) { + if(this.validOSList) { + if(this.validOSList.indexOf(device.OS as OS) == -1) { return false; } + } - var list1 = this.validOSList ? this.validOSList : ['any']; - var list2 = other.validOSList ? other.validOSList : ['any']; - - if(list1.sort().join(',') != list2.sort().join(',')) { + if(this.validBrowsers) { + if(this.validBrowsers.indexOf(device.browser as BROWSER) == -1) { return false; } + } - list1 = this.validBrowsers ? this.validBrowsers : ['web']; - list2 = other.validBrowsers ? other.validBrowsers : ['web']; + return true; + } - if(list1.sort().join(',') != list2.sort().join(',')) { - return false; - } + // Checks if another Constraint instance is functionally identical to this one. + equals(other: Constraint) { + if(this.target != other.target) { + return false; + } - return true; + var list1 = this.validOSList ? this.validOSList : ['any']; + var list2 = other.validOSList ? other.validOSList : ['any']; + + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; } - } - export class TestFailure { - constraint: Constraint; - test: InputEventSpecSequence; - result: string; + list1 = this.validBrowsers ? this.validBrowsers : ['web']; + list2 = other.validBrowsers ? other.validBrowsers : ['web']; - constructor(constraint: Constraint, test: InputEventSpecSequence, output: string) { - this.constraint = constraint; - this.test = test; - this.result = output; + if(list1.sort().join(',') != list2.sort().join(',')) { + return false; } + + return true; } +} - export interface TestSet> { - constraint: Constraint; +export class TestFailure { + constraint: Constraint; + test: InputEventSpecSequence; + result: string; - addTest(seq: Sequence): void; - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean): boolean; - test(proctor: Proctor): TestFailure[]; + constructor(constraint: Constraint, test: InputEventSpecSequence, output: string) { + this.constraint = constraint; + this.test = test; + this.result = output; } +} - /** - * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. - */ - export class EventSpecTestSet implements TestSet { - constraint: Constraint; - testSet: InputEventSpecSequence[]; - - constructor(constraint: Constraint|EventSpecTestSet) { - if("target" in constraint) { - this.constraint = constraint as Constraint; - this.testSet = []; - } else { - var json = constraint as EventSpecTestSet; - this.constraint = new Constraint(json.constraint); - this.testSet = []; +export interface TestSet> { + constraint: Constraint; - // Clone each test sequence / reconstruct from methodless JSON object. - for(var i=0; i < json.testSet.length; i++) { - this.testSet.push(new InputEventSpecSequence(json.testSet[i])); - } + addTest(seq: Sequence): void; + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean): boolean; + test(proctor: Proctor): TestFailure[]; +} + +/** + * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + */ +export class EventSpecTestSet implements TestSet { + constraint: Constraint; + testSet: InputEventSpecSequence[]; + + constructor(constraint: Constraint|EventSpecTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as EventSpecTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new InputEventSpecSequence(json.testSet[i])); } } + } - addTest(seq: InputEventSpecSequence) { - this.testSet.push(seq); - } + addTest(seq: InputEventSpecSequence) { + this.testSet.push(seq); + } - // Used to determine if the current EventSpecTestSet is applicable to be run on a device. - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - return this.constraint.matchesClient(device, usingOSK); - } + // Used to determine if the current EventSpecTestSet is applicable to be run on a device. + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean) { + return this.constraint.matchesClient(device, usingOSK); + } - // Validity should be checked before calling this method. - test(proctor: Proctor): TestFailure[] { - var failures: TestFailure[] = []; - let testSet = this.testSet; + // Validity should be checked before calling this method. + test(proctor: Proctor): TestFailure[] { + var failures: TestFailure[] = []; + let testSet = this.testSet; - for(var i=0; i < testSet.length; i++) { - var testSeq = this[i]; - var simResult = testSet[i].test(proctor); - if(!simResult.success) { - // Failed test! - failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); - } + for(var i=0; i < testSet.length; i++) { + var testSeq = this[i]; + var simResult = testSet[i].test(proctor); + if(!simResult.success) { + // Failed test! + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); } - - return failures.length > 0 ? failures : null; } + + return failures.length > 0 ? failures : null; } +} - /** - * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. - */ - export class RecordedSequenceTestSet implements TestSet { - constraint: Constraint; - testSet: RecordedKeystrokeSequence[]; - - constructor(constraint: Constraint|RecordedSequenceTestSet) { - if("target" in constraint) { - this.constraint = constraint as Constraint; - this.testSet = []; - } else { - var json = constraint as RecordedSequenceTestSet; - this.constraint = new Constraint(json.constraint); - this.testSet = []; +/** + * The core constraint-specific test set definition used for testing versions 10.0 to 13.0. + */ +export class RecordedSequenceTestSet implements TestSet { + constraint: Constraint; + testSet: RecordedKeystrokeSequence[]; - // Clone each test sequence / reconstruct from methodless JSON object. - for(var i=0; i < json.testSet.length; i++) { - this.testSet.push(new RecordedKeystrokeSequence(json.testSet[i])); - } + constructor(constraint: Constraint|RecordedSequenceTestSet) { + if("target" in constraint) { + this.constraint = constraint as Constraint; + this.testSet = []; + } else { + var json = constraint as RecordedSequenceTestSet; + this.constraint = new Constraint(json.constraint); + this.testSet = []; + + // Clone each test sequence / reconstruct from methodless JSON object. + for(var i=0; i < json.testSet.length; i++) { + this.testSet.push(new RecordedKeystrokeSequence(json.testSet[i])); } } + } - addTest(seq: RecordedKeystrokeSequence) { - this.testSet.push(seq); - } + addTest(seq: RecordedKeystrokeSequence) { + this.testSet.push(seq); + } - // Used to determine if the current EventSpecTestSet is applicable to be run on a device. - isValidForDevice(device: com.keyman.utils.DeviceSpec, usingOSK?: boolean) { - return this.constraint.matchesClient(device, usingOSK); - } + // Used to determine if the current EventSpecTestSet is applicable to be run on a device. + isValidForDevice(device: utils.DeviceSpec, usingOSK?: boolean) { + return this.constraint.matchesClient(device, usingOSK); + } - // Validity should be checked before calling this method. - test(proctor: Proctor): TestFailure[] { - var failures: TestFailure[] = []; - let testSet = this.testSet; + // Validity should be checked before calling this method. + test(proctor: Proctor): TestFailure[] { + var failures: TestFailure[] = []; + let testSet = this.testSet; - for(var i=0; i < testSet.length; i++) { - var testSeq = this[i]; - var simResult = testSet[i].test(proctor); - if(!simResult.success) { - // Failed test! - failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); - } + for(var i=0; i < testSet.length; i++) { + var testSeq = this[i]; + var simResult = testSet[i].test(proctor); + if(!simResult.success) { + // Failed test! + failures.push(new TestFailure(this.constraint, testSeq, simResult.result)); } + } + + return failures.length > 0 ? failures : null; + } - return failures.length > 0 ? failures : null; + toTestName(): string { + let name = "constraint: for " + this.constraint.target; + + if(this.constraint.target == 'hardware') { + name += " keyboard"; + } else { + name += " OSK"; + } + if(this.constraint.validOSList) { + name += " on OS of " + JSON.stringify(this.constraint.validOSList); + } + if(this.constraint.validBrowsers) { + name += " in browser of " + JSON.stringify(this.constraint.validBrowsers); } - toTestName(): string { - let name = "constraint: for " + this.constraint.target; + return name; + } +} - if(this.constraint.target == 'hardware') { - name += " keyboard"; - } else { - name += " OSK"; - } - if(this.constraint.validOSList) { - name += " on OS of " + JSON.stringify(this.constraint.validOSList); - } - if(this.constraint.validBrowsers) { - name += " in browser of " + JSON.stringify(this.constraint.validBrowsers); - } +export class KeyboardTest { + /** + * Indicates what version of KMW's recorder the spec conforms to. + */ + public specVersion: utils.Version = KeyboardTest.CURRENT_VERSION; - return name; - } - } - - export class KeyboardTest { - /** - * Indicates what version of KMW's recorder the spec conforms to. - */ - public specVersion: com.keyman.utils.Version = KeyboardTest.CURRENT_VERSION; - - /** - * The version of KMW in which the Recorder was first written. Worked from 10.0 to 13.0 with - * only backward-compatible changes and minor tweaks to conform to internal API shifts. - */ - public static readonly FALLBACK_VERSION = new com.keyman.utils.Version("10.0"); - public static readonly CURRENT_VERSION = new com.keyman.utils.Version("14.0"); - - /** - * The stub information to be passed into keyman.addKeyboards() in order to run the test. - */ - keyboard: KeyboardStub; - - /** - * The master array of test sets, each of which specifies constraints a client must fulfill for - * the tests contained therein to be valid. - */ - inputTestSets: TestSet[]; - - /** - * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. - * @param fromJSON - */ - constructor(fromJSON?: string|KeyboardStub|KeyboardTest) { - if(!fromJSON) { - this.keyboard = null; - this.inputTestSets = []; - return; - } else if(typeof(fromJSON) == 'string') { - fromJSON = JSON.parse(fromJSON) as KeyboardTest; - } else if(fromJSON instanceof KeyboardStub) { - this.keyboard = fromJSON; - this.inputTestSets = []; - return; - } + /** + * The version of KMW in which the Recorder was first written. Worked from 10.0 to 13.0 with + * only backward-compatible changes and minor tweaks to conform to internal API shifts. + */ + public static readonly FALLBACK_VERSION = new utils.Version("10.0"); + public static readonly CURRENT_VERSION = new utils.Version("14.0"); - if(!fromJSON.specVersion) { - fromJSON.specVersion = KeyboardTest.FALLBACK_VERSION; - } else { - // Is serialized to a String when saved. - fromJSON.specVersion = new com.keyman.utils.Version(fromJSON.specVersion as unknown as string); - } + /** + * The stub information to be passed into keyman.addKeyboards() in order to run the test. + */ + keyboard: KeyboardStub; + + /** + * The master array of test sets, each of which specifies constraints a client must fulfill for + * the tests contained therein to be valid. + */ + inputTestSets: TestSet[]; - this.keyboard = new KeyboardStub(fromJSON.keyboard); + /** + * Reconstructs a KeyboardTest object from its JSON representation, restoring its methods. + * @param fromJSON + */ + constructor(fromJSON?: string|KeyboardStub|KeyboardTest) { + if(!fromJSON) { + this.keyboard = null; + this.inputTestSets = []; + return; + } else if(typeof(fromJSON) == 'string') { + fromJSON = JSON.parse(fromJSON) as KeyboardTest; + } else if(fromJSON instanceof KeyboardStub) { + this.keyboard = fromJSON; this.inputTestSets = []; - this.specVersion = fromJSON.specVersion; + return; + } - if(this.specVersion.equals(KeyboardTest.FALLBACK_VERSION)) { - // Top-level test spec: EventSpecTestSet, based entirely on browser events. - for(var i=0; i < fromJSON.inputTestSets.length; i++) { - this.inputTestSets[i] = new EventSpecTestSet(fromJSON.inputTestSets[i] as EventSpecTestSet); - } - } else { - for(var i=0; i < fromJSON.inputTestSets.length; i++) { - this.inputTestSets[i] = new RecordedSequenceTestSet(fromJSON.inputTestSets[i] as RecordedSequenceTestSet); - } - } + if(!fromJSON.specVersion) { + fromJSON.specVersion = KeyboardTest.FALLBACK_VERSION; + } else { + // Is serialized to a String when saved. + fromJSON.specVersion = new utils.Version(fromJSON.specVersion as unknown as string); } - addTest(constraint: Constraint, seq: RecordedKeystrokeSequence) { - if(!this.specVersion.equals(KeyboardTest.CURRENT_VERSION)) { - throw new Error("The currently-loaded test was built to an outdated specification and may not be altered."); - } + this.keyboard = new KeyboardStub(fromJSON.keyboard); + this.inputTestSets = []; + this.specVersion = fromJSON.specVersion; - for(var i=0; i < this.inputTestSets.length; i++) { - if(this.inputTestSets[i].constraint.equals(constraint)) { - this.inputTestSets[i].addTest(seq); - return; - } + if(this.specVersion.equals(KeyboardTest.FALLBACK_VERSION)) { + // Top-level test spec: EventSpecTestSet, based entirely on browser events. + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new EventSpecTestSet(fromJSON.inputTestSets[i] as EventSpecTestSet); + } + } else { + for(var i=0; i < fromJSON.inputTestSets.length; i++) { + this.inputTestSets[i] = new RecordedSequenceTestSet(fromJSON.inputTestSets[i] as RecordedSequenceTestSet); } + } + } - var newSet = new RecordedSequenceTestSet(new Constraint(constraint)); - this.inputTestSets.push(newSet); - newSet.addTest(seq); + addTest(constraint: Constraint, seq: RecordedKeystrokeSequence) { + if(!this.specVersion.equals(KeyboardTest.CURRENT_VERSION)) { + throw new Error("The currently-loaded test was built to an outdated specification and may not be altered."); } - test(proctor: Proctor) { - var setHasRun = false; - var failures: TestFailure[] = []; + for(var i=0; i < this.inputTestSets.length; i++) { + if(this.inputTestSets[i].constraint.equals(constraint)) { + this.inputTestSets[i].addTest(seq); + return; + } + } - proctor.beforeAll(); + var newSet = new RecordedSequenceTestSet(new Constraint(constraint)); + this.inputTestSets.push(newSet); + newSet.addTest(seq); + } - // The original test spec requires a browser environment and thus requires its own `.run` implementation. - if(!(proctor.compatibleWithSuite(this))) { - throw Error("Cannot perform version " + KeyboardTest.FALLBACK_VERSION + "-based testing outside of browser-based environments."); - } + test(proctor: Proctor) { + var setHasRun = false; + var failures: TestFailure[] = []; - // Otherwise, the test spec instances will know how to run in any currently-supported environment. - for(var i = 0; i < this.inputTestSets.length; i++) { - var testSet = this.inputTestSets[i]; + proctor.beforeAll(); - if(proctor.matchesTestSet(testSet)) { - var testFailures = testSet.test(proctor); - if(testFailures) { - failures = failures.concat(testFailures); - } - setHasRun = true; - } - } + // The original test spec requires a browser environment and thus requires its own `.run` implementation. + if(!(proctor.compatibleWithSuite(this))) { + throw Error("Cannot perform version " + KeyboardTest.FALLBACK_VERSION + "-based testing outside of browser-based environments."); + } - if(!setHasRun) { - // The sets CAN be empty, allowing silent failure if/when we actually want that. - console.warn("No test sets for this keyboard were applicable for this device!"); - } + // Otherwise, the test spec instances will know how to run in any currently-supported environment. + for(var i = 0; i < this.inputTestSets.length; i++) { + var testSet = this.inputTestSets[i]; - // Allow the method's caller to trigger a 'fail'. - if(failures.length > 0) { - return failures; - } else { - return null; + if(proctor.matchesTestSet(testSet)) { + var testFailures = testSet.test(proctor); + if(testFailures) { + failures = failures.concat(testFailures); + } + setHasRun = true; } } - isEmpty() { - return this.inputTestSets.length == 0; + if(!setHasRun) { + // The sets CAN be empty, allowing silent failure if/when we actually want that. + console.warn("No test sets for this keyboard were applicable for this device!"); } - toPrettyJSON() { - return JSON.stringify(this, null, ' '); + // Allow the method's caller to trigger a 'fail'. + if(failures.length > 0) { + return failures; + } else { + return null; } + } - get isLegacy(): boolean { - return !this.specVersion.equals(KeyboardTest.CURRENT_VERSION); - } + isEmpty() { + return this.inputTestSets.length == 0; + } + + toPrettyJSON() { + return JSON.stringify(this, null, ' '); + } + + get isLegacy(): boolean { + return !this.specVersion.equals(KeyboardTest.CURRENT_VERSION); } } \ No newline at end of file diff --git a/common/web/recorder/src/nodeProctor.ts b/common/web/recorder/src/nodeProctor.ts index 0ae4b5adcc515ed7686a6902e1b745eb13fc6ba1..5548905e6e96a48b2e8a9a233086ed3dd87b5a70 100644 --- a/common/web/recorder/src/nodeProctor.ts +++ b/common/web/recorder/src/nodeProctor.ts @@ -1,94 +1,101 @@ +import Proctor, { AssertCallback } from "./proctor.js"; +import { + KeyboardTest, + TestSet, + TestSequence, + RecordedKeystrokeSequence, + RecordedPhysicalKeystroke, + RecordedSyntheticKeystroke +} from "./index.js"; + +import { KeyboardInterface, KeyEvent, KeyEventSpec, KeyboardProcessor, Mock, type OutputTarget, KeyboardHarness } from "@keymanapp/keyboard-processor"; +import { DeviceSpec } from "@keymanapp/web-utils"; + +export default class NodeProctor extends Proctor { + private keyboardWithHarness: KeyboardHarness; + public __debug = false; + + constructor(kbdHarness: KeyboardHarness, device: DeviceSpec, assert: AssertCallback) { + super(device, assert); + + this.keyboardWithHarness = kbdHarness; + } -namespace KMWRecorder { - export class NodeProctor extends Proctor { - private keyboard: com.keyman.keyboards.Keyboard; - public __debug = false; - - constructor(keyboard: com.keyman.keyboards.Keyboard, device: com.keyman.utils.DeviceSpec, assert: AssertCallback) { - super(device, assert); - - this.keyboard = keyboard; - } + beforeAll() { + // + } - beforeAll() { - // - } + before() { + // + } - before() { - // - } + compatibleWithSuite(testSuite: KeyboardTest): boolean { + // Original-version tests did not supply core-compatible KeyEvent data. + return !testSuite.specVersion.equals(KeyboardTest.FALLBACK_VERSION); + } - compatibleWithSuite(testSuite: KeyboardTest): boolean { - // Original-version tests did not supply core-compatible KeyEvent data. - return !testSuite.specVersion.equals(KeyboardTest.FALLBACK_VERSION); - } + get debugMode(): boolean { + return this.__debug; + } - get debugMode(): boolean { - return this.__debug; - } + set debugMode(value: boolean) { + this.__debug = value; + } - set debugMode(value: boolean) { - this.__debug = value; - } + matchesTestSet(testSet: TestSet) { + // KeyboardProcessor is abstract enough to run tests aimed at any platform. + return true; + } - matchesTestSet(testSet: TestSet) { - // KeyboardProcessor is abstract enough to run tests aimed at any platform. - return true; + simulateSequence(sequence: TestSequence, target?: OutputTarget): string { + // Start with an empty OutputTarget and a fresh KeyboardProcessor. + if(!target) { + target = new Mock(); } - simulateSequence(sequence: TestSequence, target?: com.keyman.text.OutputTarget): string { - // Start with an empty OutputTarget and a fresh KeyboardProcessor. - if(!target) { - target = new com.keyman.text.Mock(); - } - - // Establish a fresh processor, setting its keyboard appropriately for the test. - let processor = new com.keyman.text.KeyboardProcessor(this.device); - processor.activeKeyboard = this.keyboard; - - if(sequence instanceof RecordedKeystrokeSequence) { - for(let keystroke of sequence.inputs) { - let keyEvent: com.keyman.text.KeyEvent; - if(keystroke instanceof RecordedPhysicalKeystroke) { - // Use the keystroke's stored data to reconstruct the KeyEvent. - keyEvent = { - Lcode: keystroke.keyCode, - Lmodifiers: keystroke.modifiers, - LmodifierChange: keystroke.modifierChanged, - vkCode: keystroke.vkCode, - Lstates: keystroke.states, - kName: '', - device: this.device, - isSynthetic: false, - LisVirtualKey: this.keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards. - } - } else if(keystroke instanceof RecordedSyntheticKeystroke) { - let key = this.keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName); - keyEvent = key.constructKeyEvent(processor, this.device); + // Establish a fresh processor, setting its keyboard appropriately for the test. + let processor = new KeyboardProcessor(this.device); + processor.keyboardInterface = this.keyboardWithHarness as KeyboardInterface; + const keyboard = processor.activeKeyboard; + + if(sequence instanceof RecordedKeystrokeSequence) { + for(let keystroke of sequence.inputs) { + let keyEvent: KeyEventSpec; + if(keystroke instanceof RecordedPhysicalKeystroke) { + // Use the keystroke's stored data to reconstruct the KeyEvent. + keyEvent = { + Lcode: keystroke.keyCode, + Lmodifiers: keystroke.modifiers, + LmodifierChange: keystroke.modifierChanged, + vkCode: keystroke.vkCode, + Lstates: keystroke.states, + kName: '', + device: this.device, + isSynthetic: false, + LisVirtualKey: keyboard.definesPositionalOrMnemonic // Only false for 1.0 keyboards. } + } else if(keystroke instanceof RecordedSyntheticKeystroke) { + let key = keyboard.layout(this.device.formFactor).getLayer(keystroke.layer).getKey(keystroke.keyName); + keyEvent = keyboard.constructKeyEvent(key, this.device, processor.stateKeys); + } - // Fill in the final details of the KeyEvent... - keyEvent.device = this.device; + // Fill in the final details of the KeyEvent... + keyEvent.device = this.device; - // And now, execute the keystroke! - // We don't care too much about particularities of per-keystroke behavior yet. - // ... we _could_ if we wanted to, though. The framework is mostly in place; - // it's a matter of actually adding the feature. - let ruleBehavior = processor.processKeystroke(keyEvent, target); + // And now, execute the keystroke! + // We don't care too much about particularities of per-keystroke behavior yet. + // ... we _could_ if we wanted to, though. The framework is mostly in place; + // it's a matter of actually adding the feature. + let ruleBehavior = processor.processKeystroke(new KeyEvent(keyEvent), target); - if(this.debugMode) { - console.log(JSON.stringify(target, null, ' ')); - console.log(JSON.stringify(ruleBehavior, null, ' ')); - } + if(this.debugMode) { + console.log(JSON.stringify(target, null, ' ')); + console.log(JSON.stringify(ruleBehavior, null, ' ')); } - } else { - throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present."); } - return target.getText(); + } else { + throw new Error("NodeProctor only supports RecordedKeystrokeSequences for testing at present."); } - + return target.getText(); } -} - -// Export the namespace itself, giving access to all contained classes. -module.exports = KMWRecorder; \ No newline at end of file +} \ No newline at end of file diff --git a/common/web/recorder/src/nodeProctor.tsconfig.json b/common/web/recorder/src/nodeProctor.tsconfig.json deleted file mode 100644 index 5b80993dc60b2a000381da8e0a525d8f5d841ad7..0000000000000000000000000000000000000000 --- a/common/web/recorder/src/nodeProctor.tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "extends": "../../../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": true, - "module": "none", - "outDir": "../build/nodeProctor/", - "outFile": "../build/nodeProctor/index.js", - "inlineSources": true, - "inlineSourceMap": true, - "target": "es5", - "types": ["node"], - "lib": ["es6"] - }, - - "files": [ - "index.ts", - "proctor.ts", - "nodeProctor.ts" - ], - - "references": [ - { "path": "../../keyman-version" }, - { "path": "../../utils" }, - { "path": "../../keyboard-processor/src" }, - { "path": "../../lm-message-types" } - ] -} diff --git a/common/web/recorder/src/proctor.ts b/common/web/recorder/src/proctor.ts index 38ffd9997aa295993c7ef79d93af285a97beb1e3..50b07032b6d71a6d9a5b2c7d5d10e49eddd2d92d 100644 --- a/common/web/recorder/src/proctor.ts +++ b/common/web/recorder/src/proctor.ts @@ -1,50 +1,53 @@ -namespace KMWRecorder { - export type AssertCallback = (s1: any, s2: any, msg?: string) => void; +import { type DeviceSpec } from "@keymanapp/web-utils"; +import { type OutputTarget } from "@keymanapp/keyboard-processor"; - /** - * Facilitates running Recorder-generated tests on various platforms. - * - * Note that DOM-aware KeymanWeb will implement a Browser-based version, while - * keyboard-processor and input-processor will use a Node-based version instead. - */ - export abstract class Proctor { - device: com.keyman.utils.DeviceSpec; +import type { KeyboardTest, TestSet, TestSequence } from "./index.js"; - _assert: AssertCallback; +export type AssertCallback = (s1: any, s2: any, msg?: string) => void; - constructor(device: com.keyman.utils.DeviceSpec, assert: AssertCallback) { - this.device = device; +/** + * Facilitates running Recorder-generated tests on various platforms. + * + * Note that DOM-aware KeymanWeb will implement a Browser-based version, while + * keyboard-processor and input-processor will use a Node-based version instead. + */ +export default abstract class Proctor { + device: DeviceSpec; - this._assert = assert; - } + _assert: AssertCallback; + + constructor(device: DeviceSpec, assert: AssertCallback) { + this.device = device; - assertEquals(s1: unknown, s2: unknown, msg?: string) { - if(this._assert) { - this._assert(s1, s2, msg); - } + this._assert = assert; + } + + assertEquals(s1: unknown, s2: unknown, msg?: string) { + if(this._assert) { + this._assert(s1, s2, msg); } + } - // Performs global test prep. - abstract beforeAll(); + // Performs global test prep. + abstract beforeAll(); - // Performs per-test setup - abstract before(); + // Performs per-test setup + abstract before(); - /** - * Allows the proctor to indicate if is capable of executing a suite of tests or not. - * @param testSuite - */ - abstract compatibleWithSuite(testSuite: KeyboardTest): boolean; + /** + * Allows the proctor to indicate if is capable of executing a suite of tests or not. + * @param testSuite + */ + abstract compatibleWithSuite(testSuite: KeyboardTest): boolean; - /** - * Indicates whether or not this Proctor is capable of running the specified set of tests. - */ - abstract matchesTestSet(testSet: TestSet); + /** + * Indicates whether or not this Proctor is capable of running the specified set of tests. + */ + abstract matchesTestSet(testSet: TestSet); - /** - * Simulates the specified test sequence for use in testing. - * @param sequence The recorded sequence, generally provided by a test set. - */ - abstract simulateSequence(sequence: TestSequence, target?: com.keyman.text.OutputTarget); - } + /** + * Simulates the specified test sequence for use in testing. + * @param sequence The recorded sequence, generally provided by a test set. + */ + abstract simulateSequence(sequence: TestSequence, target?: OutputTarget); } \ No newline at end of file diff --git a/common/web/recorder/src/tsconfig.json b/common/web/recorder/src/tsconfig.json deleted file mode 100644 index 00cb00a48221f7340f9afb09874f57574967afe5..0000000000000000000000000000000000000000 --- a/common/web/recorder/src/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "extends": "../../../../tsconfig-base.json", - - "compilerOptions": { - "allowJs": true, - "module": "none", - "outDir": "../build/", - "inlineSources": true, - "inlineSourceMap": true, - "target": "es5", - "types": ["node"], - "lib": ["es6"], - "outFile": "../build/index.js" - }, - - "files": [ - "index.ts", - "proctor.ts" - ], - - "references": [ - { "path": "../../keyman-version" }, - { "path": "../../utils" }, - { "path": "../../keyboard-processor/src" }, - { "path": "../../lm-message-types" } - ] -} diff --git a/common/web/recorder/tsconfig.json b/common/web/recorder/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..1a38d48f035c1698ee57e2c64c059170ff65b02a --- /dev/null +++ b/common/web/recorder/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig.kmw-main-base.json", + + "compilerOptions": { + "types": ["node"], + "baseUrl": "./", + "outDir": "build/obj/", + "tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo", + "rootDir": "./src" + }, + + "include": [ + "src/**/*.ts" + ], + + "references": [ + { "path": "../keyman-version" }, + { "path": "../utils/" }, + { "path": "../keyboard-processor/" }, + { "path": "../lm-message-types" } + ], +} diff --git a/common/web/sentry-manager/build-bundler.js b/common/web/sentry-manager/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..9a9e651d82a110dccd27d6d872d84f90f2012087 --- /dev/null +++ b/common/web/sentry-manager/build-bundler.js @@ -0,0 +1,22 @@ +/* + * Note: while this file is not meant to exist long-term, it provides a nice + * low-level proof-of-concept for esbuild bundling of the various Web submodules. + * + * Add some extra code at the end of src/index.ts and run it to verify successful bundling! + */ + +import esbuild from 'esbuild'; + +await esbuild.build({ + bundle: true, + sourcemap: true, + format: "iife", + nodePaths: ['../../../node_modules'], + entryPoints: { + 'index': 'build/obj/index.js', + }, + outfile: 'build/lib/index.js', + target: "es5", + tsconfig: './src/tsconfig.json', + sourceRoot: '@keymanapp/keyman/common/web/sentry-manager/build/lib/' +}); \ No newline at end of file diff --git a/common/web/sentry-manager/build.sh b/common/web/sentry-manager/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..ce20b57a679bdcc8862559bd20eea1344ef7d25e --- /dev/null +++ b/common/web/sentry-manager/build.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# +# Compiles sentry manager + +set -eu + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +builder_describe "Builds the Sentry-reporting module used with Keyman Engine for Web" \ + "@/common/web/keyman-version" \ + clean configure build + +builder_describe_outputs \ + configure /node_modules \ + build /common/web/sentry-manager/build/lib/index.js + +builder_parse "$@" + +### CLEAN ACTIONS + +if builder_start_action clean; then + rm -rf build/ + builder_finish_action success clean +fi + +### CONFIGURE ACTIONS + +if builder_start_action configure; then + verify_npm_setup + builder_finish_action success configure +fi + +if builder_start_action build; then + tsc --build "$THIS_SCRIPT_PATH/src/tsconfig.json" + node build-bundler.js + builder_finish_action success build +fi diff --git a/common/web/sentry-manager/package.json b/common/web/sentry-manager/package.json index dd3ccdbaba71e97972cee23175dcb9b4d13e84d3..a3e6c142bc20f50dcb6a18a0c0b4de155c9b9df8 100644 --- a/common/web/sentry-manager/package.json +++ b/common/web/sentry-manager/package.json @@ -2,6 +2,7 @@ "name": "@keymanapp/web-sentry-manager", "description": "Common setup for KeymanWeb's Sentry-based error reporting", "main": "dist/index.js", + "type": "module", "scripts": { "tsc": "tsc" }, diff --git a/common/web/sentry-manager/src/build.sh b/common/web/sentry-manager/src/build.sh deleted file mode 100755 index 7f3775702ddf6312bd482c73594cb2501d2f825f..0000000000000000000000000000000000000000 --- a/common/web/sentry-manager/src/build.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -# -# Compiles sentry manager - -set -eu - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../../../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -display_usage ( ) { - echo "build.sh [-skip-package-install]" - echo - echo " -skip-package-install skips the `npm install` dependency check." - echo " (or -S) Intended for use when this script is called by another build script." - echo "" - echo " If more than one target is specified, the last one will take precedence." - exit 1 -} - -# Establish default build parameters -set_default_vars ( ) { - FETCH_DEPS=true -} - -set_default_vars - -# Parse args -while [[ $# -gt 0 ]] ; do - key="$1" - case $key in - -skip-package-install|-S) - set_default_vars - FETCH_DEPS=false - ;; - esac - shift # past argument -done - -if [ $FETCH_DEPS = true ]; then - verify_npm_setup - # We need to build keyman-version with a script for now - "$KEYMAN_ROOT/common/web/keyman-version/build.sh" || builder_die "Could not build keyman-version" -fi - -tsc --build "$THIS_SCRIPT_PATH/tsconfig.json" -if [ $? -ne 0 ]; then - builder_die "Compilation of package for Sentry integration with KeymanWeb failed." -fi \ No newline at end of file diff --git a/common/web/sentry-manager/src/index.ts b/common/web/sentry-manager/src/index.ts index 81215a575415ab7f79ff561e5d12854202e9bb63..b5bb89c0ff55eb1074a2f02cf081a73a9045ddd8 100644 --- a/common/web/sentry-manager/src/index.ts +++ b/common/web/sentry-manager/src/index.ts @@ -1,250 +1,260 @@ -/// +import KEYMAN_VERSION from '@keymanapp/keyman-version'; -namespace com.keyman { - /** - * Controls whether or not the generated Sentry event is logged to the console (true) - * or sent to our Sentry server (false). - */ - let DEBUG = false; - - type Options = { - hostPlatform: "native-web" | "ios" | "android" - }; - - export class KeymanSentryManager { - keymanPlatform: string; - _enabled: boolean = true; - - static STANDARD_ALIASABLE_FILES = { - 'keymanweb.js': 'keymanweb.js', - 'keymanandroid.js': 'keyman.js', // Android's embedded name -> embedded compilation - 'keymanios.js': 'keyman.js', // iOS's embedded name -> embedded compilation - 'kmwuibutton.js': 'kmwuibutton.js', - 'kmwuifloat.js': 'kmwuifloat.js', - 'kmwuitoggle.js': 'kmwuitoggle.js', - 'kmwuitoolbar.js': 'kmwuitoolbar.js' - // Also add entries for the naming system used by Android and iOS - and map them to the EMBEDDED upload, not the std 'native' one. - } +// Unfortunately, we can't bundle Sentry via direct import - something in the process breaks +// esbuild due to needing to transform to ES5. +import { default as SentryType } from '@sentry/browser'; - static DEFAULT_OPTIONS: Options = { - hostPlatform: "native-web" - } +// But this will get us sufficient typing to work with as long as we also link (or prepend) +// a pre-bundled Sentry build artifact. +let Sentry: { + init: typeof SentryType.init; +} = window['Sentry']; - constructor(options: Options = KeymanSentryManager.DEFAULT_OPTIONS) { - this.keymanPlatform = options.hostPlatform; - } +/** + * Controls whether or not the generated Sentry event is logged to the console (true) + * or sent to our Sentry server (false). + */ +let DEBUG = false; - // If we've recognized one of our source files, - aliasFilename(filename: string): string|null { - if(!this.mayAlias(filename)) { - return null; - } +type Options = { + hostPlatform: "native-web" | "ios" | "android" +}; - filename = KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename]; +export class KeymanSentryManager { + keymanPlatform: string; + _enabled: boolean = true; - switch(location.protocol) { - case 'http:': - return 'http://' + location.host + '/' + filename; - case 'file:': - return 'file:///' + filename; - default: - return null; - } + static STANDARD_ALIASABLE_FILES = { + 'keymanweb.js': 'keymanweb.js', + 'keymanweb-webview.js': 'keymanweb-webview.js', + 'kmwuibutton.js': 'kmwuibutton.js', + 'kmwuifloat.js': 'kmwuifloat.js', + 'kmwuitoggle.js': 'kmwuitoggle.js', + 'kmwuitoolbar.js': 'kmwuitoolbar.js' + // Also add entries for the naming system used by Android and iOS - and map them to the EMBEDDED upload, not the std 'native' one. + } + + static DEFAULT_OPTIONS: Options = { + hostPlatform: "native-web" + } + + constructor(options: Options = KeymanSentryManager.DEFAULT_OPTIONS) { + this.keymanPlatform = options.hostPlatform; + } + + // If we've recognized one of our source files, + aliasFilename(filename: string): string|null { + if(!this.mayAlias(filename)) { + return null; } - mayAlias(filename: string): boolean { - return !!KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename]; + switch(location.protocol) { + case 'http:': + return 'http://' + location.host + '/' + filename; + case 'file:': + return 'file:///' + filename; + default: + return null; } + } + + mayAlias(filename: string): boolean { + return !!KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename]; + } - // Filters all expected but unnecessary path prefixes, affixes, and suffixes reported by Sentry from our products. - // This allows us to mask all different sorts of installations with a single uploaded path. - // Modifies original object. - pathFilter(event: any) { - // Get the underlying JS error. - let exception = event.exception; - if(!exception) { - // events are not required to have an exception property - // e.g. console.log-type events - return; + // Filters all expected but unnecessary path prefixes, affixes, and suffixes reported by Sentry from our products. + // This allows us to mask all different sorts of installations with a single uploaded path. + // Modifies original object. + pathFilter(event: any) { + // Get the underlying JS error. + let exception = event.exception; + if(!exception) { + // events are not required to have an exception property + // e.g. console.log-type events + return; + } + // Iterate through all wrapped exceptions. + for(let e of exception.values) { + // If Sentry was unable to generate a stacktrace, there's no path filtering to + // perform on its stack frames. + if(!e.stacktrace) { + continue; } - // Iterate through all wrapped exceptions. - for(let e of exception.values) { - // If Sentry was unable to generate a stacktrace, there's no path filtering to - // perform on its stack frames. - if(!e.stacktrace) { - continue; - } - for(let frame of e.stacktrace.frames) { - let URL = frame.filename as string; - let filename: string = ''; - try { - filename = URL.substr(URL.lastIndexOf('/')+1); - if(this.mayAlias(filename)) { - // Make the file look like it's at the host's root so that the Sentry server will recognize it - // regardless of actual filepath position. - frame.filename = this.aliasFilename(filename) || frame.filename; - } - } catch { - // not sure what to do yet, but... don't make more errors in the error handler. That's a bad idea. + for(let frame of e.stacktrace.frames) { + let URL = frame.filename as string; + let filename: string = ''; + try { + filename = URL.substr(URL.lastIndexOf('/')+1); + if(this.mayAlias(filename)) { + // Make the file look like it's at the host's root so that the Sentry server will recognize it + // regardless of actual filepath position. + frame.filename = this.aliasFilename(filename) || frame.filename; } + } catch { + // not sure what to do yet, but... don't make more errors in the error handler. That's a bad idea. } } } + } - // Attaches some useful debugging information to the specified object, pass-by-reference style. - attachEventMetadata(event: any) { - // Ensure that the 'extra' object exists. (May not exist for synthetic/custom Errors.) - event.extra = event.extra || {}; - event.extra.keymanState = window['keyman']['getDebugInfo'](); - event.extra.keymanHostPlatform = this.keymanPlatform; - } + // Attaches some useful debugging information to the specified object, pass-by-reference style. + attachEventMetadata(event: any) { + // Ensure that the 'extra' object exists. (May not exist for synthetic/custom Errors.) + event.extra = event.extra || {}; + event.extra.keymanState = window['keyman']['getDebugInfo'](); + event.extra.keymanHostPlatform = this.keymanPlatform; + } - // Sanitizes the event object (in-place) to remove sensitive information - // from the breadcrumbs and url (for embedded KeymanWeb) - sanitizeEvent(event: any) { - if (event && event.breadcrumbs) { - event.breadcrumbs.forEach((b: any) => { - if (b.category == 'navigation') { - let NAVIGATION_PATTERN = /(.*)?(keyboard\.html#[^-]+)-.*/; - b.data.from = b.data.from.replace(NAVIGATION_PATTERN, '$1$2'); - b.data.to = b.data.to.replace(NAVIGATION_PATTERN, '$1$2'); - } - }); - } + // Sanitizes the event object (in-place) to remove sensitive information + // from the breadcrumbs and url (for embedded KeymanWeb) + sanitizeEvent(event: any) { + if (event && event.breadcrumbs) { + event.breadcrumbs.forEach((b: any) => { + if (b.category == 'navigation') { + let NAVIGATION_PATTERN = /(.*)?(keyboard\.html#[^-]+)-.*/; + b.data.from = b.data.from.replace(NAVIGATION_PATTERN, '$1$2'); + b.data.to = b.data.to.replace(NAVIGATION_PATTERN, '$1$2'); + } + }); + } - if (event && event.request && event.request.url) { - let URL_PATTERN = /#.*$/; - event.request.url = event.request.url.replace(URL_PATTERN, ''); - } + if (event && event.request && event.request.url) { + let URL_PATTERN = /#.*$/; + event.request.url = event.request.url.replace(URL_PATTERN, ''); } + } - /** - * Pre-processes a Sentry event object (in-place) to provide more metadata and enhance - * the Sentry server's ability to match the error against release artifacts. - * Also will sanitize the Sentry event. - * @param event A Sentry-generated event - */ - prepareEvent(event: any): boolean { - this.pathFilter(event); - this.attachEventMetadata(event); - this.sanitizeEvent(event); - - if(DEBUG) { - console.log("DEBUG: event object for Sentry") - console.log(event); - return false; //event - } else if(!this._enabled) { - console.error(event); - return false; - } else { - return true; - } + /** + * Pre-processes a Sentry event object (in-place) to provide more metadata and enhance + * the Sentry server's ability to match the error against release artifacts. + * Also will sanitize the Sentry event. + * @param event A Sentry-generated event + */ + prepareEvent(event: any): boolean { + this.pathFilter(event); + this.attachEventMetadata(event); + this.sanitizeEvent(event); + + if(DEBUG) { + console.log("DEBUG: event object for Sentry") + console.log(event); + return false; //event + } else if(!this._enabled) { + console.error(event); + return false; + } else { + return true; } + } - /** - * Allows debugging our custom event preparation code without bombarding Sentry with errors - * during development. - * - * Note that Sentry expects us either to return the event object to be sent or to return `null` - * if we want to prevent the event from being sent to the server. - * @param event - */ - prepareEventDebugWrapper(event: any) { - if(DEBUG) { - try { - if(this.prepareEvent(event)) { - return event; - } else { - return null; - } - } catch(err) { - console.log(err); - } - } else { - // If not in DEBUG mode, simply forward to the actual preparer; we should be notified - // of any event-prep errors that may occur. + /** + * Allows debugging our custom event preparation code without bombarding Sentry with errors + * during development. + * + * Note that Sentry expects us either to return the event object to be sent or to return `null` + * if we want to prevent the event from being sent to the server. + * @param event + */ + prepareEventDebugWrapper(event: any) { + if(DEBUG) { + try { if(this.prepareEvent(event)) { return event; } else { return null; } + } catch(err) { + console.log(err); + } + } else { + // If not in DEBUG mode, simply forward to the actual preparer; we should be notified + // of any event-prep errors that may occur. + if(this.prepareEvent(event)) { + return event; + } else { + return null; } } + } - /** - * Capture errors and warnings logged to Console in order to get - * stack traces. We can't use CaptureConsole integration until we - * upgrade to a newer version of Sentry, which has a bit of a cascade - * of changes required, in particular a change of module type and - * transpiling down to ES5. - * - * https://stackoverflow.com/a/53214615/1836776 - */ - initConsole() { - // creating function declarations for better stacktraces (otherwise they'd be anonymous function expressions) - let oldConsoleError = console.error; - let _this = this; - // Note that Sentry may have overridden console.error so we are re-overriding it here post-init. - console.error = reportingConsoleError; // defined via function hoisting - function reportingConsoleError() { - let args = Array.prototype.slice.call(arguments); - if(_this._enabled) { - Sentry.captureException(reduceConsoleArgs(args), { level: 'error' }); - } - return oldConsoleError.apply(console, args); - }; - - let oldConsoleWarn = console.warn; - console.warn = reportingConsoleWarn; // defined via function hoisting - function reportingConsoleWarn() { - let args = Array.prototype.slice.call(arguments); - if(_this._enabled) { - Sentry.captureMessage(reduceConsoleArgs(args), { level: 'warning' }); - } - return oldConsoleWarn.apply(console, args); + /** + * Capture errors and warnings logged to Console in order to get + * stack traces. We can't use CaptureConsole integration until we + * upgrade to a newer version of Sentry, which has a bit of a cascade + * of changes required, in particular a change of module type and + * transpiling down to ES5. + * + * https://stackoverflow.com/a/53214615/1836776 + */ + initConsole() { + // creating function declarations for better stacktraces (otherwise they'd be anonymous function expressions) + let oldConsoleError = console.error; + let _this = this; + // Note that Sentry may have overridden console.error so we are re-overriding it here post-init. + console.error = reportingConsoleError; // defined via function hoisting + function reportingConsoleError() { + let args = Array.prototype.slice.call(arguments); + if(_this._enabled) { + // @ts-ignore // should be an enum with the same value... if we could import it. + Sentry.captureException(reduceConsoleArgs(args), { level: 'error' }); } + return oldConsoleError.apply(console, args); + }; - function reduceConsoleArgs(args) { - let errorMsg = args[0]; - // Make sure errorMsg is either an error or string. - // It's therefore best to pass in new Error('msg') instead of just 'msg' since - // that'll give you a stack trace leading up to the creation of that new Error - // whereas if you just pass in a plain string 'msg', the stack trace will include - // reportingConsoleError and reportingConsoleCall - if (!(errorMsg instanceof Error)) { - // stringify all args as a new Error (which creates a stack trace) - errorMsg = new Error( - args.reduce(function(accumulator, currentValue) { - return accumulator.toString() + ' ' + currentValue.toString(); - }, '') - ); - } - return errorMsg; + let oldConsoleWarn = console.warn; + console.warn = reportingConsoleWarn; // defined via function hoisting + function reportingConsoleWarn() { + let args = Array.prototype.slice.call(arguments); + if(_this._enabled) { + // @ts-ignore // should be an enum with the same value... if we could import it. + Sentry.captureMessage(reduceConsoleArgs(args), { level: 'warning' }); } + return oldConsoleWarn.apply(console, args); } - init() { - // Do the actual Sentry initialization. - //@ts-ignore - Sentry.init({ - beforeSend: this.prepareEventDebugWrapper.bind(this), - // Not using beforeBreadcrumb because that caused breadcrumbs to get lost in Sentry - debug: DEBUG, - dsn: 'https://cf96f32d107c4286ab2fd82af49c4d3b@o1005580.ingest.sentry.io/5983524', // keyman-web DSN - release: com.keyman.KEYMAN_VERSION.VERSION_GIT_TAG, - environment: com.keyman.KEYMAN_VERSION.VERSION_ENVIRONMENT, - }); - - this.initConsole(); + function reduceConsoleArgs(args) { + let errorMsg = args[0]; + // Make sure errorMsg is either an error or string. + // It's therefore best to pass in new Error('msg') instead of just 'msg' since + // that'll give you a stack trace leading up to the creation of that new Error + // whereas if you just pass in a plain string 'msg', the stack trace will include + // reportingConsoleError and reportingConsoleCall + if (!(errorMsg instanceof Error)) { + // stringify all args as a new Error (which creates a stack trace) + errorMsg = new Error( + args.reduce(function(accumulator, currentValue) { + return accumulator.toString() + ' ' + currentValue.toString(); + }, '') + ); + } + return errorMsg; } + } - get enabled(): boolean { - return this._enabled; - } + init() { + // Do the actual Sentry initialization. + //@ts-ignore + Sentry.init({ + beforeSend: this.prepareEventDebugWrapper.bind(this), + // Not using beforeBreadcrumb because that caused breadcrumbs to get lost in Sentry + debug: DEBUG, + dsn: 'https://cf96f32d107c4286ab2fd82af49c4d3b@o1005580.ingest.sentry.io/5983524', // keyman-web DSN + release: KEYMAN_VERSION.VERSION_GIT_TAG, + environment: KEYMAN_VERSION.VERSION_ENVIRONMENT, + }); - set enabled(value: boolean) { - this._enabled = value; - } + this.initConsole(); + } + + get enabled(): boolean { + return this._enabled; + } + + set enabled(value: boolean) { + this._enabled = value; } } + +// Publish to the window. +window['KeymanSentryManager'] = KeymanSentryManager; \ No newline at end of file diff --git a/common/web/sentry-manager/src/tsconfig.json b/common/web/sentry-manager/src/tsconfig.json index f72444f4b8d25d4c10ce22b914d52ca26562137c..490c315dc90372064fc14cd063dc1dfc50a1a528 100644 --- a/common/web/sentry-manager/src/tsconfig.json +++ b/common/web/sentry-manager/src/tsconfig.json @@ -2,23 +2,20 @@ "extends": "../../../../tsconfig-base.json", "compilerOptions": { "allowJs": true, - "module": "none", - "outDir": "../build/", + "allowSyntheticDefaultImports": true, + "baseUrl": "./", "inlineSources": true, + "lib": ["es6", "dom"], + "module": "es6", + "moduleResolution": "Node16", + "outDir": "../build/obj", + "rootDir": "./", "sourceMap": true, "target": "es5", - "lib": ["es6", "dom"], - "outFile": "../build/index.js" + "tsBuildInfoFile": "../build/obj/tsconfig.tsbuildinfo", }, - "references": [ - { "path": "../../keyman-version" } - ], "include": [ - "../../../../node_modules/@sentry/browser/build/bundle.min.js", - ], - - "files": [ - "index.ts", + "*.ts" ] } \ No newline at end of file diff --git a/common/web/tsconfig.kmw-main-base.json b/common/web/tsconfig.kmw-main-base.json new file mode 100644 index 0000000000000000000000000000000000000000..0ed8703e5e25edff7a4561e2a74c13c2e19888e4 --- /dev/null +++ b/common/web/tsconfig.kmw-main-base.json @@ -0,0 +1,3 @@ +{ + "extends": "../../web/tsconfig.base.json" +} diff --git a/common/web/tslib/README.md b/common/web/tslib/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4c87474f627b9528f7f774d7c0b50e2a3175bbf8 --- /dev/null +++ b/common/web/tslib/README.md @@ -0,0 +1,15 @@ +The default import setup for the `tslib` package is unfortunately incompatible with `esbuild` when in ES5 mode. But... +with a little elbow grease, we can fix that with _this_ package by importing its ES5-compatible file and exporting it +as _this_ package's default export for use in anything looking to `"importHelpers"`. + +To utilize this with `esbuild` while enabling the `"importHelpers"` compilation option in your tsconfig.json, you'll want +to set the following in your `esbuild` config: + +```javascript + alias: { + 'tslib': '@keymanapp/tslib' + }, +``` + +Note that esbuild 0.15.16 is the minimum required version to utilize the 'alias' feature necessary to replace `tslib` for +`tsc`-generated `import { /* */ } from 'tslib'` statements that result from enabling `"importHelpers"` in a tsconfig. \ No newline at end of file diff --git a/common/web/tslib/build.sh b/common/web/tslib/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..411e32d8bbb4c3817687363b738f1cc3790e5ca6 --- /dev/null +++ b/common/web/tslib/build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +cd "$THIS_SCRIPT_PATH" + +################################ Main script ################################ + +builder_describe \ + "A ES5 + esbuild compatibility wrapper for the 'tslib' package." \ + clean configure build + +builder_describe_outputs \ + configure "/node_modules" \ + build "/common/web/tslib/build/index.js" + +builder_parse "$@" + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build tsc --build \ No newline at end of file diff --git a/common/web/tslib/package.json b/common/web/tslib/package.json new file mode 100644 index 0000000000000000000000000000000000000000..ceec8c64099e23bd539dc23302fe11b39846a4b0 --- /dev/null +++ b/common/web/tslib/package.json @@ -0,0 +1,28 @@ +{ + "name": "@keymanapp/tslib", + "description": "An ES5 + esbuild-compatible wrapper for the 'tslib' library", + "main": "./build/index.js", + "exports": { + ".": { + "types": "./build/index.d.ts", + "import": "./build/index.js" + }, + "./esbuild-tools": { + "types": "./build/esbuild-tools.d.ts", + "import": "./build/esbuild-tools.js" + } + }, + "scripts": { + "build": "gosh ./build.sh build", + "clean": "gosh ./build.sh clean" + }, + "dependencies": { + "tslib": "^2.5.2", + "typescript": "^4.9.5" + }, + "devDependencies": { + "@keymanapp/resources-gosh": "*", + "esbuild": "^0.15.16" + }, + "type": "module" +} diff --git a/common/web/tslib/src/index.ts b/common/web/tslib/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f173ad83fa91de2a1a7c3f982e39bae3e5617d9 --- /dev/null +++ b/common/web/tslib/src/index.ts @@ -0,0 +1,3 @@ +export * from '../../../../node_modules/tslib/tslib.js'; + +export * as tslib from '../../../../node_modules/tslib/tslib.js'; \ No newline at end of file diff --git a/common/web/tslib/tsconfig.json b/common/web/tslib/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..c7528861ae40c24b8046c5dd0a6b9afa1ab165e2 --- /dev/null +++ b/common/web/tslib/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.kmw-main-base.json", + "compilerOptions": { + "allowJs": true, + "importHelpers": false, // This project exists to help define the helpers for the other KMW components. + "tsBuildInfoFile": "./build/tsconfig.tsbuildinfo", + "lib": ["es6", "DOM"], // The latter is b/c esbuild expects the type. + + "baseUrl": "./src", + "outDir": "./build/", + "rootDir": "./src" + }, + "include": [ + "./src/*.ts" + ] +} diff --git a/common/web/types/package.json b/common/web/types/package.json index 4cff8739a584ae8cd5c583b248a8b109a6bca330..c94f19b12d22cd11f5b927403fcd1bc2a4748e86 100644 --- a/common/web/types/package.json +++ b/common/web/types/package.json @@ -29,7 +29,7 @@ "@keymanapp/keyman-version": "*", "ajv": "^8.11.0", "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50", - "semver": "^7.3.7", + "semver": "^7.5.2", "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829" }, "devDependencies": { diff --git a/common/web/types/src/kmx/element-string.ts b/common/web/types/src/kmx/element-string.ts index 581ab683cd10bd15fc0f48d7a6dd30efd8c163f4..ed64a498ff8e26b73ab70f51f056b563a18304a8 100644 --- a/common/web/types/src/kmx/element-string.ts +++ b/common/web/types/src/kmx/element-string.ts @@ -1,16 +1,19 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; -import { Strs, StrsItem } from './kmx-plus.js'; +import { DependencySections, StrsItem, UsetItem } from './kmx-plus.js'; +import { ElementParser, ElementSegment, ElementType } from '../ldml-keyboard/pattern-parser.js'; +import { MATCH_HEX_ESCAPE, unescapeOneQuadString } from '../util/util.js'; export enum ElemElementFlags { none = 0, - unicode_set = constants.elem_flags_unicode_set, + type = constants.elem_flags_type, tertiary_base = constants.elem_flags_tertiary_base, prebase = constants.elem_flags_prebase } ; export class ElemElement { - value: StrsItem; // UnicodeSet or UCS32LE character + value?: StrsItem; // UnicodeSet or UCS32LE character + uset?: UsetItem; order: number; // -128 to +127; used only by reorder element values tertiary: number; // -128 to +127; used only by reorder element values flags: ElemElementFlags; @@ -24,14 +27,24 @@ export class ElemElement { ; export class ElementString extends Array { - constructor(strs: Strs, source: string, order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) { + /** + * @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem. + * If it is a string, will be interpreted per reorder element rules. + */ + constructor(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) { super(); //TODO-LDML: full UnicodeSet and parsing if(!source) { return; } - let items = source.split(""); + let items : ElementSegment[]; + if (Array.isArray(source)) { + items = source.map(segment => new ElementSegment(segment, ElementType.string)); + } else { + // use segmenter to analyze each part + items = ElementParser.segment(source); + } let orders: Array = order ? order.split(" ") : []; if(orders.length == 1) { @@ -55,10 +68,40 @@ export class ElementString extends Array { for(let i = 0; i < items.length; i++) { let elem = new ElemElement(); - elem.value = strs.allocString(items[i]); + const item = items[i]; + let typeFlag = 0; + if (item.type === ElementType.uset) { + typeFlag |= constants.elem_flags_type_uset; + // TODO-LDML: err on max buffer size + const needRanges = sections.usetparser.sizeUnicodeSet(item.segment); + const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges); + if (!uset) { + // TODO-LDML - error callback + throw Error(`Could not parse uset ${item.segment}`); + } + elem.uset = sections.uset.allocUset(uset, sections); + elem.value = sections.strs.allocString('', true); // no string + } else if (item.type === ElementType.codepoint || item.type === ElementType.escaped || item.type === ElementType.string) { + // some kind of a string + let str = item.segment; + if (item.type === ElementType.escaped && !MATCH_HEX_ESCAPE.test(str)) { + str = unescapeOneQuadString(str); + // TODO-LDML: any other escape forms here? + elem.value = sections.strs.allocString(str, true); + } else { + elem.value = sections.strs.allocAndUnescapeString(str, true); + } + // Now did we end up with one char or no? + if (elem.value.isOneChar) { + typeFlag |= constants.elem_flags_type_char; + } else { + typeFlag |= constants.elem_flags_type_str; + } + } elem.order = orders.length ? parseInt(orders[i], 10) : 0; elem.tertiary = tertiaries.length ? parseInt(tertiaries[i], 10) : 0; elem.flags = ElemElementFlags.none | + (ElemElementFlags.type & typeFlag) | (tertiary_bases?.[i] == '1' /* TODO-LDML: or 'true'? */ ? ElemElementFlags.tertiary_base : 0) | (prebases?.[i] == '1' /* TODO-LDML: or 'true'? */ ? ElemElementFlags.prebase : 0); this.push(elem); diff --git a/common/web/types/src/kmx/kmx-builder.ts b/common/web/types/src/kmx/kmx-builder.ts index d89c7ba33ce8aaf77772c2f840b5a9ac06fa67fe..e795759c60db8367980107f8aa0fd6d52f439bd9 100644 --- a/common/web/types/src/kmx/kmx-builder.ts +++ b/common/web/types/src/kmx/kmx-builder.ts @@ -224,4 +224,4 @@ export default class KMXBuilder { return file; } -} \ No newline at end of file +} diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-disp.ts b/common/web/types/src/kmx/kmx-plus-builder/build-disp.ts index 512bcca0b2f81d1a27a88ad1da1cb4b7b79451de..117fae2be1ef3a4dc4f40e56e981aa5e49103a25 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-disp.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-disp.ts @@ -1,6 +1,6 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import { KMXPlusData } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from './builder-section.js'; /* ------------------------------------------------------------------ @@ -11,13 +11,13 @@ import { BUILDER_SECTION } from './builder-section.js'; * Builder for the 'disp' section */ interface BUILDER_DISP_ITEM { - to: number; - display: number; + to: BUILDER_STR_REF; + display: BUILDER_STR_REF; }; export interface BUILDER_DISP extends BUILDER_SECTION { count: number; - baseCharacter: number; + baseCharacter: BUILDER_STR_REF; items: BUILDER_DISP_ITEM[]; }; diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-elem.ts b/common/web/types/src/kmx/kmx-plus-builder/build-elem.ts index 0c579037d72fe1c8c954299bd5be4ccee6d1d19b..8e325f9591a6726d7c3c9a8aad775b35426b263a 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-elem.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-elem.ts @@ -1,15 +1,19 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { ElementString } from "../element-string.js"; import { Elem } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; -import { BUILDER_SECTION } from "./builder-section.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; +import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; +import { build_uset_index, BUILDER_USET, BUILDER_USET_REF } from "./build-uset.js"; /* ------------------------------------------------------------------ * elem section ------------------------------------------------------------------ */ +/** return from build_elem_index */ +export type BUILDER_ELEM_REF = number; + interface BUILDER_ELEM_ELEMENT { - element: number; // str | UTF-32 char + element: BUILDER_STR_REF | BUILDER_USET_REF | BUILDER_U32CHAR; // str | UTF-32 char flags: number; _value: string; }; @@ -41,7 +45,7 @@ function binaryElemCompare(a: BUILDER_ELEM_STRING, b: BUILDER_ELEM_STRING): numb return 0; } -export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS): BUILDER_ELEM { +export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS, sect_uset: BUILDER_USET): BUILDER_ELEM { let result: BUILDER_ELEM = { ident: constants.hex_section_id(constants.section.elem), size: 0, // finalized below @@ -50,7 +54,6 @@ export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS): BUILDER_ strings: [], // finalized below }; - // TODO: consider padding result.strings = source_elem.strings.map(item => { let res: BUILDER_ELEM_STRING = { offset: 0, // finalized below @@ -60,10 +63,18 @@ export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS): BUILDER_ }; res.items = item.map(v => { + let element; + const type = (v.flags & constants.elem_flags_type); + if (type === constants.elem_flags_type_char || type == constants.elem_flags_type_str) { + element = build_strs_index(sect_strs, v.value); + } else if (type == constants.elem_flags_type_uset) { + element = build_uset_index(sect_uset, v.uset); + } else { + throw Error(`Internal Error: Unknown element type 0x${type.toString(16)}`); + } return { - element: build_strs_index(sect_strs, v.value), - flags: constants.elem_flags_unicode_set | - v.flags | // + element, + flags: v.flags | // ((v.order ?? 0) << constants.elem_flags_order_bitshift) | // -128 to +127; used only by reorder element values ((v.tertiary ?? 0) << constants.elem_flags_tertiary_bitshift), // -128 to +127; used only by reorder element values _value: v.value.value @@ -90,7 +101,7 @@ export function build_elem(source_elem: Elem, sect_strs: BUILDER_STRS): BUILDER_ return result; } -export function build_elem_index(sect_elem: BUILDER_ELEM, value: ElementString) { +export function build_elem_index(sect_elem: BUILDER_ELEM, value: ElementString) : BUILDER_ELEM_REF{ if(!(value instanceof ElementString)) { throw new Error('unexpected value '+value); } @@ -99,5 +110,5 @@ export function build_elem_index(sect_elem: BUILDER_ELEM, value: ElementString) if(result < 0) { throw new Error('unexpectedly missing StrsItem '+value); } - return result; + return result; } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-keys.ts b/common/web/types/src/kmx/kmx-plus-builder/build-keys.ts index 0e9d38e90865e3f6d0c5cd8e8afe8953c158ca24..d9d30e868361801501f4059a742e878aa3de3477 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-keys.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-keys.ts @@ -1,9 +1,9 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KeysFlick, KMXPlusData, StrsItem } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; -import { build_list_index, BUILDER_LIST } from "./build-list.js"; -import { BUILDER_SECTION } from "./builder-section.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; +import { build_list_index, BUILDER_LIST, BUILDER_LIST_REF } from "./build-list.js"; +import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; /* ------------------------------------------------------------------ * keys section @@ -13,15 +13,15 @@ import { BUILDER_SECTION } from "./builder-section.js"; * This struct is a single in the keys keybag */ interface BUILDER_KEYS_KEY { - to: number; // str or single codepoint + to: BUILDER_STR_REF | BUILDER_U32CHAR; // str or single codepoint flags: number; - id: number; // str with original key id + id: BUILDER_STR_REF; // str with original key id _id: string; // original key id, for sorting - switch: number; // str with layer of new l + switch: BUILDER_STR_REF; // str with layer of new l width: number; // ceil((width||1)*10), so 12 for width 1.2 - longPress: number; // list of longPress sequences - longPressDefault: number; // str with the default longPress target - multiTap: number; // list of multiTap sequences + longPress: BUILDER_LIST_REF; // list of longPress sequences + longPressDefault: BUILDER_STR_REF; // str with the default longPress target + multiTap: BUILDER_LIST_REF; // list of multiTap sequences flicks: number; // index into the flicks[] subtable for this flick list }; @@ -31,7 +31,7 @@ interface BUILDER_KEYS_KEY { interface BUILDER_KEYS_FLICKS { count: number; // number of BUILDER_KEYS_FLICK entries in this flick list flick: number; // index into the flick[] subtable of the first flick in the list - id: number; // str with the original id of this flicks + id: BUILDER_STR_REF; // str with the original id of this flicks _id: string; // copy of the flicks id, used for sorting during build _flicks: KeysFlick[]; // temporary copy of KeysFlick object }; @@ -40,9 +40,9 @@ interface BUILDER_KEYS_FLICKS { * This is a single element. */ interface BUILDER_KEYS_FLICK { - directions: number; // list of cardinal/intercardinal directions + directions: BUILDER_LIST_REF; // list of cardinal/intercardinal directions flags: number; // - to: number; // str or single codepoint + to: BUILDER_STR_REF | number; // str or single codepoint }; @@ -56,8 +56,6 @@ interface BUILDER_KEYS_KMAP { * Builder for the 'keys' section */ export interface BUILDER_KEYS extends BUILDER_SECTION { - ident: number; - size: number; keyCount: number; flicksCount: number; flickCount: number; diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts b/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts index da4e79f38f51e68c79b857eb49bd39184b57a5c1..057158f186550802708bb91591eb515c6c29a9ed 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts @@ -1,7 +1,7 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlusData, LayrEntry, LayrRow, StrsItem } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_LIST } from "./build-list.js"; import { BUILDER_SECTION } from "./builder-section.js"; @@ -24,7 +24,7 @@ interface BUILDER_LAYR_LIST { * element */ interface BUILDER_LAYR_LAYER { - id: number; // str of layer id + id: BUILDER_STR_REF; // str of layer id _id: string; // original layer id, for sorting mod: number; // bitfield with modifier info row: number; // row index into row subtable diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-list.ts b/common/web/types/src/kmx/kmx-plus-builder/build-list.ts index a1ed950463212f14a4088499efb7998e100fdcac..26659f8eb6a397a4d5a6a81e1b98645afcc62bee 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-list.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-list.ts @@ -1,13 +1,16 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { List, ListItem } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; /* ------------------------------------------------------------------ * list section ------------------------------------------------------------------ */ -/** +/** reference from build_list_index */ +export type BUILDER_LIST_REF = number; + + /** * A list entry. */ interface BUILDER_LIST_LIST { @@ -17,7 +20,7 @@ interface BUILDER_LIST_LIST { }; interface BUILDER_LIST_INDEX { - str: number; // str for this string + str: BUILDER_STR_REF; // str for this string _value: string; // for locating this string during finalization }; @@ -82,7 +85,10 @@ export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_ * @param value * @returns */ -export function build_list_index(sect_list: BUILDER_LIST, value: ListItem) { +export function build_list_index(sect_list: BUILDER_LIST, value: ListItem) : BUILDER_LIST_REF { + if (!value) { + return 0; // empty list + } if(!(value instanceof ListItem)) { throw new Error('unexpected value '+ value); } @@ -91,5 +97,5 @@ export function build_list_index(sect_list: BUILDER_LIST, value: ListItem) { if(result < 0) { throw new Error('unexpectedly missing ListItem ' + value); // TODO-LDML: it's an array of strs } - return result; + return result; } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-loca.ts b/common/web/types/src/kmx/kmx-plus-builder/build-loca.ts index e7d3198fc279a42906838f12ef714d957d557442..9a72402c8c0e4468331ab342a2f481cf45e119d5 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-loca.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-loca.ts @@ -5,7 +5,7 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlusData } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; /** @@ -13,7 +13,7 @@ import { BUILDER_SECTION } from "./builder-section.js"; */ export interface BUILDER_LOCA extends BUILDER_SECTION { count: number; - items: number[]; //str[] + items: BUILDER_STR_REF[]; //str[] }; export function build_loca(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILDER_LOCA { diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-meta.ts b/common/web/types/src/kmx/kmx-plus-builder/build-meta.ts index 5c60472f356b586d1f4f2a023fe7b79772221a4f..c1633aeca764a04204a45065c110061b5e488b93 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-meta.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-meta.ts @@ -5,19 +5,19 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlusData } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; /** * Builder for the 'meta' section */ export interface BUILDER_META extends BUILDER_SECTION { - author: number; //str - conform: number; //str - layout: number; //str - normalization: number; //str - indicator: number; //str - version: number; //str + author: BUILDER_STR_REF; //str + conform: BUILDER_STR_REF; //str + layout: BUILDER_STR_REF; //str + normalization: BUILDER_STR_REF; //str + indicator: BUILDER_STR_REF; //str + version: BUILDER_STR_REF; //str settings: number; //bitfield }; diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-name.ts b/common/web/types/src/kmx/kmx-plus-builder/build-name.ts index 9397ff0b0a643b1723a07256d7589063bda035db..9463d15204359a83ab6af0433eec689f861de442 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-name.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-name.ts @@ -5,7 +5,7 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlusData } from "../kmx-plus.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; /** @@ -13,7 +13,7 @@ import { BUILDER_SECTION } from "./builder-section.js"; */ export interface BUILDER_NAME extends BUILDER_SECTION { count: number; - items: number[]; //str[] + items: BUILDER_STR_REF[]; }; export function build_name(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS): BUILDER_NAME { diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-ordr.ts b/common/web/types/src/kmx/kmx-plus-builder/build-ordr.ts deleted file mode 100644 index 948bf769934ce0a00a8063c1191466fb74787d97..0000000000000000000000000000000000000000 --- a/common/web/types/src/kmx/kmx-plus-builder/build-ordr.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlusData } from "../kmx-plus.js"; -import { build_elem_index, BUILDER_ELEM } from "./build-elem.js"; -import { BUILDER_STRS } from "./build-strs.js"; -import { BUILDER_SECTION } from "./builder-section.js"; - -/* ------------------------------------------------------------------ -* ordr section - ------------------------------------------------------------------ */ - -interface BUILDER_ORDR_ITEM { - elements: number; //elem.string - before: number; //elem.string -}; - -export interface BUILDER_ORDR extends BUILDER_SECTION { - count: number; - items: BUILDER_ORDR_ITEM[]; -}; - -/** -* Builder for the 'ordr' section -*/ -export function build_ordr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_elem: BUILDER_ELEM): BUILDER_ORDR { - if(!kmxplus.ordr?.items.length) { - return null; - } - - let ordr: BUILDER_ORDR = { - ident: constants.hex_section_id(constants.section.ordr), - size: constants.length_ordr + constants.length_ordr_item * kmxplus.ordr.items.length, - _offset: 0, - count: kmxplus.ordr.items.length, - items: [] - }; - - for(let item of kmxplus.ordr.items) { - ordr.items.push({ - elements: build_elem_index(sect_elem, item.elements), - before: build_elem_index(sect_elem, item.before) - }); - } - - return ordr; -} diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts b/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts index a8b9c91c75ce960d558503c688e9cadb479154ca..503de2d404a37da5d89938b489657cb2b966e047 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts @@ -2,6 +2,9 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { Strs, StrsItem } from "../kmx-plus.js"; import { BUILDER_SECTION } from "./builder-section.js"; +/** reference from build_strs_index */ +export type BUILDER_STR_REF = number; + /* ------------------------------------------------------------------ * strs section ------------------------------------------------------------------ */ @@ -11,7 +14,7 @@ interface BUILDER_STRS_ITEM { // we always write a null terminator, so we can get restructure to do that for us here offset: number; //? new r.Pointer(r.uint32le, new r.String(null, 'utf16le')), length: number; // in UTF-16 code units - _value: string + _value: string; // in-memory: for finding and sorting }; /** @@ -45,14 +48,25 @@ export function build_strs(source_strs: Strs): BUILDER_STRS { return result; } -export function build_strs_index(sect_strs: BUILDER_STRS, value: StrsItem) { +/** + * @returns str index, or UTF-32 char if value.char is set (single char) + */ +export function build_strs_index(sect_strs: BUILDER_STRS, value: StrsItem) : BUILDER_STR_REF { if(!(value instanceof StrsItem)) { - throw new Error('unexpected value '+ value); + if (value === null) { + throw new Error('unexpected null StrsItem, use an empty string instead'); + } else { + throw new Error('unexpected StrsItem value '+ value); + } + } + + if(value.isOneChar) { + return value.char; } let result = sect_strs.items.findIndex(v => v._value === value.value); if(result < 0) { throw new Error('unexpectedly missing StrsItem '+value.value); } - return result; + return result; } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-tran.ts b/common/web/types/src/kmx/kmx-plus-builder/build-tran.ts index c700d1ea085cddf8bd2a9827470fd3f8a24d040f..316df6985395dea11d31da2924abf5bec8628182 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-tran.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-tran.ts @@ -1,50 +1,100 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { Bksp, Finl, Tran } from "../kmx-plus.js"; -import { build_elem_index, BUILDER_ELEM } from "./build-elem.js"; -import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { Bksp, Tran } from "../kmx-plus.js"; +import { BUILDER_ELEM, BUILDER_ELEM_REF, build_elem_index } from "./build-elem.js"; +import { BUILDER_STRS, BUILDER_STR_REF, build_strs_index } from "./build-strs.js"; import { BUILDER_SECTION } from "./builder-section.js"; /* ------------------------------------------------------------------ * tran section ------------------------------------------------------------------ */ -interface BUILDER_TRAN_ITEM { - from: number; //elem.string - to: number; //str - before: number; //elem.string - flags: number; +interface BUILDER_TRAN_GROUP { + type: number; //type of group + count: number; //count + index: number; //index into subtable +}; + +interface BUILDER_TRAN_TRANSFORM { + from: BUILDER_STR_REF; //str + to: BUILDER_STR_REF; //str + mapFrom: BUILDER_ELEM_REF; // elem + mapTo: BUILDER_ELEM_REF; // elem +}; + +interface BUILDER_TRAN_REORDER { + elements: BUILDER_ELEM_REF; //elem + before: BUILDER_ELEM_REF; //elem }; export interface BUILDER_TRAN extends BUILDER_SECTION { - count: number; - items: BUILDER_TRAN_ITEM[]; + groupCount: number; + transformCount: number; + reorderCount: number; + groups: BUILDER_TRAN_GROUP[]; + transforms: BUILDER_TRAN_TRANSFORM[]; + reorders: BUILDER_TRAN_REORDER[]; }; /** * Builder for the 'tran', 'finl', and 'bksp' sections, all of which use the BUILDER_TRAN layout */ -export function build_tran(source_tran: Tran|Finl|Bksp, sect_strs: BUILDER_STRS, sect_elem: BUILDER_ELEM): BUILDER_TRAN { - if(!source_tran?.items.length) { - return null; - } - - let tran: BUILDER_TRAN = { - ident: constants.hex_section_id(source_tran.id), - size: constants.length_tran + constants.length_tran_item * source_tran.items.length, - _offset: 0, - count: source_tran.items.length, - items: [] - }; - - for(let item of source_tran.items) { - tran.items.push({ - from: build_elem_index(sect_elem, item.from), - to: build_strs_index(sect_strs, item.to), - before: build_elem_index(sect_elem, item.before), - flags: item.flags - }); - } - - return tran; +export function build_tran(source_tran: Tran | Bksp, sect_strs: BUILDER_STRS, sect_elem: BUILDER_ELEM): BUILDER_TRAN { + if (!source_tran?.groups?.length) { + return null; + } + + let tran: BUILDER_TRAN = { + ident: constants.hex_section_id(source_tran.id), + size: 0, // need to compute total transforms + reorders + _offset: 0, + groupCount: source_tran.groups.length, + transformCount: 0, + reorderCount: 0, + groups: [], + transforms: [], + reorders: [], + }; + + for (let group of source_tran.groups) { + if (group.type === constants.tran_group_type_transform) { + tran.groups.push({ + type: group.type, + count: group.transforms.length, + index: tran.transforms.length, // index of first item + }); + for (let transform of group.transforms) { + tran.transforms.push({ + from: build_strs_index(sect_strs, transform.from), + to: build_strs_index(sect_strs, transform.to), + mapFrom: build_strs_index(sect_strs, transform.mapFrom), + mapTo: build_strs_index(sect_strs, transform.mapTo), + }); + } + } else if (group.type === constants.tran_group_type_reorder) { + tran.groups.push({ + type: group.type, + count: group.reorders.length, + index: tran.reorders.length, // index of first item + }); + for (let reorder of group.reorders) { + tran.reorders.push({ + elements: build_elem_index(sect_elem, reorder.elements), + before: build_elem_index(sect_elem, reorder.before), + }); + } + } else { + throw new Error(`Internal error: at transformGroup #${tran.groups.length + 1}: Unexpected transformGroup type ${group.type}`); + } + } + // now set the sizes + tran.transformCount = tran.transforms.length; + tran.reorderCount = tran.reorders.length; + + tran.size = constants.length_tran + + (constants.length_tran_group * source_tran.groups.length) + + (constants.length_tran_transform * tran.transforms.length) + + (constants.length_tran_reorder * tran.reorders.length); + + return tran; } diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-uset.ts b/common/web/types/src/kmx/kmx-plus-builder/build-uset.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c73afb796ec9170307b8cfed34e16f200857fd1 --- /dev/null +++ b/common/web/types/src/kmx/kmx-plus-builder/build-uset.ts @@ -0,0 +1,88 @@ +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { KMXPlusData, StrsItem, UsetItem } from "../kmx-plus.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; +import { BUILDER_SECTION, BUILDER_U32CHAR } from "./builder-section.js"; + +/** reference from build_uset_index */ +export type BUILDER_USET_REF = number; + +interface BUILDER_USET_USET { + range: number; + count: number; + pattern: BUILDER_STR_REF; + _pattern: StrsItem; // for sorting +}; + +interface BUILDER_USET_RANGE { + start: BUILDER_U32CHAR; // uchar32 + end: BUILDER_U32CHAR; // uchar32 +} + +export interface BUILDER_USET extends BUILDER_SECTION { + usetCount: number; + rangeCount: number; + usets: BUILDER_USET_USET[]; + ranges: BUILDER_USET_RANGE[]; +}; + +/** +* Builder for the 'uset' section +*/ +export function build_uset(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS ) : BUILDER_USET { + if(!kmxplus?.uset?.usets) { + return null; + } + + // uset items in pattern order + const items = [...kmxplus.uset.usets] + .sort((a,b) => a.compareTo(b)); + + const ranges : BUILDER_USET_RANGE[] = []; + + const usets : BUILDER_USET_USET[] = []; + + items.forEach(set => { + usets.push({ + range: ranges.length, // index + count: set.uset.length, + pattern: build_strs_index(sect_strs, set.str), + _pattern: set.str, + }); + // append the ranges + set.uset.ranges.forEach(([start, end]) => ranges.push({ start, end })); + }); + + const uset: BUILDER_USET = { + ident: constants.hex_section_id(constants.section.uset), + size: constants.length_uset + + (constants.length_uset_uset * usets.length) + + (constants.length_uset_range * ranges.length), + usetCount: usets.length, + rangeCount: ranges.length, + usets, + ranges, + _offset: 0, + }; + + return uset; +} + + +/** + * @returns uset index + */ +export function build_uset_index(sect_uset: BUILDER_USET, value: UsetItem) { + if(!(value instanceof UsetItem)) { + if (value === null) { + throw new Error('unexpected null UsetItem'); + } else { + throw new Error('Expected UsetItem but got '+ value); + } + } + + let result = sect_uset.usets.findIndex(v => v._pattern.value === value.str.value); + if(result < 0) { + throw new Error('unexpectedly missing UsetItem ' + value.uset.toString()); + } + return result; +} diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-vars.ts b/common/web/types/src/kmx/kmx-plus-builder/build-vars.ts new file mode 100644 index 0000000000000000000000000000000000000000..daa0e4a46c23b76eaa332eeea515ea1e25d64553 --- /dev/null +++ b/common/web/types/src/kmx/kmx-plus-builder/build-vars.ts @@ -0,0 +1,64 @@ +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { KMXPlusData } from "../kmx-plus.js"; +import { build_strs_index, BUILDER_STR_REF, BUILDER_STRS } from "./build-strs.js"; +import { BUILDER_SECTION } from "./builder-section.js"; +import { build_list_index, BUILDER_LIST, BUILDER_LIST_REF } from "./build-list.js"; +import { build_elem_index, BUILDER_ELEM, BUILDER_ELEM_REF } from "./build-elem.js"; + + +interface BUILDER_VARS_ITEM { + type: number; + id: BUILDER_STR_REF; // str + value: BUILDER_STR_REF; // str + elem?: BUILDER_ELEM_REF; // elem +}; + +export interface BUILDER_VARS extends BUILDER_SECTION { + markers: BUILDER_LIST_REF; + varCount: number; + varEntries: BUILDER_VARS_ITEM[]; +}; + +/** +* Builder for the 'vars' section +*/ +export function build_vars(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_elem: BUILDER_ELEM, sect_list: BUILDER_LIST) : BUILDER_VARS { + if(!kmxplus.vars) { + return null; + } + + const stringVars = kmxplus.vars.strings.map(v => { + type: constants.vars_entry_type_string, + id: build_strs_index(sect_strs, v.id), + value: build_strs_index(sect_strs, v.value), + }); + const setVars = kmxplus.vars.sets.map(v => { + type: constants.vars_entry_type_set, + id: build_strs_index(sect_strs, v.id), + value: build_strs_index(sect_strs, v.value), + elem: build_elem_index(sect_elem, v.items), + }); + const uniSetVars = kmxplus.vars.unicodeSets.map(v => { + type: constants.vars_entry_type_unicodeSet, + id: build_strs_index(sect_strs, v.id), + value: build_strs_index(sect_strs, v.value), + }); + + const vars: BUILDER_VARS = { + ident: constants.hex_section_id(constants.section.vars), + size: constants.length_vars + + (constants.length_vars_item * kmxplus.vars.totalCount()), + _offset: 0, + markers: build_list_index(sect_list, kmxplus.vars.markers), + varCount: kmxplus.vars.totalCount(), + varEntries: [ + ...stringVars, + ...setVars, + ...uniSetVars, + ], + }; + // sort IDs by binary order + vars.varEntries.sort((a,b) => a.id - b.id); + + return vars; +} diff --git a/common/web/types/src/kmx/kmx-plus-builder/builder-section.ts b/common/web/types/src/kmx/kmx-plus-builder/builder-section.ts index 1a6108af4e63ab09d3a24ba5ad77a95654786641..a6d81fd25e868d632e41f18afc1d32e7761b434e 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/builder-section.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/builder-section.ts @@ -1,5 +1,10 @@ +/** for a 4-byte section identity */ +export type BUILDER_IDENT = number; +/** for a single UTF-32 character (Unicode codepoint) */ +export type BUILDER_U32CHAR = number; + export interface BUILDER_SECTION { - ident: number; + ident: BUILDER_IDENT; size: number; _offset: number; // used only for building the output } diff --git a/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts b/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts index 95a36a60f2f8bcec06a4f59c3ed1af50e5480d0d..3cc58c9fe75abee4680310b3e5c785f85371fccf 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts @@ -11,13 +11,14 @@ import { BUILDER_LIST, build_list } from './build-list.js'; import { BUILDER_LOCA, build_loca } from './build-loca.js'; import { BUILDER_META, build_meta } from './build-meta.js'; import { BUILDER_NAME, build_name } from './build-name.js'; -import { BUILDER_ORDR, build_ordr } from './build-ordr.js'; import { BUILDER_STRS, build_strs } from './build-strs.js'; import { BUILDER_TRAN, build_tran } from './build-tran.js'; +import { BUILDER_USET, build_uset } from './build-uset.js'; +import { BUILDER_VARS, build_vars } from './build-vars.js'; import { BUILDER_VKEY, build_vkey } from './build-vkey.js'; type BUILDER_BKSP = BUILDER_TRAN; -type BUILDER_FINL = BUILDER_TRAN; +// type BUILDER_FINL = BUILDER_TRAN; type SectionBuilders = { // [id in SectionIdent]: BUILDER_SECTION; @@ -25,16 +26,16 @@ type SectionBuilders = { bksp?: BUILDER_BKSP; disp?: BUILDER_DISP; elem?: BUILDER_ELEM; - finl?: BUILDER_FINL; keys?: BUILDER_KEYS; layr?: BUILDER_LAYR; list?: BUILDER_LIST; loca?: BUILDER_LOCA; meta?: BUILDER_META; name?: BUILDER_NAME; - ordr?: BUILDER_ORDR; strs?: BUILDER_STRS; tran?: BUILDER_TRAN; + uset?: BUILDER_USET; + vars?: BUILDER_VARS; vkey?: BUILDER_VKEY; }; @@ -61,17 +62,17 @@ export default class KMXPlusBuilder { this.emitSection(file, this.file.COMP_PLUS_DISP, this.sect.disp); this.emitSection(file, this.file.COMP_PLUS_ELEM, this.sect.elem); this.emitElements(file); - this.emitSection(file, this.file.COMP_PLUS_FINL, this.sect.finl); this.emitSection(file, this.file.COMP_PLUS_KEYS, this.sect.keys); this.emitSection(file, this.file.COMP_PLUS_LAYR, this.sect.layr); this.emitSection(file, this.file.COMP_PLUS_LIST, this.sect.list); this.emitSection(file, this.file.COMP_PLUS_LOCA, this.sect.loca); this.emitSection(file, this.file.COMP_PLUS_META, this.sect.meta); this.emitSection(file, this.file.COMP_PLUS_NAME, this.sect.name); - this.emitSection(file, this.file.COMP_PLUS_ORDR, this.sect.ordr); this.emitSection(file, this.file.COMP_PLUS_STRS, this.sect.strs); this.emitStrings(file); this.emitSection(file, this.file.COMP_PLUS_TRAN, this.sect.tran); + this.emitSection(file, this.file.COMP_PLUS_USET, this.sect.uset); + this.emitSection(file, this.file.COMP_PLUS_VARS, this.sect.vars); this.emitSection(file, this.file.COMP_PLUS_VKEY, this.sect.vkey); return file; @@ -84,21 +85,21 @@ export default class KMXPlusBuilder { // reference them. However, they will be emitted in alpha order. this.sect.strs = build_strs(this.file.kmxplus.strs); this.sect.list = build_list(this.file.kmxplus.list, this.sect.strs); - this.sect.elem = build_elem(this.file.kmxplus.elem, this.sect.strs); + this.sect.uset = build_uset(this.file.kmxplus, this.sect.strs); + this.sect.elem = build_elem(this.file.kmxplus.elem, this.sect.strs, this.sect.uset); const build_bksp = build_tran; - const build_finl = build_tran; this.sect.bksp = build_bksp(this.file.kmxplus.bksp, this.sect.strs, this.sect.elem); this.sect.disp = build_disp(this.file.kmxplus, this.sect.strs); - this.sect.finl = build_finl(this.file.kmxplus.finl, this.sect.strs, this.sect.elem); this.sect.keys = build_keys(this.file.kmxplus, this.sect.strs, this.sect.list); this.sect.layr = build_layr(this.file.kmxplus, this.sect.strs, this.sect.list); this.sect.loca = build_loca(this.file.kmxplus, this.sect.strs); this.sect.meta = build_meta(this.file.kmxplus, this.sect.strs); this.sect.name = build_name(this.file.kmxplus, this.sect.strs); - this.sect.ordr = build_ordr(this.file.kmxplus, this.sect.strs, this.sect.elem); this.sect.tran = build_tran(this.file.kmxplus.tran, this.sect.strs, this.sect.elem); + this.sect.uset = build_uset(this.file.kmxplus, this.sect.strs); + this.sect.vars = build_vars(this.file.kmxplus, this.sect.strs, this.sect.elem, this.sect.list); this.sect.vkey = build_vkey(this.file.kmxplus); // Finalize the sect (index) section @@ -127,16 +128,16 @@ export default class KMXPlusBuilder { offset = this.finalize_sect_item(this.sect.bksp, offset); offset = this.finalize_sect_item(this.sect.disp, offset); offset = this.finalize_sect_item(this.sect.elem, offset); - offset = this.finalize_sect_item(this.sect.finl, offset); offset = this.finalize_sect_item(this.sect.keys, offset); offset = this.finalize_sect_item(this.sect.layr, offset); offset = this.finalize_sect_item(this.sect.list, offset); offset = this.finalize_sect_item(this.sect.loca, offset); offset = this.finalize_sect_item(this.sect.meta, offset); offset = this.finalize_sect_item(this.sect.name, offset); - offset = this.finalize_sect_item(this.sect.ordr, offset); offset = this.finalize_sect_item(this.sect.strs, offset); offset = this.finalize_sect_item(this.sect.tran, offset); + offset = this.finalize_sect_item(this.sect.uset, offset); + offset = this.finalize_sect_item(this.sect.vars, offset); offset = this.finalize_sect_item(this.sect.vkey, offset); this.sect.sect.total = offset; @@ -186,5 +187,4 @@ export default class KMXPlusBuilder { } } } - } diff --git a/common/web/types/src/kmx/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus.ts index d525d896af9070ced4405551345e8d8042e41fb7..795cebc69658d8e21def7f8482192404700631cd 100644 --- a/common/web/types/src/kmx/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus.ts @@ -2,8 +2,11 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import * as r from 'restructure'; import { ElementString } from './element-string.js'; import { ListItem } from './string-list.js'; -import { unescapeString } from '../util/util.js'; +import { isOneChar, toOneChar, unescapeString } from '../util/util.js'; import { KMXFile } from './kmx.js'; +import { UnicodeSetParser, UnicodeSet } from '@keymanapp/common-types'; +import { VariableParser } from '../ldml-keyboard/pattern-parser.js'; +import { MarkerParser } from '../ldml-keyboard/pattern-parser.js'; // Implementation of file structures from /core/src/ldml/C7043_ldml.md // Writer in kmx-builder.ts @@ -12,11 +15,12 @@ import { KMXFile } from './kmx.js'; export class Section { } -export class GlobalSections { - // These sections are used by other sections during compilation - strs: Strs; - elem: Elem; - list: List; +/** + * Sections which are needed as dependencies. + */ +export interface DependencySections extends KMXPlusData { + /** needed for UnicodeSet parsing */ + usetparser?: UnicodeSetParser; } // 'sect' @@ -29,12 +33,16 @@ export class Sect extends Section {}; export class Elem extends Section { strings: ElementString[] = []; - constructor(strs: Strs) { + constructor(sections: DependencySections) { super(); - this.strings.push(new ElementString(strs, '')); // C7043: null element string + this.strings.push(new ElementString(sections, '')); // C7043: null element string } - allocElementString(strs: Strs, source: string, order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString { - let s = new ElementString(strs, source, order, tertiary, tertiary_base, prebase); + /** + * @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem. + * If it is a string, will be interpreted per reorder element ruls. + */ + allocElementString(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString { + let s = new ElementString(sections, source, order, tertiary, tertiary_base, prebase); let result = this.strings.find(item => item.isEqual(s)); if(result === undefined) { result = s; @@ -44,8 +52,6 @@ export class Elem extends Section { } }; -// 'finl' -- see 'tran' - // 'keys' is now `keys2.kmap` // 'loca' @@ -81,17 +87,6 @@ export class Name extends Section { names: StrsItem[] = []; }; -// 'ordr' - -export class OrdrItem { - elements: ElementString; - before: ElementString; -}; - -export class Ordr extends Section { - items: OrdrItem[] = []; -}; - // 'strs' /** @@ -99,13 +94,28 @@ export class Ordr extends Section { * into the string table at finalization. */ export class StrsItem { + /** string value */ readonly value: string; - constructor(value: string) { + /** char value if this is a single-char placeholder item (CharStrsItem) */ + readonly char?: number; + + constructor(value: string, char?: number) { + if (char !== undefined) { + if (!isOneChar(value)) { + throw new Error(`StrsItem: ${value} is not a single char`); + } + if (char !== toOneChar(value)) { + throw new Error(`StrsItem: ${char} is not the right codepoint for ${value}`); + } + } this.value = value; + this.char = char; } + compareTo(o: StrsItem): number { return StrsItem.binaryStringCompare(this.value, o.value); } + static binaryStringCompare(a: string, b: string): number { // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islessthan if(typeof a != 'string' || typeof b != 'string') { @@ -115,6 +125,22 @@ export class StrsItem { if(a > b) return 1; return 0; } + + get isOneChar() { + return this.char !== undefined; + } +}; + +/** + * A StrsItem for a single char. Used as a placeholder and hint to the builder + */ +export class CharStrsItem extends StrsItem { + constructor(value: string) { + if (!isOneChar(value)) { + throw RangeError(`not a 1-char string`); + } + super(value, toOneChar(value)); + } }; export class Strs extends Section { @@ -122,17 +148,19 @@ export class Strs extends Section { /** * Allocate a StrsItem given the string, unescaping if necessary. * @param s escaped string + * @param singleOk if true, allocate a CharStrsItem (not in strs table) if single-char capable. * @returns */ - allocAndUnescapeString(s?: string): StrsItem { - return this.allocString(unescapeString(s)); + allocAndUnescapeString(s?: string, singleOk?: boolean): StrsItem { + return this.allocString(unescapeString(s), singleOk); } /** * Allocate a StrsItem given the string. * @param s string + * @param singleOk if true, allocate a CharStrsItem (not in strs table) if single-char capable. * @returns */ - allocString(s?: string): StrsItem { + allocString(s?: string, singleOk?: boolean): StrsItem { if(s === undefined || s === null) { // undefined or null are always equivalent to empty string, see C7043 s = ''; @@ -142,6 +170,11 @@ export class Strs extends Section { throw new Error('alloc_string: s must be a string, undefined, or null.'); } + // if it's a single char, don't push it into the list + if (singleOk && isOneChar(s)) { + return new CharStrsItem(s); + } + let result = this.strings.find(item => item.value === s); if(result === undefined) { result = new StrsItem(s); @@ -151,46 +184,227 @@ export class Strs extends Section { } }; -// 'tran' +/** + * See LKVariables + */ +export class Vars extends Section { + totalCount() : number { + return this.strings.length + this.sets.length + this.unicodeSets.length; + } + markers: ListItem; + strings: StringVarItem[] = []; // ≠ StrsItem + sets: SetVarItem[] = []; + unicodeSets: UnicodeSetItem[] = []; -export enum TranItemFlags { - none = 0, - error = constants.tran_flags_error, + /** + * + * @returns false if any invalid variables + */ + valid() : boolean { + for (const t of [this.sets, this.strings, this.unicodeSets]) { + for (const i of t) { + if (!i.valid()) { + return false; + } + } + } + return true; + } + + // utilities for making use of Vars in other sections + + substituteSets(str: string, sections: DependencySections): string { + return str.replaceAll(VariableParser.SET_REFERENCE, (_entire : string, id: string) => { + const val = Vars.findVariable(this.sets, id); + if (val === null) { + // Should have been caught during validation. + throw Error(`Internal Error: reference to missing set variable ${id}`); + } + return val.value.value; + }); + } + substituteUnicodeSets(value: string, sections: DependencySections): string { + return value.replaceAll(VariableParser.SET_REFERENCE, (_entire, id) => { + const v = Vars.findVariable(this.unicodeSets, id); + if (v === null) { + // Should have been caught during validation. + throw Error(`Internal Error: reference to missing UnicodeSet variable ${id}`); + } + return v.value.value; // string value + }); + } + substituteStrings(str: string, sections: DependencySections): string { + return str.replaceAll(VariableParser.STRING_REFERENCE, (_entire, id) => { + const val = this.findStringVariableValue(id); + if (val === null) { + // Should have been caught during validation. + throw Error(`Internal Error: reference to missing string variable ${id}`); + } + return val; + }); + } + findStringVariableValue(id: string): string { + return Vars.findVariable(this.strings, id)?.value?.value; // Unwrap: Variable, StrsItem + } + substituteSetRegex(str: string, sections: DependencySections): string { + return str.replaceAll(VariableParser.SET_REFERENCE, (_entire, id) => { + // try as set + const set = Vars.findVariable(this.sets, id); + if (set !== null) { + const { items } = set; + const inner = items.map(i => i.value.value).join('|'); + return `(?:${inner})`; // TODO-LDML: need to escape here + } + + // try as unicodeset + const uset = Vars.findVariable(this.unicodeSets, id); + if (uset !== null) { + const { unicodeSet } = uset; + const inner = unicodeSet.ranges.map(([start, end]) => { + const s = String.fromCodePoint(start); + if (start === end) { + return s; + } else { + const e = String.fromCodePoint(end); + return `${s}-${e}`; + } + }).join(''); + return `[${inner}]`; + } + + // else, missing + throw Error(`Internal Error: reference to missing set variable ${id}`); + }); + } + /** + * Variable locator facility + * @param array + * @param id + * @returns + */ + private static findVariable(array: T[], id: string) : T { + const v : T[] = array.filter(e => e.id.value === id); + if (v.length === 0){ + return null; + } else if (v.length !== 1) { + // Should have been caught during validation + throw Error(`Internal Error: Duplicate variable id ${id} crept into a variable list.`); + } else { + return v[0]; + } + } + substituteMarkerString(s : string) : string { + return MarkerParser.toSentinelString(s, this.markers); + } +}; + +/** + * Common base for variable sections + * See Variable + */ +export class VarsItem extends Section { + id: StrsItem; + value: StrsItem; + + constructor(id: string, value: string, sections: DependencySections) { + super(); + this.id = sections.strs.allocString(id); + this.value = sections.strs.allocAndUnescapeString(value); + } + + valid() : boolean { + return true; + } +}; + +export class UnicodeSetItem extends VarsItem { + constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser) { + super(id, value, sections); + // TODO-LDML: err on max buffer size + const needRanges = sections.usetparser.sizeUnicodeSet(value); + this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges); + + // _unicodeSet may be null, indicating this is invalid. + // A message will have been set in that case. + } + unicodeSet?: UnicodeSet; + valid() : boolean { + return !!this.unicodeSet; + } +}; + +export class SetVarItem extends VarsItem { + constructor(id: string, value: string[], sections: DependencySections) { + super(id, value.join(' '), sections); + this.items = sections.elem.allocElementString(sections, value); + } + items: ElementString; + valid() : boolean { + return !!this.items; + } +}; + +export class StringVarItem extends VarsItem { + constructor(id: string, value: string, sections: DependencySections) { + super(id, value, sections); + } + // no added fields }; -export class TranItem extends Section { - from: ElementString; +// 'tran' + +export class TranTransform { + from: StrsItem; to: StrsItem; + mapFrom: StrsItem; // var name + mapTo: StrsItem; // var name +} + +export class TranGroup { + type: number; // tran_group_type_transform | tran_group_type_reorder + transforms: TranTransform[] = []; + reorders: TranReorder[] = []; +} + +export class TranReorder { + elements: ElementString; before: ElementString; - flags: TranItemFlags; }; export class Tran extends Section { - items: TranItem[] = []; + groups: TranGroup[] = []; get id() { return constants.section.tran; } }; -// alias types for 'bksp', 'finl' +export class UsetItem { + constructor(public uset: UnicodeSet, public str: StrsItem) { + } + compareTo(other: UsetItem) : number { + return this.str.compareTo(other.str); + } +}; -export class Bksp extends Tran { - override get id() { - return constants.section.bksp; +export class Uset extends Section { + usets: UsetItem[] = []; + allocUset(set: UnicodeSet, sections: DependencySections) : UsetItem { + // match the same pattern + let result = this.usets.find(s => set.pattern == s.uset.pattern); + if (result === undefined) { + result = new UsetItem(set, sections.strs.allocString(set.pattern)); + this.usets.push(result); + } + return result; } }; -export class BkspItem extends TranItem {}; -export type BkspItemFlags = TranItemFlags; -export const BkspItemFlags = TranItemFlags; -export class Finl extends Tran { +// alias type for 'bksp' +export class Bksp extends Tran { override get id() { - return constants.section.finl; + return constants.section.bksp; } }; -export class FinlItem extends TranItem {}; -export type FinlItemFlags = TranItemFlags; -export const FinlItemFlags = TranItemFlags; // 'vkey' @@ -346,16 +560,16 @@ export interface KMXPlusData { bksp?: Bksp; disp?: Disp; elem?: Elem; // elem is ignored in-memory - finl?: Finl; keys?: Keys; layr?: Layr; list?: List; // list is ignored in-memory loca?: Loca; meta?: Meta; name?: Name; - ordr?: Ordr; strs?: Strs; // strs is ignored in-memory tran?: Tran; + uset?: Uset; // uset is ignored in-memory + vars?: Vars; vkey?: Vkey; }; @@ -377,10 +591,6 @@ export class KMXPlusFile extends KMXFile { public readonly COMP_PLUS_ELEM_STRING: any; public readonly COMP_PLUS_ELEM: any; - // COMP_PLUS_FINL == COMP_PLUS_TRAN - public readonly COMP_PLUS_FINL_ITEM: any; - public readonly COMP_PLUS_FINL: any; - // COMP_PLUS_KEYS is now COMP_PLUS_KEYS_KMAP public readonly COMP_PLUS_LAYR_ENTRY: any; @@ -407,28 +617,40 @@ export class KMXPlusFile extends KMXFile { public readonly COMP_PLUS_NAME_ITEM: any; public readonly COMP_PLUS_NAME: any; - public readonly COMP_PLUS_ORDR_ITEM: any; - public readonly COMP_PLUS_ORDR: any; - public readonly COMP_PLUS_STRS_ITEM: any; public readonly COMP_PLUS_STRS: any; - public readonly COMP_PLUS_TRAN_ITEM: any; + public readonly COMP_PLUS_TRAN_GROUP: any; + public readonly COMP_PLUS_TRAN_TRANSFORM: any; + public readonly COMP_PLUS_TRAN_REORDER: any; public readonly COMP_PLUS_TRAN: any; + public readonly COMP_PLUS_USET_USET: any; + public readonly COMP_PLUS_USET_RANGE: any; + public readonly COMP_PLUS_USET: any; + public readonly COMP_PLUS_VKEY_ITEM: any; public readonly COMP_PLUS_VKEY: any; + public readonly COMP_PLUS_VARS: any; + public readonly COMP_PLUS_VARS_ITEM: any; + /* File in-memory data */ public kmxplus: KMXPlusData = { }; constructor() { super(); - - // Binary-correct structures matching kmx_plus.h + // helpers + const STR_REF = r.uint32le; + const ELEM_REF = r.uint32le; + const LIST_REF = r.uint32le; + const STR_OR_CHAR32 = r.uint32le; + const CHAR32 = r.uint32le; + const STR_OR_CHAR32_OR_USET = r.uint32le; + const IDENT = r.uint32le; // 'sect' this.COMP_PLUS_SECT_ITEM = new r.Struct({ @@ -437,7 +659,7 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_SECT = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, total: r.uint32le, count: r.uint32le, @@ -448,22 +670,22 @@ export class KMXPlusFile extends KMXFile { // 'disp' this.COMP_PLUS_DISP_ITEM = new r.Struct({ - to: r.uint32le, - display: r.uint32le, + to: STR_REF, + display: STR_REF, }); this.COMP_PLUS_DISP = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, count: r.uint32le, - baseCharacter: r.uint32le, + baseCharacter: CHAR32, items: new r.Array(this.COMP_PLUS_DISP_ITEM, 'count'), }); // 'elem' this.COMP_PLUS_ELEM_ELEMENT = new r.Struct({ - element: r.uint32le, + element: STR_OR_CHAR32_OR_USET, flags: r.uint32le }); @@ -473,7 +695,7 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_ELEM = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, count: r.uint32le, strings: new r.Array(this.COMP_PLUS_ELEM_STRING, 'count') @@ -510,7 +732,7 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_LAYR = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, listCount: r.uint32le, layerCount: r.uint32le, @@ -523,26 +745,26 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_KEYS_FLICK = new r.Struct({ - directions: r.uint32le, // list + directions: LIST_REF, // list flags: r.uint32le, - to: r.uint32le, // str | codepoint + to: STR_OR_CHAR32, // str | codepoint }); this.COMP_PLUS_KEYS_FLICKS = new r.Struct({ count: r.uint32le, flick: r.uint32le, - id: r.uint32le, // str + id: STR_REF, // str }); this.COMP_PLUS_KEYS_KEY = new r.Struct({ - to: r.uint32le, // str | codepoint + to: STR_OR_CHAR32, // str | codepoint flags: r.uint32le, - id: r.uint32le, // str - switch: r.uint32le, // str + id: STR_REF, // str + switch: STR_REF, // str width: r.uint32le, // width*10 ( 1 = 0.1 keys) - longPress: r.uint32le, // list index - longPressDefault: r.uint32le, // str - multiTap: r.uint32le, // list index + longPress: LIST_REF, // list index + longPressDefault: STR_REF, // str + multiTap: LIST_REF, // list index flicks: r.uint32le, // index into flicks table }); @@ -553,7 +775,7 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_KEYS = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, keyCount: r.uint32le, flicksCount: r.uint32le, @@ -573,11 +795,11 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_LIST_INDEX = new r.Struct({ - str: r.uint32le, // str + str: STR_REF, // str }); this.COMP_PLUS_LIST = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, listCount: r.uint32le, indexCount: r.uint32le, @@ -590,7 +812,7 @@ export class KMXPlusFile extends KMXFile { this.COMP_PLUS_LOCA_ITEM = r.uint32le; //str this.COMP_PLUS_LOCA = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, count: r.uint32le, items: new r.Array(this.COMP_PLUS_LOCA_ITEM, 'count') @@ -599,14 +821,14 @@ export class KMXPlusFile extends KMXFile { // 'meta' this.COMP_PLUS_META = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, - author: r.uint32le, //str - conform: r.uint32le, //str - layout: r.uint32le, //str - normalization: r.uint32le, //str - indicator: r.uint32le, //str - version: r.uint32le, //str + author: STR_REF, //str + conform: STR_REF, //str + layout: STR_REF, //str + normalization: STR_REF, //str + indicator: STR_REF, //str + version: STR_REF, //str settings: r.uint32le, //new r.Bitfield(r.uint32le, ['fallback', 'transformFailure', 'transformPartial']) }); @@ -615,25 +837,13 @@ export class KMXPlusFile extends KMXFile { this.COMP_PLUS_NAME_ITEM = r.uint32le; //str this.COMP_PLUS_NAME = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, count: r.uint32le, items: new r.Array(this.COMP_PLUS_NAME_ITEM, 'count') }); - // 'ordr' - - this.COMP_PLUS_ORDR_ITEM = new r.Struct({ - elements: r.uint32le, //elem - before: r.uint32le //elem - }); - - this.COMP_PLUS_ORDR = new r.Struct({ - ident: r.uint32le, - size: r.uint32le, - count: r.uint32le, - items: new r.Array(this.COMP_PLUS_ORDR_ITEM, 'count') - }); + // 'ordr' now part of 'tran' // 'strs' @@ -645,7 +855,7 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_STRS = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, count: r.uint32le, items: new r.Array(this.COMP_PLUS_STRS_ITEM, 'count') @@ -654,18 +864,71 @@ export class KMXPlusFile extends KMXFile { // 'tran' - this.COMP_PLUS_TRAN_ITEM = new r.Struct({ - from: r.uint32le, //elem - to: r.uint32le, //str - before: r.uint32le, //elem - flags: r.uint32le //bitfield + this.COMP_PLUS_TRAN_GROUP = new r.Struct({ + type: r.uint32le, //type of group + count: r.uint32le, //number of items + index: r.uint32le, //index into subtable + }); + + this.COMP_PLUS_TRAN_TRANSFORM = new r.Struct({ + from: STR_REF, //str + to: STR_REF, //str + mapFrom: ELEM_REF, //elem + mapTo: ELEM_REF //elem + }); + + this.COMP_PLUS_TRAN_REORDER = new r.Struct({ + elements: ELEM_REF, //elem + before: ELEM_REF, //elem }); this.COMP_PLUS_TRAN = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, + groupCount: r.uint32le, + transformCount: r.uint32le, + reorderCount: r.uint32le, + groups: new r.Array(this.COMP_PLUS_TRAN_GROUP, 'groupCount'), + transforms: new r.Array(this.COMP_PLUS_TRAN_TRANSFORM, 'transformCount'), + reorders: new r.Array(this.COMP_PLUS_TRAN_REORDER, 'reorderCount'), + }); + + // 'uset' + this.COMP_PLUS_USET_USET = new r.Struct({ + range: r.uint32le, count: r.uint32le, - items: new r.Array(this.COMP_PLUS_TRAN_ITEM, 'count') + pattern: STR_REF, // str + }); + + this.COMP_PLUS_USET_RANGE = new r.Struct({ + start: CHAR32, + end: CHAR32, + }); + + this.COMP_PLUS_USET = new r.Struct({ + ident: IDENT, + size: r.uint32le, + usetCount: r.uint32le, + rangeCount: r.uint32le, + usets: new r.Array(this.COMP_PLUS_USET_USET, 'usetCount'), + ranges: new r.Array(this.COMP_PLUS_USET_RANGE, 'rangeCount'), + }); + + // 'vars' + + this.COMP_PLUS_VARS_ITEM = new r.Struct({ + type: r.uint32le, + id: STR_REF, // str + value: STR_REF, // str + elem: ELEM_REF, + }); + + this.COMP_PLUS_VARS = new r.Struct({ + ident: IDENT, + size: r.uint32le, + markers: LIST_REF, + varCount: r.uint32le, + varEntries: new r.Array(this.COMP_PLUS_VARS_ITEM, 'varCount'), }); // 'vkey' @@ -676,7 +939,7 @@ export class KMXPlusFile extends KMXFile { }); this.COMP_PLUS_VKEY = new r.Struct({ - ident: r.uint32le, + ident: IDENT, size: r.uint32le, count: r.uint32le, items: new r.Array(this.COMP_PLUS_VKEY_ITEM, 'count') @@ -684,9 +947,6 @@ export class KMXPlusFile extends KMXFile { // Aliases - this.COMP_PLUS_BKSP_ITEM = this.COMP_PLUS_TRAN_ITEM; - this.COMP_PLUS_FINL_ITEM = this.COMP_PLUS_TRAN_ITEM; this.COMP_PLUS_BKSP = this.COMP_PLUS_TRAN; - this.COMP_PLUS_FINL = this.COMP_PLUS_TRAN; } } diff --git a/common/web/types/src/kmx/string-list.ts b/common/web/types/src/kmx/string-list.ts index 4b5716b85d7801e33287df7ce9d914be2745fd64..f9f4b46e912feb9bade78472ea4db675d65beadc 100644 --- a/common/web/types/src/kmx/string-list.ts +++ b/common/web/types/src/kmx/string-list.ts @@ -1,3 +1,4 @@ +import { OrderedStringList } from 'src/ldml-keyboard/pattern-parser.js'; import { Strs, StrsItem } from './kmx-plus.js'; /** @@ -22,7 +23,7 @@ export class ListIndex { * A string list in memory. This will be replaced with an index * into the string table at finalization. */ -export class ListItem extends Array { +export class ListItem extends Array implements OrderedStringList { /** * Construct a new list from an array of strings. * Use List. This is meant to be called by the List.allocString*() functions. @@ -41,6 +42,9 @@ export class ListItem extends Array { this.push(index); } } + getItemOrder(item: string): number { + return this.findIndex(({value}) => value.value === item); + } isEqual(a: ListItem | string[]): boolean { if (a.length != this.length) { return false; @@ -68,7 +72,12 @@ export class ListItem extends Array { return 0; } } + /** for debugging, print as single string */ toString(): string { - return this.map(v => v.value.value).join(' '); + return this.toStringArray().join(' '); + } + /** for debugging, map to string array */ + toStringArray(): string[] { + return this.map(v => v.value.value); } }; diff --git a/common/web/types/src/kpj/kpj-file-reader.ts b/common/web/types/src/kpj/kpj-file-reader.ts index 5f768c54f1ea159dccd318e1eb997be1d1d5e5c6..21e92c975b2313a3dd0f1508cbd663ef20654188 100644 --- a/common/web/types/src/kpj/kpj-file-reader.ts +++ b/common/web/types/src/kpj/kpj-file-reader.ts @@ -27,8 +27,8 @@ export class KPJFileReader { return data as KPJFile; } - public validate(source: KPJFile, schemaBuffer: Buffer): void { - const schema = JSON.parse(schemaBuffer.toString('utf8')); + public validate(source: KPJFile, schemaBuffer: Uint8Array): void { + const schema = JSON.parse(new TextDecoder().decode(schemaBuffer)); const ajv = new Ajv(); if(!ajv.validate(schema, source)) { throw new Error(ajv.errorsText()); diff --git a/common/web/types/src/kvk/kvks-file-reader.ts b/common/web/types/src/kvk/kvks-file-reader.ts index 1a4c5a8b056646a608b1868125d6d0e4a1d76663..13c6c8716c11c1b6a02e98f8b63fc18ed12cb6d0 100644 --- a/common/web/types/src/kvk/kvks-file-reader.ts +++ b/common/web/types/src/kvk/kvks-file-reader.ts @@ -7,12 +7,6 @@ import { VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKey, VisualKey import { USVirtualKeyCodes } from '../consts/virtual-key-constants.js'; import { BUILDER_KVK_HEADER_VERSION } from './kvk-file.js'; -export enum KVKSParseErrorType { invalidVkey }; -export class KVKSParseError extends Error { - public type: KVKSParseErrorType; - public vkey: string; -}; - export default class KVKSFileReader { public read(file: Uint8Array): KVKSourceFile { let source: KVKSourceFile; @@ -33,12 +27,21 @@ export default class KVKSFileReader { // rather than using the version tagged on npmjs.com. }); - parser.parseString(file, (e: unknown, r: unknown) => { source = r as KVKSourceFile }); - source = this.boxArrays(source); - this.cleanupUnderscore('visualkeyboard', source.visualkeyboard); + parser.parseString(file, (e: unknown, r: unknown) => { if(e) { throw e }; source = r as KVKSourceFile }); + if(source) { + source = this.boxArrays(source); + this.cleanupFlags(source); + this.cleanupUnderscore('visualkeyboard', source.visualkeyboard); + } return source; } + private cleanupFlags(source: any) { + if(source.visualkeyboard?.header?.flags === '') { + source.visualkeyboard.header.flags = {}; + } + } + /** * The only element that allows spaces is . Remove * all other empty whitespace-only values. @@ -65,15 +68,15 @@ export default class KVKSFileReader { } } - public validate(source: KVKSourceFile, schemaBuffer: Buffer): void { - const schema = JSON.parse(schemaBuffer.toString('utf8')); + public validate(source: KVKSourceFile, schemaBuffer: Uint8Array): void { + const schema = JSON.parse(new TextDecoder().decode(schemaBuffer)); const ajv = new Ajv(); if(!ajv.validate(schema, source)) { throw new Error(ajv.errorsText()); } } - public transform(source: KVKSourceFile, errors?: KVKSParseError[]): VisualKeyboard { + public transform(source: KVKSourceFile, invalidVkeys?: string[]): VisualKeyboard { // NOTE: at this point, the xml should have been validated // and matched the schema result so we can assume properties exist let result: VisualKeyboard = { @@ -111,11 +114,8 @@ export default class KVKSFileReader { for(let sourceKey of layer.key) { let vkey = (USVirtualKeyCodes as any)[sourceKey.$?.vkey]; if(!vkey) { - if(errors) { - let e = new KVKSParseError(); - e.type = KVKSParseErrorType.invalidVkey; - e.vkey = sourceKey.$?.vkey; - errors.push(e); + if(typeof invalidVkeys !== 'undefined') { + invalidVkeys.push(sourceKey.$?.vkey); } continue; } diff --git a/common/web/types/src/kvk/kvks-file-writer.ts b/common/web/types/src/kvk/kvks-file-writer.ts index 613c6caa5108289ac9c99901ddc00a68383313dd..1b3a819f580f2d2ef23f1582aade95c98a6b97a7 100644 --- a/common/web/types/src/kvk/kvks-file-writer.ts +++ b/common/web/types/src/kvk/kvks-file-writer.ts @@ -96,7 +96,6 @@ export default class KVKSFileWriter { l.key.push(k); } - // console.dir(kvks, {depth:8}); let result = builder.buildObject(kvks); return result; //Uint8Array.from(result); } diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts index 35d7f627956417b508e9b358473ce019147afc80..fa74126462451b8a0dd4c70a30532f7f82cc6c53 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -14,16 +14,20 @@ interface NameAndProps { '$$'?: any; // children }; -export default class LDMLKeyboardXMLSourceFileReader { - callbacks: CompilerCallbacks; +export class LDMLKeyboardXMLSourceFileReaderOptions { + importsPath: string; +}; + +export class LDMLKeyboardXMLSourceFileReader { + constructor(private options: LDMLKeyboardXMLSourceFileReaderOptions, private callbacks : CompilerCallbacks) { + } - constructor(callbacks : CompilerCallbacks) { - this.callbacks = callbacks; + static get defaultImportsURL() { + return new URL(`../import/`, import.meta.url); } - readImportFile(version: string, subpath: string): Buffer { - // TODO-LDML: sanitize input string - let importPath = new URL(`../import/${version}/${subpath}`, import.meta.url); + readImportFile(version: string, subpath: string): Uint8Array { + const importPath = this.callbacks.resolveFilename(this.options.importsPath, `${version}/${subpath}`); return this.callbacks.loadFile(importPath); } @@ -201,8 +205,8 @@ export default class LDMLKeyboardXMLSourceFileReader { /** * @returns true if valid, false if invalid */ - public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile, schemaSource: Buffer): boolean { - const schema = JSON.parse(schemaSource.toString('utf8')); + public validate(source: LDMLKeyboardXMLSourceFile | LDMLKeyboardTestDataXMLSourceFile, schemaSource: Uint8Array): boolean { + const schema = JSON.parse(new TextDecoder().decode(schemaSource)); const ajv = new Ajv(); if(!ajv.validate(schema, source)) { for (let err of ajv.errors) { diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts index 80178c10a8be2c7925ad50f0f373c4e9e55aa912..6216f7f5b4ab8b5c862257dff61b45d7eb6ee695 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts @@ -27,8 +27,8 @@ export interface LKKeyboard { displays?: LKDisplays; layers?: LKLayers[]; vkeys?: LKVkeys; + variables?: LKVariables; transforms?: LKTransforms[]; - reorders?: LKReorders; }; /** @@ -137,29 +137,46 @@ export interface LKVkey { to?: string; }; +export interface LKVariables { + string?: LKString[]; + set?: LKSet[]; + unicodeSet?: LKUnicodeSet[]; +}; + +/** + * Shared interface for all three variable types + */ +export interface Variable { + id?: string; + value?: string; +}; + +export interface LKString extends Variable {}; +export interface LKSet extends Variable {}; +export interface LKUnicodeSet extends Variable {}; + export interface LKTransforms { - type?: "simple" | "final"; - transform: LKTransform[]; + type?: "simple" | "backspace"; + transformGroup?: LKTransformGroup[]; }; export interface LKTransform { from?: string; to?: string; - before?: string; - error?: "fail"; }; -export interface LKReorders { - reorder: LKReorder[]; +export interface LKTransformGroup { + // one or the other, not both + transform?: LKTransform[]; + reorder?: LKReorder[]; }; - export interface LKReorder { from?: string; before?: string; order?: string; tertiary?: string; - tertiary_base?: string; - prebase?: string; + tertiaryBase?: string; + preBase?: string; }; export interface LKDisplayOptions { @@ -169,6 +186,7 @@ export interface LKDisplayOptions { export interface LKDisplay { to?: string; display?: string; + id?: string; // TODO-LDML: This comes in a near-future CLDR update }; export interface LKDisplays { diff --git a/common/web/types/src/ldml-keyboard/pattern-parser.ts b/common/web/types/src/ldml-keyboard/pattern-parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..faee3b15946a590f369237edbee48c0a150827df --- /dev/null +++ b/common/web/types/src/ldml-keyboard/pattern-parser.ts @@ -0,0 +1,262 @@ +/** + * Utilities for transform and marker processing + */ + +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { MATCH_QUAD_ESCAPE, isOneChar, unescapeOneQuadString, unescapeString } from "../util/util.js"; + + +/** + * Helper function for extracting matched items + * @param str input string + * @param match global RegEx to use + * @returns array of matched values + */ +function matchArray(str: string, match: RegExp) : string[] { + const refs = (str || '').matchAll(match); + return Array.from(refs).map(r => r[1]); +} + +/** + * Common regex for an ID + */ +const COMMON_ID = /^[0-9A-Za-z_]{1,32}$/; + +/** for use with markers, means an ordering can be determined */ +export interface OrderedStringList { + /** @returns the ordering of an item (0..), or -1 if not found */ + getItemOrder(item : string) : number; +} + +/** + * Class for helping with markers + */ +export class MarkerParser { + /** + * A marker id has the same constraint as a key id. TODO-LDML: Needs to be reflected in the spec + */ + public static readonly ID = COMMON_ID; + + /** + * Special marker reference referring to any marker + */ + public static readonly ANY_MARKER = '\\m{.}'; + + /** + * id of the 'any' marker + */ + public static readonly ANY_MARKER_ID = '.'; + + /** + * Marker sentinel as a string - U+FFFF + */ + public static readonly SENTINEL = String.fromCodePoint(constants.marker_sentinel); + + /** + * Matches all markers. + */ + public static readonly SENTINEL_ALL_MARKERS = this.SENTINEL + this.SENTINEL; + + /** Minimum ID (trailing code unit) */ + public static readonly MIN_MARKER_INDEX = constants.marker_min_index; + /** Index meaning 'any marker' == `\m{.}` */ + public static readonly ANY_MARKER_INDEX = constants.marker_any_index; + /** Maximum usable marker index */ + public static readonly MAX_MARKER_INDEX = constants.marker_max_index; + /** Max count of markers */ + public static readonly MAX_MARKER_COUNT = constants.marker_max_count; + + /** + * Pattern for matching a marker reference, OR the special marker \m{.} + */ + public static readonly REFERENCE = /\\m{([0-9A-Za-z_]{1,32}|\.)}/g; + + /** + * parse a string into marker references + * @param str input string such as "\m{a} … \m{.}" + * @returns `[]` or an array of all markers referenced + */ + public static allReferences(str: string): string[] { + if (!str) { + return []; + } + return matchArray(str, this.REFERENCE); + } + + /** @returns string for marker #n */ + public static markerOutput(n: number): string { + if (n < MarkerParser.MIN_MARKER_INDEX || n > MarkerParser.ANY_MARKER_INDEX) { + throw RangeError(`Internal Error: marker index out of range ${n}`); + } + return this.SENTINEL + String.fromCharCode(n); + } + + /** @returns all marker strings as sentinel values */ + public static toSentinelString(s: string, markers?: OrderedStringList) : string { + if (!s) return s; + return s.replaceAll(this.REFERENCE, (sub, arg) => { + if (arg === MarkerParser.ANY_MARKER_ID) { + return MarkerParser.SENTINEL_ALL_MARKERS; + } + if (!markers) { + throw RangeError(`Internal Error: Could not find marker \\m{${arg}} (no markers defined)`); + } + const order = markers.getItemOrder(arg); + if (order === -1) { + throw RangeError(`Internal Error: Could not find marker \\m{${arg}}`); + } else if(order >= MarkerParser.MAX_MARKER_INDEX) { + throw RangeError(`Internal Error: marker \\m{${arg}} has out of range index ${order}`); + } else { + return MarkerParser.markerOutput(order+1); + } + }); + } +} + +/** + * Class for helping with markers + */ +export class VariableParser { + /** + * A marker id has the same constraint as a key id. TODO-LDML: Needs to be reflected in the spec + */ + public static readonly ID = COMMON_ID; + + /** + * Pattern for matching a string reference `$(str)` + */ + public static readonly STRING_REFERENCE = /\${([0-9A-Za-z_]{1,32})}/g; + + /** + * Pattern for matching a set reference `$[set]` + */ + public static readonly SET_REFERENCE = /\$\[([0-9A-Za-z_]{1,32})\]/g; + + /** + * Pattern for matching a capture set reference `($[set])` + */ + public static readonly CAPTURE_SET_REFERENCE = /\(\$\[([0-9A-Za-z_]{1,32})\]\)/g; + + /** + * `$[1:variable]` + * This regex matches the whole string. + */ + public static readonly MAPPED_SET_REFERENCE = /^\$\[1:([0-9A-Za-z_]{1,32})\]$/; + + /** + * parse a string into references + * @param str input string + * @returns `[]` or an array of all string references referenced + */ + public static allStringReferences(str: string): string[] { + return matchArray(str, this.STRING_REFERENCE); + } + + /** + * parse a string into references + * @param str input string + * @returns `[]` or an array of all string references referenced + */ + public static allSetReferences(str: string): string[] { + return matchArray(str, this.SET_REFERENCE); + } + + /** + * Split an input string into a proper set + * @param str input string + * @returns + */ + public static setSplitter(str: string): string[] { + const s = str?.trim(); + if (!s) return []; + return s.split(/\s+/); + } +} + +/** for ElementParser.segment() */ +export enum ElementType { + codepoint = '.', + escaped ='\\', + uset = '[', + string = '*', +}; + +/** one portion of a segmented element string */ +export class ElementSegment { + public readonly type: ElementType; + /** + * @param segment the string in the segment + * @param type type of segment. Will be calculated if not provided. + */ + constructor(public segment: string, type?: ElementType) { + if (type) { + this.type = type; + } else if (ElementParser.MATCH_USET.test(segment)) { + this.type = ElementType.uset; + } else if(ElementParser.MATCH_ESCAPED.test(segment)) { + this.type = ElementType.escaped; + } else { + this.type = ElementType.codepoint; + } + } + + /** unescaped format */ + get unescaped() : string { + if (this.type !== ElementType.escaped) { + return this.segment; + } else { + if (MATCH_QUAD_ESCAPE.test(this.segment)) { + return unescapeOneQuadString(this.segment); + } else { + return unescapeString(this.segment); + } + } + } +}; + +/** Class for helping with Element strings (i.e. reorder) */ +export class ElementParser { + /** + * Matches any complex UnicodeSet that would otherwise be misinterpreted + * by `MATCH_ELEMENT_SEGMENTS` due to nested `[]`'s. + * For example, `[[a-z]-[aeiou]]` could be + * mis-segmented into `[[a-z]`, `-`, `[aeiou]`, `]` */ + public static readonly MATCH_NESTED_SQUARE_BRACKETS = /\[[^\]]*\[/; + + /** Match (segment) UnicodeSets OR hex escapes OR single Unicode codepoints */ + public static readonly MATCH_ELEMENT_SEGMENTS = + /(?:\[[^\]]*\]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]{1,6}\}|\\u\{(?:[0-9a-fA-F]{1,6})(?: [0-9a-fA-F]{1,6}){1,}\}|.)/gu; + + /** Does it start with a UnicodeSet? Used to test the segments. */ + public static readonly MATCH_USET = /^\[/; + + /** Does it start with an escaped char? Used to test the segments. */ + public static readonly MATCH_ESCAPED = /^\\u/; + + /** Split a string into ElementSegments */ + public static segment(str: string): ElementSegment[] { + if (this.MATCH_NESTED_SQUARE_BRACKETS.test(str)) { + throw Error(`Unsupported: nested square brackets in element segment: ${str}`); + } + const list: ElementSegment[] = []; + for(let m of str.match(ElementParser.MATCH_ELEMENT_SEGMENTS)) { + const e = new ElementSegment(m); + if (e.type === ElementType.escaped) { + // unescape + const { unescaped } = e; + if (isOneChar(unescaped)) { + list.push(e); + } else { + // need to split the escaped segment, \u{41 42} -> \u{41}, \u{42} + for (let s of unescaped) { + list.push(new ElementSegment(`\\u{${s.codePointAt(0).toString(16)}}`)); + } + } + } else { + // all others + list.push(e); + } + } + return list; + } +}; diff --git a/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts b/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts new file mode 100644 index 0000000000000000000000000000000000000000..0c890aa95a96be199672de624c926c3c37d4624e --- /dev/null +++ b/common/web/types/src/ldml-keyboard/unicodeset-parser-api.ts @@ -0,0 +1,43 @@ + +/** + * Abstraction for an interface capable of parsing a UnicodeSet + */ +export interface UnicodeSetParser { + /** + * Parse a UnicodeSet into ranges + * @param pattern string to parse such as `[a-z]` + * @param rangeCount number of ranges to allow for + */ + parseUnicodeSet(pattern: string, rangeCount: number) : UnicodeSet | null; + /** + * Calculate the number of ranges in a UnicodeSet + * @param pattern string to parse such as `[a-z]` + * @returns number of ranges, or -1 (with callback-reported err) on err + */ + sizeUnicodeSet(pattern: string) : number; +} + +/** + * Parsed UnicodeSet, return value of of parseUnicodeSet() + */ +export class UnicodeSet { + /** + * A UnicodeSet in range form. + * For example, `[ħa-z]` will parse to + * ranges = `[[0x41, 0x7A], [0x0127, 0x0127]]` meaning `[a,z], [ħ,ħ]` + * @param pattern the pattern per UnicodeSet syntax such as `[a-z]` + * @param ranges array of 2-element arrays, 'start' and 'end'. + */ + constructor(public pattern: string, public ranges: number[][]) { + } + /** + * Number of ranges + */ + get length() : number { + return this.ranges.length; + } + + toString() : string { + return this.pattern; + } +} diff --git a/common/web/types/src/main.ts b/common/web/types/src/main.ts index 4e7755038403dc3b57ceecc96d2dbd9470a19aa9..b2c3246ffe2d06c8e88f42f314e4f5e3b50c7856 100644 --- a/common/web/types/src/main.ts +++ b/common/web/types/src/main.ts @@ -1,4 +1,3 @@ - export * as KMX from './kmx/kmx.js'; export * as KMXPlus from './kmx/kmx-plus.js'; export { default as KMXBuilder } from './kmx/kmx-builder.js'; @@ -14,11 +13,13 @@ export * as KvksFile from './kvk/kvk-file.js'; export * as LDMLKeyboard from './ldml-keyboard/ldml-keyboard-xml.js'; export { LDMLKeyboardTestDataXMLSourceFile } from './ldml-keyboard/ldml-keyboard-testdata-xml.js'; -export { default as LDMLKeyboardXMLSourceFileReader } from './ldml-keyboard/ldml-keyboard-xml-reader.js'; +export { UnicodeSetParser, UnicodeSet } from './ldml-keyboard/unicodeset-parser-api.js'; +export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js'; +export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './ldml-keyboard/ldml-keyboard-xml-reader.js'; export * as Constants from './consts/virtual-key-constants.js'; -export { CompilerCallbacks, CompilerSchema, CompilerEvent, CompilerErrorNamespace, CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerMessageSpec, compilerErrorSeverityName, compilerExceptionToString, compilerErrorFormatCode } from './util/compiler-interfaces.js'; +export { CompilerCallbacks, CompilerSchema, CompilerEvent, CompilerErrorNamespace, CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerMessageSpec, compilerErrorSeverityName, compilerExceptionToString, compilerErrorFormatCode, compilerEventFormat } from './util/compiler-interfaces.js'; export { CommonTypesMessages } from './util/common-events.js'; export * as TouchLayout from './keyman-touch-layout/keyman-touch-layout-file.js'; diff --git a/common/web/types/src/util/compiler-interfaces.ts b/common/web/types/src/util/compiler-interfaces.ts index c7058b157854c100efeae32bea4d8503f16fb68d..a52c44fd8ee0d5fad349b896a2f5dd2e44291173 100644 --- a/common/web/types/src/util/compiler-interfaces.ts +++ b/common/web/types/src/util/compiler-interfaces.ts @@ -15,8 +15,13 @@ export enum CompilerErrorSeverity { Error = 0x300000, // Severe error where we can't continue Fatal = 0x400000, // OOM or should-not-happen internal problem - Severity_Mask = 0xF00000, // includes reserved bits - Error_Mask = 0x0FFFFF, + // Mask values for mapping compiler errors + // TODO: make this a separate enum? + Severity_Mask = 0x00F00000, // includes reserved bits, 16 possible severity levels + Error_Mask = 0x000FFFFF, // error | namespace + Namespace_Mask = 0x000FF000, // 256 possible namespaces + BaseError_Mask = 0x00000FFF, // error code, 2,048 possible error codes per namespace + Reserved_Mask = 0xFF000000, // do not use these error values at this time }; export function compilerErrorSeverityName(code: number): string { @@ -44,6 +49,21 @@ export function compilerErrorFormatCode(code: number): string { return `${severityName}:0x${errorCodeString}`; } +/** + * @param e event or array of events + * @returns string + */ +export function compilerEventFormat(e : CompilerEvent | CompilerEvent[]) : string { + if (!e) { + return ""; + } + if (Array.isArray(e)) { + return e.map(item => compilerEventFormat(item)).join('\n'); + } + const {code, message} = e; + return `${compilerErrorFormatCode(code)}: “${message}”`; +} + /** * Defines the error code ranges for various compilers. Once defined, these * ranges must not be changed as external modules may depend on specific error @@ -109,6 +129,7 @@ export interface CompilerFileSystemCallbacks { readFileSync(path: string, options?: { encoding?: null; flag?: string; } | null): Uint8Array; readFileSync(path: string, options: { encoding: string; flag?: string; } | string): string; readFileSync(path: string, options?: { encoding?: string | null; flag?: string; } | string | null): string | Uint8Array; + writeFileSync(path: string, data: Uint8Array): void; existsSync(name: string): boolean; } @@ -119,13 +140,11 @@ export interface CompilerFileSystemCallbacks { export interface CompilerCallbacks { /** * Attempt to load a file. Return falsy if not found. - * TODO: accept only string * TODO: never return falsy, just throw if not found? - * TODO: Buffer is Node-only. * @param baseFilename * @param filename */ - loadFile(filename: string | URL): Buffer; + loadFile(filename: string): Uint8Array; get path(): CompilerPathCallbacks; get fs(): CompilerFileSystemCallbacks; @@ -137,7 +156,7 @@ export interface CompilerCallbacks { */ resolveFilename(baseFilename: string, filename: string): string; - loadSchema(schema: CompilerSchema): Buffer; + loadSchema(schema: CompilerSchema): Uint8Array; reportMessage(event: CompilerEvent): void; debug(msg: string): void; }; diff --git a/common/web/types/src/util/util.ts b/common/web/types/src/util/util.ts index 7d70d001d4ca243ff210eeed28be6a661a84e527..ce5ce9ce469bb80bd0c293640257dde742a26e70 100644 --- a/common/web/types/src/util/util.ts +++ b/common/web/types/src/util/util.ts @@ -19,12 +19,42 @@ export function boxXmlArray(o: any, x: string): void { // TODO-LDML: #7569 the below regex works, but captures more than it should // (it would include \u{fffffffffffffffff } which // is overlong and has a space at the end.) The second regex does not work yet. -const MATCH_HEX_ESCAPE = /\\u{([0-9a-fA-F ]{1,})}/g; +export const MATCH_HEX_ESCAPE = /\\u{([0-9a-fA-F ]{1,})}/g; // const MATCH_HEX_ESCAPE = /\\u{((?:(?:[0-9a-fA-F]{1,5})|(?:10[0-9a-fA-F]{4})(?: (?!}))?)+)}/g; +/** regex for single quad escape such as \u0127 */ +export const MATCH_QUAD_ESCAPE = /\\u([0-9a-fA-F]{4})/g; + export class UnescapeError extends Error { } +/** + * Unescape one codepoint + * @param hex one codepoint in hex, such as '0127' + * @returns the unescaped codepoint + */ +function unescapeOne(hex: string): string { + const codepoint = Number.parseInt(hex, 16); + return String.fromCodePoint(codepoint); +} + +/** + * Unescape one single quad string such as \u0127. + * Throws exception if the string doesn't match MATCH_QUAD_ESCAPE + * @param s input string + * @returns output + */ +export function unescapeOneQuadString(s: string): string { + if (!s || !s.match(MATCH_QUAD_ESCAPE)) { + throw new UnescapeError(`Not a quad escape: ${s}`); + } + function processMatch(str: string, matched: string): string { + return unescapeOne(matched); + } + s = s.replace(MATCH_QUAD_ESCAPE, processMatch); + return s; +} + /** * Unescapes a string according to UTS#18§1.1, see * @param s escaped string @@ -35,15 +65,6 @@ export function unescapeString(s: string): string { return s; } try { - /** - * Unescape one codepoint - * @param hex one codepoint in hex, such as '0127' - * @returns the unescaped codepoint - */ - function unescapeOne(hex: string) : string { - const codepoint = Number.parseInt(hex, 16); - return String.fromCodePoint(codepoint); - } /** * process one regex match * @param str ignored @@ -59,10 +80,24 @@ export function unescapeString(s: string): string { } catch(e) { if (e instanceof RangeError) { throw new UnescapeError(`Out of range while unescaping '${s}': ${e.message}`, { cause: e }); + /* c8 ignore next 3 */ } else { - throw e; + throw e; // pass through some other error } } - return s; } + +/** True if this string *could* be a UTF-32 single char */ +export function +isOneChar(value: string) : boolean { + return [...value].length === 1; +} + +export function +toOneChar(value: string) : number { + if (!isOneChar(value)) { + throw Error(`Not a single char: ${value}`); + } + return value.codePointAt(0); +} diff --git a/common/web/types/test/helpers/TestCompilerCallbacks.ts b/common/web/types/test/helpers/TestCompilerCallbacks.ts index 596a731d58880a1fbf4d7dab95a81c795bf212e1..8316264a774bf8b85e4adeb94c48937935247fdf 100644 --- a/common/web/types/test/helpers/TestCompilerCallbacks.ts +++ b/common/web/types/test/helpers/TestCompilerCallbacks.ts @@ -42,7 +42,7 @@ export class TestCompilerCallbacks implements CompilerCallbacks { return resolveFilename(baseFilename, filename); } - loadFile(filename: string | URL): Buffer { + loadFile(filename: string): Uint8Array { // TODO: error management, does it belong here? try { return loadFile(filename); diff --git a/common/web/types/test/helpers/index.ts b/common/web/types/test/helpers/index.ts index 58dc32dc9c1618da13acb027d39ad3a379a53394..06906df148589fb5a10808b92e685888d5fd8a7b 100644 --- a/common/web/types/test/helpers/index.ts +++ b/common/web/types/test/helpers/index.ts @@ -28,7 +28,10 @@ export function loadSchema(schema: CompilerSchema): Buffer { } export function resolveFilename(baseFilename: string, filename: string) { - const basePath = path.dirname(baseFilename); + const basePath = + baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? + baseFilename : + path.dirname(baseFilename); // Transform separators to platform separators -- we are agnostic // in our use here but path prefers files may use // either / or \, although older kps files were always \. diff --git a/common/web/types/test/helpers/reader-callback-test.ts b/common/web/types/test/helpers/reader-callback-test.ts index 8c85a759acf270a8a99e5eb947faf8b51db8bb93..204e0ec5bf4875168e082630354ff2453b756def 100644 --- a/common/web/types/test/helpers/reader-callback-test.ts +++ b/common/web/types/test/helpers/reader-callback-test.ts @@ -1,11 +1,16 @@ import 'mocha'; import {assert} from 'chai'; import { loadFile, makePathToFixture, loadSchema } from '../helpers/index.js'; -import LDMLKeyboardXMLSourceFileReader from '../../src/ldml-keyboard/ldml-keyboard-xml-reader.js'; +import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from '../../src/ldml-keyboard/ldml-keyboard-xml-reader.js'; import { CompilerEvent } from '../../src/util/compiler-interfaces.js'; import { LDMLKeyboardXMLSourceFile } from '../../src/ldml-keyboard/ldml-keyboard-xml.js'; import { LDMLKeyboardTestDataXMLSourceFile } from '../ldml-keyboard/ldml-keyboard-testdata-xml.js'; import { TestCompilerCallbacks } from './TestCompilerCallbacks.js'; +import { fileURLToPath } from 'url'; + +const readerOptions: LDMLKeyboardXMLSourceFileReaderOptions = { + importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) +}; export interface CompilationCase { /** @@ -71,7 +76,7 @@ export interface TestDataCase { export function testReaderCases(cases : CompilationCase[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); - const reader = new LDMLKeyboardXMLSourceFileReader(callbacks); + const reader = new LDMLKeyboardXMLSourceFileReader(readerOptions, callbacks); for (let testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to load: ${testcase.subpath}`: @@ -122,7 +127,7 @@ export function testReaderCases(cases : CompilationCase[]) { export function testTestdataReaderCases(cases : TestDataCase[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); - const reader = new LDMLKeyboardXMLSourceFileReader(callbacks); + const reader = new LDMLKeyboardXMLSourceFileReader(readerOptions, callbacks); for (let testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to load: ${testcase.subpath}`: diff --git a/common/web/types/test/kpj/test-kpj-file-reader.ts b/common/web/types/test/kpj/test-kpj-file-reader.ts index b7c2f710131db0d9bdf8be3f9190a267ee4a2ad5..95b5f1d01ae86030139e1ff594eed17fb5796713 100644 --- a/common/web/types/test/kpj/test-kpj-file-reader.ts +++ b/common/web/types/test/kpj/test-kpj-file-reader.ts @@ -15,7 +15,6 @@ describe('kpj-file-reader', function () { const input = fs.readFileSync(path); const reader = new KPJFileReader(callbacks); const kpj = reader.read(input); - console.dir(kpj); assert.doesNotThrow(() => { reader.validate(kpj, loadSchema('kpj')); }); @@ -79,7 +78,6 @@ describe('kpj-file-reader', function () { assert.lengthOf(project.files, 2); let f: KeymanDeveloperProjectFile10 = project.files[0]; - console.dir(f); assert.equal(f.id, 'id_f347675c33d2e6b1c705c787fad4941a'); assert.equal(f.filename, 'khmer_angkor.kmn'); assert.equal(f.filePath, 'source/khmer_angkor.kmn'); diff --git a/common/web/types/test/kvk/test-kvk-round-trip.ts b/common/web/types/test/kvk/test-kvk-round-trip.ts index 41dbe2dbc0cb25b553c790172998f006269c416b..031ad60b9e6e1c2fda395c2dda7caf8614794a90 100644 --- a/common/web/types/test/kvk/test-kvk-round-trip.ts +++ b/common/web/types/test/kvk/test-kvk-round-trip.ts @@ -5,7 +5,7 @@ import Hexy from 'hexy'; import gitDiff from 'git-diff'; const { hexy } = Hexy; import { loadSchema, makePathToFixture } from '../helpers/index.js'; -import KvksFileReader, { KVKSParseError } from "../../src/kvk/kvks-file-reader.js"; +import KvksFileReader from "../../src/kvk/kvks-file-reader.js"; import KvkFileReader from "../../src/kvk/kvk-file-reader.js"; import KvkFileWriter from "../../src/kvk/kvk-file-writer.js"; import KvksFileWriter from "../../src/kvk/kvks-file-writer.js"; @@ -53,9 +53,9 @@ describe('kvks-file-reader', function () { assert.doesNotThrow(() => { reader.validate(kvks, loadSchema('kvks')); }); - const errors: KVKSParseError[] = []; - const vk = reader.transform(kvks, errors); - assert.isEmpty(errors); + const invalidVkeys: string[] = []; + const vk = reader.transform(kvks, invalidVkeys); + assert.isEmpty(invalidVkeys); const writer = new KvkFileWriter(); const output = writer.write(vk); assertBufferMatch(Buffer.from(output), compiled); @@ -76,9 +76,9 @@ describe('kvks-file-reader', function () { assert.doesNotThrow(() => { kvksReader.validate(kvks, loadSchema('kvks')); }); - const errors: KVKSParseError[] = []; - const vk2 = kvksReader.transform(kvks, errors); - assert.isEmpty(errors); + const invalidVkeys: string[] = []; + const vk2 = kvksReader.transform(kvks, invalidVkeys); + assert.isEmpty(invalidVkeys); // make sure the binary is the same assert.deepEqual(vk2, vk); diff --git a/common/web/types/test/kvk/test-kvks-file.ts b/common/web/types/test/kvk/test-kvks-file.ts index 523bfaf54d618ce21da9f9137c67d9b04c39bd6a..4cbb299a0085b188d405d5a46aa23e964ad1a344 100644 --- a/common/web/types/test/kvk/test-kvks-file.ts +++ b/common/web/types/test/kvk/test-kvks-file.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import 'mocha'; import { loadSchema, makePathToFixture } from '../helpers/index.js'; -import KvksFileReader, { KVKSParseError } from "../../src/kvk/kvks-file-reader.js"; +import KvksFileReader from "../../src/kvk/kvks-file-reader.js"; import KvksFileWriter from "../../src/kvk/kvks-file-writer.js"; import { verify_khmer_angkor } from './test-kvk-utils.js'; import { assert } from 'chai'; @@ -16,9 +16,9 @@ describe('kvks-file-reader', function() { assert.doesNotThrow(() => { reader.validate(kvks, loadSchema('kvks')); }); - const errors: KVKSParseError[] = []; - const vk = reader.transform(kvks, errors); - assert.isEmpty(errors); + const invalidVkeys: string[] = []; + const vk = reader.transform(kvks, invalidVkeys); + assert.isEmpty(invalidVkeys); verify_khmer_angkor(vk); }); }); @@ -30,9 +30,9 @@ describe('kvks-file-writer', function() { const reader = new KvksFileReader(); const kvksExpected = reader.read(input); - const errors: KVKSParseError[] = []; - const vk = reader.transform(kvksExpected, errors); - assert.isEmpty(errors); + const invalidVkeys: string[] = []; + const vk = reader.transform(kvksExpected, invalidVkeys); + assert.isEmpty(invalidVkeys); const writer = new KvksFileWriter(); const output = writer.write(vk); diff --git a/common/web/types/test/ldml-keyboard/test-pattern-parser.ts b/common/web/types/test/ldml-keyboard/test-pattern-parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d00ab82bf523ce61ba58a928b3fbc74113b94dd --- /dev/null +++ b/common/web/types/test/ldml-keyboard/test-pattern-parser.ts @@ -0,0 +1,281 @@ +import 'mocha'; +import { assert } from 'chai'; +import { ElementParser, ElementSegment, ElementType, MarkerParser, OrderedStringList, VariableParser } from '../../src/ldml-keyboard/pattern-parser.js'; + +describe('Test of Pattern Parsers', () => { + describe('should test MarkerParser', () => { + it('should accept matching ids', () => { + for (const id of [ + 'm1', + 'dead_key', + 'alif', + 'Alif', + 'Alif2', + ]) { + assert.ok(MarkerParser.ID.test(id), `expected ok: ${id}`); + } + }); + it('should reject non matching ids', () => { + for (const id of [ + '', + 'Some Thing', + 'ναι', + 'SUPERCALIFRAGILISTICEXPIALIDOCIOUS', + '.', // reserved + ]) { + assert.notOk(MarkerParser.ID.test(id), `expected false: ${id}`); + } + }); + // indirectly tests REFERENCE + it('should match reference strings', () => { + const cases: string[][] = [ + ['\\m{acute}', 'acute'], + ['\\m{acute}≈\\m{acute}', 'acute acute'], // not deduped + ['\\m{grave}≠\\m{acute}', 'grave acute'], + [MarkerParser.ANY_MARKER, MarkerParser.ANY_MARKER_ID], + ['[a-z]\\m{.}', MarkerParser.ANY_MARKER_ID], + ]; + for (const [str, refs] of cases) { + const reflist = refs.split(' ').sort(); + assert.sameDeepMembers(MarkerParser.allReferences(str), reflist, `for ${str}`); + } + }); + it('non matching reference strings', () => { + for (const str of [ + '', + 'Some Thing', + 'ναι', + '\m{issed opportunity', + // 'This is: \\m{escaped}', // need a backreference + ]) { + assert.deepEqual(MarkerParser.allReferences(str), [], `expected no markers: ${str}`); + } + }); + it('should be able to emit sentinel values', () => { + assert.equal(MarkerParser.markerOutput(295), '\uFFFF\u0127', 'Wrong sentinel value emitted'); + assert.equal(MarkerParser.markerOutput(MarkerParser.ANY_MARKER_INDEX), MarkerParser.SENTINEL_ALL_MARKERS, 'Wrong sentinel value emitted for ffff'); + assert.throws(() => MarkerParser.markerOutput(0)); // below MIN + assert.throws(() => MarkerParser.markerOutput(0x10000)); // above MAX + }); + it('should be able to output sentinel strings', () => { + // with nothing (no markers) + assert.equal( + MarkerParser.toSentinelString(`No markers here!`), + `No markers here!` + ); + assert.throws(() => + MarkerParser.toSentinelString(`Marker \\m{sorryNoMarkers}`) + ); + // with a custom class + class MyMarkers implements OrderedStringList { + getItemOrder(item: string): number { + const m : any = { + 'a': 0, + 'b': 1, + 'c': 2, + 'zzz': 0x2FFFFF, + }; + const o = m[item]; + if (o === undefined) return -1; + return o; + } + }; + const markers = new MyMarkers(); + assert.equal(MarkerParser.toSentinelString( + `No markers here!`, markers), + `No markers here!` + ); + assert.equal(MarkerParser.toSentinelString( + `Give me \\m{a} and \\m{c}, or \\m{.}.`, markers), + `Give me \uFFFF\u0001 and \uFFFF\u0003, or \uFFFF\uFFFF.` + ); + assert.throws(() => + MarkerParser.toSentinelString( + `Want to see something funny? \\m{zzz}`, // out of range + markers + ) + ); + assert.throws(() => + MarkerParser.toSentinelString( + `Want to see something sad? \\m{nothing}`, // non existent + markers + ) + ); + }); + }); + describe('should test VariableParser', () => { + // same test as for markers + it('should accept matching ids', () => { + for (const id of [ + 'm1', + 'dead_key', + 'alif', + 'Alif', + 'Alif2', + ]) { + assert.ok(VariableParser.ID.test(id), `expected ok: ${id}`); + } + }); + it('should reject non matching ids', () => { + for (const id of [ + '', + 'Some Thing', + 'ναι', + 'SUPERCALIFRAGILISTICEXPIALIDOCIOUS', + '.', // reserved + ]) { + assert.notOk(VariableParser.ID.test(id), `expected false: ${id}`); + } + }); + }); + describe('ElementParser', () => { + const samplePatterns = [ + `\\u1A60`, + `[\\u1A75-\\u1A79]`, + `\\u1A60\\u1A45`, + `\\u1A60[\\u1A75-\\u1A79]\\u1A45`, + `ែ្ម`, + ] + describe('try out the regexes', () => { + it('should detect usets', () => { + [ + `[a-z]`, + `[[a-z]-[aeiou]]`, + ].forEach(s => assert.ok(ElementParser.MATCH_USET.test(s), `expected true: ${s}`)); + }); + it('should detect non usets', () => { + [ + `\\u0127`, + `\\u{22}`, + `x`, + ].forEach(s => assert.notOk(ElementParser.MATCH_USET.test(s), `expected false: ${s}`)); + }); + it('should detect escaped', () => { + [ + `\\u0127`, + `\\u{22}`, + ].forEach(s => assert.ok(ElementParser.MATCH_ESCAPED.test(s), `expected true: ${s}`)); + }); + it('should detect non escaped', () => { + [ + `[a-z]`, + `[[a-z]-[aeiou]]`, + `ê`, + ].forEach(s => assert.notOk(ElementParser.MATCH_ESCAPED.test(s), `expected false: ${s}`)); + }); + it('should reject nested square brackets', () => { + [ + `[[a-z]-[aeiou]]`, + ].forEach(s => assert.ok(ElementParser.MATCH_NESTED_SQUARE_BRACKETS.test(s), `expected true: ${s}`)); + }); + it('should allow non-nested square brackets', () => { + [ + `[a-z]`, + `ê`, + ...samplePatterns, + ].forEach(s => assert.notOk(ElementParser.MATCH_NESTED_SQUARE_BRACKETS.test(s), `expected false: ${s}`)); + }); + it('should be able to run some splitters', () => { + assert.sameDeepMembers(VariableParser.allStringReferences( + `` + ), [ + ], `running allStringReferences('')`); + + assert.sameDeepMembers(VariableParser.allStringReferences( + '${str1} ${str2}' + ), [ + 'str1', 'str2', + ], `running allStringReferences('\${str1} \${str2}')`); + + assert.sameDeepMembers(VariableParser.allSetReferences( + '', + ), [ + ], `running allSetReferences('')`); + assert.sameDeepMembers(VariableParser.allSetReferences( + ' $[set1] $[set2] ', + ), [ + 'set1', 'set2', + ], `running allSetReferences(' \$[set1] \$[set2]')`); + assert.sameDeepMembers(VariableParser.setSplitter( + `` + ), [ + ], `running setSplitter('')`); + assert.sameDeepMembers(VariableParser.setSplitter( + ` A B C` + ), [ + 'A', 'B', 'C', + ], `running setSplitter(' A B C')`); + }); + }); + describe('segment some strings', () => { + it('should have a functioning ElementSegment() c’tor', () => { + assert.equal(new ElementSegment('String', ElementType.string).type, ElementType.string); + assert.equal(new ElementSegment('String', ElementType.string).unescaped, 'String'); + assert.equal(new ElementSegment('\\u0041').unescaped, 'A'); + assert.equal(new ElementSegment('\\u{0041}').unescaped, 'A'); + }); + it('should be able to segment strings from the spec and samples', () => { + samplePatterns.forEach(str => assert.ok(ElementParser.segment(str))); + }); + it('should throw on nested brackets', () => { + [ + `[[a-z]-[aeiou]]`, + ].forEach(str => assert.throws(() => ElementParser.segment(str))); + }); + [ + { + str: `ê🙀`, + expect: [{ + segment: 'ê', + type: ElementType.codepoint + },{ + segment: '🙀', + type: ElementType.codepoint + }], + }, + { + str: `\\u1A60[\\u1A75-\\u1A79]\\u1A45ħ`, + expect: [{ + segment: '\\u1A60', + type: ElementType.escaped, + },{ + segment: '[\\u1A75-\\u1A79]', + type: ElementType.uset, + },{ + segment: '\\u1A45', + type: ElementType.escaped, + },{ + segment: 'ħ', + type: ElementType.codepoint, + }], + }, + { + str: `\\u{22}\\u{0127}`, + expect: [{ + segment: `\\u{22}`, + type: ElementType.escaped, + }, + { + segment: `\\u{0127}`, + type: ElementType.escaped, + }], + }, + { + str: `\\u{22 0127}`, + expect: [{ + segment: `\\u{22}`, + type: ElementType.escaped, + }, + { + segment: `\\u{127}`, // resegmented with minimal hex + type: ElementType.escaped, + }], + }, + ].forEach(({str, expect}) => it(`Segment: ${str}`, () => { + const segmented = ElementParser.segment(str); + assert.ok(segmented, `segmenting ${str}`); + assert.deepEqual(segmented, expect, `segments of ${str}`) + })); + }); + }); +}); diff --git a/common/web/types/test/tsconfig.json b/common/web/types/test/tsconfig.json index 9c97b463afd01e2855a701a35ed22430b978ac90..c522129c99a432b0bd3a7c7b0639488578e44d4b 100644 --- a/common/web/types/test/tsconfig.json +++ b/common/web/types/test/tsconfig.json @@ -14,7 +14,7 @@ "./helpers/*.ts", ], "references": [ - { "path": "../../keyman-version/tsconfig.esm.json" }, + { "path": "../../keyman-version" }, { "path": "../../../../core/include/ldml/"}, { "path": "../" }, ] diff --git a/common/web/types/test/util/test-unescape.ts b/common/web/types/test/util/test-unescape.ts index 5708046a80d3f3389a6c161ff2e6eafb3151a681..c3630954b361fb38a6f09b2ce532b5a31f22a72e 100644 --- a/common/web/types/test/util/test-unescape.ts +++ b/common/web/types/test/util/test-unescape.ts @@ -1,6 +1,26 @@ import 'mocha'; import {assert} from 'chai'; -import {unescapeString, UnescapeError} from '../../src/util/util.js'; +import {unescapeString, UnescapeError, isOneChar, toOneChar, unescapeOneQuadString} from '../../src/util/util.js'; + +describe('test UTF32 functions()', function() { + it('should properly categorize strings', () => { + [ + 'x', + '🙀', + ].forEach(s => assert.isTrue(isOneChar(s), `isOneChar(${s})`)); + + [ + 'xx', + '🙀🙀', + ].forEach(s => assert.isFalse(isOneChar(s), `!isOneChar(${s})`)); + }); + + it('should convert to single chars', function() { + assert.equal(toOneChar('ħ'), 295); + assert.equal(toOneChar('🙀'), 0x1F640); + assert.throws(() => toOneChar('ħħħ'), /Not a single char/); + }); +}); describe('test unescapeString()', function() { it("should correctly handle multi strings", function() { @@ -36,3 +56,15 @@ describe('test unescapeString()', function() { assert.throws(() => unescapeString('\\u{110000}'), UnescapeError); }); }); + +describe('test unescapeOneQuadString()', () => { + it('should be able to convert', () => { + // testing that `\u0127` is unescaped correctly (to U+0127: 'ħ') + assert.equal(unescapeOneQuadString('\\u0127'), '\u{0127}'); + // test the fail cases + }); + it('should fail when it needs to fail', () => { + assert.throws(() => unescapeOneQuadString(null), null); + assert.throws(() => unescapeOneQuadString('\uFFFFFFFFFFFF')); + }); +}); diff --git a/common/web/types/tsconfig.json b/common/web/types/tsconfig.json index a32b8e1ed7f7fd098b0cdc87b64517623f6503ad..983a09fda6721a964099200885c0af74a68024c9 100644 --- a/common/web/types/tsconfig.json +++ b/common/web/types/tsconfig.json @@ -11,7 +11,7 @@ "src/**/*.ts" ], "references": [ - { "path": "../keyman-version/tsconfig.esm.json" }, + { "path": "../keyman-version" }, { "path": "../../../core/include/ldml/tsconfig.json"}, ] } diff --git a/common/web/utils/.c8rc.json b/common/web/utils/.c8rc.json new file mode 100644 index 0000000000000000000000000000000000000000..d9074b62f31d0d2de635ea8425cb10fcba339f33 --- /dev/null +++ b/common/web/utils/.c8rc.json @@ -0,0 +1,17 @@ +{ + "all": true, + "check-coverage": false, + "clean": true, + "exclude": [ + "src/deviceSpec.ts", + "src/globalObject.ts", + "node_modules/*", + "src/test" + ], + "exclude-after-remap": true, + "reporter": ["text", "text-summary"], + "reports-dir": "build/coverage", + "src": [ + "src/" + ] +} \ No newline at end of file diff --git a/common/web/utils/build.sh b/common/web/utils/build.sh index 012f6ad41e9ace13da34a665ec2048e52ff2d724..2d5e4321ab7d25ade46ff42a3edb6d4206d412e1 100755 --- a/common/web/utils/build.sh +++ b/common/web/utils/build.sh @@ -19,30 +19,37 @@ cd "$THIS_SCRIPT_PATH" builder_describe \ "Compiles the web-oriented utility function module." \ "@/common/web/keyman-version" \ - clean configure build test + "@/common/web/es-bundling" \ + clean configure build test \ + "--ci For use with action ${BUILDER_TERM_START}test${BUILDER_TERM_END} - emits CI-friendly test reports" builder_describe_outputs \ configure "/node_modules" \ - build "/common/web/utils/build/index.js" + build "/common/web/utils/build/obj/index.js" builder_parse "$@" -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi - -if builder_start_action clean; then - npm run clean - builder_finish_action success clean -fi - -if builder_start_action build; then - # Note: in a dependency build, we'll expect utils to be built by tsc -b - if builder_is_dep_build; then - builder_echo "skipping tsc -b; will be completed by $builder_dep_parent" - else - tsc --build "$THIS_SCRIPT_PATH/tsconfig.json" +function do_build() { + tsc --build "$THIS_SCRIPT_PATH/tsconfig.json" + + # May be useful one day, for building a mass .d.ts for KMW as a whole. + # So... tsc does declaration-bundling on its own pretty well, at least for local development. + tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts +} + +function do_test() { + builder_heading "Running web-utils test suite" + + local FLAGS= + if builder_has_option --ci; then + echo "Replacing user-friendly test reports with CI-friendly versions." + FLAGS="$FLAGS --reporter mocha-teamcity-reporter" fi - builder_finish_action success build -fi \ No newline at end of file + + c8 mocha --recursive $FLAGS ./src/test/ +} + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf build/ +builder_run_action build do_build +builder_run_action test do_test \ No newline at end of file diff --git a/common/web/utils/package.json b/common/web/utils/package.json index fbe518e2b8ae0a82bcc0df6e69311453cb18af9b..210ac4b6be5a87083aea2d3f9c72966b2d79b615 100644 --- a/common/web/utils/package.json +++ b/common/web/utils/package.json @@ -1,12 +1,16 @@ { "name": "@keymanapp/web-utils", "description": "Common utility functions used throughout other Keyman packages", - "main": "./build/index.js", - "types": "./build/index.d.ts", + "main": "./build/obj/index.js", + "exports": { + ".": { + "types": "./build/obj/index.d.ts", + "import": "./build/obj/index.js" + } + }, "scripts": { "build": "gosh ./build.sh", - "clean": "tsc -b --clean", - "tsc": "tsc" + "clean": "gosh ./build.sh clean" }, "repository": { "type": "git", @@ -21,7 +25,15 @@ "devDependencies": { "@keymanapp/resources-gosh": "*", "@keymanapp/keyman-version": "*", + "c8": "^7.12.0", + "chai": "^4.3.4", + "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", "@types/node": "^14.0.5", "typescript": "^4.9.5" + }, + "type": "module", + "paths": { + "@keymanapp/keyman-version": "*" } } diff --git a/common/web/utils/src/deepCopy.ts b/common/web/utils/src/deepCopy.ts index ce344391c4b4c37df77e7ad88e2fefccf971adcd..ce05270365ccad985dfae891f2cfd60e92005d31 100644 --- a/common/web/utils/src/deepCopy.ts +++ b/common/web/utils/src/deepCopy.ts @@ -1,25 +1,23 @@ -namespace com.keyman.utils { - /** - * Function deepCopy - * Scope Private - * @param {Object} p object to copy - * @param {Array=} c0 array member being copied - * @return {Object} clone ('deep copy') of object - * Description Makes an actual copy (not a reference) of an object, copying simple members, - * arrays and member objects but not functions, so use with care! - */ - export function deepCopy(p:T, c0?): T { - var c = c0 || {}; - for (var i in p) { - if(typeof p[i] === 'object' && p[i] != null) { - c[i] = (p[i].constructor === Array ) ? [] : {}; - deepCopy(p[i],c[i]); - } - else { - c[i] = p[i]; - } +/** + * Function deepCopy + * Scope Private + * @param {Object} p object to copy + * @param {Array=} c0 array member being copied + * @return {Object} clone ('deep copy') of object + * Description Makes an actual copy (not a reference) of an object, copying simple members, + * arrays and member objects but not functions, so use with care! + */ +export default function deepCopy(p:T, c0?): T { + var c = c0 || {}; + for (var i in p) { + if(typeof p[i] === 'object' && p[i] != null) { + c[i] = (p[i].constructor === Array ) ? [] : {}; + deepCopy(p[i],c[i]); + } + else { + c[i] = p[i]; } - - return c; } + + return c; } \ No newline at end of file diff --git a/common/web/utils/src/deviceSpec.ts b/common/web/utils/src/deviceSpec.ts index 7e2932e471232b9882a6a919e88eade829b5eb01..3ab0352fc468ecb6e51e7a8ff79ac28116db06cd 100644 --- a/common/web/utils/src/deviceSpec.ts +++ b/common/web/utils/src/deviceSpec.ts @@ -1,4 +1,56 @@ -namespace com.keyman.utils { +/** + * This class provides an abstract version of com.keyman.Device that is core-friendly, + * containing only the information needed by web-core for text processing use, devoid + * of any direct references to the DOM. + */ +export class DeviceSpec { + readonly browser: DeviceSpec.Browser; + readonly formFactor: DeviceSpec.FormFactor; + readonly OS: DeviceSpec.OperatingSystem; + readonly touchable: boolean; + + constructor(browser: string, formFactor: string, OS: string, touchable: boolean) { + switch(browser.toLowerCase() as DeviceSpec.Browser) { + case DeviceSpec.Browser.Chrome: + case DeviceSpec.Browser.Edge: + case DeviceSpec.Browser.Firefox: + case DeviceSpec.Browser.Native: + case DeviceSpec.Browser.Opera: + case DeviceSpec.Browser.Safari: + this.browser = browser.toLowerCase() as DeviceSpec.Browser; + break; + default: + this.browser = DeviceSpec.Browser.Other; + } + + switch(formFactor.toLowerCase() as DeviceSpec.FormFactor) { + case DeviceSpec.FormFactor.Desktop: + case DeviceSpec.FormFactor.Phone: + case DeviceSpec.FormFactor.Tablet: + this.formFactor = formFactor.toLowerCase() as DeviceSpec.FormFactor; + break; + default: + throw ("Invalid form factor specified for device: " + formFactor); + } + + switch(OS.toLowerCase() as DeviceSpec.OperatingSystem) { + case DeviceSpec.OperatingSystem.Windows.toLowerCase(): + case DeviceSpec.OperatingSystem.macOS.toLowerCase(): + case DeviceSpec.OperatingSystem.Linux.toLowerCase(): + case DeviceSpec.OperatingSystem.Android.toLowerCase(): + case DeviceSpec.OperatingSystem.iOS.toLowerCase(): + this.OS = OS.toLowerCase() as DeviceSpec.OperatingSystem; + break; + default: + this.OS = DeviceSpec.OperatingSystem.Other; + } + + this.touchable = touchable; + } +} + +// Namespaces these under DeviceSpec, as each is primarily used with it. +export namespace DeviceSpec { export enum Browser { Chrome = 'chrome', Edge = 'edge', @@ -23,55 +75,10 @@ namespace com.keyman.utils { Phone = 'phone', Tablet = 'tablet' } +} - /** - * This class provides an abstract version of com.keyman.Device that is core-friendly, - * containing only the information needed by web-core for text processing use, devoid - * of any direct references to the DOM. - */ - export class DeviceSpec { - readonly browser: Browser; - readonly formFactor: FormFactor; - readonly OS: OperatingSystem; - readonly touchable: boolean; - - constructor(browser: string, formFactor: string, OS: string, touchable: boolean) { - switch(browser.toLowerCase() as Browser) { - case Browser.Chrome: - case Browser.Edge: - case Browser.Firefox: - case Browser.Native: - case Browser.Opera: - case Browser.Safari: - this.browser = browser.toLowerCase() as Browser; - break; - default: - this.browser = Browser.Other; - } +export function physicalKeyDeviceAlias(device: DeviceSpec) { + return new DeviceSpec(device.browser, DeviceSpec.FormFactor.Desktop, device.OS, false); +} - switch(formFactor.toLowerCase() as FormFactor) { - case FormFactor.Desktop: - case FormFactor.Phone: - case FormFactor.Tablet: - this.formFactor = formFactor.toLowerCase() as FormFactor; - break; - default: - throw ("Invalid form factor specified for device: " + formFactor); - } - - switch(OS.toLowerCase() as OperatingSystem) { - case OperatingSystem.Windows.toLowerCase(): - case OperatingSystem.macOS.toLowerCase(): - case OperatingSystem.Linux.toLowerCase(): - case OperatingSystem.Android.toLowerCase(): - case OperatingSystem.iOS.toLowerCase(): - this.OS = OS.toLowerCase() as OperatingSystem; - break; - default: - this.OS = OperatingSystem.Other; - } - - this.touchable = touchable; - } - } -} \ No newline at end of file +export default DeviceSpec; \ No newline at end of file diff --git a/common/web/utils/src/globalObject.ts b/common/web/utils/src/globalObject.ts index 08eb08df6069d04bc77c4608bcd610724eecc8aa..b26868721696d332bf720df6dc2fcc80bde1aaa4 100644 --- a/common/web/utils/src/globalObject.ts +++ b/common/web/utils/src/globalObject.ts @@ -1,32 +1,30 @@ -namespace com.keyman.utils { - /** - * Returns the base global object available to the current JS platform. - * - In browsers, returns `window`. - * - In WebWorkers, returns `self`. - * - In Node, returns `global`. - */ - export function getGlobalObject(): typeof globalThis { - // Evergreen browsers have started defining 'globalThis'. - // Refer to https://devblogs.microsoft.com/typescript/announcing-typescript-3-4/#type-checking-for-globalthis - // and its referenced polyfill. Said polyfill is very complex, so we opt for this far leaner variant. - if(typeof globalThis != 'undefined') { - return globalThis; // Not available in IE or older Edge versions - // @ts-ignore (TS will throw errors for whatever platform we're not compiling for.) - } else if(typeof window != 'undefined') { - // @ts-ignore - return window; // The browser-based classic - // @ts-ignore - } else if(typeof self != 'undefined') { - // @ts-ignore - return self; // WebWorker global - } else { - // Assumption - if neither of the above exist, we're in Node, for unit-testing. - // Node doesn't have as many methods and properties as the other two, but what - // matters for us is that it's the base global. - // - // Some other headless JS solutions use 'this' instead, but Node's enough for our needs. - // @ts-ignore - return (global as any) as typeof globalThis; - } +/** + * Returns the base global object available to the current JS platform. + * - In browsers, returns `window`. + * - In WebWorkers, returns `self`. + * - In Node, returns `global`. + */ +export default function getGlobalObject(): typeof globalThis { + // Evergreen browsers have started defining 'globalThis'. + // Refer to https://devblogs.microsoft.com/typescript/announcing-typescript-3-4/#type-checking-for-globalthis + // and its referenced polyfill. Said polyfill is very complex, so we opt for this far leaner variant. + if(typeof globalThis != 'undefined') { + return globalThis; // Not available in IE or older Edge versions + // @ts-ignore (TS will throw errors for whatever platform we're not compiling for.) + } else if(typeof window != 'undefined') { + // @ts-ignore + return window; // The browser-based classic + // @ts-ignore + } else if(typeof self != 'undefined') { + // @ts-ignore + return self; // WebWorker global + } else { + // Assumption - if neither of the above exist, we're in Node, for unit-testing. + // Node doesn't have as many methods and properties as the other two, but what + // matters for us is that it's the base global. + // + // Some other headless JS solutions use 'this' instead, but Node's enough for our needs. + // @ts-ignore + return (global as any) as typeof globalThis; } } \ No newline at end of file diff --git a/common/web/utils/src/index.ts b/common/web/utils/src/index.ts index 8d7a2e7773ed44839e31701c4856b33171cb3ba9..dc3f868973aa089ddd51bccad60d31fd7233b6a8 100644 --- a/common/web/utils/src/index.ts +++ b/common/web/utils/src/index.ts @@ -1,6 +1,27 @@ // References all utility includes from a single file, making import/export simple. -/// -/// -/// -/// -/// \ No newline at end of file + +export { default as deepCopy } from "./deepCopy.js"; + +export { default as DeviceSpec, physicalKeyDeviceAlias } from "./deviceSpec.js"; + +/* + // An example valid use, post-import: + let testSpec = new DeviceSpec(DeviceSpec.Browser.Chrome, + DeviceSpec.FormFactor.Tablet, + DeviceSpec.OperatingSystem.Android, + true); + */ + +export { default as Version } from "./version.js"; + +export { default as globalObject } from "./globalObject.js"; + +export { default as extendString } from "./kmwstring.js"; + + + +export { default as ManagedPromise } from "./managedPromise.js"; + +// // Uncomment the following line and run the bundled output to verify successful +// // esbuild bundling of this submodule: +// console.log(Version.CURRENT.toString()); \ No newline at end of file diff --git a/common/web/utils/src/kmwstring.ts b/common/web/utils/src/kmwstring.ts index 397b9041a5434f8ba1cdd01989231f8b75f32e18..21d165e8ff778c2044f9349aa8877527c06aeb0c 100644 --- a/common/web/utils/src/kmwstring.ts +++ b/common/web/utils/src/kmwstring.ts @@ -3,468 +3,479 @@ Copyright 2020 SIL International ***/ + /* * TODO: Remove this file as part of addressing https://github.com/keymanapp/keyman/issues/2492. */ -interface StringConstructor { - kmwFromCharCode(cp0: number): string, - _kmwFromCharCode(cp0: number): string, - kmwEnableSupplementaryPlane(bEnable: boolean) -} +declare global { + interface StringConstructor { + kmwFromCharCode(cp0: number): string, + _kmwFromCharCode(cp0: number): string, + kmwEnableSupplementaryPlane(bEnable: boolean) + } -interface String { - kmwCharCodeAt(codePointIndex: number): number, - kmwCharAt(codePointIndex: number) : string, - kmwIndexOf(searchValue: string, fromIndex?: number) : number, - kmwLastIndexOf(searchValue: string, fromIndex?: number) : number, - kmwSlice(beginSlice: number, endSlice: number) : string, - kmwSubstring(start: number, length: number) : string, - kmwSubstr(start: number, length?: number) : string, - kmwBMPSubstr(start: number, length?: number) : string, - kmwLength(): number, - kmwBMPLength(): number, - kmwNextChar(codeUnitIndex: number): number, - kmwBMPNextChar(codeUnitIndex: number): number, - kmwPrevChar(codeUnitIndex: number): number, - kmwBMPPrevChar(codeUnitIndex: number): number, - kmwCodePointToCodeUnit(codePointIndex: number) : number, - kmwBMPCodePointToCodeUnit(codePointIndex: number) : number, - kmwCodeUnitToCodePoint(codeUnitIndex: number) : number, - kmwBMPCodeUnitToCodePoint(codeUnitIndex: number) : number, - _kmwCharCodeAt(codePointIndex: number): number, - _kmwCharAt(codePointIndex: number) : string, - _kmwIndexOf(searchValue: string, fromIndex?: number) : number, - _kmwLastIndexOf(searchValue: string, fromIndex?: number) : number, - _kmwSlice(beginSlice: number, endSlice: number) : string, - _kmwSubstring(start: number, length?: number) : string, - _kmwSubstr(start: number, length?: number) : string, - _kmwLength(): number, - _kmwNextChar(codeUnitIndex: number): number, - _kmwPrevChar(codeUnitIndex: number): number, - _kmwCodePointToCodeUnit(codePointIndex: number) : number, - _kmwCodeUnitToCodePoint(codeUnitIndex: number) : number, + interface String { + kmwCharCodeAt(codePointIndex: number): number, + kmwCharAt(codePointIndex: number) : string, + kmwIndexOf(searchValue: string, fromIndex?: number) : number, + kmwLastIndexOf(searchValue: string, fromIndex?: number) : number, + kmwSlice(beginSlice: number, endSlice: number) : string, + kmwSubstring(start: number, length: number) : string, + kmwSubstr(start: number, length?: number) : string, + kmwBMPSubstr(start: number, length?: number) : string, + kmwLength(): number, + kmwBMPLength(): number, + kmwNextChar(codeUnitIndex: number): number, + kmwBMPNextChar(codeUnitIndex: number): number, + kmwPrevChar(codeUnitIndex: number): number, + kmwBMPPrevChar(codeUnitIndex: number): number, + kmwCodePointToCodeUnit(codePointIndex: number) : number, + kmwBMPCodePointToCodeUnit(codePointIndex: number) : number, + kmwCodeUnitToCodePoint(codeUnitIndex: number) : number, + kmwBMPCodeUnitToCodePoint(codeUnitIndex: number) : number, + _kmwCharCodeAt(codePointIndex: number): number, + _kmwCharAt(codePointIndex: number) : string, + _kmwIndexOf(searchValue: string, fromIndex?: number) : number, + _kmwLastIndexOf(searchValue: string, fromIndex?: number) : number, + _kmwSlice(beginSlice: number, endSlice: number) : string, + _kmwSubstring(start: number, length?: number) : string, + _kmwSubstr(start: number, length?: number) : string, + _kmwLength(): number, + _kmwNextChar(codeUnitIndex: number): number, + _kmwPrevChar(codeUnitIndex: number): number, + _kmwCodePointToCodeUnit(codePointIndex: number) : number, + _kmwCodeUnitToCodePoint(codeUnitIndex: number) : number + } } -/** - * Constructs a string from one or more Unicode character codepoint values - * passed as integer parameters. - * - * @param {number} cp0,... 1 or more Unicode codepoints, e.g. 0x0065, 0x10000 - * @return {string|null} The new String object. - */ -String.kmwFromCharCode = function(cp0) { - var chars = [], i; - for (i = 0; i < arguments.length; i++) { - var c = Number(arguments[i]); - if (!isFinite(c) || c < 0 || c > 0x10FFFF || Math.floor(c) !== c) { - throw new RangeError("Invalid code point " + c); - } - if (c < 0x10000) { - chars.push(c); - } else { - c -= 0x10000; - chars.push((c >> 10) + 0xD800); - chars.push((c % 0x400) + 0xDC00); +export default function extendString() { + /** + * Constructs a string from one or more Unicode character codepoint values + * passed as integer parameters. + * + * @param {number} cp0,... 1 or more Unicode codepoints, e.g. 0x0065, 0x10000 + * @return {string|null} The new String object. + */ + String.kmwFromCharCode = function(cp0) { + var chars = [], i; + for (i = 0; i < arguments.length; i++) { + var c = Number(arguments[i]); + if (!isFinite(c) || c < 0 || c > 0x10FFFF || Math.floor(c) !== c) { + throw new RangeError("Invalid code point " + c); + } + if (c < 0x10000) { + chars.push(c); + } else { + c -= 0x10000; + chars.push((c >> 10) + 0xD800); + chars.push((c % 0x400) + 0xDC00); + } } + return String.fromCharCode.apply(undefined, chars); } - return String.fromCharCode.apply(undefined, chars); -} -/** - * Returns a number indicating the Unicode value of the character at the given - * code point index, with support for supplementary plane characters. - * - * @param {number} codePointIndex The code point index into the string (not - the code unit index) to return - * @return {number} The Unicode character value - */ -String.prototype.kmwCharCodeAt = function(codePointIndex) { - var str = String(this); - var codeUnitIndex = 0; - - if (codePointIndex < 0 || codePointIndex >= str.length) { - return NaN; - } + /** + * Returns a number indicating the Unicode value of the character at the given + * code point index, with support for supplementary plane characters. + * + * @param {number} codePointIndex The code point index into the string (not + the code unit index) to return + * @return {number} The Unicode character value + */ + String.prototype.kmwCharCodeAt = function(codePointIndex) { + var str = String(this); + var codeUnitIndex = 0; - for(var i = 0; i < codePointIndex; i++) { - codeUnitIndex = str.kmwNextChar(codeUnitIndex); - if(codeUnitIndex === null) return NaN; - } - - var first = str.charCodeAt(codeUnitIndex); - if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) { - var second = str.charCodeAt(codeUnitIndex + 1); - if (second >= 0xDC00 && second <= 0xDFFF) { - return ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000; + if (codePointIndex < 0 || codePointIndex >= str.length) { + return NaN; } + + for(var i = 0; i < codePointIndex; i++) { + codeUnitIndex = str.kmwNextChar(codeUnitIndex); + if(codeUnitIndex === null) return NaN; + } + + var first = str.charCodeAt(codeUnitIndex); + if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) { + var second = str.charCodeAt(codeUnitIndex + 1); + if (second >= 0xDC00 && second <= 0xDFFF) { + return ((first - 0xD800) << 10) + (second - 0xDC00) + 0x10000; + } + } + return first; } - return first; -} -/** - * Returns the code point index within the calling String object of the first occurrence - * of the specified value, or -1 if not found. - * - * @param {string} searchValue The value to search for - * @param {number} [fromIndex] Optional code point index to start searching from - * @return {number} The code point index of the specified search value - */ -String.prototype.kmwIndexOf = function(searchValue, fromIndex) { - var str = String(this); - var codeUnitIndex = str.indexOf(searchValue, fromIndex); - - if(codeUnitIndex < 0) { - return codeUnitIndex; + /** + * Returns the code point index within the calling String object of the first occurrence + * of the specified value, or -1 if not found. + * + * @param {string} searchValue The value to search for + * @param {number} [fromIndex] Optional code point index to start searching from + * @return {number} The code point index of the specified search value + */ + String.prototype.kmwIndexOf = function(searchValue, fromIndex) { + var str = String(this); + var codeUnitIndex = str.indexOf(searchValue, fromIndex); + + if(codeUnitIndex < 0) { + return codeUnitIndex; + } + + var codePointIndex = 0; + for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++; + return codePointIndex; } - - var codePointIndex = 0; - for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++; - return codePointIndex; -} -/** - * Returns the code point index within the calling String object of the last occurrence - * of the specified value, or -1 if not found. - * - * @param {string} searchValue The value to search for - * @param {number} fromIndex Optional code point index to start searching from - * @return {number} The code point index of the specified search value - */ -String.prototype.kmwLastIndexOf = function(searchValue, fromIndex) -{ - var str = String(this); - var codeUnitIndex = str.lastIndexOf(searchValue, fromIndex); - - if(codeUnitIndex < 0) { - return codeUnitIndex; + /** + * Returns the code point index within the calling String object of the last occurrence + * of the specified value, or -1 if not found. + * + * @param {string} searchValue The value to search for + * @param {number} fromIndex Optional code point index to start searching from + * @return {number} The code point index of the specified search value + */ + String.prototype.kmwLastIndexOf = function(searchValue, fromIndex) + { + var str = String(this); + var codeUnitIndex = str.lastIndexOf(searchValue, fromIndex); + + if(codeUnitIndex < 0) { + return codeUnitIndex; + } + + var codePointIndex = 0; + for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++; + return codePointIndex; } - - var codePointIndex = 0; - for(var i = 0; i !== null && i < codeUnitIndex; i = str.kmwNextChar(i)) codePointIndex++; - return codePointIndex; -} -/** - * Returns the length of the string in code points, as opposed to code units. - * - * @return {number} The length of the string in code points - */ -String.prototype.kmwLength = function() { - var str = String(this); - - if(str.length == 0) return 0; - - for(var i = 0, codeUnitIndex = 0; codeUnitIndex !== null; i++) - codeUnitIndex = str.kmwNextChar(codeUnitIndex); - return i; -} + /** + * Returns the length of the string in code points, as opposed to code units. + * + * @return {number} The length of the string in code points + */ + String.prototype.kmwLength = function() { + var str = String(this); -/** - * Extracts a section of a string and returns a new string. - * - * @param {number} beginSlice The start code point index in the string to - * extract from - * @param {number} endSlice Optional end code point index in the string - * to extract to - * @return {string} The substring as selected by beginSlice and - * endSlice - */ -String.prototype.kmwSlice = function(beginSlice, endSlice) { - var str = String(this); - var beginSliceCodeUnit = str.kmwCodePointToCodeUnit(beginSlice); - var endSliceCodeUnit = str.kmwCodePointToCodeUnit(endSlice); - if(beginSliceCodeUnit === null || endSliceCodeUnit === null) - return ''; - else - return str.slice(beginSliceCodeUnit, endSliceCodeUnit); -} + if(str.length == 0) return 0; -/** - * Returns the characters in a string beginning at the specified location through - * the specified number of characters. - * - * @param {number} start The start code point index in the string to - * extract from - * @param {number=} length Optional length to extract - * @return {string} The substring as selected by start and length - */ -String.prototype.kmwSubstr = function(start, length?) -{ - var str = String(this); - if(start < 0) - { - start = str.kmwLength() + start; + for(var i = 0, codeUnitIndex = 0; codeUnitIndex !== null; i++) + codeUnitIndex = str.kmwNextChar(codeUnitIndex); + return i; } - if(start < 0) start = 0; - var startCodeUnit = str.kmwCodePointToCodeUnit(start); - var endCodeUnit = startCodeUnit; - - if(startCodeUnit === null) return ''; - - if(arguments.length < 2) { - endCodeUnit = str.length; - } else { - for(var i = 0; i < length; i++) endCodeUnit = str.kmwNextChar(endCodeUnit); + + /** + * Extracts a section of a string and returns a new string. + * + * @param {number} beginSlice The start code point index in the string to + * extract from + * @param {number} endSlice Optional end code point index in the string + * to extract to + * @return {string} The substring as selected by beginSlice and + * endSlice + */ + String.prototype.kmwSlice = function(beginSlice, endSlice) { + var str = String(this); + var beginSliceCodeUnit = str.kmwCodePointToCodeUnit(beginSlice); + var endSliceCodeUnit = str.kmwCodePointToCodeUnit(endSlice); + if(beginSliceCodeUnit === null || endSliceCodeUnit === null) + return ''; + else + return str.slice(beginSliceCodeUnit, endSliceCodeUnit); } - if(endCodeUnit === null) - return str.substring(startCodeUnit); - else - return str.substring(startCodeUnit, endCodeUnit); -} -/** - * Returns the characters in a string between two indexes into the string. - * - * @param {number} indexA The start code point index in the string to - * extract from - * @param {number} indexB The end code point index in the string to - * extract to - * @return {string} The substring as selected by indexA and indexB - */ -String.prototype.kmwSubstring = function(indexA, indexB) -{ - var str = String(this),indexACodeUnit,indexBCodeUnit; - - if(typeof(indexB) == 'undefined') + /** + * Returns the characters in a string beginning at the specified location through + * the specified number of characters. + * + * @param {number} start The start code point index in the string to + * extract from + * @param {number=} length Optional length to extract + * @return {string} The substring as selected by start and length + */ + String.prototype.kmwSubstr = function(start, length?) { - indexACodeUnit = str.kmwCodePointToCodeUnit(indexA); - indexBCodeUnit = str.length; - } - else - { - if(indexA > indexB) { var c = indexA; indexA = indexB; indexB = c; } - - indexACodeUnit = str.kmwCodePointToCodeUnit(indexA); - indexBCodeUnit = str.kmwCodePointToCodeUnit(indexB); + var str = String(this); + if(start < 0) + { + start = str.kmwLength() + start; + } + if(start < 0) start = 0; + var startCodeUnit = str.kmwCodePointToCodeUnit(start); + var endCodeUnit = startCodeUnit; + + if(startCodeUnit === null) return ''; + + if(arguments.length < 2) { + endCodeUnit = str.length; + } else { + for(var i = 0; i < length; i++) endCodeUnit = str.kmwNextChar(endCodeUnit); + } + if(endCodeUnit === null) + return str.substring(startCodeUnit); + else + return str.substring(startCodeUnit, endCodeUnit); } - if(isNaN(indexACodeUnit) || indexACodeUnit === null) indexACodeUnit = 0; - if(isNaN(indexBCodeUnit) || indexBCodeUnit === null) indexBCodeUnit = str.length; - return str.substring(indexACodeUnit, indexBCodeUnit); -} + /** + * Returns the characters in a string between two indexes into the string. + * + * @param {number} indexA The start code point index in the string to + * extract from + * @param {number} indexB The end code point index in the string to + * extract to + * @return {string} The substring as selected by indexA and indexB + */ + String.prototype.kmwSubstring = function(indexA, indexB) + { + var str = String(this),indexACodeUnit,indexBCodeUnit; -/* - Helper functions -*/ - -/** - * Returns the code unit index for the next code point in the string, accounting for - * supplementary pairs - * - * @param {number|null} codeUnitIndex The code unit position to increment - * @return {number|null} The index of the next code point in the string, - * in code units - */ -String.prototype.kmwNextChar = function(codeUnitIndex) { - var str = String(this); + if(typeof(indexB) == 'undefined') + { + indexACodeUnit = str.kmwCodePointToCodeUnit(indexA); + indexBCodeUnit = str.length; + } + else + { + if(indexA > indexB) { var c = indexA; indexA = indexB; indexB = c; } + + indexACodeUnit = str.kmwCodePointToCodeUnit(indexA); + indexBCodeUnit = str.kmwCodePointToCodeUnit(indexB); + } + if(isNaN(indexACodeUnit) || indexACodeUnit === null) indexACodeUnit = 0; + if(isNaN(indexBCodeUnit) || indexBCodeUnit === null) indexBCodeUnit = str.length; - if(codeUnitIndex === null || codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) { - return null; + return str.substring(indexACodeUnit, indexBCodeUnit); } - - var first = str.charCodeAt(codeUnitIndex); - if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) { - var second = str.charCodeAt(codeUnitIndex + 1); - if (second >= 0xDC00 && second <= 0xDFFF) { - if(codeUnitIndex == str.length - 2) { - return null; + + /* + Helper functions + */ + + /** + * Returns the code unit index for the next code point in the string, accounting for + * supplementary pairs + * + * @param {number|null} codeUnitIndex The code unit position to increment + * @return {number|null} The index of the next code point in the string, + * in code units + */ + String.prototype.kmwNextChar = function(codeUnitIndex) { + var str = String(this); + + if(codeUnitIndex === null || codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) { + return null; + } + + var first = str.charCodeAt(codeUnitIndex); + if (first >= 0xD800 && first <= 0xDBFF && str.length > codeUnitIndex + 1) { + var second = str.charCodeAt(codeUnitIndex + 1); + if (second >= 0xDC00 && second <= 0xDFFF) { + if(codeUnitIndex == str.length - 2) { + return null; + } + return codeUnitIndex + 2; } - return codeUnitIndex + 2; } + return codeUnitIndex + 1; } - return codeUnitIndex + 1; -} -/** - * Returns the code unit index for the previous code point in the string, accounting - * for supplementary pairs - * - * @param {number|null} codeUnitIndex The code unit position to decrement - * @return {number|null} The index of the previous code point in the - * string, in code units -*/ -String.prototype.kmwPrevChar = function(codeUnitIndex) { - var str = String(this); - - if(codeUnitIndex == null || codeUnitIndex <= 0 || codeUnitIndex > str.length) { - return null; - } - - var second = str.charCodeAt(codeUnitIndex - 1); - if (second >= 0xDC00 && second <= 0xDFFF && codeUnitIndex > 1) { - var first = str.charCodeAt(codeUnitIndex - 2); - if(first >= 0xD800 && first <= 0xDBFF) { - return codeUnitIndex - 2; + /** + * Returns the code unit index for the previous code point in the string, accounting + * for supplementary pairs + * + * @param {number|null} codeUnitIndex The code unit position to decrement + * @return {number|null} The index of the previous code point in the + * string, in code units + */ + String.prototype.kmwPrevChar = function(codeUnitIndex) { + var str = String(this); + + if(codeUnitIndex == null || codeUnitIndex <= 0 || codeUnitIndex > str.length) { + return null; + } + + var second = str.charCodeAt(codeUnitIndex - 1); + if (second >= 0xDC00 && second <= 0xDFFF && codeUnitIndex > 1) { + var first = str.charCodeAt(codeUnitIndex - 2); + if(first >= 0xD800 && first <= 0xDBFF) { + return codeUnitIndex - 2; + } } + return codeUnitIndex - 1; } - return codeUnitIndex - 1; -} -/** - * Returns the corresponding code unit index to the code point index passed - * - * @param {number|null} codePointIndex A code point index in the string - * @return {number|null} The corresponding code unit index - */ -String.prototype.kmwCodePointToCodeUnit = function(codePointIndex) { - - if(codePointIndex === null) return null; - - var str = String(this); - var codeUnitIndex = 0; - - if(codePointIndex < 0) { - codeUnitIndex = str.length; - for(var i = 0; i > codePointIndex; i--) - codeUnitIndex = str.kmwPrevChar(codeUnitIndex); + /** + * Returns the corresponding code unit index to the code point index passed + * + * @param {number|null} codePointIndex A code point index in the string + * @return {number|null} The corresponding code unit index + */ + String.prototype.kmwCodePointToCodeUnit = function(codePointIndex) { + + if(codePointIndex === null) return null; + + var str = String(this); + var codeUnitIndex = 0; + + if(codePointIndex < 0) { + codeUnitIndex = str.length; + for(var i = 0; i > codePointIndex; i--) + codeUnitIndex = str.kmwPrevChar(codeUnitIndex); + return codeUnitIndex; + } + + if(codePointIndex == str.kmwLength()) return str.length; + + for(var i = 0; i < codePointIndex; i++) + codeUnitIndex = str.kmwNextChar(codeUnitIndex); return codeUnitIndex; } - - if(codePointIndex == str.kmwLength()) return str.length; - - for(var i = 0; i < codePointIndex; i++) - codeUnitIndex = str.kmwNextChar(codeUnitIndex); - return codeUnitIndex; -} -/** - * Returns the corresponding code point index to the code unit index passed - * - * @param {number|null} codeUnitIndex A code unit index in the string - * @return {number|null} The corresponding code point index - */ -String.prototype.kmwCodeUnitToCodePoint = function(codeUnitIndex) { - var str = String(this); - - if(codeUnitIndex === null) - return null; - else if(codeUnitIndex == 0) - return 0; - else if(codeUnitIndex < 0) - return str.substr(codeUnitIndex).kmwLength(); - else - return str.substr(0,codeUnitIndex).kmwLength(); -} + /** + * Returns the corresponding code point index to the code unit index passed + * + * @param {number|null} codeUnitIndex A code unit index in the string + * @return {number|null} The corresponding code point index + */ + String.prototype.kmwCodeUnitToCodePoint = function(codeUnitIndex) { + var str = String(this); -/** - * Returns the character at a the code point index passed - * - * @param {number} codePointIndex A code point index in the string - * @return {string} The corresponding character - */ -String.prototype.kmwCharAt = function(codePointIndex) { - var str = String(this); - - if(codePointIndex >= 0) return str.kmwSubstr(codePointIndex,1); else return ''; -} + if(codeUnitIndex === null) + return null; + else if(codeUnitIndex == 0) + return 0; + else if(codeUnitIndex < 0) + return str.substr(codeUnitIndex).kmwLength(); + else + return str.substr(0,codeUnitIndex).kmwLength(); + } -/** - * String prototype library extensions for basic plane characters, - * to simplify enabling or disabling supplementary plane functionality (I3319) - */ - -/** - * Returns the code unit index for the next code point in the string - * - * @param {number} codeUnitIndex A code point index in the string - * @return {number|null} The corresponding character - */ -String.prototype.kmwBMPNextChar = function(codeUnitIndex) -{ - var str = String(this); - if(codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) { - return null; + /** + * Returns the character at a the code point index passed + * + * @param {number} codePointIndex A code point index in the string + * @return {string} The corresponding character + */ + String.prototype.kmwCharAt = function(codePointIndex) { + var str = String(this); + + if(codePointIndex >= 0) return str.kmwSubstr(codePointIndex,1); else return ''; } - return codeUnitIndex + 1; -} - -/** - * Returns the code unit index for the previous code point in the string - * - * @param {number} codeUnitIndex A code unit index in the string - * @return {number|null} The corresponding character - */ -String.prototype.kmwBMPPrevChar = function(codeUnitIndex) -{ - var str = String(this); - - if(codeUnitIndex <= 0 || codeUnitIndex > str.length) { - return null; + + /** + * String prototype library extensions for basic plane characters, + * to simplify enabling or disabling supplementary plane functionality (I3319) + */ + + /** + * Returns the code unit index for the next code point in the string + * + * @param {number} codeUnitIndex A code point index in the string + * @return {number|null} The corresponding character + */ + String.prototype.kmwBMPNextChar = function(codeUnitIndex) + { + var str = String(this); + if(codeUnitIndex < 0 || codeUnitIndex >= str.length - 1) { + return null; + } + return codeUnitIndex + 1; } - return codeUnitIndex - 1; -} - -/** - * Returns the code unit index for a code point index - * - * @param {number} codePointIndex A code point index in the string - * @return {number} The corresponding character - */ -String.prototype.kmwBMPCodePointToCodeUnit = function(codePointIndex) -{ - return codePointIndex; -} -/** - * Returns the code point index for a code unit index - * - * @param {number} codeUnitIndex A code point index in the string - * @return {number} The corresponding character - */ -String.prototype.kmwBMPCodeUnitToCodePoint = function(codeUnitIndex) -{ - return codeUnitIndex; -} + /** + * Returns the code unit index for the previous code point in the string + * + * @param {number} codeUnitIndex A code unit index in the string + * @return {number|null} The corresponding character + */ + String.prototype.kmwBMPPrevChar = function(codeUnitIndex) + { + var str = String(this); -/** - * Returns the length of a BMP string - * - * @return {number} The length in code points - */ -String.prototype.kmwBMPLength = function() -{ - var str = String(this); - return str.length; -} + if(codeUnitIndex <= 0 || codeUnitIndex > str.length) { + return null; + } + return codeUnitIndex - 1; + } + /** + * Returns the code unit index for a code point index + * + * @param {number} codePointIndex A code point index in the string + * @return {number} The corresponding character + */ + String.prototype.kmwBMPCodePointToCodeUnit = function(codePointIndex) + { + return codePointIndex; + } -/** - * Returns a substring - * - * @param {number} n - * @param {number=} ln - * @return {string} - */ -String.prototype.kmwBMPSubstr = function(n,ln?) -{ - var str=String(this); - if(n > -1) - return str.substr(n,ln); - else - return str.substr(str.length+n,-n); -} + /** + * Returns the code point index for a code unit index + * + * @param {number} codeUnitIndex A code point index in the string + * @return {number} The corresponding character + */ + String.prototype.kmwBMPCodeUnitToCodePoint = function(codeUnitIndex) + { + return codeUnitIndex; + } -/** - * Enable or disable supplementary plane string handling - * - * @param {boolean} bEnable - */ -String.kmwEnableSupplementaryPlane = function(bEnable) -{ - var p=String.prototype; - String._kmwFromCharCode = bEnable ? String.kmwFromCharCode : String.fromCharCode; - p._kmwCharAt = bEnable ? p.kmwCharAt : p.charAt; - p._kmwCharCodeAt = bEnable ? p.kmwCharCodeAt : p.charCodeAt; - p._kmwIndexOf = bEnable ? p.kmwIndexOf :p.indexOf; - p._kmwLastIndexOf = bEnable ? p.kmwLastIndexOf : p.lastIndexOf ; - p._kmwSlice = bEnable ? p.kmwSlice : p.slice; - p._kmwSubstring = bEnable ? p.kmwSubstring : p.substring; - p._kmwSubstr = bEnable ? p.kmwSubstr : p.kmwBMPSubstr; - p._kmwLength = bEnable ? p.kmwLength : p.kmwBMPLength; - p._kmwNextChar = bEnable ? p.kmwNextChar : p.kmwBMPNextChar; - p._kmwPrevChar = bEnable ? p.kmwPrevChar : p.kmwBMPPrevChar; - p._kmwCodePointToCodeUnit = bEnable ? p.kmwCodePointToCodeUnit : p.kmwBMPCodePointToCodeUnit; - p._kmwCodeUnitToCodePoint = bEnable ? p.kmwCodeUnitToCodePoint : p.kmwBMPCodeUnitToCodePoint; + /** + * Returns the length of a BMP string + * + * @return {number} The length in code points + */ + String.prototype.kmwBMPLength = function() + { + var str = String(this); + return str.length; + } + + /** + * Returns a substring + * + * @param {number} n + * @param {number=} ln + * @return {string} + */ + String.prototype.kmwBMPSubstr = function(n,ln?) + { + var str=String(this); + if(n > -1) + return str.substr(n,ln); + else + return str.substr(str.length+n,-n); + } + + /** + * Enable or disable supplementary plane string handling + * + * @param {boolean} bEnable + */ + String.kmwEnableSupplementaryPlane = function(bEnable) + { + var p=String.prototype; + String._kmwFromCharCode = bEnable ? String.kmwFromCharCode : String.fromCharCode; + p._kmwCharAt = bEnable ? p.kmwCharAt : p.charAt; + p._kmwCharCodeAt = bEnable ? p.kmwCharCodeAt : p.charCodeAt; + p._kmwIndexOf = bEnable ? p.kmwIndexOf :p.indexOf; + p._kmwLastIndexOf = bEnable ? p.kmwLastIndexOf : p.lastIndexOf ; + p._kmwSlice = bEnable ? p.kmwSlice : p.slice; + p._kmwSubstring = bEnable ? p.kmwSubstring : p.substring; + p._kmwSubstr = bEnable ? p.kmwSubstr : p.kmwBMPSubstr; + p._kmwLength = bEnable ? p.kmwLength : p.kmwBMPLength; + p._kmwNextChar = bEnable ? p.kmwNextChar : p.kmwBMPNextChar; + p._kmwPrevChar = bEnable ? p.kmwPrevChar : p.kmwBMPPrevChar; + p._kmwCodePointToCodeUnit = bEnable ? p.kmwCodePointToCodeUnit : p.kmwBMPCodePointToCodeUnit; + p._kmwCodeUnitToCodePoint = bEnable ? p.kmwCodeUnitToCodePoint : p.kmwBMPCodeUnitToCodePoint; + } + + // Ensure that _all_ String extensions are established, even if disabled by default. + if(!String._kmwFromCharCode) { + String.kmwEnableSupplementaryPlane(false); + } } +// For side-effect imports: +extendString(); \ No newline at end of file diff --git a/common/web/utils/src/managedPromise.ts b/common/web/utils/src/managedPromise.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac616b9a350f0d527af2981859a623d2f77ea106 --- /dev/null +++ b/common/web/utils/src/managedPromise.ts @@ -0,0 +1,64 @@ +type ResolveSignature = (value: Type | PromiseLike) => void; +type RejectSignature = (reason?: any) => void; + +export default class ManagedPromise { + public resolve: ResolveSignature; + public reject: RejectSignature; + + private _hasResolved: boolean = false; + private _hasRejected: boolean = false; + + public get hasResolved(): boolean { + return this._hasResolved; + } + + public get hasRejected(): boolean { + return this._hasRejected; + } + + public get hasFinalized(): boolean { + return this.hasResolved || this.hasRejected; + } + + private _promise: Promise; + + constructor(); + constructor(executor: (resolve: ResolveSignature, reject: RejectSignature) => Type); + constructor(executor?: (resolve: ResolveSignature, reject: RejectSignature) => Type) { + this._promise = new Promise((resolve, reject) => { + this.resolve = (value) => { + this._hasResolved = true; + resolve(value); + }; + + this.reject = (reason) => { + this._hasRejected = true; + reject(reason); + }; + + if(executor) { + executor(this.resolve, this.reject); + } + }); + } + + // Cannot actually extend the Promise class in ES5; attempt to use it will throw errors. + // So, we just implement a Promise-like interface. + + then(onfulfilled?: ((value: Type) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise { + return this._promise.then(onfulfilled, onrejected); + } + + catch(onrejected?: (reason: any) => PromiseLike): Promise { + return this._promise.catch(onrejected); + } + + finally(onfinally?: () => void): Promise { + return this._promise.finally(onfinally); + } + + // And for things that actually need to provide something typed to Promise... well... + get corePromise(): Promise { + return this._promise; + } +} \ No newline at end of file diff --git a/common/web/utils/src/test/managedPromise.js b/common/web/utils/src/test/managedPromise.js new file mode 100644 index 0000000000000000000000000000000000000000..7d2cfaf816fa47fa6b7af415fd362e977c79224f --- /dev/null +++ b/common/web/utils/src/test/managedPromise.js @@ -0,0 +1,49 @@ +import { assert } from 'chai'; + +import { ManagedPromise } from '@keymanapp/web-utils'; + +describe("ManagedPromise", () => { + it(' resolution', async () => { + let promise = new ManagedPromise(); + + promise.resolve(); + + await promise.corePromise; + }); + + it(' resolution', async () => { + let promise = new ManagedPromise(); + + promise.resolve("foobar"); + + let text = await promise.corePromise; + + assert.equal(text, "foobar"); + }); + + it('rejection', async () => { + let promise = new ManagedPromise(); + + promise.reject(new Error("foobar")); + + try { + await promise.corePromise; + assert.fail(); + } catch (err) { + assert.equal(err.message, "foobar"); + } + }); + + it('then()', async () => { + let promise = new ManagedPromise(); + + let thenPromise = promise.then((val) => { + return {text: val + "bar"} + }); + + promise.resolve("foo"); + let val = await thenPromise; + + assert.deepEqual(val, {text: "foobar"}); + }); +}); \ No newline at end of file diff --git a/common/web/keyboard-processor/tests/cases/versions.js b/common/web/utils/src/test/versions.js similarity index 59% rename from common/web/keyboard-processor/tests/cases/versions.js rename to common/web/utils/src/test/versions.js index 2e7f6ae142cd1c4c6fb6a007e4928567d97e511e..36ed51cbacddd5f611196ecc8fead03958072180 100644 --- a/common/web/keyboard-processor/tests/cases/versions.js +++ b/common/web/utils/src/test/versions.js @@ -1,33 +1,31 @@ -var assert = require('chai').assert; -let KeyboardProcessor = require('../../build/index.bundled.js'); +import { assert } from 'chai'; -// Required initialization setup. -global.com = KeyboardProcessor.com; // exports all keyboard-processor namespacing. +import { Version } from '@keymanapp/web-utils'; describe('Version Logic', function() { it('Should provide a default, fallback value when nothing is specified', function() { - var fallback = new com.keyman.utils.Version(undefined); - assert.isTrue(fallback.equals(com.keyman.utils.Version.DEVELOPER_VERSION_FALLBACK)); + var fallback = new Version(undefined); + assert.isTrue(fallback.equals(Version.DEVELOPER_VERSION_FALLBACK)); }); it('Should properly process a simple major.minor version string.', function() { - var version = new com.keyman.utils.Version("1.2"); + var version = new Version("1.2"); assert.equal(version.major, 1); assert.equal(version.minor, 2); }); it('Should handle long/deep version specifications.', function() { - var version = new com.keyman.utils.Version("1.2.3.4.5.6"); + var version = new Version("1.2.3.4.5.6"); assert.equal(version.components.length, 6); assert.equal(version.major, 1); assert.equal(version.minor, 2); }); it('Should properly compare two versions.', function() { - var v9_0_1 = new com.keyman.utils.Version("9.0.1"); - var v9_1_0 = new com.keyman.utils.Version("9.1.0"); - var v10_0 = new com.keyman.utils.Version("10.0"); - var v10_0_0 = new com.keyman.utils.Version("10.0.0"); + var v9_0_1 = new Version("9.0.1"); + var v9_1_0 = new Version("9.1.0"); + var v10_0 = new Version("10.0"); + var v10_0_0 = new Version("10.0.0"); // "Precede" checks assert.equal(v9_0_1.compareTo(v9_1_0), -1); diff --git a/common/web/utils/src/version.ts b/common/web/utils/src/version.ts index ab392938c1349ce7b2819b79c9abfafcfab60825..27e865ee1e8a66ccc3a14dc034d5b2c0becb62d8 100644 --- a/common/web/utils/src/version.ts +++ b/common/web/utils/src/version.ts @@ -1,112 +1,109 @@ -// Ensure that this class contains no reference into core KMW code - it is referenced -// by components intended to be modular and possible to separate from core KMW. - -namespace com.keyman.utils { - // Dotted-decimal version - export class Version { - public static readonly CURRENT = new Version(com.keyman.KEYMAN_VERSION.VERSION_RELEASE); - - // Represents a default version value for keyboards compiled before this was compiled into keyboards. - // The exact version is unknown at this point, but the value is "good enough" for what we need. - public static readonly DEVELOPER_VERSION_FALLBACK = new Version([9, 0, 0]); - - // For 12.0, the old default behavior of adding missing keycaps to the default layers was removed, - // as it results in unexpected, bug-like behavior for keyboard designers when it is unwanted. - public static readonly NO_DEFAULT_KEYCAPS = new Version([12, 0]); - - public static readonly MAC_POSSIBLE_IPAD_ALIAS = new Version([10, 15]); - - private readonly components: number[] - - /** - * Parses version information, preparing it for use in comparisons. - * @param text Either a string representing a version number (ex: "9.0.0") or an array representing - * its components (ex: [9, 0, 0]). - */ - constructor(text: String | number[]) { - // If a keyboard doesn't specify a version, use the DEVELOPER_VERSION_FALLBACK values. - if(text === undefined || text === null) { - this.components = [].concat(Version.DEVELOPER_VERSION_FALLBACK.components); +import KEYMAN_VERSION from "@keymanapp/keyman-version"; + +// Dotted-decimal version +export default class Version { + public static readonly CURRENT = new Version(KEYMAN_VERSION.VERSION_RELEASE); + + // Represents a default version value for keyboards compiled before this was compiled into keyboards. + // The exact version is unknown at this point, but the value is "good enough" for what we need. + public static readonly DEVELOPER_VERSION_FALLBACK = new Version([9, 0, 0]); + + // For 12.0, the old default behavior of adding missing keycaps to the default layers was removed, + // as it results in unexpected, bug-like behavior for keyboard designers when it is unwanted. + public static readonly NO_DEFAULT_KEYCAPS = new Version([12, 0]); + + public static readonly MAC_POSSIBLE_IPAD_ALIAS = new Version([10, 15]); + + private readonly components: number[] + + /** + * Parses version information, preparing it for use in comparisons. + * @param text Either a string representing a version number (ex: "9.0.0") or an array representing + * its components (ex: [9, 0, 0]). + */ + constructor(text: String | number[]) { + // If a keyboard doesn't specify a version, use the DEVELOPER_VERSION_FALLBACK values. + if(text === undefined || text === null) { + this.components = [].concat(Version.DEVELOPER_VERSION_FALLBACK.components); + return; + } + + if(Array.isArray(text)) { + let components = text as number[]; + if(components.length < 2) { + throw new Error("Version string must have at least a major and minor component!"); + } else { + this.components = [].concat(components); return; } + } - if(Array.isArray(text)) { - let components = text as number[]; - if(components.length < 2) { - throw new Error("Version string must have at least a major and minor component!"); - } else { - this.components = [].concat(components); - return; - } - } + // else, standard constructor path. + let parts = text.split('.'); + let componentArray: number[] = []; - // else, standard constructor path. - let parts = text.split('.'); - let componentArray: number[] = []; + if(parts.length < 2) { + throw new Error("Version string must have at least a major and minor component!"); + } - if(parts.length < 2) { - throw new Error("Version string must have at least a major and minor component!"); + for(let i=0; i < parts.length; i++) { + let value = parseInt(parts[i], 10); + if(isNaN(value)) { + throw new Error("Version string components must be numerical!"); } - for(let i=0; i < parts.length; i++) { - let value = parseInt(parts[i], 10); - if(isNaN(value)) { - throw new Error("Version string components must be numerical!"); - } + componentArray.push(value); + } - componentArray.push(value); - } + this.components = componentArray; + } - this.components = componentArray; - } + get major(): number { + return this.components[0]; + } - get major(): number { - return this.components[0]; - } + get minor(): number { + return this.components[1]; + } - get minor(): number { - return this.components[1]; - } + toString(): string { + return this.components.join('.'); + } - toString(): string { - return this.components.join('.'); - } + toJSON(): string { + return this.toString(); + } - toJSON(): string { - return this.toString(); - } + equals(other: Version): boolean { + return this.compareTo(other) == 0; + } - equals(other: Version): boolean { - return this.compareTo(other) == 0; - } + precedes(other: Version): boolean { + return this.compareTo(other) < 0; + } - precedes(other: Version): boolean { - return this.compareTo(other) < 0; - } + compareTo(other: Version): number { + // If the version info depth differs, we need a flag to indicate which instance is shorter. + var isShorter: boolean = this.components.length < other.components.length; + var maxDepth: number = (this.components.length < other.components.length) ? this.components.length : other.components.length; - compareTo(other: Version): number { - // If the version info depth differs, we need a flag to indicate which instance is shorter. - var isShorter: boolean = this.components.length < other.components.length; - var maxDepth: number = (this.components.length < other.components.length) ? this.components.length : other.components.length; - - var i: number; - for(i = 0; i < maxDepth; i++) { - let delta = this.components[i] - other.components[i]; - if(delta != 0) { - return delta; - } + var i: number; + for(i = 0; i < maxDepth; i++) { + let delta = this.components[i] - other.components[i]; + if(delta != 0) { + return delta; } + } - var longList = isShorter ? other.components : this.components; - do { - if(longList[i] > 0) { - return isShorter ? -1 : 1; - } - i++; - } while (i < longList.length); + var longList = isShorter ? other.components : this.components; + do { + if(longList[i] > 0) { + return isShorter ? -1 : 1; + } + i++; + } while (i < longList.length); - // Equal. - return 0; - } + // Equal. + return 0; } } \ No newline at end of file diff --git a/common/web/utils/tsconfig.json b/common/web/utils/tsconfig.json index 4bdd955c5fd83d1696900a4260a60d9000111e89..cb87a6a7c4a3bfb5d57ea460afe912e1a2c80caf 100644 --- a/common/web/utils/tsconfig.json +++ b/common/web/utils/tsconfig.json @@ -1,20 +1,18 @@ { - "extends": "../../../tsconfig-base.json", + "extends": "../tsconfig.kmw-main-base.json", "compilerOptions": { - "allowJs": true, - "module": "none", - "inlineSources": true, - "sourceMap": true, - "declaration": true, - "target": "es5", - "types": ["node"], - "lib": ["es6"], - "outFile": "build/index.js" + "tsBuildInfoFile": "./build/obj/tsconfig.tsbuildinfo", + "baseUrl": "./", + "outDir": "./build/obj/", + "rootDir": "./src" }, "references": [ { "path": "../keyman-version"} ], "include": [ "src/*.ts" + ], + "exclude": [ + "src/test/**/*.js" ] } diff --git a/common/windows/cpp/include/legacy_kmx_file.h b/common/windows/cpp/include/legacy_kmx_file.h index ae18543312f2303575ac276e1521c7bff9c20eb7..d961250912f78fc5bab8726af0d652f6b6db86e6 100644 --- a/common/windows/cpp/include/legacy_kmx_file.h +++ b/common/windows/cpp/include/legacy_kmx_file.h @@ -409,7 +409,6 @@ typedef COMP_GROUP *PCOMP_GROUP; typedef struct _COMPILER_OPTIONS { DWORD dwSize; BOOL ShouldAddCompilerVersion; - BOOL UseKmcmpLib; } COMPILER_OPTIONS; typedef COMPILER_OPTIONS *PCOMPILER_OPTIONS; diff --git a/common/windows/delphi/Defines.mak b/common/windows/delphi/Defines.mak index 2450586e934195325ac0432ab96ba2e2b006f6d5..50210e5fea15ff4c799b67573cd28343e18bbae1 100644 --- a/common/windows/delphi/Defines.mak +++ b/common/windows/delphi/Defines.mak @@ -22,9 +22,13 @@ COMMON_BIN=$(KEYMAN_ROOT)\windows\bin # INCLUDE=$(ROOT)\src\global\inc;$(INCLUDE) !IFDEF DEBUG +GO_FAST=1 MAKEFLAG_DEBUG="DEBUG=$(DEBUG)" DELPHI_MSBUILD_FLAG_DEBUG="/p:Config=Debug" !ELSE +!IFDEF TEAMCITY_PR_NUMBER +GO_FAST=1 +!ENDIF DELPHI_MSBUILD_FLAG_DEBUG="/p:Config=Release" !ENDIF @@ -164,7 +168,11 @@ MSBUILD_CLEAN=/t:Clean /p:Configuration=Release COPY=copy ISXBUILD=C:\PROGRA~1\INSTALLSHIELD\Express\System\IsExpCmdBld WZZIPPATH="C:\program files\7-zip\7z.exe" -WZZIP=$(WZZIPPATH) a +!IFDEF GO_FAST +WZZIP=$(WZZIPPATH) a -mx1 +!ELSE +WZZIP=$(WZZIPPATH) a -mx9 +!ENDIF WZUNZIP=$(WZZIPPATH) e # we are using cmd /c because tds2dbg is failing on direct execution @@ -177,12 +185,22 @@ WIXPATH="c:\program files (x86)\WiX Toolset v3.11\bin" WIXCANDLE=$(WIXPATH)\candle.exe -wx -nologo !IFDEF LINT -WIXLIGHT=$(WIXPATH)\light.exe -wx -nologo +WIXLIGHTLINT= !ELSE # we suppress ICE82 because it reports spurious errors with merge module keymanengine to do with duplicate sequence numbers. Safely ignored. -WIXLIGHT=$(WIXPATH)\light.exe -wx -nologo -sice:ICE82 -sice:ICE80 +WIXLIGHTLINT= -sice:ICE82 -sice:ICE80 !ENDIF +!IFDEF GO_FAST +# for debug builds, we turn off compression because it is so hideously slow +# for test builds, we also turn off compression +WIXLIGHTCOMPRESSION=-dcl:none +!ELSE +WIXLIGHTCOMPRESSION=-dcl:high +!ENDIF + +WIXLIGHT=$(WIXPATH)\light.exe -wx -nologo $(WIXLIGHTLINT) $(WIXLIGHTCOMPRESSION) + WIXLIT=$(WIXPATH)\lit.exe -wx -nologo WIXHEAT=$(WIXPATH)\heat.exe diff --git a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas index e4cec5d252289031342665ea8848fe07c352cd18..bffcd952d2a044689a2ffefa56ec85586e0b4c64 100644 --- a/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas +++ b/common/windows/delphi/chromium/Keyman.UI.UframeCEFHost.pas @@ -437,6 +437,11 @@ begin URL.StartsWith('http://127.0.0.1/'); end; +function IsPDFURL(URL: string): Boolean; +begin + Result := LowerCase(URL).EndsWith('.pdf'); +end; + procedure TframeCEFHost.Handle_CEF_BEFOREBROWSE(var message: TMessage); var params: TStringList; @@ -464,7 +469,7 @@ begin FOnBeforeBrowse(Self, url, isPopup, wasHandled); end; - if FShouldOpenRemoteUrlsInBrowser and not IsLocalURL(URL) then + if FShouldOpenRemoteUrlsInBrowser and (not IsLocalURL(URL) or IsPDFURL(URL)) then begin {$MESSAGE HINT 'Refactor how remote URLs are handled'} // TODO: refactor links @@ -534,7 +539,7 @@ begin else begin // Use OnBeforeBrowse for other URLs - if not Handled and FShouldOpenRemoteUrlsInBrowser and not IsLocalURL(URL) then + if not Handled and FShouldOpenRemoteUrlsInBrowser and (not IsLocalURL(URL) or IsPDFURL(URL)) then Handled := True; params := TStringList.Create; diff --git a/common/windows/delphi/tools/build_standards_data/Keyman.System.BuildLangTags.pas b/common/windows/delphi/tools/build_standards_data/Keyman.System.BuildLangTags.pas index aecb1a9dcba76a14aa7bf64f8a72088568723100..037a78fd83d862692ecffc5394380160c8cea126 100644 --- a/common/windows/delphi/tools/build_standards_data/Keyman.System.BuildLangTags.pas +++ b/common/windows/delphi/tools/build_standards_data/Keyman.System.BuildLangTags.pas @@ -7,6 +7,10 @@ type class procedure Build(const LangTagsFile, DestinationFile: string); end; +const + { Index of the _version record in the langtags.json file } + IndexOfVersionRecord = 3; + implementation uses @@ -85,7 +89,7 @@ begin end; try - for i := 0 to 2 do + for i := 0 to IndexOfVersionRecord do begin if (FLangTags.Items[i] as TJSONObject).Values['tag'].Value = '_version' then begin @@ -153,11 +157,11 @@ begin FResult.Add(''); FResult.Add('{ TLangTagsMap }'); FResult.Add(''); - FResult.Add(Format('const CLangTags: array[0..%d] of TLangTag = (', [FLangTags.Count - 4])); + FResult.Add(Format('const CLangTags: array[0..%d] of TLangTag = (', [FLangTags.Count - IndexOfVersionRecord - 2])); comma := ','; FTagMapCount := 0; - for i := 3 to FLangTags.Count - 1 do + for i := IndexOfVersionRecord + 1 to FLangTags.Count - 1 do begin if i = FLangTags.Count - 1 then comma := ''; FLangTag := FLangTags.Items[i] as TJSONObject; @@ -194,7 +198,7 @@ begin FResult.Add(Format('const CAllTags: array[0..%d] of TStringMap = (', [FTagMapCount-1])); comma := ''; - for i := 3 to FLangTags.Count - 1 do + for i := IndexOfVersionRecord + 1 to FLangTags.Count - 1 do begin FLangTag := FLangTags.Items[i] as TJSONObject; diff --git a/core/commands.inc.sh b/core/commands.inc.sh index 2e5fb9e7b9ffda235b6d665e250d27e69fc760f1..6916b61a9d97f887e7e7aeab62926a46c3daccd2 100644 --- a/core/commands.inc.sh +++ b/core/commands.inc.sh @@ -1,4 +1,7 @@ -#!/usr/bin/env bash +# shellcheck shell=bash +# no hashbang for .inc.sh + +. "$KEYMAN_ROOT/resources/locate_emscripten.inc.sh" # ---------------------------------------------------------------------------- # clean @@ -114,34 +117,6 @@ do_uninstall() { # utility functions # ---------------------------------------------------------------------------- -# -# We don't want to rely on emcc being on the path, because Emscripten puts far -# too many things onto the path (in particular for us, node). -# -# The following comment suggests that we don't need emcc on the path. -# https://github.com/emscripten-core/emscripten/issues/4848#issuecomment-1097357775 -# -# So we try and locate emcc in common locations ourselves. The search pattern -# is: -# -# 1. Look for $EMSCRIPTEN_BASE (our primary emscripten variable), which should -# point to the folder that emcc is located in -# 2. Look for $EMCC which should point to the emcc executable -# 3. Look for emcc on the path -# -locate_emscripten() { - if [[ -z ${EMSCRIPTEN_BASE+x} ]]; then - if [[ -z ${EMCC+x} ]]; then - local EMCC=`which emcc` - [[ -z $EMCC ]] && builder_die "locate_emscripten: Could not locate emscripten (emcc) on the path or with \$EMCC or \$EMSCRIPTEN_BASE" - fi - [[ -x $EMCC ]] || builder_die "locate_emscripten: Variable EMCC ($EMCC) does not point to a valid executable emcc" - EMSCRIPTEN_BASE="$(dirname "$EMCC")" - fi - - [[ -x ${EMSCRIPTEN_BASE}/emcc ]] || builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) does not point to emcc's folder" -} - build_meson_cross_file_for_wasm() { if [ $BUILDER_OS == win ]; then local R=$(cygpath -w $(echo $EMSCRIPTEN_BASE) | sed 's_\\_\\\\_g') @@ -159,6 +134,8 @@ build_meson_cross_file_for_wasm() { # This link.exe is not a linker. # You may need to reorder entries to your %PATH% variable to resolve this. # +# TODO: is this still required with meson 1.0? +# cleanup_visual_studio_path() { local _split_path _new_path="" diff --git a/core/doc/hotdoc.json b/core/doc/hotdoc.json index 61a0028fb0bd464c8e01ff40a8d4b22a32b41d51..4ece50d0f52c505abb5a21b46c5da2e8199d9227 100644 --- a/core/doc/hotdoc.json +++ b/core/doc/hotdoc.json @@ -1,10 +1,14 @@ { "project_name": "@project_name@", "project_version": "@project_version@", - "sitemap": "../../doc/sitemap.txt", - "index": "../../doc/markdown_files/index.md", + "sitemap": "@doc_dir@/sitemap.txt", + "index": "@doc_dir@/markdown_files/index.md", "c_sources": [ - "../include/keyboardprocessor.h" + "@include_dir@/keyman/keyboardprocessor.h" + ], + "c_include_directories": [ + "@include_dir@", + "@doc_dir@/../../common/include" ], "c_smart_index" : true, "output": ".", diff --git a/core/doc/meson.build b/core/doc/meson.build index 2772819ce7ff8b54cea5774e59f13cbea7802a00..01fcb5c98ad60250fb7e3ac3561c5ac5d9791a22 100644 --- a/core/doc/meson.build +++ b/core/doc/meson.build @@ -12,18 +12,20 @@ if hotdoc.found() cfg = configuration_data() cfg.set('project_name', meson.project_name()) cfg.set('project_version', meson.project_version()) + cfg.set('doc_dir', meson.current_source_dir()) + cfg.set('include_dir', meson.current_source_dir() / '../include') configure_file(input: 'hotdoc.json', output: 'hotdoc.json', configuration: cfg) deps = files( - '../include/keyman/keyboardprocessor.h.in', + '../include/keyman/keyboardprocessor.h', '../src/jsonpp.hpp', '../src/utfcodec.hpp' ) docs = custom_target('docs', output: ['html'], - input: ['sitemap.txt', 'markdown_files/index.md'], + input: ['sitemap.txt', 'markdown_files/index.md', deps], command: [hotdoc, '--verbose', '--conf-file=doc/hotdoc.json', '--output=doc', 'run'], depend_files: deps, install: true, diff --git a/core/include/keyman/keyboardprocessor.h b/core/include/keyman/keyboardprocessor.h index 3513cdede2e88223e47ada46547975b3d3bc856a..429a8ae6cbf9118850a12460db2a289c262920f1 100644 --- a/core/include/keyman/keyboardprocessor.h +++ b/core/include/keyman/keyboardprocessor.h @@ -779,7 +779,7 @@ km_kbp_keyboard_get_key_list(km_kbp_keyboard const *keyboard, km_kbp_keyboard_key **out); -/* +/** ``` ### `km_kbp_keyboard_key_list_dispose` ##### Description: @@ -794,28 +794,41 @@ returned by `km_kbp_keyboard_get_key_list`. KMN_API void km_kbp_keyboard_key_list_dispose(km_kbp_keyboard_key *key_list); - /** - * Returns the list of IMX libraries and function names that are referenced by - * the keyboard. The matching dispose call needs to be called to free the memory. + * km_kbp_keyboard_get_imx_list: + * + * Returns: the list of IMX libraries and function names that are referenced by + * the keyboard.The matching dispose call needs to be called to free the memory. */ KMN_API -km_kbp_status km_kbp_keyboard_get_imx_list(km_kbp_keyboard const *keyboard, km_kbp_keyboard_imx** imx_list); +km_kbp_status km_kbp_keyboard_get_imx_list(km_kbp_keyboard const *keyboard, km_kbp_keyboard_imx **imx_list); /** + * km_kbp_keyboard_imx_list_dispose: + * * Disposes of the IMX list + * + * Returns: -- */ KMN_API void km_kbp_keyboard_imx_list_dispose(km_kbp_keyboard_imx *imx_list); /** + * km_kbp_state_imx_register_callback: + * * Register the IMX callback endpoint for the client. + * + * Returns: -- */ KMN_API void km_kbp_state_imx_register_callback(km_kbp_state *state, km_kbp_keyboard_imx_platform imx_callback, void *callback_object); /** + * km_kbp_state_imx_deregister_callback: + * * De-register IMX callback endpoint for the client. + * + * Returns: -- */ KMN_API void km_kbp_state_imx_deregister_callback(km_kbp_state *state); @@ -1042,6 +1055,8 @@ enum km_kbp_tech_value { }; /** + * km_kbp_event_flags: + * * Bit flags to be used with the event_flags parameter of km_kbp_process_event */ enum km_kbp_event_flags { @@ -1162,10 +1177,8 @@ km_kbp_event( ); enum km_kbp_event_code { - /** - * A keyboard has been activated by the user. The processor may use this - * event, for example, to switch caps lock state or provide other UX. - */ + // A keyboard has been activated by the user. The processor may use this + // event, for example, to switch caps lock state or provide other UX. KM_KBP_EVENT_KEYBOARD_ACTIVATED = 1, //future: KM_KBP_EVENT_KEYBOARD_DEACTIVATED = 2, }; diff --git a/core/include/ldml/keyboardprocessor_ldml.h b/core/include/ldml/keyboardprocessor_ldml.h index 2922a8fb033995a7184f7804812437957d4336b6..262a18e6f05b49f89810a577bcb35ac458952d77 100644 --- a/core/include/ldml/keyboardprocessor_ldml.h +++ b/core/include/ldml/keyboardprocessor_ldml.h @@ -22,11 +22,14 @@ #define LDML_CLDR_VERSION_TECHPREVIEW "techpreview" #define LDML_ELEM_FLAGS_ORDER_BITSHIFT 0x10 #define LDML_ELEM_FLAGS_ORDER_MASK 0xFF0000 -#define LDML_ELEM_FLAGS_PREBASE 0x4 -#define LDML_ELEM_FLAGS_TERTIARY_BASE 0x2 +#define LDML_ELEM_FLAGS_PREBASE 0x8 +#define LDML_ELEM_FLAGS_TERTIARY_BASE 0x4 #define LDML_ELEM_FLAGS_TERTIARY_BITSHIFT 0x18 #define LDML_ELEM_FLAGS_TERTIARY_MASK 0xFF000000 -#define LDML_ELEM_FLAGS_UNICODE_SET 0x1 +#define LDML_ELEM_FLAGS_TYPE 0x3 +#define LDML_ELEM_FLAGS_TYPE_CHAR 0x0 +#define LDML_ELEM_FLAGS_TYPE_STR 0x1 +#define LDML_ELEM_FLAGS_TYPE_USET 0x2 #define LDML_FINL_FLAGS_ERROR 0x1 #define LDML_KEYS_FLICK_FLAGS_EXTEND 0x1 #define LDML_KEYS_KEY_FLAGS_EXTEND 0x1 @@ -75,16 +78,26 @@ #define LDML_LENGTH_META 0x24 #define LDML_LENGTH_NAME 0xC #define LDML_LENGTH_NAME_ITEM 0x4 -#define LDML_LENGTH_ORDR 0xC -#define LDML_LENGTH_ORDR_ITEM 0x8 #define LDML_LENGTH_SECT 0x10 #define LDML_LENGTH_SECT_ITEM 0x8 #define LDML_LENGTH_STRS 0xC #define LDML_LENGTH_STRS_ITEM 0x8 -#define LDML_LENGTH_TRAN 0xC -#define LDML_LENGTH_TRAN_ITEM 0x10 +#define LDML_LENGTH_TRAN 0x14 +#define LDML_LENGTH_TRAN_GROUP 0xC +#define LDML_LENGTH_TRAN_REORDER 0x8 +#define LDML_LENGTH_TRAN_TRANSFORM 0x10 +#define LDML_LENGTH_USET 0x10 +#define LDML_LENGTH_USET_RANGE 0x8 +#define LDML_LENGTH_USET_USET 0xC +#define LDML_LENGTH_VARS 0x10 +#define LDML_LENGTH_VARS_ITEM 0x10 #define LDML_LENGTH_VKEY 0xC #define LDML_LENGTH_VKEY_ITEM 0x8 +#define LDML_MARKER_ANY_INDEX 0xFFFF +#define LDML_MARKER_MAX_COUNT 0xFFFD +#define LDML_MARKER_MAX_INDEX 0xFFFE +#define LDML_MARKER_MIN_INDEX 0x1 +#define LDML_MARKER_SENTINEL 0xFFFF #define LDML_META_SETTINGS_FALLBACK_OMIT 0x1 #define LDML_META_SETTINGS_TRANSFORMFAILURE_OMIT 0x2 #define LDML_META_SETTINGS_TRANSFORMPARTIAL_HIDE 0x4 @@ -94,8 +107,6 @@ #define LDML_SECTIONNAME_DISP "disp" #define LDML_SECTIONID_ELEM 0x6D656C65 /* "elem" */ #define LDML_SECTIONNAME_ELEM "elem" -#define LDML_SECTIONID_FINL 0x6C6E6966 /* "finl" */ -#define LDML_SECTIONNAME_FINL "finl" #define LDML_SECTIONID_KEYS 0x7379656B /* "keys" */ #define LDML_SECTIONNAME_KEYS "keys" #define LDML_SECTIONID_LAYR 0x7279616C /* "layr" */ @@ -108,15 +119,22 @@ #define LDML_SECTIONNAME_META "meta" #define LDML_SECTIONID_NAME 0x656D616E /* "name" */ #define LDML_SECTIONNAME_NAME "name" -#define LDML_SECTIONID_ORDR 0x7264726F /* "ordr" */ -#define LDML_SECTIONNAME_ORDR "ordr" #define LDML_SECTIONID_SECT 0x74636573 /* "sect" */ #define LDML_SECTIONNAME_SECT "sect" #define LDML_SECTIONID_STRS 0x73727473 /* "strs" */ #define LDML_SECTIONNAME_STRS "strs" #define LDML_SECTIONID_TRAN 0x6E617274 /* "tran" */ #define LDML_SECTIONNAME_TRAN "tran" +#define LDML_SECTIONID_USET 0x74657375 /* "uset" */ +#define LDML_SECTIONNAME_USET "uset" +#define LDML_SECTIONID_VARS 0x73726176 /* "vars" */ +#define LDML_SECTIONNAME_VARS "vars" #define LDML_SECTIONID_VKEY 0x79656B76 /* "vkey" */ #define LDML_SECTIONNAME_VKEY "vkey" #define LDML_TRAN_FLAGS_ERROR 0x1 +#define LDML_TRAN_GROUP_TYPE_REORDER 0x1 +#define LDML_TRAN_GROUP_TYPE_TRANSFORM 0x0 +#define LDML_VARS_ENTRY_TYPE_SET 0x1 +#define LDML_VARS_ENTRY_TYPE_STRING 0x0 +#define LDML_VARS_ENTRY_TYPE_UNICODESET 0x2 #define LDML_VERSION "1.0" diff --git a/core/include/ldml/keyboardprocessor_ldml.ts b/core/include/ldml/keyboardprocessor_ldml.ts index db9ef7a41ff4637177512b4f71a4c37f09cf1b27..fdfc63da18013aeec714b20e992e1cffe9e97d8e 100644 --- a/core/include/ldml/keyboardprocessor_ldml.ts +++ b/core/include/ldml/keyboardprocessor_ldml.ts @@ -28,16 +28,16 @@ export type SectionIdent = 'bksp' | 'disp' | 'elem' | - 'finl' | 'keys' | 'layr' | 'list' | 'loca' | 'meta' | 'name' | - 'ordr' | 'strs' | 'tran' | + 'uset' | + 'vars' | 'vkey'; @@ -140,13 +140,17 @@ class Constants { readonly length_elem_item_element = 8; /** - * bitwise or value for unicode_set in elem[elemstr][element].flags. - * If bit is 1, then 'element' is a UnicodeSet string. - * If bit is 0, then 'element' is a UTF-32LE codepoint + * bitwise or value for type in elem[elemstr][element].flags. + * If bits are 00b, then 'element' is a UTF-32LE codepoint. + * If bits are 01b, then 'element' is a string index. + * If bits are 10b (2), then 'element' is a uset index. * - * `unicode_set = flags & elem_flags_unicode_set` + * `type = flags & elem_flags_type` */ - readonly elem_flags_unicode_set = 0x00000001; + readonly elem_flags_type = 0x00000003; + readonly elem_flags_type_char = 0x00000000; + readonly elem_flags_type_str = 0x00000001; + readonly elem_flags_type_uset = 0x00000002; /** * bitwise or value for tertiary_base in elem[elemstr][element].flags. @@ -157,7 +161,7 @@ class Constants { * * `tertiary_base = flags & elem_flags_tertiary_base` */ - readonly elem_flags_tertiary_base = 0x00000002; + readonly elem_flags_tertiary_base = 0x00000004; /** * bitwise or value for tertiary_base in elem[elemstr][element].flags. @@ -168,7 +172,7 @@ class Constants { * * `prebase = flags & elem_flags_prebase` */ - readonly elem_flags_prebase = 0x00000004; + readonly elem_flags_prebase = 0x00000008; /** * bitwise mask for order in elem[elemstr][element].flags. @@ -207,8 +211,8 @@ class Constants { readonly elem_flags_tertiary_bitshift = 24; /* ------------------------------------------------------------------ - * finl section - ------------------------------------------------------------------ */ + * finl section + ------------------------------------------------------------------ */ /** * Minimum length of the 'finl' section, not including entries @@ -452,19 +456,6 @@ class Constants { */ readonly length_name_item = 4; - /* ------------------------------------------------------------------ - * ordr section - ------------------------------------------------------------------ */ - - /** - * Minimum length of the 'ordr' section, not including entries - */ - readonly length_ordr = 12; - /** - * Length of each item in the 'ordr' section variable part - */ - readonly length_ordr_item = 8; - /* ------------------------------------------------------------------ * strs section ------------------------------------------------------------------ */ @@ -485,16 +476,34 @@ class Constants { /** * Minimum length of the 'tran' section, not including entries */ - readonly length_tran = 12; + readonly length_tran = 20; + /** + * Length of each transform group item + */ + readonly length_tran_group = 12; /** - * Length of each item in the 'tran' section variable part + * Length of each transform item */ - readonly length_tran_item = 16; + readonly length_tran_transform = 16; + /** + * Length of each reorder subtable item + */ + readonly length_tran_reorder = 8; + /** * bitwise or value for error="fail" in transform */ readonly tran_flags_error = 0x0001; + /** + * this group is full of transform items + */ + readonly tran_group_type_transform = 0; + /** + * this group is full of reorder items + */ + readonly tran_group_type_reorder = 1; + /* ------------------------------------------------------------------ * vkey section ------------------------------------------------------------------ */ @@ -508,6 +517,51 @@ class Constants { */ readonly length_vkey_item = 8; + /* ------------------------------------------------------------------ + * vars section + * ------------------------------------------------------------------ */ + + /** + * Minimum length of the 'vars' section not including variable parts + */ + readonly length_vars = 16; + /** + * Length of each item in the 'vars' section variable part + */ + readonly length_vars_item = 16; + + /** + * String variable + */ + readonly vars_entry_type_string = 0; + /** + * Set variable + */ + readonly vars_entry_type_set = 1; + /** + * unicodeSet variable + */ + readonly vars_entry_type_unicodeSet = 2; + + /* ------------------------------------------------------------------ + * uset section + * ------------------------------------------------------------------ */ + + /* + * Minimum length of the 'uset' section not including variable parts + */ + readonly length_uset = 16; + + /** + * Length of each entry in the uset.usets subtable + */ + readonly length_uset_uset = 12; + + /** + * Length of each entry in the uset.ranges subtable + */ + readonly length_uset_range = 8; + /** * All section IDs. */ @@ -516,17 +570,17 @@ class Constants { bksp: 'bksp', disp: 'disp', elem: 'elem', - finl: 'finl', keys: 'keys', layr: 'layr', list: 'list', loca: 'loca', meta: 'meta', name: 'name', - ordr: 'ordr', sect: 'sect', strs: 'strs', tran: 'tran', + uset: 'uset', + vars: 'vars', vkey: 'vkey', }; @@ -552,13 +606,26 @@ class Constants { * @returns string such as 'sect' */ str_section_id(hex:number) : string { - let chars : string[] = []; + const chars : string[] = []; for (let i = 3; i>=0; i--) { chars.push(String.fromCharCode(hex & 0xFF)); hex >>= 8; } return chars.join(''); } + + // ---- marker stuff ---- + /** sentinel value indicating a marker follows */ + readonly marker_sentinel = 0xFFFF; + /** minimum usable marker index */ + readonly marker_min_index = 0x0001; + /** index value referring to the 'any' marker match */ + readonly marker_any_index = 0xFFFF; + /** maximum marker index prior to the 'any' value */ + readonly marker_max_index = this.marker_any_index - 1; + /** maximum count of markers (not including 'any') */ + readonly marker_max_count = this.marker_max_index - this.marker_min_index; + }; export const constants = new Constants(); diff --git a/core/src/kmx/kmx_plus.cpp b/core/src/kmx/kmx_plus.cpp index 68dd5bacb0756b96248be9848b535b010611bd3e..b02fec77d85c999d06024c843cb7c1010082f70b 100644 --- a/core/src/kmx/kmx_plus.cpp +++ b/core/src/kmx/kmx_plus.cpp @@ -18,6 +18,20 @@ namespace km { namespace kbp { namespace kmx { +/** + * \def KMXPLUS_DEBUG_LOAD set to 1 to print messages on KMXPLUS loading. + * Off by default. +*/ +#ifndef KMXPLUS_DEBUG_LOAD +#define KMXPLUS_DEBUG_LOAD 0 +#endif + +#if KMXPLUS_DEBUG_LOAD +#define DebugLoad(msg,...) DebugLog(msg, __VA_ARGS__) +#else +#define DebugLoad(msg,...) +#endif + // double check these modifier mappings static_assert(LCTRLFLAG == LDML_KEYS_MOD_CTRLL, "LDML modifier bitfield vs. kmx_file.h #define mismatch"); static_assert(RCTRLFLAG == LDML_KEYS_MOD_CTRLR, "LDML modifier bitfield vs. kmx_file.h #define mismatch"); @@ -231,7 +245,7 @@ COMP_KMXPLUS_DISP::valid(KMX_DWORD _kmn_unused(length)) const { DebugLog("disp: baseCharacter str#0x%X", baseCharacter); } for (KMX_DWORD i=0; i str0x%X", i, entries[i].to, entries[i].display); + DebugLoad("disp#%d: to: str0x%X -> str0x%X", i, entries[i].to, entries[i].display); if (entries[i].to == 0 || entries[i].display == 0) { DebugLog("disp to: or display: has a zero string"); assert(false); @@ -250,7 +264,6 @@ COMP_KMXPLUS_STRS::valid(KMX_DWORD _kmn_unused(length)) const { return false; } for (KMX_DWORD i=0; i header.size) { @@ -266,11 +279,23 @@ COMP_KMXPLUS_STRS::valid(KMX_DWORD _kmn_unused(length)) const { return false; } // TODO-LDML: validate valid UTF-16LE? - DebugLog("#0x%X: '%s'", i, Debug_UnicodeString(start)); + DebugLoad("strs #0x%X: '%s'", i, Debug_UnicodeString(start)); } return true; } +/** + * helper for extracting single char values + * @param v field with char type + * @return value a string + */ +std::u16string COMP_KMXPLUS_STRS::str_from_char(KMX_DWORD v) { + char16_single buf; + const int len = Utf32CharToUtf16(v, buf); + return std::u16string(buf.ch, len); +} + + bool COMP_KMXPLUS_SECT::valid(KMX_DWORD length) const { DebugLog("sect: total 0x%X\n", total); @@ -358,26 +383,149 @@ COMP_KMXPLUS_ELEM::getElementList(KMX_DWORD elementNumber, KMX_DWORD &length) co return reinterpret_cast(rawdata + entry.offset); } + std::u16string -COMP_KMXPLUS_ELEM_ELEMENT::get_string() const { - assert(!(flags & LDML_ELEM_FLAGS_UNICODE_SET)); // should not be called. - char16_single buf; - const int len = Utf32CharToUtf16(element, buf); - return std::u16string(buf.ch, len); +COMP_KMXPLUS_ELEM_ELEMENT::get_element_string() const { + assert((flags & LDML_ELEM_FLAGS_TYPE) == LDML_ELEM_FLAGS_TYPE_CHAR); // should only be called on char + return COMP_KMXPLUS_STRS::str_from_char(element); } +// Note: shared with subclass COMP_KMXPLUS_BKSP bool COMP_KMXPLUS_TRAN::valid(KMX_DWORD _kmn_unused(length)) const { - if (header.size < sizeof(*this)+(sizeof(entries[0])*count)) { + if (header.size < sizeof(*this) + (sizeof(COMP_KMXPLUS_TRAN_GROUP) * groupCount) + + (sizeof(COMP_KMXPLUS_TRAN_TRANSFORM) * transformCount) + + (sizeof(COMP_KMXPLUS_TRAN_REORDER) * reorderCount)) { DebugLog("header.size < expected size"); assert(false); return false; } - // TODO-LDML - DebugLog("!! More to do here."); return true; } +COMP_KMXPLUS_TRAN_Helper::COMP_KMXPLUS_TRAN_Helper() + : tran(nullptr), is_valid(false), groups(nullptr), transforms(nullptr), reorders(nullptr) { +} + +bool COMP_KMXPLUS_TRAN_Helper::valid() const { + return is_valid; +} + +const COMP_KMXPLUS_TRAN_GROUP * +COMP_KMXPLUS_TRAN_Helper::getGroup(KMX_DWORD group) const { + if (!valid() || group >= tran->groupCount) { + assert(false); + return nullptr; + } + return groups + group; +} + + +const COMP_KMXPLUS_TRAN_TRANSFORM * +COMP_KMXPLUS_TRAN_Helper::getTransform(KMX_DWORD transform) const { + if (!valid() || transform >= tran->transformCount) { + assert(false); + return nullptr; + } + return transforms + transform; +} + + +const COMP_KMXPLUS_TRAN_REORDER * +COMP_KMXPLUS_TRAN_Helper::getReorder(KMX_DWORD reorder) const { + if (!valid() || reorder >= tran->reorderCount) { + assert(false); + return nullptr; + } + return reorders + reorder; +} + + +bool +COMP_KMXPLUS_TRAN_Helper::setTran(const COMP_KMXPLUS_TRAN *newTran) { + is_valid = true; + if (newTran == nullptr) { + DebugLog("tran helper: invalid, newTran=%p", newTran); + // Note: kmx_plus::kmx_plus has already called section_from_bytes() + // which validates this section's length. Will be nullptr here if invalid. + is_valid = false; + // No assert here: just a missing layer + return false; + } + DebugLog("tran helper: validating '%c%c%c%c' newTran=%p", DEBUG_IDENT(newTran->header.ident), newTran); + tran = newTran; + const uint8_t *rawdata = reinterpret_cast(tran); + rawdata += LDML_LENGTH_TRAN; // skip past non-dynamic portion + + // groups + if (tran->groupCount > 0) { + groups = reinterpret_cast(rawdata); + } else { + groups = nullptr; + is_valid = false; + assert(is_valid); + } + rawdata += sizeof(COMP_KMXPLUS_TRAN_GROUP) * tran->groupCount; + + // transforms + if (tran->transformCount > 0) { + transforms = reinterpret_cast(rawdata); + } else { + transforms = nullptr; + // is_valid = false; + // assert(is_valid); + } + rawdata += sizeof(COMP_KMXPLUS_TRAN_TRANSFORM) * tran->transformCount; + + // reorders + if (tran->reorderCount > 0) { + reorders = reinterpret_cast(rawdata); + } else { + reorders = nullptr; + // is_valid = false; + // assert(is_valid); + } + // rawdata += sizeof(COMP_KMXPLUS_TRAN_REORDER) * tran->reorderCount; + + // Now, validate offsets by walking + if (is_valid) { + for(KMX_DWORD i = 0; is_valid && i < tran->groupCount; i++) { + const COMP_KMXPLUS_TRAN_GROUP &group = groups[i]; + // is the count off the end? + DebugLog( + " %d: type 0x%X, entries [%d..%d]", i, group.type, group.index, + group.index + group.count - 1); + if (group.type == LDML_TRAN_GROUP_TYPE_TRANSFORM) { + DebugLog(" .. type=transform"); + if ((group.index >= tran->transformCount) || (group.index + group.count > tran->transformCount)) { + DebugLog("COMP_KMXPLUS_TRAN_Helper: group[%d] would access transform %d+%d, > count %d", + i, group.index, group.count, tran->transformCount); + is_valid = false; + assert(is_valid); + } + } else if (group.type == LDML_TRAN_GROUP_TYPE_REORDER) { + DebugLog(" .. type=reorder"); + if ((group.index >= tran->reorderCount) || (group.index + group.count > tran->reorderCount)) { + DebugLog("COMP_KMXPLUS_TRAN_Helper: group[%d] would access reorder %d+%d, > count %d", + i, group.index, group.count, tran->reorderCount); + is_valid = false; + assert(is_valid); + } + } else { + DebugLog(" .. type=illegal 0x%X", group.type); + is_valid = false; + assert(is_valid); + } + } + } + // Return results + DebugLog("COMP_KMXPLUS_TRAN_Helper.setTran(): %s", is_valid ? "valid" : "invalid"); + assert(is_valid); + return is_valid; +} + + + bool COMP_KMXPLUS_LAYR::valid(KMX_DWORD _kmn_unused(length)) const { if (header.size < sizeof(*this) @@ -404,7 +552,8 @@ COMP_KMXPLUS_LAYR_Helper::setLayr(const COMP_KMXPLUS_LAYR *newLayr) { DebugLog("validating newLayr=%p", newLayr); is_valid = true; if (newLayr == nullptr) { - // null = invalid + // Note: kmx_plus::kmx_plus has already called section_from_bytes() + // which validates this section's length. Will be nullptr here if invalid. is_valid = false; // No assert here: just a missing layer return false; @@ -561,7 +710,8 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { DebugLog("validating newKeys=%p", newKeys); is_valid = true; if (newKeys == nullptr) { - // null = invalid + // Note: kmx_plus::kmx_plus has already called section_from_bytes() + // which validates this section's length. Will be nullptr here if invalid. is_valid = false; // No assert here: just a missing layer return false; @@ -604,7 +754,7 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { for(KMX_DWORD i = 0; is_valid && i < key2->keyCount; i++) { const auto &key = keys[i]; // is the count off the end? - DebugLog( " id=0x%X, to=0x%X, flicks=%d", i, key.id, key.to, key.flicks); // TODO-LDML: could dump more fields here + DebugLoad( " id=0x%X, to=0x%X, flicks=%d", i, key.id, key.to, key.flicks); // TODO-LDML: could dump more fields here if (key.flicks >0 && key.flicks >= key2->flicksCount) { DebugLog("key[%d] has invalid flicks index %d", i, key.flicks); is_valid = false; @@ -614,7 +764,7 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { for(KMX_DWORD i = 0; is_valid && i < key2->flicksCount; i++) { const auto &e = flickLists[i]; // is the count off the end? - DebugLog(" %d: index %d, count %d", i, e.flick, e.count); + DebugLoad(" %d: index %d, count %d", i, e.flick, e.count); if (i == 0) { if (e.flick != 0 || e.count != 0) { DebugLog("Error: Invalid Flick #0"); @@ -629,17 +779,22 @@ COMP_KMXPLUS_KEYS_Helper::setKeys(const COMP_KMXPLUS_KEYS *newKeys) { } for(KMX_DWORD i = 0; is_valid && i < key2->flickCount; i++) { const auto &e = flickElements[i]; - // is the count off the end? - DebugLog(" %d: to=0x%X, directions=0x%X, flags=0x%X", i, e.to, e.directions, e.flags); + // validate to is present + if (e.to == 0 || e.directions == 0) { + DebugLog("flickElement[%d] has empty to=%0x%X or directions=%0x%X", i, e.to, e.directions); + is_valid = false; + assert(is_valid); + } + DebugLoad(" %d: to=0x%X, directions=0x%X, flags=0x%X", i, e.to, e.directions, e.flags); } // now the kmap - DebugLog(" kmap count: #0x%X\n", key2->kmapCount); + DebugLoad(" kmap count: #0x%X", key2->kmapCount); for (KMX_DWORD i = 0; i < key2->kmapCount; i++) { - DebugLog(" #0x%d\n", i); + DebugLoad(" #0x%d\n", i); auto &entry = kmap[i]; - DebugLog(" vkey\t0x%X\n", entry.vkey); - DebugLog(" mod\t0x%X\n", entry.mod); - DebugLog(" key\t#0x%X\n", entry.key); + DebugLoad(" vkey\t0x%X", entry.vkey); + DebugLoad(" mod\t0x%X", entry.mod); + DebugLoad(" key\t#0x%X", entry.key); if (!LDML_IS_VALID_MODIFIER_BITS(entry.mod)) { DebugLog("Invalid modifier value"); assert(false); @@ -679,9 +834,13 @@ COMP_KMXPLUS_KEYS_Helper::findKeyByStringId(KMX_DWORD strId, KMX_DWORD &i) const } const COMP_KMXPLUS_KEYS_KEY* -COMP_KMXPLUS_KEYS_Helper::findKeyByStringTo(KMX_DWORD strId, KMX_DWORD &i) const { +COMP_KMXPLUS_KEYS_Helper::findKeyByStringTo(const std::u16string& str, KMX_DWORD strId, KMX_DWORD &i) const { for (; i < key2->keyCount; i++) { - if (keys[i].to == strId) { + if (keys[i].flags & LDML_KEYS_KEY_FLAGS_EXTEND) { + if (strId != 0 && keys[i].to == strId) { + return &keys[i]; + } + } else if (keys[i].get_to_string() == str) { return &keys[i]; } } @@ -716,11 +875,9 @@ COMP_KMXPLUS_KEYS_Helper::getKmap(KMX_DWORD i) const { } std::u16string -COMP_KMXPLUS_KEYS_KEY::get_string() const { +COMP_KMXPLUS_KEYS_KEY::get_to_string() const { assert(!(flags & LDML_KEYS_KEY_FLAGS_EXTEND)); // should not be called. - char16_single buf; - const int len = Utf32CharToUtf16(to, buf); - return std::u16string(buf.ch, len); + return COMP_KMXPLUS_STRS::str_from_char(to); } // LIST @@ -746,7 +903,8 @@ COMP_KMXPLUS_LIST_Helper::setList(const COMP_KMXPLUS_LIST *newList) { DebugLog("validating newList=%p", newList); is_valid = true; if (newList == nullptr) { - // null = invalid + // Note: kmx_plus::kmx_plus has already called section_from_bytes() + // which validates this section's length. Will be nullptr here if invalid. is_valid = false; // No assert here: just a missing layer return false; @@ -788,10 +946,12 @@ COMP_KMXPLUS_LIST_Helper::setList(const COMP_KMXPLUS_LIST *newList) { assert(is_valid); } } +#if KMXPLUS_DEBUG_LOAD for (KMX_DWORD i = 0; is_valid && i < list->indexCount; i++) { const auto &e = indices[i]; - DebugLog(" index %d: str 0x%X", i, e); + DebugLoad(" index %d: str 0x%X", i, e); } +#endif } // Return results DebugLog("COMP_KMXPLUS_LIST_Helper.setList(): %s", is_valid ? "valid" : "invalid"); @@ -817,13 +977,171 @@ COMP_KMXPLUS_LIST_Helper::getIndex(KMX_DWORD i) const { return indices + i; } + +// USET + +bool +COMP_KMXPLUS_USET::valid(KMX_DWORD _kmn_unused(length)) const { + if (header.size < sizeof(*this) + + (usetCount * sizeof(COMP_KMXPLUS_USET_USET)) + + (rangeCount * sizeof(COMP_KMXPLUS_USET_RANGE))) { + DebugLog("header.size < expected size"); + assert(false); + return false; + } + return true; // see helper +} + +COMP_KMXPLUS_USET_RANGE::COMP_KMXPLUS_USET_RANGE(KMX_DWORD s, KMX_DWORD e) : start(s), end(e) { +} + +COMP_KMXPLUS_USET_RANGE::COMP_KMXPLUS_USET_RANGE(const COMP_KMXPLUS_USET_RANGE &other) : start(other.start), end(other.end) { +} + +COMP_KMXPLUS_USET_Helper::COMP_KMXPLUS_USET_Helper() : uset(nullptr), is_valid(false), usets(nullptr), ranges(nullptr) { +} + +bool +COMP_KMXPLUS_USET_Helper::setUset(const COMP_KMXPLUS_USET *newUset) { + DebugLoad("validating newUset=%p", newUset); + is_valid = true; + if (newUset == nullptr) { + // Note: kmx_plus::kmx_plus has already called section_from_bytes() + // which validates this section's length. Will be nullptr here if invalid. + is_valid = false; + // No assert here: just a missing layer + return false; + } + uset = newUset; + const uint8_t *rawdata = reinterpret_cast(newUset); + rawdata += LDML_LENGTH_USET; // skip past non-dynamic portion + // usets + if (uset->usetCount > 0) { + usets = reinterpret_cast(rawdata); + } else { + usets = nullptr; + // not invalid, just empty. + } + rawdata += sizeof(COMP_KMXPLUS_USET_USET) * uset->usetCount; + // entries + if (uset->rangeCount > 0) { + ranges = reinterpret_cast(rawdata); + } else { + ranges = nullptr; + } + + // Now, validate offsets by walking + // is_valid must be true at this point. + for (KMX_DWORD i = 0; is_valid && i < uset->usetCount; i++) { + const auto &e = usets[i]; + // is the count off the end? + DebugLog("uset 0x%X: range %d, count %d, pattern 0x%X", i, e.range, e.count, e.pattern); + if ((e.range >= uset->rangeCount) || (e.range + e.count > uset->rangeCount)) { + DebugLog("uset[%d] would access range %d+%d, > count %d", i, e.range, e.count, uset->rangeCount); + is_valid = false; + assert(is_valid); + } else { + /** last lastEnd value */ + KMX_DWORD lastEnd = 0x0; + for (KMX_DWORD r = 0; is_valid && r < e.count; r++) { + const auto &range = ranges[e.range + r]; // already range-checked 'r' above + if (!Uni_IsValid(range.start, range.end)) { + DebugLog("uset[%d][%d] not valid: [U+%04X-U+%04X]", i, r, range.start, range.end); + is_valid = false; + assert(is_valid); + } else if (range.end < range.start) { + // range swapped + DebugLog("uset[%d]: range[%d+%d] end 0x%X prior range (overlap/ranges unsorted?)", + i, e.range, r, range.start); + is_valid = false; + assert(is_valid); + } else { + lastEnd = range.end; + } + } + } + } + // Return results + DebugLog("COMP_KMXPLUS_USET_Helper.setUset(): %s", is_valid ? "valid" : "invalid"); + assert(is_valid); + return is_valid; +} + +USet::USet(const COMP_KMXPLUS_USET_RANGE *newRange, size_t newCount) { + for (size_t i = 0; i < newCount; i++) { + ranges.emplace_back(newRange[i].start, newRange[i].end); + } +} + +USet::USet() { +} + +bool USet::contains(km_kbp_usv ch) const { + for (const auto &range : ranges) { + if (range.start <= ch && range.end >= ch) { + return true; + } + } + return false; +} + +bool +USet::valid() const { + // double check + for (const auto &range : ranges) { + if (!Uni_IsValid(range.start, range.end)) { + DebugLog("Invalid UnicodeSet (contains noncharacters): [U+%04X,U+%04X]", (int)range.start, (int)range.end); + return false; + } + } + return true; +} + +void +USet::dump() const { + DebugLog(" - USet size=%d", ranges.size()); + for (const auto &range : ranges) { + if (range.start == range.end) { + DebugLog(" - [U+%04X]", (uint32_t)range.start); + } else { + DebugLog(" - [U+%04X-U+%04X]", (uint32_t)range.start, (uint32_t)range.end); + } + } +} + +USet +COMP_KMXPLUS_USET_Helper::getUset(KMXPLUS_USET i) const { + if (!valid() || i >= uset->usetCount) { + assert(false); + return USet(nullptr, 0); // empty set + } + auto &set = usets[i]; + return USet(getRange(set.range), set.count); +} + +const COMP_KMXPLUS_USET_RANGE * +COMP_KMXPLUS_USET_Helper::getRange(KMX_DWORD i) const { + if (!valid() || i >= uset->rangeCount) { + assert(false); + return nullptr; + } + return ranges + i; +} + // ---- constructor kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) - : loca(nullptr), meta(nullptr), - sect(nullptr), strs(nullptr), vkey(nullptr), valid(false) { - - DebugLog("kmx_plus(): Got a COMP_KEYBOARD at %p\n", keyboard); + : bksp(nullptr), disp(nullptr), elem(nullptr), key2(nullptr), layr(nullptr), list(nullptr), loca(nullptr), meta(nullptr), + sect(nullptr), strs(nullptr), tran(nullptr), vars(nullptr), vkey(nullptr), valid(false) { + DebugLog("kmx_plus: Got a COMP_KEYBOARD at %p\n", keyboard); +#if !KMXPLUS_DEBUG_LOAD + DebugLog("Note: define KMXPLUS_DEBUG_LOAD=1 at compile time for more verbosity in loading"); +#endif if (!(keyboard->dwFlags & KF_KMXPLUS)) { DebugLog("Err: flags COMP_KEYBOARD.dwFlags did not have KF_KMXPLUS set"); valid = false; @@ -833,7 +1151,7 @@ kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) const COMP_KEYBOARD_EX* ex = reinterpret_cast(keyboard); DebugLog("kmx_plus(): KMXPlus offset 0x%X, KMXPlus size 0x%X\n", ex->kmxplus.dpKMXPlus, ex->kmxplus.dwKMXPlusSize); - if (ex->kmxplus.dpKMXPlus < sizeof(kmx::COMP_KEYBOARD_EX)) { + if (ex->kmxplus.dpKMXPlus < sizeof(COMP_KEYBOARD_EX)) { DebugLog("dwKMXPlus is not past the end of COMP_KEYBOARD_EX"); valid = false; assert(valid); @@ -855,7 +1173,8 @@ kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) } else { valid = true; // load other sections, validating as we go - // these will be nullptr if they don't validate + // each field will be set to nullptr if validation fails + bksp = section_from_sect(sect); disp = section_from_sect(sect); elem = section_from_sect(sect); key2 = section_from_sect(sect); @@ -865,12 +1184,20 @@ kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) meta = section_from_sect(sect); strs = section_from_sect(sect); tran = section_from_sect(sect); + uset = section_from_sect(sect); + vars = section_from_sect(sect); vkey = section_from_sect(sect); - // calculate and validate the dynamic parts + // Initialize the helper objects for sections with dynamic parts. + // Note: all of these setters will be passed 'nullptr' + // if any section had failed validation. + + (void)bkspHelper.setTran(bksp); // bksp handled by …TRAN_Helper (void)key2Helper.setKeys(key2); (void)layrHelper.setLayr(layr); (void)listHelper.setList(list); + (void)tranHelper.setTran(tran); + (void)usetHelper.setUset(uset); } } @@ -910,6 +1237,17 @@ KMX_DWORD COMP_KMXPLUS_STRS::find(const std::u16string& s) const { return 0; // not found } +bool +COMP_KMXPLUS_VARS::valid(KMX_DWORD _kmn_unused(length)) const { + if (header.size < sizeof(*this) + + (varCount * sizeof(COMP_KMXPLUS_VARS_ITEM))) { + DebugLog("header.size < expected size"); + assert(false); + return false; + } + return true; +} + } // namespace kmx } // namespace kbp diff --git a/core/src/kmx/kmx_plus.h b/core/src/kmx/kmx_plus.h index 7944d7a712d2c8ed642a603c59307136f4fa414b..684da2a1920472b4b6c7b356584c2ed8def5e400 100644 --- a/core/src/kmx/kmx_plus.h +++ b/core/src/kmx/kmx_plus.h @@ -11,6 +11,7 @@ #include #include #include +#include namespace km { namespace kbp { @@ -38,8 +39,10 @@ typedef KMX_DWORD KMXPLUS_LIST; typedef KMX_DWORD KMXPLUS_ELEM; // forward declarations -struct COMP_KMXPLUS_TRAN_ENTRY; struct COMP_KMXPLUS_TRAN; +struct COMP_KMXPLUS_TRAN_GROUP; +struct COMP_KMXPLUS_TRAN_TRANSFORM; +struct COMP_KMXPLUS_TRAN_REORDER; struct COMP_KMXPLUS_HEADER { KMX_DWORD ident; // 0000 Section name @@ -83,13 +86,6 @@ struct COMP_KMXPLUS_SECT { static_assert(sizeof(struct COMP_KMXPLUS_SECT) == LDML_LENGTH_SECT, "mismatched size of section sect"); static_assert(sizeof(struct COMP_KMXPLUS_SECT) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -/* ------------------------------------------------------------------ - * bksp section - ------------------------------------------------------------------ */ - -typedef COMP_KMXPLUS_TRAN_ENTRY COMP_KMXPLUS_BKSP_ENTRY; -typedef COMP_KMXPLUS_TRAN COMP_KMXPLUS_BKSP; - /* ------------------------------------------------------------------ * elem section ------------------------------------------------------------------ */ @@ -98,11 +94,11 @@ struct COMP_KMXPLUS_ELEM_ELEMENT { KMX_DWORD element; // str: output string or UTF-32LE codepoint KMX_DWORD flags; // flag and order values /** - * @brief Get the 'to' as a string, if flags&LDML_ELEM_FLAGS_UNICODE_SET is not set + * @brief Get the 'element' as a string, if flags&LDML_ELEM_FLAGS_TYPE = CHAR * * @return std::u16string */ - std::u16string get_string() const; + std::u16string get_element_string() const; }; struct COMP_KMXPLUS_ELEM_ENTRY { @@ -133,15 +129,9 @@ static_assert(sizeof(struct COMP_KMXPLUS_ELEM) % 0x4 == 0, "Structs prior to var static_assert(sizeof(struct COMP_KMXPLUS_ELEM) == LDML_LENGTH_ELEM, "mismatched size of section elem"); /* ------------------------------------------------------------------ - * finl section + * finl section is no more ------------------------------------------------------------------ */ -// TODO-LDML: IDENT -typedef COMP_KMXPLUS_TRAN_ENTRY COMP_KMXPLUS_FINL_ENTRY; - -// TODO-LDML: IDENT -typedef COMP_KMXPLUS_TRAN COMP_KMXPLUS_FINL; - /* ------------------------------------------------------------------ * keys section is now key2.kmap ------------------------------------------------------------------ */ @@ -212,29 +202,6 @@ struct COMP_KMXPLUS_NAME { static_assert(sizeof(struct COMP_KMXPLUS_NAME) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); static_assert(sizeof(struct COMP_KMXPLUS_NAME) == LDML_LENGTH_NAME, "mismatched size of section name"); -/* ------------------------------------------------------------------ - * ordr section - ------------------------------------------------------------------ */ - -struct COMP_KMXPLUS_ORDR_ENTRY { - KMXPLUS_ELEM elements; - KMXPLUS_ELEM before; -}; - -struct COMP_KMXPLUS_ORDR { - static const KMX_DWORD IDENT = LDML_SECTIONID_ORDR; - COMP_KMXPLUS_HEADER header; - KMX_DWORD count; - COMP_KMXPLUS_ORDR_ENTRY entries[]; - /** - * @brief True if section is valid. - */ - bool valid(KMX_DWORD length) const; -}; - -static_assert(sizeof(struct COMP_KMXPLUS_ORDR) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); -static_assert(sizeof(struct COMP_KMXPLUS_ORDR) == LDML_LENGTH_ORDR, "mismatched size of section ordr"); - /* ------------------------------------------------------------------ * strs section ------------------------------------------------------------------ */ @@ -268,6 +235,9 @@ struct COMP_KMXPLUS_STRS { * @brief True if section is valid. */ bool valid(KMX_DWORD length) const; + + /** convert a single char to a string*/ + static std::u16string str_from_char(KMX_DWORD v); }; static_assert(sizeof(struct COMP_KMXPLUS_STRS) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); @@ -277,27 +247,107 @@ static_assert(sizeof(struct COMP_KMXPLUS_STRS) == LDML_LENGTH_STRS, "mismatched * tran section ------------------------------------------------------------------ */ -struct COMP_KMXPLUS_TRAN_ENTRY { - KMXPLUS_ELEM from; +struct COMP_KMXPLUS_TRAN_GROUP { + KMX_DWORD type; + KMX_DWORD count; + KMX_DWORD index; +}; + +struct COMP_KMXPLUS_TRAN_TRANSFORM { + KMXPLUS_STR from; KMXPLUS_STR to; - KMXPLUS_ELEM before; - KMX_DWORD flags; + KMXPLUS_ELEM mapFrom; + KMXPLUS_ELEM mapTo; }; +struct COMP_KMXPLUS_TRAN_REORDER { + KMXPLUS_ELEM elements; + KMXPLUS_ELEM before; +}; struct COMP_KMXPLUS_TRAN { static const KMX_DWORD IDENT = LDML_SECTIONID_TRAN; COMP_KMXPLUS_HEADER header; - KMX_DWORD count; - COMP_KMXPLUS_TRAN_ENTRY entries[]; + KMX_DWORD groupCount; + KMX_DWORD transformCount; + KMX_DWORD reorderCount; + // Variable part: + // COMP_KMXPLUS_TRAN_GROUP groups[] + // COMP_KMXPLUS_TRAN_TRANSFORM transforms[] + // COMP_KMXPLUS_TRAN_REORDER reorders[] /** * @brief True if section is valid. */ bool valid(KMX_DWORD length) const; }; +class COMP_KMXPLUS_TRAN_Helper { +public: + COMP_KMXPLUS_TRAN_Helper(); + /** + * Initialize the helper to point at a layr section. + * @return true if valid + */ + bool setTran(const COMP_KMXPLUS_TRAN *newTran); + bool valid() const; + + const COMP_KMXPLUS_TRAN_GROUP *getGroup(KMX_DWORD n) const; + const COMP_KMXPLUS_TRAN_TRANSFORM *getTransform(KMX_DWORD n) const; + const COMP_KMXPLUS_TRAN_REORDER *getReorder(KMX_DWORD n) const; + +private: + const COMP_KMXPLUS_TRAN *tran; + bool is_valid; + const COMP_KMXPLUS_TRAN_GROUP *groups; + const COMP_KMXPLUS_TRAN_TRANSFORM *transforms; + const COMP_KMXPLUS_TRAN_REORDER *reorders; +}; + + static_assert(sizeof(struct COMP_KMXPLUS_TRAN) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); static_assert(sizeof(struct COMP_KMXPLUS_TRAN) == LDML_LENGTH_TRAN, "mismatched size of section tran"); +static_assert(sizeof(struct COMP_KMXPLUS_TRAN_GROUP) == LDML_LENGTH_TRAN_GROUP, "mismatched size of tran group"); +static_assert(sizeof(struct COMP_KMXPLUS_TRAN_TRANSFORM) == LDML_LENGTH_TRAN_TRANSFORM, "mismatched size of tran transform"); +static_assert(sizeof(struct COMP_KMXPLUS_TRAN_REORDER) == LDML_LENGTH_TRAN_REORDER, "mismatched size of tran reorder"); + + +/* ------------------------------------------------------------------ + * bksp section + ------------------------------------------------------------------ */ + +typedef COMP_KMXPLUS_TRAN_Helper COMP_KMXPLUS_BKSP_Helper; + +struct COMP_KMXPLUS_BKSP : public COMP_KMXPLUS_TRAN { + static const KMX_DWORD IDENT = LDML_SECTIONID_BKSP; +}; + + +/* ------------------------------------------------------------------ + * vkey section + ------------------------------------------------------------------ */ + +struct COMP_KMXPLUS_VARS_ITEM { + KMX_DWORD type; + KMX_DWORD id; + KMX_DWORD value; + KMX_DWORD elem; +}; + +struct COMP_KMXPLUS_VARS { + static const KMX_DWORD IDENT = LDML_SECTIONID_VARS; + COMP_KMXPLUS_HEADER header; + KMX_DWORD markers; + KMX_DWORD varCount; + COMP_KMXPLUS_VARS_ITEM varEntries[]; + /** + * @brief True if section is valid. + */ + bool valid(KMX_DWORD length) const; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_VARS) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); +static_assert(sizeof(struct COMP_KMXPLUS_VARS) == LDML_LENGTH_VARS, "mismatched size of section vars"); +static_assert(sizeof(struct COMP_KMXPLUS_VARS_ITEM) == LDML_LENGTH_VARS_ITEM, "mismatched size of vars item"); /* ------------------------------------------------------------------ * vkey section @@ -460,6 +510,8 @@ struct COMP_KMXPLUS_KEYS_FLICK_ELEMENT { KMXPLUS_LIST directions; KMX_DWORD flags; KMXPLUS_STR to; // string or codepoint + /** get the 'to' string if a char */ + std::u16string get_to_string() const; }; struct COMP_KMXPLUS_KEYS_FLICK_LIST { @@ -479,7 +531,7 @@ struct COMP_KMXPLUS_KEYS_KEY { KMXPLUS_LIST multiTap; KMX_DWORD flicks; // index - std::u16string get_string() const; + std::u16string get_to_string() const; }; struct COMP_KMXPLUS_KEYS_KMAP { @@ -512,11 +564,12 @@ public: const COMP_KMXPLUS_KEYS_KEY *findKeyByStringId(KMX_DWORD strId, KMX_DWORD &index) const; /** * Search for a key by 'to' string id + * @param str string to search for (for single char strings) * @param strID id to search for * @param index on entry, id to start with such as 0. On exit, index of item if found. Undefined otherwise. * @return pointer to key or nullptr */ - const COMP_KMXPLUS_KEYS_KEY *findKeyByStringTo(KMX_DWORD strId, KMX_DWORD &index) const; + const COMP_KMXPLUS_KEYS_KEY *findKeyByStringTo(const std::u16string& str, KMX_DWORD strId, KMX_DWORD &index) const; private: const COMP_KMXPLUS_KEYS *key2; @@ -591,6 +644,90 @@ static_assert(sizeof(struct COMP_KMXPLUS_LIST) == LDML_LENGTH_LIST, "mismatched static_assert(sizeof(struct COMP_KMXPLUS_LIST_ITEM) == LDML_LENGTH_LIST_ITEM, "mismatched size of section list.lists subtable"); static_assert(sizeof(COMP_KMXPLUS_LIST_INDEX) == LDML_LENGTH_LIST_INDEX, "mismatched size of section list.indices subtable"); + + +/* ------------------------------------------------------------------ + * uset section + ------------------------------------------------------------------ */ +/** + * uset.range index + */ +typedef KMX_DWORD KMXPLUS_USET; + +struct COMP_KMXPLUS_USET { + static const KMX_DWORD IDENT = LDML_SECTIONID_USET; + COMP_KMXPLUS_HEADER header; + KMX_DWORD usetCount; + KMX_DWORD rangeCount; + // see helper for: usets sub-table + // see helper for: ranges sub-table + + /** + * @brief True if section is valid. + */ + bool valid(KMX_DWORD length) const; +}; + +/** + * uset.usets subtable + */ +struct COMP_KMXPLUS_USET_USET { + KMX_DWORD range; + KMX_DWORD count; + KMXPLUS_STR pattern; +}; + +struct COMP_KMXPLUS_USET_RANGE { + km_kbp_usv start; + km_kbp_usv end; + public: + COMP_KMXPLUS_USET_RANGE(const COMP_KMXPLUS_USET_RANGE& other); + COMP_KMXPLUS_USET_RANGE(KMX_DWORD start, KMX_DWORD end); +}; + +/** + * represents one of the uset elements + */ +class USet { + public: + /** construct a set over the specified range. Data is copied. */ + USet(const COMP_KMXPLUS_USET_RANGE* newStart, size_t newCount); + /** empty set */ + USet(); + /** true if the uset contains this char */ + bool contains(km_kbp_usv ch) const; + /** debugging */ + void dump() const; + bool valid() const; + private: + std::list ranges; +}; + +class COMP_KMXPLUS_USET_Helper { +public: + COMP_KMXPLUS_USET_Helper(); + /** + * Initialize the helper to point at a uset section. + * @return true if valid + */ + bool setUset(const COMP_KMXPLUS_USET *newUset); + inline bool valid() const { return is_valid; } + + USet getUset(KMXPLUS_USET list) const; + const COMP_KMXPLUS_USET_RANGE *getRange(KMX_DWORD index) const; + +private: + const COMP_KMXPLUS_USET *uset; + bool is_valid; + const COMP_KMXPLUS_USET_USET *usets; + const COMP_KMXPLUS_USET_RANGE *ranges; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_USET) % 0x4 == 0, "Structs prior to variable part should align to 32-bit boundary"); +static_assert(sizeof(struct COMP_KMXPLUS_USET) == LDML_LENGTH_USET, "mismatched size of section uset"); +static_assert(sizeof(struct COMP_KMXPLUS_USET_RANGE) == LDML_LENGTH_USET_RANGE, "mismatched size of section uset.ranges subtable"); +static_assert(sizeof(struct COMP_KMXPLUS_USET_USET) == LDML_LENGTH_USET_USET, "mismatched size of section uset.usets subtable"); + /** * @brief helper accessor object for KMX Plus data * @@ -607,6 +744,7 @@ class kmx_plus { */ kmx_plus(const COMP_KEYBOARD *keyboard, size_t length); // keep the next elements sorted + const COMP_KMXPLUS_BKSP *bksp; const COMP_KMXPLUS_DISP *disp; const COMP_KMXPLUS_ELEM *elem; const COMP_KMXPLUS_KEYS *key2; @@ -617,11 +755,16 @@ class kmx_plus { const COMP_KMXPLUS_SECT *sect; const COMP_KMXPLUS_STRS *strs; const COMP_KMXPLUS_TRAN *tran; + const COMP_KMXPLUS_USET *uset; + const COMP_KMXPLUS_VARS *vars; const COMP_KMXPLUS_VKEY *vkey; inline bool is_valid() { return valid; } + COMP_KMXPLUS_BKSP_Helper bkspHelper; + COMP_KMXPLUS_KEYS_Helper key2Helper; COMP_KMXPLUS_LAYR_Helper layrHelper; COMP_KMXPLUS_LIST_Helper listHelper; - COMP_KMXPLUS_KEYS_Helper key2Helper; + COMP_KMXPLUS_TRAN_Helper tranHelper; + COMP_KMXPLUS_USET_Helper usetHelper; private: bool valid; // true if valid }; diff --git a/core/src/kmx/kmx_xstring.h b/core/src/kmx/kmx_xstring.h index fafa277ed79faddb87643d18267958e13ac2d5f0..9b479dac7ad4e5c1d481a4a76134ae228141d90a 100644 --- a/core/src/kmx/kmx_xstring.h +++ b/core/src/kmx/kmx_xstring.h @@ -6,22 +6,43 @@ namespace km { namespace kbp { namespace kmx { +const char16_t Uni_LEAD_SURROGATE_START = 0xD800; +const char16_t Uni_LEAD_SURROGATE_END = 0xDBFF; +const char16_t Uni_TRAIL_SURROGATE_START = 0xDC00; +const char16_t Uni_TRAIL_SURROGATE_END = 0xDFFF; +const char16_t Uni_SURROGATE_START = Uni_LEAD_SURROGATE_START; +const char16_t Uni_SURROGATE_END = Uni_TRAIL_SURROGATE_END; +const char16_t Uni_FD_NONCHARACTER_START = 0xFDD0; +const char16_t Uni_FD_NONCHARACTER_END = 0xFDEF; +const char16_t Uni_FFFE_NONCHARACTER = 0xFFFE; +const char16_t Uni_FFFF_NONCHARACTER = 0xFFFF; +const char16_t Uni_BMP_END = 0xFFFF; +const km_kbp_usv Uni_SMP_START = 0x010000; +const km_kbp_usv Uni_PLANE_MASK = 0x1F0000; +const km_kbp_usv Uni_MAX_CODEPOINT = 0x10FFFF; + /** * @brief True if a lead surrogate * \def Uni_IsSurrogate1 */ -#define Uni_IsSurrogate1(ch) ((ch) >= 0xD800 && (ch) <= 0xDBFF) +#define Uni_IsSurrogate1(ch) ((ch) >= km::kbp::kmx::Uni_LEAD_SURROGATE_START && (ch) <= km::kbp::kmx::Uni_LEAD_SURROGATE_END) /** * @brief True if a trail surrogate * \def Uni_IsSurrogate2 */ -#define Uni_IsSurrogate2(ch) ((ch) >= 0xDC00 && (ch) <= 0xDFFF) +#define Uni_IsSurrogate2(ch) ((ch) >= km::kbp::kmx::Uni_TRAIL_SURROGATE_START && (ch) <= km::kbp::kmx::Uni_TRAIL_SURROGATE_END) + +/** + * @brief True if any surrogate + * \def UniIsSurrogate +*/ +#define Uni_IsSurrogate(ch) (Uni_IsSurrogate1(ch) || Uni_IsSurrogate2(ch)) /** * @brief Returns true if BMP (Plane 0) * \def Uni_IsBMP */ -#define Uni_IsBMP(ch) ((ch) < 0x10000) +#define Uni_IsBMP(ch) ((ch) <= km::kbp::kmx::Uni_BMP_END) /** * @brief Convert two UTF-16 surrogates into one UTF-32 codepoint @@ -29,17 +50,35 @@ namespace kmx { * @param cl trail surrogate - Uni_IsSurrogate2(cl) must == true * \def Uni_SurrogateToUTF */ -#define Uni_SurrogateToUTF32(ch, cl) (((ch) - 0xD800) * 0x400 + ((cl) - 0xDC00) + 0x10000) +#define Uni_SurrogateToUTF32(ch, cl) (((ch) - km::kbp::kmx::Uni_LEAD_SURROGATE_START) * 0x400 + ((cl) - km::kbp::kmx::Uni_TRAIL_SURROGATE_START) + km::kbp::kmx::Uni_SMP_START) /** * @brief Convert UTF-32 BMP to UTF-16 BMP * @param ch codepoint - Uni_IsBMP(ch) must == true * \def Uni_UTF32BMPToUTF16 */ -#define Uni_UTF32BMPToUTF16(ch) (ch & 0xFFFF) +#define Uni_UTF32BMPToUTF16(ch) ((ch) & Uni_FFFF_NONCHARACTER) -#define Uni_UTF32ToSurrogate1(ch) (char16_t)(((ch) - 0x10000) / 0x400 + 0xD800) -#define Uni_UTF32ToSurrogate2(ch) (char16_t)(((ch) - 0x10000) % 0x400 + 0xDC00) +#define Uni_UTF32ToSurrogate1(ch) (char16_t)(((ch) - km::kbp::kmx::Uni_SMP_START) / 0x400 + km::kbp::kmx::Uni_LEAD_SURROGATE_START) +#define Uni_UTF32ToSurrogate2(ch) (char16_t)(((ch) - km::kbp::kmx::Uni_SMP_START) % 0x400 + km::kbp::kmx::Uni_TRAIL_SURROGATE_START) + +/** + * @returns true if the character is a noncharacter +*/ +bool Uni_IsNonCharacter(km_kbp_usv ch); + +/** + * @returns true if the character is a valid Unicode code point. + * Surrogates belong to UTF-16 and are invalid. +*/ +bool Uni_IsValid(km_kbp_usv ch); + +/** + * @returns true if the character is a valid Unicode code point range, that is, [start-end] are all + * valid. + * Surrogates belong to UTF-16 and are invalid. +*/ +bool Uni_IsValid(km_kbp_usv start, km_kbp_usv range); /** * char16_t array big enough to hold a single Unicode codepoint, @@ -133,6 +172,44 @@ u16string_to_u32string(const std::u16string &source) { return out; } +inline bool Uni_IsEndOfPlaneNonCharacter(km_kbp_usv ch) { + return (((ch) & Uni_FFFE_NONCHARACTER) == Uni_FFFE_NONCHARACTER); // matches FFFF or FFFE +} + +inline bool Uni_IsNoncharacter(km_kbp_usv ch) { + return (((ch) >= Uni_FD_NONCHARACTER_START && (ch) <= Uni_FD_NONCHARACTER_END) || Uni_IsEndOfPlaneNonCharacter(ch)); +} + +inline bool Uni_InCodespace(km_kbp_usv ch) { + return ((ch) <= Uni_MAX_CODEPOINT); +}; + +inline bool Uni_IsValid(km_kbp_usv ch) { + return (Uni_InCodespace(ch) && !Uni_IsSurrogate(ch) && !Uni_IsNoncharacter(ch)); +} + +inline bool Uni_IsValid(km_kbp_usv start, km_kbp_usv end) { + if (!Uni_IsValid(end) || !Uni_IsValid(start) || (end < start)) { + // start or end out of range, or inverted range + return false; + } else if ((start <= Uni_SURROGATE_END) && (end >= Uni_SURROGATE_START)) { + // contains some of the surrogate range + return false; + } else if ((start <= Uni_FD_NONCHARACTER_END) && (end >= Uni_FD_NONCHARACTER_START)) { + // contains some of the noncharacter range + return false; + } else if ((start & Uni_PLANE_MASK) != (end & Uni_PLANE_MASK)) { + // start and end are on different planes, meaning that the U+__FFFE/U+__FFFF noncharacters + // are contained. + // As a reminder, we already checked that start/end are themselves valid, + // so we know that 'end' is not on a noncharacter at end of plane. + return false; + } else { + return true; + } +} + + } // namespace kmx } // namespace kbp } // namespace km diff --git a/core/src/ldml/C7043_ldml.md b/core/src/ldml/C7043_ldml.md index 16849985fb54a2574296db28aba31a1a9878ad5a..9436d3d1b74be1b1d5f54119a66f94699333a429 100644 --- a/core/src/ldml/C7043_ldml.md +++ b/core/src/ldml/C7043_ldml.md @@ -60,9 +60,9 @@ This list is in sorted order based on the `sect` identifier. ### C7043.2.2 `bksp`—Backspace transform -See [C7043.2.11](#c7043211-tran-finl-and-bksptransforms). +See [C7043.2.11](#c7043211-tran-and-bksptransforms). -### C7043.2.3 `elem`—Transform and Reorder element strings +### C7043.2.3 `elem`—Transform, Variable, and Reorder element strings | ∆ | Bits | Name | Description | |---|------|----------|------------------------------------------| @@ -87,27 +87,25 @@ Each element string is made up of elements with the following item structure: | 0 | 32 | element | str: output string OR UTF-32LE codepoint | | 4 | 32 | flags | flags and order values | -- `element`: either a UnicodeSet stored in a `strs` section entry, or a UTF-32LE - codepoint; see also `unicode_set` flag. +- `element`: either a UnicodeSet stored in a `strs` section entry, a UTF-32LE + codepoint, or a `uset` section entry. see also `type` flag. - `flags`: a 32-bit bitfield defined as below: - | Bit position | Meaning | Description | - |--------------|---------------|----------------------------------| - | 0 | unicode_set | `element` is 0: UTF-32LE, 1: str | - | 1 | tertiary_base | 1: tertiary_base is true | - | 2 | prebase | 1: prebase is true | - | 3-15 | reserved | reserved | - | 16-23 | order | signed int: -128 to +127 | - | 24-31 | tertiary | signed int: -128 to +127 | + | Bit position | Meaning | Description | + |--------------|---------------|-------------------------------------------| + | 0-1 | type | `element` is 0: UTF-32LE, 1: str, 2: uset | + | 2 | tertiary_base | 1: tertiary_base is true | + | 3 | prebase | 1: prebase is true | + | 4-15 | reserved | reserved | + | 16-23 | order | signed int: -128 to +127 | + | 24-31 | tertiary | signed int: -128 to +127 | - For transforms, only `flags.unicode_set` will be used. The remaining flags are + For transforms and sets, only `flags.type` will be used. The remaining flags are used for reorders, `from` attribute only. -### C7043.2.4 `finl`—Final transform +### C7043.2.4 Removed: `finl` -See [C7043.2.11](#c7043211-tran-finl-and-bksptransforms). - -### C7043.2.5 Removed: `keys` +### C7043.2.5 Removed: old-format `keys` _this section has been merged into the new `keys.kmap`_ @@ -174,24 +172,9 @@ For each name in `count`: Names are stored in source file order, and the semantic meaning of each name is not defined here. -### C7043.2.9 `ordr`—Reorders - -| ∆ | Bits | Name | Description | -|---|------|----------|------------------------------------------| -| 0 | 32 | ident | `ordr` | -| 4 | 32 | size | int: Length of section | -| 8 | 32 | count | int: Number of reorders | +### C7043.2.9 Removed: `ordr` -For each reorder item: - -| ∆ | Bits | Name | Description | -|---|------|----------|----------------------------------------------------------| -|16+| 32 | elements | elem: string of elements, index into `elem` section | -|20+| 32 | before | elem: look-behind required match, index into `elem` | - -- `elements`: index into the `elem` section, coding `from`, `order`, `tertiary`, - `tertiary_base`, and `prebase` properties. -- `before`: follows `transform/@before` +_Reorder entries are now a subtable of `tran`/`bksp`, see [`tran`/`bksp`](#c70432112-tranreorders-subtable)_ ### C7043.2.10 `strs`—Strings @@ -227,41 +210,83 @@ A distinction between zero-length string and optional should be avoided (e.g. the difference between "" and null in Javascript). If this is truly required, a separate flag field must be used to denote the difference. -### C7043.2.11 `tran`, `finl`, and `bksp`—Transforms +### C7043.2.11 `tran`, and `bksp`—Transforms -All three of these tables have the same format. and differ only in their -identity. The simple transform table has the ident `tran`; the final transform -table has the ident `finl`, and the backspaces table has the ident `bksp`. +These tables represent the `` element. +Both of these tables have the same format. and differ only in their +identity. The `simple` transform table has the ident `tran` +and the `backspace` table has the ident `bksp`. -| ∆ | Bits | Name | Description | -|---|------|----------|------------------------------------------| -| 0 | 32 | ident | `tran` / `finl` / `bksp` | -| 4 | 32 | size | int: Length of section | -| 8 | 32 | count | int: Number of transforms | +| ∆ | Bits | Name | Description | +|---|------|----------------|------------------------------------------| +| 0 | 32 | ident | `tran` / `bksp` | +| 4 | 32 | size | int: Length of section | +| 8 | 32 | groupCount | int: Number of transformGroups | +|12 | 32 | transformCount | int: Number of transforms | +|16 | 32 | reorderCount | int: Number of reorders | +|20+| - | groups | transformGroup subtable | +| - | - | transforms | transforms subtable | +| - | - | reorders | reorders subtable | + +The transformGroups are in file order. + +Each transformGroup is either a `transform` group or a `reorder` group. + +### C7043.2.11.1 `tran.groups` subtable + +For each transformGroup: + +| ∆ | Bits | Name | Description | +|---|------|---------|------------------------------------------| +| 0+| 32 | type | int: type of transformgroup | +| 4+| 32 | count | int: number of items in this group | +| 8+| 32 | index | int: index into subtable | + +- `type`: one of the following: + + - `0`: `transform` elements beginning at 'index' + - `1`: `reorder` elements beginning at 'index' The transforms are sorted in binary order based on the `from` field. -For each transform: +### C7043.2.11.1 `tran.transforms` subtable + +For each transform in the subtable: | ∆ | Bits | Name | Description | |---|------|---------|------------------------------------------| -|16+| 32 | from | elem: combination of characters | -|20+| 32 | to | str: output text | -|24+| 32 | before | elem: look-behind text (0 = omitted) | -|28+| 32 | flags | int: per-transform flags | +| 0+| 32 | from | str: processed regex of from= side | +| 4+| 32 | to | str: output pattern | +| 8+| 32 | mapFrom | str: name of set variable for 'from' $1 | +| 8+| 32 | mapTo | str: name of set variable for 'to' $1 | - `from`: the source text, index into `elem` section. - `to`: sequence of Unicode codepoints that replace `from`. May be the null string for `bksp` entries. -- `before`: optional look-behind text occurring before `from`, index into `elem` - section -- `flags`: a 32-bit bitfield defined as below: +- `mapFrom` and `mapTo` work as a pair. If present, implementation must: + - use `from` as the regex to match against + - search `mapFrom` for the value of captured group 1 ($1) + - replace the entire matched `from` regex with the same indexed value in `mapTo` + - `to` will be null in this case + - Debugging note: The variables table can be searched for matching `elem` pointers. + - For example, from="($[upper])" to="$[1:lower]" will result in: + mapFrom: "upper" + mapTo: "lower" + +### C7043.2.11.2 `tran.reorders` subtable - | Bit position | Meaning | Description | - |--------------|----------|----------------------| - | 0 | error | 1: `error="fail"` | +For each reorder item: + +| ∆ | Bits | Name | Description | +|---|------|----------|----------------------------------------------------------| +| 0+| 32 | elements | elem: string of elements, index into `elem` section | +| 4+| 32 | before | elem: look-behind required match, index into `elem` | + +- `elements`: index into the `elem` section, coding `from`, `order`, `tertiary`, + `tertiaryBase`, and `preBase` properties. +- `before`: follows `reorder/@before` ### C7043.2.12 `vkey`—VKey Map @@ -526,6 +551,79 @@ representing a 0-length string. These indices are a pool of indexes into the string table. The strings order are significant. There is not a 'null' string at the end of each list. +### C7043.2.17 `vars`—Variables + +| ∆ | Bits | Name | Description | +|---|------|---------------|------------------------------------------| +| 0 | 32 | ident | `vars` | +| 4 | 32 | size | int: Length of section | +| 8 | 32 | markers | list: Index to marker list, or 0 | +|12 | 32 | varCount | int: Total number of variable elements | +| - | var | varEntries | variables sub-table | + +#### C7043.2.17.1 `vars.varEntries` subtable + +For each variable, + +| ∆ | Bits | Name | Description | +|---|------|---------|------------------------------------------| +| 0+| 32 | type | int: Type of variable | +| 4+| 32 | id | str: Variable's id | +| 8+| 32 | value | str: Value in string form | +|12+| 32 | elem | elem: Set as array | + +- `type` is per below: + - 0 : string + - 1 : set + - 2 : unicodeSet + +TODO-LDML: note that at present, unicodeSet variables are not stored using the `uset` type + +Items are sorted by id + +### C7043.2.18 `uset` UnicodeSets table + +This table contains serialized [UnicodeSet](http://www.unicode.org/reports/tr35/#Unicode_Sets)s, +together with their original patterns. Each UnicodeSet is converted into an array of ranges. +Per the Keyboard spec, sets with multi-character strings are not allowed. + +For example, `[a-c q]` would be converted into the two ranges (U+0061-U+0063) and (U+0071-U+0071), +the latter being the single codepoint `q`. + +| ∆ | Bits | Name | Description | +|---|------|---------------|------------------------------------------| +| 0 | 32 | ident | `uset` | +| 4 | 32 | size | int: Length of section | +| 8 | 32 | usetCount | int: Total number of range lists | +|12 | 32 | rangeCount | int: Total number of range elements | +|16 | var | usets | uset list sub-table | +| - | var | ranges | range sub-table | + +#### `uset.usets` sub-table + +Each entry in this subtable represents a UnicodeSet, and is referenced by +index by other tables. + +| ∆ | Bits | Name | Description | +|---|------|---------|------------------------------------ | +| 0+| 32 | range | int: Index into 'ranges' subtable | +| 4+| 32 | count | int: Number of ranges in this set | +| 8+| 32 | pattern | str: UnicodeSet as string | + +The usets are sorted in order of their pattern string's binary +order. + +#### `uset.ranges` sub-table + +Each represents a UnicodeSet + +| ∆ | Bits | Name | Description | +|---|------|---------|------------------------------------ | +| 0+| 32 | start | int: UTF-32 char start | +| 4+| 32 | end | int: UTF-32 char end | + +`start` is always <= `end`. `start` and `end` may be equal if a single codepoint +is represented. + ## TODO-LDML: various things that need to be completed here or fixed in-spec -> * UnicodeSets > * spec: ABNT2 key has hex value 0xC1 (even if kbdus.dll doesn't produce that) diff --git a/core/src/ldml/C7532_ldml_updating.md b/core/src/ldml/C7532_ldml_updating.md index 168d205cea3ff959ee5a8f36e3dbceb5182c1d79..a4978902f5c84c1027e8f15dc048a6a8a13abf41 100644 --- a/core/src/ldml/C7532_ldml_updating.md +++ b/core/src/ldml/C7532_ldml_updating.md @@ -10,8 +10,8 @@ working on ‘layr’, using ‘disp’ as a model from https://github.com/keyma ## Constants and Scaffolding - *Edit/Commit*: `core/include/ldml/keyboardprocessor_ldml.ts` - - update `SectionIdent` and keep in order - - update `SectionMap` and keep in order + - update `SectionIdent` and keep in order: `'layr' |` + - update `Constants.section` (near the end of the file) and keep in order: `'layr': 'layr',` - add a comment block in order `layr section` - add `length_layr` with the nonvariable length - add a `length_layr_*` for each subitem @@ -50,6 +50,7 @@ working on ‘layr’, using ‘disp’ as a model from https://github.com/keyma - add a test case such as `developer/src/kmc-ldml/test/test-key2.ts` - add a compiler - `developer/src/kmc-ldml/compiler/key2.ts` + - make sure the compiler has an `id()` function returning the correct id, such as `key2` - link it in to `developer/src/kmc-ldml/src/compiler/compiler.ts` - add the import - add to `SECTION_COMPILERS` @@ -66,6 +67,7 @@ working on ‘layr’, using ‘disp’ as a model from https://github.com/keyma - Finally, add `this.emitSection(file, this.file.COMP_PLUS_DISP, this.sect_disp);` to `compile()` — and, in order. - Note that some variable length parts (such as the actual text data in `strs`) are sometimes in a separate emit function. Anything that's not in the `COMP_PLUS_STRS` `r.Struct` definition needs one of these. - Also note that restructure will happily ignore (write zeros for) any fields where the BUILDER_* fields don't match the COMP_PLUS_* fields. + - Pro Tip: If you see `Error: Not a fixed size` (or have any other data generation issues), double check that the `COMP_PLUS_DISP*` entries in `kmx-plus.ts` has _exactly_ the same properties (and property names!) as the `BUILDER_DISP*` structures in your `build-disp.ts` file. Restructure will happily insert zeros on mismatches, but complains if a variable length item doesn't match. - update basic.xml and basic.txt - Tweak `eveloper/src/kmc-ldml/test/fixtures/basic.xml` as needed diff --git a/core/src/ldml/C9134_ldml_markers.md b/core/src/ldml/C9134_ldml_markers.md new file mode 100644 index 0000000000000000000000000000000000000000..0b2436b97f461f9ce414ed33160583b8ba5f8f29 --- /dev/null +++ b/core/src/ldml/C9134_ldml_markers.md @@ -0,0 +1,79 @@ +# LDML Markers in Keyman + +## Background + +- This is the proposed implementation of markers for Keyman + +> Markers are placeholders which record some state, but without producing normal visible text output. They were designed particularly to support dead-keys. + +- See [tr35-keyboards-markers][] for source spec. + - [CLDR-16837][] has been filed to capture feedback on the marker spec, please add to that with any commnets. + +### Format + +- The general format is `\m{id}` where `id` is presumed to be `[0-9A-Za-z_]{1,32}` +- `\m{.}` in matching matches a single marker. + +### Location + +Markers can appear in both 'emitting' and 'matching-only' areas: + +#### Emitting + +- `key to=` for emitting markers +- `transform to=` to emit markers +- `string value=` for matching or emitting markers + +#### Match only + +- `transform from=` to match markers +- `display to=` for matching keys which contain markers + +## Theory / Encoding + +- Keyman already uses U_SENTINEL `U+FFFF` (noncharacter) +- The general proposal here is to use the sequence `U+FFFF U+XXXX` to represent marker #XXXX (starting with `U+0001`) +- `U+FFFF` cannot otherwise occur in text, so it is unique +- `U+FFFF U+FFFF` to indicate 'any marker' corresponds to `\m{.}` +- This scheme allows for 65,534 (0xFFFE) unique markers, from `U+FFFF U+0001` through `U+FFFF U+FFFE` + +## Terminology +- A marker's "number" is its position in the `markers` list, starting at index 1 (U+0001) being the first element in that list. +Note that this is different from other 0-based indices in KMX+. If there are three markers in a keyboard file, they will be numbered 1, 2, 3. +- A marker's "id" is its string name such as `a` or `acute` + +## Compiler (kmc) + +- `U+FFFF` needs to be illegal as a literal or escaped sequence. So `\u{FFFF}` is not allowed, for example, nor as a literal in the UTF-8 .xml stream. + +### `vars` + +- `vars` compiler will prescan all of the elements listed under [Emitting](#emitting), but not match-only. +- All found markers will be added to a single master `list`, which will be added under the `vars.markers` section. +- Subsequent processes/compilers will be able to check this list for any missing markers. For example, a `` can then trigger an error failing validation, because nothing is defined which emits that marker. + +### Other sections + +- `string value='\m{…}'` will simply store `\m{…}`, for application when expanded as with other variables. +- Other emitters, such as `key`, `transform` will include the string `U+FFFF U+XXXX` where XXXX corresponds to the marker's 1-based number. +- Transforms will need to match against the marker or markers desired, so may need to emit sequences such as `(?:\uFFFF\u0123)` meaning a match to marker #0x0123 +- matching `\m{.}` may then expand to `(?:\uFFFF.)` + +## Binary (.kmx plus) + +- The `vars.markers` is a pointer into the `list` section with a list (binary order) of the marker names +- Other strings will be in `U+FFFF U+0123` form etc. as if it was in the original text stream as such. + +## Implementation (core) + +- Core needs to recognize `U+FFFF …` sequences and convert them to markers in the context stream, with `state->context().push_marker(marker_number)` +- For normal processing, Core does _not_ need to correlate the marker _number_ with a marker _id_, although this would be helpful for a debugging or tracing facility. I.e. `U+FFFF U+0123` corresponding to entry 0x0123 in the `vars.markers` -> `list` table. +- Core needs to remove `U+FFFF …` sequences before they are passed to the OS. + +- Transform processing needs to recognize these markers in the context stream and pass them to the transforms appropriately. +- User-defined backspace processing ` may specifically operate on backspaces in the context stream, just as with other transform processing. +- The default backspace processing needs to recognize these markers in the context stream and remove them as appropriate. + + +[tr35-keyboards-markers]: https://github.com/unicode-org/cldr/blob/keyboard-preview/docs/ldml/tr35-keyboards.md#markers +[CLDR-16837]: https://unicode-org.atlassian.net/browse/CLDR-16837 diff --git a/core/src/ldml/ldml_processor.cpp b/core/src/ldml/ldml_processor.cpp index 8f1f02d4b0b051fe994fad4e7153a1de7af5e876..41ed5d419f2617b244c392282eab8730cf940eaa 100644 --- a/core/src/ldml/ldml_processor.cpp +++ b/core/src/ldml/ldml_processor.cpp @@ -36,7 +36,7 @@ namespace kbp { ldml_processor::ldml_processor(path const & kb_path, const std::vector &data) : abstract_processor( keyboard_attributes(kb_path.stem(), KM_KBP_LMDL_PROCESSOR_VERSION, kb_path.parent(), {}) - ), _valid(false), transforms(), keys() + ), _valid(false), transforms(), bksp_transforms(), keys() { if(data.size() <= sizeof(kmx::COMP_KEYBOARD_EX)) { @@ -71,7 +71,8 @@ ldml_processor::ldml_processor(path const & kb_path, const std::vector auto keyEntry = kplus.key2Helper.getKeys(kmapEntry->key); assert(keyEntry != nullptr); - if (keyEntry->flags && LDML_KEYS_KEY_FLAGS_EXTEND) { + // TODO-LDML: LDML_KEYS_KEY_FLAGS_NOTRANSFORM + if (keyEntry->flags & LDML_KEYS_KEY_FLAGS_EXTEND) { if (nullptr == kplus.strs) { DebugLog("for keys: kplus.strs == nullptr"); // need a string table to get strings assert(false); @@ -79,68 +80,27 @@ ldml_processor::ldml_processor(path const & kb_path, const std::vector } str = kplus.strs->get(keyEntry->to); } else { - str = keyEntry->get_string(); + str = keyEntry->get_to_string(); } keys.add((km_kbp_virtual_key)kmapEntry->vkey, (uint16_t)kmapEntry->mod, str); } } // else: no keys! but still valid. Just, no keys. - if (kplus.tran != nullptr) { - if (nullptr == kplus.elem) { - DebugLog("for tran: kplus.elem == nullptr"); - assert(false); - return; - } - if (nullptr == kplus.strs) { - DebugLog("for tran: kplus.strs == nullptr"); // need a string table to get strings - assert(false); - return; - } - for (KMX_DWORD i = 0; i < kplus.tran->count; i++) { - const kmx::COMP_KMXPLUS_TRAN_ENTRY &entry = kplus.tran->entries[i]; - // 'to' is always a string, unlike keys - const std::u16string tostr = kplus.strs->get(entry.to); - // now, fetch the 'from' - ldml::string_list list; - - // TODO-LDML: before= - // TODO-LDML: error= - - // process From - const KMX_DWORD elemNo = entry.from; - if (elemNo >= kplus.elem->count) { - DebugLog("tran[%d].from = %d, out of range for elem->count=%d", i, elemNo, kplus.elem->count); - assert(false); - return; - } - // TODO-LDML: refactor this - KMX_DWORD elemListLength = 0; - const kmx::COMP_KMXPLUS_ELEM_ELEMENT *elemList = kplus.elem->getElementList(elemNo, elemListLength); - if (elemList == nullptr) { - DebugLog("tran[%d].from = %d, could not load element list", i, elemNo); - assert(false); - return; - } - if (elemListLength == 0) { - DebugLog("tran[%d].from = %d, element list has 0 length", i, elemNo); - assert(false); - return; - } + // load transforms + if (kplus.tran != nullptr && kplus.tran->groupCount > 0) { + transforms.reset(km::kbp::ldml::transforms::load(kplus, kplus.tran, kplus.tranHelper)); + if (!transforms) { + DebugLog("Failed to load tran transforms"); + return; // failed to load + } + } - // now we have an array of elements - for (KMX_DWORD e = 0; e < elemListLength; e++) { - const kmx::COMP_KMXPLUS_ELEM_ELEMENT &element = elemList[e]; - std::u16string str; - if (element.flags && LDML_ELEM_FLAGS_UNICODE_SET) { - str = kplus.strs->get(element.element); - // TODO-LDML: actually a UnicodeSet here - } else { - str = element.get_string(); // Get single UTF-16 - } - list.push_back(str); // add the string to the end - } - // Now, add the list to the map - transforms.add(list, tostr); + // load bksp transforms + if (kplus.bksp != nullptr && kplus.bksp->groupCount > 0) { + bksp_transforms.reset(km::kbp::ldml::transforms::load(kplus, kplus.bksp, kplus.bkspHelper)); + if (!bksp_transforms) { + DebugLog("Failed to load bksp transforms"); + return; // failed to load } } // Only valid if we reach here @@ -235,6 +195,17 @@ ldml_processor::process_event( switch (vk) { case KM_KBP_VKEY_BKSP: { + if (!!bksp_transforms) { + // TODO-LDML: process bksp + // std::u16string outputString; + // // don't bother if no backspace transforms! + // // TODO-LDML: unroll ctxt into a str + // std::u16string ctxtstr; + // for (size_t i = 0; i < ctxt.size(); i++) { + // ctxtstr.append(ctxt[i]); + // } + // const size_t matchedContext = transforms->apply(ctxtstr, outputString); + } KMX_DWORD last_char = 0UL; // attempt to get the last char auto end = state->context().rbegin(); @@ -244,75 +215,95 @@ ldml_processor::process_event( } } if (last_char == 0UL) { + /* + We couldn't find a character at end of context (context is empty), + so we'll pass the backspace keystroke on to the app to process; the + app might want to use backspace to move between contexts or delete + a text box, etc. Or it might be a legacy app and we've had our caret + dumped in somewhere unknown, so we will have to depend on the app to + be sensible about backspacing because we know nothing. + */ state->actions().push_backspace(KM_KBP_BT_UNKNOWN); } else { state->actions().push_backspace(KM_KBP_BT_CHAR, last_char); + state->context().pop_back(); } - state->context().pop_back(); } break; default: - // from kmx_processor.cpp - // Construct a context buffer from the items up until the last KM_KBP_CT_MARKER marker - ldml::string_list ctxt; - auto cp = state->context(); - // We're only interested in as much of the context as is a KM_KBP_CT_CHAR. - // This will stop at the KM_KBP_CT_MARKER type. - uint8_t last_type = KM_KBP_BT_UNKNOWN; - for (auto c = cp.rbegin(); c != cp.rend(); c++) { - last_type = c->type; - if (last_type != KM_KBP_BT_CHAR) { - break; + { + // adapted from kmx_processor.cpp + + /** a copy of the current/changed context, for transform use */ + ldml::string_list ctxt; + + // Construct a context buffer of all the KM_KBP_BT_CHAR items + // Extract the context into 'ctxt' for transforms to process + if (!!transforms) { + // if no transforms, no reason to do this extraction (ctxt will remain empty) + auto &cp = state->context(); + // We're only interested in as much of the context as is a KM_KBP_BT_CHAR. + uint8_t last_type = KM_KBP_BT_UNKNOWN; + for (auto c = cp.rbegin(); c != cp.rend(); c++) { + last_type = c->type; + if (last_type != KM_KBP_BT_CHAR) { + // not a char, stop here + // TODO-LDML: markers? + break; + } + ctxt.emplace_front(1, c->character); + // extract UTF-32 to 1 or 2 UTF-16 chars in a string + } } - km::kbp::kmx::char16_single buf; - const int len = km::kbp::kmx::Utf32CharToUtf16(c->character, buf); - const std::u16string str(buf.ch, len); - ctxt.push_front(str); // prepend to string - } - if (last_type != KM_KBP_BT_MARKER) { - // There was no beginning-of-translation marker. - //Add one. - state->context().push_marker(0x0); - state->actions().push_marker(0x0); - } - // Look up the key - const std::u16string str = keys.lookup(vk, modifier_state); - if (str.empty()) { - // not found - state->actions().commit(); // finish up and - return KM_KBP_STATUS_OK; // Nothing to do- no key - } - const std::u32string str32 = kmx::u16string_to_u32string(str); - for(size_t i=0; icontext().push_character(str32[i]); - state->actions().push_character(str32[i]); - } - // add the newly added char - ctxt.push_back(str); - // Now process transforms - std::u16string outputString; - // Process the transforms - const size_t matchedContext = transforms.matchContext(ctxt, outputString); - - if (matchedContext > 0) { - // Found something. - // Now, clear out the old context - for (size_t i = 0; i < matchedContext; i++) { - state->context().pop_back(); // Pop off last - auto deletedChar = ctxt[ctxt.size() - i - 1][0]; - state->actions().push_backspace(KM_KBP_BT_CHAR, deletedChar); // Cause prior char to be removed + + // Look up the key + const std::u16string str = keys.lookup(vk, modifier_state); + if (str.empty()) { + // not found, so pass the keystroke on to the Engine + state->actions().push_invalidate_context(); + state->actions().push_emit_keystroke(); + break; // ----- commit and exit + } + // found the correct string - push it into the context and actions + const std::u32string str32 = kmx::u16string_to_u32string(str); + for (const auto &ch : str32) { + state->context().push_character(ch); + state->actions().push_character(ch); } - // Now, add in the updated text - const std::u32string outstr32 = kmx::u16string_to_u32string(outputString); - for (size_t i = 0; i < outstr32.length(); i++) { - state->context().push_character(outstr32[i]); - state->actions().push_character(outstr32[i]); + // Now process transforms + // Process the transforms + if (!!transforms) { + // add the newly added char to ctxt + ctxt.push_back(str32); + + std::u32string outputString; + + // TODO-LDML: unroll ctxt into a str. Would be better to have transforms be able to process a vector + std::u32string ctxtstr; + for (const auto &ch : ctxt) { + ctxtstr.append(ch); + } + // check if the context matched, and if so how much (at the end) + const size_t matchedContext = transforms->apply(ctxtstr, outputString); + + if (matchedContext > 0) { + // Found something. + // Now, clear out the old context + for (size_t i = 0; i < matchedContext; i++) { + state->context().pop_back(); // Pop off last + auto deletedChar = ctxt[ctxt.size() - i - 1][0]; + state->actions().push_backspace(KM_KBP_BT_CHAR, deletedChar); // Cause prior char to be removed + } + // Now, add in the updated text + for (const auto &ch : outputString) { + state->context().push_character(ch); + state->actions().push_character(ch); + } + } } - // Add a marker so we don't retransform this text. - state->context().push_marker(0x0); - state->actions().push_marker(0x0); } } + // end of normal processing: commit and exit state->actions().commit(); } catch (std::bad_alloc &) { state->actions().clear(); diff --git a/core/src/ldml/ldml_processor.hpp b/core/src/ldml/ldml_processor.hpp index 8a4dfb02e6d8f5f839fdb76571b23e49c1d76533..a098960a6a9f74e9f0a92a1b24cc0d1deb60fd8e 100644 --- a/core/src/ldml/ldml_processor.hpp +++ b/core/src/ldml/ldml_processor.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include "processor.hpp" #include "option.hpp" @@ -26,7 +27,7 @@ namespace kbp { class ldml_processor : public abstract_processor { private: bool _valid; - ldml::transforms transforms; + std::unique_ptr transforms, bksp_transforms; ldml::vkeys keys; public: ldml_processor( diff --git a/core/src/ldml/ldml_transforms.cpp b/core/src/ldml/ldml_transforms.cpp index 86982c79726fd1342fbdf9a66089781351f671e2..4d469abfd973f422395b1c40a7b5fcc7e8b1476a 100644 --- a/core/src/ldml/ldml_transforms.cpp +++ b/core/src/ldml/ldml_transforms.cpp @@ -6,62 +6,670 @@ */ #include "ldml_transforms.hpp" +#include "debuglog.h" +#include +#include +#include "kmx/kmx_xstring.h" + + +#ifndef assert +#define assert(x) // TODO-LDML +#endif namespace km { namespace kbp { namespace ldml { -transforms::transforms() : simple() { +/** + * \def KMXPLUS_DEBUG_TRANSFORM + * define KMXPLUS_DEBUG_TRANSFORM=1 to enable verbose processing of transforms/reorders + * The default is 0, which only notes initialization and exceptional cases +*/ + +#ifndef KMXPLUS_DEBUG_TRANSFORM +#define KMXPLUS_DEBUG_TRANSFORM 0 +#endif + +#if KMXPLUS_DEBUG_TRANSFORM +#define DebugTran(msg, ...) DebugLog(msg, ##__VA_ARGS__) +#else +#define DebugTran(msg, ...) +#endif + +element::element(const USet &new_u, KMX_DWORD new_flags) + : chr(), uset(new_u), flags((new_flags & ~LDML_ELEM_FLAGS_TYPE) | LDML_ELEM_FLAGS_TYPE_USET) { +} + +element::element(km_kbp_usv ch, KMX_DWORD new_flags) + : chr(ch), uset(), flags((new_flags & ~LDML_ELEM_FLAGS_TYPE) | LDML_ELEM_FLAGS_TYPE_CHAR) { +} + +bool +element::is_uset() const { + return (flags & LDML_ELEM_FLAGS_TYPE) == LDML_ELEM_FLAGS_TYPE_USET; +} + +signed char +element::get_order() const { + unsigned char uorder = ((flags & LDML_ELEM_FLAGS_ORDER_MASK) >> LDML_ELEM_FLAGS_ORDER_BITSHIFT); + return (signed char)uorder; +} + +signed char +element::get_tertiary() const { + unsigned char uorder = ((flags & LDML_ELEM_FLAGS_TERTIARY_MASK) >> LDML_ELEM_FLAGS_TERTIARY_BITSHIFT); + return (signed char)uorder; +} + +bool +element::is_prebase() const { + return !!(flags & LDML_ELEM_FLAGS_PREBASE); +} + +bool +element::is_tertiary_base() const { + return !!(flags & LDML_ELEM_FLAGS_TERTIARY_BASE); +} + +KMX_DWORD +element::get_flags() const { + return flags; +} + +bool +element::matches(km_kbp_usv ch) const { + if (is_uset()) { + return uset.contains(ch); + } else { + return chr == ch; + } +} + +void +element::dump() const { + if (is_uset()) { + DebugLog("element order=%d USET", (int)get_order()); + uset.dump(); + } else { + DebugLog("element order=%d U+%04X", (int)get_order(), (int)chr); + } +} + +int +reorder_sort_key::compare(const reorder_sort_key &other) const { + int primaryResult = (int)primary - (int)other.primary; + int secondaryResult = (int)secondary - (int)other.secondary; + int tertiaryResult = (int)tertiary - (int)other.tertiary; + int quaternaryResult = (int)quaternary - (int)other.quaternary; + + if (primaryResult) { + return primaryResult; + } else if (secondaryResult) { + return secondaryResult; + } else if (tertiaryResult) { + return tertiaryResult; + } else if (quaternaryResult) { + return quaternaryResult; + } else { + // We don't expect to get here. quaternaryResult is the string index, which + // should be unequal. + assert(quaternaryResult); + // We have the underlying character, so use the binary order as a tiebreaker. + int identityResult = (int)ch - (int)other.ch; // tie breaker + return identityResult; + } +} + +bool +reorder_sort_key::operator<(const reorder_sort_key &other) const { + return (compare(other) < 0); +} + +bool +reorder_sort_key::operator>(const reorder_sort_key &other) const { + return (compare(other) > 0); +} + +std::deque +reorder_sort_key::from(const std::u32string &str) { + // construct a 'baseline' sort key, that is, in the absence of + // any match rules. + std::deque keylist; + auto s = str.begin(); // str iterator + size_t c = 0; // str index + for (auto e = str.begin(); e < str.end(); e++, s++, c++) { + // primary weight: 0 + // seconary weight: c (the string index) + // tertiary weight: 0 + // quaternary weight: c (the index again) + keylist.emplace_back(reorder_sort_key{*s, 0, c, 0, c}); + } + return keylist; } void -transforms::add(const string_list &list, const std::u16string &tostr) { - simple.insert({list, tostr}); +reorder_sort_key::dump() const { + // for debugging… + DebugLog("- U+%04X\t(%d, %d, %d, %d)", ch, (int)primary, (int)secondary, (int)tertiary, (int)quaternary); } size_t -transforms::matchContext(const string_list &ctxt, std::u16string &outputString) const { - if (simple.size() == 0) { - return 0; // no transforms, no match - } - - // int longest_index = -1; // index to longest match in simple_transforms - size_t longest_length = 0; // length of longest match - // auto i = 0; // counter - // TODO-LDML: need to handle partial matches. - // bool partial = false; // true if the longest was a partial match - for (auto it = simple.begin(); it != simple.end(); it++ /*,i++*/) { - auto list = it->first; - // see if it matches - if (longest_length >= list.size()) { - continue; // already have a longer match +element_list::match_end(const std::u32string &str) const { + if (str.size() < size()) { + // input string too short, can't possibly match. + // This assumes each element is a single char, no string elements. + return 0; + } + // s: iterate from end to front of string + // For example, if str = 'abcd', we try to match 'd', then 'c', then 'b', then 'a' + // starting with the end of the element list. + auto s = str.rbegin(); + // e: end to front on elements. + // we know the # of elements is <= length of string, + // so we don't need to check the string's boundaries + for (auto e = rbegin(); e < rend(); e++) { + assert(s < str.rend()); // double check + if (!e->matches(*s)) { + // break out if this element doesn't match + return 0; + } + s++; + } + return size(); // match size = element size +} + +bool +element_list::load(const kmx::kmx_plus &kplus, kmx::KMXPLUS_ELEM id) { + KMX_DWORD elementsLength; + auto elements = kplus.elem->getElementList(id, elementsLength); // pointer to beginning of element list + assert((elementsLength == 0) || (elements != nullptr)); // it could be a 0-length list + for (size_t i = 0; i & +element_list::update_sort_key(size_t offset, std::deque &key) const { + /** string index */ + size_t c = 0; + for (auto e = begin(); e < end(); e++, c++) { + /** update this key */ + auto &k = key.at(offset + c); + // we double check that the character matches. otherwise something + // has really gone awry, because we shouldn't be here if this element list doesn't apply. + if (!e->matches(k.ch)) { + DebugLog("!! Internal Error: updateSortKey(%d+%d): element did not re-match the sortkey", offset, c); + k.dump(); + // TODO-LDML: assertion follows + assert(e->matches(k.ch)); // double check that this element matches } - if (list.size() > ctxt.size()) { - continue; // not enough ctxt for a match (TODO-LDML: partial match.) + // we only update primary and tertiary weights + k.primary = e->get_order(); + // TODO-LDML: need more detailed tertiary work + k.tertiary = e->get_tertiary(); +#if KMXPLUS_DEBUG_TRANSFORM + DebugTran("Updating at +%d", c); + k.dump(); +#endif + } + return key; +} + +void +element_list::dump() const { + DebugLog("element_list[%d]", size()); + for (const auto &e : *this) { + e.dump(); + } +} + +reorder_entry::reorder_entry(const element_list &new_elements) : elements(new_elements), before() { +} +reorder_entry::reorder_entry(const element_list &new_elements, const element_list &new_before) : elements(new_elements), before(new_before) { +} + +size_t +reorder_entry::match_end(std::u32string &str, size_t offset, size_t len) const { + auto substr = str.substr(offset, len); + // first, see if the elements match. If not, this entry doesn't apply + size_t match_len = elements.match_end(substr); + if (match_len == 0) { + return 0; + } + + // Now we need to check if there is a "before=" element string that + // is also a precondition. + if (!before.empty()) { + // does not match before offset + std::u32string prefix = substr.substr(0, substr.size() - match_len); + // make sure the 'before' is present + if (before.match_end(prefix) == 0) { + return 0; // break out. } - // Now, check for match - bool is_match = true; - for (size_t j = 0; j < list.size(); j++) { - const auto found = ctxt.at(ctxt.size() - j - 1); - const auto expect = list.at(list.size() - j - 1); - if (found != expect) { - // mismatch, break - is_match = false; - break; + } + return match_len; +} + +bool +reorder_group::apply(std::u32string &str) const { + /** did we apply anything */ + bool applied = false; + /** did we match anything */ + bool some_match = false; + + // get a baseline sort key + auto sort_keys = reorder_sort_key::from(str); + + // apply ALL reorders in the group. + for (const auto &r : list) { + // work backward from end of string forward + // That is, see if "abc" matches "abc" or "ab" or "a" + for (size_t s = str.size(); s > 0; s--) { + size_t submatch = r.match_end(str, 0, s); + if (submatch != 0) { +#if KMXPLUS_DEBUG_TRANSFORM + DebugTran("Matched: %S (off=%d, len=%d)", str.c_str(), 0, s); + r.elements.dump(); +#endif + // update the sort key + size_t sub_match_start = s - submatch; + r.elements.update_sort_key(sub_match_start, sort_keys); + some_match = true; // record that there was a match } } - if (!is_match) { - continue; + } + if (!some_match) { + // get out if nothing matched. + // the sortkey won't be "interesting", and the sort + // will be a no-op. + DebugTran("Skip: No reorder elements matched."); + return false; // nothing matched, so no work. + } + +#if KMXPLUS_DEBUG_TRANSFORM + DebugTran("Updated sortkey"); + for (const auto &r : sort_keys) { + r.dump(); + } +#endif + + // TODO-LDML: for now, assume matches entire string. + // A needed optimization here would be to detect a common substring + // at the end of the old and new strings, and keep the match_len + // minimal. This reduces thrash in core's context. + size_t match_len = str.size(); + + // 'prefix' is the unmatched string before the match + // TODO-LDML: right now, this is empty. + std::u32string prefix = str; + prefix.resize(str.size() - match_len); // just the part before the matched part. + + // Now, we need to actually do the sorting, but we must only sort + // 'runs' beginning with 0-weight keys. + + // Consider the 'roast' example in the spec, you might end up with the following: + // codepoint (pri, sec, ter, quat) + // U+1A21 (0, 0, 0, 0) + // U+1A60 (127, 1, 0, 1) + // U+1A45 (0, 2, 0, 2) + // U+1A6B (42, 3, 0, 3) + // U+1A76 (55, 4, 0, 4) + // This example happens to be in order, but must be sorted in two diferent ranges, + // with secondary (index) values of [0,1] and [2,4] + // + // Another example might look like the following: + // U+1A21 (0, 0, 0, 0) + // U+1A6B (42, 1, 0, 1) + // U+1A76 (55, 2, 0, 2) + // U+1A60 (10, 3, 0, 3) + // U+1A45 (10, 4, 0, 3) + // Here there is only a single range to sort [0,4] + + /** pointer to the beginning of the current run. */ + std::deque::iterator run_start = sort_keys.begin(); + for(auto e = run_start; e != sort_keys.end(); e++) { + if ((e->primary == 0) && (e != run_start)) { // it's a base + auto run_end = e - 1; + DebugTran("Sorting subrange quaternary=[%d..]", run_start->quaternary); + std::sort(run_start, run_end); // reversed because it's a reverse iterator…? + // move the start + run_start = e; // next run starts here + } + } + // sort the last run in the string as well. + if (run_start != sort_keys.end()) { // TODO-LDML: skip if a single-char run + DebugTran("Sorting final subrange quaternary=[%d..]", run_start->quaternary); + std::sort(run_start, sort_keys.end()); // reversed because it's a reverse iterator…? + } + // recombine into a string by pulling out the 'ch' value + // that's in each sortkey element. + std::u32string newSuffix; + size_t q = sort_keys.begin()->quaternary; // start with the first quaternary + for (auto e = sort_keys.begin(); e < sort_keys.end(); e++, q++) { + if (q != e->quaternary) { + // something rearranged in this subrange, because the quaternary values are out of order. + applied = true; + } + // collect the characters + newSuffix.append(1, e->ch); + } + if (applied) { + str.resize(prefix.size()); + str.append(newSuffix); + } else { + DebugTran("Skip: sorting caused no reordering"); + } +#if KMXPLUS_DEBUG_TRANSFORM + DebugTran("Sorted sortkey"); + for (const auto &r : sort_keys) { + r.dump(); + } +#endif + return applied; +} + +transform_entry::transform_entry(const std::u32string &from, const std::u32string &to) : fFrom(from), fTo(to) { +} + +size_t +transform_entry::match(const std::u32string &input) const { + if (input.length() < fFrom.length()) { + // TODO-LDML: regex + // Too small, can't match. + return 0; + } + // string at end + auto substr = input.substr(input.length() - fFrom.length(), fFrom.length()); + if (substr != fFrom) { + // end of string doesn't match + return 0; + } + // match length == fFrom.length + return substr.length(); +} + +std::u32string +transform_entry::apply(const std::u32string & /*input*/, size_t /*matchLen*/) const { + // TODO-LDML: regex + // For now, we just return the 'to' string literally. + return fTo; +} + +any_group::any_group(const transform_group &g) : type(any_group_type::transform), transform(g), reorder() { +} +any_group::any_group(const reorder_group &g) : type(any_group_type::reorder), transform(), reorder(g) { +} + +transforms::transforms() : transform_groups() { +} + +void +transforms::addGroup(const transform_group &s) { + transform_groups.emplace_back(s); +} + +void +transforms::addGroup(const reorder_group &s) { + transform_groups.emplace_back(s); +} + +transform_group::transform_group() { +} + +/** + * return the first transform match in this group + */ +const transform_entry * +transform_group::match(const std::u32string &input, size_t &subMatched) const { + for (auto transform = begin(); (subMatched == 0) && (transform < end()); transform++) { + // TODO-LDML: non regex implementation + // is the match area too short? + subMatched = transform->match(input); + if (subMatched != 0) { + return &(*transform); // return alias to transform } - // Found a match - longest_length = list.size(); - // longest_index = i; - outputString = it->second; } + return nullptr; +} - return longest_length; - // TODO-LDML: reorder - // TODO-LDML: final +/** + * Apply this entire transform set to the input. + * Example: input "abc" -> output="xyz", return=2: replace last two chars "bc" with "xyz", so final output = "abxyz"; + * @param input input string, will match at end: unmodified + * @param output on output: if return>0, contains text to replace + * @return match length: number of chars at end of input string to modify. 0 if no match. + */ +size_t +transforms::apply(const std::u32string &input, std::u32string &output) { + /** + * Example: + * Group0: za -> c, a -> bb + * Group1: bb -> ccc + * Group2: cc -> d + * Group3: tcd -> e + * + * Initial condition: matched = subMatched = 0. updatedInput = input = 'ta', output = '' + * + * Group0: matches 'a', subMatched=1, output='bb', updatedInput='tbb', matched=1 + * Initial match. subMatched=1 which is > the previous output.length, + * so matched += (1-0) == 1 + * + * Group1: matches 'bb', subMatched=2, output='ccc', updatedInput='tccc', matched=1 + * Although the transform group matched 2 chars ('bb'), the match in the original is 1 + * This time subMatched=2 but previous output.length was 2, so matched remains at 1 + * In other words, we didn't match outside of the existing match boundary. + * + * Group2: matches 'cc', subMatched=2, output='cd', updatedInput='tcd', matched=1 + * subMatched <= previous output.length, so no change to mathed + * + * Group3: matches 'tcd', subMatched=3, output='e', updatedInput='e', matched=2 + * Now subMatched=3, but previous output ('cd').length is only 2. + * In other words, we matched before the previous output's start - we matched the 't' also + * or this reason, matched+=(3-2) == 1. So matched is now 2. + */ + + /** + * Accumulate the maximum matched size of the end of 'input' here. + * It will be replaced with 'output'. + * Matched can increment. + */ + size_t matched = 0; + /** modified copy of input */ + std::u32string updatedInput = input; + for (auto group = transform_groups.begin(); group < transform_groups.end(); group++) { + // for each transform group + // break out once there's a match + // TODO-LDML: reorders + // Assume it's a non reorder group + /** Length of match within this group*/ + size_t subMatched = 0; + + // find the first match in this group (if present) + // TODO-LDML: check if reorder + if (group->type == any_group_type::transform) { + auto entry = group->transform.match(updatedInput, subMatched); + + if (entry != nullptr) { + // now apply the found transform + + // update subOutput (string) and subMatched + // the returned string must replace the last "subMatched" chars of the string. + std::u32string subOutput = entry->apply(updatedInput, subMatched); + + // remove the matched part of the updatedInput + updatedInput.resize(updatedInput.length() - subMatched); // chop of the subMatched part at end + updatedInput.append(subOutput); // subOutput could be empty such as in backspace transform + + if (subMatched > output.size()) { + // including first time through + // expand match by amount subMatched prior to output + matched += (subMatched - output.size()); + } // else: didn't match prior to the existing output, so don't expand 'match' + + // now update 'output' + if (subOutput.length() >= output.length() || subMatched > output.length()) { + output = subOutput; // replace all output + } else { + // replace output with new output + output.resize(output.length() - subMatched); + output.append(subOutput); + } + } + } else if (group->type == any_group_type::reorder) { + // TODO-LDML: cheesy solution. We should be finding a smaller + // common match here. + std::u32string str2 = updatedInput; + if (group->reorder.apply(str2)) { + // pretend the whole thing matched + output.resize(0); + output.append(str2); + updatedInput.resize(0); + updatedInput.append(str2); + matched = output.length(); + } + } + // else: continue to next group + } + /** + * TODO-LDML: optimization to contract 'matched' if possible. + * We could decrement 'matched' for every char of output + * which is already in input. Example (regex example): + * - str = "xxyyzz"; + * - s/zz$/z/ -> match=2, output='z' + * - s/z$/zz/ -> match=2, output='zz' + * (but could contract to match=0, output='' as already present) + * - s/zz$/zw/ -> match=2, output='zw' + * (but could contract to match=1, output='w') + * + * could also handle from="x" to="x" as match=0 + */ + return matched; +} + +bool +transforms::apply(std::u32string &str) { + // simple implementation for tests + std::u32string output; + size_t matchLength = apply(str, output); + if (matchLength == 0) { + return false; + } + str.resize(str.size() - matchLength); + str.append(output); + return true; +} + +transforms * +transforms::load( + const kmx::kmx_plus &kplus, + const kbp::kmx::COMP_KMXPLUS_TRAN *tran, + const kbp::kmx::COMP_KMXPLUS_TRAN_Helper &tranHelper) { + if (tran == nullptr) { + DebugLog("for tran: tran is null"); + assert(false); + return nullptr; + } + if (!tranHelper.valid()) { + DebugLog("for tran: tranHelper is invalid"); + assert(false); + return nullptr; + } + if (nullptr == kplus.elem) { + DebugLog("for tran: kplus.elem == nullptr"); + assert(false); + return nullptr; + } + if (nullptr == kplus.strs) { + DebugLog("for tran: kplus.strs == nullptr"); // need a string table to get strings + assert(false); + return nullptr; + } + if (nullptr == kplus.vars) { + DebugLog("for tran: kplus.vars == nullptr"); // need a vars table to get maps + assert(false); + return nullptr; + } + + // with that out of the way, let's set it up + + transforms *transforms = new ldml::transforms(); + + for (KMX_DWORD groupNumber = 0; groupNumber < tran->groupCount; groupNumber++) { + const kmx::COMP_KMXPLUS_TRAN_GROUP *group = tranHelper.getGroup(groupNumber); + // C++20 returns a reference here + // auto &newGroup = allGroups->emplace_back(); + + if (group->type == LDML_TRAN_GROUP_TYPE_TRANSFORM) { + transform_group newGroup; + + for (KMX_DWORD itemNumber = 0; itemNumber < group->count; itemNumber++) { + const kmx::COMP_KMXPLUS_TRAN_TRANSFORM *element = tranHelper.getTransform(group->index + itemNumber); + const std::u32string fromStr = kmx::u16string_to_u32string(kplus.strs->get(element->from)); + const std::u32string toStr = kmx::u16string_to_u32string(kplus.strs->get(element->to)); + std::u16string mapFrom, mapTo; + + if (element->mapFrom && element->mapTo) { + // strings: variable name of from/to + // TODO-LDML: not implemented + mapFrom = kplus.strs->get(element->mapFrom); + mapTo = kplus.strs->get(element->mapTo); + } + + newGroup.emplace_back(fromStr, toStr /* ,mapFrom, mapTo */); // creating a transform_entry + } + transforms->addGroup(newGroup); + } else if (group->type == LDML_TRAN_GROUP_TYPE_REORDER) { + reorder_group newGroup; + + // fetch each reorder in the group + for (KMX_DWORD itemNumber = 0; itemNumber < group->count; itemNumber++) { + const kmx::COMP_KMXPLUS_TRAN_REORDER *reorder = tranHelper.getReorder(group->index + itemNumber); + + element_list elements; + element_list before; + + bool load_ok = elements.load(kplus, reorder->elements) && before.load(kplus, reorder->before); + assert(load_ok); + if (load_ok) { + newGroup.list.emplace_back(elements, before); + } else { + DebugLog("reorder elements(%d+%d) failed to load", group->index, itemNumber); + return nullptr; + } + } + transforms->addGroup(newGroup); + } else { + // internal error - some other type - should have been caught by validation + DebugLog("ERROR: some other type"); + assert(false); + return nullptr; + } + } + return transforms; } } // namespace ldml diff --git a/core/src/ldml/ldml_transforms.hpp b/core/src/ldml/ldml_transforms.hpp index c3edff1f14ef3c54dd26159a7220b6fd951d1aba..6296399f2b35a11784f63ba0a28cef5a3e38a0bc 100644 --- a/core/src/ldml/ldml_transforms.hpp +++ b/core/src/ldml/ldml_transforms.hpp @@ -7,6 +7,7 @@ #pragma once +#include "kmx/kmx_plus.h" #include #include #include @@ -17,36 +18,227 @@ namespace km { namespace kbp { namespace ldml { +using km::kbp::kmx::USet; + +/** + * Type of a group + */ +enum any_group_type { + transform = LDML_TRAN_GROUP_TYPE_TRANSFORM, + reorder = LDML_TRAN_GROUP_TYPE_REORDER, +}; + +/** + * Corresponds to an 'elem' entry + */ +class element { +public: + /** construct from a USet */ + element(const USet &u, KMX_DWORD flags); + /** construct from a single char */ + element(km_kbp_usv ch, KMX_DWORD flags); + + /** @returns true if a USet type */ + bool is_uset() const; + /** @returns true if prebase bit set*/ + bool is_prebase() const; + /** @returns true if tertiary base bit set */ + bool is_tertiary_base() const; + /** @returns the primary order */ + signed char get_order() const; + /** @returns the tertiary order */ + signed char get_tertiary() const; + /** @returns raw elem flags */ + KMX_DWORD get_flags() const; + /** @returns true if matches this character*/ + bool matches(km_kbp_usv ch) const; + /** debugging: dump this element via DebugLog() */ + void dump() const; + +private: + // TODO-LDML: support multi-char strings? + const km_kbp_usv chr; + const USet uset; + const KMX_DWORD flags; +}; + +/** + * Inner element, representing + */ +class transform_entry { +public: + transform_entry( + const std::u32string &from, + const std::u32string &to + /*TODO-LDML: mapFrom, mapTo*/ + ); + + /** + * @returns length if it's a match + */ + size_t match(const std::u32string &input) const; + + /** + * @returns output string + */ + std::u32string apply(const std::u32string &input, size_t matchLen) const; + +private: + const std::u32string fFrom; // TODO-LDML: regex + const std::u32string fTo; +}; + /** * An ordered list of strings. */ -typedef std::deque string_list; +typedef std::deque string_list; + +/** + * a group of entries - a + */ +class transform_group : public std::deque { +public: + transform_group(); + + /** + * Find the first match in the group + * @param input input string to match + * @param subMatched on output, the matched length + * @returns alias to transform_entry or nullptr + */ + const transform_entry *match(const std::u32string &input, size_t &subMatched) const; +}; + +/** a single char, categorized according to reorder rules*/ +struct reorder_sort_key { + km_kbp_usv ch; // the single char value + signed char primary; // primary order value + size_t secondary; // index position + signed char tertiary; // tertiary value, defaults to 0 + size_t quaternary; // index again + + /** @returns -1, 0, 1 depending on ordering */ + int compare(const reorder_sort_key &other) const; + bool operator<(const reorder_sort_key &other) const; + bool operator>(const reorder_sort_key &other) const; + + /** create a 'baseline' sort key, with each character having primary weight 0 */ + static std::deque from(const std::u32string &str); + + /** TODO-LDML: for debugging. */ + void dump() const; +}; + +/** + * List of elements. + */ +class element_list : public std::deque { +public: + /** @returns 0 if no match, or number of chars at end matched */ + size_t match_end(const std::u32string &str) const; + + /** + * Update the deque (see reorder_sort_key::from()) with the weights from this element list + * starting at the beginning of this element list + * @param offset start at this offset in the deque. Still starts at the first element + * @param key key deque to update + * @returns the key parameter + */ + std::deque &update_sort_key(size_t offset, std::deque &key) const; + + /** construct from KMX+ elem id*/ + bool + load(const kmx::kmx_plus& kplus, kmx::KMXPLUS_ELEM id); + + /** TODO-LDML: for debugging */ + void dump() const; +}; + +class reorder_entry { +public: + /** construct an entry from elements */ + reorder_entry(const element_list &elements); + /** construct an element from elements and before */ + reorder_entry(const element_list &elements, const element_list &before); + /** + * Does it match? + * @param str string to match + * @param offset start matching at this offset + * @return 0 if no match otherwise length matched + */ + size_t match_end(std::u32string &str, size_t offset, size_t len) const; + +public: + element_list elements; + element_list before; +}; + +/** a list of entries, such as in a group */ +typedef std::deque reorder_list; + +/** subtype that's a list of reorders */ +struct reorder_group { +public: + reorder_list list; + /** apply this reordering. Return true if changed. */ + bool apply(std::u32string &str) const; +}; + +/** container for either a transform or a reorder group */ +class any_group { +public: + any_group(const transform_group &g); + any_group(const reorder_group &g); + any_group_type type; // transform or reorder + transform_group transform; + reorder_group reorder; +}; + /** - * map from transform list to string + * A list of any_groups */ -typedef std::map simple_transforms; +typedef std::deque group_list; +/** + * This represents an entire element + */ class transforms { private: - simple_transforms simple; + group_list transform_groups; public: transforms(); /** - * Add a simple transform rule - * @param match list of matching string elements - * @param output output string for this rule + * Add a transform group */ - void add(const string_list &match, const std::u16string &output); + void addGroup(const transform_group &s); + /** + * Add a reorder group + */ + void addGroup(const reorder_group &s); /** - * Check for any matching transform rules - * @param ctxt context array for matching - * @param output fillin: will contain the output string for this rule, if there is a match - * @return 0 if no match, or the length of the ctxt string matched, starting at the end + * Attempt to match and apply a pattern change. + * + * @param input original input text. Will not be altered. + * @param output if matched, contains the replacement output text + * @return length in chars of the input (counting from the end) which matched context */ - size_t matchContext(const string_list &ctxt, std::u16string &output) const; + size_t apply(const std::u32string &input, std::u32string &output); + + /** + * For tests - TODO-LDML only supports reorder + * @return true if str was altered + */ + bool apply(std::u32string &str); + +public: + /** load from a kmx_plus data section, either tran or bksp */ + static transforms * + load(const kmx::kmx_plus &kplus, + const kbp::kmx::COMP_KMXPLUS_TRAN *tran, + const kbp::kmx::COMP_KMXPLUS_TRAN_Helper &tranHelper); }; } // namespace ldml diff --git a/core/tests/unit/kmnkbd/test_kmx_xstring.cpp b/core/tests/unit/kmnkbd/test_kmx_xstring.cpp index d578fa7e3813aa0e86e64a8855a360d5c78a5988..e3c40776225a471b24b2deea40997888baab78e0 100644 --- a/core/tests/unit/kmnkbd/test_kmx_xstring.cpp +++ b/core/tests/unit/kmnkbd/test_kmx_xstring.cpp @@ -1238,6 +1238,7 @@ test_xstrlen_ignoreifopt() { void test_utf32() { + std::cout << "== " << __FUNCTION__ << std::endl; const KMX_DWORD u295 = 0x0127; // ħ assert(Uni_IsBMP(u295)); @@ -1270,6 +1271,7 @@ test_utf32() { void test_u16string_to_u32string() { + std::cout << "== " << __FUNCTION__ << std::endl; // normal cases { const std::u32string str = u16string_to_u32string(u""); @@ -1327,6 +1329,56 @@ test_u16string_to_u32string() { } } +void test_is_valid() { + std::cout << "== " << __FUNCTION__ << std::endl; + // valid + assert_equal(Uni_IsValid(0x0000), true); + assert_equal(Uni_IsValid(0x0127), true); + assert_equal(Uni_IsValid(U'🙀'), true); + + // invalid + assert_equal(Uni_IsValid(0xDECAFBAD), false); // out of range + assert_equal(Uni_IsValid(0x566D4128), false); + assert_equal(Uni_IsValid(0xFFFF), false); // nonchar + assert_equal(Uni_IsValid(0xFFFE), false); // nonchar + assert_equal(Uni_IsValid(0x10FFFF), false); // nonchar + assert_equal(Uni_IsValid(0x10FFFE), false); // nonchar + assert_equal(Uni_IsValid(0x01FFFF), false); // nonchar + assert_equal(Uni_IsValid(0x01FFFE), false); // nonchar + assert_equal(Uni_IsValid(0x02FFFF), false); // nonchar + assert_equal(Uni_IsValid(0x02FFFE), false); // nonchar + assert_equal(Uni_IsValid(0xFDD1), false); // nonchar + assert_equal(Uni_IsValid(0xFDD0), false); // nonchar + + + // positive range test + assert_equal(Uni_IsValid(0x100000, 0x10FFFD), true); + assert_equal(Uni_IsValid(0x10, 0x20), true); + assert_equal(Uni_IsValid(0x100000, 0x10FFFD), true); + + // all valid ranges in BMP + assert_equal(Uni_IsValid(0x0000, 0xD7FF), true); + assert_equal(Uni_IsValid(0xD800, 0xDFFF), false); + assert_equal(Uni_IsValid(0xE000, 0xFDCF), true); + assert_equal(Uni_IsValid(0xFDD0, 0xFDEF), false); + assert_equal(Uni_IsValid(0xFDF0, 0xFDFF), true); + assert_equal(Uni_IsValid(0xFDF0, 0xFFFD), true); + + // negative range test + assert_equal(Uni_IsValid(0, 0x10FFFF), false); // ends with nonchar + assert_equal(Uni_IsValid(0, 0x10FFFD), false); // contains lots o' nonchars + assert_equal(Uni_IsValid(0x20, 0x10), false); // swapped + assert_equal(Uni_IsValid(0xFDEF, 0xFDF0), false); // just outside range + assert_equal(Uni_IsValid(0x0000, 0x010000), false); // crosses noncharacter plane boundary and other stuff + assert_equal(Uni_IsValid(0x010000, 0x020000), false); // crosses noncharacter plane boundary + assert_equal(Uni_IsValid(0x0000, 0xFFFF), false); // crosses other BMP prohibited and plane boundary + assert_equal(Uni_IsValid(0x0000, 0xFFFD), false); // crosses other BMP prohibited + assert_equal(Uni_IsValid(0x0000, 0xE000), false); // crosses surrogate space + assert_equal(Uni_IsValid(0x0000, 0x20FFFF), false); // out of bounds + assert_equal(Uni_IsValid(0x10FFFD, 0x20FFFF), false); // out of bounds + + } + constexpr const auto help_str = u"\ test_kmx_xstring [--color]\n\ \n\ @@ -1349,6 +1401,7 @@ int main(int argc, char *argv []) { test_xstrlen_ignoreifopt(); test_utf32(); test_u16string_to_u32string(); + test_is_valid(); return 0; } diff --git a/core/tests/unit/ldml/keyboards/k_003_transform.xml b/core/tests/unit/ldml/keyboards/k_003_transform.xml index 577b0e314d5990d322022ec74453268d6b0e3005..aa90e79a3cbfff855f92a7ff018b1c23c712c53e 100644 --- a/core/tests/unit/ldml/keyboards/k_003_transform.xml +++ b/core/tests/unit/ldml/keyboards/k_003_transform.xml @@ -3,14 +3,8 @@ @@ -18,33 +12,50 @@ Note: keys should start with [K_A][K_B][K_C] not [K_Y] when transforms are funct - + - - - + + + - - + + + + + + + + + + + + + - - - - - + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_006_backspace.xml b/core/tests/unit/ldml/keyboards/k_006_backspace.xml new file mode 100644 index 0000000000000000000000000000000000000000..f1ab58c6958d5a2d033ed1a8cf482f433eedbb56 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_006_backspace.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/fr-t-k0-azerty-test.xml b/core/tests/unit/ldml/keyboards/k_020_fr-test.xml similarity index 85% rename from core/tests/unit/ldml/keyboards/fr-t-k0-azerty-test.xml rename to core/tests/unit/ldml/keyboards/k_020_fr-test.xml index 21f9d64cc4f6611afa51f339a671c7cde48d8cd7..04066ae220b71998253dd13345c07a04827abc24 100644 --- a/core/tests/unit/ldml/keyboards/fr-t-k0-azerty-test.xml +++ b/core/tests/unit/ldml/keyboards/k_020_fr-test.xml @@ -1,10 +1,11 @@ + - + - + diff --git a/core/tests/unit/ldml/keyboards/fr-t-k0-azerty.xml b/core/tests/unit/ldml/keyboards/k_020_fr.xml similarity index 94% rename from core/tests/unit/ldml/keyboards/fr-t-k0-azerty.xml rename to core/tests/unit/ldml/keyboards/k_020_fr.xml index 8a634bda65d786574e1e21c6fdfdd9eaaa686d22..1f6fc2c32f1c0441cf94bb52489270accc8dc6f1 100644 --- a/core/tests/unit/ldml/keyboards/fr-t-k0-azerty.xml +++ b/core/tests/unit/ldml/keyboards/k_020_fr.xml @@ -1,4 +1,5 @@ + @@ -7,11 +8,11 @@ - + - - + + @@ -22,15 +23,15 @@ - + - + - + diff --git a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml new file mode 100644 index 0000000000000000000000000000000000000000..d23862ca2bb5b8e7f956263b53ab864ffe515015 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana-test.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana.xml b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana.xml new file mode 100644 index 0000000000000000000000000000000000000000..8df0d5ffd7c341c9678bcc284abece3f67ec2f79 --- /dev/null +++ b/core/tests/unit/ldml/keyboards/k_200_reorder_nod_Lana.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/tests/unit/ldml/keyboards/meson.build b/core/tests/unit/ldml/keyboards/meson.build index 367e0056d92ad415f82286cecc8403c82d3aaf67..3d1bfdddcced7a1303a91f8f37e8014829ffd897 100644 --- a/core/tests/unit/ldml/keyboards/meson.build +++ b/core/tests/unit/ldml/keyboards/meson.build @@ -9,8 +9,8 @@ # tests in resources/standards-data/ldml-keyboards/techpreview/test/ tests_from_cldr = [ 'ja-Latn', - 'pt-k0-abnt2', - # 'fr-t-k0-azerty', # vkey issues + # 'pt-k0-abnt2', #TODO-LDML: marker syntax fail! + 'fr-t-k0-azerty', ] tests_without_testdata = [ @@ -20,17 +20,20 @@ tests_without_testdata = [ 'k_003_transform', 'k_004_tinyshift', 'k_005_modbittest', + # 'k_006_backspace', ## not quite there yet. TODO-LDML 'k_010_mt', 'k_011_mt_iso', 'k_100_keytest', 'k_101_keytest', 'k_102_keytest', + ] # These tests have a k_001_tiny-test.xml file as well. tests_with_testdata = [ 'k_001_tiny', - 'fr-t-k0-azerty', # TODO-LDML: move to cldr above (fix vkey) + 'k_020_fr', # TODO-LDML: move to cldr above (fix vkey) + 'k_200_reorder_nod_Lana', ] tests = tests_without_testdata diff --git a/core/tests/unit/ldml/ldml_test_source.cpp b/core/tests/unit/ldml/ldml_test_source.cpp index 614a4606888876ff499fdd3e2d9b4c730786ef42..da65e62a27dd47e48b970ac4cd8359eadac11e98 100644 --- a/core/tests/unit/ldml/ldml_test_source.cpp +++ b/core/tests/unit/ldml/ldml_test_source.cpp @@ -565,18 +565,11 @@ LdmlJsonRepertoireTestSource::next_action(ldml_action &fillin) { // First, find the string as an id // TODO-LDML: will not work for multi string cases - KMX_DWORD strId = kmxplus->strs->find(chstr); - if (strId == 0) { // will also get here if id is empty. - fillin.string = u"No string for repertoire test: "; - fillin.string.append(chstr); - fillin.type = LDML_ACTION_FAIL; - return; - } - assert(strId != 0); + KMX_DWORD strId = kmxplus->strs->find(chstr); // not an error if chstr is 0, may be single ch // OK. Now we can search the keybag KMX_DWORD keyIndex = 0; - auto *key2 = kmxplus->key2Helper.findKeyByStringTo(strId, keyIndex); + auto *key2 = kmxplus->key2Helper.findKeyByStringTo(chstr, strId, keyIndex); if (key2 == nullptr) { fillin.string = u"No key for repertoire test: "; fillin.string.append(chstr); diff --git a/core/tests/unit/ldml/meson.build b/core/tests/unit/ldml/meson.build index a816658134e0ff263dd018294e3583e207ec32d1..679acbe5994d5b9c5c2d55e52d94d353fa3dfbff 100644 --- a/core/tests/unit/ldml/meson.build +++ b/core/tests/unit/ldml/meson.build @@ -105,3 +105,10 @@ e = executable('test_kmx_plus', 'test_kmx_plus.cpp', link_args: links + tests_flags, objects: lib.extract_all_objects(recursive: false)) test('test_kmx_plus', e, suite: 'ldml') + +t = executable('test_transforms', 'test_transforms.cpp', + cpp_args: defns + warns, + include_directories: [inc, libsrc, '../../../../developer/src/ext/json'], + link_args: links + tests_flags, + objects: lib.extract_all_objects(recursive: false)) +test('test_transforms', t, suite: 'ldml') diff --git a/core/tests/unit/ldml/test_kmx_plus.cpp b/core/tests/unit/ldml/test_kmx_plus.cpp index a4d83a69a7294653e49925e4c5b45aa825d8cce3..74961f797e1a0d35c547f7b1c12ae349dcdac85e 100644 --- a/core/tests/unit/ldml/test_kmx_plus.cpp +++ b/core/tests/unit/ldml/test_kmx_plus.cpp @@ -26,30 +26,30 @@ int test_COMP_KMXPLUS_KEYS_KEY() { COMP_KMXPLUS_ELEM_ELEMENT elems[2] = { { 0x00000127, // to = U+0127 = 295 - 0x00000000 // flags: !LDML_ELEM_FLAGS_UNICODE_SET + 0x00000000 // flags: CHAR }, { 0x0001F640, // to - 0x00000000 // flags: !LDML_ELEM_FLAGS_UNICODE_SET + 0x00000000 // flags: CHAR }}; - std::u16string s0 = e[0].get_string(); + std::u16string s0 = e[0].get_to_string(); assert_equal(s0.length(), 1); assert_equal(s0.at(0), 0x0127); assert(s0 == std::u16string(u"ħ")); - std::u16string s1 = e[1].get_string(); + std::u16string s1 = e[1].get_to_string(); assert_equal(s1.length(), 2); assert_equal(s1.at(0), 0xD83D); assert_equal(s1.at(1), 0xDE40); assert(s1 == std::u16string(u"🙀")); // now, elems. Parallel. - std::u16string es0 = elems[0].get_string(); + std::u16string es0 = elems[0].get_element_string(); assert_equal(es0.length(), 1); assert_equal(es0.at(0), 0x0127); assert(es0 == std::u16string(u"ħ")); - std::u16string es1 = elems[1].get_string(); + std::u16string es1 = elems[1].get_element_string(); assert_equal(es1.length(), 2); assert_equal(es1.at(0), 0xD83D); assert_equal(es1.at(1), 0xDE40); @@ -144,6 +144,26 @@ int test_ldml_vkeys() { return EXIT_SUCCESS; } +int test_uset() { + std::cout << "== " << __FUNCTION__ << std::endl; + + const COMP_KMXPLUS_USET_RANGE r[] = { + {0x61, 0x7a}, // [a-z] + {0x127, 0x127} // [ħ] + }; + + USet u0(&r[0], 2); + assert_equal(u0.contains(0x62), true); // b + assert_equal(u0.contains(0x41), false); // A + assert_equal(u0.contains(0x127), true); // ħ + + USet uempty; + assert_equal(uempty.contains(0x62), false); + assert_equal(uempty.contains(0x127), false); + + return EXIT_SUCCESS; +} + int main(int argc, const char *argv[]) { int rc = EXIT_SUCCESS; @@ -156,6 +176,10 @@ main(int argc, const char *argv[]) { rc = EXIT_FAILURE; } + if (test_uset() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } + if (rc == EXIT_FAILURE) { std::cout << "== FAILURE" << std::endl; } else { diff --git a/core/tests/unit/ldml/test_transforms.cpp b/core/tests/unit/ldml/test_transforms.cpp new file mode 100644 index 0000000000000000000000000000000000000000..7097bcb64bfee3f249f20d3339fe2d86bf0c60cf --- /dev/null +++ b/core/tests/unit/ldml/test_transforms.cpp @@ -0,0 +1,428 @@ +#include "../../../src/ldml/ldml_transforms.hpp" +#include "kmx/kmx_plus.h" +#include "kmx/kmx_xstring.h" +#include +#include +#include + +// TODO-LDML: normal asserts wern't working, so using some hacks. +// #include "ldml_test_utils.hpp" +// #include +// #include "debuglog.h" + +#ifndef zassert_string_equal +#define zassert_string_equal(actual, expected) \ + { \ + if (actual != expected) { \ + std::cerr << __FILE__ << ":" << __LINE__ << ": " \ + << "got: " << actual << " expected " << expected << std::endl; \ + return EXIT_FAILURE; \ + } \ + } +#endif + +#ifndef zassert_equal +#define zassert_equal(actual, expected) zassert_string_equal(actual, expected) +#endif + +// needed for streaming operators +#include "utfcodec.hpp" + +using namespace km::kbp::ldml; +using namespace km::kbp::kmx; + +// using km::kbp::kmx::u16cmp; + +int +test_transforms() { + std::cout << "== " << __FUNCTION__ << std::endl; + + std::cout << __FILE__ << ":" << __LINE__ << " - basic " << std::endl; + { + // start with one + transform_entry te(std::u32string(U"e^"), std::u32string(U"E")); // keep it simple + // OK now make a group do it + transforms tr; + transform_group st; + + st.push_back(te); + + tr.addGroup(st); + + // see if we can match the same + { + std::u32string src(U"barQ^"); + bool res = tr.apply(src); + zassert_equal(res, false); + zassert_string_equal(src, std::u32string(U"barQ^")); // no change + } + + { + std::u32string src(U"fooe^"); + bool res = tr.apply(src); + zassert_equal(res, true); + zassert_string_equal(src, std::u32string(U"fooE")); + } + } + + std::cout << __FILE__ << ":" << __LINE__ << " - more complex " << std::endl; + + { + transforms tr; + + // setup + { + transform_group st; + st.emplace_back(std::u32string(U"za"), std::u32string(U"c")); + st.emplace_back(std::u32string(U"a"), std::u32string(U"bb")); + tr.addGroup(st); + } + { + transform_group st; + st.emplace_back(std::u32string(U"bb"), std::u32string(U"ccc")); + tr.addGroup(st); + } + { + transform_group st; + st.emplace_back(std::u32string(U"cc"), std::u32string(U"d")); + tr.addGroup(st); + } + { + transform_group st; + st.emplace_back(std::u32string(U"tcd"), std::u32string(U"e")); + tr.addGroup(st); + } + + // now test + + // see if we can match the same + { + std::u32string src(U"ta"); + bool res = tr.apply(src); + // pipe (|) symbol shows where the 'output' is delineated + // t|a --> t|bb --> t|ccc --> t|cd --> |e + zassert_string_equal(src, std::u32string(U"e")); + zassert_equal(res, true); + } + { + std::u32string src(U"qza"); + bool res = tr.apply(src); + // pipe (|) symbol shows where the 'output' is delineated + // q|za -> q|c + zassert_string_equal(src, std::u32string(U"qc")); + zassert_equal(res, true); + } + { + std::u32string src(U"qa"); + bool res = tr.apply(src); + zassert_string_equal(src, std::u32string(U"qcd")); + zassert_equal(res, true); + } + { + std::u32string src(U"tb"); + bool res = tr.apply(src); + zassert_string_equal(src, std::u32string(U"tb")); + zassert_equal(res, false); + } + } + + std::cout << __FILE__ << ":" << __LINE__ << " - hindi example " << std::endl; + + { + transforms tr; + { + transform_group st; + st.emplace_back(std::u32string(U"िह"), std::u32string(U"हि")); + tr.addGroup(st); + } + { + std::u32string src(U"िह"); + bool res = tr.apply(src); + zassert_string_equal(src, std::u32string(U"हि")); + zassert_equal(res, true); + } + } + + return EXIT_SUCCESS; +} + +int +test_reorder_standalone() { + std::cout << "== " << __FUNCTION__ << std::endl; + + std::cout << __FILE__ << ":" << __LINE__ << " - nod-Lana " << std::endl; + { + const std::u32string roasts[] = { + // from the spec + U"\u1A21\u1A60\u1A45\u1A6B\u1A76", // 'ideal' + U"\u1A21\u1A6B\u1A76\u1A60\u1A45", // vowel first + U"\u1A21\u1A6B\u1A60\u1A76\u1A45", // vowel first, NFC + U"\u1A21\u1A6B\u1A60\u1A45\u1A76", // tone after lower + }; + /** we expect this is what comes out the other side */ + const std::u32string expect = roasts[0]; + // now setup the rules + const COMP_KMXPLUS_USET_RANGE ranges[] = {// 0 + COMP_KMXPLUS_USET_RANGE(0x1A75, 0x1A79)}; + const COMP_KMXPLUS_USET_USET usets[] = {{0, 1, 0xFFFFFFFF}}; + const COMP_KMXPLUS_USET_USET &toneMarksUset = usets[0]; + const USet toneMarks(&ranges[toneMarksUset.range], toneMarksUset.count); + // validate that the range [1A75, 1A79] matches + assert_equal(toneMarks.contains(0x1A76), true); + assert_equal(toneMarks.contains(0x1A60), false); + + std::cout << __FILE__ << ":" << __LINE__ << " - element test " << std::endl; + // element test + { + element es(U'a', 0xF4500000 | LDML_ELEM_FLAGS_PREBASE | LDML_ELEM_FLAGS_TERTIARY_BASE); // tertiary -12, primary 80 + std::cout << "es flags" << std::hex << es.get_flags() << std::dec << std::endl; + // verify element metadata + assert_equal(es.is_uset(), false); + assert_equal(es.get_order(), 0x50); + assert_equal(es.get_tertiary(), -12); + assert_equal(es.is_prebase(), true); + assert_equal(es.is_tertiary_base(), true); + // verify element matching + assert_equal(es.matches(U'a'), true); + assert_equal(es.matches(U'b'), false); + + element eu(toneMarks, 0x37F40000); + // element metadata + std::cout << "eu flags" << std::hex << eu.get_flags() << std::dec << std::endl; + assert_equal(eu.is_uset(), true); + std::cout << "order" << (int)eu.get_order() << std::endl; + assert_equal(eu.get_order(), -12); + assert_equal(eu.get_tertiary(), 55); + assert_equal(eu.is_prebase(), false); + assert_equal(eu.is_tertiary_base(), false); + // element matching + assert_equal(eu.matches(U'a'), false); + assert_equal(eu.matches(U'\u1A76'), true); + assert_equal(eu.matches(U'\u1A75'), true); + + element_list l; // '[tones]a' + l.emplace_back(es); + l.emplace_back(eu); + + std::cout << __FILE__ << ":" << __LINE__ << " - list test " << std::endl; + assert_equal(l.match_end(U"asdfasdf"), 0); // no match + assert_equal(l.match_end(U"a"), 0); // partial substring, fastpath because it's short + assert_equal(l.match_end(U"\u1A76"), 0); // partial substring, fastpath because it's short + assert_equal(l.match_end(U"a\u1A76"), 2); // Match + assert_equal(l.match_end(U"a\u1A75"), 2); // Match + assert_equal(l.match_end(U"SomethingSomethingSomethinga\u1A76"), 2); // Sub-Match + + // generate sort keys + std::cout << __FILE__ << ":" << __LINE__ << " - get_sort_key test " << std::endl; + std::u32string str = U"a\u1A78"; + // get the baseline sortkey + auto keylist = reorder_sort_key::from(str); + std::cout << __FILE__ << ":" << __LINE__ << " baseline sortkey" << std::endl; + for (auto i = keylist.begin(); i < keylist.end(); i++) { + i->dump(); + } + // make sure it matches + assert_equal(l.match_end(str), 2); + // update the keylist with these elements. + l.update_sort_key(0, keylist); + std::cout << __FILE__ << ":" << __LINE__ << " updated sortkey" << std::endl; + assert_equal(keylist.size(), 2); + size_t secondary = 0; + for (auto i = keylist.begin(); i < keylist.end(); i++) { + i->dump(); + assert_equal(i->secondary, secondary); + assert_equal(i->quaternary, secondary); + secondary++; + } + std::cout << std::endl; + + // spot check first sortkey + assert_equal(keylist.begin()->primary, 80); + assert_equal(keylist.begin()->tertiary, -12); + assert_equal(keylist.begin()->ch, 0x61); + std::cout << __FILE__ << ":" << __LINE__ << " sorted sortkey" << std::endl; + + // now sort them + std::sort(keylist.begin(), keylist.end()); + for (auto i = keylist.begin(); i < keylist.end(); i++) { + i->dump(); + } + std::cout << std::endl; + // spot check first sort key + assert_equal(keylist.begin()->primary, -12); + assert_equal(keylist.begin()->tertiary, 55); + assert_equal(keylist.begin()->ch, 0x1A78); + } + + std::cout << __FILE__ << ":" << __LINE__ << " - key test " << std::endl; + { + // can we sort an array of reorder sort keys? + reorder_sort_key keys0[] = { + {U'\u1A21', 0, 0, 0, 0}, + {U'\u1A6B', 42, 1, 0, 1}, + {U'\u1A76', 55, 2, 0, 2}, + {U'\u1A60', 10, 3, 0, 3}, + {U'\u1A45', 10, 4, 0, 3}}; + // add it to a list + std::deque keylist; + for (size_t i = 0; i < sizeof(keys0) / sizeof(keys0[0]); i++) { + keylist.emplace_back(keys0[i]); + // print it out + keys0[i].dump(); + } + // now sort it + std::cout << __FILE__ << ":" << __LINE__ << " sort" << std::endl; + std::sort(keylist.begin(), keylist.end()); + std::u32string sorted; + for (auto i = keylist.begin(); i < keylist.end(); i++) { + i->dump(); + sorted.append(1, i->ch); + } + // did the roast come out? + zassert_string_equal(sorted, expect); + } + std::cout << "now prepare the reorder elements" << std::endl; + transforms tr; + { + reorder_group rg; + + // + element_list e0; + e0.emplace_back(U'\u1A60', 127 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + rg.list.emplace_back(e0); + + // + element_list e1; + e1.emplace_back(U'\u1A6B', 42 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + rg.list.emplace_back(e1); + + // + element_list e2; + e2.emplace_back(toneMarks, 55 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + rg.list.emplace_back(e2); + + // + element_list e3; + e3.emplace_back(U'\u1A60', 10 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + e3.emplace_back(U'\u1A45', 10 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + element_list e3before; + e3before.emplace_back(U'\u1A6B', 0); + rg.list.emplace_back(e3, e3before); + + // + element_list e4; + e4.emplace_back(U'\u1A60', 10 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + e4.emplace_back(U'\u1A45', 10 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + element_list e4before; + e4before.emplace_back(U'\u1A6B', 0); + e4before.emplace_back(toneMarks, 0); + rg.list.emplace_back(e4, e4before); + + // + element_list e5; + e5.emplace_back(U'\u1A60', 10 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + e5.emplace_back(toneMarks, 55 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + e5.emplace_back(U'\u1A45', 10 << LDML_ELEM_FLAGS_ORDER_BITSHIFT); + element_list e5before; + e5before.emplace_back(U'\u1A6B', 0); + rg.list.emplace_back(e5, e5before); + + tr.addGroup(rg); + } + + // now actually test it + std::cout << __FILE__ << ":" << __LINE__ << " - back to nod-Lana " << std::endl; + // TODO-LDML: move this into test code perhaps + for (size_t r = 0; r < sizeof(roasts) / sizeof(roasts[0]); r++) { + const auto &roast = roasts[r]; + std::cout << __FILE__ << ":" << __LINE__ << " - trying roast #" << r << "=" << roast << std::endl; + // try apply with string + { + std::cout << "- try apply(text, output)" << std::endl; + std::u32string text = roast; + std::u32string output; + size_t len = tr.apply(text, output); + if (len == 0) { + std::cout << " (did not apply)" << std::endl; + } else { + std::cout << " applied, matchLen= " << len << std::endl; + text.resize(text.size()-len); // shrink + text.append(output); + std::cout << " = " << text << std::endl; + } + zassert_string_equal(text, expect); + } + // try all-at-once + { + std::cout << "- try apply(text)" << std::endl; + std::u32string text = roast; + if (!tr.apply(text)) { + std::cout << " (did not apply)" << std::endl; + } else if (text == roast) { + std::cout << " (suboptimal: apply returned true but made no change)" << std::endl; + } else { + std::cout << " changed to " << text; + } + zassert_string_equal(text, expect); + std::cout << " matched (converting all at once)!" << std::endl; + } + // simulate typing this one char at a time; + { + std::cout << "- try key-at-a-time" << std::endl; + std::u32string text; + for (auto ch = roast.begin(); ch < roast.end(); ch++) { + // append the string + text.append(1, *ch); + std::cout << "-: " << text << std::endl; + if (!tr.apply(text)) { + std::cout << " (did not apply)" << std::endl; + } + } + // now the moment of truth + zassert_string_equal(text, expect); + std::cout << " matched! (converting char at a time)" << std::endl; + std::cout << std::endl; + } + } + // special test + { + std::cout << __FILE__ << ":" << __LINE__ << " - special test " << std::endl; + const std::u32string expect = U"\u1A21\u1A60\u1A45"; // this string shouldn't mutate at all. + { + std::u32string text = expect; + tr.apply(text); + zassert_string_equal(text, expect); + } + { + // try submatch + std::u32string text = expect; + std::u32string output; + size_t len = tr.apply(text, output); + zassert_string_equal(output, U""); + assert_equal(len, 0); + } + } + } + return EXIT_SUCCESS; +} + +int +main(int argc, const char *argv[]) { + int rc = EXIT_SUCCESS; + + if (test_transforms() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } + + if (test_reorder_standalone() != EXIT_SUCCESS) { + rc = EXIT_FAILURE; + } + + if (rc == EXIT_FAILURE) { + std::cout << "== FAILURE" << std::endl; + } else { + std::cout << "== PASS" << std::endl; + } + + return rc; +} diff --git a/developer/src/common/delphi/compiler/compile.pas b/developer/src/common/delphi/compiler/compile.pas index 9b304b39d83d5dd75687a441ce4c054c9e570628..0124150ef6297561d33f62191f572bc06cf03945 100644 --- a/developer/src/common/delphi/compiler/compile.pas +++ b/developer/src/common/delphi/compiler/compile.pas @@ -126,6 +126,8 @@ type dpDeadKeyArray: PFILE_DEADKEY; // temp - dead key array cxVKDictionary: DWORD; // I3438 dpVKDictionary: PFILE_VKDICTIONARY; // I3438 // temp - virtual key dictionary + + extra: Pointer; end; PFILE_KEYBOARD = ^FILE_KEYBOARD; @@ -152,13 +154,13 @@ const const // kcframe --sizeof returns these values {$IFDEF WIN64} - FILE_KEYBOARD_SIZE = 2984; + FILE_KEYBOARD_SIZE = 2992; FILE_GROUP_SIZE = 200; FILE_STORE_SIZE = 200; FILE_KEY_SIZE = 32; FILE_DEADKEY_SIZE = 160; {$ELSE} - FILE_KEYBOARD_SIZE = 2952; + FILE_KEYBOARD_SIZE = 2956; FILE_GROUP_SIZE = 188; FILE_STORE_SIZE = 192; FILE_KEY_SIZE = 20; @@ -174,7 +176,6 @@ type TCompilerOptions = record dwSize: DWORD; ShouldAddCompilerVersion: BOOL; - UseKmcmpLib: BOOL; end; COMPILER_OPTIONS = TCompilerOptions; diff --git a/developer/src/common/include/kmn_compiler_errors.h b/developer/src/common/include/kmn_compiler_errors.h index 6cfae03fc9ea00c559fe53f1b55ddb0e5759b66a..d9804fc17073a203daba773a8b5d769d6b8bb912 100644 --- a/developer/src/common/include/kmn_compiler_errors.h +++ b/developer/src/common/include/kmn_compiler_errors.h @@ -23,16 +23,28 @@ #ifndef _kmn_compiler_errors_h #define _kmn_compiler_errors_h -// Note: these are not be a bitmask +// Compiler Error Masks + +#define CERR_SEVERITY_MASK 0x0000F000 +#define CERR_MESSAGE_MASK 0x00000FFF +#define CERR_MASK 0x0000FFFF + +// Severity codes +// +// Note: these may not be combined, and are not a bitmask, for historical +// reasons they are separate bits #define CERR_FATAL 0x00008000 #define CERR_ERROR 0x00004000 #define CERR_WARNING 0x00002000 #define CERR_HINT 0x00001000 - -// Any messages from the lexical model compiler occupy this range: -#define CERR_LEXICAL_MODEL_MIN 0x00000800 -#define CERR_LEXICAL_MODEL_MAX 0x000008FF - +#define CERR_INFO 0x00000000 + +// Message codes +// +// All errors here and below are mirrored in kmc-kmn/src/compiler/messages.ts; +// if you add a new error here be sure to update that file also. Note that this +// correlation is currently maintained manually. All values must be below +// 0x1000 (exclusive of severity code). #define CERR_None 0x00000000 #define CERR_EndOfFile 0x00000001 @@ -224,8 +236,8 @@ #define CWARN_KeyShouldIncludeNCaps 0x000020AD #define CHINT_UnreachableRule 0x000010AE -#define CHINT_FilenameHasDifferingCase 0x000010AF -#define CWARN_MissingFile 0x000020B0 +#define CHINT_FilenameHasDifferingCase 0x000010AF // only used in kmcmpdll +#define CWARN_MissingFile 0x000020B0 // only used in kmcmpdll #define CERR_BufferOverflow 0x000080C0 #define CERR_Break 0x000080C1 diff --git a/developer/src/common/web/test-helpers/index.ts b/developer/src/common/web/test-helpers/index.ts index 7f34107dc91a8406b6d82c395f9fc0b0a6f21e36..a134aa67dfd40551100ede0ac74d57ac2ea0a075 100644 --- a/developer/src/common/web/test-helpers/index.ts +++ b/developer/src/common/web/test-helpers/index.ts @@ -36,7 +36,7 @@ export class TestCompilerCallbacks implements CompilerCallbacks { /* CompilerCallbacks */ - loadFile(filename: string | URL): Buffer { + loadFile(filename: string): Uint8Array { try { return fs.readFileSync(filename); } catch(e) { @@ -57,7 +57,10 @@ export class TestCompilerCallbacks implements CompilerCallbacks { } resolveFilename(baseFilename: string, filename: string): string { - const basePath = path.dirname(baseFilename); + const basePath = + baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? + baseFilename : + path.dirname(baseFilename); // Transform separators to platform separators -- we are agnostic // in our use here but path prefers files may use // either / or \, although older kps files were always \. @@ -77,7 +80,7 @@ export class TestCompilerCallbacks implements CompilerCallbacks { this.messages.push(event); } - loadSchema(schema: CompilerSchema): Buffer { + loadSchema(schema: CompilerSchema): Uint8Array { return fs.readFileSync(new URL(SCHEMA_BASE + schema + '.schema.json', import.meta.url)); } diff --git a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts index 223ab430f3319d58b294be7695c2dacafc4bae5a..7b32a3b6db556ab4d1ecf7469e8e2a15d86aa6ba 100644 --- a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts +++ b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts @@ -8,6 +8,7 @@ import {assert, expect} from 'chai'; // * that names are consistent between the message functions and constants // * that constants have unique codes // * that constants have the right error mask for their type +// * that constants don't creep outside their namespace // // This means we don't have to worry about DRYing out the constants, which would // probably require us to have a preprocessing step, which has its own @@ -18,7 +19,7 @@ import {assert, expect} from 'chai'; const toTitleCase = (s: string) => s.substring(0, 1).toUpperCase() + s.substring(1).toLowerCase(); -export function verifyCompilerMessagesObject(source: Record) { +export function verifyCompilerMessagesObject(source: Record, namespace: number) { const keys = Object.keys(source); const m = source as Record; @@ -58,8 +59,12 @@ export function verifyCompilerMessagesObject(source: Record) { const mask = compilerErrorSeverityName(m[key]); expect(o[1]).to.equal(mask, `Mask value for ${key} does not match`); + expect(m[key] & CompilerErrorSeverity.Reserved_Mask).to.equal(0, `Constant value ${key} uses a reserved value`); + const code = m[key] & CompilerErrorSeverity.Error_Mask; expect(codes).to.not.contain(code, `Constant value ${key} is not unique`); + expect(m[key] & CompilerErrorSeverity.Namespace_Mask).to.equal(namespace, + `Constant value ${key} is not in the correct namespace (0x${namespace.toString(16)})`); codes.push(code); } } diff --git a/developer/src/inst/kmdev.wxs b/developer/src/inst/kmdev.wxs index 1c685ff113e8ad0637820563cf76d008c00cf782..d43183fd27c26c6b59ce546161cd05436d48c8b3 100644 --- a/developer/src/inst/kmdev.wxs +++ b/developer/src/inst/kmdev.wxs @@ -297,7 +297,7 @@ - + diff --git a/developer/src/kmc-kmn/.eslintrc.cjs b/developer/src/kmc-kmn/.eslintrc.cjs index 01dae6b737e69e50f7ece590cc3fa7716d70ed11..bd0835f0be3dfd4b9f81dc76e17fe2e70f3e0747 100644 --- a/developer/src/kmc-kmn/.eslintrc.cjs +++ b/developer/src/kmc-kmn/.eslintrc.cjs @@ -5,7 +5,7 @@ module.exports = { overrides: [ { files:"src/**/*.ts", - // extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], + extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], } ], rules: { diff --git a/developer/src/kmc-kmn/build.sh b/developer/src/kmc-kmn/build.sh index 8c57d71e984920db23b54dd6b18a0a07b17d52f1..a69dec7646607d8ae3d79176e0fe019d08637031 100755 --- a/developer/src/kmc-kmn/build.sh +++ b/developer/src/kmc-kmn/build.sh @@ -51,11 +51,15 @@ fi #------------------------------------------------------------------------------------------------------------------- -if builder_start_action build; then +function copy_deps() { mkdir -p build/src/import/kmcmplib mkdir -p src/import/kmcmplib cp ../kmcmplib/build/wasm/$BUILDER_CONFIGURATION/src/wasm-host.js ./src/import/kmcmplib/wasm-host.js cp ../kmcmplib/build/wasm/$BUILDER_CONFIGURATION/src/wasm-host.wasm ./build/src/import/kmcmplib/wasm-host.wasm +} + +if builder_start_action build; then + copy_deps tsc --build builder_finish_action success build fi @@ -63,6 +67,7 @@ fi #------------------------------------------------------------------------------------------------------------------- if builder_start_action test; then + copy_deps tsc --build test/ npm run lint c8 --reporter=lcov --reporter=text mocha "${builder_extra_params[@]}" diff --git a/developer/src/kmc-kmn/src/compiler/compiler.ts b/developer/src/kmc-kmn/src/compiler/compiler.ts index 2f83e83aee43d243c7e1c35d5cc86f14fa068aaa..987aea874ff350e684d893cb5d1a0e405f568733 100644 --- a/developer/src/kmc-kmn/src/compiler/compiler.ts +++ b/developer/src/kmc-kmn/src/compiler/compiler.ts @@ -1,31 +1,27 @@ /* TODO: implement additional interfaces: - extern "C" bool kmcmp_CompileKeyboardFileToBuffer( - char* pszInfile, - void* pfkBuffer, - bool ACompilerWarningsAsErrors, - bool AWarnDeprecatedCode, - kmcmp_CompilerMessageProc pMsgproc, - void* AmsgprocContext, - int Target - ); - - typedef bool (*kmcmp_ValidateJsonMessageProc)(int64_t offset, const char* szText, void* context); - - extern "C" bool kmcmp_ValidateJsonFile( - std::fstream& f, - std::fstream& fd, - kmcmp_ValidateJsonMessageProc MessageProc, - void* context - ); + typedef bool (*kmcmp_ValidateJsonMessageProc)(); + extern "C" bool kmcmp_ValidateJsonFile(); */ // TODO: rename wasm-host? -import { CompilerCallbacks, CompilerEvent } from '@keymanapp/common-types'; +import { UnicodeSetParser, UnicodeSet } from '@keymanapp/common-types'; +import { CompilerCallbacks, CompilerEvent, KvkFileWriter, KvksFileReader } from '@keymanapp/common-types'; import loadWasmHost from '../import/kmcmplib/wasm-host.js'; import { CompilerMessages, mapErrorFromKmcmplib } from './messages.js'; +export interface CompilerResultFile { + filename: string; + data: Uint8Array; +}; + +export interface CompilerResult { + kmx?: CompilerResultFile; + kvk?: CompilerResultFile; + js?: CompilerResultFile; +}; + export interface CompilerOptions { shouldAddCompilerVersion?: boolean; saveDebug?: boolean; @@ -47,62 +43,34 @@ const baseOptions: CompilerOptions = { */ let callbackProcIdentifier = 0; -/** - * Pointer in wasm-space - */ -type WasmPtr = number; - -/** - * The wrapped functions - */ -class WasmWrapper { - Module: any; - - compileKeyboardFile?: (pszInfile: string, pszOutfile: string, aSaveDebug: number, aCompilerWarningsAsErrors: number, aWarnDeprecatedCode: number, msgProc: string) => boolean; - parseUnicodeSet?: (pat: string, buf: WasmPtr, length: number) => number; - setCompilerOptions?: (shouldAddCompilerVersion: number) => boolean; +const + callbackPrefix = 'kmnCompilerCallbacks_'; - constructor(wasmModule: any) { - this.Module = wasmModule; - if (!wasmModule) { - throw Error(`wasm host did not load`); - } - this.compileKeyboardFile = this.Module.cwrap('kmcmp_Wasm_CompileKeyboardFile', 'boolean', ['string', 'string', 'number', 'number', 'number', 'string']); - this.parseUnicodeSet = this.Module.cwrap('kmcmp_Wasm_ParseUnicodeSet', 'number', ['string', 'number', 'number']); - this.setCompilerOptions = this.Module.cwrap('kmcmp_Wasm_SetCompilerOptions', 'boolean', ['number']); - - if (this.parseUnicodeSet === undefined - || this.setCompilerOptions === undefined - || this.compileKeyboardFile === undefined) { - throw Error(`some wasm functions did not load properly.`); - } - } - - /** - * Entry point into Wasm functions - * @returns WasmWrapper - */ - public static async load() : Promise { - return new WasmWrapper(await loadWasmHost()); - } +interface MallocAndFree { + malloc(sz: number) : number; + free(p: number) : null; }; -export class KmnCompiler { - callbackName: string; + +export class KmnCompiler implements UnicodeSetParser { + private Module: any; + callbackID: string; // a unique numeric id added to globals with prefixed names callbacks: CompilerCallbacks; - wasm: WasmWrapper; + wasmExports: MallocAndFree; constructor() { - this.callbackName = 'kmnCompilerCallback' + callbackProcIdentifier; + this.callbackID = callbackPrefix + callbackProcIdentifier.toString(); callbackProcIdentifier++; } public async init(callbacks: CompilerCallbacks): Promise { this.callbacks = callbacks; - if(!this.wasm) { + if(!this.Module) { try { - this.wasm = await WasmWrapper.load(); + this.Module = await loadWasmHost(); + this.wasmExports = (this.Module.wasmExports ?? this.Module.asm); } catch(e: any) { + /* c8 ignore next 3 */ this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({e})); return false; } @@ -119,7 +87,9 @@ export class KmnCompiler { // Can't report a message here. throw Error('Must call Compiler.init(callbacks) before proceeding'); } - if(!this.wasm) { // fail if wasm not loaded or function not found + if(!this.Module) { + /* c8 ignore next 4 */ + // fail if wasm not loaded or function not found this.callbacks.reportMessage(CompilerMessages.Fatal_MissingWasmModule({})); return false; } @@ -127,16 +97,19 @@ export class KmnCompiler { } public run(infile: string, outfile: string, options?: CompilerOptions): boolean { - if(!this.verifyInitialized()) { - return false; - } - - options = {...baseOptions, ...options}; - (globalThis as any)[this.callbackName] = this.compilerMessageCallback; - // TODO: use callbacks for file access -- so kmc-kmn is entirely fs agnostic let result = this.runCompiler(infile, outfile, options); - delete (globalThis as any)[this.callbackName]; - return result; + if(result) { + if(result.kmx) { + this.callbacks.fs.writeFileSync(result.kmx.filename, result.kmx.data); + } + if(result.kvk) { + this.callbacks.fs.writeFileSync(result.kvk.filename, result.kvk.data); + } + if(result.js) { + this.callbacks.fs.writeFileSync(result.js.filename, result.js.data); + } + } + return !!result; } private compilerMessageCallback = (line: number, code: number, msg: string): number => { @@ -144,61 +117,169 @@ export class KmnCompiler { return 1; } - private runCompiler(infile: string, outfile: string, options: CompilerOptions): boolean { + private cachedFile: {filename: string; data: Uint8Array} = { + filename: null, + data: null + }; + + private loadFileCallback = (filename: string, baseFilename: string, buffer: number, bufferSize: number): number => { + let resolvedFilename = this.callbacks.resolveFilename(baseFilename, filename); + let data: Uint8Array; + if(this.cachedFile.filename == resolvedFilename) { + data = this.cachedFile.data; + } + else { + data = this.callbacks.loadFile(resolvedFilename); + if(!data) { + return -1; + } + this.cachedFile.filename = resolvedFilename; + this.cachedFile.data = data; + } + + if(buffer == 0) { + /* We need to return buffer size required */ + return data.byteLength; + } + + if(bufferSize != data.byteLength) { + /* c8 ignore next 2 */ + throw new Error(`loadFileCallback: second call, expected file size ${bufferSize} == ${data.byteLength}`); + } + + this.Module.HEAP8.set(data, buffer); + + return 1; + } + + public runCompiler(infile: string, outfile: string, options: CompilerOptions): CompilerResult { + if(!this.verifyInitialized()) { + /* c8 ignore next 2 */ + return null; + } + + options = {...baseOptions, ...options}; + + (globalThis as any)[this.callbackID] = { + message: this.compilerMessageCallback, + loadFile: this.loadFileCallback + }; + + let result: CompilerResult = {}; + let wasm_interface = new this.Module.CompilerInterface(); + let wasm_options = new this.Module.CompilerOptions(); + let wasm_result = null; try { - if (!this.wasm.setCompilerOptions(options.shouldAddCompilerVersion ? 1 : 0)) { - this.callbacks.reportMessage(CompilerMessages.Fatal_UnableToSetCompilerOptions()); + wasm_options.saveDebug = options.saveDebug; + wasm_options.compilerWarningsAsErrors = options.compilerWarningsAsErrors; + wasm_options.warnDeprecatedCode = options.warnDeprecatedCode; + wasm_options.shouldAddCompilerVersion = options.shouldAddCompilerVersion; + wasm_options.target = 0; //CKF_KEYMAN; TODO, support KMW + wasm_interface.callbacksKey = this.callbackID; // key of object on globalThis + wasm_result = this.Module.kmcmp_compile(infile, wasm_options, wasm_interface); + if(!wasm_result.result) { + return null; + } + + if(wasm_result.kvksFilename) { + result.kvk = this.runKvkCompiler(wasm_result.kvksFilename, infile, outfile); + if(!result.kvk) { + return null; + } } - return this.wasm.compileKeyboardFile( - infile, - outfile, - options.saveDebug ? 1 : 0, - options.compilerWarningsAsErrors ? 1 : 0, - options.warnDeprecatedCode ? 1 : 0, - this.callbackName); + + result.kmx = { + filename: outfile, + data: new Uint8Array(this.Module.HEAP8.buffer, wasm_result.kmx, wasm_result.kmxSize) + }; + + return result; } catch(e) { + /* c8 ignore next 3 */ this.callbacks.reportMessage(CompilerMessages.Fatal_UnexpectedException({e:e})); - return false; + return null; + } finally { + if(wasm_result) { + wasm_result.delete(); + } + wasm_interface.delete(); + wasm_options.delete(); + delete (globalThis as any)[this.callbackID]; + } + } + + private runKvkCompiler(kvksFilename: string, kmnFilename: string, kmxFilename: string) { + // The compiler detected a .kvks file, which needs to be captured + let reader = new KvksFileReader(); + kvksFilename = this.callbacks.resolveFilename(kmnFilename, kvksFilename); + let filename = this.callbacks.path.basename(kvksFilename); + let kvks = null; + try { + kvks = reader.read(this.callbacks.loadFile(kvksFilename)); + reader.validate(kvks, this.callbacks.loadSchema('kvks')); + } catch(e) { + this.callbacks.reportMessage(CompilerMessages.Error_InvalidKvksFile({filename, e})); + return null; + } + let invalidVkeys: string[] = []; + let vk = reader.transform(kvks, invalidVkeys); + for(let invalidVkey of invalidVkeys) { + this.callbacks.reportMessage(CompilerMessages.Warn_InvalidVkeyInKvksFile({filename, invalidVkey})); } + let writer = new KvkFileWriter(); + return { + filename: this.callbacks.path.join(this.callbacks.path.dirname(kmxFilename), this.callbacks.path.basename(kvksFilename, '.kvks') + '.kvk'), + data: writer.write(vk) + }; } /** * * @param pattern UnicodeSet pattern such as `[a-z]` - * @param bufferSize guess as to the buffer size + * @param rangeCount number of ranges to allocate * @returns UnicodeSet accessor object, or null on failure */ - public parseUnicodeSet(pattern: string, bufferSize: number) : UnicodeSet | null { + public parseUnicodeSet(pattern: string, rangeCount: number) : UnicodeSet | null { if(!this.verifyInitialized()) { + /* c8 ignore next 2 */ return null; } - if (!bufferSize) { - bufferSize = 100; // TODO-LDML: Preflight mode? Reuse buffer? - } - const { Module } = this.wasm; - const buf = Module.asm.malloc(bufferSize * 2 * Module.HEAPU32.BYTES_PER_ELEMENT); // TODO-LDML: Catch OOM - const rc = this.wasm.parseUnicodeSet(pattern, buf, bufferSize); + const buf = this.wasmExports.malloc(rangeCount * 2 * this.Module.HEAPU32.BYTES_PER_ELEMENT); + /** If <= 0: return code. If positive: range count */ + const rc = this.Module.kmcmp_parseUnicodeSet(pattern, buf, rangeCount * 2); if (rc >= 0) { const ranges = []; - const startu = (buf / Module.HEAPU32.BYTES_PER_ELEMENT); + const startu = (buf / this.Module.HEAPU32.BYTES_PER_ELEMENT); for (let i = 0; i < rc; i++) { - const low = Module.HEAPU32[startu + (i * 2) + 0]; - const high = Module.HEAPU32[startu + (i * 2) + 1]; - ranges.push([low, high]); + const start = this.Module.HEAPU32[startu + (i * 2) + 0]; + const end = this.Module.HEAPU32[startu + (i * 2) + 1]; + ranges.push([start, end]); } - // TODO-LDML: no free?? - // Module.asm.free(buf); + this.wasmExports.free(buf); return new UnicodeSet(pattern, ranges); } else { - // translate error - // TODO-LDML: no free?? - // Module.asm.free(buf); + this.wasmExports.free(buf); + // translate error code into callback this.callbacks.reportMessage(getUnicodeSetError(rc)); return null; } } + public sizeUnicodeSet(pattern: string) : number { + if(!this.verifyInitialized()) { + /* c8 ignore next 2 */ + return null; + } + // call with rangeCount = 0 to invoke in 'preflight' mode. + const rc = this.Module.kmcmp_parseUnicodeSet(pattern, 0, 0); + if (rc >= 0) { + return rc; + } else { + this.callbacks.reportMessage(getUnicodeSetError(rc)); + return -1; + } + } } /** @@ -206,7 +287,7 @@ export class KmnCompiler { * @param rc parseUnicodeSet error code * @returns the compiler event */ -function getUnicodeSetError(rc: number) : CompilerEvent { +function getUnicodeSetError(rc: number) : CompilerEvent { // from kmcmplib.h const KMCMP_ERROR_SYNTAX_ERR = -1; const KMCMP_ERROR_HAS_STRINGS = -2; @@ -222,24 +303,8 @@ function getUnicodeSetError(rc: number) : CompilerEvent { case KMCMP_FATAL_OUT_OF_RANGE: return CompilerMessages.Fatal_UnicodeSetOutOfRange(); default: + /* c8 ignore next */ return CompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}); } } -/** - * Represents a parsed UnicodeSet - */ -export class UnicodeSet { - constructor(public pattern: string, public ranges: number[][]) { - } - /** - * Number of ranges - */ - get length() : number { - return this.ranges.length; - } - - toString() : string { - return this.pattern; - } -} diff --git a/developer/src/kmc-kmn/src/compiler/messages.ts b/developer/src/kmc-kmn/src/compiler/messages.ts index 10578870d7c1499350936abe16981a503cc8167a..40926ff3fca61817483945d86872cdf68149e33b 100644 --- a/developer/src/kmc-kmn/src/compiler/messages.ts +++ b/developer/src/kmc-kmn/src/compiler/messages.ts @@ -32,42 +32,262 @@ const LogLevelToSeverity: Record = { } export const enum KmnCompilerMessageRanges { - RANGE_KMN_COMPILER_MIN = 0x0001, // from kmn_compiler_errors.h - RANGE_KMN_COMPILER_MAX = 0x07FF, // from kmn_compiler_errors.h - RANGE_LEXICAL_MODEL_MIN = 0x0800, // from kmn_compiler_errors.h, deprecated -- this range will not be used in future versions - RANGE_LEXICAL_MODEL_MAX = 0x08FF, // from kmn_compiler_errors.h, deprecated -- this range will not be used in future versions - RANGE_CompilerMessage_Min = 0x1000, // All compiler messages listed here must be >= this value + RANGE_KMN_COMPILER_MIN = 0x001, // from kmn_compiler_errors.h + RANGE_KMN_COMPILER_MAX = 0x7FF, // from kmn_compiler_errors.h + RANGE_LEXICAL_MODEL_MIN = 0x800, // from kmn_compiler_errors.h, deprecated -- this range will not be used in future versions + RANGE_LEXICAL_MODEL_MAX = 0x8FF, // from kmn_compiler_errors.h, deprecated -- this range will not be used in future versions + RANGE_CompilerMessage_Min = 0x900, // All compiler messages listed here must be >= this value + RANGE_CompilerMessage_Max = 0xFFF, // Highest available error code for kmc-kmn } /* - The messages in this class share the namespace with messages from kmn_compiler_errors.h - and the below ranges are reserved. + The messages in this class share the namespace with messages from + kmn_compiler_errors.h, which are generated by kmcmplib, so values below 0x1000 + are reserved for kmcmplib messages. */ export class CompilerMessages { static Fatal_UnexpectedException = (o:{e: any}) => m(this.FATAL_UnexpectedException, `Unexpected exception: ${exc(o.e)}`); - static FATAL_UnexpectedException = SevFatal | 0x1000; + static FATAL_UnexpectedException = SevFatal | 0x900; static Fatal_MissingWasmModule = (o:{e?: any}) => m(this.FATAL_MissingWasmModule, `Could not instantiate WASM compiler module or initialization failed: ${exc(o.e)}`); - static FATAL_MissingWasmModule = SevFatal | 0x1001; + static FATAL_MissingWasmModule = SevFatal | 0x901; static Fatal_UnableToSetCompilerOptions = () => m(this.FATAL_UnableToSetCompilerOptions, `Unable to set compiler options`); - static FATAL_UnableToSetCompilerOptions = SevFatal | 0x1002; + static FATAL_UnableToSetCompilerOptions = SevFatal | 0x902; static Fatal_CallbacksNotSet = () => m(this.FATAL_CallbacksNotSet, `Callbacks were not set with init`); - static FATAL_CallbacksNotSet = SevFatal | 0x1003; + static FATAL_CallbacksNotSet = SevFatal | 0x903; static Fatal_UnicodeSetOutOfRange = () => m(this.FATAL_UnicodeSetOutOfRange, `UnicodeSet buffer was too small`); - static FATAL_UnicodeSetOutOfRange = SevFatal | 0x1004; + static FATAL_UnicodeSetOutOfRange = SevFatal | 0x904; static Error_UnicodeSetHasStrings = () => m(this.ERROR_UnicodeSetHasStrings, `UnicodeSet contains strings, not allowed`); - static ERROR_UnicodeSetHasStrings = SevError | 0x1005; + static ERROR_UnicodeSetHasStrings = SevError | 0x905; static Error_UnicodeSetHasProperties = () => m(this.ERROR_UnicodeSetHasProperties, `UnicodeSet contains properties, not allowed`); - static ERROR_UnicodeSetHasProperties = SevError | 0x1006; + static ERROR_UnicodeSetHasProperties = SevError | 0x906; static Error_UnicodeSetSyntaxError = () => m(this.ERROR_UnicodeSetSyntaxError, `UnicodeSet had a Syntax Error while parsing`); - static ERROR_UnicodeSetSyntaxError = SevError | 0x1007; -} + static ERROR_UnicodeSetSyntaxError = SevError | 0x907; + + static Error_InvalidKvksFile = (o:{filename: string, e: any}) => m(this.ERROR_InvalidKvksFile, + `Error encountered parsing ${o.filename}: ${o.e}`); + static ERROR_InvalidKvksFile = SevError | 0x908; + + static Warn_InvalidVkeyInKvksFile = (o:{filename: string, invalidVkey: string}) => m(this.WARN_InvalidVkeyInKvksFile, + `Invalid virtual key ${o.invalidVkey} found in ${o.filename}`); + static WARN_InvalidVkeyInKvksFile = SevWarn | 0x909; +}; + +/** + * This class defines messages from kmcmplib. They should correspond to codes in + * kmn_compiler_errors.h, exclusive severity bits. For example: + * + * ``` + * kmcmplib.CERR_BadCallParams = CERR_FATAL | 0x002 = 0x8002 + * ERROR_BadCallParams = SevError | 0x0002 = 0x300000 | 0x2000 | 0x002 = 0x302002 + * ``` + * + * CERR_ messages that are actually fatals have been renamed to FATAL_. + * + * Message text is defined by kmcmplib and passed through a callback. + */ +export class KmnCompilerMessages { + static INFO_None = SevInfo | 0x000; + static INFO_EndOfFile = SevInfo | 0x001; + + static FATAL_BadCallParams = SevFatal | 0x002; + static FATAL_CannotAllocateMemory = SevFatal | 0x004; + static FATAL_InfileNotExist = SevFatal | 0x005; + static FATAL_CannotCreateOutfile = SevFatal | 0x006; + static FATAL_UnableToWriteFully = SevFatal | 0x007; + static FATAL_CannotReadInfile = SevFatal | 0x008; + static FATAL_SomewhereIGotItWrong = SevFatal | 0x009; + + static ERROR_InvalidToken = SevError | 0x00A; + static ERROR_InvalidBegin = SevError | 0x00B; + static ERROR_InvalidName = SevError | 0x00C; + static ERROR_InvalidVersion = SevError | 0x00D; + static ERROR_InvalidLanguageLine = SevError | 0x00E; + static ERROR_LayoutButNoLanguage = SevError | 0x00F; + static ERROR_InvalidLayoutLine = SevError | 0x010; + static ERROR_NoVersionLine = SevError | 0x011; + static ERROR_InvalidGroupLine = SevError | 0x012; + static ERROR_InvalidStoreLine = SevError | 0x013; + static ERROR_InvalidCodeInKeyPartOfRule = SevError | 0x014; + static ERROR_InvalidDeadkey = SevError | 0x015; + static ERROR_InvalidValue = SevError | 0x016; + static ERROR_ZeroLengthString = SevError | 0x017; + static ERROR_TooManyIndexToKeyRefs = SevError | 0x018; + static ERROR_UnterminatedString = SevError | 0x019; + static ERROR_StringInVirtualKeySection = SevError | 0x01A; + static ERROR_AnyInVirtualKeySection = SevError | 0x01B; + static ERROR_InvalidAny = SevError | 0x01C; + static ERROR_StoreDoesNotExist = SevError | 0x01D; + static ERROR_BeepInVirtualKeySection = SevError | 0x01E; + static ERROR_IndexInVirtualKeySection = SevError | 0x01F; + static ERROR_InvalidIndex = SevError | 0x020; + static ERROR_OutsInVirtualKeySection = SevError | 0x021; + static ERROR_InvalidOuts = SevError | 0x022; + static ERROR_ContextInVirtualKeySection = SevError | 0x024; + static ERROR_InvalidUse = SevError | 0x025; + static ERROR_GroupDoesNotExist = SevError | 0x026; + static ERROR_VirtualKeyNotAllowedHere = SevError | 0x027; + static ERROR_InvalidSwitch = SevError | 0x028; + static ERROR_NoTokensFound = SevError | 0x029; + static ERROR_InvalidLineContinuation = SevError | 0x02A; + static ERROR_LineTooLong = SevError | 0x02B; + static ERROR_InvalidCopyright = SevError | 0x02C; + static ERROR_CodeInvalidInThisSection = SevError | 0x02D; + static ERROR_InvalidMessage = SevError | 0x02E; + static ERROR_InvalidLanguageName = SevError | 0x02F; + static ERROR_InvalidBitmapLine = SevError | 0x030; + static ERROR_CannotReadBitmapFile = SevError | 0x031; + static ERROR_IndexDoesNotPointToAny = SevError | 0x032; + static ERROR_ReservedCharacter = SevError | 0x033; + static ERROR_InvalidCharacter = SevError | 0x034; + static ERROR_InvalidCall = SevError | 0x035; + static ERROR_CallInVirtualKeySection = SevError | 0x036; + static ERROR_CodeInvalidInKeyStore = SevError | 0x037; + static ERROR_CannotLoadIncludeFile = SevError | 0x038; + + static ERROR_60FeatureOnly_EthnologueCode = SevError | 0x039; + static ERROR_60FeatureOnly_MnemonicLayout = SevError | 0x03A; + static ERROR_60FeatureOnly_OldCharPosMatching = SevError | 0x03B; + static ERROR_60FeatureOnly_NamedCodes = SevError | 0x03C; + static ERROR_60FeatureOnly_Contextn = SevError | 0x03D; + static ERROR_501FeatureOnly_Call = SevError | 0x03E; + + static ERROR_InvalidNamedCode = SevError | 0x03F; + static ERROR_InvalidSystemStore = SevError | 0x040; + + static ERROR_60FeatureOnly_VirtualCharKey = SevError | 0x044; + + static ERROR_VersionAlreadyIncluded = SevError | 0x045; + + static ERROR_70FeatureOnly = SevError | 0x046; + + static ERROR_80FeatureOnly = SevError | 0x047; + static ERROR_InvalidInVirtualKeySection = SevError | 0x048; + static ERROR_InvalidIf = SevError | 0x049; + static ERROR_InvalidReset = SevError | 0x04A; + static ERROR_InvalidSet = SevError | 0x04B; + static ERROR_InvalidSave = SevError | 0x04C; + + static ERROR_InvalidEthnologueCode = SevError | 0x04D; + + static FATAL_CannotCreateTempfile = SevFatal | 0x04E; + + static ERROR_90FeatureOnly_IfSystemStores = SevError | 0x04F; + static ERROR_IfSystemStore_NotFound = SevError | 0x050; + static ERROR_90FeatureOnly_SetSystemStores = SevError | 0x051; + static ERROR_SetSystemStore_NotFound = SevError | 0x052; + static ERROR_90FeatureOnlyVirtualKeyDictionary = SevError | 0x053; + + static ERROR_NotSupportedInKeymanWebContext = SevError | 0x054; + static ERROR_NotSupportedInKeymanWebOutput = SevError | 0x055; + static ERROR_NotSupportedInKeymanWebStore = SevError | 0x056; + static ERROR_VirtualCharacterKeysNotSupportedInKeymanWeb = SevError | 0x057; + static ERROR_VirtualKeysNotValidForMnemonicLayouts = SevError | 0x058; + static ERROR_InvalidTouchLayoutFile = SevError | 0x059; + static ERROR_TouchLayoutInvalidIdentifier = SevError | 0x05A; + static ERROR_InvalidKeyCode = SevError | 0x05B; + static ERROR_90FeatureOnlyLayoutFile = SevError | 0x05C; + static ERROR_90FeatureOnlyKeyboardVersion = SevError | 0x05D; + static ERROR_KeyboardVersionFormatInvalid = SevError | 0x05E; + static ERROR_ContextExHasInvalidOffset = SevError | 0x05F; + static ERROR_90FeatureOnlyEmbedCSS = SevError | 0x060; + static ERROR_90FeatureOnlyTargets = SevError | 0x061; + static ERROR_ContextAndIndexInvalidInMatchNomatch = SevError | 0x062; + static ERROR_140FeatureOnlyContextAndNotAnyWeb = SevError | 0x063; + + static ERROR_ExpansionMustFollowCharacterOrVKey = SevError | 0x064; + static ERROR_VKeyExpansionMustBeFollowedByVKey = SevError | 0x065; + static ERROR_CharacterExpansionMustBeFollowedByCharacter = SevError | 0x066; + static ERROR_VKeyExpansionMustUseConsistentShift = SevError | 0x067; + static ERROR_ExpansionMustBePositive = SevError | 0x068; + + static ERROR_CasedKeysMustContainOnlyVirtualKeys = SevError | 0x069; + static ERROR_CasedKeysMustNotIncludeShiftStates = SevError | 0x06A; + static ERROR_CasedKeysNotSupportedWithMnemonicLayout = SevError | 0x06B; + + static ERROR_CannotUseReadWriteGroupFromReadonlyGroup = SevError | 0x06C; + static ERROR_StatementNotPermittedInReadonlyGroup = SevError | 0x06D; + static ERROR_OutputInReadonlyGroup = SevError | 0x06E; + static ERROR_NewContextGroupMustBeReadonly = SevError | 0x06F; + static ERROR_PostKeystrokeGroupMustBeReadonly = SevError | 0x070; + + static ERROR_DuplicateGroup = SevError | 0x071; + static ERROR_DuplicateStore = SevError | 0x072; + + static ERROR_RepeatedBegin = SevError | 0x073; + + static WARN_TooManyWarnings = SevWarn | 0x080; + static WARN_OldVersion = SevWarn | 0x081; + static WARN_BitmapNotUsed = SevWarn | 0x082; + static WARN_CustomLanguagesNotSupported = SevWarn | 0x083; + static WARN_KeyBadLength = SevWarn | 0x084; + static WARN_IndexStoreShort = SevWarn | 0x085; + static WARN_UnicodeInANSIGroup = SevWarn | 0x086; + static WARN_ANSIInUnicodeGroup = SevWarn | 0x087; + static WARN_UnicodeSurrogateUsed = SevWarn | 0x088; + static WARN_ReservedCharacter = SevWarn | 0x089; + static WARN_Info = SevWarn | 0x08A; + static WARN_VirtualKeyWithMnemonicLayout = SevWarn | 0x08B; + static WARN_VirtualCharKeyWithPositionalLayout = SevWarn | 0x08C; + static WARN_StoreAlreadyUsedAsOptionOrCall = SevWarn | 0x08D; + static WARN_StoreAlreadyUsedAsStoreOrCall = SevWarn | 0x08E; + static WARN_StoreAlreadyUsedAsStoreOrOption = SevWarn | 0x08F; + + static WARN_PunctuationInEthnologueCode = SevWarn | 0x090; + + static WARN_TouchLayoutMissingLayer = SevWarn | 0x091; + static WARN_TouchLayoutCustomKeyNotDefined = SevWarn | 0x092; + static WARN_TouchLayoutMissingRequiredKeys = SevWarn | 0x093; + static WARN_HelpFileMissing = SevWarn | 0x094; + static WARN_EmbedJsFileMissing = SevWarn | 0x095; + static WARN_TouchLayoutFileMissing = SevWarn | 0x096; + static WARN_VisualKeyboardFileMissing = SevWarn | 0x097; + static WARN_ExtendedShiftFlagsNotSupportedInKeymanWeb = SevWarn | 0x098; + static WARN_TouchLayoutUnidentifiedKey = SevWarn | 0x099; + static HINT_UnreachableKeyCode = SevHint | 0x09A; + + static WARN_CouldNotCopyJsonFile = SevWarn | 0x09B; + static WARN_PlatformNotInTargets = SevWarn | 0x09C; + + static WARN_HeaderStatementIsDeprecated = SevWarn | 0x09D; + static WARN_UseNotLastStatementInRule = SevWarn | 0x09E; + + static WARN_TouchLayoutFontShouldBeSameForAllPlatforms = SevWarn | 0x09F; + static WARN_InvalidJSONMetadataFile = SevWarn | 0x0A0; + static WARN_JSONMetadataOSKFontShouldMatchTouchFont = SevWarn | 0x0A1; + static WARN_KVKFileIsInSourceFormat = SevWarn | 0x0A2; + + static WARN_DontMixChiralAndNonChiralModifiers = SevWarn | 0x0A3; + static WARN_MixingLeftAndRightModifiers = SevWarn | 0x0A4; + + static WARN_LanguageHeadersDeprecatedInKeyman10 = SevWarn | 0x0A5; + + static HINT_NonUnicodeFile = SevHint | 0x0A6; + + static WARN_TooManyErrorsOrWarnings = SevWarn | 0x0A7; + + static WARN_HotkeyHasInvalidModifier = SevWarn | 0x0A8; + + static WARN_TouchLayoutSpecialLabelOnNormalKey = SevWarn | 0x0A9; + + static WARN_OptionStoreNameInvalid = SevWarn | 0x0AA; + + static WARN_NulNotFirstStatementInContext = SevWarn | 0x0AB; + static WARN_IfShouldBeAtStartOfContext = SevWarn | 0x0AC; + + static WARN_KeyShouldIncludeNCaps = SevWarn | 0x0AD; + + static HINT_UnreachableRule = SevHint | 0x0AE; + + static FATAL_BufferOverflow = SevFatal | 0x0C0; + static FATAL_Break = SevFatal | 0x0C1; +}; export function mapErrorFromKmcmplib(line: number, code: number, msg: string): CompilerEvent { const severity = LogLevelToSeverity[code & LogLevel.LEVEL_MASK]; diff --git a/developer/src/kmc-kmn/src/main.ts b/developer/src/kmc-kmn/src/main.ts index 70d11989c6bfca468558bb217b42c628cf7017de..68ea2bb3a5480dc12eb687a915e714c53d65494a 100644 --- a/developer/src/kmc-kmn/src/main.ts +++ b/developer/src/kmc-kmn/src/main.ts @@ -1,2 +1,3 @@ +export { CompilerMessages } from './compiler/messages.js'; export { KmnCompiler } from './compiler/compiler.js'; diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/cerr_duplicate_group.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/cerr_duplicate_group.kmn new file mode 100644 index 0000000000000000000000000000000000000000..8382b5ea7f4002161b422d513ef5ecd973ffd300 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/cerr_duplicate_group.kmn @@ -0,0 +1,30 @@ +c Description: Verifies that kmcmplib picks up on duplicated groups with Unicode names, +c and that the Unicode names are correctly reported in error messages in UTF-8 + +store(&NAME) 'cerr_duplicate_group' +store(&VERSION) '9.0' + +begin unicode > use(ខ្មែរ) + +group(ខ្មែរ) using keys + +store(c_key) [K_K] [K_X] [SHIFT K_K] [SHIFT K_X] [K_G] \ + [K_C] [K_Q] [SHIFT K_C] [SHIFT K_Q] [SHIFT K_J] \ + [K_D] [K_Z] [SHIFT K_D] [SHIFT K_Z] [SHIFT K_N] \ + [K_T] [K_F] [SHIFT K_T] [SHIFT K_F] [K_N] \ + [K_B] [K_P] [SHIFT K_B] [SHIFT K_P] [K_M] \ + [K_Y] [K_R] [K_L] [K_V] [K_S] [K_H] [SHIFT K_L] [SHIFT K_G] \ + [RALT K_K] [RALT K_B] +store(c_out) U+1780 U+1781 U+1782 U+1783 U+1784 \ + U+1785 U+1786 U+1787 U+1788 U+1789 \ + U+178A U+178B U+178C U+178D U+178E \ + U+178F U+1790 U+1791 U+1792 U+1793 \ + U+1794 U+1795 U+1796 U+1797 U+1798 \ + U+1799 U+179A U+179B U+179C U+179F U+17A0 U+17A1 U+17A2 \ + U+179D U+179E c deprecated, but they are used in minority languages + ++ any(c_key) > index(c_out, 1) + +group(ខ្មែរ) using keys + ++ 'a' > 'oops two groups!' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/cerr_duplicate_store.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/cerr_duplicate_store.kmn new file mode 100644 index 0000000000000000000000000000000000000000..14d27d5be608f2dd7a480072836e243cb8a529f7 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/cerr_duplicate_store.kmn @@ -0,0 +1,13 @@ +c Description: Verifies that kmcmplib picks up on duplicated stores with Unicode names, +c and that the Unicode names are correctly reported in error messages in UTF-8 + +store(&NAME) 'cerr_duplicate_store' +store(&VERSION) '9.0' + +begin unicode > use(ខ្មែរ) + +group(ខ្មែរ) using keys + +store(ខ្មែរ) 'abc' +store(ខ្មែរ) 'def' + diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_invalid_kvks_file.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_invalid_kvks_file.kmn new file mode 100644 index 0000000000000000000000000000000000000000..f37d6397c5099dbbb61bfb3fa464c90c29dc9958 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_invalid_kvks_file.kmn @@ -0,0 +1,12 @@ +c Description: Verifies that kmc throws an error with an invalid .kvks file + +store(&NAME) 'error_invalid_kvks_file' +store(&VERSION) '9.0' +store(&VISUALKEYBOARD) 'error_invalid_kvks_file.kvks' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > 'ខ្មែរ' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_invalid_kvks_file.kvks b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_invalid_kvks_file.kvks new file mode 100644 index 0000000000000000000000000000000000000000..c681064f5b188af0703f5c1a31c66332abccf68c --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/error_invalid_kvks_file.kvks @@ -0,0 +1,9 @@ + + +

+ 10.0 + caps_lock_layer_3620 + + +
+ diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_invalid_vkey_in_kvks_file.kmn b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_invalid_vkey_in_kvks_file.kmn new file mode 100644 index 0000000000000000000000000000000000000000..18a88973e93d128ea46c93c30f5346cc0395c3b1 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_invalid_vkey_in_kvks_file.kmn @@ -0,0 +1,12 @@ +c Description: Verifies that kmc reports a warning with an invalid vkey in the kvks + +store(&NAME) 'warn_invalid_vkey_in_kvks_file' +store(&VERSION) '9.0' +store(&VISUALKEYBOARD) 'warn_invalid_vkey_in_kvks_file.kvks' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > 'ខ្មែរ' diff --git a/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_invalid_vkey_in_kvks_file.kvks b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_invalid_vkey_in_kvks_file.kvks new file mode 100644 index 0000000000000000000000000000000000000000..f7b247db34e1f9ea8072c40c3a57289d3b461380 --- /dev/null +++ b/developer/src/kmc-kmn/test/fixtures/invalid-keyboards/warn_invalid_vkey_in_kvks_file.kvks @@ -0,0 +1,14 @@ + + +
+ 10.0 + caps_lock_layer_3620 + +
+ + + + + + +
diff --git a/developer/src/kmc-kmn/test/helpers/index.ts b/developer/src/kmc-kmn/test/helpers/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8250dfd101a0a07fd2966cf3e5537ac3386a92f5 --- /dev/null +++ b/developer/src/kmc-kmn/test/helpers/index.ts @@ -0,0 +1,17 @@ +/** + * Helpers and utilities for the Mocha tests. + */ +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Builds a path to the fixture with the given path components. + * + * e.g., makePathToFixture('example.qaa.trivial') + * e.g., makePathToFixture('example.qaa.trivial', 'model.ts') + * + * @param components One or more path components. + */ + export function makePathToFixture(...components: string[]): string { + return fileURLToPath(new URL(path.join('..', '..', '..', 'test', 'fixtures', ...components), import.meta.url)); +} diff --git a/developer/src/kmc-kmn/test/test-compiler.ts b/developer/src/kmc-kmn/test/test-compiler.ts index 2eacd219e6c7c522baaa715131d702ae1c3568de..8c353983613f6d766f6fb8780b91153558c8b51c 100644 --- a/developer/src/kmc-kmn/test/test-compiler.ts +++ b/developer/src/kmc-kmn/test/test-compiler.ts @@ -1,7 +1,5 @@ import 'mocha'; -import sinon from 'sinon'; -import chai, { assert } from 'chai'; -import sinonChai from 'sinon-chai'; +import { assert } from 'chai'; import { KmnCompiler } from '../src/main.js'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; @@ -9,21 +7,10 @@ import fs from 'fs'; import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; const __dirname = dirname(fileURLToPath(import.meta.url)).replace(/\\/g, '/'); -const baselineDir = __dirname + '/../../../../../common/test/keyboards/baseline/'; -chai.use(sinonChai); +const keyboardsDir = __dirname + '/../../../../../common/test/keyboards/'; +const baselineDir = keyboardsDir + 'baseline/'; describe('Compiler class', function() { - let consoleLog: any; - - // TODO: do we need this? - beforeEach(function() { - consoleLog = sinon.spy(console, 'log'); - }); - - afterEach(function() { - consoleLog.restore(); - }); - it('should throw on failure', async function() { const compiler = new KmnCompiler(); const callbacks : any = null; // ERROR @@ -87,4 +74,35 @@ describe('Compiler class', function() { } } }); + + it('should compile a keyboard with visual keyboard', async function() { + const compiler = new KmnCompiler(); + const callbacks = new TestCompilerCallbacks(); + assert(await compiler.init(callbacks)); + assert(compiler.verifyInitialized()); + + const fixtureDir = keyboardsDir + 'caps_lock_layer_3620/' + const infile = fixtureDir + 'source/caps_lock_layer_3620.kmn'; + const kmxFixture = fixtureDir + '/binary/caps_lock_layer_3620.kmx'; + const kvkFixture = fixtureDir + '/binary/caps_lock_layer_3620.kvk'; + + const resultingKmxfile = __dirname + '/caps_lock_layer_3620.kmx'; + const resultingKvkfile = __dirname + '/caps_lock_layer_3620.kvk'; + + assert(compiler.run(infile, resultingKmxfile, {saveDebug: true, shouldAddCompilerVersion: false})); + + assert(fs.existsSync(resultingKmxfile)); + assert(fs.existsSync(resultingKvkfile)); + + const kmxData = fs.readFileSync(resultingKmxfile); + const kmxFixtureData = fs.readFileSync(kmxFixture); + assert.equal(kmxData.byteLength, kmxFixtureData.byteLength); + assert.deepEqual(kmxData, kmxFixtureData); + + const kvkData = fs.readFileSync(resultingKvkfile); + const kvkFixtureData = fs.readFileSync(kvkFixture); + assert.equal(kvkData.byteLength, kvkFixtureData.byteLength); + assert.deepEqual(kvkData, kvkFixtureData); + }); + }); diff --git a/developer/src/kmc-kmn/test/test-messages.ts b/developer/src/kmc-kmn/test/test-messages.ts index 0dee53f12112d47b67ca19bc266630a791dd4bf9..0541cb18ad5d8a6128e07e34379f3d382a50dca8 100644 --- a/developer/src/kmc-kmn/test/test-messages.ts +++ b/developer/src/kmc-kmn/test/test-messages.ts @@ -1,9 +1,70 @@ import 'mocha'; +import path from 'path'; +import { assert } from 'chai'; import { CompilerMessages } from '../src/compiler/messages.js'; -import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { makePathToFixture } from './helpers/index.js'; +import { KmnCompiler } from '../src/main.js'; +import { CompilerErrorNamespace } from '@keymanapp/common-types'; describe('CompilerMessages', function () { + const callbacks = new TestCompilerCallbacks(); + it('should have a valid CompilerMessages object', function() { - return verifyCompilerMessagesObject(CompilerMessages); + return verifyCompilerMessagesObject(CompilerMessages, CompilerErrorNamespace.KmnCompiler); + }); + + // + // Message tests + // + + async function testForMessage(context: Mocha.Context, fixture: string[], messageId?: number) { + context.timeout(10000); + + callbacks.clear(); + + const compiler = new KmnCompiler(); + assert(await compiler.init(callbacks)); + assert(compiler.verifyInitialized()); + + const kmnPath = makePathToFixture(...fixture); + const outfile = path.basename(kmnPath, '.kmn') + '.kmx'; + + // Note: throwing away compile results (just to memory) + compiler.runCompiler(kmnPath, outfile, {saveDebug: true, shouldAddCompilerVersion: false}); + + if(messageId) { + assert.isTrue(callbacks.hasMessage(messageId), `messageId ${messageId.toString(16)} not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + assert.lengthOf(callbacks.messages, 1); + } else { + assert.lengthOf(callbacks.messages, 0, `messages should be empty, but instead got: `+JSON.stringify(callbacks.messages,null,2)); + } + } + + // ERROR_InvalidKvksFile + + it('should generate ERROR_InvalidKvksFile if the kvks is not valid XML', async function() { + await testForMessage(this, ['invalid-keyboards', 'error_invalid_kvks_file.kmn'], CompilerMessages.ERROR_InvalidKvksFile); }); + + // WARN_InvalidVkeyInKvksFile + + it('should generate WARN_InvalidVkeyInKvksFile if the kvks contains an invalid virtual key', async function() { + await testForMessage(this, ['invalid-keyboards', 'warn_invalid_vkey_in_kvks_file.kmn'], CompilerMessages.WARN_InvalidVkeyInKvksFile); + }); + + // CERR_DuplicateGroup + + it('should generate CERR_DuplicateGroup if the kmn contains two groups with the same name', async function() { + await testForMessage(this, ['invalid-keyboards', 'cerr_duplicate_group.kmn'], 0x302071); //TODO: consolidate messages from kmcmplib, CompilerMessages.CERR_DuplicateGroup + assert.equal(callbacks.messages[0].message, "A group with this name has already been defined. Group 'ខ្មែរ' declared on line 9"); + }); + + // CERR_DuplicateStore + + it('should generate CERR_DuplicateStore if the kmn contains two stores with the same name', async function() { + await testForMessage(this, ['invalid-keyboards', 'cerr_duplicate_store.kmn'], 0x302072); //TODO: consolidate messages from kmcmplib, CompilerMessages.CERR_DuplicateStore + assert.equal(callbacks.messages[0].message, "A store with this name has already been defined. Store 'ខ្មែរ' declared on line 11"); + }); + }); diff --git a/developer/src/kmc-kmn/test/test-wasm-uset.ts b/developer/src/kmc-kmn/test/test-wasm-uset.ts index a63b408a959dfbe410703b3f73b28df5ce915bb4..a924f9fe05a048bbc348146c6c0eb24fcb952bb5 100644 --- a/developer/src/kmc-kmn/test/test-wasm-uset.ts +++ b/developer/src/kmc-kmn/test/test-wasm-uset.ts @@ -25,6 +25,11 @@ describe('Compiler UnicodeSet function', function() { assert(set.length === 1); assert(set.ranges[0][0] === 'a'.charCodeAt(0)); assert(set.ranges[0][1] === 'c'.charCodeAt(0)); + assert.deepEqual(callbacks.messages, []); + callbacks.clear(); + const len2 = compiler.sizeUnicodeSet(pat); + assert.deepEqual(callbacks.messages, []); + assert.equal(len2, set.length); }); it('should compile a more complex uset', async function() { const compiler = new KmnCompiler(); @@ -40,6 +45,11 @@ describe('Compiler UnicodeSet function', function() { assert.equal(set.ranges[0][1], 'A'.charCodeAt(0)); assert.equal(set.ranges[1][0], 0x1F640); assert.equal(set.ranges[1][1], 0x1F640); + assert.deepEqual(callbacks.messages, []); + callbacks.clear(); + const len2 = compiler.sizeUnicodeSet(pat); + assert.deepEqual(callbacks.messages, []); + assert.equal(len2, set.length); }); it('should fail in various ways', async function() { const compiler = new KmnCompiler(); @@ -55,12 +65,25 @@ describe('Compiler UnicodeSet function', function() { '[[]': CompilerMessages.ERROR_UnicodeSetSyntaxError, }; for(const [pat, expected] of Object.entries(failures)) { - callbacks.clear(); - assert.notOk(compiler.parseUnicodeSet(pat, 1)); - assert.equal(callbacks.messages.length, 1); - const firstMessage = callbacks.messages[0]; - const code = firstMessage.code; - assert.equal(code, expected, `${compilerErrorFormatCode(code)}≠${compilerErrorFormatCode(expected)} got ${firstMessage.message} for ${pat}`); + { + // verify fails parse + callbacks.clear(); + assert.notOk(compiler.parseUnicodeSet(pat, 1)); + assert.equal(callbacks.messages.length, 1); + const firstMessage = callbacks.messages[0]; + const code = firstMessage.code; + assert.equal(code, expected, `${compilerErrorFormatCode(code)}≠${compilerErrorFormatCode(expected)} got ${firstMessage.message} for parsing ${pat}`); + } + // skip 'out of range' because that one won't fail during sizing. + if (expected !== CompilerMessages.FATAL_UnicodeSetOutOfRange) { + // verify fails size + callbacks.clear(); + assert.equal(compiler.sizeUnicodeSet(pat), -1, `sizing ${pat}`); + assert.equal(callbacks.messages.length, 1); + const firstMessage = callbacks.messages[0]; + const code = firstMessage.code; + assert.equal(code, expected, `${compilerErrorFormatCode(code)}≠${compilerErrorFormatCode(expected)} got ${firstMessage.message} for sizing ${pat}`); + } } }); }); diff --git a/developer/src/kmc-kmn/test/tsconfig.json b/developer/src/kmc-kmn/test/tsconfig.json index 115412f57f397294d7bb8336dfed238cd9b3040c..eb43dc68f19d86f3f6b9b1d0637e6de450c6bc03 100644 --- a/developer/src/kmc-kmn/test/tsconfig.json +++ b/developer/src/kmc-kmn/test/tsconfig.json @@ -13,10 +13,11 @@ }, }, "include": [ - "**/test-*.ts" + "**/test-*.ts", + "./helpers/index.ts" ], "references": [ - { "path": "../../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../../common/web/keyman-version/" }, { "path": "../../../../common/web/types/" }, { "path": "../../../../common/tools/hextobin/" }, { "path": "../../common/web/test-helpers/" }, diff --git a/developer/src/kmc-kmn/tsconfig.json b/developer/src/kmc-kmn/tsconfig.json index b828a5076487c095a83cfaf700f73801f44d758b..04683902eae928e1f470248642611aa843d3f2e5 100644 --- a/developer/src/kmc-kmn/tsconfig.json +++ b/developer/src/kmc-kmn/tsconfig.json @@ -16,7 +16,7 @@ "src/import/kmcmplib/wasm-host.js" ], "references": [ - { "path": "../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../common/web/keyman-version/tsconfig.json" }, { "path": "../../../common/web/types/" }, ] } diff --git a/developer/src/kmc-ldml/build.sh b/developer/src/kmc-ldml/build.sh index 71f146e835adc94adac256997240c25f4a21317b..507b26274b270d72cb3073cacac3570a4b6700ef 100755 --- a/developer/src/kmc-ldml/build.sh +++ b/developer/src/kmc-ldml/build.sh @@ -19,6 +19,7 @@ cd "$THIS_SCRIPT_PATH" builder_describe "Build Keyman kmc Keyboard Compiler module" \ "@/common/web/keyman-version" \ "@/common/web/types" \ + "@/developer/src/kmc-kmn" \ "@/developer/src/common/web/test-helpers" \ "configure" \ "build" \ diff --git a/developer/src/kmc-ldml/package.json b/developer/src/kmc-ldml/package.json index fda0b44272aa05a36300be18b6e3fa9a5877a790..519913bf1b18e976168f4d4b1f83da00153dbd74 100644 --- a/developer/src/kmc-ldml/package.json +++ b/developer/src/kmc-ldml/package.json @@ -26,11 +26,12 @@ "url": "https://github.com/keymanapp/keyman/issues" }, "dependencies": { + "@keymanapp/kmc-kmn": "*", "@keymanapp/keyman-version": "*", "@keymanapp/ldml-keyboard-constants": "*", "ajv": "^8.11.0", "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50", - "semver": "^7.3.7", + "semver": "^7.5.2", "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829" }, "devDependencies": { diff --git a/developer/src/kmc-ldml/src/compiler/compiler-options.ts b/developer/src/kmc-ldml/src/compiler/compiler-options.ts index 3d29e6393ad3de65cca0db87f464c8cc4f91c8f1..a123d34374ccb044628fdef223743917c0bec3a7 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler-options.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler-options.ts @@ -1,4 +1,5 @@ - +/* c8 ignore start */ +import { LDMLKeyboardXMLSourceFileReaderOptions } from "@keymanapp/common-types"; export interface CompilerOptions { /** @@ -10,4 +11,13 @@ export interface CompilerOptions { * Add metadata about the compiler version to .kmx file when compiling */ addCompilerVersion?: boolean; + + /** + * Paths and other options required for reading .xml files + */ + readerOptions: LDMLKeyboardXMLSourceFileReaderOptions; }; + +/* c8 ignore end */ +// TODO-LDML: this file should already be ignored for coverage +// bug: https://github.com/bcoe/c8/issues/359#issuecomment-1272646184 diff --git a/developer/src/kmc-ldml/src/compiler/compiler.ts b/developer/src/kmc-ldml/src/compiler/compiler.ts index b7b46e49b2a2e9304f0b2616ff9d22eab6ab0268..29ff91f90c0f5cbf048d0df4f80a0ba7a45c5c23 100644 --- a/developer/src/kmc-ldml/src/compiler/compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/compiler.ts @@ -1,48 +1,81 @@ -import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile } from '@keymanapp/common-types'; +import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus, CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, UnicodeSetParser } from '@keymanapp/common-types'; import { CompilerOptions } from './compiler-options.js'; import { CompilerMessages } from './messages.js'; -import { BkspCompiler, FinlCompiler, TranCompiler } from './tran.js'; +import { BkspCompiler, TranCompiler } from './tran.js'; import { DispCompiler } from './disp.js'; import { KeysCompiler } from './keys.js'; import { LayrCompiler } from './layr.js'; import { LocaCompiler } from './loca.js'; import { MetaCompiler } from './meta.js'; import { NameCompiler } from './name.js'; -import { OrdrCompiler } from './ordr.js'; import { VkeyCompiler } from './vkey.js'; +import { VarsCompiler } from './vars.js'; +import { StrsCompiler, ElemCompiler, ListCompiler, UsetCompiler } from './empty-compiler.js'; + import LDMLKeyboardXMLSourceFile = LDMLKeyboard.LDMLKeyboardXMLSourceFile; import KMXPlusFile = KMXPlus.KMXPlusFile; - -const SECTION_COMPILERS = [ +import DependencySections = KMXPlus.DependencySections; +import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; +import { KmnCompiler } from '@keymanapp/kmc-kmn'; + +export const SECTION_COMPILERS = [ + // These are in dependency order. + + // First the former 'global' sections + StrsCompiler, + ListCompiler, + ElemCompiler, + UsetCompiler, + // Next, Vars, which depends on others + VarsCompiler, + // Now all others: BkspCompiler, DispCompiler, - FinlCompiler, KeysCompiler, LayrCompiler, LocaCompiler, MetaCompiler, NameCompiler, - OrdrCompiler, TranCompiler, VkeyCompiler, ]; export class LdmlKeyboardCompiler { private readonly callbacks: CompilerCallbacks; - // private readonly options: CompilerOptions; // not currently used + private readonly options: CompilerOptions; + + // uset parser + private usetparser?: UnicodeSetParser = undefined; - constructor (callbacks: CompilerCallbacks, _options?: CompilerOptions) { - /* + constructor (callbacks: CompilerCallbacks, options: CompilerOptions) { this.options = { debug: false, addCompilerVersion: true, ...options }; - */ this.callbacks = callbacks; } + /** + * Construct or return a UnicodeSetParser, aka KmnCompiler + * @returns the held UnicodeSetParser + */ + async getUsetParser() : Promise { + if (this.usetparser === undefined) { + // initialize + const compiler = new KmnCompiler(); + const ok = await compiler.init(this.callbacks); + if (ok) { + this.usetparser = compiler; + } else { + /* c8 ignore next 2 */ + this.usetparser = null; // Store null on failure + } + } + return this.usetparser; + } + private buildSections(source: LDMLKeyboardXMLSourceFile) { return SECTION_COMPILERS.map(c => new c(source, this.callbacks)); } @@ -54,18 +87,21 @@ export class LdmlKeyboardCompiler { * @returns the source file, or null if invalid */ public load(filename: string): LDMLKeyboardXMLSourceFile | null { - const reader = new LDMLKeyboardXMLSourceFileReader(this.callbacks); + const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); + // load the file from disk into a string const data = this.callbacks.loadFile(filename); if(!data) { this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to read XML file'})); return null; } + // parse (load) the string into an object tree const source = reader.load(data); if(!source) { this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to load XML file'})); return null; } try { + // validate the object tree against the .xsd schema if (!reader.validate(source, this.callbacks.loadSchema('ldml-keyboard'))) { return null; } @@ -84,13 +120,14 @@ export class LdmlKeyboardCompiler { * @returns the source file, or null if invalid */ public loadTestData(filename: string): LDMLKeyboardTestDataXMLSourceFile | null { - const reader = new LDMLKeyboardXMLSourceFileReader(this.callbacks); + const reader = new LDMLKeyboardXMLSourceFileReader(this.options.readerOptions, this.callbacks); const data = this.callbacks.loadFile(filename); if(!data) { this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to read XML file'})); return null; } const source = reader.loadTestData(data); + /* c8 ignore next 4 */ if(!source) { this.callbacks.reportMessage(CompilerMessages.Error_InvalidFile({errorText: 'Unable to load XML file'})); return null; @@ -126,27 +163,41 @@ export class LdmlKeyboardCompiler { * @param source in-memory representation of LDML keyboard xml file * @returns KMXPlusFile intermediate file */ - public compile(source: LDMLKeyboardXMLSourceFile): KMXPlusFile { + public async compile(source: LDMLKeyboardXMLSourceFile): Promise { const sections = this.buildSections(source); let passed = true; const kmx = new KMXPlusFile(); - // These two sections are required by other sections - kmx.kmxplus.strs = new KMXPlus.Strs(); - kmx.kmxplus.elem = new KMXPlus.Elem(kmx.kmxplus.strs); - kmx.kmxplus.list = new KMXPlus.List(kmx.kmxplus.strs); - for(let section of sections) { if(!section.validate()) { + // TODO-LDML: coverage passed = false; // We'll keep validating other sections anyway, so we get a full set of // errors for the keyboard developer. continue; } - const sect = section.compile({strs: kmx.kmxplus.strs, elem: kmx.kmxplus.elem, list: kmx.kmxplus.list}); - - /* istanbul ignore if */ + // clone + const globalSections : DependencySections = Object.assign({}, kmx.kmxplus); + // pre-initialize the usetparser + globalSections.usetparser = await this.getUsetParser(); + const dependencies = section.dependencies; + Object.keys(constants.section).forEach((sectstr : string) => { + const sectid : SectionIdent = constants.section[sectstr]; + if (dependencies.has(sectid)) { + /* c8 ignore next 4 */ + if (!kmx.kmxplus[sectid]) { + // Internal error useful during section bring-up + throw new Error(`Internal error: section ${section.id} depends on uninitialized dependency ${sectid}, check ordering`); + } + } else { + // delete dependencies that aren't referenced + delete globalSections[sectid]; + } + }); + const sect = section.compile(globalSections); + + /* c8 ignore next 7 */ if(!sect) { // This should not happen -- validate() should have told us // if something is going to fail to compile @@ -154,6 +205,11 @@ export class LdmlKeyboardCompiler { passed = false; continue; } + /* c8 ignore next 4 */ + if(kmx.kmxplus[section.id]) { + // Internal error useful during section bring-up + throw new Error(`Internal error: section ${section.id} would be assigned twice`); + } kmx.kmxplus[section.id] = sect as any; } diff --git a/developer/src/kmc-ldml/src/compiler/disp.ts b/developer/src/kmc-ldml/src/compiler/disp.ts index 98223de132ee3cfafbaec954d541c662b3277989..241c5af6856178c43ec8836d63b0b2506a9e71bd 100644 --- a/developer/src/kmc-ldml/src/compiler/disp.ts +++ b/developer/src/kmc-ldml/src/compiler/disp.ts @@ -1,14 +1,20 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlus } from '@keymanapp/common-types'; +import { KMXPlus, LDMLKeyboard, MarkerParser } from '@keymanapp/common-types'; import { CompilerMessages } from "./messages.js"; import { SectionCompiler } from "./section-compiler.js"; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Disp = KMXPlus.Disp; import DispItem = KMXPlus.DispItem; +import { MarkerTracker, MarkerUse } from "./marker-tracker.js"; export class DispCompiler extends SectionCompiler { + static validateMarkers(keyboard: LDMLKeyboard.LKKeyboard, mt : MarkerTracker): boolean { + keyboard.displays?.display?.forEach(({ to }) => + mt.add(MarkerUse.match, MarkerParser.allReferences(to))); + return true; + } public get id() { return constants.section.disp; @@ -32,17 +38,19 @@ export class DispCompiler extends SectionCompiler { return valid; } - public compile(sections: GlobalSections): Disp { + public compile(sections: DependencySections): Disp { let result = new Disp(); // displayOptions result.baseCharacter = sections.strs.allocAndUnescapeString(this.keyboard.displays?.displayOptions?.baseCharacter); + // TODO-LDML: substitute variables! + // displays result.disps = this.keyboard.displays?.display.map(display => ({ - to: sections.strs.allocAndUnescapeString(display.to), + to: sections.strs.allocAndUnescapeString(sections.vars.substituteMarkerString(display.to)), display: sections.strs.allocAndUnescapeString(display.display), - })) || []; + })) || []; // TODO-LDML: need coverage for the [] result.disps.sort((a: DispItem, b: DispItem) => a.to.compareTo(b.to)); diff --git a/developer/src/kmc-ldml/src/compiler/empty-compiler.ts b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts new file mode 100644 index 0000000000000000000000000000000000000000..93476d449706d4f13c3469f3be16cdfff3c3be2b --- /dev/null +++ b/developer/src/kmc-ldml/src/compiler/empty-compiler.ts @@ -0,0 +1,75 @@ +import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; +import { SectionCompiler } from "./section-compiler.js"; +import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types"; +import { VarsCompiler } from './vars.js'; + +/** + * Compiler for typrs that don't actually consume input XML + */ +export abstract class EmptyCompiler extends SectionCompiler { + private _id: SectionIdent; + constructor(id: SectionIdent, source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + super(source, callbacks); + this._id = id; + } + + public get id(): SectionIdent { + return this._id; + } + public get dependencies(): Set { + return new Set(); // no dependencies + } +} + +export class StrsCompiler extends EmptyCompiler { + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + super(constants.section.strs, source, callbacks); + } + public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { + return new KMXPlus.Strs(); + } +} + +export class ElemCompiler extends EmptyCompiler { + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + super(constants.section.elem, source, callbacks); + } + public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { + return new KMXPlus.Elem(sections); + } + public get dependencies(): Set { + const strsOnly = new Set([constants.section.strs]); + return strsOnly; + } +} + +export class ListCompiler extends EmptyCompiler { + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + super(constants.section.list, source, callbacks); + } + public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { + return new KMXPlus.List(sections.strs); + } + public get dependencies(): Set { + const strsOnly = new Set([constants.section.strs]); + return strsOnly; + } +} + +export class UsetCompiler extends EmptyCompiler { + constructor(source: LDMLKeyboard.LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + super(constants.section.uset, source, callbacks); + } + public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { + return new KMXPlus.Uset(); + } + public get dependencies(): Set { + const strsOnly = new Set([constants.section.strs]); + return strsOnly; + } +} + +/** + * For test use. The top compilers. + */ +export const BASIC_DEPENDENCIES = [ StrsCompiler, ListCompiler, ElemCompiler, VarsCompiler ]; diff --git a/developer/src/kmc-ldml/src/compiler/keys.ts b/developer/src/kmc-ldml/src/compiler/keys.ts index fab1d8b28ab23391bebb4672b3be1b937bc66ed3..89d93178d3474ae15fd5fa3dd790d3506ca132a3 100644 --- a/developer/src/kmc-ldml/src/compiler/keys.ts +++ b/developer/src/kmc-ldml/src/compiler/keys.ts @@ -1,15 +1,25 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; -import { LDMLKeyboard, KMXPlus, Constants } from '@keymanapp/common-types'; +import { LDMLKeyboard, KMXPlus, Constants, MarkerParser } from '@keymanapp/common-types'; import { CompilerMessages } from './messages.js'; import { SectionCompiler } from "./section-compiler.js"; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Keys = KMXPlus.Keys; import ListItem = KMXPlus.ListItem; import KeysFlicks = KMXPlus.KeysFlicks; import { allUsedKeyIdsInLayers, calculateUniqueKeys, translateLayerAttrToModifier, validModifier } from '../util/util.js'; +import { MarkerTracker, MarkerUse } from './marker-tracker.js'; export class KeysCompiler extends SectionCompiler { + static validateMarkers( + keyboard: LDMLKeyboard.LKKeyboard, + mt: MarkerTracker + ): boolean { + keyboard.keys?.key?.forEach(({ to }) => + mt.add(MarkerUse.emit, MarkerParser.allReferences(to)) + ); + return true; + } public get id() { return constants.section.keys; @@ -20,7 +30,7 @@ export class KeysCompiler extends SectionCompiler { * @returns just the non-touch layers. */ public hardwareLayers() { - return this.keyboard.layers?.filter(({form}) => form !== 'touch'); + return this.keyboard.layers?.filter(({ form }) => form !== "touch"); } public validate() { @@ -30,7 +40,7 @@ export class KeysCompiler extends SectionCompiler { const usedKeys = allUsedKeyIdsInLayers(this.keyboard?.layers); const uniqueKeys = calculateUniqueKeys([...this.keyboard.keys?.key]); for (let key of uniqueKeys) { - const {id, flicks} = key; + const { id, flicks } = key; if (!usedKeys.has(id)) { continue; // unused key, ignore } @@ -38,10 +48,14 @@ export class KeysCompiler extends SectionCompiler { if (!flicks) { continue; // no flicks } - const flickEntry = this.keyboard.keys?.flicks?.find(x => x.id === flicks); - if (!flickEntry ) { + const flickEntry = this.keyboard.keys?.flicks?.find( + (x) => x.id === flicks + ); + if (!flickEntry) { valid = false; - this.callbacks.reportMessage(CompilerMessages.Error_MissingFlicks({flicks, id})); + this.callbacks.reportMessage( + CompilerMessages.Error_MissingFlicks({ flicks, id }) + ); } } @@ -53,20 +67,21 @@ export class KeysCompiler extends SectionCompiler { if (hardwareLayers.length >= 1) { // validate all errors for (let layers of hardwareLayers) { - for(let layer of layers.layer) { - valid = this.validateHardwareLayerForKmap(layers.form, layer) && valid; // note: always validate even if previously invalid results found + for (let layer of layers.layer) { + valid = + this.validateHardwareLayerForKmap(layers.form, layer) && valid; // note: always validate even if previously invalid results found } } - } else { - // TODO-LDML: Touch? + // TODO-LDML: } else { touch? } return valid; } - public compile(sections: GlobalSections): Keys { + public compile(sections: DependencySections): Keys { + /* c8 ignore next 4 */ if (!this.keyboard?.keys?.key && !this.keyboard?.keys?.flicks) { - // short-circuit if no keys or flicks + // short-circuit if no keys or flicks. Doesn't happen in practice due to implied import. return null; } @@ -81,13 +96,16 @@ export class KeysCompiler extends SectionCompiler { // Finally, kmap // Use LayerMap + keys to generate compiled keys for hardware const hardwareLayers = this.hardwareLayers(); + /* c8 ignore next 3 */ if (hardwareLayers.length > 1) { // validation should have already caught this - throw Error(`Internal error: Expected 0 or 1 hardware layer, not ${hardwareLayers.length}`); + throw Error( + `Internal error: Expected 0 or 1 hardware layer, not ${hardwareLayers.length}` + ); } else if (hardwareLayers.length === 1) { const theLayers = hardwareLayers[0]; const { form } = theLayers; - for(let layer of theLayers.layer) { + for (let layer of theLayers.layer) { this.compileHardwareLayerToKmap(sections, layer, sect, form); } } // else: TODO-LDML do nothing if only touch layers @@ -95,19 +113,26 @@ export class KeysCompiler extends SectionCompiler { return sect; } - public loadFlicks(sections: GlobalSections, sect: Keys) { + public loadFlicks(sections: DependencySections, sect: Keys) { for (let lkflicks of this.keyboard.keys.flicks) { - let flicks: KeysFlicks = new KeysFlicks(sections.strs.allocString(lkflicks.id)); + let flicks: KeysFlicks = new KeysFlicks( + sections.strs.allocString(lkflicks.id) + ); for (let lkflick of lkflicks.flick) { let flags = 0; - // TODO-LDML: single char - const to = sections.strs.allocAndUnescapeString(lkflick.to); - flags |= constants.keys_flick_flags_extend; - let directions : ListItem = sections.list.allocListFromSpaces(sections.strs, lkflick.directions); + const to = sections.strs.allocAndUnescapeString(lkflick.to, true); + if (!to.isOneChar) { + flags |= constants.keys_flick_flags_extend; + } + let directions: ListItem = sections.list.allocListFromSpaces( + sections.strs, + lkflick.directions + ); flicks.flicks.push({ directions, flags, + // TODO-LDML: markers,variables to, }); } @@ -116,7 +141,7 @@ export class KeysCompiler extends SectionCompiler { } } - public loadKeys(sections: GlobalSections, sect: Keys) { + public loadKeys(sections: DependencySections, sect: Keys) { const usedKeys = allUsedKeyIdsInLayers(this.keyboard?.layers); const uniqueKeys = calculateUniqueKeys([...this.keyboard.keys?.key]); @@ -130,17 +155,33 @@ export class KeysCompiler extends SectionCompiler { if (!!key.gap) { flags |= constants.keys_key_flags_gap; } - if (key.transform === 'no') { + if (key.transform === "no") { flags |= constants.keys_key_flags_notransform; } const id = sections.strs.allocString(key.id); - const longPress: ListItem = sections.list.allocListFromEscapedSpaces(sections.strs, key.longPress); - const longPressDefault = sections.strs.allocAndUnescapeString(key.longPressDefault); - const multiTap: ListItem = sections.list.allocListFromEscapedSpaces(sections.strs, key.multiTap); + const longPress: ListItem = sections.list.allocListFromEscapedSpaces( + sections.strs, + // TODO-LDML: markers,variables + key.longPress + ); + const longPressDefault = sections.strs.allocAndUnescapeString( + // TODO-LDML: markers,variables + key.longPressDefault + ); + const multiTap: ListItem = sections.list.allocListFromEscapedSpaces( + sections.strs, + // TODO-LDML: markers,variables + key.multiTap + ); const keySwitch = sections.strs.allocString(key.switch); // 'switch' is a reserved word - flags |= constants.keys_key_flags_extend; - const to = sections.strs.allocAndUnescapeString(key.to); // TODO-LDML: single char - const width = Math.ceil((key.width || 1) * 10.0); // default, width=1 + const toRaw = key.to; + // TODO-LDML: variables + let toCooked = sections.vars.substituteMarkerString(toRaw); + const to = sections.strs.allocAndUnescapeString(toCooked, true); + if (!to.isOneChar) { + flags |= constants.keys_key_flags_extend; + } + const width = Math.ceil((key.width || 1) * 10.0); // default, width=1 sect.keys.push({ flags, flicks, @@ -162,32 +203,49 @@ export class KeysCompiler extends SectionCompiler { * @param layer * @returns */ - private validateHardwareLayerForKmap(hardware: string, layer: LDMLKeyboard.LKLayer) { + private validateHardwareLayerForKmap( + hardware: string, + layer: LDMLKeyboard.LKLayer + ) { let valid = true; const { modifier } = layer; if (!validModifier(modifier)) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidModifier({ modifier, layer: layer.id })); + this.callbacks.reportMessage( + CompilerMessages.Error_InvalidModifier({ modifier, layer: layer.id }) + ); valid = false; } const keymap = Constants.HardwareToKeymap.get(hardware); + /* c8 ignore next 5 */ if (!keymap) { - this.callbacks.reportMessage(CompilerMessages.Error_InvalidHardware({ form: hardware })); + // not reached due to XML validation + this.callbacks.reportMessage( + CompilerMessages.Error_InvalidHardware({ form: hardware }) + ); valid = false; } const uniqueKeys = calculateUniqueKeys([...this.keyboard.keys?.key]); if (layer.row.length > keymap.length) { - this.callbacks.reportMessage(CompilerMessages.Error_HardwareLayerHasTooManyRows()); + this.callbacks.reportMessage( + CompilerMessages.Error_HardwareLayerHasTooManyRows() + ); valid = false; } for (let y = 0; y < layer.row.length && y < keymap.length; y++) { - const keys = layer.row[y].keys.split(' '); + const keys = layer.row[y].keys.split(" "); if (keys.length > keymap[y].length) { - this.callbacks.reportMessage(CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({ row: y + 1, hardware, modifier })); + this.callbacks.reportMessage( + CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({ + row: y + 1, + hardware, + modifier, + }) + ); valid = false; } @@ -195,14 +253,24 @@ export class KeysCompiler extends SectionCompiler { for (let key of keys) { x++; - let keydef = uniqueKeys.find(x => x.id == key); + let keydef = uniqueKeys.find((x) => x.id == key); if (!keydef) { - this.callbacks.reportMessage(CompilerMessages.Error_KeyNotFoundInKeyBag({ keyId: key, col: x + 1, row: y + 1, layer: layer.id, form: 'hardware' })); + this.callbacks.reportMessage( + CompilerMessages.Error_KeyNotFoundInKeyBag({ + keyId: key, + col: x + 1, + row: y + 1, + layer: layer.id, + form: "hardware", + }) + ); valid = false; continue; } if (!keydef.to && !keydef.gap && !keydef.switch) { - this.callbacks.reportMessage(CompilerMessages.Error_KeyMissingToGapOrSwitch({ keyId: key })); + this.callbacks.reportMessage( + CompilerMessages.Error_KeyMissingToGapOrSwitch({ keyId: key }) + ); valid = false; continue; } @@ -213,10 +281,10 @@ export class KeysCompiler extends SectionCompiler { } private compileHardwareLayerToKmap( - sections: GlobalSections, + sections: DependencySections, layer: LDMLKeyboard.LKLayer, sect: Keys, - hardware: string, + hardware: string ): Keys { const mod = translateLayerAttrToModifier(layer); const keymap = Constants.HardwareToKeymap.get(hardware); @@ -225,7 +293,7 @@ export class KeysCompiler extends SectionCompiler { for (let row of layer.row) { y++; - const keys = row.keys.split(' '); + const keys = row.keys.split(" "); let x = -1; for (let key of keys) { x++; diff --git a/developer/src/kmc-ldml/src/compiler/layr.ts b/developer/src/kmc-ldml/src/compiler/layr.ts index f506b3e2379469edee063a18869b6b60cbd91067..e0be7a6f5eba9f88bdd6ff740d3b089d14dd428d 100644 --- a/developer/src/kmc-ldml/src/compiler/layr.ts +++ b/developer/src/kmc-ldml/src/compiler/layr.ts @@ -5,7 +5,7 @@ import { SectionCompiler } from "./section-compiler.js"; import { translateLayerAttrToModifier, validModifier } from '../util/util.js'; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Layr = KMXPlus.Layr; import LayrEntry = KMXPlus.LayrEntry; import LayrList = KMXPlus.LayrList; @@ -36,6 +36,8 @@ export class LayrCompiler extends SectionCompiler { valid = false; this.callbacks.reportMessage(CompilerMessages.Error_ExcessHardware({form})); } else if (!constants.layr_list_hardware_map.get(form)) { + /* c8 ignore next 4 */ + // not reached due to XML validation valid = false; this.callbacks.reportMessage(CompilerMessages.Error_InvalidHardware({form})); } @@ -57,7 +59,7 @@ export class LayrCompiler extends SectionCompiler { return valid; } - public compile(sections: GlobalSections): Layr { + public compile(sections: DependencySections): Layr { const sect = new Layr(); sect.lists = this.keyboard.layers.map((layers) => { diff --git a/developer/src/kmc-ldml/src/compiler/loca.ts b/developer/src/kmc-ldml/src/compiler/loca.ts index da7d2fb8d2ee797db12c87dd2292092722baeda6..a57a8224ae7dcca10c20f9498a526f48c3421a05 100644 --- a/developer/src/kmc-ldml/src/compiler/loca.ts +++ b/developer/src/kmc-ldml/src/compiler/loca.ts @@ -3,7 +3,7 @@ import { LDMLKeyboard, KMXPlus } from '@keymanapp/common-types'; import { SectionCompiler } from "./section-compiler.js"; import { CompilerMessages } from "./messages.js"; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Loca = KMXPlus.Loca; import LKKeyboard = LDMLKeyboard.LKKeyboard; @@ -33,6 +33,7 @@ export class LocaCompiler extends SectionCompiler { this.callbacks.reportMessage(CompilerMessages.Error_InvalidLocale({tag})); valid = false; } else { + /* c8 ignore next 2 */ throw e; } } @@ -40,7 +41,7 @@ export class LocaCompiler extends SectionCompiler { return valid; } - public compile(sections: GlobalSections): Loca { + public compile(sections: DependencySections): Loca { let result = new Loca(); // This also minimizes locales according to Remove Likely Subtags algorithm: diff --git a/developer/src/kmc-ldml/src/compiler/marker-tracker.ts b/developer/src/kmc-ldml/src/compiler/marker-tracker.ts new file mode 100644 index 0000000000000000000000000000000000000000..8007f29e5bf41afd35f4c24c07fb8ba8c4d67038 --- /dev/null +++ b/developer/src/kmc-ldml/src/compiler/marker-tracker.ts @@ -0,0 +1,72 @@ +/** + * Verb for MarkerTracker.add() + */ +export enum MarkerUse { + /** outputs this marker into context (e.g. transform to= or key to=) */ + emit, + /** consumes this marker out of the context (e.g. transform from=) */ + consume, + /** matches the marker, but doesn't consume (e.g. display to=) */ + match, + /** variable definition: might consume, emit, or match. */ + variable, +} + +type MarkerSet = Set; + +/** Tracks usage of markers */ +export class MarkerTracker { + /** markers that were emitted */ + emitted: MarkerSet; + /** markers that were consumed and removed from the context */ + consumed: MarkerSet; + /** markers that were matched, but not necessarily consumed */ + matched: MarkerSet; + /** all markers */ + all: MarkerSet; + + constructor() { + this.emitted = new Set(); + this.consumed = new Set(); + this.matched = new Set(); + this.all = new Set(); + } + + /** + * + * @param verb what kind of use we are adding + * @param markers list of markers to add + */ + add(verb: MarkerUse, markers: string[]) { + if (!markers.length) { + return; // skip if empty + } + if (verb == MarkerUse.emit) { + markers.forEach((m) => { + this.emitted.add(m); + this.all.add(m); + }); + } else if (verb == MarkerUse.consume) { + markers.forEach((m) => { + this.consumed.add(m); + this.all.add(m); + }); + } else if (verb == MarkerUse.match) { + markers.forEach((m) => { + this.matched.add(m); + this.all.add(m); + }); + } else if (verb == MarkerUse.variable) { + markers.forEach((m) => { + // we don't know, so add it to all three + this.matched.add(m); + this.emitted.add(m); + this.consumed.add(m); + this.all.add(m); + }); + /* c8 skip next 3 */ + } else { + throw Error(`Internal error: unsupported verb ${verb} for match`); + } + } +} diff --git a/developer/src/kmc-ldml/src/compiler/messages.ts b/developer/src/kmc-ldml/src/compiler/messages.ts index 32eb943a683ee6ef1995932b685e6f9a467f3669..e3ec8bcbf24c33317baaeab8e42d939c5ddfa2d9 100644 --- a/developer/src/kmc-ldml/src/compiler/messages.ts +++ b/developer/src/kmc-ldml/src/compiler/messages.ts @@ -2,7 +2,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler; -// const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.KeyboardCompiler; +// const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevError = CompilerErrorSeverity.Error | CompilerErrorNamespace.LdmlKeyboardCompiler; const SevFatal = CompilerErrorSeverity.Fatal | CompilerErrorNamespace.LdmlKeyboardCompiler; @@ -35,9 +35,9 @@ export class CompilerMessages { m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${o.sourceLocale}' is not minimal or correctly formatted and should be '${o.locale}'`); static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008; - static Error_VkeyIsNotValid = (o:{vkey: string}) => - m(this.ERROR_VkeyIsNotValid, `Virtual key '${o.vkey}' is not found in the CLDR VKey Enum table.`); - static ERROR_VkeyIsNotValid = SevError | 0x0009; + static Hint_VkeyIsNotValid = (o:{vkey: string}) => + m(this.HINT_VkeyIsNotValid, `Virtual key '${o.vkey}' is not found in the CLDR VKey Enum table.`); + static HINT_VkeyIsNotValid = SevHint | 0x0009; static Hint_VkeyIsRedundant = (o:{vkey: string}) => m(this.HINT_VkeyIsRedundant, `Virtual key '${o.vkey}' is mapped to itself, which is redundant.`); @@ -89,5 +89,50 @@ export class CompilerMessages { static Error_MissingFlicks = (o:{flicks: string, id: string}) => m(this.ERROR_MissingFlicks, `key id=${o.id} refers to missing flicks=${o.flicks}`); static ERROR_MissingFlicks = SevError | 0x0015; + + static Error_DuplicateVariable = (o:{ids: string}) => m(this.ERROR_DuplicateVariable, + `duplicate variables: id=${o.ids}`); + static ERROR_DuplicateVariable = SevError | 0x0016; + + // Not hit due to XML parsing + static Error_InvalidTransformsType = (o:{types: string[]}) => + m(this.ERROR_InvalidTransformsType, `Invalid transforms types: '${o.types?.join(',')}'`); + static ERROR_InvalidTransformsType = SevError | 0x0018; + + static Error_DuplicateTransformsType = (o:{types: string[]}) => + m(this.ERROR_DuplicateTransformsType, `Duplicate transforms types: '${o.types?.join(',')}'`); + static ERROR_DuplicateTransformsType = SevError | 0x0019; + + static Error_MixedTransformGroup = () => + m(this.ERROR_MixedTransformGroup, `transformGroup cannot contain both reorder and transform elements`); + static ERROR_MixedTransformGroup = SevError | 0x001A; + + static Error_EmptyTransformGroup = () => + m(this.ERROR_EmptyTransformGroup, `transformGroup must have either reorder or transform elements`); + static ERROR_EmptyTransformGroup = SevError | 0x001B; + + static Error_MissingStringVariable = (o:{id: string}) => + m(this.ERROR_MissingStringVariable, `Reference to undefined string variable: \${${o.id}}`); + static ERROR_MissingStringVariable = SevError | 0x001C; + + static Error_MissingSetVariable = (o:{id: string}) => + m(this.ERROR_MissingSetVariable, `Reference to undefined set variable: \$[${o.id}]`); + static ERROR_MissingSetVariable = SevError | 0x001D; + + static Error_MissingUnicodeSetVariable = (o:{id: string}) => + m(this.ERROR_MissingUnicodeSetVariable, `Reference to undefined UnicodeSet variable: \$[${o.id}]`); + static ERROR_MissingUnicodeSetVariable = SevError | 0x001E; + + static Error_NeedSpacesBetweenSetVariables = (o:{item: string}) => + m(this.ERROR_NeedSpacesBetweenSetVariables, `Need spaces between set variables: ${o.item}`); + static ERROR_NeedSpacesBetweenSetVariables = SevError | 0x001F; + + static Error_CantReferenceSetFromUnicodeSet = (o:{id: string}) => + m(this.ERROR_CantReferenceSetFromUnicodeSet, `Illegal use of set variable from within UnicodeSet: \$[${o.id}]`); + static ERROR_CantReferenceSetFromUnicodeSet = SevError | 0x0020; + + static Error_MissingMarkers = (o: { ids: string[] }) => + m(this.ERROR_MissingMarkers, `Markers used for matching but not defined: ${o.ids?.join(',')}`); + static ERROR_MissingMarkers = SevError | 0x0021; } diff --git a/developer/src/kmc-ldml/src/compiler/meta.ts b/developer/src/kmc-ldml/src/compiler/meta.ts index feb4d79f173b52fd368eacd24c80a8413e8d4d2b..2606692d99a80d21eaa3060e87aa92eabd8cb44e 100644 --- a/developer/src/kmc-ldml/src/compiler/meta.ts +++ b/developer/src/kmc-ldml/src/compiler/meta.ts @@ -6,7 +6,7 @@ import { CompilerMessages } from "./messages.js"; import { SectionCompiler } from "./section-compiler.js"; import semver from "semver"; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Meta = KMXPlus.Meta; import Meta_NormalizationForm = KMXPlus.Meta_NormalizationForm; import KeyboardSettings = KMXPlus.KeyboardSettings; @@ -51,7 +51,7 @@ export class MetaCompiler extends SectionCompiler { return true; } - public compile(sections: GlobalSections): Meta { + public compile(sections: DependencySections): Meta { let result = new Meta(); result.author = sections.strs.allocString(this.keyboard.info?.author); result.conform = sections.strs.allocString(this.keyboard.conformsTo); @@ -65,4 +65,4 @@ export class MetaCompiler extends SectionCompiler { (this.keyboard.settings?.transformPartial == "hide" ? KeyboardSettings.transformPartial : 0); return result; } -} \ No newline at end of file +} diff --git a/developer/src/kmc-ldml/src/compiler/name.ts b/developer/src/kmc-ldml/src/compiler/name.ts index 33fad1ef9aa6f927f20a4a66b6ab1cc88711ec68..21307cbc00c9548ec894191b69c08ebcf726caab 100644 --- a/developer/src/kmc-ldml/src/compiler/name.ts +++ b/developer/src/kmc-ldml/src/compiler/name.ts @@ -2,7 +2,7 @@ import { constants } from "@keymanapp/ldml-keyboard-constants"; import { KMXPlus } from '@keymanapp/common-types'; import { SectionCompiler } from "./section-compiler.js"; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Name = KMXPlus.Name; export class NameCompiler extends SectionCompiler { @@ -17,7 +17,7 @@ export class NameCompiler extends SectionCompiler { return valid; } - public compile(sections: GlobalSections): Name { + public compile(sections: DependencySections): Name { let result = new Name(); result.names = this.keyboard.names?.name?.map(v => sections.strs.allocString(v.value)) ?? []; return result; diff --git a/developer/src/kmc-ldml/src/compiler/ordr.ts b/developer/src/kmc-ldml/src/compiler/ordr.ts deleted file mode 100644 index 89785f107d621877212820590c7e44c32528ec31..0000000000000000000000000000000000000000 --- a/developer/src/kmc-ldml/src/compiler/ordr.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlus, LDMLKeyboard } from '@keymanapp/common-types'; -import { SectionCompiler } from "./section-compiler.js"; - -import GlobalSections = KMXPlus.GlobalSections; -import Ordr = KMXPlus.Ordr; -import OrdrItem = KMXPlus.OrdrItem; -import LKReorder = LDMLKeyboard.LKReorder; -import LKReorders = LDMLKeyboard.LKReorders; - -export class OrdrCompiler extends SectionCompiler { - - public get id() { - return constants.section.ordr; - } - - public validate(): boolean { - let valid = true; - // TODO-LDML: linting here should check for identical before+from, but this involves a double-parse which is ugly - // TODO-LDML: unicodesets means that either we fully parse them and verify conflicting rules or the linting is imperfect - return valid; - } - - private compileReorder(sections: GlobalSections, reorder: LKReorder): OrdrItem { - let result = new OrdrItem(); - result.elements = sections.elem.allocElementString(sections.strs, reorder.from, reorder.order, reorder.tertiary, reorder.tertiary_base, reorder.prebase); - result.before = sections.elem.allocElementString(sections.strs, reorder.before); - return result; - } - - private compileReorders(sections: GlobalSections, reorders: LKReorders): Ordr { - let result = new Ordr(); - - if(reorders?.reorder) { - for(let reorder of reorders.reorder) { - result.items.push(this.compileReorder(sections, reorder)); - } - } - - return result; - } - - public compile(sections: GlobalSections): Ordr { - return this.compileReorders(sections, this.keyboard.reorders); - } -} diff --git a/developer/src/kmc-ldml/src/compiler/section-compiler.ts b/developer/src/kmc-ldml/src/compiler/section-compiler.ts index a96d935190336ea1af257d6e3ec9700af103f400..9e187fafe621cba14199402ca7d063e9d20ed744 100644 --- a/developer/src/kmc-ldml/src/compiler/section-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/section-compiler.ts @@ -1,5 +1,5 @@ import { LDMLKeyboard, KMXPlus, CompilerCallbacks } from "@keymanapp/common-types"; -import { SectionIdent } from '@keymanapp/ldml-keyboard-constants'; +import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants'; /* istanbul ignore next */ export class SectionCompiler { @@ -11,19 +11,30 @@ export class SectionCompiler { this.callbacks = callbacks; } + /* c8 ignore next 11 */ public get id(): SectionIdent { - return null; + throw Error(`Internal Error: id() not implemented`); } - public get required(): boolean { - return true; - } - - public compile(sections: KMXPlus.GlobalSections): KMXPlus.Section { - return null; + public compile(sections: KMXPlus.DependencySections): KMXPlus.Section { + throw Error(`Internal Error: compile() not implemented`); } public validate(): boolean { return true; } + + /** + * Get the dependencies for this compiler. + * @returns set of dependent sections + */ + public get dependencies(): Set { + const defaults = new Set([ + constants.section.strs, + constants.section.list, + constants.section.elem, + constants.section.vars, + ]); + return defaults; + } } diff --git a/developer/src/kmc-ldml/src/compiler/tran.ts b/developer/src/kmc-ldml/src/compiler/tran.ts index 428dbd64f77db8bbb93b67fe5a3de9b91566e06d..fa1f828622a7794d3d138f2a37f68e3c5072716a 100644 --- a/developer/src/kmc-ldml/src/compiler/tran.ts +++ b/developer/src/kmc-ldml/src/compiler/tran.ts @@ -1,22 +1,35 @@ -import { constants } from "@keymanapp/ldml-keyboard-constants"; -import { KMXPlus, LDMLKeyboard, CompilerCallbacks } from '@keymanapp/common-types'; +import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants"; +import { KMXPlus, LDMLKeyboard, CompilerCallbacks, VariableParser, MarkerParser } from '@keymanapp/common-types'; import { SectionCompiler } from "./section-compiler.js"; -import Finl = KMXPlus.Finl; -import FinlItem = KMXPlus.FinlItem; import Bksp = KMXPlus.Bksp; -import BkspItem = KMXPlus.BkspItem; -import GlobalSections = KMXPlus.GlobalSections; +import DependencySections = KMXPlus.DependencySections; import Tran = KMXPlus.Tran; -import TranItem = KMXPlus.TranItem; -import TranItemFlags = KMXPlus.TranItemFlags; +import TranGroup = KMXPlus.TranGroup; +import TranReorder = KMXPlus.TranReorder; +import TranTransform = KMXPlus.TranTransform; import LDMLKeyboardXMLSourceFile = LDMLKeyboard.LDMLKeyboardXMLSourceFile; +import LKTransformGroup = LDMLKeyboard.LKTransformGroup; +import LKReorder = LDMLKeyboard.LKReorder; import LKTransform = LDMLKeyboard.LKTransform; import LKTransforms = LDMLKeyboard.LKTransforms; +import { verifyValidAndUnique } from "../util/util.js"; +import { CompilerMessages } from "./messages.js"; +import { MarkerTracker, MarkerUse } from "./marker-tracker.js"; -type TransformCompilerType = 'simple' | 'final' | 'backspace'; +type TransformCompilerType = 'simple' | 'backspace'; -class TransformCompiler extends SectionCompiler { +export class TransformCompiler extends SectionCompiler { + + static validateMarkers(keyboard: LDMLKeyboard.LKKeyboard, mt : MarkerTracker): boolean { + keyboard?.transforms?.forEach(transforms => + transforms.transformGroup.forEach(transformGroup => { + transformGroup.transform?.forEach(({ to, from }) => { + mt.add(MarkerUse.emit, MarkerParser.allReferences(to)); + mt.add(MarkerUse.consume, MarkerParser.allReferences(from)); + })})); + return true; + } protected type: T; @@ -25,59 +38,161 @@ class TransformCompiler type); + if (!verifyValidAndUnique(types, + types => reportMessage(CompilerMessages.Error_DuplicateTransformsType({ types })), + new Set(['simple', 'backspace']), + types => reportMessage(CompilerMessages.Error_InvalidTransformsType({ types })))) { + valid = false; + } + + // check for mixed groups + let mixed = false; + let empty = false; + transforms.forEach(({transformGroup}) => transformGroup.forEach((transformGroup) => { + if (transformGroup.reorder?.length && transformGroup.transform?.length) { + mixed = true; + } + if (!transformGroup.reorder?.length && !transformGroup.transform?.length) { + empty = true; + } + })); + if (mixed) { + valid = false; + reportMessage(CompilerMessages.Error_MixedTransformGroup()); // report this once + } + if (empty) { + valid = false; + reportMessage(CompilerMessages.Error_EmptyTransformGroup()); // report this once + } + + // TODO-LDML: linting here should check for identical from, but this involves a double-parse which is ugly + // TODO-LDML: unicodesets means that either we fully parse them and verify conflicting rules or the linting is imperfect + } return valid; } - protected newTranItem(): TranItemBase { - return null; + /* c8 ignore next 4 */ + /** allocate a new TranBase subclass */ + protected newTran(): TranBase { + throw Error(`Internal Error: newTran() not implemented`); } - protected newTran(): TranBase { - return null; + private compileTransforms(sections: DependencySections, transforms: LKTransforms): TranBase { + let result = this.newTran(); + + if (transforms?.transformGroup) { + for (let transformGroup of transforms.transformGroup) { + result.groups.push(this.compileTransformGroup(sections, transformGroup)); + } + } + return result; } - private compileTransform(sections: GlobalSections, transform: LKTransform): TranItemBase { - let result = this.newTranItem(); - result.from = sections.elem.allocElementString(sections.strs, transform.from); - result.to = sections.strs.allocAndUnescapeString(transform.to); - result.before = sections.elem.allocElementString(sections.strs, transform.before); - result.flags = transform.error == 'fail' ? TranItemFlags.error : TranItemFlags.none; + private compileTransformGroup(sections: DependencySections, transformGroup: LKTransformGroup): TranGroup { + if (transformGroup.reorder.length && transformGroup.transform.length) { + /* c8 ignore next 2 */ + // should have been caught by validate + throw Error(`Internal error: transformGroup has both reorder and transform elements.`); + } else if (transformGroup.reorder.length) { + return this.compileReorderTranGroup(sections, transformGroup.reorder); + } else if (transformGroup.transform.length) { + return this.compileTransformTranGroup(sections, transformGroup.transform); + } else { + /* c8 ignore next */ + throw Error(`Internal error: transformGroup has neither reorder nor transform elements.`); + } + } + + private compileTransformTranGroup(sections: DependencySections, transforms: LKTransform[]): TranGroup { + const result : TranGroup = { + type: constants.tran_group_type_transform, + transforms: transforms.map(transform => this.compileTransform(sections, transform)), + reorders: [], + } return result; } - private compileTransforms(sections: GlobalSections, transforms: LKTransforms): TranBase { - let result = this.newTran(); + private compileTransform(sections: DependencySections, transform: LKTransform) : TranTransform { + let result = new TranTransform(); + let cookedFrom = transform.from; + let cookedTo = transform.to; + + cookedFrom = sections.vars.substituteStrings(cookedFrom, sections); + // TODO: handle 'map' case + const mapFrom = VariableParser.CAPTURE_SET_REFERENCE.exec(cookedFrom); + const mapTo = VariableParser.MAPPED_SET_REFERENCE.exec(cookedTo || ''); + if (mapFrom && mapTo) { // TODO-LDML: error cases + result.mapFrom = sections.strs.allocString(mapFrom[1]); // var name + result.mapTo = sections.strs.allocString(mapTo[1]); // var name + } else { + result.mapFrom = sections.strs.allocString(''); // TODO-LDML + result.mapTo = sections.strs.allocString(''); // TODO-LDML + } - if(transforms?.transform) { - for(let transform of transforms.transform) { - result.items.push(this.compileTransform(sections, transform)); - } + cookedFrom = sections.vars.substituteSetRegex(cookedFrom, sections); + + if (cookedTo) { + cookedTo = sections.vars.substituteStrings(cookedTo, sections); } + // add in markers. idempotent if no markers. + cookedFrom = sections.vars.substituteMarkerString(cookedFrom); // TODO-LDML: need to support \m{.} here, maybe other edge cases + cookedTo = sections.vars.substituteMarkerString(cookedTo); + + result.from = sections.strs.allocAndUnescapeString(cookedFrom); // TODO-LDML: not unescaped here, done previously + result.to = sections.strs.allocAndUnescapeString(cookedTo); // TODO-LDML: not unescaped here, done previously return result; } - public compile(sections: GlobalSections): TranBase { + private compileReorderTranGroup(sections: DependencySections, reorders: LKReorder[]): TranGroup { + const result : TranGroup = { + type: constants.tran_group_type_reorder, + transforms: [], + reorders: reorders.map(reorder => this.compileReorder(sections, reorder)), + } + return result; + } + + private compileReorder(sections: DependencySections, reorder: LKReorder): TranReorder { + let result = new TranReorder(); + result.elements = sections.elem.allocElementString(sections, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase); + result.before = sections.elem.allocElementString(sections, reorder.before); + return result; + } + + public compile(sections: DependencySections): TranBase { for(let t of this.keyboard.transforms) { if(t.type == this.type) { + // compile only the transforms of the correct type return this.compileTransforms(sections, t); } } - return this.newTran(); + return this.newTran(); // empty: nothing of this type found. + } + public get dependencies(): Set { + const defaults = new Set([ + constants.section.strs, + constants.section.list, + constants.section.elem, + constants.section.vars, + constants.section.uset, + ]); + defaults.delete(this.id); + return defaults; } } -export class TranCompiler extends TransformCompiler<'simple', Tran, TranItem> { +export class TranCompiler extends TransformCompiler<'simple', Tran /*, TranItem*/> { constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { super(source, callbacks); this.type = 'simple'; } - protected newTranItem(): TranItem { - return new TranItem(); - } protected newTran(): Tran { return new Tran(); } @@ -86,30 +201,11 @@ export class TranCompiler extends TransformCompiler<'simple', Tran, TranItem> { } }; -export class FinlCompiler extends TransformCompiler<'final', Finl, FinlItem> { - constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { - super(source, callbacks); - this.type = 'final'; - } - protected newTranItem(): FinlItem { - return new FinlItem(); - } - protected newTran(): Finl { - return new Finl(); - } - public get id() { - return constants.section.finl; - } -}; - -export class BkspCompiler extends TransformCompiler<'backspace', Bksp, BkspItem> { +export class BkspCompiler extends TransformCompiler<'backspace', Bksp /*, BkspItem*/> { constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { super(source, callbacks); this.type = 'backspace'; } - protected newTranItem(): BkspItem { - return new BkspItem(); - } protected newTran(): Bksp { return new Bksp(); } diff --git a/developer/src/kmc-ldml/src/compiler/vars.ts b/developer/src/kmc-ldml/src/compiler/vars.ts new file mode 100644 index 0000000000000000000000000000000000000000..b6e61367508190180037f9d2710466da3543f434 --- /dev/null +++ b/developer/src/kmc-ldml/src/compiler/vars.ts @@ -0,0 +1,245 @@ +import { SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants"; +import { KMXPlus, LDMLKeyboard, CompilerCallbacks, MarkerParser } from '@keymanapp/common-types'; +import { VariableParser } from '@keymanapp/common-types'; +import { SectionCompiler } from "./section-compiler.js"; +import Vars = KMXPlus.Vars; +import StringVarItem = KMXPlus.StringVarItem; +import SetVarItem = KMXPlus.SetVarItem; +import UnicodeSetItem = KMXPlus.UnicodeSetItem; +import DependencySections = KMXPlus.DependencySections; +import LDMLKeyboardXMLSourceFile = LDMLKeyboard.LDMLKeyboardXMLSourceFile; +import { CompilerMessages } from "./messages.js"; +import { KeysCompiler } from "./keys.js"; +import { TransformCompiler } from "./tran.js"; +import { DispCompiler } from "./disp.js"; +import { MarkerTracker, MarkerUse } from "./marker-tracker.js"; +export class VarsCompiler extends SectionCompiler { + public get id() { + return constants.section.vars; + } + + public get dependencies(): Set { + const defaults = new Set([ + constants.section.strs, + constants.section.elem, + constants.section.list, + ]); + defaults.delete(this.id); + return defaults; + } + + constructor(source: LDMLKeyboardXMLSourceFile, callbacks: CompilerCallbacks) { + super(source, callbacks); + } + + public validate(): boolean { + let valid = true; + + // Check for duplicate ids + const allIds = new Set(); + const dups = new Set(); + const variables = this.keyboard?.variables; + + if (!variables) return valid; + + const allStrings = new Set(); + const allSets = new Set(); + const allUnicodeSets = new Set(); + + /** + * add an ID to check for duplicates + * @param id id + */ + function addId(id : string) : void { + if (allIds.has(id)) { + dups.add(id); + } else { + allIds.add(id); + } + } + + // Strings + for (const {id, value} of variables?.string) { + addId(id); + allStrings.add(id); + const stringrefs = VariableParser.allStringReferences(value); + for (const id2 of stringrefs) { + if (!allStrings.has(id2)) { + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_MissingStringVariable({id: id2})); + } + } + } + // Sets + for (const {id, value} of variables.set) { + addId(id); + allSets.add(id); + // check for illegal references, here. + const stringrefs = VariableParser.allStringReferences(value); + for (const id2 of stringrefs) { + if (!allStrings.has(id2)) { + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_MissingStringVariable({id: id2})); + } + } + + // Now split into spaces. + const items : string[] = VariableParser.setSplitter(value); + for (const item of items) { + const setrefs = VariableParser.allSetReferences(item); + if (setrefs.length > 1) { + // this is the form $[seta]$[setb] + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_NeedSpacesBetweenSetVariables({item})); + } else { + for (const id2 of setrefs) { + if (!allSets.has(id2)) { + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_MissingSetVariable({id: id2})); + } + } + } + // TODO-LDML: Are there other illegal cases here? what about "x$[set]"? + } + } + // UnicodeSets + for (const {id, value} of variables.unicodeSet) { + addId(id); + allUnicodeSets.add(id); + const stringrefs = VariableParser.allStringReferences(value); + for (const id2 of stringrefs) { + if (!allStrings.has(id2)) { + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_MissingStringVariable({id: id2})); + } + } + const setrefs = VariableParser.allSetReferences(value); + for (const id2 of setrefs) { + if (!allUnicodeSets.has(id2)) { + valid = false; + if (allSets.has(id2)) { + // $[set] in a UnicodeSet must be another UnicodeSet. + this.callbacks.reportMessage(CompilerMessages.Error_CantReferenceSetFromUnicodeSet({id: id2})); + } else { + this.callbacks.reportMessage(CompilerMessages.Error_MissingUnicodeSetVariable({id: id2})); + } + } + } + } + + // one report if any dups + if (dups.size > 0) { + this.callbacks.reportMessage(CompilerMessages.Error_DuplicateVariable({ + ids: Array.from(dups.values()).sort().join(', ') + })); + valid = false; + } + + valid = this.validateMarkers() && valid; // accumulate validity + + return valid; + } + + private collectMarkers(mt : MarkerTracker) : boolean { + let valid = true; + + // call our friends to validate + valid = this.validateVarsMarkers(this.keyboard, mt) && valid; // accumulate validity + valid = KeysCompiler.validateMarkers(this.keyboard, mt) && valid; // accumulate validity + valid = TransformCompiler.validateMarkers(this.keyboard, mt) && valid; // accumulate validity + valid = DispCompiler.validateMarkers(this.keyboard, mt) && valid; // accumulate validity + + return valid; + } + + private validateMarkers(): boolean { + const mt = new MarkerTracker(); + let valid = this.collectMarkers(mt); + // see if there are any matched-but-not-emitted + const matchedNotEmitted : Set = new Set(); + for (const m of mt.matched.values()) { + if (m === MarkerParser.ANY_MARKER_ID) continue; // match-all marker + if (!mt.emitted.has(m)) { + matchedNotEmitted.add(m); + } + } + for (const m of mt.consumed.values()) { + if (m === MarkerParser.ANY_MARKER_ID) continue; // match-all marker + if (!mt.emitted.has(m)) { + matchedNotEmitted.add(m); + } + } + + // report once + if (matchedNotEmitted.size > 0) { + this.callbacks.reportMessage(CompilerMessages.Error_MissingMarkers({ ids: Array.from(matchedNotEmitted.values()).sort() })); + valid = false; + } + return valid; + } + + validateVarsMarkers(keyboard: LDMLKeyboard.LKKeyboard, mt : MarkerTracker) : boolean { + keyboard?.variables?.string?.forEach(({value}) => + mt.add(MarkerUse.variable, MarkerParser.allReferences(value))); + return true; + } + + public compile(sections: DependencySections): Vars { + const result = new Vars(); + + const variables = this.keyboard?.variables; + // we always have vars, it's depended on by other sections + if (!variables) return result; // Empty vars, to simplify other sections + + // we already know the variables do not conflict with each other + // need to add these one by one, because they depend on each other. + // we don't add these in completely 'natural' order because xml2js has already lost that. + // instead we go: strings, sets, then unicodeSets. + + // first, strings. + variables?.string?.forEach((e) => + this.addString(result, e, sections)); + variables?.set?.forEach((e) => + this.addSet(result, e, sections)); + variables?.unicodeSet?.forEach((e) => + this.addUnicodeSet(result, e, sections)); + + // reload markers - TODO-LDML: double work! + const mt = new MarkerTracker(); + this.collectMarkers(mt); + + // collect all markers, excluding the match-all + const allMarkers : string[] = Array.from(mt.all).filter(m => m !== MarkerParser.ANY_MARKER_ID).sort(); + result.markers = sections.list.allocList(sections.strs, allMarkers); + + return result.valid() ? result : null; + } + + // routines for initializing Vars remain here, in the compiler. + addString(result: Vars, e: LDMLKeyboard.LKString, sections: DependencySections): void { + const { id } = e; + let { value } = e; + // fix any variables + value = result.substituteStrings(value, sections); + result.strings.push(new StringVarItem(id, value, sections)); + } + addSet(result: Vars, e: LDMLKeyboard.LKSet, sections: DependencySections): void { + const { id } = e; + let { value } = e; + // first substitute strings + value = result.substituteStrings(value, sections); + // OK to do this as a substitute, because we've already validated the set above. + value = result.substituteSets(value, sections); + const items : string[] = VariableParser.setSplitter(value); + result.sets.push(new SetVarItem(id, items, sections)); + } + addUnicodeSet(result: Vars, e: LDMLKeyboard.LKUnicodeSet, sections: DependencySections): void { + const { id } = e; + let { value } = e; + value = result.substituteStrings(value, sections); + value = result.substituteUnicodeSets(value, sections); + result.unicodeSets.push(new UnicodeSetItem(id, value, sections, sections.usetparser)); + } + // routines for using/substituting variables have been moved to the Vars class and its + // properties +} diff --git a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts index d951080b9b41467553e235db92cede920f2789c1..0d0c80209540395eb89258ef97155f6d00a9d531 100644 --- a/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts +++ b/developer/src/kmc-ldml/src/compiler/visual-keyboard-compiler.ts @@ -44,6 +44,10 @@ export class LdmlKeyboardVisualKeyboardCompiler { let keydef = source.keyboard.keys?.key?.find(x => x.id == key); + if (!keydef) { + throw Error(`Internal Error: could not find key id="${key}" in layer "${layer.id || ''}", row "${y}"`); + } + vk.keys.push({ flags: VisualKeyboard.VisualKeyboardKeyFlags.kvkkUnicode, shift: shift, diff --git a/developer/src/kmc-ldml/src/compiler/vkey.ts b/developer/src/kmc-ldml/src/compiler/vkey.ts index c9b58b693c64730d2fdd625b5a409b9b07ae9c53..2d6d44d9d014efc9822a4040ac2100223eed9479 100644 --- a/developer/src/kmc-ldml/src/compiler/vkey.ts +++ b/developer/src/kmc-ldml/src/compiler/vkey.ts @@ -12,10 +12,6 @@ export class VkeyCompiler extends SectionCompiler { return constants.section.vkey; } - public get required(): boolean { - return !!this.keyboard.vkeys; - } - public validate(): boolean { let valid = true; if(this.keyboard.vkeys) { @@ -23,13 +19,15 @@ export class VkeyCompiler extends SectionCompiler { this.keyboard.vkeys.vkey.forEach(vk => { if(LdmlVkeyNames[vk.from] === undefined) { - this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid({vkey: vk.from})); - valid = false; + // TODO-LDML: When we do #7135 this may need to change back to an error. + this.callbacks.reportMessage(CompilerMessages.Hint_VkeyIsNotValid({vkey: vk.from})); + return; } if(LdmlVkeyNames[vk.to] === undefined) { - this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid({vkey: vk.to})); - valid = false; + // TODO-LDML: When we do #7135 this may need to change back to an error. + this.callbacks.reportMessage(CompilerMessages.Hint_VkeyIsNotValid({vkey: vk.to})); + return; } if(vk.from == vk.to) { @@ -54,7 +52,8 @@ export class VkeyCompiler extends SectionCompiler { public compile(): Vkey { let result = new Vkey(); if(!this.keyboard.vkeys) { - return result; + /* c8 ignore next 2 */ + return result; // not hit due to boxing } result.vkeys = this.keyboard.vkeys?.vkey.map(vk => { diff --git a/developer/src/kmc-ldml/src/util/util.ts b/developer/src/kmc-ldml/src/util/util.ts index 7caad74e3a317f3cc26422bb768f58b0723428f4..bfbd9eb47f94f95b2235ed2b08e306803958ef63 100644 --- a/developer/src/kmc-ldml/src/util/util.ts +++ b/developer/src/kmc-ldml/src/util/util.ts @@ -55,6 +55,48 @@ export function allUsedKeyIdsInLayers(layersList : LDMLKeyboard.LKLayers[] | nul return s; } +/** + * Helper function for validating child elements. Written for the convenience of message passing functions. + * + * @param values array of values to check + * @param onDuplicate callback with array of duplicate values, deduped + * @param allowed optional set of valid values + * @param onInvalid callback with array of invalid values, deduped + * @returns true if all OK + */ +export function verifyValidAndUnique( + values: string[], + onDuplicate: (duplicates: string[]) => void, + allowed?: Set, + onInvalid?: (invalids: string[]) => void) + : boolean { + const dups: string[] = []; + const invalids: string[] = []; + const seen = new Set(); + for (const value of values) { + if (allowed && !allowed.has(value)) { + invalids.push(value); + } + if (seen.has(value)) { + dups.push(value); + } else { + seen.add(value); + } + } + + function dedupedSortedArray(values: string[]) : string[] { + return Array.from(new Set(values)).sort(); + } + + if (dups.length > 0 && onDuplicate) { + onDuplicate(dedupedSortedArray(dups)); + } + if (invalids.length > 0 && onInvalid) { + onInvalid(dedupedSortedArray(invalids)); + } + return (!dups.length && !invalids.length); +} + /** * Determine modifier from layer info * @param layer layer obj diff --git a/developer/src/kmc-ldml/test/fixtures/basic.txt b/developer/src/kmc-ldml/test/fixtures/basic.txt index 195066648dd526b2d48342035e9cf4ff514a4540..cbbb4038915ca9a7a8afaef08680b2204b5d6ec6 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.txt +++ b/developer/src/kmc-ldml/test/fixtures/basic.txt @@ -15,6 +15,9 @@ # This will compile both the .xml and the .txt to build/test/fixtures and also emit the # checksum for basic-xml.kmx so you can patch that into this file. # +# For the format of this file, see “hextobin.ts” +# +# P.S. surprisingly, the Dart language highlighter in VSCode does a helpful job on this file. block(kmxheader) # struct COMP_KEYBOARD { 4b 58 54 53 # KMX_DWORD dwIdentifier; // 0000 Keyman compiled keyboard id @@ -53,7 +56,7 @@ block(stores) # struct COMP_STORE { offset(store_name_name) # KMX_DWORD dpName; offset(store_name_string) # KMX_DWORD dpString; # }; - # excluding this so we don't have compiled version changes + # excluding this so we don’t have compiled version changes # 14 00 00 00 # KMX_DWORD dwSystemID; - TSS_COMPILEDVERSION # offset(store_compiledversion_name) # KMX_DWORD dpName; # offset(store_compiledversion_string) # KMX_DWORD dpString; @@ -95,8 +98,8 @@ block(sect) # struct COMP_KMXPLUS_SECT { # KMX_DWORD offset; // 0014+ Section offset relative to dpKMXPlus of section block(sectitems) - # 62 6b 73 70 - # diff(sect,bksp) + 62 6b 73 70 + diff(sect,bksp) 64 69 73 70 diff(sect,disp) @@ -104,9 +107,6 @@ block(sectitems) 65 6c 65 6d diff(sect,elem) - # 66 69 6e 6c - # diff(sect,finl) - 6b 65 79 73 diff(sect,keys) @@ -125,14 +125,17 @@ block(sectitems) 6e 61 6d 65 diff(sect,name) - # 6f 72 64 72 - # diff(sect,ordr) - 73 74 72 73 diff(sect,strs) - # 74 72 61 6e - # diff(sect,tran) + 74 72 61 6e + diff(sect,tran) + + 75 73 65 74 + diff(sect,uset) + + 76 61 72 73 + diff(sect,vars) 76 6b 65 79 diff(sect,vkey) @@ -143,18 +146,24 @@ block(endsect) # bksp # ---------------------------------------------------------------------------------------------------- -# block(bksp) -# 62 6b 73 70 -# sizeof(bksp) -# 01 00 00 00 # KMX_DWORD count; // 0008 number of entries -# # }; +block(bksp) + # TODO-LDML: see 'tran' for a more programmatic way to calculate the fields + 62 6b 73 70 + sizeof(bksp) + 01 00 00 00 # KMX_DWORD groupCount + 01 00 00 00 # KMX_DWORD transformCount + 00 00 00 00 # KMX_DWORD reorderCount + + # group 0 + 00 00 00 00 # KMX_DWORD type = transform + 01 00 00 00 # KMX_DWORD count = 1 + 00 00 00 00 # KMX_DWORD index = 0 -# # struct COMP_KMXPLUS_TRAN_ENTRY { -# index(elemNull,elemBkspFrom) # KMXPLUS_ELEM from; -# 00 00 00 00 # KMXPLUS_STR to; -# 00 00 00 00 # KMXPLUS_ELEM before; -# 00 00 00 00 # KMX_DWORD flags; -# # }; + # transforms 0 + index(strNull,strElemTranFrom1b,2) # KMX_DWORD str from ^e + index(strNull,strNull,2) # KMX_DWORD str to 0 + index(strNull,strNull,2) # KMX_DWORD str mapFrom 0 + index(strNull,strNull,2) # KMX_DWORD str mapTo 0 # ---------------------------------------------------------------------------------------------------- @@ -169,7 +178,7 @@ block(disp) # struct COMP_KMXPLUS_DISP { # }; # only entry: - index(strNull,strElemTranFrom2,2) # KMX_DWORD to // 000C baseCharacter = 'a' + index(strNull,strA,2) # KMX_DWORD to // 000C baseCharacter = 'a' index(strNull,strElemTranFrom1,2) # KMX_DWORD display // 000C baseCharacter = '^' @@ -184,76 +193,52 @@ block(elem) # struct COMP_KMXPLUS_ELEM { index(elemNull,endelem) # KMX_DWORD count; // 0008 number of entries # }; +#strings + #elem #0 null element 00 00 00 00 # KMX_DWORD offset; // 0010+ offset from this blob sizeof(elemNull,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) - # diff(elem,elemFinlFrom) # KMX_DWORD offset; // 0010+ offset from this blob - # sizeof(elemFinlFrom,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + #elem #1 count=03 + #elem #2 count=03 + #elem #3 count=01 - # diff(elem,elemTranFrom) # KMX_DWORD offset; // 0010+ offset from this blob - # sizeof(elemTranFrom,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) - # diff(elem,elemBkspFrom) # KMX_DWORD offset; // 0010+ offset from this blob - # sizeof(elemBkspFrom,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + diff(elem,elemSet) # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemSet,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) - # diff(elem,elemOrdrFrom) # KMX_DWORD offset; // 0010+ offset from this blob - # sizeof(elemOrdrFrom,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) - - # diff(elem,elemOrdrBefore) # KMX_DWORD offset; // 0010+ offset from this blob - # sizeof(elemOrdrBefore,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + diff(elem,elemOrdrFrom) # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemOrdrFrom,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) + diff(elem,elemOrdrBefore) # KMX_DWORD offset; // 0010+ offset from this blob + sizeof(elemOrdrBefore,8) # KMX_DWORD length; // 0014+ str length (ELEMENT units) +# now the elements block(elemNull) -# block(elemFinlFrom) -# index(strNull,strElemTranFrom1,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset -# index(strNull,strElemTranFrom1,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset - -# block(elemTranFrom) -# index(strNull,strElemTranFrom1,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset -# index(strNull,strElemTranFrom2,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset - -# block(elemBkspFrom) -# index(strNull,strElemTranFrom1,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset -# index(strNull,strElemBkspFrom2,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset - -# block(elemOrdrFrom) -# # -# index(strNull,strElemOrdrFrom1,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 0A 00 # KMX_DWORD flags; // flag and order values - unicodeset -# index(strNull,strElemOrdrFrom2,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 37 00 # KMX_DWORD flags; // flag and order values - unicodeset -# index(strNull,strElemOrdrFrom3,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 0A 00 # KMX_DWORD flags; // flag and order values - unicodeset - -# block(elemOrdrBefore) -# index(strNull,strElemOrdrBefore,2) # KMX_DWORD element; // str: output string or UTF-32LE codepoint -# 01 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset +block(elemSet) + 61 00 00 00 # KMX_DWORD element; 'a' + 00 00 00 00 # KMX_DWORD flags; + 62 00 00 00 # KMX_DWORD element; 'b' + 00 00 00 00 # KMX_DWORD flags; + 63 00 00 00 # KMX_DWORD element; 'c' + 00 00 00 00 # KMX_DWORD flags; + +block(elemOrdrFrom) + # from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" + 60 1a 00 00 # KMX_DWORD element; '᩠' // str: output string or UTF-32LE codepoint + 00 00 0A 00 # KMX_DWORD flags; // flag and order values - cp + 00 00 00 00 # TODO-LDML: uset #0 # KMX_DWORD element; [uset] // str: output string or UTF-32LE codepoint + 02 00 37 00 # KMX_DWORD flags; // flag and order values - unicodeset + 45 1A 00 00 # KMX_DWORD element; 'ᩅ' // str: output string or UTF-32LE codepoint + 00 00 0A 00 # KMX_DWORD flags; // flag and order values - unicodeset + +block(elemOrdrBefore) + # before="\u{1A6B}" + 6b 1a 00 00 # KMX_DWORD element; 'ᩫ' // str: output string or UTF-32LE codepoint + 00 00 00 00 # KMX_DWORD flags; // flag and order values - unicodeset block(endelem) -# ---------------------------------------------------------------------------------------------------- -# finl -# ---------------------------------------------------------------------------------------------------- - -# block(finl) -# 66 69 6e 6c -# sizeof(finl) -# 01 00 00 00 # KMX_DWORD count; // 0008 number of entries -# # }; - -# # struct COMP_KMXPLUS_TRAN_ENTRY { -# index(elemNull,elemFinlFrom) # KMXPLUS_ELEM from; -# index(strNull,strElemTranFrom1,2) # KMXPLUS_STR to; -# 00 00 00 00 # KMXPLUS_ELEM before; -# 01 00 00 00 # KMX_DWORD flags; -# # }; # ---------------------------------------------------------------------------------------------------- # keys # ---------------------------------------------------------------------------------------------------- @@ -267,12 +252,12 @@ block(keys) # struct COMP_KMXPLUS_KEYS { 02 00 00 00 # KMX_DWORD kmapCount; // 0008 number of kmap # keys # (#0000) hmaqtugha - index(strNull,strKey1,2) # KMXPLUS_STR 'U+0127' - 01 00 00 00 # KMX_DWORD (flags: extend) + 27 01 00 00 # UTF-32 'U+0127' + 00 00 00 00 # KMX_DWORD (flags: none) index(strNull,strHmaqtugha,2) # KMXPLUS_STR 'hmaqtugha' 00 00 00 00 # KMXPLUS_STR switch 0A 00 00 00 # KMX_DWORD width*10 - 01 00 00 00 # TODO: index(listNull,indexAe,4) # LIST longPress 'a e' + 02 00 00 00 # TODO: index(listNull,indexAe,4) # LIST longPress 'a e' 00 00 00 00 # STR longPressDefault 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap 00 00 00 00 # flicks 0 @@ -292,7 +277,7 @@ block(keys) # struct COMP_KMXPLUS_KEYS { 00 00 00 00 # KMX_DWORD flick 00 00 00 00 # KMX_STR id # flick - # Right now there aren't any flick elements. + # Right now there aren’t any flick elements. #00 00 00 00 # LIST directions #00 00 00 01 # flags #00 00 00 00 # str: to @@ -339,23 +324,27 @@ block(layr) # struct COMP_KMXPLUS_LAYR { block(list) # struct COMP_KMXPLUS_LAYR_LIST { 6c 69 73 74 # KMX_DWORD header.ident; // 0000 Section name - list diff(list,endList) # KMX_DWORD header.size; // 0004 Section length - 02 00 00 00 # KMX_DWORD listCount (should be 2) - 02 00 00 00 # KMX_DWORD indexCount (should be 2) + 03 00 00 00 # KMX_DWORD listCount (should be 2) + 03 00 00 00 # KMX_DWORD indexCount (should be 2) # list #0 the null list block(listNull) 00 00 00 00 #index(indexNull,indexNull,2) # KMX_DWORD list index (0) 00 00 00 00 # KMX_DWORD lists[0].count - # list #1 the ae list + block(listA) + 00 00 00 00 # first index + 01 00 00 00 #count block(listAe) - 00 00 00 00 # index(indexAe,indexNull,2) # KMX_DWORD list index (also 0) + 01 00 00 00 # index(indexAe,indexNull,2) # KMX_DWORD list index (also 0) 02 00 00 00 # KMX_DWORD count block(endLists) # indices #block(indexNull) # No null index # index(strNull,strNull,2) # KMXPLUS_STR string index + block(indexA) + index(strNull,strA,2) # a block(indexAe) - index(strNull,strElemTranFrom2,2) # KMXPLUS_STR a + index(strNull,strA,2) # KMXPLUS_STR a index(strNull,strElemBkspFrom2,2) # KMXPLUS_STR e block(endIndices) block(endList) @@ -396,19 +385,6 @@ block(name) # struct COMP_KMXPLUS_META { index(strNull,strName,2) # KMXPLUS_STR name; // 000C+ name string entry = 'TestKbd' # }; -# ---------------------------------------------------------------------------------------------------- -# ordr -# ---------------------------------------------------------------------------------------------------- - -# block(ordr) -# 6f 72 64 72 # KMX_DWORD header.ident; // 0000 Section name - ordr -# sizeof(ordr) # KMX_DWORD header.size; // 0004 Section length -# 01 00 00 00 # KMX_DWORD count; // 0008 count of str entries -# # }; - -# index(elemNull,elemOrdrFrom) # KMXPLUS_ELEM elements; -# index(elemNull,elemOrdrBefore) # KMXPLUS_ELEM before; - # ---------------------------------------------------------------------------------------------------- # strs # ---------------------------------------------------------------------------------------------------- @@ -427,8 +403,15 @@ block(strs) # struct COMP_KMXPLUS_STRS { diff(strs,strVersion) sizeof(strVersion,2) diff(strs,strNorm) sizeof(strNorm,2) diff(strs,strName) sizeof(strName,2) + diff(strs,strFromSet) sizeof(strFromSet,2) + diff(strs,strUSet) sizeof(strUSet,2) + diff(strs,strAmarker) sizeof(strAmarker,2) diff(strs,strElemTranFrom1) sizeof(strElemTranFrom1,2) - diff(strs,strElemTranFrom2) sizeof(strElemTranFrom2,2) + diff(strs,strElemTranFrom1a) sizeof(strElemTranFrom1a,2) + diff(strs,strElemTranFrom1b) sizeof(strElemTranFrom1b,2) + diff(strs,strA) sizeof(strA,2) + diff(strs,strSet) sizeof(strSet,2) + diff(strs,strSet2) sizeof(strSet2,2) diff(strs,strBase) sizeof(strBase,2) diff(strs,strElemBkspFrom2) sizeof(strElemBkspFrom2,2) diff(strs,strHmaqtugha) sizeof(strHmaqtugha,2) @@ -437,44 +420,52 @@ block(strs) # struct COMP_KMXPLUS_STRS { diff(strs,strAuthor) sizeof(strAuthor,2) diff(strs,strConformsTo) sizeof(strConformsTo,2) diff(strs,strThat) sizeof(strThat,2) - # diff(strs,strTranTo) sizeof(strTranTo,2) - diff(strs,strKey1) sizeof(strKey1,2) + diff(strs,strVse) sizeof(strVse,2) + diff(strs,strVst) sizeof(strVst,2) + diff(strs,strVus) sizeof(strVus,2) + diff(strs,strTranTo) sizeof(strTranTo,2) diff(strs,strKeys) sizeof(strKeys,2) - # diff(strs,strElemOrdrFrom3) sizeof(strElemOrdrFrom3,2) - # diff(strs,strElemOrdrFrom1) sizeof(strElemOrdrFrom1,2) - # diff(strs,strElemOrdrBefore) sizeof(strElemOrdrBefore,2) - # diff(strs,strElemOrdrFrom2) sizeof(strElemOrdrFrom2,2) diff(strs,strIndicator) sizeof(strIndicator,2) + diff(strs,strSentinel0001) sizeof(strSentinel0001,2) # String table -- block(x) is used to store the null u16char at end of each string # without interfering with sizeof() calculation above +#str #00 block(strNull) block(x) 00 00 # the zero-length string block(strVersion) 31 00 2e 00 30 00 2e 00 30 00 block(x) 00 00 # '1.0.0' block(strNorm) 4e 00 46 00 43 00 block(x) 00 00 # 'NFC' block(strName) 54 00 65 00 73 00 74 00 4b 00 62 00 64 00 block(x) 00 00 # 'TestKbd' + block(strFromSet) 5B 00 5C 00 75 00 31 00 41 00 37 00 35 00 2D 00 5C 00 75 00 31 00 41 00 37 00 39 00 5D 00 block(x) 00 00 # [\u1a75-\u1a79] + block(strUSet) 5b 00 61 00 62 00 63 00 5d 00 block(x) 00 00 # '[abc]' + block(strAmarker) 5C 00 6D 00 7B 00 61 00 7D 00 block(x) 00 00 # '\m{a}' block(strElemTranFrom1) 5E 00 block(x) 00 00 # '^' - block(strElemTranFrom2) 61 00 block(x) 00 00 # 'a' + block(strElemTranFrom1a) 5E 00 61 00 block(x) 00 00 # '^a' + block(strElemTranFrom1b) 5E 00 65 00 block(x) 00 00 # '^e' + block(strA) 61 00 block(x) 00 00 # 'a' + block(strSet) 61 00 20 00 62 00 20 00 63 00 block(x) 00 00 # 'a b c' +#str #0A + block(strSet2) 61 00 62 00 63 00 block(x) 00 00 # 'abc' block(strBase) 62 00 61 00 73 00 65 00 block(x) 00 00 # 'base' block(strElemBkspFrom2) 65 00 block(x) 00 00 # 'e' block(strHmaqtugha) 68 00 6d 00 61 00 71 00 74 00 75 00 67 00 68 00 61 00 block(x) 00 00 # 'hmaqtugha' +#str #10 block(strLocale) 6d 00 74 00 block(x) 00 00 # 'mt' block(strLayout) 71 00 77 00 65 00 72 00 74 00 79 00 block(x) 00 00 # 'qwerty' block(strAuthor) 73 00 72 00 6c 00 32 00 39 00 35 00 block(x) 00 00 # 'srl295' block(strConformsTo) 74 00 65 00 63 00 68 00 70 00 72 00 65 00 76 00 69 00 65 00 77 00 block(x) 00 00 # 'techpreview' block(strThat) 74 00 68 00 61 00 74 00 block(x) 00 00 # 'that' - # block(strTranTo) E2 00 block(x) 00 00 # 'â' - block(strKey1) 27 01 block(x) 00 00 # 'ħ' + block(strVse) 76 00 73 00 65 00 block(x) 00 00 # 'vse' + block(strVst) 76 00 73 00 74 00 block(x) 00 00 # 'vst' + block(strVus) 76 00 75 00 73 00 block(x) 00 00 # 'vus' + block(strTranTo) E2 00 block(x) 00 00 # 'â' +#str #1a block(strKeys) 90 17 b6 17 block(x) 00 00 # 'ថា' # - # block(strElemOrdrFrom3) 45 1a block(x) 00 00 # 'ᩅ' - # block(strElemOrdrFrom1) 60 1a block(x) 00 00 # '᩠' - # block(strElemOrdrBefore) 6b 1a block(x) 00 00 # 'ᩫ' - # block(strElemOrdrFrom2) 75 1a block(x) 00 00 # '᩵' block(strIndicator) 3d d8 40 de block(x) 00 00 # '🙀' - + block(strSentinel0001) FF FF 01 00 block(x) 00 00 # U+FFFF U+0001 @@ -484,18 +475,103 @@ block(endstrs) # end of strs block # tran # ---------------------------------------------------------------------------------------------------- -# block(tran) # struct COMP_KMXPLUS_TRAN { -# 74 72 61 6e # KMX_DWORD header.ident; // 0000 Section name - tran -# sizeof(tran) # KMX_DWORD header.size; // 0004 Section length -# 01 00 00 00 # KMX_DWORD count; // 0008 number of entries -# # }; - -# # struct COMP_KMXPLUS_TRAN_ENTRY { -# index(elemNull,elemTranFrom) # KMXPLUS_ELEM from; -# index(strNull,strTranTo,2) # KMXPLUS_STR to; -# 00 00 00 00 # KMXPLUS_ELEM before; -# 00 00 00 00 # KMX_DWORD flags; -# # }; +block(tran) # struct COMP_KMXPLUS_TRAN { + 74 72 61 6e # KMX_DWORD header.ident; // 0000 Section name - tran + diff(tran,tranEnd) # KMX_DWORD header.size; // 0004 Section length + diff(tranGroupStart,tranTransformStart,12) # KMX_DWORD groupCount; + diff(tranTransformStart,tranReorderStart,16) # KMX_DWORD transformCount; + diff(tranReorderStart,tranEnd,8) # KMX_DWORD reorderCount; + + block(tranGroupStart) # COMP_KMXPLUS_TRAN_GROUP + # group 0 + 00 00 00 00 # KMX_DWORD type = transform + 02 00 00 00 # KMX_DWORD count + diff(tranTransformStart,tranTransform0,16) # KMX_DWORD index + + # group 1 + 00 00 00 00 # KMX_DWORD type = transform + 01 00 00 00 # KMX_DWORD count + diff(tranTransformStart,tranTransform2,16) # KMX_DWORD index + + # group 2 + 01 00 00 00 # KMX_DWORD type = reorder + 01 00 00 00 # KMX_DWORD count + diff(tranReorderStart,tranReorder0,8) # KMX_DWORD index + + # transforms + block(tranTransformStart) # COMP_KMXPLUS_TRAN_TRANSFORM + block(tranTransform0) + index(strNull,strElemTranFrom1a,2) # KMXPLUS_STR from; + index(strNull,strTranTo,2) # KMXPLUS_STR to; + index(strNull,strNull,2) # mapFrom + index(strNull,strNull,2) # mapTo + + block(tranTransform1) + index(strNull,strA,2) # KMXPLUS_STR from; 'a' + index(strNull,strSentinel0001,2) # KMXPLUS_STR to; \m{a} + index(strNull,strNull,2) # mapFrom + index(strNull,strNull,2) # mapTo + + block(tranTransform2) # Next group + index(strNull,strSentinel0001,2) # KMXPLUS_STR from; (\m{a}) + index(strNull,strNull,2) # KMXPLUS_STR to; (none) + index(strNull,strNull,2) # mapFrom + index(strNull,strNull,2) # mapTo + + # reorders + block(tranReorderStart) # COMP_KMXPLUS_TRAN_REORDER + block(tranReorder0) + index(elemNull,elemOrdrFrom) # KMXPLUS_ELEM elements; + index(elemNull,elemOrdrBefore) # KMXPLUS_ELEM before; + +block(tranEnd) + +block(uset) + 75 73 65 74 # KMX_DWORD header.ident; // 0000 Section name - uset + diff(uset,usetEnd) # KMX_DWORD header.size; // 0004 Section length + 01 00 00 00 # lists + 01 00 00 00 # elements + # lists + 00 00 00 00 # first list + 01 00 00 00 # size: 1 + index(strNull,strFromSet,2) # str + # ranges + # range @0 + 75 1A 00 00 # start + 79 1A 00 00 # end + block(usetEnd) + +block(vars) # struct COMP_KMXPLUS_VARS { + 76 61 72 73 # KMX_DWORD header.ident; // 0000 Section name - vars + diff(vars,varsEnd) # KMX_DWORD header.size; // 0004 Section length + 01 00 00 00 # KMX_DWORD markers - list 1 ['a'] + diff(varsBegin,varsEnd,16) # KMX_DWORD varCount + + block(varsBegin) + # var 0 + 00 00 00 00 # KMX_DWORD type = str + index(strNull,strA,2) # KMXPLUS_STR id 'a' + index(strNull,strAmarker,2) # KMXPLUS_STR value '\m{a}' + 00 00 00 00 # KMXPLUS_ELEM + + # var 1 + 01 00 00 00 # KMX_DWORD type = set + index(strNull,strVse,2) # KMXPLUS_STR id 'vse' + index(strNull,strSet,2) # KMXPLUS_STR value 'a b c' + 01 00 00 00 # KMXPLUS_ELEM elem 'a b c' see 'elemSet' + + # var 2 + 00 00 00 00 # KMX_DWORD type = string + index(strNull,strVst,2) # KMXPLUS_STR id 'vst' + index(strNull,strSet2,2) # KMXPLUS_STR value 'abc' + 00 00 00 00 # KMXPLUS_ELEM elem + + # var 3 + 02 00 00 00 # KMX_DWORD type = string + index(strNull,strVus,2) # KMXPLUS_STR id 'vus' + index(strNull,strUSet,2) # KMXPLUS_STR value '[abc]' + 00 00 00 00 # KMXPLUS_ELEM elem +block(varsEnd) block(vkey) # struct COMP_KMXPLUS_VKEY { 76 6b 65 79 # KMX_DWORD header.ident; // 0000 Section name - vkey diff --git a/developer/src/kmc-ldml/test/fixtures/basic.xml b/developer/src/kmc-ldml/test/fixtures/basic.xml index 4f9208cb20674f4846eb579abd4aaa526bffb20f..2d345324d9a93d07928824127279848052511380 100644 --- a/developer/src/kmc-ldml/test/fixtures/basic.xml +++ b/developer/src/kmc-ldml/test/fixtures/basic.xml @@ -37,6 +37,7 @@ + @@ -45,11 +46,16 @@ + + + + + - - + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/keys/minimal.xml b/developer/src/kmc-ldml/test/fixtures/sections/keys/minimal.xml index dc498b31f6eb489d72c9d3f55be9c832450c5f02..d8f9a64bff24c6cec1653a61dcea9a5c238f9615 100644 --- a/developer/src/kmc-ldml/test/fixtures/sections/keys/minimal.xml +++ b/developer/src/kmc-ldml/test/fixtures/sections/keys/minimal.xml @@ -8,11 +8,12 @@ + - + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/ordr/multi-escape.xml b/developer/src/kmc-ldml/test/fixtures/sections/ordr/multi-escape.xml new file mode 100644 index 0000000000000000000000000000000000000000..754d8fc0032030162e52ee5ea3202ca1a5312e7d --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/ordr/multi-escape.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/ordr/nod-Lana.xml b/developer/src/kmc-ldml/test/fixtures/sections/ordr/nod-Lana.xml new file mode 100644 index 0000000000000000000000000000000000000000..be2da524692fdb552cb6a9bb62b7e79a1dffc96e --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/ordr/nod-Lana.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-duplicate-type.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-duplicate-type.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac26d5b175938b9ab34f3654dec9e9cc8cc13df2 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-duplicate-type.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-empty.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-empty.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec295a4261ec4adab1121a70ff58c278d49fcfda --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-empty.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-invalid-duplicate-type.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-invalid-duplicate-type.xml new file mode 100644 index 0000000000000000000000000000000000000000..3cf5de198234985e42e0a2585b7f9d3c873972f2 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-invalid-duplicate-type.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-invalid-type.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-invalid-type.xml new file mode 100644 index 0000000000000000000000000000000000000000..e881965f6680f928f37196696748219a76835636 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-invalid-type.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-mixed.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-mixed.xml new file mode 100644 index 0000000000000000000000000000000000000000..56f962e52d75d0b726aa25b395108d0fa1fed0ec --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/fail-mixed.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/minimal.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/minimal.xml index 6504c6920f5b18c4c02d4c614ce66ef570b374fa..d93ef22dd5716e3ebb63f29488f793f1ea6793f0 100644 --- a/developer/src/kmc-ldml/test/fixtures/sections/tran/minimal.xml +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/minimal.xml @@ -12,5 +12,8 @@ + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/tran/tran-vars.xml b/developer/src/kmc-ldml/test/fixtures/sections/tran/tran-vars.xml new file mode 100644 index 0000000000000000000000000000000000000000..383ecd43867a68f6952f185b2b9097b1e523a484 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/tran/tran-vars.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/dup0.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/dup0.xml new file mode 100644 index 0000000000000000000000000000000000000000..6cd9fdf987b251a136799bbaf857bc1df3fb35a1 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/dup0.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/dup1.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/dup1.xml new file mode 100644 index 0000000000000000000000000000000000000000..e5d53b507b0aadeb77bd93f33d8bb19cd446b4a9 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/dup1.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-0.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-0.xml new file mode 100644 index 0000000000000000000000000000000000000000..af37a292da10bc3c0dff468c14cc1641a95feaeb --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-0.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-1.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-1.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec687e7bf08ba3e42cd1ab7b431a4b2a3d242d91 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-1.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-2.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-2.xml new file mode 100644 index 0000000000000000000000000000000000000000..dc4e143a8f9ff9bcb31b9cd79905d619cacf6dfe --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-3.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-3.xml new file mode 100644 index 0000000000000000000000000000000000000000..c0a100ceb25bb1e267c998b03d1f54e43ea311e2 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-3.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-4.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-4.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fcca4d0e0d033cf1b0efad21907921737d06d54 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-4.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-5.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-5.xml new file mode 100644 index 0000000000000000000000000000000000000000..161c69154a0322bc1e0f5ea6764a2566d7b297d2 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-5.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-6.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-6.xml new file mode 100644 index 0000000000000000000000000000000000000000..2b24b4c71e6854fb813f48a5a2b80ac4098d9c09 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-badref-6.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-markers-badref-0.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-markers-badref-0.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd63c36b0251f6f240147d467b823bc582fa9932 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-markers-badref-0.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-props1.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-props1.xml new file mode 100644 index 0000000000000000000000000000000000000000..c7930181d3b9c6b3aa5d791b9522b569cd808c77 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-props1.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-props2.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-props2.xml new file mode 100644 index 0000000000000000000000000000000000000000..761c2251c3a71fa075ca5a4f4f8fc96134887b7a --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-props2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-strings.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..a207df13f15caeb710d02573ed699211edfff0bd --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-strings.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-syntax.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-syntax.xml new file mode 100644 index 0000000000000000000000000000000000000000..0fd29f5babb20ec00fe5278d646a3f68ecc805ca --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/fail-uset-syntax.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/markers-maximal.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/markers-maximal.xml new file mode 100644 index 0000000000000000000000000000000000000000..cbe36d4db1ce7c6ca2aed6776cc055028b23e6ad --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/markers-maximal.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/maximal.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/maximal.xml new file mode 100644 index 0000000000000000000000000000000000000000..1747026a7678d9a1a08da7dabad81d0b16bddb7e --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/maximal.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/fixtures/sections/vars/minimal.xml b/developer/src/kmc-ldml/test/fixtures/sections/vars/minimal.xml new file mode 100644 index 0000000000000000000000000000000000000000..1113008dc6853cc1cb90a5c17d520b05d2ecac15 --- /dev/null +++ b/developer/src/kmc-ldml/test/fixtures/sections/vars/minimal.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-ldml/test/helpers/index.ts b/developer/src/kmc-ldml/test/helpers/index.ts index 8ba8a28aa84ded8a6549461d6ea8b657302bb431..a6e05ebf39bc32abb80e07973da6c315b61a81f3 100644 --- a/developer/src/kmc-ldml/test/helpers/index.ts +++ b/developer/src/kmc-ldml/test/helpers/index.ts @@ -5,8 +5,8 @@ import 'mocha'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { SectionCompiler } from '../../src/compiler/section-compiler.js'; -import { KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile } from '@keymanapp/common-types'; -import { LdmlKeyboardCompiler } from '../../src/compiler/compiler.js'; +import { KMXPlus, LDMLKeyboardXMLSourceFileReader, VisualKeyboard, CompilerEvent, LDMLKeyboardTestDataXMLSourceFile, compilerEventFormat, LDMLKeyboard, UnicodeSetParser, CompilerCallbacks } from '@keymanapp/common-types'; +import { LdmlKeyboardCompiler } from '../../src/main.js'; // make sure main.js compiles import { assert } from 'chai'; import { KMXPlusMetadataCompiler } from '../../src/compiler/metadata-compiler.js'; import { CompilerOptions } from '../../src/compiler/compiler-options.js'; @@ -14,11 +14,13 @@ import { LdmlKeyboardVisualKeyboardCompiler } from '../../src/compiler/visual-ke import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import KMXPlusFile = KMXPlus.KMXPlusFile; -import Elem = KMXPlus.Elem; -import GlobalSections = KMXPlus.GlobalSections; +import LDMLKeyboardXMLSourceFile = LDMLKeyboard.LDMLKeyboardXMLSourceFile; +import DependencySections = KMXPlus.DependencySections; import Section = KMXPlus.Section; -import Strs = KMXPlus.Strs; -import List = KMXPlus.List; +import { ElemCompiler, ListCompiler, StrsCompiler } from '../../src/compiler/empty-compiler.js'; +import { KmnCompiler } from '@keymanapp/kmc-kmn'; +import { VarsCompiler } from '../../src/compiler/vars.js'; +// import Vars = KMXPlus.Vars; /** * Builds a path to the fixture with the given path components. @@ -33,6 +35,12 @@ export function makePathToFixture(...components: string[]): string { export const compilerTestCallbacks = new TestCompilerCallbacks(); +export const compilerTestOptions: CompilerOptions = { + readerOptions: { + importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + } +}; + beforeEach(function() { compilerTestCallbacks.clear(); }); @@ -44,15 +52,15 @@ afterEach(function() { }); -export function loadSectionFixture(compilerClass: typeof SectionCompiler, filename: string, callbacks: TestCompilerCallbacks): Section { +export async function loadSectionFixture(compilerClass: typeof SectionCompiler, filename: string, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]): Promise
{ callbacks.messages = []; const inputFilename = makePathToFixture(filename); const data = callbacks.loadFile(inputFilename); - assert.isNotNull(data); + assert.isNotNull(data, `Failed to read file ${inputFilename}`); - const reader = new LDMLKeyboardXMLSourceFileReader(callbacks); + const reader = new LDMLKeyboardXMLSourceFileReader(compilerTestOptions.readerOptions, callbacks); const source = reader.load(data); - assert.isNotNull(source); + assert.isNotNull(source, `Failed to load XML from ${inputFilename}`); if (!reader.validate(source, callbacks.loadSchema('ldml-keyboard'))) { return null; // mimic kmc behavior - bail if validate fails @@ -64,15 +72,41 @@ export function loadSectionFixture(compilerClass: typeof SectionCompiler, filena return null; } - let globalSections: GlobalSections = { - strs: new Strs(), - elem: null, - list: null, + let sections: DependencySections = { + usetparser: await getTestUnicodeSetParser(callbacks) }; - globalSections.elem = new Elem(globalSections.strs); - globalSections.list = new List(globalSections.strs); - return compiler.compile(globalSections); + // load dependencies first + await loadDepsFor(sections, compiler, source, callbacks, dependencies); + + // make sure all dependencies are loaded + compiler.dependencies.forEach(dep => assert.ok(sections[dep], + `Required dependency '${dep}' for '${compiler.id}' was not supplied: Check the 'dependencies' argument to loadSectionFixture or testCompilationCases`)); + + return compiler.compile(sections); +} + +/** + * Recursively load dependencies. Normally they are loaded in SECTION_COMPILERS order + */ +async function loadDepsFor(sections: DependencySections, parentCompiler: SectionCompiler, source: LDMLKeyboardXMLSourceFile, callbacks: TestCompilerCallbacks, dependencies?: typeof SectionCompiler[]) { + const parentId = parentCompiler.id; + if (!dependencies) { + // default dependencies + dependencies = [ StrsCompiler, ListCompiler, ElemCompiler, VarsCompiler ]; + } + for (const dep of dependencies) { + const compiler = new dep(source, callbacks); + assert.notEqual(compiler.id, parentId, `${parentId} depends on itself`); + assert.ok(compiler.validate(), `while setting up ${parentId}: ${compiler.id} failed validate()`); + + const sect = compiler.compile(sections); + + assert.ok(sect, `while setting up ${parentId}: ${compiler.id} failed compile()`); + assert.notOk(sections[compiler.id], `while setting up ${parentId}: ${compiler.id} was already in the sections[] table, probably a bad dependency`); + + sections[compiler.id] = sect as any; + } } export function loadTestdata(inputFilename: string, options: CompilerOptions) : LDMLKeyboardTestDataXMLSourceFile { @@ -81,7 +115,7 @@ export function loadTestdata(inputFilename: string, options: CompilerOptions) : return source; } -export function compileKeyboard(inputFilename: string, options: CompilerOptions): KMXPlusFile { +export async function compileKeyboard(inputFilename: string, options: CompilerOptions): Promise { const k = new LdmlKeyboardCompiler(compilerTestCallbacks, options); const source = k.load(inputFilename); checkMessages(); @@ -91,7 +125,7 @@ export function compileKeyboard(inputFilename: string, options: CompilerOptions) checkMessages(); assert.isTrue(valid, 'k.validate should not have failed'); - const kmx = k.compile(source); + const kmx = await k.compile(source); checkMessages(); assert.isNotNull(kmx, 'k.compile should not have returned null'); @@ -123,7 +157,7 @@ export function checkMessages() { if(compilerTestCallbacks.messages.length > 0) { console.log(compilerTestCallbacks.messages); } - assert.isEmpty(compilerTestCallbacks.messages); + assert.isEmpty(compilerTestCallbacks.messages, compilerEventFormat(compilerTestCallbacks.messages)); } export interface CompilationCase { @@ -134,7 +168,7 @@ export interface CompilationCase { /** * expected error messages. If falsy, expected to succeed. All must be present to pass. */ - errors?: CompilerEvent[]; + errors?: CompilerEvent[] | boolean; /** * expected warning messages. All must be present to pass. */ @@ -147,6 +181,10 @@ export interface CompilationCase { * if present, expect compiler to throw (use .* to match all) */ throws?: RegExp; + /** + * Optional dependent sections to load. Will be strs+list+elem if falsy. + */ + dependencies?: (typeof SectionCompiler)[]; } /** @@ -155,30 +193,30 @@ export interface CompilationCase { * @param compiler argument to loadSectionFixture() * @param callbacks argument to loadSectionFixture() */ -export function testCompilationCases(compiler: typeof SectionCompiler, cases : CompilationCase[]) { +export function testCompilationCases(compiler: typeof SectionCompiler, cases : CompilationCase[], dependencies?: (typeof SectionCompiler)[]) { // we need our own callbacks rather than using the global so messages don't get mixed const callbacks = new TestCompilerCallbacks(); for (let testcase of cases) { const expectFailure = testcase.throws || !!(testcase.errors); // if true, we expect this to fail const testHeading = expectFailure ? `should fail to compile: ${testcase.subpath}`: `should compile: ${testcase.subpath}`; - it(testHeading, function () { + it(testHeading, async function () { callbacks.clear(); // special case for an expected exception if (testcase.throws) { - assert.throws(() => loadSectionFixture(compiler, testcase.subpath, callbacks), testcase.throws, 'expected exception from compilation'); + assert.throws(async () => await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies), testcase.throws, 'expected exception from compilation'); return; } - let section = loadSectionFixture(compiler, testcase.subpath, callbacks); + let section = await loadSectionFixture(compiler, testcase.subpath, callbacks, testcase.dependencies || dependencies); if (expectFailure) { - assert.isNull(section, 'expected compilation result to be null, but got something'); + assert.isNull(section, 'expected compilation result failure (null)'); } else { - assert.isNotNull(section, `expected successful compilation, but got null and ${JSON.stringify(callbacks.messages)}`); + assert.isNotNull(section, `failed with ${compilerEventFormat(callbacks.messages)}`); } // if we expected errors or warnings, show them - if (testcase.errors) { - assert.includeDeepMembers(callbacks.messages, testcase.errors, 'expected errors to be included'); + if (testcase.errors && testcase.errors !== true) { + assert.includeDeepMembers(callbacks.messages, testcase.errors, 'expected errors to be included'); } if (testcase.warnings) { assert.includeDeepMembers(callbacks.messages, testcase.warnings, 'expected warnings to be included'); @@ -194,3 +232,16 @@ export function testCompilationCases(compiler: typeof SectionCompiler, cases : C }); } } +async function getTestUnicodeSetParser(callbacks: CompilerCallbacks): Promise { + // for tests, just create a new one + // see LdmlKeyboardCompiler.getUsetParser() + const compiler = new KmnCompiler(); + const ok = await compiler.init(callbacks); + assert.ok(ok, `Could not initialize KmnCompiler (UnicodeSetParser), see callback messages`); + if (ok) { + return compiler; + } else { + return null; + } +} + diff --git a/developer/src/kmc-ldml/test/test-bksp.ts b/developer/src/kmc-ldml/test/test-bksp.ts deleted file mode 100644 index 92a55077b517140b24383409d8de43bf020fa8e5..0000000000000000000000000000000000000000 --- a/developer/src/kmc-ldml/test/test-bksp.ts +++ /dev/null @@ -1,28 +0,0 @@ -import 'mocha'; -import { assert } from 'chai'; -import { BkspCompiler } from '../src/compiler/tran.js'; -import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; -import { KMXPlus } from '@keymanapp/common-types'; - -import Bksp = KMXPlus.Bksp; -import BkspItemFlags = KMXPlus.BkspItemFlags; - -describe('bksp', function () { - this.slow(500); // 0.5 sec -- json schema validation takes a while - - it('should compile minimal bksp data', function() { - let bksp = loadSectionFixture(BkspCompiler, 'sections/bksp/minimal.xml', compilerTestCallbacks) as Bksp; - assert.lengthOf(compilerTestCallbacks.messages, 0); - - it.skip('TODO-LDML rewriting this #7377', () => { - assert.lengthOf(bksp.items, 1); - assert.lengthOf(bksp.items[0].from, 2); - assert.strictEqual(bksp.items[0].from[0].value.value, "្"); - assert.strictEqual(bksp.items[0].from[1].value.value, "ម"); - assert.strictEqual(bksp.items[0].flags, BkspItemFlags.none); - assert.isEmpty(bksp.items[0].before); - assert.strictEqual(bksp.items[0].to.value, ""); - }); - }); -}); - diff --git a/developer/src/kmc-ldml/test/test-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-compiler-e2e.ts index ec711549051a4f4a5d43911f8ae8b2924996b962..7ab6ba0e2fd44e006ea10630bc4f3405774b4df9 100644 --- a/developer/src/kmc-ldml/test/test-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-compiler-e2e.ts @@ -2,7 +2,9 @@ import 'mocha'; import {assert} from 'chai'; import x_hextobin from '@keymanapp/hextobin'; import { KMXBuilder } from '@keymanapp/common-types'; -import {checkMessages, compileKeyboard, makePathToFixture} from './helpers/index.js'; +import {checkMessages, compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js'; +import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; +import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; const hextobin = (x_hextobin as any).default; @@ -17,7 +19,7 @@ describe('compiler-tests', function() { const binaryFilename = makePathToFixture('basic.txt'); // Compile the keyboard - const kmx = compileKeyboard(inputFilename, {debug: true, addCompilerVersion: false}); + const kmx = await compileKeyboard(inputFilename, {...compilerTestOptions, debug: true, addCompilerVersion: false}); assert.isNotNull(kmx); // Use the builder to generate the binary output file @@ -28,6 +30,47 @@ describe('compiler-tests', function() { // Compare output let expected = await hextobin(binaryFilename, undefined, {silent:true}); + assert.deepEqual(code, expected); }); -}); \ No newline at end of file + + it('should handle non existent files', () => { + const filename = 'DOES_NOT_EXIST.xml'; + const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, debug: true, addCompilerVersion: false }); + const source = k.load(filename); + assert.notOk(source, `Trying to load(${filename})`); + }); + it('should handle unparseable files', () => { + const filename = makePathToFixture('basic-kvk.txt'); // not an .xml file + const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, debug: true, addCompilerVersion: false }); + const source = k.load(filename); + assert.notOk(source, `Trying to load(${filename})`); + }); + it('should handle not-valid files', () => { + const filename = makePathToFixture('test-fr.xml'); // not a keyboard .xml file + const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, debug: true, addCompilerVersion: false }); + const source = k.load(filename); + assert.notOk(source, `Trying to load(${filename})`); + }); + it('should throw on broken schema', () => { + const filename = makePathToFixture('basic.xml'); + const callbacks = new TestCompilerCallbacks(); + // simulate broken schema + callbacks.loadSchema = () => new Uint8Array(); + const k = new LdmlKeyboardCompiler(callbacks, { ...compilerTestOptions, debug: true, addCompilerVersion: false }); + const source = k.load(filename); + assert.notOk(source, `Trying to load(${filename})`); + }); + it('should handle non existent test files', () => { + const filename = 'DOES_NOT_EXIST.xml'; + const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, debug: true, addCompilerVersion: false }); + const source = k.loadTestData(filename); + assert.notOk(source, `Trying to loadTestData(${filename})`); + }); + it('should handle unparseable test files', () => { + const filename = makePathToFixture('basic-kvk.txt'); // not an .xml file + const k = new LdmlKeyboardCompiler(compilerTestCallbacks, { ...compilerTestOptions, debug: true, addCompilerVersion: false }); + const source = k.load(filename); + assert.notOk(source, `Trying to loadTestData(${filename})`); + }); +}); diff --git a/developer/src/kmc-ldml/test/test-dependencies.ts b/developer/src/kmc-ldml/test/test-dependencies.ts new file mode 100644 index 0000000000000000000000000000000000000000..e58fd36c55a37a9a2d9b06c8b0e31a3bda595d11 --- /dev/null +++ b/developer/src/kmc-ldml/test/test-dependencies.ts @@ -0,0 +1,25 @@ +import 'mocha'; +import {assert} from 'chai'; +import { SECTION_COMPILERS } from '../src/compiler/compiler.js'; +import { SectionIdent } from '@keymanapp/ldml-keyboard-constants'; +describe('test of section compiler dependencies', () => { + it('should have dependencies in the right order', () => { + const sects : Set = new Set(); + for (const sect of SECTION_COMPILERS) { + // construct the compiler + const c = new sect({ keyboard: null }, null); // For now, this is OK for the inspection + const id = c.id; + assert.ok(id); + assert.isFalse(sects.has(id), `Duplicate compiler ${id} in SECTION_COMPILERS`); + assert.ok(c.dependencies); + // make sure all deps are present + for (const dep of c.dependencies.values()) { + assert.ok(dep); + assert.isTrue(sects.has(dep), `Section ${id} requested uninitialized dependency ${dep}: check SECTION_COMPILERS order`); + } + // add this to the list + sects.add(id); + } + }); +}); + diff --git a/developer/src/kmc-ldml/test/test-disp.ts b/developer/src/kmc-ldml/test/test-disp.ts index 5f3876585943091d4bf4be3c95ba200930b3da7f..d41c1cc230fbfa0d8f4438969f7bac93e1af61fb 100644 --- a/developer/src/kmc-ldml/test/test-disp.ts +++ b/developer/src/kmc-ldml/test/test-disp.ts @@ -10,15 +10,15 @@ import Disp = KMXPlus.Disp; describe('disp', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile minimal disp', function() { - let disp = loadSectionFixture(DispCompiler, 'sections/disp/minimal.xml', compilerTestCallbacks) as Disp; + it('should compile minimal disp', async function() { + let disp = await loadSectionFixture(DispCompiler, 'sections/disp/minimal.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.ok(disp?.disps); assert.equal(disp.disps.length, 0); }); - it('should compile typical disp', function() { - let disp = loadSectionFixture(DispCompiler, 'sections/disp/typical.xml', compilerTestCallbacks) as Disp; + it('should compile typical disp', async function() { + let disp = await loadSectionFixture(DispCompiler, 'sections/disp/typical.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.ok(disp?.disps); @@ -26,8 +26,8 @@ describe('disp', function () { assert.equal(disp.disps[0].to?.value, '\u0300'); assert.equal(disp.disps[0].display?.value, '`'); }); - it('should compile maximal disp', function() { - let disp = loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; + it('should compile maximal disp', async function() { + let disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); @@ -37,8 +37,8 @@ describe('disp', function () { assert.equal(disp.disps[1].to?.value, 'f'); assert.equal(disp.disps[1].display?.value, '(f)'); }); - it('should compile escaped disp', function() { - let disp = loadSectionFixture(DispCompiler, 'sections/disp/escaped.xml', compilerTestCallbacks) as Disp; + it('should compile escaped disp', async function() { + let disp = await loadSectionFixture(DispCompiler, 'sections/disp/escaped.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); @@ -48,15 +48,15 @@ describe('disp', function () { assert.equal(disp.disps[1].to?.value, 'f'); assert.equal(disp.disps[1].display?.value, '(f)'); }); - it('should compile options-only disp', function() { - let disp = loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; + it('should compile options-only disp', async function() { + let disp = await loadSectionFixture(DispCompiler, 'sections/disp/maximal.xml', compilerTestCallbacks) as Disp; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(disp?.baseCharacter?.value, 'x'); assert.ok(disp?.disps); }); - it('should reject duplicate tos', function() { - let disp = loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp; + it('should reject duplicate tos', async function() { + let disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp; assert.isNull(disp); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_DisplayIsRepeated({to:'e'})); diff --git a/developer/src/kmc-ldml/test/test-finl.ts b/developer/src/kmc-ldml/test/test-finl.ts deleted file mode 100644 index 89189031e187c6b9bc65aa8e82255ba7407cf377..0000000000000000000000000000000000000000 --- a/developer/src/kmc-ldml/test/test-finl.ts +++ /dev/null @@ -1,28 +0,0 @@ -import 'mocha'; -import { assert } from 'chai'; -import { FinlCompiler } from '../src/compiler/tran.js'; -import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; -import { KMXPlus } from '@keymanapp/common-types'; - -import Finl = KMXPlus.Finl; -import FinlItemFlags = KMXPlus.FinlItemFlags; - -describe('finl', function () { - this.slow(500); // 0.5 sec -- json schema validation takes a while - - it('should compile minimal finl data', function() { - let finl = loadSectionFixture(FinlCompiler, 'sections/finl/minimal.xml', compilerTestCallbacks) as Finl; - assert.lengthOf(compilerTestCallbacks.messages, 0); - - it.skip('TODO-LDML rewriting this #7377', () => { - assert.lengthOf(finl.items, 1); - assert.lengthOf(finl.items[0].from, 2); - assert.strictEqual(finl.items[0].from[0].value.value, "x"); - assert.strictEqual(finl.items[0].from[1].value.value, "x"); - assert.strictEqual(finl.items[0].flags, FinlItemFlags.error); - assert.isEmpty(finl.items[0].before); - assert.strictEqual(finl.items[0].to.value, "x"); - }); - }); -}); - diff --git a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts b/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts index fb4f28daeeedf1fef9a6c191df57b55e0c5f2933..711820e5e4051a751304f1620369e7f1f131d151 100644 --- a/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts +++ b/developer/src/kmc-ldml/test/test-keymanweb-compiler.ts @@ -1,6 +1,6 @@ import 'mocha'; import { assert } from 'chai'; -import { checkMessages, compilerTestCallbacks, makePathToFixture } from './helpers/index.js'; +import { checkMessages, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js'; import { LdmlKeyboardKeymanWebCompiler } from '../src/compiler/keymanweb-compiler.js'; import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js'; import * as fs from 'fs'; @@ -16,7 +16,7 @@ describe('LdmlKeyboardKeymanWebCompiler', function() { // Load input data; we'll use the LDML keyboard compiler loader to save us // effort here - const k = new LdmlKeyboardCompiler(compilerTestCallbacks, {debug: true, addCompilerVersion: false}); + const k = new LdmlKeyboardCompiler(compilerTestCallbacks, {...compilerTestOptions, debug: true, addCompilerVersion: false}); const source = k.load(inputFilename); checkMessages(); assert.isNotNull(source, 'k.load should not have returned null'); @@ -27,7 +27,7 @@ describe('LdmlKeyboardKeymanWebCompiler', function() { assert.isTrue(valid, 'k.validate should not have failed'); // Actual test: compile to javascript - const jsCompiler = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {debug: true}); + const jsCompiler = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, debug: true}); const output = jsCompiler.compile('basic.xml', source); assert.isNotNull(output); @@ -36,7 +36,7 @@ describe('LdmlKeyboardKeymanWebCompiler', function() { assert.strictEqual(output, outputFixture); // Second test: compile to javascript without debug formatting - const jsCompilerNoDebug = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {debug: false}); + const jsCompilerNoDebug = new LdmlKeyboardKeymanWebCompiler(compilerTestCallbacks, {...compilerTestOptions, debug: false}); const outputNoDebug = jsCompilerNoDebug.compile('basic.xml', source); assert.isNotNull(outputNoDebug); diff --git a/developer/src/kmc-ldml/test/test-keys.ts b/developer/src/kmc-ldml/test/test-keys.ts index df978de773cada0195258caf91e07d804effd13b..e20094d9d46da11dd11d6b031c526bd4675ee66a 100644 --- a/developer/src/kmc-ldml/test/test-keys.ts +++ b/developer/src/kmc-ldml/test/test-keys.ts @@ -18,10 +18,16 @@ describe('keys', function () { const keys = sect; assert.ok(keys); assert.equal(compilerTestCallbacks.messages.length, 0); - assert.equal(keys.keys.length, 1); + assert.equal(keys.keys.length, 2); assert.equal(keys.flicks.length, 1); // there's always a 'null' flick - assert.equal(keys.keys[0].to.value, String.fromCodePoint(0x1FAA6)); - assert.equal(keys.keys[0].id.value, 'grave'); + assert.equal(keys.keys[0].to.value, 'oops'); + assert.isFalse(keys.keys[0].to.isOneChar); + assert.equal(keys.keys[0].flags, constants.keys_key_flags_extend); + assert.equal(keys.keys[0].id.value, 'mistake'); + assert.isTrue(keys.keys[1].to.isOneChar); + assert.equal(keys.keys[1].to.value, String.fromCodePoint(0x1FAA6)); + assert.equal(keys.keys[1].flags, 0); + assert.equal(keys.keys[1].id.value, 'grave'); }, }, { @@ -116,11 +122,11 @@ describe('keys', function () { describe('keys.kmap', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile minimal kmap data', function() { - let keys = loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Keys; + it('should compile minimal kmap data', async function() { + let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Keys; assert.isNotNull(keys); assert.equal(compilerTestCallbacks.messages.length, 0); - assert.equal(keys.kmap.length, 1); + assert.equal(keys.kmap.length, 2); }); testCompilationCases(KeysCompiler, [ @@ -217,37 +223,37 @@ describe('keys.kmap', function () { }, ]); - it('should reject layouts with too many hardware rows', function() { - let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks) as Keys; + it('should reject layouts with too many hardware rows', async function() { + let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-rows.xml', compilerTestCallbacks) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_HardwareLayerHasTooManyRows()); }); - it('should reject layouts with too many hardware keys', function() { - let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks) as Keys; + it('should reject layouts with too many hardware keys', async function() { + let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-hardware-too-many-keys.xml', compilerTestCallbacks) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({row: 1, hardware: 'us', modifier: 'none'})); }); - it('should reject layouts with undefined keys', function() { - let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks) as Keys; + it('should reject layouts with undefined keys', async function() { + let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-undefined-key.xml', compilerTestCallbacks) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_KeyNotFoundInKeyBag({col: 1, form: 'hardware', keyId: 'foo', layer: 'base', row: 1})); }); - it('should reject layouts with invalid keys', function() { - let keys = loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks) as Keys; + it('should reject layouts with invalid keys', async function() { + let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/invalid-key-missing-attrs.xml', compilerTestCallbacks) as Keys; assert.isNull(keys); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_KeyMissingToGapOrSwitch({keyId: 'Q'})); }); - it('should accept layouts with gap/switch keys', function() { - let keys = loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks) as Keys; + it('should accept layouts with gap/switch keys', async function() { + let keys = await loadSectionFixture(KeysCompiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks) as Keys; assert.isNotNull(keys); assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(keys.keys.length, 4); diff --git a/developer/src/kmc-ldml/test/test-layr.ts b/developer/src/kmc-ldml/test/test-layr.ts index e38a1b728dc273a6476116541413f63c742dc1ef..e9288826980f4b39a802055607346ec5b4dbc0d2 100644 --- a/developer/src/kmc-ldml/test/test-layr.ts +++ b/developer/src/kmc-ldml/test/test-layr.ts @@ -21,8 +21,8 @@ describe('layr', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while // reuse the keys minimal file - it('should compile minimal keys data', function () { - let layr = loadSectionFixture(LayrCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Layr; + it('should compile minimal keys data', async function () { + let layr = await loadSectionFixture(LayrCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Layr; assert.ok(layr); assert.equal(compilerTestCallbacks.messages.length, 0); @@ -36,7 +36,7 @@ describe('layr', function () { assert.equal(layer0.rows.length, 1); const row0 = layer0.rows[0]; assert.ok(row0); - assert.equal(row0.keys.length, 1); + assert.equal(row0.keys.length, 2); assert.equal(layer0.id.value, 'base'); assert.equal(layer0.mod, constants.keys_mod_none); @@ -91,6 +91,15 @@ describe('layr', function () { subpath: 'sections/layr/invalid-missing-hardware.xml', errors: [], }, + { + subpath: 'sections/keys/invalid-bad-modifier.xml', + errors: [ + CompilerMessages.Error_InvalidModifier({ + layer: 'base', + modifier: 'altR-shift' + }), + ], + }, { subpath: 'sections/layr/invalid-multi-hardware.xml', errors: [CompilerMessages.Error_ExcessHardware({ form: 'iso' })], diff --git a/developer/src/kmc-ldml/test/test-loca.ts b/developer/src/kmc-ldml/test/test-loca.ts index 317902316c140bdfd7b6970278b4c6e99cad26b6..bfc975fb1b00c8029456aaede5796fbd2a656df3 100644 --- a/developer/src/kmc-ldml/test/test-loca.ts +++ b/developer/src/kmc-ldml/test/test-loca.ts @@ -10,8 +10,8 @@ import Loca = KMXPlus.Loca; describe('loca', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile minimal loca data', function() { - let loca = loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks) as Loca; + it('should compile minimal loca data', async function() { + let loca = await loadSectionFixture(LocaCompiler, 'sections/loca/minimal.xml', compilerTestCallbacks) as Loca; assert.isObject(loca); assert.equal(compilerTestCallbacks.messages.length, 0); @@ -20,8 +20,8 @@ describe('loca', function () { assert.equal(loca.locales[0].value.toLowerCase(), 'mt'); }); - it('should compile multiple locales', function() { - let loca = loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks) as Loca; + it('should compile multiple locales', async function() { + let loca = await loadSectionFixture(LocaCompiler, 'sections/loca/multiple.xml', compilerTestCallbacks) as Loca; assert.isObject(loca); // Note: multiple.xml includes fr-FR twice, with differing case, which should be canonicalized @@ -40,8 +40,8 @@ describe('loca', function () { assert.equal(loca.locales[4].value, 'en-fonipa'); }); - it('should reject structurally invalid locales', function() { - let loca = loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks) as Loca; + it('should reject structurally invalid locales', async function() { + let loca = await loadSectionFixture(LocaCompiler, 'sections/loca/invalid-locale.xml', compilerTestCallbacks) as Loca; assert.isNull(loca); assert.equal(compilerTestCallbacks.messages.length, 1); // We'll only test one invalid BCP 47 tag to verify that we are properly calling BCP 47 validation routines. diff --git a/developer/src/kmc-ldml/test/test-messages.ts b/developer/src/kmc-ldml/test/test-messages.ts index 0dee53f12112d47b67ca19bc266630a791dd4bf9..a88da8bbbfe31a168b955b6ea43442d9c845efd6 100644 --- a/developer/src/kmc-ldml/test/test-messages.ts +++ b/developer/src/kmc-ldml/test/test-messages.ts @@ -1,9 +1,10 @@ import 'mocha'; import { CompilerMessages } from '../src/compiler/messages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { CompilerErrorNamespace } from '@keymanapp/common-types'; describe('CompilerMessages', function () { it('should have a valid CompilerMessages object', function() { - return verifyCompilerMessagesObject(CompilerMessages); + return verifyCompilerMessagesObject(CompilerMessages, CompilerErrorNamespace.LdmlKeyboardCompiler); }); }); diff --git a/developer/src/kmc-ldml/test/test-meta.ts b/developer/src/kmc-ldml/test/test-meta.ts index e507222866a6a49d9a9b81f7af2c3bf5d1d5aa0c..a5f5939a67f7fe7010d72995b0bb010a7cc0a5d2 100644 --- a/developer/src/kmc-ldml/test/test-meta.ts +++ b/developer/src/kmc-ldml/test/test-meta.ts @@ -11,8 +11,8 @@ import Meta = KMXPlus.Meta; describe('meta', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile minimal metadata', function() { - let meta = loadSectionFixture(MetaCompiler, 'sections/meta/minimal.xml', compilerTestCallbacks) as Meta; + it('should compile minimal metadata', async function() { + let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/minimal.xml', compilerTestCallbacks) as Meta; assert.equal(compilerTestCallbacks.messages.length, 0); assert.isEmpty(meta.author.value); // TODO-LDML: default author string "unknown"? @@ -23,8 +23,8 @@ describe('meta', function () { assert.equal(meta.settings, KeyboardSettings.none); }); - it('should compile maximal metadata', function() { - let meta = loadSectionFixture(MetaCompiler, 'sections/meta/maximal.xml', compilerTestCallbacks) as Meta; + it('should compile maximal metadata', async function() { + let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/maximal.xml', compilerTestCallbacks) as Meta; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(meta.author.value, 'The Keyman Team'); @@ -36,20 +36,20 @@ describe('meta', function () { assert.equal(meta.settings, KeyboardSettings.fallback); }); - it('should reject invalid normalization', function() { - let meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-normalization.xml', compilerTestCallbacks) as Meta; + it('should reject invalid normalization', async function() { + let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-normalization.xml', compilerTestCallbacks) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidNormalization({form:'NFQ'})); }); - it('should reject invalid version', function() { - let meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', compilerTestCallbacks) as Meta; + it('should reject invalid version', async function() { + let meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-1.0.xml', compilerTestCallbacks) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'1.0'})); - meta = loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', compilerTestCallbacks) as Meta; + meta = await loadSectionFixture(MetaCompiler, 'sections/meta/invalid-version-v1.0.3.xml', compilerTestCallbacks) as Meta; assert.isNull(meta); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_InvalidVersion({version:'v1.0.3'})); diff --git a/developer/src/kmc-ldml/test/test-metadata-compiler.ts b/developer/src/kmc-ldml/test/test-metadata-compiler.ts index 59500d09950c313568948fb70b8322ea0799b0d7..3bdcf8b2b756ef46469be03bcce36a881db2cd26 100644 --- a/developer/src/kmc-ldml/test/test-metadata-compiler.ts +++ b/developer/src/kmc-ldml/test/test-metadata-compiler.ts @@ -1,6 +1,6 @@ import 'mocha'; import { assert } from 'chai'; -import { checkMessages, compileKeyboard, makePathToFixture } from './helpers/index.js'; +import { checkMessages, compileKeyboard, compilerTestOptions, makePathToFixture } from './helpers/index.js'; import { KMX } from '@keymanapp/common-types'; import KEYMAN_VERSION from '@keymanapp/keyman-version'; @@ -9,11 +9,11 @@ import KMXFile = KMX.KMXFile; describe('kmx metadata compiler', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile metadata with debug and compiler version', function() { + it('should compile metadata with debug and compiler version', async function() { const inputFilename = makePathToFixture('basic.xml'); // Compile the keyboard - const kmx = compileKeyboard(inputFilename, {debug:true, addCompilerVersion:true}); + const kmx = await compileKeyboard(inputFilename, {...compilerTestOptions, debug:true, addCompilerVersion:true}); checkMessages(); assert.isNotNull(kmx); @@ -44,11 +44,11 @@ describe('kmx metadata compiler', function () { assert.equal(kmx.keyboard.stores[3].dwSystemID, KMXFile.TSS_TARGETS); }); - it('should compile metadata with no compiler version', function() { + it('should compile metadata with no compiler version', async function() { const inputFilename = makePathToFixture('basic.xml'); // Compile the keyboard - const kmx = compileKeyboard(inputFilename, {debug:true, addCompilerVersion:false}); + const kmx = await compileKeyboard(inputFilename, {...compilerTestOptions, debug:true, addCompilerVersion:false}); checkMessages(); assert.isNotNull(kmx); diff --git a/developer/src/kmc-ldml/test/test-name.ts b/developer/src/kmc-ldml/test/test-name.ts index d206c8a486846be09838cd8dcf1d99d865837a72..47bf1a2936c9d799084d43c951be523d45bd9173 100644 --- a/developer/src/kmc-ldml/test/test-name.ts +++ b/developer/src/kmc-ldml/test/test-name.ts @@ -9,16 +9,16 @@ import Name = KMXPlus.Name; describe('name', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile minimal name data', function() { - let name = loadSectionFixture(NameCompiler, 'sections/name/minimal.xml', compilerTestCallbacks) as Name; + it('should compile minimal name data', async function() { + let name = await loadSectionFixture(NameCompiler, 'sections/name/minimal.xml', compilerTestCallbacks) as Name; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(name.names.length, 1); assert.equal(name.names[0].value, 'My First Keyboard'); }); - it('should compile multiple names', function() { - let name = loadSectionFixture(NameCompiler, 'sections/name/multiple.xml', compilerTestCallbacks) as Name; + it('should compile multiple names', async function() { + let name = await loadSectionFixture(NameCompiler, 'sections/name/multiple.xml', compilerTestCallbacks) as Name; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(name.names.length, 5); diff --git a/developer/src/kmc-ldml/test/test-ordr.ts b/developer/src/kmc-ldml/test/test-ordr.ts deleted file mode 100644 index 6f764e7b633ecc7f87c84a42419900b389ea03b8..0000000000000000000000000000000000000000 --- a/developer/src/kmc-ldml/test/test-ordr.ts +++ /dev/null @@ -1,31 +0,0 @@ -import 'mocha'; -import { assert } from 'chai'; -import { OrdrCompiler } from '../src/compiler/ordr.js'; -import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; -import { KMXPlus } from '@keymanapp/common-types'; - -import Ordr = KMXPlus.Ordr; - -describe('ordr', function () { - this.slow(500); // 0.5 sec -- json schema validation takes a while - - it('should compile minimal ordr data', function() { - let ordr = loadSectionFixture(OrdrCompiler, 'sections/ordr/minimal.xml', compilerTestCallbacks) as Ordr; - assert.lengthOf(compilerTestCallbacks.messages, 0); - - it.skip('TODO-LDML rewriting this #7377', () => { - assert.lengthOf(ordr.items, 1); - assert.lengthOf(ordr.items[0].elements, 4); - assert.strictEqual(ordr.items[0].elements[0].value.value, "ខ"); - assert.strictEqual(ordr.items[0].elements[1].value.value, "ែ"); - assert.strictEqual(ordr.items[0].elements[2].value.value, "្"); - assert.strictEqual(ordr.items[0].elements[3].value.value, "ម"); - assert.strictEqual(ordr.items[0].elements[0].order, 1); - assert.strictEqual(ordr.items[0].elements[1].order, 3); - assert.strictEqual(ordr.items[0].elements[2].order, 4); - assert.strictEqual(ordr.items[0].elements[3].order, 2); - assert.isEmpty(ordr.items[0].before); - }); - }); -}); - diff --git a/developer/src/kmc-ldml/test/test-testdata-e2e.ts b/developer/src/kmc-ldml/test/test-testdata-e2e.ts index 25ac1963213a5fed475292821837b4fbb8c005fb..ece4ce85608174bac50522c48810ca3f89fac3df 100644 --- a/developer/src/kmc-ldml/test/test-testdata-e2e.ts +++ b/developer/src/kmc-ldml/test/test-testdata-e2e.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; import 'mocha'; import {assert} from 'chai'; -import {loadTestdata, makePathToFixture} from './helpers/index.js'; +import {compilerTestOptions, loadTestdata, makePathToFixture} from './helpers/index.js'; describe('testdata-tests', function() { this.slow(500); // 0.5 sec -- json schema validation takes a while @@ -14,7 +14,7 @@ describe('testdata-tests', function() { const jsonFilename = makePathToFixture('test-fr.json'); // Compile the keyboard - const testData = loadTestdata(inputFilename, {debug: true, addCompilerVersion: false}); + const testData = loadTestdata(inputFilename, {...compilerTestOptions, debug: true, addCompilerVersion: false}); assert.isNotNull(testData); const jsonData = JSON.parse(readFileSync(jsonFilename, 'utf-8')); diff --git a/developer/src/kmc-ldml/test/test-tran.ts b/developer/src/kmc-ldml/test/test-tran.ts index 0661af62a520179ad85ea61633db5c49d07f2f0c..ec77ab02f2d0cb46766c58acec6efccc5bcb6b50 100644 --- a/developer/src/kmc-ldml/test/test-tran.ts +++ b/developer/src/kmc-ldml/test/test-tran.ts @@ -1,28 +1,214 @@ import 'mocha'; import { assert } from 'chai'; -import { TranCompiler } from '../src/compiler/tran.js'; -import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; +import { TranCompiler, BkspCompiler } from '../src/compiler/tran.js'; +import { BASIC_DEPENDENCIES, UsetCompiler } from '../src/compiler/empty-compiler.js'; +import { CompilerMessages } from '../src/compiler/messages.js'; +import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js'; import { KMXPlus } from '@keymanapp/common-types'; -import Tran = KMXPlus.Tran; -import TranItemFlags = KMXPlus.TranItemFlags; +import Tran = KMXPlus.Tran;// for tests… +import Bksp = KMXPlus.Bksp;// for tests… +import { constants } from '@keymanapp/ldml-keyboard-constants'; +const tranDependencies = [ ...BASIC_DEPENDENCIES, UsetCompiler ]; +const bkspDependencies = tranDependencies; describe('tran', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while + testCompilationCases(TranCompiler, [ + { + subpath: 'sections/tran/minimal.xml', + callback(sect) { + const tran = sect; + assert.ok(tran); + assert.lengthOf(compilerTestCallbacks.messages, 0); + assert.lengthOf(tran.groups, 2); + assert.lengthOf(tran.groups[0].transforms, 1); + assert.strictEqual(tran.groups[0].transforms[0].from.value, "xx"); + assert.strictEqual(tran.groups[0].transforms[0].to.value, "x"); + assert.strictEqual(tran.groups[1].reorders[0].before.length, 0); + assert.strictEqual(tran.groups[1].reorders[0].elements.length, 4); + const [ reorder0, reorder1, reorder2, reorder3 ] = tran.groups[1].reorders[0].elements; + assert.strictEqual(reorder0.order, 1); + assert.strictEqual(reorder1.order, 3); + assert.strictEqual(reorder2.order, 4); + assert.strictEqual(reorder3.order, 2); + assert.strictEqual(reorder0.value.value, "ខ"); + assert.strictEqual(reorder1.value.value, "\u17c2"); + assert.strictEqual(reorder2.value.value, "\u17d2"); + assert.strictEqual(reorder3.value.value, "ម"); + } + }, + { + subpath: 'sections/tran/tran-vars.xml', + callback(sect) { + const tran = sect; + assert.ok(tran); + assert.lengthOf(compilerTestCallbacks.messages, 0); + // cautiously destructure + assert.lengthOf(tran.groups, 1); + const [ g0 ] = tran.groups; + assert.lengthOf(g0.transforms, 4); + const [ g0t0, g0t1, g0t2, g0t3 ] = g0.transforms; + assert.strictEqual(g0t0.from.value, "yes"); + assert.strictEqual(g0t0.to.value, "no"); - it('should compile minimal tran data', function() { - let tran = loadSectionFixture(TranCompiler, 'sections/tran/minimal.xml', compilerTestCallbacks) as Tran; - assert.lengthOf(compilerTestCallbacks.messages, 0); - - it.skip('TODO-LDML rewriting this #7377', () => { - assert.lengthOf(tran.items, 1); - assert.lengthOf(tran.items[0].from, 2); - assert.strictEqual(tran.items[0].from[0].value.value, "x"); - assert.strictEqual(tran.items[0].from[1].value.value, "x"); - assert.strictEqual(tran.items[0].flags, TranItemFlags.error); - assert.isEmpty(tran.items[0].before); - assert.strictEqual(tran.items[0].to.value, "x"); - }); - }); + assert.strictEqual(g0t1.from.value, "q(?:A|B|C|D|FF|E)x"); + const g0t1r = new RegExp(g0t1.from.value); + assert.ok(g0t1r.test('qFFx')); + assert.notOk(g0t1r.test('qZZx')); + + assert.strictEqual(g0t2.from.value, "[b-df-hj-np-tv-z]"); + const g0t2r = new RegExp(g0t2.from.value); + assert.ok(g0t2r.test('k')); + assert.notOk(g0t2r.test('e')); + + assert.strictEqual(g0t3.from.value, "((?:A|B|C|D|FF|E))"); + assert.equal(g0t3.mapFrom?.value, "upper"); + assert.equal(g0t3.mapTo?.value, "lower"); + } + }, + { + subpath: 'sections/tran/fail-invalid-type.xml', + errors: true, // XML error + }, + { + subpath: 'sections/tran/fail-duplicate-type.xml', + errors: [ + CompilerMessages.Error_DuplicateTransformsType({types: ['simple']}) + ] + }, + { + subpath: 'sections/tran/fail-invalid-duplicate-type.xml', + errors: true, // XML error + }, + { + subpath: 'sections/tran/fail-mixed.xml', + errors: [ + CompilerMessages.Error_MixedTransformGroup(), + ], + }, + { + subpath: 'sections/tran/fail-empty.xml', + errors: [ + CompilerMessages.Error_EmptyTransformGroup(), + ], + }, + // reorder test + { + subpath: 'sections/ordr/minimal.xml', + callback(sect) { + const tran = sect; + assert.equal(tran.groups?.length, 1); + assert.equal(tran.groups[0].type, constants.tran_group_type_reorder); + const { reorders } = tran.groups[0]; + assert.lengthOf(reorders, 1); + assert.lengthOf(reorders[0].elements, 4); + assert.strictEqual(reorders[0].elements[0].value.value, "ខ"); + assert.strictEqual(reorders[0].elements[1].value.value, "ែ"); + assert.strictEqual(reorders[0].elements[2].value.value, "្"); + assert.strictEqual(reorders[0].elements[3].value.value, "ម"); + assert.strictEqual(reorders[0].elements[0].order, 1); + assert.strictEqual(reorders[0].elements[1].order, 3); + assert.strictEqual(reorders[0].elements[2].order, 4); + assert.strictEqual(reorders[0].elements[3].order, 2); + assert.isEmpty(reorders[0].before); + } + }, + { + // a more complicated reorder group, from the spec + subpath: 'sections/ordr/nod-Lana.xml', + callback(sect) { + const tran = sect; + assert.equal(tran.groups?.length, 1); + assert.equal(tran.groups[0].type, constants.tran_group_type_reorder); + const { reorders } = tran.groups[0]; + assert.lengthOf(reorders, 6); + + assert.equal(reorders[0].elements[0].value.value, '\u1A60'); + assert.equal(reorders[0].elements[0].order, 127); + assert.equal(reorders[0].before.length, 0); + + assert.equal(reorders[1].elements[0].value.value, '\u1A6B'); + assert.equal(reorders[1].before.length, 0); + assert.sameDeepOrderedMembers(reorders[2].elements[0].uset.uset.ranges, + [[0x1A75, 0x1A79]]); + assert.equal(reorders[2].elements[0].order, 55); + assert.equal(reorders[2].before.length, 0); + + assert.equal(reorders[3].before.length, 1); + assert.equal(reorders[3].before[0].value.char, 0x1A6B); + assert.equal(reorders[4].before.length, 2); + assert.equal(reorders[4].before[0].value.char, 0x1A6B); + assert.sameDeepOrderedMembers(reorders[4].before[1].uset.uset.ranges, [[0x1A75, 0x1A79]]); + + assert.equal(reorders[5].before.length, 1); + assert.equal(reorders[5].before[0].value.char, 0x1A6B); + assert.equal(reorders[5].elements[0].order, 10); + assert.equal(reorders[5].elements[0].value.char, 0x1A60); + assert.equal(reorders[5].elements[1].order, 55); + assert.sameDeepOrderedMembers(reorders[5].elements[1].uset.uset.ranges, + [[0x1A75, 0x1A79]]); + assert.equal(reorders[5].elements[2].value.char, 0x1A45); + assert.equal(reorders[5].elements[2].order, 10); + + } + }, + { + // test escapes with spaces + subpath: 'sections/ordr/multi-escape.xml', + callback(sect) { + const tran = sect; + assert.equal(tran.groups?.length, 1); + assert.equal(tran.groups[0].type, constants.tran_group_type_reorder); + const { reorders } = tran.groups[0]; + assert.lengthOf(reorders, 1); + assert.lengthOf(reorders[0].elements, 2); + assert.equal(reorders[0].elements[0].value.char, 0x1A60); + assert.equal(reorders[0].elements[1].value.char, 0x1A45); + assert.equal(reorders[0].before.length, 0); + assert.equal(reorders[0].elements[0].order, 10); + assert.equal(reorders[0].elements[1].order, 10); + } + }, + // bksp non-test + { + subpath: 'sections/bksp/minimal.xml', + callback(sect) { + const bksp = sect; + assert.equal(bksp.groups?.length, 0); + } + }, + // finl + { + subpath: 'sections/finl/minimal.xml', + callback(sect) { + const tran = sect; + assert.equal(tran.groups?.length, 1); + assert.equal(tran.groups[0].type, constants.tran_group_type_transform); + const { transforms } = tran.groups[0]; + assert.lengthOf(transforms, 1); + assert.strictEqual(transforms[0].from.value, "xx"); + assert.strictEqual(transforms[0].to.value, "x"); + } + }, + ], tranDependencies); +}); + +describe('bksp', function () { + this.slow(500); + testCompilationCases(BkspCompiler, [ + { + subpath: 'sections/bksp/minimal.xml', + callback(sect) { + const bksp = sect; + assert.equal(bksp.groups?.length, 1); + assert.equal(bksp.groups[0].type, constants.tran_group_type_transform); + const { transforms } = bksp.groups[0]; + assert.lengthOf(transforms, 1); + assert.strictEqual(transforms[0].from.value, "្ម"); + assert.strictEqual(transforms[0].to.value, ""); + } + }, + ], bkspDependencies); }); diff --git a/developer/src/kmc-ldml/test/test-utils.ts b/developer/src/kmc-ldml/test/test-utils.ts index de41caa64b39d58811b95184e83f3a6632276f92..b4ef73cff6b8aef7898120b75f8113b6ef0d44d7 100644 --- a/developer/src/kmc-ldml/test/test-utils.ts +++ b/developer/src/kmc-ldml/test/test-utils.ts @@ -1,6 +1,6 @@ import 'mocha'; import {assert} from 'chai'; -import { isValidEnumValue, calculateUniqueKeys, allUsedKeyIdsInLayers, translateLayerAttrToModifier, validModifier } from '../src/util/util.js'; +import { isValidEnumValue, calculateUniqueKeys, allUsedKeyIdsInLayers, translateLayerAttrToModifier, validModifier, verifyValidAndUnique } from '../src/util/util.js'; import { constants } from "@keymanapp/ldml-keyboard-constants"; import { LDMLKeyboard } from '@keymanapp/common-types'; @@ -161,4 +161,66 @@ describe('test of util/util.ts', () => { } }); }); + describe('verifyValidAndUnique()', () => { + it('should pass a valid set', () => { + assert.ok(verifyValidAndUnique( + ['a', 'b', 'c'], + dups => assert.fail(`Dups: ${dups.join(',')}`))); + assert.ok(verifyValidAndUnique( + [], + dups => assert.fail(`Dups: ${dups.join(',')}`))); + assert.ok(verifyValidAndUnique( + ['a', 'b', 'c'], + dups => assert.fail(`Dups: ${dups.join(',')}`), + new Set(['a', 'b', 'c', 'd']), + invalids => assert.fail(`Invalid: ${invalids.join(',')}`))); + }); + describe('should fail invalid valid sets', () => { + it('should fail a dup set', () => { + let dups = null; + assert.notOk(verifyValidAndUnique( + ['a', 'b', 'c', 'b'], d => (dups = d))); + assert.deepEqual(dups, ['b']); + }); + it('should fail a highly duplicated set', () => { + let dups = null; + assert.notOk(verifyValidAndUnique( + ['a', 'b', 'c', 'b', 'c', 'c', 'b'], d => (dups = d))); + assert.deepEqual(dups, ['b', 'c']); + }); + it('should fail another dup set', () => { + let dups = null; + assert.notOk(verifyValidAndUnique( + ['a', 'b', 'c', 'b'], d => (dups = d), + new Set(['a', 'b', 'c', 'd']), + invalids => assert.fail(`Invalid: ${invalids.join(',')}`))); + assert.deepEqual(dups, ['b']); + }); + it('should fail a highly duplicated and invalid set', () => { + let dups = null; + let invalids = null; + assert.notOk(verifyValidAndUnique( + ['a', 'b', 'c', 'b', 'c', 'c', 'b', 'q'], d => (dups = d), + new Set(['a', 'b', 'c', 'd']), i => (invalids = i))); + assert.deepEqual(dups, ['b', 'c']); + assert.deepEqual(invalids, ['q']); + }); + it('should fail a highly duplicated and highly invalid set', () => { + let dups = null; + let invalids = null; + assert.notOk(verifyValidAndUnique( + ['a', 'b', 'c', 'q', 'b', 'c', 'c', 'b', 'q', 'z'], d => (dups = d), + new Set(['a', 'b', 'c', 'd']), i => (invalids = i))); + assert.deepEqual(dups, ['b', 'c', 'q']); + assert.deepEqual(invalids, ['q', 'z']); + }); + it('should fail an invalid set', () => { + let invalids = null; + assert.notOk(verifyValidAndUnique( + ['a', 'b', 'c', 'q', 'z'], dups => assert.fail(`Dups: ${dups.join(',')}`), + new Set(['a', 'b', 'c', 'd']), i => (invalids = i))); + assert.deepEqual(invalids, ['q', 'z']); + }); + }); + }); }); diff --git a/developer/src/kmc-ldml/test/test-vars.ts b/developer/src/kmc-ldml/test/test-vars.ts new file mode 100644 index 0000000000000000000000000000000000000000..b18f546f278fc3870c189ac231c10e6e2285cbc1 --- /dev/null +++ b/developer/src/kmc-ldml/test/test-vars.ts @@ -0,0 +1,216 @@ +import 'mocha'; +import { assert } from 'chai'; +import { VarsCompiler } from '../src/compiler/vars.js'; +import { CompilerMessages } from '../src/compiler/messages.js'; +import { CompilerMessages as KmnCompilerMessages } from '@keymanapp/kmc-kmn'; +import { testCompilationCases } from './helpers/index.js'; +import { KMXPlus } from '@keymanapp/common-types'; +import { BASIC_DEPENDENCIES } from '../src/compiler/empty-compiler.js'; +// now that 'everything' depends on vars, we need an explicit dependency here +const varsDependencies = BASIC_DEPENDENCIES.filter(c => c !== VarsCompiler); + +import Vars = KMXPlus.Vars; + +/** + * Shorthand for code point + * @param ch 1-character string + * @returns number for the codepoint + */ +function cp(ch : string) : number { + return ch.codePointAt(0); +} + +describe('vars', function () { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + testCompilationCases(VarsCompiler, [ + { + subpath: 'sections/vars/minimal.xml', + callback(sect) { + const vars = sect; + assert.equal(1, vars.sets?.length); + assert.equal(1, vars.strings?.length); + assert.equal(1, vars.unicodeSets?.length); + const set0 = vars.sets[0]; + assert.equal(set0.id.value, "upper"); + assert.equal(set0.value.value, "A B C D E FF"); + assert.sameDeepOrderedMembers( + Array.from(set0.items.values()).map(e => e.value.value), + ["A", "B", "C", "D", "E", "FF"]); + const string0 = vars.strings[0]; + assert.equal(string0.id.value, "y"); + assert.equal(string0.value.value, "yes"); + const unicodeSet0 = vars.unicodeSets[0]; + assert.equal(unicodeSet0.id.value, "consonants"); + assert.equal(unicodeSet0.value.value, "[कसतनमह]"); + assert.sameDeepOrderedMembers(unicodeSet0.unicodeSet.ranges, [ + [cp('क'), cp('क')], // range of 1 + [cp('त'), cp('त')], // range of 1 + [cp('न'), cp('न')], // range of 1 + [cp('म'), cp('म')], // range of 1 + [cp('स'), cp('ह')], // range of 2 + ]); + }, + }, + { + subpath: 'sections/vars/maximal.xml', + callback(sect) { + // strings + const vars = sect; + assert.equal(2, vars.strings?.length); + const string1 = vars.strings[1]; + assert.equal(string1.id.value, "yes"); + assert.equal(string1.value.value, "Yes!"); + + // sets + assert.equal(3, vars.sets?.length); + assert.sameDeepOrderedMembers( + Array.from(vars.sets[0].items.values()).map(e => e.value.value), + [ + "A", "B", "C", "D", "E", "FF", "Yes" + ]); + + assert.sameDeepOrderedMembers( + Array.from(vars.sets[1].items.values()).map(e => e.value.value), + [ + "a", "b", "c" + ]); + + assert.sameDeepOrderedMembers( + Array.from(vars.sets[2].items.values()).map(e => e.value.value), + [ + "A", "B", "C", "D", "E", "FF", "Yes", "a", "b", "c", "Z", + ]); + + assert.equal(2, vars.unicodeSets?.length); + const unicodeSet0 = vars.unicodeSets[0]; + assert.equal(unicodeSet0.id.value, "consonants"); + assert.equal(unicodeSet0.value.value, "[कसतनमह]"); + assert.sameDeepOrderedMembers(unicodeSet0.unicodeSet.ranges, [ + [cp('क'), cp('क')], // range of 1 + [cp('त'), cp('त')], // range of 1 + [cp('न'), cp('न')], // range of 1 + [cp('म'), cp('म')], // range of 1 + [cp('स'), cp('ह')], // range of 2 + ]); + + const unicodeSet1 = vars.unicodeSets[1]; + assert.equal(unicodeSet1.id.value, "mixture"); + assert.equal(unicodeSet1.value.value, "[[abc][कसतनमह]]"); // not canonicalized, just the raw expansion + assert.sameDeepOrderedMembers(unicodeSet1.unicodeSet.ranges, [ + [cp('a'), cp('c')], // range of 3 + [cp('क'), cp('क')], // range of 1 + [cp('त'), cp('त')], // range of 1 + [cp('न'), cp('न')], // range of 1 + [cp('म'), cp('म')], // range of 1 + [cp('स'), cp('ह')], // range of 2 + ]); + }, + }, + { + subpath: 'sections/vars/dup0.xml', + errors: [ + CompilerMessages.Error_DuplicateVariable({ids: 'y'}) + ], + }, + { + subpath: 'sections/vars/dup1.xml', + errors: [ + CompilerMessages.Error_DuplicateVariable({ids: 'upper, y'}) + ], + }, + { + subpath: 'sections/vars/fail-uset-props1.xml', + errors: [ + KmnCompilerMessages.Error_UnicodeSetHasProperties() + ], + }, + { + subpath: 'sections/vars/fail-uset-props2.xml', + errors: [ + KmnCompilerMessages.Error_UnicodeSetHasProperties() + ], + }, + { + subpath: 'sections/vars/fail-uset-strings.xml', + errors: [ + KmnCompilerMessages.Error_UnicodeSetHasStrings() + ], + }, + { + subpath: 'sections/vars/fail-uset-syntax.xml', + errors: [ + KmnCompilerMessages.Error_UnicodeSetSyntaxError() + ], + }, + { + subpath: 'sections/vars/fail-badref-0.xml', + errors: [ + CompilerMessages.Error_MissingStringVariable({id: "yes"}), + ], + }, + { + subpath: 'sections/vars/fail-badref-1.xml', + errors: [ + CompilerMessages.Error_MissingSetVariable({id: "lower"}), + ], + }, + { + subpath: 'sections/vars/fail-badref-2.xml', + errors: [ + CompilerMessages.Error_MissingStringVariable({ id: 'doesnotexist' }) + ], + }, + { + subpath: 'sections/vars/fail-badref-3.xml', + errors: [ + CompilerMessages.Error_MissingUnicodeSetVariable({id: 'doesnotexist'}) + ], + }, + { + subpath: 'sections/vars/fail-badref-4.xml', + errors: [ + CompilerMessages.Error_NeedSpacesBetweenSetVariables({item: '$[vowels]$[consonants]'}) + ], + }, + { + subpath: 'sections/vars/fail-badref-5.xml', + errors: [ + CompilerMessages.Error_CantReferenceSetFromUnicodeSet({id: 'nonUnicodeSet'}) + ], + }, + { + subpath: 'sections/vars/fail-badref-6.xml', + errors: [ + CompilerMessages.Error_MissingStringVariable({id: 'missingStringInSet'}) + ], + }, + ], varsDependencies); + describe('markers', function () { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + testCompilationCases(VarsCompiler, [ + { + subpath: 'sections/vars/markers-maximal.xml', + callback(sect) { + const vars = sect; + assert.ok(vars.markers); + assert.sameDeepOrderedMembers(vars.markers.toStringArray(), + ['m','x']); + }, + }, + { + subpath: 'sections/vars/fail-markers-badref-0.xml', + errors: [ + CompilerMessages.Error_MissingMarkers({ + ids: [ + 'doesnt_exist_1', + 'doesnt_exist_2', + 'doesnt_exist_3', + ] + }), + ], + }, + ], varsDependencies); + }); +}); diff --git a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts index 1b1e34069e5ac2b13982f5ee18654b80d1bacb6f..4af68278387c8dd89283108fcbe6d031ee8b51a8 100644 --- a/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts +++ b/developer/src/kmc-ldml/test/test-visual-keyboard-compiler-e2e.ts @@ -2,7 +2,7 @@ import 'mocha'; import {assert} from 'chai'; import x_hextobin from '@keymanapp/hextobin'; import { KvkFileWriter } from '@keymanapp/common-types'; -import {checkMessages, compileVisualKeyboard, makePathToFixture} from './helpers/index.js'; +import {checkMessages, compilerTestOptions, compileVisualKeyboard, makePathToFixture} from './helpers/index.js'; const hextobin = (x_hextobin as any).default; @@ -17,7 +17,7 @@ describe('visual-keyboard-compiler', function() { const binaryFilename = makePathToFixture('basic-kvk.txt'); // Compile the visual keyboard - const vk = compileVisualKeyboard(inputFilename, {debug: true, addCompilerVersion: false}); + const vk = compileVisualKeyboard(inputFilename, {...compilerTestOptions, debug: true, addCompilerVersion: false}); assert.isNotNull(vk); // Use the builder to generate the binary output file diff --git a/developer/src/kmc-ldml/test/test-vkey.ts b/developer/src/kmc-ldml/test/test-vkey.ts index 185754c976ff9c0c4310006e69670b3ef9b1bf61..dec24125e2a29e649caf2ac5e3541b6e392731bc 100644 --- a/developer/src/kmc-ldml/test/test-vkey.ts +++ b/developer/src/kmc-ldml/test/test-vkey.ts @@ -11,8 +11,8 @@ import USVirtualKeyCodes = Constants.USVirtualKeyCodes; describe('vkey compiler', function () { this.slow(500); // 0.5 sec -- json schema validation takes a while - it('should compile minimal vkey data', function() { - let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/minimal.xml', compilerTestCallbacks) as Vkey; + it('should compile minimal vkey data', async function() { + let vkey = await loadSectionFixture(VkeyCompiler, 'sections/vkey/minimal.xml', compilerTestCallbacks) as Vkey; assert.equal(compilerTestCallbacks.messages.length, 0); assert.equal(vkey.vkeys.length, 4); @@ -23,37 +23,37 @@ describe('vkey compiler', function () { assert.deepEqual(vkey.vkeys[3], {vkey: USVirtualKeyCodes.K_Z, target: USVirtualKeyCodes.K_W}); }); - it('should hint on redundant data', function() { - let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/redundant.xml', compilerTestCallbacks) as Vkey; + it('should hint on redundant data', async function() { + let vkey = await loadSectionFixture(VkeyCompiler, 'sections/vkey/redundant.xml', compilerTestCallbacks) as Vkey; assert.isNotNull(vkey); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_VkeyIsRedundant({vkey: 'A'})); }); - it('should report an info message if same target found', function() { - let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/same-target.xml', compilerTestCallbacks) as Vkey; + it('should report an info message if same target found', async function() { + let vkey = await loadSectionFixture(VkeyCompiler, 'sections/vkey/same-target.xml', compilerTestCallbacks) as Vkey; assert.isNotNull(vkey); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Info_MultipleVkeysHaveSameTarget({vkey: 'Q'})); }); - it('should error on invalid "from" vkey', function() { - let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-from-vkey.xml', compilerTestCallbacks) as Vkey; - assert.isNull(vkey); + it('should hint on invalid "from" vkey', async function() { + let vkey = await loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-from-vkey.xml', compilerTestCallbacks) as Vkey; + assert.isNotNull(vkey); assert.equal(compilerTestCallbacks.messages.length, 2); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid({vkey: 'q'})); - assert.deepEqual(compilerTestCallbacks.messages[1], CompilerMessages.Error_VkeyIsNotValid({vkey: 'HYFEN'})); + assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_VkeyIsNotValid({vkey: 'q'})); + assert.deepEqual(compilerTestCallbacks.messages[1], CompilerMessages.Hint_VkeyIsNotValid({vkey: 'HYFEN'})); }); - it('should error on invalid "to" vkey', function() { - let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-to-vkey.xml', compilerTestCallbacks) as Vkey; - assert.isNull(vkey); + it('should hint on invalid "to" vkey', async function() { + let vkey = await loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-to-vkey.xml', compilerTestCallbacks) as Vkey; + assert.isNotNull(vkey); assert.equal(compilerTestCallbacks.messages.length, 1); - assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid({vkey: 'A-ACUTE'})); + assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Hint_VkeyIsNotValid({vkey: 'A-ACUTE'})); }); - it('should error on repeated vkeys', function() { - let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-repeated-vkey.xml', compilerTestCallbacks) as Vkey; + it('should error on repeated vkeys', async function() { + let vkey = await loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-repeated-vkey.xml', compilerTestCallbacks) as Vkey; assert.isNull(vkey); assert.equal(compilerTestCallbacks.messages.length, 1); assert.deepEqual(compilerTestCallbacks.messages[0], CompilerMessages.Error_VkeyIsRepeated({vkey: 'A'})); diff --git a/developer/src/kmc-ldml/test/tsconfig.json b/developer/src/kmc-ldml/test/tsconfig.json index cfc7583fa48620c06c92c20039f6b11dfa724f4d..f47c5c5f71f31821ad1ee65b7cb9a29929404617 100644 --- a/developer/src/kmc-ldml/test/tsconfig.json +++ b/developer/src/kmc-ldml/test/tsconfig.json @@ -19,7 +19,7 @@ "./helpers/index.ts" ], "references": [ - { "path": "../../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../../common/web/keyman-version" }, { "path": "../../../../common/web/types/" }, { "path": "../../../../common/tools/hextobin/" }, { "path": "../../common/web/test-helpers/" }, diff --git a/developer/src/kmc-ldml/tsconfig.json b/developer/src/kmc-ldml/tsconfig.json index a9dcdbbb2a8e8039b5bd7a448ab80c93434df207..c9ec649b40fd46a0310220ff162b435ae1a0bafa 100644 --- a/developer/src/kmc-ldml/tsconfig.json +++ b/developer/src/kmc-ldml/tsconfig.json @@ -10,6 +10,7 @@ "paths": { // "@keymanapp/keyman-version": ["../../../common/web/keyman-version/keyman-version.mts"], "@keymanapp/common-types": ["../../../common/web/types/src/main"], + "@keymanapp/kmc-kmn": ["../kmc-kmn/src/main"], // "@keymanapp/": ["core/include/ldml/ldml-keyboard-constants"], }, @@ -18,8 +19,9 @@ "src/**/*.ts" ], "references": [ - { "path": "../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../common/web/keyman-version" }, { "path": "../../../common/web/types/" }, + { "path": "../kmc-kmn/" }, { "path": "../../../core/include/ldml/"}, ] } diff --git a/developer/src/kmc-model-info/build.sh b/developer/src/kmc-model-info/build.sh index 7c2119826f5c3dba16e250d92b387e60bb6cfbc1..641ad569dcf36ae73dab92676ecd05300ed74dfc 100755 --- a/developer/src/kmc-model-info/build.sh +++ b/developer/src/kmc-model-info/build.sh @@ -50,7 +50,8 @@ fi #------------------------------------------------------------------------------------------------------------------- if builder_start_action test; then - #npm test - no tests as yet + npm test + # TODO: no unit tests yet, later add: `&& cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha` builder_finish_action success test fi diff --git a/developer/src/kmc-model-info/package.json b/developer/src/kmc-model-info/package.json index 709f9d2d6fc0b29a0b732361a6463e6b487e42ad..eee18422925005f546d53e1b222eb4fbd69e509d 100644 --- a/developer/src/kmc-model-info/package.json +++ b/developer/src/kmc-model-info/package.json @@ -18,7 +18,7 @@ "scripts": { "build": "tsc -b", "lint": "eslint .", - "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha", + "test": "npm run lint", "prepublishOnly": "npm run build" }, "author": "Marc Durdin (https://github.com/mcdurdin)", diff --git a/developer/src/kmc-model/package.json b/developer/src/kmc-model/package.json index bbeefca05ed595d2787340a1942ab281b3970764..4a54942879314688518ab5e858e0b68b446c5161 100644 --- a/developer/src/kmc-model/package.json +++ b/developer/src/kmc-model/package.json @@ -16,8 +16,7 @@ "/build/src/" ], "scripts": { - "build": "tsc -b && npm run build-cjs", - "build-cjs": "esbuild build/src/lexical-model-compiler.js --bundle --platform=node --external:../../node_modules/* > build/cjs-src/lexical-model-compiler.cjs", + "build": "tsc -b", "lint": "eslint .", "test": "npm run lint && cd test && tsc -b && cd .. && c8 --reporter=lcov --reporter=text mocha", "prepublishOnly": "npm run build" diff --git a/developer/src/kmc-model/src/main.ts b/developer/src/kmc-model/src/main.ts index 3d808057d5981cef795bd59545735b1ef6cc1e31..32e2b03afd317532e68c22c63f205f3073743987 100644 --- a/developer/src/kmc-model/src/main.ts +++ b/developer/src/kmc-model/src/main.ts @@ -8,6 +8,8 @@ import LexicalModelCompiler from './lexical-model-compiler.js'; import { LexicalModelSource } from './lexical-model.js'; import { ModelCompilerError, ModelCompilerMessageContext, ModelCompilerMessages } from './model-compiler-errors.js'; +export { default as LexicalModelCompiler } from './lexical-model-compiler.js'; + /** * Compiles a model.ts file, using paths relative to its location. * diff --git a/developer/src/kmc-model/test/test-messages.ts b/developer/src/kmc-model/test/test-messages.ts index 51394313040cd07c0bfff2777d5ee8a9671ba5f6..bd0f2e38f1b66f1140d392e7b2c3c187d91546dd 100644 --- a/developer/src/kmc-model/test/test-messages.ts +++ b/developer/src/kmc-model/test/test-messages.ts @@ -1,9 +1,10 @@ import 'mocha'; import { ModelCompilerMessages } from '../src/model-compiler-errors.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { CompilerErrorNamespace } from '@keymanapp/common-types'; describe('ModelCompilerMessages', function () { it('should have a valid ModelCompilerMessages object', function() { - return verifyCompilerMessagesObject(ModelCompilerMessages); + return verifyCompilerMessagesObject(ModelCompilerMessages, CompilerErrorNamespace.ModelCompiler); }); }); diff --git a/developer/src/kmc-model/tsconfig.json b/developer/src/kmc-model/tsconfig.json index 8ad34413fa273854677e75ad7d41264a6434e234..724ebd74d0b6dba26ff2942965ebae101f06062c 100644 --- a/developer/src/kmc-model/tsconfig.json +++ b/developer/src/kmc-model/tsconfig.json @@ -11,7 +11,7 @@ "src/**/*.ts" ], "references": [ - { "path": "../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../common/web/keyman-version" }, { "path": "../../../common/models/types" }, { "path": "../../../common/web/types" }, ] diff --git a/developer/src/kmc-package/src/compiler/keyman-targets.ts b/developer/src/kmc-package/src/compiler/keyman-targets.ts new file mode 100644 index 0000000000000000000000000000000000000000..14fc6572c4931446c8753bbbe981d04db31ca10e --- /dev/null +++ b/developer/src/kmc-package/src/compiler/keyman-targets.ts @@ -0,0 +1,53 @@ + +// +// This mirrors UKeymanTargets.pas +// + +export enum + KeymanTarget { + any = 'any', + windows = 'windows', macosx = 'macosx', linux = 'linux', + web = 'web', iphone = 'iphone', ipad = 'ipad', androidphone = 'androidphone', androidtablet = 'androidtablet', + mobile = 'mobile', desktop = 'desktop', tablet = 'tablet' + }; + +export const + // Note: if these change, update the copied values in KeyboardParser.pas accordingly + AllKeymanTargets: KeymanTarget[] = [ + KeymanTarget.any, + KeymanTarget.windows, KeymanTarget.macosx, KeymanTarget.linux, + KeymanTarget.web, KeymanTarget.iphone, KeymanTarget.ipad, KeymanTarget.androidphone, KeymanTarget.androidtablet, + KeymanTarget.mobile, KeymanTarget.desktop, KeymanTarget.tablet + ], + + TouchKeymanTargets: KeymanTarget[] = [ + KeymanTarget.iphone, KeymanTarget.ipad, KeymanTarget.androidphone, KeymanTarget.androidtablet, + KeymanTarget.mobile, KeymanTarget.tablet + ], + + // Compile to .kmx + KMXKeymanTargets: KeymanTarget[] = [ + KeymanTarget.windows, KeymanTarget.macosx, KeymanTarget.linux, KeymanTarget.desktop + ], + + // Compile to .js + KMWKeymanTargets: KeymanTarget[] = [ + KeymanTarget.web, KeymanTarget.iphone, KeymanTarget.ipad, KeymanTarget.androidphone, KeymanTarget.androidtablet, + KeymanTarget.mobile, KeymanTarget.tablet + ], + + // Supports .kvks + KeymanTargetsUsingKVK: KeymanTarget[] = [ + KeymanTarget.windows, KeymanTarget.macosx, KeymanTarget.linux, KeymanTarget.desktop, KeymanTarget.web + ], + + // Friendly names for targets, e.g. for documentation + + SKeymanTargetNames: { [index: string]: string } = { + [KeymanTarget.any]: 'All', + [KeymanTarget.windows]: 'Windows', [KeymanTarget.macosx]: 'macOS', [KeymanTarget.linux]: 'Linux', + [KeymanTarget.web]: 'Web', [KeymanTarget.iphone]: 'iPhone', [KeymanTarget.ipad]: 'iPad', + [KeymanTarget.androidphone]: 'Android phone', [KeymanTarget.androidtablet]: 'Android tablet', + [KeymanTarget.mobile]: 'Mobile devices', [KeymanTarget.desktop]: 'Desktop devices', + [KeymanTarget.tablet]: 'Tablet devices' + }; diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index b660641df9c564972064f4ed644433dd793c3f65..198a1ef2de2823b43902867c517aa9b85aa66b91 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -16,7 +16,12 @@ export class KmpCompiler { public transformKpsToKmpObject(kpsFilename: string): KmpJsonFile.KmpJsonFile { // Load the KPS data from XML as JS structured data. - const data = this.callbacks.fs.readFileSync(kpsFilename, 'utf-8'); + const buffer = this.callbacks.loadFile(kpsFilename); + if(!buffer) { + this.callbacks.reportMessage(CompilerMessages.Error_FileDoesNotExist({filename: kpsFilename})); + return null; + } + const data = new TextDecoder().decode(buffer); const kpsPackage = (() => { let a: KpsFile.KpsPackage; @@ -24,7 +29,8 @@ export class KmpCompiler { tagNameProcessors: [xml2js.processors.firstCharLowerCase], explicitArray: false }); - parser.parseString(data, (e: unknown, r: unknown) => { a = r as KpsFile.KpsPackage }); + // TODO: add unit test for xml errors parsing .kps file + parser.parseString(data, (e: unknown, r: unknown) => { if(e) throw e; a = r as KpsFile.KpsPackage }); return a; })(); @@ -284,9 +290,20 @@ export class KmpCompiler { * we want that to remain the responsibility of the keyboard compiler, so we'll warn the * few users who are still doing this */ - private warnIfKvkFileIsNotBinary(filename: string, data: Buffer) { - // TODO: Buffer is not available on web - if(filename.match(/\.kvk$/) && data.compare(Buffer.from(KvkFile.KVK_HEADER_IDENTIFIER_BYTES), 0, 3, 0, 3) != 0) { + private warnIfKvkFileIsNotBinary(filename: string, data: Uint8Array) { + if(!filename.match(/\.kvk$/)) { + return; + } + + if(data.byteLength < 4) { + // TODO: Not a valid .kvk file; should we be reporting this? + return; + } + + if(data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] || + data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] || + data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] || + data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) { this.callbacks.reportMessage(CompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename})); } } diff --git a/developer/src/kmc-package/src/compiler/messages.ts b/developer/src/kmc-package/src/compiler/messages.ts index 35eb5a851258d4066cc438b645a6613927e0bc12..56727fea9bf61927ca7b2007df1a62103164c25b 100644 --- a/developer/src/kmc-package/src/compiler/messages.ts +++ b/developer/src/kmc-package/src/compiler/messages.ts @@ -110,5 +110,9 @@ export class CompilerMessages { static Error_PackageMustContainAModelOrAKeyboard = () => m(this.ERROR_PackageMustContainAModelOrAKeyboard, `Package must contain a lexical model or a keyboard.`); static ERROR_PackageMustContainAModelOrAKeyboard = SevError | 0x0019; + + static Warn_JsKeyboardFileIsMissing = (o:{id: string}) => m(this.WARN_JsKeyboardFileIsMissing, + `Keyboard ${o.id} targets touch devices but corresponding ${o.id}.js file is not in the package.`); + static WARN_JsKeyboardFileIsMissing = SevWarn | 0x001A; } diff --git a/developer/src/kmc-package/src/compiler/package-validation.ts b/developer/src/kmc-package/src/compiler/package-validation.ts index 0fd139bb47c27218791052fc08c07e270a93dfec..d4317a3da012fdbf8838149daf785022552d6e35 100644 --- a/developer/src/kmc-package/src/compiler/package-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-validation.ts @@ -130,6 +130,8 @@ export class PackageValidation { if(!this.checkForDuplicatedOrNonMinimalLanguages('keyboard', keyboard.id, keyboard.languages)) { return false; } + // Note: package-version-validation verifies that there is a corresponding + // content file for each keyboard } return true; diff --git a/developer/src/kmc-package/src/compiler/package-version-validation.ts b/developer/src/kmc-package/src/compiler/package-version-validation.ts index ffd7565912dfbfedadb75758c040633e733bd1f5..4f8ad4a41c1d3b232ffe3ee244f76f7b347ec96e 100644 --- a/developer/src/kmc-package/src/compiler/package-version-validation.ts +++ b/developer/src/kmc-package/src/compiler/package-version-validation.ts @@ -1,6 +1,13 @@ import { KmpJsonFile, CompilerCallbacks, KpsFile, KmxFileReader, KMX } from '@keymanapp/common-types'; +import { KeymanTarget, TouchKeymanTargets } from './keyman-targets.js'; import { CompilerMessages } from './messages.js'; +// The DEFAULT_VERSION used to be '1.0', but we now use '0.0' to allow +// pre-release 0.x keyboards to be considered later than a keyboard without +// any version metadata at all. +// TODO: move to a better location +const DEFAULT_VERSION = '0.0'; + export class PackageVersionValidation { constructor(private callbacks: CompilerCallbacks) {} @@ -33,14 +40,32 @@ export class PackageVersionValidation { // We now know we have at least one keyboard in the package for(let keyboard of kmp.keyboards) { - result = this.updateKeyboardVersionFromKmx(kpsFilename, kmp, keyboard) && result; + const kmx = this.getKmxData(kpsFilename, kmp, keyboard); + if(!kmx) { + // Warnings or errors will have been raised by getKmxData + result = false; + continue; + } + + // get the targets from the .kmx + this.verifyTargets(keyboard, kmx, kmp); + + // Note: there is often version metadata in the .kps element, but + // we don't read from the metadata because we want to ensure we have the + // most up-to-date keyboard version data here, from the compiled keyboard. + let keyboardVersion = this.getKeyboardVersionFromKmx(kmx); + if(followKeyboardVersion && keyboardVersion === null) { + this.callbacks.reportMessage(CompilerMessages.Info_KeyboardFileHasNoKeyboardVersion({filename: keyboard.id})); + } + keyboard.version = keyboardVersion ?? DEFAULT_VERSION; + if(result) { if(kmp.keyboards[0].version !== keyboard.version) { this.callbacks.reportMessage(CompilerMessages.Warn_KeyboardVersionsDoNotMatch({ - keyboard:keyboard.id, - version:keyboard.version, - firstKeyboard:kmp.keyboards[0].id, - firstVersion:kmp.keyboards[0].version + keyboard: keyboard.id, + version: keyboard.version, + firstKeyboard: kmp.keyboards[0].id, + firstVersion: kmp.keyboards[0].version })); } } @@ -78,34 +103,22 @@ export class PackageVersionValidation { return true; } - private updateKeyboardVersionFromKmx( + private getKmxData( kpsFilename: string, kmp: KmpJsonFile.KmpJsonFile, keyboard: KmpJsonFile.KmpJsonFileKeyboard - ): boolean { - // The DEFAULT_VERSION used to be '1.0', but we now use '0.0' to allow - // pre-release 0.x keyboards to be considered later than a keyboard without - // any version metadata at all. - const DEFAULT_VERSION = '0.0'; - - // Note: there is often version metadata in the .kps element, but - // we don't read from the metadata because we want to ensure we have the - // most up-to-date keyboard version data here, from the compiled keyboard. - - // Reset the keyboard version to the default in the kmp.json metadata, for - // warning/failure code paths in this file - keyboard.version = DEFAULT_VERSION; + ): KMX.KEYBOARD { const file = kmp.files.find(file => this.callbacks.path.basename(file.name, '.kmx') == keyboard.id); if(!file) { this.callbacks.reportMessage(CompilerMessages.Error_KeyboardContentFileNotFound({id:keyboard.id})); - return false; + return null; } const filename = this.callbacks.resolveFilename(kpsFilename, file.name); if(!this.callbacks.fs.existsSync(filename)) { this.callbacks.reportMessage(CompilerMessages.Error_KeyboardFileNotFound({filename})); - return false; + return null; } // @@ -116,24 +129,57 @@ export class PackageVersionValidation { kmxFileData = this.callbacks.loadFile(filename); } catch(e) { this.callbacks.reportMessage(CompilerMessages.Error_FileCouldNotBeRead({filename, e})); - return false; + return null; } const kmxReader: KmxFileReader = new KmxFileReader(); const kmx: KMX.KEYBOARD = kmxReader.read(kmxFileData); if(!kmx) { // The file couldn't be read, it might not be a .kmx file this.callbacks.reportMessage(CompilerMessages.Error_KeyboardFileNotValid({filename})); - return false; + return null; } + return kmx; + } + + private getKeyboardVersionFromKmx( + kmx: KMX.KEYBOARD + ): string { const store = kmx.stores.find(store => store.dwSystemID == KMX.KMXFile.TSS_KEYBOARDVERSION); + return store ? store.dpString : null; + } + + /** + * Verify that the package contains a .js if the keyboard targets touch + * devices + */ + private verifyTargets( + keyboard: KmpJsonFile.KmpJsonFileKeyboard, + kmx: KMX.KEYBOARD, + kmp: KmpJsonFile.KmpJsonFile + ): void { + // TODO: this function doesn't really belong in version validation, but we + // don't want to read .kmx twice so we'll leave it here for now; a future + // refactor could move .kmx loading into another class + + const store = kmx.stores.find(store => store.dwSystemID == KMX.KMXFile.TSS_TARGETS); if(!store) { - // We have no version number store, so use default version - this.callbacks.reportMessage(CompilerMessages.Info_KeyboardFileHasNoKeyboardVersion({filename})); - return true; + // If there is no &TARGETS store, then there is nothing to check, + // because default is windows-only + return; } - keyboard.version = store.dpString; - return true; + // Note, if we have gotten this far, we've already located and loaded a + // .kmx, so no need to verify that the package includes a .kmx + + // If at least one target is a touch target, we need to check that the + // package also includes the .js + const targets = store.dpString.split(' '); + if(targets.some(target => ['any', ...TouchKeymanTargets].includes(target))) { + if(!kmp.files.find(file => this.callbacks.path.basename(file.name, '.js') == keyboard.id)) { + // .js version of the keyboard is not found, warn + this.callbacks.reportMessage(CompilerMessages.Warn_JsKeyboardFileIsMissing({id: keyboard.id})); + } + } } } diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps index a95f5c6fe169f68d7e0c86e786ceaa1fee3abdca..de17212a8451980f4c24b2e8cd46c723eef86677 100644 --- a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps +++ b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps @@ -17,7 +17,7 @@ Binary KVK File - 1.3 + 1.0 @@ -27,17 +27,17 @@ .kvk - ../../invalid/khmer_angkor.kmx - Keyboard Khmer Angkor + ../../invalid/basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Khmer diff --git a/developer/src/kmc-package/test/fixtures/invalid/WARN_PackageNameDoesNotFollowKeyboardConventions.kps b/developer/src/kmc-package/test/fixtures/invalid/WARN_PackageNameDoesNotFollowKeyboardConventions.kps index 867129bac3676d0825d64d1eab50b2423d645727..70f58d779b399f349a59a1d32d6a5e1c00502c2c 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/WARN_PackageNameDoesNotFollowKeyboardConventions.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/WARN_PackageNameDoesNotFollowKeyboardConventions.kps @@ -9,21 +9,21 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/basic.kmn b/developer/src/kmc-package/test/fixtures/invalid/basic.kmn new file mode 100644 index 0000000000000000000000000000000000000000..48705f6b9939ae0229a0892fe352502d1a31ffa0 --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/basic.kmn @@ -0,0 +1,6 @@ +store(&name) 'basic desktop-only kmx' +store(&keyboardversion) '1.0' + +begin unicode > use(main) + +group(main) using keys \ No newline at end of file diff --git a/developer/src/kmc-package/test/fixtures/invalid/basic.kmx b/developer/src/kmc-package/test/fixtures/invalid/basic.kmx new file mode 100644 index 0000000000000000000000000000000000000000..bd6b832cab772d03a45460e6c982d79772a9ab6e Binary files /dev/null and b/developer/src/kmc-package/test/fixtures/invalid/basic.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_language_tag_is_not_valid.kps b/developer/src/kmc-package/test/fixtures/invalid/error_language_tag_is_not_valid.kps index 3c47540d2b1d6742c75af554ea56caa42e812c7a..06d95266512afa80722fc23b1506d5350cf559de 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/error_language_tag_is_not_valid.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/error_language_tag_is_not_valid.kps @@ -8,21 +8,21 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 English (Australian script) as spoken in Latin diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_cannot_contain_both_models_and_keyboards.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_cannot_contain_both_models_and_keyboards.kps index 5ee547dcf1ead34086979570c7f772c7e2a23ad5..6483fe1c10c2ae5964920eb0d992446e5b3f667f 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/error_package_cannot_contain_both_models_and_keyboards.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_cannot_contain_both_models_and_keyboards.kps @@ -8,13 +8,13 @@ Khmer Angkor © 2015-2022 SIL International Makara Sok - 1.3 + 1.0 https://keyman.com/keyboards/khmer_angkor - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx @@ -38,9 +38,9 @@ - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank.kps index 870d8ae55b81b3c97c1ea08a71244d16bb354004..5b7d6d3d46a07fe429c515b5431b3a2977ff49e8 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank.kps @@ -9,21 +9,21 @@ © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank_2.kps b/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank_2.kps index 01eba9981d2b0d5d04db00c85e84fbdff97a627b..f9beefebb986641a4b95ba5a7916c68be15053df 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank_2.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/error_package_name_cannot_be_blank_2.kps @@ -8,21 +8,21 @@ © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/my special keyboard.kmx b/developer/src/kmc-package/test/fixtures/invalid/my special keyboard.kmx index c3d0289f4863c7c4a998c348eb1d425caadca79d..bd6b832cab772d03a45460e6c982d79772a9ab6e 100644 Binary files a/developer/src/kmc-package/test/fixtures/invalid/my special keyboard.kmx and b/developer/src/kmc-package/test/fixtures/invalid/my special keyboard.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/invalid/my_special-keyboard.kmx b/developer/src/kmc-package/test/fixtures/invalid/my_special-keyboard.kmx index c3d0289f4863c7c4a998c348eb1d425caadca79d..bd6b832cab772d03a45460e6c982d79772a9ab6e 100644 Binary files a/developer/src/kmc-package/test/fixtures/invalid/my_special-keyboard.kmx and b/developer/src/kmc-package/test/fixtures/invalid/my_special-keyboard.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_doc_file_dangerous.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_doc_file_dangerous.kps index 045e14b740c0611550baacf8746279aee6be84ea..54fc8df0ebe9e61d6ac41e5113087e6bb5314d1a 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_doc_file_dangerous.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_doc_file_dangerous.kps @@ -8,12 +8,12 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx @@ -27,9 +27,9 @@ - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Khmer diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions.kps index 6523df4813ef75f66770085fdc79a5923cfe0af2..9a8c6d588677f2399ef9baf8272bc463f731f55a 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions.kps @@ -8,7 +8,7 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 @@ -23,7 +23,7 @@ Khmer Angkor my special keyboard - 1.3 + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions_2.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions_2.kps index d29a01c580139f3582c285d05b5e24e1f8ff4e4c..426592c2715940c8ba1ad34a1e0ae713b8b7c4e6 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions_2.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_file_in_package_does_not_follow_filename_conventions_2.kps @@ -8,7 +8,7 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 @@ -20,7 +20,7 @@ - khmer_angkor.kmx + basic.kmx Keyboard my special keyboard 0 .kmx @@ -29,7 +29,7 @@ Khmer Angkor - khmer_angkor + basic 1.3 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kmn b/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kmn new file mode 100644 index 0000000000000000000000000000000000000000..fe4f8457c498dd295aaa0de432e826b19d2daff5 --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kmn @@ -0,0 +1,8 @@ +store(&NAME) "warn_js_keyboard_file_is_missing" +store(&VERSION) "10.0" +store(&keyboardversion) "1.0" +store(&targets) "any" + +begin unicode > use(main) + +group(main) using keys \ No newline at end of file diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kmx b/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kmx new file mode 100644 index 0000000000000000000000000000000000000000..ca697e872c2dadb3afd3a9f659bb38539d19607d Binary files /dev/null and b/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kmx differ diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kps new file mode 100644 index 0000000000000000000000000000000000000000..82227ffea62b9323dc80aadcec469e0255a446db --- /dev/null +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_js_keyboard_file_is_missing.kps @@ -0,0 +1,40 @@ + + + + 15.0.266.0 + 7.0 + + + + + + + + + + Khmer Angkor + © 2015-2022 SIL International + Makara Sok + + https://keyman.com/keyboards/khmer_angkor + + + + warn_js_keyboard_file_is_missing.kmx + Keyboard Khmer Angkor + 0 + .kmx + + + + + Khmer Angkor + warn_js_keyboard_file_is_missing + 1.0 + + Central Khmer (Khmer, Cambodia) + + + + + diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_keyboard_versions_do_not_match.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_keyboard_versions_do_not_match.kps index 2eb09374c6ee134c8cf6980932857e25eb70d8dd..d6a8aaf20182cc63481a3e41700dd0e3b6538445 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_keyboard_versions_do_not_match.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_keyboard_versions_do_not_match.kps @@ -8,12 +8,12 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx @@ -26,9 +26,9 @@ - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Khmer diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_language_tag_is_not_minimal.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_language_tag_is_not_minimal.kps index 8e4440e3da3e290bfa927a489679b1d0ce9f5733..cc757d6431cc46887d755d7ad2cf84a8cc14089a 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_language_tag_is_not_minimal.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_language_tag_is_not_minimal.kps @@ -8,21 +8,21 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_package_should_not_repeat_languages.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_package_should_not_repeat_languages.kps index a1c6cffe9f2461aa002fc35de8b17c1cf81e15b7..ef8fcfb89df78010987478c92fc0d73c82b5f749 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_package_should_not_repeat_languages.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_package_should_not_repeat_languages.kps @@ -8,21 +8,21 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Central Khmer (Khmer, Cambodia) diff --git a/developer/src/kmc-package/test/fixtures/invalid/warn_redist_file_should_not_be_in_package.kps b/developer/src/kmc-package/test/fixtures/invalid/warn_redist_file_should_not_be_in_package.kps index 1b5d1e246dc3b5af9210e95f8da5de1373f7a609..10d6212d67701e2dd340510b806e337d69056a57 100644 --- a/developer/src/kmc-package/test/fixtures/invalid/warn_redist_file_should_not_be_in_package.kps +++ b/developer/src/kmc-package/test/fixtures/invalid/warn_redist_file_should_not_be_in_package.kps @@ -8,12 +8,12 @@ SENĆOŦEN (Saanich Dialect) Lexical Model © 2019 National Research Council Canada Eddie Antonio Santos - 1.3 + 1.0 - khmer_angkor.kmx - Keyboard Khmer Angkor + basic.kmx + Keyboard Basic 0 .kmx @@ -27,9 +27,9 @@ - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Khmer diff --git a/developer/src/kmc-package/test/fixtures/xml_kvk_file/source/xml_kvk_file.kps b/developer/src/kmc-package/test/fixtures/xml_kvk_file/source/xml_kvk_file.kps index 9378181ad1b28cbf9e9b362b57ff17d0271fd0af..816a66d633883252ca3f51daee0bf4ac5e50f70f 100644 --- a/developer/src/kmc-package/test/fixtures/xml_kvk_file/source/xml_kvk_file.kps +++ b/developer/src/kmc-package/test/fixtures/xml_kvk_file/source/xml_kvk_file.kps @@ -17,7 +17,7 @@ XML KVK File - 1.3 + 1.0 @@ -27,17 +27,17 @@ .kvk - ../../invalid/khmer_angkor.kmx - Keyboard Khmer Angkor + ../../invalid/basic.kmx + Keyboard Basic 0 .kmx - Khmer Angkor - khmer_angkor - 1.3 + Basic + basic + 1.0 Khmer diff --git a/developer/src/kmc-package/test/test-messages.ts b/developer/src/kmc-package/test/test-messages.ts index 48976fc6adbb8db7133ba399e03a6a6424505393..1c8cce5198fe9fdb0ce6c06226c52a33238a9e79 100644 --- a/developer/src/kmc-package/test/test-messages.ts +++ b/developer/src/kmc-package/test/test-messages.ts @@ -1,9 +1,201 @@ import 'mocha'; -import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { assert } from 'chai'; +import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { CompilerMessages } from '../src/compiler/messages.js'; +import { makePathToFixture } from './helpers/index.js'; +import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; +import { PackageValidation } from '../src/compiler/package-validation.js'; +import { CompilerErrorNamespace } from '@keymanapp/common-types'; + +const debug = false; +const callbacks = new TestCompilerCallbacks(); describe('CompilerMessages', function () { it('should have a valid CompilerMessages object', function() { - return verifyCompilerMessagesObject(CompilerMessages); + return verifyCompilerMessagesObject(CompilerMessages, CompilerErrorNamespace.PackageCompiler); + }); + + + // + // Message tests + // + + function testForMessage(context: Mocha.Context, fixture: string[], messageId?: number) { + context.timeout(10000); + + callbacks.clear(); + + const kpsPath = makePathToFixture(...fixture); + const kmpCompiler = new KmpCompiler(callbacks); + + let kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath); + if(kmpJson && callbacks.messages.length == 0) { + const validator = new PackageValidation(callbacks); + validator.validate(kpsPath, kmpJson); // we'll ignore return value and rely on the messages + } + + if(kmpJson && callbacks.messages.length == 0) { + // We'll try building the package if we have not yet received any messages + kmpCompiler.buildKmpFile(kpsPath, kmpJson) + } + + if(debug) callbacks.printMessages(); + + if(messageId) { + assert.lengthOf(callbacks.messages, 1); + assert.isTrue(callbacks.hasMessage(messageId)); + } else { + assert.lengthOf(callbacks.messages, 0); + } + } + + // WARN_FileIsNotABinaryKvkFile + + it('should generate WARN_FileIsNotABinaryKvkFile if a non-binary kvk file is included', async function() { + testForMessage(this, ['xml_kvk_file', 'source', 'xml_kvk_file.kps'], CompilerMessages.WARN_FileIsNotABinaryKvkFile); + }); + + it('should not warn if a binary kvk file is included', async function() { + testForMessage(this, ['binary_kvk_file', 'source', 'binary_kvk_file.kps']); + }); + + // ERROR_FollowKeyboardVersionNotAllowedForModelPackages + + it('should generate ERROR_FollowKeyboardVersionNotAllowedForModelPackages if is set for model packages', async function() { + testForMessage(this, ['invalid', 'followkeyboardversion.qaa.sencoten.model.kps'], CompilerMessages.ERROR_FollowKeyboardVersionNotAllowedForModelPackages); + }); + + // ERROR_FollowKeyboardVersionButNoKeyboards + + it('should generate ERROR_FollowKeyboardVersionButNoKeyboards if is set for a package with no keyboards', async function() { + testForMessage(this, ['invalid', 'followkeyboardversion.empty.kps'], CompilerMessages.ERROR_FollowKeyboardVersionButNoKeyboards); + }); + + // ERROR_KeyboardContentFileNotFound + + it('should generate ERROR_KeyboardContentFileNotFound if a is listed in a package but not found in ', async function() { + testForMessage(this, ['invalid', 'keyboardcontentfilenotfound.kps'], CompilerMessages.ERROR_KeyboardContentFileNotFound); + }); + + // ERROR_KeyboardFileNotValid + + it('should generate ERROR_KeyboardFileNotValid if a .kmx is not valid in ', async function() { + testForMessage(this, ['invalid', 'keyboardfilenotvalid.kps'], CompilerMessages.ERROR_KeyboardFileNotValid); + }); + + // INFO_KeyboardFileHasNoKeyboardVersion + + it('should generate INFO_KeyboardFileHasNoKeyboardVersion if is set but keyboard has no version', async function() { + testForMessage(this, ['invalid', 'nokeyboardversion.kps'], CompilerMessages.INFO_KeyboardFileHasNoKeyboardVersion); + }); + + // ERROR_PackageCannotContainBothModelsAndKeyboards + + it('should generate ERROR_PackageCannotContainBothModelsAndKeyboards if package has both keyboards and models', async function() { + testForMessage(this, ['invalid', 'error_package_cannot_contain_both_models_and_keyboards.kps'], CompilerMessages.ERROR_PackageCannotContainBothModelsAndKeyboards); + }); + + // WARN_PackageShouldNotRepeatLanguages (models) + + it('should generate WARN_PackageShouldNotRepeatLanguages if model has same language repeated', async function() { + testForMessage(this, ['invalid', 'keyman.en.warn_package_should_not_repeat_languages.model.kps'], CompilerMessages.WARN_PackageShouldNotRepeatLanguages); + }); + + // WARN_PackageShouldNotRepeatLanguages (keyboards) + + it('should generate WARN_PackageShouldNotRepeatLanguages if keyboard has same language repeated', async function() { + testForMessage(this, ['invalid', 'warn_package_should_not_repeat_languages.kps'], CompilerMessages.WARN_PackageShouldNotRepeatLanguages); + }); + + // WARN_PackageNameDoesNotFollowLexicalModelConventions + + it('should generate WARN_PackageNameDoesNotFollowLexicalModelConventions if filename has wrong conventions', async function() { + testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowLexicalModelConventions.kps'], CompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions); + }); + + // WARN_PackageNameDoesNotFollowKeyboardConventions + + it('should generate WARN_PackageNameDoesNotFollowKeyboardConventions if filename has wrong conventions', async function() { + testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowKeyboardConventions.kps'], CompilerMessages.WARN_PackageNameDoesNotFollowKeyboardConventions); }); + + // WARN_FileInPackageDoesNotFollowFilenameConventions + + it('should generate WARN_FileInPackageDoesNotFollowFilenameConventions if content filename has wrong conventions', async function() { + testForMessage(this, ['invalid', 'warn_file_in_package_does_not_follow_filename_conventions.kps'], CompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions); + testForMessage(this, ['invalid', 'warn_file_in_package_does_not_follow_filename_conventions_2.kps'], CompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions); + }); + + // ERROR_PackageNameCannotBeBlank + + it('should generate ERROR_PackageNameCannotBeBlank if package info has empty name', async function() { + testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank.kps'], CompilerMessages.ERROR_PackageNameCannotBeBlank); // blank field + testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank_2.kps'], CompilerMessages.ERROR_PackageNameCannotBeBlank); // missing field + }); + + // ERROR_KeyboardFileNotFound + + it('should generate ERROR_KeyboardFileNotFound if a is listed in a package but not found in ', async function() { + testForMessage(this, ['invalid', 'keyboardfilenotfound.kps'], CompilerMessages.ERROR_KeyboardFileNotFound); + }); + + // WARN_KeyboardVersionsDoNotMatch + + it('should generate WARN_KeyboardVersionsDoNotMatch if two have different versions', async function() { + testForMessage(this, ['invalid', 'warn_keyboard_versions_do_not_match.kps'], CompilerMessages.WARN_KeyboardVersionsDoNotMatch); + }); + + // WARN_KeyboardVersionsDoNotMatchPackageVersion + + it('should generate WARN_KeyboardVersionsDoNotMatchPackageVersion if version does not match package version', async function() { + testForMessage(this, ['invalid', 'warn_keyboard_versions_do_not_match_package_version.kps'], CompilerMessages.WARN_KeyboardVersionsDoNotMatchPackageVersion); + }); + + // ERROR_LanguageTagIsNotValid + + it('should generate ERROR_LanguageTagIsNotValid if keyboard has an invalid language tag', async function() { + testForMessage(this, ['invalid', 'error_language_tag_is_not_valid.kps'], CompilerMessages.ERROR_LanguageTagIsNotValid); + }); + + // WARN_LanguageTagIsNotMinimal + + it('should generate WARN_LanguageTagIsNotMinimal if keyboard has a non-minimal language tag', async function() { + testForMessage(this, ['invalid', 'warn_language_tag_is_not_minimal.kps'], CompilerMessages.WARN_LanguageTagIsNotMinimal); + }); + + // ERROR_MustHaveAtLeastOneLanguage + + it('should generate ERROR_MustHaveAtLeastOneLanguage if model or keyboard has zero language tags', async function() { + testForMessage(this, ['invalid', 'keyman.en.error_must_have_at_least_one_language.model.kps'], + CompilerMessages.ERROR_MustHaveAtLeastOneLanguage); + }); + + // WARN_RedistFileShouldNotBeInPackage + + it('should generate WARN_RedistFileShouldNotBeInPackage if package contains a redist file', async function() { + testForMessage(this, ['invalid', 'warn_redist_file_should_not_be_in_package.kps'], + CompilerMessages.WARN_RedistFileShouldNotBeInPackage); + }); + + // WARN_DocFileDangerous + + it('should generate WARN_DocFileDangerous if package contains a .doc file', async function() { + testForMessage(this, ['invalid', 'warn_doc_file_dangerous.kps'], + CompilerMessages.WARN_DocFileDangerous); + }); + + // ERROR_PackageMustContainAPackageOrAKeyboard + + it('should generate ERROR_PackageMustContainAModelOrAKeyboard if package contains a .doc file', async function() { + testForMessage(this, ['invalid', 'error_package_must_contain_a_model_or_a_keyboard.kps'], + CompilerMessages.ERROR_PackageMustContainAModelOrAKeyboard); + }); + + // WARN_JsKeyboardFileIsMissing + + it('should generate WARN_JsKeyboardFileIsMissing if package is missing corresponding .js for a touch .kmx', async function() { + testForMessage(this, ['invalid', 'warn_js_keyboard_file_is_missing.kps'], + CompilerMessages.WARN_JsKeyboardFileIsMissing); + }); + }); diff --git a/developer/src/kmc-package/test/test-package-compiler.ts b/developer/src/kmc-package/test/test-package-compiler.ts index 731925a25e64ee24c3b6cf3882ac26684ea9be45..59d2acb55a32cd477c1f7f3f88794cd97dbafdfe 100644 --- a/developer/src/kmc-package/test/test-package-compiler.ts +++ b/developer/src/kmc-package/test/test-package-compiler.ts @@ -10,10 +10,9 @@ import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers'; import { makePathToFixture } from './helpers/index.js'; import { KmpCompiler } from '../src/compiler/kmp-compiler.js'; -import { PackageValidation } from '../src/compiler/package-validation.js'; import { CompilerMessages } from '../src/compiler/messages.js'; -const debug = true; +const debug = false; describe('KmpCompiler', function () { const MODELS : string[] = [ @@ -149,179 +148,4 @@ describe('KmpCompiler', function () { assert.deepEqual(callbacks.messages[0].code, CompilerMessages.WARN_AbsolutePath); assert.deepEqual(callbacks.messages[1].code, CompilerMessages.ERROR_FileDoesNotExist); }); - - // - // Message tests - // - - function testForMessage(context: Mocha.Context, fixture: string[], messageId?: number) { - context.timeout(10000); - - callbacks.clear(); - - const kpsPath = makePathToFixture(...fixture); - const kmpCompiler = new KmpCompiler(callbacks); - - let kmpJson = kmpCompiler.transformKpsToKmpObject(kpsPath); - if(kmpJson && callbacks.messages.length == 0) { - const validator = new PackageValidation(callbacks); - validator.validate(kpsPath, kmpJson); // we'll ignore return value and rely on the messages - } - - if(kmpJson && callbacks.messages.length == 0) { - // We'll try building the package if we have not yet received any messages - kmpCompiler.buildKmpFile(kpsPath, kmpJson) - } - - if(debug) callbacks.printMessages(); - - if(messageId) { - assert.lengthOf(callbacks.messages, 1); - assert.isTrue(callbacks.hasMessage(messageId)); - } else { - assert.lengthOf(callbacks.messages, 0); - } - } - - // WARN_FileIsNotABinaryKvkFile - - it('should generate WARN_FileIsNotABinaryKvkFile if a non-binary kvk file is included', async function() { - testForMessage(this, ['xml_kvk_file', 'source', 'xml_kvk_file.kps'], CompilerMessages.WARN_FileIsNotABinaryKvkFile); - }); - - it('should not warn if a binary kvk file is included', async function() { - testForMessage(this, ['binary_kvk_file', 'source', 'binary_kvk_file.kps']); - }); - - // ERROR_FollowKeyboardVersionNotAllowedForModelPackages - - it('should generate ERROR_FollowKeyboardVersionNotAllowedForModelPackages if is set for model packages', async function() { - testForMessage(this, ['invalid', 'followkeyboardversion.qaa.sencoten.model.kps'], CompilerMessages.ERROR_FollowKeyboardVersionNotAllowedForModelPackages); - }); - - // ERROR_FollowKeyboardVersionButNoKeyboards - - it('should generate ERROR_FollowKeyboardVersionButNoKeyboards if is set for a package with no keyboards', async function() { - testForMessage(this, ['invalid', 'followkeyboardversion.empty.kps'], CompilerMessages.ERROR_FollowKeyboardVersionButNoKeyboards); - }); - - // ERROR_KeyboardContentFileNotFound - - it('should generate ERROR_KeyboardContentFileNotFound if a is listed in a package but not found in ', async function() { - testForMessage(this, ['invalid', 'keyboardcontentfilenotfound.kps'], CompilerMessages.ERROR_KeyboardContentFileNotFound); - }); - - // ERROR_KeyboardFileNotValid - - it('should generate ERROR_KeyboardFileNotValid if a .kmx is not valid in ', async function() { - testForMessage(this, ['invalid', 'keyboardfilenotvalid.kps'], CompilerMessages.ERROR_KeyboardFileNotValid); - }); - - // INFO_KeyboardFileHasNoKeyboardVersion - - it('should generate INFO_KeyboardFileHasNoKeyboardVersion if is set but keyboard has no version', async function() { - testForMessage(this, ['invalid', 'nokeyboardversion.kps'], CompilerMessages.INFO_KeyboardFileHasNoKeyboardVersion); - }); - - // ERROR_PackageCannotContainBothModelsAndKeyboards - - it('should generate ERROR_PackageCannotContainBothModelsAndKeyboards if package has both keyboards and models', async function() { - testForMessage(this, ['invalid', 'error_package_cannot_contain_both_models_and_keyboards.kps'], CompilerMessages.ERROR_PackageCannotContainBothModelsAndKeyboards); - }); - - // WARN_PackageShouldNotRepeatLanguages (models) - - it('should generate WARN_PackageShouldNotRepeatLanguages if model has same language repeated', async function() { - testForMessage(this, ['invalid', 'keyman.en.warn_package_should_not_repeat_languages.model.kps'], CompilerMessages.WARN_PackageShouldNotRepeatLanguages); - }); - - // WARN_PackageShouldNotRepeatLanguages (keyboards) - - it('should generate WARN_PackageShouldNotRepeatLanguages if keyboard has same language repeated', async function() { - testForMessage(this, ['invalid', 'warn_package_should_not_repeat_languages.kps'], CompilerMessages.WARN_PackageShouldNotRepeatLanguages); - }); - - // WARN_PackageNameDoesNotFollowLexicalModelConventions - - it('should generate WARN_PackageNameDoesNotFollowLexicalModelConventions if filename has wrong conventions', async function() { - testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowLexicalModelConventions.kps'], CompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions); - }); - - // WARN_PackageNameDoesNotFollowKeyboardConventions - - it('should generate WARN_PackageNameDoesNotFollowKeyboardConventions if filename has wrong conventions', async function() { - testForMessage(this, ['invalid', 'WARN_PackageNameDoesNotFollowKeyboardConventions.kps'], CompilerMessages.WARN_PackageNameDoesNotFollowKeyboardConventions); - }); - - // WARN_FileInPackageDoesNotFollowFilenameConventions - - it('should generate WARN_FileInPackageDoesNotFollowFilenameConventions if content filename has wrong conventions', async function() { - testForMessage(this, ['invalid', 'warn_file_in_package_does_not_follow_filename_conventions.kps'], CompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions); - testForMessage(this, ['invalid', 'warn_file_in_package_does_not_follow_filename_conventions_2.kps'], CompilerMessages.WARN_FileInPackageDoesNotFollowFilenameConventions); - }); - - // ERROR_PackageNameCannotBeBlank - - it('should generate ERROR_PackageNameCannotBeBlank if package info has empty name', async function() { - testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank.kps'], CompilerMessages.ERROR_PackageNameCannotBeBlank); // blank field - testForMessage(this, ['invalid', 'error_package_name_cannot_be_blank_2.kps'], CompilerMessages.ERROR_PackageNameCannotBeBlank); // missing field - }); - - // ERROR_KeyboardFileNotFound - - it('should generate ERROR_KeyboardFileNotFound if a is listed in a package but not found in ', async function() { - testForMessage(this, ['invalid', 'keyboardfilenotfound.kps'], CompilerMessages.ERROR_KeyboardFileNotFound); - }); - - // WARN_KeyboardVersionsDoNotMatch - - it('should generate WARN_KeyboardVersionsDoNotMatch if two have different versions', async function() { - testForMessage(this, ['invalid', 'warn_keyboard_versions_do_not_match.kps'], CompilerMessages.WARN_KeyboardVersionsDoNotMatch); - }); - - // WARN_KeyboardVersionsDoNotMatchPackageVersion - - it('should generate WARN_KeyboardVersionsDoNotMatchPackageVersion if version does not match package version', async function() { - testForMessage(this, ['invalid', 'warn_keyboard_versions_do_not_match_package_version.kps'], CompilerMessages.WARN_KeyboardVersionsDoNotMatchPackageVersion); - }); - - // ERROR_LanguageTagIsNotValid - - it('should generate ERROR_LanguageTagIsNotValid if keyboard has an invalid language tag', async function() { - testForMessage(this, ['invalid', 'error_language_tag_is_not_valid.kps'], CompilerMessages.ERROR_LanguageTagIsNotValid); - }); - - // WARN_LanguageTagIsNotMinimal - - it('should generate WARN_LanguageTagIsNotMinimal if keyboard has a non-minimal language tag', async function() { - testForMessage(this, ['invalid', 'warn_language_tag_is_not_minimal.kps'], CompilerMessages.WARN_LanguageTagIsNotMinimal); - }); - - // ERROR_MustHaveAtLeastOneLanguage - - it('should generate ERROR_MustHaveAtLeastOneLanguage if model or keyboard has zero language tags', async function() { - testForMessage(this, ['invalid', 'keyman.en.error_must_have_at_least_one_language.model.kps'], - CompilerMessages.ERROR_MustHaveAtLeastOneLanguage); - }); - - // WARN_RedistFileShouldNotBeInPackage - - it('should generate WARN_RedistFileShouldNotBeInPackage if package contains a redist file', async function() { - testForMessage(this, ['invalid', 'warn_redist_file_should_not_be_in_package.kps'], - CompilerMessages.WARN_RedistFileShouldNotBeInPackage); - }); - - // WARN_DocFileDangerous - - it('should generate WARN_DocFileDangerous if package contains a .doc file', async function() { - testForMessage(this, ['invalid', 'warn_doc_file_dangerous.kps'], - CompilerMessages.WARN_DocFileDangerous); - }); - - // ERROR_PackageMustContainAPackageOrAKeyboard - - it('should generate ERROR_PackageMustContainAModelOrAKeyboard if package contains a .doc file', async function() { - testForMessage(this, ['invalid', 'error_package_must_contain_a_model_or_a_keyboard.kps'], - CompilerMessages.ERROR_PackageMustContainAModelOrAKeyboard); - }); }); diff --git a/developer/src/kmc-package/test/tsconfig.json b/developer/src/kmc-package/test/tsconfig.json index 9700969d6220af9e2837776691cc084cda01cf31..05d10505504675c2732a7c608f32b600cb80f884 100644 --- a/developer/src/kmc-package/test/tsconfig.json +++ b/developer/src/kmc-package/test/tsconfig.json @@ -18,7 +18,7 @@ "references": [ { "path": "../" }, { "path": "../../../../common/web/types" }, - { "path": "../../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../../common/web/keyman-version" }, { "path": "../../common/web/test-helpers/" }, ] } \ No newline at end of file diff --git a/developer/src/kmc-package/tsconfig.json b/developer/src/kmc-package/tsconfig.json index 8913057047eadc303181a6e070dbb16889d459ed..0ab78a91ffbf7025199113da2b69fcdbc06900f4 100644 --- a/developer/src/kmc-package/tsconfig.json +++ b/developer/src/kmc-package/tsconfig.json @@ -10,7 +10,7 @@ "src/**/*.ts", ], "references": [ - { "path": "../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../common/web/keyman-version" }, { "path": "../../../common/web/types" }, ] } diff --git a/developer/src/kmc/.eslintrc.cjs b/developer/src/kmc/.eslintrc.cjs index f348275f8a6aed22101ec03bfbfe9218998c4b8a..569f35fdb394c860058a997eacba26164a908011 100644 --- a/developer/src/kmc/.eslintrc.cjs +++ b/developer/src/kmc/.eslintrc.cjs @@ -1,5 +1,4 @@ module.exports = { - // extends: ["../../../common/web/eslint/eslintNoNodeImports.js"], parserOptions: { project: ["./tsconfig.json", "./test/tsconfig.json"], }, diff --git a/developer/src/kmc/src/commands/build/BuildLdmlKeyboard.ts b/developer/src/kmc/src/commands/build/BuildLdmlKeyboard.ts index 024f7f534002f6231322b561c7b36c0315debe04..18493dc391e63d0661d4f2e4b9d43d3b6bb6505e 100644 --- a/developer/src/kmc/src/commands/build/BuildLdmlKeyboard.ts +++ b/developer/src/kmc/src/commands/build/BuildLdmlKeyboard.ts @@ -1,8 +1,9 @@ import * as path from 'path'; import * as fs from 'fs'; import * as kmcLdml from '@keymanapp/kmc-ldml'; -import { KvkFileWriter, CompilerCallbacks } from '@keymanapp/common-types'; +import { KvkFileWriter, CompilerCallbacks, LDMLKeyboardXMLSourceFileReader } from '@keymanapp/common-types'; import { BuildActivity, BuildActivityOptions } from './BuildActivity.js'; +import { fileURLToPath } from 'url'; export class BuildLdmlKeyboard extends BuildActivity { public get name(): string { return 'LDML keyboard'; } @@ -12,7 +13,7 @@ export class BuildLdmlKeyboard extends BuildActivity { public async build(infile: string, callbacks: CompilerCallbacks, options: BuildActivityOptions): Promise { // TODO-LDML: consider hardware vs touch -- touch-only layout will not have a .kvk // Compile: - let [kmx,kvk,kmw] = buildLdmlKeyboardToMemory(infile, callbacks, options); + let [kmx,kvk,kmw] = await buildLdmlKeyboardToMemory(infile, callbacks, options); // Output: const fileBaseName = options.outFile ?? infile; @@ -42,20 +43,23 @@ export class BuildLdmlKeyboard extends BuildActivity { } } -function buildLdmlKeyboardToMemory(inputFilename: string, callbacks: CompilerCallbacks, options: BuildActivityOptions): [Uint8Array, Uint8Array, Uint8Array] { +async function buildLdmlKeyboardToMemory(inputFilename: string, callbacks: CompilerCallbacks, options: BuildActivityOptions): Promise<[Uint8Array, Uint8Array, Uint8Array]> { let compilerOptions: kmcLdml.CompilerOptions = { debug: options.debug ?? false, addCompilerVersion: options.compilerVersion ?? true, + readerOptions: { + importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + } // TODO: warnDeprecatedCode: options.warnDeprecatedCode, // TODO: treatWarningsAsErrors: options.treatWarningsAsErrors, } - const k = new kmcLdml.LdmlKeyboardCompiler(callbacks, options); + const k = new kmcLdml.LdmlKeyboardCompiler(callbacks, compilerOptions); let source = k.load(inputFilename); if (!source) { return [null, null, null]; } - let kmx = k.compile(source); + let kmx = await k.compile(source); if (!kmx) { return [null, null, null]; } diff --git a/developer/src/kmc/src/commands/buildTestData/index.ts b/developer/src/kmc/src/commands/buildTestData/index.ts index 0990b91edb55de750acdb544ffa0a6e1105cbd09..b6ebf34a739326e797565407d2dddb217a39a622 100644 --- a/developer/src/kmc/src/commands/buildTestData/index.ts +++ b/developer/src/kmc/src/commands/buildTestData/index.ts @@ -1,8 +1,9 @@ import * as fs from 'fs'; import * as path from 'path'; import * as kmc from '@keymanapp/kmc-ldml'; -import { CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile } from '@keymanapp/common-types'; +import { CompilerCallbacks, LDMLKeyboardTestDataXMLSourceFile, LDMLKeyboardXMLSourceFileReader } from '@keymanapp/common-types'; import { NodeCompilerCallbacks } from '../../messages/NodeCompilerCallbacks.js'; +import { fileURLToPath } from 'url'; export interface BuildTestDataOptions { outFile?: string; @@ -11,7 +12,10 @@ export interface BuildTestDataOptions { export function buildTestData(infile: string, options: BuildTestDataOptions) { let compilerOptions: kmc.CompilerOptions = { debug: false, - addCompilerVersion: false + addCompilerVersion: false, + readerOptions: { + importsPath: fileURLToPath(LDMLKeyboardXMLSourceFileReader.defaultImportsURL) + } }; let testData = loadTestData(infile, compilerOptions); diff --git a/developer/src/kmc/src/messages/NodeCompilerCallbacks.ts b/developer/src/kmc/src/messages/NodeCompilerCallbacks.ts index ceae18c4c59e7f8165a39460fab40558621fd211..dfcb7e2ecbfae760a3c72f66b9b1baad50819040 100644 --- a/developer/src/kmc/src/messages/NodeCompilerCallbacks.ts +++ b/developer/src/kmc/src/messages/NodeCompilerCallbacks.ts @@ -1,14 +1,52 @@ import * as fs from 'fs'; import * as path from 'path'; import { CompilerCallbacks, CompilerSchema, CompilerEvent, compilerErrorSeverityName, CompilerPathCallbacks, CompilerFileSystemCallbacks } from '@keymanapp/common-types'; +import { InfrastructureMessages } from './messages.js'; /** * Concrete implementation for CLI use */ +// TODO: Make a common class for all the CompilerCallbacks implementations + export class NodeCompilerCallbacks implements CompilerCallbacks { - // TODO: REMOVE! - loadFile(filename: string | URL): Buffer { + /* NodeCompilerCallbacks */ + + messages: CompilerEvent[] = []; + silent: boolean; + + constructor(silent?: boolean) { + this.silent = !!silent; + } + + clear() { + this.messages = []; + } + + hasMessage(code: number): boolean { + return this.messages.find((item) => item.code == code) === undefined ? false : true; + } + + private verifyFilenameConsistency(originalFilename: string): void { + if(fs.existsSync(originalFilename)) { + // Note, we only check this if the file exists, because + // if it is not found, that will be returned as an error + // from loadFile anyway. + const filename = fs.realpathSync(originalFilename); + const nativeFilename = fs.realpathSync.native(filename); + if(filename != nativeFilename) { + this.reportMessage(InfrastructureMessages.Hint_FilenameHasDifferingCase({ + reference: originalFilename, + filename: nativeFilename + })); + } + } + } + + /* CompilerCallbacks */ + + loadFile(filename: string): Uint8Array { + this.verifyFilenameConsistency(filename); try { return fs.readFileSync(filename); } catch (e) { @@ -29,6 +67,10 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { } reportMessage(event: CompilerEvent): void { + this.messages.push(event); + if(this.silent) { + return; + } const code = event.code.toString(16); if(event.line) { console.log(`${compilerErrorSeverityName(event.code)} ${code} [${event.line}]: ${event.message}`); @@ -41,7 +83,7 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { console.debug(msg); } - loadSchema(schema: CompilerSchema) { + loadSchema(schema: CompilerSchema): Uint8Array { let schemaPath = new URL('../util/' + schema + '.schema.json', import.meta.url); return fs.readFileSync(schemaPath); } @@ -51,7 +93,10 @@ export class NodeCompilerCallbacks implements CompilerCallbacks { } resolveFilename(baseFilename: string, filename: string) { - const basePath = path.dirname(baseFilename); + const basePath = + baseFilename.endsWith('/') || baseFilename.endsWith('\\') ? + baseFilename : + path.dirname(baseFilename); // Transform separators to platform separators -- we are agnostic // in our use here but path prefers files may use // either / or \, although older kps files were always \. diff --git a/developer/src/kmc/src/messages/messages.ts b/developer/src/kmc/src/messages/messages.ts index 1f1761e715d6f70f523d95d074b2eebab1fe1500..358a59406bb3a80f1c025f9c88ed80a9e1945009 100644 --- a/developer/src/kmc/src/messages/messages.ts +++ b/developer/src/kmc/src/messages/messages.ts @@ -2,7 +2,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m const Namespace = CompilerErrorNamespace.Infrastructure; const SevInfo = CompilerErrorSeverity.Info | Namespace; -// const SevHint = CompilerErrorSeverity.Hint | Namespace; +const SevHint = CompilerErrorSeverity.Hint | Namespace; // const SevWarn = CompilerErrorSeverity.Warn | Namespace; const SevError = CompilerErrorSeverity.Error | Namespace; const SevFatal = CompilerErrorSeverity.Fatal | Namespace; @@ -39,5 +39,9 @@ export class InfrastructureMessages { static Error_InvalidProjectFile = (o:{message:string}) => m(this.ERROR_InvalidProjectFile, `Project file is not valid: ${o.message}`); static ERROR_InvalidProjectFile = SevError | 0x0008; + + static Hint_FilenameHasDifferingCase = (o:{reference:string, filename:string}) => m(this.HINT_FilenameHasDifferingCase, + `File ${o.filename} differs in case from reference ${o.reference}; this will fail on platforms with case-sensitive filesystems.`); + static HINT_FilenameHasDifferingCase = SevHint | 0x0009; } diff --git a/developer/src/kmc/test/fixtures/invalid-keyboards/hint_filename_has_differing_case.kmn b/developer/src/kmc/test/fixtures/invalid-keyboards/hint_filename_has_differing_case.kmn new file mode 100644 index 0000000000000000000000000000000000000000..5792016e10383d7bee622a0a49d6ec5e9ee03e09 --- /dev/null +++ b/developer/src/kmc/test/fixtures/invalid-keyboards/hint_filename_has_differing_case.kmn @@ -0,0 +1,6 @@ +store(&name) 'hint_filename_has_differing_case' +store(&version) '7.0' + +begin unicode > use(main) + +group(main) using keys diff --git a/developer/src/kmc/test/test-messages.ts b/developer/src/kmc/test/test-messages.ts index 7b591ad72de7a2e3dec348e55f52155c77030cbb..83fb5162b80cdd14a08c44f71311d5e7c6413195 100644 --- a/developer/src/kmc/test/test-messages.ts +++ b/developer/src/kmc/test/test-messages.ts @@ -1,9 +1,81 @@ import 'mocha'; +import { assert } from 'chai'; import { InfrastructureMessages } from '../src/messages/messages.js'; import { verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; +import { makePathToFixture } from './helpers/index.js'; +import { NodeCompilerCallbacks } from '../src/messages/NodeCompilerCallbacks.js'; +import { CompilerErrorNamespace } from '@keymanapp/common-types'; describe('InfrastructureMessages', function () { it('should have a valid InfrastructureMessages object', function() { - return verifyCompilerMessagesObject(InfrastructureMessages); + return verifyCompilerMessagesObject(InfrastructureMessages, CompilerErrorNamespace.Infrastructure); + }); + + // + // Message tests + // + + /* + TODO: + + let callbacks = new TestCompilerCallbacks(); + + async function testForMessage(context: Mocha.Context, fixture: string[], messageId?: number) { + context.timeout(10000); + + callbacks.clear(); + + const builder = new BuildKmnKeyboard(); + const path = makePathToFixture(...fixture); + let result = await builder.build(path, callbacks, { + compilerVersion: false, + compilerWarningsAsErrors: true, + debug: false, + warnDeprecatedCode: true, + }); + + if(messageId) { + assert.isTrue(callbacks.hasMessage(messageId), `messageId ${messageId.toString(16)} not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); + assert.lengthOf(callbacks.messages, 1); + } else { + assert.lengthOf(callbacks.messages, 0, `messages should be empty, but instead got: `+JSON.stringify(callbacks.messages,null,2)); + assert.isTrue(result); + } + } + + // ERROR_FileDoesNotExist + + it('should generate ERROR_FileDoesNotExist if a file does not exist', async function() { + await testForMessage(this, ['invalid-keyboards', 'error_file_does_not_exist.kmn'], CompilerMessages.ERROR_FileDoesNotExist); + }); + + // ERROR_FileTypeNotRecognized + + it('should generate ERROR_FileTypeNotRecognized if a file is not a recognized type', async function() { + await testForMessage(this, ['invalid-keyboards', 'error_file_type_not_recognized.xxx'], CompilerMessages.ERROR_FileTypeNotRecognized); + }); + + // ERROR_OutFileNotValidForProjects + + it('should generate ERROR_OutFileNotValidForProjects if an output file is specified for a project build', async function() { + await testForMessage(this, ['invalid-keyboards', 'error_out_file_not_valid_for_projects.kpj'], CompilerMessages.ERROR_OutFileNotValidForProjects); + }); + + // ERROR_InvalidProjectFile + + it('should generate ERROR_InvalidProjectFile if a project file is invalid', async function() { + await testForMessage(this, ['invalid-keyboards', 'error_invalid_project_file.kpj'], CompilerMessages.ERROR_InvalidProjectFile); + }); + */ + + // HINT_FilenameHasDifferingCase + + it('should generate HINT_FilenameHasDifferingCase if a referenced file has differing case', async function() { + // This message is generated by NodeCompilerCallbacks, because that's where the filesystem is visible, + // so we can't use our usual testForMessage pattern. + const ncb = new NodeCompilerCallbacks(true); + ncb.loadFile(makePathToFixture('invalid-keyboards', 'Hint_Filename_Has_Differing_Case.kmn')); + assert.isTrue(ncb.hasMessage(InfrastructureMessages.HINT_FilenameHasDifferingCase), + `HINT_FilenameHasDifferingCase not generated, instead got: `+JSON.stringify(ncb.messages,null,2)); }); }); diff --git a/developer/src/kmc/test/test-project-build.ts b/developer/src/kmc/test/test-project-build.ts index 68b5926933fd2f23772a7cbd5624b8661ad35df7..e16c9fd464db80b0cd808dde22aaf37a72707ca8 100644 --- a/developer/src/kmc/test/test-project-build.ts +++ b/developer/src/kmc/test/test-project-build.ts @@ -16,9 +16,9 @@ describe('BuildProject', function () { debug: false, warnDeprecatedCode: true, }); - // 5 messages == starting build, info: no keyboard version, build successful x 2 + // 4 messages == starting build x 2, build successful x 2 // callbacks.printMessages(); - assert.equal(callbacks.messages.length, 5); + assert.equal(callbacks.messages.length, 4); assert.isTrue(result); }); }); diff --git a/developer/src/kmc/tsconfig.json b/developer/src/kmc/tsconfig.json index 9b6eb601acd121a775c5d7943a0c7477bb9e9fda..0493499987ecca63a769351bf2fd4577da8c060e 100644 --- a/developer/src/kmc/tsconfig.json +++ b/developer/src/kmc/tsconfig.json @@ -18,7 +18,7 @@ "src/**/*.ts" ], "references": [ - { "path": "../../../common/web/keyman-version/tsconfig.esm.json" }, + { "path": "../../../common/web/keyman-version" }, { "path": "../../../common/web/types" }, { "path": "../kmc-kmn" }, { "path": "../kmc-ldml" }, diff --git a/developer/src/kmcmpdll/Compiler.cpp b/developer/src/kmcmpdll/Compiler.cpp index 8cc28d2ab7f18bfdc5e6e65dd78b99bf1fe8afe4..100e43fc5bd4356d1fe7cd95b427b6e28a6afe6d 100644 --- a/developer/src/kmcmpdll/Compiler.cpp +++ b/developer/src/kmcmpdll/Compiler.cpp @@ -90,8 +90,6 @@ #include "UnreachableRules.h" #include "CheckForDuplicates.h" -#include "../kmcmplib/include/kmcmplibapi.h" - int xatoi(PWSTR *p); int atoiW(PWSTR p); void safe_wcsncpy(PWSTR out, PWSTR in, int cbMax); @@ -291,33 +289,15 @@ BOOL AddCompileMessage(DWORD msg) return FALSE; } -bool flag_use_new_kmcomp = true; // flag to switch to new kmcompx - extern "C" BOOL __declspec(dllexport) SetCompilerOptions(PCOMPILER_OPTIONS options) { if(!options || options->dwSize < sizeof(COMPILER_OPTIONS)) { return FALSE; } - flag_use_new_kmcomp = options->UseKmcmpLib; - - //printf("\n---> started in SetCompilerOptions() of kmcmpdll\n"); - if ( flag_use_new_kmcomp ) - { - KMCMP_COMPILER_OPTIONS kmcmp_options = {0}; - kmcmp_options.dwSize = sizeof(KMCMP_COMPILER_OPTIONS); - kmcmp_options.ShouldAddCompilerVersion = options->ShouldAddCompilerVersion; - return kmcmp_SetCompilerOptions(&kmcmp_options); - } - //printf("---> stayed in SetCompilerOptions() of kmcmpdll\n"); - FShouldAddCompilerVersion = options->ShouldAddCompilerVersion; return TRUE; } -int kmcmpMsgproc(int line, uint32_t dwMsgCode, char* szText, void* context) { - return static_cast(context)(line, dwMsgCode, szText); -} - extern "C" BOOL __declspec(dllexport) CompileKeyboardFile(PSTR pszInfile, PSTR pszOutfile, BOOL ASaveDebug, BOOL ACompilerWarningsAsErrors, BOOL AWarnDeprecatedCode, CompilerMessageProc pMsgProc) // I4865 // I4866 { HANDLE hInfile = INVALID_HANDLE_VALUE, hOutfile = INVALID_HANDLE_VALUE; @@ -325,15 +305,6 @@ extern "C" BOOL __declspec(dllexport) CompileKeyboardFile(PSTR pszInfile, PSTR p DWORD len; char str[260]; - //printf("\n---> started in CompileKeyboardFile() of kmcmpdll\n"); - - if ( flag_use_new_kmcomp ) - { - return kmcmp_CompileKeyboardFile(pszInfile, pszOutfile, ASaveDebug, ACompilerWarningsAsErrors,AWarnDeprecatedCode, kmcmpMsgproc, (void*) pMsgProc); - } - - //printf("---> stayed in CompileKeyboardFile() of kmcmpdll\n"); - FSaveDebug = ASaveDebug; FCompilerWarningsAsErrors = ACompilerWarningsAsErrors; // I4865 FWarnDeprecatedCode = AWarnDeprecatedCode; // I4866 @@ -418,15 +389,6 @@ extern "C" BOOL __declspec(dllexport) CompileKeyboardFileToBuffer(PSTR pszInfile DWORD len; char str[260]; - //printf("\n---> started in CompileKeyboardFileToBuffer() of kmcmpdll\n"); - if ( flag_use_new_kmcomp ) - { - return kmcmp_CompileKeyboardFileToBuffer( pszInfile, (void*) pfkBuffer, ACompilerWarningsAsErrors, AWarnDeprecatedCode, kmcmpMsgproc, (void*) pMsgProc, Target); - } - - //printf("---> stayed in CompileKeyboardFileToBuffer() of kmcmpdll\n"); - - FSaveDebug = TRUE; // I3681 FCompilerWarningsAsErrors = ACompilerWarningsAsErrors; // I4865 FWarnDeprecatedCode = AWarnDeprecatedCode; // I4866 diff --git a/developer/src/kmcmpdll/compfile.h b/developer/src/kmcmpdll/compfile.h index 4fd6e68eb546a87d7f41ed22bca6421b36a13501..9e65c36600e893f302a7c687b25ef7f998fda0dc 100644 --- a/developer/src/kmcmpdll/compfile.h +++ b/developer/src/kmcmpdll/compfile.h @@ -22,6 +22,13 @@ #include "../../../common/windows/cpp/include/legacy_kmx_file.h" + +// This file is deprecated; see kmcmplib/src/compfile.h. However, as long as +// this file continues to live, we need to keep structures in it in exact sync +// with kmcmplib/src/compfile.h, as FILE_KEYBOARD is passed between kmcmplib and +// kmcmpdll. KMX_WCHAR on Windows is identical to wchar_t, so it is safe to map +// accordingly. + #define LINESIZE 8192 #define GLOBAL_BUFSIZE 4096 @@ -70,9 +77,9 @@ enum FileStoreType { FST_STORE, FST_OPTION, FST_RESERVED }; struct FILE_STORE { - DWORD dwSystemID; - WCHAR szName[SZMAX_STORENAME]; // the name of the store - PWSTR dpString; // from start of store structure + DWORD dwSystemID; + WCHAR szName[SZMAX_STORENAME]; // the name of the store + PWSTR dpString; // from start of store structure //FileStoreType fstType; BOOL fIsStore; BOOL fIsReserved; @@ -80,77 +87,75 @@ struct FILE_STORE { BOOL fIsDebug; BOOL fIsCall; int line; // TODO: int vs dword, line vs Line (see FILE_KEY, FILE_GROUP) - }; +}; typedef FILE_STORE *PFILE_STORE; struct FILE_KEY { - WCHAR Key; // WCHAR for consistency; only a byte used however - WORD LineStoreIndex; - DWORD Line; - DWORD ShiftFlags; - PWSTR dpOutput; // from start of key structure - PWSTR dpContext; // from start of key structure - }; - + WCHAR Key; // WCHAR for consistency; only a byte used however + WORD LineStoreIndex; + DWORD Line; + DWORD ShiftFlags; + PWSTR dpOutput; // from start of key structure + PWSTR dpContext; // from start of key structure +}; typedef FILE_KEY *PFILE_KEY; struct FILE_GROUP { - WCHAR szName[SZMAX_GROUPNAME]; - PFILE_KEY dpKeyArray; // address of first item in key array, from start of group structure - PWSTR dpMatch; // from start of group structure - PWSTR dpNoMatch; // from start of group structure - DWORD cxKeyArray; // in array items - BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not + WCHAR szName[SZMAX_GROUPNAME]; + PFILE_KEY dpKeyArray; // address of first item in key array, from start of group structure + PWSTR dpMatch; // from start of group structure + PWSTR dpNoMatch; // from start of group structure + DWORD cxKeyArray; // in array items + BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not BOOL fReadOnly; // group(xx) [readonly] <-- specified or not DWORD Line; }; typedef FILE_GROUP *PFILE_GROUP; -struct FILE_DEADKEY -{ - WCHAR szName[SZMAX_DEADKEYNAME]; +struct FILE_DEADKEY { + WCHAR szName[SZMAX_DEADKEYNAME]; }; typedef FILE_DEADKEY *PFILE_DEADKEY; -struct FILE_VKDICTIONARY -{ +struct FILE_VKDICTIONARY { WCHAR szName[SZMAX_VKDICTIONARYNAME]; }; - typedef FILE_VKDICTIONARY *PFILE_VKDICTIONARY; struct FILE_KEYBOARD { - DWORD KeyboardID; // as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts + DWORD KeyboardID; // deprecated, unused - DWORD version; // keyboard file version with VERSION keyword + DWORD version; // keyboard file version with VERSION keyword - PFILE_STORE dpStoreArray; // address of first item in store array, from start of store structure - PFILE_GROUP dpGroupArray; // address of first item in group array, from start of group structure + PFILE_STORE dpStoreArray; // address of first item in store array, from start of store structure + PFILE_GROUP dpGroupArray; // address of first item in group array, from start of group structure - DWORD cxStoreArray; // in number of items - DWORD cxGroupArray; // in number of items - DWORD StartGroup[2]; // index of starting groups [ANSI=0, Unicode=1] + DWORD cxStoreArray; // in number of items + DWORD cxGroupArray; // in number of items + DWORD StartGroup[2]; // index of starting groups [ANSI=0, Unicode=1] - DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) + DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) - WCHAR szName[SZMAX_KEYBOARDNAME]; // Keyboard layout name - WCHAR szLanguageName[SZMAX_LANGUAGENAME]; // Language name - WCHAR szCopyright[SZMAX_COPYRIGHT]; // Copyright information - WCHAR szMessage[SZMAX_MESSAGE]; // General information about the keyboard - PBYTE lpBitmap; - DWORD dwBitmapSize; - DWORD dwFlags; // Flags for the keyboard file + WCHAR szName[SZMAX_KEYBOARDNAME]; // Keyboard layout name + WCHAR szLanguageName[SZMAX_LANGUAGENAME]; // Language name + WCHAR szCopyright[SZMAX_COPYRIGHT]; // Copyright information + WCHAR szMessage[SZMAX_MESSAGE]; // General information about the keyboard + PBYTE lpBitmap; + DWORD dwBitmapSize; + DWORD dwFlags; // Flags for the keyboard file - DWORD currentGroup; // temp - current processing group - DWORD currentStore; // temp - current processing store - DWORD cxDeadKeyArray; - PFILE_DEADKEY dpDeadKeyArray; // temp - dead key array + DWORD currentGroup; // temp - current processing group + DWORD currentStore; // temp - current processing store + DWORD cxDeadKeyArray; + PFILE_DEADKEY dpDeadKeyArray; // temp - dead key array DWORD cxVKDictionary; PFILE_VKDICTIONARY dpVKDictionary; // temp - virtual key dictionary - }; + + void* extra; // used by kmcmplib and its consumers; unused in kmcmpdll +}; typedef FILE_KEYBOARD *PFILE_KEYBOARD; @@ -168,34 +173,34 @@ const DWORD sz_FILE_VKDICTIONARY = sizeof(FILE_VKDICTIONARY); const DWORD sz_FILE_KEYBOARD = sizeof(FILE_KEYBOARD); struct COMPMSG { - char szText[SZMAX_ERRORTEXT]; - DWORD Line; - DWORD dwMsgCode; - }; + char szText[SZMAX_ERRORTEXT]; + DWORD Line; + DWORD dwMsgCode; +}; typedef COMPMSG *PCOMPMSG; struct COMPILEMESSAGES { - int nMessages; - int nErrors; + int nMessages; + int nErrors; - PCOMPMSG cm; + PCOMPMSG cm; - DWORD fatalCode; - char szFatalText[SZMAX_ERRORTEXT]; + DWORD fatalCode; + char szFatalText[SZMAX_ERRORTEXT]; - DWORD currentLine; - }; + DWORD currentLine; +}; typedef COMPILEMESSAGES *PCOMPILEMESSAGES; /* struct TVersion { - //int MinVersion; // 0x0500 usually - //int CompilerVersion[4]; - //int MinCompilerVersion[4]; - int KeyboardVersion; // 0x0500 usually + //int MinVersion; // 0x0500 usually + //int CompilerVersion[4]; + //int MinCompilerVersion[4]; + int KeyboardVersion; // 0x0500 usually }; extern TVersion FVersionInfo; diff --git a/developer/src/kmcmpdll/json-validation.cpp b/developer/src/kmcmpdll/json-validation.cpp index e35dc0485dcae1480b00b1f002bad5fa75b56d26..c3c1af0484318b176cacef53dec22f58c7f2f118 100644 --- a/developer/src/kmcmpdll/json-validation.cpp +++ b/developer/src/kmcmpdll/json-validation.cpp @@ -1,14 +1,11 @@ #include #include -#include "../kmcmplib/include/kmcmplibapi.h" #include typedef bool (*kmcmp_ValidateJsonMessageProc)(int64_t offset, const char* szText, void* context); -extern bool flag_use_new_kmcomp; - using nlohmann::json; using nlohmann::json_uri; using nlohmann::json_schema_draft4::json_validator; @@ -35,13 +32,6 @@ bool kmcmpMessageProc(int64_t offset, const char* szText, void* context) { extern "C" BOOL __declspec(dllexport) ValidateJsonFile(PWSTR pwszSchemaFile, PWSTR pwszJsonFile, ValidateJsonMessageProc MessageProc) { - if ( flag_use_new_kmcomp ) - { - std::fstream f(pwszSchemaFile); - std::fstream fd(pwszJsonFile); - return kmcmp_ValidateJsonFile(f, fd, kmcmpMessageProc, (void*)MessageProc); - } - std::fstream f(pwszSchemaFile); if (!f.good()) { MessageProc(-1, "Schema file could not be loaded."); diff --git a/developer/src/kmcmpdll/kcframe/kcframe.cpp b/developer/src/kmcmpdll/kcframe/kcframe.cpp index 4ac80bfe18011e36a6eb5bc909b3035f5af8afec..22402e52300d194079fae95422eea2ad6dd01d54 100644 --- a/developer/src/kmcmpdll/kcframe/kcframe.cpp +++ b/developer/src/kmcmpdll/kcframe/kcframe.cpp @@ -24,6 +24,8 @@ #include +extern "C" BOOL CompileKeyboardFile(PSTR pszInfile, PSTR pszOutfile, BOOL FSaveDebug, BOOL ACompilerWarningsAsErrors, BOOL AWarnDeprecatedCode, CompilerMessageProc pMsgProc); // I4865 // I4866 + int WINAPI msgproc(int line, DWORD dwMsgCode, LPSTR szText) { printf("line %d error %x %s\n", line, (unsigned int) dwMsgCode, szText); diff --git a/developer/src/kmcmpdll/kmcmpdll.vcxproj b/developer/src/kmcmpdll/kmcmpdll.vcxproj index c7f7e1f48f062c068d8cc15e1846f86f25942d9f..2bcc038bd00df813dc7fc9cd7db7a3f3b268d034 100644 --- a/developer/src/kmcmpdll/kmcmpdll.vcxproj +++ b/developer/src/kmcmpdll/kmcmpdll.vcxproj @@ -89,25 +89,25 @@ $(VC_IncludePath);$(WindowsSDK_IncludePath);$(KEYMAN_ROOT)\developer\src\common\include;$(KEYMAN_ROOT)\developer\src\ext\json;$(KEYMAN_ROOT)\developer\src\ext\json-schema-validator $(ProjectDir)bin\$(Platform)\$(Configuration)\ - $(KEYMAN_ROOT)\developer\src\kmcmplib\build\x86\$(Configuration)\src;$(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86) + $(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86) $(VC_IncludePath);$(WindowsSDK_IncludePath);$(KEYMAN_ROOT)\developer\src\common\include;$(KEYMAN_ROOT)\developer\src\ext\json;$(KEYMAN_ROOT)\developer\src\ext\json-schema-validator $(ProjectDir)obj\$(Platform)\$(Configuration)\ $(ProjectName).x64 $(ProjectDir)bin\$(Platform)\$(Configuration)\ - $(KEYMAN_ROOT)\developer\src\kmcmplib\build\x64\$(Configuration)\src;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) + $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) $(VC_IncludePath);$(WindowsSDK_IncludePath);$(KEYMAN_ROOT)\developer\src\common\include;$(KEYMAN_ROOT)\developer\src\ext\json;$(KEYMAN_ROOT)\developer\src\ext\json-schema-validator - $(KEYMAN_ROOT)\developer\src\kmcmplib\build\x86\$(Configuration)\src;$(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86) + $(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86) $(VC_IncludePath);$(WindowsSDK_IncludePath);$(KEYMAN_ROOT)\developer\src\common\include;$(KEYMAN_ROOT)\developer\src\ext\json;$(KEYMAN_ROOT)\developer\src\ext\json-schema-validator $(ProjectDir)obj\$(Platform)\$(Configuration)\ $(ProjectName).x64 $(ProjectDir)bin\$(Platform)\$(Configuration)\ - $(KEYMAN_ROOT)\developer\src\kmcmplib\build\x64\$(Configuration)\src;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) + $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) @@ -142,7 +142,7 @@ $(KEYMAN_ROOT)\windows\src\global\inc - libkmcmplib.a;version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) + version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) true true @@ -190,7 +190,7 @@ $(KEYMAN_ROOT)\windows\src\global\inc - libkmcmplib.a;version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) + version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) true true Windows @@ -233,7 +233,7 @@ $(KEYMAN_ROOT)\windows\src\global\inc - libkmcmplib.a;version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) + version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) true true true @@ -277,7 +277,7 @@ $(KEYMAN_ROOT)\windows\src\global\inc - libkmcmplib.a;version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) + version.lib;setupapi.lib;iphlpapi.lib;imm32.lib;crypt32.lib;wintrust.lib;imagehlp.lib;ws2_32.lib;%(AdditionalDependencies) true true true diff --git a/developer/src/kmcmplib/checkout-keyboards.inc.sh b/developer/src/kmcmplib/checkout-keyboards.inc.sh index defcb479ef4070a759b2767115d1949e7cc78ac1..6c76d53caa1fc9f39eeef1a9bcdce0140c57ed86 100644 --- a/developer/src/kmcmplib/checkout-keyboards.inc.sh +++ b/developer/src/kmcmplib/checkout-keyboards.inc.sh @@ -9,8 +9,8 @@ function locate_keyboards_repo() { builder_die "keyboards_commit_ref.txt does not exist, run prep.sh" fi - if [[ ! -d "$THIS_SCRIPT_PATH/tests/fixtures" ]]; then - builder_die "fixtures folder does not exist, run prep.sh" + if [[ ! -d "$THIS_SCRIPT_PATH/tests/fixtures/keyboards-repo" ]]; then + builder_die "fixtures/keyboards-repo folder does not exist, run prep.sh" fi if [[ -z "${KEYBOARDS_ROOT+x}" ]]; then diff --git a/developer/src/kmcmplib/commands.inc.sh b/developer/src/kmcmplib/commands.inc.sh index bdb8e0f139bfa19b078db1e94e0508dbf24fb2a7..d3647c41dd55e6219181ecf8d535711f432fed99 100644 --- a/developer/src/kmcmplib/commands.inc.sh +++ b/developer/src/kmcmplib/commands.inc.sh @@ -1,4 +1,7 @@ -#!/usr/bin/env bash +# shellcheck shell=bash +# no hashbang for .inc.sh + +. "$KEYMAN_ROOT/resources/locate_emscripten.inc.sh" # ---------------------------------------------------------------------------- # clean @@ -115,34 +118,6 @@ do_command() { # utility functions # ---------------------------------------------------------------------------- -# -# We don't want to rely on emcc being on the path, because Emscripten puts far -# too many things onto the path (in particular for us, node). -# -# The following comment suggests that we don't need emcc on the path. -# https://github.com/emscripten-core/emscripten/issues/4848#issuecomment-1097357775 -# -# So we try and locate emcc in common locations ourselves. The search pattern -# is: -# -# 1. Look for $EMSCRIPTEN_BASE (our primary emscripten variable), which should -# point to the folder that emcc is located in -# 2. Look for $EMCC which should point to the emcc executable -# 3. Look for emcc on the path -# -locate_emscripten() { - if [[ -z ${EMSCRIPTEN_BASE+x} ]]; then - if [[ -z ${EMCC+x} ]]; then - local EMCC=`which emcc` - [[ -z $EMCC ]] && builder_die "locate_emscripten: Could not locate emscripten (emcc) on the path or with \$EMCC or \$EMSCRIPTEN_BASE" - fi - [[ -x $EMCC ]] || builder_die "locate_emscripten: Variable EMCC ($EMCC) does not point to a valid executable emcc" - EMSCRIPTEN_BASE="$(dirname "$EMCC")" - fi - - [[ -x ${EMSCRIPTEN_BASE}/emcc ]] || builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) does not point to emcc's folder" -} - build_meson_cross_file_for_wasm() { if [ $BUILDER_OS == win ]; then local R=$(cygpath -w $(echo $EMSCRIPTEN_BASE) | sed 's_\\_\\\\_g') diff --git a/developer/src/kmcmplib/include/kmcmplibapi.h b/developer/src/kmcmplib/include/kmcmplibapi.h index 97d46c3c364c777fd3c8fe4c89009844d2d4635b..f37e393eea3fc34b9d08c80e4ac334b04b95498c 100644 --- a/developer/src/kmcmplib/include/kmcmplibapi.h +++ b/developer/src/kmcmplib/include/kmcmplibapi.h @@ -4,6 +4,8 @@ #ifdef __EMSCRIPTEN__ #include +#include + #else #define EMSCRIPTEN_KEEPALIVE #endif @@ -14,53 +16,56 @@ #define EXTERN EMSCRIPTEN_KEEPALIVE #endif -typedef struct _KMCMP_COMPILER_OPTIONS { - uint32_t dwSize; - bool ShouldAddCompilerVersion; -} KMCMP_COMPILER_OPTIONS; - -EXTERN bool kmcmp_SetCompilerOptions( - KMCMP_COMPILER_OPTIONS* options -); - -typedef int (*kmcmp_CompilerMessageProc)(int line, uint32_t dwMsgCode, char* szText, void* context); - -EXTERN bool kmcmp_CompileKeyboardFile( - char* pszInfile, - char* pszOutfile, - bool ASaveDebug, - bool ACompilerWarningsAsErrors, - bool AWarnDeprecatedCode, - kmcmp_CompilerMessageProc pMsgproc, - void* AmsgprocContext -); - /* Compile target */ #define CKF_KEYMAN 0 #define CKF_KEYMANWEB 1 -EXTERN bool kmcmp_CompileKeyboardFileToBuffer( - char* pszInfile, - void* pfkBuffer, - bool ACompilerWarningsAsErrors, - bool AWarnDeprecatedCode, - kmcmp_CompilerMessageProc pMsgproc, - void* AmsgprocContext, - int Target -); +struct KMCMP_COMPILER_OPTIONS { + bool saveDebug; + bool compilerWarningsAsErrors; + bool warnDeprecatedCode; + bool shouldAddCompilerVersion; + int target; // CKF_KEYMAN, CKF_KEYMANWEB +}; -typedef bool (*kmcmp_ValidateJsonMessageProc)(int64_t offset, const char* szText, void* context); +struct KMCMP_COMPILER_RESULT { + void* kmx; + size_t kmxSize; + std::string kvksFilename; +}; -EXTERN bool kmcmp_ValidateJsonFile( - std::fstream& f, - std::fstream& fd, - kmcmp_ValidateJsonMessageProc MessageProc, - void* context +/** + * @param szText UTF-8 string +*/ +typedef int (*kmcmp_CompilerMessageProc)(int line, uint32_t dwMsgCode, const char* szText, void* context); + +// parameters in UTF-8 +// TODO typical usage: +// if(!kmcmp_LoadFileProc("filename.ico", "/tmp/filename.kmn", nullptr, &size)) { +// return error; +// } +// buf = new unsigned char[size]; +// if(!kmcmp_LoadFileProc("filename.ico", "/tmp/filename.kmn", buf, &size)) { +// delete[] buf; +// return error; +// } +typedef bool (*kmcmp_LoadFileProc)(const char* loadFilename, const char* baseFilename, void* buffer, int* bufferSize, void* context); + +/** + * @param pszInfile UTF-8 path to file.kmn + */ +EXTERN bool kmcmp_CompileKeyboard( + const char* pszInfile, + const KMCMP_COMPILER_OPTIONS& options, + kmcmp_CompilerMessageProc messageProc, + kmcmp_LoadFileProc loadFileProc, + const void* procContext, + KMCMP_COMPILER_RESULT& result ); /** - * kmcmp_ParseUnicodeSet is successful if it returns >= USET_OK + * kmcmp_parseUnicodeSet is successful if it returns >= USET_OK */ static const int KMCMP_USET_OK = 0; @@ -81,24 +86,19 @@ static const int KMCMP_ERROR_UNSUPPORTED_PROPERTY = -3; */ static const int KMCMP_FATAL_OUT_OF_RANGE = -4; -/** - * Function pointer to kmcmp_ParseUnicodeSet - */ -typedef int (*kmcmp_ParseUnicodeSetProc)(const char* szText, uint32_t* output, uint32_t outputLength); - /** * Parse a UnicodeSet into 32-bit ranges. * For example, "[]" will return 0 (KMCMP_USET_OK) as a zero-length set. * "[" will return KMCMP_ERROR_SYNTAX_ERR, * and "[x A-C]" will return 2 and [0x41, 0x43, 0x78, 0x78] - * @param szText input txt, null terminated, in UTF-8 format + * @param text input txt, null terminated, in UTF-8 format * @param outputBuffer output buffer, owned by caller: Pairs of ranges in order * @param outputBufferSize length of output buffer. Needs to be twice the number of expected ranges * @return If >= KMCMP_USET_OK, number of ranges, otherwise one of the negative error values. */ -EXTERN int kmcmp_ParseUnicodeSet( - const char* szText, - uint32_t* outputBuffer, +EXTERN int kmcmp_parseUnicodeSet( + const std::string text, + uintptr_t outputBuffer_, uint32_t outputBufferSize ); diff --git a/developer/src/kmcmplib/include/meson.build b/developer/src/kmcmplib/include/meson.build index f98a4d8c762da2134f33054a4b18948aa3a5e658..dde8e69f6b0cd52d1c8f1ce66f2a65c413e3809f 100644 --- a/developer/src/kmcmplib/include/meson.build +++ b/developer/src/kmcmplib/include/meson.build @@ -20,7 +20,5 @@ configure_file( inc = include_directories( '.', '../../common/include', - '../../../../common/include', - '../../ext/json', - '../../ext/json-schema-validator' + '../../../../common/include' ) diff --git a/developer/src/kmcmplib/meson.build b/developer/src/kmcmplib/meson.build index ae8fd6ac62130addd0b3a1a7642b1f4530571b4b..c996981f47a896530db7f9070c90467ce98a1315 100644 --- a/developer/src/kmcmplib/meson.build +++ b/developer/src/kmcmplib/meson.build @@ -10,7 +10,8 @@ project('kmcmplib', 'cpp', 'c', default_options : ['buildtype=release', 'cpp_std=c++14', 'b_vscrt=static_from_buildtype', - 'default_library=static']) + 'default_library=static', + 'debug=true']) # Import our standard compiler defines; this is copied from # /resources/build/standard.meson.build by build.sh, because diff --git a/developer/src/kmcmplib/src/CheckFilenameConsistency.cpp b/developer/src/kmcmplib/src/CheckFilenameConsistency.cpp deleted file mode 100644 index 627298bf375de6155aeae55fb1ddff96a157df14..0000000000000000000000000000000000000000 --- a/developer/src/kmcmplib/src/CheckFilenameConsistency.cpp +++ /dev/null @@ -1,145 +0,0 @@ - -#define _SILENCE_EXPERIMENTAL_FILESYSTEM_DEPRECATION_WARNING 1 -#include "pch.h" -#include "compfile.h" -#include -#include "kmcmplib.h" -#include -#include "CheckFilenameConsistency.h" -#include "kmx_u16.h" -#include "filesystem.h" - -#ifdef _MSC_VER -#include -#endif - -namespace kmcmp { - extern KMX_CHAR CompileDir[260]; // TODO: this should not be a fixed buffer -} -bool IsRelativePath(KMX_CHAR const * p) { - // Relative path (returns TRUE): - // ..\...\BITMAP.BMP - // PATH\BITMAP.BMP - // BITMAP.BMP - - // Semi-absolute path (returns FALSE): - // \...\BITMAP.BMP - - // Absolute path (returns FALSE): - // C:\...\BITMAP.BMP - // \\SERVER\SHARE\...\BITMAP.BMP - - if ((*p == '\\') || (*p == '/')) return FALSE; - if (*p && *(p + 1) == ':') return FALSE; - - return TRUE; -} - -bool IsRelativePath(KMX_WCHAR const * p) { - // Relative path (returns TRUE): - // ..\...\BITMAP.BMP - // PATH\BITMAP.BMP - // BITMAP.BMP - - // Semi-absolute path (returns FALSE): - // \...\BITMAP.BMP - - // Absolute path (returns FALSE): - // C:\...\BITMAP.BMP - // \\SERVER\SHARE\...\BITMAP.BMP - - if ((*p == u'\\') || (*p == u'/'))return FALSE; - if (*p && *(p + 1) == u':') return FALSE; - - return TRUE; -} - -KMX_DWORD CheckFilenameConsistency( KMX_CHAR const * Filename, bool ReportMissingFile) { - PKMX_WCHAR WFilename = strtowstr(( KMX_CHAR *)Filename); - KMX_DWORD const result = CheckFilenameConsistency(WFilename, ReportMissingFile); - delete WFilename; - return result; -} - -KMX_DWORD CheckFilenameConsistency(KMX_WCHAR const * Filename, bool ReportMissingFile) { - // not ready yet: needs more attention-> common includes for non-Windows platforms - KMX_WCHAR Name[260]; // TODO: fixed buffer sizes bad - - - if (IsRelativePath(Filename)) { - PKMX_WCHAR WCompileDir = strtowstr(kmcmp::CompileDir); - u16ncpy(Name, WCompileDir, _countof(Name)); // I3481 - u16ncat(Name, Filename, _countof(Name)); // I3481 - delete[] WCompileDir; - } else { - u16ncpy(Name, Filename, _countof(Name)); // I3481 - } - -#ifndef _MSC_VER - // Filename consistency only needs to be checked on Windows, because other - // platforms are going to fail if the filename is inconsistent anyway! - if(!kmcmp_FileExists(Name)) { - if (ReportMissingFile) { - u16cpy(ErrExtraW, u"referenced file '"); - u16ncat(ErrExtraW, Filename, 256); - u16ncat(ErrExtraW, u"'", 256); - strcpy(ErrExtraLIB, string_from_u16string(ErrExtraW).c_str()); - AddWarning(CWARN_MissingFile); - } - return CERR_None; - } - return CERR_None; -#else - _wfinddata_t fi; - intptr_t n; - if ((n = _wfindfirst((const wchar_t*) Name, &fi)) == -1) { - if (ReportMissingFile) { - sprintf(ErrExtraLIB, "referenced file '%ls'", (wchar_t*) Filename); - AddWarning(CWARN_MissingFile); - } - return CERR_None; - } - - _findclose(n); - - KMX_WCHAR FName[_MAX_FNAME], Ext[_MAX_EXT]; - wchar_t WChName[_MAX_PATH]; - _wsplitpath_s((const wchar_t*)Filename, nullptr, 0, nullptr, 0, (wchar_t*) FName, _MAX_FNAME, (wchar_t*) Ext, _MAX_EXT); - _wmakepath_s(WChName, _MAX_PATH, nullptr, nullptr, (const wchar_t*) FName, (const wchar_t*) Ext); - if (wcscmp(WChName, fi.name) != 0) { - sprintf(ErrExtraLIB, "reference '%ls' does not match actual filename '%ls'", WChName, fi.name); - - AddWarning(CHINT_FilenameHasDifferingCase); - - } -#endif - - return CERR_None; -} - -KMX_DWORD CheckFilenameConsistencyForCalls(PFILE_KEYBOARD fk) { - // call() statements depend on a fairly ugly hack for js, - // where store(DllFunction) "my.dll:func" will look for a - // file called function.call_js. ( or should this be func.call_js ? ) - // This is ripe for rewrite! - // But let's check what we have anyway - - PFILE_STORE sp; - KMX_DWORD i, msg; - for (i = 0, sp = fk->dpStoreArray; i < fk->cxStoreArray; i++, sp++) { - if (!sp->fIsCall) continue; - - const std::u16string callsite(sp->dpString); - const auto colon = callsite.find(':'); - if (colon == std::u16string::npos) continue; - - auto func1 = callsite.substr(colon + 1); - std::u16string str_js(u".call_js"); - std::u16string func = func1+ str_js; - - if ((msg = CheckFilenameConsistency(func.c_str(), FALSE)) != CERR_None) { - return msg; - } - } - return CERR_None; -} diff --git a/developer/src/kmcmplib/src/CheckFilenameConsistency.h b/developer/src/kmcmplib/src/CheckFilenameConsistency.h deleted file mode 100644 index 39f86db391b1ac6b870d7a521f297372059bd9b4..0000000000000000000000000000000000000000 --- a/developer/src/kmcmplib/src/CheckFilenameConsistency.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include "compfile.h" -#include "kmcmplib.h" - -KMX_DWORD CheckFilenameConsistencyForCalls(PFILE_KEYBOARD fk); -KMX_DWORD CheckFilenameConsistency(KMX_CHAR const * Filename, bool ReportMissingFile); -KMX_DWORD CheckFilenameConsistency(KMX_WCHAR const * Filename, bool ReportMissingFile); - -bool IsRelativePath(KMX_CHAR const * p); -bool IsRelativePath(KMX_WCHAR const * p); diff --git a/developer/src/kmcmplib/src/CheckForDuplicates.cpp b/developer/src/kmcmplib/src/CheckForDuplicates.cpp index 71268a4db59ac9eac0ef8fcdeed64931e6ab5d86..56b6195fb42d83ae232edfaa06f11730623d840b 100644 --- a/developer/src/kmcmplib/src/CheckForDuplicates.cpp +++ b/developer/src/kmcmplib/src/CheckForDuplicates.cpp @@ -18,8 +18,7 @@ KMX_DWORD CheckForDuplicateGroup(PFILE_KEYBOARD fk, PFILE_GROUP gp) noexcept { continue; } if (u16icmp(gp0->szName, gp->szName) == 0) { - u16sprintf(ErrExtraW, 256, L" Group '%ls' declared on line %d", u16fmt(gp0->szName).c_str(), gp0->Line); - strcpy(ErrExtraLIB, string_from_u16string(ErrExtraW).c_str()); + snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, " Group '%s' declared on line %d", string_from_u16string(gp->szName).c_str(), gp0->Line); return CERR_DuplicateGroup; } } @@ -39,8 +38,7 @@ KMX_DWORD CheckForDuplicateStore(PFILE_KEYBOARD fk, PFILE_STORE sp) noexcept { continue; } if (u16icmp(sp0->szName, sp->szName) == 0) { - u16sprintf(ErrExtraW,256, L" Store '%ls' declared on line %d", u16fmt(sp0->szName).c_str(), sp0->line); - strcpy(ErrExtraLIB, string_from_u16string(ErrExtraW).c_str()); + snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, " Store '%s' declared on line %d", string_from_u16string(sp0->szName).c_str(), sp0->line); return CERR_DuplicateStore; } } diff --git a/developer/src/kmcmplib/src/CompMsg.cpp b/developer/src/kmcmplib/src/CompMsg.cpp index 2fa0d68322dc1217207bb8541a62e304fee40f76..3e2e894d3cf00a36d3e3f5ce7b943d475f354e01 100644 --- a/developer/src/kmcmplib/src/CompMsg.cpp +++ b/developer/src/kmcmplib/src/CompMsg.cpp @@ -111,8 +111,8 @@ const struct CompilerError CompilerErrors[] = { { CERR_DuplicateStore , "A store with this name has already been defined."}, { CERR_RepeatedBegin , "Begin has already been set"}, - { CHINT_FilenameHasDifferingCase , "Casing differences may fail on some platforms: "}, { CHINT_UnreachableRule , "This rule will never be matched as another rule takes precedence"}, + { CHINT_NonUnicodeFile , "Keyman Developer has detected that the file has ANSI encoding. Consider converting this file to UTF-8"}, { CWARN_TooManyWarnings , "Too many warnings or errors"}, { CWARN_OldVersion , "The keyboard file is an old version"}, @@ -142,7 +142,6 @@ const struct CompilerError CompilerErrors[] = { { CWARN_NulNotFirstStatementInContext , "nul must be the first statement in the context"}, { CWARN_IfShouldBeAtStartOfContext , "if, platform and baselayout should be at start of context (after nul, if present)"}, { CWARN_KeyShouldIncludeNCaps , "Other rules which reference this key include CAPS or NCAPS modifiers, so this rule must include NCAPS modifier to avoid inconsistent matches"}, - { CWARN_MissingFile , "The referenced file could not be found: "}, { 0, nullptr } }; diff --git a/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e90938f18a735ffca4ab986ee53004bb3703a62a --- /dev/null +++ b/developer/src/kmcmplib/src/CompileKeyboardBuffer.cpp @@ -0,0 +1,160 @@ +#include "pch.h" +#include +#include "kmcmplib.h" +#include "CheckNCapsConsistency.h" +#include "DeprecationChecks.h" +#include "versioning.h" +#include "CompileKeyboardBuffer.h" +#include "../../../../common/windows/cpp/include/keymanversion.h" + +bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk) +{ + PKMX_WCHAR str, p; + + KMX_DWORD msg; + + kmcmp::FMnemonicLayout = FALSE; + + if (!fk) { + AddCompileError(CERR_SomewhereIGotItWrong); + return FALSE; + } + + str = new KMX_WCHAR[LINESIZE]; + if (!str) { + AddCompileError(CERR_CannotAllocateMemory); + return FALSE; + } + + fk->KeyboardID = 0; + fk->version = 0; + fk->dpStoreArray = NULL; + fk->dpGroupArray = NULL; + fk->cxStoreArray = 0; + fk->cxGroupArray = 0; + fk->StartGroup[0] = fk->StartGroup[1] = -1; + fk->szName[0] = 0; + fk->szCopyright[0] = 0; + fk->dwFlags = KF_AUTOMATICVERSION; + fk->currentGroup = 0xFFFFFFFF; + fk->currentStore = 0; + fk->cxDeadKeyArray = 0; + fk->dpDeadKeyArray = NULL; + fk->cxVKDictionary = 0; // I3438 + fk->dpVKDictionary = NULL; // I3438 + fk->extra->kvksFilename = u""; +/* fk->szMessage[0] = 0; + fk->szLanguageName[0] = 0;*/ + fk->dwBitmapSize = 0; + fk->dwHotKey = 0; + + kmcmp::BeginLine[BEGIN_ANSI] = -1; + kmcmp::BeginLine[BEGIN_UNICODE] = -1; + kmcmp::BeginLine[BEGIN_NEWCONTEXT] = -1; + kmcmp::BeginLine[BEGIN_POSTKEYSTROKE] = -1; + + + /* Add a store for the Keyman 6.0 copyright information string */ + + if(kmcmp::FShouldAddCompilerVersion) { + u16sprintf(str,LINESIZE, L"Created with Keyman Developer version %d.%d.%d.%d", KEYMAN_VersionMajor, KEYMAN_VersionMinor, KEYMAN_VersionPatch, 0); + AddStore(fk, TSS_KEYMANCOPYRIGHT, str); + } + + /* Add a system store for the Keyman edition number */ + AddStore(fk, TSS_CUSTOMKEYMANEDITION, u"0"); + AddStore(fk, TSS_CUSTOMKEYMANEDITIONNAME, u"Keyman"); + + int offset = 0; + + // must preprocess for group and store names -> this isn't really necessary, but never mind! + while ((msg = ReadLine(infile, sz, offset, str, TRUE)) == CERR_None) + { + p = str; + switch (LineTokenType(&p)) + { + case T_VERSION: + *(p + 4) = 0; + if ((msg = AddStore(fk, TSS_VERSION, p)) != CERR_None) { + AddCompileError(msg); + return FALSE; + } + break; + + case T_GROUP: + if ((msg = ProcessGroupLine(fk, p)) != CERR_None) { + AddCompileError(msg); + return FALSE; + } + break; + + case T_STORE: + if ((msg = ProcessStoreLine(fk, p)) != CERR_None) { + AddCompileError(msg); + return FALSE; + } + break; + + default: + break; + } + } + + if (msg != CERR_EndOfFile) { + AddCompileError(msg); + return FALSE; + } + + offset = 0; + kmcmp::currentLine = 0; + + /* Reindex the list of codeconstants after stores added */ + + kmcmp::CodeConstants->reindex(); + + /* ReadLine will automatically skip over $Keyman lines, and parse wrapped lines */ + while ((msg = ReadLine(infile, sz, offset, str, FALSE)) == CERR_None) + { + msg = ParseLine(fk, str); + if (msg != CERR_None) { + AddCompileError(msg); + return FALSE; + } + } + + if (msg != CERR_EndOfFile) { + AddCompileError(msg); + return FALSE; + } + + ProcessGroupFinish(fk); + + if (kmcmp::FSaveDebug) kmcmp::RecordDeadkeyNames(fk); + + /* Add the compiler version as a system store */ + if ((msg = kmcmp::AddCompilerVersionStore(fk)) != CERR_None) { + AddCompileError(msg); + return FALSE; + } + + if ((msg = BuildVKDictionary(fk)) != CERR_None) { + AddCompileError(msg); + return FALSE; + } + + delete str; + + if (!kmcmp::CheckKeyboardFinalVersion(fk)) { + return FALSE; + } + + /* Warn on inconsistent use of NCAPS */ + if (!kmcmp::FMnemonicLayout) { + CheckNCapsConsistency(fk); + } + + /* Flag presence of deprecated features */ + kmcmp::CheckForDeprecatedFeatures(fk); + + return TRUE; +} diff --git a/developer/src/kmcmplib/src/CompileKeyboardBuffer.h b/developer/src/kmcmplib/src/CompileKeyboardBuffer.h new file mode 100644 index 0000000000000000000000000000000000000000..78bac6665399d721b52baf563c62de3732a570fb --- /dev/null +++ b/developer/src/kmcmplib/src/CompileKeyboardBuffer.h @@ -0,0 +1,5 @@ +#pragma once + +#include "compfile.h" + +bool CompileKeyboardBuffer(KMX_BYTE* infile, int sz, PFILE_KEYBOARD fk); diff --git a/developer/src/kmcmplib/src/Compiler.cpp b/developer/src/kmcmplib/src/Compiler.cpp index 06e707e7ea1153f7738f3a1d8b52b94931a5a29c..f9fa44ee10ad2e92d77348a6bed2c5c5f3ac9245 100644 --- a/developer/src/kmcmplib/src/Compiler.cpp +++ b/developer/src/kmcmplib/src/Compiler.cpp @@ -75,7 +75,7 @@ #include "versioning.h" #include "kmcmplib.h" #include "DeprecationChecks.h" - +#include "cp1252.h" #include "virtualcharkeys.h" // TODO: These three should be under common/cpp/include -- not windows specific @@ -94,11 +94,12 @@ #include "CasedKeys.h" #include #include -#include "CheckFilenameConsistency.h" +#include +#include + #include "UnreachableRules.h" #include "CheckForDuplicates.h" #include "kmx_u16.h" -#include "filesystem.h" #include /* These macros are adapted from winnt.h and legacy use only */ @@ -110,8 +111,7 @@ using namespace kmcmp; - char ErrExtraLIB[ERR_EXTRA_LIB_LEN]; - KMX_WCHAR ErrExtraW[ERR_EXTRA_W_LEN]; + char ErrExtraLIB[ERR_EXTRA_LIB_LEN]; // utf-8 KMX_BOOL AWarnDeprecatedCode_GLOBAL_LIB; namespace kmcmp{ @@ -122,7 +122,6 @@ namespace kmcmp{ KMX_BOOL FMnemonicLayout = FALSE; KMX_BOOL FOldCharPosMatching = FALSE; int CompileTarget; - KMX_CHAR CompileDir[260]; // TODO: this should not be a fixed buffer int BeginLine[4]; KMX_BOOL IsValidCallStore(PFILE_STORE fs); @@ -230,6 +229,8 @@ enum LinePrefixType { lptNone, lptKeymanAndKeymanWeb, lptKeymanWebOnly, lptKeyma /* Compile target */ kmcmp_CompilerMessageProc msgproc = NULL; +kmcmp_LoadFileProc loadfileproc = NULL; + void* msgprocContext = NULL; int kmcmp::currentLine = 0; @@ -784,7 +785,7 @@ KMX_DWORD ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) return CheckForDuplicateStore(fk, sp); } -KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, KMX_WCHAR const * str, KMX_DWORD *dwStoreID) +KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID) { PFILE_STORE sp; sp = new FILE_STORE[fk->cxStoreArray + 1]; @@ -862,7 +863,6 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE int i, j; KMX_DWORD msg; PKMX_WCHAR p, q; - KMX_CHAR *pp; if (!pssBuf) pssBuf = new KMX_WCHAR[GLOBAL_BUFSIZE]; PKMX_WCHAR buf = pssBuf; @@ -912,13 +912,9 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE case TSS_INCLUDECODES: VERIFY_KEYBOARD_VERSION(fk, VERSION_60, CERR_60FeatureOnly_NamedCodes); - pp = wstrtostr(sp->dpString); - if (!kmcmp::CodeConstants->LoadFile(pp)) - { - delete[] pp; + if (!kmcmp::CodeConstants->LoadFile(fk, sp->dpString)) { return CERR_CannotLoadIncludeFile; } - delete[] pp; kmcmp::CodeConstants->reindex(); // I4982 break; @@ -1023,6 +1019,9 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE case TSS_VISUALKEYBOARD: VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); { + // Store extra metadata for callers as we mutate this store during + // compilation + fk->extra->kvksFilename = sp->dpString; // Strip path from the store, leaving bare filename only p = sp->dpString; @@ -1044,10 +1043,6 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE delete[] sp->dpString; sp->dpString = q; - - if ((msg = CheckFilenameConsistency( (sp->dpString), FALSE)) != CERR_None) { - return msg; - } } break; case TSS_KMW_RTL: @@ -1058,16 +1053,10 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE case TSS_KMW_HELPFILE: case TSS_KMW_EMBEDJS: VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); - if ((msg = CheckFilenameConsistency(sp->dpString, FALSE)) != CERR_None) { - return msg; - } break; case TSS_KMW_EMBEDCSS: VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyEmbedCSS); - if ((msg = CheckFilenameConsistency(sp->dpString, FALSE)) != CERR_None) { - return msg; - } break; case TSS_TARGETS: // I4504 @@ -1117,9 +1106,6 @@ KMX_DWORD ProcessSystemStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, PFILE_STORE case TSS_LAYOUTFILE: // I3483 VERIFY_KEYBOARD_VERSION(fk, VERSION_90, CERR_90FeatureOnlyLayoutFile); // I4140 - if ((msg = CheckFilenameConsistency(sp->dpString, FALSE)) != CERR_None) { - return msg; - } // Used by KMW compiler break; @@ -2888,7 +2874,7 @@ KMX_BOOL kmcmp::CheckStoreUsage(PFILE_KEYBOARD fk, int storeIndex, KMX_BOOL fIsS return TRUE; } -KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, FILE* fp_out) +KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataSize) { PFILE_GROUP fgp; PFILE_STORE fsp; @@ -2903,6 +2889,9 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, FILE* fp_out) size_t size; KMX_DWORD i, j; + *data = nullptr; + dataSize = 0; + // Calculate how much memory to allocate size = sizeof(COMP_KEYBOARD) + @@ -3056,20 +3045,13 @@ KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, FILE* fp_out) SetChecksum(buf, &ck->dwCheckSum, (KMX_DWORD)size); - KMX_DWORD dwBytesWritten = 0; - dwBytesWritten = (KMX_DWORD)fwrite(buf,1, (KMX_DWORD)size , fp_out); - - if (dwBytesWritten != size) { - delete[] buf; - return CERR_UnableToWriteFully; - } - - delete[] buf; + *data = buf; + dataSize = size; return CERR_None; } -KMX_DWORD ReadLine(FILE* fp_in , PKMX_WCHAR wstr, KMX_BOOL PreProcess) +KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_BOOL PreProcess) { KMX_DWORD len; PKMX_WCHAR p; @@ -3077,21 +3059,26 @@ KMX_DWORD ReadLine(FILE* fp_in , PKMX_WCHAR wstr, KMX_BOOL PreProcess) KMX_DWORD n; KMX_WCHAR currentQuotes = 0; KMX_WCHAR str[LINESIZE + 3]; - len = (KMX_DWORD)fread( str , 1 ,LINESIZE * 2,fp_in); - if (ferror(fp_in) ) return CERR_CannotReadInfile; - len /= 2; - str[len] = 0; auto cur = ftell(fp_in); - fseek(fp_in, 0, SEEK_END); - auto fsize = ftell(fp_in); - fseek(fp_in, cur, SEEK_SET); - if (cur == fsize) + if(offset >= sz) { + return CERR_EndOfFile; + } + + len = offset + LINESIZE*2 > sz ? sz-offset : LINESIZE*2; + memcpy(str, infile+offset, len); + offset += len; + len /= 2; + str[len] = 0; - // \r\n is still added here even though Linux doesn`t use \r. - // This is to ensure to still have a working windows-only-version - u16ncat(str, u"\r\n", _countof(str)); // I3481 // Always a "\r\n" to the EOF, avoids funny bugs + if(offset == sz) { + // \r\n is still added here even though Linux doesn`t use \r. + // This is to ensure to still have a working windows-only-version + u16ncat(str, u"\r\n", _countof(str)); // I3481 // Always a "\r\n" to the EOF, avoids funny bugs + } - if (len == 0) return CERR_EndOfFile; + if (len == 0) { + return CERR_EndOfFile; + } // neccessary to add this block for using on non-windows platforms (removes all \r for platforms that use \n instead of \r\n) for (p = str, n = 0; n < len; n++, p++) { @@ -3173,9 +3160,13 @@ KMX_DWORD ReadLine(FILE* fp_in , PKMX_WCHAR wstr, KMX_BOOL PreProcess) return (PreProcess ? CERR_None : CERR_LineTooLong); } - if (*p == L'\n') kmcmp::currentLine++; + kmcmp::currentLine++; - fseek(fp_in, -(int)(len * 2 - (int)(p - str) * 2 - 2), SEEK_CUR); + offset -= (int)(len * 2 - (int)(p - str) * 2 - 2); + if(offset >= sz) { + // If we've appended a \n, we can go past EOF + offset = sz; + } p--; while (p >= str && iswspace(*p)) p--; @@ -3221,58 +3212,38 @@ KMX_BOOL IsSameToken(PKMX_WCHAR *p, KMX_WCHAR const * token) return FALSE; } -KMX_DWORD ImportBitmapFile(PFILE_KEYBOARD fk, PKMX_WCHAR szName, PKMX_DWORD FileSize, PKMX_BYTE *Buf) +static bool endsWith(const std::string& str, const std::string& suffix) { - FILE *fp; - KMX_WCHAR szNewName[260]; - - if (IsRelativePath(szName)) - { - PKMX_WCHAR WCompileDir = strtowstr(kmcmp::CompileDir); - u16ncpy(szNewName, WCompileDir, _countof(szNewName)); // I3481 - u16ncat(szNewName,szName, _countof(szNewName )); // I3481 - } - else - u16ncpy(szNewName, szName, _countof(szNewName)); // I3481 - - fp=Open_File(szNewName, u"rb"); - - if ( fp == NULL) - { - // else if filename.bmp is not in the folder -> attempt to open filename.bmp.bmp ! - if ( u16cmp(szNewName+u16len(szNewName)-4, u".bmp") ) - u16ncat(szNewName, u".bmp", _countof(szNewName)); // I3481 + return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix); +} - fp= Open_File(szNewName, u"rb"); +KMX_DWORD ImportBitmapFile(PFILE_KEYBOARD fk, PKMX_WCHAR szName, PKMX_DWORD FileSize, PKMX_BYTE *Buf) +{ + auto szNameUtf8 = string_from_u16string(szName); - if ( fp == NULL) + if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), nullptr, (int*) FileSize, msgprocContext)) { + // Append .bmp and try again + if(endsWith(szNameUtf8, ".bmp")) { return CERR_CannotReadBitmapFile; + } + szNameUtf8.append(".bmp"); + if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), nullptr, (int*) FileSize, msgprocContext)) { + return CERR_CannotReadBitmapFile; + } } - KMX_DWORD msg; - if ((msg = CheckFilenameConsistency(szNewName, FALSE)) != CERR_None) { - return msg; - } - - fseek(fp, 0, SEEK_END); - *FileSize = (KMX_DWORD)ftell(fp); - fseek(fp ,0,SEEK_SET); - if (*FileSize < 0) { - fclose(fp); + if(*FileSize < 2) { + // Zero-byte file is invalid; 2 byte file is too, but we only really care + // about the prolog at this point so we don't overrun our buffer return CERR_CannotReadBitmapFile; } - if (*FileSize < 2) return CERR_CannotReadBitmapFile; *Buf = new KMX_BYTE[*FileSize]; - - if (fread(*Buf, 1, *FileSize, fp) < (size_t) *FileSize) { - delete[] * Buf; - *Buf = NULL; + if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), *Buf, (int*) FileSize, msgprocContext)) { + delete[] *Buf; return CERR_CannotReadBitmapFile; } - fclose(fp); - /* Test for version 7.0 icon support */ if (*((PKMX_CHAR)*Buf) != 'B' && *(((PKMX_CHAR)*Buf) + 1) != 'M') { VERIFY_KEYBOARD_VERSION(fk, VERSION_70, CERR_70FeatureOnly); @@ -3428,81 +3399,41 @@ KMX_BOOL kmcmp::IsValidCallStore(PFILE_STORE fs) return i == 1; } -FILE* CreateTempFile() -{ - return tmpfile(); -} - /////////////////// -FILE* UTF16TempFromUTF8(FILE* fp_in , KMX_BOOL hasPreamble) -{ - FILE *fp_out = CreateTempFile(); - if(fp_out == NULL) // I3228 // I3510 - { - fclose(fp_in); - return NULL; //return INVALID_HANDLE_VALUE; _S2 can I exchange that? +bool hasPreamble(std::u16string result) { + return result.size() > 0 && result[0] == 0xFEFF; +} + +bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16) { + if(sz == 0) { + return FALSE; } - PKMX_BYTE buf, p; - PKMX_WCHAR outbuf, poutbuf; - KMX_DWORD len; - KMX_DWORD len2; - KMX_WCHAR prolog = 0xFEFF; - fwrite(&prolog,2, 1, fp_out); - - fseek(fp_in, 0, SEEK_END); - len = (KMX_DWORD)ftell(fp_in); - fseek(fp_in, 0, SEEK_SET); - if (hasPreamble) { - fseek( fp_in,3,SEEK_SET); // Cut off UTF-8 marker - len -= 3; - } - - buf = new KMX_BYTE[len + 1]; // null terminated - outbuf = new KMX_WCHAR[len + 1]; - - len2= (KMX_DWORD)fread(buf,1,len,fp_in); - if (len2) { - buf[len2] = 0; - p = buf; - poutbuf = outbuf; - if (hasPreamble) { - // We have a preamble, so we attempt to read as UTF-8 and allow conversion errors to be filtered. This is not great for a - // compiler but matches existing behaviour -- in future versions we may not do lenient conversion. - ConvertUTF8toUTF16(&p, &buf[len2], (UTF16 **)&poutbuf, (const UTF16 *)&outbuf[len], lenientConversion); - fwrite(outbuf, (KMX_DWORD)(poutbuf - outbuf) * 2 , 1, fp_out); - } - else { - // No preamble, so we attempt to read as strict UTF-8 and fall back to ANSI if that fails - ConversionResult cr = ConvertUTF8toUTF16(&p, &buf[len2], (UTF16 **)&poutbuf, (const UTF16 *)&outbuf[len], strictConversion); - if (cr == sourceIllegal) { - // Not a valid UTF-8 file, so fall back to ANSI - // AddCompileError(CHINT_NonUnicodeFile); - // note, while this message is defined, for now we will not emit it - // because we don't support HINT/INFO messages yet and we don't want - // this to cause a blocking compile at this stage - // do strtowstr only when no invalid characters are found - if( p==0){ - poutbuf = strtowstr((PKMX_STR)buf); - fwrite(poutbuf, (KMX_DWORD)u16len(poutbuf) * 2 , 1, fp_out); - delete[] poutbuf; - } - else - AddCompileError(CERR_InvalidCharacter); - } + std::u16string result; - else { - fwrite(outbuf, (KMX_DWORD)(poutbuf - outbuf) * 2 , 1, fp_out); - } + try { + std::wstring_convert, char16_t> converter; + result = converter.from_bytes((char*)infile, (char*)infile+sz); + } catch(std::range_error e) { + AddCompileError(CHINT_NonUnicodeFile); + result.resize(sz); + for(int i = 0; i < sz; i++) { + result[i] = CP1252_UNICODE[infile[i]]; } } - fclose( fp_in); - delete[] buf; - delete[] outbuf; - fseek( fp_out,2,SEEK_SET); - return fp_out; + if(hasPreamble(result)) { + *sz16 = result.size() * 2 - 2; + *tempfile = new KMX_BYTE[*sz16]; + memcpy(*tempfile, result.c_str() + 1, *sz16); + } else { + *sz16 = result.size() * 2; + *tempfile = new KMX_BYTE[*sz16]; + memcpy(*tempfile, result.c_str(), *sz16); + } + + return TRUE; } PFILE_STORE FindSystemStore(PFILE_KEYBOARD fk, KMX_DWORD dwSystemID) diff --git a/developer/src/kmcmplib/src/CompilerInterfaces.cpp b/developer/src/kmcmplib/src/CompilerInterfaces.cpp index 89c4655d16e4a04e42f1cfb86551e31f87abd3eb..fdfbdf19613cc26d3de1161ca47e2dfdeb9c7bd7 100644 --- a/developer/src/kmcmplib/src/CompilerInterfaces.cpp +++ b/developer/src/kmcmplib/src/CompilerInterfaces.cpp @@ -1,377 +1,110 @@ #include "pch.h" - #include #include #include "kmcmplib.h" -#include "filesystem.h" -#include "CheckFilenameConsistency.h" -#include "CheckNCapsConsistency.h" -#include "DeprecationChecks.h" -#include "versioning.h" #include "../../../../common/windows/cpp/include/ConvertUTF.h" -#include "../../../../common/windows/cpp/include/keymanversion.h" - -#define SetError(err) { if(AddCompileError(err) || (err & CERR_FATAL)) return FALSE; } - -bool CompileKeyboardHandle(FILE* fp_in, PFILE_KEYBOARD fk); - -EXTERN bool kmcmp_SetCompilerOptions(KMCMP_COMPILER_OPTIONS* options) { - //printf("°°-> changed to SetCompilerOptions() of kmcmplib \n"); - if(!options || options->dwSize < sizeof(KMCMP_COMPILER_OPTIONS)) { - return FALSE; - } - kmcmp::FShouldAddCompilerVersion = options->ShouldAddCompilerVersion; - return TRUE; -} - -#ifdef __EMSCRIPTEN__ - -/* - WASM interface for compiler message callback -*/ -EM_JS(int, wasm_msgproc, (int line, int msgcode, char* text, char* context), { - const proc = globalThis[UTF8ToString(context)]; - if(!proc || typeof proc != 'function') { - console.log(`[${line}: ${msgcode}: ${UTF8ToString(text)}]`); - return 0; - } else { - return proc(line, msgcode, UTF8ToString(text)); - } -}); - -int wasm_CompilerMessageProc(int line, uint32_t dwMsgCode, char* szText, void* context) { - char* msgProc = static_cast(context); - return wasm_msgproc(line, dwMsgCode, szText, msgProc); -} - -EXTERN bool kmcmp_Wasm_SetCompilerOptions(int ShouldAddCompilerVersion) { - KMCMP_COMPILER_OPTIONS options; - options.dwSize = sizeof(KMCMP_COMPILER_OPTIONS); - options.ShouldAddCompilerVersion = ShouldAddCompilerVersion; - return kmcmp_SetCompilerOptions(&options); -} - -EXTERN bool kmcmp_Wasm_CompileKeyboardFile(char* pszInfile, - char* pszOutfile, int ASaveDebug, int ACompilerWarningsAsErrors, - int AWarnDeprecatedCode, char* msgProc -) { - return kmcmp_CompileKeyboardFile( - pszInfile, - pszOutfile, - ASaveDebug, - ACompilerWarningsAsErrors, - AWarnDeprecatedCode, - wasm_CompilerMessageProc, - msgProc - ); -} - -EXTERN int kmcmp_Wasm_ParseUnicodeSet(char* pat, - uint32_t* buf, int length -) { - return kmcmp_ParseUnicodeSet( - pat, buf, length - ); -} - -#endif - -EXTERN bool kmcmp_CompileKeyboardFile(char* pszInfile, - char* pszOutfile, bool ASaveDebug, bool ACompilerWarningsAsErrors, - bool AWarnDeprecatedCode, kmcmp_CompilerMessageProc pMsgproc, void* AmsgprocContext +#include "CompileKeyboardBuffer.h" + +EXTERN bool kmcmp_CompileKeyboard( + const char* pszInfile, + const KMCMP_COMPILER_OPTIONS& options, + kmcmp_CompilerMessageProc messageProc, + kmcmp_LoadFileProc loadFileProc, + const void* procContext, + KMCMP_COMPILER_RESULT& result ) { - FILE* fp_in = NULL; - FILE* fp_out = NULL; - KMX_CHAR str[260]; - - //printf("°°-> changed to CompileKeyboardFile() of kmcmplib \n"); - - kmcmp::FSaveDebug = ASaveDebug; - kmcmp::FCompilerWarningsAsErrors = ACompilerWarningsAsErrors; // I4865 - AWarnDeprecatedCode_GLOBAL_LIB = AWarnDeprecatedCode; - - kmcmp::CompileTarget = CKF_KEYMAN; - - if (!pMsgproc || !pszInfile || !pszOutfile) SetError(CERR_BadCallParams); - - PKMX_STR p; - - if ((p = strrchr_slash(pszInfile)) != nullptr) - { - strncpy(kmcmp::CompileDir, pszInfile, (int)(p - pszInfile + 1)); // I3481 - kmcmp::CompileDir[(int)(p - pszInfile + 1)] = 0; - } - else - kmcmp::CompileDir[0] = 0; - - msgproc = pMsgproc; - msgprocContext = AmsgprocContext; - kmcmp::currentLine = 0; - kmcmp::nErrors = 0; - - fp_in = Open_File(pszInfile, "rb"); - - if (fp_in == NULL) { - SetError(CERR_InfileNotExist); - } - - // Transfer the file to a memory stream for processing UTF-8 or ANSI to UTF-16? - // What about really large files? Transfer to a temp file... - if (!fread(str, 1, 3, fp_in)) { - fclose(fp_in); - SetError(CERR_CannotReadInfile); - } - - fseek(fp_in, 0, SEEK_SET); - if (str[0] == UTF8Sig[0] && str[1] == UTF8Sig[1] && str[2] == UTF8Sig[2]) - fp_in = UTF16TempFromUTF8(fp_in, TRUE); - else if (str[0] == UTF16Sig[0] && str[1] == UTF16Sig[1]) - fseek(fp_in, 2, SEEK_SET); - else - fp_in = UTF16TempFromUTF8(fp_in, FALSE); - if (fp_in == NULL) { - SetError(CERR_CannotCreateTempfile); - } - - fp_out = Open_File(pszOutfile, "wb"); - - if (fp_out == NULL) { - SetError(CERR_CannotCreateOutfile); - } FILE_KEYBOARD fk; - kmcmp::CodeConstants = new kmcmp::NamedCodeConstants; - bool result = CompileKeyboardHandle(fp_in, &fk); - if(result) { - KMX_DWORD msg; - if ((msg = WriteCompiledKeyboard(&fk, fp_out)) != CERR_None) { - result = FALSE; - AddCompileError(msg); - } - } else { - AddCompileError(CERR_InvalidValue); - } - - fclose(fp_in); - fclose(fp_out); + fk.extra = new FILE_KEYBOARD_EXTRA; + fk.extra->kmnFilename = pszInfile; - delete kmcmp::CodeConstants; + kmcmp::FSaveDebug = options.saveDebug; // I3681 + kmcmp::FCompilerWarningsAsErrors = options.compilerWarningsAsErrors; // I4865 + AWarnDeprecatedCode_GLOBAL_LIB = options.warnDeprecatedCode; + kmcmp::FShouldAddCompilerVersion = options.shouldAddCompilerVersion; + kmcmp::CompileTarget = options.target; - if (kmcmp::nErrors > 0) - { - remove(pszOutfile); + if (!messageProc || !loadFileProc || !pszInfile) { + AddCompileError(CERR_BadCallParams); return FALSE; } - return result; -} - - - -EXTERN bool kmcmp_CompileKeyboardFileToBuffer(char* pszInfile, void* pfkBuffer, bool ACompilerWarningsAsErrors, bool AWarnDeprecatedCode, - kmcmp_CompilerMessageProc pMsgproc, void* AmsgprocContext, int Target) // I4865 // I4866 -{ - //printf("°°-> changed to CompileKeyboardFileToBuffer() of kmcmplib \n"); - FILE* fp_in = NULL; - KMX_CHAR str[260]; - - kmcmp::FSaveDebug = TRUE; // I3681 - kmcmp::FCompilerWarningsAsErrors = ACompilerWarningsAsErrors; // I4865 - AWarnDeprecatedCode_GLOBAL_LIB = AWarnDeprecatedCode; - kmcmp::CompileTarget = Target; - - if (!pMsgproc || !pszInfile || !pfkBuffer) { - SetError(CERR_BadCallParams); - } - - PKMX_STR p; - - if ((p = strrchr_slash(pszInfile)) != nullptr) { - strncpy(kmcmp::CompileDir, pszInfile, (int)(p - pszInfile + 1)); // I3481 - kmcmp::CompileDir[(int)(p - pszInfile + 1)] = 0; - } - else { - kmcmp::CompileDir[0] = 0; - } - - msgproc = pMsgproc; - msgprocContext = AmsgprocContext; + msgproc = messageProc; + loadfileproc = loadFileProc; + msgprocContext = (void*)procContext; kmcmp::currentLine = 0; kmcmp::nErrors = 0; - fp_in = Open_File(pszInfile,"rb"); - - if (fp_in == NULL) { - SetError(CERR_InfileNotExist); - } - - // Transfer the file to a memory stream for processing UTF-8 or ANSI to UTF-16? - // What about really large files? Transfer to a temp file... - - if( !fread(str,1,3,fp_in)) - { - fclose(fp_in); - SetError(CERR_CannotReadInfile); - } - - fseek( fp_in,0,SEEK_SET); - if (str[0] == UTF8Sig[0] && str[1] == UTF8Sig[1] && str[2] == UTF8Sig[2]) - fp_in = UTF16TempFromUTF8(fp_in, TRUE); - else if (str[0] == UTF16Sig[0] && str[1] == UTF16Sig[1]) - fseek( fp_in,2,SEEK_SET); - else - fp_in = UTF16TempFromUTF8(fp_in, FALSE); - - kmcmp::CodeConstants = new kmcmp::NamedCodeConstants; - - bool result = CompileKeyboardHandle(fp_in, static_cast(pfkBuffer)); - delete kmcmp::CodeConstants; - fclose(fp_in); - - if (kmcmp::nErrors > 0) { + int sz; + if(!loadFileProc(pszInfile, "", nullptr, &sz, msgprocContext)) { + AddCompileError(CERR_InfileNotExist); return FALSE; } - return result; -} - -bool CompileKeyboardHandle(FILE* fp_in, PFILE_KEYBOARD fk) -{ - PKMX_WCHAR str, p; - - KMX_DWORD msg; - - kmcmp::FMnemonicLayout = FALSE; - - if (!fk) { - SetError(CERR_SomewhereIGotItWrong); - } - - str = new KMX_WCHAR[LINESIZE]; - if (!str) { - SetError(CERR_CannotAllocateMemory); - } - - fk->KeyboardID = 0; - fk->version = 0; - fk->dpStoreArray = NULL; - fk->dpGroupArray = NULL; - fk->cxStoreArray = 0; - fk->cxGroupArray = 0; - fk->StartGroup[0] = fk->StartGroup[1] = -1; - fk->szName[0] = 0; - fk->szCopyright[0] = 0; - fk->dwFlags = KF_AUTOMATICVERSION; - fk->currentGroup = 0xFFFFFFFF; - fk->currentStore = 0; - fk->cxDeadKeyArray = 0; - fk->dpDeadKeyArray = NULL; - fk->cxVKDictionary = 0; // I3438 - fk->dpVKDictionary = NULL; // I3438 - -/* fk->szMessage[0] = 0; - fk->szLanguageName[0] = 0;*/ - fk->dwBitmapSize = 0; - fk->dwHotKey = 0; - - kmcmp::BeginLine[BEGIN_ANSI] = -1; - kmcmp::BeginLine[BEGIN_UNICODE] = -1; - kmcmp::BeginLine[BEGIN_NEWCONTEXT] = -1; - kmcmp::BeginLine[BEGIN_POSTKEYSTROKE] = -1; - - /* Add a store for the Keyman 6.0 copyright information string */ - - if(kmcmp::FShouldAddCompilerVersion) { - u16sprintf(str,LINESIZE, L"Created with Keyman Developer version %d.%d.%d.%d", KEYMAN_VersionMajor, KEYMAN_VersionMinor, KEYMAN_VersionPatch, 0); - AddStore(fk, TSS_KEYMANCOPYRIGHT, str); + if(sz < 3) { + // Technically, a 3 byte file can never be a valid .kmn, so we can shortcut + // here and avoid testing outside memory bounds for looking at BOM + AddCompileError(CERR_CannotReadInfile); + return FALSE; } - /* Add a system store for the Keyman edition number */ - u16sprintf(str, LINESIZE, L"%d", 0); // I3481 - AddStore(fk, TSS_CUSTOMKEYMANEDITION, str); - - PKMX_WCHAR tbuf = strtowstr((KMX_CHAR*) "Keyman"); - AddStore(fk, TSS_CUSTOMKEYMANEDITIONNAME, tbuf); - delete tbuf; - - // must preprocess for group and store names -> this isn't really necessary, but never mind! - while ((msg = ReadLine(fp_in, str, TRUE)) == CERR_None) - { - p = str; - switch (LineTokenType(&p)) - { - case T_VERSION: - *(p + 4) = 0; - if ((msg = AddStore(fk, TSS_VERSION, p)) != CERR_None) SetError(msg); - break; - - case T_GROUP: - if ((msg = ProcessGroupLine(fk, p)) != CERR_None) SetError(msg); - break; - - case T_STORE: - if ((msg = ProcessStoreLine(fk, p)) != CERR_None) SetError(msg); - break; - - default: - break; - } + KMX_BYTE* infile = new KMX_BYTE[sz+1]; + if(!infile) { + AddCompileError(CERR_CannotAllocateMemory); + return FALSE; } - - if (msg != CERR_EndOfFile) { - SetError(msg); + if(!loadFileProc(pszInfile, "", infile, &sz, msgprocContext)) { + delete[] infile; + AddCompileError(CERR_CannotReadInfile); + return FALSE; } + infile[sz] = 0; // zero-terminate for safety, not technically needed but helps avoid memory bugs - fseek( fp_in,2,SEEK_SET); - kmcmp::currentLine = 0; - - /* Reindex the list of codeconstants after stores added */ - - kmcmp::CodeConstants->reindex(); - - /* ReadLine will automatically skip over $Keyman lines, and parse wrapped lines */ - while ((msg = ReadLine(fp_in, str, FALSE)) == CERR_None) - { - msg = ParseLine(fk, str); - if (msg != CERR_None) { - SetError(msg); + int offset = 0; + if(infile[0] == (KMX_BYTE) UTF16Sig[0] && infile[1] == (KMX_BYTE) UTF16Sig[1]) { + // UTF-16 source file + offset = 2; + } else { + // UTF-8 source file + KMX_BYTE* infile16; + int sz16; + if(!UTF16TempFromUTF8(infile, sz, &infile16, &sz16)) { + delete[] infile; + AddCompileError(CERR_CannotCreateTempfile); + return FALSE; } + delete[] infile; + infile = infile16; + sz = sz16; } - if (msg != CERR_EndOfFile) { - SetError(msg); - } - - ProcessGroupFinish(fk); - - if (kmcmp::FSaveDebug) kmcmp::RecordDeadkeyNames(fk); + kmcmp::CodeConstants = new kmcmp::NamedCodeConstants; + bool success = CompileKeyboardBuffer(infile+offset, sz-offset, &fk); + delete kmcmp::CodeConstants; - /* Add the compiler version as a system store */ - if ((msg = kmcmp::AddCompilerVersionStore(fk)) != CERR_None) { - SetError(msg); - } + delete[] infile; - if ((msg = BuildVKDictionary(fk)) != CERR_None) { - SetError(msg); // I3438 + if (kmcmp::nErrors > 0 || !success) { + return FALSE; } - if ((msg = CheckFilenameConsistencyForCalls(fk)) != CERR_None) { - SetError(msg); - } + // fill in result data + KMX_DWORD msg; + KMX_BYTE* data = nullptr; + size_t dataSize = 0; + msg = WriteCompiledKeyboard(&fk, &data, dataSize); - delete str; + //TODO: FreeKeyboardPointers(fk); - if (!kmcmp::CheckKeyboardFinalVersion(fk)) { + if(msg != CERR_None) { + AddCompileError(msg); return FALSE; } - /* Warn on inconsistent use of NCAPS */ - if (!kmcmp::FMnemonicLayout) { - CheckNCapsConsistency(fk); - } - - /* Flag presence of deprecated features */ - kmcmp::CheckForDeprecatedFeatures(fk); + result.kmx = data; + result.kmxSize = dataSize; + result.kvksFilename = string_from_u16string(fk.extra->kvksFilename); // convert to UTF8 return TRUE; } + diff --git a/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp new file mode 100644 index 0000000000000000000000000000000000000000..0aab9ba51d4abf4262595356feed46819d2c0c4a --- /dev/null +++ b/developer/src/kmcmplib/src/CompilerInterfacesWasm.cpp @@ -0,0 +1,117 @@ +#include "pch.h" +#include + +#ifdef __EMSCRIPTEN__ + +/* + WASM interface for compiler message callback +*/ +EM_JS(int, wasm_msgproc, (int line, int msgcode, const char* text, char* context), { + const proc = globalThis[UTF8ToString(context)].message; + if(!proc || typeof proc != 'function') { + console.log(`[${line}: ${msgcode}: ${UTF8ToString(text)}]`); + return 0; + } else { + return proc(line, msgcode, UTF8ToString(text)); + } +}); + +EM_JS(int, wasm_loadfileproc, (const char* filename, const char* baseFilename, void* buffer, int bufferSize, char* context), { + const proc = globalThis[UTF8ToString(context)].loadFile; + if(!proc || typeof proc != 'function') { + return 0; + } else { + if(buffer == 0) { + return proc(UTF8ToString(filename), UTF8ToString(baseFilename), 0, 0); + } else { + return proc(UTF8ToString(filename), UTF8ToString(baseFilename), buffer, bufferSize); + } + } +}); + +bool wasm_LoadFileProc(const char* filename, const char* baseFilename, void* buffer, int* bufferSize, void* context) { + char* msgProc = static_cast(context); + if(buffer == nullptr) { + *bufferSize = wasm_loadfileproc(filename, baseFilename, 0, 0, msgProc); + return *bufferSize != -1; + } else { + return wasm_loadfileproc(filename, baseFilename, buffer, *bufferSize, msgProc) == 1; + } +} + +int wasm_CompilerMessageProc(int line, uint32_t dwMsgCode, const char* szText, void* context) { + char* msgProc = static_cast(context); + return wasm_msgproc(line, dwMsgCode, szText, msgProc); +} + +struct WASM_COMPILER_INTERFACE { + std::string callbacksKey; // key of callbacks object on globalThis +}; + +struct WASM_COMPILER_RESULT { + bool result; + // Following are pointer offsets in heap + buffer size + int kmx; + int kmxSize; + // Following are compiler side-channel data, required for + // follow-on transform + std::string kvksFilename; + // TODO: additional data to be passed back +}; + +WASM_COMPILER_RESULT kmcmp_wasm_compile(std::string pszInfile, const KMCMP_COMPILER_OPTIONS options, const WASM_COMPILER_INTERFACE intf) { + WASM_COMPILER_RESULT r = {false}; + KMCMP_COMPILER_RESULT kr; + + r.kmx = 0; + r.kmxSize = 0; + r.kvksFilename = ""; + + r.result = kmcmp_CompileKeyboard( + pszInfile.c_str(), + options, + wasm_CompilerMessageProc, + wasm_LoadFileProc, + intf.callbacksKey.c_str(), + kr + ); + + if(r.result) { + // TODO: additional data as required by kmc_kmw + r.kmx = (int) kr.kmx; + r.kmxSize = (int) kr.kmxSize; + r.kvksFilename = kr.kvksFilename; + } + + return r; +} + +EMSCRIPTEN_BINDINGS(compiler_interface) { + + emscripten::class_("CompilerOptions") + .constructor<>() + .property("saveDebug", &KMCMP_COMPILER_OPTIONS::saveDebug) + .property("compilerWarningsAsErrors", &KMCMP_COMPILER_OPTIONS::compilerWarningsAsErrors) + .property("warnDeprecatedCode", &KMCMP_COMPILER_OPTIONS::warnDeprecatedCode) + .property("shouldAddCompilerVersion", &KMCMP_COMPILER_OPTIONS::shouldAddCompilerVersion) + .property("target", &KMCMP_COMPILER_OPTIONS::target) + ; + + emscripten::class_("CompilerInterface") + .constructor<>() + .property("callbacksKey", &WASM_COMPILER_INTERFACE::callbacksKey) + ; + + emscripten::class_("CompilerResult") + .constructor<>() + .property("result", &WASM_COMPILER_RESULT::result) + .property("kmx", &WASM_COMPILER_RESULT::kmx) + .property("kmxSize", &WASM_COMPILER_RESULT::kmxSize) + .property("kvksFilename", &WASM_COMPILER_RESULT::kvksFilename) + ; + + emscripten::function("kmcmp_compile", &kmcmp_wasm_compile); + emscripten::function("kmcmp_parseUnicodeSet", &kmcmp_parseUnicodeSet); +} + +#endif diff --git a/developer/src/kmcmplib/src/NamedCodeConstants.cpp b/developer/src/kmcmplib/src/NamedCodeConstants.cpp index e6b1ae6d70ce20846ba23d24e0ab0efd05d2120a..a8af8fa783c23ac87f211452266d1c85cb9b87c8 100644 --- a/developer/src/kmcmplib/src/NamedCodeConstants.cpp +++ b/developer/src/kmcmplib/src/NamedCodeConstants.cpp @@ -24,18 +24,14 @@ #include "pch.h" #include #include "NamedCodeConstants.h" -#include "CheckFilenameConsistency.h" #include #include "kmcompx.h" -#include "filesystem.h" using namespace kmcmp; int IsHangulSyllable(const KMX_WCHAR *codename, int *code); namespace kmcmp { - extern KMX_CHAR CompileDir[]; - int __cdecl sort_entries(const void *elem1, const void *elem2) { return u16icmp( @@ -117,85 +113,57 @@ char *kmc_strupr(char *s) { return s; } -KMX_BOOL NamedCodeConstants::IntLoadFile(const KMX_CHAR *filename) -{ +KMX_BOOL NamedCodeConstants::LoadFile(PFILE_KEYBOARD fk, const KMX_WCHAR *filename) { const int str_size = 256; - FILE *fp = NULL; - if (CheckFilenameConsistency(filename, FALSE) != 0) { + auto szNameUtf8 = string_from_u16string(filename); + + int FileSize; + KMX_BYTE* Buf; + if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), nullptr, &FileSize, msgprocContext)) { return FALSE; } - fp = Open_File(filename, "rt"); - if(fp == NULL) { - return FALSE; // I3481 + Buf = new KMX_BYTE[FileSize+1]; + if(!loadfileproc(szNameUtf8.c_str(), fk->extra->kmnFilename.c_str(), Buf, &FileSize, msgprocContext)) { + delete[] Buf; + return FALSE; } + Buf[FileSize] = 0; // zero-terminate for strtok - KMX_CHAR str[str_size], *p, *q, *context = NULL; - KMX_BOOL isEol , first = TRUE; + char* filetok; + char* filecontext; + filetok = strtok_r((char*)Buf, "\n", &filecontext); - while(fgets(str, str_size, fp)) - { - isEol = *(strchr(str, 0) - 1) == '\n'; - p = strtok_r(str, ";", &context); // I3481 - q = strtok_r(NULL, ";\n", &context); - if(p && q) - { - if(first && *p == (KMX_CHAR)0xEF && *(p+1) == (KMX_CHAR)0xBB && *(p+2) == (KMX_CHAR)0xBF) p += 3; // I3056 UTF-8 // I3512 - first = FALSE; + if(*filetok == (KMX_CHAR)0xEF && *(filetok+1) == (KMX_CHAR)0xBB && *(filetok+2) == (KMX_CHAR)0xBF) filetok += 3; // I3056 UTF-8 // I3512 + + while(filetok) { + KMX_CHAR str[str_size], *p, *q, *context = NULL; + + if(strlen(filetok) >= str_size) { + delete[] Buf; + // TODO chuck a wobbly + return FALSE; + } + strcpy(str, filetok); + p = strtok_r(str, ";\r", &context); // I3481 + q = strtok_r(nullptr, ";\r", &context); + if(p && q) { kmc_strupr(q); // I3481 // I3641 - long n = strtol(p, NULL, 16); + long n = strtol(p, nullptr, 16); if (*q != '<') { PKMX_WCHAR q0 = strtowstr(q); AddCode_IncludedCodes((int)n, q0); delete[] q0; } } - if(!isEol ) - { - while(fgets(str, str_size, fp)) if(*(strchr(str, 0)-1) == '\n') break; - } + filetok = strtok_r(nullptr, "\n", &filecontext); } - fclose(fp); - - return TRUE; -} - -KMX_BOOL NamedCodeConstants::LoadFile(const KMX_CHAR *filename) -{ - const int buf_size = 260; - KMX_CHAR buf[buf_size]; - // Look in current directory first -- REMOVED AS DANGEROUS - /* strncpy(buf, filename, (buf_size-1)); buf[buf_size-1] = 0; // I3481 - if(kmcmp_FileExists(buf)) - return IntLoadFile(buf); - */ - // Then look in keyboard file directory (CompileDir) - strncpy(buf, CompileDir, (buf_size-1)); buf[buf_size-1] = 0; // I3481 - strncat(buf, filename, (buf_size-1)-strlen(CompileDir)); buf[buf_size-1] = 0; - if(kmcmp_FileExists(buf)) - return IntLoadFile(buf); - - //TODO: sort out how to find common includes in non-Windows platforms: - #ifdef _WINDOWS_ - // Finally look in kmcmpdll.dll directory - GetModuleFileName(0, buf, buf_size); - - KMX_CHAR *p = strrchr_slash(buf); - if(p) - p++; - else - p = buf; - *p = 0; - strncat_s(buf, _countof(buf), filename, (buf_size-1)-strlen(buf)); buf[buf_size-1] = 0; // I3481 // I3641 - if(kmcmp_FileExists(buf)) - return IntLoadFile(buf); - #endif + delete[] Buf; reindex(); - - return FALSE; + return TRUE; } void NamedCodeConstants::reindex() diff --git a/developer/src/kmcmplib/src/NamedCodeConstants.h b/developer/src/kmcmplib/src/NamedCodeConstants.h index 83d707d092cc4535a24cf3676aaa3d2a44d7b859..c537a73361b40f5539a75275fe86787e244b3541 100644 --- a/developer/src/kmcmplib/src/NamedCodeConstants.h +++ b/developer/src/kmcmplib/src/NamedCodeConstants.h @@ -2,6 +2,8 @@ #ifndef NAMEDCODECONSTANTS_H #define NAMEDCODECONSTANTS_H +#include "compfile.h" + #define MAX_ENAME 128 #define ALLOC_SIZE 256 @@ -23,14 +25,13 @@ namespace kmcmp{ int GetCode_IncludedCodes(const KMX_WCHAR *codename); void AddCode_IncludedCodes(int n, const KMX_WCHAR *p); - KMX_BOOL IntLoadFile(const KMX_CHAR *filename); public: NamedCodeConstants(); ~NamedCodeConstants(); void reindex(); void AddCode(int n, const KMX_WCHAR *p, KMX_DWORD storeIndex); - KMX_BOOL LoadFile(const KMX_CHAR *filename); + KMX_BOOL LoadFile(PFILE_KEYBOARD fk, const KMX_WCHAR *filename); int GetCode(const KMX_WCHAR *codename, KMX_DWORD *storeIndex); }; } diff --git a/developer/src/kmcmplib/src/UnreachableRules.cpp b/developer/src/kmcmplib/src/UnreachableRules.cpp index 9c6aaa23adb32e2e7162794bda48f19d9d182ee8..d8980599a2bd1865795159daea26b6f4eefaba83 100644 --- a/developer/src/kmcmplib/src/UnreachableRules.cpp +++ b/developer/src/kmcmplib/src/UnreachableRules.cpp @@ -41,8 +41,7 @@ KMX_DWORD VerifyUnreachableRules(PFILE_GROUP gp) { if (kp->Line != k1.Line && reportedLines.count(kp->Line) == 0) { reportedLines.insert(kp->Line); kmcmp::currentLine = kp->Line; - u16sprintf(ErrExtraW, ERR_EXTRA_W_LEN, L" Overridden by rule on line %d", k1.Line); - strcpy(ErrExtraLIB, string_from_u16string(ErrExtraW).c_str()); + snprintf(ErrExtraLIB, ERR_EXTRA_LIB_LEN, " Overridden by rule on line %d", k1.Line); AddWarning(CHINT_UnreachableRule); } } diff --git a/developer/src/kmcmplib/src/compfile.h b/developer/src/kmcmplib/src/compfile.h index 48be5f20482f444dde7ec9b516b835b1d15ba77e..c54663aba7659f871ce6271089494aa81420f6d0 100644 --- a/developer/src/kmcmplib/src/compfile.h +++ b/developer/src/kmcmplib/src/compfile.h @@ -22,6 +22,7 @@ #include "kmcompx.h" #include +#include #define LINESIZE 8192 #define GLOBAL_BUFSIZE 4096 @@ -71,85 +72,94 @@ enum FileStoreType { FST_STORE, FST_OPTION, FST_RESERVED }; struct FILE_STORE { - KMX_DWORD dwSystemID; - KMX_WCHAR szName[SZMAX_STORENAME]; // the name of the store - PKMX_WCHAR dpString; // from start of store structure - //FileStoreType fstType; - KMX_BOOL fIsStore; - KMX_BOOL fIsReserved; - KMX_BOOL fIsOption; - KMX_BOOL fIsDebug; - KMX_BOOL fIsCall; - int line; - }; - -typedef FILE_STORE * PFILE_STORE; + KMX_DWORD dwSystemID; + KMX_WCHAR szName[SZMAX_STORENAME]; // the name of the store + PKMX_WCHAR dpString; // from start of store structure + //FileStoreType fstType; + KMX_BOOL fIsStore; + KMX_BOOL fIsReserved; + KMX_BOOL fIsOption; + KMX_BOOL fIsDebug; + KMX_BOOL fIsCall; + int line; +}; +typedef FILE_STORE *PFILE_STORE; struct FILE_KEY { - KMX_WCHAR Key; // WCHAR for consistency; only a byte used however - KMX_WORD LineStoreIndex; - KMX_DWORD Line; - KMX_DWORD ShiftFlags; - PKMX_WCHAR dpOutput; // from start of key structure - PKMX_WCHAR dpContext; // from start of key structure - }; + KMX_WCHAR Key; // WCHAR for consistency; only a byte used however + KMX_WORD LineStoreIndex; + KMX_DWORD Line; + KMX_DWORD ShiftFlags; + PKMX_WCHAR dpOutput; // from start of key structure + PKMX_WCHAR dpContext; // from start of key structure +}; typedef FILE_KEY *PFILE_KEY; - struct FILE_GROUP { - KMX_WCHAR szName[SZMAX_GROUPNAME]; - PFILE_KEY dpKeyArray; // address of first item in key array, from start of group structure - PKMX_WCHAR dpMatch; // from start of group structure - PKMX_WCHAR dpNoMatch; // from start of group structure - KMX_DWORD cxKeyArray; // in array items - KMX_BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not - KMX_BOOL fReadOnly; // group(xx) [readonly] <-- specified or not - KMX_DWORD Line; - }; + KMX_WCHAR szName[SZMAX_GROUPNAME]; + PFILE_KEY dpKeyArray; // address of first item in key array, from start of group structure + PKMX_WCHAR dpMatch; // from start of group structure + PKMX_WCHAR dpNoMatch; // from start of group structure + KMX_DWORD cxKeyArray; // in array items + KMX_BOOL fUsingKeys; // group(xx) [using keys] <-- specified or not + KMX_BOOL fReadOnly; // group(xx) [readonly] <-- specified or not + KMX_DWORD Line; +}; typedef FILE_GROUP *PFILE_GROUP; -struct FILE_DEADKEY -{ - KMX_WCHAR szName[SZMAX_DEADKEYNAME]; +struct FILE_DEADKEY { + KMX_WCHAR szName[SZMAX_DEADKEYNAME]; }; + typedef FILE_DEADKEY *PFILE_DEADKEY; -struct FILE_VKDICTIONARY -{ - KMX_WCHAR szName[SZMAX_VKDICTIONARYNAME]; +struct FILE_VKDICTIONARY { + KMX_WCHAR szName[SZMAX_VKDICTIONARYNAME]; }; typedef FILE_VKDICTIONARY *PFILE_VKDICTIONARY; +/** + * Extra metadata for API consumers + */ +struct FILE_KEYBOARD_EXTRA { + std::string kmnFilename; // utf-8 + std::u16string kvksFilename; // utf-16, original TSS_VISUALKEYBOARD value +}; + +typedef struct FILE_KEYBOARD_EXTRA* PFILE_KEYBOARD_EXTRA; + struct FILE_KEYBOARD { - KMX_DWORD KeyboardID; // as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts + KMX_DWORD KeyboardID; // deprecated, unused - KMX_DWORD version; // keyboard file version with VERSION keyword + KMX_DWORD version; // keyboard file version with VERSION keyword - PFILE_STORE dpStoreArray; // address of first item in store array, from start of store structure - PFILE_GROUP dpGroupArray; // address of first item in group array, from start of group structure + PFILE_STORE dpStoreArray; // address of first item in store array, from start of store structure + PFILE_GROUP dpGroupArray; // address of first item in group array, from start of group structure - KMX_DWORD cxStoreArray; // in number of items - KMX_DWORD cxGroupArray; // in number of items - KMX_DWORD StartGroup[2]; // index of starting groups [ANSI=0, Unicode=1] + KMX_DWORD cxStoreArray; // in number of items + KMX_DWORD cxGroupArray; // in number of items + KMX_DWORD StartGroup[2]; // index of starting groups [ANSI=0, Unicode=1] - KMX_DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) + KMX_DWORD dwHotKey; // standard windows hotkey (hiword=shift/ctrl/alt stuff, loword=vkey) - KMX_WCHAR szName[SZMAX_KEYBOARDNAME]; // Keyboard layout name - KMX_WCHAR szLanguageName[SZMAX_LANGUAGENAME]; // Language name - KMX_WCHAR szCopyright[SZMAX_COPYRIGHT]; // Copyright information - KMX_WCHAR szMessage[SZMAX_MESSAGE]; // General information about the keyboard - PKMX_BYTE lpBitmap; - KMX_DWORD dwBitmapSize; - KMX_DWORD dwFlags; // Flags for the keyboard file + KMX_WCHAR szName[SZMAX_KEYBOARDNAME]; // Keyboard layout name + KMX_WCHAR szLanguageName[SZMAX_LANGUAGENAME]; // Language name + KMX_WCHAR szCopyright[SZMAX_COPYRIGHT]; // Copyright information + KMX_WCHAR szMessage[SZMAX_MESSAGE]; // General information about the keyboard + PKMX_BYTE lpBitmap; + KMX_DWORD dwBitmapSize; + KMX_DWORD dwFlags; // Flags for the keyboard file - KMX_DWORD currentGroup; // temp - current processing group - KMX_DWORD currentStore; // temp - current processing store - KMX_DWORD cxDeadKeyArray; - PFILE_DEADKEY dpDeadKeyArray; // temp - dead key array - KMX_DWORD cxVKDictionary; - PFILE_VKDICTIONARY dpVKDictionary; // temp - virtual key dictionary - }; + KMX_DWORD currentGroup; // temp - current processing group + KMX_DWORD currentStore; // temp - current processing store + KMX_DWORD cxDeadKeyArray; + PFILE_DEADKEY dpDeadKeyArray; // temp - dead key array + KMX_DWORD cxVKDictionary; + PFILE_VKDICTIONARY dpVKDictionary; // temp - virtual key dictionary + + PFILE_KEYBOARD_EXTRA extra; +}; typedef FILE_KEYBOARD *PFILE_KEYBOARD; @@ -167,54 +177,25 @@ const KMX_DWORD sz_FILE_VKDICTIONARY = sizeof(FILE_VKDICTIONARY); const KMX_DWORD sz_FILE_KEYBOARD = sizeof(FILE_KEYBOARD); struct COMPMSG { - KMX_CHAR szText[SZMAX_ERRORTEXT]; - KMX_DWORD Line; - KMX_DWORD dwMsgCode; - }; + KMX_CHAR szText[SZMAX_ERRORTEXT]; + KMX_DWORD Line; + KMX_DWORD dwMsgCode; +}; typedef COMPMSG *PCOMPMSG; struct COMPILEMESSAGES { - int nMessages; - int nErrors; - - PCOMPMSG cm; + int nMessages; + int nErrors; - KMX_DWORD fatalCode; - KMX_CHAR szFatalText[SZMAX_ERRORTEXT]; + PCOMPMSG cm; - KMX_DWORD currentLine; - }; + KMX_DWORD fatalCode; + KMX_CHAR szFatalText[SZMAX_ERRORTEXT]; -typedef COMPILEMESSAGES *PCOMPILEMESSAGES; - -/* -struct TVersion -{ - //int MinVersion; // 0x0500 usually - //int CompilerVersion[4]; - //int MinCompilerVersion[4]; - int KeyboardVersion; // 0x0500 usually + KMX_DWORD currentLine; }; -extern TVersion FVersionInfo; -*/ - -/* -#define bstrcpy(c,d) (LPBYTE)strcpy((LPSTR)(c),(LPSTR)(d)) -#define bstrlen(c) strlen((LPSTR)(c)) -#define bstrcmp(c,d) strcmp((LPSTR)(c),(LPSTR)(d)) -#define bstrncmp(c,d,n) strncmp((LPSTR)(c),(LPSTR)(d),(n)) -#define bstrnicmp(c,d,n) strnicmp((LPSTR)(c),(LPSTR)(d),(n)) -#define bstricmp(c,d) stricmp((LPSTR)(c),(LPSTR)(d)) -#define bstrchr(c,ch) (LPBYTE)strchr((LPSTR)(c),(char)ch) -#define bstrncpy(c,d,n) (LPBYTE)strncpy((LPSTR)(c),(LPSTR)(d),(n)) -#define bstrtok(c,d) (LPBYTE)strtok((LPSTR)(c),(LPSTR)(d)) -#define bstrcat(c,d) (LPBYTE)strcat((LPSTR)(c),(LPSTR)(d)) -#define bstrncat(c,d,n) (LPBYTE)strncat((LPSTR)(c),(LPSTR)(d),(n)) -#define bstrrev(c) (LPBYTE)strrev((LPSTR)(c)) -#define batoi(c) atoi((LPSTR)(c)) -#define bstrtol(c,d,n) strtol((LPSTR)(c),(LPSTR *)(d),(n)) -*/ +typedef COMPILEMESSAGES *PCOMPILEMESSAGES; #endif // _COMPFILE_H diff --git a/developer/src/kmcmplib/src/cp1252.cpp b/developer/src/kmcmplib/src/cp1252.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8812bd641677cfd1205b997429e2236a56da0fd6 --- /dev/null +++ b/developer/src/kmcmplib/src/cp1252.cpp @@ -0,0 +1,263 @@ +#include "cp1252.h" +// Source: https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT +// _undefined_ values have been mapped to the same USV even though this isn't a +// legit character because that's hopefully what the keyboard designer had +// intended... +const KMX_WORD CP1252_UNICODE[256] = { + /* 0x00 */ 0x0000, // #NULL + /* 0x01 */ 0x0001, // #START OF HEADING + /* 0x02 */ 0x0002, // #START OF TEXT + /* 0x03 */ 0x0003, // #END OF TEXT + /* 0x04 */ 0x0004, // #END OF TRANSMISSION + /* 0x05 */ 0x0005, // #ENQUIRY + /* 0x06 */ 0x0006, // #ACKNOWLEDGE + /* 0x07 */ 0x0007, // #BELL + /* 0x08 */ 0x0008, // #BACKSPACE + /* 0x09 */ 0x0009, // #HORIZONTAL TABULATION + /* 0x0A */ 0x000A, // #LINE FEED + /* 0x0B */ 0x000B, // #VERTICAL TABULATION + /* 0x0C */ 0x000C, // #FORM FEED + /* 0x0D */ 0x000D, // #CARRIAGE RETURN + /* 0x0E */ 0x000E, // #SHIFT OUT + /* 0x0F */ 0x000F, // #SHIFT IN + /* 0x10 */ 0x0010, // #DATA LINK ESCAPE + /* 0x11 */ 0x0011, // #DEVICE CONTROL ONE + /* 0x12 */ 0x0012, // #DEVICE CONTROL TWO + /* 0x13 */ 0x0013, // #DEVICE CONTROL THREE + /* 0x14 */ 0x0014, // #DEVICE CONTROL FOUR + /* 0x15 */ 0x0015, // #NEGATIVE ACKNOWLEDGE + /* 0x16 */ 0x0016, // #SYNCHRONOUS IDLE + /* 0x17 */ 0x0017, // #END OF TRANSMISSION BLOCK + /* 0x18 */ 0x0018, // #CANCEL + /* 0x19 */ 0x0019, // #END OF MEDIUM + /* 0x1A */ 0x001A, // #SUBSTITUTE + /* 0x1B */ 0x001B, // #ESCAPE + /* 0x1C */ 0x001C, // #FILE SEPARATOR + /* 0x1D */ 0x001D, // #GROUP SEPARATOR + /* 0x1E */ 0x001E, // #RECORD SEPARATOR + /* 0x1F */ 0x001F, // #UNIT SEPARATOR + /* 0x20 */ 0x0020, // #SPACE + /* 0x21 */ 0x0021, // #EXCLAMATION MARK + /* 0x22 */ 0x0022, // #QUOTATION MARK + /* 0x23 */ 0x0023, // #NUMBER SIGN + /* 0x24 */ 0x0024, // #DOLLAR SIGN + /* 0x25 */ 0x0025, // #PERCENT SIGN + /* 0x26 */ 0x0026, // #AMPERSAND + /* 0x27 */ 0x0027, // #APOSTROPHE + /* 0x28 */ 0x0028, // #LEFT PARENTHESIS + /* 0x29 */ 0x0029, // #RIGHT PARENTHESIS + /* 0x2A */ 0x002A, // #ASTERISK + /* 0x2B */ 0x002B, // #PLUS SIGN + /* 0x2C */ 0x002C, // #COMMA + /* 0x2D */ 0x002D, // #HYPHEN-MINUS + /* 0x2E */ 0x002E, // #FULL STOP + /* 0x2F */ 0x002F, // #SOLIDUS + /* 0x30 */ 0x0030, // #DIGIT ZERO + /* 0x31 */ 0x0031, // #DIGIT ONE + /* 0x32 */ 0x0032, // #DIGIT TWO + /* 0x33 */ 0x0033, // #DIGIT THREE + /* 0x34 */ 0x0034, // #DIGIT FOUR + /* 0x35 */ 0x0035, // #DIGIT FIVE + /* 0x36 */ 0x0036, // #DIGIT SIX + /* 0x37 */ 0x0037, // #DIGIT SEVEN + /* 0x38 */ 0x0038, // #DIGIT EIGHT + /* 0x39 */ 0x0039, // #DIGIT NINE + /* 0x3A */ 0x003A, // #COLON + /* 0x3B */ 0x003B, // #SEMICOLON + /* 0x3C */ 0x003C, // #LESS-THAN SIGN + /* 0x3D */ 0x003D, // #EQUALS SIGN + /* 0x3E */ 0x003E, // #GREATER-THAN SIGN + /* 0x3F */ 0x003F, // #QUESTION MARK + /* 0x40 */ 0x0040, // #COMMERCIAL AT + /* 0x41 */ 0x0041, // #LATIN CAPITAL LETTER A + /* 0x42 */ 0x0042, // #LATIN CAPITAL LETTER B + /* 0x43 */ 0x0043, // #LATIN CAPITAL LETTER C + /* 0x44 */ 0x0044, // #LATIN CAPITAL LETTER D + /* 0x45 */ 0x0045, // #LATIN CAPITAL LETTER E + /* 0x46 */ 0x0046, // #LATIN CAPITAL LETTER F + /* 0x47 */ 0x0047, // #LATIN CAPITAL LETTER G + /* 0x48 */ 0x0048, // #LATIN CAPITAL LETTER H + /* 0x49 */ 0x0049, // #LATIN CAPITAL LETTER I + /* 0x4A */ 0x004A, // #LATIN CAPITAL LETTER J + /* 0x4B */ 0x004B, // #LATIN CAPITAL LETTER K + /* 0x4C */ 0x004C, // #LATIN CAPITAL LETTER L + /* 0x4D */ 0x004D, // #LATIN CAPITAL LETTER M + /* 0x4E */ 0x004E, // #LATIN CAPITAL LETTER N + /* 0x4F */ 0x004F, // #LATIN CAPITAL LETTER O + /* 0x50 */ 0x0050, // #LATIN CAPITAL LETTER P + /* 0x51 */ 0x0051, // #LATIN CAPITAL LETTER Q + /* 0x52 */ 0x0052, // #LATIN CAPITAL LETTER R + /* 0x53 */ 0x0053, // #LATIN CAPITAL LETTER S + /* 0x54 */ 0x0054, // #LATIN CAPITAL LETTER T + /* 0x55 */ 0x0055, // #LATIN CAPITAL LETTER U + /* 0x56 */ 0x0056, // #LATIN CAPITAL LETTER V + /* 0x57 */ 0x0057, // #LATIN CAPITAL LETTER W + /* 0x58 */ 0x0058, // #LATIN CAPITAL LETTER X + /* 0x59 */ 0x0059, // #LATIN CAPITAL LETTER Y + /* 0x5A */ 0x005A, // #LATIN CAPITAL LETTER Z + /* 0x5B */ 0x005B, // #LEFT SQUARE BRACKET + /* 0x5C */ 0x005C, // #REVERSE SOLIDUS + /* 0x5D */ 0x005D, // #RIGHT SQUARE BRACKET + /* 0x5E */ 0x005E, // #CIRCUMFLEX ACCENT + /* 0x5F */ 0x005F, // #LOW LINE + /* 0x60 */ 0x0060, // #GRAVE ACCENT + /* 0x61 */ 0x0061, // #LATIN SMALL LETTER A + /* 0x62 */ 0x0062, // #LATIN SMALL LETTER B + /* 0x63 */ 0x0063, // #LATIN SMALL LETTER C + /* 0x64 */ 0x0064, // #LATIN SMALL LETTER D + /* 0x65 */ 0x0065, // #LATIN SMALL LETTER E + /* 0x66 */ 0x0066, // #LATIN SMALL LETTER F + /* 0x67 */ 0x0067, // #LATIN SMALL LETTER G + /* 0x68 */ 0x0068, // #LATIN SMALL LETTER H + /* 0x69 */ 0x0069, // #LATIN SMALL LETTER I + /* 0x6A */ 0x006A, // #LATIN SMALL LETTER J + /* 0x6B */ 0x006B, // #LATIN SMALL LETTER K + /* 0x6C */ 0x006C, // #LATIN SMALL LETTER L + /* 0x6D */ 0x006D, // #LATIN SMALL LETTER M + /* 0x6E */ 0x006E, // #LATIN SMALL LETTER N + /* 0x6F */ 0x006F, // #LATIN SMALL LETTER O + /* 0x70 */ 0x0070, // #LATIN SMALL LETTER P + /* 0x71 */ 0x0071, // #LATIN SMALL LETTER Q + /* 0x72 */ 0x0072, // #LATIN SMALL LETTER R + /* 0x73 */ 0x0073, // #LATIN SMALL LETTER S + /* 0x74 */ 0x0074, // #LATIN SMALL LETTER T + /* 0x75 */ 0x0075, // #LATIN SMALL LETTER U + /* 0x76 */ 0x0076, // #LATIN SMALL LETTER V + /* 0x77 */ 0x0077, // #LATIN SMALL LETTER W + /* 0x78 */ 0x0078, // #LATIN SMALL LETTER X + /* 0x79 */ 0x0079, // #LATIN SMALL LETTER Y + /* 0x7A */ 0x007A, // #LATIN SMALL LETTER Z + /* 0x7B */ 0x007B, // #LEFT CURLY BRACKET + /* 0x7C */ 0x007C, // #VERTICAL LINE + /* 0x7D */ 0x007D, // #RIGHT CURLY BRACKET + /* 0x7E */ 0x007E, // #TILDE + /* 0x7F */ 0x007F, // #DELETE + /* 0x80 */ 0x20AC, // #EURO SIGN + /* 0x81 */ 0x0081, // #UNDEFINED --- diverges from CP1252.txt + /* 0x82 */ 0x201A, // #SINGLE LOW-9 QUOTATION MARK + /* 0x83 */ 0x0192, // #LATIN SMALL LETTER F WITH HOOK + /* 0x84 */ 0x201E, // #DOUBLE LOW-9 QUOTATION MARK + /* 0x85 */ 0x2026, // #HORIZONTAL ELLIPSIS + /* 0x86 */ 0x2020, // #DAGGER + /* 0x87 */ 0x2021, // #DOUBLE DAGGER + /* 0x88 */ 0x02C6, // #MODIFIER LETTER CIRCUMFLEX ACCENT + /* 0x89 */ 0x2030, // #PER MILLE SIGN + /* 0x8A */ 0x0160, // #LATIN CAPITAL LETTER S WITH CARON + /* 0x8B */ 0x2039, // #SINGLE LEFT-POINTING ANGLE QUOTATION MARK + /* 0x8C */ 0x0152, // #LATIN CAPITAL LIGATURE OE + /* 0x8D */ 0x008D, // #UNDEFINED --- diverges from CP1252.txt + /* 0x8E */ 0x017D, // #LATIN CAPITAL LETTER Z WITH CARON + /* 0x8F */ 0x008F, // #UNDEFINED --- diverges from CP1252.txt + /* 0x90 */ 0x0090, // #UNDEFINED --- diverges from CP1252.txt + /* 0x91 */ 0x2018, // #LEFT SINGLE QUOTATION MARK + /* 0x92 */ 0x2019, // #RIGHT SINGLE QUOTATION MARK + /* 0x93 */ 0x201C, // #LEFT DOUBLE QUOTATION MARK + /* 0x94 */ 0x201D, // #RIGHT DOUBLE QUOTATION MARK + /* 0x95 */ 0x2022, // #BULLET + /* 0x96 */ 0x2013, // #EN DASH + /* 0x97 */ 0x2014, // #EM DASH + /* 0x98 */ 0x02DC, // #SMALL TILDE + /* 0x99 */ 0x2122, // #TRADE MARK SIGN + /* 0x9A */ 0x0161, // #LATIN SMALL LETTER S WITH CARON + /* 0x9B */ 0x203A, // #SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + /* 0x9C */ 0x0153, // #LATIN SMALL LIGATURE OE + /* 0x9D */ 0x009D, // #UNDEFINED --- diverges from CP1252.txt + /* 0x9E */ 0x017E, // #LATIN SMALL LETTER Z WITH CARON + /* 0x9F */ 0x0178, // #LATIN CAPITAL LETTER Y WITH DIAERESIS + /* 0xA0 */ 0x00A0, // #NO-BREAK SPACE + /* 0xA1 */ 0x00A1, // #INVERTED EXCLAMATION MARK + /* 0xA2 */ 0x00A2, // #CENT SIGN + /* 0xA3 */ 0x00A3, // #POUND SIGN + /* 0xA4 */ 0x00A4, // #CURRENCY SIGN + /* 0xA5 */ 0x00A5, // #YEN SIGN + /* 0xA6 */ 0x00A6, // #BROKEN BAR + /* 0xA7 */ 0x00A7, // #SECTION SIGN + /* 0xA8 */ 0x00A8, // #DIAERESIS + /* 0xA9 */ 0x00A9, // #COPYRIGHT SIGN + /* 0xAA */ 0x00AA, // #FEMININE ORDINAL INDICATOR + /* 0xAB */ 0x00AB, // #LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + /* 0xAC */ 0x00AC, // #NOT SIGN + /* 0xAD */ 0x00AD, // #SOFT HYPHEN + /* 0xAE */ 0x00AE, // #REGISTERED SIGN + /* 0xAF */ 0x00AF, // #MACRON + /* 0xB0 */ 0x00B0, // #DEGREE SIGN + /* 0xB1 */ 0x00B1, // #PLUS-MINUS SIGN + /* 0xB2 */ 0x00B2, // #SUPERSCRIPT TWO + /* 0xB3 */ 0x00B3, // #SUPERSCRIPT THREE + /* 0xB4 */ 0x00B4, // #ACUTE ACCENT + /* 0xB5 */ 0x00B5, // #MICRO SIGN + /* 0xB6 */ 0x00B6, // #PILCROW SIGN + /* 0xB7 */ 0x00B7, // #MIDDLE DOT + /* 0xB8 */ 0x00B8, // #CEDILLA + /* 0xB9 */ 0x00B9, // #SUPERSCRIPT ONE + /* 0xBA */ 0x00BA, // #MASCULINE ORDINAL INDICATOR + /* 0xBB */ 0x00BB, // #RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + /* 0xBC */ 0x00BC, // #VULGAR FRACTION ONE QUARTER + /* 0xBD */ 0x00BD, // #VULGAR FRACTION ONE HALF + /* 0xBE */ 0x00BE, // #VULGAR FRACTION THREE QUARTERS + /* 0xBF */ 0x00BF, // #INVERTED QUESTION MARK + /* 0xC0 */ 0x00C0, // #LATIN CAPITAL LETTER A WITH GRAVE + /* 0xC1 */ 0x00C1, // #LATIN CAPITAL LETTER A WITH ACUTE + /* 0xC2 */ 0x00C2, // #LATIN CAPITAL LETTER A WITH CIRCUMFLEX + /* 0xC3 */ 0x00C3, // #LATIN CAPITAL LETTER A WITH TILDE + /* 0xC4 */ 0x00C4, // #LATIN CAPITAL LETTER A WITH DIAERESIS + /* 0xC5 */ 0x00C5, // #LATIN CAPITAL LETTER A WITH RING ABOVE + /* 0xC6 */ 0x00C6, // #LATIN CAPITAL LETTER AE + /* 0xC7 */ 0x00C7, // #LATIN CAPITAL LETTER C WITH CEDILLA + /* 0xC8 */ 0x00C8, // #LATIN CAPITAL LETTER E WITH GRAVE + /* 0xC9 */ 0x00C9, // #LATIN CAPITAL LETTER E WITH ACUTE + /* 0xCA */ 0x00CA, // #LATIN CAPITAL LETTER E WITH CIRCUMFLEX + /* 0xCB */ 0x00CB, // #LATIN CAPITAL LETTER E WITH DIAERESIS + /* 0xCC */ 0x00CC, // #LATIN CAPITAL LETTER I WITH GRAVE + /* 0xCD */ 0x00CD, // #LATIN CAPITAL LETTER I WITH ACUTE + /* 0xCE */ 0x00CE, // #LATIN CAPITAL LETTER I WITH CIRCUMFLEX + /* 0xCF */ 0x00CF, // #LATIN CAPITAL LETTER I WITH DIAERESIS + /* 0xD0 */ 0x00D0, // #LATIN CAPITAL LETTER ETH + /* 0xD1 */ 0x00D1, // #LATIN CAPITAL LETTER N WITH TILDE + /* 0xD2 */ 0x00D2, // #LATIN CAPITAL LETTER O WITH GRAVE + /* 0xD3 */ 0x00D3, // #LATIN CAPITAL LETTER O WITH ACUTE + /* 0xD4 */ 0x00D4, // #LATIN CAPITAL LETTER O WITH CIRCUMFLEX + /* 0xD5 */ 0x00D5, // #LATIN CAPITAL LETTER O WITH TILDE + /* 0xD6 */ 0x00D6, // #LATIN CAPITAL LETTER O WITH DIAERESIS + /* 0xD7 */ 0x00D7, // #MULTIPLICATION SIGN + /* 0xD8 */ 0x00D8, // #LATIN CAPITAL LETTER O WITH STROKE + /* 0xD9 */ 0x00D9, // #LATIN CAPITAL LETTER U WITH GRAVE + /* 0xDA */ 0x00DA, // #LATIN CAPITAL LETTER U WITH ACUTE + /* 0xDB */ 0x00DB, // #LATIN CAPITAL LETTER U WITH CIRCUMFLEX + /* 0xDC */ 0x00DC, // #LATIN CAPITAL LETTER U WITH DIAERESIS + /* 0xDD */ 0x00DD, // #LATIN CAPITAL LETTER Y WITH ACUTE + /* 0xDE */ 0x00DE, // #LATIN CAPITAL LETTER THORN + /* 0xDF */ 0x00DF, // #LATIN SMALL LETTER SHARP S + /* 0xE0 */ 0x00E0, // #LATIN SMALL LETTER A WITH GRAVE + /* 0xE1 */ 0x00E1, // #LATIN SMALL LETTER A WITH ACUTE + /* 0xE2 */ 0x00E2, // #LATIN SMALL LETTER A WITH CIRCUMFLEX + /* 0xE3 */ 0x00E3, // #LATIN SMALL LETTER A WITH TILDE + /* 0xE4 */ 0x00E4, // #LATIN SMALL LETTER A WITH DIAERESIS + /* 0xE5 */ 0x00E5, // #LATIN SMALL LETTER A WITH RING ABOVE + /* 0xE6 */ 0x00E6, // #LATIN SMALL LETTER AE + /* 0xE7 */ 0x00E7, // #LATIN SMALL LETTER C WITH CEDILLA + /* 0xE8 */ 0x00E8, // #LATIN SMALL LETTER E WITH GRAVE + /* 0xE9 */ 0x00E9, // #LATIN SMALL LETTER E WITH ACUTE + /* 0xEA */ 0x00EA, // #LATIN SMALL LETTER E WITH CIRCUMFLEX + /* 0xEB */ 0x00EB, // #LATIN SMALL LETTER E WITH DIAERESIS + /* 0xEC */ 0x00EC, // #LATIN SMALL LETTER I WITH GRAVE + /* 0xED */ 0x00ED, // #LATIN SMALL LETTER I WITH ACUTE + /* 0xEE */ 0x00EE, // #LATIN SMALL LETTER I WITH CIRCUMFLEX + /* 0xEF */ 0x00EF, // #LATIN SMALL LETTER I WITH DIAERESIS + /* 0xF0 */ 0x00F0, // #LATIN SMALL LETTER ETH + /* 0xF1 */ 0x00F1, // #LATIN SMALL LETTER N WITH TILDE + /* 0xF2 */ 0x00F2, // #LATIN SMALL LETTER O WITH GRAVE + /* 0xF3 */ 0x00F3, // #LATIN SMALL LETTER O WITH ACUTE + /* 0xF4 */ 0x00F4, // #LATIN SMALL LETTER O WITH CIRCUMFLEX + /* 0xF5 */ 0x00F5, // #LATIN SMALL LETTER O WITH TILDE + /* 0xF6 */ 0x00F6, // #LATIN SMALL LETTER O WITH DIAERESIS + /* 0xF7 */ 0x00F7, // #DIVISION SIGN + /* 0xF8 */ 0x00F8, // #LATIN SMALL LETTER O WITH STROKE + /* 0xF9 */ 0x00F9, // #LATIN SMALL LETTER U WITH GRAVE + /* 0xFA */ 0x00FA, // #LATIN SMALL LETTER U WITH ACUTE + /* 0xFB */ 0x00FB, // #LATIN SMALL LETTER U WITH CIRCUMFLEX + /* 0xFC */ 0x00FC, // #LATIN SMALL LETTER U WITH DIAERESIS + /* 0xFD */ 0x00FD, // #LATIN SMALL LETTER Y WITH ACUTE + /* 0xFE */ 0x00FE, // #LATIN SMALL LETTER THORN + /* 0xFF */ 0x00FF // #LATIN SMALL LETTER Y WITH DIAERESIS +}; diff --git a/developer/src/kmcmplib/src/cp1252.h b/developer/src/kmcmplib/src/cp1252.h new file mode 100644 index 0000000000000000000000000000000000000000..e5c4a81d23c75b5a77c5058128dc9b0b29832f46 --- /dev/null +++ b/developer/src/kmcmplib/src/cp1252.h @@ -0,0 +1,4 @@ +#pragma once +#include "pch.h" + +extern const KMX_WORD CP1252_UNICODE[]; diff --git a/developer/src/kmcmplib/src/json-validation.cpp b/developer/src/kmcmplib/src/json-validation.cpp deleted file mode 100644 index f58d8c950534943427285a4f400fb39c190217ff..0000000000000000000000000000000000000000 --- a/developer/src/kmcmplib/src/json-validation.cpp +++ /dev/null @@ -1,68 +0,0 @@ - -#include -#include - -#include -#include "kmcompx.h" - -using nlohmann::json; -using nlohmann::json_uri; -using nlohmann::json_schema_draft4::json_validator; - -static void loader(const json_uri &uri, json &schema) -{ - std::fstream lf("." + uri.path()); - if (!lf.good()) - throw std::invalid_argument("could not open " + uri.url() + " tried with " + uri.path()); - - lf >> schema; -} - -EXTERN bool kmcmp_ValidateJsonFile(std::fstream& f, std::fstream& fd, kmcmp_ValidateJsonMessageProc MessageProc, void* context) { - if (!f.good()) { - MessageProc(-1, "Schema file could not be loaded.", context); - return FALSE; - } - - // 1) Read the schema for the document you want to validate - json schema; - try { - f >> schema; - } - catch (std::exception &e) { - MessageProc(f.tellp(), e.what(), context); - return FALSE; - } - - // 2) create the validator and - json_validator validator(loader, [](const std::string &, const std::string &) {}); - - try { - // insert this schema as the root to the validator - // this resolves remote-schemas, sub-schemas and references via the given loader-function - validator.set_root_schema(schema); - } - catch (std::exception &e) { - MessageProc(-2, e.what(), context); - return FALSE; - } - - // 3) do the actual validation of the document - json document; - - if (!fd.good()) { - MessageProc(-3, "Json file could not be loaded.", context); - return FALSE; - } - - try { - fd >> document; - validator.validate(document); - } - catch (std::exception &e) { - MessageProc(fd.tellp(), e.what(), context); - return FALSE; - } - - return TRUE; -} \ No newline at end of file diff --git a/developer/src/kmcmplib/src/kmcmplib.h b/developer/src/kmcmplib/src/kmcmplib.h index a851915de2b13ec9257e6e9ab44e4178968b86dc..0d7c4f90042d8bed81a142f3f967ec0c604513bc 100644 --- a/developer/src/kmcmplib/src/kmcmplib.h +++ b/developer/src/kmcmplib/src/kmcmplib.h @@ -14,7 +14,6 @@ namespace kmcmp { extern KMX_BOOL FMnemonicLayout; extern KMX_BOOL FOldCharPosMatching; extern int CompileTarget; - extern KMX_CHAR CompileDir[260]; // TODO: this should not be a fixed buffer extern int BeginLine[4]; extern int currentLine; extern NamedCodeConstants *CodeConstants; @@ -24,13 +23,12 @@ namespace kmcmp { } extern kmcmp_CompilerMessageProc msgproc; +extern kmcmp_LoadFileProc loadfileproc; extern void* msgprocContext; extern KMX_BOOL AWarnDeprecatedCode_GLOBAL_LIB; #define ERR_EXTRA_LIB_LEN 256 -#define ERR_EXTRA_W_LEN 256 extern char ErrExtraLIB[ERR_EXTRA_LIB_LEN]; -extern KMX_WCHAR ErrExtraW[ERR_EXTRA_W_LEN]; KMX_BOOL AddCompileError(KMX_DWORD msg); /// Use AddWarningBool for functions that return bool or KMX_BOOL @@ -40,10 +38,10 @@ KMX_BOOL AddCompileError(KMX_DWORD msg); PKMX_WCHAR strtowstr(PKMX_STR in); PFILE_STORE FindSystemStore(PFILE_KEYBOARD fk, KMX_DWORD dwSystemID); -FILE* UTF16TempFromUTF8(FILE* fp_in , KMX_BOOL hasPreamble); -KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, FILE* fp_out); -KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, KMX_WCHAR const * str, KMX_DWORD *dwStoreID= NULL); -KMX_DWORD ReadLine(FILE* fp_in , PKMX_WCHAR wstr, KMX_BOOL PreProcess); +bool UTF16TempFromUTF8(KMX_BYTE* infile, int sz, KMX_BYTE** tempfile, int *sz16); +KMX_DWORD WriteCompiledKeyboard(PFILE_KEYBOARD fk, KMX_BYTE**data, size_t& dataSize); +KMX_DWORD AddStore(PFILE_KEYBOARD fk, KMX_DWORD SystemID, const KMX_WCHAR * str, KMX_DWORD *dwStoreID= NULL); +KMX_DWORD ReadLine(KMX_BYTE* infile, int sz, int& offset, PKMX_WCHAR wstr, KMX_BOOL PreProcess); KMX_DWORD ParseLine(PFILE_KEYBOARD fk, PKMX_WCHAR str); KMX_DWORD ProcessGroupLine(PFILE_KEYBOARD fk, PKMX_WCHAR p); KMX_DWORD ProcessGroupFinish(PFILE_KEYBOARD fk); diff --git a/developer/src/kmcmplib/src/meson.build b/developer/src/kmcmplib/src/meson.build index fa51ca28a502faf4405f20967bd2aedb70b2b915..8d68c54979015d4001bb6f6b080845660871f4c7 100644 --- a/developer/src/kmcmplib/src/meson.build +++ b/developer/src/kmcmplib/src/meson.build @@ -7,6 +7,7 @@ # TODO: is this required? It should be Keyman Core only defns += ['-DKMN_KBP_EXPORTING'] version_res = [] +lib_links = [] if cpp_compiler.get_id() == 'gcc' or cpp_compiler.get_id() == 'clang' warns += [ @@ -24,12 +25,18 @@ endif name_suffix = [] if cpp_compiler.get_id() == 'emscripten' - links += ['-lnodefs.js', '-sMODULARIZE', '-sEXPORT_ES6', '-sEXPORTED_RUNTIME_METHODS=[\'cwrap\', \'UTF8ToString\']'] - # tests are building as ES6 so we need to declare the file extension - # note that meson currently struggles with the sanitycheckc_cross.exe - # program, because it has a hard coded extension (.exe) which is not - # valid for node programs in module mode. - name_suffix = '.mjs' + # wasm-exceptions supported in Node 18+, Chrome 95+, Firefox 100+, Safari 15.2+ + flags += ['-fwasm-exceptions'] + lib_links = ['--whole-archive', '-sMODULARIZE', '-sEXPORT_ES6'] + links += ['-fwasm-exceptions', '--bind'] + if cpp_compiler.version().version_compare('>=3.1.44') + # emscripten 3.1.44 removes .asm object and so we need to export `wasmExports` + # #9375; https://github.com/emscripten-core/emscripten/blob/main/ChangeLog.md#3144---072523 + links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\',\'wasmExports\']'] + else + # emscripten < 3.1.44 does not include `wasmExports` + links += ['-sEXPORTED_RUNTIME_METHODS=[\'UTF8ToString\']'] + endif endif icu = subproject('icu-for-uset', default_options: [ 'default_library=static', 'cpp_std=c++17', 'warning_level=0', 'werror=false']) @@ -38,36 +45,30 @@ icuuc_dep = icu.get_variable('icuuc_dep') lib = library('kmcmplib', 'CasedKeys.cpp', 'CharToKeyConversion.cpp', + 'CheckForDuplicates.cpp', + 'CheckNCapsConsistency.cpp', + 'CompileKeyboardBuffer.cpp', 'Compiler.cpp', 'CompilerInterfaces.cpp', + 'CompilerInterfacesWasm.cpp', + 'CompMsg.cpp', + 'cp1252.cpp', 'DeprecationChecks.cpp', 'Edition.cpp', - 'filesystem.cpp', - 'json-validation.cpp', + 'kmx_u16.cpp', 'NamedCodeConstants.cpp', + 'UnreachableRules.cpp', + 'uset-api.cpp', 'versioning.cpp', 'virtualcharkeys.cpp', 'xstring.cpp', - 'CharToKeyConversion.cpp', - 'CheckFilenameConsistency.cpp', - 'CheckForDuplicates.cpp', - 'CheckNCapsConsistency.cpp', - 'UnreachableRules.cpp', - 'kmx_u16.cpp', - 'CompMsg.cpp', - 'uset-api.cpp', - '../../../../common/windows/cpp/src/ConvertUTF.c', '../../../../common/windows/cpp/src/crc32.cpp', '../../../../common/windows/cpp/src/vkeys.cpp', - 'xstring.cpp', - '../../ext/json-schema-validator/json-schema-draft4.json.cpp', - '../../ext/json-schema-validator/json-uri.cpp', - '../../ext/json-schema-validator/json-validator.cpp', version_res, cpp_args: defns + warns + flags, - link_args: links, + link_args: links + lib_links, version: meson.project_version(), include_directories: inc, install: true, @@ -81,7 +82,7 @@ if cpp_compiler.get_id() == 'emscripten' host = executable('wasm-host', #'wasm-host.cpp', cpp_args: defns, include_directories: inc, - link_args: links, + link_args: links + lib_links, objects: lib.extract_all_objects(), dependencies: icuuc_dep) endif diff --git a/developer/src/kmcmplib/src/pch.h b/developer/src/kmcmplib/src/pch.h index f878ad11be4f892ad4e9f0ece5864712c6af5c6b..358e28998abc8e65bba25cf2b594ca95cbed5b57 100644 --- a/developer/src/kmcmplib/src/pch.h +++ b/developer/src/kmcmplib/src/pch.h @@ -10,6 +10,4 @@ #include "../../../../common/windows/cpp/include/crc32.h" #include -#include - #include diff --git a/developer/src/kmcmplib/src/uset-api.cpp b/developer/src/kmcmplib/src/uset-api.cpp index 1aa22477b2b63465a8cb0db8b7d624409d9ce083..b107f07f5bcc0af90bb0916325d1753255ccf012 100644 --- a/developer/src/kmcmplib/src/uset-api.cpp +++ b/developer/src/kmcmplib/src/uset-api.cpp @@ -4,16 +4,12 @@ #include "unicode/uniset.h" #include "unicode/unistr.h" -EXTERN int kmcmp_ParseUnicodeSet( - const char* szText, - uint32_t* outputBuffer, +EXTERN int kmcmp_parseUnicodeSet( + const std::string text, + uintptr_t outputBuffer_, uint32_t outputBufferSize ) { - if (szText == nullptr) { - // null string coming in - return KMCMP_ERROR_SYNTAX_ERR; - } - const icu::UnicodeString str = icu::UnicodeString::fromUTF8(szText); + const icu::UnicodeString str = icu::UnicodeString::fromUTF8(text.c_str()); if (str.isBogus() || str.isEmpty()) { // empty string return KMCMP_ERROR_SYNTAX_ERR; @@ -34,12 +30,17 @@ EXTERN int kmcmp_ParseUnicodeSet( } else if (uset.hasStrings()) { // Error, strings are not allowed return KMCMP_ERROR_HAS_STRINGS; - } else if (outputBuffer == nullptr) { + } + const int32_t count = uset.getRangeCount(); + if (outputBufferSize == 0) { + // pure preflight - return buffer size needed as negative + return count; + } + uint32_t* outputBuffer = reinterpret_cast(outputBuffer_); + if (outputBuffer == nullptr) { // fail if nullptr passed return KMCMP_FATAL_OUT_OF_RANGE; } - - const int32_t count = uset.getRangeCount(); if ((count * 2L) > outputBufferSize) { // output buffer too small return KMCMP_FATAL_OUT_OF_RANGE; diff --git a/developer/src/kmcmplib/tests/api-test.cpp b/developer/src/kmcmplib/tests/api-test.cpp index 4c46b1f1a408c166cda98dda0ef43d7449471a1e..cb0337caef680107910d8b219bca8a69171c5a15 100644 --- a/developer/src/kmcmplib/tests/api-test.cpp +++ b/developer/src/kmcmplib/tests/api-test.cpp @@ -18,32 +18,20 @@ #include #include "../src/compfile.h" #include +#include "util_filesystem.h" +#include "util_callbacks.h" void setup(); -void test_kmcmp_CompileKeyboardFile(); -void test_kmcmp_CompileKeyboardFileToBuffer(); - -std::vector error_vec; - -int msgproc(int line, uint32_t dwMsgCode, char* szText, void* context) { - error_vec.push_back(dwMsgCode); - const char*t = "unknown"; - switch(dwMsgCode & 0xF000) { - case CERR_HINT: t=" hint"; break; - case CERR_WARNING: t="warning"; break; - case CERR_ERROR: t=" error"; break; - case CERR_FATAL: t=" fatal"; break; - } - printf("line %d %s %04.4x: %s\n", line, t, (unsigned int)dwMsgCode, szText); - return 1; -} +void test_kmcmp_CompileKeyboard(char *kmn_file); int main(int argc, char *argv[]) { + if(argc < 1) { + puts("Usage: api-test "); + puts("Warning: blank_keyboard will be overwritten"); + return 1; + } setup(); - test_kmcmp_CompileKeyboardFile(); - - setup(); - test_kmcmp_CompileKeyboardFileToBuffer(); + test_kmcmp_CompileKeyboard(argv[1]); return 0; } @@ -52,36 +40,27 @@ void setup() { error_vec.clear(); } -void test_kmcmp_CompileKeyboardFile() { - char kmn_file[L_tmpnam], kmx_file[L_tmpnam]; - tmpnam(kmn_file); - tmpnam(kmx_file); - - // Create an empty file - FILE *fp = fopen(kmn_file, "w"); - fclose(fp); - - // It should fail when a zero-byte file is passed in - assert(!kmcmp_CompileKeyboardFile(kmn_file, kmx_file, true, false, true, msgproc, nullptr)); - assert(error_vec.size() == 1); - assert(error_vec[0] == CERR_CannotReadInfile); - - unlink(kmn_file); -} - -void test_kmcmp_CompileKeyboardFileToBuffer() { - char kmn_file[L_tmpnam], kmx_file[L_tmpnam]; - tmpnam(kmn_file); - tmpnam(kmx_file); +/* + TODO: tests to run: + 4. ANSI (no BOM of course) #8884 + 8. file without blank last line (cannot compare with fixture due to bug in kmcmpdll...) + Hint to add: k004_ansi.kmn: Hint: 10A6 Keyman Developer has detected that the file has ANSI encoding. Consider converting this file to UTF-8 +*/ +void test_kmcmp_CompileKeyboard(char *kmn_file) { // Create an empty file - FILE *fp = fopen(kmn_file, "w"); + FILE *fp = Open_File(kmn_file, "wb"); fclose(fp); - FILE_KEYBOARD fk; - // It should fail when a zero-byte file is passed in - assert(!kmcmp_CompileKeyboardFileToBuffer(kmn_file, &fk, true, false, msgproc, nullptr, CKF_KEYMAN)); + KMCMP_COMPILER_RESULT result; + KMCMP_COMPILER_OPTIONS options; + options.saveDebug = true; + options.compilerWarningsAsErrors = false; + options.warnDeprecatedCode = true; + options.shouldAddCompilerVersion = false; + options.target = CKF_KEYMAN; + assert(!kmcmp_CompileKeyboard(kmn_file, options, msgproc, loadfileProc, nullptr, result)); assert(error_vec.size() == 1); assert(error_vec[0] == CERR_CannotReadInfile); diff --git a/developer/src/kmcmplib/tests/fixtures/adiga_danef.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/adiga_danef.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/adiga_danef.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/adiga_danef.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ahom_star.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ahom_star.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ahom_star.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ahom_star.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/aksarabali_panlex.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/aksarabali_panlex.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/aksarabali_panlex.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/aksarabali_panlex.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/alephwithbeth.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/alephwithbeth.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/alephwithbeth.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/alephwithbeth.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/anii.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/anii.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/anii.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/anii.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/arabic_izza.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/arabic_izza.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/arabic_izza.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/arabic_izza.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/aramaic_hebrew.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/aramaic_hebrew.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/aramaic_hebrew.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/aramaic_hebrew.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/armenian_mnemonic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/armenian_mnemonic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/armenian_mnemonic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/armenian_mnemonic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/armenian_mnemonic_r.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/armenian_mnemonic_r.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/armenian_mnemonic_r.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/armenian_mnemonic_r.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/athinkra_vai.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/athinkra_vai.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/athinkra_vai.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/athinkra_vai.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/athinkra_vai_typewriter.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/athinkra_vai_typewriter.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/athinkra_vai_typewriter.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/athinkra_vai_typewriter.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ausephon.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ausephon.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ausephon.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ausephon.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/balochi_inpage.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_inpage.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/balochi_inpage.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_inpage.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/balochi_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/balochi_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/balochi_persian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_persian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/balochi_persian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_persian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/balochi_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/balochi_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/balochi_scientific.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_scientific.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/balochi_scientific.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_scientific.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/balochi_urdu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_urdu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/balochi_urdu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/balochi_urdu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bangla_joy.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bangla_joy.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bangla_joy.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bangla_joy.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bangla_munir.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bangla_munir.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bangla_munir.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bangla_munir.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bangla_probhat.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bangla_probhat.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bangla_probhat.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bangla_probhat.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbda1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbda1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbda1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbda1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbda2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbda2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbda2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbda2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbda3.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbda3.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbda3.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbda3.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdadlm.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdadlm.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdadlm.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdadlm.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdal.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdal.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdal.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdal.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdarme.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarme.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdarme.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarme.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdarmph.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarmph.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdarmph.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarmph.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdarmty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarmty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdarmty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarmty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdarmw.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarmw.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdarmw.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdarmw.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdaze.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdaze.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdaze.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdaze.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdazel.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdazel.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdazel.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdazel.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdazst.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdazst.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdazst.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdazst.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbash.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbash.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbash.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbash.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbe.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbe.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbe.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbe.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbene.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbene.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbene.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbene.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbgph.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbgph.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbgph.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbgph.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbgph1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbgph1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbgph1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbgph1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbhc.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbhc.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbhc.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbhc.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdblr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdblr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdblr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdblr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbug.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbug.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbug.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbug.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdbulg.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbulg.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdbulg.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdbulg.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdca.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdca.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdca.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdca.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcher.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcher.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcher.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcher.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcherp.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcherp.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcherp.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcherp.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcz.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcz.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcz.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcz.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcz1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcz1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcz1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcz1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdcz2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcz2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdcz2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdcz2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdda.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdda.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdda.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdda.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbddiv1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddiv1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbddiv1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddiv1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbddiv2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddiv2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbddiv2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddiv2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbddv.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddv.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbddv.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddv.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbddzo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddzo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbddzo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbddzo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdes.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdes.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdes.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdes.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdest.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdest.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdest.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdest.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfi1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfi1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfi1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfi1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdfthrk.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfthrk.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdfthrk.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdfthrk.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgae.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgae.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgae.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgae.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgeo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgeo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgeoer.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeoer.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgeoer.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeoer.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgeome.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeome.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgeome.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeome.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgeooa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeooa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgeooa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeooa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgeoqw.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeoqw.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgeoqw.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgeoqw.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgkl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgkl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgkl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgkl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgn.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgn.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgn.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgn.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgr1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgr1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgr1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgr1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgrlnd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgrlnd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgrlnd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgrlnd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdgthc.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgthc.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdgthc.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdgthc.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhau.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhau.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhau.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhau.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhaw.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhaw.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhaw.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhaw.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhe.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhe.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhe.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhe.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhe220.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhe220.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhe220.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhe220.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhe319.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhe319.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhe319.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhe319.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdheb.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdheb.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdheb.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdheb.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhebl3.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhebl3.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhebl3.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhebl3.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhela2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhela2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhela2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhela2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhela3.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhela3.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhela3.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhela3.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhept.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhept.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhept.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhept.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdhu1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhu1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdhu1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdhu1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdibo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdibo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdibo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdibo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinasa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinasa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinasa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinasa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinbe2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinbe2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinbe2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinbe2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinben.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinben.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinben.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinben.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdindev.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdindev.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdindev.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdindev.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinen.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinen.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinen.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinen.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinguj.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinguj.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinguj.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinguj.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinhin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinhin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinhin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinhin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinkan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinkan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinkan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinkan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinmal.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinmal.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinmal.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinmal.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinmar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinmar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinmar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinmar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinori.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinori.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinori.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinori.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinpun.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinpun.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinpun.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinpun.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdintam.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdintam.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdintam.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdintam.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdintel.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdintel.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdintel.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdintel.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdinuk2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinuk2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdinuk2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdinuk2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdir.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdir.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdir.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdir.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdit.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdit.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdit.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdit.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdit142.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdit142.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdit142.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdit142.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdiulat.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdiulat.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdiulat.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdiulat.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdjav.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdjav.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdjav.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdjav.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdkaz.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkaz.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdkaz.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkaz.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdkhmr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkhmr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdkhmr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkhmr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdkni.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkni.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdkni.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkni.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdkurd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkurd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdkurd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkurd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdkyr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkyr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdkyr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdkyr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdla.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdla.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdla.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdla.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlao.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlao.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlao.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlao.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlisub.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlisub.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlisub.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlisub.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlisus.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlisus.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlisus.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlisus.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlt.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlt.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlt.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlt.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlt1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlt1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlt1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlt1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlt2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlt2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlt2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlt2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlv.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlv.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlv.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlv.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlv1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlv1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlv1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlv1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdlvst.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlvst.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdlvst.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdlvst.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmac.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmac.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmac.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmac.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmacst.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmacst.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmacst.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmacst.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmaori.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmaori.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmaori.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmaori.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmlt47.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmlt47.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmlt47.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmlt47.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmlt48.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmlt48.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmlt48.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmlt48.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmon.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmon.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmon.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmon.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmonmo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmonmo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmonmo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmonmo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmonst.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmonst.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmonst.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmonst.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdmyan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmyan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdmyan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdmyan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdne.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdne.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdne.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdne.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdnepr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdnepr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdnepr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdnepr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdnko.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdnko.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdnko.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdnko.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdno.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdno.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdno.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdno.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdno1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdno1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdno1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdno1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdnso.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdnso.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdnso.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdnso.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdntl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdntl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdntl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdntl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdogham.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdogham.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdogham.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdogham.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdolch.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdolch.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdolch.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdolch.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdoldit.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdoldit.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdoldit.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdoldit.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdosa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdosa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdosa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdosa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdosm.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdosm.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdosm.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdosm.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdpash.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpash.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdpash.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpash.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdphags.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdphags.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdphags.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdphags.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdpl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdpl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdpl1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpl1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdpl1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpl1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdpo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdpo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdpo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdropr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdropr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdropr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdropr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdrost.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdrost.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdrost.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdrost.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdru.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdru.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdru.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdru.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdru1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdru1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdru1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdru1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdrum.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdrum.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdrum.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdrum.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsf.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsf.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsf.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsf.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsg.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsg.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsg.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsg.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsl1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsl1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsl1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsl1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsmsfi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsmsfi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsmsfi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsmsfi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsmsno.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsmsno.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsmsno.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsmsno.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsn1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsn1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsn1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsn1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsora.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsora.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsora.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsora.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsorex.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsorex.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsorex.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsorex.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsors1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsors1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsors1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsors1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsp.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsp.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsp.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsp.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsw.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsw.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsw.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsw.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsw09.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsw09.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsw09.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsw09.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsyr1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsyr1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsyr1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsyr1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdsyr2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsyr2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdsyr2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdsyr2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtaile.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtaile.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtaile.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtaile.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtajik.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtajik.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtajik.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtajik.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtam99.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtam99.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtam99.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtam99.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdth0.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth0.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdth0.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth0.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdth1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdth1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdth2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdth2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdth3.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth3.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdth3.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdth3.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtifi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtifi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtifi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtifi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtifi2.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtifi2.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtifi2.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtifi2.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtiprd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtiprd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtiprd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtiprd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtt102.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtt102.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtt102.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtt102.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtuf.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtuf.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtuf.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtuf.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtuq.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtuq.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtuq.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtuq.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdturme.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdturme.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdturme.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdturme.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdtzm.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtzm.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdtzm.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdtzm.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdughr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdughr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdughr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdughr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdughr1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdughr1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdughr1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdughr1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbduk.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbduk.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbduk.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbduk.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdukx.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdukx.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdukx.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdukx.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdur.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdur.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdur.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdur.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdur1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdur1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdur1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdur1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdurdu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdurdu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdurdu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdurdu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdus.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdus.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdus.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdus.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdusa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdusa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdusl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdusl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdusr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdusr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdusx.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusx.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdusx.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdusx.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbduzb.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbduzb.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbduzb.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbduzb.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdvntc.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdvntc.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdvntc.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdvntc.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdwol.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdwol.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdwol.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdwol.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdyak.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdyak.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdyak.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdyak.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdyba.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdyba.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdyba.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdyba.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdycc.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdycc.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdycc.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdycc.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/basic_kbdycl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdycl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/basic_kbdycl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/basic_kbdycl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/batak.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/batak.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/batak.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/batak.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/baybayin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/baybayin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/baybayin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/baybayin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bhaiksuki_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bhaiksuki_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bhaiksuki_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bhaiksuki_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_cree_east.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_east.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_cree_east.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_east.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_cree_east_james_bay.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_east_james_bay.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_cree_east_james_bay.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_east_james_bay.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_cree_east_latn.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_east_latn.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_cree_east_latn.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_east_latn.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_cree_west_latn.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_west_latn.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_cree_west_latn.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_west_latn.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_cree_woods.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_woods.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_cree_woods.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_cree_woods.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_innu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_innu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_innu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_innu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_innu_phonemic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_innu_phonemic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_innu_phonemic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_innu_phonemic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_mista_wasaha_cree.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_mista_wasaha_cree.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_mista_wasaha_cree.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_mista_wasaha_cree.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_naskapi_classic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_naskapi_classic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_naskapi_classic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_naskapi_classic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_naskapi_common.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_naskapi_common.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_naskapi_common.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_naskapi_common.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bj_oji_cree.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_oji_cree.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bj_oji_cree.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bj_oji_cree.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/brahmi_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/brahmi_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/brahmi_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/brahmi_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/btl_kenya.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/btl_kenya.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/btl_kenya.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/btl_kenya.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/bu_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/bu_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/bu_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/bu_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/buhid.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/buhid.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/buhid.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/buhid.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/burushaski_girminas.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/burushaski_girminas.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/burushaski_girminas.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/burushaski_girminas.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/cabecar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/cabecar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/cabecar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/cabecar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/carian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/carian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/carian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/carian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/chalchiteko.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/chalchiteko.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/chalchiteko.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/chalchiteko.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/chechen_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/chechen_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/chechen_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/chechen_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/chinuk_wawa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/chinuk_wawa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/chinuk_wawa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/chinuk_wawa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/choctaw_modern.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/choctaw_modern.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/choctaw_modern.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/choctaw_modern.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/cim.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/cim.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/cim.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/cim.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/clavbur9.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/clavbur9.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/clavbur9.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/clavbur9.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/colchis_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/colchis_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/colchis_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/colchis_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/colchis_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/colchis_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/colchis_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/colchis_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/common_devanagari.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/common_devanagari.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/common_devanagari.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/common_devanagari.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/coptic_greek.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/coptic_greek.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/coptic_greek.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/coptic_greek.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/coptic_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/coptic_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/coptic_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/coptic_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/dagbani.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/dagbani.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/dagbani.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/dagbani.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/dega.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/dega.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/dega.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/dega.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/dene.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/dene.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/dene.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/dene.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/deseret.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/deseret.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/deseret.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/deseret.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/dogra_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/dogra_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/dogra_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/dogra_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/easy_chakma.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/easy_chakma.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/easy_chakma.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/easy_chakma.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ekwtamil99uni.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ekwtamil99uni.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ekwtamil99uni.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ekwtamil99uni.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/el_dinka.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_dinka.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/el_dinka.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_dinka.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/el_harari_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_harari_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/el_harari_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_harari_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/el_naija.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_naija.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/el_naija.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_naija.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/el_nuer.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_nuer.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/el_nuer.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_nuer.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/el_pasifika.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_pasifika.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/el_pasifika.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_pasifika.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/el_yolngu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_yolngu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/el_yolngu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/el_yolngu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/embera_north.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/embera_north.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/embera_north.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/embera_north.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/enga.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/enga.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/enga.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/enga.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/enggano.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/enggano.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/enggano.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/enggano.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/english_shavian_igc.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/english_shavian_igc.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/english_shavian_igc.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/english_shavian_igc.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/english_shavian_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/english_shavian_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/english_shavian_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/english_shavian_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/engram.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/engram.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/engram.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/engram.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/esperuni.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/esperuni.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/esperuni.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/esperuni.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/farsiman.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/farsiman.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/farsiman.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/farsiman.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/finongan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/finongan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/finongan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/finongan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fulfulde_ajami_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fulfulde_ajami_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fulfulde_ajami_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fulfulde_ajami_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fulfulde_latin_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fulfulde_latin_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fulfulde_latin_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fulfulde_latin_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_dakelh.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_dakelh.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_dakelh.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_dakelh.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_dane_zaa_zaage.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_dane_zaa_zaage.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_dane_zaa_zaage.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_dane_zaa_zaage.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_denesuline_epsilon.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_denesuline_epsilon.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_denesuline_epsilon.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_denesuline_epsilon.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_diitiidatx.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_diitiidatx.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_diitiidatx.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_diitiidatx.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_gitsenimx.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_gitsenimx.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_gitsenimx.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_gitsenimx.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_gwichin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_gwichin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_gwichin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_gwichin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_hailzaqvla.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_hailzaqvla.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_hailzaqvla.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_hailzaqvla.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_haisla.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_haisla.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_haisla.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_haisla.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_halqemeylem.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_halqemeylem.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_halqemeylem.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_halqemeylem.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_han.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_han.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_han.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_han.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_henqeminem.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_henqeminem.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_henqeminem.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_henqeminem.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_hlgaagilda_xaayda_kil.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_hlgaagilda_xaayda_kil.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_hlgaagilda_xaayda_kil.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_hlgaagilda_xaayda_kil.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_kanienkeha_e.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_kanienkeha_e.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_kanienkeha_e.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_kanienkeha_e.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_ktunaxa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_ktunaxa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_ktunaxa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_ktunaxa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_kwakwala.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_kwakwala.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_kwakwala.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_kwakwala.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_kwakwala_liqwala.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_kwakwala_liqwala.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_kwakwala_liqwala.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_kwakwala_liqwala.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_migmaq.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_migmaq.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_migmaq.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_migmaq.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_natwits.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_natwits.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_natwits.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_natwits.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_nisgaa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nisgaa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_nisgaa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nisgaa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_nlekepmxcin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nlekepmxcin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_nlekepmxcin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nlekepmxcin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_northern_tutchone.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_northern_tutchone.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_northern_tutchone.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_northern_tutchone.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_nsilxcen.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nsilxcen.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_nsilxcen.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nsilxcen.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_nuucaanul.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nuucaanul.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_nuucaanul.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_nuucaanul.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_secwepemctsin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_secwepemctsin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_secwepemctsin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_secwepemctsin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_sencoten.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_sencoten.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_sencoten.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_sencoten.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_shashishalhem.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_shashishalhem.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_shashishalhem.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_shashishalhem.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_smalgyax.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_smalgyax.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_smalgyax.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_smalgyax.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_southern_tutchone.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_southern_tutchone.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_southern_tutchone.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_southern_tutchone.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_statimcets.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_statimcets.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_statimcets.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_statimcets.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_stlatlimxec.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_stlatlimxec.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_stlatlimxec.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_stlatlimxec.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_tagizi_dene.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tagizi_dene.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_tagizi_dene.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tagizi_dene.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_taltan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_taltan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_taltan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_taltan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_tlingit.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tlingit.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_tlingit.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tlingit.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_tsekehne.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tsekehne.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_tsekehne.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tsekehne.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_tsilhqotin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tsilhqotin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_tsilhqotin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_tsilhqotin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_uwikala.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_uwikala.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_uwikala.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_uwikala.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/fv_xaislakala.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_xaislakala.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/fv_xaislakala.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/fv_xaislakala.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/galaxie_greek_mnemonic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_greek_mnemonic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/galaxie_greek_mnemonic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_greek_mnemonic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/galaxie_greek_positional.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_greek_positional.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/galaxie_greek_positional.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_greek_positional.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/galaxie_hebrew_mnemonic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_hebrew_mnemonic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/galaxie_hebrew_mnemonic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_hebrew_mnemonic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/galaxie_hebrew_positional.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_hebrew_positional.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/galaxie_hebrew_positional.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/galaxie_hebrew_positional.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gandhari.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gandhari.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gandhari.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gandhari.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/geezbrhan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/geezbrhan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/geezbrhan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/geezbrhan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_amh_7.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_amh_7.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_amh_7.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_amh_7.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_amharic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_amharic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_amharic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_amharic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_blin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_blin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_blin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_blin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_ethiopic_7.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_ethiopic_7.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_ethiopic_7.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_ethiopic_7.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_geez.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_geez.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_geez.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_geez.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_gurage.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_gurage.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_gurage.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_gurage.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_gurage_legacy.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_gurage_legacy.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_gurage_legacy.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_gurage_legacy.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_musnad.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_musnad.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_musnad.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_musnad.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_tigrinya_eritrea.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_tigrinya_eritrea.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_tigrinya_eritrea.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_tigrinya_eritrea.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gff_tigrinya_ethiopia.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_tigrinya_ethiopia.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gff_tigrinya_ethiopia.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gff_tigrinya_ethiopia.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ghana.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ghana.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ghana.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ghana.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gilaki.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gilaki.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gilaki.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gilaki.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gilaki_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gilaki_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gilaki_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gilaki_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gondi_dev.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gondi_dev.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gondi_dev.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gondi_dev.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gondi_gunjala.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gondi_gunjala.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gondi_gunjala.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gondi_gunjala.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/gondi_tel.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/gondi_tel.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/gondi_tel.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/gondi_tel.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/greekclassical.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/greekclassical.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/greekclassical.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/greekclassical.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hanunoo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hanunoo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hanunoo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hanunoo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/haroi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/haroi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/haroi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/haroi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hatran_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hatran_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hatran_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hatran_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hausa_ajami_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hausa_ajami_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hausa_ajami_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hausa_ajami_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hausa_kano.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hausa_kano.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hausa_kano.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hausa_kano.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hcesar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hcesar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hcesar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hcesar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hieroglyphic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hieroglyphic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hieroglyphic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hieroglyphic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/himyarit_musnad.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/himyarit_musnad.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/himyarit_musnad.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/himyarit_musnad.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/hindi_modular.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/hindi_modular.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/hindi_modular.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/hindi_modular.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/indigenous_nt.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/indigenous_nt.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/indigenous_nt.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/indigenous_nt.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/indonesia.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/indonesia.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/indonesia.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/indonesia.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/indonesian_suku.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/indonesian_suku.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/indonesian_suku.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/indonesian_suku.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/inuktitut_naqittaut.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/inuktitut_naqittaut.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/inuktitut_naqittaut.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/inuktitut_naqittaut.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ishkashimi_cyrillic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ishkashimi_cyrillic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ishkashimi_cyrillic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ishkashimi_cyrillic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_bengali.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_bengali.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_bengali.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_bengali.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_devanagari_hindi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_devanagari_hindi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_devanagari_hindi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_devanagari_hindi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_devanagari_sanskrit_vedic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_devanagari_sanskrit_vedic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_devanagari_sanskrit_vedic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_devanagari_sanskrit_vedic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_gujarati.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_gujarati.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_gujarati.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_gujarati.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_gurmukhi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_gurmukhi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_gurmukhi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_gurmukhi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_odia.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_odia.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_odia.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_odia.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/itrans_roman.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_roman.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/itrans_roman.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/itrans_roman.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/jawa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/jawa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/jawa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/jawa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/jorai.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/jorai.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/jorai.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/jorai.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/karakalpak_cyrillic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/karakalpak_cyrillic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/karakalpak_cyrillic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/karakalpak_cyrillic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/karakalpak_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/karakalpak_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/karakalpak_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/karakalpak_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/kayan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/kayan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/kayan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/kayan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/kbdsn1.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/kbdsn1.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/kbdsn1.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/kbdsn1.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/kharoshthi_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/kharoshthi_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/kharoshthi_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/kharoshthi_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/khmer_advanced.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/khmer_advanced.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/khmer_advanced.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/khmer_advanced.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/khmer_angkor.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/khmer_angkor.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/khmer_angkor.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/khmer_angkor.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/khojki_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/khojki_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/khojki_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/khojki_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/kmhmu_2008.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/kmhmu_2008.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/kmhmu_2008.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/kmhmu_2008.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/koalibrere.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/koalibrere.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/koalibrere.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/koalibrere.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/korean_rr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/korean_rr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/korean_rr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/korean_rr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/koreguaje.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/koreguaje.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/koreguaje.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/koreguaje.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/krung.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/krung.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/krung.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/krung.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lahu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lahu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lahu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lahu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lamkaang.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lamkaang.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lamkaang.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lamkaang.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/landuma.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/landuma.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/landuma.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/landuma.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lao_2008_basic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_2008_basic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lao_2008_basic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_2008_basic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lao_2008_rapid.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_2008_rapid.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lao_2008_rapid.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_2008_rapid.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lao_pali.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_pali.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lao_pali.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_pali.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lao_pali_us.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_pali_us.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lao_pali_us.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_pali_us.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lao_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lao_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lao_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/lazuri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/lazuri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/lazuri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/lazuri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/libtralo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/libtralo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/libtralo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/libtralo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/makasar_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/makasar_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/makasar_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/makasar_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/malar_braille.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_braille.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/malar_braille.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_braille.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/malar_malayalam.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_malayalam.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/malar_malayalam.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_malayalam.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/malar_malayalam_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_malayalam_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/malar_malayalam_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_malayalam_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/malar_tirhuta.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_tirhuta.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/malar_tirhuta.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/malar_tirhuta.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/maltese.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/maltese.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/maltese.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/maltese.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/mandaic_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/mandaic_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/mandaic_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/mandaic_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/masaram_gondi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/masaram_gondi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/masaram_gondi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/masaram_gondi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/me_en.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/me_en.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/me_en.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/me_en.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/meitei_legacy.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/meitei_legacy.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/meitei_legacy.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/meitei_legacy.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/miluk_hanis_siuslaw.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/miluk_hanis_siuslaw.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/miluk_hanis_siuslaw.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/miluk_hanis_siuslaw.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/modi_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/modi_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/modi_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/modi_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/mon_anonta.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/mon_anonta.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/mon_anonta.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/mon_anonta.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/mon_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/mon_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/mon_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/mon_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/mongolian_cyrillic_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/mongolian_cyrillic_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/mongolian_cyrillic_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/mongolian_cyrillic_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/mozhi_malayalam.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/mozhi_malayalam.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/mozhi_malayalam.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/mozhi_malayalam.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/mro_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/mro_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/mro_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/mro_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/multani_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/multani_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/multani_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/multani_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/multi_pak_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/multi_pak_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/multi_pak_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/multi_pak_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/munji.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/munji.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/munji.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/munji.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/myancode_san.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/myancode_san.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/myancode_san.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/myancode_san.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nabataean_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nabataean_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nabataean_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nabataean_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/naijatype.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/naijatype.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/naijatype.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/naijatype.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nailangs.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nailangs.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nailangs.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nailangs.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nasa_yuwe.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nasa_yuwe.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nasa_yuwe.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nasa_yuwe.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nepali_traditional.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nepali_traditional.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nepali_traditional.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nepali_traditional.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/newa_romanized.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/newa_romanized.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/newa_romanized.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/newa_romanized.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/newa_traditional.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/newa_traditional.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/newa_traditional.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/newa_traditional.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nias.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nias.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nias.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nias.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nisenan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nisenan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nisenan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nisenan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nko.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nko.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nko.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nko.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nkonya.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nkonya.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nkonya.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nkonya.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_bengali_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_bengali_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_bengali_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_bengali_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_devanagari_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_devanagari_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_devanagari_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_devanagari_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_gujarati_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_gujarati_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_gujarati_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_gujarati_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_gurmukhi_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_gurmukhi_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_gurmukhi_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_gurmukhi_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_ipa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_ipa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_ipa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_ipa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_kannada_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_kannada_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_kannada_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_kannada_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_malayalam_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_malayalam_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_malayalam_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_malayalam_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_oriya_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_oriya_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_oriya_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_oriya_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_tamil_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_tamil_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_tamil_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_tamil_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nlci_telugu_winscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_telugu_winscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nlci_telugu_winscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nlci_telugu_winscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nrc_makah.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nrc_makah.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nrc_makah.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nrc_makah.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ntl_onekey.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ntl_onekey.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ntl_onekey.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ntl_onekey.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/numanggang.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/numanggang.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/numanggang.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/numanggang.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/nw_iranian_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/nw_iranian_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/nw_iranian_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/nw_iranian_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/o_tissi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/o_tissi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/o_tissi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/o_tissi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/obolo_chwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/obolo_chwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/obolo_chwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/obolo_chwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/obolo_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/obolo_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/obolo_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/obolo_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/old_hungarian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/old_hungarian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/old_hungarian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/old_hungarian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/old_turkic_udw21_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/old_turkic_udw21_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/old_turkic_udw21_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/old_turkic_udw21_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/orma.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/orma.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/orma.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/orma.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/osage_nation.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/osage_nation.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/osage_nation.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/osage_nation.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/osage_nation_new.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/osage_nation_new.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/osage_nation_new.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/osage_nation_new.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/otoe_missouria.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/otoe_missouria.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/otoe_missouria.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/otoe_missouria.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/persian_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/persian_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/persian_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/persian_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/pid_piaroa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/pid_piaroa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/pid_piaroa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/pid_piaroa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/pingelap.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/pingelap.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/pingelap.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/pingelap.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/postmodern_english_uk_dualstroke.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_uk_dualstroke.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/postmodern_english_uk_dualstroke.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_uk_dualstroke.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/postmodern_english_uk_natural.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_uk_natural.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/postmodern_english_uk_natural.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_uk_natural.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/postmodern_english_us_dualstroke.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_us_dualstroke.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/postmodern_english_us_dualstroke.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_us_dualstroke.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/postmodern_english_us_natural.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_us_natural.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/postmodern_english_us_natural.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/postmodern_english_us_natural.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/pukapuka.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/pukapuka.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/pukapuka.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/pukapuka.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/qom.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/qom.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/qom.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/qom.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/quinault.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/quinault.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/quinault.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/quinault.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/qwerty_farang.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/qwerty_farang.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/qwerty_farang.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/qwerty_farang.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_aer.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_aer.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_aer.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_aer.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_arabic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_arabic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_arabic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_arabic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_balti.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_balti.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_balti.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_balti.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_brahui.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_brahui.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_brahui.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_brahui.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_brahui_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_brahui_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_brahui_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_brahui_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_burushaski.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_burushaski.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_burushaski.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_burushaski.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_dameli.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_dameli.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_dameli.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_dameli.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_dhatki.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_dhatki.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_dhatki.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_dhatki.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_dogri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_dogri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_dogri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_dogri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_gawar_bati.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_gawar_bati.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_gawar_bati.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_gawar_bati.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_gawri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_gawri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_gawri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_gawri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_hazaragi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_hazaragi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_hazaragi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_hazaragi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_hindko.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_hindko.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_hindko.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_hindko.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_indus_kohistani.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_indus_kohistani.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_indus_kohistani.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_indus_kohistani.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_kalasha.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_kalasha.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_kalasha.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_kalasha.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_kashmir_shina.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_kashmir_shina.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_kashmir_shina.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_kashmir_shina.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_kashmiri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_kashmiri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_kashmiri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_kashmiri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_khowar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_khowar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_khowar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_khowar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_marwari.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_marwari.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_marwari.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_marwari.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_munji.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_munji.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_munji.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_munji.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_oadki.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_oadki.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_oadki.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_oadki.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_ormuri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_ormuri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_ormuri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_ormuri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_pahari.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_pahari.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_pahari.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_pahari.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_palula.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_palula.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_palula.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_palula.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_parkari_koli.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_parkari_koli.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_parkari_koli.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_parkari_koli.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_pashai.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_pashai.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_pashai.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_pashai.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_pashto.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_pashto.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_pashto.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_pashto.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_saraiki.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_saraiki.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_saraiki.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_saraiki.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_shina.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_shina.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_shina.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_shina.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_sindhi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_sindhi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_sindhi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_sindhi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_torwali.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_torwali.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_torwali.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_torwali.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_urdu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_urdu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_urdu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_urdu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_ushojo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_ushojo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_ushojo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_ushojo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_uyghur.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_uyghur.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_uyghur.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_uyghur.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_wadiyara.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_wadiyara.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_wadiyara.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_wadiyara.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_wakhi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_wakhi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_wakhi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_wakhi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_western_punjabi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_western_punjabi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_western_punjabi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_western_punjabi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rac_yidgha.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_yidgha.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rac_yidgha.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rac_yidgha.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rawang.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rawang.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rawang.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rawang.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rejang.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rejang.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rejang.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rejang.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/remington_gail.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/remington_gail.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/remington_gail.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/remington_gail.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rohingya_arab.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rohingya_arab.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rohingya_arab.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rohingya_arab.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/rossel.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/rossel.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/rossel.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/rossel.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/runeboard.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/runeboard.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/runeboard.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/runeboard.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/russian_mnemonic_r.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/russian_mnemonic_r.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/russian_mnemonic_r.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/russian_mnemonic_r.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sabdalipi_assamese.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sabdalipi_assamese.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sabdalipi_assamese.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sabdalipi_assamese.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sahaptin_umatilla.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sahaptin_umatilla.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sahaptin_umatilla.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sahaptin_umatilla.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sahaptin_yakima.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sahaptin_yakima.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sahaptin_yakima.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sahaptin_yakima.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sanjha_punjabi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sanjha_punjabi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sanjha_punjabi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sanjha_punjabi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/santali_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/santali_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/santali_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/santali_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/saraiki.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/saraiki.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/saraiki.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/saraiki.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/satere.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/satere.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/satere.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/satere.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/shahmukhi_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/shahmukhi_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/shahmukhi_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/shahmukhi_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/shan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/shan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/shan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/shan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/shaw_2layer.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/shaw_2layer.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/shaw_2layer.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/shaw_2layer.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/siddham_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/siddham_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/siddham_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/siddham_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_akha_act.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_akha_act.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_akha_act.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_akha_act.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_arabic_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_arabic_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_arabic_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_arabic_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_areare.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_areare.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_areare.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_areare.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_bari.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bari.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_bari.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bari.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_bengali_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bengali_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_bengali_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bengali_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_bolivia.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bolivia.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_bolivia.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bolivia.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_boonkit.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_boonkit.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_boonkit.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_boonkit.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_brao.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_brao.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_brao.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_brao.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_bru.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bru.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_bru.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bru.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_buang.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_buang.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_buang.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_buang.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_bunong.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bunong.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_bunong.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bunong.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_busa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_busa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_busa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_busa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_bwe_karen.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bwe_karen.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_bwe_karen.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_bwe_karen.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_cameroon_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cameroon_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_cameroon_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cameroon_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_cameroon_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cameroon_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_cameroon_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cameroon_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_cherokee_nation.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cherokee_nation.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_cherokee_nation.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cherokee_nation.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_cheyenne.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cheyenne.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_cheyenne.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cheyenne.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_cipher_music.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cipher_music.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_cipher_music.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_cipher_music.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_devanagari_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_devanagari_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_devanagari_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_devanagari_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_devanagari_romanized.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_devanagari_romanized.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_devanagari_romanized.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_devanagari_romanized.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_devanagari_typewriter.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_devanagari_typewriter.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_devanagari_typewriter.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_devanagari_typewriter.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_dzongkha.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_dzongkha.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_dzongkha.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_dzongkha.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_eastern_congo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_eastern_congo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_eastern_congo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_eastern_congo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_el_ethiopian_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_el_ethiopian_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_el_ethiopian_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_el_ethiopian_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_ethiopic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ethiopic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_ethiopic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ethiopic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_ethiopic_power_g.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ethiopic_power_g.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_ethiopic_power_g.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ethiopic_power_g.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_euro_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_euro_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_euro_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_euro_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_extended_urdu_np.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_extended_urdu_np.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_extended_urdu_np.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_extended_urdu_np.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_greek_polytonic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_greek_polytonic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_greek_polytonic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_greek_polytonic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_hawaiian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hawaiian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_hawaiian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hawaiian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_hebr_grek_trans.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hebr_grek_trans.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_hebr_grek_trans.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hebr_grek_trans.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_hebrew.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hebrew.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_hebrew.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hebrew.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_hebrew_legacy.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hebrew_legacy.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_hebrew_legacy.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hebrew_legacy.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_hmd_plrd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hmd_plrd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_hmd_plrd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_hmd_plrd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_indic_roman.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_indic_roman.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_indic_roman.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_indic_roman.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_ipa.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ipa.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_ipa.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ipa.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_jarai.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_jarai.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_jarai.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_jarai.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_kayah_kali.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kayah_kali.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_kayah_kali.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kayah_kali.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_kayah_latn.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kayah_latn.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_kayah_latn.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kayah_latn.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_kayah_mymr.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kayah_mymr.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_kayah_mymr.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kayah_mymr.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_khamti.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_khamti.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_khamti.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_khamti.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_khmer.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_khmer.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_khmer.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_khmer.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_khowar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_khowar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_khowar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_khowar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_kmhmu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kmhmu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_kmhmu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kmhmu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_korda_jamo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_korda_jamo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_korda_jamo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_korda_jamo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_korda_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_korda_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_korda_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_korda_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_korean_morse.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_korean_morse.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_korean_morse.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_korean_morse.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_kvl_kayaw.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kvl_kayaw.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_kvl_kayaw.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_kvl_kayaw.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_lepcha.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lepcha.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_lepcha.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lepcha.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_limbu_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_limbu_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_limbu_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_limbu_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_limbu_typewriter.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_limbu_typewriter.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_limbu_typewriter.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_limbu_typewriter.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_lisu_basic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lisu_basic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_lisu_basic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lisu_basic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_lisu_standard.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lisu_standard.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_lisu_standard.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lisu_standard.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_lpo_plrd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lpo_plrd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_lpo_plrd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_lpo_plrd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_madi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_madi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_madi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_madi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_makuri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_makuri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_makuri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_makuri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_mali_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_mali_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_mali_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_mali_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_mali_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_mali_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_mali_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_mali_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_mali_qwertz.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_mali_qwertz.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_mali_qwertz.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_mali_qwertz.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_moore.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_moore.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_moore.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_moore.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_myanmar_my3.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_myanmar_my3.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_myanmar_my3.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_myanmar_my3.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_myanmar_mywinext.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_myanmar_mywinext.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_myanmar_mywinext.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_myanmar_mywinext.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_nigeria_dot.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nigeria_dot.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_nigeria_dot.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nigeria_dot.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_nigeria_odd_vowels.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nigeria_odd_vowels.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_nigeria_odd_vowels.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nigeria_odd_vowels.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_nigeria_underline.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nigeria_underline.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_nigeria_underline.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nigeria_underline.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_nko.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nko.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_nko.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nko.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_nubian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nubian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_nubian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_nubian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_pan_africa_mnemonic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_pan_africa_mnemonic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_pan_africa_mnemonic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_pan_africa_mnemonic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_pan_africa_positional.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_pan_africa_positional.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_pan_africa_positional.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_pan_africa_positional.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_philippines.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_philippines.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_philippines.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_philippines.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_sahu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_sahu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_sahu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_sahu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_bsc_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_bsc_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_bsc_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_bsc_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_cou_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_cou_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_cou_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_cou_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_dyo_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_dyo_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_dyo_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_dyo_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_gsl_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_gsl_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_gsl_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_gsl_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_krx_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_krx_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_krx_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_krx_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_ndv_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_ndv_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_ndv_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_ndv_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_sav_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_sav_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_sav_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_sav_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_snf_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_snf_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_snf_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_snf_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_srr_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_srr_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_srr_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_srr_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_senegal_wo_azerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_wo_azerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_senegal_wo_azerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_senegal_wo_azerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_sgaw_karen.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_sgaw_karen.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_sgaw_karen.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_sgaw_karen.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_shan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_shan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_shan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_shan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tai_dam.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tai_dam.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tai_dam_lao.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam_lao.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tai_dam_lao.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam_lao.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tai_dam_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tai_dam_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tai_dam_typewriter.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam_typewriter.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tai_dam_typewriter.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tai_dam_typewriter.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tawallammat.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tawallammat.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tawallammat.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tawallammat.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tchad.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tchad.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tchad.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tchad.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tepehuan.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tepehuan.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tepehuan.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tepehuan.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_torwali.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_torwali.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_torwali.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_torwali.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_tunisian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tunisian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_tunisian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_tunisian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_uganda_tanzania.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_uganda_tanzania.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_uganda_tanzania.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_uganda_tanzania.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_vai.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_vai.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_vai.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_vai.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_wayuu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_wayuu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_wayuu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_wayuu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_ygp_plrd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ygp_plrd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_ygp_plrd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ygp_plrd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yna_plrd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yna_plrd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yna_plrd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yna_plrd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yoruba8.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yoruba8.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yoruba8.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yoruba8.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yoruba_bar.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yoruba_bar.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yoruba_bar.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yoruba_bar.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yoruba_dot.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yoruba_dot.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yoruba_dot.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yoruba_dot.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yupik_cyrillic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yupik_cyrillic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yupik_cyrillic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yupik_cyrillic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_yupik_cyrillic_ru.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yupik_cyrillic_ru.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_yupik_cyrillic_ru.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_yupik_cyrillic_ru.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sil_ywq_plrd.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ywq_plrd.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sil_ywq_plrd.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sil_ywq_plrd.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/slc_saliba.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/slc_saliba.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/slc_saliba.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/slc_saliba.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/soqotri_arabic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/soqotri_arabic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/soqotri_arabic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/soqotri_arabic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/srr_ajami_qwerty.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/srr_ajami_qwerty.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/srr_ajami_qwerty.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/srr_ajami_qwerty.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sundanese.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sundanese.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sundanese.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sundanese.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sundanese_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sundanese_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sundanese_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sundanese_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/swanalekha_malayalam.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/swanalekha_malayalam.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/swanalekha_malayalam.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/swanalekha_malayalam.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sxava.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sxava.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sxava.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sxava.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sxava_eo.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sxava_eo.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sxava_eo.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sxava_eo.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/sylheti_nagri.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/sylheti_nagri.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/sylheti_nagri.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/sylheti_nagri.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/syriac_arabic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/syriac_arabic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/syriac_arabic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/syriac_arabic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/syriac_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/syriac_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/syriac_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/syriac_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tagbanwa_inscript.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tagbanwa_inscript.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tagbanwa_inscript.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tagbanwa_inscript.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/taigi_poj.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/taigi_poj.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/taigi_poj.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/taigi_poj.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tainua.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tainua.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tainua.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tainua.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tangsa_lakhum.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tangsa_lakhum.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tangsa_lakhum.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tangsa_lakhum.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tawallammat_latin.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tawallammat_latin.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tawallammat_latin.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tawallammat_latin.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/teggargrent_lat.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/teggargrent_lat.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/teggargrent_lat.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/teggargrent_lat.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tem_kdh.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tem_kdh.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tem_kdh.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tem_kdh.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/thamizha_anjal_paangu.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_anjal_paangu.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/thamizha_anjal_paangu.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_anjal_paangu.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/thamizha_bamini.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_bamini.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/thamizha_bamini.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_bamini.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/thamizha_new_typewriter.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_new_typewriter.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/thamizha_new_typewriter.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_new_typewriter.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/thamizha_tamil99_ext.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_tamil99_ext.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/thamizha_tamil99_ext.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/thamizha_tamil99_ext.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tibetan_direct_input.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tibetan_direct_input.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tibetan_direct_input.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tibetan_direct_input.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tibetan_ewts.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tibetan_ewts.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tibetan_ewts.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tibetan_ewts.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tirhuta.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tirhuta.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tirhuta.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tirhuta.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tlahuica.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tlahuica.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tlahuica.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tlahuica.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tsakonian.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tsakonian.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tsakonian.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tsakonian.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/tuareg_tifinagh.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/tuareg_tifinagh.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/tuareg_tifinagh.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/tuareg_tifinagh.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/turkmen_cyrl.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/turkmen_cyrl.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/turkmen_cyrl.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/turkmen_cyrl.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/txo_toto.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/txo_toto.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/txo_toto.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/txo_toto.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/udi_keyboard.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/udi_keyboard.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/udi_keyboard.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/udi_keyboard.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/ukwuani.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/ukwuani.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/ukwuani.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/ukwuani.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/uma_graphic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/uma_graphic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/uma_graphic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/uma_graphic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/uma_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/uma_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/uma_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/uma_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/urdu_phonetic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/urdu_phonetic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/urdu_phonetic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/urdu_phonetic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/urdu_phonetic_crulp.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/urdu_phonetic_crulp.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/urdu_phonetic_crulp.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/urdu_phonetic_crulp.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/venetia_et_histria.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/venetia_et_histria.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/venetia_et_histria.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/venetia_et_histria.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/vm_tamil_modular.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/vm_tamil_modular.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/vm_tamil_modular.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/vm_tamil_modular.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/vm_tamil_typewriter.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/vm_tamil_typewriter.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/vm_tamil_typewriter.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/vm_tamil_typewriter.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/wakhi_anglicized.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/wakhi_anglicized.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/wakhi_anglicized.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/wakhi_anglicized.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/wakhi_cyrillic.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/wakhi_cyrillic.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/wakhi_cyrillic.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/wakhi_cyrillic.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/wakhi_standard.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/wakhi_standard.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/wakhi_standard.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/wakhi_standard.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/wancho.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/wancho.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/wancho.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/wancho.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/warang_citi.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/warang_citi.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/warang_citi.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/warang_citi.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/wolofal.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/wolofal.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/wolofal.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/wolofal.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/xinaliq.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/xinaliq.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/xinaliq.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/xinaliq.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/yiddish_pasekh.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/yiddish_pasekh.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/yiddish_pasekh.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/yiddish_pasekh.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/yidgha.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/yidgha.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/yidgha.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/yidgha.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/younger_futhark_short_twig.kmx b/developer/src/kmcmplib/tests/fixtures/keyboards-repo/younger_futhark_short_twig.kmx similarity index 100% rename from developer/src/kmcmplib/tests/fixtures/younger_futhark_short_twig.kmx rename to developer/src/kmcmplib/tests/fixtures/keyboards-repo/younger_futhark_short_twig.kmx diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/compile_legacy.bat b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/compile_legacy.bat new file mode 100644 index 0000000000000000000000000000000000000000..5e8b891ea6cf163c1e4dce6c089a835c6b153e78 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/compile_legacy.bat @@ -0,0 +1,4 @@ +@echo off +echo Compiles the keyboards using the legacy kmcomp.exe +echo to use as baseline comparisons for kmcmplib +for %%d in (*.kmn) do ..\..\..\..\..\bin\kmcomp -no-compiler-version -d %%d \ No newline at end of file diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k001_utf16.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k001_utf16.kmn new file mode 100644 index 0000000000000000000000000000000000000000..2b3180c36ab4b473e74b1b3487fc8f0a61d623f6 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k001_utf16.kmn differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k001_utf16.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k001_utf16.kmx new file mode 100644 index 0000000000000000000000000000000000000000..13ea2a686e09c16db68ff7bb355e114b9125d76d Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k001_utf16.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k002_utf8_without_bom.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k002_utf8_without_bom.kmn new file mode 100644 index 0000000000000000000000000000000000000000..da6d4e70873e5762dd49a41d13721abebb5ba668 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k002_utf8_without_bom.kmn @@ -0,0 +1,11 @@ +c Description: Verifies that kmcmplib can compile a UTF-8 file (without BOM) + +store(&NAME) 'k002_utf8_without_bom' +store(&VERSION) '9.0' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > 'ខ្មែរ' diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k002_utf8_without_bom.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k002_utf8_without_bom.kmx new file mode 100644 index 0000000000000000000000000000000000000000..0b9c9294182d9387165c98e1ffc84c8fe45452e3 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k002_utf8_without_bom.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k003_utf8_with_bom.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k003_utf8_with_bom.kmn new file mode 100644 index 0000000000000000000000000000000000000000..b2bccb036caad4d891ef62c5bc31c02ebaaffddd --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k003_utf8_with_bom.kmn @@ -0,0 +1,11 @@ +c Description: Verifies that kmcmplib can compile a UTF-8 file (with BOM) + +store(&NAME) 'k003_utf8_with_bom' +store(&VERSION) '9.0' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > 'ខ្មែរ' diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k003_utf8_with_bom.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k003_utf8_with_bom.kmx new file mode 100644 index 0000000000000000000000000000000000000000..ee1cf4225502e3c7944b58f301e5be649af4ec00 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k003_utf8_with_bom.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k004_ansi.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k004_ansi.kmn new file mode 100644 index 0000000000000000000000000000000000000000..50a1b31ea1c7b4d369e66dc69021be22f9cdeacb --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k004_ansi.kmn @@ -0,0 +1,13 @@ +c Description: Verifies that kmcmplib can compile an ANSI file +c This has some high-ascii letters in cp1252 to ensure that +c it fails to load as 'utf8 without bom' + +store(&NAME) 'k004_ansi' +store(&VERSION) '9.0' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > '' diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k004_ansi.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k004_ansi.kmx new file mode 100644 index 0000000000000000000000000000000000000000..6c41da979cb2a3ec20aadf6de0ca778fb8ce5ec1 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k004_ansi.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.bmp b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.bmp new file mode 100644 index 0000000000000000000000000000000000000000..509b89ae6df7f09a442ae60f215a6c7b3211b34f Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.bmp differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.kmn new file mode 100644 index 0000000000000000000000000000000000000000..926ccdeeafbf5bf648903d0bdc530a5e0ffa9ce9 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.kmn @@ -0,0 +1,12 @@ +c Description: Verifies that kmcmplib can load a bitmap without file extension + +store(&NAME) 'k005_bitmap' +store(&VERSION) '9.0' +store(&BITMAP) 'k005_bitmap' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > 'ខ្មែរ' diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.kmx new file mode 100644 index 0000000000000000000000000000000000000000..bc9bc9b671724050e169e59fce976a18d54a030e Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k005_bitmap.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.ico b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e1b64cf9013e6702420d97fd88b120490f33b137 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.ico differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.kmn new file mode 100644 index 0000000000000000000000000000000000000000..4ad3e61659683213ddc6b44a0454c87cc7576d11 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.kmn @@ -0,0 +1,12 @@ +c Description: Verifies that kmcmplib can load an icon with file extension + +store(&NAME) 'k006_icon' +store(&VERSION) '9.0' +store(&BITMAP) 'k006_icon.ico' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > 'a' +'a' + [K_B] > 'ខ្មែរ' diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.kmx new file mode 100644 index 0000000000000000000000000000000000000000..f994a1dcac9e5ae797eb88eed9cee4b0e6f85b00 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k006_icon.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.kmn new file mode 100644 index 0000000000000000000000000000000000000000..71d72d5120b522cd20303f3d0843e4720791230c --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.kmn @@ -0,0 +1,12 @@ +c Description: Verifies that kmcmplib can load an includecodes file with \r\n line endings + +store(&NAME) 'k007_includecodes_r_n' +store(&VERSION) '9.0' +store(&includecodes) 'k007_includecodes_r_n.txt' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > $LOWER_A +$LOWER_A + [K_B] > $LOWER_B diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.kmx new file mode 100644 index 0000000000000000000000000000000000000000..f8b4eac1b0d9103c7767e487b5109a06aed8e1ec Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.txt b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.txt new file mode 100644 index 0000000000000000000000000000000000000000..4cc3fe4351ddeb205cdd985d04648ec403065e35 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k007_includecodes_r_n.txt @@ -0,0 +1,2 @@ +0061;LOWER_A +0062;LOWER_B diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.kmn new file mode 100644 index 0000000000000000000000000000000000000000..ea357b4b0ef6fc8331efb7f63aca9e3fc9291652 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.kmn @@ -0,0 +1,12 @@ +c Description: Verifies that kmcmplib can load an includecodes file with \n line endings + +store(&NAME) 'k008_includecodes_n' +store(&VERSION) '9.0' +store(&includecodes) 'k008_includecodes_n.txt' + +begin unicode > use(main) + +group(main) using keys + ++ [K_A] > $LOWER_A +$LOWER_A + [K_B] > $LOWER_B diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.kmx new file mode 100644 index 0000000000000000000000000000000000000000..aa158cdd546cdde915e4a55c1db521e57d4abb04 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.kmx differ diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.txt b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.txt new file mode 100644 index 0000000000000000000000000000000000000000..4cc3fe4351ddeb205cdd985d04648ec403065e35 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k008_includecodes_n.txt @@ -0,0 +1,2 @@ +0061;LOWER_A +0062;LOWER_B diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k009_long_lines.kmn b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k009_long_lines.kmn new file mode 100644 index 0000000000000000000000000000000000000000..a0ad9d0d9fbc820432982d13455f5a02acda4018 --- /dev/null +++ b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k009_long_lines.kmn @@ -0,0 +1,31 @@ +c Description: Verifies that kmcmplib can deal with long lines continued with backslashes, and also comments + +store(&NAME) 'k009_long_lines' +store(&VERSION) '9.0' + +begin unicode > use(main) + +group(main) using keys + +c +c ***Note*** This file has intentional whitespace at the end of line ending in [SHIFT K_N] +c in order to verify that line continuations are working correctly +c + +store(c_key) [K_K] [K_X] [SHIFT K_K] [SHIFT K_X] [K_G] \ + [K_C] [K_Q] [SHIFT K_C] [SHIFT K_Q] [SHIFT K_J] \ + [K_D] [K_Z] [SHIFT K_D] [SHIFT K_Z] [SHIFT K_N] \ + [K_T] [K_F] [SHIFT K_T] [SHIFT K_F] [K_N] \ + [K_B] [K_P] [SHIFT K_B] [SHIFT K_P] [K_M] \ + [K_Y] [K_R] [K_L] [K_V] [K_S] [K_H] [SHIFT K_L] [SHIFT K_G] \ + [RALT K_K] [RALT K_B] +store(c_out) U+1780 U+1781 U+1782 U+1783 U+1784 \ + U+1785 U+1786 U+1787 U+1788 U+1789 \ + U+178A U+178B U+178C U+178D U+178E \ + U+178F U+1790 U+1791 U+1792 U+1793 \ + U+1794 U+1795 U+1796 U+1797 U+1798 \ + U+1799 U+179A U+179B U+179C U+179F U+17A0 U+17A1 U+17A2 \ + U+179D U+179E c deprecated, but they are used in minority languages + ++ any(c_key) > index(c_out, 1) + diff --git a/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k009_long_lines.kmx b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k009_long_lines.kmx new file mode 100644 index 0000000000000000000000000000000000000000..4acd4cc051adc10d35c6ae842b24be5cf43f0cf4 Binary files /dev/null and b/developer/src/kmcmplib/tests/fixtures/valid-keyboards/k009_long_lines.kmx differ diff --git a/developer/src/kmcmplib/tests/kmcompxtest.cpp b/developer/src/kmcmplib/tests/kmcompxtest.cpp index 1ce542917d389df52ad136047d9b7ae09d15d9dd..454481ff5ecc76a4bea2236fdc22233ccfa9cdcc 100644 --- a/developer/src/kmcmplib/tests/kmcompxtest.cpp +++ b/developer/src/kmcmplib/tests/kmcompxtest.cpp @@ -13,6 +13,8 @@ #include #include #include +#include "util_filesystem.h" +#include "util_callbacks.h" #ifdef _MSC_VER #else @@ -21,29 +23,11 @@ using namespace std; -vector < int > error_vec; - #define CERR_FATAL 0x00008000 #define CERR_ERROR 0x00004000 #define CERR_WARNING 0x00002000 #define CERR_HINT 0x00001000 -int msgproc(int line, uint32_t dwMsgCode, char* szText, void* context) -{ - error_vec.push_back(dwMsgCode); - const char*t = "unknown"; - switch(dwMsgCode & 0xF000) { - case CERR_HINT: t=" hint"; break; - case CERR_WARNING: t="warning"; break; - case CERR_ERROR: t=" error"; break; - case CERR_FATAL: t=" fatal"; break; - } - printf("line %d %s %04.4x: %s\n", line, t, (unsigned int)dwMsgCode, szText); - return 1; -} - -#include "../src/filesystem.h" - int main(int argc, char *argv[]) { if(argc < 4) { @@ -71,14 +55,15 @@ int main(int argc, char *argv[]) char first5[6] = "CERR_"; char* pfirst5 = first5; - KMCMP_COMPILER_OPTIONS kcopts; - kcopts.dwSize = sizeof(KMCMP_COMPILER_OPTIONS); - kcopts.ShouldAddCompilerVersion = false; // So we can compare against existing compiled keyboards that also don't have compiler version - if(!kmcmp_SetCompilerOptions(&kcopts)) { - return __LINE__; - } + KMCMP_COMPILER_RESULT result; + KMCMP_COMPILER_OPTIONS options; + options.saveDebug = true; + options.compilerWarningsAsErrors = false; + options.warnDeprecatedCode = true; + options.shouldAddCompilerVersion = false; + options.target = CKF_KEYMAN; - if(kmcmp_CompileKeyboardFile(kmn_file, kmx_file, true, false, true, msgproc, nullptr)) { + if(kmcmp_CompileKeyboard(kmn_file, options, msgproc, loadfileProc, nullptr, result)) { char* testname = strrchr( (char*) kmn_file, '/') + 1; if(strncmp(testname, pfirst5, 5) == 0){ return __LINE__; // exit code: CERR_ in Name + no Error found @@ -87,26 +72,24 @@ int main(int argc, char *argv[]) // On non-win32 platforms, we cannot get kmcmpdll.dll to build keyboards // legacy-mode, so we'll compare to a hopefully existing file that we've // been passed - FILE* fp1 = Open_File(kmx_file, "rb"); + FILE* fp1 = Open_File(kmx_file, "wb"); if(!fp1) return __LINE__; + // Write out for reference + fwrite(result.kmx, 1, result.kmxSize, fp1); + fclose(fp1); FILE* fp2 = Open_File(reference_kmx, "rb"); if(!fp2) return __LINE__; // exit code: fail if no reference kmx file in build-folder - fseek(fp1, 0, SEEK_END); - auto sz1 = ftell(fp1); - fseek(fp1, 0, SEEK_SET); fseek(fp2, 0, SEEK_END); auto sz2 = ftell(fp2); fseek(fp2, 0, SEEK_SET); - if (sz1 != sz2) return __LINE__; // exit code: size of kmx-file in build differs from size of kmx-file in source folder + if (result.kmxSize != sz2) return __LINE__; // exit code: size of kmx-file in build differs from size of kmx-file in source folder - char* buf1 = new char[sz1]; - char* buf2 = new char[sz1]; - fread(buf1, 1, sz1, fp1); - fread(buf2, 1, sz1, fp2); - return memcmp(buf1, buf2, sz1) ? __LINE__ : 0; // exit code: when contents of kmx-file in build differs from contents of kmx-file in source folder + char* buf2 = new char[result.kmxSize]; + fread(buf2, 1, result.kmxSize, fp2); + return memcmp(result.kmx, buf2, result.kmxSize) ? __LINE__ : 0; // exit code: when contents of kmx-file in build differs from contents of kmx-file in source folder // success: when contents of kmx-file in build and source folder are the same } else { /*if Errors found: check number (e.g. CERR_4061_balochi_phonetic.kmn should produce Error 4061)*/ diff --git a/developer/src/kmcmplib/tests/meson.build b/developer/src/kmcmplib/tests/meson.build index b81429806e16a3d414c2626c34fba9caa4d2bc12..ae023d70b0a2ffdb9ed1f0550ec6bf935eb9d583 100644 --- a/developer/src/kmcmplib/tests/meson.build +++ b/developer/src/kmcmplib/tests/meson.build @@ -6,16 +6,20 @@ fs = import('fs') -tests_flags = [] +tests_links = [] + +if cpp_compiler.get_id() == 'emscripten' + tests_links += ['-lnodefs.js'] +endif input_path = meson.current_source_dir() / '../../../../common/test/keyboards/baseline' output_path = meson.current_build_dir() -kmcompxtest = executable('kmcompxtest', 'kmcompxtest.cpp', - cpp_args: defns, +kmcompxtest = executable('kmcompxtest', ['kmcompxtest.cpp','util_filesystem.cpp','util_callbacks.cpp'], + cpp_args: defns + flags, include_directories: inc, name_suffix: name_suffix, - link_args: links + tests_flags, + link_args: links + tests_links, objects: lib.extract_all_objects(), dependencies: icuuc_dep, ) @@ -76,6 +80,26 @@ foreach kbd : tests test(kbd, kmcompxtest, args: [kbd_src, kbd_obj, join_paths(input_path, kbd) + '.kmx']) endforeach +valid_keyboard_tests = [ + 'k001_utf16', + 'k002_utf8_without_bom', + 'k003_utf8_with_bom', + 'k004_ansi', + 'k005_bitmap', + 'k006_icon', + 'k007_includecodes_r_n', + 'k008_includecodes_n', + 'k009_long_lines', +] + +fixtures_path = meson.current_source_dir() / 'fixtures/valid-keyboards' + +foreach kbd : valid_keyboard_tests + kbd_src = join_paths(fixtures_path, kbd) + '.kmn' + kbd_obj = join_paths(output_path, kbd) + '.kmx' + test(kbd, kmcompxtest, args: [kbd_src, kbd_obj, join_paths(fixtures_path, kbd) + '.kmx']) +endforeach + # Test fixtures that come from keyboards repo -- but only for a "full" test, # which typically we run on CI no more than once a day, because it's expensive. @@ -96,7 +120,7 @@ if get_option('full_test') # kbd is going to be an absolute path kbd_obj = output_path / test_basename + '.kmx' - reference_kmx = meson.current_source_dir() / 'fixtures' / test_basename + '.kmx' + reference_kmx = meson.current_source_dir() / 'fixtures/keyboards-repo' / test_basename + '.kmx' if fs.is_file(reference_kmx) test(test_basename, kmcompxtest, args: [kbd, kbd_obj, reference_kmx]) @@ -107,22 +131,22 @@ endif # Test the API endpoints -apitest = executable('api-test', 'api-test.cpp', - cpp_args: defns, +apitest = executable('api-test', ['api-test.cpp','util_filesystem.cpp','util_callbacks.cpp'], + cpp_args: defns + flags, include_directories: inc, name_suffix: name_suffix, - link_args: links + tests_flags, + link_args: links + tests_links, objects: lib.extract_all_objects(), - dependencies: icuuc_dep, + dependencies: icuuc_dep ) -test('api-test', apitest) +test('api-test', apitest, args: [output_path / 'blank_keyboard.kmx']) usetapitest = executable('uset-api-test', 'uset-api-test.cpp', - cpp_args: defns, + cpp_args: defns + flags, include_directories: inc, name_suffix: name_suffix, - link_args: links + tests_flags, + link_args: links + tests_links, objects: lib.extract_all_objects(), dependencies: icuuc_dep, ) diff --git a/developer/src/kmcmplib/tests/prep.sh b/developer/src/kmcmplib/tests/prep.sh index 058d4a1b9b0bfdd50fe13d29bcbdff7af508bd3c..8ad9b81171f165903fe74d4b7eb47f6f39154800 100755 --- a/developer/src/kmcmplib/tests/prep.sh +++ b/developer/src/kmcmplib/tests/prep.sh @@ -39,30 +39,30 @@ if [[ ! -d "$KEYBOARDS_ROOT/.git" ]] || [[ ! -f "$KEYBOARDS_ROOT/tools/regressio fi if builder_start_action clean; then - rm -rf "$THIS_SCRIPT_PATH/fixtures" + rm -rf "$THIS_SCRIPT_PATH/fixtures/keyboards-repo" rm -f "$THIS_SCRIPT_PATH/keyboards_commit_ref.txt" builder_finish_action success clean fi if builder_start_action build; then # yes, remove existing fixtures before build so we don't end up with stale fixtures - rm -rf "$THIS_SCRIPT_PATH/fixtures" + rm -rf "$THIS_SCRIPT_PATH/fixtures/keyboards-repo" # record the revision of the keyboards repo pushd "$KEYBOARDS_ROOT" > /dev/null git rev-parse head > "$THIS_SCRIPT_PATH/keyboards_commit_ref.txt" popd > /dev/null - "$KEYBOARDS_ROOT/tools/regression.sh" --use-legacy-compiler --local "$KEYMAN_ROOT/developer/bin/" --output "$THIS_SCRIPT_PATH/fixtures/" + "$KEYBOARDS_ROOT/tools/regression.sh" --use-legacy-compiler --local "$KEYMAN_ROOT/developer/bin/" --output "$THIS_SCRIPT_PATH/fixtures/keyboards-repo/" cd "$THIS_SCRIPT_PATH" # We don't need visual keyboards - rm ./fixtures/*.kvk + rm ./fixtures/keyboards-repo/*.kvk # Nor do we need kmw outputs - rm ./fixtures/*.js + rm ./fixtures/keyboards-repo/*.js # Finally, we remove any keyboards over 500kb, as we won't automatically test them for now # TODO: consider if we want to keep those large keyboards in the future - find ./fixtures/ -size +500k -exec rm {} \+ + find ./fixtures/keyboards-repo/ -size +500k -exec rm {} \+ builder_finish_action success build fi \ No newline at end of file diff --git a/developer/src/kmcmplib/tests/uset-api-test.cpp b/developer/src/kmcmplib/tests/uset-api-test.cpp index 4fe599ca2e83a1af9d50d61368bafa4b337f3c82..7a24f5cd4a5108aeb7191c23aa717034ee36ad72 100644 --- a/developer/src/kmcmplib/tests/uset-api-test.cpp +++ b/developer/src/kmcmplib/tests/uset-api-test.cpp @@ -19,41 +19,57 @@ #include "../src/compfile.h" #include -void test_kmcmp_ParseUnicodeSetProc(); +void test_kmcmp_parseUnicodeSet(); // std::vector error_vec; int main(int argc, char *argv[]) { - test_kmcmp_ParseUnicodeSetProc(); + test_kmcmp_parseUnicodeSet(); return 0; } -void test_kmcmp_ParseUnicodeSetProc() { +void test_kmcmp_parseUnicodeSet() { { // null test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[]", buf_, bufsiz); assert(rc == KMCMP_USET_OK); } + { + // preflight null test + const auto bufsiz = 0; + uintptr_t buf_ = 0L; + int rc = kmcmp_parseUnicodeSet(u8"[]", buf_, bufsiz); + assert(rc == KMCMP_USET_OK); // == 0 + } { // basic test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[x A-C]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[x A-C]", buf_, bufsiz); assert(rc == 2); assert(buf[0] == 0x41); assert(buf[1] == 0x43); assert(buf[2] == 0x78); assert(buf[3] == 0x78); } + { + uintptr_t buf_ = 0L; + const auto bufsiz = 0; + int rc = kmcmp_parseUnicodeSet(u8"[x A-C]", buf_, bufsiz); // preflight + assert(rc == 2); // 2 ranges + } { // bigger test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[[🙀A-C]-[CB]]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[[🙀A-C]-[CB]]", buf_, bufsiz); assert(rc == 2); assert(buf[0] == 0x41); assert(buf[1] == 0x41); @@ -64,35 +80,68 @@ void test_kmcmp_ParseUnicodeSetProc() { // overflow test const auto bufsiz = 1; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[x A-C]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[x A-C]", buf_, bufsiz); assert(rc == KMCMP_FATAL_OUT_OF_RANGE); } { // err test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[:Adlm:]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[:Adlm:]", buf_, bufsiz); assert(rc == KMCMP_ERROR_UNSUPPORTED_PROPERTY); } { // err test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[[\\p{Mn}]&[A-Z]]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[[\\p{Mn}]&[A-Z]]", buf_, bufsiz); assert(rc == KMCMP_ERROR_UNSUPPORTED_PROPERTY); } { // err test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[abc{def}]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[abc{def}]", buf_, bufsiz); assert(rc == KMCMP_ERROR_HAS_STRINGS); } { // err test const auto bufsiz = 128; uint32_t buf[bufsiz]; - int rc = kmcmp_ParseUnicodeSet(u8"[[]", buf, bufsiz); + uintptr_t buf_ = reinterpret_cast(buf); + int rc = kmcmp_parseUnicodeSet(u8"[[]", buf_, bufsiz); + assert(rc == KMCMP_ERROR_SYNTAX_ERR); + } + { + // preflight err test + const auto bufsiz = 0; // preflight + uintptr_t buf_ = 0L; + int rc = kmcmp_parseUnicodeSet(u8"[:Adlm:]", buf_, bufsiz); + assert(rc == KMCMP_ERROR_UNSUPPORTED_PROPERTY); + } + { + // preflight err test + const auto bufsiz = 0; // preflight + uintptr_t buf_ = 0L; + int rc = kmcmp_parseUnicodeSet(u8"[[\\p{Mn}]&[A-Z]]", buf_, bufsiz); + assert(rc == KMCMP_ERROR_UNSUPPORTED_PROPERTY); + } + { + // preflight err test + const auto bufsiz = 0; // preflight + uintptr_t buf_ = 0L; + int rc = kmcmp_parseUnicodeSet(u8"[abc{def}]", buf_, bufsiz); + assert(rc == KMCMP_ERROR_HAS_STRINGS); + } + { + // preflight err test + const auto bufsiz = 0; // preflight + uintptr_t buf_ = 0L; + int rc = kmcmp_parseUnicodeSet(u8"[[]", buf_, bufsiz); assert(rc == KMCMP_ERROR_SYNTAX_ERR); } } diff --git a/developer/src/kmcmplib/tests/util_callbacks.cpp b/developer/src/kmcmplib/tests/util_callbacks.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e610a54c09e8beb6974675d23deb0cc5199140d6 --- /dev/null +++ b/developer/src/kmcmplib/tests/util_callbacks.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include "util_filesystem.h" +#include "../src/compfile.h" +#include + +std::vector error_vec; + +int msgproc(int line, uint32_t dwMsgCode, const char* szText, void* context) { + error_vec.push_back(dwMsgCode); + const char*t = "unknown"; + switch(dwMsgCode & 0xF000) { + case CERR_HINT: t=" hint"; break; + case CERR_WARNING: t="warning"; break; + case CERR_ERROR: t=" error"; break; + case CERR_FATAL: t=" fatal"; break; + } + printf("line %d %s %04.4x: %s\n", line, t, (unsigned int)dwMsgCode, szText); + return 1; +} + +bool loadfileProc(const char* filename, const char* baseFilename, void* data, int* size, void* context) { + std::string resolvedFilename = filename; + if(baseFilename && *baseFilename && IsRelativePath(filename)) { + char* p; + if ((p = strrchr_slash((char*)baseFilename)) != nullptr) { + std::string basePath = std::string(baseFilename, (int)(p - baseFilename + 1)); + resolvedFilename = basePath; + resolvedFilename.append(filename); + } + } + + FILE* fp = Open_File(resolvedFilename.c_str(), "rb"); + if(!fp) { + return false; + } + + if(!data) { + // return size + if(fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return false; + } + *size = ftell(fp); + if(*size == -1L) { + fclose(fp); + return false; + } + } else { + // return data + if(fread(data, 1, *size, fp) != *size) { + fclose(fp); + return false; + } + } + fclose(fp); + return true; +} \ No newline at end of file diff --git a/developer/src/kmcmplib/tests/util_callbacks.h b/developer/src/kmcmplib/tests/util_callbacks.h new file mode 100644 index 0000000000000000000000000000000000000000..345de64b9f484b568f28c1652e800ee20e7307ee --- /dev/null +++ b/developer/src/kmcmplib/tests/util_callbacks.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +int msgproc(int line, uint32_t dwMsgCode, const char* szText, void* context); +bool loadfileProc(const char* filename, const char* baseFilename, void* data, int* size, void* context); + +extern std::vector error_vec; \ No newline at end of file diff --git a/developer/src/kmcmplib/src/filesystem.cpp b/developer/src/kmcmplib/tests/util_filesystem.cpp similarity index 84% rename from developer/src/kmcmplib/src/filesystem.cpp rename to developer/src/kmcmplib/tests/util_filesystem.cpp index a343641acc128ed5537e0e1835d45a2253c29f2e..7f9edd6b367a303573e165767ca1805f13bfd558 100644 --- a/developer/src/kmcmplib/src/filesystem.cpp +++ b/developer/src/kmcmplib/tests/util_filesystem.cpp @@ -7,7 +7,7 @@ #include #include #include -#include "filesystem.h" +#include "util_filesystem.h" #ifdef _MSC_VER #include @@ -186,3 +186,42 @@ KMX_BOOL kmcmp_FileExists(const KMX_WCHAR* filename) { return FALSE; }; + + +bool IsRelativePath(KMX_CHAR const * p) { + // Relative path (returns TRUE): + // ..\...\BITMAP.BMP + // PATH\BITMAP.BMP + // BITMAP.BMP + + // Semi-absolute path (returns FALSE): + // \...\BITMAP.BMP + + // Absolute path (returns FALSE): + // C:\...\BITMAP.BMP + // \\SERVER\SHARE\...\BITMAP.BMP + + if ((*p == '\\') || (*p == '/')) return FALSE; + if (*p && *(p + 1) == ':') return FALSE; + + return TRUE; +} + +bool IsRelativePath(KMX_WCHAR const * p) { + // Relative path (returns TRUE): + // ..\...\BITMAP.BMP + // PATH\BITMAP.BMP + // BITMAP.BMP + + // Semi-absolute path (returns FALSE): + // \...\BITMAP.BMP + + // Absolute path (returns FALSE): + // C:\...\BITMAP.BMP + // \\SERVER\SHARE\...\BITMAP.BMP + + if ((*p == u'\\') || (*p == u'/'))return FALSE; + if (*p && *(p + 1) == u':') return FALSE; + + return TRUE; +} \ No newline at end of file diff --git a/developer/src/kmcmplib/src/filesystem.h b/developer/src/kmcmplib/tests/util_filesystem.h similarity index 82% rename from developer/src/kmcmplib/src/filesystem.h rename to developer/src/kmcmplib/tests/util_filesystem.h index 786b7268ac0c09bdda634296445e7e448404cc1e..4c94dae6bf4e4049d80efe8fa5d52d4b4fd7bb93 100644 --- a/developer/src/kmcmplib/src/filesystem.h +++ b/developer/src/kmcmplib/tests/util_filesystem.h @@ -1,7 +1,7 @@ #pragma once #include -#include "kmx_u16.h" +#include "../src/kmx_u16.h" // Opens files on windows and non-windows platforms. Datatypes for Filename and mode must be the same. // returns FILE* if file could be opened; FILE needs to be closed in calling function @@ -10,3 +10,6 @@ FILE* Open_File(const KMX_WCHART* Filename, const KMX_WCHART* mode); FILE* Open_File(const KMX_WCHAR* Filename, const KMX_WCHAR* mode); KMX_BOOL kmcmp_FileExists(const KMX_CHAR *filename); KMX_BOOL kmcmp_FileExists(const KMX_WCHAR *filename); + +bool IsRelativePath(KMX_CHAR const * p); +bool IsRelativePath(KMX_WCHAR const * p); diff --git a/developer/src/kmcomp/main.pas b/developer/src/kmcomp/main.pas index 5a29927a807bccd47dea671dbb6ce8d629a1f727..4aa29cf6c1ace54494285d0f9bd9c1f701d45040 100644 --- a/developer/src/kmcomp/main.pas +++ b/developer/src/kmcomp/main.pas @@ -65,7 +65,7 @@ uses UKeymanTargets; function CompileKeyboard(FInFile, FOutFile: string; FDebug, FWarnAsError: Boolean): Boolean; forward; // I4706 -function KCSetCompilerOptions(const FInFile: string; FShouldAddCompilerVersion, FUseKmcmpLib: Boolean): Boolean; forward; +function KCSetCompilerOptions(const FInFile: string; FShouldAddCompilerVersion: Boolean): Boolean; forward; //function CompilerMessage(line: Integer; msgcode: LongWord; text: PAnsiChar): Integer; stdcall; forward; procedure FixupPathSlashes(var path: string); forward; @@ -90,7 +90,6 @@ var FParamDistribution: Boolean; FMergingValidateIds: Boolean; FShouldAddCompilerVersion: Boolean; - FUseKmcmpLib: Boolean; FJsonSchemaPath: string; FParamSourcePath: string; FParamHelpLink: string; @@ -117,7 +116,6 @@ begin FColorMode := cmDefault; FShouldAddCompilerVersion := True; - FUseKmcmpLib := True; FParamInfile := ''; FParamOutfile := ''; @@ -193,8 +191,6 @@ begin FColorMode := cmForceNoColor else if s = '-no-compiler-version' then FShouldAddCompilerVersion := False - else if s = '-use-legacy-compiler' then - FUseKmcmpLib := False else if (s = '-help') or (s = '-h') then begin // Force help @@ -223,8 +219,6 @@ begin writeln(SKeymanDeveloperName + ' Compiler (32-bit)'); {$ENDIF} writeln('Version ' + CKeymanVersionInfo.VersionWithTag + ', ' + GetVersionCopyright); - if not FUseKmcmpLib then - writeln('Note: using legacy compiler'); end; if FError or (FParamInfile = '') then @@ -234,7 +228,7 @@ begin writeln(''); writeln('Usage: '+cmd+' [-s[s]] [-nologo] [-c] [-d] [-w] [-cfc] [-v[s|d]] [-source-path path] [-schema-path path] '); writeln(' '+spc+' [-m] infile [-m infile] [-t target] [outfile.kmx|outfile.js [error.log]]'); // I4699 - writeln(' '+spc+' [-add-help-link path] [-color|-no-color] [-no-compiler-version] [-use-legacy-compiler]'); + writeln(' '+spc+' [-add-help-link path] [-color|-no-color] [-no-compiler-version]'); writeln(' '+spc+' [-extract-keyboard-info field[,field...]]'); writeln(' infile can be a .kmn file (Keyboard Source, .kps file (Package Source), or .kpj (project)'); // I4699 // I4825 writeln(' if -v specified, can also be a .keyboard_info file'); @@ -258,7 +252,6 @@ begin writeln(' uses console mode to determine whether color should be used.'); writeln; writeln(' -no-compiler-version Don''t embed the compiler version stores, useful for regression tests.'); - writeln(' -use-legacy-compiler Use legacy compiler (will be removed in 18.0)'); writeln; writeln(' JSON .keyboard_info compile targets:'); writeln(' -v[s] validate infile against source schema'); @@ -298,7 +291,7 @@ begin TProjectLogConsole.Create(FSilent, FFullySilent, hOutfile, FColorMode); - KCSetCompilerOptions(FParamInfile, FShouldAddCompilerVersion, FUseKmcmpLib); + KCSetCompilerOptions(FParamInfile, FShouldAddCompilerVersion); if FValidateRepoChanges then FError := not TValidateRepoChanges.Execute(FParamInfile, FParamOutfile) @@ -324,7 +317,7 @@ begin ExitCode := 1; end; -function KCSetCompilerOptions(const FInFile: string; FShouldAddCompilerVersion, FUseKmcmpLib: Boolean): Boolean; +function KCSetCompilerOptions(const FInFile: string; FShouldAddCompilerVersion: Boolean): Boolean; var opt: TCompilerOptions; begin @@ -332,7 +325,6 @@ begin opt.dwSize := sizeof(TCompilerOptions); opt.ShouldAddCompilerVersion := FShouldAddCompilerVersion; - opt.UseKmcmpLib := FUseKmcmpLib; Result := SetCompilerOptions(@opt, @CompilerMessageW); diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index 53585f1df6b567f62a912e47239d8177c2aa8a22..c735e26fecb3f93b9cd7962f5726cc48969c656c 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -124,20 +124,18 @@ fi if (( build_keymanweb )); then pushd "$KEYMAN_ROOT/web/" - ./build.sh --no-minify + ./build.sh build --debug popd fi if (( copy_keymanweb )); then - WEB_SRC="$KEYMAN_ROOT/web/build/app/web/debug" - UI_SRC="$KEYMAN_ROOT/web/build/app/ui/debug" + WEB_SRC="$KEYMAN_ROOT/web/build/publish/debug" DST="$(dirname "$THIS_SCRIPT")/src/site/resource" rm -rf "$DST" mkdir -p "$DST/osk" mkdir -p "$DST/ui" cp "$WEB_SRC/"*.js "$WEB_SRC/"*.js.map "$DST/" - cp "$UI_SRC/"*.js "$UI_SRC/"*.js.map "$DST/" cp -R "$WEB_SRC/osk/"* "$DST/osk/" cp -R "$WEB_SRC/ui/"* "$DST/ui/" cp "$KEYMAN_ROOT/web/LICENSE" "$DST/" diff --git a/developer/src/server/src/site/chargrid.js b/developer/src/server/src/site/chargrid.js index 1ce9206bae57604d9fcf6ee7abec88a38098413b..f6c31f2df30dd4acd221dcc6cdc0004f357354d4 100644 --- a/developer/src/server/src/site/chargrid.js +++ b/developer/src/server/src/site/chargrid.js @@ -73,17 +73,10 @@ function updateLogCursor() { var i, selStart, selLength, selDirection; - if(keyman.isPositionSynthesized()) { // this is an internal function - // For touch devices, we need to ask KMW - selStart = 0; - selLength = 0; - selDirection = 'forward'; - } else { - // For desktop devices, we use the position reported by the textarea control - selStart = ta1.selectionStart; - selLength = ta1.selectionEnd - ta1.selectionStart; - selDirection = ta1.selectionDirection; - } + // We use the position reported by the textarea control + selStart = ta1.selectionStart; + selLength = ta1.selectionEnd - ta1.selectionStart; + selDirection = ta1.selectionDirection; selLength = calculateLengthByCodepoint(ta1.value, selStart, selLength); selStart = calculateLengthByCodepoint(ta1.value, 0, selStart); diff --git a/developer/src/server/src/site/test.js b/developer/src/server/src/site/test.js index 139765cc4c9df05b477351d3992e7d78d0d09fad..60a27cc484f6cb85ef1df344bafeefbbed4eec79 100644 --- a/developer/src/server/src/site/test.js +++ b/developer/src/server/src/site/test.js @@ -221,7 +221,7 @@ window.onload = function() { // Create a new on screen keyboard view and tell KeymanWeb that // we are using the targetDevice for context input. - newOSK = new com.keyman.osk.InlinedOSKView(targetDevice, keyman.util.device.coreSpec); + newOSK = new keyman.views.InlinedOSKView(keyman, { device: targetDevice }); keyman.core.contextDevice = targetDevice; keyman.osk = newOSK; @@ -238,11 +238,10 @@ window.onload = function() { keyman.addEventListener('keyboardchange', function(keyboardProperties) { if(newOSK) { keyman.osk = newOSK; - newOSK.activeKeyboard = keyman.core.activeKeyboard; + newOSK.activeKeyboard = keyman.contextManager.activeKeyboard; // Private API refs on both sides } keyboardDropdown.set(keyboardProperties.internalName); window.sessionStorage.setItem('current-keyboard', keyboardProperties.internalName); - keyman.alignInputs(); }); } @@ -285,7 +284,7 @@ function unloadKeyboardsAndModels() { const lastModel = keyman.core.activeModel; if(lastModel) { console.log('Unregistering model '+lastModel.id); - keyman.modelManager.deregister(lastModel.id); + keyman.removeModel(lastModel.id); } modelDropdown.removeAll(); @@ -360,7 +359,7 @@ function selectModel(modelId) { const lastModel = keyman.core.activeModel; if(lastModel) { - keyman.modelManager.deregister(lastModel.id); + keyman.removeModel(lastModel.id); } if(model) { diff --git a/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFileAction.pas b/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFileAction.pas index 703a1e4a0b6edd891950b647e5240c2e1250f02b..2b6dea94ec580d34adc0c54f471f021683ec3b44 100644 --- a/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFileAction.pas +++ b/developer/src/tike/project/Keyman.Developer.System.Project.kmnProjectFileAction.pas @@ -115,7 +115,7 @@ begin TProject.CompilerMessageFile := Self; options.dwSize := sizeof(COMPILER_OPTIONS); options.ShouldAddCompilerVersion := addVersion; - options.UseKmcmpLib := not useLegacyCompiler; + // TODO: useLegacyCompiler means we switch to kmcmpdll vs kmc if not SetCompilerOptions(@options, ProjectCompilerMessage) then begin Log(plsFatal, 'Unable to set compiler options', CERR_FATAL, 0); diff --git a/docs/build/linux-ubuntu.md b/docs/build/linux-ubuntu.md index 36583d4576d991e7d5c044732ed5e4659f0af6ae..3c4f4c57c1c4b00df0803814e023d63e0b85c65e 100644 --- a/docs/build/linux-ubuntu.md +++ b/docs/build/linux-ubuntu.md @@ -4,28 +4,30 @@ On Linux, you can build the following projects: -* [Keyman for Linux](#keyman-for-linux) -* [Keyman Core](#keyman-core) (Linux only) (aka core) -* [Keyman for Android](#keyman-for-android) - -* Keyman Core (wasm targets) -* Common/Web -* KeymanWeb +- [Keyman Core](#keyman-core) (aka core) +- [Keyman for Linux](#keyman-for-linux) +- [Keyman Web](#keyman-web) +- [Keyman for Android](#keyman-for-android) + +- Common/Web The following projects **cannot** be built on Linux: -* Keyman for Windows -* Keyman Developer -* Keyman for macOS -* Keyman for iOS +- Keyman for Windows +- Keyman Developer +- Keyman for macOS +- Keyman for iOS -## System Requirements +## Requirements -* Minimum Ubuntu version: Ubuntu 20.04 +### System Requirements + +- Minimum Ubuntu version: Ubuntu 20.04 Other Linux distributions will also work if appropriate dependencies are installed. -## Repository Paths +### Repository Paths Recommended filesystem layout: @@ -38,7 +40,7 @@ $HOME/keyman/ ... ``` -## Prerequisites +### Prerequisites The current list of dependencies can be found in the `Build-Depends` section of `linux/debian/control`. They are most easily installed with the `mk-build-deps` tool: @@ -49,98 +51,214 @@ sudo apt install devscripts equivs sudo mk-build-deps --install linux/debian/control ``` -## Keyman for Linux +#### Node.js + +Node.js v18 is required for Core builds, Web builds, and Developer command line tool builds and usage. -All dependencies are already installed if you followed the instructions under [Prerequisites](#Prerequisites). +You can install it with: -Building: +```shell +curl -sL https://deb.nodesource.com/setup_18.x | bash +apt-get -q -y install nodejs +``` -* [Building Keyman for Linux](../../linux/README.md) +#### Emscripten + +You'll also have to install `emscripten` (version 3.1.44 is known to work): + +```shell +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install latest +./emsdk activate latest +export EMSCRIPTEN_BASE=$(pwd)/upstream/emscripten +``` + +**NOTE:** Don't put EMSDK on the path, i.e. don't source `emsdk_env.sh`. ## Keyman Core -All dependencies are already installed if you followed the instructions under [Prerequisites](#Prerequisites). +All dependencies are already installed if you followed the instructions under +[Prerequisites](#prerequisites). -Building: +### Building Keyman Core -* [Building Keyman Core](../../core/doc/BUILDING.md) +Keyman Core can be built with the `core/build.sh` script. -## Docker Builder +- [Building Keyman Core](../../core/doc/BUILDING.md) -The Docker builder allows you to perform a linux build from anywhere Docker is supported. -To build the docker image: +## Keyman for Linux -```shell -cd ../../linux -docker pull ubuntu:latest -docker build . -t keymanapp/keyman-linux-builder:latest -``` +All dependencies are already installed if you followed the instructions +under [Prerequisites](#prerequisites). -Once the image is built, it may be used to build parts of Keyman. +### Building Keyman for Linux -- core +Keyman for Linux can be built with the `linux/build.sh` script. -```shell -# build 'core' in docker -cd ../core -# keep linux build artifacts separate -mkdir -p build/linux -docker run -it --rm -v $(pwd)/..:/home/build -v $(pwd)/build/linux:/home/build/core/build keyman-linux-builder:latest bash -c 'cd core; bash build.sh -d' +- [Building Keyman for Linux](../../linux/README.md) + +## Keyman Web + +Most dependencies are already installed if you followed the instructions under +[Prerequisites](#prerequisites). You'll still have to install Chrome: + +```bash +wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +sudo apt install ./google-chrome-stable_current_amd64.deb ``` -- linux +And add the `CHROME_BIN` environment variable to `.bashrc: -```shell -# build 'linux' installation in docker -# runs as root! -# note: run 'git clean -fdx linux' afterwards to cleanup -cd keymanapp/keyman -docker run -it --rm -v $(pwd):/home/build/src/keyman -u root -w /home/build/src/keyman keymanapp/keyman-linux-builder:latest bash -c "cd linux && make reconf && make fullbuild && make install" +```bash +export CHROME_BIN=/opt/google/chrome/chrome ``` +### Environment variables for Keyman Web + +`CHROME_BIN` pointing to the Google Chrome binary. + +### Building Keyman Web + +Keyman Web can be built with the `web/build.sh` script. + +- [Building Keyman Web](../../web/README.md) + ## Keyman for Android -**Dependencies**: -* [Base](#base-dependencies) -* [Web](./windows#web-dependencies) +**Dependencies:** + +Most dependencies are already installed if you followed the instructions +under [Prerequisites](#prerequisites). + +**Additional requirements:** -**Additional requirements**: -* Android SDK -* [Android Studio](https://developer.android.com/studio/install#linux) -* Gradle -* Maven -* OpenJDK 11 (for Keyman 17.0+) +- [Android Studio](https://developer.android.com/studio/install#linux) + or sdkmanager +- Maven +- pandoc +- Android SDK +- Gradle +- jq +If you only use the command line you don't need Android Studio, however +to do development it's recommended to install it. Run Android Studio once after installation to install additional components such as emulator images and SDK updates. -**Required environment variables**: -* [`JAVA_HOME`](#java_home) +Maven, jq and pandoc can be installed with: -Building: -* [Building Keyman for Android](../../android/README.md) +```shell +sudo apt update +sudo apt install maven pandoc jq +``` + +If necessary, Android SDK and Gradle will be installed by the build script. +In order for that to work, run the following command once. You won't need +this if you install Android SDK through Android Studio. + +```shell +sudo apt install sdkmanager +sudo sdkmanager platform-tools +sudo chown -R $USER:$USER /opt/android-sdk/ +sdkmanager --licenses +``` + +### Environment variables for Keyman for Android + +**Required environment variable:** -## Prerequisites +- `ANDROID_HOME` pointing to Android SDK (`$HOME/Android/Sdk`) -Many dependencies are only required for specific projects. +**Recommended environment variable:** -### Base Dependencies +- [`JAVA_HOME`](#java_home) -**Environment variables**: +### Building Keyman for Android +Keyman for Android can be built with the `android/build.sh` script. -## Notes on Environment Variables +- [Building Keyman for Android](../../android/README.md) -### JAVA_HOME +### Notes on Environment Variables + +#### JAVA_HOME + +This environment variable tells Gradle what version of Java to use for building +Keyman for Android. OpenJDK 11 is used for master. + +It's recommended to set the environment variables to: -This environment variable tells Gradle what version of Java to use for building Keyman for Android. -OpenJDK 11 is used for master. It's recommended to set the environment variables to: ```bash -EXPORT JAVA_HOME="[path to OpenJDK 11]" +export JAVA_HOME="[path to OpenJDK 11]" ``` Also edit `/etc/profile.d/jvm.sh` as sudo: + ```bash -EXPORT JAVA_HOME="[path to OpenJDK 11]" +export JAVA_HOME="[path to OpenJDK 11]" ``` + +**Multiple versions of Java:** If you need to build Keyman for Android 16.0 or +older versions, you can set `JAVA_HOME_11` to the OpenJDK 11 path and +`JAVA_HOME` to the OpenJDK 8 path. This will build both versions correctly +from command line. But note that you do need to update your `JAVA_HOME` env +var to the associated version before opening Android Studio and loading any +Android projects. `JAVA_HOME_11` is mostly used by CI. + +## Docker Builder + +The Docker builder allows you to perform a build from anywhere Docker is supported. + +To build the docker image: + +```shell +cd linux +docker pull ubuntu:latest +docker build . -t keymanapp/keyman-linux-builder:latest +``` + +Once the image is built, it may be used to build parts of Keyman. + +**Note** that it's not yet possible to run tests in the Docker container. + +- core + + ```shell + # build 'Keyman Core' in docker + # keep linux build artifacts separate + mkdir -p $(git rev-parse --show-toplevel)/core/build/linux + docker run -it --rm -v $(git rev-parse --show-toplevel):/home/build/build \ + -v $(git rev-parse --show-toplevel)/core/build/linux:/home/build/build/core/build \ + keymanapp/keyman-linux-builder:latest \ + core/build.sh --debug + ``` + +- linux + + ```shell + # build 'Keyman for Linux' installation in docker + docker run -it --rm -v $(git rev-parse --show-toplevel):/home/build/build \ + --entrypoint /bin/bash keymanapp/keyman-linux-builder:latest \ + -c 'DESTDIR=/home/build /usr/bin/bashwrapper linux/build.sh --debug build install' + ``` + +- Keyman Web + + ```shell + # build 'Keyman Web' in docker + docker run --privileged -it --rm \ + -v $(git rev-parse --show-toplevel):/home/build/build \ + keymanapp/keyman-linux-builder:latest \ + web/build.sh --debug + ``` + +- Keyman for Android + + ```shell + # build 'Keyman for Android' in docker + docker run -it --rm -v $(git rev-parse --show-toplevel):/home/build/build \ + keymanapp/keyman-linux-builder:latest \ + android/build.sh --debug + ``` diff --git a/docs/build/macos.md b/docs/build/macos.md index d8e4f61696ca7b32183d6257051d04221a166688..d670001e074e6dd38b5ee8aaea47d6a3546dbc99 100644 --- a/docs/build/macos.md +++ b/docs/build/macos.md @@ -66,7 +66,7 @@ These dependencies are also listed below if you'd prefer to install manually. PATH="$HOMEBREW_PREFIX/opt/coreutils/libexec/gnubin:$PATH" ``` -* Web: node.js, emscripten, openjdk 8 +* Web: node.js 18+, emscripten, openjdk 8 ```shell brew install node emscripten openjdk@8 diff --git a/docs/build/windows.md b/docs/build/windows.md index ef4a77e18fc890a80417f968f2d5cfc6211ed68c..9faebc3d63223d358b549c9c0f44c335d65eccd7 100644 --- a/docs/build/windows.md +++ b/docs/build/windows.md @@ -153,6 +153,7 @@ choco install git jq python ninja pandoc refreshenv # choco meson (0.55) is too old, 1.0 required: python -m pip install meson +``` **Environment variables**: * [`KEYMAN_ROOT`](#keyman_root) @@ -183,7 +184,7 @@ You can use Windows Settings to add these environment variables permanently: **Requirements**: * emscripten 3.1.32 -* node.js 18 +* node.js 18+ * [openjdk 11](https://learn.microsoft.com/en-us/java/openjdk/download#openjdk-11)+ ```ps1 diff --git a/docs/linux/README.md b/docs/linux/README.md index 8690b46a55e5db337187b12fd0de336e1f7f5c7e..d848e215ed1ff1fe06195a6d1b7fdb71674cd220 100644 --- a/docs/linux/README.md +++ b/docs/linux/README.md @@ -2,29 +2,18 @@ ## Projects -- [keyman-config](../../linux/keyman-config) - km-config and some other tools to install, uninstall - and view information about Keyman keyboard packages. +- [keyman-config](../../linux/keyman-config) - `km-config` and some other tools + to install, uninstall and view information about Keyman keyboard packages. - [ibus-keyman](../../linux/ibus-keyman) - IBUS integration to use .kmp Keyman keyboards +- [keyman-system-service](../../linux/keyman-system-service) - A DBus system service + that allows to perform keyboard related actions when running under Wayland. - [core](../../core) - common keyboardprocessor library See [license information](../../linux/LICENSE.md) about licensing. ## Linux Requirements/Setup -- It is helpful to be using the [packages.sil.org](http://packages.sil.org) repo - -- Install packages required for building and developing Keyman for Linux - - ```bash - sudo apt install cdbs debhelper libx11-dev autotools-dev build-essential \ - dh-autoreconf flex bison libibus-1.0-dev python3-setuptools meson \ - libjson-glib-dev libgtk-3-dev libxml2-utils help2man python3-lxml \ - python3-magic python3-numpy python3-pil python3-pip python3-qrcode \ - python3-requests python3-requests-cache python3 python3-gi dconf-cli \ - dconf-editor cargo python3-dbus gir1.2-webkit2-4.0 ibus libglib2.0-bin \ - liblocale-gettext-perl xvfb xserver-xephyr metacity mutter \ - libicu-dev pkg-config - ``` +See [document in ../build](../build/linux-ubuntu.md). ## Compiling from Command Line @@ -48,29 +37,11 @@ See [license information](../../linux/LICENSE.md) about licensing. - run `sudo linux/build.sh uninstall` to uninstall everything and put it back again -#### Tmp install - -Used by TC for validating PRs - -Run `make tmpinstall` to build and install **keyboardprocessor** and **ibus-keyman** to `/tmp/keyman` - -This is only for testing the build, not for running **ibus-keyman** in ibus - -### Manually - -- **ibus-keyman** requires headers and lib from **keyboardprocessor** - -So - -- **keyboardprocessor** must be built before **ibus-keyman** - -For each project run `./build.sh && ./build.sh install`. - ## Continuous integration -Teamcity PR builds will run `make tmpinstall` +Teamcity PR builds will run `linux/build.sh install` -Master builds run `make tmpinstall` and create source packages +Master builds run `linux/build.sh install` and create source packages Nightly and release builds upload the most recent new master build to @@ -81,15 +52,18 @@ for details on building Linux packages for Keyman. ## Testing -### keyman-config - -The unit tests can be run with the following command: +The tests can be run with the following command: ```bash -cd linux/keyman-config -./run-tests.sh +linux/build.sh test ``` +To just run the unit tests without integration tests, add the +`--no-integration` parameter. + +It's also possible to only run the tests for one of the subprojects. You +can use `build.sh` in the subdirectory for that. + ### ibus-keyman If you want to run the ibus-keyman tests with Wayland, you'll have to diff --git a/docs/linux/keyman-config.md b/docs/linux/keyman-config.md index f050fd96ad0896b2bf9a83d064884d49dc064045..cfea5d14370a0b0b660c770adb7e1df6d045920c 100644 --- a/docs/linux/keyman-config.md +++ b/docs/linux/keyman-config.md @@ -35,10 +35,7 @@ or install it with pip: pip3 install sentry-sdk ``` -Run the script `./createkeymandirs.sh` to create the directories for these -programs to install the packages to. - -Also copy and compile the GSettings schema: +Copy and compile the GSettings schema: ```bash cd keyman_config @@ -50,15 +47,16 @@ sudo glib-compile-schemas /usr/share/glib-2.0/schemas Running `km-config` requires a language tag mapping file `keyman_config/standards/lang_tags_map.py`. This file gets generated during a package -build, and also when running `make`. +build, and also when running `build.sh build`. ## Installing manually from the repo -`make && sudo make install` will install locally to `/usr/local`. +`linux/keyman-config/build.sh build && sudo linux/keyman-config/build.sh install` +will install locally to `/usr/local`. `pip3 help install` will give you more install options. -To uninstall you can run `sudo make uninstall`. +To uninstall you can run `sudo linux/keyman-config/build.sh uninstall`. ## Things to run from the command line diff --git a/docs/linux/packaging.md b/docs/linux/packaging.md index 91154934b44be5059f04bd4dca47717ea832a7bb..c133575d049d52e007c2696b718266013e8e21fe 100644 --- a/docs/linux/packaging.md +++ b/docs/linux/packaging.md @@ -10,7 +10,7 @@ We use different channels to build and distribute the Linux packages: [alpha](https://launchpad.net/~keymanapp/+archive/ubuntu/keyman-alpha) versions - [pso](http://packages.sil.org/) and [llso](http://linux.lsdev.sil.org/ubuntu/) for stable, beta, and alpha versions -- artifacts on [Jenkins](https://jenkins.lsdev.sil.org/view/Keyman/view/Pipeline/job/pipeline-keyman-packaging/view/change-requests/) +- artifacts on [GitHub](https://github.com/keymanapp/keyman/actions/workflows/deb-packaging.yml) for pull requests Packages on [llso](http://linux.lsdev.sil.org/ubuntu/) are uploaded automatically and are @@ -21,191 +21,63 @@ pso enabled. ## Package builds Package builds happen on [Launchpad](#package-builds-on-launchpad) and -[Jenkins](#package-builds-on-jenkins). Package builds for the official Ubuntu/Debian +[GitHub](#github-actions-package-builds). Package builds for the official Ubuntu/Debian repos happen outside of our control. However, we [upload source packages](#uploading-debian-source-packages) to the Debian community. -## Package builds on Jenkins +## GitHub Actions package builds ### Build jobs -The definition of the packaging jobs, the triggering of the jobs and the necessary build scripts -are scattered over several source repos: - -- [ci-builder-scripts](https://github.com/sillsdev/ci-builder-scripts) contains the definition of - a meta job (multi-branch pipeline job) that gets triggered when a change gets pushed to the - [Keyman GitHub repo](https://github.com/keymanapp/keyman). The meta job creates a new build - configuration/job for each branch/pull request on GitHub. The new job gets triggered to initialize - itself, but then exits immediately. We use the Jenkins - [Job DSL plugin](https://github.com/jenkinsci/job-dsl-plugin/wiki) to define the meta job. - - ci-builder-scripts also contains several generic scripts to set up a package build environment - (using `sbuilder`) and for building source and binary packages. These scripts are shared with - other projects. - - The Keyman GitHub repo defines a webhook that triggers the meta job on Jenkins. - - Changes to ci-builder-scripts go through [Gerrit](https://gerrit.lsdev.sil.org). See - [CONTRIBUTING.md](https://github.com/sillsdev/ci-builder-scripts/blob/master/CONTRIBUTING.md) - for details. - - File structure: - - - [groovy/KeymanPackagingJobs.groovy](https://github.com/sillsdev/ci-builder-scripts/blob/master/groovy/KeymanPackagingJobs.groovy) - contains the meta job definition - - The [bash/](https://github.com/sillsdev/ci-builder-scripts/tree/master/bash) subdirectory - contains `bash` scripts: - - - [setup.sh](https://github.com/sillsdev/ci-builder-scripts/blob/master/bash/setup.sh) - - setup sbuild chroot environment - - [update](https://github.com/sillsdev/ci-builder-scripts/blob/master/bash/update) - - update the sbuild chroot environment - - [build-package](https://github.com/sillsdev/ci-builder-scripts/blob/master/bash/build-package) - - create a binary package - -- [lsdev-pipeline-library](https://github.com/sillsdev/lsdev-pipeline-library) contains a reusable - Jenkins pipeline library. The - [vars/keymanPackaging.groovy](https://github.com/sillsdev/lsdev-pipeline-library/blob/master/vars/keymanPackaging.groovy) - file contains the bulk of the logic of the Keyman packaging job. - -- The [Keyman GitHub repo](https://github.com/keymanapp/keyman) contains various scripts that are - used to trigger a build and as part of the package build, and of course the source code for the - packages: - - - [resources/build/run-required-test-builds.sh](https://github.com/keymanapp/keyman/blob/master/resources/build/run-required-test-builds.sh) - runs on [TeamCity](https://build.palaso.org/buildConfiguration/Keyman_Test?) to trigger the - builds for the various platforms, among them the Jenkins package build. - - [resources/build/increment-version.sh](https://github.com/keymanapp/keyman/blob/master/resources/build/increment-version.sh) - runs on [TeamCity](https://build.palaso.org/buildConfiguration/Keyman_TriggerReleaseBuildsMaster?) - and increments the version number before triggering the builds for the various platforms. - - [linux/Jenkinsfile](https://github.com/keymanapp/keyman/blob/master/linux/Jenkinsfile) is a flag - for the meta job. If the meta job finds this file, it will create a new build configuration. This - file simply calls the packaging functionality defined in `lsdev-pipeline-library` and passes the - distributions and architectures to build as parameters. - - [linux/build/agent/install-deps](https://github.com/keymanapp/keyman/blob/master/linux/build/agent/install-deps) - installs dependencies on the current build agent. - - The [linux/scripts](https://github.com/keymanapp/keyman/tree/master/linux/scripts) subdirectory - contains `bash` scripts that are used during the package build. Some are only needed for - Launchpad builds. - - - [jenkins.sh](https://github.com/keymanapp/keyman/blob/master/linux/scripts/jenkins.sh) - gets called from `lsdev-pipeline-library` to create a source package. - - - [linux/debian](https://github.com/keymanapp/keyman/tree/master/linux/debian) - this is the `debian` - subdirectory for Keyman for Linux with the meta data for the Linux package. - See [Debian New Maintainers' Guide](https://www.debian.org/doc/manuals/maint-guide/) for - details to the various files. +The [Keyman GitHub repo](https://github.com/keymanapp/keyman) contains various +scripts that are used to trigger a build and as part of the package build, +and of course the source code for the packages: + +- [.github/workflows/deb-packaging.yml](https://github.com/keymanapp/keyman/blob/master/.github/workflows/deb-packaging.yml) + contains the definition of the packaging GHA +- [resources/build/run-required-test-builds.sh](https://github.com/keymanapp/keyman/blob/master/resources/build/run-required-test-builds.sh) + runs on [TeamCity](https://build.palaso.org/buildConfiguration/Keyman_Test?) + to trigger the builds for the various platforms, among them the GHA package build. +- [resources/build/increment-version.sh](https://github.com/keymanapp/keyman/blob/master/resources/build/increment-version.sh) + runs on [TeamCity](https://build.palaso.org/buildConfiguration/Keyman_TriggerReleaseBuildsMaster?) + and increments the version number before triggering the builds for the + various platforms. +- The [linux/scripts](https://github.com/keymanapp/keyman/tree/master/linux/scripts) + subdirectory contains `bash` scripts that are used during the package build. + Some are only needed for Launchpad builds. + + - [deb-packaging.sh](https://github.com/keymanapp/keyman/blob/master/linux/scripts/deb-packaging.sh) + gets called by the packaging GHA to install dependencies, create the source + package and to verify the API. + +- [linux/debian](https://github.com/keymanapp/keyman/tree/master/linux/debian) - + this is the `debian` subdirectory for Keyman for Linux with the meta data + for the Linux package. + See [Debian New Maintainers' Guide](https://www.debian.org/doc/manuals/maint-guide/) + for details to the various files in the `debian` directory. ### Flow of a Linux package build -- TeamCity jobs [Keyman_Test](https://build.palaso.org/buildConfiguration/Keyman_Test) or - [Keyman_TriggerReleaseBuilds*](https://build.palaso.org/buildConfiguration/Keyman_TriggerReleaseBuildsBeta) - trigger a build on [Jenkins](https://jenkins.lsdev.sil.org/view/Keyman/view/Pipeline/job/pipeline-keyman-packaging/) -- Jenkins verifies the build parameters and starts the matching build configuration for the PR or - branch -- The [build job](https://github.com/sillsdev/lsdev-pipeline-library/blob/master/vars/keymanPackaging.groovy) runs several checks: - - - it exits immediately if the build is not manually triggered and no parameters are passed in - (i.e. it got triggered by the GitHub webhook) - - it doesn't build if this is a PR, didn't get triggered manually and the PR is not from a trusted - user - - it doesn't build if no Linux-relevant files changed unless the parameter `force` was passed - - manually triggered builds will always build - -- build job installs - [dependencies](https://github.com/keymanapp/keyman/blob/master/linux/build/agent/install-deps) - on the current build agent -- build job creates a source package for the linux packages (keyman, kmflcomp, - libkmfl, and ibus-kmfl). This is done by calling - [scripts/jenkins.sh](https://github.com/keymanapp/keyman/blob/master/linux/scripts/jenkins.sh). -- build job creates the binary package for each linux package on each distribution (currently - bionic, focal, and groovy) and each architecture (amd64, i386 only for bionic) -- at the end of the build if it is not a build of a PR, the `.deb` file gets uploaded to llso - (alpha packages to e.g. `bionic-experimental`, beta packages to `bionic-proposed` and - packages build from the stable branch to the main section `bionic`) +- TeamCity jobs [Keyman_Test](https://build.palaso.org/buildConfiguration/Keyman_Test) + or [Keyman_TriggerReleaseBuilds*](https://build.palaso.org/buildConfiguration/Keyman_TriggerReleaseBuildsBeta) + trigger a packaging GHA build +- packaging GHA calls [deb-packaging.sh](https://github.com/keymanapp/keyman/blob/master/linux/scripts/deb-packaging.sh) + which installs dependencies and creates the source package +- packaging GHA creates the binary package for each linux package on each + distribution +- packaging GHA verifies that the API didn't change with the help of + [deb-packaging.sh](https://github.com/keymanapp/keyman/blob/master/linux/scripts/deb-packaging.sh) +- at the end of the build if it is not a build of a PR, the `.deb` files get + uploaded to llso (alpha packages to e.g. `jammy-experimental`, beta + packages to `jammy-proposed` and packages build from the stable branch + to the main section `jammy`) - if the build is successful the job archives the artifacts -The Jenkins build progress is visible in two ways: - -- [traditional view](https://jenkins.lsdev.sil.org/view/Keyman/view/Pipeline/job/pipeline-keyman-packaging/) -- [blue ocean view](https://jenkins.lsdev.sil.org/blue/organizations/jenkins/pipeline-keyman-packaging/activity) - -**Note:** TC release builds pass the git tag to build to the Jenkins job. The same tag -gets passed twice as parameters `tag` and `tag2`. The first parameter gets persisted between -builds, allowing to retrigger a tag-build. The second parameter is necessary to distinguish -if this is a retriggered build of a tag-build. - -### Local package builds - -It is possible to use the usual Debian/Ubuntu tools to create the package locally. For someone who -only occasionally deals with packaging it might be easier to use the scripts that Jenkins runs: - -#### Prerequisites for local package builds - -Install `sbuild` (and probably some other packages that I forgot). - -You’ll need a chroot image before you can use sbuild. The scripts in -[ci-builder-scripts](https://github.com/sillsdev/ci-builder-scripts) will help -with that. [`setup.sh`](https://github.com/sillsdev/ci-builder-scripts/blob/master/bash/setup.sh) -can setup such chroots: - -```bash -bash/setup.sh --dists "focal bionic" --arches "amd64 i386" -``` - -[`update`](https://github.com/sillsdev/ci-builder-scripts/blob/master/bash/update) is used to -later update those chroots: - -```bash -bash/update --dists "focal bionic" --arches "amd64 i386" -``` - -Set the `DEBSIGNKEY` environment variable to your public GPG key that will be used to sign -the packages. - -#### Building packages - -Building packages happen in the [Keyman source tree](https://github.com/keymanapp/keyman). - -The Keyman -[`linux/scripts/jenkins.sh`](https://github.com/keymanapp/keyman/blob/master/linux/scripts/jenkins.sh) -script can be used to create a source package. - -```bash -cd linux -./scripts/jenkins.sh keyman ${DEBSIGNKEY} -``` - -This creates a source package (`keyman_-1.dsc`) and some `*.tar.?z` -files in the source root directory for `keyman`. - -ci-builder-script's [`build-package`](https://github.com/sillsdev/ci-builder-scripts/blob/master/bash/build-package) -script creates the binary packages: - -```bash -cd $KEYMAN_ROOT -~/ci-builder-scripts/bash/build-package \ - --dists "focal bionic" --arches "amd64 i386" \ - --debkeyid ${DEBSIGNKEY} --build-in-place --no-upload -``` - -This will create the binary package `keyman_-1+1_.deb`. - -To speed up package building you might want to limit the build to a single dist -(e.g. `--dists "bionic"`) and arch (e.g. `--arches "amd64"`). - -After building packages it might be a good idea to clean up the source tree -before doing further work: - -```bash -git clean -dxf -``` - -### Local package builds (Docker) +### Local package builds with Docker It is possible to use the usual Debian/Ubuntu tools to create the package locally. For someone who only occasionally deals with packaging it might be easier to use -the scripts that run on GitHub actions: +Docker and the scripts that run on GitHub actions: #### Prerequisites for local package builds with Docker @@ -238,8 +110,10 @@ You'll have to create the docker image. ```bash cd $KEYMAN_ROOT - docker run -v $(pwd):/source -i -t -w /source --platform=linux/amd64 \ - sillsdev/jammy keyman_*.dsc /source + DIST=jammy + docker run -v $(pwd):/source -i -t -w /source --platform=amd64 \ + --env INPUT_DIST=$DIST --env INPUT_SOURCEPACKAGE=$(ls keyman_*.dsc) \ + --env INPUT_SOURCE_DIR=. sillsdev/$DIST ``` This will create the binary packages in `$KEYMAN_ROOT/artifacts`. diff --git a/docs/settings/linux/tasks.json b/docs/settings/linux/tasks.json index 77df789fe318c41f7099a86255d6476ce05c3658..9751c95a44c9a325c5a3770eb4c21f5821291d58 100644 --- a/docs/settings/linux/tasks.json +++ b/docs/settings/linux/tasks.json @@ -139,14 +139,32 @@ }, { "type": "shell", - "label": "ibus-keyman: test", + "label": "ibus-keyman: unit tests", "command": "./build.sh", - "args": ["test", "--debug"], + "args": [ + "test", + "--debug", + "--no-integration" + ], "options": { "cwd": "${workspaceFolder}/linux/ibus-keyman/", }, "group": "build", - "detail": "run tests of ibus-keyman" + "detail": "run unit tests of ibus-keyman" + }, + { + "type": "shell", + "label": "ibus-keyman: test (unit+integration)", + "command": "./build.sh", + "args": [ + "test", + "--debug" + ], + "options": { + "cwd": "${workspaceFolder}/linux/ibus-keyman/", + }, + "group": "build", + "detail": "run unit and integration tests of ibus-keyman" }, { "type": "shell", @@ -158,5 +176,100 @@ "group": "test", "detail": "unit test keyman-config" }, + { + "type": "shell", + "label": "keyman-system-service: clean", + "command": "${workspaceFolder}/linux/keyman-system-service/build.sh", + "args": [ + "--debug", + "clean" + ], + "options": { + "cwd": "${workspaceFolder}/linux/keyman-system-service" + }, + "group": "build", + "detail": "clean keyman-system-service" + }, + { + "type": "shell", + "label": "keyman-system-service: build", + "command": "${workspaceFolder}/linux/keyman-system-service/build.sh", + "args": [ + "--debug", + "build" + ], + "options": { + "cwd": "${workspaceFolder}/linux/keyman-system-service" + }, + "problemMatcher": [ + { + "owner": "cpp", + "source": "compiler", + "fileLocation": [ + "relative", + "${workspaceFolder}/linux/keyman-system-service/" + ], + "pattern": [ + { + "regexp": "^../../../(.*):(\\d+):(\\d+):\\s+(warning|error|note):\\s+(.*)(\\s+\\[.*\\])?$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + }, + { + "owner": "cpp", + "source": "linker", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^(/usr/bin/ld:\\s+)?(.*):(\\d+):\\s+(.*)$", + "file": 2, + "location": 3, + "message": 4 + } + ] + } + ], + "presentation": { + "clear": true + }, + "group": "build", + "detail": "build keyman-system-service" + }, + { + "type": "shell", + "label": "keyman-system-service: configure", + "command": "${workspaceFolder}/linux/keyman-system-service/build.sh", + "args": [ + "--debug", + "configure" + ], + "options": { + "cwd": "${workspaceFolder}/linux/keyman-system-service", + }, + "presentation": { + "clear": true + }, + "group": "build", + "detail": "configure keyman-system-service" + }, + { + "type": "shell", + "label": "keyman-system-service: test", + "command": "${workspaceFolder}/linux/keyman-system-service/build.sh", + "args": [ + "--debug", + "test:arch" + ], + "problemMatcher": [ + "$gcc" + ], + "group": "test", + "detail": "run tests for keyman-system-service" + }, ] } diff --git a/ios/.build-builder b/ios/.build-builder new file mode 100644 index 0000000000000000000000000000000000000000..898cd491a3f0afde798a8ed3f52e678683b73972 --- /dev/null +++ b/ios/.build-builder @@ -0,0 +1 @@ +The presence of this file tells CI to use the newer, builder-based script format when building iOS. \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore index a810f0a3eff366333571a69d80fbebd46e31819d..31d4700aecc1f7dcd9bc0e338e814157fc3955dc 100644 --- a/ios/.gitignore +++ b/ios/.gitignore @@ -15,7 +15,10 @@ engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanios.js engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-osk.ttf engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/kmwosk.css engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman.js.map +engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-webview.js +engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keymanweb-webview.js.map engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyman-sentry.js +engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/sentry.min.js Keyman 2* Carthage diff --git a/ios/README.md b/ios/README.md index 49c3d57233be6ddfbc0fcdf4037193d1569964fa..0c70ccf326b535df4464159a84c12dc838d55dbe 100644 --- a/ios/README.md +++ b/ios/README.md @@ -8,9 +8,10 @@ The source for our published iOS Keyman app is found within the **keyman/** subd the views specific to the Keyman app, while the core functionality is found in KeymanEngine.framework. ### Building -To build Keyman for iOS, please run the build.sh build script within this folder. -You will need to run with the command-line argument `-no-codesign` or establish alternate code-signing permissions to +To build Keyman for iOS, please run the build.sh build script within this folder against the `:app` target. +You will need to run with the command-line argument `--debug` or establish alternate code-signing permissions to complete the build for the final app. + - Thus: `./build.sh build:engine --debug` To run the app within a simulator after build.sh is completed, 1. Open **keymanios.xcworkspace** in Xcode. @@ -18,6 +19,12 @@ To run the app within a simulator after build.sh is completed, The Keyman app relies on KeymanEngine.framework, which will automatically be rebuilt by Xcode whenever it is changed. +Each individual component also possesses its own separate build script; this folder's `./build.sh` can be used +to run any and all of them as desired. + +For CI builds, note the parallel `./ci.sh` - this is the primary script used to launch builds as part of our +continuous integration processes. It may be used to set configuration for calls of the primary `./build.sh` and others. + ### Running the app on a physical device In order to test any code changes on a physical device, Apple requires that an app be code-signed. (Note that code-signing is not necessary for Simulator runs.) The quickest way to handle this for personal testing: @@ -43,7 +50,7 @@ KeymanEngine is a Swift 4 framework containing the core functionality of Keyman, * Keyboard selection and downloading. ### Building -To build using command line, run `./build.sh -only-framework`. +To build using command line, run `./build.sh build:engine --debug`. To build in Xcode, 1. Open **engine/KMEI/KeymanEngine.xcodeproj**. @@ -51,14 +58,6 @@ To build in Xcode, The framework will be built to **engine/KMEI/build/(Debug|Release)-universal/KeymanEngine.framework**. -If it doesn't build, and you have upgraded from Xcode 10.0 (or earlier) to 10.1 (or later), it may not -build due to "Could not find any available simulators for iOS" error from Carthage, probably while -building DeviceKit. Xcode 10.1 changed the output format which confuses Carthage. Upgrade Carthage: -``` -brew upgrade carthage -brew link --overwrite carthage -``` - ### Linking with KeymanEngine 1. Add KeymanEngine.framework to 'Linked Frameworks and Libraries' and 'Embedded Binaries' in your project targets. diff --git a/ios/build.sh b/ios/build.sh index 4a7f87a31af9c998ccf49189e697f713b1b695b7..933e3ca013d49da53ee81f1fb8115023ff46b081 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -1,13 +1,31 @@ #!/usr/bin/env bash -set -e -# set -x -# set -u <-- difficult to test if variable is set this way... +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE -./kmbuild.sh $* +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" -if [ ! -z "$RELEASE_OEM" ]; then - pushd ../oem/firstvoices/ios > /dev/null - ./build.sh -lib-nobuild $* - popd > /dev/null -fi +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +verify_on_mac + +builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices - iPhone and iPad." \ + "clean" \ + "configure" \ + "build" \ + ":engine Builds KeymanEngine.xcframework, usable by our main app and by third-party apps" \ + ":app=keyman Builds the Keyman app for iOS platforms" \ + ":sample1=Samples/KMSample1 Builds the first KeymanEngine sample app" \ + ":sample2=Samples/KMSample2 Builds the second KeymanEngine sample app" \ + ":fv=../oem/firstvoices/ios Builds OEM FirstVoices for iOS platforms" \ + "--sim-artifact+ Also outputs a simulator-friendly test artifact corresponding to the build" + +builder_parse "$@" + +builder_run_child_actions clean configure build \ No newline at end of file diff --git a/ios/ci.sh b/ios/ci.sh new file mode 100755 index 0000000000000000000000000000000000000000..fbcb46409f63271770b1a50a5a758f8c09b81bec --- /dev/null +++ b/ios/ci.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +verify_on_mac + +builder_describe "Defines actions for use in CI-related iOS builds." \ + "build" \ + "prep-release" + # For consideration: clean ("Cleanup Carthage Mess", "Clean XCode DerivedData Mess") + # For consideration: publish:testflight ("Upload to TestFlight (PR area)") + # For consideration: publish:app-store ("Push * to the App Store for Deployment") + # For consideration: publish:fastlane (for matching release-build steps) + +builder_parse "$@" + +function do_build() { + ./build.sh clean configure build:engine build:app --sim-artifact + + # Since we may be disabling it via build-agent environment variable at times... + if [ ! -z "${RELEASE_OEM+x}" ]; then + "$KEYMAN_ROOT/oem/firstvoices/ios/build.sh" clean configure build --sim-artifact + fi +} + +builder_run_action build do_build +# The script called below isn't builder-based, but... eh, "if it ain't broke." +builder_run_action prep_release tools/prepRelease.sh \ No newline at end of file diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 4f21ff9830f0aa9a9133a0bdf840679f565200fa..f0205767d541e09d8049a35abcf30ebc3f037a35 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -308,12 +308,12 @@ 2949146F2738DA6700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/ResourceInfoView.strings"; sourceTree = ""; }; 294914702738DD7400400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; }; 294914712738DD9700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ff-NG"; path = "ff-NG.lproj/Localizable.stringsdict"; sourceTree = ""; }; - 298566B929802892004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/ResourceInfoView.strings; sourceTree = ""; }; - 298566BA298028A8004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; - 298566BB298028AC004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 297810FE297FAEDF007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/ResourceInfoView.strings; sourceTree = ""; }; 297810FF297FAEF8007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; }; 29781100297FAF06007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kn; path = kn.lproj/Localizable.stringsdict; sourceTree = ""; }; + 298566B929802892004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/ResourceInfoView.strings; sourceTree = ""; }; + 298566BA298028A8004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + 298566BB298028AC004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = ""; }; 298566C52980C5BB004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/ResourceInfoView.strings; sourceTree = ""; }; 298566C62980C5C7004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; 298566C72980C5CC004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -528,6 +528,9 @@ CEF888D123FE6ADF00667693 /* Simple 10.0 Migration.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = "Simple 10.0 Migration.bundle"; sourceTree = ""; }; CEF888D423FE786C00667693 /* KeyboardScaleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardScaleTests.swift; sourceTree = ""; }; CEF9367724D3B0B20031C70D /* PackageInstallView_iPhone.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PackageInstallView_iPhone.xib; sourceTree = ""; }; + CEFFECD52A417F1300D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ResourceInfoView.strings; sourceTree = ""; }; + CEFFECD62A417F2900D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + CEFFECD72A417F2E00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = es; path = es.lproj/Localizable.stringsdict; sourceTree = ""; }; F243887E14BBD43000A3E055 /* KeymanEngineDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KeymanEngineDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; F27FCAEE157FD59100FBBA20 /* Keyman-iphoneos.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Keyman-iphoneos.xcconfig"; sourceTree = ""; }; F27FCAEF157FD59100FBBA20 /* Keyman-iphonesimulator.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Keyman-iphonesimulator.xcconfig"; sourceTree = ""; }; @@ -1215,6 +1218,7 @@ sv, uk, ru, + es, ); mainGroup = F243887314BBD43000A3E055; productRefGroup = F243887F14BBD43000A3E055 /* Products */; @@ -1613,6 +1617,7 @@ 298566C72980C5CC004ACA95 /* sv */, 298566D32980D39F004ACA95 /* uk */, 298566DF2980E48C004ACA95 /* ru */, + CEFFECD72A417F2E00D58C36 /* es */, ); name = Localizable.stringsdict; sourceTree = ""; @@ -1636,10 +1641,11 @@ 29BFA76B28294813009FCCC3 /* pl */, 29B27FF029062D100036917E /* nl */, 298566BA298028A8004ACA95 /* cs */, - 297810FF297FAEF8007C886D /* kn */, + 297810FF297FAEF8007C886D /* kn */, 298566C62980C5C7004ACA95 /* sv */, 298566D22980D39A004ACA95 /* uk */, 298566DE2980E486004ACA95 /* ru */, + CEFFECD62A417F2900D58C36 /* es */, ); name = Localizable.strings; sourceTree = ""; @@ -1668,6 +1674,7 @@ 298566C52980C5BB004ACA95 /* sv */, 298566D12980D390004ACA95 /* uk */, 298566DD2980E477004ACA95 /* ru */, + CEFFECD52A417F1300D58C36 /* es */, ); name = ResourceInfoView.xib; sourceTree = ""; diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift index 27495bd51e0ce504d3e6340d6037b42704c2aa57..6ab2ea2a68134b7fd33f1023c7f9b3371bf2200e 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Keyboard/KeymanWebViewController.swift @@ -336,7 +336,7 @@ extension KeymanWebViewController { } func deregisterLexicalModel(_ lexicalModel: InstallableLexicalModel) { - webView!.evaluateJavaScript("keyman.modelManager.deregister(\"\(lexicalModel.id)\")") + webView!.evaluateJavaScript("keyman.removeModel(\"\(lexicalModel.id)\")") } func registerLexicalModel(_ lexicalModel: InstallableLexicalModel) throws { @@ -397,7 +397,7 @@ extension KeymanWebViewController { webView!.evaluateJavaScript("enableSuggestions(\(stubString), \(predict), \(correct))") self.activeModel = predict } else { // We're registering a model in the background - don't change settings. - webView!.evaluateJavaScript("keyman.registerModel(\(stubString));", completionHandler: nil) + webView!.evaluateJavaScript("keyman.addModel(\(stubString));", completionHandler: nil) } setBannerHeight(to: Int(InputViewController.topBarHeight)) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift index a6e3dc70aacfe725200e6b25843b3d7869e5d1a2..a344d445b956ef3f23830fa6b8317d3b6b814161 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/Storage.swift @@ -124,11 +124,11 @@ class Storage { return nil } } - + func keyboardURL(for keyboard: InstallableKeyboard) -> URL { return resourceDir(for: keyboard)!.appendingPathComponent("\(keyboard.id).js") } - + func lexicalModelURL(for lexicalModel: InstallableLexicalModel) -> URL { return resourceDir(for: lexicalModel)!.appendingPathComponent("\(lexicalModel.id).model.js") } @@ -154,7 +154,7 @@ class Storage { return nil } } - + func lexicalModelPackageURL(for lexicalModel: InstallableLexicalModel) -> URL { return resourceDir(for: lexicalModel)!.appendingPathComponent("\(lexicalModel.id).model.kmp") } @@ -178,11 +178,11 @@ class Storage { } return legacyKeyboardDir(forID: keyboardID).appendingPathComponent("\(keyboardID)-\(version).js") } - + func legacyLexicalModelURL(forID lexicalModelID: String, version: String) -> URL { return legacyLexicalModelDir(forID: lexicalModelID).appendingPathComponent("\(lexicalModelID)-\(version).model.js") } - + func legacyLexicalModelPackageURL(forID lexicalModelID: String, version: String, asZip: Bool = false) -> URL { // Our unzipping dependency requires a .zip extension to function, so we append that here. return legacyLexicalModelDir(forID: lexicalModelID).appendingPathComponent("\(lexicalModelID)-\(version).model.kmp\(asZip ? ".zip" : "")") @@ -225,7 +225,11 @@ extension Storage { dstDir: baseDir, excludeFromBackup: true) try Storage.copy(from: bundle, - resourceName: "keymanios.js", + resourceName: "keymanweb-webview.js", + dstDir: baseDir, + excludeFromBackup: true) + try Storage.copy(from: bundle, + resourceName: "sentry.min.js", dstDir: baseDir, excludeFromBackup: true) try Storage.copy(from: bundle, @@ -233,9 +237,9 @@ extension Storage { dstDir: baseDir, excludeFromBackup: true) // For debug compilations - IF we have a sourcemap file, copy that over too. - if bundle.url(forResource: "keyman.js.map", withExtension: nil) != nil { + if bundle.url(forResource: "keymanweb-webview.js.map", withExtension: nil) != nil { try Storage.copy(from: bundle, - resourceName: "keyman.js.map", + resourceName: "keymanweb-webview.js.map", dstDir: baseDir, excludeFromBackup: true) } @@ -286,7 +290,7 @@ extension Storage { // Perform an auto-install of the lexical model's KMP if not already installed. let defaultKMPFile = defaultLexicalModelDir.appendingPathComponent("\(Defaults.lexicalModel.id).model.kmp") let package = try ResourceFileManager.shared.prepareKMPInstall(from: defaultKMPFile) as! LexicalModelKeymanPackage - + // Install all languages for the model, not just the default-listed one. try ResourceFileManager.shared.install(resourcesWithIDs: package.installables[0].map { $0.fullID }, from: package) } catch { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/es.lproj/ResourceInfoView.strings b/ios/engine/KMEI/KeymanEngine/Classes/es.lproj/ResourceInfoView.strings new file mode 100644 index 0000000000000000000000000000000000000000..e6ddc961a2f8e475aa0f469e21bb287717053c3e --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/es.lproj/ResourceInfoView.strings @@ -0,0 +1,2 @@ +/* Class = "UILabel"; text = "Scan this code to load this keyboard on another device"; ObjectID = "z2O-MT-IoV"; */ +"z2O-MT-IoV.text" = "Escanee este código para cargar este teclado en otro dispositivo"; diff --git a/ios/engine/KMEI/KeymanEngine/es.lproj/Localizable.strings b/ios/engine/KMEI/KeymanEngine/es.lproj/Localizable.strings new file mode 100644 index 0000000000000000000000000000000000000000..d2e753247be7e46aade5d49b2ea3f52310e5301d --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/es.lproj/Localizable.strings @@ -0,0 +1,257 @@ +/* A descriptive message used for errors when the app is already busy downloading */ +"alert-download-error-busy" = "Ya estoy ocupado descargando."; + +/* A descriptive message used when a download fails */ +"alert-download-error-detail" = "Se ha producido un error durante la descarga o la instalación."; + +/* Title for a "download failed" alert */ +"alert-download-error-title" = "Error de descarga"; + +/* Title for a general alert about errors */ +"alert-error-title" = "Error"; + +/* A descriptive message used when no internet connection is detected */ +"alert-no-connection-detail" = "No se puede conectar con el servidor de Keyman. Por favor, inténtelo de nuevo más tarde."; + +/* Title for a "no connection" alert */ +"alert-no-connection-title" = "Error de conexión"; + +/* Short text for a 'back' navigational command. Used to return to the previous screen */ +"command-back" = "Atrás"; + +/* Short text for a 'cancel' command. Used to back out of a menu or install process without making changes */ +"command-cancel" = "Cancelar"; + +/* Short text for a 'done' command. Used to back out of settings menus after changes have been made */ +"command-done" = "Completado"; + +/* Short text for an 'install' command. Used to confirm installation of a package. */ +"command-install" = "Instalar"; + +/* Short text for a 'next' navigational command. Used to advance to the next screen */ +"command-next" = "Siguiente"; + +/* Short text for an 'OK' command. Used for some error alerts */ +"command-ok" = "Aceptar"; + +/* Short text for confirmining 'uninstall' commands. */ +"command-uninstall" = "Desinstalar"; + +/* Text for the command to uninstall a keyboard */ +"command-uninstall-keyboard" = "Desinstalar teclado"; + +/* Confirmation text to display before uninstalling a keyboard */ +"command-uninstall-keyboard-confirm" = "¿Le gustaría desinstalar este teclado?"; + +/* Text for the command to uninstall a lexical model */ +"command-uninstall-lexical-model" = "Desinstalar diccionario"; + +/* Confirmation text to display before uninstalling a lexical model */ +"command-uninstall-lexical-model-confirm" = "¿Quieres desinstalar este diccionario?"; + +/* Text for error when a keyboard cannot load properly */ +"error-loading-keyboard" = "No se puede cargar el teclado solicitado"; + +/* Text for error when a lexical model cannot load properly */ +"error-loading-lexical-model" = "No se puede cargar el diccionario solicitado"; + +/* Text for error when an installed file is unexpectedly missing */ +"error-missing-file" = "No se ha encontrado un archivo requerido."; + +/* Text for error when an installed file is unexpectedly missing */ +"error-missing-file-critical" = "No se pudo encontrar un archivo crítico. Intente reinstalar la aplicación."; + +/* Text for errors in data received from search queries */ +"error-query-decoding" = "El servidor está experimentando dificultades técnicas"; + +/* A descriptive message used when a download fails */ +"error-query-general" = "Error de comunicación con el servidor"; + +/* Text for error when a search query is unexpectedly empty */ +"error-query-no-data" = "El servidor no ha respondido"; + +/* Text for general errors where not much information is known */ +"error-unknown" = "Se ha producido un error inesperado"; + +/* Text for error when updating a resource: cannot download because no source is available */ +"error-update-no-link" = "No hay ninguna fuente de actualizaciones disponible para este paquete"; + +/* Text for error when updating a resource: Keyman does not know how to update the resource */ +"error-update-not-managed" = "No se puede actualizar un recurso no gestionado por el motor Keyman"; + +/* Text for the command to open the help page of a keyboard, as shown on resource information views */ +"info-command-help-keyboard" = "Ayuda del teclado"; + +/* Text for the command to open the help page of a lexical model, as shown on resource information views */ +"info-command-help-lexical-model" = "Ayuda del diccionario"; + +/* Text label for displaying a keyboard's version, as shown on resource information views */ +"info-label-version-keyboard" = "Versión del teclado"; + +/* Text label for displaying a lexical model's version, as shown on resource information views */ +"info-label-version-lexical-model" = "Versión del diccionario"; + +/* Label used for the "package info" / readme tab with the package installation prompt on phones. */ +"installer-label-package-info" = "Información de paquete"; + +/* Label used for the language-selection tab with the package installation prompt on phones. */ +"installer-label-select-languages" = "Seleccionar idioma(s)"; + +/* Label used with a package's version, as seen within the package installation prompt. Example: "Version: 14.0.0" */ +"installer-label-version" = "Versión: %@"; + +/* Section header for languages supported by a package, as seen within the package installation prompt */ +"installer-section-available-languages" = "Idiomas disponibles"; + +/* Text for the help popup for changing keyboards with the globe key */ +"keyboard-help-change" = "Pulsa aquí para cambiar el teclado"; + +/* Text for the command to exit the Keyman keyboard in favor of other keyboards installed on the system */ +"keyboard-menu-exit" = "Cerrar %@"; + +/* Error installing a Keyman package - could not allocate a location to install it */ +"kmp-error-file-system" = "Error al instalar el paquete - error en el sistema de archivos"; + +/* Error installing a Keyman package - could not copy package files */ +"kmp-error-file-copying" = "Error al instalar el paquete - no se pudieron copiar los archivos requeridos"; + +/* Error opening a Keyman package - package is not valid */ +"kmp-error-invalid" = "El archivo del paquete está corrupto."; + +/* Error opening a Keyman package - it does not exist / the specified location is wrong */ +"kmp-error-missing" = "El paquete especificado no existe."; + +/* Error installing a Keyman package - expected resource (keyboard or dictionary) is missing */ +"kmp-error-missing-resource" = "Este paquete no contiene el teclado o el diccionario solicitado."; + +/* Error installing a Keyman package with a version of Keyman that does not support it */ +"kmp-error-unsupported-keyman-version" = "Este paquete requiere una versión más reciente de Keyman."; + +/* Error opening a Keyman package - cannot parse contents */ +"kmp-error-no-metadata" = "Este paquete no fue compilado correctamente - contenidos desconocidos."; + +/* Error opening a Keyman package - package's contents are for desktop platforms only */ +"kmp-error-unsupported" = "Este paquete no incluye asistencia para su dispositivo."; + +/* Error opening a Keyman package - package contains unexpected resource (keyboard or dictionary) type */ +"kmp-error-wrong-type" = "Este paquete no contiene el tipo de recurso esperado."; + +/* Title for the Installed Languages menu */ +"menu-installed-languages-title" = "Idiomas instalados"; + +/* Section header for lexical models within a language-specific settings menu */ +"menu-langsettings-label-lexical-models" = "Diccionarios"; + +/* Section header for keyboards within a language-specific settings menu */ +"menu-langsettings-section-keyboards" = "Teclados"; + +/* Section header for the settings toggles within a language-specific settings menu */ +"menu-langsettings-section-settings" = "Configuración de idioma"; + +/* Title for the language-specific settings menus */ +"menu-langsettings-title" = "%@ Configuración"; + +/* Label for the toggle that enables corrections that is displayed within a language-specific settings menu */ +"menu-langsettings-toggle-correct" = "Habilitar correcciones"; + +/* Label for the toggle that enables predictions that is displayed within a language-specific settings menu */ +"menu-langsettings-toggle-predict" = "Habilitar predicciones"; + +/* Help message for a prompt that appears for confirming a lexical model download: language (1): lexical model (dictionary) name (2) */ +"menu-lexical-model-install-message" = "¿Quieres instalar este diccionario?"; + +/* Title for a prompt that appears for confirming a lexical model download: language (1): lexical model (dictionary) name (2) */ +"menu-lexical-model-install-title" = "%1$@: %2$@"; + +/* Text for an info alert indicating that no lexical models are available */ +"menu-lexical-model-none-message" = "No hay diccionarios disponibles"; + +/* Title for the lexical model menu, a submenu of the language-specific settings menus */ +"menu-lexical-model-title" = "%@ Diccionarios"; + +/* Title for the keyboard picker */ +"menu-picker-title" = "Teclados"; + +/* Primary text for the Settings menu option to report keyboard crashes */ +"menu-settings-error-report" = "Permitir reporte de error"; + +/* Secondary text for the Settings menu option to report keyboard crashes */ +"menu-settings-error-report-description" = "Puede requerir \"acceso completo\""; + +/* Primary text for the Settings menu local-file package installation option */ +"menu-settings-install-from-file" = "Instalar desde archivo"; + +/* Secondary text for the Settings menu local-file package installation option */ +"menu-settings-install-from-file-description" = "Buscar archivos .kmp"; + +/* Primary text for the Settings menu option that displays the iOS system menu options for the app's keyboard */ +"menu-settings-system-keyboard-menu" = "Configuración del teclado del sistema"; + +/* Label for the "Show Banner" toggle on the main settings screen */ +"menu-settings-show-banner" = "Mostrar banner"; + +/* Label for the "Get Started" automatic display toggle seen in the Settings menu */ +"menu-settings-startup-get-started" = "Mostrar \"Comenzar\" al inicio"; + +/* Title for the main Settings menu */ +"menu-settings-title" = "Configuración de Keyman"; + +/* Secondary text showing current setting for spacebar caption - blank */ +"menu-settings-spacebar-hint-blank" = "No mostrar el título en la barra espaciadora"; + +/* Secondary text showing current setting for spacebar caption - keyboard */ +"menu-settings-spacebar-hint-keyboard" = "Mostrar nombre del teclado en la barra espaciadora"; + +/* Secondary text showing current setting for spacebar caption - language */ +"menu-settings-spacebar-hint-language" = "Mostrar el nombre del idioma en la barra espaciadora"; + +/* Secondary text showing current setting for spacebar caption - language + keyboard */ +"menu-settings-spacebar-hint-languageKeyboard" = "Mostrar el idioma y el nombre del teclado en la barra espaciadora"; + +/* Label for the "Spacebar Caption" item on the main settings screen */ +"menu-settings-spacebar-text" = "Leyenda del teclado"; + +/* Title for the "Spacebar Caption" settings screen */ +"menu-settings-spacebar-title" = "Leyenda del teclado"; + +/* Text showing name of spacebar caption - blank */ +"menu-settings-spacebar-item-blank" = "Vacío"; + +/* Text showing name of spacebar caption - keyboard */ +"menu-settings-spacebar-item-keyboard" = "Teclado"; + +/* Text showing name of spacebar caption - language */ +"menu-settings-spacebar-item-language" = "Idioma"; + +/* Text showing name of spacebar caption - language + keyboard */ +"menu-settings-spacebar-item-languageKeyboard" = "Idioma y teclado"; + +/* Short text for notification: download failure for keyboard */ +"notification-download-failure-keyboard" = "Error al descargar el teclado"; + +/* Short text for notification: download failure for lexical model */ +"notification-download-failure-lexical-model" = "Error al descargar el diccionario"; + +/* Short text for notification: download success for keyboard */ +"notification-download-success-keyboard" = "Teclado descargado correctamente"; + +/* Short text for notification: download success for lexical model */ +"notification-download-success-lexical-model" = "Modelo léxico descargado correctamente"; + +/* Short text for notification: downloading a keyboard */ +"notification-downloading-keyboard" = "Descargando teclado\U2026"; + +/* Short text for notification: downloading a lexical model */ +"notification-downloading-lexical-model" = "Descargando diccionario\U2026"; + +/* Short text for notification: an update is available */ +"notification-update-available" = "Actualización disponible"; + +/* Short text for notification: currently updating */ +"notification-update-processing" = "Actualizando\U2026"; + +/* Text indicating success at installing new keyboards or dictionaries */ +"success-install" = "Instalado correctamente."; + +/* A title to use in alerts indicating 'success' at whatever task the user requested */ +"success-title" = "Completado"; diff --git a/ios/engine/KMEI/KeymanEngine/es.lproj/Localizable.stringsdict b/ios/engine/KMEI/KeymanEngine/es.lproj/Localizable.stringsdict new file mode 100644 index 0000000000000000000000000000000000000000..ba3bad468264d57ea2f65f998be3e96763e832d1 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/es.lproj/Localizable.stringsdict @@ -0,0 +1,118 @@ + + + + + menu-langsettings-lexical-model-count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u diccionario instalado + other + %u diccionarios instalados + + + notification-update-failed + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u actualización fallida + other + %u actualizaciones fallidas + + + notification-update-success + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u actualización exitosa + other + %u actualizaciones exitosas + + + settings-keyboards-installed-count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u teclado instalado + other + %u teclados instalados + + + settings-languages-installed-count + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + %u idioma instalado + other + %u idiomas instalados + + + package-default-found-keyboards + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + Se encontró %u teclado en el paquete: + other + Se encontró %u teclados en el paquete: + + + package-default-found-lexical-models + + NSStringLocalizedFormatKey + %#@count@ + count + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + u + one + Se encontró %u diccionario en el paquete: + other + Se encontró %u diccionarios en el paquete: + + + + diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js index 63caa18869bde7ccf779876a783eef188bb3006c..6c31618fc48f141598e1daf7fbac68b872811038 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/ios-host.js @@ -8,8 +8,9 @@ if(_debug) { document.documentElement.appendChild(iframe); iframe.parentNode.removeChild(iframe); iframe = null; - if (typeof(window.webkit) != 'undefined') + if (typeof(window.webkit) != 'undefined') { window.webkit.messageHandlers.keyman.postMessage("ios-log:#iOS#" + log); + } }; console.debug = console.log; console.info = console.log; @@ -22,10 +23,12 @@ if(_debug) { var oskHeight = 0; var oskWidth = 0; +var bannerHeight = 0; -var sentryManager = new com.keyman.KeymanSentryManager({ +var sentryManager = new KeymanSentryManager({ hostPlatform: "ios" }); + sentryManager.init(); window.addEventListener('load', init, false); @@ -40,57 +43,60 @@ function init() { document.body.style.height = window.outerHeight; var kmw=window['keyman']; - // We could convert to relying on the promise, but the underlying input element - // tends to show a bit due to the delay when we do so. - kmw.init({'app':device,'fonts':'fonts/'});//.then(function() { - kmw['util']['setOption']('attachType','manual'); - kmw['oninserttext'] = insertText; - kmw['showKeyboardList'] = menuKeyDown; - kmw['hideKeyboard'] = hideKeyboard; - kmw['getOskHeight'] = getOskHeight; - kmw['getOskWidth'] = getOskWidth; - kmw['beepKeyboard'] = beepKeyboard; - kmw['setActiveElement']('ta'); - - //}); + kmw['showKeyboardList'] = menuKeyDown; + kmw['menuKeyUp'] = menuKeyUp; + kmw['hideKeyboard'] = hideKeyboard; + kmw['getOskHeight'] = getOskHeight; + kmw['getOskWidth'] = getOskWidth; + kmw['beepKeyboard'] = beepKeyboard; + kmw.init({ + 'embeddingApp':device, + 'fonts':'fonts/', + oninserttext: insertText + }).then(function() { + if(bannerHeight > 0) { + // The OSK is not available until initialization is complete. + kmw.osk.bannerView.activeBannerHeight = bannerHeight; + keyman.refreshOskLayout(); + } + }); } function verifyLoaded() { // During proper loads of KMW, a keyboard will be set very early on. // There's a chance that the keyboard is still loading, so we double-check // against the stub count. - if(!keyman.core.activeKeyboard && keyman.keyboardManager.keyboardStubs.length == 0) { + if(!keyman.core.activeKeyboard && !keyman.keyboardRequisitioner.cache.defaultStub) { location.reload(); } } function showBanner(flag) { console.log("Setting banner display for dictionaryless keyboards to " + flag); - keyman.osk.banner.setOptions({'alwaysShow': flag}); + keyman.osk.bannerController.setOptions({'alwaysShow': flag}); } function setBannerImage(path) { var kmw=window['keyman']; - kmw.osk.banner.setOptions({"imagePath": path}); + if(kmw.osk) { + kmw.osk.bannerController.setOptions({"imagePath": path}); + } } function setBannerHeight(h) { - var kmw = com.keyman.singleton; - var osk = kmw.osk; - - if(h >= 0) { + if(h >= 0) { // The banner itself may not be loaded yet. This will preemptively help set // its eventual display height. - com.keyman.osk.Banner.DEFAULT_HEIGHT = h; + bannerHeight = h; - if(osk.banner.activeType != 'blank') { - osk.banner.height = h; - } + if(keyman.osk) { + keyman.osk.bannerView.activeBannerHeight = h; } + } - // Refresh KMW's OSK - kmw.correctOSKTextSize(); - doResetContext(); + // Refresh KMW's OSK + keyman.refreshOskLayout(); + doResetContext(); } function setOskHeight(height) { @@ -99,8 +105,7 @@ function setOskHeight(height) { if(kmw && kmw.core && kmw.core.activeKeyboard) { kmw.core.activeKeyboard.refreshLayouts(); } - kmw.osk.show(true); - kmw.correctOSKTextSize(); + kmw.refreshOskLayout(); doResetContext(); } @@ -129,11 +134,11 @@ function getOskHeight() { var keyboardOffset = 0; function setKeymanLanguage(stub) { var kmw = window.keyman; - + KeymanWeb.registerStub(stub); - - kmw.setActiveKeyboard(stub.KP + '::' + stub.KI, stub.KLC).then(function() { - kmw.osk.show(true); + + kmw.setActiveKeyboard(stub.KI, stub.KLC).then(function() { + keyman.refreshOskLayout(); doResetContext(); }); } @@ -264,30 +269,32 @@ function doResetContext() { } function setCursorRange(pos, length) { + var context = keyman.context; + //console.log('setCursorRange('+pos+', '+length+')'); - var ta = document.getElementById('ta'); - var resetContext = (ta.selectionStart != pos || ta.selectionEnd != pos + length); - ta.selectionStart = ta._KeymanWebSelectionStart = pos; - ta.selectionEnd = ta._KeymanWebSelectionEnd = pos + length; - if(resetContext) { - //console.log(' setCursorRange: resetting context'); - doResetContext(); + + var start = pos; + var end = pos + length; + + if(context.selStart != start || context.selEnd != end) { + keyman.context.setSelection(start, end); + keyman.resetContext(); } - return ta.selectionEnd; } function setKeymanVal(text) { //console.log('setKeymanVal('+JSON.stringify(text)+')'); - if(undefined == text) text = ''; - var ta = document.getElementById('ta'); - var resetContext = ta.value != text; - ta.value = text; - if(resetContext) { - //console.log(' setKeymanVal: resetting context'); - doResetContext(); + if(text == undefined) { + text = ''; } - return ta.value; + + if(keyman.context.getText() != text) { + keyman.context.setText(text); + keyman.resetContext(); + } + + return keyman.context.getText(); } function executePopupKey(keyID, keyText) { @@ -316,16 +323,13 @@ function toHex(theString) { function enableSuggestions(model, mayPredict, mayCorrect) { // Set the options first so that KMW's ModelManager can properly handle model enablement states // the moment we actually register the new model. - keyman.osk.banner.setOptions({ - 'mayPredict': mayPredict, - 'mayCorrect': mayCorrect - }); + keyman.core.languageProcessor.mayPredict = mayPredict; + keyman.core.languageProcessor.mayCorrect = mayCorrect; - keyman.modelManager.register(model); + keyman.addModel(model); } function setSpacebarText(mode) { - keyman.options['spacebarText'] = mode; - keyman.osk.show(true); + keyman.config.spacebarText = mode; } diff --git a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyboard.html b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyboard.html index 7f03e7c2a151ce568c5e8d25ab7f4e1fc2c86463..266a9ea1b435b9e730b3e8785185df504c75c0d6 100644 --- a/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyboard.html +++ b/ios/engine/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources/keyboard.html @@ -3,7 +3,8 @@ Keyman - + + - diff --git a/ios/engine/build.sh b/ios/engine/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..8a77c519ce15271026b0c65ab8f8b061150b68d3 --- /dev/null +++ b/ios/engine/build.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-download-resources.sh" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +verify_on_mac + +builder_describe "Builds Keyman Engine for use on iOS devices - iPhone and iPad." \ + "@/web/src/app/webview build" \ + "@/common/web/sentry-manager build" \ + "clean" \ + "configure" \ + "build" \ + "--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build" + +builder_parse "$@" + +CONFIG="Release" +if builder_is_debug_build; then + CONFIG="Debug" +fi + +builder_describe_outputs \ + configure /ios/Carthage/Build \ + build /ios/build/Build/Products/Release/KeymanEngine.xcframework + +# Base definitions (must be before do_clean call) +DERIVED_DATA="$KEYMAN_ROOT/ios/build" +BUILD_PATH="$DERIVED_DATA/Build/Products" + +# Extended path definitions +BUNDLE_PATH="${THIS_SCRIPT_PATH}/KMEI/KeymanEngine/resources/Keyman.bundle/Contents/Resources" + +# Needed before `configure` action +DEFAULT_KBD_ID="sil_euro_latin" +DEFAULT_LM_ID="nrc.en.mtnt" + +# Engine library build path +KEYMAN_XCFRAMEWORK="$BUILD_PATH/$CONFIG/KeymanEngine.xcframework" + +XCODEFLAGS="-quiet -configuration $CONFIG" +XCODEFLAGS_EXT="$XCODEFLAGS -derivedDataPath \"$DERIVED_DATA\" -workspace ../keymanios.xcworkspace" + +CODE_SIGN= +if builder_is_debug_build; then + CODE_SIGN="CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO ${DEV_TEAM:-} CODE_SIGN_ENTITLEMENTS= CODE_SIGNING_ALLOWED=NO" +fi + +### START OF THE BUILD ### + +function do_clean () { + # Possible TODO: can we clean the engine target without also cleaning the app target? + rm -rf "$BUILD_PATH" + rm -rf Carthage +} + +function carthage_die() { + local msg="$1" + + # Don't leave a trace of the failed folder; we'd have to rebuild stuff anyway. + # This way, a later re-run doesn't think `configure` succeeded when it did not. + rm -rf Carthage + builder_die "$1" +} + +function do_carthage() { + # Carthage setup must be handled from the directory with the Cartfile / Cartfile.resolved, it seems. + pushd .. > /dev/null + + carthage checkout || carthage_die "Carthage dependency checkout failed" + + # --no-use-binaries: due to https://github.com/Carthage/Carthage/issues/3134, + # which affects the sentry-cocoa dependency. + carthage build --use-xcframeworks --no-use-binaries --platform iOS || carthage_die "Carthage dependency loading failed" + + popd > /dev/null +} + +function do_packages() { + mkdir -p "$BUNDLE_PATH" + + downloadKeyboardPackage "$DEFAULT_KBD_ID" "$BUNDLE_PATH/$DEFAULT_KBD_ID.kmp" + downloadModelPackage "$DEFAULT_LM_ID" "$BUNDLE_PATH/$DEFAULT_LM_ID.model.kmp" +} + +function do_configure ( ) { + do_packages + do_carthage +} + +# Manages KeymanEngine.bundle, which is included inside the :engine target. +function update_bundle ( ) { + mkdir -p "$BUNDLE_PATH" + + KMW_PRODUCT="$KEYMAN_ROOT/web/build/app/webview/$CONFIG" + KMW_RESOURCES="$KEYMAN_ROOT/web/build/app/resources" + + #Copy over the relevant resources! It's easiest to do if we navigate to the resulting folder. + cp "$KMW_RESOURCES/osk/kmwosk.css" "$BUNDLE_PATH/kmwosk.css" + cp "$KMW_RESOURCES/osk/keymanweb-osk.ttf" "$BUNDLE_PATH/keymanweb-osk.ttf" + cp "$KMW_PRODUCT/keymanweb-webview.js" "$BUNDLE_PATH/keymanweb-webview.js" + + if builder_is_debug_build; then + cp "$KMW_PRODUCT/keymanweb-webview.js.map" "$BUNDLE_PATH/keymanweb-webview.js.map" + else + rm -f "$BUNDLE_PATH/keymanweb-webview.js.map" + fi + + cp "$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" "$BUNDLE_PATH/sentry.min.js" + cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "$BUNDLE_PATH/keyman-sentry.js" +} + +# First things first - update our dependencies. +function build_engine() { + update_bundle + + echo + echo "Build products will be set with the following version metadata:" + echo " * VERSION=$VERSION" + echo " * VERSION_WITH_TAG=$VERSION_WITH_TAG" + echo " * VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT" + echo " * UPLOAD_SENTRY=$UPLOAD_SENTRY" + echo + echo "Building KMEI..." + + rm -rf "$BUILD_PATH/$CONFIG-universal" + + run_xcodebuild $XCODEFLAGS_EXT \ + $CODE_SIGN \ + -scheme KME-universal \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG \ + VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ + UPLOAD_SENTRY=$UPLOAD_SENTRY + + assertDirExists "$KEYMAN_XCFRAMEWORK" +} + +builder_run_action clean do_clean +builder_run_action configure do_configure +builder_run_action build build_engine \ No newline at end of file diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index 012d3ba08f1b61dfdea3f05f12b0c80ec06a540e..5f2fad4ced7406c1abcf10672bb73b337e60fa14 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -205,8 +205,8 @@ 16A9229D20325253003CC98E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Keyman/Info.plist; sourceTree = SOURCE_ROOT; }; 293EA3E027059C6900545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/Localizable.strings; sourceTree = ""; }; 294914742738DF7700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; }; - 298566B829802828004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 297810FD297FA818007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; }; + 298566B829802828004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 298566C42980C493004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; 298566D02980D354004ACA95 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 298566DC2980E416004ACA95 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; @@ -994,7 +994,7 @@ outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; + shellPath = "/usr/bin/env bash"; shellScript = "if which swiftlint >/dev/null; then\n swiftlint --config ../../.swiftlint.yml\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; }; CE7E13D8240E285800252C00 /* Run Script - set versions */ = { @@ -1030,7 +1030,7 @@ outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; + shellPath = "/usr/bin/env bash"; shellScript = ". \"$KEYMAN_ROOT/resources/build/xcode-utils.sh\"\n\n# Calls resource script to update build products' versioning.\nphaseSetBundleVersions\n"; showEnvVarsInLog = 0; }; diff --git a/ios/keyman/Keyman/Keyman/es.lproj/Localizable.strings b/ios/keyman/Keyman/Keyman/es.lproj/Localizable.strings new file mode 100644 index 0000000000000000000000000000000000000000..34032ade1fae11de5664ed7280dc8246663fa9f2 --- /dev/null +++ b/ios/keyman/Keyman/Keyman/es.lproj/Localizable.strings @@ -0,0 +1,81 @@ +/* Title for the bookmarks menu within the embedded browser */ +"browser-bookmarks-add-title" = "Añadir a Favoritos"; + +/* Title for the bookmarks menu within the embedded browser */ +"browser-bookmarks-none" = "No hay favoritos"; + +/* Title for the bookmarks menu within the embedded browser */ +"browser-bookmarks-title" = "Favoritos"; + +/* Used to confirm a user's desire to install a package */ +"confirm-install" = "Instalar"; + +/* The label for the toggle to stop automatically showing the "Get Started" tutorial popup. */ +"disable-get-started" = "No mostrar de nuevo"; + +/* Indicates an error opening a web page requested by a user */ +"error-opening-page" = "No se puede abrir la página"; + +/* Long-form used when installing a font in order to display a language properly. */ +"font-install-description" = "Toque Instalar para que %@ se muestre correctamente en todas sus aplicaciones"; + +/* The name of the iOS Settings menu option for keyboards */ +"ios-settings-keyboards" = "Teclados"; + +/* The name of the iOS Settings menu option for giving the keyboard full access. (The menu entry +underneath the app's name within the app-specific keyboard menu.) */ +"ios-settings-allow-full-access" = "Permitir Acceso Total"; + +/* Short-form used when installing a font in order to display a language properly. */ +"language-for-font" = "%@ Fuente"; + +/* Used for 'add' options within menus */ +"menu-add" = "Añadir"; + +/* Used to indicate a sequence of menu options that a user needs to copy. May be chained. Example: "Keyboards (1) > Enable Keyman (2)" */ +"menu-breadcrumbing" = "%1$@ > %2$@"; + +/* Used to exit a menu without choosing an option */ +"menu-cancel" = "Cancelar"; + +/* Menu option that erases all previously-typed text. */ +"menu-clear-text" = "Borrar texto"; + +/* Menu option that displays a list designed to help users start using the app */ +"menu-get-started" = "Empezar"; + +/* Menu option that displays help for the app */ +"menu-help" = "Información"; + +/* Menu option that displays the main screen's drop-down menu */ +"menu-more" = "Más"; + +/* Menu option used to edit keyboard and dictionary settings */ +"menu-settings" = "Configuración"; + +/* Menu option that displays the iOS Share menu */ +"menu-share" = "Compartir"; + +/* Menu option that displays an embedded web browser. */ +"menu-show-browser" = "Navegador"; + +/* Menu option used to control in-app font size. */ +"menu-text-size" = "Tamaño del texto"; + +/* Used to describe the current font size */ +"text-size-label" = "Tamaño de texto: %i"; + +/* Text to indicate that a user should set a toggle within iOS Settings to 'active'. */ +"toggle-to-enable" = "Activar %@"; + +/* First option on the Get Started tutorial - Add a keyboard for your language */ +"tutorial-add-keyboard" = "Añadir un teclado para su idioma"; + +/* Third option on the Get Started tutorial - Displays app help. */ +"tutorial-show-help" = "Más información"; + +/* Second option on the Get Started tutorial - Set up Keyman as system-wide keyboard */ +"tutorial-system-keyboard" = "Configurar Keyman como teclado de sistema"; + +/* Used to display app version (as in \"Version: 1.0.2\" */ +"version-label" = "Versión %@"; diff --git a/ios/keyman/build.sh b/ios/keyman/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..64b2cc2c976b1f4a5b29de737fa43e96e9132698 --- /dev/null +++ b/ios/keyman/build.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/resources/build/build-help.inc.sh" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. + +builder_describe "Builds Keyman Engine and the Keyman app for use on iOS devices - iPhone and iPad." \ + "@/ios/engine build" \ + "clean" \ + "configure" \ + "build" \ + "--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build" + +builder_parse "$@" + +CONFIG="Release" +if builder_is_debug_build; then + CONFIG="Debug" +fi + +builder_describe_outputs \ + build /ios/build/Build/Products/Release-iphoneos/Keyman.xcarchive + +# Base definitions (must be before do_clean call) +DERIVED_DATA="$KEYMAN_ROOT/ios/build" +BUILD_PATH="$DERIVED_DATA/Build/Products" + +# Needed before `configure` action +DEFAULT_KBD_ID="sil_euro_latin" +DEFAULT_LM_ID="nrc.en.mtnt" + +# Build product paths +APP_BUNDLE_PATH="$BUILD_PATH/${CONFIG}-iphoneos/Keyman.app" +KEYBOARD_BUNDLE_PATH="$BUILD_PATH/${CONFIG}-iphoneos/SWKeyboard.appex" +ARCHIVE_PATH="$BUILD_PATH/${CONFIG}-iphoneos/Keyman.xcarchive" + +# Engine library build path +KEYMAN_XCFRAMEWORK="$BUILD_PATH/$CONFIG/KeymanEngine.xcframework" + +XCODEFLAGS="-quiet -configuration $CONFIG" +XCODEFLAGS_EXT="$XCODEFLAGS -derivedDataPath \"$DERIVED_DATA\" -workspace ../keymanios.xcworkspace" + +CODE_SIGN= +if builder_is_debug_build; then + CODE_SIGN="CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO ${DEV_TEAM:-} CODE_SIGN_ENTITLEMENTS= CODE_SIGNING_ALLOWED=NO" +fi + +### START OF THE BUILD ### + +function do_clean () { + rm -rf "$BUILD_PATH" +} + +function do_configure() { + # do literally nothing + true +} + +function build_app() { + echo "Building offline help." + build_help_html ios keyman/Keyman/Keyman/resources/OfflineHelp.bundle/Contents/Resources + + echo "" + echo "Building Keyman app." + + # Provides a needed link for codesigning for our CI. + if ! [ -z "${DEVELOPMENT_TEAM+x}" ]; then + DEV_TEAM="DEVELOPMENT_TEAM=${DEVELOPMENT_TEAM}" + else + DEV_TEAM= + fi + + # Time to prepare the deployment archive data. + echo "" + echo "Preparing .xcarchive for real devices." + run_xcodebuild $XCODEFLAGS_EXT \ + $CODE_SIGN \ + -scheme Keyman \ + -archivePath "$ARCHIVE_PATH" \ + archive \ + -allowProvisioningUpdates \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG \ + VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ + UPLOAD_SENTRY=$UPLOAD_SENTRY + + assertDirExists "$ARCHIVE_PATH" + + if ! builder_is_debug_build; then + echo "Preparing .ipa file for deployment to real devices" + # Do NOT use the _EXT variant here; there's no scheme to ref, which will lead + # Xcode to generate a build error. + run_xcodebuild $XCODEFLAGS \ + -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportOptionsPlist ../exportAppStore.plist \ + -exportPath "$BUILD_PATH/${CONFIG}-iphoneos" \ + -allowProvisioningUpdates + fi + + if builder_has_option --sim-artifact; then + echo "Preparing .app file for simulator-targeted artifact for testing" + run_xcodebuild $XCODEFLAGS_EXT \ + $CODE_SIGN \ + -scheme Keyman \ + -sdk iphonesimulator \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG \ + VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ + UPLOAD_SENTRY=$UPLOAD_SENTRY + fi +} + +builder_run_action clean do_clean +builder_run_action configure do_configure +builder_run_action build build_app \ No newline at end of file diff --git a/ios/kmbuild.sh b/ios/kmbuild.sh deleted file mode 100755 index f443f04c2589d9cf10be7c9d7ec91428482f6100..0000000000000000000000000000000000000000 --- a/ios/kmbuild.sh +++ /dev/null @@ -1,309 +0,0 @@ -#!/usr/bin/env bash - -# set -e: Terminate script if a command returns an error -set -e -# set -u: Terminate script if an unset variable is used -set -u -# set -x: Debugging use, print each statement -# set -x - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -# This script runs from its own folder -cd "$(dirname "$THIS_SCRIPT")" - -# Include our resource functions; they're pretty useful! -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" -. "$KEYMAN_ROOT/resources/build/build-download-resources.sh" -. "$KEYMAN_ROOT/resources/build/build-help.inc.sh" - -# Please note that this build script (understandably) assumes that it is running on Mac OS X. -verify_on_mac - -display_usage ( ) { - echo "build.sh [-clean] [-no-kmw] [-only-framework] [-no-codesign] [-no-archive] [-add-sim-artifact] [-no-build] [-upload-sentry]" - echo - echo " -clean Removes all previously-existing build products for KMEI and the Keyman app before building." - echo " -no-kmw Uses existing keyman.js, doesn't try to build" - echo " -only-framework Builds only the KeymanEngine framework; does not attempt to build the app." - echo " -no-carthage Disables downloading and building for dependencies." - echo " -no-codesign Disables code-signing for the Keyman application, allowing it to be performed separately later." - echo " Will not construct the archive and .ipa. (includes -no-archive)" - echo " -no-archive Bypasses the archive and .ipa preparation stage." - echo " -no-build Cancels the build entirely. Useful with 'build.sh -clean -no-build'." - echo " -upload-sentry Uploads debug symbols, etc, to Sentry" - echo " -debug Sets the configuration to debug mode instead of release." - echo " -download-resources Download up-to-date versions of the engine's default resources from downloads.keyman.com." - echo " -add-sim-artifact Also produce a Simulator-installable .app build artifact" -exit 1 -} - -do_clean ( ) { - rm -rf $BUILD_PATH - rm -rf Carthage -} - -### START OF THE BUILD ### - -# Base definitions (must be before do_clean call) -DERIVED_DATA=build -BUILD_PATH=$DERIVED_DATA/Build/Products - -# Default is building and copying to assets -DO_KMW_BUILD=true -DO_KEYMANAPP=true -DO_ARCHIVE=true -DO_SIMULATOR_TARGET=false -DO_CARTHAGE=true -CLEAN_ONLY=false -CONFIG=Release -DO_KMP_DOWNLOADS=false -CODE_SIGN= -DO_CODE_SIGN=true - -# Parse args -while [[ $# -gt 0 ]] ; do - key="$1" - case $key in - -no-kmw) - DO_KMW_BUILD=false - ;; - -h|-\?|-help) - display_usage - exit 0 - ;; - -only-framework) - DO_KEYMANAPP=false - ;; - -no-codesign) - CODE_SIGN="CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO ${DEV_TEAM:-} CODE_SIGN_ENTITLEMENTS= CODE_SIGNING_ALLOWED=NO" - DO_CODE_SIGN=false - ;; - -no-archive) - DO_ARCHIVE=false - ;; - -clean) - do_clean - ;; - -no-build) - CLEAN_ONLY=true - # Overrides default set by build-utils.sh. - UPLOAD_SENTRY=false - ;; - -no-carthage) - DO_CARTHAGE=false - ;; - -upload-sentry) - # Overrides default set by build-utils.sh. - UPLOAD_SENTRY=true - ;; - -debug) - CONFIG=Debug - ;; - -download-resources) - DO_KMP_DOWNLOADS=true - ;; - -add-sim-artifact) - DO_SIMULATOR_TARGET=true - ;; - esac - shift # past argument -done - -# Extended path definitions -KMEI_RESOURCES=engine/KMEI/KeymanEngine/resources -BUNDLE_PATH=$KMEI_RESOURCES/Keyman.bundle/contents/resources -KMW_ROOT=../web - -DEFAULT_KBD_ID="sil_euro_latin" -DEFAULT_LM_ID="nrc.en.mtnt" - -# Build product paths -APP_BUNDLE_PATH=$BUILD_PATH/${CONFIG}-iphoneos/Keyman.app -KEYBOARD_BUNDLE_PATH=$BUILD_PATH/${CONFIG}-iphoneos/SWKeyboard.appex -ARCHIVE_PATH=$BUILD_PATH/${CONFIG}-iphoneos/Keyman.xcarchive - -# Engine library build path -KEYMAN_XCFRAMEWORK=$BUILD_PATH/$CONFIG/KeymanEngine.xcframework - -XCODEFLAGS="-quiet -configuration $CONFIG" -XCODEFLAGS_EXT="$XCODEFLAGS -derivedDataPath $DERIVED_DATA -workspace keymanios.xcworkspace" - -if [ $CLEAN_ONLY = true ]; then - exit 0 -fi - -echo -echo "KMW_ROOT: $KMW_ROOT" -echo "DO_KMW_BUILD: $DO_KMW_BUILD" -echo "DO_KMP_DOWNLOADS: $DO_KMP_DOWNLOADS" -echo "CONFIGURATION: $CONFIG" -echo - -update_bundle ( ) { - if ! [ -d "$BUNDLE_PATH" ]; then - mkdir -p "$BUNDLE_PATH" - fi - - local base_dir="$(pwd)" - - if [ $DO_KMW_BUILD = true ]; then - echo Building KeymanWeb from $KMW_ROOT - - KMW_PRODUCT=web/build/app/embed/ - if [ "$CONFIG" == "Debug" ]; then - KMWFLAGS="configure:embed build:embed --debug" - KMW_PRODUCT="$KMW_PRODUCT/debug" - else - KMWFLAGS="configure:embed build:embed" - KMW_PRODUCT="$KMW_PRODUCT/release" - fi - - # Local development optimization - cross-target Sentry uploading when requested - # by developer. As it's not CI, the Web artifacts won't exist otherwise... - # unless the developer manually runs the correct build configuration accordingly. - if [[ $VERSION_ENVIRONMENT == "local" ]] && [[ $UPLOAD_SENTRY == true ]]; then - # TODO: handle the -upload-sentry in its eventual new form - KMWFLAGS="$KMWFLAGS -upload-sentry" - fi - - "$KEYMAN_ROOT/web/build.sh" $KMWFLAGS - if [ $? -ne 0 ]; then - builder_die "ERROR: KeymanWeb's build.sh failed." - fi - - #Copy over the relevant resources! It's easiest to do if we navigate to the resulting folder. - cp "$KEYMAN_ROOT/$KMW_PRODUCT/osk/kmwosk.css" "$base_dir/$BUNDLE_PATH/kmwosk.css" - cp "$KEYMAN_ROOT/$KMW_PRODUCT/osk/keymanweb-osk.ttf" "$base_dir/$BUNDLE_PATH/keymanweb-osk.ttf" - cp "$KEYMAN_ROOT/$KMW_PRODUCT/keyman.js" "$base_dir/$BUNDLE_PATH/keymanios.js" - - if [ "$CONFIG" == "Debug" ]; then - cp "$KEYMAN_ROOT/$KMW_PRODUCT/keyman.js.map" "$base_dir/$BUNDLE_PATH/keyman.js.map" - elif [ -f "$base_dir/$BUNDLE_PATH/keyman.js.map" ]; then - rm "$base_dir/$BUNDLE_PATH/keyman.js.map" - fi - - cp "$KEYMAN_ROOT/common/web/sentry-manager/build/index.js" "$base_dir/$BUNDLE_PATH/keyman-sentry.js" - - fi - - # Our default resources are part of the bundle, so let's check on them. - if [ ! -f "$base_dir/$BUNDLE_PATH/$DEFAULT_KBD_ID.kmp" ]; then - DO_KMP_DOWNLOADS=true - builder_warn "OVERRIDE: Performing -download-resources run, as the keyboard package is missing!" - elif [ ! -f "$base_dir/$BUNDLE_PATH/$DEFAULT_LM_ID.model.kmp" ]; then - DO_KMP_DOWNLOADS=true - builder_warn "OVERRIDE: Performing -download-resources run, as the lexical model package is missing!" - fi - - if [ $DO_KMP_DOWNLOADS = true ]; then - builder_heading "Downloading up-to-date packages for default resources" - - downloadKeyboardPackage "$DEFAULT_KBD_ID" "$base_dir/$BUNDLE_PATH/$DEFAULT_KBD_ID.kmp" - downloadModelPackage "$DEFAULT_LM_ID" "$base_dir/$BUNDLE_PATH/$DEFAULT_LM_ID.model.kmp" - - echo "${COLOR_GREEN}Packages successfully updated${COLOR_RESET}" - - # If we aren't downloading resources, make sure copies of them already exist! - fi -} - -# First things first - update our dependencies. -update_bundle - -if [ $DO_CARTHAGE = true ]; then - echo - echo "Load dependencies with Carthage" - - carthage checkout || builder_die "Carthage dependency loading failed" - - # --no-use-binaries: due to https://github.com/Carthage/Carthage/issues/3134, - # which affects the sentry-cocoa dependency. - carthage build --use-xcframeworks --no-use-binaries --platform iOS || builder_die "Carthage dependency loading failed" -fi - -echo -echo "Build products will be set with the following version metadata:" -echo " * VERSION=$VERSION" -echo " * VERSION_WITH_TAG=$VERSION_WITH_TAG" -echo " * VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT" -echo " * UPLOAD_SENTRY=$UPLOAD_SENTRY" -echo -echo "Building KMEI..." - -if [ -d "$BUILD_PATH/$CONFIG-universal" ]; then - rm -r $BUILD_PATH/$CONFIG-universal -fi - -run_xcodebuild $XCODEFLAGS_EXT $CODE_SIGN -scheme KME-universal \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG \ - VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ - UPLOAD_SENTRY=$UPLOAD_SENTRY - -assertDirExists "$KEYMAN_XCFRAMEWORK" - -echo "KMEI build complete." - -if [ $DO_KEYMANAPP = true ]; then - echo "" - echo "Building offline help." - build_help_html ios keyman/Keyman/Keyman/resources/OfflineHelp.bundle/Contents/Resources - - echo "" - echo "Building Keyman app." - - # Provides a needed link for codesigning for our CI. - if ! [ -z "${DEVELOPMENT_TEAM+x}" ]; then - DEV_TEAM="DEVELOPMENT_TEAM=${DEVELOPMENT_TEAM}" - else - DEV_TEAM= - fi - - if [ $DO_ARCHIVE = false ]; then - run_xcodebuild $XCODEFLAGS_EXT $CODE_SIGN -scheme Keyman \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG \ - VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ - UPLOAD_SENTRY=$UPLOAD_SENTRY - else - # Time to prepare the deployment archive data. - echo "" - echo "Preparing .xcarchive for real devices." - run_xcodebuild $XCODEFLAGS_EXT $CODE_SIGN -scheme Keyman \ - -archivePath $ARCHIVE_PATH \ - archive -allowProvisioningUpdates \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG \ - VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ - UPLOAD_SENTRY=$UPLOAD_SENTRY - - assertDirExists "$ARCHIVE_PATH" - - if [ $DO_CODE_SIGN == true ]; then - echo "Preparing .ipa file for deployment to real devices" - # Do NOT use the _EXT variant here; there's no scheme to ref, which will lead - # Xcode to generate a build error. - run_xcodebuild $XCODEFLAGS -exportArchive -archivePath $ARCHIVE_PATH \ - -exportOptionsPlist exportAppStore.plist \ - -exportPath $BUILD_PATH/${CONFIG}-iphoneos -allowProvisioningUpdates - fi - fi - - if [ $DO_SIMULATOR_TARGET == true ]; then - echo "Preparing .app file for simulator-targeted artifact for testing" - run_xcodebuild $XCODEFLAGS_EXT $CODE_SIGN -scheme Keyman \ - -sdk iphonesimulator \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG \ - VERSION_ENVIRONMENT=$VERSION_ENVIRONMENT \ - UPLOAD_SENTRY=$UPLOAD_SENTRY - fi - - echo - echo "Build succeeded." -fi diff --git a/ios/samples/KMSample1/build.sh b/ios/samples/KMSample1/build.sh index 21e79a79e6a09fac05310e02682b91e3b31fec29..f7bb8798c7db54377765d483a8a8dc75b07a8253 100755 --- a/ios/samples/KMSample1/build.sh +++ b/ios/samples/KMSample1/build.sh @@ -1,4 +1,21 @@ #!/usr/bin/env bash +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +verify_on_mac + +source "${THIS_SCRIPT%/*}/../common.inc.sh" + TARGET=KMSample1 -source ../build_common.sh +execute_sample_build "$@" \ No newline at end of file diff --git a/ios/samples/KMSample2/build.sh b/ios/samples/KMSample2/build.sh index a0ad47b5012aa2c41799879025244d855bec3e56..0b0fdc542d55c5c331a1fd5ee2001656a91452a8 100755 --- a/ios/samples/KMSample2/build.sh +++ b/ios/samples/KMSample2/build.sh @@ -1,4 +1,21 @@ #!/usr/bin/env bash +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +verify_on_mac + +source "${THIS_SCRIPT%/*}/../common.inc.sh" + TARGET=KMSample2 -source ../build_common.sh +execute_sample_build "$@" \ No newline at end of file diff --git a/ios/samples/build_common.sh b/ios/samples/build_common.sh deleted file mode 100755 index e1ed7ee95fb07307caf721d4c718084df9e333bf..0000000000000000000000000000000000000000 --- a/ios/samples/build_common.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env bash - -# set -e: Terminate script if a command returns an error -set -e -# set -u: Terminate script if an unset variable is used -set -u -# set -x: Debugging use, print each statement -# set -x - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -# Please note that this build script (understandably) assumes that it is running on Mac OS X. -if [[ "${OSTYPE}" == *"darwin" ]]; then - echo "This build script will only run in a Mac environment." - exit 1 -fi - -if [ -z "$TARGET" ]; then - exit 1 -fi - -display_usage ( ) { - echo "build.sh [-no-update] | [-lib-build] | [-lib-ignore] | [-clean]" - echo - echo " -clean Removes all previously-existing build products for this project before building." - echo " -no-update If an in-place copy of KeymanEngine.xcframework exists, does not seek out an updated copy." - echo " -lib-build Actively rebuilds KMEI before copying its build products to project resources." - echo " -lib-nobuild Prevents the build script from building KeymanEngine under any circumstances." - echo " -no-codesign Performs the build without code signing." - echo " -debug Sets the configuration to debug mode instead of release." - echo - echo " If no settings are specified this script will grab a copy of the most recent build of KeymanEngine," - echo " performing an initial build of it if necessary." - exit 1 -} - -assert ( ) { - if ! [ -f $1 ]; then - echo "Build failed: missing $1" - exit 1 - fi -} - -verify_KMEI ( ) { - KMEI_BUILD_EXISTS=true - [ -d "$KEYMAN_ENGINE_FRAMEWORK_SRC" ] || KMEI_BUILD_EXISTS=false -} - -KMEI_BUILD_DIR="../../" - -BUILD_FOLDER=build - -do_clean ( ) { - rm -rf $BUILD_FOLDER -} - -### START OF THE BUILD ### - -DO_UPDATE=true -FORCE_KMEI_BUILD=false -ALLOW_KMEI_BUILD=true -CODE_SIGN=true -CONFIG=Release -KMEI_FLAGS= - -while [[ $# -gt 0 ]] ; do - key="$1" - case $key in - -no-update) - DO_UPDATE=false - ALLOW_KMEI_BUILD=false - ;; - -lib-build) - FORCE_KMEI_BUILD=true - ;; - -lib-nobuild) - ALLOW_KMEI_BUILD=false - ;; - -no-codesign) - CODE_SIGN=false - KMEI_FLAGS="$KMEI_FLAGS -no-codesign" - ;; - -h|-\?) - display_usage - ;; - -clean) - do_clean - KMEI_FLAGS="$KMEI_FLAGS -clean" - FORCE_KMEI_BUILD=true - ;; - -debug) - CONFIG=Debug - KMEI_FLAGS="$KMEI_FLAGS -debug" - ;; - esac - shift -done - -KEYMAN_ENGINE_FRAMEWORK_SRC="../../build/Build/Products/$CONFIG/KeymanEngine.xcframework" -KEYMAN_ENGINE_FRAMEWORK_DST=./ - -if [ $DO_UPDATE = true ]; then - # Does a prior build of KMEI exist? - verify_KMEI - - if [ $ALLOW_KMEI_BUILD = true ] && [ $FORCE_KMEI_BUILD = false ] && [ $KMEI_BUILD_EXISTS = false ]; then - echo "Previous KeymanEngine build information is unavailable." - FORCE_KMEI_BUILD=true - fi - - if [ $FORCE_KMEI_BUILD = true ]; then - echo "Building KeymanEngine..." - base_dir="$(pwd)" - - cd $KMEI_BUILD_DIR - ./kmbuild.sh -only-framework $KMEI_FLAGS - cd $base_dir - fi - - verify_KMEI - - if ! [ $KMEI_BUILD_EXISTS ]; then - echo "Build failed: could not build required KeymanEngine resources." - exit 1 - fi - - # Copy resources. - /bin/cp -Rf "$KEYMAN_ENGINE_FRAMEWORK_SRC" "$KEYMAN_ENGINE_FRAMEWORK_DST" -fi - -if [ $CODE_SIGN = true ]; then - run_xcodebuild -quiet -target "$TARGET" -config "$CONFIG" -else - run_xcodebuild -quiet CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED="NO" CODE_SIGNING_ENTITLEMENTS="" -target "$TARGET" -config "$CONFIG" -fi - -if [ $? = 0 ]; then - echo "Build complete." -fi diff --git a/ios/samples/common.inc.sh b/ios/samples/common.inc.sh new file mode 100755 index 0000000000000000000000000000000000000000..fb8d8e969c18c76ee3839d65c3905f8d04f2dcea --- /dev/null +++ b/ios/samples/common.inc.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# This script is included by each sample build.sh. Accordingly, it inherits build-utils.sh, etc +# from the including script. + +function do_build() { + # Copy resources. + cp -Rf "$KEYMAN_ENGINE_FRAMEWORK_SRC" "$KEYMAN_ENGINE_FRAMEWORK_DST" + + CODE_SIGN= + # `builder_is_debug_build` appears to fail here, while referring to the option does not? + # Perhaps it's due to the main build definition being within a function? + if builder_has_option --debug; then + CODE_SIGN=CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED="NO" CODE_SIGNING_ENTITLEMENTS="" + fi + + run_xcodebuild -quiet \ + $CODE_SIGN \ + -target "$TARGET" \ + -config "$CONFIG" +} + +function execute_sample_build() { + if [ -z "$TARGET" ]; then + builder_die "Usage of `execute_sample_build` must specify a target iOS sample project" + fi + + builder_describe "Builds sample app $TARGET that demos the Keyman Engine for iPhone and iPad" \ + "@/ios/engine build" \ + "clean" \ + "configure" \ + "build" \ + "--debug Avoids codesigning and adds full sourcemaps for the embedded predictive-text engine" \ + "--sim-artifact+ Unused by this build at present" + + builder_parse "$@" + + local CONFIG=Release + if builder_is_debug_build; then + CONFIG="Debug" + fi + + local BUILD_FOLDER="ios/samples/$TARGET/build" + + builder_describe_outputs \ + build "/$BUILD_FOLDER" + + ### START OF THE BUILD ### + + # NOT local b/c they're needed by do_build. + KEYMAN_ENGINE_FRAMEWORK_SRC="$KEYMAN_ROOT/ios/build/Build/Products/$CONFIG/KeymanEngine.xcframework" + KEYMAN_ENGINE_FRAMEWORK_DST=./ + + builder_run_action clean rm -rf "$KEYMAN_ROOT/$BUILD_FOLDER" + builder_run_action build do_build +} \ No newline at end of file diff --git a/ios/tools/prepRelease.sh b/ios/tools/prepRelease.sh index 39b46722f79c973bee51d128192bfac1cc647c6f..c36ad97b45c9829e19d72f1d706c0be6b7677269 100755 --- a/ios/tools/prepRelease.sh +++ b/ios/tools/prepRelease.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash ## Note - this script is designed solely for use within our Continuous -## Integration processes with `pwd` as this script's parent folder. +## Integration processes (thus, by ../ci.sh) with `pwd` as this script's parent folder. # Exit if program call fails set -e diff --git a/linux/.gitignore b/linux/.gitignore index 478f418b608e52d3898a613528475097a4d10e4d..0abbd99c585433153b15c9eca0fd5e604ebb33d2 100644 --- a/linux/.gitignore +++ b/linux/.gitignore @@ -17,7 +17,7 @@ dist/ upload/ launchpad launchpad-* -keyman-config/build +build/ keyman-config/debian/man keyman-config/experiments/*.txt keyman-config/keyman_config/version.py diff --git a/linux/.pbuilderrc b/linux/.pbuilderrc index 1384eec453548017b10ee36eebab6d17b842d352..6a77a0ab08151144fb936131210097535b006920 100644 --- a/linux/.pbuilderrc +++ b/linux/.pbuilderrc @@ -14,7 +14,7 @@ DEBIAN_SUITES=($UNSTABLE_CODENAME $TESTING_CODENAME $STABLE_CODENAME $STABLE_BAC "experimental" "unstable" "testing" "stable") # List of Ubuntu suites. Update these when needed. -UBUNTU_SUITES=("lunar" "kinetic" "jammy" "focal") +UBUNTU_SUITES=("mantic" "lunar" "jammy" "focal") # Mirrors to use. Update these to your preferred mirror. DEBIAN_MIRROR="deb.debian.org" diff --git a/linux/Dockerfile b/linux/Dockerfile index 8572b983f8fcce7f882aa08f207bd796cd0ae37d..6852b542384f1522bf875a2eaee10b7bb4b7a7f3 100644 --- a/linux/Dockerfile +++ b/linux/Dockerfile @@ -1,4 +1,4 @@ -# Copyright (c) 2022 SIL International. All rights reserved. +# Copyright (c) 2022-2023 SIL International. All rights reserved. # # builder image for a linux build # see ../docs/build/linux-ubuntu.md @@ -7,20 +7,80 @@ FROM --platform=amd64 ubuntu:latest LABEL org.opencontainers.image.authors="SIL International." LABEL org.opencontainers.image.url="https://github.com/keymanapp/keyman.git" LABEL org.opencontainers.image.title="Keyman Linux Build Image" + # We will switch to a build user after some installation USER root -RUN useradd -c "Build user" -d $HOME -m build ENV HOME /home/build -VOLUME /home/build -WORKDIR /home/build +RUN useradd -c "Build user" --home-dir $HOME --create-home --shell /usr/bin/bashwrapper build +VOLUME /home/build/build +WORKDIR /home/build/build ENV DEBIAN_FRONTEND noninteractive +ENV DEBIAN_PRIORITY critical +ENV DEBCONF_NOWARNINGS yes + # Update to the latest -RUN apt-get -q -y update && apt-get -q -y upgrade -RUN apt-get -q -y install devscripts equivs meson python3 python3-setuptools +RUN apt-get -q -y update && \ + apt-get -q -y install devscripts equivs meson python3 python3-setuptools software-properties-common curl && \ + add-apt-repository ppa:keymanapp/keyman && \ + add-apt-repository ppa:keymanapp/keyman-alpha +RUN apt-get -q -y update && \ + apt-get -q -y upgrade + +# Install dependencies ADD debian/control /tmp/control # Answer 'yes' to install questions -RUN (yes | mk-build-deps --install /tmp/control) || true +RUN (yes | mk-build-deps --install /tmp/control) || true && \ + rm /tmp/control + +# Install Node +RUN curl -sL https://deb.nodesource.com/setup_18.x | bash && \ + apt-get -q -y install nodejs + +# Install emscripten +RUN cd /usr/share && \ + git clone https://github.com/emscripten-core/emsdk.git && \ + cd emsdk && \ + ./emsdk install latest && \ + ./emsdk activate latest && \ + echo "#!/bin/bash" > /usr/bin/bashwrapper && \ + echo "export EMSCRIPTEN_BASE=/usr/share/emsdk/upstream/emscripten" >> /usr/bin/bashwrapper + +# Keyman Web +RUN curl --output google-chrome-stable_current_amd64.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \ + apt-get -q -y install ./google-chrome-stable_current_amd64.deb && \ + rm google-chrome-stable_current_amd64.deb && \ + echo "export CHROME_BIN=/opt/google/chrome/chrome" >> /usr/bin/bashwrapper + +# Keyman for Android +RUN apt-get -q -y install gradle maven pandoc sdkmanager jq && \ + sdkmanager platform-tools && \ + yes | sdkmanager --licenses && \ + chown -R build:build /opt/android-sdk/ && \ + echo "export ANDROID_HOME=/opt/android-sdk" >> /usr/bin/bashwrapper && \ + echo "export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64" >> /usr/bin/bashwrapper + +# Finish bashwrapper script and adjust permissions +RUN echo "\${@:-bash}" >> /usr/bin/bashwrapper && \ + chmod +x /usr/bin/bashwrapper && \ + chown -R build:build $HOME + # now, switch to build user -RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -RUN apt-get -q -y install nodejs USER build + +# Pre-install gradle. This will put files in ~/.gradle which will speed up builds. +RUN mkdir -p $HOME/tmp/gradle/wrapper && \ + # KMEA uses gradle-7.5.1-bin + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.jar && \ + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradle/wrapper/gradle-wrapper.properties && \ + curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/KMEA/gradlew && \ + chmod +x $HOME/tmp/gradlew && \ + $HOME/tmp/gradlew --quiet && \ + # Some projects use gradle-7.5.1-all, so we pre-install that as well + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.jar https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.jar && \ + curl --location --output $HOME/tmp/gradle/wrapper/gradle-wrapper.properties https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradle/wrapper/gradle-wrapper.properties && \ + curl --location --output $HOME/tmp/gradlew https://raw.githubusercontent.com/keymanapp/keyman/master/android/Samples/KMSample1/gradlew && \ + chmod +x $HOME/tmp/gradlew && \ + $HOME/tmp/gradlew --quiet && \ + rm -rf $HOME/tmp + +ENTRYPOINT [ "/usr/bin/bashwrapper" ] diff --git a/linux/Jenkinsfile b/linux/Jenkinsfile deleted file mode 100644 index b70cb95d706cfc3c24ccf6e90a242f407d74ccae..0000000000000000000000000000000000000000 --- a/linux/Jenkinsfile +++ /dev/null @@ -1,11 +0,0 @@ -#!groovy -// Copyright (c) 2019-2022 SIL International -// This software is licensed under the MIT license (http://opensource.org/licenses/MIT) - -@Library('lsdev-pipeline-library') _ - -keymanPackaging { - distributionsToPackage = 'focal jammy kinetic lunar' - arches = 'amd64 i386' - packagesToBuild = ['keyman'] -} diff --git a/linux/README.md b/linux/README.md index 3dfb34047793b0e1c704dfb5bf5c111f19bb85bc..e4b50cdc90583e9e95e948d55e6f91c700a8a47e 100644 --- a/linux/README.md +++ b/linux/README.md @@ -1,3 +1,5 @@ # Keyman for Linux See [/docs/linux/README.md](../docs/linux/README.md) for documentation. + +. \ No newline at end of file diff --git a/linux/build.sh b/linux/build.sh index 8bdbb368c7ea260e50671ab0196d3527e1505a63..3b3648dd1468703fbb299d5afe6a339f78b27ba4 100755 --- a/linux/build.sh +++ b/linux/build.sh @@ -14,14 +14,16 @@ cd "$THIS_SCRIPT_PATH" builder_describe \ "Build Keyman for Linux." \ - ":engine=ibus-keyman ibus-keyman" \ - ":config=keyman-config keyman-config" \ + ":config=keyman-config keyman-config" \ + ":engine=ibus-keyman ibus-keyman" \ + ":service=keyman-system-service keyman-system-service" \ "clean" \ "configure" \ "build" \ "test" \ "install install artifacts" \ - "uninstall uninstall artifacts" + "uninstall uninstall artifacts" \ + "--no-integration+ don't run integration tests" builder_parse "$@" diff --git a/linux/debian/changelog b/linux/debian/changelog index 9f69cc9a12fb8cff409c44c39751958f1cf4d7c9..1ad0f861fb3eb8edc055f475e069d9629946535a 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,18 @@ +keyman (16.0.141-1) unstable; urgency=medium + + * Work around mips64el build failure (#1041499) + * New upstream release. + * Re-release to Debian + + -- Eberhard Beilharz Thu, 27 Jul 2023 16:30:04 +0200 + +keyman (16.0.140-1) unstable; urgency=medium + + * New upstream release (closes: #1037707). + * Re-release to Debian + + -- Eberhard Beilharz Mon, 24 Jul 2023 11:41:07 +0200 + keyman (16.0.139-4) unstable; urgency=medium * debian/tests: Revert previous change and ignore s390x from autopkgtests diff --git a/linux/debian/com.keyman.config.appdata.xml b/linux/debian/com.keyman.config.appdata.xml index f137be461df2923d2b9d3d197e7fd306d808e46e..9df49a37dc3a0500bbd40533c2ef608e4c3da5f0 100644 --- a/linux/debian/com.keyman.config.appdata.xml +++ b/linux/debian/com.keyman.config.appdata.xml @@ -14,8 +14,8 @@

Keyman makes it possible for you to type in over 2,000 languages on Windows, macOS, Linux, iPhone, iPad, Android tablets and phones, and even instantly in your web browser. With the -world’s most powerful keyboarding engine, intuitive and rapid text input is now possible in -your language, and for over 99% of the global population’s mother tongues! +world's most powerful keyboarding engine, intuitive and rapid text input is now possible in +your language, and for over 99% of the global population's mother tongues!

Originally created in 1993 to type Lao on Windows, Keyman is now a free and @@ -40,7 +40,7 @@ and mobile platforms. The Keyman for Linux configuration window with a keyboard welcome window. - https://keyman.com/go/linux/11.0/linux-configuration.png + https://keyman.com/go/linux/17.0/linux-configuration.png diff --git a/linux/debian/com.keyman.ibus_keyman.metainfo.xml b/linux/debian/com.keyman.ibus_keyman.metainfo.xml index eb02001da0d2a355fc4fe4dcbc5f2b97bbc54ec0..1f021d2fce346c76b90b38512af83d3cfc737b0e 100644 --- a/linux/debian/com.keyman.ibus_keyman.metainfo.xml +++ b/linux/debian/com.keyman.ibus_keyman.metainfo.xml @@ -1,39 +1,39 @@ -​ com.keyman.ibus_keyman -​ MIT + com.keyman.ibus_keyman + MIT GPL-2+ Keyman SIL International https://keyman.com/cdn/dev/img/keyman-logo.png -​ ibus-keyman -​

Keyman input method for IBus -​ -​

- This input method provides the Keyman IM engine for IBus. With this module, you can - use keyboard layouts designed for Keyman under the IBus platform. -

-

- Originally created in 1993 to type Lao on Windows, Keyman is now a free and - open source keyboarding platform which allows anyone to write a keyboard layout - for their language. Keyman is available for many platforms, including Windows, - macOS, iOS, Android, Linux and the web. -

+ ibus-keyman + Keyman input method for IBus +

- Keyboard layouts are defined with a clear and easy to understand keyboard - grammar. Keyman's contextual input model means keyboard layouts can be - intelligent and make it simple to type even the most complex languages. - Keyboard layouts are distributed through an open catalog to all major desktop - and mobile platforms. + This input method provides the Keyman IM engine for IBus. With this module, you can + use keyboard layouts designed for Keyman under the IBus platform. +

+

+ Originally created in 1993 to type Lao on Windows, Keyman is now a free and + open source keyboarding platform which allows anyone to write a keyboard layout + for their language. Keyman is available for many platforms, including Windows, + macOS, iOS, Android, Linux and the web. +

+

+ Keyboard layouts are defined with a clear and easy to understand keyboard + grammar. Keyman's contextual input model means keyboard layouts can be + intelligent and make it simple to type even the most complex languages. + Keyboard layouts are distributed through an open catalog to all major desktop + and mobile platforms. +

+

+ Input methods are typing systems allowing users to input complex languages.

-

-​ Input methods are typing systems allowing users to input complex languages. -​

-​
+
https://keyman.com/linux/ https://github.com/keymanapp/keyman/issues https://help.keyman.com/ https://community.software.sil.org/c/keyman https://donate.keyman.com/ -​ + diff --git a/linux/debian/control b/linux/debian/control index 40b2065d7f7fb9292ce55170977ec5e758471b59..624f89fc0adc2e8a0406377363f5c70d14a5531b 100644 --- a/linux/debian/control +++ b/linux/debian/control @@ -28,10 +28,12 @@ Build-Depends: python3-setuptools, meson (>= 0.53), ninja-build, + libevdev-dev, libgtk-3-dev, libicu-dev, libibus-1.0-dev (>= 1.2), libjson-glib-dev (>= 1.4.0), + libsystemd-dev, pkg-config, xserver-xephyr, xvfb, @@ -56,9 +58,9 @@ Recommends: Description: Type in your language with Keyman for Linux Keyman makes it possible for you to type in over 2,000 languages on Windows, macOS, Linux, iPhone, iPad, Android tablets and phones, and even instantly - in your web browser. With the world’s most powerful keyboarding engine, + in your web browser. With the world's most powerful keyboarding engine, intuitive and rapid text input is now possible in your language, and for - over 99% of the global population’s mother tongues! + over 99% of the global population's mother tongues! . Originally created in 1993 to type Lao on Windows, Keyman is now a free and open source keyboarding platform which allows anyone to write a keyboard @@ -160,6 +162,7 @@ Architecture: amd64 arm64 armel armhf i386 mipsel mips64el ppc64el riscv64 Depends: ibus (>= 1.3.7), libkmnkbp0-0 (= ${binary:Version}), + keyman-system-service (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}, Provides: @@ -181,3 +184,24 @@ Description: Keyman engine for IBus . This package provides the Keyman IM engine for IBus. With this module, you can use keyboard layouts designed for Keyman under the IBus platform. + +Package: keyman-system-service +Architecture: any +Depends: + libevdev2, + ${misc:Depends}, + ${shlibs:Depends}, +Description: System service for Keyman + Originally created in 1993 to type Lao on Windows, Keyman is now a free and + open source keyboarding platform which allows anyone to write a keyboard + layout for their language. Keyman is available for many platforms, including + Windows, macOS, iOS, Android, Linux and the web. + . + Keyboard layouts are defined with a clear and easy to understand keyboard + grammar. Keyman's contextual input model means keyboard layouts can be + intelligent and make it simple to type even the most complex languages. + Keyboard layouts are distributed through an open catalog to all major desktop + and mobile platforms. + . + This package provides a DBus/systemd system service as a helper for + running Keyman with Wayland. diff --git a/linux/debian/keyman-system-service.install b/linux/debian/keyman-system-service.install new file mode 100644 index 0000000000000000000000000000000000000000..f7ce93bd82b3a8f77baee2d76a13124ba317500f --- /dev/null +++ b/linux/debian/keyman-system-service.install @@ -0,0 +1,5 @@ +usr/lib/systemd/system/systemd-keyman.service +usr/libexec/keyman-system-service +usr/share/dbus-1/interfaces/com.keyman.SystemService1.System.xml +usr/share/dbus-1/system-services/com.keyman.SystemService1.service +usr/share/dbus-1/system.d/com.keyman.SystemService1.conf diff --git a/linux/debian/keyman-system-service.manpages b/linux/debian/keyman-system-service.manpages new file mode 100644 index 0000000000000000000000000000000000000000..cb4d9f0500ee07505ba9e3e6271904dda002f802 --- /dev/null +++ b/linux/debian/keyman-system-service.manpages @@ -0,0 +1 @@ +linux/keyman-system-service/man/keyman-system-service.1 diff --git a/linux/debian/keyman.install b/linux/debian/keyman.install index 8096e324aec4be58e43a6b10dc081bca41c79ad1..a036eb8a7361535fe2db5b3b2e7f1065c50656c2 100644 --- a/linux/debian/keyman.install +++ b/linux/debian/keyman.install @@ -1,4 +1,3 @@ -linux/keyman-config/com.keyman.gschema.xml usr/share/glib-2.0/schemas/ linux/keyman-config/icons/128/km-config.png usr/share/icons/hicolor/128x128/apps/ linux/keyman-config/icons/16/application-x-kmp.png usr/share/icons/hicolor/16x16/mimetypes/ linux/keyman-config/icons/24/application-x-kmp.png usr/share/icons/hicolor/24x24/mimetypes/ @@ -10,5 +9,6 @@ linux/keyman-config/icons/48/km-config.png usr/share/icons/hicolor/48x48/apps/ linux/keyman-config/icons/64/application-x-kmp.png usr/share/icons/hicolor/64x64/mimetypes/ linux/keyman-config/icons/64/km-config.png usr/share/icons/hicolor/64x64/apps/ linux/keyman-config/keyman_config/icons/* usr/share/keyman/icons/ -linux/keyman-config/km-config.desktop usr/share/applications +linux/keyman-config/resources/com.keyman.gschema.xml usr/share/glib-2.0/schemas/ +linux/keyman-config/resources/km-config.desktop usr/share/applications debian/com.keyman.config.appdata.xml usr/share/metainfo diff --git a/linux/debian/rules b/linux/debian/rules index 774371d252ab67ea06bb123d740c8920fec9218e..ec9178298cac3a121e8f353e8c8337d6b0ec5c2c 100755 --- a/linux/debian/rules +++ b/linux/debian/rules @@ -9,7 +9,9 @@ export PYBUILD_INSTALL_ARGS=--install-scripts=/usr/share/keyman-config/ export KEYMAN_PKG_BUILD=1 +export HOME=$(shell mktemp -d) export XDG_DATA_HOME=$(shell mktemp -d) +export XDG_CONFIG_HOME=$(shell mktemp -d) # xenial needs this to be explicit export LC_ALL=C.UTF-8 @@ -24,57 +26,48 @@ include /usr/share/dpkg/pkg-info.mk override_dh_auto_configure: # keymankeyboardprocessor - we need to configure and build core before # we can configure ibus-keyman! - cd core && \ - ./build.sh --no-tests configure:arch build:arch -- --wrap-mode=nodownload --prefix=/usr --sysconfdir=/etc \ - --localstatedir=/var --libdir=lib/$(DEB_TARGET_MULTIARCH) \ - --libexecdir=lib/$(DEB_TARGET_MULTIARCH) - # ibus-keyman - cd linux/ibus-keyman && \ - ./build.sh configure -- --wrap-mode=nodownload --prefix=/usr --sysconfdir=/etc \ - --localstatedir=/var - # keyman-config + core/build.sh --no-tests configure:arch build:arch -- \ + --wrap-mode=nodownload --prefix=/usr --sysconfdir=/etc \ + --localstatedir=/var --libdir=lib/$(DEB_TARGET_MULTIARCH) \ + --libexecdir=lib/$(DEB_TARGET_MULTIARCH) + linux/ibus-keyman/build.sh configure -- \ + --wrap-mode=nodownload --prefix=/usr --sysconfdir=/etc --localstatedir=/var + linux/keyman-system-service/build.sh configure -- \ + --wrap-mode=nodownload --prefix=/usr --sysconfdir=/etc --localstatedir=/var + linux/keyman-config/build.sh configure override_dh_auto_build: - # keymankeyboardprocessor - cd core && \ - ./build.sh --no-tests build:arch - # ibus-keyman - cd linux/ibus-keyman && \ - ./build.sh build - # keyman-config + cp linux/keyman-config/resources/keyman.sharedmimeinfo debian/ + core/build.sh --no-tests build:arch + linux/ibus-keyman/build.sh build + linux/keyman-system-service/build.sh build + linux/keyman-config/build.sh build cd linux/keyman-config && \ - make man && \ sed -i -e "s/^__pkgversion__ = \"[^\"]*\"/__pkgversion__ = \"$(DEB_VERSION)\"/g" keyman_config/version.py && \ make compile-po - dh_auto_build --sourcedir=linux/keyman-config --buildsystem=pybuild $@ override_dh_auto_test: ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) - # keymankeyboardprocessor - cd core && \ - ./build.sh --no-tests test:arch - # ibus-keyman - cd linux/ibus-keyman && \ - ./build.sh test - # keyman-config - dh_auto_test --sourcedir=linux/keyman-config --buildsystem=pybuild $@ + core/build.sh --no-tests test:arch + linux/ibus-keyman/build.sh test + linux/keyman-system-service/build.sh test + linux/keyman-config/build.sh test endif override_dh_auto_install: install -d $(CURDIR)/debian/tmp - cd core && \ - DESTDIR=$(CURDIR)/debian/tmp ./build.sh --no-tests install:arch - # ibus-keyman - cd linux/ibus-keyman && \ - DESTDIR=$(CURDIR)/debian/tmp ./build.sh install + DESTDIR=$(CURDIR)/debian/tmp core/build.sh --no-tests install:arch + DESTDIR=$(CURDIR)/debian/tmp linux/ibus-keyman/build.sh install + DESTDIR=$(CURDIR)/debian/tmp linux/keyman-system-service/build.sh install # keyman-config install -d $(CURDIR)/debian/keyman/usr/share/ cp -r linux/keyman-config/locale/ $(CURDIR)/debian/keyman/usr/share/ rm $(CURDIR)/debian/keyman/usr/share/locale/*.po* + # Don't call `build.sh install` - dh_auto_install does some extra smarts dh_auto_install --sourcedir=linux/keyman-config --buildsystem=pybuild $@ # Unfortunately bash-completion 2.10 (focal) doesn't yet provide dh-sequence-bash-completion, # which we could add as build-dependency, so we'll have to explicitly call - # dh_bash_completion here... + # dh_bash_completion here instead of using `dh $@ --with-python3 --with bash-completion` dh_bash-completion -O--buildsystem=pybuild dh_python3 -O--buildsystem=pybuild diff --git a/linux/help/reference/index.md b/linux/help/reference/index.md index 1089382c55113e46bb4d2a9b44af18f940653220..7b8f704018f0f0b1b7090300f678e63592fa5f5c 100644 --- a/linux/help/reference/index.md +++ b/linux/help/reference/index.md @@ -1,8 +1,8 @@ --- -title: Keyman 16.0 for Linux Reference +title: Keyman 17.0 for Linux Reference --- -## Keyman 16.0 for Linux Overview +## Keyman 17.0 for Linux Overview Keyman for Linux makes it possible for you to type in over 2,000 languages on Linux. diff --git a/linux/ibus-keyman/build.sh b/linux/ibus-keyman/build.sh index 8a16bbb1deafe1f08024fd91c3174f5429d35068..58bc0576916789946abdd5f848b54f4f9a744731 100755 --- a/linux/ibus-keyman/build.sh +++ b/linux/ibus-keyman/build.sh @@ -19,7 +19,8 @@ builder_describe \ "test" \ "install install artifacts" \ "uninstall uninstall artifacts" \ - "@/core:arch" + "@/core:arch" \ + "--no-integration don't run integration tests" builder_parse "$@" @@ -59,7 +60,11 @@ fi if builder_start_action test; then cd "$THIS_SCRIPT_PATH/$MESON_PATH" - meson test --print-errorlogs $builder_verbose + if builder_has_option --no-integration; then + meson test --print-errorlogs $builder_verbose setup-src-test keymanutil-tests print-kmpdetails-test print-kmp-test bcp47-util-tests teardown-src-test + else + meson test --print-errorlogs $builder_verbose + fi builder_finish_action success test fi diff --git a/linux/ibus-keyman/man/ibus-engine-keyman.1 b/linux/ibus-keyman/man/ibus-engine-keyman.1 index 30144d50dae564cd080f37eb7d87713b78c0164b..a9f0030bf6987d8d209102b43364dc9041272974 100644 --- a/linux/ibus-keyman/man/ibus-engine-keyman.1 +++ b/linux/ibus-keyman/man/ibus-engine-keyman.1 @@ -1,28 +1,25 @@ -.TH IBUS-ENGINE-KEYMAN "1" "September 2018" "ibus-engine-keyman" "User Commands" +.TH IBUS-ENGINE-KEYMAN "1" "September 2023" "ibus-engine-keyman" "User Commands" .SH NAME ibus-engine-keyman \- multiple language input engine based on ibus .SH DESCRIPTION -\&ibus\-engine\-keyman [options] [engines] +\&ibus\-engine\-keyman [options] .TP this command should run from ibus, don't run it directly. .TP \fB\-h\fR, \fB\-\-help\fR show this message. .TP -\fB\-d\fR, \fB\-\-daemonize\fR -daemonize ibus engine +\fB\-x\fR, \fB\-\-xml\fR +generate XML listing all keyboards. This is used by ibus. .TP -\fB\-l\fR, \fB\-\-list\fR -list all Keyman input methods +\fB\-i\fR, \fB\-\-ibus\fR +Component is executed by ibus .TP -\fB\-a\fR, \fB\-\-all\fR -enable all Keyman input methods +\fB\-\-testing\fR +Component is executed by integration tests. .SS "example:" .IP -\&./ibus\-engine\-keyman zh:py ja:trycode +\&./ibus\-engine\-keyman --ibus -\&./ibus\-engine\-keyman zh:py,pinyin hi:inscript - -\&./ibus\-engine\-keyman \fB\-a\fR .SH "SEE ALSO" .BR ibus(1) diff --git a/linux/ibus-keyman/meson.build b/linux/ibus-keyman/meson.build index e8548432d4234201853fe8bcfcce2079ddfe35fd..2e402b868e710427b01a656f65ab8cdd86613836 100644 --- a/linux/ibus-keyman/meson.build +++ b/linux/ibus-keyman/meson.build @@ -9,16 +9,17 @@ project('ibus-keyman', 'c', 'cpp', cc = meson.get_compiler('c') conf = configuration_data() -ibus = dependency('ibus-1.0', version: '>= 1.2.0') -gtk = dependency('gtk+-3.0', version: '>= 2.4') -x11 = dependency('x11', version: '>= 1.6') -json_glib = dependency('json-glib-1.0', version: '>= 1.0') +ibus = dependency('ibus-1.0', version: '>= 1.2.0') +gtk = dependency('gtk+-3.0', version: '>= 2.4') +json_glib = dependency('json-glib-1.0', version: '>= 1.0') +systemd = dependency('libsystemd') +icu = dependency('icu-i18n') core_dir = meson.current_source_dir() / '../../core' common_dir = meson.current_source_dir() / '../../common' kmnkbp_lib = cc.find_library( - 'libkmnkbp0', + 'kmnkbp0', # meson will prefix 'lib' dirs: [ core_dir / 'build/arch' / get_option('buildtype') / 'src' ] ) diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp new file mode 100644 index 0000000000000000000000000000000000000000..90f459d1a21e853e0f5509756822c4292e2fd299 --- /dev/null +++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.cpp @@ -0,0 +1,106 @@ +#include +#include +#include "KeymanSystemServiceClient.h" + +#define KEYMAN_BUS_NAME "com.keyman.SystemService1" +#define KEYMAN_INTERFACE_NAME "com.keyman.SystemService1.System" +#define KEYMAN_OBJECT_PATH "/com/keyman/SystemService1/System" + +extern gboolean testing; + +class KeymanSystemServiceClient { +private: + sd_bus_error *error = NULL; + sd_bus_message *msg = NULL; + sd_bus *bus = NULL; + +public: + KeymanSystemServiceClient(); + ~KeymanSystemServiceClient(); + + void SetCapsLockIndicator(guint32 capsLock); + gint32 GetCapsLockIndicator(); +}; + +KeymanSystemServiceClient::KeymanSystemServiceClient() { + int result; + if (testing) { + result = sd_bus_open_user(&bus); + } else { + result = sd_bus_default_system(&bus); + } + if (result < 0) { + g_error("%s: Can't get connection: %s", __FUNCTION__, strerror(-result)); + } +} + +KeymanSystemServiceClient::~KeymanSystemServiceClient() { + if (error) { sd_bus_error_free(error); } + if (msg) { sd_bus_message_unref(msg); } + if (bus) { sd_bus_unref(bus); } +} + +void KeymanSystemServiceClient::SetCapsLockIndicator(guint32 capsLock) { + // If Set/GetCapsLockIndicator is called more than once and previously + // failed, we will leak `error`. + assert(error == NULL); + + if (!bus) { + // we already reported the error, so just return + return; + } + + if (capsLock > 1) { + g_error("%s: Invalid value '%d' for parameter capsLock", __FUNCTION__, capsLock); + return; + } + + int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH, + KEYMAN_INTERFACE_NAME, "SetCapsLockIndicator", error, &msg, "b", capsLock); + if (result < 0) { + g_error("%s: Failed to call method SetCapsLockIndicator: %s. %s. %s.", + __FUNCTION__, strerror(-result), error->name, error->message); + return; + } +} + +gint32 KeymanSystemServiceClient::GetCapsLockIndicator() { + // If Set/GetCapsLockIndicator is called more than once and previously + // failed, we will leak `error`. + assert(error == NULL); + + if (!bus) { + // we already reported the error, so just return + return -1; + } + + int result = sd_bus_call_method(bus, KEYMAN_BUS_NAME, KEYMAN_OBJECT_PATH, + KEYMAN_INTERFACE_NAME, "GetCapsLockIndicator", error, &msg, ""); + if (result < 0) { + g_error("%s: Failed to call method GetCapsLockIndicator: %s. %s. %s.", + __FUNCTION__, strerror(-result), error->name, error->message); + return -1; + } + + guint32 capsLock = 0; + result = sd_bus_message_read(msg, "b", &capsLock); + if (result < 0) { + g_error("%s: Failed to parse response message: %s", + __FUNCTION__, strerror(-result)); + return -1; + } + + return capsLock; +} + +void set_capslock_indicator( + guint32 capsLock +) { + KeymanSystemServiceClient client; + client.SetCapsLockIndicator(capsLock); +} + +gint32 get_capslock_indicator() { + KeymanSystemServiceClient client; + return client.GetCapsLockIndicator(); +} diff --git a/linux/ibus-keyman/src/KeymanSystemServiceClient.h b/linux/ibus-keyman/src/KeymanSystemServiceClient.h new file mode 100644 index 0000000000000000000000000000000000000000..746974df5194e43fca411ef8081fdae8f6a0b9ae --- /dev/null +++ b/linux/ibus-keyman/src/KeymanSystemServiceClient.h @@ -0,0 +1,17 @@ +#ifndef __KEYMANSYSTEMSERVICECLIENT_H__ +#define __KEYMANSYSTEMSERVICECLIENT_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void set_capslock_indicator(guint32 capsLockState); +gint32 get_capslock_indicator(); + +#ifdef __cplusplus +} +#endif + +#endif // __KEYMANSYSTEMSERVICECLIENT_H__ diff --git a/linux/ibus-keyman/src/bcp47util.c b/linux/ibus-keyman/src/bcp47util.c new file mode 100644 index 0000000000000000000000000000000000000000..0b2013038a0245d997dfcd8aae215cf0358704b9 --- /dev/null +++ b/linux/ibus-keyman/src/bcp47util.c @@ -0,0 +1,99 @@ +#include +#include +#include + +/// Minimize the BCP-47 `tag` so that unnecessary parts get ommitted. +/// The result gets stored in `minimzedTag`. +/// +/// @param tag The tag to process +/// @param minimizedTag Caller-provided character array for the +/// resulting minimized tag. +/// @param tagCapacity Array size of `minimizedTag` +/// @return The length of the minimized tag, or -1 in error case +int bcp47_minimize(const char* tag, char* minimizedTag, int tagCapacity) { + UErrorCode status = U_ZERO_ERROR; + if (!tag || strlen(tag) == 0) { + strncpy(minimizedTag, "", tagCapacity); + return -1; + } + + // special treatment for `und-Latn` which is used by sil_ipa keyboard + if (strcmp(tag, "und-Latn") == 0) { + strncpy(minimizedTag, "und-Latn", tagCapacity); + return strlen(minimizedTag); + } + + int capacity = 255; + char workingTag[capacity]; + + // special treatment for tags that start with `und`: replace `und` with `en`. + // ICU 70 doesn't properly treat `und`. + int isUnd = strncmp(tag, "und", 3) == 0; + if (isUnd) { + strcpy(workingTag, "en"); + strncat(&workingTag[2], &tag[3], capacity - 3); + } else { + strncpy(workingTag, tag, capacity - 1); + } + workingTag[capacity - 1] = 0; + + char localeId[capacity]; + uloc_forLanguageTag(workingTag, localeId, capacity, NULL, &status); + if (U_FAILURE(status)) { + g_error("%s: uloc_forLanguageTag returned %0x", __FUNCTION__, status); + return -1; + } + + char minimizedLocaleId[capacity]; + uloc_minimizeSubtags(localeId, minimizedLocaleId, capacity, &status); + if (U_FAILURE(status)) { + g_error("%s: uloc_minimizeSubtags returned %0x", __FUNCTION__, status); + return -1; + } + + int taglen = uloc_toLanguageTag(minimizedLocaleId, minimizedTag, tagCapacity, FALSE, &status); + if (U_FAILURE(status)) { + g_error("%s: uloc_toLanguageTag returned %0x", __FUNCTION__, status); + return -1; + } + + if (isUnd) { + // Replace 'en' with 'und' again + strncpy(workingTag, &minimizedTag[2], capacity - 1); + workingTag[capacity - 1] = 0; + strcpy(minimizedTag, "und"); + strncat(minimizedTag, workingTag, tagCapacity - 4); + minimizedTag[tagCapacity - 1] = 0; + taglen = strlen(minimizedTag); + } + return taglen; +} + +/// Extract the language code from the BCP-47 `tag` +/// +/// @param tag The BCP-47 tag +/// @param lang_code Caller-provided character array that will receive +/// the language code extracted from `tag` +/// @param capacity Array size of `lang_code` +/// @return TRUE if successful, otherwise FALSE +int bcp47_get_language_code(const char* tag, char* lang_code, int capacity) { + UErrorCode status = U_ZERO_ERROR; + if (!tag || strlen(tag) == 0) { + strncpy(lang_code, "", capacity); + return FALSE; + } + + // ICU 70 doesn't properly treat `und` + if (strncmp(tag, "und", 3) == 0) { + strncpy(lang_code, "und", capacity); + return TRUE; + } + + uloc_getLanguage(tag, lang_code, capacity, &status); + if (U_FAILURE(status)) { + g_error("%s: uloc_getLanguage returned %0x", __FUNCTION__, status); + return FALSE; + } + + return TRUE; +} diff --git a/linux/ibus-keyman/src/bcp47util.h b/linux/ibus-keyman/src/bcp47util.h new file mode 100644 index 0000000000000000000000000000000000000000..8a3a057f852e31fe619287ba4c180f0e6709f9d0 --- /dev/null +++ b/linux/ibus-keyman/src/bcp47util.h @@ -0,0 +1,7 @@ +#ifndef __BCP47UTIL_H__ +#define __BCP47UTIL_H__ + +int bcp47_minimize(const char* tag, char* minimizedTag, int capacity); +int bcp47_get_language_code(const char* tag, char* lang_code, int capacity); + +#endif // __BCP47UTIL_H__ diff --git a/linux/ibus-keyman/src/engine.c b/linux/ibus-keyman/src/engine.c index b7699f55623e46ac7f84f95697768d0ca141e401..49b9161f9a3f766786a8f60014470f4e2ba0a057 100644 --- a/linux/ibus-keyman/src/engine.c +++ b/linux/ibus-keyman/src/engine.c @@ -3,7 +3,7 @@ /* * Keyman Input Method for IBUS (The Input Bus) * - * Copyright (C) 2009-2022 SIL International + * Copyright (C) 2009-2023 SIL International * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public @@ -28,21 +28,13 @@ #include #include -#ifdef GDK_WINDOWING_X11 -#include -#include -#endif - -#ifdef GDK_WINDOWING_WAYLAND -#include -#endif - #include #include #include "config.h" #include "keymanutil.h" #include "keyman-service.h" +#include "KeymanSystemServiceClient.h" #include "engine.h" #include "keycodes.h" @@ -90,12 +82,6 @@ struct _IBusKeymanEngine { IBusLookupTable *table; IBusProperty *status_prop; IBusPropList *prop_list; -#ifdef GDK_WINDOWING_X11 - Display *xdisplay; -#endif -#ifdef GDK_WINDOWING_WAYLAND - GdkWaylandDisplay *wldisplay; -#endif commit_queue_item commit_queue[MAX_QUEUE_SIZE]; commit_queue_item *commit_item; @@ -311,29 +297,6 @@ ibus_keyman_engine_init(IBusKeymanEngine *keyman) { ibus_prop_list_append(keyman->prop_list, keyman->status_prop); keyman->state = NULL; -#ifdef GDK_WINDOWING_X11 - keyman->xdisplay = NULL; -#endif -#ifdef GDK_WINDOWING_WAYLAND - keyman->wldisplay = NULL; -#endif - - GdkDisplay *gdkDisplay = gdk_display_get_default(); -#ifdef GDK_WINDOWING_X11 - if (GDK_IS_X11_DISPLAY(gdkDisplay)) { - g_debug("Using X11"); - keyman->xdisplay = GDK_DISPLAY_XDISPLAY(gdkDisplay); - } else -#endif -#ifdef GDK_WINDOWING_WAYLAND - if (GDK_IS_WAYLAND_DISPLAY(gdkDisplay)) { - g_debug("Using Wayland"); - keyman->wldisplay = GDK_WAYLAND_DISPLAY(gdkDisplay); - } else -#endif - { - g_error("unsupported GDK backend"); - } } static km_kbp_cp* get_base_layout() @@ -752,19 +715,7 @@ process_capslock_action( ) { g_message("%s: %s caps-lock", __FUNCTION__, action_item->capsLock ? "Enable" : "Disable"); -#ifdef GDK_WINDOWING_X11 - if (keyman->xdisplay) { - XkbLockModifiers(keyman->xdisplay, XkbUseCoreKbd, LockMask, action_item->capsLock ? LockMask : 0); - - XSync(keyman->xdisplay, False); - } -#endif -#ifdef GDK_WINDOWING_WAYLAND - // TODO - if (keyman->wldisplay) { - - } -#endif + set_capslock_indicator(action_item->capsLock); return TRUE; } diff --git a/linux/ibus-keyman/src/keymanutil.c b/linux/ibus-keyman/src/keymanutil.c index 4efd941becad1a3e22e8ecaf56dfc71a1c8f0edb..6726aee4e3dd22f347e99aafa214f8301c69cfd0 100644 --- a/linux/ibus-keyman/src/keymanutil.c +++ b/linux/ibus-keyman/src/keymanutil.c @@ -57,6 +57,7 @@ #include #include +#include "bcp47util.h" #include "keymanutil.h" #include "kmpdetails.h" #include "keyman-version.h" @@ -163,8 +164,6 @@ ibus_keyman_add_engines(GList * engines, GList * kmpdir_list) get_kmp_details(kmp_dir, details); for (k=details->keyboards; k != NULL; k = k->next) { - gchar *lang=NULL; - gchar *name_with_lang = NULL; kmp_keyboard *keyboard = (kmp_keyboard *) k->data; gboolean alreadyexists = FALSE; @@ -191,35 +190,50 @@ ibus_keyman_add_engines(GList * engines, GList * kmpdir_list) for (l=keyboard->languages; l != NULL; l = l->next) { kmp_language *language = (kmp_language *) l->data; if (language->id != NULL) { - gchar **tagparts = g_strsplit(language->id, "-", 2); - lang = g_strdup(tagparts[0]); - g_strfreev(tagparts); - // If ibus doesn't know about the language then append the - // language name to the keyboard name - if (language->name != NULL) { - if (g_strcmp0(ibus_get_untranslated_language_name (lang), "Other") == 0) { - name_with_lang = g_strjoin(" - ", keyboard->name, language->name, NULL); - } + int capacity = 255; + gchar *name_with_lang = NULL; + gchar *minimized_tag = g_new0(gchar, capacity); + int result = bcp47_minimize(language->id, minimized_tag, capacity); + if (result < 0) { + g_strlcpy(minimized_tag, language->id, capacity); + } + + gchar *lang_code = g_new0(gchar, capacity); + if (!bcp47_get_language_code(minimized_tag, lang_code, capacity)) { + g_strlcpy(lang_code, minimized_tag, capacity); + } + + // If ibus doesn't know about the language then append the + // language name to the keyboard name + if (language->name != NULL) { + gchar *ibus_lang = ibus_get_untranslated_language_name(lang_code); + g_debug("%s: untranslated ibus language for %s: %s", __FUNCTION__, minimized_tag, ibus_lang); + if (g_strcmp0(ibus_lang, "Other") == 0) { + name_with_lang = g_strjoin(" - ", keyboard->name, language->name, NULL); } - - gchar *id_with_lang = g_strjoin(":", language->id, abs_kmx, NULL); - - g_message("adding engine %s", id_with_lang); - engines = g_list_append (engines, - ibus_keyman_engine_desc_new (id_with_lang, // lang:kmx full path - name_with_lang ? name_with_lang : keyboard->name, // longname - kbd_details->description, // description - details->info.copyright, // copyright if available - lang, // language, most are ignored by ibus except major languages - kbd_details->license, // license - details->info.author_desc, // author name only, not email - keyman_get_icon_file(abs_kmx), // icon full path - "us", // layout defaulting to us (en-US) - keyboard->version)); - g_free(lang); - g_free(id_with_lang); - g_free(name_with_lang); - name_with_lang = NULL; + g_free(ibus_lang); + } + + gchar *id_with_lang = g_strjoin(":", minimized_tag, abs_kmx, NULL); + + g_message("adding engine %s", id_with_lang); + engines = g_list_append( + engines, + ibus_keyman_engine_desc_new( + id_with_lang, // lang:kmx full path + name_with_lang ? name_with_lang : keyboard->name, // longname + kbd_details->description, // description + details->info.copyright, // copyright if available + lang_code, // language, most are ignored by ibus except major languages + kbd_details->license, // license + details->info.author_desc, // author name only, not email + keyman_get_icon_file(abs_kmx), // icon full path + "us", // layout defaulting to us (en-US) + keyboard->version)); + g_free(lang_code); + g_free(minimized_tag); + g_free(id_with_lang); + g_free(name_with_lang); } } } @@ -230,7 +244,7 @@ ibus_keyman_add_engines(GList * engines, GList * kmpdir_list) keyboard->name, // longname kbd_details->description, // description details->info.copyright, // copyright if available - lang, // language, most are ignored by ibus except major languages + NULL, // language, most are ignored by ibus except major languages kbd_details->license, // license details->info.author_desc, // author name only, not email keyman_get_icon_file(abs_kmx), // icon full path diff --git a/linux/ibus-keyman/src/main.c b/linux/ibus-keyman/src/main.c index f791cb45640db610194765626ed3fb36c4a307c0..0938c05b74389a808d645a8e0e19dc0c5cb7f4cb 100644 --- a/linux/ibus-keyman/src/main.c +++ b/linux/ibus-keyman/src/main.c @@ -35,16 +35,17 @@ static IBusFactory *factory = NULL; /* options */ static gboolean xml = FALSE; static gboolean ibus = FALSE; -static gboolean verbose = FALSE; +gboolean testing = FALSE; static const GOptionEntry entries[] = { { "xml", 'x', 0, G_OPTION_ARG_NONE, &xml, "generate xml for engines", NULL }, { "ibus", 'i', 0, G_OPTION_ARG_NONE, &ibus, "component is executed by ibus", NULL }, - { "verbose", 'v', 0, G_OPTION_ARG_NONE, &verbose, "verbose", NULL }, + { "testing", 0, 0, G_OPTION_ARG_NONE, &testing, "component is executed by integration testing", NULL}, { NULL }, }; +// Add an environment variable to see debug messages: export G_MESSAGES_DEBUG=all static void ibus_disconnected_cb (IBusBus *bus, diff --git a/linux/ibus-keyman/src/meson.build b/linux/ibus-keyman/src/meson.build index 29f265290d51ab76130db7f37d03c4c435cfb1c0..f7fe8ada1d02bfc1e95a7f3c4fe1602e403d5dfc 100644 --- a/linux/ibus-keyman/src/meson.build +++ b/linux/ibus-keyman/src/meson.build @@ -1,17 +1,14 @@ util_files = files( 'keymanutil.c', - 'keymanutil.h', 'kmpdetails.c', - 'kmpdetails.h', + 'bcp47util.c', ) engine_files = files( 'main.c', 'engine.c', - 'engine.h', - 'keycodes.h', 'keyman-service.c', - 'keyman-service.h', + 'KeymanSystemServiceClient.cpp', ) include_dirs = [ @@ -20,7 +17,7 @@ include_dirs = [ include_directories(meson.current_build_dir() / '..'), ] -deps = [ibus, gtk, x11, json_glib, kmnkbp_lib] +deps = [gtk, ibus, icu, json_glib, kmnkbp_lib, systemd] prefix = get_option('prefix') cfg = configuration_data() diff --git a/linux/ibus-keyman/src/test/bcp47util_tests.c b/linux/ibus-keyman/src/test/bcp47util_tests.c new file mode 100644 index 0000000000000000000000000000000000000000..a82d30476f35e990a51900d7b206585ed9c189d0 --- /dev/null +++ b/linux/ibus-keyman/src/test/bcp47util_tests.c @@ -0,0 +1,100 @@ +#include +#include +#include +#include "bcp47util.h" + +typedef struct { +} Bcp47UtilFixture; + +typedef struct { + const char *tag; + const char *expected; + int expectedResult; +} TestData; + +static void +test_bcp47_minimize(Bcp47UtilFixture *fixture, gconstpointer user_data) { + TestData *testData = (TestData *)user_data; + int capacity = 255; + char minimizedTag[capacity]; + int result = bcp47_minimize(testData->tag, minimizedTag, capacity); + g_assert_cmpint(result, ==, testData->expectedResult); + g_assert_cmpstr(minimizedTag, ==, testData->expected); +} + +static void +test_bcp47_get_language_code(Bcp47UtilFixture *fixture, gconstpointer user_data) { + TestData *testData = (TestData *)user_data; + int capacity = 255; + char lang_code[capacity]; + int result = bcp47_get_language_code(testData->tag, lang_code, capacity); + g_assert_cmpint(result, ==, testData->expectedResult); + g_assert_cmpstr(lang_code, ==, testData->expected); +} + +int +main(int argc, char *argv[]) { + gtk_init(&argc, &argv); + g_test_init(&argc, &argv, NULL); + g_test_set_nonfatal_assertions(); + + TestData testData1 = {NULL, "", -1}; + g_test_add("/bcp47util/minimize/NULL", Bcp47UtilFixture, &testData1, NULL, test_bcp47_minimize, NULL); + + TestData testData2 = {"", "", -1}; + g_test_add("/bcp47util/minimize/EmptyString", Bcp47UtilFixture, &testData2, NULL, test_bcp47_minimize, NULL); + + TestData testData3 = { "fuf", "fuf", 3 }; + g_test_add("/bcp47util/minimize/fuf", Bcp47UtilFixture, &testData3, NULL, test_bcp47_minimize, NULL); + + TestData testData4 = {"fuf-Latn", "fuf", 3}; + g_test_add("/bcp47util/minimize/fuf-Latn", Bcp47UtilFixture, &testData4, NULL, test_bcp47_minimize, NULL); + + TestData testData5 = {"fuf-Arab", "fuf-Arab", 8}; + g_test_add("/bcp47util/minimize/fuf-Arab", Bcp47UtilFixture, &testData5, NULL, test_bcp47_minimize, NULL); + + TestData testData6 = {"fuf-Adlm-ML", "fuf-Adlm-ML", 11}; + g_test_add("/bcp47util/minimize/fuf-Adlm-ML", Bcp47UtilFixture, &testData6, NULL, test_bcp47_minimize, NULL); + + TestData testData7 = {"und", "und", 3}; + g_test_add("/bcp47util/minimize/und", Bcp47UtilFixture, &testData7, NULL, test_bcp47_minimize, NULL); + + TestData testData8 = {"und-Latn", "und-Latn", 8}; + g_test_add("/bcp47util/minimize/und-Latn", Bcp47UtilFixture, &testData8, NULL, test_bcp47_minimize, NULL); + + TestData testData9 = {"und-fonipa", "und-fonipa", 10}; + g_test_add("/bcp47util/minimize/und-fonipa", Bcp47UtilFixture, &testData9, NULL, test_bcp47_minimize, NULL); + + TestData testData10 = {"und-Latn-fonipa", "und-fonipa", 10}; + g_test_add("/bcp47util/minimize/und-Latn-fonipa", Bcp47UtilFixture, &testData10, NULL, test_bcp47_minimize, NULL); + + TestData testData11 = {"mul", "mul", 3}; + g_test_add("/bcp47util/minimize/mul", Bcp47UtilFixture, &testData11, NULL, test_bcp47_minimize, NULL); + + // bcp47_get_language_code tests + TestData testData21 = {NULL, "", FALSE}; + g_test_add("/bcp47util/langcode/NULL", Bcp47UtilFixture, &testData21, NULL, test_bcp47_get_language_code, NULL); + + TestData testData22 = {"", "", FALSE}; + g_test_add("/bcp47util/langcode/EmptyString", Bcp47UtilFixture, &testData22, NULL, test_bcp47_get_language_code, NULL); + + TestData testData23 = {"fuf", "fuf", TRUE}; + g_test_add("/bcp47util/langcode/fuf", Bcp47UtilFixture, &testData23, NULL, test_bcp47_get_language_code, NULL); + + TestData testData24 = {"fuf-Latn", "fuf", TRUE}; + g_test_add("/bcp47util/langcode/fuf-Latn", Bcp47UtilFixture, &testData24, NULL, test_bcp47_get_language_code, NULL); + + TestData testData25 = {"fuf-Arab", "fuf", TRUE}; + g_test_add("/bcp47util/langcode/fuf-Arab", Bcp47UtilFixture, &testData25, NULL, test_bcp47_get_language_code, NULL); + + TestData testData26 = {"fuf-Adlm-ML", "fuf", TRUE}; + g_test_add("/bcp47util/langcode/fuf-Adlm-ML", Bcp47UtilFixture, &testData26, NULL, test_bcp47_get_language_code, NULL); + + TestData testData27 = {"und", "und", TRUE}; + g_test_add("/bcp47util/langcode/und", Bcp47UtilFixture, &testData27, NULL, test_bcp47_get_language_code, NULL); + + TestData testData28 = {"und-Latn", "und", TRUE}; + g_test_add("/bcp47util/langcode/und-Latn", Bcp47UtilFixture, &testData28, NULL, test_bcp47_get_language_code, NULL); + + return g_test_run(); +} diff --git a/linux/ibus-keyman/src/test/meson.build b/linux/ibus-keyman/src/test/meson.build index c652d8f783f7bbe0be8aa3201bc388d725305f77..1ad841c6d6e9a539b445c43100c487bec857a05d 100644 --- a/linux/ibus-keyman/src/test/meson.build +++ b/linux/ibus-keyman/src/test/meson.build @@ -3,11 +3,13 @@ keymanutil_sources = [ util_files, ] -keymanutil_deps = [ibus, gtk, x11, json_glib, kmnkbp_lib] +keymanutil_deps = [gtk, ibus, icu, json_glib, kmnkbp_lib] test_env = [ 'G_TEST_SRCDIR=' + meson.current_source_dir(), 'G_TEST_BUILDDIR=' + meson.current_build_dir(), + 'TOP_SRCDIR=' + meson.global_source_root(), + 'TOP_BINDIR=' + meson.build_root(), ] test_include_dirs = [ @@ -16,47 +18,102 @@ test_include_dirs = [ include_directories(meson.current_build_dir() / '..'), ] -executable( +env_file = '/tmp/env-src-test.txt' +pid_file = '/tmp/ibus-keyman-src-test-pids' + +setup_src_test_tests = find_program('setup-tests.sh', dirs: [meson.current_source_dir()]) +teardown_tests = find_program('teardown-tests.sh', dirs: [meson.current_source_dir() / '../../tests/scripts']) +run_src_test = find_program('run-single-test.sh', dirs: [meson.current_source_dir()]) + +keymanutil_tests = executable( 'keymanutil-tests', sources: keymanutil_sources, dependencies: keymanutil_deps, include_directories : test_include_dirs ) +print_kmpdetails_test = executable( + 'print_kmpdetails', + sources: [ + 'print_kmpdetails.c', + '../kmpdetails.c' + ], + dependencies: [ json_glib ], + include_directories: test_include_dirs +) + +print_kmp_test = executable( + 'print_kmp', + sources: [ + 'print_kmp.c', + ], + dependencies: [ json_glib ], + include_directories: test_include_dirs +) + +bcp47_util_tests = executable( + 'bcp47-util-tests', + sources: [ + 'bcp47util_tests.c', + '../bcp47util.c' + ], + dependencies: [ gtk, icu ], + include_directories: test_include_dirs +) + +test( + 'setup-src-test', + setup_src_test_tests, + args: ['--x11', env_file, pid_file], + env: test_env, + priority: -1, + is_parallel: false, + protocol: 'exitcode' +) + +test( + 'teardown-src-test', + teardown_tests, + args: [pid_file], + priority: -9, + is_parallel: false, + protocol: 'exitcode' +) + test( 'keymanutil-tests', - find_program('run-tests.sh'), + run_src_test, + args: [ '--tap', '-k', '--env', env_file, '--', keymanutil_tests], env: test_env, + priority: -2, + is_parallel: false, protocol: 'tap', ) test( 'print-kmpdetails-test', - executable( - 'print_kmpdetails', - sources: [ - 'print_kmpdetails.c', - '../kmpdetails.c' - ], - dependencies: [ json_glib ], - include_directories: test_include_dirs - ), - args: [ meson.current_source_dir() ], + run_src_test, + args: [ '--', print_kmpdetails_test, meson.current_source_dir() ], env: test_env, + priority: -2, protocol: 'exitcode', ) test( 'print-kmp-test', - executable( - 'print_kmp', - sources: [ - 'print_kmp.c', - ], - dependencies: [ json_glib ], - include_directories: test_include_dirs - ), - args: [ meson.current_source_dir() / 'kmp.json' ], + run_src_test, + args: [ '--', print_kmp_test, meson.current_source_dir() / 'kmp.json' ], env: test_env, + priority: -2, protocol: 'exitcode', ) + +test( + 'bcp47-util-tests', + run_src_test, + args: [ '--tap', '-k', '--env', env_file, '--', bcp47_util_tests ], + env: test_env, + priority: -2, + is_parallel: false, + protocol: 'tap', +) diff --git a/linux/ibus-keyman/src/test/run-single-test.sh b/linux/ibus-keyman/src/test/run-single-test.sh new file mode 100755 index 0000000000000000000000000000000000000000..1ad87144490fa7c9538edb7e3418bae5939d29b4 --- /dev/null +++ b/linux/ibus-keyman/src/test/run-single-test.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +function help() { + echo "Usage:" + echo " $0 [--env ] [-k] [--tap] [--] TESTFILE [TESTARGS]" + echo + echo "Arguments:" + echo " --help, -h, -? Display this help" + echo " --verbose, -v Run tests verbosely" + echo " --debug debug test logging output" + echo " -k passed to GLib testing framework" + echo " --tap output in TAP format. Passed to GLib testing framework" + echo " --env Name of the file containing environment variables to use" + exit 0 +} + +function run_tests() { + # Output these lines to stderr - the first line on stdout has to be the TAP version number + # which running ${TESTFILE} outputs + echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" >&2 + echo "" >&2 + + echo "# Starting tests..." >&2 + + # Note: -k and --tap are consumed by the GLib testing framework + # shellcheck disable=SC2086 + "${TESTFILE}" ${ARG_K-} ${ARG_TAP-} ${ARG_VERBOSE-} ${ARG_DEBUG-} "$@" + echo "# Finished tests." +} + +while (( $# )); do + case $1 in + --help|-h|-\?) help ;; + -k) ARG_K=$1 ;; + --tap) ARG_TAP=$1 ;; + --verbose|-v) ARG_VERBOSE=--verbose;; + --debug) ARG_DEBUG=--debug-log;; + --env) shift ; ARG_ENV=$1 ;; + --) shift ; TESTFILE=$1; shift ; break ;; + *) echo "Error: Unexpected argument \"$1\". Exiting." ; exit 4 ;; + esac + shift || (echo "Error: The last argument is missing a value. Exiting."; false) || exit 5 +done + +# shellcheck source=/dev/null +. "$ARG_ENV" + +run_tests "$@" diff --git a/linux/ibus-keyman/src/test/run-tests.sh b/linux/ibus-keyman/src/test/run-tests.sh index 932f64b886163c707f2dd684e6b0f051a490fb5b..efeb747ddc7f9ed0c4449ab46b57ffd0c4c03b49 100755 --- a/linux/ibus-keyman/src/test/run-tests.sh +++ b/linux/ibus-keyman/src/test/run-tests.sh @@ -7,8 +7,8 @@ if [ -v KEYMAN_PKG_BUILD ]; then # During package builds we skip these tests - they often fail, e.g. # during Debian reproducibility testing with an error like # "cannot open display: :32" - echo "1..1" - echo "ok 1 # SKIP on package build" + echo "TAP version 14" + echo "1..0 # SKIP on package build" exit 0 fi @@ -53,9 +53,9 @@ SCHEMA_DIR=$TEMP_DATA_DIR/glib-2.0/schemas export XDG_DATA_DIRS=$TEMP_DATA_DIR:$XDG_DATA_DIRS mkdir -p "$SCHEMA_DIR" -cp "$SRCDIR/../keyman-config/com.keyman.gschema.xml" "$SCHEMA_DIR/" +cp "$SRCDIR/../keyman-config/resources/com.keyman.gschema.xml" "$SCHEMA_DIR/" glib-compile-schemas "$SCHEMA_DIR" export GSETTINGS_BACKEND=memory -${G_TEST_BUILDDIR:-.}/keymanutil-tests "$@" +"${G_TEST_BUILDDIR:-.}/keymanutil-tests" "$@" diff --git a/linux/ibus-keyman/src/test/setup-tests.sh b/linux/ibus-keyman/src/test/setup-tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..736650270de59eb28e3d23cfc5c5c070e1390f75 --- /dev/null +++ b/linux/ibus-keyman/src/test/setup-tests.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -eu + +. "$(dirname "$0")/../../tests/scripts/test-helper.inc.sh" + +setup_display_server_only "$1" "$2" "$3" diff --git a/linux/ibus-keyman/tests/KmDbusTestServer.cpp b/linux/ibus-keyman/tests/KmDbusTestServer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1d4746163d5171fa3fcbf10d58db8d099e97b961 --- /dev/null +++ b/linux/ibus-keyman/tests/KmDbusTestServer.cpp @@ -0,0 +1,146 @@ +// DBus test server. The server will start and listen on a non-standard DBus. +// It runs until the DBus Exit method gets called. +#include +#include +#include +#include + +#ifndef KEYMAN_TEST_SERVICE_PATH +#warning KEYMAN_TEST_SERVICE_PATH is undefined +#define KEYMAN_TEST_SERVICE_PATH "." +#endif + +#define KEYMAN_TESTSVC_BUS_NAME "com.keyman.ExitTestService" +#define KEYMAN_TESTSVC_INTERFACE_NAME "com.keyman.ExitTestService.Exit" +#define KEYMAN_TESTSVC_OBJECT_PATH "/com/keyman/ExitTestService/Exit" + +using namespace std; + +class KmDbusTestServer +{ +private: + GTestDBus *dbus; + sd_bus_slot *slot = NULL; + sd_bus *bus = NULL; + +public: + KmDbusTestServer(); + ~KmDbusTestServer(); + + void Loop(); +}; + +static int32_t +on_exit_method(sd_bus_message *msg, void *user_data, sd_bus_error *ret_error) { + g_debug("%s called", __FUNCTION__); + guint *exitFlag = (guint *)user_data; + *exitFlag = TRUE; + + return sd_bus_reply_method_return(msg, ""); +} + +static const sd_bus_vtable exit_test_service_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("Exit", "", "", on_exit_method, 0), + SD_BUS_VTABLE_END +}; + +KmDbusTestServer::KmDbusTestServer() +{ + dbus = g_test_dbus_new(G_TEST_DBUS_NONE); + + // Add the private directory with our in-tree service files. + g_test_dbus_add_service_dir(dbus, KEYMAN_TEST_SERVICE_PATH); + + std::cout << "Add service dir to " << KEYMAN_TEST_SERVICE_PATH << std::endl; + + // Start the private D-Bus daemon + g_test_dbus_up(dbus); + + const gchar *address = g_test_dbus_get_bus_address(dbus); + + std::cout << "Test server running on: " << address << std::endl; + + ofstream file("/tmp/km-test-server.env", ios_base::out); + if (file) { + // write the address to a file which can be sourced + file << "export DBUS_SESSION_BUS_ADDRESS=" << address << std::endl; + file.close(); + } +} + +KmDbusTestServer::~KmDbusTestServer() +{ + if (bus) sd_bus_release_name(bus, KEYMAN_TESTSVC_BUS_NAME); + if (slot) sd_bus_slot_unref(slot); + if (bus) sd_bus_close_unref(bus); + + g_test_dbus_down(dbus); + g_object_unref(dbus); +} + +void KmDbusTestServer::Loop() +{ + int ret; + + ret = sd_bus_open_user(&bus); + + if (ret < 0) { + g_error("Failed to connect to system bus: %s", strerror(-ret)); + return; + } + + guint exitFlag = FALSE; + + // Install the object + ret = sd_bus_add_object_vtable(bus, &slot, KEYMAN_TESTSVC_OBJECT_PATH, + KEYMAN_TESTSVC_INTERFACE_NAME, exit_test_service_vtable, &exitFlag); + if (ret < 0) { + g_error("Failed to issue method call: %s", strerror(-ret)); + return; + } + + // Take a well-known service name so that clients can find us + ret = sd_bus_request_name(bus, KEYMAN_TESTSVC_BUS_NAME, 0); + if (ret < 0) { + g_error("Failed to acquire service name: %s", strerror(-ret)); + return; + } + + for (;;) { + // Process requests + ret = sd_bus_process(bus, NULL); + g_debug("sd_bus_process returned %d, exitFlag=%d", ret, exitFlag); + if (ret < 0) { + g_error("Failed to process bus: %s", strerror(-ret)); + return; + } + if (exitFlag) { + // `exitFlag` can be modified by the callback function + // `on_exit_method` which can be called by `sd_bus_process` + g_debug("Exiting loop"); + return; + } + + if (ret > 0) { + // we processed a request, try to process another one, right-away + continue; + } + + // Wait for the next request to process + ret = sd_bus_wait(bus, (uint64_t)-1); + g_debug("sd_bus_wait returned %d", ret); + if (ret < 0) { + g_error("Failed to wait on bus: %s", strerror(-ret)); + return; + } + } +} + +int +main(int argc, char *argv[]) { + KmDbusTestServer testServer; + testServer.Loop(); + + return 0; +} diff --git a/linux/ibus-keyman/tests/StopTestServer.cpp b/linux/ibus-keyman/tests/StopTestServer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..97ce5bfbe7f8200624e02e8defa05bbb4d4ece42 --- /dev/null +++ b/linux/ibus-keyman/tests/StopTestServer.cpp @@ -0,0 +1,58 @@ +// Call the Exit method on the km-dbus-test-server. Remember to source +// `/tmp/km-test-server.env` prior to running stop-test-server in order +// to run on our non-standard DBus. +#include +#include + +using namespace std; + +class StopTestServer +{ +private: + sd_bus_error* error = NULL; + sd_bus_message* msg = NULL; + sd_bus* bus = NULL; + +public: + StopTestServer(); + ~StopTestServer(); + + int Run(); +}; + +StopTestServer::StopTestServer() +{ +} + +StopTestServer::~StopTestServer() +{ + if (error) { sd_bus_error_free(error); } + if (msg) { sd_bus_message_unref(msg); } + if (bus) { sd_bus_unref(bus); } +} + +int StopTestServer::Run() +{ + int result; + + result = sd_bus_open_user(&bus); + if (result < 0) { + std::cerr << "Can't get connection: " << strerror(-result) << std::endl; + return 1; + } + + result = sd_bus_call_method(bus, "com.keyman.ExitTestService", + "/com/keyman/ExitTestService/Exit", "com.keyman.ExitTestService.Exit", + "Exit", error, &msg, ""); + if (result < 0) { + std::cerr << "Failed to issue method call: " << strerror(-result) << std::endl; + return 1; + } + return 0; +} + +int +main(int argc, char* argv[]) { + StopTestServer stopServer; + return stopServer.Run(); +} diff --git a/linux/ibus-keyman/tests/meson.build b/linux/ibus-keyman/tests/meson.build index c67f9920a937ae28ea03cf05ed69e50ed169935a..20d82e7e2c4800c5b69463b48dd350ef8de7421c 100644 --- a/linux/ibus-keyman/tests/meson.build +++ b/linux/ibus-keyman/tests/meson.build @@ -1,17 +1,17 @@ test_files = [ 'ibusimcontext.c', - 'ibusimcontext.h', 'testfixture.cpp', 'testmodule.c', - 'testmodule.h', + '../src/KeymanSystemServiceClient.cpp', ] kmnkbp_tests_lib = cc.find_library( - 'libkmnkbp-tests', + 'kmnkbp-tests', # meson will prefix 'lib' dirs: [ core_dir / 'build/arch' / get_option('buildtype') / 'tests/kmx_test_source' ] ) -deps = [ibus, gtk, x11, json_glib, kmnkbp_lib, kmnkbp_tests_lib] +test_deps = [gtk, ibus, icu, json_glib, kmnkbp_lib, kmnkbp_tests_lib, systemd] +dbus_deps = [gtk, systemd] test_env = [ 'G_TEST_SRCDIR=' + meson.current_source_dir(), @@ -28,10 +28,27 @@ test_include_dirs = [ test_exe = executable( 'ibus-keyman-tests', test_files, util_files, - dependencies: deps, + dependencies: test_deps, include_directories: test_include_dirs, ) +system_service_dir = meson.current_source_dir() / '../../keyman-system-service/build' / build_machine.cpu_family() / get_option('buildtype') / 'tests/services' + +dbus_test_server = executable( + 'km-dbus-test-server', + 'KmDbusTestServer.cpp', + dependencies: dbus_deps, + cpp_args: [ + '-DKEYMAN_TEST_SERVICE_PATH="' + system_service_dir + '"' + ] +) + +stop_test_server = executable( + 'stop-test-server', + 'StopTestServer.cpp', + dependencies: dbus_deps, +) + env_file = '/tmp/env.txt' pid_file = '/tmp/ibus-keyman-test-pids' @@ -40,6 +57,10 @@ teardown_tests = find_program('teardown-tests.sh', dirs: [meson.current_source_d run_test = find_program('run-single-test.sh', dirs: [meson.current_source_dir() / 'scripts']) find_tests = find_program('find-tests.sh', dirs: [meson.current_source_dir() / 'scripts']) +# Mutter 40.x added the --headless option wich we need in order to be able to run the Wayland tests +mutter = find_program('mutter', required: false, version: '>=40') +can_build_wayland = mutter.found() + test( 'setup-x11', setup_tests, @@ -50,16 +71,6 @@ test( protocol: 'exitcode' ) -test( - 'setup-wayland', - setup_tests, - args: ['--wayland', env_file, pid_file], - env: test_env, - priority: -20, - is_parallel: false, - protocol: 'exitcode' -) - test( 'teardown-x11', teardown_tests, @@ -69,14 +80,26 @@ test( protocol: 'exitcode' ) -test( - 'teardown-wayland', - teardown_tests, - args: [pid_file], - priority: -29, - is_parallel: false, - protocol: 'exitcode' -) +if can_build_wayland + test( + 'setup-wayland', + setup_tests, + args: ['--wayland', env_file, pid_file], + env: test_env, + priority: -20, + is_parallel: false, + protocol: 'exitcode' + ) + + test( + 'teardown-wayland', + teardown_tests, + args: [pid_file], + priority: -29, + is_parallel: false, + protocol: 'exitcode' + ) +endif kmxtest_files = run_command( find_tests, @@ -113,26 +136,28 @@ foreach kmx: kmxtest_files timeout: 120, protocol: 'tap', ) - test( - 'Wayland-' + testname + '__surrounding-text', - run_test, - args: [ '--wayland', '--surrounding-text', test_args], - env: test_env, - depends: [test_exe], - priority: -21, - is_parallel: false, - timeout: 120, - protocol: 'tap', - ) - test( - 'Wayland-' + testname + '__no-surrounding-text', - run_test, - args: [ '--wayland', '--no-surrounding-text', test_args], - env: test_env, - depends: [test_exe], - priority: -22, - is_parallel: false, - timeout: 120, - protocol: 'tap', - ) + if can_build_wayland + test( + 'Wayland-' + testname + '__surrounding-text', + run_test, + args: [ '--wayland', '--surrounding-text', test_args], + env: test_env, + depends: [test_exe], + priority: -21, + is_parallel: false, + timeout: 120, + protocol: 'tap', + ) + test( + 'Wayland-' + testname + '__no-surrounding-text', + run_test, + args: [ '--wayland', '--no-surrounding-text', test_args], + env: test_env, + depends: [test_exe], + priority: -22, + is_parallel: false, + timeout: 120, + protocol: 'tap', + ) + endif endforeach diff --git a/linux/ibus-keyman/tests/scripts/run-single-test.sh b/linux/ibus-keyman/tests/scripts/run-single-test.sh index 8e4e84f2cac5cd7aa91f51cb0307035a25934798..b4660f83689689ae42bcff83a9fcd7fb69a6cbef 100755 --- a/linux/ibus-keyman/tests/scripts/run-single-test.sh +++ b/linux/ibus-keyman/tests/scripts/run-single-test.sh @@ -2,15 +2,15 @@ set -eu TESTDIR=${XDG_DATA_HOME:-$HOME/.local/share}/keyman/test_kmx -. "$(dirname "$0")"/test-helper.sh +. "$(dirname "$0")"/test-helper.inc.sh if [ -v KEYMAN_PKG_BUILD ]; then # During package builds we skip these tests that require to start ibus because # ibus requires to find /var/lib/dbus/machine-id or /etc/machine-id, otherwise it fails with: # "Bail out! IBUS-FATAL-WARNING: Unable to load /var/lib/dbus/machine-id: Failed to open file # “/var/lib/dbus/machine-id”: No such file or directory" - echo "1..1" - echo "ok 1 - Integration tests # SKIP on package build" + echo "TAP version 14" + echo "1..0 # SKIP on package build" exit 0 fi @@ -38,10 +38,13 @@ function help() { } function run_tests() { - echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" - echo "" + # Output these lines to stderr - the first line on stdout has to be the TAP version number + # which running ${TESTFILE} outputs + echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" >&2 + echo "" >&2 + + echo "# Starting tests..." >&2 - echo "# Starting tests..." # Note: -k and --tap are consumed by the GLib testing framework # shellcheck disable=SC2086 "${G_TEST_BUILDDIR:-.}"/ibus-keyman-tests ${ARG_K-} ${ARG_TAP-} \ diff --git a/linux/ibus-keyman/tests/scripts/run-tests.sh b/linux/ibus-keyman/tests/scripts/run-tests.sh index 4c0934511f35324e88f67c9acf739642129b4517..ae58705f110b25ba089984d4e79f73ba6f14bd78 100755 --- a/linux/ibus-keyman/tests/scripts/run-tests.sh +++ b/linux/ibus-keyman/tests/scripts/run-tests.sh @@ -5,16 +5,20 @@ TOP_SRCDIR=${top_srcdir:-$(realpath "$(dirname "$0")/../..")} TESTBASEDIR=${XDG_DATA_HOME:-$HOME/.local/share}/keyman TESTDIR=${TESTBASEDIR}/test_kmx PID_FILE=/tmp/ibus-keyman-test-pids +ENV_FILE=/tmp/keyman-env.txt -. "$(dirname "$0")"/test-helper.sh +. "$(dirname "$0")"/test-helper.inc.sh + +local_cleanup() { + cleanup "$PID_FILE" +} if [ -v KEYMAN_PKG_BUILD ]; then # During package builds we skip these tests that require to start ibus because # ibus requires to find /var/lib/dbus/machine-id or /etc/machine-id, otherwise it fails with: # "Bail out! IBUS-FATAL-WARNING: Unable to load /var/lib/dbus/machine-id: Failed to open file # “/var/lib/dbus/machine-id”: No such file or directory" - echo "1..1" - echo "ok 1 - Integration tests # SKIP on package build" + echo "1..0 # SKIP on package build" exit 0 fi @@ -23,16 +27,6 @@ if ! which Xvfb > /dev/null || ! which Xephyr > /dev/null || ! which metacity > exit 1 fi -function cleanup() { - if [ -f "$PID_FILE" ]; then - echo - echo "# Shutting down processes..." - bash "$PID_FILE" > /dev/null 2>&1 - rm "$PID_FILE" - echo "# Finished shutdown of processes." - fi -} - function help() { echo "Usage:" echo " $0 [-k] [--tap] [--surrounding-text] [--no-surrounding-text] [--no-wayland] [--no-x11] [[--] TEST...]" @@ -57,68 +51,22 @@ function run_tests() { DISPLAY_SERVER=$1 shift - echo > "$PID_FILE" - TEMP_DATA_DIR=$(mktemp --directory) - echo "rm -rf ${TEMP_DATA_DIR} || true" >> "$PID_FILE" - - COMMON_ARCH_DIR= - [ -d "${TOP_SRCDIR}"/../../core/build/arch ] && COMMON_ARCH_DIR=${TOP_SRCDIR}/../../core/build/arch - - if [ -d "${COMMON_ARCH_DIR}"/release ]; then - COMMON_ARCH_DIR=${COMMON_ARCH_DIR}/release - elif [ -d "${COMMON_ARCH_DIR}"/debug ]; then - COMMON_ARCH_DIR=${COMMON_ARCH_DIR}/debug + if [ -d "$(dirname "$0")/../../../build/$(arch)/debug" ]; then + CONFIG=debug + elif [ -d "$(dirname "$0")/../../../build/$(arch)/release" ]; then + CONFIG=release else - echo "Can't find neither ${COMMON_ARCH_DIR}/release nor ${COMMON_ARCH_DIR}/debug" - exit 2 + echo "Cannot find ../../../build/$(arch)/debug or ../../../build/$(arch)/release" + exit 9 fi - link_test_keyboards "${TOP_SRCDIR}/../../common/test/keyboards/baseline" "$TESTDIR" "$TESTBASEDIR" + G_TEST_BUILDDIR="$(dirname "$0")/../../../build/$(arch)/${CONFIG}/tests" - generate_kmpjson "$TESTDIR" + setup "$DISPLAY_SERVER" "$ENV_FILE" "$PID_FILE" echo "# NOTE: When the tests fail check /tmp/ibus-engine-keyman.log and /tmp/ibus-daemon.log!" echo "" - if [ "$DISPLAY_SERVER" == "wayland" ]; then - if ! can_run_wayland; then - # support for --headless got added in mutter 40.x - echo "ERROR: mutter doesn't support running headless. Can't run Wayland tests." - exit 7 - fi - echo "# Running on Wayland..." - TMPFILE=$(mktemp) - # mutter-Message: 18:56:15.422: Using Wayland display name 'wayland-1' - mutter --wayland --headless --no-x11 --virtual-monitor 1024x768 &> "$TMPFILE" & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1s - export WAYLAND_DISPLAY - WAYLAND_DISPLAY=$(grep "Using Wayland display" "$TMPFILE" | cut -d"'" -f2) - rm "$TMPFILE" - else - echo "# Starting Xvfb..." - Xvfb -screen 0 1024x768x24 :33 &> /dev/null & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1 - echo "# Starting Xephyr..." - DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1 - echo "# Starting metacity" - metacity --display=:32 &> /dev/null & - echo "kill -9 $! || true" >> "$PID_FILE" - - export DISPLAY=:32 - fi - - # Install schema to temporary directory. This removes the build dependency on the keyman package. - SCHEMA_DIR=$TEMP_DATA_DIR/glib-2.0/schemas - export XDG_DATA_DIRS=$TEMP_DATA_DIR:$XDG_DATA_DIRS - - mkdir -p "$SCHEMA_DIR" - cp "${TOP_SRCDIR}"/../keyman-config/com.keyman.gschema.xml "$SCHEMA_DIR"/ - glib-compile-schemas "$SCHEMA_DIR" - if [ $# -gt 0 ]; then #shellcheck disable=SC2206 TESTFILES=($@) @@ -128,38 +76,11 @@ function run_tests() { popd > /dev/null || exit fi - export LD_LIBRARY_PATH=${COMMON_ARCH_DIR}/src:${LD_LIBRARY_PATH-} - - # Ubuntu 18.04 Bionic doesn't have ibus-memconf, and glib is not compiled with the keyfile - # backend enabled, so we just use the default backend. Otherwise we use the keyfile - # store which interferes less when running on a dev machine. - if [ -f /usr/libexec/ibus-memconf ]; then - export GSETTINGS_BACKEND=keyfile - IBUS_CONFIG=--config=/usr/libexec/ibus-memconf - fi - - #shellcheck disable=SC2086 - ibus-daemon "${ARG_VERBOSE-}" --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}"/test-ibus ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1s - - IBUS_ADDRESS=$(ibus address) - export IBUS_ADDRESS - - if [ -d "../../build/$(arch)/debug" ]; then - CONFIG=debug - elif [ -d "../../build/$(arch)/release" ]; then - CONFIG=release - else - echo "Can't find neither ../../build/$(arch)/debug nor ../../build/$(arch)/release" - exit 9 - fi - - "../../build/$(arch)/${CONFIG}/src/ibus-engine-keyman" "${ARG_VERBOSE-}" &> /tmp/ibus-engine-keyman.log & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1s - echo "# Starting tests..." + # shellcheck disable=SC1090 + source "$ENV_FILE" + echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" + # Note: -k and --tap are consumed by the GLib testing framework #shellcheck disable=SC2068 # we want to split array elements! #shellcheck disable=SC2086 @@ -168,7 +89,7 @@ function run_tests() { --directory "$TESTDIR" --"${DISPLAY_SERVER}" ${TESTFILES[@]} echo "# Finished tests." - cleanup + cleanup "$PID_FILE" } USE_WAYLAND=1 @@ -191,7 +112,7 @@ while (( $# )); do shift || (echo "Error: The last argument is missing a value. Exiting."; false) || exit 5 done -if ! can_run_wayland; then +if ! can_run_wayland && [ "$USE_WAYLAND" == "0" ]; then # support for --headless got added in mutter 40.x echo "# WARNING: mutter doesn't support running headless. Skipping Wayland tests." USE_WAYLAND=0 @@ -212,7 +133,7 @@ if [ ! -f "${G_TEST_BUILDDIR}/ibus-keyman-tests" ]; then fi echo > "$PID_FILE" -trap cleanup EXIT SIGINT +trap local_cleanup EXIT SIGINT if [ "$USE_WAYLAND" == "1" ]; then ( run_tests wayland "$@" ) diff --git a/linux/ibus-keyman/tests/scripts/setup-tests.sh b/linux/ibus-keyman/tests/scripts/setup-tests.sh index 62f6a978175f9ad020acf6f4d5235a6facf9083f..67bbcbb1bf565c87eb0f914959c9e7c34d0e6441 100755 --- a/linux/ibus-keyman/tests/scripts/setup-tests.sh +++ b/linux/ibus-keyman/tests/scripts/setup-tests.sh @@ -1,109 +1,8 @@ #!/usr/bin/env bash set -eu -DISPLAY_SERVER=$1 -ENV_FILE=$2 -PID_FILE=$3 -TOP_SRCDIR=${G_TEST_SRCDIR:-$(realpath "$(dirname "$0")/..")}/.. -TOP_BINDIR=${G_TEST_BUILDDIR:-$(realpath "$(dirname "$0/..")")}/.. -TESTBASEDIR=${XDG_DATA_HOME:-$HOME/.local/share}/keyman -TESTDIR=${TESTBASEDIR}/test_kmx +. "$(dirname "$0")/test-helper.inc.sh" -# shellcheck source=linux/ibus-keyman/tests/scripts/test-helper.sh -. "$(dirname "$0")"/test-helper.sh +exit_on_package_build -echo > "$ENV_FILE" - -if [ -f "$PID_FILE" ]; then - # kill previous instances - "$(dirname "$0")"/teardown-tests.sh "$PID_FILE" -fi - -echo > "$PID_FILE" -TEMP_DATA_DIR=$(mktemp --directory) -echo "rm -rf ${TEMP_DATA_DIR} || true" >> "$PID_FILE" - -COMMON_ARCH_DIR= -[ -d "${TOP_SRCDIR}"/../../core/build/arch ] && COMMON_ARCH_DIR=${TOP_SRCDIR}/../../core/build/arch - -if [ -d "${COMMON_ARCH_DIR}"/release ]; then - COMMON_ARCH_DIR=${COMMON_ARCH_DIR}/release -elif [ -d "${COMMON_ARCH_DIR}"/debug ]; then - COMMON_ARCH_DIR=${COMMON_ARCH_DIR}/debug -else - echo "Can't find neither ${COMMON_ARCH_DIR}/release nor ${COMMON_ARCH_DIR}/debug" - exit 2 -fi - -link_test_keyboards "${TOP_SRCDIR}/../../common/test/keyboards/baseline" "$TESTDIR" "$TESTBASEDIR" - -generate_kmpjson "$TESTDIR" - -if [ "$DISPLAY_SERVER" == "wayland" ]; then - if ! can_run_wayland; then - # support for --headless got added in mutter 40.x - echo "ERROR: mutter doesn't support running headless. Can't run Wayland tests." - exit 7 - fi - echo "Running on Wayland..." - TMPFILE=$(mktemp) - # mutter-Message: 18:56:15.422: Using Wayland display name 'wayland-1' - mutter --wayland --headless --no-x11 --virtual-monitor 1024x768 &> "$TMPFILE" & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1s - export WAYLAND_DISPLAY - WAYLAND_DISPLAY=$(grep "Using Wayland display" "$TMPFILE" | cut -d"'" -f2) - rm "$TMPFILE" - echo "export WAYLAND_DISPLAY=\"$WAYLAND_DISPLAY\"" >> "$ENV_FILE" -else - echo "Starting Xvfb..." - Xvfb -screen 0 1024x768x24 :33 &> /dev/null & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1 - echo "Starting Xephyr..." - DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null & - echo "kill -9 $! || true" >> "$PID_FILE" - sleep 1 - echo "Starting metacity" - metacity --display=:32 &> /dev/null & - echo "kill -9 $! || true" >> "$PID_FILE" - - export DISPLAY=:32 - echo "export DISPLAY=\"$DISPLAY\"" >> "$ENV_FILE" -fi - -# Install schema to temporary directory. This removes the build dependency on the keyman package. -SCHEMA_DIR=$TEMP_DATA_DIR/glib-2.0/schemas -export XDG_DATA_DIRS=$TEMP_DATA_DIR:${XDG_DATA_DIRS-} -echo "export XDG_DATA_DIRS=\"$XDG_DATA_DIRS\"" >> "$ENV_FILE" - -mkdir -p "$SCHEMA_DIR" -cp "${TOP_SRCDIR}"/../keyman-config/com.keyman.gschema.xml "$SCHEMA_DIR"/ -glib-compile-schemas "$SCHEMA_DIR" - -export LD_LIBRARY_PATH=${COMMON_ARCH_DIR}/src:${LD_LIBRARY_PATH-} -echo "export LD_LIBRARY_PATH=\"$LD_LIBRARY_PATH\"" >> "$ENV_FILE" - -# Ubuntu 18.04 Bionic doesn't have ibus-memconf, and glib is not compiled with the keyfile -# backend enabled, so we just use the default backend. Otherwise we use the keyfile -# store which interferes less when running on a dev machine. -if [ -f /usr/libexec/ibus-memconf ]; then - export GSETTINGS_BACKEND=keyfile - echo "export GSETTINGS_BACKEND=\"$GSETTINGS_BACKEND\"" >> "$ENV_FILE" - IBUS_CONFIG=--config=/usr/libexec/ibus-memconf -fi - -#shellcheck disable=SC2086 -ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log -echo "kill -9 $! || true" >> "$PID_FILE" -sleep 1s - -IBUS_ADDRESS=$(ibus address) -export IBUS_ADDRESS - -echo "export IBUS_ADDRESS=\"$IBUS_ADDRESS\"" >> "$ENV_FILE" - -#shellcheck disable=SC2086 -"${TOP_BINDIR}/src/ibus-engine-keyman" ${ARG_VERBOSE-} &> /tmp/ibus-engine-keyman.log & -echo "kill -9 $! || true" >> "$PID_FILE" -sleep 1s +setup "$1" "$2" "$3" diff --git a/linux/ibus-keyman/tests/scripts/teardown-tests.sh b/linux/ibus-keyman/tests/scripts/teardown-tests.sh index 45cfce605df00c9a2a885546373648809e2c637d..c74513f2818c3e6eafceccd99a05f03afabde906 100755 --- a/linux/ibus-keyman/tests/scripts/teardown-tests.sh +++ b/linux/ibus-keyman/tests/scripts/teardown-tests.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -eu -PID_FILE=$1 -echo "Shutting down processes..." -bash "$PID_FILE" > /dev/null 2>&1 -rm "$PID_FILE" -echo "Finished shutdown of processes." +. "$(dirname "$0")/test-helper.inc.sh" + +exit_on_package_build + +cleanup "$1" diff --git a/linux/ibus-keyman/tests/scripts/test-helper.inc.sh b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh new file mode 100755 index 0000000000000000000000000000000000000000..9ea5ffe3fc1aa4a5454655422f4c7435b240fe80 --- /dev/null +++ b/linux/ibus-keyman/tests/scripts/test-helper.inc.sh @@ -0,0 +1,293 @@ +#!/usr/bin/env bash + +function can_run_wayland() { + local MUTTER_VERSION + MUTTER_VERSION=$(mutter --version | head -1 | cut -f2 -d' ' | cut -f1 -d'.') + if (( MUTTER_VERSION < 40 )); then + return 1 + else + return 0 + fi +} + +function _generate_kmpjson() { + local TESTDIR + TESTDIR="$1" + pushd "$TESTDIR" > /dev/null || exit + KMPFILE="${TESTDIR}/kmp.json" + cat <<-EOF > "$KMPFILE" + { + "system": { + "keymanDeveloperVersion": "10.0.1099.0", + "fileVersion": "7.0" + }, + "info": { + "name": { + "description": "Test Keyboards" + }, + "version": { + "description": "0.0" + } + }, + "files": [ + { + "name": "kmp.json", + "description": "Package information (JSON)" + } +EOF + for f in k_*.kmx; do + keyboard=$(basename "$f") + keyboard="${keyboard%.*}" + echo " ," >> "$KMPFILE" + cat <<-EOF >> "$KMPFILE" + { + "name": "$keyboard", + "description": "$keyboard" + } +EOF + done + cat <<-EOF >> "$KMPFILE" + ], + "keyboards": [ +EOF + FIRST=true + for f in k_*.kmx; do + keyboard=$(basename "$f") + keyboard="${keyboard%.*}" + if $FIRST; then + FIRST=false + else + echo " ," >> "$KMPFILE" + fi + cat <<-EOF >> "$KMPFILE" + { + "name": "$keyboard", + "id": "$keyboard", + "version": "0.0", + "languages": [ + { + "name": "Undetermined", + "id": "und" + } + ] + } +EOF + done + echo " ]" >> "$KMPFILE" + echo "}" >> "$KMPFILE" + popd > /dev/null || exit +} + +function _link_test_keyboards() { + KMX_TEST_DIR=$1 + TESTDIR=$2 + TESTBASEDIR=$3 + + if [[ $(find "${KMX_TEST_DIR}/" -name k_\*.kmx 2>/dev/null | wc -l) -gt 0 ]]; then + mkdir -p "$(realpath --canonicalize-missing "$TESTBASEDIR")" + rm -f "$TESTDIR" + ln -sf "$(realpath "${KMX_TEST_DIR}")" "$TESTDIR" + else + echo "Can't find test kmx files in ${KMX_TEST_DIR}" + exit 3 + fi +} + +function _setup_init() { + local ENV_FILE PID_FILE + ENV_FILE=$1 + PID_FILE=$2 + + if [ -z "${TOP_SRCDIR:-}" ]; then + TOP_SRCDIR=${G_TEST_SRCDIR:-$(realpath "$(dirname "$0")/..")}/.. + fi + if [ -z "${TOP_BINDIR:-}" ]; then + TOP_BINDIR=${G_TEST_BUILDDIR:-$(realpath "$(dirname "$0/..")")}/.. + fi + + echo > "$ENV_FILE" + + if [ -f "$PID_FILE" ]; then + # kill previous instances + "$(dirname "$0")"/teardown-tests.sh "$PID_FILE" || true + fi + + echo > "$PID_FILE" + TEMP_DATA_DIR=$(mktemp --directory) + echo "rm -rf ${TEMP_DATA_DIR} || true" >> "$PID_FILE" + + COMMON_ARCH_DIR= + [ -d "${TOP_SRCDIR}"/../../core/build/arch ] && COMMON_ARCH_DIR=${TOP_SRCDIR}/../../core/build/arch + + if [ -d "${COMMON_ARCH_DIR}"/release ]; then + COMMON_ARCH_DIR=${COMMON_ARCH_DIR}/release + elif [ -d "${COMMON_ARCH_DIR}"/debug ]; then + COMMON_ARCH_DIR=${COMMON_ARCH_DIR}/debug + else + echo "Can't find neither ${COMMON_ARCH_DIR}/release nor ${COMMON_ARCH_DIR}/debug" + exit 2 + fi + + export LD_LIBRARY_PATH=${COMMON_ARCH_DIR}/src:${LD_LIBRARY_PATH-} + echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> "$ENV_FILE" +} + +function _setup_test_dbus_server() { + local ENV_FILE PID_FILE + ENV_FILE=$1 + PID_FILE=$2 + + # Start test dbus server. This will create `/tmp/km-test-server.env`. + "${TOP_BINDIR}/tests/km-dbus-test-server" &> /dev/null & + sleep 1 + + cat /tmp/km-test-server.env >> "$ENV_FILE" + cat /tmp/km-test-server.env >> "$PID_FILE" + echo "${TOP_BINDIR}/tests/stop-test-server" >> "$PID_FILE" + + source /tmp/km-test-server.env + echo "# DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" +} + +function _setup_display_server() { + local DISPLAY_SERVER ENV_FILE PID_FILE + ENV_FILE=$1 + PID_FILE=$2 + DISPLAY_SERVER=$3 + + if [ "$DISPLAY_SERVER" == "--wayland" ]; then + if ! can_run_wayland; then + # support for --headless got added in mutter 40.x + echo "ERROR: mutter doesn't support running headless. Can't run Wayland tests." + exit 7 + fi + echo "Running on Wayland:" + echo "Starting mutter..." + TMPFILE=$(mktemp) + # mutter-Message: 18:56:15.422: Using Wayland display name 'wayland-1' + mutter --wayland --headless --no-x11 --virtual-monitor 1024x768 &> "$TMPFILE" & + echo "kill -9 $! || true" >> "$PID_FILE" + sleep 1s + export WAYLAND_DISPLAY + WAYLAND_DISPLAY=$(grep "Using Wayland display" "$TMPFILE" | cut -d"'" -f2) + rm "$TMPFILE" + echo "export WAYLAND_DISPLAY=\"$WAYLAND_DISPLAY\"" >> "$ENV_FILE" + else + echo "Running on X11:" + echo "Starting Xvfb..." + Xvfb -screen 0 1024x768x24 :33 &> /dev/null & + echo "kill -9 $! || true" >> "$PID_FILE" + sleep 1 + echo "Starting Xephyr..." + DISPLAY=:33 Xephyr :32 -screen 1024x768 &> /dev/null & + echo "kill -9 $! || true" >> "$PID_FILE" + sleep 1 + echo "Starting metacity" + metacity --display=:32 &> /dev/null & + echo "kill -9 $! || true" >> "$PID_FILE" + + export DISPLAY=:32 + echo "export DISPLAY=\"$DISPLAY\"" >> "$ENV_FILE" + fi +} + +function _setup_schema_and_gsettings() { + local ENV_FILE PID_FILE + ENV_FILE=$1 + PID_FILE=$2 + + # Install schema to temporary directory. This removes the build dependency on the keyman package. + SCHEMA_DIR=$TEMP_DATA_DIR/glib-2.0/schemas + export XDG_DATA_DIRS=$TEMP_DATA_DIR:${XDG_DATA_DIRS-} + echo "export XDG_DATA_DIRS=$XDG_DATA_DIRS" >> "$ENV_FILE" + + export GSETTINGS_SCHEMA_DIR=${SCHEMA_DIR} + echo "export GSETTINGS_SCHEMA_DIR=\"$SCHEMA_DIR\"" >> "$ENV_FILE" + + mkdir -p "$SCHEMA_DIR" + cp "${TOP_SRCDIR}"/../keyman-config/resources/com.keyman.gschema.xml "$SCHEMA_DIR"/ + glib-compile-schemas "$SCHEMA_DIR" + + # Ubuntu 18.04 Bionic doesn't have ibus-memconf, and glib is not compiled with the keyfile + # backend enabled, so we just use the default backend. Otherwise we use the keyfile + # store which interferes less when running on a dev machine. + if [ -f /usr/libexec/ibus-memconf ]; then + export GSETTINGS_BACKEND=keyfile + echo "export GSETTINGS_BACKEND=\"$GSETTINGS_BACKEND\"" >> "$ENV_FILE" + IBUS_CONFIG=--config=/usr/libexec/ibus-memconf + fi +} + +function _setup_ibus() { + local ENV_FILE PID_FILE + ENV_FILE=$1 + PID_FILE=$2 + + #shellcheck disable=SC2086 + ibus-daemon ${ARG_VERBOSE-} --daemonize --panel=disable --address=unix:abstract="${TEMP_DATA_DIR}/test-ibus" ${IBUS_CONFIG-} &> /tmp/ibus-daemon.log + echo "kill -9 $! || true" >> "$PID_FILE" + sleep 1s + + IBUS_ADDRESS=$(ibus address) + export IBUS_ADDRESS + + echo "export IBUS_ADDRESS=\"$IBUS_ADDRESS\"" >> "$ENV_FILE" + + #shellcheck disable=SC2086 + "${TOP_BINDIR}/src/ibus-engine-keyman" --testing ${ARG_VERBOSE-} &> /tmp/ibus-engine-keyman.log & + echo "kill -9 $! || true" >> "$PID_FILE" + sleep 1s + +} +function setup() { + local DISPLAY_SERVER ENV_FILE PID_FILE TESTBASEDIR TESTDIR + DISPLAY_SERVER=$1 + ENV_FILE=$2 + PID_FILE=$3 + + _setup_init "${ENV_FILE}" "${PID_FILE}" + + TESTBASEDIR=${XDG_DATA_HOME:-$HOME/.local/share}/keyman + TESTDIR=${TESTBASEDIR}/test_kmx + + _link_test_keyboards "${TOP_SRCDIR}/../../common/test/keyboards/baseline" "$TESTDIR" "$TESTBASEDIR" + + _generate_kmpjson "$TESTDIR" + + _setup_test_dbus_server "${ENV_FILE}" "${PID_FILE}" + _setup_display_server "${ENV_FILE}" "${PID_FILE}" "${DISPLAY_SERVER}" + _setup_schema_and_gsettings "${ENV_FILE}" "${PID_FILE}" + _setup_ibus "${ENV_FILE}" "${PID_FILE}" +} + +function setup_display_server_only() { + local DISPLAY_SERVER ENV_FILE PID_FILE TESTBASEDIR TESTDIR + DISPLAY_SERVER=$1 + ENV_FILE=$2 + PID_FILE=$3 + + _setup_init "${ENV_FILE}" "${PID_FILE}" + _setup_display_server "${ENV_FILE}" "${PID_FILE}" "${DISPLAY_SERVER}" + _setup_schema_and_gsettings "${ENV_FILE}" "${PID_FILE}" +} + +function cleanup() { + local PID_FILE + PID_FILE=$1 + + if [ -f "$PID_FILE" ]; then + echo + echo "# Shutting down processes..." + bash "$PID_FILE" > /dev/null 2>&1 + rm "$PID_FILE" + echo "# Finished shutdown of processes." + fi +} + +function exit_on_package_build() { + if [ -v KEYMAN_PKG_BUILD ]; then + # Skip setup during package builds - can't run headless and we won't + # run the other tests anyway + exit 0 + fi +} diff --git a/linux/ibus-keyman/tests/scripts/test-helper.sh b/linux/ibus-keyman/tests/scripts/test-helper.sh deleted file mode 100755 index efee4c26f1c7019b2f7a3ec6bb901dcc3405f360..0000000000000000000000000000000000000000 --- a/linux/ibus-keyman/tests/scripts/test-helper.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash - -function can_run_wayland() { - local MUTTER_VERSION - MUTTER_VERSION=$(mutter --version | head -1 | cut -f2 -d' ' | cut -f1 -d'.') - if (( MUTTER_VERSION < 40 )); then - return 1 - else - return 0 - fi -} - -function generate_kmpjson() { - local TESTDIR - TESTDIR="$1" - pushd "$TESTDIR" > /dev/null || exit - KMPFILE="${TESTDIR}/kmp.json" - cat <<-EOF > "$KMPFILE" - { - "system": { - "keymanDeveloperVersion": "10.0.1099.0", - "fileVersion": "7.0" - }, - "info": { - "name": { - "description": "Test Keyboards" - }, - "version": { - "description": "0.0" - } - }, - "files": [ - { - "name": "kmp.json", - "description": "Package information (JSON)" - } -EOF - for f in k_*.kmx; do - keyboard=$(basename "$f") - keyboard="${keyboard%.*}" - echo " ," >> "$KMPFILE" - cat <<-EOF >> "$KMPFILE" - { - "name": "$keyboard", - "description": "$keyboard" - } -EOF - done - cat <<-EOF >> "$KMPFILE" - ], - "keyboards": [ -EOF - FIRST=true - for f in k_*.kmx; do - keyboard=$(basename "$f") - keyboard="${keyboard%.*}" - if $FIRST; then - FIRST=false - else - echo " ," >> "$KMPFILE" - fi - cat <<-EOF >> "$KMPFILE" - { - "name": "$keyboard", - "id": "$keyboard", - "version": "0.0", - "languages": [ - { - "name": "Undetermined", - "id": "und" - } - ] - } -EOF - done - echo " ]" >> "$KMPFILE" - echo "}" >> "$KMPFILE" - popd > /dev/null || exit -} - -function link_test_keyboards() { - KMX_TEST_DIR=$1 - TESTDIR=$2 - TESTBASEDIR=$3 - - if [[ $(find "${KMX_TEST_DIR}/" -name k_\*.kmx 2>/dev/null | wc -l) -gt 0 ]]; then - mkdir -p "$(realpath --canonicalize-missing "$TESTBASEDIR")" - rm -f "$TESTDIR" - ln -sf "$(realpath "${KMX_TEST_DIR}")" "$TESTDIR" - else - echo "Can't find test kmx files in ${KMX_TEST_DIR}" - exit 3 - fi -} diff --git a/linux/ibus-keyman/tests/testfixture.cpp b/linux/ibus-keyman/tests/testfixture.cpp index 3e189258bdb11e19c6ba9b69aea1982473572163..84aab933c836a7fde6518be66a77ddbef1bddada 100644 --- a/linux/ibus-keyman/tests/testfixture.cpp +++ b/linux/ibus-keyman/tests/testfixture.cpp @@ -13,17 +13,10 @@ #include "ibusimcontext.h" #include "keycodes.h" #include "keymanutil.h" +#include "KeymanSystemServiceClient.h" #include "kmx_test_source.hpp" #include "testmodule.h" -#ifdef GDK_WINDOWING_X11 -#include -#include -#endif -#ifdef GDK_WINDOWING_WAYLAND -#include -#endif - typedef struct { IBusBus *bus; GtkIMContext *context; @@ -42,12 +35,7 @@ static gboolean use_wayland = FALSE; static GdkWindow *window = NULL; static GMainLoop *thread_loop = NULL; static GTypeModule *module = NULL; -#ifdef GDK_WINDOWING_X11 -static Display *display = NULL; -#endif -#ifdef GDK_WINDOWING_WAYLAND -static GdkWaylandDisplay *wldisplay = NULL; -#endif +gboolean testing = TRUE; static void module_register(GTypeModule *module) { @@ -69,18 +57,6 @@ ibus_keyman_tests_fixture_set_up(IBusKeymanTestsFixture *fixture, gconstpointer window = gtk_widget_get_window(widget); } - GdkDisplay *gdkDisplay = gdk_display_get_default(); -#ifdef GDK_WINDOWING_X11 - if (!use_wayland && !display && GDK_IS_X11_DISPLAY(gdkDisplay)) { - display = GDK_DISPLAY_XDISPLAY(gdkDisplay); - } -#endif -#ifdef GDK_WINDOWING_WAYLAND - if (use_wayland && !wldisplay && GDK_IS_WAYLAND_DISPLAY(gdkDisplay)) { - wldisplay = GDK_WAYLAND_DISPLAY(gdkDisplay); - } -#endif - if (!loaded) { ibus_init(); @@ -114,35 +90,12 @@ ibus_keyman_tests_fixture_tear_down(IBusKeymanTestsFixture *fixture, gconstpoint static void set_caps_lock_state(IBusKeymanTestsFixture *fixture, bool caps_lock_on) { -#ifdef GDK_WINDOWING_X11 - if (!use_wayland && display) { - XkbLockModifiers(display, XkbUseCoreKbd, LockMask, caps_lock_on ? LockMask : 0); - XSync(display, False); - } -#endif -#ifdef GDK_WINDOWING_WAYLAND - if (use_wayland && wldisplay) { - // TODO - } -#endif + set_capslock_indicator((guint32)caps_lock_on); } static bool get_caps_lock_state(IBusKeymanTestsFixture *fixture) { -#ifdef GDK_WINDOWING_X11 - if (!use_wayland && display) { - XKeyboardState state; - XGetKeyboardControl(display, &state); - return state.led_mask & 1; - } -#endif -#ifdef GDK_WINDOWING_WAYLAND - if (use_wayland && wldisplay) { - // TODO - } -#endif - - return false; + return get_capslock_indicator(); } static void @@ -493,23 +446,6 @@ main(int argc, char *argv[]) { return 2; } - if (!argWayland && !argX11) { - GdkDisplay *gdkDisplay = gdk_display_get_default(); -#ifdef GDK_WINDOWING_WAYLAND - if (GDK_IS_WAYLAND_DISPLAY(gdkDisplay)) { - argWayland = true; - } else -#endif -#ifdef GDK_WINDOWING_X11 - if (GDK_IS_X11_DISPLAY(gdkDisplay)) { - argX11 = true; - } else -#endif - { - g_error("Neither X11 nor Wayland display!"); - } - } - use_wayland = argWayland; int nTests = argc - iArg; char **tests = &argv[iArg]; @@ -528,13 +464,6 @@ main(int argc, char *argv[]) { skipReason = "mnemonic keyboards are not yet supported on Linux (#3345)"; } - // Wayland related tests - #4273 - if (use_wayland && ( - strcmp(filename->str, "k_031___caps_lock") == 0 || - strcmp(filename->str, "k_032___caps_control") == 0)) { - skipReason = "capslock related keyboards are not yet working with Wayland (#4273)"; - } - if (runSurroundingTextTests) { add_test(directory, filename->str, TRUE, skipReason, use_wayland); } @@ -549,20 +478,6 @@ main(int argc, char *argv[]) { // Run tests int retVal = g_test_run(); - // Cleanup -#ifdef GDK_WINDOWING_X11 - if (!use_wayland && display) { - XCloseDisplay(display); - display = NULL; - } -#endif -#ifdef GDK_WINDOWING_WAYLAND - if (use_wayland) { - // REVIEW: do we have to call something for cleanup? - wldisplay = NULL; - } -#endif - test_module_unuse(module); return retVal; } diff --git a/linux/keyman-config/MANIFEST.in b/linux/keyman-config/MANIFEST.in index e27020f36c9b8a46396a1ff85d8ce50bfd8e5e3f..20705c6fc3e973a6e504395b04e1eeaeeacf9ac6 100644 --- a/linux/keyman-config/MANIFEST.in +++ b/linux/keyman-config/MANIFEST.in @@ -2,4 +2,4 @@ recursive-include keyman_config/icons * recursive-include icons * include README.md include *.bash-completion -include *.gschema.xml +include resources/*.gschema.xml diff --git a/linux/keyman-config/Makefile b/linux/keyman-config/Makefile index 41eaab30efb727be2c2e527059e32a1f2d7aa9e4..f057dcad0ce8c0f81e2e82af0014c99e429a41f8 100644 --- a/linux/keyman-config/Makefile +++ b/linux/keyman-config/Makefile @@ -1,49 +1,19 @@ #!/usr/bin/make -default: clean version man langtags - python3 setup.py build - -langtags: - cd buildtools && python3 ./build-langtags.py +default: + ./build.sh clean build install: - if [ -n "${SUDO_USER}" ]; then \ - make install-sudo; \ - else \ - make install-temp; \ - fi - -install-sudo: # run as sudo - pip3 install qrcode sentry-sdk - # eventually change this to: pip3 install . - python3 setup.py install - # install icons - mkdir -p /usr/local/share/keyman/icons - cp keyman_config/icons/* /usr/local/share/keyman/icons - # install man pages - mkdir -p /usr/local/share/man/man1 - cp ../../debian/man/*.1 /usr/local/share/man/man1 - -install-temp: - mkdir -p /tmp/keyman/$(shell python3 -c 'import sys;import os;pythonver="python%d.%d" % (sys.version_info[0], sys.version_info[1]);sitedir = os.path.join("lib", pythonver, "site-packages");print(sitedir)') - # when we no longer have to support old pip version (python > 3.6) change this to: - # pip3 install --prefix /tmp/keyman . - PYTHONUSERBASE=/tmp/keyman python3 setup.py install --user - -uninstall: clean # run as sudo - rm -rf /usr/local/share/keyman/icons - rm -f /usr/local/share/man/man1/km-*.1 - pip3 uninstall keyman_config - rm -f /usr/local/bin/km-config - rm -f /usr/local/bin/km-kvk2ldml - rm -f /usr/local/bin/km-package-get - rm -f /usr/local/bin/km-package-install - rm -f /usr/local/bin/km-package-list-installed - rm -f /usr/local/bin/km-package-uninstall + ./build.sh install + +uninstall: # run as sudo + ./build.sh uninstall clean: - -rm -rf dist make_deb build keyman_config/version.py *.egg-info __pycache__ \ - keyman_config/standards/lang_tags_map.py + ./build.sh clean + +check: + ./build.sh test devdist: version python3 setup.py egg_info -b.`TZ=UTC git log -1 --pretty=format:%cd --date=format-local:%Y%m%d%H%M` sdist @@ -93,6 +63,3 @@ $(MOFILES): %/LC_MESSAGES/keyman-config.mo: %.po msgfmt -v --check --output-file=$@ $^ compile-po: $(MOFILES) - -check: - ./run-tests.sh diff --git a/linux/keyman-config/build-help.sh b/linux/keyman-config/build-help.sh index 97f06ca3d0012649012eaa781a061f46000331ed..e05b8d284e4e90677ae22905c52de2e3fb769b9f 100755 --- a/linux/keyman-config/build-help.sh +++ b/linux/keyman-config/build-help.sh @@ -3,13 +3,11 @@ set -e set -u -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -## END STANDARD BUILD SCRIPT INCLUDE - THIS_DIR=$(dirname "$THIS_SCRIPT") +cd "${THIS_DIR}" + function display_usage { echo "Usage: $0 [--man] [--md] [--no-reconf]" echo " $0 --help" @@ -50,13 +48,13 @@ while [[ $# -gt 0 ]] ; do shift # past the processed argument done -if [ -z "$generate_man" -a -z "$generate_help" ]; then +if [ -z "$generate_man" ] && [ -z "$generate_help" ]; then generate_man=1 generate_help=1 fi if [ -n "$reconfigure" ]; then - pushd $THIS_DIR/.. > /dev/null + pushd "$THIS_DIR/.." > /dev/null ./scripts/reconf.sh popd > /dev/null fi diff --git a/linux/keyman-config/build.sh b/linux/keyman-config/build.sh index 38cf22e917b40ab8a37c60b15a5748681bc5c66e..e96d4b0d5dd37d424e6453c2e42f9ffa1cdd018f 100755 --- a/linux/keyman-config/build.sh +++ b/linux/keyman-config/build.sh @@ -16,6 +16,7 @@ builder_describe \ "test" \ "install install artifacts" \ "uninstall uninstall artifacts" \ + "--no-integration don't run integration tests" builder_parse "$@" @@ -24,9 +25,100 @@ cd "$THIS_SCRIPT_PATH" builder_describe_outputs \ build "/linux/keyman-config/keyman_config/standards/lang_tags_map.py" -builder_run_action clean make clean +clean_action() { + rm -rf dist make_deb build keyman_config/version.py ./*.egg-info __pycache__ \ + keyman_config/standards/lang_tags_map.py +} + +execute_with_temp_schema() { + local TEMP_DATA_DIR SCHEMA_DIR + TEMP_DATA_DIR=$(mktemp -d) + SCHEMA_DIR="${TEMP_DATA_DIR}/glib-2.0/schemas" + export XDG_DATA_DIRS="${TEMP_DATA_DIR}":${XDG_DATA_DIRS-} + export GSETTINGS_SCHEMA_DIR="${SCHEMA_DIR}" + mkdir -p "${SCHEMA_DIR}" + cp resources/com.keyman.gschema.xml "${SCHEMA_DIR}"/ + glib-compile-schemas "${SCHEMA_DIR}" + "$@" + export XDG_DATA_DIRS=${XDG_DATA_DIRS#*:} + unset GSETTINGS_SCHEMA_DIR + rm -rf "${TEMP_DATA_DIR}" +} + +build_man_and_help_pages() { + execute_with_temp_schema ./build-help.sh --no-reconf +} + +build_action() { + builder_echo "Create version.py" + pushd keyman_config + sed \ + -e "s/_VERSION_/${VERSION}/g" \ + -e "s/_VERSIONWITHTAG_/${VERSION_WITH_TAG}/g" \ + -e "s/_VERSIONGITTAG_/${VERSION_GIT_TAG}/g" \ + -e "s/_MAJORVERSION_/${VERSION_MAJOR}/g" \ + -e "s/_RELEASEVERSION_/${VERSION_RELEASE}/g" \ + -e "s/_TIER_/${TIER}/g" \ + -e "s/_ENVIRONMENT_/${VERSION_ENVIRONMENT}/g" \ + -e "s/_UPLOADSENTRY_/${UPLOAD_SENTRY}/g" \ + version.py.in > version.py + popd + pushd buildtools + if [ -f build-langtags.py ]; then + builder_echo "Create lang_tags_map.py" + python3 ./build-langtags.py + else + builder_echo "Skip building lang_tags_map.py during package build" + fi + popd + builder_echo "Building man and help pages" + build_man_and_help_pages + builder_echo "Building keyman-config" + python3 setup.py build +} + +test_action() { + execute_with_temp_schema ./run-tests.sh +} + +install_action() { + if [ -n "${SUDO_USER:-}" ]; then + # with sudo install into /usr/local + pip3 install qrcode sentry-sdk + # eventually change this to: pip3 install . + python3 setup.py install + # install icons + mkdir -p /usr/local/share/keyman/icons + cp keyman_config/icons/* /usr/local/share/keyman/icons + # install man pages + mkdir -p /usr/local/share/man/man1 + cp ../../debian/man/*.1 /usr/local/share/man/man1 + else + # without sudo install into /tmp/keyman (or $DESTDIR) + mkdir -p "/tmp/keyman/$(python3 -c 'import sys;import os;pythonver="python%d.%d" % (sys.version_info[0], sys.version_info[1]);sitedir = os.path.join("lib", pythonver, "site-packages");print(sitedir)')" + # when we no longer have to support old pip version (python > 3.6) change this to: + # pip3 install --prefix /tmp/keyman . + PYTHONUSERBASE=${DESTDIR:-/tmp/keyman} python3 setup.py install --user + fi +} + +uninstall_action() { + # run as sudo + clean_action + rm -rf /usr/local/share/keyman/icons + rm -f /usr/local/share/man/man1/km-*.1 + pip3 uninstall keyman_config + rm -f /usr/local/bin/km-config + rm -f /usr/local/bin/km-kvk2ldml + rm -f /usr/local/bin/km-package-get + rm -f /usr/local/bin/km-package-install + rm -f /usr/local/bin/km-package-list-installed + rm -f /usr/local/bin/km-package-uninstall +} + +builder_run_action clean clean_action builder_run_action configure # nothing to do -builder_run_action build make -builder_run_action test ./run-tests.sh -builder_run_action install make install -builder_run_action uninstall make uninstall +builder_run_action build build_action +builder_run_action test test_action +builder_run_action install install_action +builder_run_action uninstall uninstall_action diff --git a/linux/keyman-config/createkeymandirs.sh b/linux/keyman-config/createkeymandirs.sh deleted file mode 100755 index d9befe7f5d505992f4ed0383abb2a0e587a7b5fa..0000000000000000000000000000000000000000 --- a/linux/keyman-config/createkeymandirs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -sudo mkdir -p /usr/local/share/keyman -sudo mkdir -p /usr/local/share/fonts/keyman -sudo mkdir -p /usr/local/share/doc/keyman -sudo chmod 00777 /usr/local/share/keyman -sudo chmod 00777 /usr/local/share/fonts/keyman -sudo chmod 00777 /usr/local/share/doc/keyman diff --git a/linux/keyman-config/keyman_config/__init__.py b/linux/keyman-config/keyman_config/__init__.py index e5cecf4cc82d6b6ae333dab67dd5aa615454b3e2..bc015b3aa6529ed9d5a29c6ccdef2520346a469c 100644 --- a/linux/keyman-config/keyman_config/__init__.py +++ b/linux/keyman-config/keyman_config/__init__.py @@ -1,20 +1,18 @@ -import getpass import gettext -import importlib import logging -import os -import platform -import sys -from .version import __version__ -from .version import __versionwithtag__ -from .version import __versiongittag__ -from .version import __majorversion__ -from .version import __releaseversion__ -from .version import __tier__ -from .version import __pkgversion__ -from .version import __environment__ -from .version import __uploadsentry__ +from keyman_config.sentry_handling import SentryErrorHandling +from keyman_config.version import ( + __version__, + __versionwithtag__, + __versiongittag__, + __majorversion__, + __releaseversion__, + __tier__, + __pkgversion__, + __environment__, + __uploadsentry__ +) def _(txt): @@ -24,7 +22,7 @@ def _(txt): return translation -def secure_lookup(data, key1, key2 = None): +def secure_lookup(data, key1, key2=None): """ Return data[key1][key2] while dealing with data being None or key1 or key2 not existing """ @@ -38,18 +36,34 @@ def secure_lookup(data, key1, key2 = None): return None -def before_send(event, hint): - if 'exc_info' in hint: - exc_type, exc_value, tb = hint['exc_info'] - if isinstance(exc_value, KeyboardInterrupt): - # Ignore KeyboardInterrupt exception - return None - return event +def initialize_logging(args): + if args.verbose: + logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') + elif args.veryverbose: + logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') + else: + logging.basicConfig(format='%(levelname)s:%(message)s') + + +def initialize_sentry(): + SentryErrorHandling().initialize_sentry() + + +def add_standard_arguments(parser): + if __pkgversion__: + versionstring = f"{__versionwithtag__} (package version {__pkgversion__})" + else: + versionstring = f"{__versionwithtag__}" + + parser.add_argument('--version', action='version', version=f'%(prog)s version {versionstring}') + parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') + parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') gettext.bindtextdomain('keyman-config', '/usr/share/locale') gettext.textdomain('keyman-config') + #if __tier__ == 'alpha' or __tier__ == 'beta': // #7227 disabling: # Alpha and beta versions will work against the staging server so that they # can access new APIs etc that will only be available there. The staging @@ -60,60 +74,6 @@ gettext.textdomain('keyman-config') KeymanComUrl = 'https://keyman.com' KeymanApiUrl = 'https://api.keyman.com' + # There's no staging site for downloads KeymanDownloadsUrl = 'https://downloads.keyman.com' - -if 'unittest' in sys.modules.keys(): - print('Not reporting to Sentry', file=sys.stderr) -elif os.environ.get('KEYMAN_NOSENTRY'): - print('Not reporting to Sentry because KEYMAN_NOSENTRY environment variable set', file=sys.stderr) -elif not __uploadsentry__: - print('Not reporting to Sentry because UPLOAD_SENTRY is false (%s)' % __environment__, file=sys.stderr) -else: - try: - # Try new sentry-sdk first - sentry_sdk = importlib.import_module('sentry_sdk') - from sentry_sdk import configure_scope, set_user - from sentry_sdk.integrations.logging import LoggingIntegration - HaveSentryNewSdk = True - - sentry_logging = LoggingIntegration( - level=logging.INFO, # Capture info and above as breadcrumbs - event_level=logging.CRITICAL # Send critical errors as events - ) - SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357@o1005580.ingest.sentry.io/5983525" - sentry_sdk.init( - dsn=SentryUrl, - environment=__environment__, - release=__versiongittag__, - integrations=[sentry_logging], - before_send=before_send - ) - set_user({'id': hash(getpass.getuser())}) - with configure_scope() as scope: - scope.set_tag("app", os.path.basename(sys.argv[0])) - scope.set_tag("pkgversion", __pkgversion__) - scope.set_tag("platform", platform.platform()) - scope.set_tag("system", platform.system()) - scope.set_tag("tier", __tier__) - except ImportError: - try: - # sentry-sdk is not available, so use older raven - raven = importlib.import_module('raven') - from raven import Client - HaveSentryNewSdk = False - - # Note, legacy raven API requires secret (https://github.com/keymanapp/keyman/pull/5787#discussion_r721457909) - SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357:e6d5a81ee6944fc79bd9f0cbb1f2c2a4@o1005580.ingest.sentry.io/5983525" - client = Client(SentryUrl, environment=__environment__, release=__versiongittag__) - client.user_context({'id': hash(getpass.getuser())}) - client.tags_context({ - 'app': os.path.basename(sys.argv[0]), - 'pkgversion': __pkgversion__, - 'platform': platform.platform(), - 'system': platform.system(), - 'tier': __tier__, - }) - except ImportError: - # even raven is not available. This is the case on Ubuntu 16.04. Just ignore. - print(_('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.'), file=sys.stderr) diff --git a/linux/keyman-config/keyman_config/convertico.py b/linux/keyman-config/keyman_config/convertico.py index 1a836f9610cc7ce7a9139bf76eddf0d3aba757a4..fa6f7b39abda168dc398579077533e83d9a87443 100755 --- a/linux/keyman-config/keyman_config/convertico.py +++ b/linux/keyman-config/keyman_config/convertico.py @@ -8,7 +8,6 @@ import sys import numpy as np from PIL import Image, ImageFile -Image.LOAD_TRUNCATED_IMAGES = True ImageFile.LOAD_TRUNCATED_IMAGES = True @@ -20,8 +19,7 @@ def changeblacktowhite(im): white_areas = (red == 0) & (blue == 0) & (green == 0) data[..., :-1][white_areas.T] = (255, 255, 255) # Transpose back needed - im2 = Image.fromarray(data) - return im2 + return Image.fromarray(data) def checkandsaveico(icofile): @@ -35,32 +33,34 @@ def checkandsaveico(icofile): icofile (str): path to ico file """ name, ext = os.path.splitext(icofile) - bmpfile = name + ".bmp" + bmpfile = f"{name}.bmp" if ext == '.ico': - im = Image.open(icofile) - im = im.convert('RGBA') - im2 = im - num, colour = max(im.getcolors(im.size[0] * im.size[1])) - logging.debug("checkandsaveico maxcolour: num {0}: colour {1}".format(num, colour)) - if num > 160 and colour == (0, 0, 0, 0): - logging.info("checkandsaveico:" + icofile + " mostly black so changing black to white") - im2 = changeblacktowhite(im) - im2.save(bmpfile) - + _convert_ico_to_bmp(icofile, bmpfile) try: - im3 = Image.open(bmpfile) - im4 = im3.resize([64, 64], Image.ANTIALIAS) - # Using .bmp.png file extension so it won't conflict if the package already contains .png - im4.save(bmpfile + '.png', 'png') + with Image.open(bmpfile) as im3: + with im3.resize((64, 64), Image.LANCZOS) as im4: + # Using .bmp.png file extension so it won't conflict if the package already contains .png + im4.save(f'{bmpfile}.png', 'png') except (IOError, OSError): logging.error("Cannot convert %s to png", icofile) - pass finally: # Clean up intermediary .bmp file if it was generated if ext == '.ico': os.remove(bmpfile) +def _convert_ico_to_bmp(icofile, bmpfile): + with Image.open(icofile) as im: + with im.convert('RGBA') as im2: + num, colour = max(im.getcolors(im2.size[0] * im2.size[1])) + logging.debug(f"checkandsaveico maxcolour: num {num}: colour {colour}") + if num > 160 and colour == (0, 0, 0, 0): + logging.info(f"checkandsaveico:{icofile} mostly black so changing black to white") + im2.close() + im2 = changeblacktowhite(im) + im2.save(bmpfile) + + def extractico(kmxfile): """ Extract icon file from compiled kmx keyboard @@ -95,9 +95,9 @@ def extractico(kmxfile): # Read first two bytes to determine if icon is .bmp or .ico if bitmap.startswith(b'BM'): - imagefilename = imagefilename + ".bmp" + imagefilename = f"{imagefilename}.bmp" else: - imagefilename = imagefilename + ".ico" + imagefilename = f"{imagefilename}.ico" try: with open(imagefilename, mode='wb') as imagefile: diff --git a/linux/keyman-config/keyman_config/dconf_util.py b/linux/keyman-config/keyman_config/dconf_util.py index 09b95d1815a5f42021674cba68b40a6c2f7887fa..a2775a45e12d628d4238a27ad523791c2054807f 100644 --- a/linux/keyman-config/keyman_config/dconf_util.py +++ b/linux/keyman-config/keyman_config/dconf_util.py @@ -2,25 +2,25 @@ from gi.repository import Gio -# DConf path destkop/ibus/keyman/options -DCONF_BASE = "com.keyman.options" +# GSettings path destkop/ibus/keyman/options +GSETTINGS_BASE = "com.keyman.options" -# Utilities to get and set Keyman options in DConf: +# Utilities to get and set Keyman options in GSettings: # /desktop/ibus/keyman/options/packageID/keyboardID/options def get_child_schema(info): - settings = Gio.Settings.new(DCONF_BASE) + settings = Gio.Settings.new(GSETTINGS_BASE) path = settings.get_property('path') if not path.endswith('/'): path += '/' path += info['packageID'] + '/' + info['keyboardID'] + '/' - return Gio.Settings(DCONF_BASE + '.child', path) + return Gio.Settings(f'{GSETTINGS_BASE}.child', path) def get_option(info): """ - Get the Keyman keyboard options from DConf + Get the Keyman keyboard options from GSettings Convert from list of comma-separated strings into dictionary Args: @@ -41,7 +41,7 @@ def get_option(info): def set_option(info, options): """ - Store the Keyman keyboard options in DConf as a list of strings + Store the Keyman keyboard options in GSettings as a list of strings Args: info: dictionary @@ -51,11 +51,8 @@ def set_option(info, options): key and values to store """ if "packageID" in info and "keyboardID" in info and options: - # Convert dictionary of options into a list of comma-separated option strings - list_options = [] - for key, value in options.items(): - list_options.append(key + "=" + value) - + # Convert dictionary of options into a list of option strings + list_options = [f"{key}={value}" for key, value in options.items()] child_schema = get_child_schema(info) child_schema.set_strv("options", list_options) diff --git a/linux/keyman-config/keyman_config/get_kmp.py b/linux/keyman-config/keyman_config/get_kmp.py index af7b34063d3336fd2c451c4ddddbdcf4073a1570..ed3f7ec8a444fdc178238f495f9ef29ca05f41f9 100755 --- a/linux/keyman-config/keyman_config/get_kmp.py +++ b/linux/keyman-config/keyman_config/get_kmp.py @@ -9,6 +9,7 @@ import requests import requests_cache from gi.repository import GObject +from keyman_config import _ from keyman_config import KeymanApiUrl, KeymanDownloadsUrl from keyman_config.deprecated_decorator import deprecated @@ -20,6 +21,16 @@ class InstallLocation(GObject.GEnum): Unknown = 99 +def get_install_area_string(area): + if area == InstallLocation.OS: + return _('System') + elif area == InstallLocation.Shared: + return _('Shared') + elif area == InstallLocation.User: + return _('User') + return _('Unknown') + + def get_package_download_data(packageID, weekCache=False): """ Get package download data from keyboards download api. diff --git a/linux/keyman-config/keyman_config/gnome_keyboards_util.py b/linux/keyman-config/keyman_config/gnome_keyboards_util.py index 4b0d5cfbca41c6504a5bab5b08cb47da8cf99c43..5c2eca6dfc16e7995ba36ec0f1fcb8ab7a66e275 100644 --- a/linux/keyman-config/keyman_config/gnome_keyboards_util.py +++ b/linux/keyman-config/keyman_config/gnome_keyboards_util.py @@ -48,8 +48,8 @@ def get_ibus_keyboard_id(keyboard, packageDir, language=None, ignore_language=Fa return kmx_file if language is not None and language != '': logging.debug(language) - return "%s:%s" % (language, kmx_file) + return f"{language}:{kmx_file}" if "languages" in keyboard and len(keyboard["languages"]) > 0: logging.debug(keyboard["languages"][0]) - return "%s:%s" % (keyboard["languages"][0]['id'], kmx_file) + return f"{keyboard['languages'][0]['id']}:{kmx_file}" return kmx_file diff --git a/linux/keyman-config/keyman_config/gsettings.py b/linux/keyman-config/keyman_config/gsettings.py index 151f9b56f7d30b791e9dcb9c4d6e09a022439ec8..cdba58e446a6ead439b9959433212c5fff216c6e 100644 --- a/linux/keyman-config/keyman_config/gsettings.py +++ b/linux/keyman-config/keyman_config/gsettings.py @@ -4,10 +4,9 @@ import os import subprocess import sys -from gi.repository import Gio # needs to come before gi.overrides.GLib! +from gi.repository import Gio # needs to come before gi.overrides.GLib! from gi.overrides.GLib import Variant - class GSettings(): def __init__(self, schema_id): """ @@ -29,7 +28,7 @@ class GSettings(): if variant.get_type_string() == 'as': return variant.get_strv() - assert(variant.get_type_string() == 'a(ss)') + assert variant.get_type_string() == 'a(ss)' values = [] # Process variant of type "a(ss)" (array of tuples with two strings) @@ -38,10 +37,8 @@ class GSettings(): # Process variant of type "(ss)" (tuple with two strings) val = variant.get_child_value(i) typeVariant = val.get_child_value(0) - type = typeVariant.get_string() idVariant = val.get_child_value(1) - id = idVariant.get_string() - values.append((type, id)) + values.append((typeVariant.get_string(), idVariant.get_string())) return values def _convert_array_to_variant(self, array, type): @@ -51,7 +48,7 @@ class GSettings(): if type == 'as': return Variant('as', array) - assert(type == 'a(ss)') + assert type == 'a(ss)' children = [] for (type, id) in array: @@ -64,14 +61,14 @@ class GSettings(): def get(self, key): if self.is_sudo: args = ['sudo', '-H', '-u', os.environ.get('SUDO_USER'), - 'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%s/bus' % os.environ.get('SUDO_UID'), + f"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{os.environ.get('SUDO_UID')}/bus", 'gsettings', 'get', self.schema_id, key] if sys.version_info.major <= 3 and sys.version_info.minor < 7: # capture_output got added in Python 3.7 try: output = subprocess.check_output(args) value = eval(output) - except(subprocess.CalledProcessError): + except subprocess.CalledProcessError: value = None logging.warning('Could not convert to sources') else: @@ -86,12 +83,12 @@ class GSettings(): value = self._convert_variant_to_array(variant) return value - def set(self, key, value, type): + def set(self, key, value, type) -> None: if self.is_sudo: variant = str(value) subprocess.run( ['sudo', '-H', '-u', os.environ.get('SUDO_USER'), - 'DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%s/bus' % os.environ.get('SUDO_UID'), + f"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{os.environ.get('SUDO_UID')}/bus", 'gsettings', 'set', self.schema_id, key, variant]) else: variant = self._convert_array_to_variant(value, type) diff --git a/linux/keyman-config/keyman_config/handle_install.py b/linux/keyman-config/keyman_config/handle_install.py index 4938f23d413a7461227225e2a1d338e67f975073..f551a63dd626c1cae08c3a6f5767cbdfe56c8d7a 100644 --- a/linux/keyman-config/keyman_config/handle_install.py +++ b/linux/keyman-config/keyman_config/handle_install.py @@ -22,9 +22,9 @@ def download_and_install_package(url): bcp47 = _extract_bcp47(parsedUrl.query) if parsedUrl.scheme == 'keyman': - logging.info("downloading " + url) + logging.info(f"downloading {url}") if not url.startswith('keyman://download/keyboard/'): - logging.error("Don't know what to do with URL " + url) + logging.error(f"Don't know what to do with URL {url}") return packageId = parsedUrl.path[len('/keyboard/'):] @@ -33,22 +33,22 @@ def download_and_install_package(url): return downloadFile = os.path.join(get_download_folder(), packageId) - downloadUrl = KeymanComUrl + '/go/package/download/' + packageId + '?platform=linux&tier=' + __tier__ + downloadUrl = f'{KeymanComUrl}/go/package/download/{packageId}?platform=linux&tier={__tier__}' packageFile = download_kmp_file(downloadUrl, downloadFile) if packageFile is None: return - elif parsedUrl.scheme == '' or parsedUrl.scheme == 'file': + elif parsedUrl.scheme in ['', 'file']: packageFile = parsedUrl.path else: - logging.error("Invalid URL: " + url) + logging.error(f"Invalid URL: {url}") return if not is_zipfile(packageFile): - logging.error("Not a valid KMP package: " + url) + logging.error(f"Not a valid KMP package: {url}") return if packageFile and not _install_package(packageFile, bcp47): - logging.error("Can't find file " + url) + logging.error(f"Can't find file {url}") def _extract_bcp47(query): diff --git a/linux/keyman-config/keyman_config/ibus_util.py b/linux/keyman-config/keyman_config/ibus_util.py index ad42f7af8465f16b8db07800ab4fd16f2e597fa8..d3132fd9921eba6cb20c62ba34f74701b8595385 100644 --- a/linux/keyman-config/keyman_config/ibus_util.py +++ b/linux/keyman-config/keyman_config/ibus_util.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +import getpass import logging import os import re @@ -39,7 +40,7 @@ def get_ibus_bus(): except Exception as e: logging.warning("Failed get bus") logging.warning(e) - logging.warning("could not find connected IBus.Bus") + logging.info("could not find connected IBus.Bus") return None @@ -82,12 +83,7 @@ def restart_ibus_subp(): def verify_ibus_daemon(start): realuser = os.environ.get('SUDO_USER') - user = os.environ.get('USER') - if realuser: - user = realuser - elif not user: - user = os.environ.get('LOGNAME') - + user = realuser or getpass.getuser() try: ps = subprocess.run(('ps', '--user', user, '-o', 's=', '-o', 'cmd'), stdout=subprocess.PIPE).stdout ibus_daemons = re.findall('^[^ZT] ibus-daemon .*--xim.*', ps.decode('utf-8'), re.MULTILINE) @@ -126,8 +122,7 @@ def _start_ibus_daemon(realuser): def restart_ibus(bus=None): verify_ibus_daemon(False) - realuser = os.environ.get('SUDO_USER') - if realuser: + if realuser := os.environ.get('SUDO_USER'): # we have been called with `sudo`. Restart ibus for the real user. logging.info('restarting IBus by subprocess for user %s', realuser) subprocess.run(['sudo', '-u', realuser, 'ibus', 'restart']) @@ -159,9 +154,8 @@ def bus_has_engine(bus, name): def get_current_engine(bus): try: contextname = bus.current_input_context() - ic = IBus.InputContext.get_input_context(contextname, bus.get_connection()) - engine = ic.get_engine() - if engine: + inputContext = IBus.InputContext.get_input_context(contextname, bus.get_connection()) + if engine := inputContext.get_engine(): return engine.get_name() except Exception as e: logging.warning("Failed to get current engine") @@ -171,11 +165,11 @@ def get_current_engine(bus): def change_to_keyboard(bus, keyboard_id): try: contextname = bus.current_input_context() - ic = IBus.InputContext.get_input_context(contextname, bus.get_connection()) + inputContext = IBus.InputContext.get_input_context(contextname, bus.get_connection()) if bus_has_engine(bus, keyboard_id) <= 0: - logging.warning("Could not find engine %s" % keyboard_id) + logging.warning(f"Could not find engine {keyboard_id}") else: - ic.set_engine(keyboard_id) + inputContext.set_engine(keyboard_id) except Exception as e: logging.warning("Failed to change keyboard") logging.warning(e) diff --git a/linux/keyman-config/keyman_config/install_kmp.py b/linux/keyman-config/keyman_config/install_kmp.py index fcf8eabc5df0c988bc90ee92647acc7c1ee232ac..95d1e294e0497aaae93ec21220601e5e338145f2 100755 --- a/linux/keyman-config/keyman_config/install_kmp.py +++ b/linux/keyman-config/keyman_config/install_kmp.py @@ -64,11 +64,10 @@ class InstallKmp(): # Check for write access of keyman dir to be able to create subdir if not os.access(keyman_dir, os.X_OK | os.W_OK): raise InstallError(InstallStatus.Abort, error_message) - else: - # Check for write access of basedir and create keyman subdir if we can - if not os.access(basedir, os.X_OK | os.W_OK): - raise InstallError(InstallStatus.Abort, error_message) + elif os.access(basedir, os.X_OK | os.W_OK): os.mkdir(keyman_dir) + else: + raise InstallError(InstallStatus.Abort, error_message) def _extract_package_id(self, inputfile): packageID, ext = os.path.splitext(os.path.basename(inputfile)) @@ -122,18 +121,10 @@ class InstallKmp(): info, system, options, keyboards, files = get_metadata(self.packageDir) - if system: - fileVersion = secure_lookup(system, 'fileVersion') - if not fileVersion: - fileVersion = '7.0' - if parse_version(fileVersion) > parse_version(__version__): - logging.error("install_kmp.py: error: %s requires a newer version of Keyman (%s)", - inputfile, fileVersion) - rmtree(self.packageDir) - message = _("{packageFile} requires Keyman {keymanVersion} or higher").format( - packageFile=inputfile, keymanVersion=fileVersion) - raise InstallError(InstallStatus.Abort, message) + self._check_version(inputfile, system) + if keyboards: + # sourcery skip: extract-method logging.info("Installing %s", secure_lookup(info, 'name', 'description')) process_keyboard_data(self.packageID, self.packageDir) for kb in keyboards: @@ -143,47 +134,7 @@ class InstallKmp(): if files is None: return self.install_keyboards(keyboards, self.packageDir, language) - for f in files: - fpath = os.path.join(self.packageDir, f['name']) - ftype = f['type'] - - if ftype == KMFileTypes.KM_DOC or ftype == KMFileTypes.KM_IMAGE: - # Special handling of doc and images to hard link them into doc dir - logging.info("Installing %s as documentation", f['name']) - if self._safeMakeDirs(self.kmpdocdir): - kmpdocpath = os.path.join(self.kmpdocdir, f['name']) - self._safeLinkFile(fpath, kmpdocpath) - elif ftype == KMFileTypes.KM_FONT: - # Special handling of font to hard link it into font dir - logging.info("Installing %s as font", f['name']) - if self._safeMakeDirs(self.kmpfontdir): - fontpath = os.path.join(self.kmpfontdir, f['name']) - self._safeLinkFile(fpath, fontpath) - elif ftype == KMFileTypes.KM_OSK: - # Special handling to convert kvk into LDML - logging.info("Converting %s to LDML and installing both as as keyman file", - f['name']) - ldml = convert_kvk_to_ldml(fpath) - name, ext = os.path.splitext(f['name']) - ldmlfile = os.path.join(self.packageDir, name + ".ldml") - output_ldml(ldmlfile, ldml) - elif ftype == KMFileTypes.KM_ICON: - # Special handling of icon to convert to PNG - logging.info("Converting %s to PNG and installing both as keyman files", - f['name']) - checkandsaveico(fpath) - elif ftype == KMFileTypes.KM_SOURCE: - # TODO for the moment just leave it for ibus-keyman to ignore if it doesn't load - pass - elif ftype == KMFileTypes.KM_KMX: - # Sanitize keyboard filename if not lower case - kmx_id, ext = os.path.splitext(os.path.basename(f['name'])) - for kb in keyboards: - if kmx_id.lower() == kb['id'] and kmx_id != kb['id']: - os.rename(os.path.join(self.packageDir, f['name']), - os.path.join(self.packageDir, kb['id'] + '.kmx')) - fpath = os.path.join(self.packageDir, kb['id'] + '.kmx') - extractico(fpath) + self._install_files(keyboards, files) return self.install_keyboards(keyboards, self.packageDir, language) else: @@ -196,6 +147,64 @@ class InstallKmp(): packageFile=inputfile) raise InstallError(InstallStatus.Abort, message) + def _check_version(self, inputfile, system): + if not system: + return + + fileVersion = secure_lookup(system, 'fileVersion') + if not fileVersion: + fileVersion = '7.0' + if parse_version(fileVersion) > parse_version(__version__): + logging.error("install_kmp.py: error: %s requires a newer version of Keyman (%s)", + inputfile, fileVersion) + rmtree(self.packageDir) + message = _("{packageFile} requires Keyman {keymanVersion} or higher").format( + packageFile=inputfile, keymanVersion=fileVersion) + raise InstallError(InstallStatus.Abort, message) + + def _install_files(self, keyboards, files): + for f in files: + fpath = os.path.join(self.packageDir, f['name']) + ftype = f['type'] + + if ftype in [KMFileTypes.KM_DOC, KMFileTypes.KM_IMAGE]: + # Special handling of doc and images to hard link them into doc dir + logging.info("Installing %s as documentation", f['name']) + if self._safeMakeDirs(self.kmpdocdir): + kmpdocpath = os.path.join(self.kmpdocdir, f['name']) + self._safeLinkFile(fpath, kmpdocpath) + elif ftype == KMFileTypes.KM_FONT: + # Special handling of font to hard link it into font dir + logging.info("Installing %s as font", f['name']) + if self._safeMakeDirs(self.kmpfontdir): + fontpath = os.path.join(self.kmpfontdir, f['name']) + self._safeLinkFile(fpath, fontpath) + elif ftype == KMFileTypes.KM_OSK: + # Special handling to convert kvk into LDML + logging.info("Converting %s to LDML and installing both as as keyman file", + f['name']) + ldml = convert_kvk_to_ldml(fpath) + name, ext = os.path.splitext(f['name']) + ldmlfile = os.path.join(self.packageDir, f"{name}.ldml") + output_ldml(ldmlfile, ldml) + elif ftype == KMFileTypes.KM_ICON: + # Special handling of icon to convert to PNG + logging.info("Converting %s to PNG and installing both as keyman files", + f['name']) + checkandsaveico(fpath) + elif ftype == KMFileTypes.KM_SOURCE: + # TODO for the moment just leave it for ibus-keyman to ignore if it doesn't load + pass + elif ftype == KMFileTypes.KM_KMX: + # Sanitize keyboard filename if not lower case + kmx_id, ext = os.path.splitext(os.path.basename(f['name'])) + for kb in keyboards: + if kmx_id.lower() == kb['id'] and kmx_id != kb['id']: + os.rename(os.path.join(self.packageDir, f['name']), + os.path.join(self.packageDir, kb['id'] + '.kmx')) + fpath = os.path.join(self.packageDir, kb['id'] + '.kmx') + extractico(fpath) + def _safeMakeDirs(self, dir): if not os.path.isdir(dir): try: @@ -229,11 +238,11 @@ class InstallKmp(): if not language: return language - language = CanonicalLanguageCodeUtils.findBestTag(language, False, True) + language = CanonicalLanguageCodeUtils.findBestTag(language, False, False) for supportedLanguage in supportedLanguages: - id = CanonicalLanguageCodeUtils.findBestTag(supportedLanguage['id'], False, True) - if id == language: - return id + tag = CanonicalLanguageCodeUtils.findBestTag(supportedLanguage['id'], False, False) + if tag == language: + return tag return None def install_keyboards(self, keyboards, packageDir, language=None): @@ -249,6 +258,7 @@ class InstallKmp(): return self._install_keyboards_to_ibus(keyboards, packageDir, language) def _install_keyboards_to_ibus(self, keyboards, packageDir, language=None): + # sourcery skip: split-or-ifs bus = get_ibus_bus() if bus or os.environ.get('SUDO_USER'): # install all kmx for first lang not just packageID @@ -285,29 +295,25 @@ def extract_kmp(kmpfile, directory): with zipfile.ZipFile(kmpfile, "r") as zip_ref: zip_ref.extractall(directory) except zipfile.BadZipFile as e: - raise InstallError(InstallStatus.Abort, e) + raise InstallError(InstallStatus.Abort, e) from e def process_keyboard_data(keyboardID, packageDir) -> None: - kbdata = get_keyboard_data(keyboardID) - if kbdata: - if not os.path.isdir(packageDir) and os.access(os.path.join(packageDir, os.pardir), os.X_OK | os.W_OK): - try: - os.makedirs(packageDir) - except Exception as e: - logging.warning('Exception %s creating %s %s', type(e), packageDir, e.args) - - if os.access(packageDir, os.X_OK | os.W_OK): - try: - with open(os.path.join(packageDir, keyboardID + '.json'), 'w') as outfile: - json.dump(kbdata, outfile) - logging.info("Installing api data file %s.json as keyman file", keyboardID) - except Exception as e: - logging.warning('Exception %s writing %s/%s.json %s', type(e), packageDir, keyboardID, e.args) - # else: - # message = "install_kmp.py: error: cannot download keyboard data so not installing." - # rmtree(kbdir) - # raise InstallError(InstallStatus.Abort, message) + if not (kbdata := get_keyboard_data(keyboardID)): + return + if not os.path.isdir(packageDir) and os.access(os.path.join(packageDir, os.pardir), os.X_OK | os.W_OK): + try: + os.makedirs(packageDir) + except Exception as e: + logging.warning('Exception %s creating %s %s', type(e), packageDir, e.args) + + if os.access(packageDir, os.X_OK | os.W_OK): + try: + with open(os.path.join(packageDir, f'{keyboardID}.json'), 'w') as outfile: + json.dump(kbdata, outfile) + logging.info("Installing api data file %s.json as keyman file", keyboardID) + except Exception as e: + logging.warning('Exception %s writing %s/%s.json %s', type(e), packageDir, keyboardID, e.args) def install_kmp(inputfile, sharedarea=False, language=None): diff --git a/linux/keyman-config/keyman_config/install_window.py b/linux/keyman-config/keyman_config/install_window.py index b0684d79f984936fafb64f4665c2748d01e1aeb3..77fd55c61fdd88eed8db9e33d7d2c83edd8b8525 100755 --- a/linux/keyman-config/keyman_config/install_window.py +++ b/linux/keyman-config/keyman_config/install_window.py @@ -2,6 +2,7 @@ # Install confirmation with details window +import contextlib import logging import os import pathlib @@ -49,6 +50,7 @@ class InstallKmpWindow(Gtk.Dialog): def __init__(self, kmpfile, viewkmp=None, language=None): logging.debug("InstallKmpWindow: kmpfile: %s", kmpfile) + self.checkcontinue = True self.is_error = False self.kmpfile = kmpfile self.viewwindow = viewkmp @@ -60,7 +62,7 @@ class InstallKmpWindow(Gtk.Dialog): logging.info("installed kmp version %s", installed_kmp_ver) windowtitle = _("Installing keyboard/package {keyboardid}").format(keyboardid=keyboardid) - Gtk.Dialog.__init__(self, windowtitle, viewkmp) + super().__init__(windowtitle, parent=viewkmp) init_accel(self) self.set_border_width(12) @@ -76,228 +78,96 @@ class InstallKmpWindow(Gtk.Dialog): return info, system, options, keyboards, files = get_metadata(tmpdirname) - if not keyboards: - # Likely not a keyboard .kmp file - logging.info("%s is not a Keyman keyboard package" % kmpfile) - dialog = Gtk.MessageDialog(viewkmp, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, _( - "The file '{kmpfile}' is not a Keyman keyboard package!").format(kmpfile=kmpfile)) - dialog.run() - dialog.destroy() - self.checkcontinue = False + if self._get_keyboard_name(keyboardid, viewkmp, keyboards, kmpfile): + return + + if not self._uninstall_prev_version_if_necessary(keyboardid, viewkmp, installed_kmp_ver, info): return - if len(keyboards) > 0 and 'name' in keyboards[0]: - self.kbname = keyboards[0]['name'] - else: - self.kbname = keyboardid - self.checkcontinue = True - - if installed_kmp_ver and secure_lookup(info, 'version', 'description'): - if secure_lookup(info, 'version', 'description') == installed_kmp_ver: - dialog = Gtk.MessageDialog( - viewkmp, 0, Gtk.MessageType.QUESTION, - Gtk.ButtonsType.YES_NO, _("Keyboard is installed already")) - dialog.format_secondary_text( - _("The {name} keyboard is already installed at version {version}. " - "Do you want to uninstall then reinstall it?") - .format(name=self.kbname, version=installed_kmp_ver)) - response = dialog.run() - dialog.destroy() - if response == Gtk.ResponseType.YES: - logging.debug("QUESTION dialog closed by clicking YES button") - uninstall_kmp(keyboardid) - elif response == Gtk.ResponseType.NO: - logging.debug("QUESTION dialog closed by clicking NO button") - self.checkcontinue = False - return - else: - try: - logging.info("package version %s", secure_lookup(info, 'version', 'description')) - logging.info("installed kmp version %s", installed_kmp_ver) - if parse_version(secure_lookup(info, 'version', - 'description')) <= parse_version(installed_kmp_ver): - dialog = Gtk.MessageDialog( - viewkmp, 0, Gtk.MessageType.QUESTION, - Gtk.ButtonsType.YES_NO, _("Keyboard is installed already")) - dialog.format_secondary_text( - _("The {name} keyboard is already installed with a newer version {installedversion}. " - "Do you want to uninstall it and install the older version {version}?") - .format(name=self.kbname, installedversion=installed_kmp_ver, - version=secure_lookup(info, 'version', 'description'))) - response = dialog.run() - dialog.destroy() - if response == Gtk.ResponseType.YES: - logging.debug("QUESTION dialog closed by clicking YES button") - uninstall_kmp(keyboardid) - elif response == Gtk.ResponseType.NO: - logging.debug("QUESTION dialog closed by clicking NO button") - self.checkcontinue = False - return - except: # noqa: E722 - logging.warning("Exception uninstalling an old kmp, continuing") - pass - - image = Gtk.Image() - if secure_lookup(options, 'graphicFile'): - image.set_from_file(os.path.join(tmpdirname, options['graphicFile'])) - else: - img_default = find_keyman_image("defaultpackage.gif") - image.set_from_file(img_default) - - mainhbox.pack_start(image, False, False, 0) - - self.page1 = Gtk.Box() - self.page1.set_border_width(12) - - grid = Gtk.Grid() - self.page1.add(grid) - - label1 = Gtk.Label() - label1.set_text(_("Keyboard layouts: ")) - label1.set_halign(Gtk.Align.END) - grid.add(label1) - prevlabel = label1 - label = Gtk.Label() - keyboardlayout = "" - for kb in keyboards: - if 'name' in kb: - if keyboardlayout != "": - keyboardlayout = keyboardlayout + "\n" - keyboardlayout = keyboardlayout + kb['name'] - label.set_text(keyboardlayout) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, label1, Gtk.PositionType.RIGHT, 1, 1) - - fonts = get_fonts(files) - if fonts: - label2 = Gtk.Label() - # Fonts are optional - label2.set_text(_("Fonts: ")) - label2.set_halign(Gtk.Align.END) - grid.attach_next_to(label2, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = label2 - label = Gtk.Label() - fontlist = "" - for font in fonts: - if 'description' in font: - if fontlist != "": - fontlist = fontlist + "\n" - if font['description'][:5] == "Font ": - fontdesc = font['description'][5:] - else: - fontdesc = font['description'] - fontlist = fontlist + fontdesc - label.set_text(fontlist) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, label2, Gtk.PositionType.RIGHT, 1, 1) - - label3 = Gtk.Label() - label3.set_text(_("Package version: ")) - label3.set_halign(Gtk.Align.END) - grid.attach_next_to(label3, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = label3 - label = Gtk.Label() - description = secure_lookup(info, 'version', 'description') - if description: - label.set_text(description) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, label3, Gtk.PositionType.RIGHT, 1, 1) - - author = secure_lookup(info, 'author') - if author: - author = author - label4 = Gtk.Label() - label4.set_text(_("Author: ")) - label4.set_halign(Gtk.Align.END) - grid.attach_next_to(label4, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = label4 - label = Gtk.Label() - if secure_lookup(author, 'url') and secure_lookup(author, 'description'): - label.set_markup( - "" + author['description'] + "") - elif secure_lookup(author, 'description'): - label.set_text(author['description']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, label4, Gtk.PositionType.RIGHT, 1, 1) - - if secure_lookup(info, 'website'): - label5 = Gtk.Label() - # Website is optional and may be a mailto for the author - label5.set_text(_("Website: ")) - label5.set_halign(Gtk.Align.END) - grid.attach_next_to(label5, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = label5 - label = Gtk.Label() - website_description = secure_lookup(info, 'website', 'description') - if website_description: - label.set_markup( - "" + website_description + "") - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, label5, Gtk.PositionType.RIGHT, 1, 1) - - if secure_lookup(info, 'copyright'): - label6 = Gtk.Label() - label6.set_text(_("Copyright: ")) - label6.set_halign(Gtk.Align.END) - grid.attach_next_to(label6, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - label = Gtk.Label() - copyright_description = secure_lookup(info, 'copyright', 'description') - if copyright_description: - label.set_text(copyright_description) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, label6, Gtk.PositionType.RIGHT, 1, 1) - - self.page2 = Gtk.Box() - webview = WebKit2.WebView() - webview.connect("decide-policy", self.doc_policy) - - readmeFile = secure_lookup(options, 'readmeFile') - if readmeFile: - self.readme = readmeFile - else: - self.readme = "noreadme" - readme_file = os.path.join(tmpdirname, self.readme) - - if os.path.isfile(readme_file): - try: - with open(readme_file, "r") as read_file: - readme_data = read_file.read() - readme_added = True - readme_uri = pathlib.Path(readme_file).as_uri() - logging.debug(readme_data) - webview.load_html(readme_data, readme_uri) - s = Gtk.ScrolledWindow() - s.add(webview) - self.page2.pack_start(s, True, True, 0) - - self.notebook = Gtk.Notebook() - self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM) - mainhbox.pack_start(self.notebook, True, True, 0) - self.notebook.append_page( - self.page1, - Gtk.Label(_('Details'))) - self.notebook.append_page( - self.page2, - Gtk.Label(_('README'))) - except UnicodeDecodeError: - readme_added = False - else: - readme_added = False - if not readme_added: + mainhbox.pack_start(self._add_image(tmpdirname, options), False, False, 0) + + self.page1 = self._add_details_page(info, keyboards, files) + self.page2 = self._add_readme_page(tmpdirname, options) + + if self.page2: + self.notebook = Gtk.Notebook() + self.notebook.set_tab_pos(Gtk.PositionType.BOTTOM) + self.notebook.append_page( + self.page1, + Gtk.Label(_('Details'))) + self.notebook.append_page( + self.page2, + Gtk.Label(_('README'))) + mainhbox.pack_start(self.notebook, True, True, 0) + else: mainhbox.pack_start(self.page1, True, True, 0) self.get_content_area().pack_start(mainhbox, True, True, 0) - hbox = Gtk.Box(spacing=6) + hbox = self._add_buttons() self.get_content_area().pack_start(hbox, False, False, 0) + self.resize(800, 450) + self.show_all() + + def _get_keyboard_name(self, keyboardid, viewkmp, keyboards, kmpfile): + if not keyboards: + self._show_no_pkg_error(kmpfile, viewkmp) + return True + if len(keyboards) > 0 and 'name' in keyboards[0]: + self.kbname = keyboards[0]['name'] + else: + self.kbname = keyboardid + return False + + def _show_no_pkg_error(self, kmpfile, viewkmp): + # Likely not a keyboard .kmp file + logging.info(f"{kmpfile} is not a Keyman keyboard package") + dialog = Gtk.MessageDialog(viewkmp, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, _( + "The file '{kmpfile}' is not a Keyman keyboard package!").format(kmpfile=kmpfile)) + dialog.run() + dialog.destroy() + self.checkcontinue = False + + def _uninstall_prev_version_if_necessary(self, keyboardid, viewkmp, installed_kmp_ver, info): + if not installed_kmp_ver or not secure_lookup(info, 'version', 'description'): + return True + logging.info("package version %s", secure_lookup(info, 'version', 'description')) + logging.info("installed kmp version %s", installed_kmp_ver) + if parse_version(secure_lookup(info, 'version', 'description')) >= parse_version(installed_kmp_ver): + return True + response = self._show_version_message_dlg( + viewkmp, + # i18n: The words in braces (e.g. `{name}`) are placeholders. Don't translate them! + _("The {name} keyboard is already installed with a newer version {installedversion}. " + "Do you want to uninstall it and install the older version {version}?") + .format(name=self.kbname, installedversion=installed_kmp_ver, + version=secure_lookup(info, 'version', 'description'))) + try: + if response == Gtk.ResponseType.YES: + logging.debug("QUESTION dialog closed by clicking YES button") + uninstall_kmp(keyboardid, False, False) + return True + elif response == Gtk.ResponseType.NO: + logging.debug("QUESTION dialog closed by clicking NO button") + self.checkcontinue = False + return False + except Exception: # noqa: E722 + logging.warning("Exception uninstalling an old kmp, continuing") + return True + + def _show_version_message_dlg(self, viewkmp, msg): + dialog = Gtk.MessageDialog( + viewkmp, 0, Gtk.MessageType.QUESTION, + Gtk.ButtonsType.YES_NO, _("Keyboard is installed already")) + dialog.format_secondary_text(msg) + response = dialog.run() + dialog.destroy() + return response + + def _add_buttons(self): + hbox = Gtk.Box(spacing=6) + button = Gtk.Button.new_with_mnemonic(_("_Install")) button.connect("clicked", self.on_install_clicked) hbox.pack_start(button, False, False, 0) @@ -306,14 +176,164 @@ class InstallKmpWindow(Gtk.Dialog): button.connect("clicked", self.on_cancel_clicked) hbox.pack_end(button, False, False, 0) bind_accelerator(self.accelerators, button, 'w') + return hbox - self.resize(800, 450) - self.show_all() + def _add_image(self, tmpdirname, options): + image = Gtk.Image() + if secure_lookup(options, 'graphicFile'): + image.set_from_file(os.path.join(tmpdirname, options['graphicFile'])) + else: + img_default = find_keyman_image("defaultpackage.gif") + image.set_from_file(img_default) + return image + + def _add_readme_page(self, tmpdirname, options): + box = Gtk.Box() + webview = WebKit2.WebView() + webview.connect("decide-policy", self.doc_policy) + + if readmeFile := secure_lookup(options, 'readmeFile'): + self.readme = readmeFile + else: + self.readme = "noreadme" + readme_file = os.path.join(tmpdirname, self.readme) + + if os.path.isfile(readme_file): + with contextlib.suppress(UnicodeDecodeError): + with open(readme_file, "r") as read_file: + readme_data = read_file.read() + readme_uri = pathlib.Path(readme_file).as_uri() + logging.debug(readme_data) + webview.load_html(readme_data, readme_uri) + s = Gtk.ScrolledWindow() + s.add(webview) + box.pack_start(s, True, True, 0) + return box + return None + + def _add_details_page(self, info, keyboards, files): + box = Gtk.Box() + box.set_border_width(12) + + grid = Gtk.Grid() + box.add(grid) + + prevlabel = self._add_keyboard_layouts_label(keyboards, grid) + prevlabel = self._add_fonts_label(files, grid, prevlabel) + prevlabel = self._add_pkgversion_label(info, grid, prevlabel) + prevlabel = self._add_author_label(info, grid, prevlabel) + prevlabel = self._add_website_label(info, grid, prevlabel) + prevlabel = self._add_copyright_label(info, grid, prevlabel) + return box + + def _add_keyboard_layouts_label(self, keyboards, grid): + label1 = Gtk.Label() + label1.set_text(_("Keyboard layouts: ")) + label1.set_halign(Gtk.Align.END) + grid.add(label1) + label = Gtk.Label() + keyboardlayout = "" + for kb in keyboards: + if 'name' in kb: + if keyboardlayout != "": + keyboardlayout = keyboardlayout + "\n" + keyboardlayout = keyboardlayout + kb['name'] + label.set_text(keyboardlayout) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label1, Gtk.PositionType.RIGHT, 1, 1) + return label1 + + def _add_fonts_label(self, files, grid, prevlabel): + if not (fonts := get_fonts(files)): + return prevlabel + label2 = Gtk.Label() + label2.set_text(_("Fonts: ")) + label2.set_halign(Gtk.Align.END) + grid.attach_next_to(label2, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + label = Gtk.Label() + fontlist = "" + for font in fonts: + if 'description' in font: + if fontlist != "": + fontlist = fontlist + "\n" + if font['description'][:5] == "Font ": + fontdesc = font['description'][5:] + else: + fontdesc = font['description'] + fontlist = fontlist + fontdesc + label.set_text(fontlist) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label2, Gtk.PositionType.RIGHT, 1, 1) + return label2 + + def _add_pkgversion_label(self, info, grid, prevlabel): + label3 = Gtk.Label() + label3.set_text(_("Package version: ")) + label3.set_halign(Gtk.Align.END) + grid.attach_next_to(label3, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + label = Gtk.Label() + if description := secure_lookup(info, 'version', 'description'): + label.set_text(description) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label3, Gtk.PositionType.RIGHT, 1, 1) + return label3 + + def _add_author_label(self, info, grid, prevlabel): + if not (author := secure_lookup(info, 'author')): + return prevlabel + author = author + label4 = Gtk.Label() + label4.set_text(_("Author: ")) + label4.set_halign(Gtk.Align.END) + grid.attach_next_to(label4, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + label = Gtk.Label() + if secure_lookup(author, 'url') and secure_lookup(author, 'description'): + label.set_markup(f"{author['description']}") + elif secure_lookup(author, 'description'): + label.set_text(author['description']) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label4, Gtk.PositionType.RIGHT, 1, 1) + return label4 + + def _add_website_label(self, info, grid, prevlabel): + if not secure_lookup(info, 'website'): + return prevlabel + label5 = Gtk.Label() + # Website is optional and may be a mailto for the author + label5.set_text(_("Website: ")) + label5.set_halign(Gtk.Align.END) + grid.attach_next_to(label5, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + label = Gtk.Label() + if website_description := secure_lookup(info, 'website', 'description'): + label.set_markup("" + website_description + "") + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label5, Gtk.PositionType.RIGHT, 1, 1) + return label5 + + def _add_copyright_label(self, info, grid, prevlabel): + if not secure_lookup(info, 'copyright'): + return prevlabel + label6 = Gtk.Label() + label6.set_text(_("Copyright: ")) + label6.set_halign(Gtk.Align.END) + grid.attach_next_to(label6, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + label = Gtk.Label() + if copyright_description := secure_lookup( + info, 'copyright', 'description' + ): + label.set_text(copyright_description) + label.set_halign(Gtk.Align.START) + label.set_selectable(True) + grid.attach_next_to(label, label6, Gtk.PositionType.RIGHT, 1, 1) + return label6 def run(self): - if self.checkcontinue: - return Gtk.Dialog.run(self) - return Gtk.ResponseType.CANCEL + return Gtk.Dialog.run(self) if self.checkcontinue else Gtk.ResponseType.CANCEL def doc_policy(self, web_view, decision, decision_type): logging.info("Checking policy") @@ -333,8 +353,7 @@ class InstallKmpWindow(Gtk.Dialog): def on_install_clicked(self, button): logging.info("Installing keyboard") try: - result = install_kmp(self.kmpfile, language=self.language) - if result: + if result := install_kmp(self.kmpfile, language=self.language): # If install_kmp returns a string, it is an instruction for the end user, # because for fcitx they will need to take extra steps to complete # installation themselves. @@ -359,8 +378,8 @@ class InstallKmpWindow(Gtk.Dialog): w.destroy() else: dialog = Gtk.MessageDialog( - self, 0, Gtk.MessageType.INFO, - Gtk.ButtonsType.OK, _("Keyboard {name} installed").format(name=self.kbname)) + self, 0, Gtk.MessageType.INFO, + Gtk.ButtonsType.OK, _("Keyboard {name} installed").format(name=self.kbname)) dialog.run() dialog.destroy() except InstallError as e: @@ -374,38 +393,38 @@ class InstallKmpWindow(Gtk.Dialog): def _handle_install_error(self, e, kbname): if e.status == InstallStatus.Abort: message = _("Keyboard {name} could not be installed.").format(name=kbname) \ - + "\n\n" + _("Error Message:") + "\n %s" % (e.message) + + "\n\n" + _("Error Message:") + "\n %s" % (e.message) logging.error(message) message_type = Gtk.MessageType.ERROR else: message = _("Keyboard {name} could not be installed.").format(name=kbname) \ - + "\n\n" + _("Warning Message:") + "\n %s" % (e.message) + + "\n\n" + _("Warning Message:") + "\n %s" % (e.message) logging.warning(message) message_type = Gtk.MessageType.WARNING dialog = Gtk.MessageDialog( - self, 0, message_type, - Gtk.ButtonsType.OK, message) + self, 0, message_type, + Gtk.ButtonsType.OK, message) dialog.run() dialog.destroy() -def main(argv): - if len(sys.argv) != 2: - logging.error("install_window.py ") - sys.exit(2) +def _log_error(arg): + logging.error(arg) + logging.error("install_window.py ") + sys.exit(2) - name, ext = os.path.splitext(sys.argv[1]) - if ext != ".kmp": - logging.error("install_window.py Input file", sys.argv[1], "is not a kmp file.") - logging.error("install_window.py ") - sys.exit(2) - if not os.path.isfile(sys.argv[1]): - logging.error("install_window.py Keyman kmp file", sys.argv[1], "not found.") +def main(argv): + if len(argv) != 2: logging.error("install_window.py ") sys.exit(2) - w = InstallKmpWindow(sys.argv[1]) + _, ext = os.path.splitext(argv[1]) + if ext != ".kmp": + _log_error(f'install_window.py Input file {argv[1]} is not a kmp file.') + if not os.path.isfile(argv[1]): + _log_error(f'install_window.py Keyman kmp file {argv[1]} not found.') + w = InstallKmpWindow(argv[1]) w.run() w.destroy() diff --git a/linux/keyman-config/keyman_config/keyboard_details.py b/linux/keyman-config/keyman_config/keyboard_details.py index 8f71168c8cd968fbbb132f3bfafa010adfb543a2..11f87f1b7bcd2552f175a4f4ac0b827ce68fdbd2 100644 --- a/linux/keyman-config/keyman_config/keyboard_details.py +++ b/linux/keyman-config/keyman_config/keyboard_details.py @@ -4,15 +4,17 @@ import json import logging +import re import tempfile from os import path import gi import qrcode +import qrcode.constants gi.require_version('Gtk', '3.0') -from gi.repository import Gtk +from gi.repository import GLib, Gtk from keyman_config import KeymanComUrl, _, secure_lookup from keyman_config.accelerators import init_accel @@ -33,35 +35,27 @@ class KeyboardDetailsView(Gtk.Dialog): # especially what is displayed for Keyman on Windows # TODO clean up file once have what we want def __init__(self, parent, kmp): + self.kmp = kmp # kmp has name, version, packageID, area - if "keyboard" in kmp["name"].lower(): - wintitle = kmp["name"] + if "keyboard" in self.kmp["name"].lower(): + wintitle = self.kmp["name"] else: - wintitle = _("{name} keyboard").format(name=kmp["name"]) - Gtk.Dialog.__init__(self, wintitle, parent) + wintitle = _("{name} keyboard").format(name=self.kmp["name"]) + super().__init__(wintitle, parent=parent) init_accel(self) self.set_border_width(6) - packageDir = path.join(kmp['areapath'], kmp['packageID']) - kmp_json = path.join(packageDir, "kmp.json") - info, system, options, keyboards, files = parsemetadata(kmp_json) - - if info is None: - # Dialog when invalid metadata - self.add_button(_("_Close"), Gtk.ResponseType.CLOSE) - grid = Gtk.Grid() - self.get_content_area().pack_start(grid, True, True, 12) - lbl_invalid_metadata = Gtk.Label() - lbl_invalid_metadata.set_text(_("ERROR: Keyboard metadata is damaged.\nPlease \"Uninstall\" and then \"Install\" the keyboard.")) - lbl_invalid_metadata.set_halign(Gtk.Align.END) - grid.add(lbl_invalid_metadata) - self.resize(700, 200) - self.show_all() + self.packageDir = path.join(self.kmp['areapath'], self.kmp['packageID']) + kmp_json = path.join(self.packageDir, "kmp.json") + self.info, system, options, keyboards, files = parsemetadata(kmp_json) + + if self.info is None: + self._display_invalid_metadata() return kbdata = None - jsonfile = path.join(packageDir, kmp['packageID'] + ".json") + jsonfile = path.join(self.packageDir, self.kmp['packageID'] + ".json") if path.isfile(jsonfile): try: with open(jsonfile, "r") as read_file: @@ -69,274 +63,146 @@ class KeyboardDetailsView(Gtk.Dialog): except Exception as e: logging.warning('Exception %s reading %s %s', type(e), jsonfile, e.args) - grid = Gtk.Grid() - # grid.set_column_homogeneous(True) + self.grid = Gtk.Grid() - # kbdatapath = path.join("/usr/local/share/keyman", kmp["id"], kmp["id"] + ".json") + scrolledWindow = Gtk.ScrolledWindow() + scrolledWindow.add(self.grid) + # self.grid.set_column_homogeneous(True) - # Package info + # kbdatapath = path.join("/usr/local/share/keyman", self.kmp["id"], self.kmp["id"] + ".json") - lbl_pkg_name = Gtk.Label() - lbl_pkg_name.set_text(_("Package name: ")) - lbl_pkg_name.set_halign(Gtk.Align.END) - grid.add(lbl_pkg_name) - prevlabel = lbl_pkg_name - label = Gtk.Label() - if secure_lookup(info, 'name', 'description'): - label.set_text(secure_lookup(info, 'name', 'description')) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_pkg_name, Gtk.PositionType.RIGHT, 1, 1) - - lbl_pkg_id = Gtk.Label() - lbl_pkg_id.set_text(_("Package id: ")) - lbl_pkg_id.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pkg_id, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_pkg_id - label = Gtk.Label() - if secure_lookup(kmp, 'packageID'): - label.set_text(kmp['packageID']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_pkg_id, Gtk.PositionType.RIGHT, 1, 1) - - lbl_pkg_vrs = Gtk.Label() - lbl_pkg_vrs.set_text(_("Package version: ")) - lbl_pkg_vrs.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pkg_vrs, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_pkg_vrs - label = Gtk.Label() - if secure_lookup(info, 'version', 'description'): - label.set_text(secure_lookup(info, 'version', 'description')) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_pkg_vrs, Gtk.PositionType.RIGHT, 1, 1) - - if secure_lookup(kbdata, 'description'): - lbl_pkg_desc = Gtk.Label() - lbl_pkg_desc.set_text(_("Package description: ")) - lbl_pkg_desc.set_halign(Gtk.Align.END) - lbl_pkg_desc.set_valign(Gtk.Align.START) - grid.attach_next_to(lbl_pkg_desc, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_pkg_desc - label = Gtk.Label() - label.set_text(kbdata['description']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - label.set_line_wrap(80) - grid.attach_next_to(label, lbl_pkg_desc, Gtk.PositionType.RIGHT, 1, 1) - - if secure_lookup(info, 'author', 'description'): - lbl_pkg_auth = Gtk.Label() - lbl_pkg_auth.set_text(_("Package author: ")) - lbl_pkg_auth.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pkg_auth, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_pkg_auth - label = Gtk.Label() - label.set_text(secure_lookup(info, 'author', 'description')) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_pkg_auth, Gtk.PositionType.RIGHT, 1, 1) - - if secure_lookup(info, 'copyright', 'description'): - lbl_pkg_cpy = Gtk.Label() - lbl_pkg_cpy.set_text(_("Package copyright: ")) - lbl_pkg_cpy.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pkg_cpy, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_pkg_cpy - label = Gtk.Label() - label.set_text(secure_lookup(info, 'copyright', 'description')) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_pkg_cpy, Gtk.PositionType.RIGHT, 1, 1) - - # Padding and full width horizontal divider - lbl_pad = Gtk.Label() - lbl_pad.set_text("") - lbl_pad.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pad, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) - prevlabel = lbl_pad - - divider_pkg = Gtk.HSeparator() - grid.attach_next_to(divider_pkg, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) - prevlabel = divider_pkg + # Package info + prevlabel = self._add_label_with_content( + _("Package name: "), secure_lookup(self.info, 'name', 'description'), None) + prevlabel = self._add_label_with_content( + _("Package id: "), secure_lookup(self.kmp, 'packageID'), prevlabel) + prevlabel = self._add_label_with_content( + _("Package version: "), secure_lookup(self.info, 'version', 'description'), prevlabel) + + prevlabel = self._add_label_with_content( + _("Package description: "), secure_lookup(kbdata, 'description'), prevlabel, + alwaysAdd=False) + prevlabel = self._add_label_with_content( + _("Package author: "), secure_lookup(self.info, 'author', 'description'), prevlabel, + alwaysAdd=False) + prevlabel = self._add_label_with_content( + _("Package copyright: "), secure_lookup(self.info, 'copyright', 'description'), prevlabel, + alwaysAdd=False) + + prevlabel = self._add_horizontal_divider(self._add_padding(prevlabel)) # Keyboard info for each keyboard + prevlabel = self._add_info_for_all_keyboards(keyboards, prevlabel) + + # Add an entire row of padding + prevlabel = self._add_padding(prevlabel) + prevlabel = self._add_qr_code(prevlabel) + + self.add_button(_("_Close"), Gtk.ResponseType.CLOSE) + + self.get_content_area().pack_start(scrolledWindow, True, True, 12) + self.resize(800, 450) + scrolledWindow.set_vadjustment(None) + self.show_all() + + def _add_info_for_all_keyboards(self, keyboards, prevlabel) -> Gtk.Widget: if keyboards: for kbd in keyboards: - kbdata = None - jsonfile = path.join(packageDir, kbd['id'] + ".json") - if path.isfile(jsonfile): - try: - with open(jsonfile, "r") as read_file: - kbdata = json.load(read_file) - except Exception as e: - logging.warning('Exception %s reading %s %s', type(e), jsonfile, e.args) - - # start with padding - lbl_pad = Gtk.Label() - lbl_pad.set_text("") - lbl_pad.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pad, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) - prevlabel = lbl_pad - - # show the icon somewhere - - lbl_kbd_file = Gtk.Label() - lbl_kbd_file.set_text(_("Keyboard filename: ")) - lbl_kbd_file.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_file, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_file - label = Gtk.Label() - label.set_text(path.join(packageDir, kbd['id'] + ".kmx")) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_kbd_file, Gtk.PositionType.RIGHT, 1, 1) - - if kbdata and secure_lookup(kbdata, 'id') != secure_lookup(kmp, 'packageID'): - lbl_kbd_name = Gtk.Label() - lbl_kbd_name.set_text(_("Keyboard name: ")) - lbl_kbd_name.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_name, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_name - label = Gtk.Label() - if secure_lookup(kbdata, 'name'): - label.set_text(kbdata['name']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_kbd_name, Gtk.PositionType.RIGHT, 1, 1) - - lbl_kbd_id = Gtk.Label() - lbl_kbd_id.set_text(_("Keyboard id: ")) - lbl_kbd_id.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_id, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_id - label = Gtk.Label() - if secure_lookup(kbdata, 'id'): - label.set_text(kbdata['id']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_kbd_id, Gtk.PositionType.RIGHT, 1, 1) - - lbl_kbd_vrs = Gtk.Label() - lbl_kbd_vrs.set_text(_("Keyboard version: ")) - lbl_kbd_vrs.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_vrs, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_vrs - label = Gtk.Label() - if secure_lookup(kbdata, 'version'): - label.set_text(kbdata['version']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_kbd_vrs, Gtk.PositionType.RIGHT, 1, 1) - - if secure_lookup(info, "author"): - lbl_kbd_auth = Gtk.Label() - lbl_kbd_auth.set_text(_("Keyboard author: ")) - lbl_kbd_auth.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_auth, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_auth - label = Gtk.Label() - if secure_lookup(kbdata, 'authorName'): - label.set_text(kbdata['authorName']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_kbd_auth, Gtk.PositionType.RIGHT, 1, 1) - - lbl_kbd_lic = Gtk.Label() - lbl_kbd_lic.set_text(_("Keyboard license: ")) - lbl_kbd_lic.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_lic, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_lic - label = Gtk.Label() - if secure_lookup(kbdata, 'license'): - label.set_text(kbdata['license']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - grid.attach_next_to(label, lbl_kbd_lic, Gtk.PositionType.RIGHT, 1, 1) - - lbl_kbd_desc = Gtk.Label() - lbl_kbd_desc.set_text(_("Keyboard description: ")) - lbl_kbd_desc.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_kbd_desc, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - prevlabel = lbl_kbd_desc - label = Gtk.Label() - if secure_lookup(kbdata, 'description'): - label.set_text(kbdata['description']) - label.set_halign(Gtk.Align.START) - label.set_selectable(True) - label.set_line_wrap(80) - grid.attach_next_to(label, lbl_kbd_desc, Gtk.PositionType.RIGHT, 1, 1) - - # Padding and full width horizontal divider - lbl_pad = Gtk.Label() - lbl_pad.set_text("") - lbl_pad.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pad, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) - prevlabel = lbl_pad - - divider_pkg = Gtk.HSeparator() - grid.attach_next_to(divider_pkg, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) - - # label7 = Gtk.Label() - # label7.set_text(_("On Screen Keyboard: ")) - # label7.set_halign(Gtk.Align.END) - # grid.attach_next_to(label7, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - # prevlabel = label7 - # # label = Gtk.Label() - # # label.set_text(secure_lookup(info, 'version', 'description')) - # # label.set_halign(Gtk.Align.START) - # # label.set_selectable(True) - # # grid.attach_next_to(label, label7, Gtk.PositionType.RIGHT, 1, 1) - - # label8 = Gtk.Label() - # label8.set_text(_("Documentation: ")) - # label8.set_halign(Gtk.Align.END) - # grid.attach_next_to(label8, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - # prevlabel = label8 - # #TODO need to know which area keyboard is installed in to show this - # # label = Gtk.Label() - # # welcome_file = path.join("/usr/local/share/doc/keyman", kmp["id"], "welcome.htm") - # # if path.isfile(welcome_file): - # # label.set_text(_("Installed")) - # # else: - # # label.set_text(_("Not installed")) - # # label.set_halign(Gtk.Align.START) - # # label.set_selectable(True) - # # grid.attach_next_to(label, label8, Gtk.PositionType.RIGHT, 1, 1) - - # label9 = Gtk.Label() - # # stored in kmx - # label9.set_text(_("Message: ")) - # label9.set_halign(Gtk.Align.END) - # grid.attach_next_to(label9, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) - # prevlabel = label9 - # label = Gtk.Label() - # label.set_line_wrap(True) - # label.set_text( - # "This keyboard is distributed under the MIT license (MIT) as described somewhere") - # #label.set_text(kmp["description"]) - # label.set_halign(Gtk.Align.START) - # label.set_selectable(True) - # grid.attach_next_to(label, label9, Gtk.PositionType.RIGHT, 1, 1) + prevlabel = self._add_info_for_keyboard(kbd, prevlabel) + return prevlabel - # Add an entire row of padding - lbl_pad = Gtk.Label() - lbl_pad.set_text("") - lbl_pad.set_halign(Gtk.Align.END) - grid.attach_next_to(lbl_pad, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) - prevlabel = lbl_pad + def _add_info_for_keyboard(self, kbd, prevlabel: Gtk.Widget) -> Gtk.Widget: + kbdata = None + jsonfile = path.join(self.packageDir, kbd['id'] + ".json") + if path.isfile(jsonfile): + try: + with open(jsonfile, "r") as read_file: + kbdata = json.load(read_file) + except Exception as e: + logging.warning('Exception %s reading %s %s', type(e), jsonfile, e.args) + + # start with padding + prevlabel = self._add_padding(prevlabel) + + # show the icon somewhere + prevlabel = self._add_label_with_content( + _("Keyboard filename: "), path.join(self.packageDir, kbd['id'] + ".kmx"), prevlabel) + if kbdata and secure_lookup(kbdata, 'id') != secure_lookup(self.kmp, 'packageID'): + prevlabel = self._add_keyboard_details(kbdata, prevlabel) + + return prevlabel + + def _add_keyboard_details(self, kbdata, prevlabel: Gtk.Widget) -> Gtk.Widget: + prevlabel = self._add_label_with_content(_("Keyboard name: "), secure_lookup(kbdata, 'name'), prevlabel) + prevlabel = self._add_label_with_content(_("Keyboard id: "), secure_lookup(kbdata, 'id'), prevlabel) + prevlabel = self._add_label_with_content(_("Keyboard version: "), secure_lookup(kbdata, 'version'), prevlabel) + prevlabel = self._add_label_with_content( + _("Keyboard author: "), secure_lookup(kbdata, 'authorName'), prevlabel, + alwaysAdd=False) + prevlabel = self._add_label_with_content( + _("Keyboard license: "), secure_lookup(kbdata, 'license'), prevlabel) + prevlabel = self._add_label_with_content( + _("Keyboard description: "), secure_lookup(kbdata, 'description'), prevlabel) + + prevlabel = self._add_horizontal_divider(self._add_padding(prevlabel)) + return prevlabel + + def _add_label(self, text: str, prevlabel) -> Gtk.Label: + label = Gtk.Label() + label.set_text(text) + label.set_halign(Gtk.Align.END) + label.set_valign(Gtk.Align.START) + if prevlabel: + self.grid.attach_next_to(label, prevlabel, Gtk.PositionType.BOTTOM, 1, 1) + else: + self.grid.add(label) + return label + + def _add_label_with_content(self, text: str, content, prevlabel, alwaysAdd: bool = True): + if not alwaysAdd and not content: + return prevlabel + label = self._add_label(text, prevlabel) + contentLabel = Gtk.Label() + if content: + contentLabel.set_text(re.sub(r'( )?]*>', ' ', content)) + contentLabel.set_halign(Gtk.Align.START) + contentLabel.set_selectable(True) + contentLabel.set_line_wrap(True) + self.grid.attach_next_to(contentLabel, label, Gtk.PositionType.RIGHT, 1, 1) + return label + + def _add_padding(self, prevlabel) -> Gtk.Label: + return self._add_label("", prevlabel) + + def _add_horizontal_divider(self, prevlabel) -> Gtk.HSeparator: + divider = Gtk.HSeparator() + self.grid.attach_next_to(divider, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) + return divider + + def _display_invalid_metadata(self): + self.add_button(_("_Close"), Gtk.ResponseType.CLOSE) + self.grid = Gtk.Grid() + self.get_content_area().pack_start(self.grid, True, True, 12) + lbl_invalid_metadata = Gtk.Label() + lbl_invalid_metadata.set_text( + _("ERROR: Keyboard metadata is damaged.\nPlease \"Uninstall\" and then \"Install\" the keyboard.")) + lbl_invalid_metadata.set_halign(Gtk.Align.END) + self.grid.add(lbl_invalid_metadata) + self.resize(700, 200) + self.show_all() + + def _add_qr_code(self, prevlabel): # If it doesn't exist, generate QR code to share keyboard package - path_qr = path.join(tempfile.gettempdir(), kmp['packageID'] + '_qrcode.png') - url = KeymanComUrl + "/go/keyboard/" + kmp['packageID'] + "/share" + path_qr = path.join(tempfile.gettempdir(), self.kmp['packageID'] + '_qrcode.png') + url = f"{KeymanComUrl}/go/keyboard/{self.kmp['packageID']}/share" if not path.isfile(path_qr): qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_H, - box_size=4, - border=4) + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, + box_size=4, + border=4) qr.add_data(url) qr.make(fit=True) @@ -346,17 +212,13 @@ class KeyboardDetailsView(Gtk.Dialog): # Display QR Code, spanning 2 columns so it will be centered image = Gtk.Image() image.set_from_file(path_qr) - grid.attach_next_to(image, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) + self.grid.attach_next_to(image, prevlabel, Gtk.PositionType.BOTTOM, 2, 1) lbl_share_kbd = Gtk.Label() - lbl_share_kbd.set_markup(_("Scan this code to load this keyboard\non another device or share online").format(uri=url)) + lbl_share_kbd.set_markup( + _("Scan this code to load this keyboard\non another device or share online") + .format(uri=url)) lbl_share_kbd.set_halign(Gtk.Align.CENTER) lbl_share_kbd.set_line_wrap(True) - grid.attach_next_to(lbl_share_kbd, image, Gtk.PositionType.BOTTOM, 2, 1) - prevlabel = lbl_share_kbd - - self.add_button(_("_Close"), Gtk.ResponseType.CLOSE) - - self.get_content_area().pack_start(grid, True, True, 12) - self.resize(800, 450) - self.show_all() + self.grid.attach_next_to(lbl_share_kbd, image, Gtk.PositionType.BOTTOM, 2, 1) + return lbl_share_kbd diff --git a/linux/keyman-config/keyman_config/keyboard_layouts_model.py b/linux/keyman-config/keyman_config/keyboard_layouts_model.py new file mode 100644 index 0000000000000000000000000000000000000000..d6e9ff742ebd877a8d50f893deb3ff9563d6a642 --- /dev/null +++ b/linux/keyman-config/keyman_config/keyboard_layouts_model.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 + +from enum import IntEnum + +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import GdkPixbuf, Gtk + + +class Model(IntEnum): + # Keep in sync with _add_model() below + ICON = 0 + NAME = 1 + VERSION = 2 + PACKAGEID = 3 + LOCATION = 4 + AREA = 5 + INSTALLPATH = 6 + WELCOMEFILE = 7 + OPTIONSFILE = 8 + + +def create_kbd_layouts_model(): + return Gtk.ListStore( + # Keep in sync with Model above + GdkPixbuf.Pixbuf, # icon + str, # name + str, # installed package version + str, # package ID + int, # enum InstallLocation + str, # InstallLocation area + str, # Tooltip with InstallLocation path + str, # path to welcome file if it exists or None + str) # path to options file if it exists or None diff --git a/linux/keyman-config/keyman_config/keyboard_layouts_widget.py b/linux/keyman-config/keyman_config/keyboard_layouts_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..d676413216269429b26a9cae483cdb156fe030e0 --- /dev/null +++ b/linux/keyman-config/keyman_config/keyboard_layouts_widget.py @@ -0,0 +1,228 @@ +#!/usr/bin/python3 + +import logging +import os +import pathlib + +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +from keyman_config import _ +from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir, + get_keyman_dir) +from keyman_config.keyboard_details import KeyboardDetailsView +from keyman_config.keyboard_layouts_model import Model +from keyman_config.kmpmetadata import get_fonts, parsemetadata +from keyman_config.keyboard_options_view import KeyboardOptionsView +from keyman_config.uninstall_kmp import uninstall_kmp +from keyman_config.welcome import WelcomeView + + +class KeyboardLayoutsWidget(Gtk.Box): + def __init__(self, parent, store, restart_func) -> None: + super().__init__(orientation=Gtk.Orientation.HORIZONTAL) + self.restart = restart_func + self.parent = parent + + scrolledWindow = Gtk.ScrolledWindow() + self.pack_start(scrolledWindow, True, True, 0) + + self.tree = Gtk.TreeView(store) + + renderer = Gtk.CellRendererPixbuf() + # i18n: column header in table displaying installed keyboards + column = Gtk.TreeViewColumn(_("Icon"), renderer, pixbuf=Model.ICON) + self.tree.append_column(column) + renderer = Gtk.CellRendererText() + # i18n: column header in table displaying installed keyboards + column = Gtk.TreeViewColumn(_("Name"), renderer, text=Model.NAME) + self.tree.append_column(column) + # i18n: column header in table displaying installed keyboards + column = Gtk.TreeViewColumn(_("Version"), renderer, text=Model.VERSION) + self.tree.append_column(column) + # i18n: column header in table displaying installed keyboards + column = Gtk.TreeViewColumn(_("Area"), renderer, text=Model.AREA) + self.tree.append_column(column) + + self.tree.set_tooltip_column(column=Model.INSTALLPATH) + + select = self.tree.get_selection() + select.connect("changed", self.on_tree_selection_changed) + + scrolledWindow.add(self.tree) + + self.pack_start(self._add_keyboard_buttons(), False, False, 12) + + def _add_keyboard_buttons(self): + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + + bbox_top = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.VERTICAL) + bbox_top.set_layout(Gtk.ButtonBoxStyle.START) + + self.uninstall_button = Gtk.Button.new_with_mnemonic(_("_Uninstall")) + self.uninstall_button.set_tooltip_text(_("Uninstall keyboard")) + self.uninstall_button.connect("clicked", self.on_uninstall_clicked) + self.uninstall_button.set_sensitive(False) + bbox_top.add(self.uninstall_button) + + self.about_button = Gtk.Button.new_with_mnemonic(_("_About")) + self.about_button.set_tooltip_text(_("About keyboard")) + self.about_button.connect("clicked", self.on_about_clicked) + self.about_button.set_sensitive(False) + bbox_top.add(self.about_button) + + self.help_button = Gtk.Button.new_with_mnemonic(_("_Help")) + self.help_button.set_tooltip_text(_("Help for keyboard")) + self.help_button.connect("clicked", self.on_kbd_help_clicked) + self.help_button.set_sensitive(False) + bbox_top.add(self.help_button) + + self.options_button = Gtk.Button.new_with_mnemonic(_("_Options")) + self.options_button.set_tooltip_text(_("Settings for keyboard")) + self.options_button.connect("clicked", self.on_options_clicked) + self.options_button.set_sensitive(False) + bbox_top.add(self.options_button) + + vbox.pack_start(bbox_top, False, False, 12) + return vbox + + def on_tree_selection_changed(self, selection): + model, treeiter = selection.get_selected() + if treeiter is not None: + # sourcery skip: extract-method + self.uninstall_button.set_tooltip_text( + _("Uninstall keyboard {package}").format(package=model[treeiter][Model.NAME])) + self.help_button.set_tooltip_text( + _("Help for keyboard {package}").format(package=model[treeiter][Model.NAME])) + self.about_button.set_tooltip_text( + _("About keyboard {package}").format(package=model[treeiter][Model.NAME])) + self.options_button.set_tooltip_text( + _("Settings for keyboard {package}").format(package=model[treeiter][Model.NAME])) + logging.debug("You selected %s version %s", model[treeiter][Model.NAME], model[treeiter][Model.VERSION]) + self.about_button.set_sensitive(True) + if model[treeiter][Model.LOCATION] == InstallLocation.User: + logging.debug("Enabling uninstall button for %s in %s", + model[treeiter][Model.PACKAGEID], model[treeiter][Model.LOCATION]) + self.uninstall_button.set_sensitive(True) + else: + self.uninstall_button.set_sensitive(False) + logging.debug("Disabling uninstall button for %s in %s", + model[treeiter][Model.PACKAGEID], model[treeiter][Model.LOCATION]) + # welcome file if it exists + if model[treeiter][Model.WELCOMEFILE]: + self.help_button.set_sensitive(True) + else: + self.help_button.set_sensitive(False) + # options file if it exists + if model[treeiter][Model.OPTIONSFILE]: + self.options_button.set_sensitive(True) + else: + self.options_button.set_sensitive(False) + else: + self.uninstall_button.set_tooltip_text(_("Uninstall keyboard")) + self.help_button.set_tooltip_text(_("Help for keyboard")) + self.about_button.set_tooltip_text(_("About keyboard")) + self.options_button.set_tooltip_text(_("Settings for keyboard")) + self.uninstall_button.set_sensitive(False) + self.about_button.set_sensitive(False) + self.help_button.set_sensitive(False) + self.options_button.set_sensitive(False) + + def on_uninstall_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is None: + return + + logging.info(f"Uninstall keyboard {model[treeiter][Model.PACKAGEID]}?") + dialog = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, + _("Uninstall keyboard package?")) + msg = _("Are you sure that you want to uninstall the {keyboard} keyboard?").format( + keyboard=model[treeiter][Model.NAME]) + kbdir = get_keyboard_dir(InstallLocation.User, model[treeiter][Model.PACKAGEID]) + kmpjson = os.path.join(kbdir, "kmp.json") + if os.path.isfile(kmpjson): + info, system, options, keyboards, files = parsemetadata(kmpjson, False) + if fontlist := self._get_font_list(files): + msg += "\n\n" + _("The following fonts will also be uninstalled:\n") + fontlist + dialog.format_secondary_text(msg) + response = dialog.run() + dialog.destroy() + if response == Gtk.ResponseType.YES: + logging.info(f"Uninstalling keyboard{model[treeiter][Model.NAME]}") + # can only uninstall with the gui from user area + msg = uninstall_kmp(model[treeiter][Model.PACKAGEID]) + if msg != '': + md = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, + _("Uninstalling keyboard failed.\n\nError message: ") + msg) + md.run() + md.destroy() + logging.info("need to restart window after uninstalling a keyboard") + self.restart() + elif response == Gtk.ResponseType.NO: + logging.info(f"Not uninstalling keyboard {model[treeiter][Model.NAME]}") + + def _get_font_list(self, files): + if not (fonts := get_fonts(files)): + return None + fontlist = "" + for font in fonts: + if 'description' in font: + if fontlist != "": + fontlist = fontlist + "\n" + if font['description'][:5] == "Font ": + fontdesc = font['description'][5:] + else: + fontdesc = font['description'] + fontlist = fontlist + fontdesc + return fontlist + + def on_about_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is not None and model[treeiter] is not None: + logging.info(f"Show keyboard details of {model[treeiter][Model.NAME]}") + areapath = get_keyman_dir(model[treeiter][Model.LOCATION]) + kmp = { + "name": model[treeiter][Model.NAME], + "version": model[treeiter][Model.VERSION], + "packageID": model[treeiter][Model.PACKAGEID], + "areapath": areapath + } + w = KeyboardDetailsView(self.parent, kmp) + w.run() + w.destroy() + + def on_kbd_help_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is not None: + logging.info("Open welcome.htm for %s if available", model[treeiter][Model.NAME]) + welcome_file = model[treeiter][Model.WELCOMEFILE] + if welcome_file and os.path.isfile(welcome_file): + uri_path = pathlib.Path(welcome_file).as_uri() + logging.info(f"opening {uri_path}") + w = WelcomeView(self.parent, uri_path, model[treeiter][Model.PACKAGEID]) + w.run() + w.destroy() + else: + logging.info("welcome.htm not available") + + def on_options_clicked(self, button): + model, treeiter = self.tree.get_selection().get_selected() + if treeiter is not None: + logging.info("Open options.htm for %s if available", model[treeiter][Model.NAME]) + options_file = model[treeiter][Model.OPTIONSFILE] + if options_file and os.path.isfile(options_file): + # sourcery skip: extract-method + uri_path = pathlib.Path(options_file).as_uri() + logging.info(f"opening {uri_path}") + # TODO: Determine keyboardID + info = {"optionurl": uri_path, "packageID": model[treeiter] + [Model.PACKAGEID], "keyboardID": model[treeiter][Model.PACKAGEID]} + w = KeyboardOptionsView(info) + w.resize(800, 600) + w.show_all() + else: + logging.info("options.htm not available") diff --git a/linux/keyman-config/keyman_config/options.py b/linux/keyman-config/keyman_config/keyboard_options_view.py similarity index 97% rename from linux/keyman-config/keyman_config/options.py rename to linux/keyman-config/keyman_config/keyboard_options_view.py index 3a34308fbb3b8059aa46a1a3442ed6f7a0fb1077..103d8f74a226d163bd4861795de1985db0be53e5 100644 --- a/linux/keyman-config/keyman_config/options.py +++ b/linux/keyman-config/keyman_config/keyboard_options_view.py @@ -17,7 +17,7 @@ from keyman_config.accelerators import init_accel from keyman_config.dconf_util import get_option, set_option -class OptionsView(Gtk.Window): +class KeyboardOptionsView(Gtk.Window): def __init__(self, info): self.accelerators = None @@ -25,7 +25,7 @@ class OptionsView(Gtk.Window): self.packageID = info["packageID"] self.keyboardID = info["keyboardID"] kbtitle = _("{packageId} Settings").format(packageId=self.packageID) - Gtk.Window.__init__(self, title=kbtitle) + super().__init__(title=kbtitle) init_accel(self) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) diff --git a/linux/keyman-config/keyman_config/kmpmetadata.py b/linux/keyman-config/keyman_config/kmpmetadata.py index 4632531e752544f85f5e67dbec7010965cae8a08..82e90b138d2a678e76279a673efd51b31d1938a3 100755 --- a/linux/keyman-config/keyman_config/kmpmetadata.py +++ b/linux/keyman-config/keyman_config/kmpmetadata.py @@ -51,7 +51,6 @@ def print_info(info): print(type(e)) # the exception instance print(e.args) # arguments stored in .args print(e) # __str__ allows args to be printed directly, pass - pass def print_system(system): @@ -67,7 +66,6 @@ def print_system(system): print(type(e)) # the exception instance print(e.args) # arguments stored in .args print(e) # __str__ allows args to be printed directly, pass - pass def print_options(options): @@ -83,7 +81,6 @@ def print_options(options): print(type(e)) # the exception instance print(e.args) # arguments stored in .args print(e) # __str__ allows args to be printed directly, pass - pass def print_keyboards(keyboards): @@ -107,7 +104,6 @@ def print_keyboards(keyboards): print(type(e)) # the exception instance print(e.args) # arguments stored in .args print(e) # __str__ allows args to be printed directly, pass - pass def determine_filetype(kblist, filename): @@ -148,7 +144,7 @@ def determine_filetype(kblist, filename): return KMFileTypes.KM_KMX elif ext.lower() == ".kvk": return KMFileTypes.KM_OSK - elif ext.lower() == ".ttf" or ext.lower() == ".otf": + elif ext.lower() in [".ttf", ".otf"]: return KMFileTypes.KM_FONT elif ext.lower() == ".js": if kblist is None: @@ -156,24 +152,31 @@ def determine_filetype(kblist, filename): if name in kblist: return KMFileTypes.KM_TOUCH else: - if name == "keyrenderer": # currently 2018-09-21 this is the own known non touch js file - return KMFileTypes.KM_DOC - else: - return KMFileTypes.KM_UNKNOWN - elif ext.lower() == ".txt" or ext.lower() == ".pdf" or ext.lower() == ".htm" \ - or ext.lower() == ".html" or ext.lower() == ".doc" or ext.lower() == ".docx" \ - or ext.lower() == ".css" or ext.lower() == ".chm" or ext.lower() == "" \ - or ext.lower() == ".md" or ext.lower() == ".odt" or ext.lower() == ".rtf" \ - or ext.lower() == ".dot" or ext.lower() == ".mht" or ext.lower() == ".woff" \ - or ext.lower() == ".php": + return KMFileTypes.KM_DOC if name == "keyrenderer" else KMFileTypes.KM_UNKNOWN + elif ext.lower() in [ + ".txt", + ".pdf", + ".htm", + ".html", + ".doc", + ".docx", + ".css", + ".chm", + "", + ".md", + ".odt", + ".rtf", + ".dot", + ".mht", + ".woff", + ".php", + ]: return KMFileTypes.KM_DOC - elif ext.lower() == ".inf" or ext.lower() == ".json": + elif ext.lower() in [".inf", ".json"]: return KMFileTypes.KM_META - elif ext.lower() == ".png" or ext.lower() == ".jpeg" \ - or ext.lower() == ".jpg" or ext.lower() == ".gif" \ - or ext.lower() == ".bmp": + elif ext.lower() in [".png", ".jpeg", ".jpg", ".gif", ".bmp"]: return KMFileTypes.KM_IMAGE - elif ext.lower() == ".tec" or ext.lower() == ".map": + elif ext.lower() in [".tec", ".map"]: return KMFileTypes.KM_TECKIT elif ext.lower() == ".cct": return KMFileTypes.KM_CC @@ -206,7 +209,6 @@ def print_files(files, extracted_dir): print(type(e)) # the exception instance print(e.args) # arguments stored in .args print(e) # __str__ allows args to be printed directly, pass - pass def get_fonts(files): @@ -275,9 +277,9 @@ def parseinfdata(inffile, verbose=False): if not info: info = {} for item in config.items('Info'): - if item[0] == 'Name' or item[0] == 'name': + if item[0] in ['Name', 'name']: info['name'] = {'description': item[1].split("\"")[1]} - elif item[0] == 'Copyright' or item[0] == 'copyright': + elif item[0] in ['Copyright', 'copyright']: info['copyright'] = {'description': item[1].split("\"")[1]} elif item[0] == 'Version': info['version'] = {'description': item[1].split("\"")[1]} @@ -292,12 +294,12 @@ def parseinfdata(inffile, verbose=False): info = {} info['version'] = {'description': "1.0"} for item in config.items('PackageInfo'): - if item[0] == 'Name' or item[0] == 'name': + if item[0] in ['Name', 'name']: if item[1].split("\"")[1]: info['name'] = {'description': item[1].split("\"")[1]} else: info['name'] = {'description': item[1].split("\"")[2]} - elif item[0] == 'Copyright' or item[0] == 'copyright': + elif item[0] in ['Copyright', 'copyright']: if item[1].split("\"")[1]: info['copyright'] = {'description': item[1].split("\"")[1]} else: @@ -329,9 +331,7 @@ def parseinfdata(inffile, verbose=False): options['disableKeepFonts'] = item[1] elif item[0] == 'BothVersionsIncluded': options['bothVersionsIncluded'] = item[1] - elif item[0] == 'ExecuteProgram': - pass - else: + elif item[0] != 'ExecuteProgram': print("Unknown item in Package:", item[0]) system['keymanDeveloperVersion'] = "" elif "Keyboard" in section: @@ -391,9 +391,9 @@ def parseinfdata(inffile, verbose=False): if kbfile['type'] == KMFileTypes.KM_KMX: id = os.path.basename(os.path.splitext(kbfile['name'])[0]) keyboards = [{ - 'name': id, - 'id': id, - 'version': secure_lookup(info, 'version', 'description') + 'name': id, + 'id': id, + 'version': secure_lookup(info, 'version', 'description') }] kblist = [] @@ -469,22 +469,21 @@ def parsemetadata(jsonfile, verbose=False): data = {"nonexistent": "none"} for x in data: - if x == 'info': + if x == 'files': + files = data[x] + elif x == 'info': info = data[x] - elif x == 'system': - system = data[x] elif x == 'keyboards': keyboards = data[x] - elif x == 'files': - files = data[x] - elif x == 'options': - options = data[x] elif x == 'nonexistent': nonexistent = data[x] + elif x == 'options': + options = data[x] + elif x == 'system': + system = data[x] kblist = [] if keyboards: - for k in keyboards: - kblist.append(k['id']) + kblist.extend(k['id'] for k in keyboards) if files: for kbfile in files: kbfile['type'] = determine_filetype(kblist, kbfile['name']) diff --git a/linux/keyman-config/keyman_config/list_installed_kmp.py b/linux/keyman-config/keyman_config/list_installed_kmp.py index d0a07247af64705cd604e568bdd4972e12aab4bd..2b6a32acc42b023c4f0dd7e5471af711761631b3 100755 --- a/linux/keyman-config/keyman_config/list_installed_kmp.py +++ b/linux/keyman-config/keyman_config/list_installed_kmp.py @@ -3,6 +3,7 @@ import json import logging import os +from pkg_resources import parse_version from keyman_config import secure_lookup from keyman_config.deprecated_decorator import deprecated @@ -30,8 +31,8 @@ def get_installed_kmp(area): id (str): Keyboard ID name (str): Keyboard name kmpname (str): Keyboard name in local - version (str): Keyboard version - kmpversion (str): + version (str): available Keyboard version + kmpversion (str): installed Keyboard version path (str): base path where keyboard is installed description (str): Keyboard description """ @@ -122,32 +123,21 @@ def get_kmp_version(packageID): None: if not found """ version = None - user_kmp = get_installed_kmp(InstallLocation.User) - shared_kmp = get_installed_kmp(InstallLocation.Shared) - os_kmp = get_installed_kmp(InstallLocation.OS) - - if packageID in os_kmp: - version = os_kmp[packageID]['version'] - - if packageID in shared_kmp: - shared_version = shared_kmp[packageID]['version'] - if version: - if version < shared_version: - version = shared_version - else: - version = shared_version - - if packageID in user_kmp: - user_version = user_kmp[packageID]['version'] - if version: - if version < user_version: - version = user_version - else: - version = user_version - + version = _get_kmp_version_internal(packageID, InstallLocation.OS, version) + version = _get_kmp_version_internal(packageID, InstallLocation.Shared, version) + version = _get_kmp_version_internal(packageID, InstallLocation.User, version) return version +def _get_kmp_version_internal(packageId, location, previousVersion): + kmp = get_installed_kmp(location) + if packageId in kmp: + version = kmp[packageId]['kmpversion'] + if not previousVersion or parse_version(previousVersion) < parse_version(version): + previousVersion = version + return previousVersion + + def get_kmp_version_user(packageID): """ Get version of the kmp for a kmp ID. @@ -159,11 +149,7 @@ def get_kmp_version_user(packageID): str: kmp version if kmp ID is installed None: if not found """ - user_kmp = get_installed_kmp(InstallLocation.User) - if packageID in user_kmp: - return user_kmp[packageID]['version'] - else: - return None + return _get_kmp_version_internal(packageID, InstallLocation.User, None) def get_kmp_version_shared(packageID): @@ -177,8 +163,4 @@ def get_kmp_version_shared(packageID): str: kmp version if kmp ID is installed None: if not found """ - shared_kmp = get_installed_kmp(InstallLocation.Shared) - if packageID in shared_kmp: - return shared_kmp[packageID]['version'] - else: - return None + return _get_kmp_version_internal(packageID, InstallLocation.Shared, None) diff --git a/linux/keyman-config/keyman_config/options_widget.py b/linux/keyman-config/keyman_config/options_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..889483733e37422d9f81e6705773bfbadc91d776 --- /dev/null +++ b/linux/keyman-config/keyman_config/options_widget.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 + +import gi + +gi.require_version('Gtk', '3.0') + +from gi.repository import Gtk + +from keyman_config import _ + + +class OptionsWidget(Gtk.Box): + def __init__(self, sentry) -> None: + super().__init__(orientation=Gtk.Orientation.VERTICAL) + label = Gtk.Label(_("General")) + label.set_padding(5, 5) + label.set_halign(Gtk.Align.START) + self.pack_start(label, False, False, 10) + + (enabled, reason) = sentry.is_sentry_enabled() + disabledByVariable = sentry.is_sentry_disabled_by_variable() + + self.errorReportingButton = Gtk.CheckButton(_("Automatically report errors to keyman.com")) + self.errorReportingButton.set_active(enabled) + self.errorReportingButton.set_sensitive(not disabledByVariable) + sentry.bind_checkbutton(self.errorReportingButton) + self.pack_start(self.errorReportingButton, False, False, 0) + + if disabledByVariable: + label = Gtk.Label(reason) + label.set_halign(Gtk.Align.START) + label.set_padding(25, 0) + label.set_sensitive(False) + self.pack_start(label, False, False, 0) diff --git a/linux/keyman-config/keyman_config/sentry_handling.py b/linux/keyman-config/keyman_config/sentry_handling.py new file mode 100644 index 0000000000000000000000000000000000000000..6cff26b1a509a1cf30cf3d3cf47cedd0def6857e --- /dev/null +++ b/linux/keyman-config/keyman_config/sentry_handling.py @@ -0,0 +1,151 @@ +#!/usr/bin/python3 +import getpass +import importlib +import logging +import os +import platform +import sys +from keyman_config.version import ( + __version__, + __versionwithtag__, + __versiongittag__, + __majorversion__, + __releaseversion__, + __tier__, + __pkgversion__, + __environment__, + __uploadsentry__ +) + +import gi +gi.require_version('Gtk', '3.0') +from gi.repository import Gio, Gtk + + +class SentryErrorHandling: + def __init__(self) -> None: + self.settings = Gio.Settings.new('com.keyman.options') + + def initialize_sentry(self): + (enabled, reason) = self.is_sentry_enabled() + if not enabled: + print(reason, file=sys.stderr) + logging.info(reason) + return (enabled, reason) + else: + try: + self._sentry_sdk_initialize() + except ImportError: + try: + self._raven_initialize() + except ImportError: + # even raven is not available. This is the case on Ubuntu 16.04. Just ignore. + print(_('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.'), + file=sys.stderr) + logging.info('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.') + return (False, _('Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting.')) + return (True, '') + + def is_sentry_enabled(self): + if 'unittest' in sys.modules.keys(): + return (False, 'Running unit tests, not reporting to Sentry') + elif self._get_environ_nosentry(): + return (False, 'Not reporting to Sentry because KEYMAN_NOSENTRY environment variable set') + elif not __uploadsentry__: + return (False, f'Not reporting to Sentry because UPLOAD_SENTRY is false ({__environment__})') + elif not self._get_setting(): + return (False, 'Not reporting to Sentry because disabled in GSettings') + return (True, 'Reporting to Sentry') + + def is_sentry_disabled_by_variable(self): + return self._get_environ_nosentry() or not __uploadsentry__ + + def bind_checkbutton(self, button: Gtk.CheckButton): + self.settings.bind("error-reporting", button, "active", Gio.SettingsBindFlags.NO_SENSITIVITY) + self.settings.connect("changed::error-reporting", self._on_sentry_reporting_toggled) + + def set_enabled(self, enabled): + assert not self.is_sentry_disabled_by_variable() + was_enabled = self.is_sentry_enabled() + self._save_setting(enabled) + if enabled != was_enabled: + self._handle_enabled(enabled) + + def _get_environ_nosentry(self): + keyman_nosentry = os.environ.get('KEYMAN_NOSENTRY') + return keyman_nosentry and (int(keyman_nosentry) == 1) + + def _handle_enabled(self, enabled): + if enabled: + self.initialize_sentry() + else: + self._close_sentry() + + def _save_setting(self, enabled: bool): + self.settings.set_boolean('error-reporting', enabled) + + def _get_setting(self) -> bool: + return self.settings.get_boolean('error-reporting') + + def _on_sentry_reporting_toggled(self, settings, key): + self._handle_enabled(self.settings.get_boolean('error-reporting')) + + def _close_sentry(self): + from sentry_sdk import Hub + logging.info("Shutting down Sentry error reporting") + client = Hub.current.client + if client is not None: + client.close(timeout=2.0) + + def _sentry_sdk_initialize(self): + # Try new sentry-sdk first + sentry_sdk = importlib.import_module('sentry_sdk') + from sentry_sdk import configure_scope, set_user + from sentry_sdk.integrations.logging import LoggingIntegration + + sentry_logging = LoggingIntegration( + level=logging.INFO, # Capture info and above as breadcrumbs + event_level=logging.CRITICAL # Send critical errors as events + ) + SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357@o1005580.ingest.sentry.io/5983525" + sentry_sdk.init( + dsn=SentryUrl, + environment=__environment__, + release=__versiongittag__, + integrations=[sentry_logging], + before_send=self._before_send + ) + set_user({'id': hash(getpass.getuser())}) + with configure_scope() as scope: + scope.set_tag("app", os.path.basename(sys.argv[0])) + scope.set_tag("pkgversion", __pkgversion__) + scope.set_tag("platform", platform.platform()) + scope.set_tag("system", platform.system()) + scope.set_tag("tier", __tier__) + logging.info("Initialized Sentry error reporting") + + def _raven_initialize(self): + # sentry-sdk is not available, so use older raven + raven = importlib.import_module('raven') + from raven import Client + + # Note, legacy raven API requires secret (https://github.com/keymanapp/keyman/pull/5787#discussion_r721457909) + SentryUrl = "https://1d0edbf2d0dc411b87119b6e92e2c357:e6d5a81ee6944fc79bd9f0cbb1f2c2a4@o1005580.ingest.sentry.io/5983525" + client = Client(SentryUrl, environment=__environment__, release=__versiongittag__) + client.user_context({'id': hash(getpass.getuser())}) + client.tags_context({ + 'app': os.path.basename(sys.argv[0]), + 'pkgversion': __pkgversion__, + 'platform': platform.platform(), + 'system': platform.system(), + 'tier': __tier__, + }) + logging.info("Initialized Sentry error reporting (raven)") + + def _before_send(self, event, hint): + if 'exc_info' in hint: + exc_type, exc_value, tb = hint['exc_info'] + if isinstance(exc_value, KeyboardInterrupt): + # Ignore KeyboardInterrupt exception + return None + return event diff --git a/linux/keyman-config/keyman_config/uninstall_kmp.py b/linux/keyman-config/keyman_config/uninstall_kmp.py index 8eaac8f57868dcde149e5d549c491eeaec0845c7..967a3ba7dcc77e885d8712bbc69c4805c7ddb005 100755 --- a/linux/keyman-config/keyman_config/uninstall_kmp.py +++ b/linux/keyman-config/keyman_config/uninstall_kmp.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +import getpass import logging import os from shutil import rmtree @@ -12,8 +13,7 @@ from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir, from keyman_config.gnome_keyboards_util import (GnomeKeyboardsUtil, get_ibus_keyboard_id, is_gnome_shell) -from keyman_config.ibus_util import (get_ibus_bus, restart_ibus, - uninstall_from_ibus) +from keyman_config.ibus_util import IbusUtil, get_ibus_bus, restart_ibus from keyman_config.kmpmetadata import get_metadata @@ -43,29 +43,30 @@ def _uninstall_dir(what, dir): logging.info('No %s directory %s', what, dir) -def _uninstall_kmp_common(location, packageID): +def _uninstall_kmp_common(location, packageID, removeLanguages): kbdir = get_keyboard_dir(location, packageID) kbdocdir = get_keyman_doc_dir(location, packageID) kbfontdir = get_keyman_font_dir(location, packageID) - where = 'local' - if location == InstallLocation.Shared: - where = 'shared' - - logging.info("Uninstalling %s keyboard: %s", where, packageID) + where = 'shared' if location == InstallLocation.Shared else 'user' info, system, options, keyboards, files = get_metadata(kbdir) - if keyboards: - if is_fcitx_running(): - _uninstall_keyboards_from_fcitx5() - elif is_gnome_shell(): - _uninstall_keyboards_from_gnome(keyboards, kbdir) + if removeLanguages: + logging.info(f'Uninstalling {where} keyboard: "{packageID}"') + if keyboards: + if is_fcitx_running(): + _uninstall_keyboards_from_fcitx5() + elif is_gnome_shell(): + _uninstall_keyboards_from_gnome(keyboards, kbdir) + else: + _uninstall_keyboards_from_ibus(keyboards, kbdir) else: - _uninstall_keyboards_from_ibus(keyboards, kbdir) + logging.info(f'Could not uninstall {where} keyboard "{packageID}" from list of keyboards') else: - logging.warning("could not uninstall keyboards") + logging.info(f'Replacing {where} keyboard: "{packageID}"') if not os.path.isdir(kbdir): - logging.error('Keyboard directory %s for %s does not exist.', kbdir, packageID) + logging.info(f'Keyboard directory {kbdir} for "{packageID}" does not exist.') + logging.warning(f'Cannot uninstall non-existing {where} keyboard "{packageID}" for user {getpass.getuser()}') _uninstall_dir('Keyman keyboards', kbdir) _uninstall_dir('documentation', kbdocdir) @@ -76,19 +77,27 @@ def _uninstall_kmp_common(location, packageID): logging.error(msg) return msg - logging.info("Finished uninstalling %s keyboard: %s", where, packageID) + logging.info(f'Finished uninstalling {where} keyboard: "{packageID}"') return '' + def _uninstall_keyboards_from_ibus(keyboards, packageDir): bus = get_ibus_bus() - if bus or os.environ.get('SUDO_USER'): - # install all kmx for first lang not just packageID - for kb in keyboards: - ibus_keyboard_id = get_ibus_keyboard_id(kb, packageDir) - uninstall_from_ibus(bus, ibus_keyboard_id) - restart_ibus(bus) - else: - logging.warning("could not uninstall keyboards from IBus") + ibusUtil = IbusUtil() + sources = ibusUtil.read_preload_engines() + if not sources: + return + + # uninstall all specified keyboards for all languages + for kb in keyboards: + # keyboard ids are similar to `km:/path/to/keyman/khmer_angkor/khmer_angkor.kmx` + match_id = ":%s" % get_ibus_keyboard_id(kb, packageDir, ignore_language=True) + toRemove = [id for id in sources if id.endswith(match_id)] + for val in toRemove: + sources.remove(val) + + ibusUtil.write_preload_engines(bus, sources) + restart_ibus(bus) def _uninstall_keyboards_from_gnome(keyboards, packageDir): @@ -98,12 +107,12 @@ def _uninstall_keyboards_from_gnome(keyboards, packageDir): # uninstall all kmx for all languages for kb in keyboards: ibus_keyboard_id = get_ibus_keyboard_id(kb, packageDir) - tuple = ('ibus', ibus_keyboard_id) - if tuple in sources: - sources.remove(tuple) + _tuple = ('ibus', ibus_keyboard_id) + if _tuple in sources: + sources.remove(_tuple) - toRemove = [] match_id = ":%s" % get_ibus_keyboard_id(kb, packageDir, ignore_language=True) + toRemove = [] for (type, id) in sources: if type == 'ibus' and id.endswith(match_id): toRemove.append((type, id)) @@ -118,18 +127,21 @@ def _uninstall_keyboards_from_fcitx5(): return _('Please use fcitx5-configtool to remove the keyboard from the group') -def uninstall_kmp(packageID, sharedarea=False): +def uninstall_kmp(packageID, sharedarea=False, removeLanguages=True): """ - Uninstall a kmp + Uninstall a Keyman keyboard package Args: packageID (str): Keyboard package ID - sharedarea (str): whether to uninstall from shared /usr/local or ~/.local + sharedarea (boolean): whether to uninstall from shared /usr/local or ~/.local + removeLanguages (boolean): if True also uninstall the languages associated with + the keyboard, otherwise only remove keyboard (i.e. in preparation for + replacing the keyboard with a newer version) """ if sharedarea: - msg = _uninstall_kmp_common(InstallLocation.Shared, packageID) + msg = _uninstall_kmp_common(InstallLocation.Shared, packageID, removeLanguages) else: - msg = _uninstall_kmp_common(InstallLocation.User, packageID) + msg = _uninstall_kmp_common(InstallLocation.User, packageID, removeLanguages) get_keyman_config_service().keyboard_list_changed() return msg diff --git a/linux/keyman-config/keyman_config/view_installed.py b/linux/keyman-config/keyman_config/view_installed.py index 01b1a59b89b3e413619c87abd56c479667abeab2..75a29952b99819c4fd7f0faf3317c031cbead0a8 100755 --- a/linux/keyman-config/keyman_config/view_installed.py +++ b/linux/keyman-config/keyman_config/view_installed.py @@ -2,12 +2,12 @@ import logging import os -import pathlib import subprocess import sys import gi + gi.require_version('Gtk', '3.0') gi.require_version('Gdk', '3.0') @@ -19,20 +19,19 @@ from keyman_config.accelerators import bind_accelerator, init_accel from keyman_config.dbus_util import get_keyman_config_service from keyman_config.downloadkeyboard import DownloadKmpWindow from keyman_config.get_kmp import (InstallLocation, get_keyboard_dir, - get_keyman_dir) + get_install_area_string) from keyman_config.install_window import InstallKmpWindow, find_keyman_image -from keyman_config.keyboard_details import KeyboardDetailsView -from keyman_config.kmpmetadata import get_fonts, parsemetadata +from keyman_config.keyboard_layouts_model import create_kbd_layouts_model +from keyman_config.keyboard_layouts_widget import KeyboardLayoutsWidget from keyman_config.list_installed_kmp import get_installed_kmp -from keyman_config.options import OptionsView -from keyman_config.uninstall_kmp import uninstall_kmp -from keyman_config.welcome import WelcomeView +from keyman_config.options_widget import OptionsWidget +from keyman_config.sentry_handling import SentryErrorHandling class ViewInstalledWindowBase(Gtk.Window): def __init__(self): self.accelerators = None - Gtk.Window.__init__(self, title=_("Keyman Configuration")) + super().__init__(title=_("Keyman Configuration")) init_accel(self) self._config_service = get_keyman_config_service(self.refresh_installed_kmp) @@ -94,7 +93,7 @@ class ViewInstalledWindowBase(Gtk.Window): self.close() def run(self): - self.resize(576, 324) + self.resize(776, 424) self.connect("destroy", Gtk.main_quit) self.show_all() Gtk.main() @@ -102,112 +101,50 @@ class ViewInstalledWindowBase(Gtk.Window): class ViewInstalledWindow(ViewInstalledWindowBase): def __init__(self): - ViewInstalledWindowBase.__init__(self) - -# window is split left/right hbox -# right is ButtonBox -# possibly 2 ButtonBox in a vbox -# top one with _Remove, _About, ?_Welcome? or ?Read_Me?, _Options -# bottom one with _Download, _Install, Re_fresh, _Close -# left is GtkTreeView - does it need to be inside anything else apart from the hbox? -# with liststore which defines columns -# GdkPixbuf icon -# gchararray name -# gchararray version -# gchararray packageID (hidden) -# enum? area (user, shared, system) (icon or hidden?) -# gchararray welcomefile (hidden) (or just use area and packageID?) -# changing selected item in treeview changes what buttons are activated -# on selected_item_changed signal set the data that the buttons will use in their callbacks -# see https://developer.gnome.org/gtk3/stable/TreeWidget.html#TreeWidget - - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - s = Gtk.ScrolledWindow() - hbox.pack_start(s, True, True, 0) - - self.store = Gtk.ListStore( - GdkPixbuf.Pixbuf, # icon - str, # name - str, # version - str, # packageID - int, # enum InstallLocation (KmpArea is GObject version) - str, # path to welcome file if it exists or None - str) # path to options file if it exists or None - - # add installed keyboards to the the store e.g. - # treeiter = store.append([GdkPixbuf.Pixbuf.new_from_file_at_size( - # "/usr/local/share/keyman/libtralo/libtralo.ico.png", 16, 16), \ - # "LIBTRALO", "1.6.1", \ - # "libtralo", KmpArea.SHARED, True]) - - self.refresh_installed_kmp() - - self.tree = Gtk.TreeView(self.store) - - renderer = Gtk.CellRendererPixbuf() - # i18n: column header in table displaying installed keyboards - column = Gtk.TreeViewColumn(_("Icon"), renderer, pixbuf=0) - self.tree.append_column(column) - renderer = Gtk.CellRendererText() - # i18n: column header in table displaying installed keyboards - column = Gtk.TreeViewColumn(_("Name"), renderer, text=1) - self.tree.append_column(column) - # i18n: column header in table displaying installed keyboards - column = Gtk.TreeViewColumn(_("Version"), renderer, text=2) - self.tree.append_column(column) - - select = self.tree.get_selection() - select.connect("changed", self.on_tree_selection_changed) + super().__init__() - s.add(self.tree) + self.sentry = SentryErrorHandling() - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + outmostVBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + outmostVBox.pack_start(self._add_stack_sidebar(), True, True, 0) + outmostVBox.pack_end(self._add_app_buttons(), False, True, 12) + self.add(outmostVBox) - bbox_top = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.VERTICAL) - bbox_top.set_layout(Gtk.ButtonBoxStyle.START) + def _add_stack_sidebar(self): + outerHbox = Gtk.HBox() + sidebar = Gtk.StackSidebar() + stack = Gtk.Stack() - self.uninstall_button = Gtk.Button.new_with_mnemonic(_("_Uninstall")) - self.uninstall_button.set_tooltip_text(_("Uninstall keyboard")) - self.uninstall_button.connect("clicked", self.on_uninstall_clicked) - self.uninstall_button.set_sensitive(False) - bbox_top.add(self.uninstall_button) + outerHbox.pack_start(sidebar, False, False, 0) + outerHbox.pack_end(Gtk.Separator(), False, False, 0) + outerHbox.pack_end(stack, True, True, 0) + stack.set_hexpand(True) + stack.set_vexpand(True) + sidebar.set_stack(stack) - self.about_button = Gtk.Button.new_with_mnemonic(_("_About")) - self.about_button.set_tooltip_text(_("About keyboard")) - self.about_button.connect("clicked", self.on_about_clicked) - self.about_button.set_sensitive(False) - bbox_top.add(self.about_button) - - self.help_button = Gtk.Button.new_with_mnemonic(_("_Help")) - self.help_button.set_tooltip_text(_("Help for keyboard")) - self.help_button.connect("clicked", self.on_help_clicked) - self.help_button.set_sensitive(False) - bbox_top.add(self.help_button) - - self.options_button = Gtk.Button.new_with_mnemonic(_("_Options")) - self.options_button.set_tooltip_text(_("Settings for keyboard")) - self.options_button.connect("clicked", self.on_options_clicked) - self.options_button.set_sensitive(False) - bbox_top.add(self.options_button) - - vbox.pack_start(bbox_top, False, False, 12) + self.store = create_kbd_layouts_model() + self.refresh_installed_kmp() + stack.add_titled(KeyboardLayoutsWidget(self, self.store, self.restart), "KeyboardLayouts", _("Keyboard Layouts")) + stack.add_titled(OptionsWidget(self.sentry), "Options", _("Options")) + return outerHbox - bbox_bottom = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.VERTICAL) - bbox_bottom.set_layout(Gtk.ButtonBoxStyle.END) + def _add_app_buttons(self): + bbox_bottom = Gtk.ButtonBox(spacing=12, orientation=Gtk.Orientation.HORIZONTAL) + bbox_bottom.set_layout(Gtk.ButtonBoxStyle.START) - button = Gtk.Button.new_with_mnemonic(_("_Refresh")) - button.set_tooltip_text(_("Refresh keyboard list")) - button.connect("clicked", self.on_refresh_clicked) + button = Gtk.Button.new_with_mnemonic(_("_Install keyboard...")) + button.set_tooltip_text(_("Install a keyboard from a file")) + button.connect("clicked", self.on_installfile_clicked) bbox_bottom.add(button) - button = Gtk.Button.new_with_mnemonic(_("_Download")) + button = Gtk.Button.new_with_mnemonic(_("_Download keyboard...")) button.set_tooltip_text(_("Download and install a keyboard from the Keyman website")) button.connect("clicked", self.on_download_clicked) bbox_bottom.add(button) - button = Gtk.Button.new_with_mnemonic(_("_Install")) - button.set_tooltip_text(_("Install a keyboard from a file")) - button.connect("clicked", self.on_installfile_clicked) + button = Gtk.Button.new_with_mnemonic(_("_Refresh")) + button.set_tooltip_text(_("Refresh keyboard list")) + button.connect("clicked", self.on_refresh_clicked) bbox_bottom.add(button) button = Gtk.Button.new_with_mnemonic(_("_Close")) @@ -217,16 +154,15 @@ class ViewInstalledWindow(ViewInstalledWindowBase): bind_accelerator(self.accelerators, button, 'w') bbox_bottom.add(button) - vbox.pack_end(bbox_bottom, False, False, 12) + bbox_hbox = Gtk.HBox(spacing=12) + bbox_hbox.pack_start(bbox_bottom, True, True, 12) + return bbox_hbox - hbox.pack_start(vbox, False, False, 12) - self.add(hbox) + def _addlistitems(self, installed_kmp, store, install_area): + bmppng = ".bmp.png" # Icon file extension - def addlistitems(self, installed_kmp, store, install_area): for kmp in sorted(installed_kmp): kmpdata = installed_kmp[kmp] - bmppng = ".bmp.png" # Icon file extension - path = get_keyboard_dir(install_area, kmpdata['packageID']) welcome_file = os.path.join(path, "welcome.htm") @@ -246,17 +182,19 @@ class ViewInstalledWindow(ViewInstalledWindowBase): icofile = GdkPixbuf.Pixbuf.new_from_file_at_size(icofile_name, 16, 16) except GError: _, value, _ = sys.exc_info() - logging.info("Error reading icon file %s: %s" % (icofile_name, value.message)) + logging.info(f"Error reading icon file {icofile_name}: {value.message}") icofile = None store.append([ - icofile, - kmpdata['name'], - kmpdata['version'], - kmpdata['packageID'], - install_area, - welcome_file, - options_file]) + icofile, + kmpdata['name'], + kmpdata['kmpversion'], + kmpdata['packageID'], + install_area, + get_install_area_string(install_area), + path.replace(os.path.expanduser('~'), '~'), + welcome_file, + options_file]) def refresh_installed_kmp(self): logging.debug("Refreshing listview") @@ -267,144 +205,19 @@ class ViewInstalledWindow(ViewInstalledWindowBase): kmpdata = user_kmp[kmp] if kmpdata["has_kbjson"] is False: self.incomplete_kmp.append(kmpdata) - self.addlistitems(user_kmp, self.store, InstallLocation.User) + self._addlistitems(user_kmp, self.store, InstallLocation.User) shared_kmp = get_installed_kmp(InstallLocation.Shared) for kmp in sorted(shared_kmp): kmpdata = shared_kmp[kmp] if kmpdata["has_kbjson"] is False: self.incomplete_kmp.append(kmpdata) - self.addlistitems(shared_kmp, self.store, InstallLocation.Shared) + self._addlistitems(shared_kmp, self.store, InstallLocation.Shared) os_kmp = get_installed_kmp(InstallLocation.OS) for kmp in sorted(os_kmp): kmpdata = os_kmp[kmp] if kmpdata["has_kbjson"] is False: self.incomplete_kmp.append(kmpdata) - self.addlistitems(os_kmp, self.store, InstallLocation.OS) - - def on_tree_selection_changed(self, selection): - model, treeiter = selection.get_selected() - if treeiter is not None: - self.uninstall_button.set_tooltip_text( - _("Uninstall keyboard {package}").format(package=model[treeiter][1])) - self.help_button.set_tooltip_text( - _("Help for keyboard {package}").format(package=model[treeiter][1])) - self.about_button.set_tooltip_text( - _("About keyboard {package}").format(package=model[treeiter][1])) - self.options_button.set_tooltip_text( - _("Settings for keyboard {package}").format(package=model[treeiter][1])) - logging.debug("You selected %s version %s", model[treeiter][1], model[treeiter][2]) - self.about_button.set_sensitive(True) - if model[treeiter][4] == InstallLocation.User: - logging.debug("Enabling uninstall button for %s in %s", model[treeiter][3], model[treeiter][4]) - self.uninstall_button.set_sensitive(True) - else: - self.uninstall_button.set_sensitive(False) - logging.debug("Disabling uninstall button for %s in %s", model[treeiter][3], model[treeiter][4]) - # welcome file if it exists - if model[treeiter][5]: - self.help_button.set_sensitive(True) - else: - self.help_button.set_sensitive(False) - # options file if it exists - if model[treeiter][6]: - self.options_button.set_sensitive(True) - else: - self.options_button.set_sensitive(False) - else: - self.uninstall_button.set_tooltip_text(_("Uninstall keyboard")) - self.help_button.set_tooltip_text(_("Help for keyboard")) - self.about_button.set_tooltip_text(_("About keyboard")) - self.options_button.set_tooltip_text(_("Settings for keyboard")) - self.uninstall_button.set_sensitive(False) - self.about_button.set_sensitive(False) - self.help_button.set_sensitive(False) - self.options_button.set_sensitive(False) - - def on_help_clicked(self, button): - model, treeiter = self.tree.get_selection().get_selected() - if treeiter is not None: - logging.info("Open welcome.htm for %s if available", model[treeiter][1]) - welcome_file = model[treeiter][5] - if welcome_file and os.path.isfile(welcome_file): - uri_path = pathlib.Path(welcome_file).as_uri() - logging.info("opening " + uri_path) - w = WelcomeView(self, uri_path, model[treeiter][3]) - w.run() - w.destroy() - else: - logging.info("welcome.htm not available") - - def on_options_clicked(self, button): - model, treeiter = self.tree.get_selection().get_selected() - if treeiter is not None: - logging.info("Open options.htm for %s if available", model[treeiter][1]) - options_file = model[treeiter][6] - if options_file and os.path.isfile(options_file): - uri_path = pathlib.Path(options_file).as_uri() - logging.info("opening " + uri_path) - # TODO: Determine keyboardID - info = {"optionurl": uri_path, "packageID": model[treeiter][3], "keyboardID": model[treeiter][3]} - w = OptionsView(info) - w.resize(800, 600) - w.show_all() - else: - logging.info("options.htm not available") - - def on_uninstall_clicked(self, button): - model, treeiter = self.tree.get_selection().get_selected() - if treeiter is not None: - logging.info("Uninstall keyboard " + model[treeiter][3] + "?") - dialog = Gtk.MessageDialog(self, 0, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, - _("Uninstall keyboard package?")) - msg = _("Are you sure that you want to uninstall the {keyboard} keyboard?").format( - keyboard=model[treeiter][1]) - kbdir = get_keyboard_dir(InstallLocation.User, model[treeiter][3]) - kmpjson = os.path.join(kbdir, "kmp.json") - if os.path.isfile(kmpjson): - info, system, options, keyboards, files = parsemetadata(kmpjson, False) - fonts = get_fonts(files) - if fonts: - # Fonts are optional - fontlist = "" - for font in fonts: - if 'description' in font: - if fontlist != "": - fontlist = fontlist + "\n" - if font['description'][:5] == "Font ": - fontdesc = font['description'][5:] - else: - fontdesc = font['description'] - fontlist = fontlist + fontdesc - msg += "\n\n" + _("The following fonts will also be uninstalled:\n") + fontlist - dialog.format_secondary_text(msg) - response = dialog.run() - dialog.destroy() - if response == Gtk.ResponseType.YES: - logging.info("Uninstalling keyboard" + model[treeiter][1]) - # can only uninstall with the gui from user area - msg = uninstall_kmp(model[treeiter][3]) - if not msg == '': - md = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, - Gtk.ButtonsType.OK, _("Uninstalling keyboard failed.\n\nError message: ") + msg) - md.run() - md.destroy() - logging.info("need to restart window after uninstalling a keyboard") - self.restart() - elif response == Gtk.ResponseType.NO: - logging.info("Not uninstalling keyboard " + model[treeiter][1]) - - def on_about_clicked(self, button): - model, treeiter = self.tree.get_selection().get_selected() - if treeiter is not None and model[treeiter] is not None: - logging.info("Show keyboard details of " + model[treeiter][1]) - areapath = get_keyman_dir(model[treeiter][4]) - kmp = { - "name": model[treeiter][1], "version": model[treeiter][2], - "packageID": model[treeiter][3], "areapath": areapath - } - w = KeyboardDetailsView(self, kmp) - w.run() - w.destroy() + self._addlistitems(os_kmp, self.store, InstallLocation.OS) if __name__ == '__main__': diff --git a/linux/keyman-config/km-config b/linux/keyman-config/km-config index 93268113465435d49d5308275e3d5918ba428801..dfdbb7d4517c570b9d5fd65d2febc36be0817862 100755 --- a/linux/keyman-config/km-config +++ b/linux/keyman-config/km-config @@ -3,24 +3,18 @@ import argparse import logging -from keyman_config import __versionwithtag__, __pkgversion__ +from keyman_config import __versionwithtag__, __pkgversion__, add_standard_arguments, initialize_logging, initialize_sentry from keyman_config.handle_install import download_and_install_package from keyman_config.ibus_util import verify_ibus_daemon +from keyman_config.view_installed import ViewInstalledWindow if __name__ == '__main__': - if __pkgversion__: - versionstring = "%s (package version %s)" % (__versionwithtag__, __pkgversion__) - else: - versionstring = "%s" % __versionwithtag__ parser = argparse.ArgumentParser(description='km-config shows the currently installed ' + 'Keyman keyboard packages and allows you to view ' + 'information about them. It enables you to download new ' + 'keyboard packages from the website or install from ' + 'local files.') - parser.add_argument('--version', action='version', version='%(prog)s version ' + versionstring) - parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') - parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') parser.add_argument('-i', '--install', action='store', help='download and/or install .kmp ' + 'package. INSTALL can either be a downloaded .kmp file, a file:// URL ' + 'pointing to a .kmp file, or a keyman:// URL, possibly with a ' + @@ -32,14 +26,12 @@ if __name__ == '__main__': 'pointing to a .kmp file, or a keyman:// URL, possibly with a ' + 'bcp47= specified (e.g. keyman://download/keyboard/' + 'sil_el_ethiopian_latin?bcp47=ssy-latn).') + add_standard_arguments(parser) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') - elif args.veryverbose: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') - else: - logging.basicConfig(format='%(levelname)s:%(message)s') + + initialize_logging(args) + initialize_sentry() logging.info('Keyman version %s %s', __versionwithtag__, __pkgversion__) @@ -50,7 +42,6 @@ if __name__ == '__main__': elif args.url: download_and_install_package(args.url) else: - from keyman_config.view_installed import ViewInstalledWindow w = ViewInstalledWindow() try: w.run() diff --git a/linux/keyman-config/km-config.bash-completion b/linux/keyman-config/km-config.bash-completion index b1f46c0076f7522383eb66c51342b9b380f30ed5..fd0eee394c77b5bc89154870136e539e5aeaa925 100644 --- a/linux/keyman-config/km-config.bash-completion +++ b/linux/keyman-config/km-config.bash-completion @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. _km-config_completions() { diff --git a/linux/keyman-config/km-kvk2ldml b/linux/keyman-config/km-kvk2ldml index 7d5a1ca46f65e397cf021754922620c8fd2c9440..35b4f42785b963d019c83c4adc3f85f874784abc 100755 --- a/linux/keyman-config/km-kvk2ldml +++ b/linux/keyman-config/km-kvk2ldml @@ -5,7 +5,8 @@ import logging import os import sys -from keyman_config import __version__ +from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry +from keyman_config.kvk2ldml import parse_kvk_file, print_kvk, convert_ldml, output_ldml def main(): @@ -16,19 +17,12 @@ def main(): parser.add_argument('-k', "--keys", help='if printing also print all keys', action="store_true") parser.add_argument('kvkfile', help='kvk file') parser.add_argument('-o', '--output', metavar='LDMLFILE', help='output LDML file location') - parser.add_argument('--version', action='version', version='%(prog)s version ' + __version__) - parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') - parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + add_standard_arguments(parser) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') - elif args.veryverbose: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') - else: - logging.basicConfig(format='%(levelname)s:%(message)s') - from keyman_config.kvk2ldml import parse_kvk_file, print_kvk, convert_ldml, output_ldml + initialize_logging(args) + initialize_sentry() name, ext = os.path.splitext(args.kvkfile) # Check if input file extension is kvk diff --git a/linux/keyman-config/km-kvk2ldml.bash-completion b/linux/keyman-config/km-kvk2ldml.bash-completion index 3eb2950c390659f1ce058c66b06243e42713d36d..1e8231a1a1fb37044f5fba26951212c0db7292f2 100644 --- a/linux/keyman-config/km-kvk2ldml.bash-completion +++ b/linux/keyman-config/km-kvk2ldml.bash-completion @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. _km-kvk2ldml_completions() { diff --git a/linux/keyman-config/km-package-get b/linux/keyman-config/km-package-get index 2ad4517c6621eaa15758951b8d26df9bcf1b5bd3..bbca72093bc751bac288bdd927ab4bcda353a100 100755 --- a/linux/keyman-config/km-package-get +++ b/linux/keyman-config/km-package-get @@ -1,28 +1,22 @@ #!/usr/bin/python3 import argparse -import logging import os -from keyman_config import __version__ +from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry +from keyman_config.get_kmp import get_kmp, keyman_cache_dir def main(): parser = argparse.ArgumentParser(description='Download Keyman keyboard package to ~/.cache/keyman') parser.add_argument('id', help='Keyman keyboard id') - parser.add_argument('--version', action='version', version='%(prog)s version ' + __version__) - parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') - parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + add_standard_arguments(parser) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') - elif args.veryverbose: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') - else: - logging.basicConfig(format='%(levelname)s:%(message)s') - - from keyman_config.get_kmp import get_kmp, keyman_cache_dir + + initialize_logging(args) + initialize_sentry() + get_kmp(args.id) if os.path.exists(os.path.join(keyman_cache_dir(), 'kmpdirlist')): os.remove(os.path.join(keyman_cache_dir(), 'kmpdirlist')) diff --git a/linux/keyman-config/km-package-get.bash-completion b/linux/keyman-config/km-package-get.bash-completion index 9bacb1b8fef29c8b7875131a0fd487a4919f0115..0fb3a4d14c76b1600d3f951ff0c44fac8af2df94 100644 --- a/linux/keyman-config/km-package-get.bash-completion +++ b/linux/keyman-config/km-package-get.bash-completion @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. _km-package-get_completions() { diff --git a/linux/keyman-config/km-package-install b/linux/keyman-config/km-package-install index 707e144c1f1d165847e8b64a9d9c9f24753507d8..613e00368d06a5dbeef0e8db7ccc6d1466e5f44a 100755 --- a/linux/keyman-config/km-package-install +++ b/linux/keyman-config/km-package-install @@ -4,25 +4,24 @@ import argparse import datetime import logging import os +import requests +import requests_cache import sys import time from pkg_resources import parse_version from zipfile import is_zipfile -from keyman_config import KeymanApiUrl, __version__, secure_lookup -from keyman_config.install_kmp import extract_kmp +from keyman_config import KeymanApiUrl, add_standard_arguments, initialize_logging, initialize_sentry, secure_lookup +from keyman_config.get_kmp import get_keyboard_data, get_kmp, keyman_cache_dir +from keyman_config.install_kmp import extract_kmp, InstallError, InstallStatus, install_kmp from keyman_config.kmpmetadata import get_metadata +from keyman_config.list_installed_kmp import get_kmp_version_shared, get_kmp_version_user from keyman_config.uninstall_kmp import uninstall_kmp def get_languages(): - import requests - import requests_cache - - from keyman_config.get_kmp import keyman_cache_dir - logging.info("Getting language list from search") - api_url = KeymanApiUrl + "/search?q=l:*&platform=linux" + api_url = f"{KeymanApiUrl}/search?q=l:*&platform=linux" logging.debug("At URL %s", api_url) cache_dir = keyman_cache_dir() current_dir = os.getcwd() @@ -37,10 +36,7 @@ def get_languages(): logging.debug("Time: {0} / Used Cache: {1}".format(now, response.from_cache)) os.chdir(current_dir) requests_cache.uninstall_cache() - if response.status_code == 200: - return response.json() - else: - return None + return response.json() if response.status_code == 200 else None except Exception: return None @@ -66,16 +62,14 @@ def get_keyboard_list(): def write_kmpdirlist(kmpdirfile): try: with open(kmpdirfile, 'wt') as kmpdirlist: - keyboards = get_keyboard_list() - if keyboards: - for kb in get_keyboard_list(): + if keyboards := get_keyboard_list(): + for kb in keyboards: print(kb, file=kmpdirlist) except Exception as e: logging.warning('Exception %s writing %s %s', type(e), kmpdirfile, e.args) def list_keyboards(): - from keyman_config.get_kmp import keyman_cache_dir kmpdirfile = os.path.join(keyman_cache_dir(), 'kmpdirlist') if not os.path.exists(kmpdirfile): write_kmpdirlist(kmpdirfile) @@ -87,10 +81,9 @@ def list_keyboards(): def _list_languages_for_keyboard_impl(packageId, packageDir): - from keyman_config.get_kmp import keyman_cache_dir kmpfile = os.path.join(keyman_cache_dir(), packageId) if not os.path.exists(kmpfile): - kmpfile = kmpfile + '.kmp' + kmpfile = f'{kmpfile}.kmp' if not os.path.exists(kmpfile): return '' extract_kmp(kmpfile, packageDir) @@ -123,17 +116,14 @@ def main(): parser.add_argument('-f', '--file', metavar='KMPFILE', help='Keyman kmp file') parser.add_argument('-p', '--package', metavar='PACKAGE', help='Keyman package id') parser.add_argument('-l', '--bcp47', metavar='LANGTAG', help='bcp47 language tag') - parser.add_argument('--version', action='version', version='%(prog)s version ' + __version__) - parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') - parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + parser.add_argument('--force', action='store_true', help='force installation of keyboard even if it is ' + + 'a downgrade to an older version of the keyboard') + add_standard_arguments(parser) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') - elif args.veryverbose: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') - else: - logging.basicConfig(format='%(levelname)s:%(message)s') + + initialize_logging(args) + initialize_sentry() if args.package and args.file: parser.print_usage() @@ -142,12 +132,6 @@ def main(): "specify a keyboard id to download and install.") sys.exit(2) - from keyman_config.get_kmp import (get_keyboard_data, get_kmp, - keyman_cache_dir) - from keyman_config.install_kmp import (InstallError, InstallStatus, - install_kmp) - from keyman_config.list_installed_kmp import get_kmp_version_shared, get_kmp_version_user - if os.path.exists(os.path.join(keyman_cache_dir(), 'kmpdirlist')): os.remove(os.path.join(keyman_cache_dir(), 'kmpdirlist')) @@ -173,39 +157,40 @@ def main(): logging.error("km-package-install: Keyman kmp file %s not found.", args.file) logging.error("km-package-install -f ") sys.exit(2) - try_install_kmp(args.file, "file " + args.file, args.bcp47, args.shared) + try_install_kmp(args.file, f"file {args.file}", args.bcp47, args.shared) elif args.package: if args.shared: if get_kmp_version_user(args.package): - print("km-package-install: Warning: Keyboard %s is also installed for the current user." - % args.package) - installed_kmp_v = get_kmp_version_shared(args.package) + logging.warning(f'km-package-install: Warning: Keyboard {args.package} is ' + + 'also installed for the current user.') + installed_kmp_ver = get_kmp_version_shared(args.package) else: if get_kmp_version_shared(args.package): - print("km-package-install: Warning: Keyboard %s is already installed in the shared area." - % args.package) - installed_kmp_v = get_kmp_version_user(args.package) - kbdata = get_keyboard_data(args.package) - if not kbdata: + logging.warning(f'km-package-install: Warning: Keyboard {args.package} is ' + + 'already installed in the shared area.') + installed_kmp_ver = get_kmp_version_user(args.package) + pkgdata = get_keyboard_data(args.package) + if not pkgdata: logging.error("km-package-install: error: Could not download keyboard data for %s", args.package) sys.exit(3) - kbdata_v = secure_lookup(kbdata, 'version') - if installed_kmp_v and kbdata_v: - kbdata_version = parse_version(kbdata_v) - installed_kmp_ver = parse_version(installed_kmp_v) - if kbdata_version == installed_kmp_ver: - print("km-package-install: Version %s of the %s keyboard package is already installed." - % (installed_kmp_ver, args.package)) - sys.exit(1) - elif kbdata_version > installed_kmp_ver: - print("km-package-install: A newer version of %s keyboard package is available. Uninstalling old " - "version %s then downloading and installing new version %s." - % (args.package, installed_kmp_ver, kbdata_version)) - uninstall_kmp(args.package, args.shared) + pkg_version = secure_lookup(pkgdata, 'version') + if installed_kmp_ver and pkg_version: + if parse_version(installed_kmp_ver) > parse_version(pkg_version): + if args.force: + logging.warning(f'km-package-install: warning: The {args.package} keyboard ' + + f'is already installed with a newer version {installed_kmp_ver}. ' + + f'Downgrading to version {pkg_version}.') + else: + logging.error(f'km-package-install: error: The {args.package} keyboard is ' + + f'already installed with a newer version {installed_kmp_ver}. ' + + f'Not installing version {pkg_version}. Use `--force` to downgrade.') + sys.exit(1) + if args.force: + uninstall_kmp(args.package, args.shared, False) kmpfile = get_kmp(args.package) if kmpfile: - try_install_kmp(kmpfile, "keyboard package " + args.package, args.bcp47, args.shared) + try_install_kmp(kmpfile, f"keyboard package {args.package}", args.bcp47, args.shared) else: logging.error("km-package-install: error: Could not download keyboard package %s", args.package) sys.exit(2) diff --git a/linux/keyman-config/km-package-install.bash-completion b/linux/keyman-config/km-package-install.bash-completion index 1a424e8ea6f80c9bb66b97923a38562b33dd573d..6178b5ebfd218c7ba6a805e0aecaf1c7a36b4544 100644 --- a/linux/keyman-config/km-package-install.bash-completion +++ b/linux/keyman-config/km-package-install.bash-completion @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. _km-package-install_completions() { @@ -6,7 +7,7 @@ _km-package-install_completions() COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - opts="-h --help -v --verbose -vv --veryverbose --version -p --package -f --file -s --shared -l --bcp47" + opts="-h --help -v --verbose -vv --veryverbose --version -p --package -f --file -s --shared -l --bcp47 --force" cache=${XDG_CACHE_HOME:-~/.cache}/keyman/kmpdirlist diff --git a/linux/keyman-config/km-package-list-installed b/linux/keyman-config/km-package-list-installed index 7496e1b7a856d39736487f003b7515d130f5b238..c6276c77a396c7290094de51689d42ebd96fa5f5 100755 --- a/linux/keyman-config/km-package-list-installed +++ b/linux/keyman-config/km-package-list-installed @@ -1,9 +1,11 @@ #!/usr/bin/python3 import argparse -import logging import os -from keyman_config import __version__ + +from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry +from keyman_config.get_kmp import get_keyman_dir, InstallLocation +from keyman_config.list_installed_kmp import get_installed_kmp def main(): @@ -12,29 +14,22 @@ def main(): parser.add_argument('-s', "--shared", help='show keyboard packages installed in shared area', action="store_true") parser.add_argument('-o', "--os", help='show keyboard packages installed by the OS', action="store_true") parser.add_argument('-u', "--user", help='show keyboard packages installed in user area', action="store_true") - parser.add_argument('--version', action='version', version='%(prog)s version ' + __version__) - parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') - parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + add_standard_arguments(parser) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') - elif args.veryverbose: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') - else: - logging.basicConfig(format='%(levelname)s:%(message)s') - all = not args.user and not args.shared and not args.os - from keyman_config.list_installed_kmp import get_installed_kmp - from keyman_config.get_kmp import get_keyman_dir, InstallLocation - if args.user or all: + initialize_logging(args) + initialize_sentry() + + _all = not args.user and not args.shared and not args.os + if args.user or _all: installed_kmp = get_installed_kmp(InstallLocation.User) if args.verbose or args.veryverbose: print("--- Installed user Keyman keyboard packages (%s) ---" % (get_keyman_dir(InstallLocation.User))) else: print("--- Installed user Keyman keyboard packages ---") print_keyboards(installed_kmp, args.long) - if args.shared or all: + if args.shared or _all: installed_kmp = get_installed_kmp(InstallLocation.Shared) if args.verbose or args.veryverbose: print("--- Installed shared Keyman keyboard packages (%s) ---" % @@ -42,7 +37,7 @@ def main(): else: print("--- Installed shared Keyman keyboard packages ---") print_keyboards(installed_kmp, args.long) - if args.os or all: + if args.os or _all: installed_kmp = get_installed_kmp(InstallLocation.OS) if args.verbose or args.veryverbose: print("--- Installed OS Keyman keyboard packages (%s) ---" % (get_keyman_dir(InstallLocation.OS))) @@ -51,12 +46,12 @@ def main(): print_keyboards(installed_kmp, args.long) -def print_keyboards(installed_kmp, verbose): +def print_keyboards(installed_kmp, detailed): for packageID in sorted(installed_kmp): - print(installed_kmp[packageID]['name'] + ", version:", installed_kmp[packageID]['version'] + ", id:", packageID) - if verbose: + print(f"{installed_kmp[packageID]['name']}, version: {installed_kmp[packageID]['kmpversion']}, id: {packageID}") + if detailed: if installed_kmp[packageID]['version'] != installed_kmp[packageID]['kmpversion']: - print("Version mismatch. Installed keyboard package is %s. Website says it is %s." + print("New version of keyboard available. Installed keyboard package is %s. Website has %s." % (installed_kmp[packageID]['kmpversion'], installed_kmp[packageID]['version'])) if installed_kmp[packageID]['name'] != installed_kmp[packageID]['kmpname']: print("Name mismatch. Installed keyboard package is %s. Website says it is %s." diff --git a/linux/keyman-config/km-package-list-installed.bash-completion b/linux/keyman-config/km-package-list-installed.bash-completion index a3758b467eb8958d0af62dc48148249d9a69f0b5..672353b9a13a86838627bf067bf69af559cba584 100644 --- a/linux/keyman-config/km-package-list-installed.bash-completion +++ b/linux/keyman-config/km-package-list-installed.bash-completion @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. _km-package-list-installed_completions() { diff --git a/linux/keyman-config/km-package-uninstall b/linux/keyman-config/km-package-uninstall index ff86660473ab81ce079bf239ae15c0f0c748f126..5ccc32c2a95c1929cbb56c35a137f8b2b838542c 100755 --- a/linux/keyman-config/km-package-uninstall +++ b/linux/keyman-config/km-package-uninstall @@ -1,28 +1,21 @@ #!/usr/bin/python3 import argparse -import logging - -from keyman_config import __version__ +from keyman_config import add_standard_arguments, initialize_logging, initialize_sentry +from keyman_config.uninstall_kmp import uninstall_kmp def main(): parser = argparse.ArgumentParser(description='Uninstall a Keyman keyboard package.') parser.add_argument('id', help='Keyman keyboard id') parser.add_argument('-s', '--shared', action='store_true', help='Uninstall from shared area /usr/local') - parser.add_argument('--version', action='version', version='%(prog)s version ' + __version__) - parser.add_argument('-v', '--verbose', action='store_true', help='verbose logging') - parser.add_argument('-vv', '--veryverbose', action='store_true', help='very verbose logging') + add_standard_arguments(parser) args = parser.parse_args() - if args.verbose: - logging.basicConfig(level=logging.INFO, format='%(levelname)s:%(message)s') - elif args.veryverbose: - logging.basicConfig(level=logging.DEBUG, format='%(levelname)s:%(message)s') - else: - logging.basicConfig(format='%(levelname)s:%(message)s') - - from keyman_config.uninstall_kmp import uninstall_kmp + + initialize_logging(args) + initialize_sentry() + uninstall_kmp(args.id, args.shared) diff --git a/linux/keyman-config/km-package-uninstall.bash-completion b/linux/keyman-config/km-package-uninstall.bash-completion index 191e709eabaf2344951ff70f1919911f8ff1b6ed..4b81c38dacc9eaa9c0ec67e19415db9b5fb56983 100644 --- a/linux/keyman-config/km-package-uninstall.bash-completion +++ b/linux/keyman-config/km-package-uninstall.bash-completion @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +# shellcheck disable=SC2148 +# No hashbang for bash completion scripts! They are intended to be sourced, not executed. _km-package-uninstall_completions() { diff --git a/linux/keyman-config/locale/es_419.po b/linux/keyman-config/locale/es_419.po index 74c2ebfe081c4f2d824672894d64ca7c3da47fa0..89269a500385c52e59f98db10aa25c70161ffce5 100644 --- a/linux/keyman-config/locale/es_419.po +++ b/linux/keyman-config/locale/es_419.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: keyman\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2020-08-19 19:17+0200\n" -"PO-Revision-Date: 2022-10-25 04:29\n" +"PO-Revision-Date: 2023-06-19 17:25\n" "Last-Translator: \n" "Language-Team: Spanish, Latin America\n" "Language: es_419\n" diff --git a/linux/keyman-config/locale/es_ES.po b/linux/keyman-config/locale/es_ES.po new file mode 100644 index 0000000000000000000000000000000000000000..e3652606ba031b072ef9e2fb4a638db0cae0c2f7 --- /dev/null +++ b/linux/keyman-config/locale/es_ES.po @@ -0,0 +1,332 @@ +msgid "" +msgstr "" +"Project-Id-Version: keyman\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-08-19 19:17+0200\n" +"PO-Revision-Date: 2023-06-19 16:36\n" +"Last-Translator: \n" +"Language-Team: Spanish\n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Crowdin-Project: keyman\n" +"X-Crowdin-Project-ID: 386703\n" +"X-Crowdin-Language: es-ES\n" +"X-Crowdin-File: /master/linux/keyman-config.pot\n" +"X-Crowdin-File-ID: 504\n" + +#: keyman_config/__init__.py:68 +msgid "Neither sentry-sdk nor raven is available. Not enabling Sentry error reporting." +msgstr "Ni sentry-sdk ni raven están disponibles. No habilitar la notificación de errores de Sentry." + +#: keyman_config/downloadkeyboard.py:23 +msgid "Download Keyman keyboards" +msgstr "Descargar teclados Keyman" + +#: keyman_config/downloadkeyboard.py:37 keyman_config/keyboard_details.py:49 +#: keyman_config/keyboard_details.py:340 keyman_config/view_installed.py:205 +msgid "_Close" +msgstr "_Cerrar" + +#: keyman_config/install_kmp.py:99 +msgid "You do not have permissions to install the keyboard files to the shared area /usr/local/share/keyman" +msgstr "No tiene permisos para instalar los archivos del teclado en el área compartida /usr/local/share/keyman" + +#: keyman_config/install_kmp.py:103 +msgid "You do not have permissions to install the documentation to the shared documentation area /usr/local/share/doc/keyman" +msgstr "No tiene permisos para instalar la documentación en el área de documentación compartida /usr/local/share/doc/keyman" + +#: keyman_config/install_kmp.py:107 +msgid "You do not have permissions to install the font files to the shared font area /usr/local/share/fonts" +msgstr "No tiene permisos para instalar los archivos de fuentes en el área de fuentes compartidas /usr/local/share/fonts" + +#: keyman_config/install_kmp.py:179 +#, python-brace-format +msgid "install_kmp.py: error: No kmp.json or kmp.inf found in {package}" +msgstr "install_kmp.py: error: No se encontró kmp.json o kmp.inf en {package}" + +#: keyman_config/install_kmp.py:246 +#, python-brace-format +msgid "install_kmp.py: error: No kmp.json or kmp.inf found in {packageFile}" +msgstr "install_kmp.py: error: No se encontró kmp.json o kmp.inf en {packageFile}" + +#: keyman_config/install_window.py:54 +#, python-brace-format +msgid "Installing keyboard/package {keyboardid}" +msgstr "Instalación del teclado/paquete {keyboardid}" + +#: keyman_config/install_window.py:72 keyman_config/install_window.py:93 +msgid "Keyboard is installed already" +msgstr "El teclado ya está instalado" + +#: keyman_config/install_window.py:74 +#, python-brace-format +msgid "The {name} keyboard is already installed at version {version}. Do you want to uninstall then reinstall it?" +msgstr "El teclado {name} ya está instalado en la versión {version}. ¿Desea desinstalarlo y volver a instalarlo?" + +#: keyman_config/install_window.py:95 +#, python-brace-format +msgid "The {name} keyboard is already installed with a newer version {installedversion}. Do you want to uninstall it and install the older version {version}?" +msgstr "El teclado {name} ya tiene instalada una versión más reciente {installedversion}. Desea desinstalarlo e instalar la versión anterior {version}?" + +#: keyman_config/install_window.py:128 +msgid "Keyboard layouts: " +msgstr "Disposición del teclado: " + +#: keyman_config/install_window.py:147 +msgid "Fonts: " +msgstr "Fuentes: " + +#: keyman_config/install_window.py:167 keyman_config/keyboard_details.py:96 +msgid "Package version: " +msgstr "Versión del paquete: " + +#: keyman_config/install_window.py:179 +msgid "Author: " +msgstr "Autor: " + +#: keyman_config/install_window.py:197 +msgid "Website: " +msgstr "Sitio Web: " + +#: keyman_config/install_window.py:211 +msgid "Copyright: " +msgstr "Derechos de Autor: " + +#: keyman_config/install_window.py:245 +msgid "Details" +msgstr "Detalles" + +#: keyman_config/install_window.py:248 +msgid "README" +msgstr "LEEME" + +#: keyman_config/install_window.py:256 keyman_config/view_installed.py:200 +msgid "_Install" +msgstr "_Instalar" + +#: keyman_config/install_window.py:260 +msgid "_Cancel" +msgstr "_Cancelar" + +#: keyman_config/install_window.py:305 +#, python-brace-format +msgid "Keyboard {name} installed" +msgstr "Teclado {name} instalado" + +#: keyman_config/install_window.py:310 keyman_config/install_window.py:315 +#, python-brace-format +msgid "Keyboard {name} could not be installed." +msgstr "El teclado {name} no pudo ser instalado." + +#: keyman_config/install_window.py:311 +msgid "Error Message:" +msgstr "Mensajes de error:" + +#: keyman_config/install_window.py:316 +msgid "Warning Message:" +msgstr "Mensaje de advertencia:" + +#: keyman_config/keyboard_details.py:37 +#, python-brace-format +msgid "{name} keyboard" +msgstr "{name} keyboard" + +#: keyman_config/keyboard_details.py:53 +msgid "ERROR: Keyboard metadata is damaged.\n" +"Please \"Uninstall\" and then \"Install\" the keyboard." +msgstr "ERROR: Los metadatos del teclado están dañados.\n" +"Por favor, \"Desinstalar\" y luego \"Instalar\" el teclado." + +#: keyman_config/keyboard_details.py:74 +msgid "Package name: " +msgstr "Nombre del paquete: " + +#: keyman_config/keyboard_details.py:85 +msgid "Package id: " +msgstr "Id del paquete: " + +#: keyman_config/keyboard_details.py:108 +msgid "Package description: " +msgstr "Descripción del paquete: " + +#: keyman_config/keyboard_details.py:121 +msgid "Package author: " +msgstr "Autor del paquete: " + +#: keyman_config/keyboard_details.py:133 +msgid "Package copyright: " +msgstr "Derechos de autor del paquete: " + +#: keyman_config/keyboard_details.py:174 +msgid "Keyboard filename: " +msgstr "Nombre de archivo del teclado: " + +#: keyman_config/keyboard_details.py:187 +msgid "Keyboard name: " +msgstr "Nombre del teclado: " + +#: keyman_config/keyboard_details.py:198 +msgid "Keyboard id: " +msgstr "Id del teclado: " + +#: keyman_config/keyboard_details.py:209 +msgid "Keyboard version: " +msgstr "Versión de teclado: " + +#: keyman_config/keyboard_details.py:221 +msgid "Keyboard author: " +msgstr "Autor del teclado: " + +#: keyman_config/keyboard_details.py:232 +msgid "Keyboard license: " +msgstr "Licencia de teclado: " + +#: keyman_config/keyboard_details.py:243 +msgid "Keyboard description: " +msgstr "Descripción del teclado: " + +#: keyman_config/keyboard_details.py:334 +#, python-brace-format +msgid "Scan this code to load this keyboard\n" +"on another device or share online" +msgstr "Escanea este código para cargar este teclado\n" +"en otro dispositivo o compartir en línea" + +#: keyman_config/options.py:24 +#, python-brace-format +msgid "{packageId} Settings" +msgstr "{packageId} Ajustes" + +#: keyman_config/view_installed.py:30 +msgid "Keyman Configuration" +msgstr "Configuración de Keyman" + +#: keyman_config/view_installed.py:60 +msgid "Choose a kmp file..." +msgstr "Elija un archivo kmp..." + +#. i18n: file type in file selection dialog +#: keyman_config/view_installed.py:65 +msgid "KMP files" +msgstr "Archivos KMP" + +#. i18n: column header in table displaying installed keyboards +#: keyman_config/view_installed.py:141 +msgid "Icon" +msgstr "Ícono" + +#. i18n: column header in table displaying installed keyboards +#: keyman_config/view_installed.py:145 +msgid "Name" +msgstr "Nombre" + +#. i18n: column header in table displaying installed keyboards +#: keyman_config/view_installed.py:148 +msgid "Version" +msgstr "Versión" + +#: keyman_config/view_installed.py:161 +msgid "_Uninstall" +msgstr "_Desinstalar" + +#: keyman_config/view_installed.py:162 keyman_config/view_installed.py:304 +msgid "Uninstall keyboard" +msgstr "Desinstalar teclado" + +#: keyman_config/view_installed.py:167 +msgid "_About" +msgstr "_Acerca de" + +#: keyman_config/view_installed.py:168 keyman_config/view_installed.py:306 +msgid "About keyboard" +msgstr "Acerca del teclado" + +#: keyman_config/view_installed.py:173 +msgid "_Help" +msgstr "_Ayuda" + +#: keyman_config/view_installed.py:174 keyman_config/view_installed.py:305 +msgid "Help for keyboard" +msgstr "Ayuda para el teclado" + +#: keyman_config/view_installed.py:179 +msgid "_Options" +msgstr "_Opciones" + +#: keyman_config/view_installed.py:180 keyman_config/view_installed.py:307 +msgid "Settings for keyboard" +msgstr "Configuración del teclado" + +#: keyman_config/view_installed.py:190 +msgid "_Refresh" +msgstr "actualizar" + +#: keyman_config/view_installed.py:191 +msgid "Refresh keyboard list" +msgstr "Actualizar la lista de teclados" + +#: keyman_config/view_installed.py:195 +msgid "_Download" +msgstr "_Descargar" + +#: keyman_config/view_installed.py:196 +msgid "Download and install a keyboard from the Keyman website" +msgstr "Descargue e instale un teclado desde el sitio web de Keyman" + +#: keyman_config/view_installed.py:201 +msgid "Install a keyboard from a file" +msgstr "Instalar un teclado desde un archivo" + +#: keyman_config/view_installed.py:206 +msgid "Close window" +msgstr "Cerrar ventana" + +#: keyman_config/view_installed.py:278 +#, python-brace-format +msgid "Uninstall keyboard {package}" +msgstr "Desinstalar teclado {package}" + +#: keyman_config/view_installed.py:280 +#, python-brace-format +msgid "Help for keyboard {package}" +msgstr "Ayuda para el teclado {package}" + +#: keyman_config/view_installed.py:282 +#, python-brace-format +msgid "About keyboard {package}" +msgstr "Acerca del teclado {package}" + +#: keyman_config/view_installed.py:284 +#, python-brace-format +msgid "Settings for keyboard {package}" +msgstr "Configuración del teclado {package}" + +#: keyman_config/view_installed.py:349 +msgid "Uninstall keyboard package?" +msgstr "¿Desinstalar el paquete de teclado?" + +#: keyman_config/view_installed.py:351 +#, python-brace-format +msgid "Are you sure that you want to uninstall the {keyboard} keyboard and its fonts?" +msgstr "¿Está seguro de que desea desinstalar el teclado {keyboard} y sus fuentes?" + +#: keyman_config/welcome.py:22 +#, python-brace-format +msgid "{name} installed" +msgstr "{name} instalado" + +#: keyman_config/welcome.py:40 +msgid "Open in _Web browser" +msgstr "Abrir en _Explorador web" + +#: keyman_config/welcome.py:42 +msgid "Open in the default web browser to do things like printing" +msgstr "Abrir en el navegador web predeterminado para hacer cosas como imprimir" + +#: keyman_config/welcome.py:45 +msgid "_OK" +msgstr "Aceptar" + diff --git a/linux/keyman-config/com.keyman.gschema.xml b/linux/keyman-config/resources/com.keyman.gschema.xml similarity index 64% rename from linux/keyman-config/com.keyman.gschema.xml rename to linux/keyman-config/resources/com.keyman.gschema.xml index 8f05a42bf9b0c70d4bc2ee9ffdb6b3ec6716c9c3..df50060d8c44eb97e2ec4e8d4848ab23c34ef665 100644 --- a/linux/keyman-config/com.keyman.gschema.xml +++ b/linux/keyman-config/resources/com.keyman.gschema.xml @@ -1,8 +1,12 @@ - + + true + Automatic error reporting + true to enable automatic reporting of errors to Sentry + diff --git a/linux/debian/keyman.sharedmimeinfo b/linux/keyman-config/resources/keyman.sharedmimeinfo similarity index 100% rename from linux/debian/keyman.sharedmimeinfo rename to linux/keyman-config/resources/keyman.sharedmimeinfo diff --git a/linux/keyman-config/km-config.desktop b/linux/keyman-config/resources/km-config.desktop similarity index 100% rename from linux/keyman-config/km-config.desktop rename to linux/keyman-config/resources/km-config.desktop diff --git a/linux/keyman-config/run-tests.sh b/linux/keyman-config/run-tests.sh index 3b8664512adc639aa376c6e6ed4b5da8e479713c..db62cda3749cde897b98dcd3e61113fb116a7305 100755 --- a/linux/keyman-config/run-tests.sh +++ b/linux/keyman-config/run-tests.sh @@ -1,6 +1,13 @@ #!/bin/bash PYTHONPATH=.:$PYTHONPATH +XDG_CONFIG_HOME=$(mktemp --directory) +export XDG_CONFIG_HOME + +if [ -f /usr/libexec/ibus-memconf ]; then + export GSETTINGS_BACKEND=keyfile +fi + if [ -n "$TEAMCITY_VERSION" ]; then if ! pip3 list --format=columns | grep -q teamcity-messages; then pip3 install teamcity-messages @@ -9,3 +16,5 @@ if [ -n "$TEAMCITY_VERSION" ]; then else python3 -m unittest discover -v -s tests -p test_*.py fi + +rm -rf "$XDG_CONFIG_HOME" diff --git a/linux/keyman-config/tests/test_canonical_language_code_utils.py b/linux/keyman-config/tests/test_canonical_language_code_utils.py index 0f54fd2b6969c0a96cb3fea3dfb7cd326fb94754..87bcf762449f60ccfc4f8c51a1867dffa1eee575 100644 --- a/linux/keyman-config/tests/test_canonical_language_code_utils.py +++ b/linux/keyman-config/tests/test_canonical_language_code_utils.py @@ -21,15 +21,16 @@ class CanonicalLanguageCodeUtilsTests(unittest.TestCase): { 'expected': 'sqt-Arab-YE', 'tags': ['sqt', 'sqt-YE', 'sqt-Arab'] }, { 'expected': 'sqt-Latn-YE', 'tags': ['sqt-Latn', 'sqt-Latn-YE'] }, - { 'expected': 'sa-Latn', 'tags': ['sa-Latn'] }, - { 'expected': 'hi-Latn', 'tags': ['hi-Latn'] }, + {'expected': 'sa-Latn', 'tags': ['sa-Latn']}, + {'expected': 'he-Latn', 'tags': ['he-Latn']}, + {'expected': 'hi-Latn-IN', 'tags': ['hi-Latn']}, # #1282 { 'expected': 'raw-Latn-MM', 'tags': ['raw', 'raw-MM', 'raw-Latn'] }, # Various extended tags and tests { 'expected': 'km-KH', 'tags': ['km', 'km-kh', 'km-khmr', 'km-khmr-kh'] }, - { 'expected': 'th-TH', 'tags': ['th', 'th-th', 'th-thai-th'] }, + {'expected': 'th-TH', 'tags': ['th', 'th-th', 'th-thai-th', 'th-Thai']}, # A BCP 47 tag that is not in our canonicalization tables { 'expected': 'th-Latn-DE', 'tags': ['th-latn-de'] }, diff --git a/linux/keyman-config/tests/test_dconf_util.py b/linux/keyman-config/tests/test_dconf_util.py new file mode 100755 index 0000000000000000000000000000000000000000..9093f493e53635ed746175aa5db9b6dbb0a88734 --- /dev/null +++ b/linux/keyman-config/tests/test_dconf_util.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 +import unittest +from gi.repository import Gio + +from keyman_config.dconf_util import get_option, set_option, GSETTINGS_BASE + + +class TestKeymanOptions(unittest.TestCase): + def setUp(self): + self.settings = Gio.Settings.new(GSETTINGS_BASE) + + def tearDown(self): + # Reset the GSettings to its original state + self.settings.reset("options") + + def test_get_option_no_info(self): + # Test getting options with invalid info + options = get_option({}) + self.assertEqual(options, {}) + + def test_get_option_invalid_info(self): + # Test getting options with invalid info + info = {"packageID": "", "keyboardID": ""} + options = get_option(info) + self.assertEqual(options, {}) + + def test_get_option_valid_info(self): + # Test getting options with valid info + info = {"packageID": "foo", "keyboardID": "bar"} + options = get_option(info) + self.assertIsInstance(options, dict) + + def test_set_option(self): + # Test setting options + info = {"packageID": "foo", "keyboardID": "bar"} + options = {"set_nfc": "1"} + set_option(info, options) + retrieved_options = get_option(info) + self.assertEqual(retrieved_options, options) + + +if __name__ == '__main__': + unittest.main() diff --git a/linux/keyman-config/tests/test_gnome_keyboards_util.py b/linux/keyman-config/tests/test_gnome_keyboards_util.py index b5a0cac842fff324e018f25672c1b93817ef376b..a53908cdd3296aaee7dfa26b838ac0fe29042f67 100644 --- a/linux/keyman-config/tests/test_gnome_keyboards_util.py +++ b/linux/keyman-config/tests/test_gnome_keyboards_util.py @@ -9,14 +9,14 @@ class GnomeKeyboardsUtilTests(unittest.TestCase): def setUp(self): _reset_gnome_shell() - @patch('keyman_config.os.system') + @patch('os.system') def test_IsGnomeShell_RunningGnomeShell(self, mockSystem): # Setup mockSystem.return_value = 0 # Execute/Verify self.assertEqual(is_gnome_shell(), True) - @patch('keyman_config.os.system') + @patch('os.system') def test_IsGnomeShell_NotRunningGnomeShell(self, mockSystem): # Setup mockSystem.return_value = 1 diff --git a/linux/keyman-config/tests/test_install_kmp.py b/linux/keyman-config/tests/test_install_kmp.py index 2f250550cd3ecef1ea5f1f1a88debe4a292c6178..3320c532b520e93d3c7023d97a532f01aa125f7d 100644 --- a/linux/keyman-config/tests/test_install_kmp.py +++ b/linux/keyman-config/tests/test_install_kmp.py @@ -199,19 +199,22 @@ class InstallKmpTests(unittest.TestCase): languages = [ {'id': 'de'}, {'id': 'esi-Latn'}, - {'id': 'dyo'} + {'id': 'dyo'}, + {'id': 'fuh-Arab'} ] for testcase in [ {'given': 'de', 'expected': 'de'}, - {'given': 'esi', 'expected': 'esi-Latn'}, - {'given': 'esi-Latn', 'expected': 'esi-Latn'}, + {'given': 'esi', 'expected': 'esi'}, + {'given': 'esi-Latn', 'expected': 'esi'}, {'given': 'es', 'expected': None}, {'given': 'en', 'expected': None}, {'given': None, 'expected': None}, # #3399 - {'given': 'dyo-latn', 'expected': 'dyo-Latn'}, - {'given': 'dyo', 'expected': 'dyo-Latn'}, + {'given': 'dyo-latn', 'expected': 'dyo'}, + {'given': 'dyo', 'expected': 'dyo'}, + {'given': 'fuh-Arab', 'expected': 'fuh-Arab'}, + {'given': 'fuh', 'expected': None}, ]: with self.subTest(data=testcase): # Execute @@ -262,13 +265,11 @@ class InstallKmpTests(unittest.TestCase): def test_InstallKmp_FutureKeymanVersion(self): # Setup - workdir = tempfile.TemporaryDirectory().name - os.makedirs(workdir) - packagedir = tempfile.TemporaryDirectory().name - os.makedirs(packagedir) - self.mockGetKeyboardDir.return_value = packagedir - kmpfile = self._createEmptyKmp(workdir) - self._createKmpJson(packagedir, ', "fileVersion": "99.0"') + workdir = tempfile.TemporaryDirectory() + packagedir = tempfile.TemporaryDirectory() + self.mockGetKeyboardDir.return_value = packagedir.name + kmpfile = self._createEmptyKmp(workdir.name) + self._createKmpJson(packagedir.name, ', "fileVersion": "99.0"') # Execute with self.assertRaises(InstallError) as context: @@ -278,22 +279,24 @@ class InstallKmpTests(unittest.TestCase): self.assertTrue('foo.kmp requires Keyman 99.0 or higher' in context.exception.message) self.mockInstallToIbus.assert_not_called() + # Teardown + workdir.cleanup() + packagedir.cleanup() + def test_InstallKmp(self): for testcase in [ {'name': 'PreviousKeymanVersion', 'fileVersion': ', "fileVersion": "7.0"'}, - {'name': 'SameKeymanVersion', 'fileVersion': ', "fileVersion": "' + __version__ + '"'}, + {'name': 'SameKeymanVersion', 'fileVersion': f', "fileVersion": "{__version__}"'}, {'name': 'NoFileVersion', 'fileVersion': ''} ]: with self.subTest(msg=testcase['name'], data=testcase['fileVersion']): # Setup self.mockInstallToIbus.reset_mock() - workdir = tempfile.TemporaryDirectory().name - os.makedirs(workdir) - packagedir = tempfile.TemporaryDirectory().name - os.makedirs(packagedir) - self.mockGetKeyboardDir.return_value = packagedir - kmpfile = self._createEmptyKmp(workdir) - self._createKmpJson(packagedir, testcase['fileVersion']) + workdir = tempfile.TemporaryDirectory() + packagedir = tempfile.TemporaryDirectory() + self.mockGetKeyboardDir.return_value = packagedir.name + kmpfile = self._createEmptyKmp(workdir.name) + self._createKmpJson(packagedir.name, testcase['fileVersion']) # Execute InstallKmp()._install_kmp(kmpfile, 'km', InstallLocation.User) @@ -301,6 +304,10 @@ class InstallKmpTests(unittest.TestCase): # Verify self.mockInstallToIbus.assert_called_once() + # Teardown + workdir.cleanup() + packagedir.cleanup() + if __name__ == '__main__': unittest.main() diff --git a/linux/keyman-config/tests/test_package_install_completion.py b/linux/keyman-config/tests/test_package_install_completion.py index 97238e3dc2e57051c63d085d1692018267841f71..61374a6c66cbc5756e22b0c42e42810adf378b03 100644 --- a/linux/keyman-config/tests/test_package_install_completion.py +++ b/linux/keyman-config/tests/test_package_install_completion.py @@ -2,7 +2,7 @@ import os import tempfile import unittest -from unittest.mock import patch, ANY +from unittest.mock import patch from importlib.machinery import SourceFileLoader from importlib.util import module_from_spec, spec_from_loader @@ -10,17 +10,11 @@ from importlib.util import module_from_spec, spec_from_loader class PackageInstallCompletionTests(unittest.TestCase): def setUp(self): - patcher1 = patch('keyman_config.install_kmp.extract_kmp') - self.mockExtractKmp = patcher1.start() - self.addCleanup(patcher1.stop) - patcher2 = patch('keyman_config.kmpmetadata.get_metadata') - self.mockGetMetadata = patcher2.start() - self.addCleanup(patcher2.stop) - + self.mockExtractKmp = self._setupMock('keyman_config.install_kmp.extract_kmp') + self.mockGetMetadata = self._setupMock('keyman_config.kmpmetadata.get_metadata') loader = SourceFileLoader('km_package_install', os.path.join(os.path.dirname( os.path.abspath(__file__)), '../km-package-install')) - spec = spec_from_loader(loader.name, loader) - if spec: + if spec := spec_from_loader(loader.name, loader): self.mod = module_from_spec(spec) loader.exec_module(self.mod) @@ -29,32 +23,43 @@ class PackageInstallCompletionTests(unittest.TestCase): self.cacheDir = os.path.join(self.tempDir.name, 'keyman') os.makedirs(self.cacheDir) + def tearDown(self): + self.tempDir.cleanup() + + def _setupMock(self, arg0): + patcher = patch(arg0) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + def _list_languages_for_keyboard_impl(self, packageId): return self.mod._list_languages_for_keyboard_impl(packageId, 'someDir') def test_PackageCompletionNoLanguage(self): - open(os.path.join(self.cacheDir, 'foo'), 'w') - self.mockGetMetadata.return_value = (None, None, None, [{}], None) - result = self._list_languages_for_keyboard_impl('foo') - self.assertEqual(result, "") + with open(os.path.join(self.cacheDir, 'foo'), 'w'): + self.mockGetMetadata.return_value = (None, None, None, [{}], None) + result = self._list_languages_for_keyboard_impl('foo') + self.assertEqual(result, "") def test_PackageCompletionOneLanguage(self): - open(os.path.join(self.cacheDir, 'khmer_angkor'), 'w') - self.mockGetMetadata.return_value = ( - None, None, None, - [{'languages': [{'name': 'Central Khmer (Khmer, Cambodia)', 'id': 'km'}]}], - None) - result = self._list_languages_for_keyboard_impl('khmer_angkor') - self.assertEqual(result, "km") + with open(os.path.join(self.cacheDir, 'khmer_angkor'), 'w'): + self.mockGetMetadata.return_value = ( + None, None, None, + [{'languages': [{'name': 'Central Khmer (Khmer, Cambodia)', 'id': 'km'}]}], + None + ) + result = self._list_languages_for_keyboard_impl('khmer_angkor') + self.assertEqual(result, "km") def test_PackageCompletionMultipleLanguages(self): - open(os.path.join(self.cacheDir, 'sil_euro_latin'), 'w') - self.mockGetMetadata.return_value = ( - None, None, None, - [{'languages': [ - {'name': 'English', 'id': 'en'}, - {'name': 'French', 'id': 'fr'}, - {'name': 'German', 'id': 'de'}]}], - None) - result = self._list_languages_for_keyboard_impl('sil_euro_latin') - self.assertEqual(result, 'en\nfr\nde') + with open(os.path.join(self.cacheDir, 'sil_euro_latin'), 'w'): + self.mockGetMetadata.return_value = ( + None, None, None, + [{'languages': [ + {'name': 'English', 'id': 'en'}, + {'name': 'French', 'id': 'fr'}, + {'name': 'German', 'id': 'de'}]}], + None + ) + result = self._list_languages_for_keyboard_impl('sil_euro_latin') + self.assertEqual(result, 'en\nfr\nde') diff --git a/linux/keyman-config/tests/test_uninstall_kmp.py b/linux/keyman-config/tests/test_uninstall_kmp.py index 4c48200f4c7622d54efd3869d29c462b12a1e947..109b519768417e236fb00f9d67ce9e0db41dda94 100644 --- a/linux/keyman-config/tests/test_uninstall_kmp.py +++ b/linux/keyman-config/tests/test_uninstall_kmp.py @@ -2,10 +2,10 @@ import unittest from unittest.mock import patch -from keyman_config.uninstall_kmp import _uninstall_keyboards_from_gnome +from keyman_config.uninstall_kmp import _uninstall_keyboards_from_gnome, _uninstall_keyboards_from_ibus -class UninstallKmpTests(unittest.TestCase): +class UninstallKmpGnomeTests(unittest.TestCase): def setUp(self): patcher = patch('keyman_config.uninstall_kmp.GnomeKeyboardsUtil') @@ -110,3 +110,123 @@ class UninstallKmpTests(unittest.TestCase): # Verify mockGnomeKeyboardsUtilInstance.write_input_sources.assert_called_once_with( [('xkb', 'en')]) + + +class UninstallKmpIbusTests(unittest.TestCase): + + def setUp(self): + self.mockIbusUtilClass = self._setupMock('keyman_config.uninstall_kmp.IbusUtil') + self.mockRestartIbus = self._setupMock('keyman_config.uninstall_kmp.restart_ibus') + self.mockGetIbusBus = self._setupMock('keyman_config.uninstall_kmp.get_ibus_bus') + self.mockGetIbusBus.return_value = None + + def _setupMock(self, arg0): + patcher = patch(arg0) + result = patcher.start() + self.addCleanup(patcher.stop) + return result + + def test_UninstallKeyboardsFromIbus_RemoveOneKeyboardFromEmpty(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [] + # Execute + _uninstall_keyboards_from_ibus([{'id': 'foo1'}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_not_called() + + def test_UninstallKeyboardsFromIbus_RemoveNonExistingKeyboard(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'fr:fooDir/foo2.kmx'] + # Execute + _uninstall_keyboards_from_ibus([{'id': 'foo1'}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with(None, [ + 'fr:fooDir/foo2.kmx']) + + def test_UninstallKeyboardsFromIbus_RemoveOneKeyboard(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'xkb:us::eng', 'km:fooDir/foo1.kmx'] + # Execute + _uninstall_keyboards_from_ibus([{'id': 'foo1'}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with( + None, + ['xkb:us::eng']) + + def test_UninstallKeyboardsFromIbus_RemoveMultipleKeyboards(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'xkb:us::eng', 'km:fooDir/foo1.kmx', 'fr:fooDir/foo2.kmx'] + # Execute + _uninstall_keyboards_from_ibus([{'id': 'foo1'}, {'id': 'foo2'}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with( + None, + ['xkb:us::eng']) + + def test_UninstallKeyboardsFromIbus_RemoveAllKeyboards(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'km:fooDir/foo1.kmx', 'fr:fooDir/foo2.kmx'] + # Execute + _uninstall_keyboards_from_ibus([{'id': 'foo1'}, {'id': 'foo2'}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with(None, []) + + def test_UninstallKeyboardsFromIbus_RemoveKeyboard_SingleLanguage(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'xkb:us::eng', 'en:fooDir/foo1.kmx'] + # Execute + _uninstall_keyboards_from_ibus([{'id': 'foo1', 'languages': [{'id': 'en'}]}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with( + None, + ['xkb:us::eng']) + + def test_UninstallKeyboardsFromIbus_RemoveKeyboard_MultipleLanguages(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'xkb:us::eng', 'fr:fooDir/foo1.kmx'] + # Execute + _uninstall_keyboards_from_ibus( + [{'id': 'foo1', 'languages': [{'id': 'en'}, {'id': 'fr'}]}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with( + None, + ['xkb:us::eng']) + + def test_UninstallKeyboardsFromIbus_RemoveKeyboard_OneNotMatchingLanguage(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'xkb:us::eng', 'en:fooDir/foo1.kmx'] + # Execute + _uninstall_keyboards_from_ibus( + [{'id': 'foo1', 'languages': [{'id': 'fr'}]}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with( + None, + ['xkb:us::eng']) + + def test_UninstallKeyboardsFromIbus_RemoveKeyboard_RemovesAllMatching(self): + # Setup + mockIbusUtilInstance = self.mockIbusUtilClass.return_value + mockIbusUtilInstance.read_preload_engines.return_value = [ + 'xkb:us::eng', 'en:fooDir/foo1.kmx', 'fr:fooDir/foo1.kmx', 'km:fooDir/foo2.kmx'] + # Execute + _uninstall_keyboards_from_ibus( + [{'id': 'foo1', 'languages': [{'id': 'fr'}]}], 'fooDir') + # Verify + mockIbusUtilInstance.write_preload_engines.assert_called_once_with( + None, + ['xkb:us::eng', 'km:fooDir/foo2.kmx']) diff --git a/linux/keyman-system-service/README.md b/linux/keyman-system-service/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8b6ca2eba69571bbd9c3d36299f5856f6810f6a7 --- /dev/null +++ b/linux/keyman-system-service/README.md @@ -0,0 +1,28 @@ +# keyman-system-service + +A DBus system service that allows to access `/dev/input/*` devices +to toggle capslock and perform other keyboard related actions. This is +required when running under Wayland, but also used with X11. + +See , +, +, +. + +## Notes + +- If we need to limit access to the interface to only Keyman, we can use + [polkit](https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html) + which allows to add a rule with an action that checks for the program. + +- to see the log file + + ```bash + journalctl -f -u systemd-keyman + ``` + +- to monitor method calls + + ```bash + sudo busctl monitor com.keyman.SystemService1 + ``` diff --git a/linux/keyman-system-service/build.sh b/linux/keyman-system-service/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..9bca26421c7a83c56f381bf6756bb6e730a9a52d --- /dev/null +++ b/linux/keyman-system-service/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +################################ Main script ################################ + +builder_describe \ + "Build keyman-system-service." \ + ":service" \ + "clean" \ + "configure" \ + "build" \ + "test" \ + "install install artifacts" \ + "uninstall uninstall artifacts" \ + "--no-integration don't run integration tests" + +builder_parse "$@" + +cd "$THIS_SCRIPT_PATH/" + +if builder_is_debug_build; then + MESON_TARGET=debug + export CPPFLAGS=-DG_MESSAGES_DEBUG + export CFLAGS="-O0" + export CXXFLAGS="-O0" +else + MESON_TARGET=release +fi +MESON_PATH="build/$(uname -m)/$MESON_TARGET" + +builder_describe_outputs \ + configure "${MESON_PATH}/build.ninja" \ + build "${MESON_PATH}/src/keyman-system-service" + +builder_run_action clean rm -rf "$THIS_SCRIPT_PATH/build/" + +# shellcheck disable=SC2086 +builder_run_action configure meson setup "$MESON_PATH" --werror --buildtype $MESON_TARGET "${builder_extra_params[@]}" + +if builder_start_action build; then + cd "$MESON_PATH" + ninja + builder_finish_action success build +fi + +if builder_start_action test; then + cd "$MESON_PATH" + meson test --print-errorlogs $builder_verbose + builder_finish_action success test +fi + +if builder_start_action install; then + cd "$MESON_PATH" + ninja install + builder_finish_action success install +fi + +if builder_start_action uninstall; then + cd "$MESON_PATH" + ninja uninstall + builder_finish_action success uninstall +fi diff --git a/linux/keyman-system-service/man/keyman-system-service.1 b/linux/keyman-system-service/man/keyman-system-service.1 new file mode 100644 index 0000000000000000000000000000000000000000..b506bd1607aa99442aa832d60292240f3bdae57d --- /dev/null +++ b/linux/keyman-system-service/man/keyman-system-service.1 @@ -0,0 +1,10 @@ +.TH KEYMAN-SYSTEM-SERVICE "1" "May 2023" "keyman-system-service" "User Commands" +.SH NAME +keyman-system-service \- System service for Keyman +.SH DESCRIPTION +\&keyman\-system\-service +.TP +This command should not be run directly. It will be started as a DBUS +system service by systemd. +.SH "SEE ALSO" +.BR ibus-engine-keyman(1) diff --git a/linux/keyman-system-service/meson.build b/linux/keyman-system-service/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..596ba48bcd443f4afde80d691c3ab09e23deacbe --- /dev/null +++ b/linux/keyman-system-service/meson.build @@ -0,0 +1,11 @@ +project('keyman-system-service', 'c', 'cpp', + version: run_command('cat', '../../VERSION.md', check: true).stdout().strip(), + license: 'GPL-2+', + meson_version: '>=1.0') + +evdev = dependency('libevdev', version: '>= 1.9') +systemd = dependency('libsystemd') + +subdir('resources') +subdir('src') +subdir('tests') diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.conf b/linux/keyman-system-service/resources/com.keyman.SystemService1.conf new file mode 100644 index 0000000000000000000000000000000000000000..e2c564a94bc0e467c0f91c966d58c4766dde488d --- /dev/null +++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.conf @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/linux/keyman-system-service/resources/com.keyman.SystemService1.service b/linux/keyman-system-service/resources/com.keyman.SystemService1.service new file mode 100644 index 0000000000000000000000000000000000000000..3966fb1bbcb9003cffa49efa358eabd5ef591ff6 --- /dev/null +++ b/linux/keyman-system-service/resources/com.keyman.SystemService1.service @@ -0,0 +1,7 @@ +# /usr/share/dbus-1/system-services/com.keyman.SystemService1.service + +[D-BUS Service] +Name=com.keyman.SystemService1 +Exec=/bin/false +User=root +SystemdService=systemd-keyman.service diff --git a/linux/keyman-system-service/resources/meson.build b/linux/keyman-system-service/resources/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..c28cb81e585a540a0ae407509aec37ecfabc522b --- /dev/null +++ b/linux/keyman-system-service/resources/meson.build @@ -0,0 +1,3 @@ +install_data('com.keyman.SystemService1.conf', install_dir: get_option('datadir') / 'dbus-1/system.d/') +install_data('com.keyman.SystemService1.service', install_dir: get_option('datadir') / 'dbus-1/system-services/') +install_data('systemd-keyman.service', install_dir: get_option('prefix') / 'lib/systemd/system/') diff --git a/linux/keyman-system-service/resources/systemd-keyman.service b/linux/keyman-system-service/resources/systemd-keyman.service new file mode 100644 index 0000000000000000000000000000000000000000..7cb67508a89076395ba0b1b7b9e16a3f04fad3de --- /dev/null +++ b/linux/keyman-system-service/resources/systemd-keyman.service @@ -0,0 +1,51 @@ +# systemd service file +# /usr/lib/systemd/system/systemd-keyman.service +# See https://stackoverflow.com/a/31725112 + +[Unit] +Description=Keyman System Service + +[Service] +Type=dbus +BusName=com.keyman.SystemService1 +ExecStart=/usr/libexec/keyman-system-service +Restart=on-failure + +# Filesystem lockdown +ProtectSystem=strict +ProtectKernelTunables=true +ProtectControlGroups=true +ReadWritePaths=/dev/input/ +PrivateTmp=true + +# Network +PrivateNetwork=true +IPAddressDeny=any +RestrictAddressFamilies=AF_UNIX + +# Execute Mappings +MemoryDenyWriteExecute=true + +# Modules +ProtectKernelModules=true + +# Real-time +RestrictRealtime=true + +# Privilege escalation +NoNewPrivileges=true + +# Capabilities +CapabilityBoundingSet= + +# System call interfaces +LockPersonality=yes +SystemCallArchitectures=native +SystemCallFilter=@system-service + +# Namespaces +PrivateUsers=yes +RestrictNamespaces=yes + +# Locked memory +LimitMEMLOCK=0 diff --git a/linux/keyman-system-service/src/KeyboardDevice.cpp b/linux/keyman-system-service/src/KeyboardDevice.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a744a96b8282e593b5175caa3e06a3e1d551241c --- /dev/null +++ b/linux/keyman-system-service/src/KeyboardDevice.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include +#include "KeyboardDevice.h" + +using namespace std; + +KeyboardDevice::KeyboardDevice() +{ + dev = nullptr; + fd = -1; + hasCapsLockLed = -1; + debug = false; +} + +KeyboardDevice::~KeyboardDevice() +{ + if (dev) { + libevdev_free(dev); + } + if (fd != -1) { + close(fd); + } +} + +bool KeyboardDevice::Initialize(const char* name) +{ + string path("/dev/input/"); + path.append(name); + + fd = open(path.c_str(), O_RDWR); + if (fd < 0) { + std::cerr << "Failed to open device " << path << ": " << strerror(errno) << std::endl; + return false; + } + + int rc = libevdev_new_from_fd(fd, &dev); + if (rc < 0) { + std::cerr << "Failed to init libevdev for " << path << ": " << strerror(-rc) << std::endl; + close(fd); + fd = -1; + return false; + } + + return true; +} + +bool KeyboardDevice::HasCapsLockLed() +{ + if (hasCapsLockLed < 0) { + if (!libevdev_has_event_code(dev, EV_LED, LED_CAPSL)) { + hasCapsLockLed = 0; + if (debug) { + std::cerr << "Device '" << libevdev_get_name(dev) << "' doesn't have " + << libevdev_event_code_get_name(EV_LED, LED_CAPSL) << "." << std::endl; + } + } else { + hasCapsLockLed = 1; + } + } + return hasCapsLockLed == 1; +} + +void KeyboardDevice::SetCapsLockLed(bool on) +{ + if (!HasCapsLockLed()) { + return; + } + + int rc = libevdev_kernel_set_led_value(dev, LED_CAPSL, on ? LIBEVDEV_LED_ON : LIBEVDEV_LED_OFF); + if (rc != 0) { + std::cerr << "Failed to set LED " << libevdev_event_code_get_name(EV_LED, LED_CAPSL) + << ": " << strerror(-rc) << std::endl; + } +} + +bool KeyboardDevice::GetCapsLockLed() +{ + if (!HasCapsLockLed()) { + return false; + } + + int val = libevdev_get_event_value(dev, EV_LED, LED_CAPSL); + return val; +} diff --git a/linux/keyman-system-service/src/KeyboardDevice.h b/linux/keyman-system-service/src/KeyboardDevice.h new file mode 100644 index 0000000000000000000000000000000000000000..af9ce119f339db7fb2162a2f6e4a310f9330b15d --- /dev/null +++ b/linux/keyman-system-service/src/KeyboardDevice.h @@ -0,0 +1,25 @@ +#ifndef __KEYBOARDDEVICE_H__ +#define __KEYBOARDDEVICE_H__ + +#include + +class KeyboardDevice +{ + public: + KeyboardDevice(); + virtual ~KeyboardDevice(); + + bool Initialize(const char* name); + bool HasCapsLockLed(); + void SetCapsLockLed(bool on); + bool GetCapsLockLed(); + void SetDebug(bool on) { debug = on; } + + private: + struct libevdev *dev; + int fd; + int hasCapsLockLed; + bool debug; +}; + +#endif // __KEYBOARDDEVICE_H__ diff --git a/linux/keyman-system-service/src/KeymanSystemService.cpp b/linux/keyman-system-service/src/KeymanSystemService.cpp new file mode 100644 index 0000000000000000000000000000000000000000..deceafd0c39193c3693c41ab54bdb10330f50107 --- /dev/null +++ b/linux/keyman-system-service/src/KeymanSystemService.cpp @@ -0,0 +1,195 @@ +// This file implements a service which will run on the system dbus, +// based on the sd-bus library, see +// https://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "KeymanSystemService.h" +#include "KeyboardDevice.h" + +#define KEYMAN_BUS_NAME "com.keyman.SystemService1" +#define KEYMAN_INTERFACE_NAME "com.keyman.SystemService1.System" +#define KEYMAN_OBJECT_PATH "/com/keyman/SystemService1/System" + +using namespace std; + +static int32_t +on_set_caps_lock_indicator( + sd_bus_message *msg, + void *user_data, + sd_bus_error *ret_error +) { + int32_t ret; + uint32_t state; + + *ret_error = SD_BUS_ERROR_NULL; + + ret = sd_bus_message_read_basic(msg, 'b', &state); + if (ret < 0) { + syslog(LOG_USER | LOG_NOTICE, "Failed to parse parameter: %s", strerror(-ret)); + return ret; + } + + KeymanSystemService *service = static_cast(user_data); + service->SetCapsLockIndicatorOnDevices(state); + return sd_bus_reply_method_return(msg, ""); +} + +static int32_t +on_get_caps_lock_indicator( + sd_bus_message *msg, + void *user_data, + sd_bus_error *ret_error +) { + uint32_t state; + + *ret_error = SD_BUS_ERROR_NULL; + + KeymanSystemService *service = static_cast(user_data); + state = service->GetCapsLockIndicatorOnDevices(); + return sd_bus_reply_method_return(msg, "b", state); +} + +static const sd_bus_vtable system_service_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD("SetCapsLockIndicator", "b", "", on_set_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("GetCapsLockIndicator", "", "b", on_get_caps_lock_indicator, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_VTABLE_END +}; + +KeymanSystemService::KeymanSystemService() +{ + int ret; + + GetKbdDevices(); + +#ifdef KEYMAN_TESTING + ret = sd_bus_open_user(&bus); +#else + ret = sd_bus_open_system(&bus); +#endif + + if (ret < 0) { + syslog(LOG_USER | LOG_NOTICE, "Failed to connect to system bus: %s", strerror(-ret)); + failed = true; + return; + } + + // Install the object + ret = sd_bus_add_object_vtable(bus, + &slot, + KEYMAN_OBJECT_PATH, + KEYMAN_INTERFACE_NAME, + system_service_vtable, + this); + if (ret < 0) { + syslog(LOG_USER | LOG_NOTICE, "Failed to issue method call: %s", strerror(-ret)); + failed = true; + return; + } + + // Take a well-known service name so that clients can find us + ret = sd_bus_request_name(bus, KEYMAN_BUS_NAME, 0); + if (ret < 0) { + syslog(LOG_USER | LOG_NOTICE, "Failed to acquire service name: %s", strerror(-ret)); + failed = true; + return; + } +} + +KeymanSystemService::~KeymanSystemService() +{ + if (slot) sd_bus_slot_unref(slot); + if (bus) sd_bus_unref(bus); + + if (kbd_devices) { + for (KeyboardDevice *device : *kbd_devices) { + delete device; + } + delete kbd_devices; + } +} + +int KeymanSystemService::Loop() +{ + for (;;) { + // Process requests + int ret = sd_bus_process(bus, NULL); + if (ret < 0) { + syslog(LOG_USER | LOG_NOTICE, "Failed to process bus: %s", strerror(-ret)); + failed = true; + return ret; + } + if (ret > 0) { + // we processed a request, try to process another one, right-away + continue; + } + + // Wait for the next request to process + ret = sd_bus_wait(bus, (uint64_t)-1); + if (ret < 0) { + syslog(LOG_USER | LOG_NOTICE, "Failed to wait on bus: %s", strerror(-ret)); + failed = true; + return ret; + } + } + return 0; +} + +void KeymanSystemService::GetKbdDevices() { + kbd_devices = new std::list(); + + DIR *dirstream; + struct dirent *dir; + dirstream = opendir("/dev/input"); + if (dirstream) { + while ((dir = readdir(dirstream)) != NULL) { + // we're looking for a character device like `event1` + if (dir->d_type == DT_CHR && strncmp(dir->d_name, "event", 5) == 0) { + KeyboardDevice *kbdDevice = new KeyboardDevice(); + if (!kbdDevice->Initialize(dir->d_name) || !kbdDevice->HasCapsLockLed()) { + delete kbdDevice; + continue; + } + kbd_devices->push_back(kbdDevice); + } + } + closedir(dirstream); + } +} + +// Set the CapsLock indicator on all keyboard devices. +void +KeymanSystemService::SetCapsLockIndicatorOnDevices(uint32_t state) { + for (KeyboardDevice *kbdDevice : *kbd_devices) { + if (!kbdDevice->HasCapsLockLed()) { + syslog(LOG_USER | LOG_NOTICE, "%s: No kbdDevice or no CapsLockLed", __FUNCTION__); + continue; + } + syslog(LOG_USER | LOG_NOTICE, "%s: Caps Lock indicator: %s", __FUNCTION__, (state != 0 ? "ON" : "OFF")); + kbdDevice->SetCapsLockLed(state != 0); + } +} + +// Get the CapsLock indicator state from the list of keyboard devices. +// This will return true if any keyboard has the CapsLock indicator lit. +uint32_t +KeymanSystemService::GetCapsLockIndicatorOnDevices() { + bool state = false; + for (KeyboardDevice *kbdDevice : *kbd_devices) { + if (!kbdDevice->HasCapsLockLed()) { + syslog(LOG_USER | LOG_NOTICE, "%s: No kbdDevice or no CapsLockLed", __FUNCTION__); + continue; + } + state = state || kbdDevice->GetCapsLockLed(); + } + return state; +} diff --git a/linux/keyman-system-service/src/KeymanSystemService.h b/linux/keyman-system-service/src/KeymanSystemService.h new file mode 100644 index 0000000000000000000000000000000000000000..2686bfb99f95315ac28ca022ed846e9392e0444e --- /dev/null +++ b/linux/keyman-system-service/src/KeymanSystemService.h @@ -0,0 +1,33 @@ +#ifndef __KEYMANSYSTEMSERVICE_H__ +#define __KEYMANSYSTEMSERVICE_H__ + +#include +#include +#include "KeyboardDevice.h" + +using namespace std; + +class KeymanSystemService { +private: + std::list* kbd_devices = NULL; + sd_bus_slot *slot = NULL; + sd_bus *bus = NULL; + bool failed = false; + + void GetKbdDevices(); + +public: + KeymanSystemService(); + ~KeymanSystemService(); + + bool + Failed() { + return failed; + } + + int Loop(); + void SetCapsLockIndicatorOnDevices(uint32_t state); + uint32_t GetCapsLockIndicatorOnDevices(); +}; + +#endif // __KEYMANSYSTEMSERVICE_H__ diff --git a/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml b/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml new file mode 100644 index 0000000000000000000000000000000000000000..b75a1f23904be79d7483b5fa10cae0f03acb9b96 --- /dev/null +++ b/linux/keyman-system-service/src/com.keyman.SystemService1.System.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + diff --git a/linux/keyman-system-service/src/main.cpp b/linux/keyman-system-service/src/main.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9a2bb2d0de0e11f8498c87c9882b45daf71271d5 --- /dev/null +++ b/linux/keyman-system-service/src/main.cpp @@ -0,0 +1,10 @@ +#include "KeymanSystemService.h" + +int main(int argc, char *argv[]) { + KeymanSystemService dbusService; + if (dbusService.Failed()) { + return 1; + } + + return dbusService.Loop(); +} diff --git a/linux/keyman-system-service/src/meson.build b/linux/keyman-system-service/src/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..51820ab9fa9eecf70f975c7e731b1373d34251b7 --- /dev/null +++ b/linux/keyman-system-service/src/meson.build @@ -0,0 +1,17 @@ +service_files = files( + 'KeyboardDevice.cpp', + 'KeymanSystemService.cpp', + 'main.cpp', +) + +deps = [evdev, systemd] + +exe = executable( + 'keyman-system-service', + sources: [service_files], + dependencies: deps, + install: true, + install_dir: get_option('libexecdir'), +) + +install_data('com.keyman.SystemService1.System.xml', install_dir: get_option('datadir') / 'dbus-1/interfaces') diff --git a/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp b/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp new file mode 100644 index 0000000000000000000000000000000000000000..dbaa6aafab99ad620c1d3566ab5c463561f653d9 --- /dev/null +++ b/linux/keyman-system-service/tests/KeyboardDeviceMock.cpp @@ -0,0 +1,45 @@ +// A KeyboardDevice mock. +#include +#include +#include +#include +#include +#include + +#ifndef KeyboardDevice +#define KeyboardDevice KeyboardDeviceMock +#endif + +#include "KeyboardDevice.h" + +using namespace std; + +KeyboardDeviceMock::KeyboardDeviceMock() { + dev = nullptr; + fd = -1; + hasCapsLockLed = 1; + debug = false; +} + +KeyboardDeviceMock::~KeyboardDeviceMock() { +} + +bool +KeyboardDeviceMock::Initialize(const char* name) { + return true; +} + +bool +KeyboardDeviceMock::HasCapsLockLed() { + return true; +} + +void +KeyboardDeviceMock::SetCapsLockLed(bool on) { + hasCapsLockLed = on ? 1 : 0; +} + +bool +KeyboardDeviceMock::GetCapsLockLed() { + return hasCapsLockLed == 1; +} diff --git a/linux/keyman-system-service/tests/README.md b/linux/keyman-system-service/tests/README.md new file mode 100644 index 0000000000000000000000000000000000000000..af77363b65794c4c80ba897b042bfa55eef68bae --- /dev/null +++ b/linux/keyman-system-service/tests/README.md @@ -0,0 +1,11 @@ +# keyman-system-service tests + +This directory defines a keyman-test-service. This is mostly identical to +keyman-system-service. The differences are: + +- it runs on the session bus instead of system bus +- KeyboardDevice (which in production interacts with evdev) is mocked + so that the tests don't require root permissions. + +The keyman-test-service gets called/used by the ibus-keyman integration +tests. diff --git a/linux/keyman-system-service/tests/meson.build b/linux/keyman-system-service/tests/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..f90e12d16a23a8992de761ae3aa9626fecb0f36d --- /dev/null +++ b/linux/keyman-system-service/tests/meson.build @@ -0,0 +1,27 @@ +test_service_files = files( + '../src/main.cpp', + '../src/KeymanSystemService.cpp', + 'KeyboardDeviceMock.cpp', +) + +deps = [evdev, systemd] + +test_c_args = [ + '-DKEYMAN_TESTING', + '-DKeyboardDevice=KeyboardDeviceMock' +] + +exe = executable( + 'keyman-test-service', + sources: [test_service_files], + c_args: test_c_args, + cpp_args: test_c_args, + dependencies: deps, + include_directories: [ '../src' ] +) + +# we currently don't have any unit tests for keyman-system-service. +# The keyman-test-service we build here gets used by the ibus-keyman +# integration tests. + +subdir('services') diff --git a/linux/keyman-system-service/tests/services/README.md b/linux/keyman-system-service/tests/services/README.md new file mode 100644 index 0000000000000000000000000000000000000000..17c6950c556659740a761d4d31f8f5cf2d76bfd7 --- /dev/null +++ b/linux/keyman-system-service/tests/services/README.md @@ -0,0 +1,5 @@ +# tests/services + +This directory contains the service file for our keyman test service as +well as some additional files for services that are required for things +to properly work in our test environment. diff --git a/linux/keyman-system-service/tests/services/at-spi-dbus-bus.service.in b/linux/keyman-system-service/tests/services/at-spi-dbus-bus.service.in new file mode 100644 index 0000000000000000000000000000000000000000..8294093a7f88a4da344a25705667c66671e9744a --- /dev/null +++ b/linux/keyman-system-service/tests/services/at-spi-dbus-bus.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.a11y.Bus +Exec=/usr/libexec/at-spi-bus-launcher diff --git a/linux/keyman-system-service/tests/services/gvfs-daemon.service.in b/linux/keyman-system-service/tests/services/gvfs-daemon.service.in new file mode 100644 index 0000000000000000000000000000000000000000..b5357bdda3aa3766b26a86e17020ddecf468e8b2 --- /dev/null +++ b/linux/keyman-system-service/tests/services/gvfs-daemon.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gtk.vfs.Daemon +Exec=/usr/libexec/gvfsd diff --git a/linux/keyman-system-service/tests/services/keyman-test.service.in b/linux/keyman-system-service/tests/services/keyman-test.service.in new file mode 100644 index 0000000000000000000000000000000000000000..8946119f234d18af79489daf1dc480f74881a532 --- /dev/null +++ b/linux/keyman-system-service/tests/services/keyman-test.service.in @@ -0,0 +1,5 @@ +# Service definition file for keyman-test-service + +[D-BUS Service] +Name=com.keyman.SystemService1 +Exec=@installed_tests_dir@/keyman-test-service diff --git a/linux/keyman-system-service/tests/services/meson.build b/linux/keyman-system-service/tests/services/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..e741aa5fbffd4f7d69f71625f5c1dedd4c40de02 --- /dev/null +++ b/linux/keyman-system-service/tests/services/meson.build @@ -0,0 +1,16 @@ +dbus_service_files = [ + 'at-spi-dbus-bus.service', + 'gvfs-daemon.service', + 'keyman-test.service', +] + +config_data = configuration_data() +config_data.set('installed_tests_dir', meson.current_build_dir() / '..') + +foreach service_file : dbus_service_files + configure_file( + input: service_file + '.in', + output: service_file, + configuration: config_data, + ) +endforeach diff --git a/linux/scripts/cow.sh b/linux/scripts/cow.sh index e175a666ef606b03a13818df5230259b9847de57..201896a7e7cc981ed57048ce079166825a1946eb 100755 --- a/linux/scripts/cow.sh +++ b/linux/scripts/cow.sh @@ -3,7 +3,7 @@ # If needed set cowbuilder up for building Keyman Debian packages # Then cowbuilder update -distributions='focal jammy kinetic lunar' +distributions='focal jammy lunar mantic' if ! dpkg-query -l cowbuilder; then echo "installing pbuilder and cowbuilder" diff --git a/linux/scripts/deb.sh b/linux/scripts/deb.sh index 3c87af0de8c3d40eaf23e0af59e871d2bdc5b99b..35a476a55cbcec9544ea6c85d7fecf5b67f49d89 100755 --- a/linux/scripts/deb.sh +++ b/linux/scripts/deb.sh @@ -11,7 +11,7 @@ set -e -all_distributions="focal jammy" +all_distributions="focal jammy lunar mantic" distributions="" echo "all_distributions: ${all_distributions}" diff --git a/linux/scripts/dist.sh b/linux/scripts/dist.sh index f412531cd29a0227f2d9f5443379369e552eef9d..d19abf7f782ffd48491abba4ff190bc3c1c98810 100755 --- a/linux/scripts/dist.sh +++ b/linux/scripts/dist.sh @@ -33,6 +33,8 @@ dch keyman --newversion "${VERSION}" --force-bad-version --nomultimaint dpkg-source --tar-ignore=*~ --tar-ignore=.git --tar-ignore=.gitattributes \ --tar-ignore=.gitignore --tar-ignore=experiments --tar-ignore=debian \ --tar-ignore=.github --tar-ignore=.vscode --tar-ignore=android \ + --tar-ignore=.devcontainer \ + --tar-ignore=artifacts \ \ --tar-ignore=common/models \ --tar-ignore=common/predictive-text \ @@ -47,13 +49,17 @@ dpkg-source --tar-ignore=*~ --tar-ignore=.git --tar-ignore=.gitattributes \ --tar-ignore=core/build \ --tar-ignore=developer --tar-ignore=docs --tar-ignore=ios \ --tar-ignore=linux/keyman-config/buildtools/build-langtags.py --tar-ignore=__pycache__ \ - --tar-ignore=linux/help --tar-ignore=linux/Jenkinsfile \ + --tar-ignore=linux/help \ --tar-ignore=mac --tar-ignore=node_modules --tar-ignore=oem \ - --tar-ignore=linux/build --tar-ignore=core/build \ + --tar-ignore=linux/build \ + --tar-ignore=linux/builddebs \ + --tar-ignore=linux/ibus-keyman/build \ + --tar-ignore=linux/keyman-system-service/build \ --tar-ignore=resources/devbox --tar-ignore=resources/git-hooks \ --tar-ignore=resources/scopes \ --tar-ignore=resources/build/*.lua --tar-ignore=resources/build/jq* \ --tar-ignore=results \ + --tar-ignore=tmp \ --tar-ignore=web --tar-ignore=windows --tar-ignore=keyman_1* \ --tar-ignore=dist --tar-ignore=.pbuilderrc --tar-ignore=VERSION -Zgzip -b . mv ../keyman_"${VERSION}".tar.gz linux/dist/keyman-"${VERSION}".tar.gz diff --git a/linux/scripts/install.sh b/linux/scripts/install.sh index 40f2eb8f5b09bcd7f6571fa272d660391349ebaa..7e497232fc8d6ad7fb01bb643ae6ab5cb19b7d14 100755 --- a/linux/scripts/install.sh +++ b/linux/scripts/install.sh @@ -15,7 +15,7 @@ INSTALLDIR=${INSTALLDIR:-"/usr/local"} if [[ "${SUDOINSTALL}" != "no" ]]; then if [ "$EUID" -ne 0 ] then - echo "Please run 'make un/install' with sudo" + echo "Please run 'build.sh un/install' with sudo" exit fi fi @@ -24,7 +24,7 @@ if [ -f "/usr/share/ibus/component/keyman.xml" ] && [ "${SUDOINSTALL}" == "yes" if grep -Fq "/usr/lib/ibus" /usr/share/ibus/component/keyman.xml then echo "component file is in ibus-keyman package version so move it" - echo "run 'sudo make uninstall' to put it back" + echo "run 'sudo build.sh uninstall' to put it back" mv /usr/share/ibus/component/keyman.xml /usr/share/doc/ibus-keyman/ else echo "component file is local one so overwrite it" @@ -37,10 +37,10 @@ cd "$BASEDIR" cd ibus-keyman if [[ "${SUDOINSTALL}" == "uninstall" ]]; then - echo "doing make uninstall of ibus-keyman" + echo "doing build.sh uninstall of ibus-keyman" ./build.sh uninstall else - echo "doing make install of ibus-keyman" + echo "doing build.sh install of ibus-keyman" ./build.sh install fi cd "$BASEDIR" @@ -49,20 +49,20 @@ cd keyman-config echo "SUDOINSTALL: ${SUDOINSTALL}" if [[ "${SUDOINSTALL}" == "yes" ]]; then if [ ! -d build ]; then - echo "keyman-config must be built before it is installed. Run 'make configure' if needed then 'make'" + echo "keyman-config must be built before it is installed. Run 'build.sh configure build'." exit 1 fi echo "doing sudo glib-compile-schemas for keyman-config" - cp com.keyman.gschema.xml /usr/share/glib-2.0/schemas/ + cp resources/com.keyman.gschema.xml /usr/share/glib-2.0/schemas/ glib-compile-schemas /usr/share/glib-2.0/schemas/ echo "doing sudo install of keyman-config" - make install + ./build.sh install elif [[ "${SUDOINSTALL}" == "uninstall" ]]; then echo "doing sudo uninstall of keyman-config" - make uninstall + ./build.sh uninstall else echo "doing /tmp install of keyman-config" - make install-temp + ./build.sh install fi cd "$BASEDIR" diff --git a/linux/scripts/jenkins.sh b/linux/scripts/jenkins.sh deleted file mode 100755 index 5c796259559f1316bf0078e1139d439d23ff1094..0000000000000000000000000000000000000000 --- a/linux/scripts/jenkins.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# $1 - project name with appended tier, e.g. keyman-alpha -# $2 - GPG key used for signing the source package - -set -e -set -u - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -. "$THIS_SCRIPT_PATH/package-build.inc.sh" - -keyman_projects="keyman" - -tier="stable" - -if [[ "$1" =~ "-alpha" ]]; then - tier="alpha" -elif [[ "$1" =~ "-beta" ]]; then - tier="beta" -fi - -proj="$1" -proj=${proj%"-alpha"} -proj=${proj%"-beta"} - -fullsourcename="keyman" -sourcedir="$KEYMAN_ROOT" -sourcename=${fullsourcename%"-alpha"} -sourcename=${sourcename%"-beta"} - -# set Debian/changelog environment -export DEBFULLNAME="${fullsourcename} Package Signing Key" -export DEBEMAIL='jenkins@sil.org' - -checkAndInstallRequirements - -# clean up prev deb builds -builder_heading "cleaning previous builds of $1" - -rm -rf builddebs -rm -rf "$sourcedir/${1}"_*.{dsc,build,buildinfo,changes,tar.?z,log} -rm -rf "$sourcedir/../${1}"_*.{dsc,build,buildinfo,changes,tar.?z,log} - -builder_heading "Make source package for $fullsourcename" -builder_heading "reconfigure" -TIER="$tier" ./scripts/reconf.sh - -builder_heading "Make origdist" -./scripts/dist.sh origdist -builder_heading "Make deb source" -./scripts/deb.sh sourcepackage - -#sign source package -for file in builddebs/*.dsc; do - builder_heading "Signing source package $file" - debsign -k"$2" "$file" -done - -mv builddebs/* .. diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index 30495d2b0e1567278819415fcd9edf5a4a9de098..7437272c0304035f4da77bf3241b6f094d588fe0 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -33,7 +33,7 @@ else fi echo "ppa: ${ppa}" -distributions="${DIST:-focal jammy kinetic lunar}" +distributions="${DIST:-focal jammy lunar mantic}" packageversion="${PACKAGEVERSION:-1~sil1}" BASEDIR=$(pwd) diff --git a/linux/scripts/package-build.inc.sh b/linux/scripts/package-build.inc.sh index 39ea70f7d1719859c9bba2a3de0240b013628b52..fcfe492b4339326d7cf8c40c65a4616239632733 100644 --- a/linux/scripts/package-build.inc.sh +++ b/linux/scripts/package-build.inc.sh @@ -28,10 +28,7 @@ function downloadSource() { if [ "${proj:=}" == "keyman" ]; then cd "${BASEDIR}" || exit - fi - - if [ "${proj}" == "keyman" ]; then - make clean + ./build.sh clean fi # Update tier in Debian watch files (replacing any previously set tier) and remove comment @@ -52,6 +49,14 @@ function downloadSource() { sha256sum -c --ignore-missing SHA256SUMS |grep "${proj}" } +function wait_for_apt_deb { + # from https://gist.github.com/hrpatel/117419dcc3a75e46f79a9f1dce99ef52 + while sudo fuser /var/{lib/{dpkg,apt/lists},cache/apt/archives}/lock &>/dev/null 2>&1; do + echo "Waiting for apt/dpkg lock to release, sleeping 10s" + sleep 10 + done +} + function checkAndInstallRequirements() { local TOINSTALL="" @@ -66,12 +71,12 @@ function checkAndInstallRequirements() export DEBIAN_FRONTEND=noninteractive if [ -n "$TOINSTALL" ]; then - sudo apt-get update + wait_for_apt_deb && sudo apt-get update # shellcheck disable=SC2086 - sudo apt-get -qy install $TOINSTALL + wait_for_apt_deb && sudo apt-get -qy install $TOINSTALL fi sudo mk-build-deps debian/control - sudo apt-get -qy --allow-downgrades install ./keyman-build-deps_*.deb + wait_for_apt_deb && sudo apt-get -qy --allow-downgrades install ./keyman-build-deps_*.deb sudo rm -f keyman-buid-deps_* } diff --git a/linux/scripts/reconf.sh b/linux/scripts/reconf.sh index cd74571a4a1dc696934de83f04d395d4850cda05..4e4f5c1b115cbc37e02905dfcdc262ccb9941024 100755 --- a/linux/scripts/reconf.sh +++ b/linux/scripts/reconf.sh @@ -21,7 +21,7 @@ cd "$BASEDIR/ibus-keyman" ./build.sh clean configure cd "$BASEDIR/keyman-config" -make clean +./build.sh clean cd keyman_config sed \ diff --git a/linux/scripts/upload-to-debian.sh b/linux/scripts/upload-to-debian.sh index af443cac08acafec60c925ec633037a1f5ff7b33..d7665f29f8f5f4ca98862bdb0631aa0554268bd2 100755 --- a/linux/scripts/upload-to-debian.sh +++ b/linux/scripts/upload-to-debian.sh @@ -105,7 +105,7 @@ git add debian/changelog git commit -m "chore(linux): Update debian changelog" if [ -n "$PUSH" ]; then $NOOP git push --force-with-lease origin chore/linux/changelog - $NOOP gh pr create --draft --base "$stable_branch" --title "chore(linux): Update debian changelog" --body "@keymanapp-test-bot skip" + $NOOP gh pr create --draft --base "${stable_branch#origin/}" --title "chore(linux): Update debian changelog" --body "@keymanapp-test-bot skip" fi if $ISBETA; then @@ -118,7 +118,7 @@ git checkout -B chore/linux/cherry-pick/changelog ${CLBRANCH} git cherry-pick -x chore/linux/changelog if [ -n "$PUSH" ]; then $NOOP git push --force-with-lease origin chore/linux/cherry-pick/changelog - $NOOP gh pr create --draft --base ${CLBRANCH} --title "chore(linux): Update debian changelog 🍒" --body "@keymanapp-test-bot skip" + $NOOP gh pr create --draft --base ${CLBRANCH#origin/} --title "chore(linux): Update debian changelog 🍒" --body "@keymanapp-test-bot skip" fi builder_heading "Finishing" diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index a7ca883cf154c714a1d43ad19f59321b8e910d27..7de7c68525d635a8ff57fd7c68ab7c209f3e4966 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -331,6 +331,12 @@ 9A3D6C5C221531B0008785A3 /* KMOSVersion.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMOSVersion.m; sourceTree = ""; }; A71FD8BA8404531DFBD861B6 /* Pods_KeymanTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_KeymanTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B644E63217FFE54B91C71C94 /* Pods_Keyman.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Keyman.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CEFFECD82A417FEA00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMAboutWindowController.strings; sourceTree = ""; }; + CEFFECD92A417FEB00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/preferences.strings; sourceTree = ""; }; + CEFFECDA2A417FEB00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMInfoWindowController.strings; sourceTree = ""; }; + CEFFECDB2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; }; + CEFFECDC2A417FEC00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainMenu.strings; sourceTree = ""; }; + CEFFECDD2A4180FD00D58C36 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; E211769B20E1826800F8065D /* NoContextTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NoContextTestClient.h; sourceTree = ""; }; E211769C20E182DD00F8065D /* NoContextTestClient.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NoContextTestClient.m; sourceTree = ""; }; E211769E20E18C0B00F8065D /* AppleCompliantTestClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppleCompliantTestClient.h; sourceTree = ""; }; @@ -783,6 +789,7 @@ sv, uk, ru, + es, ); mainGroup = 989C9BFF1A7876DE00A20425; productRefGroup = 989C9C091A7876DE00A20425 /* Products */; @@ -1035,6 +1042,7 @@ 298566C82980C9AD004ACA95 /* sv */, 298566D42980D47C004ACA95 /* uk */, 298566E02980E579004ACA95 /* ru */, + CEFFECD82A417FEA00D58C36 /* es */, ); name = KMAboutWindowController.xib; sourceTree = ""; @@ -1060,6 +1068,7 @@ 298566C92980C9AD004ACA95 /* sv */, 298566D52980D47D004ACA95 /* uk */, 298566E12980E579004ACA95 /* ru */, + CEFFECD92A417FEB00D58C36 /* es */, ); name = preferences.xib; path = Keyman4MacIM/KMConfiguration; @@ -1085,6 +1094,7 @@ 298566CD2980C9B9004ACA95 /* sv */, 298566D92980D489004ACA95 /* uk */, 298566E52980E584004ACA95 /* ru */, + CEFFECDD2A4180FD00D58C36 /* es */, ); name = Localizable.strings; sourceTree = ""; @@ -1110,6 +1120,7 @@ 298566CA2980C9AD004ACA95 /* sv */, 298566D62980D47D004ACA95 /* uk */, 298566E22980E57A004ACA95 /* ru */, + CEFFECDA2A417FEB00D58C36 /* es */, ); name = KMInfoWindowController.xib; path = Keyman4MacIM/KMInfoWindow; @@ -1136,6 +1147,7 @@ 298566CB2980C9AE004ACA95 /* sv */, 298566D72980D47D004ACA95 /* uk */, 298566E32980E57A004ACA95 /* ru */, + CEFFECDB2A417FEC00D58C36 /* es */, ); name = KMKeyboardHelpWindowController.xib; path = Keyman4MacIM/KMKeyboardHelpWindow; @@ -1162,6 +1174,7 @@ 298566CC2980C9AE004ACA95 /* sv */, 298566D82980D47E004ACA95 /* uk */, 298566E42980E57A004ACA95 /* ru */, + CEFFECDC2A417FEC00D58C36 /* es */, ); name = MainMenu.xib; sourceTree = ""; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es.lproj/KMAboutWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es.lproj/KMAboutWindowController.strings new file mode 100644 index 0000000000000000000000000000000000000000..13aa66b589a442cfc0059eb494ada943685a864c --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMAboutWindow/es.lproj/KMAboutWindowController.strings @@ -0,0 +1,8 @@ +/* button text to close the About window */ +"Kab-Up-fYH.title" = "Cerrar"; + +/* text of link to the license agreement */ +"hYC-Bx-aoP.title" = "Acuerdo de licencia"; + +/* button text to open Configuration window */ +"vkh-b5-vO7.title" = "Configuración..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es.lproj/preferences.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es.lproj/preferences.strings new file mode 100644 index 0000000000000000000000000000000000000000..aadead65f4a181355ff6e70c1400623fc74cda78 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/es.lproj/preferences.strings @@ -0,0 +1,38 @@ +/* Checkbox text to always show on-screen keyboard */ +"4UX-80-0Vo.title" = "Mostrar siempre el teclado en pantalla"; + +/* Checkbox text to enable verbose Console logging */ +"7J6-zy-R20.title" = "Utilizar el registro detallado de la consola"; + +/* Support button text */ +"93O-x6-RLF.label" = "Soporte"; + +/* Download Keyboard button text */ +"CTw-kf-WNS.title" = "Descargar teclado..."; + +/* Keyman Configuration window title */ +"F0z-JX-Cv5.title" = "Configuración de Keyman"; + +/* button text to go back to previous page */ +"JOK-JV-n8w.title" = "Volver"; + +/* Keyboards button text */ +"MPN-9N-wWc.label" = "Teclados"; + +/* text to explain verbose Console logging option */ +"MrI-GM-7d6.title" = "Si la opción de registro detallado está activada, Keyman registrará las acciones que puedan ayudar al servicio de asistencia técnica de Keyman a diagnosticar un problema. El programa Consola puede utilizarse para ver un registro de los mensajes de Keyman. En Consola, filtre para mostrar todos los mensajes del proceso \"Keyman\". Este registro puede exportarse y enviarse al servicio de asistencia de Keyman si es necesario."; + +/* button text to move forward to the next page */ +"eXr-8V-h1g.title" = "Adelante"; + +/* button text to return to the home help page */ +"fXS-aC-CMH.title" = "Inicio"; + +/* Options button text */ +"frd-No-seV.label" = "Opciones"; + +/* Installed Keyboard column heading */ +"jex-Nd-Qzg.headerCell.title" = "Teclado instalado"; + +/* Verbose logging tooltip text */ +"uWx-3J-U0D.ibShadowedToolTip" = "Active esta opción si tiene problemas con un teclado específico o con Keyman en general."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es.lproj/KMInfoWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es.lproj/KMInfoWindowController.strings new file mode 100644 index 0000000000000000000000000000000000000000..d30022413091479890edbfd70f45324419ed654e --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInfoWindow/es.lproj/KMInfoWindowController.strings @@ -0,0 +1,8 @@ +/* Package Information window title */ +"F0z-JX-Cv5.title" = "Información del Paquete"; + +/* button text to show Details */ +"Fvy-XJ-s38.label" = "Detalles"; + +/* button text to show Read Me */ +"waA-IW-Qyn.label" = "Léame"; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/es.lproj/KMKeyboardHelpWindowController.strings b/mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/es.lproj/KMKeyboardHelpWindowController.strings new file mode 100644 index 0000000000000000000000000000000000000000..5807588acd339fa03f23bc7cf2d60f2f2d6fbc56 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMKeyboardHelpWindow/es.lproj/KMKeyboardHelpWindowController.strings @@ -0,0 +1,8 @@ +/* Keyboard Help window title */ +"F0z-JX-Cv5.title" = "Ayuda del teclado"; + +/* OK button text */ +"Zla-QF-m0K.title" = "Aceptar"; + +/* Print button text */ +"fYY-Uj-y4S.title" = "Imprimir..."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/ckl.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/ckl.lproj/Localizable.strings index 20f433682127822207057df2a6f1cf7b89496f16..2be889423a286e7e748096c94122a4ffa7ce8b0f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/ckl.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/ckl.lproj/Localizable.strings @@ -68,4 +68,4 @@ "copyright-label" = "Kopirait:"; /* message displayed to alert user to need grant accessibility permission */ -"privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +"privacy-alert-text" = "Akuya ka dzǝ ʻya kǝthlǝr daku, Keyman ada bara ɓui whalǝr lukwa suyerere:\n\nNgyar whalǝr ʻyati su akwa Sǝr Nwɛyere nam Tsar Tsaryere, Su tubwar Lapiyakur & Sǝr Kǝrngɛ.\nƁǝla hyanti ka dzǝ badicini ʻya kǝthlǝr sǝr nuwɛyere nǝngɛ ani system."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings index 59a867146bc707ab0e45e49a9fd8afecd51e3f68..4a6b485f22782dca14b2c73a4a6ad5d5d30a8695 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings +++ b/mac/Keyman4MacIM/Keyman4MacIM/es-419.lproj/Localizable.strings @@ -68,4 +68,4 @@ "copyright-label" = "Derechos de autor:"; /* message displayed to alert user to need grant accessibility permission */ -"privacy-alert-text" = "To function properly, Keyman requires accessibility features:\n\nGrant access in System Preferences, Security & Privacy.\nRestart your system."; +"privacy-alert-text" = "Para funcionar correctamente, Keyman requiere funciones de accesibilidad:\n\nPermitir el acceso en Preferencias del Sistema, Seguridad y Privacidad.\nReinicie el sistema."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings new file mode 100644 index 0000000000000000000000000000000000000000..7c8cdc73fce03b9b7a09390c347ae78fcad08b63 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/Localizable.strings @@ -0,0 +1,71 @@ +/* Message displayed to confirm delete of the Keyman keyboard selected by the user from the keyboard list */ +"message-confirm-delete-keyboard" = "¿Seguro que quieres borrar el '%@' del teclado?"; + +/* Delete keyboard confirmation info indicating that the delete action cannot be undone */ +"info-cannot-undo-delete-keyboard" = "No puede deshacer esta acción."; + +/* Button text to cancel delete of the specified installed Keyman keyboard */ +"button-cancel-delete-keyboard" = "Cancelar"; + +/* Button text to confirm delete of the specified installed Keyman keyboard */ +"button-delete-keyboard" = "Eliminar"; + +/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ +"message-confirm-install-keyboard" = "¿Instalar teclado Keyman?"; + +/* Message displayed to confirm installation of a keyboard from the specifed .kmp file double-clicked by the user */ +"info-install-keyboard-filename" = "¿Quiere que Keyman instale el teclado desde el fichero '%@'?"; + +/* Button text to cancel installation of the double-clicked Keyman file */ +"button-cancel-install-keyboard" = "Cancelar"; + +/* Button text to confirm installation of the double-clicked Keyman file */ +"button-install-keyboard" = "Instalar"; + +/* Message displayed to inform user that .kmp file could not be read/unzipped */ +"message-keyboard-file-unreadable" = "No se ha podido leer el archivo Keyman '%@'."; + +/* Message displayed in Configuration window when keyboard cannot be loaded */ +"message-error-loading-keyboard" = "(error al cargar el teclado)"; + +/* Message displayed in Configuration window when keyboard metadata cannot be loaded */ +"message-error-unknown-metadata" = "desconocido"; + +/* Button text to acknowledge that .kmp file could not be read */ +"button-keyboard-file-unreadable" = "Aceptar"; + +/* label text to identify Keyman version */ +"version-label-text" = "Versión %@"; + +/* Status text for keyboard downloading */ +"message-keyboard-downloading" = "Descargando..."; + +/* Status text for keyboard download complete */ +"message-keyboard-download-complete" = "دانلود کامل شد."; + +/* Button text to cancel downloading keyboard */ +"button-cancel-downloading" = "Cancelar"; + +/* Button text keyboard download complete */ +"button-download-complete" = "Listo"; + +/* keyboards label in the Package Information window */ +"keyboards-label" = "Teclados:"; + +/* fonts label in the Package Information window */ +"fonts-label" = "Fuentes:"; + +/* package version label in the Package Information window */ +"package-version-label" = "Versión del paquete:"; + +/* author label in the Package Information window */ +"author-label" = "Autor:"; + +/* website label in the Package Information window */ +"website-label" = "Sitio Web:"; + +/* copyright label in the Package Information window */ +"copyright-label" = "Derechos de autor:"; + +/* message displayed to alert user to need grant accessibility permission */ +"privacy-alert-text" = "Para funcionar correctamente, Keyman requiere las siguientes características de accesibilidad:\nel acceso en Preferencias del Sistema, Seguridad y Privacidad.\nel reinicio del sistema."; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings new file mode 100644 index 0000000000000000000000000000000000000000..83f4c3018e5469fe89542430a408a2a0d56a201a --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/es.lproj/MainMenu.strings @@ -0,0 +1,14 @@ +/* Configuration window menu text */ +"P1b-lE-yFw.title" = "Configuración..."; + +/* Keyboards menu text */ +"bQa-j9-nHe.title" = "Teclados"; + +/* Keyboards menu text */ +"goP-aK-3WB.title" = "Teclados"; + +/* About menu text */ +"kb2-ww-RS3.title" = "Acerca de"; + +/* On-Screen Keyboard menu text */ +"s96-JI-YDh.title" = "Teclado en pantalla"; diff --git a/mac/build.sh b/mac/build.sh index 5b6a3f525b8e4e9296e4be223f9fa0df6fe521f6..a6c7915603b58545c1d54c572ebf89d872863497 100755 --- a/mac/build.sh +++ b/mac/build.sh @@ -1,6 +1,4 @@ #!/usr/bin/env bash - -set -e ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -12,6 +10,7 @@ KEYMAN_MAC_BASE_PATH="$KEYMAN_ROOT/mac" # Include our resource functions; they're pretty useful! . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" . "$KEYMAN_ROOT/resources/build/build-help.inc.sh" +. "$KEYMAN_ROOT/mac/mac-utils.inc.sh" # This script runs from its own folder cd "$(dirname "$THIS_SCRIPT")" @@ -390,10 +389,6 @@ if $DO_KEYMANIM ; then ENTITLEMENTS_FILE=Keyman.entitlements fi - if [ -z "${DEVELOPMENT_TEAM+x}" ]; then - DEVELOPMENT_TEAM=3YE4W86L3G - fi - # We need to re-sign the app after updating the plist file if $DO_CODESIGN ; then execCodeSign --force --sign $CERTIFICATE_ID --timestamp --verbose --preserve-metadata=identifier,entitlements "$KM4MIM_BASE_PATH/build/$CONFIG/Keyman.app/Contents/Frameworks/Sentry.framework" @@ -440,7 +435,6 @@ if $PREPRELEASE || $NOTARIZE; then TARGET_PATH="$KM4MIM_BASE_PATH/build/$CONFIG" TARGET_APP_PATH="$TARGET_PATH/$PRODUCT_NAME.app" TARGET_ZIP_PATH="$TARGET_PATH/$PRODUCT_NAME.zip" - ALTOOL_LOG_PATH="$TARGET_PATH/altool.log" # Note: get-task-allow entitlement must be *off* in our release build (to do this, don't include base entitlements in project build settings) @@ -455,49 +449,8 @@ if $PREPRELEASE || $NOTARIZE; then /usr/bin/ditto -c -k --keepParent "$TARGET_APP_PATH" "$TARGET_ZIP_PATH" builder_heading "Uploading Keyman.zip to Apple for notarization" + mac_notarize "$TARGET_PATH" "$TARGET_ZIP_PATH" - xcrun altool --notarize-app --primary-bundle-id "com.Keyman.im.zip" --asc-provider "$APPSTORECONNECT_PROVIDER" --username "$APPSTORECONNECT_USERNAME" --password @env:APPSTORECONNECT_PASSWORD --file "$TARGET_ZIP_PATH" --output-format xml > $ALTOOL_LOG_PATH || ( - ALTOOL_CODE=$? - cat "$ALTOOL_LOG_PATH" - builder_die "altool failed with code $ALTOOL_CODE" - ) - cat "$ALTOOL_LOG_PATH" - - ALTOOL_UUID=$(/usr/libexec/PlistBuddy -c "Print notarization-upload:RequestUUID" "$ALTOOL_LOG_PATH") - ALTOOL_FINISHED=0 - - while [ $ALTOOL_FINISHED -eq 0 ] - do - # We'll sleep 30 seconds before checking status, to give the altool server time to process the archive - echo "Waiting 30 seconds for status" - sleep 30 - xcrun altool --notarization-info "$ALTOOL_UUID" --username "$APPSTORECONNECT_USERNAME" --password @env:APPSTORECONNECT_PASSWORD --output-format xml > "$ALTOOL_LOG_PATH" || ( - ALTOOL_CODE=$? - ALTOOL_PRODUCT_ERROR=$(/usr/libexec/PlistBuddy -c "Print product-errors:0:code" "$ALTOOL_LOG_PATH") - if [ "$ALTOOL_PRODUCT_ERROR" == 1519 ]; then - # Could not find the RequestUUID; this is a temporary error sometimes returned by Apple. - # We'll just keep retrying. - continue; - fi - cat "$ALTOOL_LOG_PATH" - builder_die "altool failed with code $ALTOOL_CODE" - ) - ALTOOL_STATUS=$(/usr/libexec/PlistBuddy -c "Print notarization-info:Status" "$ALTOOL_LOG_PATH") - if [ "$ALTOOL_STATUS" == "success" ]; then - ALTOOL_FINISHED=1 - elif [ "$ALTOOL_STATUS" != "in progress" ]; then - # Probably failing with 'invalid' - cat "$ALTOOL_LOG_PATH" - ALTOOL_LOG_URL=$(/usr/libexec/PlistBuddy -c "Print notarization-info:LogFileURL" "$ALTOOL_LOG_PATH") - curl "$ALTOOL_LOG_URL" - builder_die "Notarization failed with $ALTOOL_STATUS; check log at $ALTOOL_LOG_PATH" - fi - done - - builder_heading "Notarization completed successfully. Review logs below for any warnings." - cat "$ALTOOL_LOG_PATH" - ALTOOL_LOG_URL=$(/usr/libexec/PlistBuddy -c "Print notarization-info:LogFileURL" "$ALTOOL_LOG_PATH") - curl "$ALTOOL_LOG_URL" echo builder_heading "Attempting to staple notarization to Keyman.app" xcrun stapler staple "$TARGET_APP_PATH" || builder_die "stapler failed" diff --git a/mac/help/about/requirements.md b/mac/help/about/requirements.md index e4d211cd29f59f85d7acc9338dc69303037ecfaf..1775c0dd8c21dd0344101994dd0b179f45535b26 100644 --- a/mac/help/about/requirements.md +++ b/mac/help/about/requirements.md @@ -13,6 +13,10 @@ Keyman supports the following Mac operating systems: * macOS Mojave (10.14) * macOS Catalina (10.15) * macOS Big Sur (11.0) +* macOS Monterey (12.0) +* macOS Ventura (13.0) + +Keyman should also work on future releases of macOS, even if they are not yet listed here. ## Resource Requirements diff --git a/mac/mac-utils.inc.sh b/mac/mac-utils.inc.sh new file mode 100644 index 0000000000000000000000000000000000000000..7776100af9bb56fe160d93c6caf4f9441e98a09e --- /dev/null +++ b/mac/mac-utils.inc.sh @@ -0,0 +1,38 @@ + +if [ -z "${DEVELOPMENT_TEAM+x}" ]; then + DEVELOPMENT_TEAM=3YE4W86L3G +fi + +function mac_notarize() { + local TARGET_PATH="$1" + local TARGET_FILE="$2" + + local NOTARYTOOL_LOG_PATH="$TARGET_PATH/notarytool.log" + + xcrun notarytool submit \ + --apple-id "$APPSTORECONNECT_USERNAME" \ + --team-id "$DEVELOPMENT_TEAM" \ + --password "$APPSTORECONNECT_PASSWORD" \ + --output-format json \ + --wait \ + "$TARGET_FILE" > "$NOTARYTOOL_LOG_PATH" + # notarytool output: {"status":"Accepted","id":"ca62bba0-6c49-43c2-90d8-83a8ef306e0f","message":"Processing complete"} + + cat "$NOTARYTOOL_LOG_PATH" + local NOTARYTOOL_STATUS=`cat "$NOTARYTOOL_LOG_PATH" | jq -r .status` + local NOTARYTOOL_SUBMISSION_ID=`cat "$NOTARYTOOL_LOG_PATH" | jq -r .id` + if [[ "$NOTARYTOOL_STATUS" != Accepted ]]; then + # We won't assume notarytool returns an error code if status != Accepted + builder_die "Notarization failed with $NOTARYTOOL_STATUS" + fi + + builder_heading "Notarization completed successfully. Review logs below for any warnings." + + xcrun notarytool log \ + --apple-id "$APPSTORECONNECT_USERNAME" \ + --team-id "$DEVELOPMENT_TEAM" \ + --password "$APPSTORECONNECT_PASSWORD" \ + "$NOTARYTOOL_SUBMISSION_ID" + + rm -f "$NOTARYTOOL_LOG_PATH" +} diff --git a/mac/setup/build.sh b/mac/setup/build.sh index 40860222daf7d884a4b6f2ced65160d6059e6c7f..1fea3828f2d7da35fd65a424e48276cc497b417d 100755 --- a/mac/setup/build.sh +++ b/mac/setup/build.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -set -e -set -u - ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -11,6 +7,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" # Include our resource functions; they're pretty useful! . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" +. "$KEYMAN_ROOT/mac/mac-utils.inc.sh" # Please note that this build script (understandably) assumes that it is running on Mac OS X. verify_on_mac @@ -81,43 +78,14 @@ codesign --force --options runtime --deep --sign "${CERTIFICATE_ID}" "$TARGETAPP TARGET_ZIP_PATH="$TARGETPATH/Install Keyman.zip" TARGET_APP_PATH="$TARGETAPP" -ALTOOL_LOG_PATH="$TARGETPATH/altool.log" builder_heading "Zipping Install Keyman.app for notarization to $TARGET_ZIP_PATH" /usr/bin/ditto -c -k --keepParent "$TARGET_APP_PATH" "$TARGET_ZIP_PATH" builder_heading "Uploading Install Keyman.zip to Apple for notarization" +mac_notarize "$TARGETPATH" "$TARGET_ZIP_PATH" -xcrun altool --notarize-app --primary-bundle-id "com.Keyman.install.zip" --asc-provider "$APPSTORECONNECT_PROVIDER" --username "$APPSTORECONNECT_USERNAME" --password @env:APPSTORECONNECT_PASSWORD --file "$TARGET_ZIP_PATH" --output-format xml > $ALTOOL_LOG_PATH || builder_die "altool failed" -cat "$ALTOOL_LOG_PATH" - -ALTOOL_UUID=$(/usr/libexec/PlistBuddy -c "Print notarization-upload:RequestUUID" "$ALTOOL_LOG_PATH") -ALTOOL_FINISHED=0 - -while [ $ALTOOL_FINISHED -eq 0 ] -do - # We'll sleep 30 seconds before checking status, to give the altool server time to process the archive - echo "Waiting 30 seconds for status" - sleep 30 - xcrun altool --notarization-info "$ALTOOL_UUID" --username "$APPSTORECONNECT_USERNAME" --password @env:APPSTORECONNECT_PASSWORD --output-format xml > "$ALTOOL_LOG_PATH" || builder_die "altool failed" - ALTOOL_STATUS=$(/usr/libexec/PlistBuddy -c "Print notarization-info:Status" "$ALTOOL_LOG_PATH") - if [ "$ALTOOL_STATUS" == "success" ]; then - ALTOOL_FINISHED=1 - elif [ "$ALTOOL_STATUS" != "in progress" ]; then - # Probably failing with 'invalid' - cat "$ALTOOL_LOG_PATH" - ALTOOL_LOG_URL=$(/usr/libexec/PlistBuddy -c "Print notarization-info:LogFileURL" "$ALTOOL_LOG_PATH") - curl "$ALTOOL_LOG_URL" - builder_die "Notarization failed with $ALTOOL_STATUS; check log at $ALTOOL_LOG_PATH" - fi -done - -builder_heading "Notarization completed successfully. Review logs below for any warnings." -cat "$ALTOOL_LOG_PATH" -ALTOOL_LOG_URL=$(/usr/libexec/PlistBuddy -c "Print notarization-info:LogFileURL" "$ALTOOL_LOG_PATH") -curl "$ALTOOL_LOG_URL" -echo builder_heading "Attempting to staple notarization to Install Keyman.app" xcrun stapler staple "$TARGET_APP_PATH" || builder_die "stapler failed" diff --git a/oem/firstvoices/ios/build.sh b/oem/firstvoices/ios/build.sh index a3e7843c4399f447d9c8178c4b41c623f5dcb9a7..061d7b760e1b1058a1b1aed68ee06efbefec97fe 100755 --- a/oem/firstvoices/ios/build.sh +++ b/oem/firstvoices/ios/build.sh @@ -1,14 +1,141 @@ #!/usr/bin/env bash -set -e +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +# Include our resource functions; they're pretty useful! +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# Please note that this build script (understandably) assumes that it is running on Mac OS X. +verify_on_mac export TARGET=FirstVoices -export KEYBOARDS_TARGET="$TARGET/Keyboards" -export KEYBOARDS_CSV_TARGET="$TARGET/Keyboards/keyboards.csv" -# TODO: support passing -copy-keyboards, -debug, -clean etc in to build_keyboards -../common/build_keyboards.sh -download-keyboards +builder_describe "Builds the $TARGET app for use on iOS devices - iPhone and iPad." \ + "@/ios:engine build" \ + "clean" \ + "configure" \ + "build" \ + "--debug Avoids codesigning and adds full sourcemaps for the embedded predictive-text engine" \ + "--sim-artifact Also outputs a simulator-friendly test artifact corresponding to the build" + +builder_parse "$@" + +KMEI_BUILD_DIR="$KEYMAN_ROOT/ios/build" +DERIVED_DATA="$THIS_SCRIPT_PATH/build" + +do_clean ( ) { + rm -rf "$DERIVED_DATA" + rm -rf Carthage +} + +### START OF THE BUILD ### + +CONFIG=Release +if builder_is_debug_build; then + CONFIG="Debug" +fi + +KEYMAN_ENGINE_FRAMEWORK_SRC="$KMEI_BUILD_DIR/Build/Products/$CONFIG/KeymanEngine.xcframework" +KEYMAN_ENGINE_FRAMEWORK_DST=./ + +# First things first - update our dependencies. + +function carthage_die() { + local msg="$1" + + # Don't leave a trace of the failed folder; we'd have to rebuild stuff anyway. + # This way, a later re-run doesn't think `configure` succeeded when it did not. + rm -rf Carthage + builder_die "$1" +} + +function do_configure() { + export KEYBOARDS_TARGET="$TARGET/Keyboards" + export KEYBOARDS_CSV_TARGET="$TARGET/Keyboards/keyboards.csv" + # TODO: support passing -copy-keyboards, -debug, -clean etc in to build_keyboards + ../common/build_keyboards.sh -download-keyboards + + echo + echo "Load dependencies with Carthage" + + carthage checkout || carthage_die "Carthage dependency checkout failed" + + # --no-use-binaries: due to https://github.com/Carthage/Carthage/issues/3134, + # which affects the sentry-cocoa dependency. + carthage build --use-xcframeworks --no-use-binaries --platform iOS || carthage_die "Carthage dependency loading failed" +} + +# +# Build Target Application +# + +BUILD_PATH="$DERIVED_DATA/Build/Products" +XCODEFLAGS="-quiet -configuration $CONFIG" +XCODEFLAGS_EXT="$XCODEFLAGS -derivedDataPath \"$DERIVED_DATA\"" # -workspace $TARGET.xcworkspace" +ARCHIVE_PATH="$BUILD_PATH/${CONFIG}-iphoneos/$TARGET.xcarchive" + +CODE_SIGN= +if builder_is_debug_build; then + CODE_SIGN="CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO ${DEV_TEAM:-} CODE_SIGN_ENTITLEMENTS= CODE_SIGNING_ALLOWED=NO" +fi + +function do_build() { + # Copy resources. + cp -Rf "$KEYMAN_ENGINE_FRAMEWORK_SRC" "$KEYMAN_ENGINE_FRAMEWORK_DST" + + if ! builder_is_debug_build; then + # Huh, this is inverted from the main Keyman build, which goes .xcarchive -> .ipa instead. + + # Time to prepare the deployment archive data. + echo "" + echo "Preparing .ipa file for deployment to real devices." + run_xcodebuild $XCODEFLAGS_EXT \ + -scheme $TARGET \ + -archivePath "$ARCHIVE_PATH" \ + archive \ + -allowProvisioningUpdates \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG + + assertDirExists "$ARCHIVE_PATH" + + + # Do NOT use the _EXT variant here; there's no scheme to ref, which will lead + # Xcode to generate a build error. + run_xcodebuild $XCODEFLAGS \ + -exportArchive \ + -archivePath "$ARCHIVE_PATH" \ + -exportOptionsPlist exportAppStore.plist \ + -exportPath "$BUILD_PATH/${CONFIG}-iphoneos" + -allowProvisioningUpdates \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG + else + run_xcodebuild $CODE_SIGN \ + $XCODEFLAGS_EXT \ + -scheme "$TARGET" \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG + fi + + if builder_has_option --sim-artifact; then + echo "Preparing .app file as Simulator-targeted build artifact." + run_xcodebuild $CODE_SIGN \ + $XCODEFLAGS_EXT \ + -scheme "$TARGET" \ + -sdk iphonesimulator \ + VERSION=$VERSION \ + VERSION_WITH_TAG=$VERSION_WITH_TAG + fi +} -TARGET=FirstVoices -# TODO: in the future build_common.sh should probably be shared with all oem products? -source build_common.sh +builder_run_action clean do_clean +builder_run_action configure do_configure +builder_run_action build do_build \ No newline at end of file diff --git a/oem/firstvoices/ios/build_common.sh b/oem/firstvoices/ios/build_common.sh deleted file mode 100755 index 04c5ee24cec91cb2bb7592447b349fbb3d06ed30..0000000000000000000000000000000000000000 --- a/oem/firstvoices/ios/build_common.sh +++ /dev/null @@ -1,225 +0,0 @@ -#!/usr/bin/env bash - -## START STANDARD BUILD SCRIPT INCLUDE -# adjust relative paths as necessary -THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" -. "${THIS_SCRIPT%/*}/../../../resources/build/build-utils.sh" -## END STANDARD BUILD SCRIPT INCLUDE - -# Include our resource functions; they're pretty useful! -. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" - -# Please note that this build script (understandably) assumes that it is running on Mac OS X. -if [[ "${OSTYPE}" == *"darwin" ]]; then - echo "This build script will only run in a Mac environment." - exit 1 -fi - -if [ -z "$TARGET" ]; then - exit 1 -fi - -display_usage ( ) { - echo "build.sh [-no-update] | [-no-archive] | [-lib-build] | [-lib-ignore] | [-clean]" - echo - echo " -clean Removes all previously-existing build products for this project before building." - echo " -no-update If an in-place copy of KeymanEngine.framework exists, does not seek out an updated copy." - echo " -lib-build Actively rebuilds KMEI before copying its build products to project resources." - echo " -lib-nobuild Prevents the build script from building KeymanEngine under any circumstances." - echo " -no-carthage Disables downloading and building for dependencies." - echo " -no-codesign Performs the build without code signing." - echo " -debug Sets the configuration to debug mode instead of release." - echo " -add-sim-artifact Also produce a Simulator-installable .app build artifact" - echo - echo " If no settings are specified this script will grab a copy of the most recent build of KeymanEngine," - echo " performing an initial build of it if necessary." - exit 1 -} - -assert ( ) { - if ! [ -f $1 ]; then - echo "Build failed: missing $1" - exit 1 - fi -} - -verify_KMEI ( ) { - KMEI_BUILD_EXISTS=true - [ -d "$KEYMAN_ENGINE_FRAMEWORK_SRC" ] || KMEI_BUILD_EXISTS=false -} - -KMEI_BUILD_DIR="$KEYMAN_ROOT/ios/" - -BUILD_FOLDER=build - -do_clean ( ) { - rm -rf $BUILD_FOLDER - rm -rf Carthage -} - -### START OF THE BUILD ### - -DO_UPDATE=true -DO_ARCHIVE=true -DO_SIMULATOR_TARGET=false -DO_CARTHAGE=true -FORCE_KMEI_BUILD=false -ALLOW_KMEI_BUILD=true -CODE_SIGN=true -CONFIG=Release -KMEI_FLAGS= - -while [[ $# -gt 0 ]] ; do - key="$1" - case $key in - -no-update) - DO_UPDATE=false - ALLOW_KMEI_BUILD=false - ;; - -lib-build) - FORCE_KMEI_BUILD=true - ;; - -lib-nobuild) - ALLOW_KMEI_BUILD=false - ;; - -no-codesign) - CODE_SIGN=false - KMEI_FLAGS="$KMEI_FLAGS -no-codesign" - ;; - -no-carthage) - DO_CARTHAGE=false - KMEI_FLAGS="$KMEI_FLAGS -no-carthage" - ;; - -no-archive) - DO_ARCHIVE=false - ;; - -h|-\?) - display_usage - ;; - -clean) - do_clean - KMEI_FLAGS="$KMEI_FLAGS -clean" - # FORCE_KMEI_BUILD=true - ;; - -debug) - CONFIG=Debug - KMEI_FLAGS="$KMEI_FLAGS -debug" - ;; - -add-sim-artifact) - DO_SIMULATOR_TARGET=true - ;; - esac - shift -done - -# -# Build Keyman Engine -# - -if [ $CONFIG = "Debug" ]; then - # When doing a debug run, we'd like to be able to run the result in the Simulator. - # 'universal' can be run both on real devices and in the simulator. - PLATFORM_TARGET="universal" -else - # Actual release builds can't have Simulator-targeted content; Apple will block the submission. - PLATFORM_TARGET="iphoneos" -fi - -KEYMAN_ENGINE_FRAMEWORK_SRC="$KMEI_BUILD_DIR/build/Build/Products/$CONFIG/KeymanEngine.xcframework" -KEYMAN_ENGINE_FRAMEWORK_DST=./ - -if [ $DO_UPDATE = true ]; then - # Does a prior build of KMEI exist? - verify_KMEI - - if [ $ALLOW_KMEI_BUILD = true ] && [ $FORCE_KMEI_BUILD = false ] && [ $KMEI_BUILD_EXISTS = false ]; then - echo "Previous KeymanEngine build information is unavailable." - FORCE_KMEI_BUILD=true - fi - - if [ $FORCE_KMEI_BUILD = true ]; then - echo "Building KeymanEngine..." - base_dir="$(pwd)" - - cd $KMEI_BUILD_DIR - ./build.sh -only-framework $KMEI_FLAGS - cd $base_dir - fi - - verify_KMEI - - if ! [ $KMEI_BUILD_EXISTS ]; then - echo "Build failed: could not build required KeymanEngine resources." - exit 1 - fi - - # Copy resources. - /bin/cp -Rf "$KEYMAN_ENGINE_FRAMEWORK_SRC" "$KEYMAN_ENGINE_FRAMEWORK_DST" -fi - -# First things first - update our dependencies. - -if [ $DO_CARTHAGE = true ]; then - echo - echo "Load dependencies with Carthage" - - carthage checkout || builder_die "Carthage dependency loading failed" - - # --no-use-binaries: due to https://github.com/Carthage/Carthage/issues/3134, - # which affects the sentry-cocoa dependency. - carthage build --use-xcframeworks --no-use-binaries --platform iOS || builder_die "Carthage dependency loading failed" -fi - -# -# Build Target Application -# - -DERIVED_DATA=build -BUILD_PATH=$DERIVED_DATA/Build/Products -XCODEFLAGS="-quiet -configuration $CONFIG" -XCODEFLAGS_EXT="$XCODEFLAGS -derivedDataPath $DERIVED_DATA" # -workspace $TARGET.xcworkspace" -ARCHIVE_PATH=$BUILD_PATH/${CONFIG}-iphoneos/$TARGET.xcarchive - -if [ $CODE_SIGN = true ]; then - - if [ $DO_ARCHIVE = true ]; then - # Time to prepare the deployment archive data. - echo "" - echo "Preparing .ipa file for deployment to real devices." - run_xcodebuild $XCODEFLAGS_EXT -scheme $TARGET -archivePath $ARCHIVE_PATH archive -allowProvisioningUpdates \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG - - assertDirExists "$ARCHIVE_PATH" - - - # Do NOT use the _EXT variant here; there's no scheme to ref, which will lead - # Xcode to generate a build error. - run_xcodebuild $XCODEFLAGS -exportArchive -archivePath $ARCHIVE_PATH -exportOptionsPlist exportAppStore.plist \ - -exportPath $BUILD_PATH/${CONFIG}-iphoneos -allowProvisioningUpdates \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG - else - run_xcodebuild $XCODEFLAGS_EXT -scheme "$TARGET" \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG - fi -else - run_xcodebuild CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \ - $XCODEFLAGS_EXT -scheme "$TARGET" \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG -fi - -if [ $DO_SIMULATOR_TARGET == true ]; then - echo "Preparing .app file as Simulator-targeted build artifact." - run_xcodebuild CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \ - $XCODEFLAGS_EXT -scheme "$TARGET" \ - -sdk iphonesimulator \ - VERSION=$VERSION \ - VERSION_WITH_TAG=$VERSION_WITH_TAG -fi - -if [ $? = 0 ]; then - echo "Build complete." -fi diff --git a/oem/firstvoices/ios/exportAppStore.plist b/oem/firstvoices/ios/exportAppStore.plist index 123af31a0c437e0927eddef23f03b3a850374642..c4ac972730f0715629ebd05914f11ab9f1026ca6 100644 --- a/oem/firstvoices/ios/exportAppStore.plist +++ b/oem/firstvoices/ios/exportAppStore.plist @@ -7,9 +7,9 @@ teamID D7TR486TEH signingCertificate - EFA9DE793B2A25F38270468AE62060CFF55CD634 + C4726D4C8BD4C2FE38A551B84CEDB8E4B3FD12D6 installerSigningCertificate - EFA9DE793B2A25F38270468AE62060CFF55CD634 + C4726D4C8BD4C2FE38A551B84CEDB8E4B3FD12D6 provisioningProfiles com.firstvoices.keyboards diff --git a/oem/firstvoices/windows/src/inst/firstvoices.wxs b/oem/firstvoices/windows/src/inst/firstvoices.wxs index eb580434324349414361b76274a8578e92dfc507..6388ec9b2bcca3d4e47aa6d8b7037c127d0e5d7a 100644 --- a/oem/firstvoices/windows/src/inst/firstvoices.wxs +++ b/oem/firstvoices/windows/src/inst/firstvoices.wxs @@ -9,7 +9,7 @@ @@ -17,16 +17,16 @@ - + - - @@ -34,13 +34,13 @@ - + - + - + @@ -52,20 +52,20 @@
- + - + - + - + @@ -73,19 +73,19 @@ - + - + - + - + @@ -98,7 +98,7 @@ - + @@ -111,7 +111,7 @@ - + @@ -130,7 +130,7 @@ - + @@ -138,9 +138,9 @@ - + - + @@ -174,7 +174,7 @@ - + 1 diff --git a/package-lock.json b/package-lock.json index 20d7ce98c40cb837e61ec0faba6fead166bd9e02..f07cd1bd6d5c58ee925a823a47363dbbf12c67fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "developer/src/kmc", "developer/src/server", "common/models/*", + "common/test/resources", "common/tools/*", "common/web/*", "common/predictive-text", @@ -34,6 +35,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.59.1", "chai": "^4.3.4", + "esbuild": "^0.15.16", "eslint": "^8.39.0", "eslint-config-standard-with-typescript": "^34.0.1", "eslint-plugin-import": "^2.27.5", @@ -42,14 +44,21 @@ "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "ts-node": "^10.9.1", + "tslib": "^2.5.2", "typescript": "^4.9.5" + }, + "engines": { + "node": "^18.x" } }, "common/models/templates": { "name": "@keymanapp/models-templates", "license": "MIT", "dependencies": { - "@keymanapp/models-wordbreakers": "*" + "@keymanapp/keyman-version": "*", + "@keymanapp/models-types": "*", + "@keymanapp/models-wordbreakers": "*", + "@keymanapp/resources-gosh": "*" }, "devDependencies": { "@keymanapp/models-types": "*", @@ -57,8 +66,10 @@ "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", "@types/node": "^14.0.4", + "c8": "^7.12.0", "chai": "^4.3.4", "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", "typescript": "^4.9.5" } }, @@ -82,10 +93,13 @@ }, "devDependencies": { "@keymanapp/models-types": "*", + "@keymanapp/resources-gosh": "*", "@types/chai": "^4.2.11", "@types/mocha": "^7.0.2", + "c8": "^7.12.0", "chai": "^4.3.4", "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", "ts-node": "^10.9.1", "typescript": "^4.9.5" } @@ -136,6 +150,22 @@ "dev": true, "license": "MIT" }, + "common/test/resources": { + "name": "@keymanapp/common-test-resources", + "license": "MIT", + "devDependencies": { + "@keymanapp/resources-gosh": "*", + "@types/node": "^10.17.21", + "chai": "^4.3.4", + "typescript": "^4.9.5" + } + }, + "common/test/resources/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true + }, "common/tools/hextobin": { "name": "@keymanapp/hextobin", "license": "MIT", @@ -220,7 +250,14 @@ }, "devDependencies": { "@keymanapp/resources-gosh": "*", + "c8": "^7.12.0", "chai": "^4.3.4", + "karma": "^6.4.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-teamcity-reporter": "^1.1.0", "mocha": "^10.0.0", "mocha-teamcity-reporter": "^4.0.0", "ts-node": "^10.9.1", @@ -260,10 +297,13 @@ "string.prototype.startswith": "^0.2.0" }, "devDependencies": { + "@keymanapp/common-test-resources": "*", "@keymanapp/models-types": "*", "@keymanapp/resources-gosh": "*", "@types/node": "^10.17.21", + "c8": "^7.12.0", "chai": "^4.3.4", + "combine-source-map": "^0.8.0", "karma": "^6.4.1", "karma-browserstack-launcher": "^1.6.0", "karma-chai": "^0.1.0", @@ -315,6 +355,17 @@ "typescript": "^4.9.5" } }, + "common/web/tslib": { + "name": "@keymanapp/tslib", + "dependencies": { + "tslib": "^2.5.2", + "typescript": "^4.9.5" + }, + "devDependencies": { + "@keymanapp/resources-gosh": "*", + "esbuild": "^0.15.16" + } + }, "common/web/types": { "name": "@keymanapp/common-types", "license": "MIT", @@ -322,7 +373,7 @@ "@keymanapp/keyman-version": "*", "ajv": "^8.11.0", "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50", - "semver": "^7.3.7", + "semver": "^7.5.2", "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829" }, "devDependencies": { @@ -563,6 +614,10 @@ "@keymanapp/keyman-version": "*", "@keymanapp/resources-gosh": "*", "@types/node": "^14.0.5", + "c8": "^7.12.0", + "chai": "^4.3.4", + "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", "typescript": "^4.9.5" } }, @@ -1030,13 +1085,15 @@ } }, "developer/src/kmc-ldml": { + "name": "@keymanapp/kmc-ldml", "license": "MIT", "dependencies": { "@keymanapp/keyman-version": "*", + "@keymanapp/kmc-kmn": "*", "@keymanapp/ldml-keyboard-constants": "*", "ajv": "^8.11.0", "restructure": "git+https://github.com/keymanapp/dependency-restructure.git#49d129cf0916d082a7278bb09296fb89cecfcc50", - "semver": "^7.3.7", + "semver": "^7.5.2", "xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829" }, "devDependencies": { @@ -2139,6 +2196,38 @@ "node": ">=12" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz", + "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz", + "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2285,6 +2374,10 @@ "resolved": "resources/build/version", "link": true }, + "node_modules/@keymanapp/common-test-resources": { + "resolved": "common/test/resources", + "link": true + }, "node_modules/@keymanapp/common-types": { "resolved": "common/web/types", "link": true @@ -2377,6 +2470,10 @@ "resolved": "common/tools/sourcemap-path-remapper", "link": true }, + "node_modules/@keymanapp/tslib": { + "resolved": "common/web/tslib", + "link": true + }, "node_modules/@keymanapp/web-sentry-manager": { "resolved": "common/web/sentry-manager", "link": true @@ -2624,6 +2721,11 @@ "node": ">=6" } }, + "node_modules/@sentry/browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@sentry/cli": { "version": "2.2.0", "dev": true, @@ -2658,6 +2760,11 @@ "node": ">=6" } }, + "node_modules/@sentry/core/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@sentry/hub": { "version": "5.30.0", "license": "BSD-3-Clause", @@ -2670,6 +2777,11 @@ "node": ">=6" } }, + "node_modules/@sentry/hub/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@sentry/minimal": { "version": "5.30.0", "license": "BSD-3-Clause", @@ -2682,6 +2794,11 @@ "node": ">=6" } }, + "node_modules/@sentry/minimal/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@sentry/node": { "version": "6.19.6", "license": "BSD-3-Clause", @@ -2755,6 +2872,11 @@ "node": ">=6" } }, + "node_modules/@sentry/node/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@sentry/types": { "version": "5.30.0", "license": "BSD-3-Clause", @@ -2773,6 +2895,11 @@ "node": ">=6" } }, + "node_modules/@sentry/utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "license": "MIT", @@ -4170,6 +4297,33 @@ "color-support": "bin.js" } }, + "node_modules/combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha512-UlxQ9Vw0b/Bt/KYwCFqdEwsQ1eL8d1gibiFb7lxQJFdvTgc2hIZi6ugsg+kyhzhPV+QEpUiEIwInIAIrgoEkrg==", + "dev": true, + "dependencies": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + } + }, + "node_modules/combine-source-map/node_modules/convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha512-Y8L5rp6jo+g9VEPgvqNfEopjTR4OTYct8lXlS8iVQdmnjDvbdbzYe9rjtFCB9egC86JoNCU61WRY+ScjkZpnIg==", + "dev": true + }, + "node_modules/combine-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "dev": true, @@ -4741,9 +4895,10 @@ }, "node_modules/esbuild": { "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz", + "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -4775,13 +4930,46 @@ "esbuild-windows-arm64": "0.15.18" } }, + "node_modules/esbuild-android-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz", + "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz", + "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/esbuild-darwin-64": { "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz", + "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4790,6 +4978,278 @@ "node": ">=12" } }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz", + "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz", + "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz", + "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz", + "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz", + "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz", + "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz", + "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz", + "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz", + "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz", + "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz", + "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz", + "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz", + "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz", + "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz", + "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz", + "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.18", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz", + "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "dev": true, @@ -6067,11 +6527,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-closure-compiler-java": { - "version": "20200224.0.0", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -6400,6 +6855,24 @@ "version": "2.0.4", "license": "ISC" }, + "node_modules/inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha512-0mVWSSbNDvedDWIN4wxLsdPM4a7cIPcpyMxj3QZ406QRwQ6ePGB1YIHxVPjqpcUGbWQ5C+nHTwGNWAGvt7ggVA==", + "dev": true, + "dependencies": { + "source-map": "~0.5.3" + } + }, + "node_modules/inline-source-map/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -7215,6 +7688,12 @@ "version": "4.4.2", "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha512-eDn9kqrAmVUC1wmZvlQ6Uhde44n+tXpqPrN8olQJbttgh0oKclk+SF54P47VEGE9CEiMeRwAP8BaM7UHvBkz2A==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8982,8 +9461,9 @@ "license": "ISC" }, "node_modules/semver": { - "version": "7.3.8", - "license": "ISC", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -9761,8 +10241,9 @@ } }, "node_modules/tslib": { - "version": "1.14.1", - "license": "0BSD" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", + "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -9779,6 +10260,12 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/tunnel": { "version": "0.0.6", "license": "MIT", @@ -10103,9 +10590,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10389,7 +10876,7 @@ "devDependencies": { "@types/node": "^13.7.0", "@types/semver": "^7.1.0", - "semver": "^7.1.2", + "semver": "^7.5.2", "ts-node": "^10.9.1" }, "engines": { @@ -10554,16 +11041,18 @@ "@keymanapp/lexical-model-layer": "*", "@keymanapp/models-types": "*", "@keymanapp/recorder-core": "*", + "@keymanapp/tslib": "*", "@keymanapp/web-utils": "*", "@types/node": "^11.9.4", - "eventemitter3": "^4.0.0" + "eventemitter3": "^4.0.0", + "tslib": "^2.5.2" }, "devDependencies": { "@keymanapp/resources-gosh": "*", "@keymanapp/web-sentry-manager": "*", "@sentry/cli": "2.2.0", + "c8": "^7.12.0", "chai": "^4.3.4", - "google-closure-compiler-java": "^20200224.0.0", "karma": "^6.4.1", "karma-browserstack-launcher": "^1.6.0", "karma-chai": "^0.1.0", diff --git a/package.json b/package.json index 7a2b53d50d53a777f786760652e187d9e6d5da02..c780b4199c6a10247e14688441e321c1df49bdad 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,19 @@ "name": "root", "private": true, "devDependencies": { - "chai": "^4.3.4", - "mocha": "^10.0.0", - "mocha-teamcity-reporter": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "^4.9.5", "@typescript-eslint/eslint-plugin": "^5.59.1", + "chai": "^4.3.4", + "esbuild": "^0.15.16", "eslint": "^8.39.0", "eslint-config-standard-with-typescript": "^34.0.1", "eslint-plugin-import": "^2.27.5", "eslint-plugin-n": "^15.7.0", - "eslint-plugin-promise": "^6.1.1" + "eslint-plugin-promise": "^6.1.1", + "mocha": "^10.0.0", + "mocha-teamcity-reporter": "^4.0.0", + "ts-node": "^10.9.1", + "tslib": "^2.5.2", + "typescript": "^4.9.5" }, "scripts": {}, "workspaces": [ @@ -28,6 +30,7 @@ "developer/src/kmc", "developer/src/server", "common/models/*", + "common/test/resources", "common/tools/*", "common/web/*", "common/predictive-text", @@ -35,10 +38,13 @@ "web" ], "dependencies": { + "@keymanapp/common-types": "file:common/web/types", + "@keymanapp/developer-test-helpers": "file:developer/src/common/web/test-helpers", "@keymanapp/hextobin": "file:common/tools/hextobin", "@keymanapp/keyman-version": "file:common/web/keyman-version", - "@keymanapp/common-types": "file:common/web/types", - "@keymanapp/ldml-keyboard-constants": "file:core/include/ldml", - "@keymanapp/developer-test-helpers": "file:developer/src/common/web/test-helpers" + "@keymanapp/ldml-keyboard-constants": "file:core/include/ldml" + }, + "engines": { + "node": "^18.x" } } diff --git a/resources/build/build-utils-ci.inc.sh b/resources/build/build-utils-ci.inc.sh index de36ae9e9b606556869c22f2f7492240825070f7..cab449a4636d59e5f9a388bfad84601d165e0956 100644 --- a/resources/build/build-utils-ci.inc.sh +++ b/resources/build/build-utils-ci.inc.sh @@ -100,18 +100,7 @@ function _builder_publish_npm_package() { dry_run=--dry-run fi - # We use --no-git-tag-version because our CI system controls version numbering - # and already tags releases. We also want to have the version of this match - # the release of Keyman Developer -- these two versions should be in sync. - # Because this is a large repo with multiple projects and build systems, it's - # better for us that individual build systems don't take too much ownership of - # git tagging. :) - npm version --allow-same-version --no-git-tag-version --no-commit-hooks "$VERSION_WITH_TAG" - - # Update all @keymanapp/* [*]dependencies in package.json to the current - # version-with-tag, so that the published version has precise dependencies, - # and we don't accidentally end up with either older or newer deps here - _builder_npm_set_dependency_version + _builder_write_npm_version # Note: In either case, npm publish MUST be given --access public to publish a # package in the @keymanapp scope on the public npm package index. @@ -128,18 +117,33 @@ function _builder_publish_npm_package() { fi } -# Updates all @keymanapp/* [*]dependencies in package.json to the current -# version-with-tag, so that the published version has precise dependencies, and -# we don't accidentally end up with either older or newer deps. This overwrites -# the local package.json, so it does need to be restored afterwards -function _builder_npm_set_dependency_version() { - cat package.json | "$JQ" --arg VERSION_WITH_TAG "$VERSION_WITH_TAG" \ - ' - . + - (try { dependencies: (.dependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + - (try { devDependencies: (.devDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + - (try { bundleDependencies: (.bundleDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + - (try { optionalDependencies: (.optionalDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) - ' > package1.json - mv -f package1.json package.json +function _builder_write_npm_version() { + # We use --no-git-tag-version because our CI system controls version numbering + # and already tags releases. We also want to have the version of this match + # the release of Keyman Developer -- these two versions should be in sync. + # Because this is a large repo with multiple projects and build systems, it's + # better for us that individual build systems don't take too much ownership of + # git tagging. :) + if ! "$JQ" -e '.version' package.json > /dev/null; then + pushd "$KEYMAN_ROOT" > /dev/null + npm version --allow-same-version --no-git-tag-version --no-commit-hooks --workspaces "$VERSION_WITH_TAG" + popd > /dev/null + fi + + # Updates all @keymanapp/* [*]dependencies in all package.jsons to the current + # version-with-tag, so that the published version has precise dependencies, and + # we don't accidentally end up with either older or newer deps. This overwrites + # the local package.json files, so they do need to be restored afterwards + find "$KEYMAN_ROOT" -name "package.json" -not -path '*/node_modules/*' -print0 | \ + while IFS= read -r -d '' line; do + cat "$line" | "$JQ" --arg VERSION_WITH_TAG "$VERSION_WITH_TAG" \ + ' + . + + (try { dependencies: (.dependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + + (try { devDependencies: (.devDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + + (try { bundleDependencies: (.bundleDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + + (try { optionalDependencies: (.optionalDependencies | to_entries | . + map(select(.key | match("@keymanapp/.*")) .value |= $VERSION_WITH_TAG) | from_entries) } catch {}) + ' > "${line}_" + mv -f "${line}_" "$line" + done } \ No newline at end of file diff --git a/resources/build/build-utils.sh b/resources/build/build-utils.sh index bba5589b19f72cd994917b6fe68d3946ba6ee0e2..cdb19442461c727783c2c0666ff6abc5e00feecf 100755 --- a/resources/build/build-utils.sh +++ b/resources/build/build-utils.sh @@ -74,24 +74,31 @@ function findVersion() { VERSION_TAG= fi - if [ -z "${TEAMCITY_VERSION-}" -a -z "${JENKINS_HOME-}" ]; then - # Local dev machine, not TeamCity + if [ -z "${TEAMCITY_VERSION-}" ] && [ -z "${GITHUB_ACTIONS-}" ]; then + # Local dev machine, not TeamCity or GitHub Action VERSION_TAG="$VERSION_TAG-local" VERSION_ENVIRONMENT=local - else + elif [ -n "${TEAMCITY_PR_NUMBER-}" ]; then # On TeamCity: are we running a pull request build or a master/beta/stable build? - if [ ! -z "${TEAMCITY_PR_NUMBER-}" ]; then - VERSION_ENVIRONMENT=test - # Note TEAMCITY_PR_NUMBER can also be 'master', 'beta', or 'stable-x.y' - # This indicates we are running a Test build. - if [[ $TEAMCITY_PR_NUMBER =~ ^(master|beta|stable(-[0-9]+\.[0-9]+)?)$ ]]; then - VERSION_TAG="$VERSION_TAG-test" - else - VERSION_TAG="$VERSION_TAG-test-$TEAMCITY_PR_NUMBER" - fi + VERSION_ENVIRONMENT="test" + # Note TEAMCITY_PR_NUMBER can also be 'master', 'beta', or 'stable-x.y' + # This indicates we are running a Test build. + if [[ $TEAMCITY_PR_NUMBER =~ ^(master|beta|stable(-[0-9]+\.[0-9]+)?)$ ]]; then + VERSION_TAG="$VERSION_TAG-test" else - VERSION_ENVIRONMENT="$TIER" + VERSION_TAG="$VERSION_TAG-test-$TEAMCITY_PR_NUMBER" fi + elif [ -n "${GITHUB_ACTIONS-}" ] && ${GHA_TEST_BUILD-}; then + VERSION_ENVIRONMENT="test" + # Note GHA_BRANCH can be 'master', 'beta', or 'stable-x.y' + # This indicates we are running a Test build. + if [[ ${GHA_BRANCH-} =~ ^(master|beta|stable(-[0-9]+\.[0-9]+)?)$ ]]; then + VERSION_TAG="${VERSION_TAG}-test" + else + VERSION_TAG="${VERSION_TAG}-test-${GHA_BRANCH-unset}" + fi + else + VERSION_ENVIRONMENT="$TIER" fi VERSION_WITH_TAG="$VERSION$VERSION_TAG" @@ -293,7 +300,7 @@ set_keyman_standard_build_path() { # For CI compatbility of building Keyman for Android 16.0 with OpenJDK 8, # this overrides JAVA_HOME for the builder script to use OpenJDK 11. set_java_home() { - if [[ ! -z {$JAVA_HOME_11+x} ]]; then + if [[ ! -z ${JAVA_HOME_11+x} ]]; then builder_echo "Setting JAVA_HOME to JAVA_HOME_11 ($JAVA_HOME_11)" export JAVA_HOME="${JAVA_HOME_11}" fi diff --git a/resources/build/help-keyman-com.sh b/resources/build/help-keyman-com.sh index fbe11afce22d40ee970a426a0c2bad02c43cf9ae..336584c52ab507cd35a023a999fcf1538efdd839 100755 --- a/resources/build/help-keyman-com.sh +++ b/resources/build/help-keyman-com.sh @@ -83,7 +83,7 @@ function help_product_path { function generate_markdown_help { if [ $platform == 'linux' ]; then pushd $KEYMAN_ROOT/linux/keyman-config > /dev/null - ./build-help.sh --md + ./build.sh build popd fi } diff --git a/resources/build/increment-version.sh b/resources/build/increment-version.sh index d54990194c68f8f91258ef4e189f95c09d82f918..445459da1619aeaf243e04f171e1c7799aa4988a 100755 --- a/resources/build/increment-version.sh +++ b/resources/build/increment-version.sh @@ -143,7 +143,7 @@ if [ "$action" == "commit" ]; then popd > /dev/null # - # Trigger builds for the previous version on TeamCity, Jenkins and GitHub + # Trigger builds for the previous version on TeamCity and GitHub # triggerBuilds diff --git a/resources/build/run-required-test-builds.sh b/resources/build/run-required-test-builds.sh index 0ef9e43f9c759db55c33288610779b3050b4c0bc..300bd831567f05c6f483029cccbf9f752337f696 100755 --- a/resources/build/run-required-test-builds.sh +++ b/resources/build/run-required-test-builds.sh @@ -54,11 +54,7 @@ function triggerTestBuilds() { eval test_builds='(${'bc_test_$platform'[@]})' for test_build in "${test_builds[@]}"; do if [[ $test_build == "" ]]; then continue; fi - if [ "${test_build:(-8)}" == "_Jenkins" ]; then - local job=${test_build%_Jenkins} - echo " -- Triggering build configuration $job/$branch on Jenkins" - triggerJenkinsBuild "$job" "$branch" "$force" - elif [ "${test_build:(-7)}" == "_GitHub" ]; then + if [ "${test_build:(-7)}" == "_GitHub" ]; then local job=${test_build%_GitHub} echo " -- Triggering GitHub action build $job/$branch" triggerGitHubActionsBuild true "$job" "$branch" @@ -90,7 +86,7 @@ fi # echo ". Get information about pull request #$PRNUM from GitHub" -prinfo=`curl -s -H "User-Agent: @keymanapp" https://api.github.com/repos/keymanapp/keyman/pulls/$PRNUM` +prinfo=`curl -s -H "User-Agent: @keymanapp" -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/keymanapp/keyman/pulls/$PRNUM` prbase=`echo ${prinfo} | "$JQ" -r '.base.ref'` prhead=`echo ${prinfo} | "$JQ" -r '.head.ref'` prbaserepo=`echo ${prinfo} | "$JQ" -r '.base.repo.html_url'` diff --git a/resources/build/trigger-builds.inc.sh b/resources/build/trigger-builds.inc.sh index ac8d519d4a8dcb74e1a721442ee426645ef0d944..8d6297b9e1fa5b59174cd123e1d173bdeed56164 100644 --- a/resources/build/trigger-builds.inc.sh +++ b/resources/build/trigger-builds.inc.sh @@ -17,11 +17,7 @@ function triggerBuilds() { eval builds='(${'bc_${bcbase}_${platform}'[@]})' for build in "${builds[@]}"; do if [[ $build == "" ]]; then continue; fi - if [ "${build:(-8)}" == "_Jenkins" ]; then - local job=${build%_Jenkins} - echo Triggering Jenkins build "$job" "$base" "true" - triggerJenkinsBuild "$job" "$base" "true" - elif [ "${build:(-7)}" == "_GitHub" ]; then + if [ "${build:(-7)}" == "_GitHub" ]; then local job=${build%_GitHub} echo Triggering GitHub action build "$job" "$base" triggerGitHubActionsBuild false "$job" "$base" @@ -69,62 +65,6 @@ function triggerTeamCityBuild() { -d "$command" } -function triggerJenkinsBuild() { - local JENKINS_JOB="$1" - local JENKINS_BRANCH="${2:-master}" - - local JENKINS_SERVER=https://jenkins.lsdev.sil.org - - local FORCE="" - if [ "${3:-false}" == "true" ]; then - FORCE=", \"force\": true" - fi - - local TAG="" - # This will only be true if we created and pushed a tag - if [ "${action:-""}" == "commit" ]; then - TAG=", \"tag\": \"$VERSION_GIT_TAG\", \"tag2\": \"$VERSION_GIT_TAG\"" - fi - - if [[ $JENKINS_BRANCH != stable-* ]] && [[ $JENKINS_BRANCH =~ [0-9]+ ]]; then - JENKINS_BRANCH="PR-${JENKINS_BRANCH}" - fi - - local OUTPUT=$(curl --silent --write-out '\n' \ - -X POST \ - --header "token: $JENKINS_TOKEN" \ - --header "Content-Type: application/json" \ - $JENKINS_SERVER/generic-webhook-trigger/invoke \ - --data "{ \"project\": \"$JENKINS_JOB/$JENKINS_BRANCH\", \"branch\": \"$JENKINS_BRANCH\" $TAG $FORCE }") - - if echo "$OUTPUT" | grep -q "\"triggered\":true"; then - echo -n " job triggered: " - else - echo "##teamcity[buildProblem description='Triggering Jenkins build failed']" - echo -n " triggering failed: " - fi - - # Strip {"jobs":{ from the beginning of OUTPUT - OUTPUT=${OUTPUT#\{\"jobs\":\{} - # Split json string to lines with one job each - local jobs count - count=0 - IFS='|' jobs=(${OUTPUT//\},\"pipeline/\},|\"pipeline}) - # Find job that actually got triggered (or that we should have triggered) - for line in "${jobs[@]}"; do - if [[ $line == \"$JENKINS_JOB/$JENKINS_BRANCH* ]]; then - echo "$line" - count=$((++count)) - fi - done - if [[ $count < 1 ]]; then - # DEBUG - echo -n $OUTPUT - - echo - fi -} - function triggerGitHubActionsBuild() { local IS_TEST_BUILD="$1" local GITHUB_ACTION="$2" diff --git a/resources/build/trigger-definitions.inc.sh b/resources/build/trigger-definitions.inc.sh index 89a657242b363bbd3e5157374a5107d4b863cc22..4698719d4d7c300dd47dc54fbad05089dfe6b801 100644 --- a/resources/build/trigger-definitions.inc.sh +++ b/resources/build/trigger-definitions.inc.sh @@ -17,10 +17,15 @@ watch_web='common/models|common/predictive-text|common/web|core' watch_windows='common|core|web' watch_developer='common|core|web' +# Note, currently common_web build is a no-op because we rely on running the web +# tests on all three of our build platforms as seen below watch_common_web='common/web' -watch_common_windows='common/windows' -watch_common_mac='common/mac' -watch_common_linux='common/linux' +# Currently, we run web tests on all three of our build platforms, to ensure +# that they work in all our environments. So we need to include common/web in +# the matches for these +watch_common_windows='common/windows|common/web' +watch_common_mac='common/mac|common/web' +watch_common_linux='common/linux|common/web' # # Available build configurations and VCS identifiers; identifiers are somewhat inconsistent due @@ -29,9 +34,6 @@ watch_common_linux='common/linux' # These bc_x_y variables ARE used in trigger-builds.inc.sh by pattern so the names are important, # and you won't find them directly in a grep search. # -# _Jenkins should be appended to any build configuration (pipeline) name that is from Jenkins, -# not TeamCity. -# # _GitHub should be appended to any build configuration name that is from GitHub, not TeamCity. # Test Build Configurations @@ -41,7 +43,7 @@ bc_test_all=() bc_test_android=(KeymanAndroid_TestPullRequests KeymanAndroid_TestSamplesAndTestProjects) bc_test_ios=(Keyman_iOS_TestPullRequests Keyman_iOS_TestSamplesAndTestProjects) -bc_test_linux=(KeymanLinux_TestPullRequests Keyman_Common_KPAPI_TestPullRequests_Linux pipeline-keyman-packaging_Jenkins deb-pr-packaging_GitHub) +bc_test_linux=(KeymanLinux_TestPullRequests Keyman_Linux_Test_Integration Keyman_Common_KPAPI_TestPullRequests_Linux deb-pr-packaging_GitHub) bc_test_mac=(Keyman_KeymanMac_PullRequests Keyman_Common_KPAPI_TestPullRequests_macOS) bc_test_windows=(KeymanDesktop_TestPullRequests KeymanDesktop_TestPrRenderOnScreenKeyboards Keyman_Common_KPAPI_TestPullRequests_Windows) bc_test_web=(Keymanweb_TestPullRequests Keyman_Common_LMLayer_TestPullRequests Keyman_Common_KPAPI_TestPullRequests_WASM) @@ -61,7 +63,7 @@ vcs_test=HttpsGithubComKeymanappKeymanPRs bc_master_android=(KeymanAndroid_Build) bc_master_ios=(Keyman_iOS_Master) -bc_master_linux=(KeymanLinux_Master pipeline-keyman-packaging_Jenkins deb-release-packaging_GitHub) +bc_master_linux=(KeymanLinux_Master deb-release-packaging_GitHub) bc_master_mac=(KeymanMac_Master) bc_master_windows=(Keyman_Build) bc_master_web=(Keymanweb_Build) @@ -73,7 +75,7 @@ vcs_master=HttpsGithubComKeymanappKeyman bc_beta_android=(KeymanAndroid_Build) bc_beta_ios=(Keyman_iOS_Master) -bc_beta_linux=(KeymanLinux_Master pipeline-keyman-packaging_Jenkins deb-release-packaging_GitHub) +bc_beta_linux=(KeymanLinux_Master deb-release-packaging_GitHub) bc_beta_mac=(KeymanMac_Master) bc_beta_windows=(Keyman_Build) bc_beta_web=(Keymanweb_Build) @@ -85,7 +87,7 @@ vcs_beta=HttpsGithubComKeymanappKeyman bc_stable_14_0_android=(KeymanAndroid_Build) bc_stable_14_0_ios=(Keyman_iOS_Master) -bc_stable_14_0_linux=(KeymanLinux_Master pipeline-keyman-packaging_Jenkins deb-release-packaging_GitHub) +bc_stable_14_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) bc_stable_14_0_mac=(KeymanMac_Master) bc_stable_14_0_windows=(Keyman_Build) bc_stable_14_0_web=(Keymanweb_Build) @@ -100,7 +102,7 @@ vcs_stable_14_0=HttpsGithubComKeymanappKeyman bc_stable_15_0_android=(KeymanAndroid_Build) bc_stable_15_0_ios=(Keyman_iOS_Master) -bc_stable_15_0_linux=(KeymanLinux_Master pipeline-keyman-packaging_Jenkins deb-release-packaging_GitHub) +bc_stable_15_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub) bc_stable_15_0_mac=(KeymanMac_Master) bc_stable_15_0_windows=(Keyman_Build) bc_stable_15_0_web=(Keymanweb_Build) @@ -111,7 +113,7 @@ vcs_stable_15_0=HttpsGithubComKeymanappKeyman bc_stable_16_0_android=(KeymanAndroid_Build) bc_stable_16_0_ios=(Keyman_iOS_Master) -bc_stable_16_0_linux=(KeymanLinux_Master pipeline-keyman-packaging_Jenkins) +bc_stable_16_0_linux=(KeymanLinux_Master) bc_stable_16_0_mac=(KeymanMac_Master) bc_stable_16_0_windows=(Keyman_Build) bc_stable_16_0_web=(Keymanweb_Build) diff --git a/resources/build/version/package.json b/resources/build/version/package.json index 8f639b50cc967bfecac75d5c57cc025412e8ea78..93911e79701a543ec711c9c4e7ca60f8ba6218ef 100644 --- a/resources/build/version/package.json +++ b/resources/build/version/package.json @@ -9,7 +9,7 @@ "devDependencies": { "@types/node": "^13.7.0", "@types/semver": "^7.1.0", - "semver": "^7.1.2", + "semver": "^7.5.2", "ts-node": "^10.9.1" }, "engines": { diff --git a/resources/builder.inc.sh b/resources/builder.inc.sh index f9440ae83eb36f3f293a56efaee5de96bccce8ce..6a47cbd5295aeeda7f5f0c0a7622fad5bf7b24f9 100755 --- a/resources/builder.inc.sh +++ b/resources/builder.inc.sh @@ -346,12 +346,12 @@ _builder_failure_trap() { fi builder_finish_action failure $action$target - - # Make 100% sure that the exit code chains fully. - # Without this, nested scripts have failed to chain errors from npm calls past the script - # that directly executed the failed npm command. - exit $trappedExitCode fi + + # Make 100% sure that the exit code chains fully. + # Without this, nested scripts have failed to chain errors from npm calls past the script + # that directly executed the failed npm command. + exit $trappedExitCode } # @@ -711,6 +711,30 @@ _builder_expand_action_targets() { fi } +_builder_child_base= +# +# Describes the path from the build script's working directory to the common subfolder +# containing child scripts / projects without defined custom paths. +# +# This function must be called to set the child base path before builder_describe is +# called in order to work correctly. Furthermore, note that this setting will be +# ignored by targets with custom paths. +# +# ### Usage +# +# ```bash +# builder_set_child_base path +# ``` +# +# ### Parameters +# +# * `path` The relative path from the directory containing the calling script to +# the base folder to use for child-project detection and resolution +# +builder_set_child_base() { + _builder_child_base="$1/" +} + # # Describes a build script, defines available parameters and their meanings. Use # together with `builder_parse` to process input parameters. @@ -827,8 +851,8 @@ builder_describe() { else # If the target name matches a folder name, implicitly # make it available as a child project - if [[ -d "$THIS_SCRIPT_PATH/${value:1}" ]]; then - target_path="${value:1}" + if [[ -d "$THIS_SCRIPT_PATH/$_builder_child_base${value:1}" ]]; then + target_path="$_builder_child_base${value:1}" fi fi _builder_targets+=($value) @@ -945,8 +969,8 @@ builder_describe() { # # ```bash # builder_describe_outputs \ -# "configure" "/node_modules" \ -# "build" "build/index.js" +# configure /node_modules \ +# build build/index.js # ``` # function builder_describe_outputs() { @@ -1337,6 +1361,7 @@ _builder_parse_expanded_parameters() { _builder_report_dependencies ;; *) + # script does not recognize anything of action or target form at this point. _builder_parameter_error "$0" parameter "$key" esac fi diff --git a/resources/locate_emscripten.inc.sh b/resources/locate_emscripten.inc.sh new file mode 100644 index 0000000000000000000000000000000000000000..a58b384089e30cc3faece91ee9476b11e0f6d485 --- /dev/null +++ b/resources/locate_emscripten.inc.sh @@ -0,0 +1,30 @@ +# shellcheck shell=bash +# no hashbang for .inc.sh + +# +# We don't want to rely on emcc.py being on the path, because Emscripten puts far +# too many things onto the path (in particular for us, node). +# +# The following comment suggests that we don't need emcc.py on the path. +# https://github.com/emscripten-core/emscripten/issues/4848#issuecomment-1097357775 +# +# So we try and locate emcc.py in common locations ourselves. The search pattern +# is: +# +# 1. Look for $EMSCRIPTEN_BASE (our primary emscripten variable), which should +# point to the folder that emcc.py is located in +# 2. Look for $EMCC which should point to the emcc.py executable +# 3. Look for emcc.py on the path +# +locate_emscripten() { + if [[ -z ${EMSCRIPTEN_BASE+x} ]]; then + if [[ -z ${EMCC+x} ]]; then + local EMCC=`which emcc.py` + [[ -z $EMCC ]] && builder_die "locate_emscripten: Could not locate emscripten (emcc.py) on the path or with \$EMCC or \$EMSCRIPTEN_BASE" + fi + [[ -x $EMCC ]] || builder_die "locate_emscripten: Variable EMCC ($EMCC) does not point to a valid executable emcc.py" + EMSCRIPTEN_BASE="$(dirname "$EMCC")" + fi + + [[ -x ${EMSCRIPTEN_BASE}/emcc.py ]] || builder_die "locate_emscripten: Variable EMSCRIPTEN_BASE ($EMSCRIPTEN_BASE) does not point to emcc.py's folder" +} \ No newline at end of file diff --git a/resources/standards-data/iso639-3/.gitattributes b/resources/standards-data/iso639-3/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..bb32690a4c56f07d4a76da3b2daba8ccf80c3975 --- /dev/null +++ b/resources/standards-data/iso639-3/.gitattributes @@ -0,0 +1 @@ +iso639-3.tab -whitespace diff --git a/resources/standards-data/iso639-3/iso639-3.tab b/resources/standards-data/iso639-3/iso639-3.tab index d55ba726fd8cf68a3eb67412254b90353ce4b8f3..c865a4251676ff787c1a44a4fe73b221a0eaec25 100644 --- a/resources/standards-data/iso639-3/iso639-3.tab +++ b/resources/standards-data/iso639-3/iso639-3.tab @@ -70,8 +70,8 @@ acx I L Omani Arabic acy I L Cypriot Arabic acz I L Acheron ada ada ada I L Adangme -adb I L Adabe -add I L Dzodinka +adb I L Atauran +add I L Lidzonka ade I L Adele adf I L Dhofari Arabic adg I L Andegerebinha @@ -178,17 +178,15 @@ aio I L Aiton aip I L Burumakok aiq I L Aimaq air I L Airoran -ais I L Nataoran Amis ait I E Arikem aiw I L Aari aix I L Aighon aiy I L Ali -aja I L Aja (Sudan) +aja I L Aja (South Sudan) ajg I L Aja (Benin) aji I L Ajië ajn I L Andajin -ajp I L South Levantine Arabic -ajt I L Judeo-Tunisian Arabic +ajs I L Algerian Jewish Sign Language aju I L Judeo-Moroccan Arabic ajw I E Ajawa ajz I L Amri Karbi @@ -298,7 +296,6 @@ aod I L Andarum aoe I L Angal Enen aof I L Bragat aog I L Angoram -aoh I E Arma aoi I L Anindilyakwa aoj I L Mufian aok I L Arhö @@ -312,7 +309,7 @@ aou I L A'ou aox I L Atorada aoz I L Uab Meto apb I L Sa'a -apc I L North Levantine Arabic +apc I L Levantine Arabic apd I L Sudanese Arabic ape I L Bukiyip apf I L Pahanan Agta @@ -339,6 +336,7 @@ apz I L Safeyoka aqc I L Archi aqd I L Ampari Dogon aqg I L Arigidi +aqk I L Aninka aqm I L Atohwaim aqn I L Northern Alta aqp I E Atakapa @@ -371,9 +369,8 @@ arz I L Egyptian Arabic asa I L Asu (Tanzania) asb I L Assiniboine asc I L Casuarina Coast Asmat -asd I L Asas ase I L American Sign Language -asf I L Australian Sign Language +asf I L Auslan asg I L Cishingini ash I E Abishira asi I L Buruwai @@ -499,7 +496,6 @@ ayr I L Central Aymara ays I L Sorsogon Ayta ayt I L Magbukun Ayta ayu I L Ayu -ayy I E Tayabas Ayta ayz I L Mai Brat aza I L Azha azb I L South Azerbaijani @@ -558,7 +554,6 @@ bbv I L Karnai bbw I L Baba bbx I L Bubia bby I L Befang -bbz I L Babalia Creole Arabic bca I L Central Bai bcb I L Bainouk-Samik bcc I L Southern Balochi @@ -569,7 +564,7 @@ bcg I L Baga Pokur bch I L Bariai bci I L Baoulé bcj I L Bardi -bck I L Bunaba +bck I L Bunuba bcl I L Central Bikol bcm I L Bannoni bcn I L Bali (Nigeria) @@ -591,9 +586,9 @@ bdd I L Bunama bde I L Bade bdf I L Biage bdg I L Bonggi -bdh I L Baka (Sudan) +bdh I L Baka (South Sudan) bdi I L Burun -bdj I L Bai +bdj I L Bai (South Sudan) bdk I L Budukh bdl I L Indonesian Bajau bdm I L Buduma @@ -711,12 +706,10 @@ bhy I L Bhele bhz I L Bada (Indonesia) bia I L Badimaya bib I L Bissa -bic I L Bikaru bid I L Bidiyo bie I L Bepour bif I L Biafada big I L Biangai -bij I L Vaghat-Ya-Bijim-Legeri bik bik bik M L Bikol bil I L Bile bim I L Bimoba @@ -787,13 +780,12 @@ blc I L Bella Coola bld I L Bolango ble I L Balanta-Kentohe blf I L Buol -blg I L Balau blh I L Kuwaa bli I L Bolia blj I L Bolongan blk I L Pa'o Karen bll I E Biloxi -blm I L Beli (Sudan) +blm I L Beli (South Sudan) bln I L Southern Catanduanes Bikol blo I L Anii blp I L Blablanga @@ -881,8 +873,9 @@ box I L Buamu boy I L Bodo (Central African Republic) boz I L Tiéyaxo Bozo bpa I L Daakaka -bpb I E Barbacoas +bpc I L Mbuk bpd I L Banda-Banda +bpe I L Bauni bpg I L Bonggo bph I L Botlikh bpi I L Bagupi @@ -929,11 +922,11 @@ bqx I L Baangi bqy I L Bengkala Sign Language bqz I L Bakaka bra bra bra I L Braj -brb I L Lave +brb I L Brao brc I E Berbice Creole Dutch brd I L Baraamu bre bre bre br I L Breton -brf I L Bera +brf I L Bira brg I L Baure brh I L Brahui bri I L Mokpwe @@ -1111,7 +1104,7 @@ byi I L Buyu byj I L Bina (Nigeria) byk I L Biao byl I L Bayono -bym I L Bidyara +bym I L Bidjara byn byn byn I L Bilin byo I L Biyo byp I L Bumaji @@ -1191,7 +1184,6 @@ cbu I L Candoshi-Shapra cbv I L Cacua cbw I L Kinabalian cby I L Carabayo -cca I E Cauca ccc I L Chamicuro ccd I L Cafundo Creole cce I L Chopi @@ -1206,7 +1198,6 @@ ccr I E Cacaopera cda I L Choni cde I L Chenchu cdf I L Chiru -cdg I L Chamari cdh I L Chambeali cdi I L Chodri cdj I L Churahi @@ -1224,6 +1215,7 @@ cek I L Eastern Khumi Chin cen I L Cen ces cze ces cs I L Czech cet I L Centúúm +cey I L Ekai Chin cfa I L Dijim-Bwilim cfd I L Cara cfg I L Como Karim @@ -1239,7 +1231,7 @@ chd I L Highland Oaxaca Chontal che che che ce I L Chechen chf I L Tabasco Chontal chg chg chg I E Chagatai -chh I L Chinook +chh I E Chinook chj I L Ojitlán Chinantec chk chk chk I L Chuukese chl I L Cahuilla @@ -1284,6 +1276,7 @@ cjy I L Jinyu Chinese ckb I L Central Kurdish ckh I L Chak ckl I L Cibak +ckm I L Chakavian ckn I L Kaang Chin cko I L Anufo ckq I L Kajakse @@ -1330,6 +1323,9 @@ cni I L Asháninka cnk I L Khumi Chin cnl I L Lalana Chinantec cno I L Con +cnp I L Northern Ping Chinese +cnq I L Chung +cnr cnr cnr I L Montenegrin cns I L Central Asmat cnt I L Tepetotutla Chinantec cnu I L Chenoua @@ -1410,16 +1406,18 @@ csl I L Chinese Sign Language csm I L Central Sierra Miwok csn I L Colombian Sign Language cso I L Sochiapam Chinantec +csp I L Southern Ping Chinese csq I L Croatia Sign Language csr I L Costa Rican Sign Language css I E Southern Ohlone cst I L Northern Ohlone csv I L Sumtu Chin csw I L Swampy Cree +csx I L Cambodian Sign Language csy I L Siyin Chin csz I L Coos cta I L Tataltepec Chatino -ctc I L Chetco +ctc I E Chetco ctd I L Tedim Chin cte I L Tepinapa Chinantec ctg I L Chittagonian @@ -1432,11 +1430,11 @@ ctp I L Western Highland Chatino cts I L Northern Catanduanes Bikol ctt I L Wayanad Chetti ctu I L Chol +cty I L Moundadan Chetty ctz I L Zacatepec Chatino cua I L Cua cub I L Cubeo cuc I L Usila Chinantec -cug I L Chungmboko cuh I L Chuka cui I L Cuiba cuj I L Mashco Piro @@ -1451,6 +1449,7 @@ cuu I L Tai Ya cuv I L Cuvok cuw I L Chukwa cux I L Tepeuxila Cuicatec +cuy I L Cuitlatec cvg I L Chug cvn I L Valle Nacional Chinantec cwa I L Kabwa @@ -1459,6 +1458,7 @@ cwd I L Woods Cree cwe I L Kwere cwg I L Chewong cwt I L Kuwaataay +cxh I L Cha'ari cya I L Nopala Chatino cyb I E Cayubaba cym wel cym cy I L Welsh @@ -1555,7 +1555,6 @@ dgo I L Dogri (individual language) dgr dgr dgr I L Dogrib dgs I L Dogoso dgt I E Ndra'ngith -dgu I L Degaru dgw I E Daungwurrung dgx I L Doghoro dgz I L Daga @@ -1590,7 +1589,6 @@ dip I L Northeastern Dinka diq I L Dimli (individual language) dir I L Dirim dis I L Dimasa -dit I E Dirari diu I L Diriku div div div dv I L Dhivehi diw I L Northwestern Dinka @@ -1607,12 +1605,13 @@ dji I L Djinang djj I L Djeebbana djk I L Eastern Maroon Creole djm I L Jamsay Dogon -djn I L Djauan +djn I L Jawoyn djo I L Jangkang djr I L Djambarrpuyngu dju I L Kapriman djw I E Djawi dka I L Dakpakha +dkg I L Kadung dkk I L Dakka dkr I L Kuijau dks I L Southeastern Dinka @@ -1626,6 +1625,7 @@ dmb I L Mombo Dogon dmc I L Gavak dmd I E Madhi Madhi dme I L Dugwor +dmf I E Medefaidrin dmg I L Upper Kinabatangan dmk I L Domaaki dml I L Dameli @@ -1646,6 +1646,7 @@ dni I L Lower Grand Valley Dani dnj I L Dan dnk I L Dengka dnn I L Dzùùngoo +dno I L Ndrulo dnr I L Danaru dnt I L Mid Grand Valley Dani dnu I L Danau @@ -1684,7 +1685,6 @@ drl I L Paakantyi drn I L West Damar dro I L Daro-Matu Melanau drq I E Dura -drr I E Dororo drs I L Gedeo drt I L Drents dru I L Rukai @@ -1693,10 +1693,12 @@ dsb dsb dsb I L Lower Sorbian dse I L Dutch Sign Language dsh I L Daasanach dsi I L Disa +dsk I L Dokshi dsl I L Danish Sign Language -dsn I L Dusner +dsn I E Dusner dso I L Desiya dsq I L Tadaksahak +dsz I L Mardin Sign Language dta I L Daur dtb I L Labuk-Kinabatangan Kadazan dtd I L Ditidaht @@ -1715,7 +1717,6 @@ dty I L Dotyali dua dua dua I L Duala dub I L Dubli duc I L Duna -dud I L Hun-Saare due I L Umiray Dumaget Agta duf I L Dumbea dug I L Duruma @@ -1738,11 +1739,13 @@ duy I E Dicamay Agta duz I E Duli-Gey dva I L Duau dwa I L Diri +dwk I L Dawik Kui dwr I L Dawro dws I C Dutton World Speedwords dwu I L Dhuwal dww I L Dawawa dwy I L Dhuwaya +dwz I L Dewas Rai dya I L Dyan dyb I E Dyaberdyaber dyd I E Dyugun @@ -1751,15 +1754,18 @@ dyi I L Djimini Senoufo dym I L Yanda Dom Dogon dyn I L Dyangadi dyo I L Jola-Fonyi +dyr I L Dyarim dyu dyu dyu I L Dyula -dyy I L Dyaabugay +dyy I L Djabugay dza I L Tunzu +dzd I L Daza dze I E Djiwarli dzg I L Dazaga dzl I L Dzalakha dzn I L Dzando dzo dzo dzo dz I L Dzongkha eaa I E Karenggapa +ebc I L Beginci ebg I L Ebughu ebk I L Eastern Bontok ebo I L Teke-Ebo @@ -1774,15 +1780,16 @@ efe I L Efe efi efi efi I L Efik ega I L Ega egl I L Emilian +egm I L Benamanga ego I L Eggon egy egy egy I A Egyptian (Ancient) +ehs I L Miyakubo Sign Language ehu I L Ehueun eip I L Eipomek eit I L Eitiep eiv I L Askopan eja I L Ejamat eka eka eka I L Ekajuk -ekc I E Eastern Karnic eke I L Ekit ekg I L Ekari eki I L Eki @@ -1811,11 +1818,13 @@ emk I L Eastern Maninkakan emm I E Mamulique emn I L Eman emp I L Northern Emberá +emq I L Eastern Minyag ems I L Pacific Gulf Yupik emu I L Eastern Muria emw I L Emplawas emx I L Erromintxela -emy I E Epigraphic Mayan +emy I A Epigraphic Mayan +emz I L Mbessa ena I L Apali enb I L Markweeta enc I L En @@ -1830,7 +1839,7 @@ eno I L Enggano enq I L Enga enr I L Emumu enu I L Enu -env I L Enwan (Edu State) +env I L Enwan (Edo State) enw I L Enwan (Akwa Ibom State) enx I L Enxet eot I L Beti (Côte d'Ivoire) @@ -1871,6 +1880,7 @@ ett I A Etruscan etu I L Ejagham etx I L Eten etz I L Semimi +eud I L Eudeve eus baq eus eu I L Basque eve I L Even evh I L Uvbie @@ -1895,7 +1905,7 @@ fal I L South Fali fam I L Fam fan fan fan I L Fang (Equatorial Guinea) fao fao fao fo I L Faroese -fap I L Palor +fap I L Paloor far I L Fataleka fas per fas fa M L Persian fat fat fat I L Fanti @@ -1911,6 +1921,7 @@ ffm I L Maasina Fulfulde fgr I L Fongoro fia I L Nobiin fie I L Fyer +fif I L Faifi fij fij fij fj I L Fijian fil fil fil I L Filipino fin fin fin fi I L Finnish @@ -1943,7 +1954,7 @@ fqs I L Fas fra fre fra fr I L French frc I L Cajun French frd I L Fordata -frk I E Frankish +frk I H Frankish frm frm frm I H Middle French (ca. 1400-1600) fro fro fro I H Old French (842-ca. 1400) frp I L Arpitan @@ -2002,7 +2013,7 @@ gay gay gay I L Gayo gaz I L West Central Oromo gba gba gba M L Gbaya (Central African Republic) gbb I L Kaytetye -gbd I L Karadjeri +gbd I L Karajarri gbe I L Niksek gbf I L Gaikundi gbg I L Gbanziri @@ -2057,6 +2068,7 @@ gea I L Geruma geb I L Kire gec I L Gboloo Grebo ged I L Gade +gef I L Gerai geg I L Gengle geh I L Hutterite German gei I L Gebe @@ -2075,7 +2087,7 @@ gft I E Gafat gga I L Gao ggb I L Gbii ggd I E Gugadj -gge I L Guragone +gge I L Gurr-goni ggg I L Gurgula ggk I E Kungarakany ggl I L Ganglau @@ -2083,7 +2095,7 @@ ggt I L Gitua ggu I L Gagu ggw I L Gogodala gha I L Ghadamès -ghc I E Hiberno-Scottish Gaelic +ghc I H Hiberno-Scottish Gaelic ghe I L Southern Ghale ghh I L Northern Ghale ghk I L Geko Karen @@ -2093,13 +2105,14 @@ gho I E Ghomara ghr I L Ghera ghs I L Guhu-Samane ght I L Kuke -gia I L Kitja +gia I L Kija gib I L Gibanawa gic I L Gail gid I L Gidar gie I L Gaɓogbo gig I L Goaria gih I L Githabul +gii I L Girirra gil gil gil I L Gilbertese gim I L Gimi (Eastern Highlands) gin I L Hinukh @@ -2113,25 +2126,25 @@ giw I L White Gelao gix I L Gilima giy I L Giyug giz I L South Giziga -gji I L Geji gjk I L Kachi Koli gjm I E Gunditjmara gjn I L Gonja gjr I L Gurindji Kriol gju I L Gujari gka I L Guya +gkd I L Magɨ (Madang Province) gke I L Ndai gkn I L Gokana gko I E Kok-Nar gkp I L Guinea Kpelle gku I E ǂUngkue gla gla gla gd I L Scottish Gaelic +glb I L Belning glc I L Bon Gula gld I L Nanai gle gle gle ga I L Irish glg glg glg gl I L Galician glh I L Northwest Pashai -gli I E Guliguli glj I L Gula Iro glk I L Gilaki gll I E Garlali @@ -2149,6 +2162,7 @@ gmh gmh gmh I H Middle High German (ca. 1050-1500) gml I H Middle Low German gmm I L Gbaya-Mbodomo gmn I L Gimnime +gmr I L Mirning gmu I L Gumalu gmv I L Gamo gmx I L Magoma @@ -2162,7 +2176,8 @@ gne I L Ganang gng I L Ngangam gnh I L Lere gni I L Gooniyandi -gnk I L //Gana +gnj I L Ngen +gnk I L ǁGana gnl I E Gangulu gnm I L Ginuman gnn I L Gumatj @@ -2194,6 +2209,7 @@ gor gor gor I L Gorontalo gos I L Gronings got got got I A Gothic gou I L Gavar +gov I L Goo gow I L Gorowa gox I L Gobu goy I L Goundo @@ -2242,7 +2258,7 @@ gua I L Shiki gub I L Guajajára guc I L Wayuu gud I L Yocoboué Dida -gue I L Gurinji +gue I L Gurindji guf I L Gupapuyngu gug I L Paraguayan Guaraní guh I L Guahibo @@ -2277,13 +2293,13 @@ gvs I L Gumawana gvy I E Guyani gwa I L Mbato gwb I L Gwa -gwc I L Kalami +gwc I L Gawri gwd I L Gawwada gwe I L Gweno gwf I L Gowro gwg I L Moo gwi gwi gwi I L Gwichʼin -gwj I L /Gwi +gwj I L ǀGwi gwm I E Awngthim gwn I L Gwandara gwr I L Gwere @@ -2302,8 +2318,10 @@ gyi I L Gyele gyl I L Gayil gym I L Ngäbere gyn I L Guyanese Creole English +gyo I L Gyalsumdo gyr I L Guarayu gyy I E Gunya +gyz I L Geji gza I L Ganza gzi I L Gazi gzn I L Gane @@ -2352,7 +2370,7 @@ heh I L Hehe hei I L Heiltsuk hem I L Hemba her her her hz I L Herero -hgm I L Hai//om +hgm I L Haiǁom hgw I L Haigwai hhi I L Hoia Hoia hhr I L Kerak @@ -2376,7 +2394,9 @@ hix I L Hixkaryána hji I L Haji hka I L Kahe hke I L Hunde +hkh I L Khah hkk I L Hunjara-Kaina Ke +hkn I L Mel-Khaonh hks I L Hong Kong Sign Language hla I L Halia hlb I L Halbi @@ -2394,7 +2414,7 @@ hmg I L Southwestern Guiyang Hmong hmh I L Southwestern Huishui Hmong hmi I L Northern Huishui Hmong hmj I L Ge -hmk I E Maek +hmk I A Maek hml I L Luopohe Hmong hmm I L Central Mashan Hmong hmn hmn hmn M L Hmong @@ -2412,7 +2432,8 @@ hmz I L Hmong Shua hna I L Mina (Cameroon) hnd I L Southern Hindko hne I L Chhattisgarhi -hnh I L //Ani +hng I L Hungu +hnh I L ǁAni hni I L Hani hnj I L Hmong Njua hnn I L Hanunoo @@ -2438,7 +2459,7 @@ hov I L Hovongan how I L Honi hoy I L Holiya hoz I L Hozo -hpo I L Hpon +hpo I E Hpon hps I L Hawai'i Sign Language (HSL) hra I L Hrangkhol hrc I L Niwer Mil @@ -2458,13 +2479,13 @@ hsh I L Hungarian Sign Language hsl I L Hausa Sign Language hsn I L Xiang Chinese hss I L Harsusi -hti I L Hoti +hti I E Hoti hto I L Minica Huitoto hts I L Hadza htu I L Hitu htx I A Middle Hittite hub I L Huambisa -huc I L =/Hua +huc I L ǂHua hud I L Huaulu hue I L San Francisco Del Mar Huave huf I L Humene @@ -2472,7 +2493,7 @@ hug I L Huachipaeri huh I L Huilliche hui I L Huli huj I L Northern Guiyang Hmong -huk I L Hulung +huk I E Hulung hul I L Hula hum I L Hungana hun hun hun hu I L Hungarian @@ -2498,6 +2519,7 @@ hwc I L Hawai'i Creole English hwo I L Hwana hya I L Hya hye arm hye hy I L Armenian +hyw I L Western Armenian iai I L Iaai ian I L Iatmul iar I L Purari @@ -2558,6 +2580,7 @@ ijj I L Ede Ije ijn I L Kalabari ijs I L Southeast Ijo ike I L Eastern Canadian Inuktitut +ikh I L Ikhin-Arokho iki I L Iko ikk I L Ika ikl I L Ikulu @@ -2590,6 +2613,7 @@ imn I L Imonda imo I L Imbongu imr I L Imroing ims I A Marsian +imt I L Imotong imy I A Milyan ina ina ina ia I C Interlingua (International Auxiliary Language Association) inb I L Inga @@ -2615,7 +2639,7 @@ iqu I L Iquito iqw I L Ikwo ire I L Iresim irh I L Irarutu -iri I L Irigwe +iri I L Rigwe irk I L Iraqw irn I L Irántxe irr I L Ir @@ -2667,6 +2691,7 @@ iya I L Iyayu iyo I L Mesaka iyx I L Yaka (Congo) izh I L Ingrian +izm I L Kizamani izr I L Izere izz I L Izii jaa I L Jamamadí @@ -2694,6 +2719,7 @@ jbe I L Judeo-Berber jbi I E Badjiri jbj I L Arandai jbk I L Barikewa +jbm I L Bijim jbn I L Nafusi jbo jbo jbo I C Lojban jbr I L Jofotek-Bromnya @@ -2724,9 +2750,9 @@ jhs I L Jhankot Sign Language jia I L Jina jib I L Jibu jic I L Tol -jid I L Bu +jid I L Bu (Kaduna State) jie I L Jilbe -jig I L Djingili +jig I L Jingulu jih I L sTodsde jii I L Jiiddu jil I L Jilim @@ -2744,6 +2770,7 @@ jkm I L Mobwa Karen jko I L Kubo jkp I L Paku Karen jkr I L Koro (India) +jks I L Amami Koniya Sign Language jku I L Labir jle I L Ngile jls I L Jamaican Sign Language @@ -2778,12 +2805,12 @@ jqr I L Jaqaru jra I L Jarai jrb jrb jrb M L Judeo-Arabic jrr I L Jiru -jrt I L Jorto +jrt I L Jakattoe jru I L Japrería jsl I L Japanese Sign Language jua I L Júma jub I L Wannu -juc I E Jurchen +juc I H Jurchen jud I L Worodougou juh I L Hõne jui I E Ngadjuri @@ -2799,7 +2826,7 @@ jut I H Jutish juu I L Ju juw I L Wãpha juy I L Juray -jvd I E Javindo +jvd I L Javindo jvn I L Caribbean Javanese jwi I L Jwira-Pepesa jya I L Jiarong @@ -2964,7 +2991,6 @@ kgi I L Selangor Sign Language kgj I L Gamale Kham kgk I L Kaiwá kgl I E Kunggari -kgm I E Karipúna kgn I L Karingani kgo I L Krongo kgp I L Kaingang @@ -2988,7 +3014,7 @@ khh I L Kehu khj I L Kuturmi khk I L Halh Mongolian khl I L Lusi -khm khm khm km I L Central Khmer +khm khm khm km I L Khmer khn I L Khandesi kho kho kho I A Khotanese khp I L Kapori @@ -3033,7 +3059,6 @@ kjb I L Q'anjob'al kjc I L Coastal Konjo kjd I L Southern Kiwai kje I L Kisar -kjf I L Khalaj kjg I L Khmu kjh I L Khakas kji I L Zabana @@ -3069,7 +3094,7 @@ kkm I L Kiong kkn I L Kon Keu kko I L Karko kkp I L Gugubera -kkq I L Kaiku +kkq I L Kaeku kkr I L Kir-Balar kks I L Giiwo kkt I L Koi @@ -3077,7 +3102,7 @@ kku I L Tumi kkv I L Kangean kkw I L Teke-Kukuya kkx I L Kohin -kky I L Guguyimidjir +kky I L Guugu Yimidhirr kkz I L Kaska kla I E Klamath-Modoc klb I L Kiliwa @@ -3088,7 +3113,7 @@ klf I L Kendeje klg I L Tagakaulo klh I L Weliki kli I L Kalumpang -klj I L Turkic Khalaj +klj I L Khalaj klk I L Kono (Nigeria) kll I L Kagan Kalagan klm I L Migum @@ -3159,7 +3184,7 @@ knz I L Kalamsé koa I L Konomala koc I E Kpati kod I L Kodi -koe I L Kacipo-Balesi +koe I L Kacipo-Bale Suri kof I E Kubi kog I L Cogui koh I L Koyo @@ -3242,7 +3267,7 @@ krj I L Kinaray-A krk I E Kerek krl krl krl I L Karelian krn I L Sapo -krp I L Korop +krp I L Durop krr I L Krung krs I L Gbaya (Sudan) krt I L Tumari Kanuri @@ -3252,7 +3277,6 @@ krw I L Western Krahn krx I L Karon kry I L Kryts krz I L Sota Kanum -ksa I L Shuwa-Zamani ksb I L Shambala ksc I L Southern Kalinga ksd I L Kuanua @@ -3302,7 +3326,7 @@ ktv I L Eastern Katu ktw I E Kato ktx I L Kaxararí kty I L Kango (Bas-Uélé District) -ktz I L Ju/'hoan +ktz I L Juǀʼhoan kua kua kua kj I L Kuanyama kub I L Kutep kuc I L Kwinsu @@ -3388,7 +3412,6 @@ kxh I L Karo (Ethiopia) kxi I L Keningau Murut kxj I L Kulfa kxk I L Zayein Karen -kxl I L Nepali Kurux kxm I L Northern Khmer kxn I L Kanowit-Tanjong Melanau kxo I E Kanoé @@ -3397,7 +3420,6 @@ kxq I L Smärky Kanum kxr I L Koro (Papua New Guinea) kxs I L Kangjia kxt I L Koiwat -kxu I L Kui (India) kxv I L Kuvi kxw I L Konai kxx I L Likuba @@ -3449,7 +3471,7 @@ kzs I L Sugut Dusun kzu I L Kayupulau kzv I L Komyandaret kzw I E Karirí-Xocó -kzx I L Kamarian +kzx I E Kamarian kzy I L Kango (Tshopo District) kzz I L Kalabra laa I L Southern Subanen @@ -3458,11 +3480,10 @@ lac I L Lacandon lad lad lad I L Ladino lae I L Pattani laf I L Lafofa -lag I L Langi +lag I L Rangi lah lah lah M L Lahnda lai I L Lambya laj I L Lango (Uganda) -lak I L Laka (Nigeria) lal I L Lalia lam lam lam I L Lamba lan I L Laru @@ -3478,7 +3499,6 @@ law I L Lauje lax I L Tiwa lay I L Lama Bai laz I E Aribwatsa -lba I E Lui lbb I L Label lbc I L Lakkia lbe I L Lak @@ -3489,7 +3509,7 @@ lbj I L Ladakhi lbk I L Central Bontok lbl I L Libon Bikol lbm I L Lodhi -lbn I L Lamet +lbn I L Rmeet lbo I L Laven lbq I L Wampar lbr I L Lohorung @@ -3499,7 +3519,7 @@ lbu I L Labu lbv I L Lavatbura-Lamusong lbw I L Tolaki lbx I L Lawangan -lby I E Lamu-Lamu +lby I E Lamalama lbz I L Lardil lcc I L Legenyem lcd I L Lola @@ -3561,8 +3581,10 @@ lgk I L Lingarak lgl I L Wala lgm I L Lega-Mwenga lgn I L T'apo +lgo I L Lango (South Sudan) lgq I L Logba lgr I L Lengo +lgs I L Guinea-Bissau Sign Language lgt I L Pahi lgu I L Longgu lgz I L Ligenza @@ -3639,7 +3661,6 @@ llk I E Lelak lll I L Lilau llm I L Lasalimu lln I L Lele (Chad) -llo I L Khlor llp I L North Efate llq I L Lolak lls I L Lithuanian Sign Language @@ -3667,7 +3688,6 @@ lmv I L Lomaiviti lmw I L Lake Miwok lmx I L Laimbue lmy I L Lamboya -lmz I E Lumbee lna I L Langbashe lnb I L Mbalanhu lnd I L Lundayeh @@ -3678,7 +3698,6 @@ lnj I E Leningitij lnl I L South Central Banda lnm I L Langam lnn I L Lorediakarkar -lno I L Lango (Sudan) lns I L Lamnso' lnu I L Longuda lnw I E Lanima @@ -3689,7 +3708,7 @@ loc I L Inonhan loe I L Saluan lof I L Logol log I L Logo -loh I L Narim +loh I L Laarim loi I L Loma (Côte d'Ivoire) loj I L Lou lok I L Loko @@ -3713,6 +3732,7 @@ lpe I L Lepki lpn I L Long Phuri Naga lpo I L Lipo lpx I L Lopit +lqr I L Logir lra I L Rara Bakati' lrc I L Northern Luri lre I E Laurentian @@ -3728,18 +3748,22 @@ lrt I L Larantuka Malay lrv I L Larevat lrz I L Lemerig lsa I L Lasgerdi +lsb I L Burundian Sign Language +lsc I L Albarradas Sign Language lsd I L Lishana Deni lse I L Lusengo -lsg I L Lyons Sign Language lsh I L Lish lsi I L Lashi lsl I L Latvian Sign Language lsm I L Saamia +lsn I L Tibetan Sign Language lso I L Laos Sign Language lsp I L Panamanian Sign Language lsr I L Aruop lss I L Lasi lst I L Trinidad and Tobago Sign Language +lsv I L Sivia Sign Language +lsw I L Seychelles Sign Language lsy I L Mauritian Sign Language ltc I H Late Middle Chinese ltg I L Latgalian @@ -3757,7 +3781,7 @@ lud I L Ludian lue I L Luvale luf I L Laua lug lug lug lg I L Ganda -lui lui lui I L Luiseno +lui lui lui I E Luiseno luj I L Luna luk I L Lunanakha lul I L Olu'bo @@ -3775,7 +3799,9 @@ luw I L Luo (Cameroon) luy M L Luyia luz I L Southern Luri lva I L Maku'a +lvi I L Lavi lvk I L Lavukaleve +lvl I L Lwel lvs I L Standard Latvian lvu I L Levuka lwa I L Lwalu @@ -3785,9 +3811,11 @@ lwh I L White Lachi lwl I L Eastern Lawa lwm I L Laomian lwo I L Luwo +lws I L Malawian Sign Language lwt I L Lewotobi lwu I L Lawu lww I L Lewo +lxm I L Lakurumau lya I L Layakha lyg I L Lyngngam lyn I L Luyana @@ -3895,7 +3923,7 @@ mdy I L Male (Ethiopia) mdz I L Suruí Do Pará mea I L Menka meb I L Ikobi -mec I L Mara +mec I L Marra med I L Melpa mee I L Mengen mef I L Megam @@ -3907,7 +3935,7 @@ mel I L Central Melanau mem I E Mangala men men men I L Mende (Sierra Leone) meo I L Kedah Malay -mep I L Miriwung +mep I L Miriwoong meq I L Merey mer I L Meru mes I L Masmaje @@ -3934,7 +3962,7 @@ mfn I L Cross River Mbembe mfo I L Mbe mfp I L Makassar Malay mfq I L Moba -mfr I L Marithiel +mfr I L Marrithiyel mfs I L Mexican Sign Language mft I L Mokerang mfu I L Mbwela @@ -4142,7 +4170,7 @@ mnz I L Moni moa I L Mwan moc I L Mocoví mod I E Mobilian -moe I L Montagnais +moe I L Innu mog I L Mongondow moh moh moh I L Mohawk moi I L Mboi @@ -4163,8 +4191,8 @@ mox I L Molima moy I L Shekkacho moz I L Mukulu mpa I L Mpoto -mpb I L Mullukmulluk -mpc I L Mangarayi +mpb I L Malak Malak +mpc I L Mangarrayi mpd I L Machinere mpe I L Majang mpg I L Marba @@ -4321,7 +4349,6 @@ mvh I L Mulgi mvi I L Miyako mvk I L Mekmek mvl I E Mbara (Australia) -mvm I L Muya mvn I L Minaveha mvo I L Marovo mvp I L Duri @@ -4356,8 +4383,6 @@ mwt I L Moken mwu I E Mittu mwv I L Mentawai mww I L Hmong Daw -mwx I L Mediak -mwy I L Mosiro mwz I L Moingi mxa I L Northwest Oaxaca Mixtec mxb I L Tezoatlán Mixtec @@ -4367,7 +4392,7 @@ mxe I L Mele-Fila mxf I L Malgbe mxg I L Mbangala mxh I L Mvuba -mxi I E Mozarabic +mxi I H Mozarabic mxj I L Miju-Mishmi mxk I L Monumbo mxl I L Maxi Gbe @@ -4388,12 +4413,10 @@ mxz I L Central Masela mya bur mya my I L Burmese myb I L Mbay myc I L Mayeka -myd I L Maramba mye I L Myene myf I L Bambassi myg I L Manta myh I L Makah -myi I L Mina (India) myj I L Mangayat myk I L Mamara Senoufo myl I L Moma @@ -4436,7 +4459,7 @@ mzz I L Maiadomu naa I L Namla nab I L Southern Nambikuára nac I L Narak -nae I L Naka'ela +nae I E Naka'ela naf I L Nabak nag I L Naga Pidgin naj I L Nalu @@ -4454,7 +4477,7 @@ nau nau nau na I L Nauru nav nav nav nv I L Navajo naw I L Nawuri nax I L Nakwi -nay I E Narrinyeri +nay I E Ngarrindjeri naz I L Coatepec Nahuatl nba I L Nyemba nbb I L Ndoe @@ -4464,7 +4487,7 @@ nbe I L Konyak Naga nbg I L Nagarchal nbh I L Ngamo nbi I L Mao Naga -nbj I L Ngarinman +nbj I L Ngarinyman nbk I L Nake nbl nbl nbl nr I L South Ndebele nbm I L Ngbaka Ma'bo @@ -4472,7 +4495,7 @@ nbn I L Kuri nbo I L Nkukoli nbp I L Nnam nbq I L Nggem -nbr I L Numana-Nunku-Gbantu-Numbu +nbr I L Numana nbs I L Namibian Sign Language nbt I L Na nbu I L Rongmei Naga @@ -4489,12 +4512,11 @@ ncg I L Nisga'a nch I L Central Huasteca Nahuatl nci I H Classical Nahuatl ncj I L Northern Puebla Nahuatl -nck I L Nakara +nck I L Na-kara ncl I L Michoacán Nahuatl ncm I L Nambo ncn I L Nauna nco I L Sibe -ncp I L Ndaktup ncq I L Northern Katang ncr I L Ncane ncs I L Nicaraguan Sign Language @@ -4507,7 +4529,7 @@ ndb I L Kenswei Nsei ndc I L Ndau ndd I L Nde-Nsele-Nta nde nde nde nd I L North Ndebele -ndf I E Nadruvian +ndf I H Nadruvian ndg I L Ndengereko ndh I L Ndali ndi I L Samba Leko @@ -4564,14 +4586,13 @@ ngc I L Ngombe (Democratic Republic of Congo) ngd I L Ngando (Central African Republic) nge I L Ngemba ngg I L Ngbaka Manza -ngh I L N/u +ngh I L Nǁng ngi I L Ngizim ngj I L Ngie ngk I L Dalabon ngl I L Lomwe ngm I L Ngatik Men's Creole ngn I L Ngwo -ngo I L Ngoni ngp I L Ngulu ngq I L Ngurimi ngr I L Engdewu @@ -4611,7 +4632,7 @@ nib I L Nakame nid I E Ngandi nie I L Niellim nif I L Nek -nig I E Ngalakan +nig I E Ngalakgan nih I L Nyiha (Tanzania) nii I L Nii nij I L Ngaju @@ -4680,6 +4701,7 @@ nli I L Grangali nlj I L Nyali nlk I L Ninia Yali nll I L Nihali +nlm I L Mankiyali nlo I L Ngul nlq I L Lao Naga nlu I L Nchumbulu @@ -4701,7 +4723,7 @@ nmj I L Ngombe (Central African Republic) nmk I L Namakura nml I L Ndemli nmm I L Manangba -nmn I L !Xóõ +nmn I L ǃXóõ nmo I L Moyon Naga nmp I E Nimanbur nmq I L Nambya @@ -4732,7 +4754,6 @@ nno nno nno nn I L Norwegian Nynorsk nnp I L Wancho Naga nnq I L Ngindo nnr I E Narungga -nns I L Ningye nnt I E Nanticoke nnu I L Dwang nnv I E Nugunu (Australia) @@ -4751,7 +4772,6 @@ noi I L Noiri noj I L Nonuya nok I E Nooksack nol I E Nomlaki -nom I E Nocamán non non non I H Old Norse nop I L Numanggang noq I L Ngongo @@ -4782,6 +4802,7 @@ nqm I L Ndom nqn I L Nen nqo nqo nqo I L N'Ko nqq I L Kyan-Karyaw Naga +nqt I L Nteng nqy I L Akyaung Ari Naga nra I L Ngom nrb I L Nara @@ -4801,6 +4822,7 @@ nru I L Narua nrx I E Ngurmbur nrz I L Lala nsa I L Sangtam Naga +nsb I E Lower Nossob nsc I L Nshi nsd I L Southern Nisu nse I L Nsenga @@ -4849,7 +4871,7 @@ nuh I L Ndunda nui I L Ngumbi nuj I L Nyole nuk I L Nuu-chah-nulth -nul I L Nusa Laut +nul I E Nusa Laut num I L Niuafo'ou nun I L Anong nuo I L Nguôn @@ -4876,6 +4898,7 @@ nwi I L Southwest Tanna nwm I L Nyamusa-Molo nwo I E Nauo nwr I L Nawaru +nww I L Ndwewe nwx I H Middle Newar nwy I E Nottoway-Meherrin nxa I L Nauete @@ -4890,7 +4913,6 @@ nxn I E Ngawun nxo I L Ndambomo nxq I L Naxi nxr I L Ninggerum -nxu I E Narau nxx I L Nafri nya nya nya ny I L Nyanja nyb I L Nyangbo @@ -4899,7 +4921,7 @@ nyd I L Nyore nye I L Nyengo nyf I L Giryama nyg I L Nyindu -nyh I L Nyigina +nyh I L Nyikina nyi I L Ama (Sudan) nyj I L Nyanga nyk I L Nyaneka @@ -4910,7 +4932,7 @@ nyo nyo nyo I L Nyoro nyp I E Nyang'i nyq I L Nayini nyr I L Nyiha (Malawi) -nys I L Nyunga +nys I L Nyungar nyt I E Nyawaygi nyu I L Nyungwe nyv I E Nyulnyul @@ -4919,9 +4941,11 @@ nyx I E Nganyaywana nyy I L Nyakyusa-Ngonde nza I L Tigon Mbembe nzb I L Njebi +nzd I L Nzadi nzi nzi nzi I L Nzima nzk I L Nzakara nzm I L Zeme Naga +nzr I L Dir-Nyamzak-Mbarimi nzs I L New Zealand Sign Language nzu I L Teke-Nzikou nzy I L Nzakambay @@ -4941,6 +4965,7 @@ obu I L Obulom oca I L Ocaina och I A Old Chinese oci oci oci oc I L Occitan (post 1500) +ocm I H Old Cham oco I H Old Cornish ocu I L Atzingo Matlatzinca oda I L Odut @@ -4959,6 +4984,7 @@ ogu I L Ogbronuagum oht I A Old Hittite ohu I H Old Hungarian oia I L Oirata +oie I L Okolie oin I L Inebu One ojb I L Northwestern Ojibwa ojc I L Central Ojibwa @@ -4970,6 +4996,7 @@ ojv I L Ontong Java ojw I L Western Ojibwa oka I L Okanagan okb I L Okobo +okc I L Kobo okd I L Okodia oke I L Okpe (Southwestern Edo) okg I E Koko Babangk @@ -4986,6 +5013,7 @@ oks I L Oko-Eni-Osayen oku I L Oku okv I L Orokaiva okx I L Okpe (Northwestern Edo) +okz I H Old Khmer ola I L Walungge old I L Mochi ole I L Olekha @@ -5010,6 +5038,7 @@ omt I L Omotik omu I E Omurano omw I L South Tairora omx I H Old Mon +omy I H Old Malay ona I L Ona onb I L Lingao one I L Oneida @@ -5058,6 +5087,7 @@ orz I L Ormu osa osa osa I L Osage osc I A Oscan osi I L Osing +osn I H Old Sundanese oso I L Ososo osp I H Old Spanish oss oss oss os I L Ossetian @@ -5112,7 +5142,6 @@ pap pap pap I L Papiamento paq I L Parya par I L Panamint pas I L Papasena -pat I L Papitalai pau pau pau I L Palauan pav I L Pakaásnovos paw I L Pawnee @@ -5127,6 +5156,7 @@ pbg I E Paraujano pbh I L E'ñapa Woromaipu pbi I L Parkwa pbl I L Mak (Nigeria) +pbm I L Puebla Mazatec pbn I L Kpasam pbo I L Papel pbp I L Badyara @@ -5180,7 +5210,7 @@ pex I L Petats pey I L Petjo pez I L Eastern Penan pfa I L Pááfang -pfe I L Peere +pfe I L Pere pfl I L Pfaelzisch pga I L Sudanese Creole Arabic pgd I H Gāndhārī @@ -5196,6 +5226,7 @@ pha I L Pa-Hng phd I L Phudagi phg I L Phuong phh I L Phukha +phj I L Pahari phk I L Phake phl I L Phalura phm I L Phimbi @@ -5215,7 +5246,6 @@ pie I E Piro pif I L Pingelapese pig I L Pisabo pih I L Pitcairn-Norfolk -pii I L Pini pij I E Pijao pil I L Yom pim I E Powhatan @@ -5234,7 +5264,7 @@ piz I L Pije pjt I L Pitjantjatjara pka I H Ardhamāgadhī Prākrit pkb I L Pokomo -pkc I E Paekche +pkc I A Paekche pkg I L Pak-Tong pkh I L Pankhu pkn I L Pakanha @@ -5252,12 +5282,10 @@ ple I L Palu'e plg I L Pilagá plh I L Paulohi pli pli pli pi I A Pali -plj I L Polci plk I L Kohistani Shina pll I L Shwe Palaung pln I L Palenquero plo I L Oluta Popoluca -plp I L Palpa plq I A Palaic plr I L Palaka Senoufo pls I L San Marcos Tlacoyalco Popoloca @@ -5275,7 +5303,6 @@ pmf I L Pamona pmh I H Māhārāṣṭri Prākrit pmi I L Northern Pumi pmj I L Southern Pumi -pmk I E Pamlico pml I E Lingua Franca pmm I L Pomo pmn I L Pam @@ -5291,8 +5318,9 @@ pmz I E Southern Pame pna I L Punan Bah-Biau pnb I L Western Panjabi pnc I L Pannei +pnd I L Mpinda pne I L Western Penan -png I L Pongu +png I L Pangu pnh I L Penrhyn pni I L Aoheng pnj I E Pinjarup @@ -5308,7 +5336,7 @@ pns I L Ponosakan pnt I L Pontic pnu I L Jiongnai Bunu pnv I L Pinigura -pnw I L Panytyima +pnw I L Banyjima pnx I L Phong-Kniang pny I L Pinyin pnz I L Pana (Central African Republic) @@ -5322,7 +5350,7 @@ pok I L Pokangá pol pol pol pl I L Polish pom I L Southeastern Pomo pon pon pon I L Pohnpeian -poo I L Central Pomo +poo I E Central Pomo pop I L Pwapwâ poq I L Texistepec Popoluca por por por pt I L Portuguese @@ -5358,7 +5386,6 @@ prl I L Peruvian Sign Language prm I L Kibiri prn I L Prasuni pro pro pro I H Old Provençal (to 1500) -prp I L Parsi prq I L Ashéninka Perené prr I E Puri prs I L Dari @@ -5368,7 +5395,7 @@ prw I L Parawen prx I L Purik prz I L Providencia Sign Language psa I L Asue Awyu -psc I L Persian Sign Language +psc I L Iranian Sign Language psd I L Plains Indian Sign Language pse I L Central Malay psg I L Penang Sign Language @@ -5388,7 +5415,7 @@ psw I L Port Sandwich psy I E Piscataway pta I L Pai Tavytera pth I E Pataxó Hã-Ha-Hãe -pti I L Pintiini +pti I L Pindiini ptn I L Patani pto I L Zo'é ptp I L Patep @@ -5436,7 +5463,9 @@ pys I L Paraguayan Sign Language pyu I L Puyuma pyx I A Pyu (Myanmar) pyy I L Pyen -pzn I L Para Naga +pze I L Pesse +pzh I L Pazeh +pzn I L Jejara Naga qua I L Quapaw qub I L Huallaga Huánuco Quechua quc I L K'iche' @@ -5549,12 +5578,13 @@ rgu I L Ringgou rhg I L Rohingya rhp I L Yahang ria I L Riang (India) +rib I L Bribri Sign Language rif I L Tarifit -ril I L Riang (Myanmar) +ril I L Riang Lang rim I L Nyaturu rin I L Nungu rir I L Ribun -rit I L Ritarungo +rit I L Ritharrngu riu I L Riung rjg I L Rajong rji I L Raji @@ -5567,7 +5597,7 @@ rkm I L Marka rkt I L Rangpuri rkw I E Arakwal rma I L Rama -rmb I L Rembarunga +rmb I L Rembarrnga rmc I L Carpathian Romani rmd I E Traveller Danish rme I L Angloromani @@ -5590,6 +5620,7 @@ rmw I L Welsh Romani rmx I L Romam rmy I L Vlax Romani rmz I L Marma +rnb I L Brunca Sign Language rnd I L Ruund rng I L Ronga rnl I L Ranglong @@ -5618,8 +5649,11 @@ rri I L Ririo rro I L Waima rrt I E Arritinngithigh rsb I L Romano-Serbian +rsk I L Ruthenian rsl I L Russian Sign Language rsm I L Miriwoong Sign Language +rsn I L Rwandan Sign Language +rsw I L Rishiwa rtc I L Rungtu Chin rth I L Ratahan rtm I L Rotuman @@ -5644,6 +5678,7 @@ ruy I L Mala (Nigeria) ruz I L Ruma rwa I L Rawo rwk I L Rwa +rwl I L Ruwila rwm I L Amba (Uganda) rwo I L Rawa rwr I L Marwari (India) @@ -5712,7 +5747,7 @@ sck I L Sadri scl I L Shina scn scn scn I L Sicilian sco sco sco I L Scots -scp I L Helambu Sherpa +scp I L Hyolmo scq I L Sa'och scs I L North Slavey sct I L Southern Katang @@ -5730,10 +5765,10 @@ sdh I L Southern Kurdish sdj I L Suundi sdk I L Sos Kundi sdl I L Saudi Arabian Sign Language -sdm I L Semandang sdn I L Gallurese Sardinian sdo I L Bukar-Sadung Bidayuh sdp I L Sherdukpen +sdq I L Semandang sdr I L Oraon Sadri sds I E Sened sdt I E Shuadit @@ -5890,7 +5925,6 @@ sll I L Salt-Yui slm I L Pangutaran Sama sln I E Salinan slp I L Lamaholot -slq I L Salchuq slr I L Salar sls I L Singapore Sign Language slt I L Sila @@ -5903,7 +5937,6 @@ slz I L Ma'ya sma sma sma I L Southern Sami smb I L Simbari smc I E Som -smd I L Sama sme sme sme se I L Northern Sami smf I L Auwe smg I L Simbali @@ -5926,7 +5959,6 @@ smx I L Samba smy I L Semnani smz I L Simeku sna sna sna sn I L Shona -snb I L Sebuyau snc I L Sinaugoro snd snd snd sd I L Sindhi sne I L Bau Bidayuh @@ -5948,7 +5980,7 @@ snv I L Sa'ban snw I L Selee snx I L Sam sny I L Saniyo-Hiyewe -snz I L Sinsauru +snz I L Kou soa I L Thai Song sob I L Sobei soc I L So (Democratic Republic of Congo) @@ -6006,6 +6038,7 @@ sqr I H Siculo Arabic sqs I L Sri Lankan Sign Language sqt I L Soqotri squ I L Squamish +sqx I L Kufr Qassem Sign Language (KQSL) sra I L Saruga srb I L Sora src I L Logudorese Sardinian @@ -6088,7 +6121,8 @@ sui I L Suki suj I L Shubi suk suk suk I L Sukuma sun sun sun su I L Sundanese -suq I L Suri +suo I L Bouni +suq I L Tirmaga-Chai Suri sur I L Mwaghavul sus sus sus I L Susu sut I E Subtiaba @@ -6104,7 +6138,7 @@ sve I L Serili svk I L Slovakian Sign Language svm I L Slavomolisano svs I L Savosavo -svx I E Skalvian +svx I H Skalvian swa swa swa sw M L Swahili (macrolanguage) swb I L Maore Comorian swc I L Congo Swahili @@ -6132,7 +6166,7 @@ swy I L Sarua sxb I L Suba sxc I A Sicanian sxe I L Sighu -sxg I L Shixing +sxg I L Shuhi sxk I E Southern Kalapuya sxl I E Selian sxm I L Samre @@ -6159,7 +6193,6 @@ syy I L Al-Sayyid Bedouin Sign Language sza I L Semelai szb I L Ngalum szc I L Semaq Beri -szd I E Seru sze I L Seze szg I L Sengele szl I L Silesian @@ -6168,6 +6201,7 @@ szp I L Suabo szs I L Solomon Islands Sign Language szv I L Isu (Fako Division) szw I L Sawai +szy I L Sakizaya taa I L Lower Tanana tab I L Tabassaran tac I L Lowland Tarahumara @@ -6194,13 +6228,12 @@ tax I L Tamki tay I L Atayal taz I L Tocho tba I L Aikanã -tbb I E Tapeba tbc I L Takia tbd I L Kaki Ae tbe I L Tanimbili tbf I L Mandara tbg I L North Tairora -tbh I E Thurawal +tbh I E Dharawal tbi I L Gaam tbj I L Tiang tbk I L Calamian Tagbanwa @@ -6287,6 +6320,7 @@ tev I L Teor tew I L Tewa (USA) tex I L Tennet tey I L Tulishi +tez I L Tetserret tfi I L Tofin Gbe tfn I L Tanaina tfo I L Tefaro @@ -6317,7 +6351,7 @@ tgx I L Tagish tgy I E Togoyo tgz I E Tagalaka tha tha tha th I L Thai -thd I L Thayore +thd I L Kuuk Thaayorre the I L Chitwania Tharu thf I L Thangmi thh I L Northern Tarahumara @@ -6333,7 +6367,6 @@ ths I L Thakali tht I L Tahltan thu I L Thuri thv I L Tahaggart Tamahaq -thw I L Thudam thy I L Tha thz I L Tayart Tamajeq tia I L Tidikelt Tamazight @@ -6362,10 +6395,12 @@ tiz I L Tai Hongjin tja I L Tajuasohn tjg I L Tunjung tji I L Northern Tujia +tjj I L Tjungundji tjl I L Tai Laing tjm I E Timucua tjn I E Tonjon tjo I L Temacine Tamazight +tjp I L Tjupany tjs I L Southern Tujia tju I E Tjurruru tjw I L Djabwurrung @@ -6421,7 +6456,6 @@ tmg I E Ternateño tmh tmh tmh M L Tamashek tmi I L Tutuba tmj I L Samarokena -tmk I L Northwestern Tamang tml I L Tamnim Citak tmm I L Tai Thanh tmn I L Taman (Indonesia) @@ -6466,7 +6500,8 @@ tog tog tog I L Tonga (Nyasa) toh I L Gitonga toi I L Tonga (Zambia) toj I L Tojolabal -tol I L Tolowa +tok I C Toki Pona +tol I E Tolowa tom I L Tombulu ton ton ton to I L Tonga (Tonga Islands) too I L Xicotepec De Juárez Totonac @@ -6498,7 +6533,6 @@ tpr I L Tuparí tpt I L Tlachichilco Tepehua tpu I L Tampuan tpv I L Tanapag -tpw I E Tupí tpx I L Acatepec Me'phaa tpy I L Trumai tpz I L Tinputz @@ -6533,7 +6567,7 @@ trr I L Taushiro trs I L Chicahuaxtla Triqui trt I L Tunggare tru I L Turoyo -trv I L Taroko +trv I L Sediq trw I L Torwali trx I L Tringgus-Sembaan Bidayuh try I E Turung @@ -6589,7 +6623,7 @@ ttw I L Long Wat tty I L Sikaritai ttz I L Tsum tua I L Wiarumus -tub I L Tübatulabal +tub I E Tübatulabal tuc I L Mutu tud I E Tuxá tue I L Tuyuca @@ -6601,7 +6635,7 @@ tuj I L Tugutil tuk tuk tuk tk I L Turkmen tul I L Tula tum tum tum I L Tumbuka -tun I E Tunica +tun I L Tunica tuo I L Tucano tuq I L Tedaga tur tur tur tr I L Turkish @@ -6614,6 +6648,7 @@ tuz I L Turka tva I L Vaghua tvd I L Tsuvadi tve I L Te'un +tvi I L Tulai tvk I L Southeast Ambrym tvl tvl tvl I L Tuvalu tvm I L Tela-Masbuar @@ -6623,6 +6658,7 @@ tvs I L Taveta tvt I L Tutsa Naga tvu I L Tunen tvw I L Sedoa +tvx I E Taivoan tvy I E Timor Pidgin twa I E Twana twb I L Western Tawbuid @@ -6677,6 +6713,7 @@ tyt I L Tày Tac tyu I L Kua tyv tyv tyv I L Tuvinian tyx I L Teke-Tyee +tyy I L Tiyaa tyz I L Tày tza I L Tanzanian Sign Language tzh I L Tzeltal @@ -6708,6 +6745,7 @@ ufi I L Ufim uga uga uga I A Ugaritic ugb I E Kuku-Ugbanh uge I L Ughele +ugh I L Kubachi ugn I L Ugandan Sign Language ugo I L Ugong ugy I L Uruguayan Sign Language @@ -6720,6 +6758,7 @@ uji I L Tanjijili uka I L Kaburi ukg I L Ukuriguma ukh I L Ukhwejo +uki I L Kui (India) ukk I L Muak Sa-aak ukl I L Ukrainian Sign Language ukp I L Ukpe-Bayobiri @@ -6727,6 +6766,7 @@ ukq I L Ukwa ukr ukr ukr uk I L Ukrainian uks I L Urubú-Kaapor Sign Language uku I L Ukue +ukv I L Kuku ukw I L Ukwuani-Aboh-Ndoni uky I E Kuuk-Yak ula I L Fungwa @@ -6735,17 +6775,18 @@ ulc I L Ulch ule I E Lule ulf I L Usku uli I L Ulithian -ulk I L Meriam +ulk I L Meriam Mir ull I L Ullatan ulm I L Ulumanda' uln I L Unserdeutsch ulu I L Uma' Lung ulw I L Ulwa +uly I L Buli uma I L Umatilla umb umb umb I L Umbundu umc I A Marrucinian umd I E Umbindhamu -umg I E Umbuygamu +umg I E Morrobalama umi I L Ukit umm I L Umon umn I L Makyan Naga @@ -6758,6 +6799,7 @@ una I L North Watut und und und S S Undetermined une I L Uneme ung I L Ngarinyin +uni I L Uni unk I L Enawené-Nawé unm I E Unami unn I L Kurnai @@ -6765,6 +6807,7 @@ unr I L Mundari unu I L Unubahe unx I L Munda unz I L Unde Kaili +uon I E Kulon upi I L Umeda upv I L Uripiv-Wala-Rano-Atchin ura I L Urarina @@ -6795,14 +6838,15 @@ ush I L Ushojo usi I L Usui usk I L Usaghade usp I L Uspanteco +uss I L us-Saare usu I L Uya uta I L Otank ute I L Ute-Southern Paiute +uth I L ut-Hun utp I L Amba (Solomon Islands) utr I L Etulo utu I L Utu uum I L Urum -uun I L Kulon-Pazeh uur I L Ura (Vanuatu) uuu I L U uve I L West Uvean @@ -6852,16 +6896,18 @@ vin I L Vinza vis I L Vishavan vit I L Viti viv I L Iduna +vjk I L Bajjika vka I E Kariyarra -vki I L Ija-Zuba vkj I L Kujarge vkk I L Kaur vkl I L Kulisusu vkm I E Kamakan +vkn I L Koro Nulu vko I L Kodeoha vkp I L Korlai Creole Portuguese vkt I L Tenggarong Kutai Malay vku I L Kurrama +vkz I L Koro Zuba vlp I L Valpei vls I L Vlaams vma I L Martuyhunira @@ -6907,8 +6953,8 @@ vut I L Vute vwa I L Awa (China) waa I L Walla Walla wab I L Wab -wac I L Wasco-Wishram -wad I L Wandamen +wac I E Wasco-Wishram +wad I L Wamesa wae I L Walser waf I E Wakoná wag I L Wa'ema @@ -6920,7 +6966,7 @@ wam I E Wampanoag wan I L Wan wao I E Wappo wap I L Wapishana -waq I L Wageman +waq I L Wagiman war war war I L Waray (Philippines) was was was I L Washo wat I L Kaninuwa @@ -6944,7 +6990,7 @@ wbp I L Warlpiri wbq I L Waddar wbr I L Wagdi wbs I L West Bengal Sign Language -wbt I L Wanman +wbt I L Warnman wbv I L Wajarri wbw I L Woi wca I L Yanomámi @@ -6953,6 +6999,7 @@ wdd I L Wandji wdg I L Wadaginam wdj I L Wadjiginy wdk I E Wadikali +wdt I L Wendat wdu I E Wadjigu wdy I E Wadjabangayi wea I E Wewaw @@ -6972,7 +7019,7 @@ wew I L Wejewa wfg I L Yafi wga I E Wagaya wgb I L Wagawaga -wgg I E Wangganguru +wgg I E Wangkangurru wgi I L Wahgi wgo I L Waigeo wgu I E Wirangu @@ -6982,10 +7029,10 @@ whg I L North Wahgi whk I L Wahau Kenyah whu I L Wahau Kayan wib I L Southern Toussian -wic I L Wichita +wic I E Wichita wie I E Wik-Epa wif I E Wik-Keyangan -wig I L Wik-Ngathana +wig I L Wik Ngathan wih I L Wik-Me'anha wii I L Minidien wij I L Wik-Iiyanh @@ -7003,6 +7050,7 @@ wka I E Kw'adza wkb I L Kumbaran wkd I L Wakde wkl I L Kalanadi +wkr I L Keerray-Woorroong wku I L Kunduvadi wkw I E Wakawaka wky I E Wangkayutyuru @@ -7010,6 +7058,7 @@ wla I L Walio wlc I L Mwali Comorian wle I L Wolane wlg I L Kunbarlang +wlh I L Welaun wli I L Waioli wlk I E Wailaki wll I L Wali (Sudan) @@ -7028,6 +7077,7 @@ wmb I L Wambaya wmc I L Wamas wmd I L Mamaindé wme I L Wambule +wmg I L Western Minyag wmh I L Waima'a wmi I E Wamin wmm I L Maiwa (Indonesia) @@ -7037,7 +7087,7 @@ wms I L Wambon wmt I L Walmajarri wmw I L Mwani wmx I L Womo -wnb I L Wanambre +wnb I L Mokati wnc I L Wantoat wnd I E Wandarang wne I L Waneci @@ -7051,7 +7101,7 @@ wnp I L Wanap wnu I L Usan wnw I L Wintu wny I L Wanyi -woa I L Tyaraity +woa I L Kuwema wob I L Wè Northern woc I L Wogeo wod I L Wolani @@ -7069,11 +7119,9 @@ wos I L Hanga Hundi wow I L Wawonii woy I E Weyto wpc I L Maco -wra I L Warapu -wrb I E Warluwara -wrd I L Warduji +wrb I E Waluwarra wrg I E Warungu -wrh I E Wiradhuri +wrh I E Wiradjuri wri I E Wariyangga wrk I L Garrwa wrl I L Warlmanpa @@ -7097,6 +7145,7 @@ wsr I L Owenia wss I L Wasa wsu I E Wasu wsv I E Wotapuri-Katarqalai +wtb I L Matambwe wtf I L Watiwa wth I E Wathawurrung wti I L Berta @@ -7123,10 +7172,10 @@ wwr I E Warrwa www I L Wawa wxa I L Waxianghua wxw I E Wardandi -wya I L Wyandot wyb I L Wangaaybuwan-Ngiyambaa wyi I E Woiwurrung wym I L Wymysorys +wyn I L Wyandot wyr I L Wayoró wyy I L Western Fijian xaa I H Andalusian Arabic @@ -7134,12 +7183,12 @@ xab I L Sambe xac I L Kachari xad I E Adai xae I A Aequian -xag I E Aghwan +xag I A Aghwan xai I E Kaimbé xaj I E Ararandewára xak I E Máku xal xal xal I L Kalmyk -xam I E /Xam +xam I E ǀXam xan I L Xamtanga xao I L Khao xap I E Apalachee @@ -7160,12 +7209,12 @@ xbi I L Kombio xbj I E Birrpayi xbm I H Middle Breton xbn I E Kenaboi -xbo I E Bolgarian +xbo I H Bolgarian xbp I E Bibbulman xbr I L Kambera xbw I E Kambiwá -xby I L Batyala -xcb I E Cumbric +xby I L Batjala +xcb I H Cumbric xcc I A Camunic xce I A Celtiberian xcg I A Cisalpine Gaulish @@ -7176,7 +7225,7 @@ xcn I E Cotoname xco I A Chorasmian xcr I A Carian xct I H Classical Tibetan -xcu I E Curonian +xcu I H Curonian xcv I E Chuvantsy xcw I E Coahuilteco xcy I E Cayuse @@ -7185,10 +7234,11 @@ xdc I A Dacian xdk I E Dharuk xdm I A Edomite xdo I L Kwandu +xdq I L Kaitag xdy I L Malayic Dayak xeb I A Eblan xed I L Hdi -xeg I E //Xegwi +xeg I E ǁXegwi xel I L Kelo xem I L Kembayan xep I A Epi-Olmec @@ -7203,15 +7253,16 @@ xgd I E Gudang xgf I E Gabrielino-Fernandeño xgg I E Goreng xgi I E Garingbal -xgl I E Galindan +xgl I H Galindan xgm I E Dharumbal xgr I E Garza xgu I L Unggumi xgw I E Guwa xha I A Harami -xhc I E Hunnic +xhc I A Hunnic xhd I A Hadrami xhe I L Khetrani +xhm I H Middle Khmer (1400 to 1850 CE) xho xho xho xh I L Xhosa xhr I A Hernican xht I A Hattic @@ -7236,7 +7287,7 @@ xkf I L Khengkha xkg I L Kagoro xki I L Kenyan Sign Language xkj I L Kajali -xkk I L Kaco' +xkk I L Kachok xkl I L Mainstream Kenyah xkn I L Kayan River Kayan xko I L Kiorr @@ -7271,7 +7322,7 @@ xmd I L Mbudum xme I A Median xmf I L Mingrelian xmg I L Mengaka -xmh I L Kuku-Muminh +xmh I L Kugu-Muminh xmj I L Majera xmk I A Ancient Macedonian xml I L Malaysian Sign Language @@ -7286,7 +7337,7 @@ xmt I L Matbat xmu I E Kamu xmv I L Antankarana Malagasy xmw I L Tsimihety Malagasy -xmx I L Maden +xmx I L Salawati xmy I L Mayaguduna xmz I L Mori Bawah xna I A Ancient North Arabian @@ -7294,9 +7345,12 @@ xnb I L Kanakanabu xng I H Middle Mongolian xnh I L Kuanhua xni I E Ngarigu +xnj I L Ngoni (Tanzania) xnk I E Nganakarti +xnm I E Ngumbarl xnn I L Northern Kankanay xno I H Anglo-Norman +xnq I L Ngoni (Mozambique) xnr I L Kangri xns I L Kanashi xnt I E Narragansett @@ -7315,22 +7369,31 @@ xop I L Kopar xor I L Korubo xow I L Kowaki xpa I E Pirriya -xpc I E Pecheneg +xpb I E Northeastern Tasmanian +xpc I H Pecheneg +xpd I E Oyster Bay Tasmanian xpe I L Liberia Kpelle +xpf I E Southeast Tasmanian xpg I A Phrygian -xpi I E Pictish +xph I E North Midlands Tasmanian +xpi I H Pictish xpj I E Mpalitjanh xpk I L Kulina Pano +xpl I E Port Sorell Tasmanian xpm I E Pumpokol xpn I E Kapinawá xpo I E Pochutec -xpp I E Puyo-Paekche +xpp I A Puyo-Paekche xpq I E Mohegan-Pequot xpr I A Parthian -xps I E Pisidian +xps I A Pisidian xpt I E Punthamara xpu I A Punic -xpy I E Puyo +xpv I E Northern Tasmanian +xpw I E Northwestern Tasmanian +xpx I E Southwestern Tasmanian +xpy I A Puyo +xpz I E Bruny Island Tasmanian xqa I H Karakhanid xqt I A Qatabanian xra I L Krahô @@ -7339,9 +7402,8 @@ xrd I E Gundungurra xre I L Kreye xrg I E Minang xri I L Krikati-Timbira -xrm I E Armazic +xrm I A Armazic xrn I E Arin -xrq I E Karranga xrr I A Raetic xrt I E Aranama-Tamique xru I L Marriammu @@ -7353,6 +7415,7 @@ xsd I A Sidetic xse I L Sempan xsh I L Shamang xsi I L Sio +xsj I L Subi xsl I L South Slavey xsm I L Kasem xsn I L Sanga (Nigeria) @@ -7360,7 +7423,6 @@ xso I E Solano xsp I L Silopi xsq I L Makhuwa-Saka xsr I L Sherpa -xss I E Assan xsu I L Sanumá xsv I E Sudovian xsy I L Saisiyat @@ -7386,7 +7448,6 @@ xtu I L Cuyamecalco Mixtec xtv I E Thawa xtw I L Tawandê xty I L Yoloxochitl Mixtec -xtz I E Tasmanian xua I L Alu Kurumba xub I L Betta Kurumba xud I E Umiida @@ -7475,6 +7536,7 @@ ych I L Chesu ycl I L Lolopo ycn I L Yucuna ycp I L Chepya +ycr I L Yilan Creole yda I E Yanda ydd I L Eastern Yiddish yde I L Yangum Dey @@ -7509,13 +7571,13 @@ yia I L Yinggarda yid yid yid yi M L Yiddish yif I L Ache yig I L Wusa Nasu -yih I L Western Yiddish +yih I E Western Yiddish yii I L Yidiny yij I L Yindjibarndi yik I L Dongshanba Lalo yil I E Yindjilandji yim I L Yimchungru Naga -yin I L Yinchia +yin I L Riang Lai yip I L Pholo yiq I L Miqie yir I L North Awyu @@ -7527,6 +7589,7 @@ yix I L Axi Yi yiz I L Azhe yka I L Yakan ykg I L Northern Yukaghir +ykh I L Khamnigan Mongol yki I L Yoke ykk I L Yakaikeke ykl I L Khlula @@ -7613,6 +7676,7 @@ ysc I E Yassic ysd I L Samatao ysg I L Sonaga ysl I L Yugoslavian Sign Language +ysm I L Myanmar Sign Language ysn I L Sani yso I L Nisi (China) ysp I L Southern Lolopo @@ -7639,7 +7703,7 @@ yum I L Quechan yun I L Bena (Nigeria) yup I L Yukpa yuq I L Yuqui -yur I L Yurok +yur I E Yurok yut I L Yopno yuw I L Yau (Morobe Province) yux I L Southern Yukaghir @@ -7692,17 +7756,21 @@ zaw I L Mitla Zapotec zax I L Xadani Zapotec zay I L Zayse-Zergulla zaz I L Zari +zba I C Balaibalan zbc I L Central Berawan zbe I L East Berawan zbl zbl zbl I C Blissymbols zbt I L Batui +zbu I L Bu (Bauchi State) zbw I L West Berawan zca I L Coatecas Altas Zapotec +zcd I L Las Delicias Zapotec zch I L Central Hongshuihe Zhuang zdj I L Ngazidja Comorian zea I L Zeeuws zeg I L Zenag zeh I L Eastern Hongshuihe Zhuang +zem I L Zeem zen zen zen I L Zenaga zga I L Kinga zgb I L Guibei Zhuang @@ -7723,27 +7791,27 @@ zik I L Zimakani zil I L Zialo zim I L Mesme zin I L Zinza -zir I E Ziriya ziw I L Zigula ziz I L Zizilivakan zka I L Kaimbulawa -zkb I E Koibal zkd I L Kadu -zkg I E Koguryo -zkh I E Khorezmian +zkg I A Koguryo +zkh I H Khorezmian zkk I E Karankawa zkn I L Kanan zko I E Kott zkp I E São Paulo Kaingáng zkr I L Zakhring -zkt I E Kitan -zku I E Kaurna +zkt I H Kitan +zku I L Kaurna zkv I E Krevinian -zkz I E Khazar +zkz I H Khazar +zla I L Zula zlj I L Liujiang Zhuang zlm I L Malay (individual language) zln I L Lianshan Zhuang zlq I L Liuqian Zhuang +zlu I L Zul zma I L Manda (Australia) zmb I L Zimba zmc I E Margany @@ -7755,7 +7823,7 @@ zmh I E Makolkol zmi I L Negeri Sembilan Malay zmj I L Maridjabin zmk I E Mandandanyi -zml I L Madngele +zml I E Matngala zmm I L Marimanindji zmn I L Mbangwe zmo I L Molo @@ -7809,7 +7877,7 @@ zpx I L San Baltazar Loxicha Zapotec zpy I L Mazaltepec Zapotec zpz I L Texmelucan Zapotec zqe I L Qiubei Zhuang -zra I E Kara (Korea) +zra I A Kara (Korea) zrg I L Mirgan zrn I L Zerenkel zro I L Záparo @@ -7833,7 +7901,6 @@ ztt I L Tejalapan Zapotec ztu I L Güilá Zapotec ztx I L Zaachila Zapotec zty I L Yatee Zapotec -zua I L Zeem zuh I L Tokano zul zul zul zu I L Zulu zum I L Kumzari diff --git a/resources/standards-data/iso639-3/readme.md b/resources/standards-data/iso639-3/readme.md deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/resources/standards-data/langtags/langtags.json b/resources/standards-data/langtags/langtags.json index 788fa9950c9beeef82762f4193e12a0bbe3362db..1ce3f2604e2fcc1697f4069c9a2e87794cd3015d 100644 --- a/resources/standards-data/langtags/langtags.json +++ b/resources/standards-data/langtags/langtags.json @@ -1,4 +1,9 @@ [ + { + "regions": [ "AA", "AC", "AD", "AN", "AQ", "AS", "AX", "BL", "BU", "BV", "CP", "CQ", "CS", "DD", "EA", "EU", "EZ", "FX", "GS", "HM", "IC", "MC", "MF", "MQ", "NT", "PM", "QM", "QN", "QO", "QP", "QQ", "QR", "QS", "QT", "QU", "QV", "QW", "QX", "QY", "QZ", "SM", "SU", "TA", "TF", "TP", "UM", "UN", "VG", "XA", "XB", "XC", "XD", "XE", "XF", "XG", "XH", "XI", "XJ", "XL", "XM", "XN", "XO", "XP", "XQ", "XR", "XS", "XT", "XU", "XV", "XW", "XY", "XZ", "YD", "YU", "ZR", "ZZ" ], + "scripts": [ "Aran", "Cpmn", "Egyd", "Egyh", "Hira", "Hrkt", "Inds", "Jamo", "Mero", "Moon", "Pcun", "Phlv", "Psin", "Qaaa", "Qaab", "Qaac", "Qaad", "Qaae", "Qaaf", "Qaag", "Qaah", "Qaai", "Qaaj", "Qaak", "Qaal", "Qaam", "Qaan", "Qaao", "Qaap", "Qaaq", "Qaar", "Qaas", "Qaat", "Qaau", "Qaav", "Qaaw", "Qaax", "Qaay", "Qaaz", "Qaba", "Qabb", "Qabc", "Qabd", "Qabe", "Qabf", "Qabg", "Qabh", "Qabi", "Qabj", "Qabk", "Qabl", "Qabm", "Qabn", "Qabo", "Qabp", "Qabq", "Qabr", "Qabs", "Qabt", "Qabu", "Qabv", "Qabw", "Qabx", "Roro", "Shui", "Syre", "Syrn", "Visp", "Zinh", "Zmth", "Zsye", "Zsym" ], + "tag": "_conformance" + }, { "tag": "_globalvar", "variants": [ "simple" ] @@ -8,8 +13,8 @@ "variants": [ "alalc97", "fonipa", "fonkirsh", "fonnapa", "fonupa", "fonxsamp" ] }, { - "api": "1.2.1", - "date": "2020-08-21", + "api": "1.3.1", + "date": "2023-05-04", "tag": "_version" }, { @@ -17,7 +22,7 @@ "iana": [ "Afar" ], "iso639_3": "aar", "localname": "Qafar", - "localnames": [ "Qafar" ], + "localnames": [ "Qafar af" ], "name": "Afar", "region": "ET", "regionname": "Ethiopia", @@ -25,7 +30,7 @@ "sldr": true, "tag": "aa", "tags": [ "aa-ET", "aa-Latn" ], - "windows": "aa-Latn" + "windows": "aa-Latn-ET" }, { "full": "aa-Arab-ET", @@ -39,14 +44,14 @@ "script": "Arab", "sldr": false, "tag": "aa-Arab", - "windows": "aa-Arab" + "windows": "aa-Arab-ET" }, { "full": "aa-Latn-DJ", "iana": [ "Afar" ], "iso639_3": "aar", "localname": "Qafar", - "localnames": [ "Qafar" ], + "localnames": [ "Qafar af" ], "name": "Afar", "region": "DJ", "regionname": "Djibouti", @@ -60,7 +65,7 @@ "iana": [ "Afar" ], "iso639_3": "aar", "localname": "Qafar", - "localnames": [ "Qafar" ], + "localnames": [ "Qafar af" ], "name": "Afar", "region": "ER", "regionname": "Eritrea", @@ -81,14 +86,15 @@ "script": "Ethi", "sldr": false, "tag": "aa-Ethi", - "windows": "aa-Ethi" + "windows": "aa-Ethi-ET" }, { "full": "aaa-Latn-NG", "iana": [ "Ghotuo" ], "iso639_3": "aaa", + "localnames": [ "Ghotuọ" ], "name": "Ghotuo", - "names": [ "Otuo", "Otwa" ], + "names": [ "Ghotuọ", "Otuo", "Otwa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -101,8 +107,8 @@ "full": "aab-Latn-NG", "iana": [ "Alumu-Tesu" ], "iso639_3": "aab", - "name": "Alumu-Tesu", - "names": [ "Alumu", "Arum-Cesu", "Arum-Chessu", "Arum-Tesu", "Təsu" ], + "name": "Arum", + "names": [ "Alomo-Atesu", "Alumu", "Alumu-Tesu", "Arum-Cesu", "Arum-Chessu", "Arum-Tesu", "Təsu" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -145,6 +151,7 @@ "iana": [ "Arbëreshë Albanian" ], "iso639_3": "aae", "localnames": [ "Tarbrisht" ], + "macrolang": "sq", "name": "Albanian, Arbëreshë", "names": [ "Arbëreshë", "Arbëreshë Albanian", "Arbërisht", "Arbërishtja", "Gjegje", "Tarbrisht" ], "region": "IT", @@ -159,6 +166,7 @@ "full": "aae-Grek-IT", "iana": [ "Arbëreshë Albanian" ], "iso639_3": "aae", + "macrolang": "sq", "name": "Albanian, Arbëreshë", "names": [ "Arbëreshë", "Arbëreshë Albanian", "Arbërisht", "Arbërishtja", "Gjegje", "Tarbrisht" ], "nophonvars": true, @@ -171,19 +179,49 @@ "windows": "aae-Grek" }, { - "full": "aaf-Zyyy-IN", + "full": "aae-Latn-IT-x-sub84", + "iana": [ "Albanian, Arbereshe" ], + "iso639_3": "aae", + "localnames": [ "Tarbrisht" ], + "macrolang": "sq", + "name": "Albanian, Arbëreshë", + "names": [ "Albanian, Arbereshe", "Arbëreshë", "Arbërisht", "Arbërishtja", "Gjegje", "Tarbrisht" ], + "region": "IT", + "regionname": "Italy", + "rod": "6861", + "script": "Latn", + "sldr": false, + "tag": "aae-x-sub84", + "tags": [ "aae-IT-x-sub84", "aae-Latn-x-sub84" ], + "windows": "aae-Latn-x-sub84" + }, + { + "full": "aaf-Mlym-IN", "iana": [ "Aranadan" ], "iso639_3": "aaf", "name": "Aranadan", "names": [ "Aranatan", "Arnatas", "Eranadans" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Mlym", "sldr": false, "tag": "aaf", - "tags": [ "aaf-IN", "aaf-Zyyy" ], - "unwritten": true, - "windows": "aaf-Zyyy" + "tags": [ "aaf-IN", "aaf-Mlym" ], + "windows": "aaf-Mlym" + }, + { + "full": "aaf-Arab-IN", + "iana": [ "Aranadan" ], + "iso639_3": "aaf", + "name": "Aranadan", + "names": [ "Aranatan", "Arnatas", "Eranadans" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Arab", + "sldr": false, + "tag": "aaf-Arab", + "windows": "aaf-Arab" }, { "full": "aag-Latn-PG", @@ -203,23 +241,23 @@ "full": "aah-Latn-PG", "iana": [ "Abu' Arapesh" ], "iso639_3": "aah", - "name": "Arapesh, Abu’", - "names": [ "Abu' Arapesh", "Abu’", "Ua" ], + "name": "Abu’", + "names": [ "Abu' Arapesh", "Abu’ Arapesh", "Ua" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "aah", "tags": [ "aah-Latn", "aah-PG" ], - "unwritten": true, "windows": "aah-Latn" }, { "full": "aai-Latn-PG", "iana": [ "Arifama-Miniafia" ], "iso639_3": "aai", + "localnames": [ "Miniafia" ], "name": "Miniafia Oyan", - "names": [ "Arifama-Miniafia", "Miniafia-Arifama" ], + "names": [ "Arifama-Miniafia", "Miniafia", "Miniafia-Arifama" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -228,6 +266,38 @@ "tags": [ "aai-Latn", "aai-PG" ], "windows": "aai-Latn" }, + { + "full": "aai-Latn-PG-x-northern-suboro", + "iana": [ "Arifama-Miniafia" ], + "iso639_3": "aai", + "localnames": [ "Miniafia" ], + "name": "Miniafia Oyan", + "names": [ "Arifama-Miniafia", "Miniafia", "Miniafia-Arifama" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "2436", + "script": "Latn", + "sldr": false, + "tag": "aai-x-northern-suboro", + "tags": [ "aai-Latn-x-northern-suboro", "aai-PG-x-northern-suboro" ], + "windows": "aai-Latn-x-northern-suboro" + }, + { + "full": "aai-Latn-PG-x-suboro", + "iana": [ "Arifama-Miniafia" ], + "iso639_3": "aai", + "localnames": [ "Miniafia" ], + "name": "Miniafia Oyan", + "names": [ "Arifama-Miniafia", "Miniafia", "Miniafia-Arifama" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "2416", + "script": "Latn", + "sldr": false, + "tag": "aai-x-suboro", + "tags": [ "aai-Latn-x-suboro", "aai-PG-x-suboro" ], + "windows": "aai-Latn-x-suboro" + }, { "full": "aak-Latn-PG", "iana": [ "Ankave" ], @@ -235,7 +305,7 @@ "localname": "Aga pɨ'ne'", "localnames": [ "Aga pɨ'ne'" ], "name": "Ankave", - "names": [ "Aga pɨ’ne’", "Angave" ], + "names": [ "Aga pɨ’ne’", "Angave", "Ankave-Anga" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -277,6 +347,7 @@ "full": "aao-Arab-DZ", "iana": [ "Algerian Saharan Arabic" ], "iso639_3": "aao", + "macrolang": "ar", "name": "Arabic, Algerian Saharan Spoken", "names": [ "Algerian Saharan Arabic", "Saharan Arabic", "Tamanghasset Arabic", "Tamanrasset Arabic" ], "region": "DZ", @@ -304,19 +375,19 @@ "windows": "aap-Latn" }, { - "full": "aaq-Zyyy-US", + "full": "aaq-Latn-US", "iana": [ "Eastern Abnaki" ], "iso639_3": "aaq", "name": "Abenaki, Eastern", "names": [ "Abenaki", "Alnombak", "Alnôbak", "Eastern Abnaki", "Wawenock" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "aaq", - "tags": [ "aaq-US", "aaq-Zyyy" ], + "tags": [ "aaq-Latn", "aaq-US" ], "unwritten": true, - "windows": "aaq-Zyyy" + "windows": "aaq-Latn" }, { "full": "aas-Latn-TZ", @@ -339,6 +410,7 @@ "iso639_3": "aat", "latnnames": [ "Arbërisht" ], "localnames": [ "αρbε̰ρίσ̈τ" ], + "macrolang": "sq", "name": "Albanian, Arvanitika", "names": [ "Arberichte", "Arbërisht", "Arvanitic", "Arvanitika", "Arvanitika Albanian" ], "region": "GR", @@ -427,7 +499,7 @@ "suppress": true, "tag": "ab", "tags": [ "ab-Cyrl", "ab-GE" ], - "windows": "ab" + "windows": "ab-GE" }, { "full": "ab-Geor-GE", @@ -443,7 +515,21 @@ "script": "Geor", "sldr": false, "tag": "ab-Geor", - "windows": "ab-Geor" + "windows": "ab-Geor-GE" + }, + { + "full": "ab-Latn-TR", + "iana": [ "Abkhazian" ], + "iso639_3": "abk", + "name": "Abkhaz", + "names": [ "Abkhazian", "Abxazo" ], + "region": "TR", + "regionname": "Turkey", + "script": "Latn", + "sldr": false, + "tag": "ab-Latn", + "tags": [ "ab-TR" ], + "windows": "ab-Latn-TR" }, { "full": "aba-Latn-CI", @@ -478,8 +564,9 @@ "full": "abc-Latn-PH", "iana": [ "Ambala Ayta" ], "iso639_3": "abc", + "localnames": [ "Ayta Ambala" ], "name": "Ayta, Ambala", - "names": [ "Ambala Agta", "Ambala Ayta", "Ambala Sambal" ], + "names": [ "Ambala Agta", "Ambala Ayta", "Ambala Sambal", "Ayta Ambala" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -552,6 +639,7 @@ "full": "abh-Arab-TJ", "iana": [ "Tajiki Arabic" ], "iso639_3": "abh", + "macrolang": "ar", "name": "Arabic, Tajiki Spoken", "names": [ "Arabi", "Bukhara Arabic", "Buxara Arabic", "Central Asian", "Jugari", "Tajiki Arabic" ], "region": "TJ", @@ -567,8 +655,9 @@ "full": "abi-Latn-CI", "iana": [ "Abidji" ], "iso639_3": "abi", + "localnames": [ "Enyembe lɛɛ Ogbru" ], "name": "Abidji", - "names": [ "Abiji" ], + "names": [ "Abiji", "Enyembe lɛɛ Ogbru" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -605,6 +694,19 @@ "tags": [ "abl-ID", "abl-Rjng" ], "windows": "abl-Rjng" }, + { + "full": "abl-Latn-ID", + "iana": [ "Lampung Nyo" ], + "iso639_3": "abl", + "name": "Lampung Nyo", + "names": [ "Abung", "Lampong" ], + "region": "ID", + "regionname": "Indonesia", + "script": "Latn", + "sldr": false, + "tag": "abl-Latn", + "windows": "abl-Latn" + }, { "full": "abm-Latn-NG", "iana": [ "Abanyom" ], @@ -617,7 +719,6 @@ "sldr": false, "tag": "abm", "tags": [ "abm-Latn", "abm-NG" ], - "unwritten": true, "windows": "abm-Latn" }, { @@ -661,7 +762,7 @@ "region": "PH", "regionname": "Philippines", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "abp", "tags": [ "abp-Latn", "abp-PH" ], "windows": "abp-Latn" @@ -681,7 +782,7 @@ "sldr": false, "tag": "abq-Cyrl", "tags": [ "abq", "abq-RU" ], - "windows": "abq-Cyrl" + "windows": "abq-Cyrl-RU" }, { "full": "abq-Latn-TR", @@ -696,13 +797,12 @@ "sldr": true, "tag": "abq-Latn", "tags": [ "abq-TR" ], - "windows": "abq-Latn" + "windows": "abq-Latn-TR" }, { "full": "abr-Latn-GH", "iana": [ "Abron" ], "iso639_3": "abr", - "localnames": [ "Brong" ], "name": "Abron", "names": [ "Bron", "Brong", "Doma", "Gyaman" ], "region": "GH", @@ -734,8 +834,8 @@ "full": "abt-Latn-PG", "iana": [ "Ambulas" ], "iso639_3": "abt", - "localname": "Ambulas", - "localnames": [ "Ambulas" ], + "localname": "Abulas", + "localnames": [ "Abulas" ], "name": "Ambulas", "names": [ "Abelam", "Abulas" ], "region": "PG", @@ -750,7 +850,7 @@ "full": "abt-Latn-PG-x-maprik", "iana": [ "Ambulas - Maprik" ], "iso639_3": "abt", - "localnames": [ "Ambulas" ], + "localnames": [ "Abulas" ], "name": "Ambulas", "names": [ "Abelam", "Abulas", "Ambulas - Maprik" ], "region": "PG", @@ -766,7 +866,7 @@ "full": "abt-Latn-PG-x-woserak", "iana": [ "Ambulas - Wosera Kamu" ], "iso639_3": "abt", - "localnames": [ "Ambulas" ], + "localnames": [ "Abulas" ], "name": "Ambulas", "names": [ "Abelam", "Abulas", "Ambulas - Wosera Kamu" ], "region": "PG", @@ -782,8 +882,9 @@ "full": "abu-Latn-CI", "iana": [ "Abure" ], "iso639_3": "abu", + "localnames": [ "ɔbule ɔyʋɛ" ], "name": "Abure", - "names": [ "Abonwa", "Abouré", "Abule", "Akaplass", "Ehie", "Eyive", "Ossouon" ], + "names": [ "Abonwa", "Abouré", "Abule", "Akaplass", "Ehie", "Eyive", "Obule oyve", "Ossouon", "ɔbule ɔyʋɛ" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -798,6 +899,7 @@ "iso639_3": "abv", "latnnames": [ "al-bahrania" ], "localnames": [ "البحرانية" ], + "macrolang": "ar", "name": "Arabic, Baharna Spoken", "names": [ "Baharna", "Baharna Arabic", "Baharnah", "Bahraini Arabic", "Bahraini Shi’ite Arabic", "Bahrani", "Bahrani Arabic" ], "region": "BH", @@ -950,11 +1052,11 @@ "iso639_3": "acf", "localname": "Kwéyòl", "localnames": [ "Kwéyòl" ], - "name": "Saint Lucian Creole French", - "names": [ "Creole", "Dominican Creole French", "Grenadian Creole French", "Kwèyòl", "Kwéyòl", "Lesser Antillean Creole French", "Patois", "Patwa", "Trinidadian Creole French" ], + "name": "Lesser Antillean French Creole", + "names": [ "Créole", "Dominican Creole French", "French Creole", "French-lexicon Creole", "French-lexified Creole", "Kwèyòl", "Kwéyòl", "Kéwòl", "Patois", "Patwa", "Saint Lucian Creole French" ], "region": "LC", "regionname": "Saint Lucia", - "regions": [ "DM", "FR", "GD", "GY", "MF", "TT" ], + "regions": [ "DM", "FR", "GD", "GY", "MF", "MQ", "TT" ], "script": "Latn", "sldr": true, "tag": "acf", @@ -982,7 +1084,7 @@ "iana": [ "Aka-Cari" ], "iso639_3": "aci", "name": "Aka-Cari", - "names": [ "Cari", "Chari", "Chariar" ], + "names": [ "Akacari", "Cari", "Chari", "Chariar", "Sare" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -1025,8 +1127,9 @@ "iso639_3": "acm", "latnnames": [ "allahjat aleiraqia" ], "localnames": [ "اللهجة العراقية" ], + "macrolang": "ar", "name": "Arabic, Mesopotamian Spoken", - "names": [ "Arabi", "Arabic", "Baghdadi", "Furati", "Iraqi Arabic", "Mesopotamian Arabic", "Mesopotamian Gelet Arabic", "North Syrian Arabic" ], + "names": [ "Arabi", "Arabic", "Baghdadi", "Furati", "Gilit Mesopotamian Arabic", "Iraqi Arabic", "Mesopotamian Arabic", "Mesopotamian Gelet Arabic", "North Syrian Arabic" ], "region": "IQ", "regionname": "Iraq", "regions": [ "IR", "JO", "NL", "SY", "TR" ], @@ -1040,8 +1143,9 @@ "full": "acn-Latn-CN", "iana": [ "Achang" ], "iso639_3": "acn", + "localnames": [ "Ngachang" ], "name": "Achang", - "names": [ "Acang", "Achung", "Ach’ang", "Ahchan", "Anchan", "Atsang", "Chung", "Lawng", "Maingtha", "Manmaw", "Mönghsa", "Ngacang", "Ngachang", "Ngac’ang", "Ngatsang", "Ngo Chang", "Ngochang", "Tai Sa’", "Xiandao" ], + "names": [ "Acang", "Achung", "Ach’ang", "Ahchan", "Anchan", "Atsang", "Chung", "Lawng", "Longchuan Achang", "Maingtha", "Manmaw", "Mönghsa", "Ngacang", "Ngachang", "Ngac’ang", "Ngatsang", "Ngo Chang", "Ngochang", "Ngoq Changh", "Ngoqchangh", "Tai Sa’", "Xiandao" ], "region": "CN", "regionname": "China", "regions": [ "MM" ], @@ -1070,11 +1174,12 @@ "full": "acq-Arab-YE", "iana": [ "Ta'izzi-Adeni Arabic" ], "iso639_3": "acq", + "macrolang": "ar", "name": "Arabic, Ta’izzi-Adeni Spoken", "names": [ "Djibouti Arabic", "Southern Yemeni Spoken Arabic", "Ta'izzi-Adeni Arabic" ], "region": "YE", "regionname": "Yemen", - "regions": [ "AE", "DJ", "EG", "ER", "KE", "LY", "OM", "SA", "SO" ], + "regions": [ "AE", "DJ", "EG", "ER", "ET", "KE", "LY", "OM", "SA", "SO", "TM" ], "script": "Arab", "sldr": false, "tag": "acq", @@ -1085,10 +1190,10 @@ "full": "acr-Latn-GT", "iana": [ "Achi" ], "iso639_3": "acr", - "localname": "Achi'", - "localnames": [ "Qach'a'teem" ], + "localname": "Qachʼaʼteem", + "localnames": [ "Qach'a'teem", "Qach'a'teem Achi" ], "name": "Achi", - "names": [ "Qach’a’teem" ], + "names": [ "A’cha’teem Achi", "Qach’a’teem", "Qach’a’teem Achi", "Qach’a’tem Achi" ], "region": "GT", "regionname": "Guatemala", "script": "Latn", @@ -1101,14 +1206,15 @@ "full": "acr-Latn-GT-x-cubulco", "iana": [ "Achi - Cubulco" ], "iso639_3": "acr", - "localnames": [ "Qach'a'teem" ], + "localname": "Qachʼaʼteem", + "localnames": [ "Qach'a'teem", "Qach'a'teem Achi" ], "name": "Achi", - "names": [ "Achi - Cubulco", "Qach’a’teem" ], + "names": [ "Achi - Cubulco", "A’cha’teem Achi", "Qach’a’teem", "Qach’a’teem Achi", "Qach’a’tem Achi" ], "region": "GT", "regionname": "Guatemala", "rod": "02098", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "acr-x-cubulco", "tags": [ "acr-GT-x-cubulco", "acr-Latn-x-cubulco" ], "windows": "acr-Latn-x-cubulco" @@ -1117,14 +1223,15 @@ "full": "acr-Latn-GT-x-rabinal", "iana": [ "Achi - Rabinal" ], "iso639_3": "acr", - "localnames": [ "Qach'a'teem" ], + "localname": "Qachʼaʼteem", + "localnames": [ "Qach'a'teem", "Qach'a'teem Achi" ], "name": "Achi", - "names": [ "Achi - Rabinal", "Qach’a’teem" ], + "names": [ "Achi - Rabinal", "A’cha’teem Achi", "Qach’a’teem", "Qach’a’teem Achi", "Qach’a’tem Achi" ], "region": "GT", "regionname": "Guatemala", "rod": "00448", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "acr-x-rabinal", "tags": [ "acr-GT-x-rabinal", "acr-Latn-x-rabinal" ], "windows": "acr-Latn-x-rabinal" @@ -1163,7 +1270,7 @@ "full": "acu-Latn-EC", "iana": [ "Achuar-Shiwiar" ], "iso639_3": "acu", - "localname": "Achuar", + "localname": "Achuár chícham", "localnames": [ "Achuár chícham" ], "name": "Achuar-Shiwiar", "names": [ "Achiar chícham", "Achual", "Achuale", "Achuar", "Achuar-Shiviar", "Achuar-Shiwiara", "Achuara", "Achuare", "Achuár chícham", "Ashuar", "Atchuara", "Jivaro", "Maina", "Áints" ], @@ -1195,6 +1302,7 @@ "full": "acw-Arab-SA", "iana": [ "Hijazi Arabic" ], "iso639_3": "acw", + "macrolang": "ar", "name": "Arabic, Hijazi Spoken", "names": [ "Hejazi Arabic", "Hijazi Arabic", "Rashaida", "Rashida", "West Arabian Colloquial Arabic" ], "region": "SA", @@ -1212,11 +1320,12 @@ "iso639_3": "acx", "latnnames": [ "ʿumāni" ], "localnames": [ "عماني" ], + "macrolang": "ar", "name": "Arabic, Omani Spoken", "names": [ "Omani Arabic", "Omani Hadari Arabic" ], "region": "OM", "regionname": "Oman", - "regions": [ "AE", "DJ", "KE", "SA", "TZ", "YE" ], + "regions": [ "AE", "DJ", "KE", "KW", "SA", "TZ", "YE" ], "script": "Arab", "sldr": false, "tag": "acx", @@ -1228,6 +1337,7 @@ "iana": [ "Cypriot Arabic" ], "iso639_3": "acy", "localnames": [ "Sanna" ], + "macrolang": "ar", "name": "Arabic, Cypriot Spoken", "names": [ "Cypriot Arabic", "Cypriot Maronite Arabic", "Kormakiti", "Maronite", "Sanna" ], "region": "CY", @@ -1242,6 +1352,7 @@ "full": "acy-Arab-CY", "iana": [ "Cypriot Arabic" ], "iso639_3": "acy", + "macrolang": "ar", "name": "Arabic, Cypriot Spoken", "names": [ "Cypriot Arabic", "Cypriot Maronite Arabic", "Kormakiti", "Maronite", "Sanna" ], "nophonvars": true, @@ -1257,6 +1368,7 @@ "full": "acy-Grek-CY", "iana": [ "Cypriot Arabic" ], "iso639_3": "acy", + "macrolang": "ar", "name": "Arabic, Cypriot Spoken", "names": [ "Cypriot Arabic", "Cypriot Maronite Arabic", "Kormakiti", "Maronite", "Sanna" ], "nophonvars": true, @@ -1301,7 +1413,7 @@ }, { "full": "adb-Latn-TL", - "iana": [ "Adabe" ], + "iana": [ "Atauran" ], "iso639_3": "adb", "name": "Atauran", "names": [ "Adabe", "Ataura", "Atauro", "Atauru", "Raklu Un", "Raklu-Un" ], @@ -1333,10 +1445,10 @@ "full": "ade-Latn-TG", "iana": [ "Adele" ], "iso639_3": "ade", - "localname": "Gidire", - "localnames": [ "Gidire" ], + "localname": "Gɩdɩrɛ", + "localnames": [ "Gɩdɩrɛ" ], "name": "Adele", - "names": [ "Bedere", "Bidire", "Gadre", "Gidere", "Gidire" ], + "names": [ "Badre", "Bedere", "Bedire", "Bidire", "Bɛdɩrɛ", "Gadre", "Gidere", "Gidire", "Gɩdɩrɛ" ], "region": "TG", "regionname": "Togo", "regions": [ "GH" ], @@ -1350,6 +1462,7 @@ "full": "adf-Arab-OM", "iana": [ "Dhofari Arabic" ], "iso639_3": "adf", + "macrolang": "ar", "name": "Arabic, Dhofari Spoken", "names": [ "Dhofari", "Dhofari Arabic", "Zofari" ], "region": "OM", @@ -1381,8 +1494,8 @@ "iana": [ "Adhola" ], "iso639_3": "adh", "localnames": [ "Dhopadhola" ], - "name": "Adhola", - "names": [ "Badama", "Dhopadhola", "Jopadhola", "Ludama" ], + "name": "Jopadhola", + "names": [ "Adhola", "Badama", "Dhopadhola", "Ludama" ], "region": "UG", "regionname": "Uganda", "script": "Latn", @@ -1397,7 +1510,7 @@ "iso639_3": "adi", "localnames": [ "Adi" ], "name": "Adi", - "names": [ "Abhor", "Abor", "Adi-Bokar", "Bengni-Boga’er", "Boga’er", "Boga’er Luoba", "Bokar", "Bori-Karko", "Lho-Pa", "Lhoba", "Luoba", "Luoba, Boga’er" ], + "names": [ "Abhor", "Abor", "Adi-Bokar", "Bengni-Boga’er", "Boga’er", "Boga’er Luoba", "Bokar", "Bori-Karko", "Bungni-Bogar", "Lho-Pa", "Lhoba", "Luoba", "Luoba, Boga’er", "Miri" ], "region": "IN", "regionname": "India", "regions": [ "CN" ], @@ -1405,14 +1518,14 @@ "sldr": false, "tag": "adi", "tags": [ "adi-IN", "adi-Latn" ], - "windows": "adi-Latn" + "windows": "adi-Latn-IN" }, { "full": "adi-Tibt-CN", "iana": [ "Adi" ], "iso639_3": "adi", "name": "Luoba, Boga’er", - "names": [ "Abhor", "Abor", "Adi", "Adi-Bokar", "Bengni-Boga’er", "Boga’er", "Boga’er Luoba", "Bokar", "Bori-Karko", "Lho-Pa", "Lhoba", "Luoba" ], + "names": [ "Abhor", "Abor", "Adi", "Adi-Bokar", "Bengni-Boga’er", "Boga’er", "Boga’er Luoba", "Bokar", "Bori-Karko", "Bungni-Bogar", "Lho-Pa", "Lhoba", "Luoba", "Miri" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -1421,16 +1534,16 @@ "sldr": false, "tag": "adi-Tibt", "tags": [ "adi-CN" ], - "windows": "adi-Tibt" + "windows": "adi-Tibt-CN" }, { "full": "adj-Latn-CI", "iana": [ "Adioukrou" ], "iso639_3": "adj", - "localname": "Adjukru", - "localnames": [ "Mɔjukru" ], + "localname": "Mↄjukru", + "localnames": [ "Mↄjukru" ], "name": "Adioukrou", - "names": [ "Adjukru", "Adyoukrou", "Adyukru", "Ajukru", "Mojukru", "Mɔjukru" ], + "names": [ "Adjukru", "Adyoukrou", "Adyukru", "Ajukru", "Mojukru", "Mↄjukru" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -1514,33 +1627,18 @@ "unwritten": true, "windows": "adr-Latn" }, - { - "full": "ads-Latn-GH", - "iana": [ "Adamorobe Sign Language" ], - "iso639_3": "ads", - "name": "Adamorobe Sign Language", - "names": [ "AdaSL" ], - "region": "GH", - "regionname": "Ghana", - "script": "Latn", - "sldr": false, - "tag": "ads", - "tags": [ "ads-GH", "ads-Latn", "sgn-ads", "sgn-ads-GH" ], - "unwritten": true, - "windows": "ads-Latn" - }, { "full": "ads-Zxxx-GH", "iana": [ "Adamorobe Sign Language" ], "iso639_3": "ads", "name": "Adamorobe Sign Language", "names": [ "AdaSL" ], - "nophonvars": true, "region": "GH", "regionname": "Ghana", "script": "Zxxx", "sldr": false, - "tag": "ads-Zxxx", + "tag": "ads", + "tags": [ "ads-GH", "ads-Zxxx", "sgn-ads", "sgn-ads-GH" ], "windows": "ads-Zxxx" }, { @@ -1592,7 +1690,7 @@ "iana": [ "Amdo Tibetan" ], "iso639_3": "adx", "name": "Tibetan, Amdo", - "names": [ "Amdo", "Amdo Tibetan", "Anduo", "Ngambo", "Panang" ], + "names": [ "Amdo", "Amdo Tibetan", "Anduo", "Ngamdo", "Panang" ], "region": "CN", "regionname": "China", "script": "Tibt", @@ -1616,7 +1714,7 @@ "sldr": false, "tag": "ady", "tags": [ "ady-Cyrl", "ady-RU" ], - "windows": "ady-Cyrl" + "windows": "ady-Cyrl-RU" }, { "full": "ady-Arab-RU", @@ -1632,7 +1730,7 @@ "script": "Arab", "sldr": false, "tag": "ady-Arab", - "windows": "ady-Arab" + "windows": "ady-Arab-RU" }, { "full": "ady-Latn-TR", @@ -1647,7 +1745,7 @@ "sldr": false, "tag": "ady-Latn", "tags": [ "ady-TR" ], - "windows": "ady-Latn" + "windows": "ady-Latn-TR" }, { "full": "adz-Latn-PG", @@ -1728,25 +1826,45 @@ "iana": [ "Tunisian Arabic" ], "iso639_3": "aeb", "latnnames": [ "Tounsi" ], - "localname": "Arabic, Tunisian Spoken", + "localname": "تونسي", "localnames": [ "تونسي" ], + "macrolang": "ar", "name": "Arabic, Tunisian Spoken", "names": [ "Derja", "Tunisian", "Tunisian Arabic", "Tunisian Darija" ], "region": "TN", "regionname": "Tunisia", - "regions": [ "BE", "DE", "FR", "LY", "NL" ], + "regions": [ "BE", "DE", "ES", "FR", "IL", "IT", "LY", "NL" ], "script": "Arab", "sldr": true, "tag": "aeb", - "tags": [ "aeb-Arab", "aeb-TN", "ar-aeb", "ar-aeb-TN" ], - "windows": "aeb-Arab" + "tags": [ "aeb-Arab", "aeb-TN", "ajt", "ajt-Arab", "ajt-Arab-TN", "ajt-TN", "ar-aeb", "ar-aeb-TN" ], + "windows": "aeb-Arab-TN" + }, + { + "full": "aeb-Hebr-IL", + "iana": [ "Tunisian Arabic" ], + "iso639_3": "aeb", + "macrolang": "ar", + "name": "Arabic, Tunisian Spoken", + "names": [ "Derja", "Tunisian", "Tunisian Arabic", "Tunisian Darija" ], + "nophonvars": true, + "obsolete": true, + "region": "IL", + "regionname": "Israel", + "regions": [ "ES", "IT", "TN" ], + "script": "Hebr", + "sldr": false, + "tag": "aeb-Hebr", + "tags": [ "aeb-IL", "ajt-Hebr", "ajt-Hebr-IL", "ajt-IL", "ar-aeb-IL" ], + "windows": "aeb-Hebr-IL" }, { "full": "aeb-Latn-TN", "iana": [ "Tunisian Arabic" ], "iso639_3": "aeb", - "localname": "Arabic, Tunisian Spoken", + "localname": "Derja", "localnames": [ "Derja" ], + "macrolang": "ar", "name": "Arabic, Tunisian Spoken", "names": [ "Derja", "Tunisian", "Tunisian Arabic", "Tunisian Darija" ], "region": "TN", @@ -1755,12 +1873,13 @@ "script": "Latn", "sldr": true, "tag": "aeb-Latn", - "windows": "aeb-Latn" + "windows": "aeb-Latn-TN" }, { "full": "aec-Arab-EG", "iana": [ "Saidi Arabic" ], "iso639_3": "aec", + "macrolang": "ar", "name": "Arabic, Sa’idi Spoken", "names": [ "Saidi Arabic", "Upper Egypt Arabic" ], "region": "EG", @@ -1851,6 +1970,7 @@ "iana": [ "Armenian Sign Language" ], "iso639_3": "aen", "name": "Armenian Sign Language", + "names": [ "ArSL", "Hayeren žesteri lezow", "HŽL" ], "region": "AM", "regionname": "Armenia", "script": "Zxxx", @@ -1880,7 +2000,7 @@ "localname": "Arrernte", "localnames": [ "Arrernte" ], "name": "Arrernte, Eastern", - "names": [ "Arrernte", "Arunta", "Eastern Aranda", "Eastern Arrernte", "Upper Aranda" ], + "names": [ "Arrernte", "Arunta", "Eastern Aranda", "Eastern Arrernte" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -1894,6 +2014,7 @@ "iana": [ "Alsea" ], "iso639_3": "aes", "name": "Alsea", + "names": [ "Alsea-Yaquina", "Alsean", "Alséya" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -1954,7 +2075,6 @@ "full": "aez-Latn-PG", "iana": [ "Aeka" ], "iso639_3": "aez", - "localnames": [ "Aeka" ], "name": "Aeka", "names": [ "Ajeka" ], "region": "PG", @@ -1963,6 +2083,7 @@ "sldr": false, "tag": "aez", "tags": [ "aez-Latn", "aez-PG" ], + "unwritten": true, "windows": "aez-Latn" }, { @@ -1980,7 +2101,7 @@ "suppress": true, "tag": "af", "tags": [ "af-Latn", "af-ZA" ], - "windows": "af" + "windows": "af-ZA" }, { "full": "af-Arab-ZA", @@ -1994,7 +2115,7 @@ "script": "Arab", "sldr": false, "tag": "af-Arab", - "windows": "af-Arab" + "windows": "af-Arab-ZA" }, { "full": "af-Brai-ZA", @@ -2008,7 +2129,7 @@ "script": "Brai", "sldr": false, "tag": "af-Brai", - "windows": "af-Brai" + "windows": "af-Brai-ZA" }, { "full": "af-Latn-NA", @@ -2021,9 +2142,8 @@ "regionname": "Namibia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "af-NA", - "windows": "af-NA" + "windows": "af-Latn-NA" }, { "full": "afb-Arab-KW", @@ -2031,6 +2151,7 @@ "iso639_3": "afb", "latnnames": [ "Khaliji" ], "localnames": [ "خليجي" ], + "macrolang": "ar", "name": "Arabic, Gulf Spoken", "names": [ "Bedawi", "Emirati Arabic", "Gulf Arabic", "Gulf Spoken", "Omani Bedawi Arabic", "Qatari" ], "region": "KW", @@ -2047,7 +2168,7 @@ "iana": [ "Andai" ], "iso639_3": "afd", "name": "Andai", - "names": [ "Pundungum", "Wangkai" ], + "names": [ "Anday", "Pundungum", "Wangkai" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -2104,7 +2225,7 @@ "iana": [ "Akrukay", "Chini" ], "iso639_3": "afi", "name": "Chini", - "names": [ "Akrukay", "Wɨrkŋɨ" ], + "names": [ "Akrukay", "Wɨrkŋɨ", "gwɨrkŋɨ", "ŋgɨgɨ ɨrkŋɨ" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -2119,7 +2240,7 @@ "iana": [ "Nanubae" ], "iso639_3": "afk", "name": "Nanubae", - "names": [ "Aunda", "Kapagmai" ], + "names": [ "Aunda", "Kapagmai", "Nanbay" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -2148,9 +2269,9 @@ "full": "afo-Latn-NG", "iana": [ "Eloyi" ], "iso639_3": "afo", - "localnames": [ "Eloyi" ], - "name": "Eloyi", - "names": [ "Afao", "Afo", "Afu", "Aho", "Epe", "Keffi" ], + "localnames": [ "Ajiri" ], + "name": "Ajiri", + "names": [ "Afao", "Afo", "Afu", "Aho", "Eloyi", "Epe" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -2164,6 +2285,7 @@ "iana": [ "Tapei" ], "iso639_3": "afp", "name": "Tapei", + "names": [ "Tape" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -2238,7 +2360,7 @@ "iana": [ "Aguano" ], "iso639_3": "aga", "name": "Aguano", - "names": [ "Aguanu", "Ahuano", "Awano", "Awáno", "Santa Crucino", "Uguano" ], + "names": [ "Awano", "Santa Crucino", "Uguano" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -2253,7 +2375,7 @@ "iana": [ "Legbo" ], "iso639_3": "agb", "name": "Legbo", - "names": [ "Agbo", "Gbo", "Igbo", "Imaban", "Itigidi", "Leggbo" ], + "names": [ "Agbo", "Gbo", "Igbo", "Igbo Imaban", "Imaban", "Itigidi", "Leggbo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -2353,18 +2475,18 @@ "windows": "agh-Latn" }, { - "full": "agi-Zyyy-IN", + "full": "agi-Deva-IN", "iana": [ "Agariya" ], "iso639_3": "agi", "name": "Agariya", "names": [ "Agaria", "Agharia", "Agoria" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "agi", - "tags": [ "agi-IN", "agi-Zyyy" ], - "windows": "agi-Zyyy" + "tags": [ "agi-Deva", "agi-IN" ], + "windows": "agi-Deva" }, { "full": "agj-Ethi-ET", @@ -2414,7 +2536,7 @@ "iana": [ "Fembe" ], "iso639_3": "agl", "name": "Fembe", - "names": [ "Agala", "Sinale" ], + "names": [ "Agala", "Febi", "Sinale" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -2508,7 +2630,7 @@ "full": "ags-Latn-CM", "iana": [ "Esimbi" ], "iso639_3": "ags", - "localnames": [ "Esimbi" ], + "localnames": [ "Eshimbi" ], "name": "Esimbi", "names": [ "Aage", "Age", "Bogen", "Bogue", "Eshimbi", "Essimbi", "Isimbi", "Mburugam", "Simpi" ], "region": "CM", @@ -2543,7 +2665,7 @@ "localname": "Qa'yol", "localnames": [ "Qa'yol" ], "name": "Awakateko", - "names": [ "Aguacatec", "Aguacateco", "Qa’yol" ], + "names": [ "Aguacatec", "Aguacateco", "Kayol", "Qa’yol" ], "region": "GT", "regionname": "Guatemala", "script": "Latn", @@ -2557,7 +2679,7 @@ "iana": [ "Remontado Dumagat" ], "iso639_3": "agv", "name": "Dumagat, Remontado", - "names": [ "Hatang-Kayey", "Remontado Agta", "Remontado Dumagat", "Sinauna", "Sinauna Tagalog" ], + "names": [ "Hatang-Kayey", "Hatang-Kayi", "Remontado Agta", "Remontado Dumagat", "Sinauna", "Sinauna Tagalog" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -2601,9 +2723,9 @@ "full": "agy-Latn-PH", "iana": [ "Southern Alta" ], "iso639_3": "agy", - "localnames": [ "Alta" ], + "localnames": [ "Kabulowan" ], "name": "Alta, Southern", - "names": [ "Alta", "Ita", "Kaboloan", "Kabulowan", "Kabuluen", "Kabuluwan", "Kabuluwen", "Southern Alta" ], + "names": [ "Ita", "Kaboloan", "Kabulowan", "Kabuluen", "Kabuluwan", "Kabuluwen", "Southern Alta" ], "obsolete": true, "region": "PH", "regionname": "Philippines", @@ -2649,7 +2771,7 @@ "iana": [ "Axamb" ], "iso639_3": "ahb", "name": "Axamb", - "names": [ "Ahamb" ], + "names": [ "Ahamb", "Naujin sdrato" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -2717,25 +2839,7 @@ "sldr": false, "tag": "ahk", "tags": [ "ahk-Latn", "ahk-MM" ], - "windows": "ahk-Latn" - }, - { - "full": "ahk-Latn-LA-x-louma", - "iana": [ "Louma" ], - "iso639_3": "ahk", - "latnnames": [ "Avkavdawv" ], - "localnames": [ "A˯ka˯daw˯" ], - "name": "Akha", - "names": [ "Ahka", "Aini", "Aka", "Ak’a", "Ikor", "Khao Ikor", "Louma", "Yani" ], - "region": "LA", - "regionname": "Lao People's Democratic Republic", - "regions": [ "CN", "MM", "TH", "VN" ], - "script": "Latn", - "sldr": false, - "tag": "ahk-Latn-x-louma", - "tags": [ "ahk-LA-x-louma" ], - "unwritten": true, - "windows": "ahk-Latn-x-louma" + "windows": "ahk-Latn-MM" }, { "full": "ahk-Mymr-MM", @@ -2750,7 +2854,7 @@ "script": "Mymr", "sldr": false, "tag": "ahk-Mymr", - "windows": "ahk-Mymr" + "windows": "ahk-Mymr-MM" }, { "full": "ahk-Latn-TH", @@ -2781,7 +2885,25 @@ "script": "Thai", "sldr": false, "tag": "ahk-Thai", - "windows": "ahk-Thai" + "windows": "ahk-Thai-TH" + }, + { + "full": "ahk-Latn-LA-x-louma", + "iana": [ "Louma" ], + "iso639_3": "ahk", + "latnnames": [ "Avkavdawv" ], + "localnames": [ "A˯ka˯daw˯" ], + "name": "Akha", + "names": [ "Ahka", "Aini", "Aka", "Ak’a", "Ikor", "Khao Ikor", "Louma", "Yani" ], + "region": "LA", + "regionname": "Lao People's Democratic Republic", + "regions": [ "CN", "MM", "TH", "VN" ], + "script": "Latn", + "sldr": false, + "tag": "ahk-x-louma", + "tags": [ "ahk-LA-x-louma", "ahk-Latn-x-louma" ], + "unwritten": true, + "windows": "ahk-Latn-LA-x-louma" }, { "full": "ahl-Latn-TG", @@ -2859,27 +2981,29 @@ "windows": "ahp-Latn" }, { - "full": "ahr-Zyyy-IN", + "full": "ahr-Deva-IN", "iana": [ "Ahirani" ], "iso639_3": "ahr", + "latnnames": [ "Ahirani" ], + "localnames": [ "अहिराणी" ], "name": "Ahirani", "names": [ "Ahiri" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "ahr", - "tags": [ "ahr-IN", "ahr-Zyyy" ], + "tags": [ "ahr-Deva", "ahr-IN" ], "unwritten": true, - "windows": "ahr-Zyyy" + "windows": "ahr-Deva" }, { "full": "ahs-Latn-NG", "iana": [ "Ashe" ], "iso639_3": "ahs", - "localnames": [ "Ishe" ], + "localnames": [ "Ishɛ" ], "name": "Ashe", - "names": [ "Ache", "Ala", "Ishe", "Koro Makama", "Koron Ache", "Koron Ala", "Ìzɛ" ], + "names": [ "Ache", "Ala", "Ishe", "Ishɛ", "Koro Makama", "Koron Ache", "Koron Ala", "Ìzɛ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -2893,7 +3017,7 @@ "iana": [ "Ahtena" ], "iso639_3": "aht", "name": "Ahtena", - "names": [ "Ahtna", "Atna", "Atnakenaege’", "Copper River", "Mednovskiy" ], + "names": [ "Ahtna", "Atna", "Atnakenaege’", "Copper River", "Koht’aene Kenaege’", "Mednovskiy" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -2919,18 +3043,18 @@ "windows": "aia-Latn" }, { - "full": "aib-Zyyy-CN", + "full": "aib-Arab-CN", "iana": [ "Ainu (China)" ], "iso639_3": "aib", "name": "Ainu", "names": [ "Aini", "Ainu (China)", "Aynu", "Ejnu", "Eynu", "Šäyxil" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "aib", - "tags": [ "aib-CN", "aib-Zyyy" ], - "windows": "aib-Zyyy" + "tags": [ "aib-Arab", "aib-CN" ], + "windows": "aib-Arab" }, { "full": "aic-Latn-PG", @@ -2973,6 +3097,7 @@ "sldr": false, "tag": "aie", "tags": [ "aie-Latn", "aie-PG" ], + "unwritten": true, "windows": "aie-Latn" }, { @@ -2994,8 +3119,8 @@ "iana": [ "Antigua and Barbuda Creole English" ], "iso639_3": "aig", "localnames": [ "Creole" ], - "name": "Antigua and Barbuda Creole English", - "names": [ "Anguillan Creole English", "Broken English", "Creole", "Dialect", "Kittitian Creole English", "Kokoy Creole English", "Leeward Caribbean Creole", "Leeward Caribbean Creole English", "Montserrat Creole English", "Patwa", "Saint Kitts Creole English", "Simaatn Creole English", "Sint Maarten Creole English" ], + "name": "Leeward Caribbean English Creole", + "names": [ "Anguillan Creole English", "Antigua and Barbuda Creole English", "Broken English", "Creole", "Dialect", "Kittitian Creole English", "Kokoy Creole English", "Montserrat Creole English", "Patwa", "Saint Kitts Creole English", "Simaatn Creole English", "Sint Maarten Creole English" ], "region": "AG", "regionname": "Antigua and Barbuda", "regions": [ "AI", "DM", "GB", "KN", "MF", "MS", "SX" ], @@ -3020,10 +3145,30 @@ "unwritten": true, "windows": "aih-Zyyy" }, + { + "full": "aii-Syrc-IQ", + "iana": [ "Assyrian Neo-Aramaic" ], + "iso639_3": "aii", + "latnnames": [ "Ātūrāyā", "Sūrët" ], + "localname": "ܐܬܘܪܝܐ", + "localnames": [ "ܐܬܘܪܝܐ", "ܣܘܪܬ" ], + "macrolang": "syr", + "name": "Assyrian Neo-Aramaic", + "names": [ "Aisorski", "Asoreren", "Assyrian", "Assyrianci", "Assyriski", "Aturaya Swadaya", "Lishana Aturaya", "Neo-Syriac", "Sooreth", "Suret", "Sureth", "Suryaya Swadaya", "Swadai", "Swadaya" ], + "region": "IQ", + "regionname": "Iraq", + "regions": [ "AM", "AT", "AU", "AZ", "BE", "BR", "CA", "CH", "CY", "DE", "GB", "GE", "GR", "IR", "IT", "LB", "NL", "NZ", "RU", "SE", "SY", "TR", "UA", "US" ], + "script": "Syrc", + "sldr": true, + "tag": "aii", + "tags": [ "aii-IQ", "aii-Syrc" ], + "windows": "aii-Syrc-IQ" + }, { "full": "aii-Cyrl-RU", "iana": [ "Assyrian Neo-Aramaic" ], "iso639_3": "aii", + "macrolang": "syr", "name": "Assyrian Neo-Aramaic", "nophonvars": true, "region": "RU", @@ -3032,26 +3177,23 @@ "sldr": false, "tag": "aii-Cyrl", "tags": [ "aii-RU" ], - "windows": "aii-Cyrl" + "windows": "aii-Cyrl-RU" }, { - "full": "aii-Syrc-IQ", + "full": "aii-Elym-IQ", "iana": [ "Assyrian Neo-Aramaic" ], "iso639_3": "aii", - "latnnames": [ "Ātūrāyā", "Sūrët" ], - "localname": "ܠܫܢܐ ܣܘܪܝܝܐ", - "localnames": [ "ܐܬܘܪܝܐ", "ܣܘܪܬ" ], + "macrolang": "syr", "name": "Assyrian Neo-Aramaic", - "names": [ "Aisorski", "Assyrian", "Assyrianci", "Assyriski", "Aturaya Swadaya", "Lishana Aturaya", "Neo-Syriac", "Sooreth", "Suret", "Sureth", "Suryaya Swadaya", "Swadai", "Swadaya" ], + "names": [ "Aisorski", "Asoreren", "Assyrian", "Assyrianci", "Assyriski", "Aturaya Swadaya", "Lishana Aturaya", "Neo-Syriac", "Sooreth", "Suret", "Sureth", "Suryaya Swadaya", "Swadai", "Swadaya" ], "nophonvars": true, "region": "IQ", "regionname": "Iraq", "regions": [ "AM", "AT", "AU", "AZ", "BE", "BR", "CA", "CH", "CY", "DE", "GB", "GE", "GR", "IR", "IT", "LB", "NL", "NZ", "RU", "SE", "SY", "TR", "UA", "US" ], - "script": "Syrc", - "sldr": true, - "tag": "aii-Syrc", - "tags": [ "aii-IQ" ], - "windows": "aii-Syrc" + "script": "Elym", + "sldr": false, + "tag": "aii-Elym", + "windows": "aii-Elym-IQ" }, { "full": "aij-Hebr-IL", @@ -3073,8 +3215,8 @@ "full": "aik-Latn-NG", "iana": [ "Ake" ], "iso639_3": "aik", - "name": "Ake", - "names": [ "Aike", "Akye", "Kapon" ], + "name": "Akye", + "names": [ "Aike", "Ake", "Ekwenunga", "Kapon" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -3088,15 +3230,14 @@ "full": "ail-Latn-PG", "iana": [ "Aimele" ], "iso639_3": "ail", - "name": "Aimele", - "names": [ "Eibela", "Kware" ], + "name": "Eibela", + "names": [ "Aimele", "Kware" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "ail", "tags": [ "ail-Latn", "ail-PG" ], - "unwritten": true, "windows": "ail-Latn" }, { @@ -3180,12 +3321,11 @@ "names": [ "Barbari", "Berberi", "Chahar-Aimaq", "Char Aimaq" ], "region": "AF", "regionname": "Afghanistan", - "regions": [ "IR", "TJ" ], + "regions": [ "IR", "TJ", "TM" ], "script": "Arab", "sldr": false, "tag": "aiq", "tags": [ "aiq-AF", "aiq-Arab" ], - "unwritten": true, "windows": "aiq-Arab" }, { @@ -3218,6 +3358,19 @@ "unwritten": true, "windows": "ait-Latn" }, + { + "full": "aiw-Latn-ET", + "iana": [ "Aari" ], + "iso639_3": "aiw", + "name": "Aari", + "region": "ET", + "regionname": "Ethiopia", + "script": "Latn", + "sldr": false, + "tag": "aiw", + "tags": [ "aiw-ET", "aiw-Latn" ], + "windows": "aiw-Latn" + }, { "full": "aiw-Arab-ET", "iana": [ "Aari" ], @@ -3235,28 +3388,18 @@ "full": "aiw-Ethi-ET", "iana": [ "Aari" ], "iso639_3": "aiw", + "latnnames": [ "Áfan ārí" ], + "localname": "አፋን፡ኣሪ፡", + "localnames": [ "አፋን፡ኣሪ፡" ], "name": "Aari", "nophonvars": true, "region": "ET", "regionname": "Ethiopia", "script": "Ethi", - "sldr": false, + "sldr": true, "tag": "aiw-Ethi", "windows": "aiw-Ethi" }, - { - "full": "aiw-Latn-ET", - "iana": [ "Aari" ], - "iso639_3": "aiw", - "localnames": [ "Aari" ], - "name": "Aari", - "region": "ET", - "regionname": "Ethiopia", - "script": "Latn", - "sldr": false, - "tag": "aiw-Latn", - "windows": "aiw-Latn" - }, { "full": "aix-Latn-PG", "iana": [ "Aighon" ], @@ -3276,6 +3419,7 @@ "iana": [ "Ali" ], "iso639_3": "aiy", "name": "Ali", + "names": [ "’Àlī" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -3348,69 +3492,19 @@ "windows": "ajn-Latn" }, { - "full": "ajp-Arab-JO", - "iana": [ "South Levantine Arabic" ], - "iso639_3": "ajp", - "latnnames": [ "Urduni" ], - "localnames": [ "أردني" ], - "name": "Arabic, South Levantine Spoken", - "names": [ "Levantine", "Levantine Arabic", "Palestanian-Jordanian Arabic", "Palestinian Arabic", "Palestinian-Jordanian", "South Levantine Arabic" ], - "region": "JO", - "regionname": "Jordan", - "regions": [ "AE", "AR", "DE", "IL", "KW", "LY", "PS", "QA", "SA", "SY" ], - "script": "Arab", - "sldr": false, - "tag": "ajp", - "tags": [ "ajp-Arab", "ajp-JO", "ar-ajp", "ar-ajp-JO" ], - "windows": "ajp-Arab" - }, - { - "full": "ajt-Arab-TN", - "iana": [ "Judeo-Tunisian Arabic" ], - "iso639_3": "ajt", - "name": "Arabic, Judeo-Tunisian", - "names": [ "Djerbian Arabic", "Judeo-Tunisian Arabic" ], - "region": "TN", - "regionname": "Tunisia", - "regions": [ "ES", "IL", "IT" ], - "script": "Arab", - "sldr": false, - "tag": "ajt", - "tags": [ "ajt-Arab", "ajt-TN" ], - "windows": "ajt-Arab" - }, - { - "full": "ajt-Hebr-IL", - "iana": [ "Judeo-Tunisian Arabic" ], - "iso639_3": "ajt", - "name": "Arabic, Judeo-Tunisian", - "names": [ "Djerbian Arabic", "Judeo-Tunisian Arabic" ], - "nophonvars": true, - "obsolete": true, + "full": "ajs-Zxxx-IL", + "iana": [ "Algerian Jewish Sign Language" ], + "iso639_3": "ajs", + "name": "Algerian Jewish Sign Language", + "names": [ "AJSL", "Ghardaia Sign Language" ], "region": "IL", "regionname": "Israel", - "regions": [ "ES", "IT", "TN" ], - "script": "Hebr", - "sldr": false, - "tag": "ajt-Hebr", - "tags": [ "ajt-IL" ], - "windows": "ajt-Hebr" - }, - { - "full": "aju-Arab-MA", - "iana": [ "Judeo-Moroccan Arabic" ], - "iso639_3": "aju", - "name": "Arabic, Judeo-Moroccan", - "names": [ "Judeo-Moroccan Arabic", "il‘arabiyya dyalna" ], - "nophonvars": true, - "region": "MA", - "regionname": "Morocco", - "regions": [ "CA", "IL" ], - "script": "Arab", + "regions": [ "DZ", "FR" ], + "script": "Zxxx", "sldr": false, - "tag": "aju-Arab", - "tags": [ "aju-MA" ], - "windows": "aju-Arab" + "tag": "ajs", + "tags": [ "ajs-IL", "ajs-Zxxx", "sgn-ajs", "sgn-ajs-IL" ], + "windows": "ajs-Zxxx" }, { "full": "ajw-Latn-NG", @@ -3456,6 +3550,7 @@ "sldr": true, "tag": "ak", "tags": [ "ak-GH", "ak-Latn", "fat", "fat-GH", "fat-Latn", "fat-Latn-GH", "tw", "tw-GH", "tw-Latn", "tw-Latn-GH" ], + "variants": [ "akuapem", "asante" ], "windows": "ak-Latn" }, { @@ -3597,7 +3692,7 @@ "iana": [ "Aiome" ], "iso639_3": "aki", "name": "Aiome", - "names": [ "Ayom" ], + "names": [ "Aren", "Ayom" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -3612,7 +3707,7 @@ "iana": [ "Aka-Jeru" ], "iso639_3": "akj", "name": "Aka-Jeru", - "names": [ "Jeru", "Yerawa" ], + "names": [ "Jero", "Jeru", "Yerawa" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -3773,7 +3868,7 @@ "iana": [ "Akhvakh" ], "iso639_3": "akv", "name": "Akhvakh", - "names": [ "Ashvado", "Axvax", "Ghahvalal", "’Aqwalazul" ], + "names": [ "Ashvado", "Axvax", "Ašwałi", "Ašwałi mic̣i", "Ghahvalal", "’Aqwalazul" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "AZ" ], @@ -3923,6 +4018,7 @@ "full": "alf-Latn-NG", "iana": [ "Alege" ], "iso639_3": "alf", + "localnames": [ "Elege" ], "name": "Elege", "names": [ "Alege", "Alegi", "Ugbe", "Uge" ], "region": "NG", @@ -3931,7 +4027,6 @@ "sldr": false, "tag": "alf", "tags": [ "alf-Latn", "alf-NG" ], - "unwritten": true, "windows": "alf-Latn" }, { @@ -3954,6 +4049,7 @@ "iana": [ "Amaimon" ], "iso639_3": "ali", "name": "Amaimon", + "names": [ "Okim" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -3976,19 +4072,19 @@ "windows": "alj-Latn" }, { - "full": "alk-Zyyy-LA", + "full": "alk-Laoo-LA", "iana": [ "Alak" ], "iso639_3": "alk", "name": "Alak", "names": [ "Halak", "Harak", "Harlak", "Hrlak" ], "region": "LA", "regionname": "Lao People's Democratic Republic", - "script": "Zyyy", + "script": "Laoo", "sldr": false, "tag": "alk", - "tags": [ "alk-LA", "alk-Zyyy" ], + "tags": [ "alk-LA", "alk-Laoo" ], "unwritten": true, - "windows": "alk-Zyyy" + "windows": "alk-Laoo" }, { "full": "all-Mlym-IN", @@ -4009,6 +4105,7 @@ "iana": [ "Amblong" ], "iso639_3": "alm", "name": "Amblong", + "names": [ "Aje", "Varavara" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -4023,6 +4120,7 @@ "iana": [ "Gheg Albanian" ], "iso639_3": "aln", "localnames": [ "Gegnisht" ], + "macrolang": "sq", "name": "Albanian, Gheg", "names": [ "Geg", "Gegnisht", "Gegnisht-Shqyp", "Gheg", "Gheg Albanian", "Guegue", "Shopni", "Shqip", "Shqyp" ], "region": "XK", @@ -4031,12 +4129,13 @@ "sldr": false, "tag": "aln", "tags": [ "aln-Latn", "aln-XK" ], - "windows": "aln-Latn" + "windows": "aln-Latn-XK" }, { "full": "aln-Brai-AL", "iana": [ "Gheg Albanian" ], "iso639_3": "aln", + "macrolang": "sq", "name": "Albanian, Gheg", "names": [ "Geg", "Gegnisht", "Gegnisht-Shqyp", "Gheg", "Gheg Albanian", "Guegue", "Shopni", "Shqip", "Shqyp" ], "nophonvars": true, @@ -4047,7 +4146,7 @@ "sldr": false, "tag": "aln-Brai", "tags": [ "aln-AL" ], - "windows": "aln-Brai" + "windows": "aln-Brai-AL" }, { "full": "alo-Latn-ID", @@ -4082,7 +4181,7 @@ "full": "alq-Latn-CA", "iana": [ "Algonquin" ], "iso639_3": "alq", - "localname": "Anicinabemowin", + "localname": "Anicinàbemowin", "localnames": [ "Anicinàbemowin" ], "name": "Algonquin", "names": [ "Algonkin", "Anicinàbemowin" ], @@ -4125,6 +4224,21 @@ "tags": [ "alt-Cyrl", "alt-RU" ], "windows": "alt-Cyrl" }, + { + "full": "alt-Cyrs-RU", + "iana": [ "Southern Altai" ], + "iso639_3": "alt", + "name": "Altai, Southern", + "names": [ "Altai", "Oirot", "Oyrot", "Southern Altai" ], + "nophonvars": true, + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "script": "Cyrs", + "sldr": false, + "tag": "alt-Cyrs", + "windows": "alt-Cyrs" + }, { "full": "alt-Mong-RU", "iana": [ "Southern Altai" ], @@ -4191,7 +4305,7 @@ "region": "AU", "regionname": "Australia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "aly", "tags": [ "aly-AU", "aly-Latn" ], "windows": "aly-Latn" @@ -4200,9 +4314,9 @@ "full": "alz-Latn-CD", "iana": [ "Alur" ], "iso639_3": "alz", - "localnames": [ "DhuAlur" ], + "localnames": [ "Dho Alur" ], "name": "Alur", - "names": [ "Aloro", "Alua", "Alulu", "Dho Alur", "DhuAlur", "Jo Alur", "Lur", "Luri" ], + "names": [ "Aloro", "Alua", "Alulu", "Dho Alur", "DhuAlur", "Dhʉ Alur", "Jo Alur", "Lur", "Luri" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "UG" ], @@ -4311,10 +4425,10 @@ "full": "ame-Latn-PE", "iana": [ "Yanesha'" ], "iso639_3": "ame", - "localname": "Yanesha’", - "localnames": [ "Yanesha'" ], + "localname": "Yaneshaʼ", + "localnames": [ "Yaneshac̈h" ], "name": "Yanesha’", - "names": [ "Amagues", "Amagé", "Amajé", "Amajó", "Amoishe", "Amueixa", "Amuese", "Amuesha", "Amueshua", "Amuetamo", "Lorenzo", "Omagé", "Yanesha'" ], + "names": [ "Amagues", "Amagé", "Amajé", "Amajó", "Amoishe", "Amueixa", "Amuese", "Amuesha", "Amueshua", "Amuetamo", "Lorenzo", "Omagé", "Yanesha'", "Yaneshac̈h", "Yanešač̣" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -4324,29 +4438,30 @@ "windows": "ame-Latn" }, { - "full": "amf-Ethi-ET", + "full": "amf-Latn-ET", "iana": [ "Hamer-Banna" ], "iso639_3": "amf", "name": "Hamer-Banna", - "nophonvars": true, "region": "ET", "regionname": "Ethiopia", - "script": "Ethi", + "script": "Latn", "sldr": false, - "tag": "amf-Ethi", - "windows": "amf-Ethi" + "tag": "amf", + "tags": [ "amf-ET", "amf-Latn" ], + "windows": "amf-Latn" }, { - "full": "amf-Latn-ET", + "full": "amf-Ethi-ET", "iana": [ "Hamer-Banna" ], "iso639_3": "amf", "name": "Hamer-Banna", + "nophonvars": true, "region": "ET", "regionname": "Ethiopia", - "script": "Latn", + "script": "Ethi", "sldr": false, - "tag": "amf-Latn", - "windows": "amf-Latn" + "tag": "amf-Ethi", + "windows": "amf-Ethi" }, { "full": "amg-Latn-AU", @@ -4375,22 +4490,9 @@ "script": "Latn", "sldr": false, "tag": "ami", - "tags": [ "ami-Latn", "ami-TW" ], + "tags": [ "ais", "ais-Latn", "ais-Latn-TW", "ais-TW", "ami-Latn", "ami-TW" ], "windows": "ami-Latn" }, - { - "full": "ami-Zyyy-TW", - "iana": [ "Amis" ], - "name": "Amis", - "names": [ "Ami", "Amia", "Bakurut", "Lam-Si-Hoan", "Maran", "Pagcah", "Pangcah", "Pangtsah", "Sabari", "Tanah" ], - "region": "TW", - "regionname": "China-Taiwan", - "script": "Zyyy", - "sldr": false, - "tag": "ami-Zyyy-TW", - "tags": [ "ais", "ais-TW", "ais-Zyyy", "ais-Zyyy-TW" ], - "windows": "ami-Zyyy-TW" - }, { "full": "amj-Latn-TD", "iana": [ "Amdang" ], @@ -4440,10 +4542,10 @@ "full": "amm-Latn-PG", "iana": [ "Ama (Papua New Guinea)" ], "iso639_3": "amm", - "localname": "Ama", + "localname": "Sawiyanu", "localnames": [ "Sawiyanu" ], - "name": "Ama", - "names": [ "Ama (Papua New Guinea)", "Sawiyanu" ], + "name": "Sawiyanu", + "names": [ "Ama", "Ama (Papua New Guinea)", "Sawiyano" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -4518,7 +4620,7 @@ "full": "amr-Latn-PE", "iana": [ "Amarakaeri" ], "iso639_3": "amr", - "localname": "aratbuten hua'a", + "localname": "aratbuten huaʼa", "localnames": [ "aratbuten hua'a" ], "name": "Amarakaeri", "names": [ "Amaracaire", "Amarakaire", "Arakmbut", "Arakmbut Háte", "Harakmbut", "Haté", "Kareneri", "Kochimberi", "Küpondirideri", "Wakitaneri", "Wintaperi", "aratbuten hua’a" ], @@ -4531,18 +4633,18 @@ "windows": "amr-Latn" }, { - "full": "ams-Zyyy-JP", + "full": "ams-Jpan-JP", "iana": [ "Southern Amami-Oshima" ], "iso639_3": "ams", "name": "Amami-Oshima, Southern", "names": [ "Southern Amami-Oshima", "Southern Amami-Osima" ], "region": "JP", "regionname": "Japan", - "script": "Zyyy", + "script": "Jpan", "sldr": false, "tag": "ams", - "tags": [ "ams-JP", "ams-Zyyy" ], - "windows": "ams-Zyyy" + "tags": [ "ams-JP", "ams-Jpan" ], + "windows": "ams-Jpan" }, { "full": "amt-Latn-PG", @@ -4555,14 +4657,13 @@ "sldr": false, "tag": "amt", "tags": [ "amt-Latn", "amt-PG" ], - "unwritten": true, "windows": "amt-Latn" }, { "full": "amu-Latn-MX", "iana": [ "Guerrero Amuzgo" ], "iso639_3": "amu", - "localname": "Amuzgo", + "localname": "Ñomndaa", "localnames": [ "Ñomndaa" ], "name": "Amuzgo, Guerrero", "names": [ "Amuzgo de Guerrero", "Guerrero Amuzgo", "Jñom’ndaa", "Nomndaa", "Ñomndaa", "Ñonda" ], @@ -4589,6 +4690,20 @@ "unwritten": true, "windows": "amv-Latn" }, + { + "full": "amw-Syrc-SY", + "iana": [ "Western Neo-Aramaic" ], + "iso639_3": "amw", + "name": "Western Neo-Aramaic", + "names": [ "Loghtha Siryanooytha", "Maaloula", "Maalula", "Neo-Western Aramaic", "Siryoon" ], + "region": "SY", + "regionname": "Syrian Arab Republic", + "script": "Syrc", + "sldr": false, + "tag": "amw", + "tags": [ "amw-SY", "amw-Syrc" ], + "windows": "amw-Syrc" + }, { "full": "amw-Arab-SY", "iana": [ "Western Neo-Aramaic" ], @@ -4654,7 +4769,7 @@ "region": "AU", "regionname": "Australia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "amx", "tags": [ "amx-AU", "amx-Latn" ], "windows": "amx-Latn" @@ -4694,7 +4809,7 @@ "iso639_3": "arg", "localnames": [ "Aragonés" ], "name": "Aragonese", - "names": [ "Altoaragonés", "Aragoieraz", "Aragonés", "Fabla Aragonesa", "High Aragonese", "Patués" ], + "names": [ "Altoaragonés", "Aragoieraz", "Aragonais", "Aragonès", "Aragonés", "Fabla", "Fabla Aragonesa", "High Aragonese", "Navarroaragonés", "Patués" ], "region": "ES", "regionname": "Spain", "script": "Latn", @@ -4773,7 +4888,6 @@ "sldr": false, "tag": "ane", "tags": [ "ane-Latn", "ane-NC" ], - "unwritten": true, "windows": "ane-Latn" }, { @@ -4803,12 +4917,27 @@ "sldr": false, "tag": "ang", "tags": [ "ang-GB", "ang-Latn" ], - "windows": "ang-Latn" + "windows": "ang-Latn-GB" + }, + { + "full": "ang-Runr-001", + "iana": [ "Old English (ca. 450-1100)" ], + "iso639_3": "ang", + "name": "Old English (ca. 450-1100)", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "ang-Runr", + "tags": [ "ang-001" ], + "windows": "ang-Runr-001" }, { "full": "anh-Latn-PG", "iana": [ "Nend" ], "iso639_3": "anh", + "localnames": [ "Nend" ], "name": "Nend", "names": [ "Angaua", "Nent" ], "region": "PG", @@ -4853,8 +4982,9 @@ "full": "ank-Latn-NG", "iana": [ "Goemai" ], "iso639_3": "ank", + "localnames": [ "Ga̱mai" ], "name": "Goemai", - "names": [ "Ankwai", "Ankwe", "Ankwei", "Gamai", "Kemai" ], + "names": [ "Ankwai", "Ankwe", "Ankwei", "Gamai", "Ga̱mai", "Kemai" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -4867,7 +4997,7 @@ "full": "anl-Latn-MM", "iana": [ "Anu-Hkongso Chin" ], "iso639_3": "anl", - "name": "Chin, Anu-Hkongso", + "name": "Chin, Anu-Khongso", "names": [ "Anu-Hkongso Chin", "Kaungso", "Kaungtso", "Khaungtso", "Khawngso", "Khong", "Khongso", "No" ], "region": "MM", "regionname": "Myanmar", @@ -4882,7 +5012,7 @@ "iana": [ "Anal" ], "iso639_3": "anm", "name": "Anal", - "names": [ "Namfau" ], + "names": [ "Anal Naga", "Anāl", "Namfau", "Pakan", "Pakan Naga" ], "region": "IN", "regionname": "India", "regions": [ "MM" ], @@ -4941,19 +5071,33 @@ "windows": "anp-Deva" }, { - "full": "anq-Zyyy-IN", + "full": "anq-Deva-IN", "iana": [ "Jarawa (India)" ], "iso639_3": "anq", "name": "Jarawa", "names": [ "Jarawa (India)" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "anq", - "tags": [ "anq-IN", "anq-Zyyy" ], + "tags": [ "anq-Deva", "anq-IN" ], "unwritten": true, - "windows": "anq-Zyyy" + "windows": "anq-Deva" + }, + { + "full": "anq-Latn-IN", + "iana": [ "Jarawa (India)" ], + "iso639_3": "anq", + "name": "Jarawa", + "names": [ "Jarawa (India)" ], + "region": "IN", + "regionname": "India", + "script": "Latn", + "sldr": false, + "tag": "anq-Latn", + "unwritten": true, + "windows": "anq-Latn" }, { "full": "anr-Deva-IN", @@ -5004,7 +5148,7 @@ "iana": [ "Anuak" ], "iso639_3": "anu", "name": "Anuak", - "names": [ "Dha Anywak", "Dho Anywaa", "dho-Anywaa" ], + "names": [ "Dha Anyuaa", "Dha Anywak", "Dho Anywaa", "dho-Anywaa" ], "region": "ET", "regionname": "Ethiopia", "regions": [ "SS" ], @@ -5012,14 +5156,14 @@ "sldr": false, "tag": "anu", "tags": [ "anu-ET", "anu-Ethi" ], - "windows": "anu-Ethi" + "windows": "anu-Ethi-ET" }, { "full": "anu-Arab-SS", "iana": [ "Anuak" ], "iso639_3": "anu", "name": "Anuak", - "names": [ "Anyuak", "Anywa", "Anywaa", "Anywak", "Dha Anywaa", "Dha Anywak", "Dho Anywaa", "Jambo", "Nuro", "dho-Anywaa" ], + "names": [ "Anyuak", "Anywa", "Anywaa", "Anywak", "Dha Anyuaa", "Dha Anywaa", "Dha Anywak", "Dho Anywaa", "Jambo", "Nuro", "dho-Anywaa" ], "nophonvars": true, "region": "SS", "regionname": "South Sudan", @@ -5027,7 +5171,7 @@ "script": "Arab", "sldr": false, "tag": "anu-Arab", - "windows": "anu-Arab" + "windows": "anu-Arab-SS" }, { "full": "anu-Latn-SS", @@ -5035,14 +5179,14 @@ "iso639_3": "anu", "localnames": [ "Dha Anywaa" ], "name": "Anuak", - "names": [ "Anyuak", "Anywa", "Anywaa", "Anywak", "Dha Anywaa", "Dha Anywak", "Dho Anywaa", "Jambo", "Nuro", "dho-Anywaa" ], + "names": [ "Anyuak", "Anywa", "Anywaa", "Anywak", "Dha Anyuaa", "Dha Anywaa", "Dha Anywak", "Dho Anywaa", "Jambo", "Nuro", "dho-Anywaa" ], "region": "SS", "regionname": "South Sudan", "regions": [ "ET" ], "script": "Latn", "sldr": false, "tag": "anu-Latn", - "windows": "anu-Latn" + "windows": "anu-Latn-SS" }, { "full": "anv-Latn-CM", @@ -5094,7 +5238,7 @@ "full": "any-Latn-CI", "iana": [ "Anyin" ], "iso639_3": "any", - "localname": "Agni Sanvi", + "localname": "Anyin", "localnames": [ "Anyin" ], "name": "Anyin", "names": [ "Agni", "Agni Sanvi", "Agny", "Anyi" ], @@ -5124,6 +5268,7 @@ "full": "aoa-Latn-ST", "iana": [ "Angolar" ], "iso639_3": "aoa", + "localnames": [ "Ngola" ], "name": "Angolar", "names": [ "Ngola" ], "region": "ST", @@ -5213,7 +5358,7 @@ "iana": [ "Angoram" ], "iso639_3": "aog", "name": "Angoram", - "names": [ "Olem", "Pondo", "Tjimundo" ], + "names": [ "Kanda", "Olem", "Pondo", "Tjimundo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -5275,7 +5420,7 @@ "regionname": "Papua New Guinea", "rod": "01158", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "aoj-x-balif", "tags": [ "aoj-Latn-x-balif", "aoj-PG-x-balif" ], "windows": "aoj-Latn-x-balif" @@ -5290,7 +5435,7 @@ "regionname": "Papua New Guinea", "rod": "20757", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "aoj-x-filifita", "tags": [ "aoj-Latn-x-filifita", "aoj-PG-x-filifita" ], "windows": "aoj-Latn-x-filifita" @@ -5347,8 +5492,8 @@ "iso639_3": "aon", "localname": "Weri", "localnames": [ "Weri" ], - "name": "Arapesh, Bumbita", - "names": [ "Bambita Arapesh", "Bumbita Arapesh", "But Arapesh", "Weri" ], + "name": "Weri", + "names": [ "Bambita Arapesh", "Bumbita Arapesh", "But Arapesh" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -5376,7 +5521,7 @@ "iana": [ "Taikat" ], "iso639_3": "aos", "name": "Taikat", - "names": [ "Arso", "Tajkat" ], + "names": [ "Abrab", "Abrap", "Arso", "Tajkat" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -5387,7 +5532,22 @@ "windows": "aos-Latn" }, { - "full": "aot-Zyyy-IN", + "full": "aot-Beng-BD", + "iana": [ "Atong (India)", "A'tong" ], + "iso639_3": "aot", + "name": "Atong", + "names": [ "A'tong", "Atong (India)", "Attong", "A’tong" ], + "region": "BD", + "regionname": "Bangladesh", + "regions": [ "IN" ], + "script": "Beng", + "sldr": false, + "tag": "aot", + "tags": [ "aot-BD", "aot-Beng" ], + "windows": "aot-Beng-BD" + }, + { + "full": "aot-Latn-IN", "iana": [ "Atong (India)", "A'tong" ], "iso639_3": "aot", "name": "Atong", @@ -5395,19 +5555,18 @@ "region": "IN", "regionname": "India", "regions": [ "BD" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, - "tag": "aot", - "tags": [ "aot-IN", "aot-Zyyy" ], - "unwritten": true, - "windows": "aot-Zyyy" + "tag": "aot-Latn", + "tags": [ "aot-IN" ], + "windows": "aot-Latn-IN" }, { "full": "aou-Zyyy-CN", "iana": [ "A'ou" ], "iso639_3": "aou", "name": "A’ou", - "names": [ "A'ou", "Ayo", "|Auo" ], + "names": [ "A'ou", "Auo", "Awou" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -5452,7 +5611,7 @@ "full": "apb-Latn-SB", "iana": [ "Sa'a" ], "iso639_3": "apb", - "localname": "Sa’a", + "localname": "Saꞌa", "localnames": [ "Sa'a" ], "name": "Sa’a", "names": [ "Apae’aa", "Sa'a", "Saa", "South Malaita" ], @@ -5469,9 +5628,11 @@ "iana": [ "North Levantine Arabic" ], "iso639_3": "apc", "latnnames": [ "Shami" ], + "localname": "شامي", "localnames": [ "شامي" ], - "name": "Arabic, North Levantine Spoken", - "names": [ "Lebanese", "Lebanese-Syrian Arabic", "Levantine Arabic", "Libano-mexicano", "North Levantine Arabic", "Syro-Lebanese Arabic", "il-lahje š-šāmiyye" ], + "macrolang": "ar", + "name": "Arabic, Levantine", + "names": [ "Lebanese", "Lebanese-Syrian Arabic", "Libano-mexicano", "North Levantine Arabic", "Syro-Lebanese Arabic", "il-lahje š-šāmiyye" ], "region": "SY", "regionname": "Syrian Arab Republic", "regions": [ "AE", "AG", "AR", "BR", "CY", "DE", "DO", "EG", "GF", "IL", "JM", "LB", "LY", "ML", "MX", "PS", "QA", "SA", "SL", "SR", "TN", "TR", "TT" ], @@ -5479,7 +5640,24 @@ "sldr": true, "tag": "apc", "tags": [ "apc-Arab", "apc-SY", "ar-apc", "ar-apc-SY" ], - "windows": "apc-Arab" + "windows": "apc-Arab-SY" + }, + { + "full": "apc-Arab-JO", + "iana": [ "North Levantine Arabic" ], + "iso639_3": "ajp", + "latnnames": [ "Shami" ], + "localnames": [ "شامي" ], + "macrolang": "ar", + "name": "North Levantine Arabic", + "region": "JO", + "regionname": "Jordan", + "regions": [ "AE", "AR", "DE", "IL", "KW", "LY", "PS", "QA", "SA" ], + "script": "Arab", + "sldr": false, + "tag": "apc-Arab-JO", + "tags": [ "ajp", "ajp-Arab", "ajp-Arab-JO", "ajp-JO", "ar-ajp", "ar-ajp-JO" ], + "windows": "apc-Arab-JO" }, { "full": "apd-Arab-TG", @@ -5487,8 +5665,9 @@ "iso639_3": "apd", "latnnames": [ "sūdānī" ], "localnames": [ "سوداني" ], + "macrolang": "ar", "name": "Arabic, Sudanese Spoken", - "names": [ "Arabi", "Khartoum Arabic", "Sudanese Arabic" ], + "names": [ "Arabi", "Dārijīya", "Khartoum Arabic", "Sudanese Arabic" ], "region": "TG", "regionname": "Togo", "regions": [ "AE", "EG", "ER", "ET", "LY", "QA", "SA", "SD", "SS", "YE" ], @@ -5496,15 +5675,17 @@ "sldr": false, "tag": "apd", "tags": [ "apd-Arab", "apd-TG", "ar-apd", "ar-apd-TG" ], - "windows": "apd-Arab" + "windows": "apd-Arab-TG" }, { "full": "apd-Latn-SD", "iana": [ "Sudanese Arabic" ], "iso639_3": "apd", - "localnames": [ "Arabi" ], + "localname": "Arabi", + "localnames": [ "Dārijīya" ], + "macrolang": "ar", "name": "Arabic, Sudanese Spoken", - "names": [ "Arabi", "Khartoum Arabic", "Sudanese Arabic" ], + "names": [ "Arabi", "Dārijīya", "Khartoum Arabic", "Sudanese Arabic" ], "region": "SD", "regionname": "Sudan", "regions": [ "EG", "ER", "ET", "LY", "QA", "SA", "SS" ], @@ -5512,7 +5693,7 @@ "sldr": true, "tag": "apd-Latn", "tags": [ "apd-SD", "ar-apd-SD" ], - "windows": "apd-Latn" + "windows": "apd-Latn-SD" }, { "full": "ape-Latn-PG", @@ -5538,7 +5719,7 @@ "regionname": "Papua New Guinea", "rod": "08441", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ape-x-coastal", "tags": [ "ape-Latn-x-coastal", "ape-PG-x-coastal" ], "windows": "ape-Latn-x-coastal" @@ -5568,7 +5749,7 @@ "region": "PH", "regionname": "Philippines", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "apf", "tags": [ "apf-Latn", "apf-PH" ], "windows": "apf-Latn" @@ -5682,10 +5863,10 @@ "full": "apn-Latn-BR", "iana": [ "Apinayé" ], "iso639_3": "apn", - "localname": "Apinayé", - "localnames": [ "Apinayé" ], - "name": "Apinayé", - "names": [ "Afotigé", "Aogé", "Apinagé", "Apinaié", "Apinajé", "Otogé", "Oupinagee", "Pinagé", "Pinaré", "Timbira Ocidentais", "Uhitische", "Utinsche", "Western Timbira" ], + "localname": "Panhĩ kapẽr", + "localnames": [ "Panhĩ kapẽr" ], + "name": "Apinagé", + "names": [ "Afotigé", "Aogé", "Apinaié", "Apinajé", "Apinayé", "Otogé", "Oupinagee", "Panhĩ kapẽr", "Pinagé", "Pinaré", "Timbira Ocidentais", "Uhitische", "Utinsche", "Western Timbira" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -5744,9 +5925,8 @@ "iana": [ "Arop-Lokep" ], "iso639_3": "apr", "localname": "Arop-Lokep", - "localnames": [ "Arop-Lokep" ], "name": "Arop-Lokep", - "names": [ "Arop-Lukep", "Lukep", "Moromiranga", "Siasi", "Siassi", "Tolokiwa" ], + "names": [ "Arop-Lukep", "Moromiranga", "Poonoo le Lokewe", "Siasi", "Siassi", "Tolokiwa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -5761,8 +5941,8 @@ "iso639_3": "aps", "localname": "Orop", "localnames": [ "Orop" ], - "name": "Arop-Sissano", - "names": [ "Arop", "Orop" ], + "name": "Orop", + "names": [ "Arop", "Arop-Sissano" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -5804,6 +5984,7 @@ "iana": [ "Alapmunte" ], "iso639_3": "apv", "name": "Alapmunte", + "names": [ "Alapmonde", "Yalapmunde", "Yalapmunte", "Yalapmunxte", "Yala’mun’du" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -5862,6 +6043,7 @@ "full": "apz-Latn-PG", "iana": [ "Safeyoka" ], "iso639_3": "apz", + "localnames": [ "Ampeeli" ], "name": "Safeyoka", "names": [ "Ambari", "Ampale", "Ampeeli", "Ampeeli-Wojokeso", "Ampele" ], "region": "PG", @@ -5917,6 +6099,21 @@ "unwritten": true, "windows": "aqg-Latn" }, + { + "full": "aqk-Latn-NG", + "iana": [ "Aninka" ], + "iso639_3": "aqk", + "name": "Aninka", + "names": [ "Kurmi", "Ninka", "Nka", "Sanga" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "aqk", + "tags": [ "aqk-Latn", "aqk-NG" ], + "unwritten": true, + "windows": "aqk-Latn" + }, { "full": "aqm-Latn-ID", "iana": [ "Atohwaim" ], @@ -5952,6 +6149,7 @@ "iana": [ "Atakapa" ], "iso639_3": "aqp", "name": "Atakapa", + "names": [ "Atakapa-Ishak", "Ishak", "Yukhiti" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -5981,7 +6179,7 @@ "iana": [ "Angaité" ], "iso639_3": "aqt", "name": "Angaité", - "names": [ "Enenlhet", "Kovalhvok", "Koyaqteves" ], + "names": [ "Kovalhvok", "Koyaqteves" ], "region": "PY", "regionname": "Paraguay", "script": "Latn", @@ -6014,13 +6212,12 @@ "name": "Arabic", "region": "EG", "regionname": "Egypt", - "regions": [ "BH", "KW", "OM", "QA", "SD", "TD", "YE" ], "script": "Arab", "sldr": true, "suppress": true, "tag": "ar", "tags": [ "ar-Arab", "ar-EG" ], - "windows": "ar" + "windows": "ar-EG" }, { "full": "ar-Arab-AE", @@ -6032,16 +6229,28 @@ "regionname": "United Arab Emirates", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-AE", - "windows": "ar-AE" + "windows": "ar-Arab-AE" + }, + { + "full": "ar-Arab-BH", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "BH", + "regionname": "Bahrain", + "script": "Arab", + "sldr": true, + "tag": "ar-BH", + "windows": "ar-Arab-BH" }, { "full": "ar-Brai-SA", "iana": [ "Arabic" ], "iso639_3": "ara", - "name": "Arabic, Standard", - "names": [ "Al-’Arabiyya", "Arabic", "Literary Arabic" ], + "name": "Arabic", + "names": [ "Al-’Arabiyya", "Arabic, Standard", "Literary Arabic" ], "nophonvars": true, "region": "SA", "regionname": "Saudi Arabia", @@ -6050,7 +6259,7 @@ "sldr": false, "tag": "ar-Brai", "tags": [ "arb-Brai", "arb-Brai-SA" ], - "windows": "ar-Brai" + "windows": "ar-Brai-SA" }, { "full": "ar-Arab-DJ", @@ -6062,9 +6271,8 @@ "regionname": "Djibouti", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-DJ", - "windows": "ar-DJ" + "windows": "ar-Arab-DJ" }, { "full": "ar-Arab-DZ", @@ -6076,9 +6284,8 @@ "regionname": "Algeria", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-DZ", - "windows": "ar-DZ" + "windows": "ar-Arab-DZ" }, { "full": "ar-Arab-EH", @@ -6090,9 +6297,8 @@ "regionname": "Western Sahara", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-EH", - "windows": "ar-EH" + "windows": "ar-Arab-EH" }, { "full": "ar-Arab-ER", @@ -6104,9 +6310,8 @@ "regionname": "Eritrea", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-ER", - "windows": "ar-ER" + "windows": "ar-Arab-ER" }, { "full": "ar-Arab-IL", @@ -6118,9 +6323,8 @@ "regionname": "Israel", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-IL", - "windows": "ar-IL" + "windows": "ar-Arab-IL" }, { "full": "ar-Arab-IQ", @@ -6132,9 +6336,8 @@ "regionname": "Iraq", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-IQ", - "windows": "ar-IQ" + "windows": "ar-Arab-IQ" }, { "full": "ar-Arab-JO", @@ -6146,9 +6349,8 @@ "regionname": "Jordan", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-JO", - "windows": "ar-JO" + "windows": "ar-Arab-JO" }, { "full": "ar-Arab-KM", @@ -6160,9 +6362,21 @@ "regionname": "Comoros", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-KM", - "windows": "ar-KM" + "windows": "ar-Arab-KM" + }, + { + "full": "ar-Arab-KW", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "KW", + "regionname": "Kuwait", + "script": "Arab", + "sldr": true, + "tag": "ar-KW", + "windows": "ar-Arab-KW" }, { "full": "ar-Arab-LB", @@ -6174,9 +6388,8 @@ "regionname": "Lebanon", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-LB", - "windows": "ar-LB" + "windows": "ar-Arab-LB" }, { "full": "ar-Arab-LY", @@ -6188,9 +6401,8 @@ "regionname": "Libya", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-LY", - "windows": "ar-LY" + "windows": "ar-Arab-LY" }, { "full": "ar-Arab-MA", @@ -6202,9 +6414,8 @@ "regionname": "Morocco", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-MA", - "windows": "ar-MA" + "windows": "ar-Arab-MA" }, { "full": "ar-Arab-MR", @@ -6216,9 +6427,21 @@ "regionname": "Mauritania", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-MR", - "windows": "ar-MR" + "windows": "ar-Arab-MR" + }, + { + "full": "ar-Arab-OM", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "OM", + "regionname": "Oman", + "script": "Arab", + "sldr": true, + "tag": "ar-OM", + "windows": "ar-Arab-OM" }, { "full": "ar-Arab-PS", @@ -6230,9 +6453,21 @@ "regionname": "State of Palestine", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-PS", - "windows": "ar-PS" + "windows": "ar-Arab-PS" + }, + { + "full": "ar-Arab-QA", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "QA", + "regionname": "Qatar", + "script": "Arab", + "sldr": true, + "tag": "ar-QA", + "windows": "ar-Arab-QA" }, { "full": "ar-Arab-SA", @@ -6240,17 +6475,29 @@ "latnnames": [ "al-ʻArabīyah" ], "localname": "العربية", "localnames": [ "العربية" ], - "name": "Arabic, Standard", - "names": [ "Al-’Arabiyya", "Arabic", "Literary Arabic" ], + "name": "Arabic", + "names": [ "Al-’Arabiyya", "Arabic, Standard", "Literary Arabic" ], "region": "SA", "regionname": "Saudi Arabia", - "regions": [ "BH", "DK", "IR", "KW", "ML", "NE", "OM", "PH", "QA", "SD", "TD", "TR", "TZ", "YE" ], + "regions": [ "DK", "IR", "ML", "NE", "PH", "TR", "TZ" ], "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-SA", "tags": [ "ar-arb", "ar-arb-SA", "arb", "arb-Arab", "arb-Arab-SA", "arb-SA" ], - "windows": "ar-SA" + "windows": "ar-Arab-SA" + }, + { + "full": "ar-Arab-SD", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "SD", + "regionname": "Sudan", + "script": "Arab", + "sldr": true, + "tag": "ar-SD", + "windows": "ar-Arab-SD" }, { "full": "ar-Arab-SO", @@ -6262,9 +6509,8 @@ "regionname": "Somalia", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-SO", - "windows": "ar-SO" + "windows": "ar-Arab-SO" }, { "full": "ar-Arab-SS", @@ -6276,9 +6522,8 @@ "regionname": "South Sudan", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-SS", - "windows": "ar-SS" + "windows": "ar-Arab-SS" }, { "full": "ar-Arab-SY", @@ -6290,9 +6535,8 @@ "regionname": "Syrian Arab Republic", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-SY", - "windows": "ar-SY" + "windows": "ar-Arab-SY" }, { "full": "ar-Syrc-SA", @@ -6307,7 +6551,20 @@ "script": "Syrc", "sldr": false, "tag": "ar-Syrc", - "windows": "ar-Syrc" + "windows": "ar-Syrc-SA" + }, + { + "full": "ar-Arab-TD", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "TD", + "regionname": "Chad", + "script": "Arab", + "sldr": true, + "tag": "ar-TD", + "windows": "ar-Arab-TD" }, { "full": "ar-Arab-TN", @@ -6319,9 +6576,55 @@ "regionname": "Tunisia", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ar-TN", - "windows": "ar-TN" + "windows": "ar-Arab-TN" + }, + { + "full": "ar-Arab-YE", + "iana": [ "Arabic" ], + "iso639_3": "ara", + "localname": "العربية", + "name": "Arabic", + "region": "YE", + "regionname": "Yemen", + "script": "Arab", + "sldr": true, + "tag": "ar-YE", + "windows": "ar-Arab-YE" + }, + { + "full": "arb-Arab-EG", + "iana": [ "Standard Arabic" ], + "iso639_3": "arb", + "latnnames": [ "al-ʻArabīyah" ], + "localnames": [ "العربية" ], + "macrolang": "ar", + "name": "Arabic, Standard", + "names": [ "Standard Arabic" ], + "region": "EG", + "regionname": "Egypt", + "script": "Arab", + "sldr": true, + "tag": "arb-EG", + "tags": [ "ar-arb-EG" ], + "windows": "arb-Arab-EG" + }, + { + "full": "arb-Arab-LB", + "iana": [ "Standard Arabic" ], + "iso639_3": "arb", + "latnnames": [ "al-ʻArabīyah" ], + "localnames": [ "العربية" ], + "macrolang": "ar", + "name": "Arabic, Standard", + "names": [ "Standard Arabic" ], + "region": "LB", + "regionname": "Lebanon", + "script": "Arab", + "sldr": true, + "tag": "arb-LB", + "tags": [ "ar-arb-LB" ], + "windows": "arb-Arab-LB" }, { "full": "arc-Armi-IR", @@ -6335,7 +6638,20 @@ "sldr": false, "tag": "arc", "tags": [ "arc-Armi", "arc-IR" ], - "windows": "arc-Armi" + "windows": "arc-Armi-IR" + }, + { + "full": "arc-Hatr-XX", + "iana": [ "Official Aramaic (700-300 BCE)", "Imperial Aramaic (700-300 BCE)" ], + "iso639_3": "arc", + "name": "Official Aramaic (700-300 BCE)", + "names": [ "Imperial Aramaic (700-300 BCE)" ], + "nophonvars": true, + "region": "XX", + "script": "Hatr", + "sldr": false, + "tag": "arc-Hatr", + "windows": "arc-Hatr-XX" }, { "full": "arc-Nbat-JO", @@ -6350,7 +6666,7 @@ "sldr": false, "tag": "arc-Nbat", "tags": [ "arc-JO" ], - "windows": "arc-Nbat" + "windows": "arc-Nbat-JO" }, { "full": "arc-Palm-SY", @@ -6365,7 +6681,7 @@ "sldr": false, "tag": "arc-Palm", "tags": [ "arc-SY" ], - "windows": "arc-Palm" + "windows": "arc-Palm-SY" }, { "full": "arc-Syrc-XX", @@ -6378,8 +6694,7 @@ "script": "Syrc", "sldr": false, "tag": "arc-Syrc", - "tags": [ "arc-XX" ], - "windows": "arc-Syrc" + "windows": "arc-Syrc-XX" }, { "full": "ard-Latn-AU", @@ -6477,9 +6792,9 @@ "iana": [ "Arabela" ], "iso639_3": "arl", "localname": "Tapueyocaca", - "localnames": [ "Tapueyocaca" ], + "localnames": [ "Tapueyocuaca" ], "name": "Arabela", - "names": [ "Chiripuno", "Chiripunu", "Tapueyocaca", "Tapueyocuaca" ], + "names": [ "Chiripuno", "Chiripunu", "Tapueyocaca", "Tapueyocuaca", "Tapweyokwaka" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -6538,15 +6853,16 @@ "full": "arq-Arab-DZ", "iana": [ "Algerian Arabic" ], "iso639_3": "arq", - "latnnames": [ "Dziria" ], - "localnames": [ "دزيرية" ], + "latnnames": [ "El'aamia", "Lahja Djazairia" ], + "localnames": [ "العامية", "اللهجة الجزائرية" ], + "macrolang": "ar", "name": "Arabic, Algerian Spoken", "names": [ "Algerian", "Algerian Arabic", "Darija", "Darja", "Dziria" ], "region": "DZ", "regionname": "Algeria", "regions": [ "BE", "DE", "EG", "ES", "FR", "NL", "PM", "TN" ], "script": "Arab", - "sldr": false, + "sldr": true, "tag": "arq", "tags": [ "ar-arq", "ar-arq-DZ", "arq-Arab", "arq-DZ" ], "windows": "arq-Arab" @@ -6572,6 +6888,7 @@ "iso639_3": "ars", "latnnames": [ "Najdi" ], "localnames": [ "نجدي" ], + "macrolang": "ar", "name": "Arabic, Najdi Spoken", "names": [ "Bedawi", "Najdi", "Najdi Arabic" ], "region": "SA", @@ -6587,8 +6904,8 @@ "full": "aru-Latn-BR", "iana": [ "Aruá (Amazonas State)", "Arawá" ], "iso639_3": "aru", - "name": "Aruá (Amazonas State)", - "names": [ "Arawá" ], + "name": "Aruá", + "names": [ "Arawá", "Aruá (Amazonas State)" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -6649,6 +6966,7 @@ "iso639_3": "ary", "latnnames": [ "Darija" ], "localnames": [ "الدارجة" ], + "macrolang": "ar", "name": "Arabic, Moroccan Spoken", "names": [ "Colloquial Arabic", "Maghrebi Arabic", "Maghribi", "Moroccan Arabic", "Moroccan Colloquial Arabic", "Moroccan Dareja", "Moroccan Darija", "Moroccan Dereja" ], "region": "MA", @@ -6665,8 +6983,9 @@ "iana": [ "Egyptian Arabic" ], "iso639_3": "arz", "latnnames": [ "Ammi", "Masri" ], - "localname": "اللغة المصرية العامية", + "localname": "عامي", "localnames": [ "عامي", "مصري" ], + "macrolang": "ar", "name": "Arabic, Egyptian Spoken", "names": [ "Egyptian Arabic", "Lower Egypt Arabic", "Massry", "Modern Egyptian Language", "Normal Egyptian Arabic" ], "region": "EG", @@ -6682,6 +7001,7 @@ "full": "arz-Brai-EG", "iana": [ "Egyptian Arabic" ], "iso639_3": "arz", + "macrolang": "ar", "name": "Arabic, Egyptian Spoken", "names": [ "Egyptian Arabic", "Lower Egypt Arabic", "Massry", "Modern Egyptian Language", "Normal Egyptian Arabic" ], "nophonvars": true, @@ -6755,7 +7075,6 @@ "sldr": false, "tag": "asc", "tags": [ "asc-ID", "asc-Latn" ], - "unwritten": true, "windows": "asc-Latn" }, { @@ -6763,10 +7082,10 @@ "iana": [ "American Sign Language" ], "iso639_3": "ase", "name": "American Sign Language", - "names": [ "#A-S-L", "ASL", "Ameslan", "Benin Sign Language", "Francophone ASL", "Gabonese Sign Language", "Haitian Sign Language", "LSAF", "LSAF Ivoirienne", "LSCI", "LSFA", "LSH", "Langue des signes américaine", "Langue des signes congolaise", "Langue des signes de Côte d’Ivoire", "Langue des signes de l’Afrique francophone", "Langue des signes du Congo", "Langue des signes d’Afrique francophone", "Langue des signes franco-africaine", "Langue des signes haïtienne", "SIGN AMERICA", "Togo Sign Language" ], + "names": [ "#A-S-L", "ASL", "Ameslan", "Benin ASL", "Benin Sign Language", "Burkina Faso ASL", "Burkina Faso Sign Language", "Central African Republic ASL", "Congolese ASL", "Francophone ASL", "Gabonese Sign Language", "Guyanese ASL", "Haitian Sign Language", "LSAF", "LSAF Ivoirienne", "LSCI", "LSFA", "LSH", "Langue des signes américaine", "Langue des signes congolaise", "Langue des signes de Côte d’Ivoire", "Langue des signes de l’Afrique francophone", "Langue des signes du Congo", "Langue des signes d’Afrique francophone", "Langue des signes franco-africaine", "Langue des signes haïtienne", "Malian ASL", "SIGN AMERICA", "Salvadoran ASL", "Togo Sign Language" ], "region": "US", "regionname": "United States", - "regions": [ "AG", "BB", "BF", "BJ", "BM", "BO", "BS", "BZ", "CA", "CF", "CG", "CI", "DM", "GA", "GD", "GU", "GY", "HT", "JM", "KN", "LC", "ML", "NG", "PH", "PR", "SV", "TD", "TG", "TT", "VC", "VI", "ZA", "ZM" ], + "regions": [ "AG", "BB", "BF", "BJ", "BM", "BO", "BS", "BZ", "CA", "CF", "CG", "CI", "DM", "GA", "GD", "GU", "GY", "HT", "JM", "KN", "KY", "LC", "ML", "NG", "PH", "PR", "SV", "TD", "TG", "TT", "VC", "VI", "ZA", "ZM" ], "script": "Sgnw", "sldr": false, "tag": "ase", @@ -6778,7 +7097,7 @@ "iana": [ "American Sign Language" ], "iso639_3": "ase", "name": "American Sign Language", - "names": [ "#A-S-L", "ASL", "Ameslan", "Benin Sign Language", "Francophone ASL", "Gabonese Sign Language", "Haitian Sign Language", "LSAF", "LSAF Ivoirienne", "LSCI", "LSFA", "LSH", "Langue des signes américaine", "Langue des signes congolaise", "Langue des signes de Côte d’Ivoire", "Langue des signes de l’Afrique francophone", "Langue des signes du Congo", "Langue des signes d’Afrique francophone", "Langue des signes franco-africaine", "Langue des signes haïtienne", "SIGN AMERICA", "Togo Sign Language" ], + "names": [ "#A-S-L", "ASL", "Ameslan", "Benin ASL", "Benin Sign Language", "Burkina Faso ASL", "Burkina Faso Sign Language", "Central African Republic ASL", "Congolese ASL", "Francophone ASL", "Gabonese Sign Language", "Guyanese ASL", "Haitian Sign Language", "LSAF", "LSAF Ivoirienne", "LSCI", "LSFA", "LSH", "Langue des signes américaine", "Langue des signes congolaise", "Langue des signes de Côte d’Ivoire", "Langue des signes de l’Afrique francophone", "Langue des signes du Congo", "Langue des signes d’Afrique francophone", "Langue des signes franco-africaine", "Langue des signes haïtienne", "Malian ASL", "SIGN AMERICA", "Salvadoran ASL", "Togo Sign Language" ], "nophonvars": true, "region": "US", "regionname": "United States", @@ -6912,7 +7231,7 @@ "iana": [ "Dano" ], "iso639_3": "aso", "name": "Dano", - "names": [ "Asaro", "Upper Asaro" ], + "names": [ "Asaro" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -6955,13 +7274,12 @@ "iso639_3": "asr", "name": "Asuri", "names": [ "Ashree", "Assur", "Asura", "Maleta" ], - "nophonvars": true, "region": "IN", "regionname": "India", "script": "Deva", "sldr": false, - "tag": "asr-Deva", - "tags": [ "asr-IN" ], + "tag": "asr", + "tags": [ "asr-Deva", "asr-IN" ], "windows": "asr-Deva" }, { @@ -6983,9 +7301,9 @@ "iana": [ "Asturian", "Asturleonese", "Bable", "Leonese" ], "iso639_3": "ast", "localname": "asturianu", - "localnames": [ "Asturianu" ], + "localnames": [ "Asturianu", "Bable" ], "name": "Asturian", - "names": [ "Astur-Leonese", "Asturian-Leonese", "Asturianu", "Asturleonese", "Bable", "Leonese" ], + "names": [ "Astur-Leonese", "Asturian-Leonese", "Asturiano", "Asturianu", "Asturleonese", "Bable", "Leonese" ], "region": "ES", "regionname": "Spain", "regions": [ "PT" ], @@ -7043,7 +7361,7 @@ "iana": [ "Muratayak" ], "iso639_3": "asx", "name": "Muratayak", - "names": [ "Asat", "Murataik" ], + "names": [ "Asat", "Maratayak", "Mratayak", "Murataik" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -7102,9 +7420,9 @@ "iana": [ "Zaiwa" ], "iso639_3": "atb", "localname": "Zaiwa", - "localnames": [ "Zaiwa" ], + "localnames": [ "Zaiwâ" ], "name": "Zaiwa", - "names": [ "Aci", "Aji", "Atshi", "Atsi", "Atsi-Maru", "Atzi", "Azi", "Szi", "Tsaiva", "Tsaiwa", "Xiaoshanhua", "Zi" ], + "names": [ "Aci", "Aji", "Atshi", "Atsi", "Atsi-Maru", "Atzi", "Azi", "Szi", "Tsaiva", "Tsaiwa", "Xiaoshanhua", "Zaiwâ", "Zi" ], "region": "CN", "regionname": "China", "regions": [ "MM" ], @@ -7119,7 +7437,7 @@ "iana": [ "Zaiwa" ], "iso639_3": "atb", "name": "Zaiwa", - "names": [ "Aci", "Aji", "Atshi", "Atsi", "Atsi-Maru", "Atzi", "Azi", "Szi", "Tsaiva", "Tsaiwa", "Xiaoshanhua", "Zi" ], + "names": [ "Aci", "Aji", "Atshi", "Atsi", "Atsi-Maru", "Atzi", "Azi", "Szi", "Tsaiva", "Tsaiwa", "Xiaoshanhua", "Zaiwâ", "Zi" ], "nophonvars": true, "obsolete": true, "region": "CN", @@ -7135,6 +7453,7 @@ "iana": [ "Atsahuaca" ], "iso639_3": "atc", "name": "Atsahuaca", + "names": [ "Astahuaca", "Atsahuaca-Yamiaca", "Atsawaca", "Atsawaka", "Atsawaka-Yamiaka", "Atswawaka" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -7151,7 +7470,7 @@ "localname": "Minanobo", "localnames": [ "Minanobo" ], "name": "Manobo, Ata", - "names": [ "Ata Manobo", "Ata of Davao", "Atao Manobo", "Langilan", "Minanobo" ], + "names": [ "Ata", "Ata Manobo", "Ata of Davao", "Atao Manobo", "Langilan", "Minanobo" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -7180,7 +7499,9 @@ "iana": [ "Ivbie North-Okpela-Arhe" ], "iso639_3": "atg", "localname": "Okphela", + "localnames": [ "Okphela" ], "name": "Ivbie North-Okpela-Arhe", + "names": [ "Okphela" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -7371,9 +7692,9 @@ "full": "atu-Latn-SS", "iana": [ "Reel" ], "iso639_3": "atu", - "localnames": [ "Thok Reel" ], + "localnames": [ "Thuɔk ë Rëël" ], "name": "Reel", - "names": [ "Atuot", "Atwot", "Thok Cieng Reel", "Thok Reel" ], + "names": [ "Atuot", "Atwot", "Thok Cieng Reel", "Thok Reel", "Thuɔk ë Rëël" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -7417,7 +7738,7 @@ "iana": [ "Arutani" ], "iso639_3": "atx", "name": "Arutani", - "names": [ "Anake", "Aoaqui", "Auakê", "Auaqué", "Awaikê", "Awake", "Awaké", "Oewaku", "Orotani", "Uruak", "Urutaní" ], + "names": [ "Anake", "Aoaqui", "Auakê", "Auaqué", "Awaikê", "Awaké", "Oewaku", "Orotani", "Uruak", "Urutaní" ], "region": "BR", "regionname": "Brazil", "regions": [ "VE" ], @@ -7537,7 +7858,7 @@ "iana": [ "Aushi" ], "iso639_3": "auh", "name": "Aushi", - "names": [ "Avaushi", "Ushi", "Usi", "Uzhil", "Uzhili", "Vouaousi" ], + "names": [ "Abaushi", "Avaushi", "Ba-Usi", "Bahushi", "Bahusi", "Baoussi", "Batushi", "Ikyaushi", "Ushi", "Usi", "Uzhil", "Uzhili", "Vouaousi", "Wa-Usi", "Waushi", "Wauzhi" ], "region": "ZM", "regionname": "Zambia", "regions": [ "CD" ], @@ -7552,6 +7873,8 @@ "full": "aui-Latn-PG", "iana": [ "Anuki" ], "iso639_3": "aui", + "localname": "Anuki", + "localnames": [ "Anuki" ], "name": "Anuki", "names": [ "Gabobora" ], "region": "PG", @@ -7563,26 +7886,54 @@ "windows": "aui-Latn" }, { - "full": "auj-Zyyy-LY", + "full": "auj-Arab-LY", "iana": [ "Awjilah" ], "iso639_3": "auj", "name": "Awjilah", "names": [ "Aoudjila", "Augila", "Aujila", "Awjila", "Awjili" ], "region": "LY", "regionname": "Libya", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "auj", - "tags": [ "auj-LY", "auj-Zyyy" ], + "tags": [ "auj-Arab", "auj-LY" ], + "windows": "auj-Arab" + }, + { + "full": "auj-Latn-LY", + "iana": [ "Awjilah" ], + "iso639_3": "auj", + "name": "Awjilah", + "names": [ "Aoudjila", "Augila", "Aujila", "Awjila", "Awjili" ], + "region": "LY", + "regionname": "Libya", + "script": "Latn", + "sldr": false, + "tag": "auj-Latn", + "windows": "auj-Latn" + }, + { + "full": "auj-Tfng-LY", + "iana": [ "Awjilah" ], + "iso639_3": "auj", + "name": "Awjilah", + "names": [ "Aoudjila", "Augila", "Aujila", "Awjila", "Awjili" ], + "nophonvars": true, + "region": "LY", + "regionname": "Libya", + "script": "Tfng", + "sldr": false, + "tag": "auj-Tfng", "unwritten": true, - "windows": "auj-Zyyy" + "windows": "auj-Tfng" }, { "full": "auk-Latn-PG", "iana": [ "Heyo" ], "iso639_3": "auk", - "name": "Heyo", - "names": [ "Arima", "Arinua", "Arinwa", "Lolopani", "Ruruhip", "Wan Wan", "Wanib" ], + "localnames": [ "Heiyoho" ], + "name": "Heiyoho", + "names": [ "Arima", "Arinua", "Arinwa", "Heyo", "Lolopani", "Ruruhip", "Wan Wan", "Wanib" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -7610,7 +7961,7 @@ "iana": [ "Asu (Nigeria)" ], "iso639_3": "aum", "name": "Asu", - "names": [ "Abewa", "Asu (Nigeria)", "Ebe" ], + "names": [ "Abewa", "Abeywa", "Asu (Nigeria)", "Ebe" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -7717,7 +8068,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "auu", "tags": [ "auu-ID", "auu-Latn" ], "windows": "auu-Latn" @@ -7747,8 +8098,8 @@ "regionname": "Brazil", "script": "Latn", "sldr": false, - "tag": "aux", - "tags": [ "aux-BR", "aux-Latn" ], + "tag": "aux-Latn", + "tags": [ "aux", "aux-BR" ], "unwritten": true, "windows": "aux-Latn" }, @@ -7772,8 +8123,9 @@ "full": "auz-Arab-UZ", "iana": [ "Uzbeki Arabic" ], "iso639_3": "auz", + "macrolang": "ar", "name": "Arabic, Uzbeki Spoken", - "names": [ "Central Asian Arabic", "Jugari", "Kashkadarya Arabic", "Uzbeki Arabic" ], + "names": [ "Central Asian Arabic", "Jugari", "Jugari Arabic", "Uzbeki Arabic", "Uzbekistan Arabic" ], "region": "UZ", "regionname": "Uzbekistan", "script": "Arab", @@ -7815,22 +8167,23 @@ "windows": "avb-Latn" }, { - "full": "avd-Zyyy-IR", + "full": "avd-Arab-IR", "iana": [ "Alviri-Vidari" ], "iso639_3": "avd", "name": "Alviri-Vidari", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "avd", - "tags": [ "avd-IR", "avd-Zyyy" ], - "windows": "avd-Zyyy" + "tags": [ "avd-Arab", "avd-IR" ], + "windows": "avd-Arab" }, { "full": "avi-Latn-CI", "iana": [ "Avikam" ], "iso639_3": "avi", + "localnames": [ "Avikam" ], "name": "Avikam", "names": [ "Avekom", "Brignan", "Brinya", "Gbanda", "Kwakwa", "Lahu" ], "region": "CI", @@ -7859,7 +8212,9 @@ "iana": [ "Eastern Egyptian Bedawi Arabic" ], "iso639_3": "avl", "latnnames": [ "Badawi" ], + "localname": "بدوي", "localnames": [ "بدوي" ], + "macrolang": "ar", "name": "Arabic, Eastern Egyptian Bedawi Spoken", "names": [ "Arabic, Levantine Bedawi Spoken", "Eastern Egyptian Bedawi Arabic", "Levantine Bedawi Arabic", "Levantine Bedawi Spoken Arabic" ], "region": "EG", @@ -7890,8 +8245,8 @@ "full": "avn-Latn-GH", "iana": [ "Avatime" ], "iso639_3": "avn", - "localname": "Si-yà", - "localnames": [ "Si-yà" ], + "localname": "Sideme", + "localnames": [ "Sideme" ], "name": "Avatime", "names": [ "Afatime", "Si-yà", "Sia", "Sideme", "Siya", "Siyase" ], "region": "GH", @@ -7962,7 +8317,7 @@ "sldr": true, "tag": "avu", "tags": [ "avu-Latn", "avu-SS" ], - "windows": "avu-Latn" + "windows": "avu-Latn-SS" }, { "full": "avu-Latn-SD", @@ -8015,8 +8370,9 @@ "iana": [ "Awa (Papua New Guinea)" ], "iso639_3": "awb", "localname": "Awa", + "localnames": [ "Ahwa" ], "name": "Awa", - "names": [ "Awa (Papua New Guinea)", "Mobuta" ], + "names": [ "Ahwa", "Awa (Papua New Guinea)", "Mobuta" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -8118,7 +8474,8 @@ "full": "awm-Latn-PG", "iana": [ "Arawum" ], "iso639_3": "awm", - "name": "Arawum", + "name": "Taupi", + "names": [ "Arawum" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -8132,6 +8489,8 @@ "full": "awn-Ethi-ET", "iana": [ "Awngi" ], "iso639_3": "awn", + "latnnames": [ "Awŋi" ], + "localnames": [ "አውጚ" ], "name": "Awngi", "region": "ET", "regionname": "Ethiopia", @@ -8145,6 +8504,7 @@ "full": "awo-Latn-NG", "iana": [ "Awak" ], "iso639_3": "awo", + "localname": "Yěbù", "localnames": [ "Yěbù" ], "name": "Awak", "names": [ "Awok", "Nìín Yěbù", "Yěbù" ], @@ -8205,7 +8565,7 @@ "iana": [ "Central Awyu" ], "iso639_3": "awu", "name": "Awyu, Central", - "names": [ "Ajau", "Auyu", "Avio", "Awju", "Awya", "Central Awyu", "Nohon" ], + "names": [ "Ajau", "Auyu", "Avio", "Awju", "Awya", "Central Auyu", "Central Awyu", "Nohon" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -8277,6 +8637,7 @@ "iana": [ "Abipon" ], "iso639_3": "axb", "name": "Abipon", + "names": [ "Abipón" ], "region": "AR", "regionname": "Argentina", "script": "Latn", @@ -8320,9 +8681,9 @@ "full": "axk-Latn-CF", "iana": [ "Yaka (Central African Republic)" ], "iso639_3": "axk", - "localnames": [ "Aka" ], + "localnames": [ "Yaâkâ" ], "name": "Yaka", - "names": [ "Aka", "Beka", "Beká", "Binga", "Mòáka", "Nyoyaka", "Pygmée de Mongoumba", "Pygmée de la Lobaye", "Pygmées de la Sanghas", "Yaka (Central African Republic)" ], + "names": [ "Aka", "Beka", "Beká", "Mòáka", "Nyoyaka", "Pygmée de Mongoumba", "Pygmée de la Lobaye", "Pygmées de la Sanghas", "Yaka (Central African Republic)", "Yaâkâ" ], "region": "CF", "regionname": "Central African Republic", "regions": [ "CG" ], @@ -8348,17 +8709,17 @@ "windows": "axl-Latn" }, { - "full": "axm-Zyyy-AM", + "full": "axm-Armn-AM", "iana": [ "Middle Armenian" ], "iso639_3": "axm", "name": "Middle Armenian", "region": "AM", "regionname": "Armenia", - "script": "Zyyy", + "script": "Armn", "sldr": false, "tag": "axm", - "tags": [ "axm-AM", "axm-Zyyy" ], - "windows": "axm-Zyyy" + "tags": [ "axm-AM", "axm-Armn" ], + "windows": "axm-Armn" }, { "full": "axx-Latn-NC", @@ -8379,9 +8740,9 @@ "full": "ay-Latn-BO", "iana": [ "Aymara" ], "iso639_3": "aym", - "localnames": [ "Aymar" ], - "name": "Aymara, Central", - "names": [ "Aimara", "Aymar", "Aymar aru", "Aymara" ], + "localnames": [ "Aymar", "Aymar aru" ], + "name": "Aymara", + "names": [ "Aimara", "Aymar", "Aymar aru", "Aymara, Central" ], "region": "BO", "regionname": "Bolivia", "regions": [ "AR", "CL", "PE" ], @@ -8426,6 +8787,7 @@ "full": "ayc-Latn-PE", "iana": [ "Southern Aymara" ], "iso639_3": "ayc", + "macrolang": "ay", "name": "Aymara, Southern", "names": [ "Aymar", "Aymar aru", "Southern Aymara" ], "region": "PE", @@ -8486,8 +8848,9 @@ "full": "ayh-Arab-YE", "iana": [ "Hadrami Arabic" ], "iso639_3": "ayh", + "macrolang": "ar", "name": "Arabic, Hadrami Spoken", - "names": [ "Hadhrami Arabic", "Hadrami", "Hadrami Arabic", "Hadromi", "Ḥaḍrami Arabic" ], + "names": [ "Hadhrami Arabic", "Hadrami", "Hadrami Arabic", "Hadromi" ], "region": "YE", "regionname": "Yemen", "regions": [ "ER", "KE" ], @@ -8495,7 +8858,6 @@ "sldr": true, "tag": "ayh", "tags": [ "ar-ayh", "ar-ayh-YE", "ayh-Arab", "ayh-YE" ], - "unwritten": true, "windows": "ayh-Arab" }, { @@ -8533,6 +8895,7 @@ "iso639_3": "ayl", "latnnames": [ "Lībi" ], "localnames": [ "ليبي" ], + "macrolang": "ar", "name": "Arabic, Libyan Spoken", "names": [ "Arabic, Western Egyptian Bedawi Spoken", "Bedawi", "Libyan Arabic", "Libyan Spoken Arabic", "Libyan Vernacular Arabic", "Maghrebi Arabic", "Sulaimitian Arabic", "Western Egyptian Bedawi Spoken Arabic" ], "region": "LY", @@ -8550,6 +8913,7 @@ "iso639_3": "ayn", "latnnames": [ "Yamani" ], "localnames": [ "يمني" ], + "macrolang": "ar", "name": "Arabic, Sanaani Spoken", "names": [ "Northern Yemeni Arabic", "Sanaani Arabic" ], "region": "YE", @@ -8566,7 +8930,7 @@ "iana": [ "Ayoreo" ], "iso639_3": "ayo", "name": "Ayoreo", - "names": [ "Ayoré", "Coroino", "Garaygosode", "Guarañoca", "Guidaigosode", "Koroino", "Moro", "Morotoco", "Poturero", "Pyeta", "Pyeta Yovai", "Pyta Jovai", "Samococio", "Takrat", "Totobiegosode", "Yanaigua", "Yovai" ], + "names": [ "Ayoeo", "Ayoré", "Ayoweo", "Coroino", "Garaygosode", "Guarañoca", "Guidaigosode", "Koroino", "Moro", "Morotoco", "Poturero", "Pyeta", "Pyeta Yovai", "Pyta Jovai", "Samococio", "Takrat", "Totobiegosode", "Yanaigua", "Yovai" ], "region": "PY", "regionname": "Paraguay", "regions": [ "BO" ], @@ -8582,6 +8946,7 @@ "iso639_3": "ayp", "latnnames": [ "Maslawi" ], "localnames": [ "مصلاوي" ], + "macrolang": "ar", "name": "Arabic, North Mesopotamian Spoken", "names": [ "Maslawi", "Mesopotamian Qeltu Arabic", "Moslawi", "North Mesopotamian Arabic", "Syro-Mesopotamian Arabic", "Syro-Mesopotamian Vernacular Arabic" ], "region": "IQ", @@ -8597,6 +8962,7 @@ "full": "ayq-Latn-PG", "iana": [ "Ayi (Papua New Guinea)" ], "iso639_3": "ayq", + "localnames": [ "Ayi" ], "name": "Ayi", "names": [ "Ayi (Papua New Guinea)" ], "region": "PG", @@ -8671,7 +9037,7 @@ "iana": [ "Mai Brat" ], "iso639_3": "ayz", "localname": "Mai Brat", - "localnames": [ "Mai Brat", "Maybrat" ], + "localnames": [ "Mai Brat" ], "name": "Mai Brat", "names": [ "Ajamaru", "Atinjo", "Ayamaru", "Brat", "Maibrat", "Maite", "Majbrat", "Maybrat", "Mey Brat" ], "region": "ID", @@ -8688,8 +9054,8 @@ "iso639_3": "aze", "localname": "azərbaycan", "localnames": [ "Azərbaycan dili", "Azərbaycanca" ], - "name": "Azerbaijani, North", - "names": [ "Azerbaijan", "Azerbaijani", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], + "name": "Azerbaijani", + "names": [ "Azerbaijan", "Azerbaijani, North", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], "region": "AZ", "regionname": "Azerbaijan", "regions": [ "AF", "AM", "DE", "EE", "GE", "IR", "KG", "KZ", "RU", "TM", "UA", "UZ" ], @@ -8698,14 +9064,14 @@ "tag": "az", "tags": [ "az-AZ", "az-Latn", "azj", "azj-AZ", "azj-Latn", "azj-Latn-AZ" ], "variants": [ "baku1926" ], - "windows": "az-Latn" + "windows": "az-Latn-AZ" }, { "full": "az-Brai-IR", "iana": [ "Azerbaijani" ], "iso639_3": "aze", - "name": "Azerbaijani, North", - "names": [ "Azerbaijan", "Azerbaijani", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], + "name": "Azerbaijani", + "names": [ "Azerbaijan", "Azerbaijani, North", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], "nophonvars": true, "region": "IR", "regionname": "Islamic Republic of Iran", @@ -8714,13 +9080,13 @@ "sldr": false, "tag": "az-Brai", "tags": [ "azj-Brai", "azj-Brai-IR" ], - "windows": "az-Brai" + "windows": "az-Brai-IR" }, { "full": "az-Cyrl-AZ", "iana": [ "Azerbaijani" ], "iso639_3": "aze", - "localname": "азәрбајҹан", + "localname": "azəri", "name": "Azerbaijani", "region": "AZ", "regionname": "Azerbaijan", @@ -8745,8 +9111,8 @@ "full": "az-Arab-IR", "iana": [ "Azerbaijani" ], "iso639_3": "aze", - "name": "Azerbaijani, North", - "names": [ "Azerbaijan", "Azerbaijani", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], + "name": "Azerbaijani", + "names": [ "Azerbaijan", "Azerbaijani, North", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], "region": "IR", "regionname": "Islamic Republic of Iran", "regions": [ "AF", "AM", "AZ", "DE", "EE", "GE", "KG", "KZ", "RU", "TM", "UA", "UZ" ], @@ -8761,8 +9127,8 @@ "iana": [ "Azerbaijani" ], "iso639_3": "aze", "localname": "азәрбајҹан", - "name": "Azerbaijani, North", - "names": [ "Azerbaijan", "Azerbaijani", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], + "name": "Azerbaijani", + "names": [ "Azerbaijan", "Azerbaijani, North", "Azerbaydzhani", "Azeri", "Azeri Turk", "Azərbaycan dili", "Azərbaycanca", "Turkler" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "AM", "DE", "EE", "GE", "IR", "KG", "KZ", "TM", "UA", "UZ" ], @@ -8791,7 +9157,9 @@ "iana": [ "South Azerbaijani" ], "iso639_3": "azb", "latnnames": [ "Azərbaycan dili", "Azərbaycanca" ], + "localname": "آذربایجان دیلی", "localnames": [ "آذربایجان دیلی", "آذربایجانجا" ], + "macrolang": "az", "name": "Azerbaijani, South", "names": [ "Azeri", "Azəricə", "South Azerbaijani", "Turk", "Turkmen", "Turkomen", "Türki" ], "region": "IR", @@ -8801,12 +9169,13 @@ "sldr": true, "tag": "azb", "tags": [ "azb-Arab", "azb-IR" ], - "windows": "azb-Arab" + "windows": "azb-Arab-IR" }, { "full": "azb-Cyrl-AZ", "iana": [ "South Azerbaijani" ], "iso639_3": "azb", + "macrolang": "az", "name": "Azerbaijani, South", "names": [ "Azeri", "Azəricə", "South Azerbaijani", "Turk", "Turkmen", "Turkomen", "Türki" ], "nophonvars": true, @@ -8816,8 +9185,23 @@ "script": "Cyrl", "sldr": false, "tag": "azb-Cyrl", - "tags": [ "azb-AZ" ], - "windows": "azb-Cyrl" + "windows": "azb-Cyrl-AZ" + }, + { + "full": "azb-Latn-AZ", + "iana": [ "South Azerbaijani" ], + "iso639_3": "azb", + "macrolang": "az", + "name": "Azerbaijani, South", + "names": [ "Azeri", "Azəricə", "South Azerbaijani", "Turk", "Turkmen", "Turkomen", "Türki" ], + "obsolete": true, + "region": "AZ", + "regionname": "Azerbaijan", + "regions": [ "CA", "IQ", "IR", "SY", "TR", "US" ], + "script": "Latn", + "sldr": false, + "tag": "azb-Latn", + "windows": "azb-Latn-AZ" }, { "full": "azd-Latn-MX", @@ -8884,9 +9268,9 @@ "full": "azo-Latn-CM", "iana": [ "Awing" ], "iso639_3": "azo", - "localnames": [ "Atembuluwe" ], + "localnames": [ "Mbɨ̂wiŋə" ], "name": "Awing", - "names": [ "Atembuluwe", "Awi", "Bambuluwe", "Mbwe’wi" ], + "names": [ "Atembuluwe", "Atembwә’wi", "Awi", "Bambuluwe", "Mbwe’wi", "Mbí̵ɨwiŋə", "Mbɨ̂wiŋə" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -8915,9 +9299,8 @@ "iana": [ "Highland Puebla Nahuatl" ], "iso639_3": "azz", "localname": "Nahuatlahtolli", - "localnames": [ "Nahuatlahtolli" ], "name": "Nahuatl, Highland Puebla", - "names": [ "Highland Puebla Nahuatl", "Mejicano", "Mejicano de Zacapoaxtla", "Mexicano", "Nahuatlahtolli", "Náhuat de la Sierra de Puebla", "Sierra Aztec", "Sierra Puebla Náhuatl", "Sierra de Zacapoaxtla Nahuatl", "Zacapoaxtla Náhuat" ], + "names": [ "Highland Puebla Nahuatl", "Mejicano", "Mejicano de Zacapoaxtla", "Mexicano", "Mexicano tlajtol", "Nahuatlahtolli", "Nauta", "Náhuatl de la Sierra Noreste de Puebla", "Náhuatl de la Sierra de Puebla", "Sierra Aztec", "Sierra Puebla Náhuatl", "Sierra de Zacapoaxtla Nahuatl", "Zacapoaxtla Náhuat" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -8943,6 +9326,22 @@ "tags": [ "ba-Cyrl", "ba-RU" ], "windows": "ba-Cyrl" }, + { + "full": "ba-Arab-RU", + "iana": [ "Bashkir" ], + "iso639_3": "bak", + "name": "Bashkort", + "names": [ "Bashkir", "Bashqort", "Basquort" ], + "nophonvars": true, + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "KG", "KZ", "TJ", "TM", "UA", "UZ" ], + "script": "Arab", + "sldr": false, + "tag": "ba-Arab", + "windows": "ba-Arab" + }, { "full": "baa-Latn-SB", "iana": [ "Babatana" ], @@ -8962,7 +9361,7 @@ "iana": [ "Bainouk-Gunyuño" ], "iso639_3": "bab", "name": "Bainouk-Gunyuño", - "names": [ "Bagnoun", "Bainounk-Gujaher", "Bainuk", "Banhum", "Banyuk", "Banyum", "Banyun", "Banyung", "Elomay", "Elunay", "Guñuun", "Gújááhár" ], + "names": [ "Bagnoun", "Bainounk-Gujaher", "Bainuk", "Banhum", "Banhun", "Banyuk", "Banyum", "Banyun", "Banyung", "Elomay", "Elunay", "Guñuun", "Gújááhár" ], "region": "GW", "regionname": "Guinea-Bissau", "script": "Latn", @@ -9022,8 +9421,9 @@ "full": "bag-Latn-CM", "iana": [ "Tuki" ], "iso639_3": "bag", + "localnames": [ "Tukí" ], "name": "Tuki", - "names": [ "Bacenga", "Baki", "Batchenga", "Betsinga", "Betzinga", "Ki", "Oki", "Osa Nanga", "Sanaga" ], + "names": [ "Bacenga", "Baki", "Batchenga", "Betsinga", "Betzinga", "Ki", "Oki", "Osa Nanga", "Sanaga", "Tukí" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -9036,8 +9436,8 @@ "full": "bah-Latn-BS", "iana": [ "Bahamas Creole English" ], "iso639_3": "bah", - "name": "Bahamas Creole English", - "names": [ "Bahamian Creole English", "Bahamian Dialect" ], + "name": "Bahamas English Creole", + "names": [ "Bahamas Creole English", "Bahamian Dialect", "Bahamian English Creole" ], "region": "BS", "regionname": "Bahamas", "regions": [ "US" ], @@ -9067,10 +9467,10 @@ "iana": [ "Baluchi" ], "iso639_3": "bal", "latnnames": [ "Balochi" ], - "localname": "Balochi, Southern", + "localname": "بلۆچی", "localnames": [ "بلوچی" ], - "name": "Balochi, Southern", - "names": [ "Baloci", "Baluchi", "Baluci", "Balóchi", "Makrani" ], + "name": "Baluchi", + "names": [ "Balochi, Southern", "Baloci", "Baluci", "Balóchi", "Makrani" ], "region": "PK", "regionname": "Pakistan", "regions": [ "AE", "IR", "OM", "SA" ], @@ -9084,10 +9484,10 @@ "full": "bal-Latn-PK", "iana": [ "Baluchi" ], "iso639_3": "bal", - "localname": "Balochi, Southern", + "localname": "Balóchi", "localnames": [ "Balóchi" ], - "name": "Balochi, Southern", - "names": [ "Baloci", "Baluchi", "Baluci", "Balóchi", "Makrani" ], + "name": "Baluchi", + "names": [ "Balochi, Southern", "Baloci", "Baluci", "Balóchi", "Makrani" ], "region": "PK", "regionname": "Pakistan", "regions": [ "AE", "AF", "IR", "OM", "SA" ], @@ -9162,17 +9562,34 @@ "full": "bap-Deva-NP", "iana": [ "Bantawa" ], "iso639_3": "bap", + "latnnames": [ "bantawa" ], + "localnames": [ "बान्‍तावा" ], "name": "Bantawa", "names": [ "An Yüng", "Bantaba", "Bantawa Dum", "Bantawa Rai", "Bantawa Yong", "Bantawa Yüng", "Bontawa", "Kirat Khambu", "Kirat Khambu Rai", "Kirawa Yüng", "Rai" ], "region": "NP", "regionname": "Nepal", "regions": [ "BT", "IN" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "bap", "tags": [ "bap-Deva", "bap-NP" ], "windows": "bap-Deva" }, + { + "full": "bap-Zzzz-IN-x-kiratrai", + "iana": [ "Bantawa" ], + "iso639_3": "bap", + "name": "Bantawa", + "names": [ "An Yüng", "Bantaba", "Bantawa Dum", "Bantawa Rai", "Bantawa Yong", "Bantawa Yüng", "Bontawa", "Kirat Khambu", "Kirat Khambu Rai", "Kirawa Yüng", "Rai" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Zzzz", + "sldr": false, + "tag": "bap-Zzzz-x-kiratrai", + "tags": [ "bap-IN-x-kiratrai" ], + "windows": "bap-Zzzz-x-kiratrai" + }, { "full": "bar-Latn-AT", "iana": [ "Bavarian" ], @@ -9194,9 +9611,9 @@ "iana": [ "Basa (Cameroon)" ], "iso639_3": "bas", "localname": "Ɓàsàa", - "localnames": [ "Basaa" ], + "localnames": [ "Hɔp ɓasàa" ], "name": "Basaa", - "names": [ "Basa", "Basa (Cameroon)", "Bassa", "Bicek", "Bikyek", "Bisaa", "Mbele", "Mee", "Mvele", "Northern Mbene", "Tupen" ], + "names": [ "Basa", "Basa (Cameroon)", "Bassa", "Bicek", "Bikyek", "Bisaa", "Hɔp ɓasàa", "Mbele", "Mee", "Mvele", "Northern Mbene", "Tupen", "Ɓasàa" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -9224,8 +9641,9 @@ "full": "bav-Latn-CM", "iana": [ "Vengo" ], "iso639_3": "bav", + "localnames": [ "Gháŋ Vəŋóo" ], "name": "Vengo", - "names": [ "Babungo", "Nge", "Ngo", "Nguu", "Ngwa", "Pengo", "Vengi", "Vengoo" ], + "names": [ "Babungo", "Ghang Vengo", "Gháŋ Vəŋóo", "Nge", "Ngo", "Nguu", "Ngwa", "Pengo", "Vengi", "Vengoo" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -9297,9 +9715,9 @@ "iana": [ "Baatonum" ], "iso639_3": "bba", "localname": "Baatonum", - "localnames": [ "Baatonum" ], + "localnames": [ "Baatɔnum" ], "name": "Baatonum", - "names": [ "Baatombu", "Baatonu", "Baatonun", "Baatonun-Kwara", "Barba", "Barganchi", "Bargawa", "Bargu", "Bariba", "Baruba", "Batonnum", "Batonu", "Berba", "Bogung", "Borgawa", "Borgu", "Burgu", "Zana" ], + "names": [ "Baatombu", "Baatonu", "Baatonun", "Baatonun-Kwara", "Baatɔnum", "Barba", "Barganchi", "Bargawa", "Bargu", "Bariba", "Baruba", "Batonnum", "Batonu", "Berba", "Bogung", "Borgawa", "Borgu", "Burgu", "Zana" ], "region": "BJ", "regionname": "Benin", "regions": [ "NG" ], @@ -9358,7 +9776,10 @@ "full": "bbd-Latn-PG", "iana": [ "Bau" ], "iso639_3": "bbd", - "name": "Bau", + "localname": "Fulumu", + "localnames": [ "Fulumu" ], + "name": "Fulumu", + "names": [ "Bau", "Efu", "Leh", "Muag", "Udun" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -9393,7 +9814,6 @@ "sldr": false, "tag": "bbf", "tags": [ "bbf-Latn", "bbf-PG" ], - "unwritten": true, "windows": "bbf-Latn" }, { @@ -9416,7 +9836,7 @@ "iana": [ "Bugan" ], "iso639_3": "bbh", "name": "Bugan", - "names": [ "Bengan", "Bogan", "Bugeng", "Hualo", "Hualuo", "Huazu", "Pakan", "Pukan", "Puqeng" ], + "names": [ "Bengan", "Ben’gan Yu", "Bogan", "Bugeng", "Hualo", "Hualuo", "Huazu", "Pakan", "Pukan", "Pukeng", "Puqeng" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -9460,31 +9880,31 @@ "full": "bbk-Latn-CM", "iana": [ "Babanki" ], "iso639_3": "bbk", + "localname": "Gaʼa-Kejom", "localnames": [ "Ga'a-Kejom" ], "name": "Babanki", "names": [ "Finge", "Ga’a Kedjom", "Ga’a-Kejom", "Kedjom", "Kejeng", "Kejom", "Kidzem", "Kidzom", "Nga-Kejom" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "bbk", "tags": [ "bbk-CM", "bbk-Latn" ], "windows": "bbk-Latn" }, { - "full": "bbl-Zyyy-GE", + "full": "bbl-Geor-GE", "iana": [ "Bats" ], "iso639_3": "bbl", "name": "Bats", "names": [ "Bac", "Batsaw", "Batsba", "Batsbi", "Batsbiitsy", "Batsi", "Tsova-Tush", "Tush" ], "region": "GE", "regionname": "Georgia", - "script": "Zyyy", + "script": "Geor", "sldr": false, "tag": "bbl", - "tags": [ "bbl-GE", "bbl-Zyyy" ], - "unwritten": true, - "windows": "bbl-Zyyy" + "tags": [ "bbl-GE", "bbl-Geor" ], + "windows": "bbl-Geor" }, { "full": "bbm-Latn-CD", @@ -9597,7 +10017,7 @@ "iana": [ "Mburku" ], "iso639_3": "bbt", "name": "Mburku", - "names": [ "Barke", "Barko", "Burkanawa", "Kariya", "Lipkawa", "Mburkanci", "Wudufu", "Wuufu" ], + "names": [ "Barke", "Barko", "Burkunawa", "Kariya", "Lipkawa", "Mburkanci", "Vә Mvәran", "Wudufu", "Wuufu" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -9626,6 +10046,7 @@ "full": "bbv-Latn-PG", "iana": [ "Karnai" ], "iso639_3": "bbv", + "localnames": [ "Karnai" ], "name": "Karnai", "region": "PG", "regionname": "Papua New Guinea", @@ -9655,7 +10076,7 @@ "iana": [ "Bubia" ], "iso639_3": "bbx", "name": "Bubia", - "names": [ "Bobe", "Bobea", "Bota", "Ewota", "Wovea", "Wuvia" ], + "names": [ "Bobe", "Bobea", "Bota", "Ewota", "Wovea", "Woweta", "Wuvia" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -9684,6 +10105,7 @@ "full": "bbz-Latn-TD", "iana": [ "Babalia Creole Arabic" ], "iso639_3": "bbz", + "macrolang": "ar", "name": "Babalia Creole Arabic", "region": "TD", "regionname": "Chad", @@ -9698,8 +10120,9 @@ "full": "bca-Latn-CN", "iana": [ "Central Bai" ], "iso639_3": "bca", + "localnames": [ "Baip‧ngvp‧zix" ], "name": "Bai, Central", - "names": [ "Báiyǔ", "Central Bai", "Labbu", "Minchia", "Minjia", "Minkia", "Nama", "Pai", "Yunnan Bai" ], + "names": [ "Baip‧ngvp‧zix", "Baizu", "Báiyǔ", "Central Bai", "Labbu", "Minchia", "Minjia", "Minkia", "Nama", "Pai", "Yunnan Bai" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -9712,10 +10135,8 @@ "full": "bca-Hani-CN", "iana": [ "Central Bai" ], "iso639_3": "bca", - "latnnames": [ "Baip‧ngvp‧zix" ], - "localnames": [ "白语" ], "name": "Bai, Central", - "names": [ "Báiyǔ", "Central Bai", "Labbu", "Minchia", "Minjia", "Minkia", "Nama", "Pai", "Yunnan Bai" ], + "names": [ "Baip‧ngvp‧zix", "Baizu", "Báiyǔ", "Central Bai", "Labbu", "Minchia", "Minjia", "Minkia", "Nama", "Pai", "Yunnan Bai" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -9795,7 +10216,6 @@ "sldr": false, "tag": "bcg", "tags": [ "bcg-GN", "bcg-Latn", "bgm", "bgm-GN", "bgm-Latn", "bgm-Latn-GN" ], - "unwritten": true, "windows": "bcg-Latn" }, { @@ -9817,8 +10237,9 @@ "full": "bci-Latn-CI", "iana": [ "Baoulé" ], "iso639_3": "bci", + "localnames": [ "Wawle" ], "name": "Baoulé", - "names": [ "Baoule", "Baule", "Baule-Ando", "Bawule" ], + "names": [ "Baoule", "Baule", "Baule-Ando", "Bawolɛ", "Bawule", "Wawle" ], "region": "CI", "regionname": "Côte d'Ivoire", "regions": [ "GH" ], @@ -9910,7 +10331,7 @@ "iana": [ "Bali (Democratic Republic of Congo)" ], "iso639_3": "bcp", "name": "Bali", - "names": [ "Baali", "Bali (Democratic Republic of Congo)", "Dhibali", "Kibaali", "Kibala", "Kibali", "Libaali", "Southeast Bwa" ], + "names": [ "Baali", "Bali (Democratic Republic of Congo)", "Dhibali", "Dibali", "Kibaali", "Kibala", "Kibali", "Libaali", "Southeast Bwa" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -9936,8 +10357,8 @@ "full": "bcq-Latn-ET", "iana": [ "Bench" ], "iso639_3": "bcq", - "latnnames": [ "Benc’non" ], - "localnames": [ "Bèntʂ nòn" ], + "localname": "Benc’non", + "localnames": [ "Benc’non" ], "name": "Bench", "region": "ET", "regionname": "Ethiopia", @@ -9965,6 +10386,7 @@ "full": "bcs-Latn-NG", "iana": [ "Kohumono" ], "iso639_3": "bcs", + "localnames": [ "Hohumono" ], "name": "Hohumono", "names": [ "Bahumono", "Bhahumono", "Ediba", "Ekumuru", "Humono", "Kohumono", "Ohumono" ], "region": "NG", @@ -10025,7 +10447,7 @@ "full": "bcw-Latn-CM", "iana": [ "Bana" ], "iso639_3": "bcw", - "localname": "kwəmà ká bàná", + "localname": "kwəma ka bana", "localnames": [ "kwəmà ká bàná" ], "name": "Bana", "names": [ "Baza", "Ka-Bana", "Koma", "Mizeran", "Parole des Bana", "kwəmà ká bàná" ], @@ -10041,8 +10463,10 @@ "full": "bcy-Latn-NG", "iana": [ "Bacama" ], "iso639_3": "bcy", + "localname": "Ɓwaare", + "localnames": [ "Ɓwaare" ], "name": "Bacama", - "names": [ "Abacama", "Bachama", "Bashama", "Bashamma", "Besema", "Bwaatye", "Bwareba", "Bwatiye", "Gboare" ], + "names": [ "Abacama", "Bacama-Bwatiye", "Bachama", "Bashama", "Bashamma", "Besema", "Bwaatye", "Bwareba", "Bwatiye", "Gboare", "Kwaa–Ɓwaare", "Ɓwaare" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -10069,8 +10493,9 @@ "full": "bda-Latn-SN", "iana": [ "Bayot" ], "iso639_3": "bda", + "localnames": [ "Jola Bayote" ], "name": "Bayot", - "names": [ "Baiot", "Baiote", "Bayote", "Bayotte", "Jola Bayote", "Kugere-Kuxinge" ], + "names": [ "Baiot", "Baiote", "Bayoot", "Bayote", "Bayotte", "Jola Bayote", "Jola-Bayoti", "Kugere-Kuxinge" ], "region": "SN", "regionname": "Senegal", "regions": [ "GM", "GW" ], @@ -10099,8 +10524,8 @@ "full": "bdc-Latn-CO", "iana": [ "Emberá-Baudó" ], "iso639_3": "bdc", - "name": "Emberá-Baudó", - "names": [ "Baudó", "Catrú", "Embena", "Embera", "Epena" ], + "name": "Embera Baudó", + "names": [ "Baudó", "Catrú", "Embena", "Embera", "Embera Bedea Baudó", "Emberá-Baudó", "Epena" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -10175,9 +10600,9 @@ "iana": [ "Baka (South Sudan)" ], "iso639_3": "bdh", "localname": "baká e", - "localnames": [ "baká e" ], + "localnames": [ "Tara Baká" ], "name": "Baka", - "names": [ "Baka (South Sudan)", "Tara Baaka", "Tara Baka", "baká e" ], + "names": [ "Baka (South Sudan)", "Tara Baaka", "Tara Baká" ], "region": "SS", "regionname": "South Sudan", "regions": [ "CD" ], @@ -10250,8 +10675,9 @@ "full": "bdm-Latn-TD", "iana": [ "Buduma" ], "iso639_3": "bdm", + "localnames": [ "Yedǝnami" ], "name": "Buduma", - "names": [ "Boudouma", "Yadena", "Yedana", "Yedima", "Yedina", "Yidana", "Yidena" ], + "names": [ "Boudouma", "Yadena", "Yedana", "Yedima", "Yedina", "Yedǝnami", "Yidana", "Yidena" ], "region": "TD", "regionname": "Chad", "regions": [ "CM", "NG" ], @@ -10295,8 +10721,9 @@ "full": "bdp-Latn-TZ", "iana": [ "Bende" ], "iso639_3": "bdp", + "localnames": [ "Sibhēnde" ], "name": "Bende", - "names": [ "Babende", "Kibende", "Si’bende" ], + "names": [ "Babende", "Bhabhēnde", "Kibende", "Sibhēnde", "Si’bende" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -10309,10 +10736,10 @@ "full": "bdq-Latn-VN", "iana": [ "Bahnar" ], "iso639_3": "bdq", - "localname": "Ba Na", + "localname": "Bahnar", "localnames": [ "Bahnar" ], "name": "Bahnar", - "names": [ "Bana" ], + "names": [ "Ba Na" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "US" ], @@ -10331,6 +10758,7 @@ "names": [ "Bajau", "Bajau Sama", "Land Bajaw", "Sama", "West Coast Bajao", "West Coast Bajau", "West Coast Bajaw" ], "region": "MY", "regionname": "Malaysia", + "regions": [ "BN" ], "script": "Latn", "sldr": false, "tag": "bdr", @@ -10357,8 +10785,10 @@ "full": "bdt-Latn-CF", "iana": [ "Bokoto" ], "iso639_3": "bdt", + "localnames": [ "Bhogoto" ], + "macrolang": "gba", "name": "Bhogoto", - "names": [ "Bhokoto", "Bogodo", "Bogoto", "Bokodo", "Bokoto", "Bokpoto" ], + "names": [ "Bhokoto", "Bogodo", "Bogoto", "Bokodo", "Bokoto", "Bokpoto", "Gbäyä-Bogoto", "Gbāyā ɓòkòtò", "Ɓòkòtò" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -10386,6 +10816,8 @@ "full": "bdv-Orya-IN", "iana": [ "Bodo Parja" ], "iso639_3": "bdv", + "latnnames": [ "Bodo Parja" ], + "localnames": [ "ବଡ଼ ପରଜା" ], "name": "Bodo Parja", "names": [ "Bodo Paraja", "Harja", "Jhaliya", "Jharia", "Jhodia Parja", "Parajhi", "Parja", "Parjhi", "Parji", "Paroja", "Poroja", "Sodia Parja" ], "region": "IN", @@ -10401,7 +10833,7 @@ "iana": [ "Baham" ], "iso639_3": "bdw", "name": "Baham", - "names": [ "Patimuni" ], + "names": [ "Bahaam", "Mbahaam maaq", "Patimuni" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -10431,7 +10863,7 @@ "iana": [ "Bandjalang" ], "iso639_3": "bdy", "name": "Bandjalang", - "names": [ "Bandjelang", "Bogganger", "Bundala", "Bundjalung" ], + "names": [ "Bandjelang", "Bogganger", "Bundala", "Bundjalung", "Middle Clarence Bandjalang" ], "obsolete": true, "region": "AU", "regionname": "Australia", @@ -10476,6 +10908,21 @@ "variants": [ "1959acad", "tarask" ], "windows": "be" }, + { + "full": "be-Arab-BY", + "iana": [ "Belarusian" ], + "iso639_3": "bel", + "name": "Belarusian", + "names": [ "Belarusan", "Belorussian", "Bielorussian", "Byelorussian", "White Russian", "White Ruthenian" ], + "nophonvars": true, + "region": "BY", + "regionname": "Belarus", + "regions": [ "AZ", "CA", "CZ", "DE", "EE", "KG", "KZ", "LT", "LV", "PL", "RU", "TJ", "TM", "UA", "US", "UZ" ], + "script": "Arab", + "sldr": false, + "tag": "be-Arab", + "windows": "be-Arab" + }, { "full": "be-Brai-BY", "iana": [ "Belarusian" ], @@ -10554,8 +11001,9 @@ "full": "bec-Latn-CM", "iana": [ "Iceve-Maci" ], "iso639_3": "bec", + "localnames": [ "Iceve" ], "name": "Iceve-Maci", - "names": [ "Bacheve", "Bechere", "Becheve", "Icheve", "Ocebe", "Ochebe", "Ocheve", "Utse", "Utser", "Utseu" ], + "names": [ "Iceve", "Icheve", "Utse", "Utser", "Utseu" ], "region": "CM", "regionname": "Cameroon", "regions": [ "NG" ], @@ -10599,7 +11047,7 @@ "iana": [ "Benabena" ], "iso639_3": "bef", "name": "Benabena", - "names": [ "Bena" ], + "names": [ "Bena", "Bena Bena", "Holupaga", "Holupaka" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -10628,7 +11076,7 @@ "full": "beh-Latn-BJ", "iana": [ "Biali" ], "iso639_3": "beh", - "localname": "Biali", + "localname": "Byali", "localnames": [ "Byali" ], "name": "Biali", "names": [ "Berba", "Bialaba", "Bieri", "Bjerb", "Bjeri", "Burba", "Byali" ], @@ -10646,7 +11094,7 @@ "iana": [ "Bekati'" ], "iso639_3": "bei", "name": "Bakati’", - "names": [ "Bakati Nyam", "Bakati Riok", "Bakatik Dayak", "Bakatiq", "Bekati", "Bekati'" ], + "names": [ "Bakati Nyam", "Bakati Riok", "Bakatik Dayak", "Bakatiq", "Bekati", "Bekati'", "Riuk Bakati’" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -10670,12 +11118,13 @@ "sldr": false, "tag": "bej", "tags": [ "bej-Arab", "bej-SD" ], - "windows": "bej-Arab" + "windows": "bej-Arab-SD" }, { "full": "bej-Latn-ER", "iana": [ "Beja", "Bedawiyet" ], "iso639_3": "bej", + "localname": "Bidhaawyeet", "localnames": [ "Bidhaawyeet", "Tubdhaawi" ], "name": "Bedawiyet", "names": [ "Bedawi", "Bedawiye", "Bedawye", "Bedja", "Bedwi", "Bedya", "Bedàwie", "Beja", "Beḍauye", "Bidhaaweet", "Bidhaawyeet", "Biďaːwyéːt", "Lobat", "Tibďaːwyě", "To Bedawiat", "To Bedawie", "Tu Bdhaawi", "Tu-Bedawie", "Tubdhaawi" ], @@ -10686,7 +11135,7 @@ "sldr": true, "tag": "bej-Latn", "tags": [ "bej-ER" ], - "windows": "bej-Latn" + "windows": "bej-Latn-ER" }, { "full": "bek-Latn-PG", @@ -10710,7 +11159,7 @@ "localname": "Ichibemba", "localnames": [ "IciBemba" ], "name": "Bemba", - "names": [ "Bemba (Zambia)", "Chibemba", "Chiwemba", "Cibemba", "Ichibemba", "IciBemba", "Icibemba", "Wemba" ], + "names": [ "Bemba (Zambia)", "Chibemba", "Chiwemba", "Cibemba", "Ichibemba", "IciBemba", "Wemba" ], "region": "ZM", "regionname": "Zambia", "regions": [ "CD" ], @@ -10725,7 +11174,7 @@ "iana": [ "Bemba (Zambia)" ], "iso639_3": "bem", "name": "Bemba", - "names": [ "Bemba (Zambia)", "Chibemba", "Chiwemba", "Cibemba", "Ichibemba", "IciBemba", "Icibemba", "Wemba" ], + "names": [ "Bemba (Zambia)", "Chibemba", "Chiwemba", "Cibemba", "Ichibemba", "IciBemba", "Wemba" ], "nophonvars": true, "region": "ZM", "regionname": "Zambia", @@ -10739,8 +11188,9 @@ "full": "beo-Latn-PG", "iana": [ "Beami" ], "iso639_3": "beo", - "name": "Beami", - "names": [ "Bedamini", "Bedamuni", "Biami", "Mougulu", "Piame" ], + "localnames": [ "Bedamuni" ], + "name": "Bedamuni", + "names": [ "Beami", "Bedamini", "Biami", "Mougulu", "Piame" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -10801,7 +11251,7 @@ "localname": "ʼbhɛtɩgbʋʋ ʼmö", "localnames": [ "ʼbhɛtɩgbʋʋ ʼmö" ], "name": "Bété, Guiberoua", - "names": [ "Central Bété", "Guiberoua Béte", "Western Bété", "ʼbhɛtegbooʼmë", "ʼbhɛtɩgbʋʋ ʼmö" ], + "names": [ "Bhete", "Central Bété", "Guiberoua Béte", "Western Bété", "ʼbhɛtegbooʼmë", "ʼbhɛtɩgbʋʋ ʼmö" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -10828,8 +11278,9 @@ "full": "bev-Latn-CI", "iana": [ "Daloa Bété" ], "iso639_3": "bev", + "localnames": [ "\"Bhɛtɩ Gbʋ" ], "name": "Bété, Daloa", - "names": [ "Daloa Bété", "Daloua Bété", "Northern Bété" ], + "names": [ "Bhete", "Daloa Bété", "Daloua Bété", "Northern Bété", "“Bhɛtɩ Gbʋ" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -10903,7 +11354,7 @@ "iana": [ "Bena (Tanzania)" ], "iso639_3": "bez", "localname": "Hibena", - "localnames": [ "Ekibena" ], + "localnames": [ "Kibena" ], "name": "Bena", "names": [ "Bena (Tanzania)", "EkiBene", "Ekibena", "Ikibena", "Kibena", "ekiBena" ], "region": "TZ", @@ -10918,8 +11369,8 @@ "full": "bfa-Latn-SS", "iana": [ "Bari" ], "iso639_3": "bfa", + "localnames": [ "Bari" ], "name": "Bari", - "names": [ "Beri" ], "region": "SS", "regionname": "South Sudan", "regions": [ "UG" ], @@ -10934,7 +11385,6 @@ "iana": [ "Bari" ], "iso639_3": "bfa", "name": "Bari", - "names": [ "Beri" ], "nophonvars": true, "region": "SS", "regionname": "South Sudan", @@ -10949,8 +11399,10 @@ "full": "bfb-Deva-IN", "iana": [ "Pauri Bareli" ], "iso639_3": "bfb", + "latnnames": [ "Bārēlā pāvarī" ], + "localnames": [ "बारेला पावरी" ], "name": "Bareli, Pauri", - "names": [ "Bareli", "Barewali", "Barli", "Pauri Bareli" ], + "names": [ "Barel Pauri", "Bareli", "Barewali", "Barli", "Pauri Bareli" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -10964,7 +11416,7 @@ "iana": [ "Panyi Bai", "Northern Bai" ], "iso639_3": "bfc", "name": "Bai, Panyi", - "names": [ "Bijiang Bai", "Lan-Bi Bai", "Leme", "Lemei", "Lemo", "Northern Bai", "Panyi", "Panyi Bai" ], + "names": [ "Bijiang Bai", "Lan-Bi Bai", "Leme", "Lemei", "Lemo", "Lushui", "Northern Bai", "Panyi", "Panyi Bai" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -11009,7 +11461,7 @@ "iana": [ "Bofi" ], "iso639_3": "bff", "name": "Bofi", - "names": [ "Boffi" ], + "names": [ "Boffi", "Ɓòfì" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -11037,8 +11489,9 @@ "full": "bfh-Latn-PG", "iana": [ "Blafe" ], "iso639_3": "bfh", + "localnames": [ "Mblafe", "Ránmo" ], "name": "Blafe", - "names": [ "Blafe Wonana", "Indorodoro", "Mblafe", "Tonda" ], + "names": [ "Blafe Wonana", "Indorodoro", "Mblafe", "Ránmo", "Tonda" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -11055,6 +11508,7 @@ "names": [ "BSL" ], "region": "GB", "regionname": "United Kingdom", + "regions": [ "IM" ], "script": "Zxxx", "sldr": false, "tag": "bfi", @@ -11067,12 +11521,13 @@ "iso639_3": "bfi", "name": "British Sign Language", "names": [ "BSL" ], + "nophonvars": true, "region": "GB", "regionname": "United Kingdom", "script": "Zzzz", "sldr": false, - "tag": "bfi-x-stokoe", - "tags": [ "bfi-GB-x-stokoe", "bfi-Zzzz-x-stokoe", "sgn-bfi-GB-x-stokoe", "sgn-bfi-x-stokoe" ], + "tag": "bfi-Zzzz-x-stokoe", + "tags": [ "bfi-GB-x-stokoe", "sgn-bfi-GB-x-stokoe" ], "windows": "bfi-Zzzz-x-stokoe" }, { @@ -11081,7 +11536,7 @@ "iso639_3": "bfj", "localnames": [ "Chufie'" ], "name": "Bafanji", - "names": [ "Bafangi", "Bafanyi", "Chufie’", "Chuufi", "Nchufie" ], + "names": [ "Bafangi", "Bafanyi", "Chufie’", "Chuufi", "Chuufiè", "Nchufie" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -11124,8 +11579,9 @@ "full": "bfm-Latn-CM", "iana": [ "Mmen" ], "iso639_3": "bfm", + "localnames": [ "Ewâmnèə Mə̂yn" ], "name": "Mmen", - "names": [ "Bafmen", "Bafmeng", "Bafoumeng", "Bafumen", "Mme" ], + "names": [ "Bafmen", "Bafmeng", "Bafoumeng", "Bafumen", "Ewâmnèə Mə̂yn", "Mme" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -11231,12 +11687,13 @@ "iso639_3": "bfq", "name": "Badaga", "names": [ "Badag", "Badagu", "Baduga", "Badugu", "Vadagu" ], + "nophonvars": true, "region": "IN", "regionname": "India", "script": "Zzzz", "sldr": false, - "tag": "bfq-x-badaga", - "tags": [ "bfq-IN-x-badaga", "bfq-Zzzz-x-badaga" ], + "tag": "bfq-Zzzz-x-badaga", + "tags": [ "bfq-IN-x-badaga" ], "windows": "bfq-Zzzz-x-badaga" }, { @@ -11257,8 +11714,9 @@ "full": "bfs-Latn-CN", "iana": [ "Southern Bai" ], "iso639_3": "bfs", + "localnames": [ "Baip‧ngvp‧zix" ], "name": "Bai, Southern", - "names": [ "Southern Bai" ], + "names": [ "Baip‧ngvp‧zix", "Baizu", "Southern Bai" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -11271,10 +11729,8 @@ "full": "bfs-Hani-CN", "iana": [ "Southern Bai" ], "iso639_3": "bfs", - "latnnames": [ "Baip‧ngvp‧zix" ], - "localnames": [ "白语" ], "name": "Bai, Southern", - "names": [ "Southern Bai" ], + "names": [ "Baip‧ngvp‧zix", "Baizu", "Southern Bai" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -11323,14 +11779,15 @@ "iso639_3": "bft", "name": "Balti", "names": [ "Baltistani", "Bhoti of Baltistan", "Bhotia of Baltistan", "Byltae", "Sbalt", "Sbalti" ], + "nophonvars": true, "obsolete": true, "region": "PK", "regionname": "Pakistan", "regions": [ "IN" ], "script": "Zzzz", "sldr": false, - "tag": "bft-x-baltia", - "tags": [ "bft-PK-x-baltia", "bft-Zzzz-x-baltia" ], + "tag": "bft-Zzzz-x-baltia", + "tags": [ "bft-PK-x-baltia" ], "windows": "bft-Zzzz-x-baltia" }, { @@ -11387,7 +11844,6 @@ "sldr": false, "tag": "bfx", "tags": [ "bfx-Latn", "bfx-PH" ], - "unwritten": true, "windows": "bfx-Latn" }, { @@ -11412,7 +11868,7 @@ "iana": [ "Mahasu Pahari" ], "iso639_3": "bfz", "name": "Pahari, Mahasu", - "names": [ "Mahasu Pahari", "Mahasui" ], + "names": [ "Mahasu Pahari", "Mahasui", "Mahasuvi" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -11456,6 +11912,22 @@ "tag": "bg-Brai", "windows": "bg-Brai" }, + { + "full": "bg-Cyrs-BG", + "iana": [ "Bulgarian" ], + "iso639_3": "bul", + "name": "Bulgarian", + "names": [ "Pomak" ], + "nophonvars": true, + "obsolete": true, + "region": "BG", + "regionname": "Bulgaria", + "regions": [ "BA", "BE", "CA", "CY", "CZ", "DE", "FI", "GR", "HR", "HU", "IL", "KZ", "LY", "MD", "PL", "RO", "RS", "RU", "SK", "TR", "UA", "US" ], + "script": "Cyrs", + "sldr": false, + "tag": "bg-Cyrs", + "windows": "bg-Cyrs" + }, { "full": "bg-Latn-BG", "iana": [ "Bulgarian" ], @@ -11504,13 +11976,14 @@ "iana": [ "Haryanvi" ], "iso639_3": "bgc", "latnnames": [ "Hariyāṇvī" ], + "localname": "हरियाणवी", "localnames": [ "हरियाणवी" ], "name": "Haryanvi", "names": [ "Bangaru", "Banger", "Bangri", "Bangru", "Chamarwa", "Desari", "Hariani", "Hariyani", "Haryani", "Jatu" ], "region": "IN", "regionname": "India", "script": "Deva", - "sldr": false, + "sldr": true, "tag": "bgc", "tags": [ "bgc-Deva", "bgc-IN" ], "windows": "bgc-Deva" @@ -11558,7 +12031,6 @@ "sldr": false, "tag": "bgf", "tags": [ "bgf-CM", "bgf-Latn" ], - "unwritten": true, "windows": "bgf-Latn" }, { @@ -11580,8 +12052,8 @@ "full": "bgi-Latn-PH", "iana": [ "Giangan" ], "iso639_3": "bgi", - "name": "Giangan", - "names": [ "Atto", "Bagobo", "Bagobo-Guiangga", "Clata", "Diangan", "Eto", "Guanga", "Gulanga", "Jangan" ], + "name": "Bagobo-Klata", + "names": [ "Bagobo", "Bagobo-Guiangga", "Clata", "Diangan", "Giangan", "Guanga", "Gulanga", "Jangan", "Klata" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -11611,7 +12083,7 @@ "iana": [ "Bit", "Buxinhua" ], "iso639_3": "bgk", "name": "Bit", - "names": [ "Boxing", "Buxing", "Buxing Khmu", "Buxinhua", "Kha Bet", "Khabit", "Khbit", "Pasing", "Phsin", "Phsing", "Pu Sing" ], + "names": [ "Boxing", "Buxin", "Buxing", "Buxing Khmu", "Buxinhua", "Kha Bet", "Khabit", "Khbit", "Pasing", "Phsin", "Phsing", "Pu Sing" ], "region": "LA", "regionname": "Lao People's Democratic Republic", "regions": [ "CN" ], @@ -11642,6 +12114,7 @@ "iso639_3": "bgn", "latnnames": [ "Balochi" ], "localnames": [ "بلوچی" ], + "macrolang": "bal", "name": "Balochi, Western", "names": [ "Baloci", "Baluchi", "Baluci", "Western Balochi", "Yarahmadza" ], "region": "PK", @@ -11651,12 +12124,13 @@ "sldr": false, "tag": "bgn", "tags": [ "bgn-Arab", "bgn-PK" ], - "windows": "bgn-Arab" + "windows": "bgn-Arab-PK" }, { "full": "bgn-Cyrl-TM", "iana": [ "Western Balochi" ], "iso639_3": "bgn", + "macrolang": "bal", "name": "Balochi, Western", "names": [ "Baloci", "Baluchi", "Baluci", "Western Balochi", "Yarahmadza" ], "nophonvars": true, @@ -11667,7 +12141,7 @@ "sldr": false, "tag": "bgn-Cyrl", "tags": [ "bgn-TM" ], - "windows": "bgn-Cyrl" + "windows": "bgn-Cyrl-TM" }, { "full": "bgo-Latn-GN", @@ -11690,6 +12164,7 @@ "iso639_3": "bgp", "latnnames": [ "Balochi" ], "localnames": [ "بلوچی" ], + "macrolang": "bal", "name": "Balochi, Eastern", "names": [ "Balochi", "Baloci", "Baluchi", "Baluci", "Eastern Balochi", "Eastern Hill Balochi" ], "region": "PK", @@ -11707,6 +12182,7 @@ "iso639_3": "bgq", "latnnames": [ "Bagri" ], "localnames": [ "बागड़ी" ], + "macrolang": "raj", "name": "Bagri", "names": [ "Bagari", "Bagria", "Bagris", "Bahgri", "Baorias", "Bawri" ], "region": "IN", @@ -11722,9 +12198,9 @@ "full": "bgr-Latn-IN", "iana": [ "Bawm Chin" ], "iso639_3": "bgr", - "localnames": [ "Bawm" ], + "localnames": [ "Bawm hawlh" ], "name": "Chin, Bawm", - "names": [ "Bawm", "Bawm Chin", "Bawn", "Bawng", "Boam", "Bom", "Laisaw", "Laizo", "Langkay" ], + "names": [ "Bawm", "Bawm Chin", "Bawm hawlh", "Bawn", "Bawng", "Boam", "Bom", "Laisaw", "Laizo", "Langkay" ], "region": "IN", "regionname": "India", "regions": [ "BD", "MM" ], @@ -11739,9 +12215,9 @@ "iana": [ "Tagabawa" ], "iso639_3": "bgs", "localname": "Bagobo", - "localnames": [ "Bagobo" ], + "localnames": [ "Tagabawà" ], "name": "Tagabawa", - "names": [ "Bagobo", "Tagabawa Bagobo", "Tagabawa Manobo" ], + "names": [ "Bagobo", "Bagóbô", "Tagabawa Bagobo", "Tagabawa Manobo", "Tagabawà", "Tagabawà Bagóbô" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -11928,6 +12404,22 @@ "unwritten": true, "windows": "bhc-Latn" }, + { + "full": "bhd-Deva-IN", + "iana": [ "Bhadrawahi" ], + "iso639_3": "bhd", + "latnnames": [ "Bhadrawahi" ], + "localnames": [ "भद्रवाही" ], + "name": "Bhadrawahi", + "names": [ "Baderwali", "Badrohi", "Bahi", "Bhadarwahi", "Bhaderbhai Jamu", "Bhaderwali Pahari", "Bhadrava", "Bhadri" ], + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "bhd", + "tags": [ "bhd-Deva", "bhd-IN" ], + "windows": "bhd-Deva" + }, { "full": "bhd-Arab-IN", "iana": [ "Bhadrawahi" ], @@ -11943,20 +12435,18 @@ "windows": "bhd-Arab" }, { - "full": "bhd-Deva-IN", + "full": "bhd-Takr-IN", "iana": [ "Bhadrawahi" ], "iso639_3": "bhd", - "latnnames": [ "Bhadrawahi" ], - "localnames": [ "भद्रवाही" ], "name": "Bhadrawahi", "names": [ "Baderwali", "Badrohi", "Bahi", "Bhadarwahi", "Bhaderbhai Jamu", "Bhaderwali Pahari", "Bhadrava", "Bhadri" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Takr", "sldr": false, - "tag": "bhd-Deva", - "windows": "bhd-Deva" + "tag": "bhd-Takr", + "windows": "bhd-Takr" }, { "full": "bhe-Arab-PK", @@ -12060,15 +12550,16 @@ "sldr": false, "tag": "bhi", "tags": [ "bhi-Deva", "bhi-IN" ], - "unwritten": true, "windows": "bhi-Deva" }, { "full": "bhj-Deva-NP", "iana": [ "Bahing" ], "iso639_3": "bhj", + "latnnames": [ "bahing" ], + "localnames": [ "बाहिङ" ], "name": "Bahing", - "names": [ "Baying", "Bayung", "Ikke lo", "Kiranti-Bayung", "Pai Lo", "Radu lo" ], + "names": [ "Baying", "Bayung", "Ikke lo", "Kiranti-Bayung", "Pai lo", "Radu lo", "Rai" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -12127,14 +12618,15 @@ "iana": [ "Bhojpuri" ], "iso639_3": "bho", "latnnames": [ "Bhōjpurī" ], + "localname": "भोजपुरी", "localnames": [ "भोजपुरी" ], "name": "Bhojpuri", - "names": [ "Bajpuri", "Bhojapuri", "Bhozpuri", "Deswali", "Khotla", "Piscimas" ], + "names": [ "Bajpuri", "Bhojapuri", "Bhozpuri", "Khotla", "Piscimas" ], "region": "IN", "regionname": "India", "regions": [ "MU", "NP" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "bho", "tags": [ "bh-Deva", "bh-Deva-IN", "bho-Deva", "bho-IN" ], "windows": "bho-Deva" @@ -12144,7 +12636,7 @@ "iana": [ "Bhojpuri" ], "iso639_3": "bho", "name": "Bhojpuri", - "names": [ "Bajpuri", "Bhojapuri", "Bhozpuri", "Deswali", "Khotla", "Piscimas" ], + "names": [ "Bajpuri", "Bhojapuri", "Bhozpuri", "Khotla", "Piscimas" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -12159,8 +12651,9 @@ "full": "bhp-Latn-ID", "iana": [ "Bima" ], "iso639_3": "bhp", + "localnames": [ "Nggahi Mbojo" ], "name": "Bima", - "names": [ "Bimanese" ], + "names": [ "Bimanese", "Nggahi Mbojo" ], "obsolete": true, "region": "ID", "regionname": "Indonesia", @@ -12190,6 +12683,7 @@ "iana": [ "Bara Malagasy" ], "iso639_3": "bhr", "localnames": [ "Bara Malagasy" ], + "macrolang": "mg", "name": "Malagasy, Bara", "names": [ "Bara Malagasy" ], "region": "MG", @@ -12204,7 +12698,7 @@ "full": "bhs-Latn-CM", "iana": [ "Buwal" ], "iso639_3": "bhs", - "localnames": [ "Buwal" ], + "localnames": [ "Ma Buwal" ], "name": "Buwal", "names": [ "Bual", "Gadala", "Ma Buwal" ], "region": "CM", @@ -12216,26 +12710,42 @@ "windows": "bhs-Latn" }, { - "full": "bht-Zyyy-IN", + "full": "bht-Deva-IN", "iana": [ "Bhattiyali" ], "iso639_3": "bht", + "latnnames": [ "Bhaṭiyālī" ], + "localnames": [ "भटियाली" ], "name": "Bhattiyali", "names": [ "Bhateali", "Bhatiali Pahari", "Bhatiyali", "Pahari" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "bht", - "tags": [ "bht-IN", "bht-Zyyy" ], + "tags": [ "bht-Deva", "bht-IN" ], + "windows": "bht-Deva" + }, + { + "full": "bht-Takr-IN", + "iana": [ "Bhattiyali" ], + "iso639_3": "bht", + "name": "Bhattiyali", + "names": [ "Bhateali", "Bhatiali Pahari", "Bhatiyali", "Pahari" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Takr", + "sldr": false, + "tag": "bht-Takr", "unwritten": true, - "windows": "bht-Zyyy" + "windows": "bht-Takr" }, { "full": "bhu-Deva-IN", "iana": [ "Bhunjia" ], "iso639_3": "bhu", "name": "Bhunjia", - "names": [ "Bhumjiya", "Bhunjiya", "Bunjia" ], + "names": [ "Bhumia", "Bhumjiya", "Bhunjiya", "Bunjia" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -12278,6 +12788,7 @@ "iana": [ "Bhalay" ], "iso639_3": "bhx", "name": "Bhalay", + "names": [ "Balahi", "Balai" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -12351,7 +12862,7 @@ "full": "bib-Latn-BF", "iana": [ "Bissa", "Bisa" ], "iso639_3": "bib", - "localname": "Bisa", + "localname": "Bɩsa", "localnames": [ "Bɩsa" ], "name": "Bisa", "names": [ "Bissa", "Bissa-Barka", "Boussanse", "Busansi", "Bɩsa" ], @@ -12364,25 +12875,11 @@ "tags": [ "bib-BF", "bib-Latn" ], "windows": "bib-Latn" }, - { - "full": "bic-Latn-PG", - "iana": [ "Bikaru" ], - "iso639_3": "bic", - "name": "Bikaru", - "names": [ "Bugalu", "Pikaru" ], - "region": "PG", - "regionname": "Papua New Guinea", - "script": "Latn", - "sldr": false, - "tag": "bic", - "tags": [ "bic-Latn", "bic-PG" ], - "unwritten": true, - "windows": "bic-Latn" - }, { "full": "bid-Latn-TD", "iana": [ "Bidiyo" ], "iso639_3": "bid", + "localnames": [ "Bidiya" ], "name": "Bidiyo", "names": [ "Bidio", "Bidiya", "Bidiyo-Waana", "Bidyo", "’Bidio", "’Bidiyo" ], "region": "TD", @@ -12412,7 +12909,7 @@ "iana": [ "Biafada" ], "iso639_3": "bif", "name": "Biafada", - "names": [ "Beafada", "Bedfola", "Biafar", "Bidyola", "Dfola", "Fada" ], + "names": [ "Beafada", "Bedfola", "Biafar", "Bidjola", "Bidyola", "Dfola", "Fada" ], "region": "GW", "regionname": "Guinea-Bissau", "script": "Latn", @@ -12437,28 +12934,13 @@ "tags": [ "big-Latn", "big-PG" ], "windows": "big-Latn" }, - { - "full": "bij-Latn-NG", - "iana": [ "Vaghat-Ya-Bijim-Legeri" ], - "iso639_3": "bij", - "name": "Vaghat-Ya-Bijim-Legeri", - "names": [ "Vwhat" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "bij", - "tags": [ "bij-Latn", "bij-NG" ], - "unwritten": true, - "windows": "bij-Latn" - }, { "full": "bik-Latn-PH", "iana": [ "Bikol" ], "iso639_3": "bik", "localnames": [ "Bikol" ], - "name": "Bikol, Central", - "names": [ "Bicol", "Bicol-Naga", "Bicolano", "Bikol", "Bikol Naga", "Bikolano", "Bikolnon", "Central Bicolano", "Coastal Bikol" ], + "name": "Bikol", + "names": [ "Bicol", "Bicol-Naga", "Bicolano", "Bikol Naga", "Bikol, Central", "Bikolano", "Bikolnon", "Central Bicolano", "Coastal Bikol", "Coastal-Naga Bikol" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -12471,8 +12953,8 @@ "full": "bik-Brai-PH", "iana": [ "Bikol" ], "iso639_3": "bik", - "name": "Bikol, Central", - "names": [ "Bicol", "Bicol-Naga", "Bicolano", "Bikol", "Bikol Naga", "Bikolano", "Bikolnon", "Central Bicolano", "Coastal Bikol" ], + "name": "Bikol", + "names": [ "Bicol", "Bicol-Naga", "Bicolano", "Bikol Naga", "Bikol, Central", "Bikolano", "Bikolnon", "Central Bicolano", "Coastal Bikol", "Coastal-Naga Bikol" ], "nophonvars": true, "region": "PH", "regionname": "Philippines", @@ -12501,8 +12983,9 @@ "full": "bim-Latn-GH", "iana": [ "Bimoba" ], "iso639_3": "bim", + "localnames": [ "Mɔɔr" ], "name": "Bimoba", - "names": [ "Moar", "Moor" ], + "names": [ "Moar", "Moor", "Mɔɔr" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -12532,7 +13015,7 @@ "iana": [ "Nai" ], "iso639_3": "bio", "name": "Nai", - "names": [ "Biaka" ], + "names": [ "Biaka", "Nai-Biaka" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -12545,6 +13028,7 @@ "full": "bip-Latn-CD", "iana": [ "Bila" ], "iso639_3": "bip", + "localnames": [ "NULL" ], "name": "Bila", "names": [ "Ebila", "Forest Bira", "KiBila", "Western Bila" ], "region": "CD", @@ -12553,7 +13037,6 @@ "sldr": false, "tag": "bip", "tags": [ "bip-CD", "bip-Latn" ], - "unwritten": true, "windows": "bip-Latn" }, { @@ -12581,7 +13064,7 @@ "script": "Latn", "sldr": false, "tag": "bir", - "tags": [ "bir-Latn", "bir-PG" ], + "tags": [ "bic", "bic-Latn", "bic-Latn-PG", "bic-PG", "bir-Latn", "bir-PG" ], "windows": "bir-Latn" }, { @@ -12589,7 +13072,7 @@ "iana": [ "Berinomo" ], "iso639_3": "bit", "name": "Berinomo", - "names": [ "Bitara" ], + "names": [ "Berinmo", "Bitara" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -12618,7 +13101,7 @@ "full": "biv-Latn-GH", "iana": [ "Southern Birifor" ], "iso639_3": "biv", - "localname": "Birifor, Southern", + "localname": "Birfoɔr", "localnames": [ "Birfoɔr" ], "name": "Birifor, Southern", "names": [ "Berfoɔr", "Birfoɔr", "Birifo", "Birifor", "Bɩ rfʊ ɔ r", "Ghana Birifor", "Lobi", "Southern Birifor" ], @@ -12658,6 +13141,7 @@ "sldr": false, "tag": "bix", "tags": [ "bix-IN", "bix-Zyyy" ], + "unwritten": true, "windows": "bix-Zyyy" }, { @@ -12679,7 +13163,7 @@ "iana": [ "Baloi" ], "iso639_3": "biz", "name": "Baloi", - "names": [ "Baato Baloi", "Boloi", "Loi", "Loi-Likila", "Rebu" ], + "names": [ "Baato Baloi", "Boloi", "Loi", "Rebu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -12738,7 +13222,7 @@ "iana": [ "Biao-Jiao Mien" ], "iso639_3": "bje", "name": "Biao-Jiao Mien", - "names": [ "Ao Biao", "Biao Chao", "Byau Min", "Dongshan Biao Min" ], + "names": [ "Ao Biao", "Biao Chao", "Biao-Jiao", "Biaomin", "Byau Min", "Dongshan Biao Min" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -12766,8 +13250,9 @@ "full": "bjg-Latn-GW", "iana": [ "Bidyogo" ], "iso639_3": "bjg", - "name": "Bidyogo", - "names": [ "Bijago", "Bijogo", "Bijougot", "Bijuga", "Budjago", "Bugago" ], + "localnames": [ "Kajoukou" ], + "name": "Bijagó", + "names": [ "Bidjago", "Bidyago", "Bidyogo", "Bijogo", "Bijougot", "Bijuga", "Budjago", "Bugago", "Kajoukou" ], "region": "GW", "regionname": "Guinea-Bissau", "script": "Latn", @@ -12792,21 +13277,34 @@ "tags": [ "bjh-Latn", "bjh-PG" ], "windows": "bjh-Latn" }, + { + "full": "bji-Latn-ET", + "iana": [ "Burji" ], + "iso639_3": "bji", + "localnames": [ "Dhaashate" ], + "name": "Burji", + "region": "ET", + "regionname": "Ethiopia", + "regions": [ "KE" ], + "script": "Latn", + "sldr": false, + "tag": "bji", + "tags": [ "bji-ET", "bji-Latn" ], + "windows": "bji-Latn" + }, { "full": "bji-Ethi-ET", "iana": [ "Burji" ], "iso639_3": "bji", - "latnnames": [ "Burji" ], "localname": "ቡረጊ", - "localnames": [ "ቡረጊ" ], "name": "Burji", + "nophonvars": true, "region": "ET", "regionname": "Ethiopia", "regions": [ "KE" ], "script": "Ethi", "sldr": true, - "tag": "bji", - "tags": [ "bji-ET", "bji-Ethi" ], + "tag": "bji-Ethi", "windows": "bji-Ethi" }, { @@ -12856,25 +13354,27 @@ "windows": "bjl-Latn" }, { - "full": "bjm-Zyyy-IQ", + "full": "bjm-Arab-IQ", "iana": [ "Bajelani" ], "iso639_3": "bjm", "name": "Bajelani", "names": [ "Bajalani", "Bajoran", "Bejwan", "Chichamachu", "Gurani" ], "region": "IQ", "regionname": "Iraq", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "bjm", - "tags": [ "bjm-IQ", "bjm-Zyyy" ], - "windows": "bjm-Zyyy" + "tags": [ "bjm-Arab", "bjm-IQ" ], + "windows": "bjm-Arab" }, { "full": "bjn-Latn-ID", "iana": [ "Banjar" ], "iso639_3": "bjn", + "localnames": [ "Bahasa Banjar" ], + "macrolang": "ms", "name": "Banjar", - "names": [ "Bandjarese", "Banjar Malay", "Banjar Melau", "Banjarese", "Benjar", "Labuhan" ], + "names": [ "Bahasa Banjar", "Bandjarese", "Banjar Malay", "Banjar Melau", "Banjarese", "Benjar", "Jaku Banjar", "Labuhan" ], "region": "ID", "regionname": "Indonesia", "regions": [ "MY" ], @@ -12890,8 +13390,9 @@ "iso639_3": "bjn", "latnnames": [ "Banjar" ], "localnames": [ "بنجر" ], + "macrolang": "ms", "name": "Banjar", - "names": [ "Bandjarese", "Banjar Malay", "Banjar Melau", "Banjarese", "Benjar", "Labuhan" ], + "names": [ "Bahasa Banjar", "Bandjarese", "Banjar Malay", "Banjar Melau", "Banjarese", "Benjar", "Jaku Banjar", "Labuhan" ], "nophonvars": true, "region": "ID", "regionname": "Indonesia", @@ -12920,6 +13421,8 @@ "full": "bjp-Latn-PG", "iana": [ "Fanamaket" ], "iso639_3": "bjp", + "localname": "Fanamaket", + "localnames": [ "Fanamaket" ], "name": "Fanamaket", "names": [ "Maket", "Tangga" ], "region": "PG", @@ -12934,10 +13437,10 @@ "full": "bjr-Latn-PG", "iana": [ "Binumarien" ], "iso639_3": "bjr", - "localname": "Binumarien", - "localnames": [ "Binumarien" ], + "localname": "Afaqinna ufa", + "localnames": [ "Afaqinna ufa" ], "name": "Binumarien", - "names": [ "Binamarir", "Binumaria" ], + "names": [ "Afaqinna ufa", "Binamarir", "Binumaria" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -12987,6 +13490,7 @@ "script": "Arab", "sldr": true, "tag": "bjt-Arab", + "unwritten": true, "windows": "bjt-Arab" }, { @@ -13024,7 +13528,9 @@ "full": "bjw-Latn-CI", "iana": [ "Bakwé" ], "iso639_3": "bjw", + "localnames": [ "Bakʋɛwii" ], "name": "Bakwé", + "names": [ "Bakʋɛwii" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -13037,8 +13543,8 @@ "full": "bjx-Latn-PH", "iana": [ "Banao Itneg" ], "iso639_3": "bjx", - "name": "Itneg, Banao", - "names": [ "Banao", "Banao Itneg", "Banaw", "Bhanaw Tinggian", "Itneg", "Timggian", "Tinguian" ], + "name": "Kalinga, Vanaw", + "names": [ "Banao", "Banao Itneg", "Banaw", "Bhanaw Tinggian", "Itneg", "Timggian", "Tinguian", "Vanaw", "Vyanaw" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -13097,6 +13603,7 @@ "full": "bkc-Latn-CM", "iana": [ "Baka (Cameroon)" ], "iso639_3": "bkc", + "localname": "baka", "localnames": [ "baka" ], "name": "Baka", "names": [ "Baka (Cameroon)", "Bayaka", "Bebayaga", "Bebayaka", "Bibaya", "Pygmees Baka", "Pygmees de l’Est", "baka" ], @@ -13130,7 +13637,7 @@ "iana": [ "Beeke" ], "iso639_3": "bkf", "name": "Beeke", - "names": [ "Beke", "Ibeeke" ], + "names": [ "Babeeke", "Beke", "Ibeeke" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -13160,8 +13667,9 @@ "full": "bkh-Latn-CM", "iana": [ "Bakoko" ], "iso639_3": "bkh", + "localnames": [ "Bakoko" ], "name": "Bakoko", - "names": [ "Basoo" ], + "names": [ "Basoo", "Koko" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -13201,18 +13709,32 @@ "unwritten": true, "windows": "bkj-Latn" }, + { + "full": "bkk-Tibt-IN", + "iana": [ "Brokskat" ], + "iso639_3": "bkj", + "name": "Brokskat", + "region": "IN", + "regionname": "India", + "script": "Tibt", + "sldr": false, + "tag": "bkk", + "tags": [ "bkk-IN", "bkk-Tibt" ], + "windows": "bkk-Tibt" + }, { "full": "bkk-Zzzz-IN-x-baltia", "iana": [ "Brokskat" ], "iso639_3": "bkk", "name": "Brokskat", "names": [ "Brok-skad", "Brokpa", "Brokpa of Dah-Hanu", "Dokskat", "Kyango", "Minaro" ], + "nophonvars": true, "region": "IN", "regionname": "India", "script": "Zzzz", "sldr": false, - "tag": "bkk-x-baltia", - "tags": [ "bkk-IN-x-baltia", "bkk-Zzzz-x-baltia" ], + "tag": "bkk-Zzzz-x-baltia", + "tags": [ "bkk-IN-x-baltia" ], "windows": "bkk-Zzzz-x-baltia" }, { @@ -13288,7 +13810,6 @@ "sldr": false, "tag": "bkp", "tags": [ "bkp-CD", "bkp-Latn" ], - "unwritten": true, "windows": "bkp-Latn" }, { @@ -13341,6 +13862,7 @@ "full": "bkt-Latn-CD", "iana": [ "Boloki" ], "iso639_3": "bkt", + "localnames": [ "Boloki" ], "name": "Boloki", "names": [ "Baloki", "Boleki", "Buluki", "River Ruki" ], "region": "CD", @@ -13379,10 +13901,26 @@ "tag": "bku-Buhd", "windows": "bku-Buhd" }, + { + "full": "bku-Hano-PH", + "iana": [ "Buhid" ], + "iso639_3": "bku", + "name": "Buhid", + "names": [ "Bangon", "Batangan", "Buhid-Mangyan", "Bukil" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "script": "Hano", + "sldr": false, + "tag": "bku-Hano", + "windows": "bku-Hano" + }, { "full": "bkv-Latn-NG", "iana": [ "Bekwarra" ], "iso639_3": "bkv", + "localname": "Ebekwara", + "localnames": [ "Ebekwara" ], "name": "Bekwarra", "names": [ "Bekwara", "Bekworra", "Ebekwara", "Yakoro" ], "region": "NG", @@ -13397,8 +13935,9 @@ "full": "bkw-Latn-CG", "iana": [ "Bekwel" ], "iso639_3": "bkw", + "localnames": [ "Bɛkwyel" ], "name": "Bekwel", - "names": [ "Bakwele", "Bakwil", "Bekwie", "Bekwil", "Okpele" ], + "names": [ "Bakwele", "Bakwil", "Bekwie", "Bekwil", "Bɛkwyel", "Okpele" ], "region": "CG", "regionname": "Congo", "regions": [ "CM", "GA" ], @@ -13489,6 +14028,7 @@ "full": "blb-Latn-SB", "iana": [ "Bilua" ], "iso639_3": "blb", + "localnames": [ "Bilua" ], "name": "Bilua", "names": [ "Mbilua", "Vella Lavella" ], "region": "SB", @@ -13533,8 +14073,9 @@ "full": "ble-Latn-GW", "iana": [ "Balanta-Kentohe" ], "iso639_3": "ble", - "name": "Balanta-Kentohe", - "names": [ "Alante", "Balanda", "Balant", "Balanta", "Balante", "Ballante", "Belante", "Brassa", "Bulanda", "Frase" ], + "localnames": [ "Fraase" ], + "name": "Balanta", + "names": [ "Alante", "Balanda", "Balant", "Balanta-Kentohe", "Balante", "Ballante", "Belante", "Brassa", "Bulanda", "Fraase", "Frase" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "GM" ], @@ -13559,21 +14100,6 @@ "tags": [ "blf-ID", "blf-Latn" ], "windows": "blf-Latn" }, - { - "full": "blg-Latn-MY", - "iana": [ "Balau" ], - "iso639_3": "blg", - "name": "Balau", - "names": [ "Bala’u" ], - "region": "MY", - "regionname": "Malaysia", - "script": "Latn", - "sldr": false, - "tag": "blg", - "tags": [ "blg-Latn", "blg-MY" ], - "unwritten": true, - "windows": "blg-Latn" - }, { "full": "blh-Latn-LR", "iana": [ "Kuwaa" ], @@ -13592,8 +14118,9 @@ "full": "bli-Latn-CD", "iana": [ "Bolia" ], "iso639_3": "bli", + "localnames": [ "Lolia" ], "name": "Bolia", - "names": [ "Bokoki", "Bulia" ], + "names": [ "Bokoki", "Bulia", "Lia-Ntomba", "Lolia" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -13621,6 +14148,8 @@ "full": "blk-Mymr-MM", "iana": [ "Pa'o Karen", "Pa'O" ], "iso639_3": "blk", + "latnnames": [ "Pa-O", "Pa-O Kham Thar" ], + "localnames": [ "ပအိုဝ်", "ပအိုဝ်ႏခမ်းသား" ], "name": "Pa’o", "names": [ "Black Karen", "Northern Taungthu", "Pa Oh", "Pa'O", "Pa'o Karen", "Pa-O", "Pa-Oh", "Pa-U", "Pa’O", "Pa’o Karen", "Taungtu" ], "region": "MM", @@ -13637,6 +14166,7 @@ "iana": [ "Biloxi" ], "iso639_3": "bll", "name": "Biloxi", + "names": [ "Tanêksąyaa ade" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -13650,9 +14180,9 @@ "full": "blm-Latn-SS", "iana": [ "Beli (South Sudan)" ], "iso639_3": "blm", - "localnames": [ "Beli" ], + "localnames": [ "'Bëlї" ], "name": "Beli", - "names": [ "Behli", "Beili", "Beli (South Sudan)", "Jur Beli", "’Beli" ], + "names": [ "Behli", "Beili", "Beli (South Sudan)", "Jur Beli", "’Beli", "’Bëlї" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -13665,6 +14195,7 @@ "full": "bln-Latn-PH", "iana": [ "Southern Catanduanes Bikol" ], "iso639_3": "bln", + "macrolang": "bik", "name": "Bikol, Southern Catanduanes", "names": [ "Coastal-Virac Bikol", "Southern Catanduanes Bikol", "Southern Catanduanes Bikolano", "Virac" ], "region": "PH", @@ -13729,22 +14260,22 @@ "iana": [ "Blang" ], "iso639_3": "blr", "name": "Blang", - "names": [ "Bulang", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], + "names": [ "Bulang", "Bulang Yu", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], "region": "CN", "regionname": "China", "regions": [ "MM", "TH" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "blr", "tags": [ "blr-CN", "blr-Latn" ], - "windows": "blr-Latn" + "windows": "blr-Latn-CN" }, { "full": "blr-Latn-MM-x-pangpung", "iana": [ "Pangpung Plang" ], "iso639_3": "blr", "name": "Blang", - "names": [ "Bulang", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Pangpung Plang", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], + "names": [ "Bulang", "Bulang Yu", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Pangpung Plang", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], "region": "MM", "regionname": "Myanmar", "regions": [ "CN", "TH" ], @@ -13752,14 +14283,14 @@ "sldr": false, "tag": "blr-Latn-x-pangpung", "tags": [ "blr-MM-x-pangpung" ], - "windows": "blr-Latn-x-pangpung" + "windows": "blr-Latn-MM-x-pangpung" }, { "full": "blr-Tale-CN", "iana": [ "Blang" ], "iso639_3": "blr", "name": "Blang", - "names": [ "Bulang", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], + "names": [ "Bulang", "Bulang Yu", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -13767,14 +14298,14 @@ "script": "Tale", "sldr": false, "tag": "blr-Tale", - "windows": "blr-Tale" + "windows": "blr-Tale-CN" }, { "full": "blr-Thai-TH", "iana": [ "Blang" ], "iso639_3": "blr", "name": "Blang", - "names": [ "Bulang", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], + "names": [ "Bulang", "Bulang Yu", "Hkawa", "Kawa", "Khon Doi", "Kontoi", "K’ala", "K’wa", "Plang", "Pula", "Pulang", "Sen Chun", "Wa" ], "nophonvars": true, "region": "TH", "regionname": "Thailand", @@ -13783,7 +14314,7 @@ "sldr": false, "tag": "blr-Thai", "tags": [ "blr-TH" ], - "windows": "blr-Thai" + "windows": "blr-Thai-TH" }, { "full": "bls-Latn-ID", @@ -13813,7 +14344,7 @@ "sldr": true, "tag": "blt", "tags": [ "blt-Tavt", "blt-VN" ], - "windows": "blt-Tavt" + "windows": "blt-Tavt-VN" }, { "full": "blt-Laoo-LA", @@ -13829,12 +14360,13 @@ "sldr": false, "tag": "blt-Laoo", "tags": [ "blt-LA" ], - "windows": "blt-Laoo" + "windows": "blt-Laoo-LA" }, { "full": "blt-Latn-VN", "iana": [ "Tai Dam" ], "iso639_3": "blt", + "localname": "ꞌTáy", "localnames": [ "'Táy", "ʼTáy Ðăm" ], "name": "Tai Dam", "names": [ "Black Tai", "Hei Dai", "Jinping Dai", "Tai Do", "Tai Noir", "Tailam", "Tailon", "Thai Den", "Thái Den", "ʼTáy Ðăm", "’Táy" ], @@ -13844,14 +14376,14 @@ "script": "Latn", "sldr": true, "tag": "blt-Latn", - "windows": "blt-Latn" + "windows": "blt-Latn-VN" }, { "full": "blv-Latn-AO", "iana": [ "Kibala", "Bolo" ], "iso639_3": "blv", "name": "Kibala", - "names": [ "Bolo", "Ipala", "Kimbundu de Kwanza Sul", "Ngoya", "Pala" ], + "names": [ "Bolo", "Ipala", "Kibala-Ngoya", "Kimbundu de Kwanza Sul", "Ngoya", "Pala", "Quibala" ], "region": "AO", "regionname": "Angola", "script": "Latn", @@ -13866,9 +14398,9 @@ "iana": [ "Balangao" ], "iso639_3": "blw", "localname": "Finarangao", - "localnames": [ "Finarangao" ], + "localnames": [ "Finarangaw" ], "name": "Balangao", - "names": [ "Balangao Bontoc", "Balangaw", "Farangao", "Finarangao" ], + "names": [ "Balangao Bontoc", "Balangaw", "Farangao", "Farangaw", "Finarangao", "Finarangaw", "Finerangaw", "Iferangaw", "iFarangao", "iFarangaw" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -13887,7 +14419,7 @@ "region": "PH", "regionname": "Philippines", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "blx", "tags": [ "blx-Latn", "blx-PH" ], "windows": "blx-Latn" @@ -14107,6 +14639,8 @@ "full": "bmj-Deva-NP", "iana": [ "Bote-Majhi" ], "iso639_3": "bmj", + "latnnames": [ "bote" ], + "localnames": [ "बोटे" ], "name": "Bote", "names": [ "Bot", "Bote-Majhi", "Pakhe-Bote", "Pani-Bote" ], "region": "NP", @@ -14153,6 +14687,7 @@ "iana": [ "Northern Betsimisaraka Malagasy" ], "iso639_3": "bmm", "localnames": [ "Betsimisaraka" ], + "macrolang": "mg", "name": "Malagasy, Northern Betsimisaraka", "names": [ "Antavaratra", "Betsimisaraka", "Northern Betsimisaraka Malagasy" ], "region": "MG", @@ -14216,7 +14751,7 @@ "localname": "Boomu", "localnames": [ "Boomu" ], "name": "Bomu", - "names": [ "Bo", "Bobo", "Bobo Oule", "Bobo Wule", "Boomu", "Boore", "Bore", "Boré", "Mu Boomu", "Western Bobo Oule", "Western Bobo Wule", "Western Bwamu", "Western Red" ], + "names": [ "Bo", "Bobo", "Bobo Oule", "Bobo Wule", "Boomu", "Boore", "Boré", "Mu Boomu", "Western Bobo Oule", "Western Bobo Wule", "Western Bwamu", "Western Red" ], "region": "ML", "regionname": "Mali", "regions": [ "BF" ], @@ -14231,9 +14766,9 @@ "iana": [ "Muinane" ], "iso639_3": "bmr", "localname": "Muinane", - "localnames": [ "Muinane" ], + "localnames": [ "Muìnánɨ" ], "name": "Muinane", - "names": [ "Bora-Muinane", "Muename", "Muinana", "Muinani" ], + "names": [ "Bora-Muinane", "Muename", "Muinana", "Muinani", "Muìnánɨ" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -14261,7 +14796,7 @@ "iana": [ "Biao Mon" ], "iso639_3": "bmt", "name": "Biao Mon", - "names": [ "Biao Mien", "Biao-Mian", "Biaoman", "Changping", "Min Yao", "Sida Min Yao" ], + "names": [ "Biao Mien", "Biao-Mian", "Biaoman", "Byaumon", "Changping", "Min Yao", "Sida Min Yao" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -14362,7 +14897,7 @@ "suppress": true, "tag": "bn", "tags": [ "bn-BD", "bn-Beng" ], - "windows": "bn" + "windows": "bn-BD" }, { "full": "bn-Brai-BD", @@ -14377,7 +14912,7 @@ "script": "Brai", "sldr": false, "tag": "bn-Brai", - "windows": "bn-Brai" + "windows": "bn-Brai-BD" }, { "full": "bn-Beng-IN", @@ -14392,9 +14927,8 @@ "regionname": "India", "script": "Beng", "sldr": true, - "suppress": true, "tag": "bn-IN", - "windows": "bn-IN" + "windows": "bn-Beng-IN" }, { "full": "bn-Newa-NP", @@ -14411,7 +14945,7 @@ "sldr": false, "tag": "bn-Newa", "tags": [ "bn-NP" ], - "windows": "bn-Newa" + "windows": "bn-Newa-NP" }, { "full": "bna-Latn-ID", @@ -14446,12 +14980,12 @@ "full": "bnc-Latn-PH", "iana": [ "Bontok" ], "iso639_3": "bnc", - "name": "Bontok, Central", - "names": [ "Bontoc", "Bontoc Igorot", "Bontok", "Central Bontoc", "Kali" ], + "name": "Bontok", + "names": [ "Bontoc", "Bontoc Igorot", "Bontok, Central", "Central Bontoc", "Kali" ], "region": "PH", "regionname": "Philippines", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "bnc", "tags": [ "bkb", "bkb-Latn", "bkb-Latn-PH", "bkb-PH", "bnc-Latn", "bnc-PH", "lbk", "lbk-Latn", "lbk-Latn-PH", "lbk-PH" ], "windows": "bnc-Latn" @@ -14505,6 +15039,8 @@ "full": "bng-Latn-GQ", "iana": [ "Benga" ], "iso639_3": "bng", + "localname": "Benga", + "localnames": [ "Benga" ], "name": "Benga", "names": [ "Boumba", "Ndowe" ], "region": "GQ", @@ -14521,7 +15057,7 @@ "iana": [ "Bangi" ], "iso639_3": "bni", "name": "Bangi", - "names": [ "Bobangi", "Bubangi", "Bungi", "Dzamba", "Lobobangi", "Rebu", "Zamba" ], + "names": [ "Bobangi", "Bubangi", "Bungi", "Lobobangi" ], "obsolete": true, "region": "CD", "regionname": "The Democratic Republic of the Congo", @@ -14537,7 +15073,7 @@ "iana": [ "Eastern Tawbuid" ], "iso639_3": "bnj", "name": "Bangon", - "names": [ "Barangan", "Batangan", "Binatangan", "Eastern Tawbuid", "Fanawbuid", "Suri", "Tabuid", "Taubuid", "Tiron" ], + "names": [ "Barangan", "Batangan", "Binatangan", "Eastern Tawbuid", "Fanawbuid", "Silangan bahagi Tawbuid", "Suri", "Tabuid", "Taubuid", "Tiron" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -14579,8 +15115,10 @@ "full": "bnm-Latn-GQ", "iana": [ "Batanga" ], "iso639_3": "bnm", - "name": "Batanga", - "names": [ "Banoho", "Banoo", "Bano’o", "Bapuku", "Batanga-Banda", "Noho", "Nohu", "Noku", "Puku" ], + "localname": "Batanga", + "localnames": [ "Batanga" ], + "name": "Bapuku", + "names": [ "Bapoko", "Bapuu", "Batanga", "Naka", "Puku" ], "region": "GQ", "regionname": "Equatorial Guinea", "regions": [ "CM" ], @@ -14628,7 +15166,7 @@ "localname": "Bola-Bakovi", "localnames": [ "Bola-Bakovi" ], "name": "Bola", - "names": [ "Bakovi", "Bola-Bakovi" ], + "names": [ "Bakovi", "Bola-Bakovi", "Talasea", "Valupai" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -14780,7 +15318,7 @@ "localname": "བོད་སྐད་", "localnames": [ "བོད་ཡིག།", "བོད་སྐད།" ], "name": "Tibetan, Central", - "names": [ "Bhotia", "Bod", "Bod Skad", "Central Tibetan", "Lhasa Tibetan", "Phoke", "Pohbetian", "Poke", "Skad", "Tebilian", "Tibate", "Tibetan", "Wei", "Weizang", "Zang", "Zang Wen" ], + "names": [ "Bhotia", "Bod", "Bod Skad", "Central Tibetan", "Lhasa Tibetan", "Literary Tibetan", "Phoke", "Pohbetian", "Poke", "Skad", "Tebilian", "Tibate", "Tibetan", "Wei", "Weizang", "Zang", "Zang Wen" ], "region": "CN", "regionname": "China", "regions": [ "CH", "NO", "NP", "TW", "US" ], @@ -14788,7 +15326,7 @@ "sldr": true, "tag": "bo", "tags": [ "bo-CN", "bo-Tibt" ], - "windows": "bo-Tibt" + "windows": "bo-Tibt-CN" }, { "full": "bo-Tibt-IN", @@ -14810,21 +15348,21 @@ "iana": [ "Tibetan" ], "iso639_3": "bod", "name": "Tibetan, Central", - "names": [ "Bhotia", "Lhasa Tibetan", "Phoke", "Tibetan", "Wei", "Weizang", "Zang" ], + "names": [ "Bhotia", "Lhasa Tibetan", "Literary Tibetan", "Phoke", "Tibetan", "Wei", "Weizang", "Zang" ], "region": "CN", "regionname": "China", "script": "Latn", "sldr": false, "tag": "bo-Latn", "variants": [ "pinyin" ], - "windows": "bo-Latn" + "windows": "bo-Latn-CN" }, { "full": "bo-Marc-CN", "iana": [ "Tibetan" ], "iso639_3": "bod", "name": "Tibetan, Central", - "names": [ "Bhotia", "Bod", "Bod Skad", "Central Tibetan", "Lhasa Tibetan", "Phoke", "Pohbetian", "Poke", "Skad", "Tebilian", "Tibate", "Tibetan", "Wei", "Weizang", "Zang", "Zang Wen" ], + "names": [ "Bhotia", "Bod", "Bod Skad", "Central Tibetan", "Lhasa Tibetan", "Literary Tibetan", "Phoke", "Pohbetian", "Poke", "Skad", "Tebilian", "Tibate", "Tibetan", "Wei", "Weizang", "Zang", "Zang Wen" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -14832,14 +15370,14 @@ "script": "Marc", "sldr": false, "tag": "bo-Marc", - "windows": "bo-Marc" + "windows": "bo-Marc-CN" }, { "full": "bo-Phag-CN", "iana": [ "Tibetan" ], "iso639_3": "bod", "name": "Tibetan, Central", - "names": [ "Bhotia", "Bod", "Bod Skad", "Central Tibetan", "Lhasa Tibetan", "Phoke", "Pohbetian", "Poke", "Skad", "Tebilian", "Tibate", "Tibetan", "Wei", "Weizang", "Zang", "Zang Wen" ], + "names": [ "Bhotia", "Bod", "Bod Skad", "Central Tibetan", "Lhasa Tibetan", "Literary Tibetan", "Phoke", "Pohbetian", "Poke", "Skad", "Tebilian", "Tibate", "Tibetan", "Wei", "Weizang", "Zang", "Zang Wen" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -14847,14 +15385,14 @@ "script": "Phag", "sldr": false, "tag": "bo-Phag", - "windows": "bo-Phag" + "windows": "bo-Phag-CN" }, { "full": "boa-Latn-PE", "iana": [ "Bora" ], "iso639_3": "boa", - "localname": "Booraa", - "localnames": [ "Booraa", "Miamunaa" ], + "localname": "Miamunaa", + "localnames": [ "Miamunaa" ], "name": "Bora", "names": [ "Booraa", "Bora-Miranya", "Boro", "Meamuyna", "Miamunaa", "Miamuna’a", "Miranas", "Miranha", "Miranya", "Miraña", "Mirañã", "Miriña", "Mirãnha", "Mirãnia", "Mïamuna" ], "region": "PE", @@ -15004,6 +15542,7 @@ "full": "bom-Latn-NG", "iana": [ "Berom" ], "iso639_3": "bom", + "localname": "Cèn Bèrom", "localnames": [ "Cèn Bèrom" ], "name": "Berom", "names": [ "Afango", "Akuut", "Baho", "Berum", "Birom", "Cèn Bèrom", "Gbang", "Kibbo", "Kibbun", "Kibo", "Kibyen", "Lêm Berom", "Sine" ], @@ -15085,7 +15624,7 @@ "regionname": "Brazil", "regions": [ "BO" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "bor", "tags": [ "bor-BR", "bor-Latn" ], "windows": "bor-Latn" @@ -15096,7 +15635,7 @@ "iso639_3": "bot", "localnames": [ "Ndüü Böngö" ], "name": "Bongo", - "names": [ "Bungu", "Dor" ], + "names": [ "Dor", "Ndüü Böngö" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -15126,7 +15665,7 @@ "localname": "Tuwuli", "localnames": [ "Tuwuli" ], "name": "Tuwuli", - "names": [ "Bawuli", "Bowili", "Bowiri", "Liwuli", "Siwuri", "Tuwili" ], + "names": [ "Bawuli", "Boviri", "Bowili", "Bowiri", "Bowuri", "Luwuli", "Tuwili" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -15154,7 +15693,7 @@ "full": "box-Latn-BF", "iana": [ "Buamu" ], "iso639_3": "box", - "localname": "Bwamu", + "localname": "Buamu", "localnames": [ "Buamu" ], "name": "Buamu", "names": [ "Bobo", "Bomo", "Bouamou", "Bwaba", "Bwamu", "Eastern Bobo Oule", "Eastern Bobo Wule", "Red Bobo" ], @@ -15185,6 +15724,7 @@ "full": "boz-Latn-ML", "iana": [ "Tiéyaxo Bozo" ], "iso639_3": "boz", + "localname": "Tieyaxo", "localnames": [ "Tieyaxo" ], "name": "Bozo, Tieyaxo", "names": [ "Boso", "Tie", "Tiemaxo", "Tieyaxo", "Tigemaxo", "Tiguémakho", "Tiéyakho", "Tiéyaxo Bozo", "Tyeyaxo", "Tégué" ], @@ -15239,6 +15779,21 @@ "unwritten": true, "windows": "bpb-Latn" }, + { + "full": "bpc-Latn-CM", + "iana": [ "Mbuk" ], + "iso639_3": "bpc", + "name": "Mbuk", + "names": [ "Bembogho", "Mbogho", "Mbyuk" ], + "region": "CM", + "regionname": "Cameroon", + "script": "Latn", + "sldr": false, + "tag": "bpc", + "tags": [ "bpc-CM", "bpc-Latn" ], + "unwritten": true, + "windows": "bpc-Latn" + }, { "full": "bpd-Latn-CF", "iana": [ "Banda-Banda" ], @@ -15254,6 +15809,21 @@ "unwritten": true, "windows": "bpd-Latn" }, + { + "full": "bpe-Latn-PG", + "iana": [ "Bauni" ], + "iso639_3": "bpe", + "localnames": [ "Bauni" ], + "name": "Bauni", + "names": [ "Barapu", "Barupu", "Warapu" ], + "region": "PG", + "regionname": "Papua New Guinea", + "script": "Latn", + "sldr": false, + "tag": "bpe", + "tags": [ "bpe-Latn", "bpe-PG", "wra", "wra-Latn", "wra-Latn-PG", "wra-PG" ], + "windows": "bpe-Latn" + }, { "full": "bpg-Latn-ID", "iana": [ "Bonggo" ], @@ -15302,8 +15872,9 @@ "full": "bpj-Latn-CD", "iana": [ "Binji" ], "iso639_3": "bpj", + "localnames": [ "Bubindji" ], "name": "Binji", - "names": [ "Bindji", "Mbagani" ], + "names": [ "Bindji", "Bubindji", "Mbagani" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -15437,8 +16008,9 @@ "full": "bps-Latn-PH", "iana": [ "Sarangani Blaan" ], "iso639_3": "bps", + "localnames": [ "Blaan" ], "name": "Blaan, Sarangani", - "names": [ "Balud", "Bilaan", "Blaan", "Sarangani Blaan", "Tumanao" ], + "names": [ "Balud", "Bilaan", "Blaan", "Sarangani Bilaan", "Sarangani Blaan", "Tumanao" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -15583,9 +16155,9 @@ "iana": [ "Boko (Benin)", "Boo" ], "iso639_3": "bqc", "localname": "Bo'o", - "localnames": [ "Bo'o" ], + "localnames": [ "Bòó" ], "name": "Boko", - "names": [ "Boko (Benin)", "Bokonya", "Boo", "Bo’o" ], + "names": [ "Boko (Benin)", "Bokonya", "Boo", "Bòó", "Bo’o" ], "region": "BJ", "regionname": "Benin", "regions": [ "NG" ], @@ -15659,7 +16231,7 @@ "iana": [ "Baima" ], "iso639_3": "bqh", "name": "Baima", - "names": [ "Bai Ma", "Pe" ], + "names": [ "Bai Ma", "Pe", "To Pu" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -15693,7 +16265,7 @@ "localname": "Gubanjalay", "localnames": [ "Gubanjalay", "Gújjolaay Eegimaa" ], "name": "Bandial", - "names": [ "Banjaal", "Eegima", "Eegimaa", "Endungo", "Gubanjalay", "Gújjolaay Eegimaa", "Jóola Banjal" ], + "names": [ "Banjaal", "Banjal", "Eegima", "Eegimaa", "Endungo", "Gubanjalay", "Gújjolaay Eegimaa", "Jóola Banjal" ], "region": "SN", "regionname": "Senegal", "regions": [ "GM" ], @@ -15724,6 +16296,7 @@ "iana": [ "Bilakura" ], "iso639_3": "bql", "name": "Bilakura", + "names": [ "Karian" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -15872,7 +16445,7 @@ "iana": [ "Koro Wachi", "Begbere-Ejar" ], "iso639_3": "bqv", "name": "Koro Wachi", - "names": [ "Agere", "Begbere-Ejar", "Koro Agwe", "Koro Makama", "Koro Myamya", "Miamia", "Miamiya" ], + "names": [ "Agere", "Begbere-Ejar", "Koro Agwe", "Koro Makama" ], "region": "CI", "regionname": "Côte d'Ivoire", "regions": [ "NG" ], @@ -15902,7 +16475,7 @@ "iana": [ "Baangi" ], "iso639_3": "bqx", "name": "Baangi", - "names": [ "Cibaangi" ], + "names": [ "Abaangi", "Baabga", "Baganci", "Bangawa", "Bangi", "Cibaangi", "Vu-Baangi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -15930,8 +16503,9 @@ "full": "bqz-Latn-CM", "iana": [ "Bakaka" ], "iso639_3": "bqz", + "localnames": [ "Mkaaʾ" ], "name": "Bakaka", - "names": [ "Central Mbo", "Ehob Mkaa", "Mkaa’" ], + "names": [ "Central Mbo", "Ehob Mkaa", "Mkaaʾ" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -15947,14 +16521,29 @@ "localname": "brezhoneg", "localnames": [ "Brezhoneg" ], "name": "Breton", - "names": [ "Berton", "Brezhoneg" ], + "names": [ "Brezhoneg" ], "region": "FR", "regionname": "France", "script": "Latn", "sldr": true, "tag": "br", "tags": [ "br-FR", "br-Latn" ], - "windows": "br-Latn" + "windows": "br-Latn-FR" + }, + { + "full": "br-Ogam-001", + "iana": [ "Breton" ], + "iso639_3": "bre", + "name": "Breton", + "names": [ "Brezhoneg" ], + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Ogam", + "sldr": false, + "tag": "br-Ogam", + "tags": [ "br-001" ], + "windows": "br-Ogam-001" }, { "full": "bra-Deva-IN", @@ -15974,7 +16563,7 @@ }, { "full": "brb-Khmr-KH", - "iana": [ "Lave" ], + "iana": [ "Brao", "Lave" ], "iso639_3": "brb", "name": "Brao", "names": [ "Braou", "Brau", "Brou", "Lave", "Laveh", "Love", "Proue", "Rawe" ], @@ -15982,17 +16571,17 @@ "regionname": "Cambodia", "regions": [ "LA", "US", "VN" ], "script": "Khmr", - "sldr": false, + "sldr": true, "tag": "brb", "tags": [ "brb-KH", "brb-Khmr" ], - "windows": "brb-Khmr" + "windows": "brb-Khmr-KH" }, { "full": "brb-Laoo-LA", - "iana": [ "Lave" ], + "iana": [ "Brao", "Lave" ], "iso639_3": "brb", - "name": "Lave", - "names": [ "Brao", "Braou", "Brau", "Brou", "Laveh", "Love", "Proue", "Rawe" ], + "name": "Brao", + "names": [ "Braou", "Brau", "Brou", "Lave", "Laveh", "Love", "Proue", "Rawe" ], "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", @@ -16001,11 +16590,11 @@ "sldr": false, "tag": "brb-Laoo", "tags": [ "brb-LA" ], - "windows": "brb-Laoo" + "windows": "brb-Laoo-LA" }, { "full": "brb-Latn-VN", - "iana": [ "Lave" ], + "iana": [ "Brao", "Lave" ], "iso639_3": "brb", "name": "Brao", "names": [ "Braou", "Brau", "Brou", "Lave", "Laveh", "Love", "Proue", "Rawe" ], @@ -16016,13 +16605,14 @@ "sldr": false, "tag": "brb-Latn", "tags": [ "brb-VN" ], - "windows": "brb-Latn" + "windows": "brb-Latn-VN" }, { "full": "brc-Latn-GY", "iana": [ "Berbice Creole Dutch" ], "iso639_3": "brc", - "name": "Berbice Creole Dutch", + "name": "Berbice Dutch Creole", + "names": [ "Berbice Creole Dutch" ], "region": "GY", "regionname": "Guyana", "script": "Latn", @@ -16048,7 +16638,7 @@ }, { "full": "brf-Latn-CD", - "iana": [ "Bera" ], + "iana": [ "Bira" ], "iso639_3": "brf", "localnames": [ "Kibira" ], "name": "Bira", @@ -16080,6 +16670,7 @@ "iana": [ "Brahui" ], "iso639_3": "brh", "latnnames": [ "brāhūʾī" ], + "localname": "براہوئی", "localnames": [ "براہوئی" ], "name": "Brahui", "names": [ "Barahui", "Birahui", "Biravi", "Brahudi", "Brahuidi", "Brahuigi", "Brahuiki", "Kur Galli", "Kurd Gali" ], @@ -16087,7 +16678,7 @@ "regionname": "Pakistan", "regions": [ "AF", "IR", "TM" ], "script": "Arab", - "sldr": false, + "sldr": true, "tag": "brh", "tags": [ "brh-Arab", "brh-PK" ], "windows": "brh-Arab" @@ -16109,8 +16700,9 @@ "full": "bri-Latn-CM", "iana": [ "Mokpwe" ], "iso639_3": "bri", + "localnames": [ "Mòkpè" ], "name": "Mokpwe", - "names": [ "Bakpwe", "Bakueri", "Bakwedi", "Bakwele", "Bakweri", "Bakwiri", "Bekwiri", "Kpe", "Kwedi", "Kweli", "Kwili", "Kwiri", "Mokpe", "Ujuwa", "Vakweli", "Vambeng" ], + "names": [ "Bakpwe", "Bakueri", "Bakwedi", "Bakwele", "Bakweri", "Bakwiri", "Bekwiri", "Kpe", "Kwedi", "Kweli", "Kwili", "Kwiri", "Mokpe", "Mòkpè", "Vakweli", "Vambeng", "Wojuwa" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -16195,19 +16787,33 @@ "windows": "brn-Latn" }, { - "full": "bro-Zyyy-BT", + "full": "bro-Tibt-BT", "iana": [ "Brokkat" ], "iso639_3": "bro", "name": "Brokkat", "names": [ "Brokskad", "Jokay" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "bro", - "tags": [ "bro-BT", "bro-Zyyy" ], + "tags": [ "bro-BT", "bro-Tibt" ], "unwritten": true, - "windows": "bro-Zyyy" + "windows": "bro-Tibt" + }, + { + "full": "bro-Latn-BT", + "iana": [ "Brokkat" ], + "iso639_3": "bro", + "name": "Brokkat", + "names": [ "Brokskad", "Jokay" ], + "region": "BT", + "regionname": "Bhutan", + "script": "Latn", + "sldr": false, + "tag": "bro-Latn", + "unwritten": true, + "windows": "bro-Latn" }, { "full": "brp-Latn-ID", @@ -16228,8 +16834,9 @@ "full": "brq-Latn-PG", "iana": [ "Breri" ], "iso639_3": "brq", + "localnames": [ "Bnyoqko" ], "name": "Breri", - "names": [ "Kuanga", "Mbñioŋko" ], + "names": [ "Bnyoqko", "Iski", "Mbñioŋko" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -16265,7 +16872,6 @@ "sldr": false, "tag": "brs", "tags": [ "brs-ID", "brs-Latn" ], - "unwritten": true, "windows": "brs-Latn" }, { @@ -16273,7 +16879,7 @@ "iana": [ "Bitare" ], "iso639_3": "brt", "name": "Bitare", - "names": [ "Njwande", "Yukutare" ], + "names": [ "Asha", "Njwande", "Yukutare" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -16290,7 +16896,7 @@ "iso639_3": "bru", "localname": "Van Kieu", "name": "Bru, Eastern", - "names": [ "Brou", "Bru Tri", "Bru Van Kieu", "Brũ", "Eastern Bru", "Quang Tri Bru", "Van Kieu" ], + "names": [ "Baru", "Brou", "Bru", "Bru Tri", "Bru Van Kieu", "Brôu", "Brũ", "B’ru", "Eastern Bru", "Quang Tri Bru", "Van Kieu" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "LA" ], @@ -16298,14 +16904,16 @@ "sldr": true, "tag": "bru", "tags": [ "bru-Latn", "bru-VN" ], - "windows": "bru-Latn" + "windows": "bru-Latn-VN" }, { "full": "bru-Laoo-LA", "iana": [ "Eastern Bru" ], "iso639_3": "bru", + "latnnames": [ "Brũ" ], + "localnames": [ "ບຣູ້" ], "name": "Bru, Eastern", - "names": [ "Brou", "Bru Tri", "Bru Van Kieu", "Brũ", "Eastern Bru", "Quang Tri Bru", "Van Kieu" ], + "names": [ "Baru", "Brou", "Bru", "Bru Tri", "Bru Van Kieu", "Brôu", "Brũ", "B’ru", "Eastern Bru", "Quang Tri Bru", "Van Kieu" ], "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", @@ -16313,14 +16921,14 @@ "script": "Laoo", "sldr": false, "tag": "bru-Laoo", - "windows": "bru-Laoo" + "windows": "bru-Laoo-LA" }, { "full": "bru-Thai-LA", "iana": [ "Eastern Bru" ], "iso639_3": "bru", "name": "Bru, Eastern", - "names": [ "Brou", "Bru Tri", "Bru Van Kieu", "Brũ", "Eastern Bru", "Quang Tri Bru", "Van Kieu" ], + "names": [ "Baru", "Brou", "Bru", "Bru Tri", "Bru Van Kieu", "Brôu", "Brũ", "B’ru", "Eastern Bru", "Quang Tri Bru", "Van Kieu" ], "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", @@ -16328,7 +16936,7 @@ "script": "Thai", "sldr": false, "tag": "bru-Thai", - "windows": "bru-Thai" + "windows": "bru-Thai-LA" }, { "full": "brv-Laoo-LA", @@ -16394,25 +17002,39 @@ "windows": "brv-Thai-x-sakonnkn" }, { - "full": "brw-Zyyy-IN", + "full": "brw-Knda-IN", "iana": [ "Bellari" ], "iso639_3": "brw", "name": "Bellari", "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Knda", "sldr": false, "tag": "brw", - "tags": [ "brw-IN", "brw-Zyyy" ], + "tags": [ "brw-IN", "brw-Knda" ], "unwritten": true, - "windows": "brw-Zyyy" + "windows": "brw-Knda" + }, + { + "full": "brw-Mlym-IN", + "iana": [ "Bellari" ], + "iso639_3": "brw", + "name": "Bellari", + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Mlym", + "sldr": false, + "tag": "brw-Mlym", + "unwritten": true, + "windows": "brw-Mlym" }, { "full": "brx-Deva-IN", "iana": [ "Bodo (India)" ], "iso639_3": "brx", "latnnames": [ "boḍo", "boḍo bʰāṣā" ], - "localname": "बड़ो", + "localname": "बर’", "localnames": [ "बोडो", "बोडो भाषा" ], "name": "Boro", "names": [ "Bara", "Bodi", "Bodo", "Bodo (India)", "Boroni", "Kachari", "Mache", "Mech", "Meche", "Mechi", "Meci" ], @@ -16490,6 +17112,7 @@ "iso639_3": "bos", "localname": "bosanski", "localnames": [ "Bosanski jezik" ], + "macrolang": "sh", "name": "Bosnian", "names": [ "Serbo-Croatian" ], "region": "BA", @@ -16502,11 +17125,29 @@ "tags": [ "bs-BA", "bs-Latn" ], "windows": "bs" }, + { + "full": "bs-Arab-BA", + "iana": [ "Bosnian" ], + "iso639_3": "bos", + "macrolang": "sh", + "name": "Bosnian", + "names": [ "Serbo-Croatian" ], + "nophonvars": true, + "obsolete": true, + "region": "BA", + "regionname": "Bosnia and Herzegovina", + "regions": [ "ME", "MK", "RS" ], + "script": "Arab", + "sldr": false, + "tag": "bs-Arab", + "windows": "bs-Arab" + }, { "full": "bs-Cyrl-BA", "iana": [ "Bosnian" ], "iso639_3": "bos", "localname": "босански", + "macrolang": "sh", "name": "Bosnian", "names": [ "Serbo-Croatian" ], "nophonvars": true, @@ -16568,8 +17209,9 @@ "full": "bse-Latn-CM", "iana": [ "Wushi" ], "iso639_3": "bse", + "localnames": [ "NULL" ], "name": "Wushi", - "names": [ "Babessi", "Cho’ Wushike", "Pesii", "Sii", "Vesi" ], + "names": [ "Babessi", "Pesii", "Sii", "Vesi" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -16583,7 +17225,7 @@ "iana": [ "Bauchi" ], "iso639_3": "bsf", "name": "Bauchi", - "names": [ "Bauchi-Guda", "Bauci", "Baushi", "Kushi" ], + "names": [ "Bauci", "Baushi", "Kushi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -16612,7 +17254,7 @@ "iana": [ "Kati" ], "iso639_3": "bsh", "name": "Kati", - "names": [ "Bashgali", "Kata viri", "Kativiri", "Nuristani", "Shekhaniwar" ], + "names": [ "Bashgali", "Kata viri", "Kativiri", "Kâtʹa-vari", "Nuristani", "Shekhaniwar" ], "region": "AF", "regionname": "Afghanistan", "regions": [ "PK" ], @@ -16660,7 +17302,7 @@ "latnnames": [ "Burushaski" ], "localnames": [ "بروشسکی" ], "name": "Burushaski", - "names": [ "Biltum", "Brooshaaski", "Brushaski", "Burucaki", "Burucaski", "Burushaki", "Burushaski Hunza", "Burushki", "Mishāski", "Miśa:ski" ], + "names": [ "Biltum", "Brooshaaski", "Brushaski", "Burucaki", "Burucaski", "Burushaki", "Burushashki", "Burushaski Hunza", "Burushki", "Mishāski", "Miśa:ski" ], "region": "PK", "regionname": "Pakistan", "regions": [ "IN" ], @@ -16676,7 +17318,7 @@ "iso639_3": "bsk", "localnames": [ "Burushaski" ], "name": "Burushaski", - "names": [ "Biltum", "Brooshaaski", "Brushaski", "Burucaki", "Burucaski", "Burushaki", "Burushaski Hunza", "Burushki", "Mishāski", "Miśa:ski" ], + "names": [ "Biltum", "Brooshaaski", "Brushaski", "Burucaki", "Burucaski", "Burushaki", "Burushashki", "Burushaski Hunza", "Burushki", "Mishāski", "Miśa:ski" ], "region": "PK", "regionname": "Pakistan", "regions": [ "IN" ], @@ -16719,9 +17361,9 @@ "iana": [ "Barasana-Eduria" ], "iso639_3": "bsn", "localname": "Barasana-Eduria", - "localnames": [ "Barasana-Eduria" ], + "localnames": [ "Jãnerã-Eduria Oca" ], "name": "Barasana-Eduria", - "names": [ "Banera yae", "Barasano", "Barasano del Sur", "Came-Masa", "Come masa", "Comea", "Comematsa", "Hadera", "Hanera", "Hanera oka", "Janena", "Janera", "Palanoa", "Panenoa", "Panera", "Paneroa", "Southern Barasano", "Taibano", "Taiwaeno", "Taiwano", "Teiuana", "Yebamasa", "Yepa-Mahsa", "Yepa-Matso" ], + "names": [ "Banera yae", "Barasano", "Barasano del Sur", "Came-Masa", "Come masa", "Comea", "Comematsa", "Hadera", "Hanera", "Hanera oka", "Janena", "Janera", "Jãnerã-Eduria Oca", "Palanoa", "Panenoa", "Panera", "Paneroa", "Southern Barasano", "Taibano", "Taiwaeno", "Taiwano", "Teiuana", "Yebamasa", "Yepa-Mahsa", "Yepa-Matso" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -16779,6 +17421,7 @@ "full": "bsq-Latn-LR", "iana": [ "Bassa" ], "iso639_3": "bsq", + "localname": "Ɓǎsɔ́ɔ̀", "localnames": [ "Ɓǎsɔ́ɔ̀" ], "name": "Bassa", "names": [ "Ɓǎsɔ́ɔ̀" ], @@ -16809,9 +17452,9 @@ "iana": [ "Akoose" ], "iso639_3": "bss", "localname": "Akoose", - "localnames": [ "Akoose" ], + "localnames": [ "Akɔ́ɔ̄sē" ], "name": "Akoose", - "names": [ "Akose", "Akosi", "Bafaramani", "Bakosi", "Bakossi", "Bekoose", "Koose", "Kosi", "Kosse", "Kossi", "Manenguba", "Ngoe", "Nkoosi", "Nkosi" ], + "names": [ "Akose", "Akosi", "Akɔɔse", "Akɔ́ɔ̄sē", "Bafaramani", "Bakosi", "Bakossi", "Bekoose", "Koose", "Kosi", "Kosse", "Kossi", "Manenguba", "Ngoe", "Nkoosi", "Nkosi" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -16825,6 +17468,7 @@ "iana": [ "Basketo" ], "iso639_3": "bst", "latnnames": [ "Basəke፟tə" ], + "localname": "ባስኬ፟ት", "localnames": [ "ባስኬ፟ት" ], "name": "Basketo", "region": "ET", @@ -16893,18 +17537,30 @@ "windows": "bsv-Arab" }, { - "full": "bsw-Zyyy-ET", + "full": "bsw-Latn-ET", "iana": [ "Baiso" ], "iso639_3": "bsw", "name": "Baiso", "region": "ET", "regionname": "Ethiopia", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "bsw", - "tags": [ "bsw-ET", "bsw-Zyyy" ], - "unwritten": true, - "windows": "bsw-Zyyy" + "tags": [ "bsw-ET", "bsw-Latn" ], + "windows": "bsw-Latn" + }, + { + "full": "bsw-Ethi-ET", + "iana": [ "Baiso" ], + "iso639_3": "bsw", + "name": "Baiso", + "nophonvars": true, + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": false, + "tag": "bsw-Ethi", + "windows": "bsw-Ethi" }, { "full": "bsx-Latn-NG", @@ -16940,7 +17596,7 @@ "iana": [ "Bata" ], "iso639_3": "bta", "name": "Bata", - "names": [ "Batta", "Bete", "Birsa", "Bwaatye", "Bwatiye", "Demsa Bata", "Dii", "Dunu", "Eastern Bwatiye", "Gboati", "Gbwata", "Gbwate", "Gwate" ], + "names": [ "Batta", "Bete", "Birsa", "Bwaara", "Bwaatye", "Bwatiye", "Demsa Bata", "Dii", "Dunu", "Eastern Bwatiye", "Gboati", "Gbwata", "Gbwate", "Gwate", "Magbwati", "Magbwatiye", "Magbwatá" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -17026,15 +17682,15 @@ "full": "btg-Latn-CI", "iana": [ "Gagnoa Bété" ], "iso639_3": "btg", + "localnames": [ "Bhete" ], "name": "Bété, Gagnoa", - "names": [ "Eastern Bété", "Gagnoa Bété", "Gagnoua-Bété", "Shyen" ], + "names": [ "Bhete", "Bhete of Gagnoa", "Eastern Bété", "Gagnoa Bété", "Gagnoua-Bété", "Shyen" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", "sldr": false, "tag": "btg", "tags": [ "btg-CI", "btg-Latn" ], - "unwritten": true, "windows": "btg-Latn" }, { @@ -17070,6 +17726,7 @@ "full": "btj-Latn-ID", "iana": [ "Bacanese Malay" ], "iso639_3": "btj", + "macrolang": "ms", "name": "Malay, Bacanese", "names": [ "Bacan", "Bacanese Malay", "Batjan" ], "region": "ID", @@ -17114,6 +17771,7 @@ "full": "bto-Latn-PH", "iana": [ "Rinconada Bikol" ], "iso639_3": "bto", + "macrolang": "bik", "name": "Bikol, Rinconada", "names": [ "Bicol-Rinconada", "Bikol-Rinconada", "Iriga Bicolano", "Rinconada", "Rinconada Bicolano", "Rinconada Bikol", "Riŋkonāda" ], "region": "PH", @@ -17129,6 +17787,7 @@ "iana": [ "Budibud" ], "iso639_3": "btp", "name": "Budibud", + "names": [ "Nada" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -17229,7 +17888,7 @@ "iana": [ "Bateri" ], "iso639_3": "btv", "name": "Bateri", - "names": [ "Batera Kohistani", "Baterawal", "Baterawal Kohistani", "Bateri Kohistani" ], + "names": [ "Batera Kohistani", "Baterawal", "Baterawal Kohistani", "Bateri Kohistani", "Bhaṭʹe-sa zʹib" ], "region": "PK", "regionname": "Pakistan", "regions": [ "IN" ], @@ -17239,6 +17898,21 @@ "tags": [ "btv-Deva", "btv-PK" ], "windows": "btv-Deva" }, + { + "full": "btv-Arab-PK", + "iana": [ "Bateri" ], + "iso639_3": "btv", + "name": "Bateri", + "names": [ "Batera Kohistani", "Baterawal", "Baterawal Kohistani", "Bateri Kohistani", "Bhaṭʹe-sa zʹib" ], + "nophonvars": true, + "region": "PK", + "regionname": "Pakistan", + "script": "Arab", + "sldr": false, + "tag": "btv-Arab", + "unwritten": true, + "windows": "btv-Arab" + }, { "full": "btw-Latn-PH", "iana": [ "Butuanon" ], @@ -17318,8 +17992,8 @@ "iso639_3": "bua", "latnnames": [ "buryaad xelen" ], "localnames": [ "буряад хэлэн" ], - "name": "Buriat, Russia", - "names": [ "Buriat", "Buriat-Mongolian", "Buryat", "Northern Mongolian" ], + "name": "Buriat", + "names": [ "Buriat, Russia", "Buriat-Mongolian", "Buryat", "Northern Mongolian" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -17362,8 +18036,8 @@ "full": "bud-Latn-TG", "iana": [ "Ntcham" ], "iso639_3": "bud", - "localname": "Ncham", - "localnames": [ "Ncham" ], + "localname": "Ncam", + "localnames": [ "Ncam" ], "name": "Ntcham", "names": [ "Basar", "Basare", "Basari", "Bassar", "Bassari", "Ncam", "Ncham", "Tobote" ], "region": "TG", @@ -17395,6 +18069,7 @@ "iana": [ "Beothuk" ], "iso639_3": "bue", "name": "Beothuk", + "names": [ "Beothukan" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -17462,6 +18137,7 @@ "sldr": false, "tag": "buh", "tags": [ "buh-CN", "buh-Latn" ], + "unwritten": true, "windows": "buh-Latn" }, { @@ -17619,9 +18295,9 @@ "iana": [ "Budu" ], "iso639_3": "buu", "localname": "Ɨbʉdhʉ", - "localnames": [ "Ɨbʉdhʉ" ], + "localnames": [ "ɨɓʉdʉ" ], "name": "Budu", - "names": [ "Bodo", "Ebudu", "Kibudu", "Ɨbʉdhʉ" ], + "names": [ "Bodo", "Ebudu", "Kibudu", "Ɨbʉdhʉ", "ɨɓʉdʉ" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -17630,6 +18306,38 @@ "tags": [ "buu-CD", "buu-Latn" ], "windows": "buu-Latn" }, + { + "full": "buu-Latn-CD-x-bafwa", + "iana": [ "Budu" ], + "iso639_3": "buu", + "localnames": [ "ɨɓʉdʉ" ], + "name": "Budu", + "names": [ "Bodo", "Ebudu", "Kibudu", "Ɨbʉdhʉ", "ɨɓʉdʉ" ], + "region": "CD", + "regionname": "The Democratic Republic of the Congo", + "rod": "8404", + "script": "Latn", + "sldr": false, + "tag": "buu-x-bafwa", + "tags": [ "buu-CD-x-bafwa", "buu-Latn-x-bafwa" ], + "windows": "buu-Latn-x-bafwa" + }, + { + "full": "buu-Latn-CD-x-ineta", + "iana": [ "Budu" ], + "iso639_3": "buu", + "localnames": [ "ɨɓʉdʉ" ], + "name": "Budu", + "names": [ "Bodo", "Ebudu", "Kibudu", "Ɨbʉdhʉ", "ɨɓʉdʉ" ], + "region": "CD", + "regionname": "The Democratic Republic of the Congo", + "rod": "8406", + "script": "Latn", + "sldr": false, + "tag": "buu-x-ineta", + "tags": [ "buu-CD-x-ineta", "buu-Latn-x-ineta" ], + "windows": "buu-Latn-x-ineta" + }, { "full": "buv-Latn-PG", "iana": [ "Bun" ], @@ -17709,7 +18417,7 @@ "iso639_3": "bva", "localnames": [ "Barayin" ], "name": "Barein", - "names": [ "Baraïn", "Guilia", "Jalkia" ], + "names": [ "Barayin", "Baraïn" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -17722,8 +18430,10 @@ "full": "bvb-Latn-GQ", "iana": [ "Bube" ], "iso639_3": "bvb", + "localname": "Bube", + "localnames": [ "Böbë" ], "name": "Bube", - "names": [ "Adeeyah", "Adija", "Bobe", "Boobe", "Boombe", "Bubi", "Ediya", "Fernandian" ], + "names": [ "Adeeyah", "Adija", "Bobe", "Boobe", "Boombe", "Bubi", "Böbë", "Ediya", "Fernandian" ], "region": "GQ", "regionname": "Equatorial Guinea", "script": "Latn", @@ -17750,6 +18460,8 @@ "full": "bvd-Latn-SB", "iana": [ "Baeggu" ], "iso639_3": "bvd", + "localname": "Baeggu", + "localnames": [ "Baeggu" ], "name": "Baeggu", "names": [ "Baegu", "Mbaenggu" ], "region": "SB", @@ -17764,6 +18476,7 @@ "full": "bve-Latn-ID", "iana": [ "Berau Malay" ], "iso639_3": "bve", + "macrolang": "ms", "name": "Malay, Berau", "names": [ "Berau", "Berau Malay", "Merau Malay" ], "region": "ID", @@ -17824,9 +18537,9 @@ "full": "bvi-Latn-SS", "iana": [ "Belanda Viri" ], "iso639_3": "bvi", - "localnames": [ "Viri" ], + "localnames": [ "Vï̃rï" ], "name": "Belanda Viri", - "names": [ "Belanda", "Biri", "Bviri", "Gamba", "Gumba", "Mbegumba", "Mvegumba", "Viri" ], + "names": [ "Belanda", "Biri", "Bviri", "Gamba", "Gumba", "Mbegumba", "Mvegumba", "Viri", "Vï̃rï" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -17881,9 +18594,9 @@ "full": "bvm-Latn-CM", "iana": [ "Bamunka" ], "iso639_3": "bvm", - "localnames": [ "Niemeng" ], + "localnames": [ "Ŋgieməkɔʼkə" ], "name": "Bamunka", - "names": [ "Bamunkun", "Mbika", "Mekoh", "Muka", "Ndop-Bamunka", "Ngiemekohke", "Niemeng" ], + "names": [ "Bamunkun", "Mbika", "Mekoh", "Muka", "Munka", "Ndop-Bamunka", "Ngiemekohke", "Niemeng", "Ŋgieməkɔʼkə" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -17911,13 +18624,13 @@ "iana": [ "Bolgo" ], "iso639_3": "bvo", "name": "Bolgo", + "names": [ "Bolgo nii", "Bólɡònî" ], "region": "TD", "regionname": "Chad", "script": "Latn", "sldr": false, "tag": "bvo", "tags": [ "bvo-Latn", "bvo-TD" ], - "unwritten": true, "windows": "bvo-Latn" }, { @@ -17985,6 +18698,7 @@ "full": "bvu-Latn-ID", "iana": [ "Bukit Malay" ], "iso639_3": "bvu", + "macrolang": "ms", "name": "Malay, Bukit", "names": [ "Bukat", "Bukit", "Bukit Malay", "Meratus" ], "region": "ID", @@ -18118,6 +18832,8 @@ "full": "bwd-Latn-PG", "iana": [ "Bwaidoka" ], "iso639_3": "bwd", + "localname": "Bwaidoka", + "localnames": [ "Bwaidoka" ], "name": "Bwaidoka", "names": [ "Bwaidoga" ], "region": "PG", @@ -18133,7 +18849,7 @@ "iana": [ "Bwe Karen" ], "iso639_3": "bwe", "name": "Karen, Bwe", - "names": [ "Baghi", "Bghai Karen", "Blimaw", "Bwe", "Bwe Karen", "Dareh", "Manaw" ], + "names": [ "Baghi", "Bghai", "Bghai Karen", "Blimaw", "Bwe", "Bwe Karen", "Dareh", "Manaw" ], "region": "MM", "regionname": "Myanmar", "script": "Mymr", @@ -18147,7 +18863,7 @@ "iana": [ "Bwe Karen" ], "iso639_3": "bwe", "name": "Karen, Bwe", - "names": [ "Baghi", "Bghai Karen", "Blimaw", "Bwe", "Bwe Karen", "Dareh", "Manaw" ], + "names": [ "Baghi", "Bghai", "Bghai Karen", "Blimaw", "Bwe", "Bwe Karen", "Dareh", "Manaw" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -18160,7 +18876,7 @@ "iana": [ "Boselewa" ], "iso639_3": "bwf", "name": "Boselewa", - "names": [ "Bosalewa", "Bosilewa", "Mwani’u" ], + "names": [ "Bosalewa", "Bosilewa", "Lagota", "Mwani’u" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -18263,7 +18979,7 @@ "iana": [ "Biwat" ], "iso639_3": "bwm", "name": "Biwat", - "names": [ "Munduguma", "Mundugumor" ], + "names": [ "Munduguma", "Mundugumor", "Mundukumo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -18291,7 +19007,7 @@ "full": "bwo-Latn-ET", "iana": [ "Boro (Ethiopia)", "Borna (Ethiopia)" ], "iso639_3": "bwo", - "localnames": [ "Bori Noona", "Borna" ], + "localnames": [ "Borni" ], "name": "Borna", "names": [ "Borna (Ethiopia)", "Boro (Ethiopia)" ], "region": "ET", @@ -18412,7 +19128,7 @@ "iana": [ "Bwa" ], "iso639_3": "bww", "name": "Bwa", - "names": [ "Boa", "Boua", "Bua", "Kibua", "Kibwa", "Libenge", "Libua", "Libwali" ], + "names": [ "Boa", "Boua", "Bua", "Kibua", "Kibwa", "Libenge", "Libua", "Libwali", "Ɓoa", "ɓʉwa" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -18426,7 +19142,7 @@ "iana": [ "Bu-Nao Bunu" ], "iso639_3": "bwx", "name": "Bunu, Bu-Nao", - "names": [ "Bu-Nao Bunu", "Bunao", "Bunu", "Po-Nau", "Pu Nu", "Punu", "Yung-ts’ung Miao" ], + "names": [ "Bu-Nao Bunu", "Bunao", "Bunu", "Po-Nau", "Pu Nu", "Punu" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -18440,7 +19156,7 @@ "iana": [ "Cwi Bwamu" ], "iso639_3": "bwy", "name": "Bwamu, Cwi", - "names": [ "Coo", "Cwi", "Cwi Bwamu", "Twi" ], + "names": [ "Bwamu Twĩ", "Coo", "Cwi", "Cwi Bwamu", "Twi" ], "region": "BF", "regionname": "Burkina Faso", "script": "Latn", @@ -18470,9 +19186,8 @@ "full": "bxa-Latn-SB", "iana": [ "Tairaha" ], "iso639_3": "bxa", - "localnames": [ "Tairaha" ], - "name": "Tairaha", - "names": [ "Bauro", "Bwauro" ], + "name": "Bauro", + "names": [ "Bwauro", "Tairaha" ], "obsolete": true, "region": "SB", "regionname": "Solomon Islands", @@ -18517,7 +19232,7 @@ "iana": [ "Pela" ], "iso639_3": "bxd", "name": "Pela", - "names": [ "Bela", "Bola", "Bula", "Pala", "Polo" ], + "names": [ "Bela", "Bola", "Bula", "Pala", "Pola", "Polo" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -18545,6 +19260,7 @@ "full": "bxf-Latn-PG", "iana": [ "Bilur", "Minigir" ], "iso639_3": "bxf", + "localnames": [ "Minigir" ], "name": "Minigir", "names": [ "Bilur", "Birar" ], "region": "PG", @@ -18575,9 +19291,9 @@ "iana": [ "Buhutu" ], "iso639_3": "bxh", "localname": "Buhutu", - "localnames": [ "Buhutu" ], + "localnames": [ "Buhutu", "Buhutu alinadi" ], "name": "Buhutu", - "names": [ "Bohutu", "Buhulu", "Siasiada", "Yaleba" ], + "names": [ "Bohutu", "Buhulu", "Buhutu alinadi", "Siasiada", "Yaleba" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -18635,6 +19351,7 @@ "full": "bxm-Cyrl-MN", "iana": [ "Mongolia Buriat" ], "iso639_3": "bxm", + "macrolang": "bua", "name": "Buriat, Mongolia", "names": [ "Buriat-Mongolian", "Burraad", "Buryat", "Mongolia Buriat", "Mongolian Buriat", "Northern Mongolian" ], "region": "MN", @@ -18649,6 +19366,7 @@ "full": "bxm-Latn-MN", "iana": [ "Mongolia Buriat" ], "iso639_3": "bxm", + "macrolang": "bua", "name": "Buriat, Mongolia", "names": [ "Buriat-Mongolian", "Burraad", "Buryat", "Mongolia Buriat", "Mongolian Buriat", "Northern Mongolian" ], "obsolete": true, @@ -18663,6 +19381,7 @@ "full": "bxm-Mong-MN", "iana": [ "Mongolia Buriat" ], "iso639_3": "bxm", + "macrolang": "bua", "name": "Buriat, Mongolia", "names": [ "Buriat-Mongolian", "Burraad", "Buryat", "Mongolia Buriat", "Mongolian Buriat", "Northern Mongolian" ], "nophonvars": true, @@ -18748,18 +19467,48 @@ "windows": "bxs-Latn" }, { - "full": "bxu-Zyyy-CN", + "full": "bxu-Mong-CN", "iana": [ "China Buriat" ], "iso639_3": "bxu", + "macrolang": "bua", "name": "Buriat, China", "names": [ "Bargu Buriat", "Ba’erhu-Buliyate", "Buriat-Mongolian", "Buryat", "China Buriat", "Northeastern Mongolian", "Northern Mongolian" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Mong", "sldr": false, "tag": "bxu", - "tags": [ "bxu-CN", "bxu-Zyyy" ], - "windows": "bxu-Zyyy" + "tags": [ "bxu-CN", "bxu-Mong" ], + "windows": "bxu-Mong" + }, + { + "full": "bxu-Cyrl-CN", + "iana": [ "China Buriat" ], + "iso639_3": "bxu", + "macrolang": "bua", + "name": "Buriat, China", + "names": [ "Bargu Buriat", "Ba’erhu-Buliyate", "Buriat-Mongolian", "Buryat", "China Buriat", "Northeastern Mongolian", "Northern Mongolian" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Cyrl", + "sldr": false, + "tag": "bxu-Cyrl", + "windows": "bxu-Cyrl" + }, + { + "full": "bxu-Latn-CN", + "iana": [ "China Buriat" ], + "iso639_3": "bxu", + "macrolang": "bua", + "name": "Buriat, China", + "names": [ "Bargu Buriat", "Ba’erhu-Buliyate", "Buriat-Mongolian", "Buryat", "China Buriat", "Northeastern Mongolian", "Northern Mongolian" ], + "region": "CN", + "regionname": "China", + "script": "Latn", + "sldr": false, + "tag": "bxu-Latn", + "windows": "bxu-Latn" }, { "full": "bxv-Latn-TD", @@ -18810,6 +19559,7 @@ "full": "bya-Latn-PH", "iana": [ "Batak" ], "iso639_3": "bya", + "localnames": [ "Batak" ], "name": "Batak", "names": [ "Babuyan", "Palawan Batak", "Tinitianes" ], "region": "PH", @@ -18911,6 +19661,8 @@ "full": "byh-Deva-NP", "iana": [ "Bhujel" ], "iso639_3": "byh", + "latnnames": [ "bhujel" ], + "localnames": [ "भुजेल" ], "name": "Bhujel", "names": [ "Bujal", "Bujhel", "Bujheli", "Bujhyal", "Pukhgyal Ngur", "Western Chepang" ], "region": "NP", @@ -18952,32 +19704,32 @@ "windows": "byj-Latn" }, { - "full": "byk-Zyyy-CN", + "full": "byk-Latn-CN", "iana": [ "Biao" ], "iso639_3": "byk", "name": "Biao", - "names": [ "Gang Bau", "Kang Bau", "Kang Beu", "Kang Pau" ], + "names": [ "Gang Bau", "Kang Bau", "Kang Beu", "Kang Pau", "Kangpau" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "byk", - "tags": [ "byk-CN", "byk-Zyyy" ], + "tags": [ "byk-CN", "byk-Latn" ], "unwritten": true, - "windows": "byk-Zyyy" + "windows": "byk-Latn" }, { "full": "byl-Latn-ID", "iana": [ "Bayono" ], "iso639_3": "byl", "name": "Bayono", + "names": [ "Benanu", "Bese", "Kofoyap" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", "sldr": false, "tag": "byl", "tags": [ "byl-ID", "byl-Latn" ], - "unwritten": true, "windows": "byl-Latn" }, { @@ -19016,7 +19768,7 @@ "full": "byn-Latn-ER", "iana": [ "Bilin", "Blin" ], "iso639_3": "byn", - "localname": "ብሊን", + "localname": "Blin", "localnames": [ "Blin" ], "name": "Bilen", "names": [ "Balen", "Belen", "Beleni", "Bilayn", "Bilein", "Bilene", "Bileno", "Bilin", "Blin", "Bogo", "Bogos", "North Agaw" ], @@ -19073,10 +19825,10 @@ "full": "byr-Latn-PG", "iana": [ "Baruya", "Yipma" ], "iso639_3": "byr", - "localname": "Yɨ'mayagaala", - "localnames": [ "Yɨ'mayagaala" ], + "localname": "Yɨꞌmayagaala", + "localnames": [ "Yɨꞌmayagaala" ], "name": "Yipma", - "names": [ "Barua", "Baruya", "Yɨ’mayagaala" ], + "names": [ "Barua", "Baruya", "Yɨꞌmayagaala" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -19089,6 +19841,7 @@ "full": "bys-Latn-NG", "iana": [ "Burak" ], "iso639_3": "bys", + "localname": "nyuwǎ Ɓúúrák", "localnames": [ "nyuwǎ Ɓúúrák" ], "name": "Burak", "names": [ "Buurak", "Shongom", "nyuwǎ Ɓúúrák" ], @@ -19163,8 +19916,9 @@ "full": "byz-Latn-PG", "iana": [ "Banaro" ], "iso639_3": "byz", - "name": "Banaro", - "names": [ "Banar", "Banara", "Waran" ], + "localnames": [ "Waran" ], + "name": "Waran", + "names": [ "Banar", "Banara", "Banaro" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -19177,8 +19931,10 @@ "full": "bza-Latn-LR", "iana": [ "Bandi" ], "iso639_3": "bza", + "localname": "Bandí", + "localnames": [ "Bandí" ], "name": "Bandi", - "names": [ "Bande", "Bandé", "Gbande", "Gbandi", "Gbunde" ], + "names": [ "Bande", "Bandí", "Gbande", "Gbandi", "Gbunde" ], "region": "LR", "regionname": "Liberia", "regions": [ "GN" ], @@ -19207,6 +19963,7 @@ "full": "bzc-Latn-MG", "iana": [ "Southern Betsimisaraka Malagasy" ], "iso639_3": "bzc", + "macrolang": "mg", "name": "Malagasy, Southern Betsimisaraka", "names": [ "Antatsimo", "Betsimisaraka", "Southern Betsimisaraka Malagasy" ], "region": "MG", @@ -19271,6 +20028,7 @@ "full": "bzf-Latn-PG", "iana": [ "Boikin" ], "iso639_3": "bzf", + "localnames": [ "Yangoru" ], "name": "Boikin", "names": [ "Boiken", "Nucum", "Yangoru", "Yengoru" ], "region": "PG", @@ -19329,8 +20087,8 @@ "iso639_3": "bzj", "localname": "Kriol", "localnames": [ "Kriol" ], - "name": "Belize Kriol English", - "names": [ "Kriol", "Miskito Coast Creole English", "Western Caribbean Creole" ], + "name": "Belize English Creole", + "names": [ "Belize Kriol English", "Kriol", "Miskito Coast English Creole", "Western Caribbean Creole" ], "region": "BZ", "regionname": "Belize", "regions": [ "US" ], @@ -19345,8 +20103,8 @@ "iana": [ "Nicaragua Creole English" ], "iso639_3": "bzk", "localnames": [ "Creole" ], - "name": "Nicaragua Creole English", - "names": [ "Creole", "Mískito Coast Creole English" ], + "name": "Nicaragua English Creole", + "names": [ "Creole", "Mískito Coast English Creole", "Nicaragua Creole English" ], "region": "NI", "regionname": "Nicaragua", "script": "Latn", @@ -19473,17 +20231,17 @@ "windows": "bzs-Zxxx" }, { - "full": "bzt-Zyyy-001", + "full": "bzt-Latn-001", "iana": [ "Brithenig" ], "iso639_3": "bzt", "name": "Brithenig", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "bzt", - "tags": [ "bzt-001", "bzt-Zyyy" ], - "windows": "bzt-Zyyy" + "tags": [ "bzt-001", "bzt-Latn" ], + "windows": "bzt-Latn" }, { "full": "bzu-Latn-ID", @@ -19519,6 +20277,7 @@ "full": "bzw-Latn-NG", "iana": [ "Basa (Nigeria)" ], "iso639_3": "bzw", + "localnames": [ "Rubassa" ], "name": "Basa", "names": [ "Abacha", "Abatsa", "Basa (Nigeria)", "Basa-Benue", "Bassa", "Rubasa", "Rubassa" ], "region": "NG", @@ -19548,8 +20307,8 @@ "full": "bzy-Latn-NG", "iana": [ "Obanliku" ], "iso639_3": "bzy", - "name": "Obanliku", - "names": [ "Abanglekuo", "Abanliku" ], + "name": "Abanglekuo", + "names": [ "Abanliku", "Obanliku" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -19591,7 +20350,7 @@ "suppress": true, "tag": "ca", "tags": [ "ca-ES", "ca-Latn" ], - "windows": "ca" + "windows": "ca-ES" }, { "full": "ca-Latn-FR", @@ -19605,9 +20364,8 @@ "regionname": "France", "script": "Latn", "sldr": true, - "suppress": true, "tag": "ca-FR", - "windows": "ca-FR" + "windows": "ca-Latn-FR" }, { "full": "ca-Latn-ES-valencia", @@ -19621,19 +20379,18 @@ "regions": [ "AD", "AR", "BE", "BR", "CH", "CL", "CO", "CU", "DE", "DO", "FR", "IT", "MX", "US", "UY", "VE" ], "script": "Latn", "sldr": false, - "suppress": true, "tag": "ca-valencia", "tags": [ "ca-ES-valencia", "ca-Latn-valencia" ], "variants": [ "valencia" ], - "windows": "ca-valencia" + "windows": "ca-Latn-ES-valencia" }, { "full": "caa-Latn-GT", "iana": [ "Chortí" ], "iso639_3": "caa", - "localname": "Ch’orti’", + "localname": "Chꞌortiꞌ", "name": "Ch’orti’", - "names": [ "Chorti’", "Chortí" ], + "names": [ "Chortiꞌ", "Chortí" ], "region": "GT", "regionname": "Guatemala", "regions": [ "HN" ], @@ -19647,10 +20404,10 @@ "full": "cab-Latn-HN", "iana": [ "Garifuna" ], "iso639_3": "cab", - "localname": "Karif", - "localnames": [ "Garifuna" ], + "localname": "Garífuna", + "localnames": [ "Garífuna" ], "name": "Garifuna", - "names": [ "Black Carib", "Caribe", "Central American Carib", "Garífuna", "Island Carib", "Karif" ], + "names": [ "Black Carib", "Caribe", "Central American Carib", "Garífuna", "Garínagu", "Karif" ], "region": "HN", "regionname": "Honduras", "regions": [ "BZ", "GT", "NI", "US" ], @@ -19666,7 +20423,7 @@ "iso639_3": "cac", "localnames": [ "Koti'" ], "name": "Chuj", - "names": [ "Chapai", "Chuh", "Chuhe", "Chuj de San Mateo Ixtatán", "Chuje", "Koti’" ], + "names": [ "Chapai", "Chuh", "Chuhe", "Chuje", "Koti’" ], "region": "GT", "regionname": "Guatemala", "regions": [ "MX" ], @@ -19695,12 +20452,13 @@ "full": "cae-Latn-SN", "iana": [ "Lehar", "Laalaa" ], "iso639_3": "cae", + "localnames": [ "Kelaalaa" ], "name": "Laalaa", - "names": [ "Lala", "Lehar", "Serer" ], + "names": [ "Kelaalaa", "Lala", "Lehar", "Serer" ], "region": "SN", "regionname": "Senegal", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "cae", "tags": [ "cae-Latn", "cae-SN" ], "windows": "cae-Latn" @@ -19709,7 +20467,7 @@ "full": "caf-Latn-CA", "iana": [ "Southern Carrier" ], "iso639_3": "caf", - "localname": "ᑕᗸᒡ", + "localname": "Dakelh", "localnames": [ "Dakelh" ], "name": "Carrier, Southern", "names": [ "Dakelh", "Southern Carrier" ], @@ -19784,7 +20542,7 @@ "full": "cak-Latn-GT", "iana": [ "Kaqchikel", "Cakchiquel" ], "iso639_3": "cak", - "localname": "Kakchikel", + "localname": "Kaqchikel", "localnames": [ "Kaqchikel" ], "name": "Kaqchikel", "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], @@ -19796,6 +20554,150 @@ "tags": [ "cak-GT", "cak-Latn" ], "windows": "cak-Latn" }, + { + "full": "cak-Latn-GT-x-central", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "12", + "script": "Latn", + "sldr": false, + "tag": "cak-x-central", + "tags": [ "cak-GT-x-central", "cak-Latn-x-central" ], + "windows": "cak-Latn-x-central" + }, + { + "full": "cak-Latn-GT-x-eastern", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4969", + "script": "Latn", + "sldr": false, + "tag": "cak-x-eastern", + "tags": [ "cak-GT-x-eastern", "cak-Latn-x-eastern" ], + "windows": "cak-Latn-x-eastern" + }, + { + "full": "cak-Latn-GT-x-northern", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4783", + "script": "Latn", + "sldr": false, + "tag": "cak-x-northern", + "tags": [ "cak-GT-x-northern", "cak-Latn-x-northern" ], + "windows": "cak-Latn-x-northern" + }, + { + "full": "cak-Latn-GT-x-subcm", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4023", + "script": "Latn", + "sldr": false, + "tag": "cak-x-subcm", + "tags": [ "cak-GT-x-subcm", "cak-Latn-x-subcm" ], + "windows": "cak-Latn-x-subcm" + }, + { + "full": "cak-Latn-GT-x-subsa", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4784", + "script": "Latn", + "sldr": false, + "tag": "cak-x-subsa", + "tags": [ "cak-GT-x-subsa", "cak-Latn-x-subsa" ], + "windows": "cak-Latn-x-subsa" + }, + { + "full": "cak-Latn-GT-x-subso", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4024", + "script": "Latn", + "sldr": false, + "tag": "cak-x-subso", + "tags": [ "cak-GT-x-subso", "cak-Latn-x-subso" ], + "windows": "cak-Latn-x-subso" + }, + { + "full": "cak-Latn-GT-x-subsu", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4027", + "script": "Latn", + "sldr": false, + "tag": "cak-x-subsu", + "tags": [ "cak-GT-x-subsu", "cak-Latn-x-subsu" ], + "windows": "cak-Latn-x-subsu" + }, + { + "full": "cak-Latn-GT-x-swestern", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4028", + "script": "Latn", + "sldr": false, + "tag": "cak-x-swestern", + "tags": [ "cak-GT-x-swestern", "cak-Latn-x-swestern" ], + "windows": "cak-Latn-x-swestern" + }, + { + "full": "cak-Latn-GT-x-xenacoj", + "iana": [ "Kaqchikel" ], + "iso639_3": "cak", + "localnames": [ "Kaqchikel" ], + "name": "Kaqchikel", + "names": [ "Cakchiquel", "Kaqchiquel", "Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4025", + "script": "Latn", + "sldr": true, + "tag": "cak-x-xenacoj", + "tags": [ "cak-GT-x-xenacoj", "cak-Latn-x-xenacoj" ], + "windows": "cak-Latn-x-xenacoj" + }, { "full": "cal-Latn-MP", "iana": [ "Carolinian" ], @@ -19860,7 +20762,7 @@ "full": "cap-Latn-BO", "iana": [ "Chipaya" ], "iso639_3": "cap", - "localname": "Puquina", + "localname": "Chipaya", "localnames": [ "Chipaya" ], "name": "Chipaya", "names": [ "Puquina" ], @@ -19937,7 +20839,7 @@ "iana": [ "Callawalla" ], "iso639_3": "caw", "name": "Callawalla", - "names": [ "Callahuaya", "Callawaya", "Kallawaya" ], + "names": [ "Callahuaya", "Callawaya", "Kallawaya", "Pohena" ], "region": "BO", "regionname": "Bolivia", "script": "Latn", @@ -20015,7 +20917,7 @@ "localname": "Carapana", "localnames": [ "Carapana" ], "name": "Carapana", - "names": [ "Carapana-Tapuya", "Carapanã", "Karapano", "Karapaná", "Karapanã", "Mextã", "Mi tea", "Mochda", "Moxdoa", "Muteamasa", "Muxtea", "Ukopinõpõna" ], + "names": [ "Carapana-Tapuya", "Carapanã", "Karapano", "Karapaná", "Karapanã", "Mehtã", "Mextã", "Mi tea", "Mochda", "Moxdoa", "Muteamasa", "Muxtea", "Mõxtã", "Möxdöá", "Ukopinõpõna" ], "region": "CO", "regionname": "Colombia", "regions": [ "BR" ], @@ -20075,7 +20977,7 @@ "iana": [ "Ede Cabe" ], "iso639_3": "cbj", "name": "Ede Cabe", - "names": [ "Caabe", "Cabe", "Tchabè" ], + "names": [ "Caabe", "Cabe", "Ede-Cabε", "Sábέε", "Ṣabẹ", "Tchabè", "Tsábε" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -20118,8 +21020,9 @@ "full": "cbl-Latn-MM", "iana": [ "Bualkhaw Chin" ], "iso639_3": "cbl", + "localnames": [ "Bualkhaw Chin" ], "name": "Chin, Bualkhaw", - "names": [ "Bualkhaw Chin", "Bualkhaw-Chin", "Bualkhua", "Phadei" ], + "names": [ "Bualkhaw Chin", "Bualkhua", "Phadei" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -20149,7 +21052,7 @@ "iana": [ "Izora" ], "iso639_3": "cbo", "name": "Izora", - "names": [ "Chokobo", "Cokobanci", "Cokobawa", "Ndazora", "Zora" ], + "names": [ "Azora", "Chokobo", "Cikobu", "Cokobanci", "Cokobawa", "Cokobo", "Ndazora", "Zora" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -20181,23 +21084,24 @@ "localname": "Kakataibo-Kashibo", "localnames": [ "Kakataibo-Kashibo" ], "name": "Kakataibo-Kashibo", - "names": [ "Aincacatai", "Cachibo", "Cacibo", "Cahivo", "Cashibo-Cacataibo", "Caxibo", "Hagueti", "Incauncanibo", "Kashibo", "Managua" ], + "names": [ "Aincacatai", "Cachibo", "Cacibo", "Cahivo", "Cashibo-Cacataibo", "Caxibo", "Hagueti", "Incauncanibo", "Kashibo", "Kashibo-Kakataibo", "Managua" ], "region": "PE", "regionname": "Peru", + "regions": [ "BR" ], "script": "Latn", "sldr": true, "tag": "cbr", - "tags": [ "cbr-Latn", "cbr-PE" ], + "tags": [ "cbr-Latn", "cbr-PE", "nom", "nom-Latn", "nom-Latn-PE", "nom-PE" ], "windows": "cbr-Latn" }, { "full": "cbs-Latn-PE", "iana": [ "Cashinahua" ], "iso639_3": "cbs", - "localname": "Hantxa Kuin", - "localnames": [ "Hantxa Kuin", "Juni Kuin" ], + "localname": "Hãtxa Kuī", + "localnames": [ "Hãtxa Kuī" ], "name": "Kashinawa", - "names": [ "Cashinahua", "Cashinahuá", "Caxinahua", "Caxinauá", "Caxinawá", "Hantxa Kuin", "Huni Kui", "Huni Kuin", "Hãtxa Kuī", "Juni Kuin", "Kashinahua", "Kaxinauá", "Kaxinawá", "Kaxynawa" ], + "names": [ "Cashinahua", "Caxinahua", "Caxinauá", "Caxinawá", "Hantxa Kuin", "Huni Kui", "Huni Kuin", "Hãtxa Kuī", "Juni Kuin", "Kashinahua", "Kaxinauá", "Kaxinawá", "Kaxynawa" ], "region": "PE", "regionname": "Peru", "regions": [ "BR" ], @@ -20227,8 +21131,9 @@ "full": "cbu-Latn-PE", "iana": [ "Candoshi-Shapra" ], "iso639_3": "cbu", + "localnames": [ "Kadoazi koko" ], "name": "Kandozi-Chapra", - "names": [ "Candoshi", "Candoshi-Shapra", "Candoxi", "Kandoshi", "Kandozi", "Murato" ], + "names": [ "Candoshi", "Candoshi-Shapra", "Candoxi", "Kadoazi koko", "Kandoshi", "Kandozi", "Murato" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -20242,9 +21147,8 @@ "iana": [ "Cacua" ], "iso639_3": "cbv", "localname": "Cacua", - "localnames": [ "Cacua" ], "name": "Cacua", - "names": [ "Báda", "Cakua", "Kakua", "Kákwa", "Macu de Cubeo", "Macu de Desano", "Macu de Guanano", "Wacara" ], + "names": [ "Báda", "Bára", "Cakua", "Kakua", "Kákwa", "Wacara" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -20345,8 +21249,9 @@ "full": "ccg-Latn-NG", "iana": [ "Samba Daka" ], "iso639_3": "ccg", + "localnames": [ "Sama Mum" ], "name": "Samba Daka", - "names": [ "Chamba", "Chamba Daka", "Daka", "Dakka", "Dekka", "Deng", "Jama", "Sama", "Samba", "Tchamba", "Tikk", "Tsamba" ], + "names": [ "Chamba", "Chamba Daka", "Daka", "Dakka", "Dekka", "Deng", "Jama", "Sama", "Sama Mum", "Samabu", "Samba", "Tchamba", "Tikk", "Tsamba" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -20374,7 +21279,7 @@ "iana": [ "Kasanga" ], "iso639_3": "ccj", "name": "Kasanga", - "names": [ "Cassanga", "Guhaaca", "Haal", "I-Hadja", "Kassanga" ], + "names": [ "Cassanga", "Guatcha", "Guhaaca", "Haal", "I-Hadja", "Kassanga" ], "region": "GW", "regionname": "Guinea-Bissau", "script": "Latn", @@ -20403,8 +21308,8 @@ "full": "ccm-Latn-MY", "iana": [ "Malaccan Creole Malay" ], "iso639_3": "ccm", - "name": "Malaccan Creole Malay", - "names": [ "Chitties Creole Malay" ], + "name": "Malaccan Malay Creole", + "names": [ "Chitties Malay Creole", "Malaccan Creole Malay" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -20418,10 +21323,10 @@ "full": "cco-Latn-MX", "iana": [ "Comaltepec Chinantec" ], "iso639_3": "cco", - "localname": "Jmii'", - "localnames": [ "Jmii'" ], + "localname": "Jmíiˊ", + "localnames": [ "Jmíiˊ" ], "name": "Chinantec, Comaltepec", - "names": [ "Comaltepec Chinantec", "Jmii’", "juu jmiih" ], + "names": [ "Chinanteco de Santiago Comaltepec", "Comaltepec Chinantec", "Jmíiˊ", "juu jmiih" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -20444,13 +21349,12 @@ "sldr": true, "tag": "ccp", "tags": [ "ccp-BD", "ccp-Cakm" ], - "windows": "ccp-Cakm" + "windows": "ccp-Cakm-BD" }, { "full": "ccp-Beng-IN", "iana": [ "Chakma" ], "iso639_3": "ccp", - "localname": "𑄌𑄋𑄴𑄟𑄳𑄦", "name": "Chakma", "names": [ "Chakama", "Changma Kodha", "Changma Vaj", "Daingnet", "Sakma", "Sangma", "Takam", "Tsakma" ], "nophonvars": true, @@ -20461,7 +21365,7 @@ "sldr": true, "tag": "ccp-Beng", "tags": [ "ccp-IN" ], - "windows": "ccp-Beng" + "windows": "ccp-Beng-IN" }, { "full": "ccp-Latn-BD", @@ -20476,7 +21380,7 @@ "script": "Latn", "sldr": false, "tag": "ccp-Latn", - "windows": "ccp-Latn" + "windows": "ccp-Latn-BD" }, { "full": "ccr-Latn-SV", @@ -20525,7 +21429,7 @@ "iana": [ "Chiru" ], "iso639_3": "cdf", "name": "Chiru", - "names": [ "Chhori" ], + "names": [ "Rem", "Riem" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -20539,7 +21443,7 @@ "iana": [ "Chiru" ], "iso639_3": "cdf", "name": "Chiru", - "names": [ "Chhori" ], + "names": [ "Rem", "Riem" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -20609,8 +21513,10 @@ "full": "cdj-Deva-IN", "iana": [ "Churahi" ], "iso639_3": "cdj", + "latnnames": [ "Kuraahi" ], + "localnames": [ "चुराही" ], "name": "Churahi", - "names": [ "Chaurahi", "Churahi Pahari", "Churai Pahari" ], + "names": [ "Chaurahi", "Churahi Pahari", "Churai Pahari", "Himachali", "Pahari" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -20623,8 +21529,10 @@ "full": "cdm-Deva-NP", "iana": [ "Chepang" ], "iso639_3": "cdm", + "latnnames": [ "chepang", "cepaang bhaasaahaang" ], + "localnames": [ "चेपाङ", "च्योःबाङ् भासा" ], "name": "Chepang", - "names": [ "Cyo’bang", "Praja Bhasa", "Tsepang" ], + "names": [ "Chyo’bang", "Cyo’bang", "Praja Bhasa", "Tsepang" ], "region": "NP", "regionname": "Nepal", "rod": "04097", @@ -20639,7 +21547,7 @@ "iana": [ "Chepang" ], "iso639_3": "cdm", "name": "Chepang", - "names": [ "Cyo’bang", "Praja Bhasa", "Tsepang" ], + "names": [ "Chyo’bang", "Cyo’bang", "Praja Bhasa", "Tsepang" ], "region": "NP", "regionname": "Nepal", "script": "Latn", @@ -20666,21 +21574,23 @@ "full": "cdo-Hans-CN", "iana": [ "Min Dong Chinese" ], "iso639_3": "cdo", + "macrolang": "zh", "name": "Chinese, Min Dong", "names": [ "Eastern Fujian", "Eastern Min", "Foochow", "Hokchiu", "Min Dong", "Min Dong Chinese" ], - "nophonvars": true, "region": "CN", "regionname": "China", "regions": [ "BN", "ID", "MY", "SG", "TH", "US" ], "script": "Hans", "sldr": false, - "tag": "cdo-Hans", + "tag": "cdo", + "tags": [ "cdo-CN", "cdo-Hans", "zh-cdo", "zh-cdo-CN" ], "windows": "cdo-Hans" }, { "full": "cdo-Hant-CN", "iana": [ "Min Dong Chinese" ], "iso639_3": "cdo", + "macrolang": "zh", "name": "Chinese, Min Dong", "names": [ "Eastern Fujian", "Eastern Min", "Foochow", "Hokchiu", "Min Dong", "Min Dong Chinese" ], "nophonvars": true, @@ -20696,6 +21606,7 @@ "full": "cdo-Latn-CN", "iana": [ "Min Dong Chinese" ], "iso639_3": "cdo", + "macrolang": "zh", "name": "Chinese, Min Dong", "names": [ "Eastern Fujian", "Eastern Min", "Foochow", "Hokchiu", "Min Dong", "Min Dong Chinese" ], "region": "CN", @@ -20811,19 +21722,19 @@ "windows": "ce-Latn" }, { - "full": "cea-Zyyy-US", + "full": "cea-Latn-US", "iana": [ "Lower Chehalis" ], "iso639_3": "cea", "name": "Chehalis, Lower", "names": [ "Lower Chehalis", "Łəw’ál’məš" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "cea", - "tags": [ "cea-US", "cea-Zyyy" ], + "tags": [ "cea-Latn", "cea-US" ], "unwritten": true, - "windows": "cea-Zyyy" + "windows": "cea-Latn" }, { "full": "ceb-Latn-PH", @@ -20861,8 +21772,9 @@ "full": "ceg-Latn-PY", "iana": [ "Chamacoco" ], "iso639_3": "ceg", + "localnames": [ "Ɨshɨr ahwoso" ], "name": "Chamacoco", - "names": [ "Ishir", "Ishiro", "Jeywo" ], + "names": [ "Ishir", "Ishiro", "Jeywo", "Yshyro", "Yshyro ahwoso", "Ɨshɨr ahwoso", "Ɨshɨro", "Ɨshɨro ahwoso" ], "region": "PY", "regionname": "Paraguay", "script": "Latn", @@ -21116,7 +22028,7 @@ "sldr": false, "tag": "cfm", "tags": [ "cfm-Latn", "cfm-MM" ], - "windows": "cfm-Latn" + "windows": "cfm-Latn-MM" }, { "full": "cfm-Beng-IN", @@ -21132,7 +22044,7 @@ "sldr": false, "tag": "cfm-Beng", "tags": [ "cfm-IN" ], - "windows": "cfm-Beng" + "windows": "cfm-Beng-IN" }, { "full": "cfm-Latn-MM-x-khuals", @@ -21140,7 +22052,7 @@ "iso639_3": "cfm", "localnames": [ "Falam Chin" ], "name": "Chin, Falam", - "names": [ "Chin, Falam Khualshim", "Falam", "Fallam", "Halam", "Halam Chin", "Hallam", "Hallam Chin", "Tipura" ], + "names": [ "Chin, Falam Khualshim", "Falam", "Falam Chin", "Fallam", "Halam", "Halam Chin", "Hallam", "Hallam Chin", "Tipura" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -21148,7 +22060,7 @@ "sldr": false, "tag": "cfm-Latn-x-khuals", "tags": [ "cfm-MM-x-khuals" ], - "windows": "cfm-Latn-x-khuals" + "windows": "cfm-Latn-MM-x-khuals" }, { "full": "cfm-Latn-MM-x-lente", @@ -21156,7 +22068,7 @@ "iso639_3": "cfm", "localnames": [ "Falam Chin" ], "name": "Chin, Falam", - "names": [ "Chin, Falam Lente", "Falam", "Fallam", "Halam", "Halam Chin", "Hallam", "Hallam Chin", "Tipura" ], + "names": [ "Chin, Falam Lente", "Falam", "Falam Chin", "Fallam", "Halam", "Halam Chin", "Hallam", "Hallam Chin", "Tipura" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -21164,7 +22076,7 @@ "sldr": false, "tag": "cfm-Latn-x-lente", "tags": [ "cfm-MM-x-lente" ], - "windows": "cfm-Latn-x-lente" + "windows": "cfm-Latn-MM-x-lente" }, { "full": "cfm-Latn-MM-x-zannia", @@ -21172,7 +22084,7 @@ "iso639_3": "cfm", "localnames": [ "Falam Chin" ], "name": "Chin, Falam", - "names": [ "Chin, Falam Zanniat", "Falam", "Fallam", "Halam", "Halam Chin", "Hallam", "Hallam Chin", "Tipura" ], + "names": [ "Chin, Falam Zanniat", "Falam", "Falam Chin", "Fallam", "Halam", "Halam Chin", "Hallam", "Hallam Chin", "Tipura" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -21180,7 +22092,7 @@ "sldr": false, "tag": "cfm-Latn-x-zannia", "tags": [ "cfm-MM-x-zannia" ], - "windows": "cfm-Latn-x-zannia" + "windows": "cfm-Latn-MM-x-zannia" }, { "full": "cga-Latn-PG", @@ -21229,19 +22141,19 @@ "windows": "cgg-Latn" }, { - "full": "cgk-Zyyy-BT", + "full": "cgk-Tibt-BT", "iana": [ "Chocangacakha" ], "iso639_3": "cgk", "name": "Chocangacakha", "names": [ "Chocangaca", "Chocha Ngacha", "Kursmadkha", "Maphekha", "Rtsamangpa’ikha", "Tsagkaglingpa’ikha", "Tsamang", "rTsamangpa’i kha" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "cgk", - "tags": [ "cgk-BT", "cgk-Zyyy" ], + "tags": [ "cgk-BT", "cgk-Tibt" ], "unwritten": true, - "windows": "cgk-Zyyy" + "windows": "cgk-Tibt" }, { "full": "ch-Latn-GU", @@ -21265,6 +22177,7 @@ "iana": [ "Chibcha" ], "iso639_3": "chb", "name": "Chibcha", + "names": [ "Mosca", "Muisca", "Muysc Cubun", "Muysca" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -21321,18 +22234,18 @@ "windows": "chf-Latn" }, { - "full": "chg-Zyyy-TM", + "full": "chg-Arab-TM", "iana": [ "Chagatai" ], "iso639_3": "chg", "name": "Chagatai", "names": [ "Chaghatai", "Chaghatay", "Jagatai", "Tschagataisch" ], "region": "TM", "regionname": "Turkmenistan", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "chg", - "tags": [ "chg-TM", "chg-Zyyy" ], - "windows": "chg-Zyyy" + "tags": [ "chg-Arab", "chg-TM" ], + "windows": "chg-Arab" }, { "full": "chh-Latn-US", @@ -21400,8 +22313,8 @@ "iso639_3": "chm", "latnnames": [ "Olyk Marij", "Olyk Marij jylme" ], "localnames": [ "олык марий", "олык марий йылме" ], - "name": "Mari, Meadow", - "names": [ "Cheremis", "Eastern Mari", "Low Mari", "Lowland Mari", "Lugovo Mari", "Mari", "Mari (Russia)", "Mari, Eastern", "Mari-Woods", "Woods Mari" ], + "name": "Mari (Russia)", + "names": [ "Cheremis", "Eastern Mari", "Low Mari", "Lowland Mari", "Lugovo Mari", "Mari", "Mari, Meadow", "Mari-Woods", "Woods Mari" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "KZ" ], @@ -21415,8 +22328,8 @@ "full": "chm-Latn-RU", "iana": [ "Mari (Russia)" ], "iso639_3": "chm", - "name": "Mari, Meadow", - "names": [ "Cheremis", "Eastern Mari", "Low Mari", "Lowland Mari", "Lugovo Mari", "Mari", "Mari (Russia)", "Mari, Eastern", "Mari-Woods", "Woods Mari" ], + "name": "Mari (Russia)", + "names": [ "Cheremis", "Eastern Mari", "Low Mari", "Lowland Mari", "Lugovo Mari", "Mari", "Mari, Meadow", "Mari-Woods", "Woods Mari" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "KZ" ], @@ -21583,8 +22496,10 @@ "full": "chx-Deva-NP", "iana": [ "Chantyal" ], "iso639_3": "chx", + "latnnames": [ "Chantyal Kham" ], + "localnames": [ "छन्त्याल खाम" ], "name": "Chantyal", - "names": [ "Chantel", "Chantel Kham", "Chentel", "Chhantel", "Chhantyal", "Khamkura" ], + "names": [ "Chantel", "Chhantel", "Chhantyal", "Khamkura" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -21599,7 +22514,7 @@ "iso639_3": "chy", "localnames": [ "Tsėhesenėstsestȯtse" ], "name": "Cheyenne", - "names": [ "Tsitsistas", "Tsėhesenėstsestȯtse" ], + "names": [ "Tsisinstsistots", "Tsitsistas", "Tsėhesenėstsestȯtse" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -21746,7 +22661,7 @@ "iana": [ "Chitkuli Kinnauri" ], "iso639_3": "cik", "name": "Kinnauri, Chitkuli", - "names": [ "Chitkhuli", "Chitkuli", "Chitkuli Kinnauri", "Kanauri", "Kinnauri", "Thebarskad", "Tsitkhuli", "Tsíhuli" ], + "names": [ "Chhitkul-Rakchham", "Chitkhuli", "Chitkuli", "Chitkuli Kinnauri", "Kanauri", "Kinnauri", "Thebarskad", "Tsitkhuli", "Tsíhuli" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -21818,6 +22733,7 @@ "iana": [ "Chippewa" ], "iso639_3": "ciw", "localnames": [ "Anishinaabemowin" ], + "macrolang": "oj", "name": "Chippewa", "names": [ "Anishinaabemowin", "Minnesota Ojibwe", "Ojibway", "Ojibwe", "Southwestern Ojibwa" ], "region": "US", @@ -21832,6 +22748,7 @@ "full": "ciw-Cans-US", "iana": [ "Chippewa" ], "iso639_3": "ciw", + "macrolang": "oj", "name": "Chippewa", "names": [ "Anishinaabemowin", "Minnesota Ojibwe", "Ojibway", "Ojibwe", "Southwestern Ojibwa" ], "nophonvars": true, @@ -21863,7 +22780,7 @@ "iana": [ "Western Cham" ], "iso639_3": "cja", "name": "Cham, Western", - "names": [ "Cam", "Cambodian Cham", "Cham", "Chiem", "Khmer Islam", "New Cham", "Tjam", "Tscham", "Tsiam", "Western Cham" ], + "names": [ "Cam", "Cambodian Cham", "Cham", "Chiem", "Chăm", "Khmer Islam", "New Cham", "Tjam", "Tscham", "Tsiam", "Western Cham" ], "region": "KH", "regionname": "Cambodia", "regions": [ "FR", "LA", "MY", "TH", "US", "VN" ], @@ -21878,7 +22795,7 @@ "iana": [ "Western Cham" ], "iso639_3": "cja", "name": "Cham, Western", - "names": [ "Cambodian Cham", "Cham", "Chiem", "New Cham", "Tjam", "Western Cham" ], + "names": [ "Cambodian Cham", "Cham", "Chiem", "Chăm", "New Cham", "Tjam", "Western Cham" ], "nophonvars": true, "region": "KH", "regionname": "Cambodia", @@ -21892,9 +22809,9 @@ "full": "cja-Latn-KH", "iana": [ "Western Cham" ], "iso639_3": "cja", - "localnames": [ "Cham" ], + "localnames": [ "Chăm" ], "name": "Cham, Western", - "names": [ "Cambodian Cham", "Cham", "Chiem", "New Cham", "Tjam", "Western Cham" ], + "names": [ "Cambodian Cham", "Cham", "Chiem", "Chăm", "New Cham", "Tjam", "Western Cham" ], "region": "KH", "regionname": "Cambodia", "regions": [ "FR", "MY", "TH", "US", "VN" ], @@ -21907,31 +22824,32 @@ "full": "cje-Latn-VN", "iana": [ "Chru" ], "iso639_3": "cje", + "localnames": [ "Chru" ], "name": "Chru", "names": [ "Cadoe Loang", "Cho Ru", "Choru", "Chrau Hma", "Chu", "Chu Ru", "Churu", "Cru", "Kru", "Seyu" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "US" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "cje", "tags": [ "cje-Latn", "cje-VN" ], "windows": "cje-Latn" }, { - "full": "cjh-Zyyy-US", + "full": "cjh-Latn-US", "iana": [ "Upper Chehalis" ], "iso639_3": "cjh", "name": "Chehalis, Upper", "names": [ "Chehalis", "Kwaiailk", "Q̉ʷay̓áyiłq̉", "Upper Chehalis" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "cjh", - "tags": [ "cjh-US", "cjh-Zyyy" ], + "tags": [ "cjh-Latn", "cjh-US" ], "unwritten": true, - "windows": "cjh-Zyyy" + "windows": "cjh-Latn" }, { "full": "cji-Cyrl-RU", @@ -21952,9 +22870,9 @@ "full": "cjk-Latn-AO", "iana": [ "Chokwe" ], "iso639_3": "cjk", - "localnames": [ "Chokwe" ], - "name": "Cokwe", - "names": [ "Chokwe", "Ciokwe", "Djok", "Imo", "Kioko", "Quioco", "Shioko", "Tchokwé", "Tschiokloe", "Tschiokwe", "Tshokwe", "Ucokwe" ], + "localnames": [ "Cokwe" ], + "name": "Chokwe", + "names": [ "Ciokwe", "Cokwe", "Djok", "Imo", "Kioko", "Quioco", "Shioko", "Tchokwé", "Tschiokloe", "Tschiokwe", "Tshokwe", "Ucokwe" ], "region": "AO", "regionname": "Angola", "regions": [ "CD", "ZM" ], @@ -21969,7 +22887,7 @@ "iana": [ "Eastern Cham" ], "iso639_3": "cjm", "name": "Cham, Eastern", - "names": [ "Bhamam", "Chiem", "Chiem Thành", "Eastern Cham", "Tjam" ], + "names": [ "Bhamam", "Chiem", "Chiem Thành", "Chăm", "Eastern Cham", "Tjam" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "US" ], @@ -21984,7 +22902,7 @@ "iana": [ "Eastern Cham" ], "iso639_3": "cjm", "name": "Cham, Eastern", - "names": [ "Bhamam", "Chiem", "Chiem Thành", "Eastern Cham", "Tjam" ], + "names": [ "Bhamam", "Chiem", "Chiem Thành", "Chăm", "Eastern Cham", "Tjam" ], "nophonvars": true, "region": "VN", "regionname": "Viet Nam", @@ -21998,8 +22916,9 @@ "full": "cjm-Latn-VN", "iana": [ "Eastern Cham" ], "iso639_3": "cjm", + "localnames": [ "Chăm" ], "name": "Cham, Eastern", - "names": [ "Bhamam", "Chiem", "Chiem Thành", "Eastern Cham", "Tjam" ], + "names": [ "Bhamam", "Chiem", "Chiem Thành", "Chăm", "Eastern Cham", "Tjam" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "US" ], @@ -22027,8 +22946,9 @@ "full": "cjo-Latn-PE", "iana": [ "Ashéninka Pajonal" ], "iso639_3": "cjo", + "localnames": [ "Ashaninka" ], "name": "Ashéninka, Pajonal", - "names": [ "Asheninka", "Ashéninca", "Ashéninka Pajonal", "Atsiri", "Pajonal" ], + "names": [ "Ashaninka", "Asheninca del Gran Pajonal", "Asheninka", "Ashéninca", "Ashéninka Pajonal", "Atsiri", "Campa Pajonalino", "Pajonal" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -22101,20 +23021,22 @@ "full": "cjy-Hans-CN", "iana": [ "Jinyu Chinese" ], "iso639_3": "cjy", + "macrolang": "zh", "name": "Chinese, Jinyu", "names": [ "Jin Chinese", "Jinese", "Jinhua", "Jinyu", "Jinyu Chinese" ], - "nophonvars": true, "region": "CN", "regionname": "China", "script": "Hans", "sldr": false, - "tag": "cjy-Hans", + "tag": "cjy", + "tags": [ "cjy-CN", "cjy-Hans", "zh-cjy", "zh-cjy-CN" ], "windows": "cjy-Hans" }, { "full": "cjy-Hant-CN", "iana": [ "Jinyu Chinese" ], "iso639_3": "cjy", + "macrolang": "zh", "name": "Chinese, Jinyu", "names": [ "Jin Chinese", "Jinese", "Jinhua", "Jinyu", "Jinyu Chinese" ], "nophonvars": true, @@ -22132,6 +23054,7 @@ "latnnames": [ "zimanê soranî" ], "localname": "کوردیی ناوەندی", "localnames": [ "زمانێ سۆرانی" ], + "macrolang": "ku", "name": "Kurdish, Central", "names": [ "Central Kurdish", "Kordi", "Korkora", "Kurdi", "Kurdy", "Sine’i", "Sorani", "Wawa" ], "region": "IQ", @@ -22140,7 +23063,7 @@ "sldr": true, "tag": "ckb", "tags": [ "ckb-Arab", "ckb-IQ" ], - "windows": "ckb-Arab" + "windows": "ckb-Arab-IQ" }, { "full": "ckb-Arab-IR", @@ -22149,6 +23072,7 @@ "latnnames": [ "zimanê soranî" ], "localname": "کوردیی ناوەندی", "localnames": [ "زمانێ سۆرانی" ], + "macrolang": "ku", "name": "Kurdish, Central", "names": [ "Central Kurdish", "Kordi", "Korkora", "Kurdi", "Kurdy", "Sine’i", "Sorani", "Wawa" ], "region": "IR", @@ -22162,6 +23086,7 @@ "full": "ckb-Latn-IQ", "iana": [ "Central Kurdish" ], "iso639_3": "ckb", + "macrolang": "ku", "name": "Kurdish, Central", "names": [ "Central Kurdish", "Kordi", "Korkora", "Kurdi", "Kurdy", "Sine’i", "Sorani", "Wawa" ], "region": "IQ", @@ -22170,7 +23095,7 @@ "script": "Latn", "sldr": false, "tag": "ckb-Latn", - "windows": "ckb-Latn" + "windows": "ckb-Latn-IQ" }, { "full": "ckh-Zyyy-BD", @@ -22192,7 +23117,7 @@ "iana": [ "Cibak" ], "iso639_3": "ckl", "name": "Kibaku", - "names": [ "Chibbak", "Chibbuk", "Chibok", "Chibuk", "Cibak", "Cibuk", "Kibbaku", "Kikuk", "Kyibaku" ], + "names": [ "Chibak", "Chibbak", "Chibbuk", "Chibok", "Chibuk", "Cibak", "Cibuk", "Cíbɔ̀k", "Kibbaku", "Kikuk", "Kyibaku" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -22203,12 +23128,14 @@ }, { "full": "ckm-Latn-HR", + "iana": [ "Chakavian" ], "iso639_3": "ckm", "localnames": [ "Čakavski" ], "name": "Chakavian", - "names": [ "Chakavski", "Chekavski", "Čakavian", "Čakavica", "Čakavski", "Čakavština", "Čekavski", "Čokovski" ], + "names": [ "Burgenland Croatian", "Burgenlandkroatische Sprache", "Chakavski", "Chekavski", "Gradišćanskohrvatski jezik", "Gradišćei Horvát Nyelv", "Čakavian", "Čakavica", "Čakavski", "Čakavština", "Čekavski", "Čokovski" ], "region": "HR", "regionname": "Croatia", + "regions": [ "AT", "HU", "SK" ], "script": "Latn", "sldr": false, "tag": "ckm", @@ -22217,6 +23144,7 @@ }, { "full": "ckm-Glag-HR", + "iana": [ "Chakavian" ], "iso639_3": "ckm", "name": "Chakavian", "names": [ "Chakavski", "Chekavski", "Čakavian", "Čakavica", "Čakavski", "Čakavština", "Čekavski", "Čokovski" ], @@ -22311,7 +23239,7 @@ "latnnames": [ "Ḷygʺoravėtḷʹėn jiḷyjiḷ" ], "localnames": [ "Ԓыгъоравэтԓьэн йиԓыйиԓ" ], "name": "Chukchi", - "names": [ "Chuchee", "Chukcha", "Chukchee", "Chukot", "Luoravetlan" ], + "names": [ "Chukcha", "Chukchee", "Chukot", "Lygoravetlen" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -22397,7 +23325,9 @@ "full": "cla-Latn-NG", "iana": [ "Ron" ], "iso639_3": "cla", + "localnames": [ "Ron" ], "name": "Ron", + "names": [ "Challawa", "Run" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -22406,6 +23336,70 @@ "tags": [ "cla-Latn", "cla-NG" ], "windows": "cla-Latn" }, + { + "full": "cla-Latn-NG-x-bokkos", + "iana": [ "Ron" ], + "iso639_3": "cla", + "localnames": [ "Ron" ], + "name": "Ron", + "names": [ "Challawa", "Run" ], + "region": "NG", + "regionname": "Nigeria", + "rod": "2469", + "script": "Latn", + "sldr": false, + "tag": "cla-x-bokkos", + "tags": [ "cla-Latn-x-bokkos", "cla-NG-x-bokkos" ], + "windows": "cla-Latn-x-bokkos" + }, + { + "full": "cla-Latn-NG-x-daffo", + "iana": [ "Ron" ], + "iso639_3": "cla", + "localnames": [ "Ron" ], + "name": "Ron", + "names": [ "Challawa", "Run" ], + "region": "NG", + "regionname": "Nigeria", + "rod": "2470", + "script": "Latn", + "sldr": false, + "tag": "cla-x-daffo", + "tags": [ "cla-Latn-x-daffo", "cla-NG-x-daffo" ], + "windows": "cla-Latn-x-daffo" + }, + { + "full": "cla-Latn-NG-x-mangar", + "iana": [ "Ron" ], + "iso639_3": "cla", + "localnames": [ "Ron" ], + "name": "Ron", + "names": [ "Challawa", "Run" ], + "region": "NG", + "regionname": "Nigeria", + "rod": "2542", + "script": "Latn", + "sldr": false, + "tag": "cla-x-mangar", + "tags": [ "cla-Latn-x-mangar", "cla-NG-x-mangar" ], + "windows": "cla-Latn-x-mangar" + }, + { + "full": "cla-Latn-NG-x-monguna", + "iana": [ "Ron" ], + "iso639_3": "cla", + "localnames": [ "Ron" ], + "name": "Ron", + "names": [ "Challawa", "Run" ], + "region": "NG", + "regionname": "Nigeria", + "rod": "2471", + "script": "Latn", + "sldr": false, + "tag": "cla-x-monguna", + "tags": [ "cla-Latn-x-monguna", "cla-NG-x-monguna" ], + "windows": "cla-Latn-x-monguna" + }, { "full": "clc-Latn-CA", "iana": [ "Chilcotin" ], @@ -22495,7 +23489,7 @@ "sldr": false, "tag": "clk", "tags": [ "clk-IN", "clk-Latn" ], - "windows": "clk-Latn" + "windows": "clk-Latn-IN" }, { "full": "clk-Tibt-CN", @@ -22511,7 +23505,7 @@ "sldr": false, "tag": "clk-Tibt", "tags": [ "clk-CN" ], - "windows": "clk-Tibt" + "windows": "clk-Tibt-CN" }, { "full": "cll-Latn-GH", @@ -22536,6 +23530,7 @@ "names": [ "Klallam", "Na’klallam", "S’klallam" ], "region": "US", "regionname": "United States", + "regions": [ "CA" ], "script": "Latn", "sldr": false, "tag": "clm", @@ -22577,6 +23572,7 @@ "full": "clu-Latn-PH", "iana": [ "Caluyanun" ], "iso639_3": "clu", + "localnames": [ "Caluyanun" ], "name": "Caluyanun", "names": [ "Caluyanen", "Caluyanhon", "Caluyanon" ], "region": "PH", @@ -22653,26 +23649,51 @@ "iana": [ "Classical Mongolian" ], "iso639_3": "cmg", "name": "Classical Mongolian", - "nophonvars": true, "region": "MN", "regionname": "Mongolia", "script": "Soyo", "sldr": false, - "tag": "cmg-Soyo", - "tags": [ "cmg-MN" ], + "tag": "cmg", + "tags": [ "cmg-MN", "cmg-Soyo" ], "windows": "cmg-Soyo" }, + { + "full": "cmg-Mong-MN", + "iana": [ "Classical Mongolian" ], + "iso639_3": "cmg", + "name": "Classical Mongolian", + "nophonvars": true, + "region": "MN", + "regionname": "Mongolia", + "script": "Mong", + "sldr": false, + "tag": "cmg-Mong", + "windows": "cmg-Mong" + }, + { + "full": "cmg-Zanb-MN", + "iana": [ "Classical Mongolian" ], + "iso639_3": "cmg", + "name": "Classical Mongolian", + "nophonvars": true, + "region": "MN", + "regionname": "Mongolia", + "script": "Zanb", + "sldr": false, + "tag": "cmg-Zanb", + "windows": "cmg-Zanb" + }, { "full": "cmi-Latn-CO", "iana": [ "Emberá-Chamí" ], "iso639_3": "cmi", - "localnames": [ "ẽbẽra" ], - "name": "Emberá-Chamí", - "names": [ "Chami", "Chamí Emberá", "Embena", "Embera", "Epena", "ẽbẽra" ], + "localnames": [ "Ẽbẽra", "Ẽbẽra bed̶ea" ], + "name": "Embera Chamí", + "names": [ "Chami", "Chamí Emberá", "Embena", "Embera", "Embera Bedea Chamí", "Emberá-Chamí", "Epena", "Ẽbẽra", "Ẽbẽra bed̶ea" ], "region": "CO", "regionname": "Colombia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "cmi", "tags": [ "cmi-CO", "cmi-Latn" ], "windows": "cmi-Latn" @@ -22681,8 +23702,8 @@ "full": "cml-Latn-ID", "iana": [ "Campalagian" ], "iso639_3": "cml", - "name": "Campalagian", - "names": [ "Koneq-Koneqe", "Kone’-Kone’e", "Tallumpanuae", "Tasing", "Tjampalagian" ], + "name": "Koneq-koneq", + "names": [ "Campalagian", "Kone-Konee", "Kone’-Kone’e", "Tallumpanuae", "Tasing", "Tjampalagian" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -22697,6 +23718,7 @@ "iana": [ "Michigamea" ], "iso639_3": "cmm", "name": "Michigamea", + "names": [ "Mitchigamea" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -22706,22 +23728,6 @@ "unwritten": true, "windows": "cmm-Zyyy" }, - { - "full": "cmo-Khmr-KH", - "iana": [ "Central Mnong" ], - "iso639_3": "cmo", - "localname": "Bunong", - "name": "Mnong, Central", - "names": [ "Bu Nong", "Budang", "Budong", "Bunong", "Central Bu Nong", "Central Mnong", "Phanong", "Phnong", "Pnong" ], - "nophonvars": true, - "region": "KH", - "regionname": "Cambodia", - "regions": [ "VN" ], - "script": "Khmr", - "sldr": true, - "tag": "cmo-Khmr", - "windows": "cmo-Khmr" - }, { "full": "cmo-Latn-VN", "iana": [ "Central Mnong" ], @@ -22734,13 +23740,14 @@ "regionname": "Viet Nam", "script": "Latn", "sldr": true, - "tag": "cmo-Latn", - "tags": [ "cmo-VN" ], - "windows": "cmo-Latn" + "tag": "cmo", + "tags": [ "cmo-Latn", "cmo-VN" ], + "windows": "cmo-Latn-VN" }, { "full": "cmo-Latn-KH", "iana": [ "Central Mnong" ], + "iso639_3": "cmo", "localname": "Bunong", "localnames": [ "Bunong" ], "name": "Mnong, Central", @@ -22749,9 +23756,27 @@ "regionname": "Cambodia", "script": "Latn", "sldr": true, - "tag": "cmo-Latn-KH", + "tag": "cmo-KH", "windows": "cmo-Latn-KH" }, + { + "full": "cmo-Khmr-KH", + "iana": [ "Central Mnong" ], + "iso639_3": "cmo", + "latnnames": [ "Bunong" ], + "localname": "ឞូន៝ង", + "localnames": [ "ឞូន៝ង" ], + "name": "Mnong, Central", + "names": [ "Bu Nong", "Budang", "Budong", "Bunong", "Central Bu Nong", "Central Mnong", "Phanong", "Phnong", "Pnong" ], + "nophonvars": true, + "region": "KH", + "regionname": "Cambodia", + "regions": [ "VN" ], + "script": "Khmr", + "sldr": true, + "tag": "cmo-Khmr", + "windows": "cmo-Khmr-KH" + }, { "full": "cmr-Latn-MM", "iana": [ "Mro-Khimi Chin" ], @@ -22816,7 +23841,7 @@ "iso639_3": "cnb", "localnames": [ "Uppu", "Uppu Chin" ], "name": "Chin, Uppu", - "names": [ "Chin Pong", "Chin Pung", "Chinbon Chin", "Chindwin Chin", "Chinpon", "Oo-pu", "Sho", "Tuishiip", "Tuiship", "Uppu", "Ütbü" ], + "names": [ "Chin Pong", "Chin Pung", "Chinbon Chin", "Chindwin Chin", "Chinpon", "Oo-pu", "Sho", "Tuishiip", "Tuiship", "Uppu", "Uppu Chin", "Ütbü" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -22934,22 +23959,26 @@ }, { "full": "cnp-Hans-CN", + "iana": [ "Northern Ping Chinese", "Northern Pinghua" ], "iso639_3": "cnp", + "macrolang": "zh", "name": "Pinghua, Northern", - "names": [ "Guìběi Pínghuà", "Northern Guangxi", "Northern Ping Chinese" ], - "nophonvars": true, + "names": [ "Guìběi Pínghuà", "Northern Guangxi", "Northern Ping Chinese", "Northern Pinghua" ], "region": "CN", "regionname": "China", "script": "Hans", "sldr": false, - "tag": "cnp-Hans", + "tag": "cnp", + "tags": [ "cnp-CN", "cnp-Hans", "zh-cnp", "zh-cnp-CN" ], "windows": "cnp-Hans" }, { "full": "cnp-Hant-CN", + "iana": [ "Northern Ping Chinese", "Northern Pinghua" ], "iso639_3": "cnp", + "macrolang": "zh", "name": "Pinghua, Northern", - "names": [ "Guìběi Pínghuà", "Northern Guangxi", "Northern Ping Chinese" ], + "names": [ "Guìběi Pínghuà", "Northern Guangxi", "Northern Ping Chinese", "Northern Pinghua" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -22958,12 +23987,28 @@ "tag": "cnp-Hant", "windows": "cnp-Hant" }, + { + "full": "cnq-Latn-CM", + "iana": [ "Chung" ], + "iso639_3": "cnq", + "localnames": [ "Chuŋ" ], + "name": "Chung", + "names": [ "Chuŋ", "Cung" ], + "region": "CM", + "regionname": "Cameroon", + "script": "Latn", + "sldr": false, + "tag": "cnq", + "tags": [ "cnq-CM", "cnq-Latn", "cug", "cug-CM", "cug-Latn", "cug-Latn-CM" ], + "windows": "cnq-Latn" + }, { "full": "cnr-Cyrl-ME", "iana": [ "Montenegrin" ], "iso639_3": "cnr", "latnnames": [ "Crnogorski" ], "localnames": [ "црногорски" ], + "macrolang": "sh", "name": "Montenegrin", "names": [ "Serbo-Croatian" ], "region": "ME", @@ -22979,6 +24024,7 @@ "full": "cnr-Latn-ME", "iana": [ "Montenegrin" ], "iso639_3": "cnr", + "macrolang": "sh", "name": "Montenegrin", "names": [ "Serbo-Croatian" ], "region": "ME", @@ -23024,7 +24070,7 @@ "iana": [ "Chenoua" ], "iso639_3": "cnu", "name": "Chenoua", - "names": [ "Chenoui" ], + "names": [ "Chenoui", "Haqbaylit̠", "Shenwa" ], "region": "DZ", "regionname": "Algeria", "script": "Zyyy", @@ -23038,6 +24084,7 @@ "full": "cnw-Latn-MM", "iana": [ "Ngawn Chin" ], "iso639_3": "cnw", + "localnames": [ "Ngawn" ], "name": "Chin, Ngawn", "names": [ "Ngawn", "Ngawn Chin", "Ngon", "Ngorn" ], "region": "MM", @@ -23081,6 +24128,7 @@ "full": "coa-Latn-AU", "iana": [ "Cocos Islands Malay" ], "iso639_3": "coa", + "macrolang": "ms", "name": "Malay, Cocos Islands", "names": [ "Cocos", "Cocos Islands", "Cocos Islands Malay", "Kokos", "Kukus", "Melayu Cocos", "Ong Pulu" ], "region": "AU", @@ -23129,8 +24177,9 @@ "full": "cod-Latn-PE", "iana": [ "Cocama-Cocamilla" ], "iso639_3": "cod", + "localnames": [ "Kokama-Kokamilla" ], "name": "Kukama-Kukamiria", - "names": [ "Cocama", "Cocama-Cocamilla", "Huallaga", "Inikana", "Kocama", "Kokama", "Kokama-Kokamilya", "Kukama", "Pampadeque", "Pandequebo", "Ucayali", "Xibitaoan" ], + "names": [ "Cocama", "Cocama-Cocamilla", "Huallaga", "Inikana", "Kocama", "Kokama", "Kokama-Kokamilla", "Kokama-Kokamilya", "Pampadeque", "Pandequebo", "Ucayali", "Xibitaoan" ], "region": "PE", "regionname": "Peru", "regions": [ "BR", "CO" ], @@ -23144,9 +24193,10 @@ "full": "coe-Latn-CO", "iana": [ "Koreguaje" ], "iso639_3": "coe", + "localname": "Koʼrevajʉ chʉʼo", "localnames": [ "Koʼrevajʉ chʉʼo" ], "name": "Koreguaje", - "names": [ "Caquetá", "Chaocha Pai", "Coreguaja", "Coreguaje", "Coreguaxe", "Correguaje", "Korebaju", "Korewahe", "Koʼrevajʉ chʉʼo", "Ko’reuaju" ], + "names": [ "Caquetá", "Chaocha Pai", "Coreguaja", "Coreguaje", "Coreguaxe", "Correguaje", "Korebaju", "Korewahe", "Koʼrehuajʉ chʉʼo", "Koʼrevajʉ chʉʼo", "Ko’reuaju" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -23159,7 +24209,7 @@ "full": "cof-Latn-EC", "iana": [ "Colorado" ], "iso639_3": "cof", - "localname": "Tsafiki", + "localname": "Tsafiqui", "localnames": [ "Tsafiki" ], "name": "Tsafiki", "names": [ "Colorado", "Tsachela", "Tsachila", "Tsafiqui" ], @@ -23192,8 +24242,9 @@ "full": "coh-Latn-KE", "iana": [ "Chonyi-Dzihana-Kauma", "Chichonyi-Chidzihana-Chikauma" ], "iso639_3": "coh", + "localnames": [ "Chichonyi-Chidzihana-Chikauma" ], "name": "Chichonyi-Chidzihana-Chikauma", - "names": [ "Chonyi", "Chonyi-Dzihana-Kauma", "Conyi", "Kichonyi" ], + "names": [ "Chonyi-Dzihana-Kauma" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -23221,10 +24272,10 @@ "full": "cok-Latn-MX", "iana": [ "Santa Teresa Cora" ], "iso639_3": "cok", - "localname": "Cora, Santa Teresa", - "localnames": [ "Kwéimarusa’na" ], + "localname": "Cuéimarusaꞌna", + "localnames": [ "Cuéimarusaꞌna" ], "name": "Cora, Santa Teresa", - "names": [ "Kwéimarusa’na", "Santa Teresa Cora" ], + "names": [ "Cuéimarusaꞌna", "Kwéimarusaꞌna", "Santa Teresa Cora" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -23267,6 +24318,7 @@ "full": "con-Latn-EC", "iana": [ "Cofán" ], "iso639_3": "con", + "localname": "A'ingae", "localnames": [ "A'ingae" ], "name": "Cofán", "names": [ "A’i", "A’ingae", "Kofan", "Kofane", "Kofán" ], @@ -23275,8 +24327,8 @@ "regions": [ "CO" ], "script": "Latn", "sldr": true, - "tag": "con", - "tags": [ "con-EC", "con-Latn" ], + "tag": "con-Latn", + "tags": [ "con", "con-EC" ], "windows": "con-Latn" }, { @@ -23298,8 +24350,9 @@ "full": "cop-Copt-EG", "iana": [ "Coptic" ], "iso639_3": "cop", + "latnnames": [ "timetremənkʰēmi" ], + "localnames": [ "ϯⲙⲉⲧⲣⲉⲙⲛ̀ⲭⲏⲙⲓ" ], "name": "Coptic", - "names": [ "Neo-Egyptian" ], "region": "EG", "regionname": "Egypt", "script": "Copt", @@ -23313,7 +24366,6 @@ "iana": [ "Coptic" ], "iso639_3": "cop", "name": "Coptic", - "names": [ "Neo-Egyptian" ], "nophonvars": true, "region": "EG", "regionname": "Egypt", @@ -23327,7 +24379,6 @@ "iana": [ "Coptic" ], "iso639_3": "cop", "name": "Coptic", - "names": [ "Neo-Egyptian" ], "nophonvars": true, "region": "EG", "regionname": "Egypt", @@ -23337,19 +24388,18 @@ "windows": "cop-Grek" }, { - "full": "coq-Zyyy-US", + "full": "coq-Latn-US", "iana": [ "Coquille" ], "iso639_3": "coq", "name": "Coquille", "names": [ "Chasta Costa", "Ko’kwel", "Miluk", "Mishikhwutmetunee", "Tutuni-Chasta Costa-Coquille", "Upper Coquille" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "coq", - "tags": [ "coq-US", "coq-Zyyy" ], - "unwritten": true, - "windows": "coq-Zyyy" + "tags": [ "coq-Latn", "coq-US" ], + "windows": "coq-Latn" }, { "full": "cot-Latn-PE", @@ -23371,10 +24421,10 @@ "full": "cou-Latn-SN", "iana": [ "Wamey" ], "iso639_3": "cou", - "localname": "Wamey", - "localnames": [ "Wamey" ], + "localname": "Wameỹ", + "localnames": [ "Wameỹ" ], "name": "Wamey", - "names": [ "Cogniagui", "Conhague", "Coniagui", "Koniagi", "Koniagui", "Konyagi", "Tenda", "Wamay", "Wamei", "Wey" ], + "names": [ "Cogniagui", "Conhague", "Coniagui", "Koniagi", "Koniagui", "Konyagi", "Tenda", "Wamay", "Wamei", "Wameỹ", "Wey" ], "region": "SN", "regionname": "Senegal", "regions": [ "GN" ], @@ -23389,7 +24439,7 @@ "iana": [ "Cao Miao" ], "iso639_3": "cov", "name": "Cao Miao", - "names": [ "Grass Miao", "Mjiuniang", "Sanjiang Miao" ], + "names": [ "Grass Miao", "Mjiuniang", "Sanjiang Miao", "mjiu ɲaŋ" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -23581,6 +24631,7 @@ "full": "cpx-Latn-CN", "iana": [ "Pu-Xian Chinese" ], "iso639_3": "cpx", + "macrolang": "zh", "name": "Chinese, Pu-Xian", "names": [ "Heng", "Henghua", "Hing", "Hinghua", "Hsien", "Hsienyu", "Hsing", "Hsinghua", "Pu-Xian Chinese", "Putian", "Putten", "Xianyou", "Xing", "Xinghua" ], "obsolete": true, @@ -23612,6 +24663,7 @@ "full": "cqd-Latn-CN", "iana": [ "Chuanqiandian Cluster Miao" ], "iso639_3": "cqd", + "macrolang": "hmn", "name": "Miao, Chuanqiandian Cluster", "names": [ "Chuanchientien Miao", "Chuanqiandian Cluster Miao", "Chuanqiandian Miao", "Core Farwestern Hmongic", "Hua Miao", "Sichuan-Guizhou-Yunnan Miao", "Western Miao" ], "region": "CN", @@ -23629,8 +24681,8 @@ "latnnames": [ "nīhithawīwin" ], "localname": "ᓀᐦᐃᖬᐍᐏᐣ", "localnames": [ "ᓀᐦᐃᖬᐍᐏᐣ" ], - "name": "Cree, Woods", - "names": [ "Cree", "Cri des bois", "Nīhithawīwin" ], + "name": "Cree", + "names": [ "Cree, Woods", "Cri des bois", "Nīhithawīwin" ], "region": "CA", "regionname": "Canada", "script": "Cans", @@ -23644,8 +24696,8 @@ "iana": [ "Cree" ], "iso639_3": "cre", "localnames": [ "Nīhithawīwin" ], - "name": "Cree, Woods", - "names": [ "Cree", "Cri des bois", "Nīhithawīwin" ], + "name": "Cree", + "names": [ "Cree, Woods", "Cri des bois", "Nīhithawīwin" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -23673,6 +24725,7 @@ "iana": [ "Island Carib" ], "iso639_3": "crb", "name": "Island Carib", + "names": [ "Igneri", "Inyeri", "Iñeri", "Kalinago" ], "region": "VC", "regionname": "Saint Vincent and the Grenadines", "regions": [ "DM" ], @@ -23746,10 +24799,10 @@ "full": "crh-Cyrl-UA", "iana": [ "Crimean Tatar", "Crimean Turkish" ], "iso639_3": "crh", - "latnnames": [ "Qırımtatar tili", "Qırımtatarca" ], - "localnames": [ "Къырымтатар тили", "Къырымтатарджа" ], + "latnnames": [ "Qırımtatar tili", "Qırımtatarca", "Qırım tili" ], + "localnames": [ "Къырымтатар тили", "Къырымтатарджа", "къырым тили" ], "name": "Crimean Tatar", - "names": [ "Crimean", "Crimean Turkish", "Qirim", "Qirimtatar", "Qırımtatar tili", "Qırımtatarca" ], + "names": [ "Crimean", "Crimean Turkish", "Qirim", "Qirim tili", "Qirimtatar", "Qırım Türkçesi", "Qırımca" ], "region": "UA", "regionname": "Ukraine", "regions": [ "BG", "KG", "MD", "RO", "RU", "TR", "US", "UZ" ], @@ -23757,15 +24810,16 @@ "sldr": false, "tag": "crh", "tags": [ "crh-Cyrl", "crh-UA" ], - "windows": "crh-Cyrl" + "windows": "crh-Cyrl-UA" }, { "full": "crh-Arab-UZ", "iana": [ "Crimean Tatar", "Crimean Turkish" ], "iso639_3": "crh", "name": "Crimean Tatar", - "names": [ "Crimean", "Crimean Turkish", "Qirim", "Qirimtatar", "Qırımtatar tili", "Qırımtatarca" ], + "names": [ "Crimean", "Crimean Turkish", "Qirim", "Qirim tili", "Qirimtatar", "Qırım Türkçesi", "Qırımca" ], "nophonvars": true, + "obsolete": true, "region": "UZ", "regionname": "Uzbekistan", "regions": [ "BG", "KG", "MD", "RO", "RU", "TR", "UA", "US" ], @@ -23773,15 +24827,14 @@ "sldr": false, "tag": "crh-Arab", "tags": [ "crh-UZ" ], - "windows": "crh-Arab" + "windows": "crh-Arab-UZ" }, { "full": "crh-Latn-UA", "iana": [ "Crimean Tatar", "Crimean Turkish" ], "iso639_3": "crh", - "localnames": [ "Qırımtatar tili", "Qırımtatarca" ], "name": "Crimean Tatar", - "names": [ "Crimean", "Crimean Turkish", "Qirim", "Qirimtatar", "Qırımtatar tili", "Qırımtatarca" ], + "names": [ "Crimean", "Crimean Turkish", "Qirim", "Qirim tili", "Qirimtatar", "Qırım Türkçesi", "Qırımca" ], "region": "UA", "regionname": "Ukraine", "regions": [ "BG", "KG", "MD", "RO", "RU", "TR", "US", "UZ" ], @@ -23789,14 +24842,15 @@ "sldr": false, "tag": "crh-Latn", "variants": [ "baku1926" ], - "windows": "crh-Latn" + "windows": "crh-Latn-UA" }, { "full": "cri-Latn-ST", "iana": [ "Sãotomense" ], "iso639_3": "cri", + "localnames": [ "Sãotomense" ], "name": "Sãotomense", - "names": [ "Forro", "Santomense", "Santomé", "São Tomense" ], + "names": [ "Dialeto", "Forro", "Fôlô", "Lungwa santome", "Santomense", "Santomé", "São Tomense" ], "region": "ST", "regionname": "Sao Tome and Principe", "regions": [ "PT" ], @@ -23812,9 +24866,11 @@ "iana": [ "Southern East Cree" ], "iso639_3": "crj", "latnnames": [ "Īnū Ayimūn" ], + "localname": "ᐄᓅ ᐊᔨᒨᓐ", "localnames": [ "ᐄᓅ ᐊᔨᒨᓐ" ], + "macrolang": "cr", "name": "Cree, Southern East", - "names": [ "Cri du Sud-Est", "Eastern James Bay Cree Southern Dialect", "James Bay Cree Southern Dialect", "Southern East Cree" ], + "names": [ "Cri du Sud-Est", "Eastern James Bay Cree Southern Dialect", "James Bay Cree Southern Dialect", "Southern East Cree", "Īnū Ayimūn" ], "region": "CA", "regionname": "Canada", "script": "Cans", @@ -23828,8 +24884,10 @@ "iana": [ "Southern East Cree" ], "iso639_3": "crj", "localname": "Īnū Ayimūn", + "localnames": [ "Īnū Ayimūn" ], + "macrolang": "cr", "name": "Cree, Southern East", - "names": [ "Cri du Sud-Est", "Eastern James Bay Cree Southern Dialect", "James Bay Cree Southern Dialect", "Southern East Cree" ], + "names": [ "Cri du Sud-Est", "Eastern James Bay Cree Southern Dialect", "James Bay Cree Southern Dialect", "Southern East Cree", "Īnū Ayimūn" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -23843,6 +24901,7 @@ "iso639_3": "crk", "latnnames": [ "nēhiyawēwin" ], "localnames": [ "ᓀᐦᐃᔭᐍᐏᐣ" ], + "macrolang": "cr", "name": "Cree, Plains", "names": [ "Cri des plaines", "Nehiyaw", "Nēhiyawēwin", "Plains Cree", "Western Cree" ], "region": "CA", @@ -23859,6 +24918,7 @@ "iana": [ "Plains Cree" ], "iso639_3": "crk", "localnames": [ "Nēhiyawēwin" ], + "macrolang": "cr", "name": "Cree, Plains", "names": [ "Cri des plaines", "Nehiyaw", "Nēhiyawēwin", "Plains Cree", "Western Cree" ], "region": "CA", @@ -23874,8 +24934,9 @@ "iana": [ "Northern East Cree" ], "iso639_3": "crl", "latnnames": [ "Īyiyū Ayimūn" ], - "localname": "Īyiyū Ayimūn", + "localname": "ᐄᔨᔫ ᐊᔨᒨᓐ", "localnames": [ "ᐄᔨᔫ ᐊᔨᒨᓐ" ], + "macrolang": "cr", "name": "Cree, Northern East", "names": [ "Cri du Nord-Est", "East Cree", "East Main Cree", "Eastern James Bay Cree Northern Dialect", "Iyiyiw-Ayimiwin", "James Bay Cree", "James Bay Cree Northern", "Northern East Cree" ], "region": "CA", @@ -23891,6 +24952,7 @@ "iana": [ "Northern East Cree" ], "iso639_3": "crl", "localnames": [ "Iyiyiw-Ayimiwin" ], + "macrolang": "cr", "name": "Cree, Northern East", "names": [ "Cri du Nord-Est", "East Cree", "East Main Cree", "Eastern James Bay Cree Northern Dialect", "Iyiyiw-Ayimiwin", "James Bay Cree", "James Bay Cree Northern", "Northern East Cree" ], "region": "CA", @@ -23906,6 +24968,7 @@ "iso639_3": "crm", "latnnames": [ "ililîmowin" ], "localnames": [ "ᐃᓕᓖᒧᐧᐃᓐ" ], + "macrolang": "cr", "name": "Cree, Moose", "names": [ "Cri de Moose", "Moose Cree", "West Main Cree", "West Shore Cree", "York Cree" ], "region": "CA", @@ -23933,6 +24996,102 @@ "tags": [ "crn-Latn", "crn-MX" ], "windows": "crn-Latn" }, + { + "full": "crn-Latn-MX-x-corapeno", + "iana": [ "Cora, El Nayar" ], + "iso639_3": "crn", + "localnames": [ "Naáyeri Nyuuca" ], + "name": "Cora, El Nayar", + "names": [ "Cora", "Cora de El Nayar", "Cora tereseño", "Kora", "Naáyeri", "Naáyeri Niuka", "Naáyeri Nyuuca" ], + "region": "MX", + "regionname": "Mexico", + "rod": "24694", + "script": "Latn", + "sldr": false, + "tag": "crn-x-corapeno", + "tags": [ "crn-Latn-x-corapeno", "crn-MX-x-corapeno" ], + "windows": "crn-Latn-x-corapeno" + }, + { + "full": "crn-Latn-MX-x-doleres", + "iana": [ "Cora, El Nayar" ], + "iso639_3": "crn", + "localnames": [ "Naáyeri Nyuuca" ], + "name": "Cora, El Nayar", + "names": [ "Cora", "Cora de El Nayar", "Cora tereseño", "Kora", "Naáyeri", "Naáyeri Niuka", "Naáyeri Nyuuca" ], + "region": "MX", + "regionname": "Mexico", + "rod": "24695", + "script": "Latn", + "sldr": false, + "tag": "crn-x-doleres", + "tags": [ "crn-Latn-x-doleres", "crn-MX-x-doleres" ], + "windows": "crn-Latn-x-doleres" + }, + { + "full": "crn-Latn-MX-x-gavilan", + "iana": [ "Cora, El Nayar" ], + "iso639_3": "crn", + "localnames": [ "Naáyeri Nyuuca" ], + "name": "Cora, El Nayar", + "names": [ "Cora", "Cora de El Nayar", "Cora tereseño", "Kora", "Naáyeri", "Naáyeri Niuka", "Naáyeri Nyuuca" ], + "region": "MX", + "regionname": "Mexico", + "rod": "24698", + "script": "Latn", + "sldr": false, + "tag": "crn-x-gavilan", + "tags": [ "crn-Latn-x-gavilan", "crn-MX-x-gavilan" ], + "windows": "crn-Latn-x-gavilan" + }, + { + "full": "crn-Latn-MX-x-meseno", + "iana": [ "Cora, El Nayar" ], + "iso639_3": "crn", + "localnames": [ "Naáyeri Nyuuca" ], + "name": "Cora, El Nayar", + "names": [ "Cora", "Cora de El Nayar", "Cora tereseño", "Kora", "Naáyeri", "Naáyeri Niuka", "Naáyeri Nyuuca" ], + "region": "MX", + "regionname": "Mexico", + "rod": "4921", + "script": "Latn", + "sldr": false, + "tag": "crn-x-meseno", + "tags": [ "crn-Latn-x-meseno", "crn-MX-x-meseno" ], + "windows": "crn-Latn-x-meseno" + }, + { + "full": "crn-Latn-MX-x-presiden", + "iana": [ "Cora, El Nayar" ], + "iso639_3": "crn", + "localnames": [ "Naáyeri Nyuuca" ], + "name": "Cora, El Nayar", + "names": [ "Cora", "Cora de El Nayar", "Cora tereseño", "Kora", "Naáyeri", "Naáyeri Niuka", "Naáyeri Nyuuca" ], + "region": "MX", + "regionname": "Mexico", + "rod": "20089", + "script": "Latn", + "sldr": false, + "tag": "crn-x-presiden", + "tags": [ "crn-Latn-x-presiden", "crn-MX-x-presiden" ], + "windows": "crn-Latn-x-presiden" + }, + { + "full": "crn-Latn-MX-x-rosarito", + "iana": [ "Cora, El Nayar" ], + "iso639_3": "crn", + "localnames": [ "Naáyeri Nyuuca" ], + "name": "Cora, El Nayar", + "names": [ "Cora", "Cora de El Nayar", "Cora tereseño", "Kora", "Naáyeri", "Naáyeri Niuka", "Naáyeri Nyuuca" ], + "region": "MX", + "regionname": "Mexico", + "rod": "24696", + "script": "Latn", + "sldr": false, + "tag": "crn-x-rosarito", + "tags": [ "crn-Latn-x-rosarito", "crn-MX-x-rosarito" ], + "windows": "crn-Latn-x-rosarito" + }, { "full": "cro-Latn-US", "iana": [ "Crow" ], @@ -23969,12 +25128,13 @@ "iana": [ "Carolina Algonquian" ], "iso639_3": "crr", "name": "Carolina Algonquian", + "names": [ "Croatoan", "Pamlico" ], "region": "US", "regionname": "United States", "script": "Zyyy", "sldr": false, "tag": "crr", - "tags": [ "crr-US", "crr-Zyyy" ], + "tags": [ "crr-US", "crr-Zyyy", "pmk", "pmk-US", "pmk-Zyyy", "pmk-Zyyy-US" ], "unwritten": true, "windows": "crr-Zyyy" }, @@ -23983,8 +25143,8 @@ "iana": [ "Seselwa Creole French" ], "iso639_3": "crs", "localnames": [ "Seselwa" ], - "name": "Seychelles Creole French", - "names": [ "Creole", "Ilois", "Kreol", "Kreol Seselwa", "Seselwa", "Seselwa Creole French", "Seychellois Creole" ], + "name": "Seychelles French Creole", + "names": [ "Creole", "Kreol", "Kreol Seselwa", "Seselwa", "Seselwa Creole French", "Seselwa French Creole", "Seychellois Creole" ], "region": "SC", "regionname": "Seychelles", "script": "Latn", @@ -24008,18 +25168,18 @@ "windows": "crt-Latn" }, { - "full": "crv-Zyyy-IN", + "full": "crv-Latn-IN", "iana": [ "Chaura" ], "iso639_3": "crv", "name": "Chaura", "names": [ "Chowra", "Tutet" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "crv", - "tags": [ "crv-IN", "crv-Zyyy" ], - "windows": "crv-Zyyy" + "tags": [ "crv-IN", "crv-Latn" ], + "windows": "crv-Latn" }, { "full": "crw-Latn-VN", @@ -24040,7 +25200,7 @@ "iana": [ "Carrier" ], "iso639_3": "crx", "localname": "Dakelh", - "localnames": [ "Dakelh" ], + "localnames": [ "Dakelh", "Dakelhne" ], "name": "Carrier", "names": [ "Central Carrier", "Dakelh", "Dakelhne", "Porteur", "Takelne", "Takulie" ], "region": "CA", @@ -24082,19 +25242,19 @@ "windows": "cry-Latn" }, { - "full": "crz-Zyyy-US", + "full": "crz-Latn-US", "iana": [ "Cruzeño" ], "iso639_3": "crz", "name": "Cruzeño", "names": [ "Chumash", "Chumasha Proper", "Island Chumash", "Island Chumashan", "Isleño", "Santa Rosa", "Ysleño" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "crz", - "tags": [ "crz-US", "crz-Zyyy" ], + "tags": [ "crz-Latn", "crz-US" ], "unwritten": true, - "windows": "crz-Zyyy" + "windows": "crz-Latn" }, { "full": "cs-Latn-CZ", @@ -24368,22 +25528,26 @@ }, { "full": "csp-Hans-CN", + "iana": [ "Southern Ping Chinese", "Southern Pinghua" ], "iso639_3": "csp", + "macrolang": "zh", "name": "Pinghua, Southern", - "names": [ "Guìnán Pínghuà", "Southern Guangxi", "Southern Ping Chinese" ], - "nophonvars": true, + "names": [ "Guìnán Pínghuà", "Southern Guangxi", "Southern Ping Chinese", "Southern Pinghua" ], "region": "CN", "regionname": "China", "script": "Hans", "sldr": false, - "tag": "csp-Hans", + "tag": "csp", + "tags": [ "csp-CN", "csp-Hans", "zh-csp", "zh-csp-CN" ], "windows": "csp-Hans" }, { "full": "csp-Hant-CN", + "iana": [ "Southern Ping Chinese", "Southern Pinghua" ], "iso639_3": "csp", + "macrolang": "zh", "name": "Pinghua, Southern", - "names": [ "Guìnán Pínghuà", "Southern Guangxi", "Southern Ping Chinese" ], + "names": [ "Guìnán Pínghuà", "Southern Guangxi", "Southern Ping Chinese", "Southern Pinghua" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -24396,8 +25560,8 @@ "full": "csq-Zxxx-HR", "iana": [ "Croatia Sign Language" ], "iso639_3": "csq", - "name": "Croatia Sign Language", - "names": [ "CSL", "HZJ", "Hrvatski znakovni jezik" ], + "name": "Croatian Sign Language", + "names": [ "CSL", "Croatia Sign Language", "HZJ", "Hrvatski znakovni jezik" ], "region": "HR", "regionname": "Croatia", "script": "Zxxx", @@ -24422,19 +25586,19 @@ "windows": "csr-Zxxx" }, { - "full": "css-Zyyy-US", + "full": "css-Latn-US", "iana": [ "Southern Ohlone" ], "iso639_3": "css", "name": "Ohlone, Southern", "names": [ "Southern Ohlone" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "css", - "tags": [ "css-US", "css-Zyyy" ], + "tags": [ "css-Latn", "css-US" ], "unwritten": true, - "windows": "css-Zyyy" + "windows": "css-Latn" }, { "full": "cst-Latn-US", @@ -24472,6 +25636,7 @@ "iso639_3": "csw", "latnnames": [ "nêhinawêwin" ], "localnames": [ "ᓀᐦᐃᓇᐍᐏᐣ" ], + "macrolang": "cr", "name": "Cree, Swampy", "names": [ "Cri des marais", "Inilîmowin", "Maskekon", "Moskégon", "Omaškêkowak", "Omushkego", "Swampy Cree", "West Main Cree", "West Shore Cree", "York Cree" ], "region": "CA", @@ -24482,6 +25647,20 @@ "tags": [ "csw-CA", "csw-Cans" ], "windows": "csw-Cans" }, + { + "full": "csx-Zxxx-KH", + "iana": [ "Cambodian Sign Language" ], + "iso639_3": "csx", + "name": "Cambodian Sign Language", + "names": [ "CBDSL", "CSL", "Cambodia Sign Language", "KSL", "Khmer Sign Language" ], + "region": "KH", + "regionname": "Cambodia", + "script": "Zxxx", + "sldr": false, + "tag": "csx", + "tags": [ "csx-KH", "csx-Zxxx", "sgn-csx", "sgn-csx-KH" ], + "windows": "csx-Zxxx" + }, { "full": "csy-Latn-MM", "iana": [ "Siyin Chin" ], @@ -24516,10 +25695,10 @@ "full": "cta-Latn-MX", "iana": [ "Tataltepec Chatino" ], "iso639_3": "cta", - "localname": "Cha’ jna’a", - "localnames": [ "Cha’ jna’a" ], + "localname": "Chaꞌ jnaꞌa", + "localnames": [ "Chaꞌ jnaꞌa" ], "name": "Chatino, Tataltepec", - "names": [ "Chatino Occidental Bajo", "Cha’ jna’a", "Lowland Chatino", "Tataltepec Chatino" ], + "names": [ "Chatino Occidental Bajo", "Chaꞌ jnaꞌa", "Lowland Chatino", "Tataltepec Chatino" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -24547,7 +25726,7 @@ "iana": [ "Tedim Chin" ], "iso639_3": "ctd", "name": "Chin, Tedim", - "names": [ "Hai-Dim", "Tedim", "Tedim Chin", "Tiddim", "Tidim", "Zomi" ], + "names": [ "Hai-Dim", "Tedim", "Tedim Chin", "Tiddim", "Tidim", "Zo pau", "Zome", "Zomi" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -24561,9 +25740,9 @@ "full": "ctd-Latn-MM", "iana": [ "Tedim Chin" ], "iso639_3": "ctd", - "localnames": [ "Tidim" ], + "localnames": [ "Tedim" ], "name": "Chin, Tedim", - "names": [ "Hai-Dim", "Tedim", "Tedim Chin", "Tiddim", "Tidim", "Zomi" ], + "names": [ "Hai-Dim", "Tedim", "Tedim Chin", "Tiddim", "Tidim", "Zo pau", "Zome", "Zomi" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -24661,19 +25840,19 @@ "windows": "ctl-Latn" }, { - "full": "ctm-Zyyy-US", + "full": "ctm-Latn-US", "iana": [ "Chitimacha" ], "iso639_3": "ctm", "name": "Chitimacha", "names": [ "Chetemacha" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ctm", - "tags": [ "ctm-US", "ctm-Zyyy" ], + "tags": [ "ctm-Latn", "ctm-US" ], "unwritten": true, - "windows": "ctm-Zyyy" + "windows": "ctm-Latn" }, { "full": "ctn-Deva-NP", @@ -24696,8 +25875,8 @@ "iana": [ "Emberá-Catío" ], "iso639_3": "cto", "localnames": [ "Ẽ́bẽra Katío" ], - "name": "Emberá-Catío", - "names": [ "Catio", "Catío", "Catío Emberá", "Embena", "Embera", "Emberá-Katío", "Epena", "Epera", "Epẽrã", "Eyabida", "Katio", "Ẽ́bẽra Katío", "ẽbẽrã bed’ea–Katío" ], + "name": "Embera Catío", + "names": [ "Catio", "Catío", "Catío Emberá", "Embena", "Embera", "Embera Bedea Katío", "Emberá-Catío", "Emberá-Katío", "Epena", "Epera", "Epẽrã", "Eyabida", "Katio", "Ẽ́bẽra Katío", "ẽbẽrã bed’ea–Katío" ], "region": "CO", "regionname": "Colombia", "regions": [ "PA" ], @@ -24711,10 +25890,10 @@ "full": "ctp-Latn-MX", "iana": [ "Western Highland Chatino" ], "iso639_3": "ctp", - "localname": "Cha’ jna’a", - "localnames": [ "Cha’ jna’a" ], + "localname": "Chaꞌ jnaꞌa", + "localnames": [ "Chaꞌ jnaꞌa" ], "name": "Chatino, Western Highland", - "names": [ "Chatino Central", "Chatino de la Zona Alta Occidental", "Cha’ jna’a", "Cha’t-An", "Sierra Occidental Chatino", "Western Highland Chatino" ], + "names": [ "Chatino Central", "Chatino de la Zona Alta Occidental", "Cha’t-An", "Chaꞌ jnaꞌa", "Sierra Occidental Chatino", "Western Highland Chatino" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -24727,6 +25906,7 @@ "full": "cts-Latn-PH", "iana": [ "Northern Catanduanes Bikol" ], "iso639_3": "cts", + "macrolang": "bik", "name": "Bikol, Northern Catanduanes", "names": [ "Northern Catanduanes Bicolano", "Northern Catanduanes Bikol", "Pandan", "Pandan Bikol" ], "region": "PH", @@ -24757,9 +25937,9 @@ "iana": [ "Chol" ], "iso639_3": "ctu", "localname": "Lakty’añ", - "localnames": [ "Lakty’añ" ], + "localnames": [ "Ch'ol", "Lakty’añ" ], "name": "Chol", - "names": [ "Lakty’añ" ], + "names": [ "Ch’ol", "Lak ty’añ", "Lak t’an", "Lakty’añ", "Lakty’añ Ch’ol" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -24772,14 +25952,15 @@ "full": "ctu-Latn-MX-x-tila", "iana": [ "Chol - Tila" ], "iso639_3": "ctu", - "localnames": [ "Lakty’añ" ], + "localname": "Lakty’añ", + "localnames": [ "Ch'ol", "Lakty’añ" ], "name": "Chol", - "names": [ "Chol - Tila", "Lakty’añ" ], + "names": [ "Chol - Tila", "Ch’ol", "Lak ty’añ", "Lak t’an", "Lakty’añ", "Lakty’añ Ch’ol" ], "region": "MX", "regionname": "Mexico", "rod": "02104", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ctu-x-tila", "tags": [ "ctu-Latn-x-tila", "ctu-MX-x-tila" ], "windows": "ctu-Latn-x-tila" @@ -24788,9 +25969,9 @@ "full": "ctu-Latn-MX-x-tumbala", "iana": [ "Chol - Tumala" ], "iso639_3": "ctu", - "localnames": [ "Lakty’añ" ], + "localnames": [ "Ch'ol", "Lakty’añ" ], "name": "Chol", - "names": [ "Chol - Tumala", "Lakty’añ" ], + "names": [ "Chol - Tumala", "Ch’ol", "Lak ty’añ", "Lak t’an", "Lakty’añ", "Lakty’añ Ch’ol" ], "region": "MX", "regionname": "Mexico", "rod": "00014", @@ -24800,6 +25981,34 @@ "tags": [ "ctu-Latn-x-tumbala", "ctu-MX-x-tumbala" ], "windows": "ctu-Latn-x-tumbala" }, + { + "full": "cty-Taml-IN", + "iana": [ "Moundadan Chetty" ], + "iso639_3": "cty", + "name": "Chetti, Moundadan", + "names": [ "Mamundadan Chetty", "Mandadan Chetty", "Moundadan Chetty", "Moundan Chetty" ], + "region": "IN", + "regionname": "India", + "script": "Taml", + "sldr": false, + "tag": "cty", + "tags": [ "cty-IN", "cty-Taml" ], + "windows": "cty-Taml" + }, + { + "full": "cty-Mlym-IN", + "iana": [ "Moundadan Chetty" ], + "iso639_3": "cty", + "name": "Chetti, Moundadan", + "names": [ "Mamundadan Chetty", "Mandadan Chetty", "Moundadan Chetty", "Moundan Chetty" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Mlym", + "sldr": false, + "tag": "cty-Mlym", + "windows": "cty-Mlym" + }, { "full": "ctz-Latn-MX", "iana": [ "Zacatepec Chatino" ], @@ -24830,7 +26039,7 @@ "sldr": true, "tag": "cu", "tags": [ "cu-Cyrl", "cu-RU" ], - "windows": "cu-Cyrl" + "windows": "cu-Cyrl-RU" }, { "full": "cu-Cyrs-RU", @@ -24844,7 +26053,7 @@ "script": "Cyrs", "sldr": false, "tag": "cu-Cyrs", - "windows": "cu-Cyrs" + "windows": "cu-Cyrs-RU" }, { "full": "cu-Glag-BG", @@ -24860,7 +26069,7 @@ "sldr": false, "tag": "cu-Glag", "tags": [ "cu-BG" ], - "windows": "cu-Glag" + "windows": "cu-Glag-BG" }, { "full": "cua-Latn-VN", @@ -24909,28 +26118,13 @@ "tags": [ "cuc-Latn", "cuc-MX" ], "windows": "cuc-Latn" }, - { - "full": "cug-Latn-CM", - "iana": [ "Chungmboko", "Cung" ], - "iso639_3": "cug", - "localnames": [ "Chungmboko" ], - "name": "Chungmboko", - "names": [ "Chung", "Cung" ], - "region": "CM", - "regionname": "Cameroon", - "script": "Latn", - "sldr": false, - "tag": "cug", - "tags": [ "cug-CM", "cug-Latn" ], - "windows": "cug-Latn" - }, { "full": "cuh-Latn-KE", "iana": [ "Chuka", "Gichuka" ], "iso639_3": "cuh", "localnames": [ "Gĩchuka" ], "name": "Gichuka", - "names": [ "Chuka", "Chuku", "Cuka", "Suka" ], + "names": [ "Chuka", "Chuku", "Cuka", "Gĩchuka", "Suka" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -24944,9 +26138,9 @@ "iana": [ "Cuiba" ], "iso639_3": "cui", "localname": "Cuiba-Wámonae", - "localnames": [ "Cuiba-Wámonae" ], + "localnames": [ "Wajume" ], "name": "Cuiba", - "names": [ "Cuiba-Wámonae", "Cuiva", "Hiwi", "Kuiva", "Maiben" ], + "names": [ "Cuiba-Wámonae", "Cuiva", "Hiwi", "Kuiva", "Maiben", "Wajume" ], "region": "CO", "regionname": "Colombia", "regions": [ "VE" ], @@ -24992,9 +26186,8 @@ "iana": [ "Culina", "Kulina" ], "iso639_3": "cul", "localname": "Madiha", - "localnames": [ "Madiha" ], "name": "Kulina", - "names": [ "Corina", "Culina", "Kollina", "Kulino", "Kulyna", "Kulína", "Kurina", "Madiha", "Madihá", "Madija" ], + "names": [ "Corina", "Culina", "Kollina", "Kulina Madihá", "Kulino", "Kulyna", "Kulína", "Kurina", "Madiha", "Madija", "Madija atti" ], "region": "BR", "regionname": "Brazil", "regions": [ "PE" ], @@ -25020,18 +26213,18 @@ "windows": "cuo-Latn" }, { - "full": "cup-Zyyy-US", + "full": "cup-Latn-US", "iana": [ "Cupeño" ], "iso639_3": "cup", "name": "Cupeño", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "cup", - "tags": [ "cup-US", "cup-Zyyy" ], + "tags": [ "cup-Latn", "cup-US" ], "unwritten": true, - "windows": "cup-Zyyy" + "windows": "cup-Latn" }, { "full": "cuq-Zyyy-CN", @@ -25052,8 +26245,8 @@ "full": "cur-Zyyy-NP", "iana": [ "Chhulung" ], "iso639_3": "cur", - "name": "Chhulung", - "names": [ "Chhiling", "Chhilling", "Chholung", "Chhûlûng Rûng", "Chiling", "Chulung", "Chülüng" ], + "name": "Chhiling", + "names": [ "Chhilling", "Chholung", "Chhulung", "Chhûlûng Rûng", "Chiling", "Chulung", "Chülüng" ], "region": "NP", "regionname": "Nepal", "script": "Zyyy", @@ -25142,16 +26335,16 @@ "windows": "cux-Latn" }, { - "full": "cuy-Latn-XX", + "full": "cuy-Latn-MX", "iana": [ "Cuitlatec" ], "iso639_3": "cuy", "name": "Cuitlatec", - "region": "XX", - "regions": [ "MX" ], + "region": "MX", + "regionname": "Mexico", "script": "Latn", "sldr": false, "tag": "cuy", - "tags": [ "cuy-Latn", "cuy-XX" ], + "tags": [ "cuy-Latn", "cuy-MX" ], "unwritten": true, "windows": "cuy-Latn" }, @@ -25160,6 +26353,7 @@ "iana": [ "Chuvash" ], "iso639_3": "chv", "latnnames": [ "Čăvaš čĕlȟi", "Čăvašla" ], + "localname": "чӑваш", "localnames": [ "Чӑваш чӗлхи", "Чӑвашла" ], "name": "Chuvash", "names": [ "Bulgar" ], @@ -25167,7 +26361,7 @@ "regionname": "Russian Federation", "regions": [ "KG", "KZ", "TM", "UZ" ], "script": "Cyrl", - "sldr": false, + "sldr": true, "tag": "cv", "tags": [ "cv-Cyrl", "cv-RU" ], "windows": "cv-Cyrl" @@ -25223,7 +26417,7 @@ "iso639_3": "cwa", "localnames": [ "Ekikabhwa" ], "name": "Kabwa", - "names": [ "Ekikabhwa", "Ekikabwa", "Kikabwa" ], + "names": [ "Ekikabhwa", "Ekikabwa", "Kikabhwa", "Kikabwa" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -25251,7 +26445,7 @@ "full": "cwe-Latn-TZ", "iana": [ "Kwere" ], "iso639_3": "cwe", - "localname": "Kwere", + "localname": "Nghwele", "localnames": [ "Nghwele" ], "name": "Kwere", "names": [ "Kakwere", "Kikwere", "Kinghwele", "Kwele", "Nghwele", "Ngh’wele", "Ngwele", "Ng’were", "Nhwele", "Nwele", "Tsinghwele" ], @@ -25288,11 +26482,25 @@ "region": "SN", "regionname": "Senegal", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "cwt", "tags": [ "cwt-Latn", "cwt-SN" ], "windows": "cwt-Latn" }, + { + "full": "cxh-Latn-NG", + "iso639_3": "cxh", + "name": "Cha’ari", + "names": [ "Chaari", "Chari", "Danchawa", "Danshe" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "cxh", + "tags": [ "cxh-Latn", "cxh-NG" ], + "unwritten": true, + "windows": "cxh-Latn" + }, { "full": "cy-Latn-GB", "iana": [ "Welsh" ], @@ -25309,7 +26517,7 @@ "suppress": true, "tag": "cy", "tags": [ "cy-GB", "cy-Latn" ], - "windows": "cy" + "windows": "cy-GB" }, { "full": "cy-Brai-GB", @@ -25324,16 +26532,31 @@ "script": "Brai", "sldr": false, "tag": "cy-Brai", - "windows": "cy-Brai" + "windows": "cy-Brai-GB" + }, + { + "full": "cy-Ogam-001", + "iana": [ "Welsh" ], + "iso639_3": "cym", + "name": "Welsh", + "names": [ "Cymraeg", "Galés" ], + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Ogam", + "sldr": false, + "tag": "cy-Ogam", + "tags": [ "cy-001" ], + "windows": "cy-Ogam-001" }, { "full": "cya-Latn-MX", "iana": [ "Nopala Chatino" ], "iso639_3": "cya", "localname": "Cha’ jna’a", - "localnames": [ "Cha’ jna’a" ], + "localnames": [ "Chaꞌ jnaꞌa" ], "name": "Chatino, Nopala", - "names": [ "Chatino Oriental Bajo", "Cha’ jna’a", "Nopala Chatino", "Teotepec Chatino" ], + "names": [ "Chatino Oriental Bajo", "Chaꞌ jnaꞌa", "Nopala Chatino", "Teotepec Chatino" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -25376,22 +26599,24 @@ "full": "czh-Hans-CN", "iana": [ "Huizhou Chinese" ], "iso639_3": "czh", + "macrolang": "zh", "name": "Chinese, Huizhou", - "names": [ "Anhui", "Hui", "Huizhou Chinese" ], - "nophonvars": true, + "names": [ "Anhui", "Hui", "Hui dialect", "Huizhou", "Huizhou Chinese" ], "region": "CN", "regionname": "China", "script": "Hans", "sldr": false, - "tag": "czh-Hans", + "tag": "czh", + "tags": [ "czh-CN", "czh-Hans", "zh-czh", "zh-czh-CN" ], "windows": "czh-Hans" }, { "full": "czh-Hant-CN", "iana": [ "Huizhou Chinese" ], "iso639_3": "czh", + "macrolang": "zh", "name": "Chinese, Huizhou", - "names": [ "Anhui", "Hui", "Huizhou Chinese" ], + "names": [ "Anhui", "Hui", "Hui dialect", "Huizhou", "Huizhou Chinese" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -25401,17 +26626,18 @@ "windows": "czh-Hant" }, { - "full": "czk-Zyyy-CZ", + "full": "czk-Hebr-CZ", "iana": [ "Knaanic" ], "iso639_3": "czk", "name": "Knaanic", + "names": [ "Canaanic", "Judaeo-Czech", "Judeo-Slavic", "Leshon Knaan" ], "region": "CZ", "regionname": "Czech Republic", - "script": "Zyyy", + "script": "Hebr", "sldr": false, "tag": "czk", - "tags": [ "czk-CZ", "czk-Zyyy" ], - "windows": "czk-Zyyy" + "tags": [ "czk-CZ", "czk-Hebr" ], + "windows": "czk-Hebr" }, { "full": "czn-Latn-MX", @@ -25432,6 +26658,7 @@ "full": "czo-Zyyy-CN", "iana": [ "Min Zhong Chinese" ], "iso639_3": "czo", + "macrolang": "zh", "name": "Chinese, Min Zhong", "names": [ "Central Fujian", "Central Min", "Min Zhong Chinese", "Minzhonghua" ], "region": "CN", @@ -25474,7 +26701,7 @@ "tag": "da", "tags": [ "da-DK", "da-Latn" ], "variants": [ "bornholm" ], - "windows": "da" + "windows": "da-DK" }, { "full": "da-Brai-DK", @@ -25489,7 +26716,21 @@ "script": "Brai", "sldr": false, "tag": "da-Brai", - "windows": "da-Brai" + "windows": "da-Brai-DK" + }, + { + "full": "da-Runr-001", + "iana": [ "Danish" ], + "iso639_3": "dan", + "name": "Danish", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "da-Runr", + "tags": [ "da-001" ], + "windows": "da-Runr-001" }, { "full": "daa-Latn-TD", @@ -25645,7 +26886,7 @@ "iana": [ "Dahalo" ], "iso639_3": "dal", "name": "Dahalo", - "names": [ "Guo Garimani", "Sanye" ], + "names": [ "Guo Garimani", "Numma guhoni", "Sanye" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -25691,7 +26932,7 @@ "iso639_3": "dao", "localnames": [ "Daai", "Daai Chin" ], "name": "Chin, Daai", - "names": [ "Chin, Kheng Daai", "Daai", "Dai", "Khyo", "Kkhyou" ], + "names": [ "Chin, Kheng Daai", "Daai", "Daai Chin", "Dai", "Khyo", "Kkhyou" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -25706,7 +26947,7 @@ "iso639_3": "dao", "localnames": [ "Daai", "Daai Chin" ], "name": "Chin, Daai", - "names": [ "Chin, Daai Matupi Licang", "Daai", "Dai", "Khyo", "Kkhyou" ], + "names": [ "Chin, Daai Matupi Licang", "Daai", "Daai Chin", "Dai", "Khyo", "Kkhyou" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -25721,7 +26962,7 @@ "iso639_3": "dao", "localnames": [ "Daai", "Daai Chin" ], "name": "Chin, Daai", - "names": [ "Chin, Daai Matupi Tuivang", "Daai", "Dai", "Khyo", "Kkhyou" ], + "names": [ "Chin, Daai Matupi Tuivang", "Daai", "Daai Chin", "Dai", "Khyo", "Kkhyou" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -25736,7 +26977,7 @@ "iso639_3": "dao", "localnames": [ "Daai", "Daai Chin" ], "name": "Chin, Daai", - "names": [ "Chin, Yindu Daar", "Daai", "Dai", "Khyo", "Kkhyou" ], + "names": [ "Chin, Yindu Daar", "Daai", "Daai Chin", "Dai", "Khyo", "Kkhyou" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -25750,7 +26991,7 @@ "iana": [ "Dandami Maria" ], "iso639_3": "daq", "name": "Maria, Dandami", - "names": [ "Bastar Koya", "Bison Horn Maria", "Dandami Madiya", "Dandami Maria", "Dhuru", "Madiya", "Maria Gond" ], + "names": [ "Bastar Koya", "Bison Horn Maria", "Dandami Madiya", "Dandami Maria", "Dhuru", "Gondi Madiya", "Madiya", "Maria Gond" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -25763,10 +27004,10 @@ "full": "dar-Cyrl-RU", "iana": [ "Dargwa" ], "iso639_3": "dar", - "latnnames": [ "Dargan mez" ], + "latnnames": [ "Dargin mez" ], "localnames": [ "дарган мез" ], "name": "Dargwa", - "names": [ "Dargan Medz", "Darghin", "Dargi", "Dargin", "Darginski", "Dargintsy", "Dargva", "Khiurkilinskii", "Uslar" ], + "names": [ "Dargan Medz", "Dargi", "Dargin", "Darginski", "Dargintsy", "Dargva", "Khiurkilinskii", "Uslar" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "AZ", "KG", "KZ", "TM", "TR", "UA", "UZ" ], @@ -25795,7 +27036,7 @@ "iana": [ "Dar Sila Daju" ], "iso639_3": "dau", "name": "Daju, Dar Sila", - "names": [ "Bokor", "Bokorike", "Bokoruge", "Dadjo", "Dajou", "Daju", "Dar Sila Daju", "Mongo-Sila", "Shila", "Sila", "Sula" ], + "names": [ "Bokor", "Bokorike", "Bokoruge", "Dadjo", "Dajou", "Daju", "Dar Sila", "Dar Sila Daju", "Mongo-Sila", "Shila", "Sila", "Sula" ], "region": "TD", "regionname": "Chad", "regions": [ "SD" ], @@ -25827,7 +27068,7 @@ "iana": [ "Davawenyo" ], "iso639_3": "daw", "name": "Davawenyo", - "names": [ "Davaoeño", "Davaweño", "Matino" ], + "names": [ "Baganggenyo", "Davaoeño", "Davaweño", "Matino", "Matinyo" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -25864,7 +27105,6 @@ "sldr": false, "tag": "daz", "tags": [ "daz-ID", "daz-Latn" ], - "unwritten": true, "windows": "daz-Latn" }, { @@ -25872,7 +27112,7 @@ "iana": [ "Bangime" ], "iso639_3": "dba", "name": "Bangime", - "names": [ "Bangana", "Bangeri Me", "Bangeri Me Dogon", "Bangerime", "Bangi Me", "Dyeni", "Elebo", "Noumandan", "Numadaw" ], + "names": [ "Bangande", "Bangeri Me", "Bangeri Me Dogon", "Bangerime", "Bangi Me", "Dyeni", "Elebo", "Noumandan", "Numadaw" ], "region": "ML", "regionname": "Mali", "script": "Latn", @@ -25887,7 +27127,7 @@ "iana": [ "Deno" ], "iso639_3": "dbb", "name": "Deno", - "names": [ "Be", "Denawa", "Denwa" ], + "names": [ "Be", "Denanchi", "Denawa", "Denwa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -25901,8 +27141,10 @@ "full": "dbd-Latn-NG", "iana": [ "Dadiya" ], "iso639_3": "dbd", + "localname": "Nyiyo Daddi̱ya", + "localnames": [ "Nyiyo Daddi̱ya" ], "name": "Dadiya", - "names": [ "Daadiya", "Dadia", "Loodiya", "Nda Dia" ], + "names": [ "Daadiya", "Dadia", "Loodiya", "Nda Dia", "Nyiyo Daddi̱ya" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -26079,9 +27321,9 @@ "iana": [ "Daba" ], "iso639_3": "dbq", "localname": "Daba", - "localnames": [ "Daba" ], + "localnames": [ "Ma Daba" ], "name": "Daba", - "names": [ "Dabba" ], + "names": [ "Dabba", "Ma Daba" ], "region": "CM", "regionname": "Cameroon", "regions": [ "NG" ], @@ -26255,6 +27497,7 @@ "full": "dde-Latn-CG", "iana": [ "Doondo" ], "iso639_3": "dde", + "localnames": [ "Doondo" ], "name": "Doondo", "names": [ "Dondo", "Kidoondo" ], "region": "CG", @@ -26291,7 +27534,6 @@ "sldr": false, "tag": "ddi", "tags": [ "ddi-Latn", "ddi-PG" ], - "unwritten": true, "windows": "ddi-Latn" }, { @@ -26348,6 +27590,7 @@ "iana": [ "Dhudhuroa" ], "iso639_3": "ddr", "name": "Dhudhuroa", + "names": [ "Dhuduroa", "Djilamatang", "Do.dor.dee", "Dodora", "Duduroa", "Dyinningmiddhang", "Tharamirttong", "Theddora", "Theddora-mittung", "Theddoramittung", "Toutourrite", "Victorian Alpine" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -26393,17 +27636,17 @@ "localname": "Deutsch", "localnames": [ "Deutsch" ], "name": "German, Standard", - "names": [ "Alemán", "Alemão", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemec", "Nemetskiy", "Niemiec", "Nimetsʹkyy", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], + "names": [ "Alemán", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemački", "Nemec", "Nemetskiy", "Niemiec", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], "region": "DE", "regionname": "Germany", - "regions": [ "AE", "AR", "AU", "BA", "BE", "BG", "BO", "BR", "CA", "CL", "CY", "CZ", "DK", "EC", "EE", "FI", "FR", "GR", "HR", "HU", "IE", "IL", "KG", "KZ", "LT", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], + "regions": [ "AE", "AR", "AU", "BA", "BE", "BG", "BO", "BR", "BY", "CA", "CL", "CM", "CY", "CZ", "DK", "EC", "EE", "ES", "FI", "FR", "GR", "HR", "HU", "IE", "IL", "KG", "KZ", "LT", "LV", "MC", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RS", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], "script": "Latn", "sldr": true, "suppress": true, "tag": "de", "tags": [ "de-DE", "de-Latn" ], "variants": [ "1901", "1996" ], - "windows": "de" + "windows": "de-DE" }, { "full": "de-Latn-AT", @@ -26416,24 +27659,23 @@ "regionname": "Austria", "script": "Latn", "sldr": true, - "suppress": true, "tag": "de-AT", - "windows": "de-AT" + "windows": "de-Latn-AT" }, { "full": "de-Brai-DE", "iana": [ "German" ], "iso639_3": "deu", "name": "German, Standard", - "names": [ "Alemán", "Alemão", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemec", "Nemetskiy", "Niemiec", "Nimetsʹkyy", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], + "names": [ "Alemán", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemački", "Nemec", "Nemetskiy", "Niemiec", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], "nophonvars": true, "region": "DE", "regionname": "Germany", - "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "FI", "FR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], + "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "BY", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "ES", "FI", "FR", "GR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "LV", "MC", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RS", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], "script": "Brai", "sldr": false, "tag": "de-Brai", - "windows": "de-Brai" + "windows": "de-Brai-DE" }, { "full": "de-Latn-CH", @@ -26446,24 +27688,23 @@ "regionname": "Switzerland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "de-CH", - "windows": "de-CH" + "windows": "de-Latn-CH" }, { "full": "de-Dupl-DE", "iana": [ "German" ], "iso639_3": "deu", "name": "German, Standard", - "names": [ "Alemán", "Alemão", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemec", "Nemetskiy", "Niemiec", "Nimetsʹkyy", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], + "names": [ "Alemán", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemački", "Nemec", "Nemetskiy", "Niemiec", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], "nophonvars": true, "region": "DE", "regionname": "Germany", - "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "FI", "FR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], + "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "BY", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "ES", "FI", "FR", "GR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "LV", "MC", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RS", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], "script": "Dupl", "sldr": false, "tag": "de-Dupl", - "windows": "de-Dupl" + "windows": "de-Dupl-DE" }, { "full": "de-Latn-IT", @@ -26476,9 +27717,8 @@ "regionname": "Italy", "script": "Latn", "sldr": true, - "suppress": true, "tag": "de-IT", - "windows": "de-IT" + "windows": "de-Latn-IT" }, { "full": "de-Latn-LI", @@ -26491,9 +27731,8 @@ "regionname": "Liechtenstein", "script": "Latn", "sldr": true, - "suppress": true, "tag": "de-LI", - "windows": "de-LI" + "windows": "de-Latn-LI" }, { "full": "de-Latn-LU", @@ -26506,41 +27745,40 @@ "regionname": "Luxembourg", "script": "Latn", "sldr": true, - "suppress": true, "tag": "de-LU", - "windows": "de-LU" + "windows": "de-Latn-LU" }, { "full": "de-Latf-DE", "iana": [ "German" ], "iso639_3": "deu", "name": "German, Standard", - "names": [ "Alemán", "Alemão", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemec", "Nemetskiy", "Niemiec", "Nimetsʹkyy", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], + "names": [ "Alemán", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemački", "Nemec", "Nemetskiy", "Niemiec", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], "nophonvars": true, "obsolete": true, "region": "DE", "regionname": "Germany", - "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "FI", "FR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], + "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "BY", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "ES", "FI", "FR", "GR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "LV", "MC", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RS", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], "script": "Latf", "sldr": false, "tag": "de-Latf", - "windows": "de-Latf" + "windows": "de-Latf-DE" }, { "full": "de-Runr-DE", "iana": [ "German" ], "iso639_3": "deu", "name": "German, Standard", - "names": [ "Alemán", "Alemão", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemec", "Nemetskiy", "Niemiec", "Nimetsʹkyy", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], + "names": [ "Alemán", "Allemand", "Deutsch", "Duits", "Däitsch", "German", "Germană", "Nemački", "Nemec", "Nemetskiy", "Niemiec", "Njemački", "Német", "Němec", "Tedesco", "Tudestg", "Tysk" ], "nophonvars": true, "obsolete": true, "region": "DE", "regionname": "Germany", - "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "FI", "FR", "GB", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], + "regions": [ "AE", "AR", "AT", "AU", "BA", "BE", "BG", "BO", "BR", "BY", "CA", "CH", "CL", "CY", "CZ", "DK", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "HR", "HU", "IE", "IL", "IT", "KG", "KZ", "LI", "LT", "LU", "LV", "MC", "MT", "MZ", "NA", "NL", "NZ", "PH", "PL", "PR", "PT", "PY", "RO", "RS", "RU", "SE", "SI", "SK", "TJ", "UA", "US", "UY", "ZA" ], "script": "Runr", "sldr": false, "tag": "de-Runr", - "windows": "de-Runr" + "windows": "de-Runr-DE" }, { "full": "dec-Latn-SD", @@ -26608,7 +27846,6 @@ "iana": [ "Degema" ], "iso639_3": "deg", "name": "Degema", - "obsolete": true, "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -26672,12 +27909,14 @@ "sldr": false, "tag": "del", "tags": [ "del-Latn", "del-US" ], - "windows": "del-Latn" + "windows": "del-Latn-US" }, { "full": "del-Latn-CA", "iana": [ "Delaware" ], + "iso639_3": "umu", "localnames": [ "Huluníixsuwaakan" ], + "macrolang": "del", "name": "Munsee", "names": [ "Delaware", "Huluníixsuwaakan", "Lunaapeew", "Munsee Delaware", "Munsiiw", "Ontario Delaware", "Stockbridge-Munsee" ], "region": "CA", @@ -26707,9 +27946,9 @@ "full": "den-Latn-CA", "iana": [ "Slave (Athapascan)" ], "iso639_3": "den", - "localnames": [ "Deh Gáh Ghotie Zhatie" ], - "name": "Slavey, South", - "names": [ "Acha’otinne", "Deh Gáh Ghotie Zhatie", "Dene", "Dene Tha’", "Denetha", "Dené", "Esclave du Sud", "Mackenzian", "Slave (Athapascan)" ], + "localnames": [ "Dene Zhatie" ], + "name": "Slave (Athapascan)", + "names": [ "Acha’otinne", "Deh Gáh Ghotie Zhatie", "Dene", "Dene Yatıé", "Dene Zhatie", "Esclave du Sud", "Mackenzian", "Slavey, South" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -26722,8 +27961,8 @@ "full": "den-Cans-CA", "iana": [ "Slave (Athapascan)" ], "iso639_3": "den", - "name": "Slavey, South", - "names": [ "Acha’otinne", "Deh Gáh Ghotie Zhatie", "Dene", "Dene Tha’", "Denetha", "Dené", "Esclave du Sud", "Mackenzian", "Slave (Athapascan)" ], + "name": "Slave (Athapascan)", + "names": [ "Acha’otinne", "Deh Gáh Ghotie Zhatie", "Dene", "Dene Yatıé", "Dene Zhatie", "Esclave du Sud", "Mackenzian", "Slavey, South" ], "nophonvars": true, "region": "CA", "regionname": "Canada", @@ -26767,22 +28006,25 @@ "full": "der-Beng-IN", "iana": [ "Deori" ], "iso639_3": "der", + "latnnames": [ "Deurai" ], + "localnames": [ "দেউৰী" ], "name": "Deori", - "names": [ "Chutiya", "Dari", "Deori-Chutiya", "Deuri", "Dewri", "Drori", "Jimochaya" ], - "nophonvars": true, + "names": [ "Chutiya", "Dari", "Deori-Chutiya", "Deuri", "Dewri", "Drori", "Jimochaya", "Jimosaya" ], "region": "IN", "regionname": "India", "script": "Beng", "sldr": false, - "tag": "der-Beng", + "tag": "der", + "tags": [ "der-Beng", "der-IN" ], "windows": "der-Beng" }, { "full": "der-Latn-IN", "iana": [ "Deori" ], "iso639_3": "der", + "localnames": [ "Deori" ], "name": "Deori", - "names": [ "Chutiya", "Dari", "Deori-Chutiya", "Deuri", "Dewri", "Drori", "Jimochaya" ], + "names": [ "Chutiya", "Dari", "Deori-Chutiya", "Deuri", "Dewri", "Drori", "Jimochaya", "Jimosaya" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -26794,8 +28036,9 @@ "full": "des-Latn-BR", "iana": [ "Desano" ], "iso639_3": "des", + "localnames": [ "Wirã ya" ], "name": "Desano", - "names": [ "Boleka", "Desana-Siriana", "Dessana", "Dessano", "Desána", "Desâna", "Kotedia", "Kusibi", "Oregu", "Uina", "Umúkomasá", "Wina", "Wira", "Wirã" ], + "names": [ "Boleka", "Boreká", "Desana-Siriana", "Dessana", "Dessano", "Desána", "Desâna", "Kotedia", "Kusibi", "Oregu", "Uina", "Umukomasã", "Wina", "Wira", "Wirã", "Wirã ya" ], "region": "BR", "regionname": "Brazil", "regions": [ "CO" ], @@ -26822,8 +28065,9 @@ "full": "dez-Latn-CD", "iana": [ "Dengese" ], "iso639_3": "dez", + "localnames": [ "Londengese" ], "name": "Dengese", - "names": [ "Ileo", "Lengese", "Ndengese", "Nkutu" ], + "names": [ "Ileo", "Lengese", "Londengese", "Ndengese", "Nkutu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -26897,8 +28141,8 @@ "full": "dge-Latn-PG", "iana": [ "Degenan" ], "iso639_3": "dge", - "name": "Degenan", - "names": [ "Dingana" ], + "name": "Degenang", + "names": [ "Degenan", "Dingana" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -26927,6 +28171,7 @@ "full": "dgh-Latn-NG", "iana": [ "Dghwede" ], "iso639_3": "dgh", + "localname": "Dghwéɗè", "localnames": [ "Dghwéɗè" ], "name": "Dghwede", "names": [ "Azaghvana", "Dehoxde", "Dghwéɗè", "Hude", "Johode", "Tghuade", "Toghwede", "Traude", "Wa’a", "Zaghvana" ], @@ -26975,12 +28220,12 @@ "iso639_3": "dgl", "name": "Andaandi", "names": [ "Danaagla", "Danaqla", "Dongola", "Dongola Nubian", "Dongolawi", "Dongolawi Nubian", "Dongolese" ], - "nophonvars": true, "region": "SD", "regionname": "Sudan", "script": "Arab", "sldr": true, - "tag": "dgl-Arab", + "tag": "dgl", + "tags": [ "dgl-Arab", "dgl-SD" ], "windows": "dgl-Arab" }, { @@ -26993,10 +28238,10 @@ "region": "SD", "regionname": "Sudan", "script": "Copt", - "sldr": false, + "sldr": true, "tag": "dgl-Copt", "tags": [ "dgl-Copt-SD", "dgl-Copt-x-olnubian", "dgl-SD-x-olnubian" ], - "windows": "dgl-Copt" + "windows": "dgl-Copt-x-olnubian" }, { "full": "dgl-Latn-SD", @@ -27028,12 +28273,12 @@ }, { "full": "dgr-Latn-CA", - "iana": [ "Dogrib" ], + "iana": [ "Dogrib", "Tłı̨chǫ" ], "iso639_3": "dgr", - "localname": "Tłı̨chǫ", + "localname": "Tłı̨chǫ Yatıì", "localnames": [ "Tłı̨chǫ Yatıì" ], "name": "Tlicho", - "names": [ "Dogrib", "Flanc-de-chien", "Thlingchadine", "Tłı̨chǫ", "Tłı̨chǫ Yatıì" ], + "names": [ "Dogrib", "Flanc-de-chien", "Thlingchadine", "Tłı̨chǫ", "Tłı̨chǫ", "Tłı̨chǫ Yatıì" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -27135,7 +28380,7 @@ "full": "dhg-Latn-AU", "iana": [ "Dhangu-Djangu", "Dhangu", "Djangu" ], "iso639_3": "dhg", - "localname": "Djangu", + "localname": "Dhangu-Djangu", "localnames": [ "Dhangu-Djangu" ], "name": "Dhangu-Djangu", "names": [ "Budalpudal", "Burada", "Buralbural", "Buratha", "Dangu", "Dhaangu", "Dhangu", "Dhangu’mi", "Djangu", "War-ramirri", "Warameri", "Waramiri", "Warramiri", "Warumeri", "Yolngu-Matha", "Yuulngu" ], @@ -27151,6 +28396,8 @@ "full": "dhi-Deva-NP", "iana": [ "Dhimal" ], "iso639_3": "dhi", + "latnnames": [ "Dhimal" ], + "localnames": [ "धिमाल" ], "name": "Dhimal", "names": [ "Dhemal" ], "region": "NP", @@ -27333,6 +28580,7 @@ "iana": [ "South Central Dinka" ], "iso639_3": "dib", "localnames": [ "Thuɔŋjäŋ" ], + "macrolang": "din", "name": "Dinka, South Central", "names": [ "Central Dinka", "South Central Dinka", "Thuɔŋjäŋ" ], "region": "SS", @@ -27362,10 +28610,10 @@ "full": "did-Latn-SS", "iana": [ "Didinga" ], "iso639_3": "did", - "localname": "Didinga", + "localname": "Dĩdĩnga", "localnames": [ "Dĩdĩnga" ], "name": "Didinga", - "names": [ "Dĩdĩnga", "Lango", "Toi", "Xaroxa", "’Di’dinga" ], + "names": [ "Dĩdĩnga", "Toi", "Xaroxa", "’Di’dinga" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -27400,7 +28648,7 @@ "regionname": "Kenya", "regions": [ "TZ" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "dig", "tags": [ "dig-KE", "dig-Latn" ], "windows": "dig-Latn" @@ -27410,7 +28658,7 @@ "iana": [ "Kumiai" ], "iso639_3": "dih", "name": "Kumiai", - "names": [ "Campo", "Cochimí", "Comeya", "Cuchimí", "Diegueno", "Diegueño", "Digueño", "Jamul Diegueño", "Jamul Tiipay", "Kamia", "Kamiai", "Kamiyahi", "Kamiyai", "Ki-Miai", "Ko’al", "Kumeyaai", "Kumeyaay", "Kumia", "Ku’ahl", "Kw’aal", "Mesa Grande Diegueño", "Quemayá", "Tiipay", "Tipai’", "Tipái", "Tipéi" ], + "names": [ "Campo", "Cochimí", "Comeya", "Cuchimí", "Diegueño", "Digueño", "Jamul Diegueño", "Jamul Tiipay", "Kamia", "Kamiai", "Kamiyahi", "Kamiyai", "Ki-Miai", "Ko’al", "Kumeyaai", "Kumeyaay", "Kumia", "Ku’ahl", "Kw’aal", "Mesa Grande Diegueño", "Quemayá", "Tiipay", "Tipai’", "Tipái", "Tipéi" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -27447,23 +28695,8 @@ "sldr": false, "tag": "dij", "tags": [ "dij-ID", "dij-Latn" ], - "unwritten": true, "windows": "dij-Latn" }, - { - "full": "dik-Arab-SS", - "iana": [ "Southwestern Dinka" ], - "iso639_3": "dik", - "name": "Dinka, Southwestern", - "names": [ "Southwestern Dinka", "Thuɔŋjäŋ", "Western Dinka" ], - "nophonvars": true, - "region": "SS", - "regionname": "South Sudan", - "script": "Arab", - "sldr": false, - "tag": "dik-Arab", - "windows": "dik-Arab" - }, { "full": "dil-Latn-SD", "iana": [ "Dilling" ], @@ -27497,10 +28730,10 @@ "full": "din-Latn-SS", "iana": [ "Dinka" ], "iso639_3": "din", - "localname": "Dinka: Southwestern", + "localname": "Thuɔŋjäŋ", "localnames": [ "Thuɔŋjäŋ" ], - "name": "Dinka, Southwestern", - "names": [ "Dinka", "Thuɔŋjäŋ", "Western Dinka" ], + "name": "Dinka", + "names": [ "Dinka, Southwestern", "Thoŋ ë Muɔnyjäŋ", "Thuɔŋjäŋ", "Western Dinka" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -27514,12 +28747,14 @@ "iana": [ "Dinka" ], "iso639_3": "din", "name": "Dinka", + "names": [ "Dinka, Southwestern", "Thoŋ ë Muɔnyjäŋ", "Thuɔŋjäŋ", "Western Dinka" ], "nophonvars": true, "region": "SS", "regionname": "South Sudan", "script": "Arab", "sldr": false, "tag": "din-Arab", + "tags": [ "dik-Arab", "dik-Arab-SS" ], "windows": "din-Arab" }, { @@ -27542,8 +28777,9 @@ "iana": [ "Northeastern Dinka" ], "iso639_3": "dip", "localnames": [ "Thuɔŋjäŋ" ], + "macrolang": "din", "name": "Dinka, Northeastern", - "names": [ "Northeastern Dinka", "Padang", "Thuɔŋjäŋ", "White Nile Dinka" ], + "names": [ "Northeastern Dinka", "Padang", "Padaŋ", "Thuɔŋjäŋ", "White Nile Dinka" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -27567,32 +28803,33 @@ "windows": "dir-Latn" }, { - "full": "dis-Beng-IN", + "full": "dis-Latn-IN", "iana": [ "Dimasa" ], "iso639_3": "dis", + "localnames": [ "Grao Dima" ], "name": "Dimasa", "names": [ "Dimasa Kachari", "Grao Dima", "Grau Dima", "Hills Kachari", "Magrau" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "dis-Beng", - "windows": "dis-Beng" + "tag": "dis", + "tags": [ "dis-IN", "dis-Latn" ], + "windows": "dis-Latn" }, { - "full": "dis-Latn-IN", + "full": "dis-Beng-IN", "iana": [ "Dimasa" ], "iso639_3": "dis", - "localnames": [ "Grao Dima" ], "name": "Dimasa", "names": [ "Dimasa Kachari", "Grao Dima", "Grau Dima", "Hills Kachari", "Magrau" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "dis-Latn", - "windows": "dis-Latn" + "tag": "dis-Beng", + "windows": "dis-Beng" }, { "full": "diu-Latn-NA", @@ -27600,7 +28837,7 @@ "iso639_3": "diu", "localnames": [ "Rumanyo" ], "name": "Gciriku", - "names": [ "Dciriku", "Diriko", "Diriku", "Manyo", "Mbogedo", "Mbogedu", "Rugciriku", "Rumanyo", "Shimbogedu" ], + "names": [ "Dciriku", "Dirico", "Diriko", "Diriku", "Manyo", "Mbogedo", "Mbogedu", "Rugciriku", "Rumanyo", "Rusambyu", "Shimbogedu" ], "region": "NA", "regionname": "Namibia", "regions": [ "AO", "BW" ], @@ -27615,8 +28852,9 @@ "iana": [ "Northwestern Dinka" ], "iso639_3": "diw", "localnames": [ "Thuɔŋjäŋ" ], + "macrolang": "din", "name": "Dinka, Northwestern", - "names": [ "Northwestern Dinka" ], + "names": [ "Northwestern Dinka", "Thuɔŋjäŋ" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -27657,8 +28895,9 @@ "full": "diz-Latn-CD", "iana": [ "Ding" ], "iso639_3": "diz", + "localnames": [ "Kidiŋ" ], "name": "Ding", - "names": [ "Di", "Din", "Dinga", "Dzing", "Idiŋ", "Idzing", "Kedeŋ-Lesye", "Kindinga" ], + "names": [ "Di", "Din", "Dinga", "Dzing", "Idiŋ", "Idzing", "Kedeŋ-Lesye", "Kidiŋ", "Kindinga" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -27700,9 +28939,9 @@ "full": "djc-Latn-TD", "iana": [ "Dar Daju Daju" ], "iso639_3": "djc", - "localnames": [ "Saaronge" ], + "localnames": [ "Dajiŋge" ], "name": "Daju, Dar Daju", - "names": [ "Dadjo", "Dadju", "Dajo", "Dajou", "Daju", "Daju Mongo", "Daju Oum Hadjer", "Dar Daju Daju", "Saaronge" ], + "names": [ "Dadjo", "Dadju", "Dajiŋge", "Dajo", "Dajou", "Daju", "Daju Mongo", "Daju Oum Hadjer", "Dar Daju Daju", "Saaronge" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -27943,19 +29182,34 @@ "windows": "djw-Latn" }, { - "full": "dka-Zyyy-BT", + "full": "dka-Tibt-BT", "iana": [ "Dakpakha" ], "iso639_3": "dka", "name": "Dakpakha", "names": [ "Dakpa" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "dka", - "tags": [ "dka-BT", "dka-Zyyy" ], + "tags": [ "dka-BT", "dka-Tibt" ], + "unwritten": true, + "windows": "dka-Tibt" + }, + { + "full": "dkg-Latn-NG", + "iana": [ "Kadung" ], + "iso639_3": "dkg", + "name": "Kadung", + "names": [ "Kadun", "Kwang", "Kwanga", "Kwangka", "Kwanka", "Kwanka-Boi-Bijim", "Kwàŋ" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "dkg", + "tags": [ "dkg-Latn", "dkg-NG" ], "unwritten": true, - "windows": "dka-Zyyy" + "windows": "dkg-Latn" }, { "full": "dkk-Latn-ID", @@ -27992,8 +29246,9 @@ "iana": [ "Southeastern Dinka" ], "iso639_3": "dks", "localnames": [ "Thuɔŋjäŋ" ], + "macrolang": "din", "name": "Dinka, Southeastern", - "names": [ "Cam", "Eastern Dinka", "Southeastern Dinka", "Thuɔŋjäŋ" ], + "names": [ "Cam", "Dinka Cam", "Eastern Dinka", "Southeastern Dinka", "Thuɔŋjäŋ" ], "region": "SS", "regionname": "South Sudan", "regions": [ "CA" ], @@ -28053,22 +29308,24 @@ "full": "dlm-Latn-HR", "iana": [ "Dalmatian" ], "iso639_3": "dlm", + "localnames": [ "Dalmato", "Langa Dalmata" ], "name": "Dalmatian", + "names": [ "Dalmatic", "Dalmatico", "Dalmato", "Dalmatski", "Langa Dalmata", "Lingua Dalmatica" ], "region": "HR", "regionname": "Croatia", "script": "Latn", "sldr": false, "tag": "dlm", "tags": [ "dlm-HR", "dlm-Latn" ], - "unwritten": true, "windows": "dlm-Latn" }, { "full": "dln-Latn-IN", "iana": [ "Darlong" ], "iso639_3": "dln", + "localnames": [ "Darlong" ], "name": "Darlong", - "names": [ "Dalong" ], + "names": [ "Dalong", "Darlong hnam" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -28154,6 +29411,7 @@ }, { "full": "dmf-Medf-NG", + "iana": [ "Medefaidrin" ], "iso639_3": "dmf", "name": "Medefaidrin", "region": "NG", @@ -28183,7 +29441,7 @@ "iana": [ "Domaaki" ], "iso639_3": "dmk", "name": "Domaaki", - "names": [ "Bericho", "Dom", "Doma", "Dumaki" ], + "names": [ "Bericho", "Dawoodi", "Dom", "Doma", "Dumaki" ], "region": "PK", "regionname": "Pakistan", "script": "Arab", @@ -28226,9 +29484,9 @@ "full": "dmo-Latn-CM", "iana": [ "Kemedzung" ], "iso639_3": "dmo", - "localnames": [ "Kɨmədzuŋ" ], + "localnames": [ "Díɛ yi Bədzumbu lə", "Kɨmədzuŋ" ], "name": "Kemedzung", - "names": [ "Diábékwálé", "Dumbo", "Dzumbo", "Kemezung", "Kumaju", "Kémézuñ", "Kɨmədzuŋ" ], + "names": [ "Dumbo", "Dzumbo", "Díɛ yi Bədzumbu lə", "Kemezung", "Kumaju", "Kɨmədzuŋ" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -28359,6 +29617,7 @@ "iana": [ "Daonda" ], "iso639_3": "dnd", "name": "Daonda", + "names": [ "Dawonda" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -28373,7 +29632,7 @@ "iana": [ "Ndendeule" ], "iso639_3": "dne", "name": "Ndendeule", - "names": [ "Kindendeule", "Kindendeuli", "Ndendeuli" ], + "names": [ "Kindendeule", "Kindendeuli", "Ndendeuli", "Ndwewe" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -28399,6 +29658,21 @@ "tags": [ "dng-Cyrl", "dng-KG" ], "windows": "dng-Cyrl" }, + { + "full": "dng-Arab-KG", + "iana": [ "Dungan" ], + "iso639_3": "dng", + "name": "Dungan", + "names": [ "Dzhunyan", "Huizu", "Kwuizwu", "Tungan", "Zwn’jan" ], + "nophonvars": true, + "region": "KG", + "regionname": "Kyrgyzstan", + "regions": [ "KZ", "RU", "TJ", "TM", "UZ" ], + "script": "Arab", + "sldr": false, + "tag": "dng-Arab", + "windows": "dng-Arab" + }, { "full": "dni-Latn-ID", "iana": [ "Lower Grand Valley Dani" ], @@ -28417,36 +29691,83 @@ "full": "dnj-Latn-CI", "iana": [ "Dan" ], "iso639_3": "dnj", - "localname": "Dan (Blowo)", - "localnames": [ "Dan" ], + "localname": "Dã̀ã̀", + "localnames": [ "Dã̀ã̀" ], "name": "Dan", - "names": [ "Da", "Gio", "Gio-Dan", "Guio", "Gyo", "Ya", "Yacouba", "Yakuba" ], + "names": [ "Da", "Dàn", "Dã̀ã̀", "Gio", "Gio-Dan", "Guio", "Gyo", "Ya", "Yacouba", "Yakuba" ], "region": "CI", "regionname": "Côte d'Ivoire", "regions": [ "GN" ], "script": "Latn", "sldr": true, "tag": "dnj", - "tags": [ "dnj-CI", "dnj-Latn" ], - "windows": "dnj-Latn" + "tags": [ "daf", "daf-CI", "daf-Latn", "daf-Latn-CI", "dnj-CI", "dnj-Latn" ], + "windows": "dnj-Latn-CI" }, { "full": "dnj-Latn-LR", "iana": [ "Dan" ], "iso639_3": "dnj", - "localname": "Dan (Blowo)", - "localnames": [ "Dan" ], + "localname": "Dan", + "localnames": [ "Dã̀ã̀" ], "name": "Dan", - "names": [ "Da", "Gio", "Gio-Dan", "Guio", "Gyo", "Ya", "Yacouba", "Yakuba" ], + "names": [ "Da", "Dàn", "Dã̀ã̀", "Gio", "Gio-Dan", "Guio", "Gyo", "Ya", "Yacouba", "Yakuba" ], "region": "LR", "regionname": "Liberia", "regions": [ "GN" ], "script": "Latn", "sldr": true, "tag": "dnj-LR", - "tags": [ "daf", "daf-LR", "daf-Latn", "daf-Latn-LR" ], "windows": "dnj-Latn-LR" }, + { + "full": "dnj-Latn-CI-x-eastern", + "iana": [ "Dan" ], + "iso639_3": "dnj", + "localnames": [ "Dã̀ã̀" ], + "name": "Dan", + "names": [ "Da", "Dàn", "Dã̀ã̀", "Gio", "Gio-Dan", "Ya", "Yacouba", "Yakuba" ], + "region": "CI", + "regionname": "Côte d'Ivoire", + "rod": "9098", + "script": "Latn", + "sldr": false, + "tag": "dnj-x-eastern", + "tags": [ "dnj-CI-x-eastern", "dnj-Latn-x-eastern" ], + "windows": "dnj-Latn-CI-x-eastern" + }, + { + "full": "dnj-Latn-CI-x-western", + "iana": [ "Dan" ], + "iso639_3": "dnj", + "localnames": [ "Dã̀ã̀" ], + "name": "Dan", + "names": [ "Da", "Dàn", "Dã̀ã̀", "Gio", "Gio-Dan", "Ya", "Yacouba", "Yakuba" ], + "region": "CI", + "regionname": "Côte d'Ivoire", + "rod": "6310", + "script": "Latn", + "sldr": false, + "tag": "dnj-x-western", + "tags": [ "dnj-CI-x-western", "dnj-Latn-x-western" ], + "windows": "dnj-Latn-CI-x-western" + }, + { + "full": "dnj-Latn-CI-x-yakouba", + "iana": [ "Dan" ], + "iso639_3": "dnj", + "localnames": [ "Dã̀ã̀" ], + "name": "Dan", + "names": [ "Da", "Dàn", "Dã̀ã̀", "Gio", "Gio-Dan", "Ya", "Yacouba", "Yakuba" ], + "region": "CI", + "regionname": "Côte d'Ivoire", + "rod": "1634", + "script": "Latn", + "sldr": false, + "tag": "dnj-x-yakouba", + "tags": [ "dnj-CI-x-yakouba", "dnj-Latn-x-yakouba" ], + "windows": "dnj-Latn-CI-x-yakouba" + }, { "full": "dnk-Latn-ID", "iana": [ "Dengka" ], @@ -28522,34 +29843,34 @@ "windows": "dnt-Latn" }, { - "full": "dnu-Zyyy-MM", + "full": "dnu-Mymr-MM", "iana": [ "Danau" ], "iso639_3": "dnu", "name": "Danau", "names": [ "Danaw" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "dnu", - "tags": [ "dnu-MM", "dnu-Zyyy" ], + "tags": [ "dnu-MM", "dnu-Mymr" ], "unwritten": true, - "windows": "dnu-Zyyy" + "windows": "dnu-Mymr" }, { - "full": "dnv-Zyyy-MM", + "full": "dnv-Mymr-MM", "iana": [ "Danu" ], "iso639_3": "dnv", "name": "Danu", "names": [ "Intha-Yaw", "Taruw", "Täruw" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "dnv", - "tags": [ "dnv-MM", "dnv-Zyyy" ], + "tags": [ "dnv-MM", "dnv-Mymr" ], "unwritten": true, - "windows": "dnv-Zyyy" + "windows": "dnv-Mymr" }, { "full": "dnw-Latn-ID", @@ -28569,7 +29890,6 @@ "full": "dny-Latn-BR", "iana": [ "Dení" ], "iso639_3": "dny", - "localnames": [ "Madiha" ], "name": "Dení", "names": [ "Dani", "Madiha" ], "region": "BR", @@ -28615,7 +29935,7 @@ "iana": [ "Northern Dong" ], "iso639_3": "doc", "name": "Dong, Northern", - "names": [ "Gam", "Kam", "Northern Dong", "Tong", "Tung", "Tung-Chia" ], + "names": [ "Gam", "Kam", "Lix Gaeml", "Northern Dong", "Tong", "Tung", "Tung-Chia" ], "region": "CN", "regionname": "China", "regions": [ "VN" ], @@ -28670,43 +29990,60 @@ "unwritten": true, "windows": "doh-Latn" }, + { + "full": "doi-Deva-IN", + "iana": [ "Dogri (macrolanguage)" ], + "iso639_3": "doi", + "latnnames": [ "Dogri" ], + "localname": "डोगरी", + "localnames": [ "डोगरी" ], + "name": "Dogri (macrolanguage)", + "names": [ "Dhogaryali", "Dogari", "Dogri", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": true, + "tag": "doi", + "tags": [ "dgo", "dgo-Deva", "dgo-Deva-IN", "dgo-IN", "doi-Deva", "doi-IN" ], + "windows": "doi-Deva" + }, { "full": "doi-Arab-IN", "iana": [ "Dogri (macrolanguage)" ], "iso639_3": "doi", - "name": "Dogri", - "names": [ "Dhogaryali", "Dogari", "Dogri (macrolanguage)", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], + "name": "Dogri (macrolanguage)", + "names": [ "Dhogaryali", "Dogari", "Dogri", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], + "nophonvars": true, "region": "IN", "regionname": "India", "script": "Arab", "sldr": false, - "tag": "doi", - "tags": [ "dgo", "dgo-Arab", "dgo-Arab-IN", "dgo-IN", "doi-Arab", "doi-IN" ], + "tag": "doi-Arab", + "tags": [ "dgo-Arab", "dgo-Arab-IN" ], "windows": "doi-Arab" }, { - "full": "doi-Deva-IN", + "full": "doi-Dogr-IN", "iana": [ "Dogri (macrolanguage)" ], "iso639_3": "doi", - "latnnames": [ "Dogri" ], - "localnames": [ "डोगरी" ], - "name": "Dogri", - "names": [ "Dhogaryali", "Dogari", "Dogri (macrolanguage)", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], + "name": "Dogri (macrolanguage)", + "names": [ "Dhogaryali", "Dogari", "Dogri", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], "nophonvars": true, + "obsolete": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Dogr", "sldr": false, - "tag": "doi-Deva", - "tags": [ "dgo-Deva", "dgo-Deva-IN" ], - "windows": "doi-Deva" + "tag": "doi-Dogr", + "tags": [ "dgo-Dogr", "dgo-Dogr-IN" ], + "windows": "doi-Dogr" }, { "full": "doi-Takr-IN", "iana": [ "Dogri (macrolanguage)" ], "iso639_3": "doi", - "name": "Dogri", - "names": [ "Dhogaryali", "Dogari", "Dogri (macrolanguage)", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], + "name": "Dogri (macrolanguage)", + "names": [ "Dhogaryali", "Dogari", "Dogri", "Dogri Jammu", "Dogri Pahari", "Dogri-Kangri", "Dongari", "Hindi Dogri", "Tokkaru" ], "nophonvars": true, "obsolete": true, "region": "IN", @@ -28749,7 +30086,6 @@ "full": "don-Latn-PG", "iana": [ "Toura (Papua New Guinea)" ], "iso639_3": "don", - "localnames": [ "Toura" ], "name": "Toura", "names": [ "Doura", "Toura (Papua New Guinea)" ], "region": "PG", @@ -28758,14 +30094,16 @@ "sldr": false, "tag": "don", "tags": [ "don-Latn", "don-PG" ], + "unwritten": true, "windows": "don-Latn" }, { "full": "doo-Latn-CD", "iana": [ "Dongo" ], "iso639_3": "doo", + "localnames": [ "Dhɔ̀ngɔ̀kɔ́" ], "name": "Dongo", - "names": [ "Donga", "Dongo Ko" ], + "names": [ "Dhɔ̀ngɔ̀kɔ́", "Donga", "Dongo Ko" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -28878,18 +30216,17 @@ "windows": "dow-Latn" }, { - "full": "dox-Zyyy-ET", + "full": "dox-Ethi-ET", "iana": [ "Bussa" ], "iso639_3": "dox", - "name": "Bussa", + "name": "Mositacha", "region": "ET", "regionname": "Ethiopia", - "script": "Zyyy", + "script": "Ethi", "sldr": false, "tag": "dox", - "tags": [ "dox-ET", "dox-Zyyy" ], - "unwritten": true, - "windows": "dox-Zyyy" + "tags": [ "dox-ET", "dox-Ethi" ], + "windows": "dox-Ethi" }, { "full": "doy-Latn-GH", @@ -28985,7 +30322,7 @@ "iana": [ "Dolpo" ], "iso639_3": "dre", "name": "Dolpo", - "names": [ "Dhol-wa", "Dolkha", "Dolpa Tibetan", "Dolpali", "Dolpike", "Phoke Dolpa" ], + "names": [ "Dolkha", "Dolpa Tibetan", "Dolpali", "Dolpike", "Phoke Dolpa" ], "region": "NP", "regionname": "Nepal", "script": "Tibt", @@ -29013,9 +30350,10 @@ "full": "dri-Latn-NG", "iana": [ "C'Lela" ], "iso639_3": "dri", - "localnames": [ "Lela" ], + "localname": "C'Lela", + "localnames": [ "C'Lela" ], "name": "C’Lela", - "names": [ "C'Lela", "Cala-Cala", "Chilala", "Chilela", "Dakakari", "Dakarkari", "Dakkarkari", "Kalla-Kalla", "Kolela", "Lalawa", "Lela" ], + "names": [ "C'Lela", "Cala-Cala", "Chilala", "Chilela", "CˀLela", "Dakakari", "Dakarci", "Dakarkari", "Dakkarkari", "Kalla-Kalla", "Kolela", "Lalawa", "Lela", "Zuru" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -29070,18 +30408,18 @@ "windows": "dro-Latn" }, { - "full": "drq-Zyyy-NP", + "full": "drq-Deva-NP", "iana": [ "Dura" ], "iso639_3": "drq", "name": "Dura", "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "drq", - "tags": [ "drq-NP", "drq-Zyyy" ], + "tags": [ "drq-Deva", "drq-NP" ], "unwritten": true, - "windows": "drq-Zyyy" + "windows": "drq-Deva" }, { "full": "drs-Ethi-ET", @@ -29132,6 +30470,8 @@ "full": "dry-Deva-NP", "iana": [ "Darai" ], "iso639_3": "dry", + "latnnames": [ "darai" ], + "localnames": [ "दरै" ], "name": "Darai", "region": "NP", "regionname": "Nepal", @@ -29167,7 +30507,7 @@ "names": [ "Dutch Sign Language", "NGT", "Nederlandse Gebarentaal", "SLN" ], "region": "NL", "regionname": "Netherlands", - "regions": [ "AW", "CW", "SR" ], + "regions": [ "AW", "BQ", "CW", "SR" ], "script": "Zxxx", "sldr": false, "tag": "dse", @@ -29205,6 +30545,20 @@ "unwritten": true, "windows": "dsi-Latn" }, + { + "full": "dsk-Latn-NG", + "iso639_3": "dsk", + "name": "Dokshi", + "names": [ "Luksanci", "Luksawa", "Lukshi", "Lushi" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "dsk", + "tags": [ "dsk-Latn", "dsk-NG" ], + "unwritten": true, + "windows": "dsk-Latn" + }, { "full": "dsl-Zxxx-DK", "iana": [ "Danish Sign Language" ], @@ -29213,7 +30567,7 @@ "names": [ "DTS", "Dansk tegnsprog" ], "region": "DK", "regionname": "Denmark", - "regions": [ "GL" ], + "regions": [ "FO", "GL" ], "script": "Zxxx", "sldr": false, "tag": "dsl", @@ -29252,34 +30606,49 @@ "windows": "dso-Orya" }, { - "full": "dsq-Arab-ML", + "full": "dsq-Latn-ML", "iana": [ "Tadaksahak" ], "iso639_3": "dsq", + "localnames": [ "Idaksahak" ], "name": "Tadaksahak", "names": [ "Daosahaq", "Daoussahaq", "Daoussak", "Dausahaq", "Dawsahaq", "Idaksahak" ], - "nophonvars": true, "region": "ML", "regionname": "Mali", "regions": [ "DZ" ], - "script": "Arab", + "script": "Latn", "sldr": false, - "tag": "dsq-Arab", - "windows": "dsq-Arab" + "tag": "dsq", + "tags": [ "dsq-Latn", "dsq-ML" ], + "windows": "dsq-Latn" }, { - "full": "dsq-Latn-ML", + "full": "dsq-Arab-ML", "iana": [ "Tadaksahak" ], "iso639_3": "dsq", - "localnames": [ "Idaksahak" ], "name": "Tadaksahak", "names": [ "Daosahaq", "Daoussahaq", "Daoussak", "Dausahaq", "Dawsahaq", "Idaksahak" ], + "nophonvars": true, "region": "ML", "regionname": "Mali", "regions": [ "DZ" ], - "script": "Latn", + "script": "Arab", "sldr": false, - "tag": "dsq-Latn", - "windows": "dsq-Latn" + "tag": "dsq-Arab", + "windows": "dsq-Arab" + }, + { + "full": "dsz-Zxxx-TR", + "iana": [ "Mardin Sign Language" ], + "iso639_3": "dsz", + "name": "Mardin Sign Language", + "names": [ "MarSL" ], + "region": "TR", + "regionname": "Turkey", + "script": "Zxxx", + "sldr": false, + "tag": "dsz", + "tags": [ "dsz-TR", "dsz-Zxxx", "sgn-dsz", "sgn-dsz-TR" ], + "windows": "dsz-Zxxx" }, { "full": "dta-Latn-CN", @@ -29479,12 +30848,29 @@ "tags": [ "dtp-Latn", "dtp-MY", "ktr", "ktr-Latn", "ktr-Latn-MY", "ktr-MY", "kzj", "kzj-Latn", "kzj-Latn-MY", "kzj-MY", "kzt", "kzt-Latn", "kzt-Latn-MY", "kzt-MY", "tdu", "tdu-Latn", "tdu-Latn-MY", "tdu-MY" ], "windows": "dtp-Latn" }, + { + "full": "dtp-Latn-MY-x-central", + "iana": [ "Kadazan Dusun" ], + "iso639_3": "dtp", + "localnames": [ "Kadazan Dusun" ], + "name": "Kadazan Dusun", + "names": [ "Central Kadazan", "Dusan", "Dusum", "Dusun", "Dusur", "Idaan", "Kadasan", "Kadayan", "Kadazandusun", "Kedayan" ], + "region": "MY", + "regionname": "Malaysia", + "rod": "11287", + "script": "Latn", + "sldr": false, + "tag": "dtp-x-central", + "tags": [ "dtp-Latn-x-central", "dtp-MY-x-central", "ktr-Latn-MY-x-central", "ktr-Latn-x-central", "ktr-MY-x-central", "ktr-x-central", "kzj-Latn-MY-x-central", "kzj-Latn-x-central", "kzj-MY-x-central", "kzj-x-central", "kzt-Latn-MY-x-central", "kzt-Latn-x-central", "kzt-MY-x-central", "kzt-x-central", "tdu-Latn-MY-x-central", "tdu-Latn-x-central", "tdu-MY-x-central", "tdu-x-central" ], + "windows": "dtp-Latn-x-central" + }, { "full": "dtr-Latn-MY", "iana": [ "Lotud" ], "iso639_3": "dtr", + "localnames": [ "Suang Lotud" ], "name": "Lotud", - "names": [ "Dusun Lotud", "Suang Lotud" ], + "names": [ "Dusun Lotud", "Dusun Tuaran", "Latod", "Latud", "Suang Lotud" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -29541,6 +30927,9 @@ "full": "dty-Deva-NP", "iana": [ "Dotyali" ], "iso639_3": "dty", + "latnnames": [ "Dotyali" ], + "localnames": [ "डोट् याली" ], + "macrolang": "ne", "name": "Dotyali", "names": [ "Dotali", "Doteli" ], "region": "NP", @@ -29557,9 +30946,9 @@ "iana": [ "Duala" ], "iso639_3": "dua", "localname": "duálá", - "localnames": [ "Duala" ], + "localnames": [ "Duálá" ], "name": "Duala", - "names": [ "Diwala", "Douala", "Dualla", "Dwala", "Dwela", "Sawa" ], + "names": [ "Diwala", "Douala", "Dualla", "Duálá", "Dwala", "Dwela", "Sawa" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -29587,9 +30976,9 @@ "iana": [ "Duna" ], "iso639_3": "duc", "localname": "Yuna", - "localnames": [ "Yuna" ], + "localnames": [ "Yunaya haga" ], "name": "Duna", - "names": [ "Yuna" ], + "names": [ "Yuna", "Yunaya haga" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -29602,8 +30991,9 @@ "full": "due-Latn-PH", "iana": [ "Umiray Dumaget Agta" ], "iso639_3": "due", + "localnames": [ "Dumaget-Bulus" ], "name": "Agta, Umiray Dumaget", - "names": [ "Dumagat-Bulos", "Dumaget-Bulus", "Umiray Agta", "Umiray Dumaget Agta", "Umirey Dumagat" ], + "names": [ "Aeta", "Agta Umiray Dumaget", "Dumagat", "Dumagat-Bulos", "Dumaget", "Dumaget-Bulus", "Dumagot", "Umiray Agta", "Umiray Dumaget Agta", "Umirey Dumagat" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -29648,13 +31038,13 @@ "iana": [ "Dungra Bhil" ], "iso639_3": "duh", "name": "Dungra Bhil", - "names": [ "Dungri Bhil" ], - "nophonvars": true, + "names": [ "Bhilori", "Dongre Bhil", "Dungra Bhili", "Dungri Bhil" ], "region": "IN", "regionname": "India", "script": "Deva", "sldr": false, - "tag": "duh-Deva", + "tag": "duh", + "tags": [ "duh-Deva", "duh-IN" ], "windows": "duh-Deva" }, { @@ -29662,7 +31052,7 @@ "iana": [ "Dungra Bhil" ], "iso639_3": "duh", "name": "Dungra Bhil", - "names": [ "Dungri Bhil" ], + "names": [ "Bhilori", "Dongre Bhil", "Dungra Bhili", "Dungri Bhil" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -29705,8 +31095,8 @@ "full": "dul-Latn-PH", "iana": [ "Alabat Island Agta" ], "iso639_3": "dul", - "name": "Agta, Inagta Alabat", - "names": [ "Agta", "Alabat Island Agta", "Alabat Island Dumagat", "Inagta" ], + "name": "Inagta Alabat", + "names": [ "Agta", "Alabat Island Agta", "Alabat Island Dumagat", "Inagta", "Inagta Alabat Agta" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -29764,6 +31154,7 @@ "full": "dup-Latn-ID", "iana": [ "Duano" ], "iso639_3": "dup", + "macrolang": "ms", "name": "Duano", "names": [ "Desin Dolak", "Desin Duano", "Duano’", "Orang Kuala", "Orang Laut" ], "region": "ID", @@ -29810,9 +31201,10 @@ "iana": [ "Dumi" ], "iso639_3": "dus", "latnnames": [ "Dumi" ], + "localname": "दुमी", "localnames": [ "दुमी" ], "name": "Dumi", - "names": [ "Dumi Bo’o", "Dumi Bro", "Dumī Rāī", "Hopupo Bro", "Lsi Rai", "Ro’do Bo’", "Sotmali" ], + "names": [ "Dumi Bo’o", "Dumi Bra", "Dumī Rāī", "Hopupo Bra", "Lsi Rai", "Ro’do Bo’", "Sotmali" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -29973,8 +31365,10 @@ }, { "full": "dwk-Orya-IN", + "iana": [ "Dawik Kui" ], "iso639_3": "dwk", "name": "Kui, Dawik", + "names": [ "Davik Kui", "Dawik Kui" ], "region": "IN", "regionname": "India", "script": "Orya", @@ -29987,8 +31381,8 @@ "full": "dwr-Latn-ET", "iana": [ "Dawro" ], "iso639_3": "dwr", - "localname": "Dawro", - "localnames": [ "Dawrogna" ], + "localname": "Dawrotsuwa", + "localnames": [ "Dawrotsuwa" ], "name": "Dawro", "region": "ET", "regionname": "Ethiopia", @@ -30002,7 +31396,9 @@ "full": "dwr-Ethi-ET", "iana": [ "Dawro" ], "iso639_3": "dwr", - "localname": "Dawro", + "latnnames": [ "Dawrotsuwa" ], + "localname": "ዳውሮꬃዋ", + "localnames": [ "ዳውሮꬃዋ" ], "name": "Dawro", "nophonvars": true, "region": "ET", @@ -30013,17 +31409,17 @@ "windows": "dwr-Ethi" }, { - "full": "dws-Zyyy-001", + "full": "dws-Latn-001", "iana": [ "Dutton World Speedwords" ], "iso639_3": "dws", "name": "Dutton World Speedwords", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "dws", - "tags": [ "dws-001", "dws-Zyyy" ], - "windows": "dws-Zyyy" + "tags": [ "dws-001", "dws-Latn" ], + "windows": "dws-Latn" }, { "full": "dwu-Latn-AU", @@ -30151,7 +31547,7 @@ "full": "dyi-Latn-CI", "iana": [ "Djimini Senoufo" ], "iso639_3": "dyi", - "localname": "Sénoufo, Djimini", + "localname": "Jinmiire", "localnames": [ "Jinmiire" ], "name": "Sénoufo, Djimini", "names": [ "Djimini", "Djimini Senoufo", "Dyimini", "Jinmiire", "Jinmini" ], @@ -30198,9 +31594,9 @@ "iana": [ "Jola-Fonyi" ], "iso639_3": "dyo", "localname": "joola", - "localnames": [ "Jóola-Fóoñi", "kújoolaay" ], + "localnames": [ "Jóola fóoñi" ], "name": "Jola-Fonyi", - "names": [ "Diola", "Diola-Fogny", "Dyola", "Jola", "Jola-Fogny", "Joola-Fogny", "Jóola", "Jóola-Fóoñi", "Kujamataak", "Kújoolaak Kati Fooñi", "Kújoolaak kati Kúfooñaak", "Yola", "kújoolaay" ], + "names": [ "Diola", "Diola-Fogny", "Dyola", "Jola", "Jola-Fogny", "Joola-Fogny", "Jóola", "Jóola fóoñi", "Kujamataak", "Kújoola", "Kújoolaak Kati Fooñi", "Kújoolaak kati Kúfooñaak", "Kújoolaay", "Yola" ], "region": "SN", "regionname": "Senegal", "regions": [ "GM", "GW" ], @@ -30216,7 +31612,7 @@ "iso639_3": "dyo", "localname": "joola", "name": "Jola-Fonyi", - "names": [ "Diola", "Diola-Fogny", "Dyola", "Jola", "Jola-Fogny", "Joola-Fogny", "Jóola", "Jóola-Fóoñi", "Kujamataak", "Kújoolaak Kati Fooñi", "Kújoolaak kati Kúfooñaak", "Yola", "kújoolaay" ], + "names": [ "Diola", "Diola-Fogny", "Dyola", "Jola", "Jola-Fogny", "Joola-Fogny", "Jóola", "Jóola fóoñi", "Kujamataak", "Kújoola", "Kújoolaak Kati Fooñi", "Kújoolaak kati Kúfooñaak", "Kújoolaay", "Yola" ], "nophonvars": true, "region": "SN", "regionname": "Senegal", @@ -30226,11 +31622,25 @@ "tag": "dyo-Arab", "windows": "dyo-Arab" }, + { + "full": "dyr-Latn-NG", + "iso639_3": "dyr", + "name": "Dyarim", + "names": [ "Kaiwari", "Kaiyorawa", "Kayaurawa", "Kayauri" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "dyr", + "tags": [ "dyr-Latn", "dyr-NG" ], + "unwritten": true, + "windows": "dyr-Latn" + }, { "full": "dyu-Latn-BF", "iana": [ "Dyula" ], "iso639_3": "dyu", - "localname": "Jula", + "localname": "Julakan", "localnames": [ "Julakan" ], "name": "Jula", "names": [ "Dioula", "Dioula Véhiculaire", "Diula", "Djula", "Dyoula", "Dyula", "Jula Kong", "Julakan", "Kong Jula", "Tagboussikan", "Trade Jula" ], @@ -30241,7 +31651,7 @@ "sldr": true, "tag": "dyu", "tags": [ "dyu-BF", "dyu-Latn" ], - "windows": "dyu-Latn" + "windows": "dyu-Latn-BF" }, { "full": "dyu-Arab-CI", @@ -30256,7 +31666,7 @@ "script": "Arab", "sldr": false, "tag": "dyu-Arab", - "windows": "dyu-Arab" + "windows": "dyu-Arab-CI" }, { "full": "dyu-Nkoo-CI", @@ -30271,7 +31681,7 @@ "script": "Nkoo", "sldr": false, "tag": "dyu-Nkoo", - "windows": "dyu-Nkoo" + "windows": "dyu-Nkoo-CI" }, { "full": "dyy-Latn-AU", @@ -30292,7 +31702,7 @@ "full": "dz-Tibt-BT", "iana": [ "Dzongkha" ], "iso639_3": "dzo", - "latnnames": [ "rDzongkha" ], + "latnnames": [ "Dzongkha" ], "localname": "རྫོང་ཁ", "localnames": [ "རྫོང་ཁ་" ], "name": "Dzongkha", @@ -30312,7 +31722,7 @@ "iana": [ "Tunzu" ], "iso639_3": "dza", "name": "Tunzuii", - "names": [ "Dugusa", "Duguza", "Itunzu", "Tunzu" ], + "names": [ "Dugusa", "Duguza", "Itunzu", "Tunzu", "àTunzû" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -30322,6 +31732,21 @@ "unwritten": true, "windows": "dza-Latn" }, + { + "full": "dzd-Latn-NG", + "iana": [ "Daza" ], + "iso639_3": "dzd", + "name": "Daza", + "names": [ "Dazawa" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "dzd", + "tags": [ "dzd-Latn", "dzd-NG" ], + "unwritten": true, + "windows": "dzd-Latn" + }, { "full": "dze-Latn-AU", "iana": [ "Djiwarli" ], @@ -30355,19 +31780,19 @@ "windows": "dzg-Latn" }, { - "full": "dzl-Zyyy-BT", + "full": "dzl-Tibt-BT", "iana": [ "Dzalakha" ], "iso639_3": "dzl", "name": "Dzalakha", "names": [ "Dzala", "Dzalamat", "Yangtsebikha" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "dzl", - "tags": [ "dzl-BT", "dzl-Zyyy" ], + "tags": [ "dzl-BT", "dzl-Tibt" ], "unwritten": true, - "windows": "dzl-Zyyy" + "windows": "dzl-Tibt" }, { "full": "dzn-Latn-CD", @@ -30400,6 +31825,7 @@ }, { "full": "ebc-Latn-ID", + "iana": [ "Beginci" ], "iso639_3": "ebc", "name": "Beginci", "names": [ "Bihak" ], @@ -30430,6 +31856,7 @@ "full": "ebk-Latn-PH", "iana": [ "Eastern Bontok" ], "iso639_3": "ebk", + "macrolang": "bkb", "name": "Bontok, Eastern", "names": [ "Eastern Bontoc", "Eastern Bontok", "Finallig", "Southern Bontoc" ], "region": "PH", @@ -30445,7 +31872,7 @@ "iana": [ "Teke-Ebo" ], "iso639_3": "ebo", "name": "Teke-Eboo", - "names": [ "Aboo", "Bamboma", "Boma", "Boo", "Boõ", "Central Teke", "Eboo Teke", "Eboom", "Iboo", "Teke, Eboo", "Teke-Boma", "Teke-Ebo" ], + "names": [ "Aboo", "Bamboma", "Boma", "Boo", "Boõ", "Central Teke", "Eboo Teke", "Eboom", "Iboo", "Ityɔɔ", "Teke, Eboo", "Teke-Boma", "Teke-Ebo", "Téké des Plateaux" ], "region": "CG", "regionname": "Congo", "regions": [ "CD" ], @@ -30459,8 +31886,9 @@ "full": "ebr-Latn-CI", "iana": [ "Ebrié" ], "iso639_3": "ebr", - "name": "Ebrié", - "names": [ "Cama", "Caman", "Kyama", "Tchaman", "Tsama", "Tyama" ], + "localnames": [ "Tyaman" ], + "name": "Tchaman", + "names": [ "Cama", "Caman", "Kyama", "Tsama", "Tyama", "Tyaman" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -30486,17 +31914,17 @@ "windows": "ebu-Latn" }, { - "full": "ecr-Zyyy-GR", + "full": "ecr-Grek-GR", "iana": [ "Eteocretan" ], "iso639_3": "ecr", "name": "Eteocretan", "region": "GR", "regionname": "Greece", - "script": "Zyyy", + "script": "Grek", "sldr": false, "tag": "ecr", - "tags": [ "ecr-GR", "ecr-Zyyy" ], - "windows": "ecr-Zyyy" + "tags": [ "ecr-GR", "ecr-Grek" ], + "windows": "ecr-Grek" }, { "full": "ecs-Zxxx-EC", @@ -30513,17 +31941,17 @@ "windows": "ecs-Zxxx" }, { - "full": "ecy-Zyyy-CY", + "full": "ecy-Cprt-CY", "iana": [ "Eteocypriot" ], "iso639_3": "ecy", "name": "Eteocypriot", "region": "CY", "regionname": "Cyprus", - "script": "Zyyy", + "script": "Cprt", "sldr": false, "tag": "ecy", - "tags": [ "ecy-CY", "ecy-Zyyy" ], - "windows": "ecy-Zyyy" + "tags": [ "ecy-CY", "ecy-Cprt" ], + "windows": "ecy-Cprt" }, { "full": "ee-Latn-GH", @@ -30541,7 +31969,7 @@ "sldr": true, "tag": "ee", "tags": [ "ee-GH", "ee-Latn" ], - "windows": "ee-Latn" + "windows": "ee-Latn-GH" }, { "full": "ee-Brai-GH", @@ -30556,7 +31984,7 @@ "script": "Brai", "sldr": false, "tag": "ee-Brai", - "windows": "ee-Brai" + "windows": "ee-Brai-GH" }, { "full": "ee-Latn-TG", @@ -30655,7 +32083,7 @@ "iso639_3": "egl", "localnames": [ "Emigliân" ], "name": "Emilian", - "names": [ "Bolognese", "Emigliân", "Ferrarese", "Modenese", "Parmigiano", "Piacentino", "Reggiano" ], + "names": [ "Emigliân" ], "region": "IT", "regionname": "Italy", "script": "Latn", @@ -30664,6 +32092,21 @@ "tags": [ "egl-IT", "egl-Latn" ], "windows": "egl-Latn" }, + { + "full": "egm-Latn-TZ", + "iana": [ "Benamanga" ], + "iso639_3": "egm", + "name": "Benamanga", + "names": [ "Bena Manga", "Kibenamanga" ], + "region": "TZ", + "regionname": "United Republic of Tanzania", + "script": "Latn", + "sldr": false, + "tag": "egm", + "tags": [ "egm-Latn", "egm-TZ" ], + "unwritten": true, + "windows": "egm-Latn" + }, { "full": "ego-Latn-NG", "iana": [ "Eggon" ], @@ -30693,6 +32136,20 @@ "tags": [ "egy-EG", "egy-Egyp" ], "windows": "egy-Egyp" }, + { + "full": "ehs-Zxxx-JP", + "iana": [ "Miyakubo Sign Language" ], + "iso639_3": "ehs", + "name": "Miyakubo Sign Language", + "names": [ "Ehime Oshima Shuwa", "Ehime-Oshima Sign Language", "Miyakubo Shuwa" ], + "region": "JP", + "regionname": "Japan", + "script": "Zxxx", + "sldr": false, + "tag": "ehs", + "tags": [ "ehs-JP", "ehs-Zxxx", "sgn-ehs", "sgn-ehs-JP" ], + "windows": "ehs-Zxxx" + }, { "full": "ehu-Latn-NG", "iana": [ "Ehueun" ], @@ -30741,8 +32198,9 @@ "full": "eiv-Latn-PG", "iana": [ "Askopan" ], "iso639_3": "eiv", + "localnames": [ "Askopan" ], "name": "Askopan", - "names": [ "Eivo" ], + "names": [ "Eivo", "Eiwo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -30755,8 +32213,9 @@ "full": "eja-Latn-GW", "iana": [ "Ejamat" ], "iso639_3": "eja", + "localnames": [ "Djola-Felupe" ], "name": "Jola-Felupe", - "names": [ "Ediamat", "Ejamat", "Feloup", "Felup", "Felupe", "Floup", "Flup", "Fulup" ], + "names": [ "Djola-Felupe", "Ediamat", "Ejamat", "Feloup", "Felup", "Felupe", "Floup", "Flup", "Fulup" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "SN" ], @@ -30870,9 +32329,9 @@ "iana": [ "Elip" ], "iso639_3": "ekm", "localname": "Nulibié", - "localnames": [ "Nulibié" ], + "localnames": [ "Nulíbíe" ], "name": "Elip", - "names": [ "Belibi", "Belip", "Central Yambassa", "Libie", "Nulibie", "Nulibié" ], + "names": [ "Belibi", "Belip", "Central Yambassa", "Libie", "Nuasúɛ", "Nulibie", "Nulíbíe" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -30887,11 +32346,11 @@ "iso639_3": "eko", "localnames": [ "Ekoti" ], "name": "Koti", - "names": [ "Angoche", "Angoje", "Angoxe", "Coti", "Ekoti" ], + "names": [ "Akoti", "Angoche", "Angoje", "Angoxe", "Coti", "Ekoti" ], "region": "MZ", "regionname": "Mozambique", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "eko", "tags": [ "eko-Latn", "eko-MZ" ], "windows": "eko-Latn" @@ -30901,7 +32360,7 @@ "iana": [ "Koti" ], "iso639_3": "eko", "name": "Koti", - "names": [ "Angoche", "Angoje", "Angoxe", "Coti", "Ekoti" ], + "names": [ "Akoti", "Angoche", "Angoje", "Angoxe", "Coti", "Ekoti" ], "nophonvars": true, "region": "MZ", "regionname": "Mozambique", @@ -30962,7 +32421,7 @@ "localname": "Ελληνικά", "localnames": [ "Νέα Ελληνικά", "ελληνικά" ], "name": "Greek", - "names": [ "Greacă", "Grec", "Greco", "Görög", "Hretsʹkyy", "Modern Greek (1453-)", "Neo-Hellenic", "Romaic", "Urum", "Yunan" ], + "names": [ "Greacă", "Greco", "Görög", "Hellenic", "Hretsʹkyy", "Modern Greek", "Modern Greek (1453-)", "Romaic", "Standard Modern Greek", "Urum", "Yunan" ], "region": "GR", "regionname": "Greece", "regions": [ "AE", "AL", "AM", "AU", "BA", "BE", "BG", "CA", "CD", "CY", "CZ", "DE", "DJ", "EG", "GB", "HU", "IL", "IT", "JO", "KG", "KZ", "LU", "LY", "MK", "MW", "MZ", "RO", "RU", "SE", "SL", "TR", "UA", "US" ], @@ -30972,14 +32431,14 @@ "tag": "el", "tags": [ "el-GR", "el-Grek" ], "variants": [ "monoton", "polyton" ], - "windows": "el" + "windows": "el-GR" }, { "full": "el-Brai-GR", "iana": [ "Modern Greek (1453-)" ], "iso639_3": "ell", "name": "Greek", - "names": [ "Greacă", "Grec", "Greco", "Görög", "Hretsʹkyy", "Modern Greek (1453-)", "Neo-Hellenic", "Romaic", "Urum", "Yunan" ], + "names": [ "Greacă", "Greco", "Görög", "Hellenic", "Hretsʹkyy", "Modern Greek", "Modern Greek (1453-)", "Romaic", "Standard Modern Greek", "Urum", "Yunan" ], "nophonvars": true, "region": "GR", "regionname": "Greece", @@ -30987,14 +32446,14 @@ "script": "Brai", "sldr": false, "tag": "el-Brai", - "windows": "el-Brai" + "windows": "el-Brai-GR" }, { "full": "el-Cyrl-UA", "iana": [ "Modern Greek (1453-)" ], "iso639_3": "ell", "name": "Greek", - "names": [ "Greacă", "Grec", "Greco", "Görög", "Hretsʹkyy", "Modern Greek (1453-)", "Neo-Hellenic", "Romaic", "Urum", "Yunan" ], + "names": [ "Greacă", "Greco", "Görög", "Hellenic", "Hretsʹkyy", "Modern Greek", "Modern Greek (1453-)", "Romaic", "Standard Modern Greek", "Urum", "Yunan" ], "nophonvars": true, "region": "UA", "regionname": "Ukraine", @@ -31003,14 +32462,14 @@ "sldr": false, "tag": "el-Cyrl", "tags": [ "el-UA" ], - "windows": "el-Cyrl" + "windows": "el-Cyrl-UA" }, { "full": "ele-Latn-PG", "iana": [ "Elepi" ], "iso639_3": "ele", "name": "Elepi", - "names": [ "Samap" ], + "names": [ "Blabla", "Samap" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -31099,7 +32558,7 @@ "iana": [ "Elu" ], "iso639_3": "elu", "name": "Elu", - "names": [ "Admiralitäts-inseln" ], + "names": [ "Admiralitäts-inseln", "Elle", "Ere" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -31110,17 +32569,17 @@ "windows": "elu-Latn" }, { - "full": "elx-Zyyy-IR", + "full": "elx-Pelm-IR", "iana": [ "Elamite" ], "iso639_3": "elx", "name": "Elamite", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Pelm", "sldr": false, "tag": "elx", - "tags": [ "elx-IR", "elx-Zyyy" ], - "windows": "elx-Zyyy" + "tags": [ "elx-IR", "elx-Pelm" ], + "windows": "elx-Pelm" }, { "full": "ema-Latn-NG", @@ -31154,8 +32613,8 @@ "full": "eme-Latn-GF", "iana": [ "Emerillon" ], "iso639_3": "eme", - "name": "Emerillon", - "names": [ "Emereñon", "Emerilon", "Melejo", "Mereo", "Mereyo", "Teco", "Teko" ], + "name": "Tekó", + "names": [ "Emereñon", "Emerillon", "Emerilon", "Melejo", "Mereo", "Mereyo", "Teco" ], "region": "GF", "regionname": "French Guiana", "script": "Latn", @@ -31229,9 +32688,9 @@ "iana": [ "Northern Emberá" ], "iso639_3": "emp", "localname": "Embera", - "localnames": [ "Embera" ], + "localnames": [ "Dadyi Ẽ́berã Bed̶ea", "Ẽ́berã Bed̶ea" ], "name": "Emberá, Northern", - "names": [ "Atrato", "Chocó", "Cholo", "Darien", "Darien Emberá", "Darién", "Ebera Bedea", "Eberã", "Eberã Bed’ea", "Embena", "Embera", "Empera", "Emperã", "Epena", "Eperã Pedea", "Epérã Pedée", "Northern Emberá", "Panama Embera", "Pede", "Pede Epenã", "Sambú" ], + "names": [ "Atrato", "Chocó", "Cholo", "Dadyi Ẽ́berã Bed̶ea", "Darien", "Darien Emberá", "Darién", "Ebera Bedea", "Eberã", "Eberã Bed’ea", "Embena", "Embera", "Empera", "Emperã", "Epena", "Eperã Pedea", "Epérã Pedée", "Northern Emberá", "Panama Embera", "Pede", "Pede Epenã", "Sambú", "Ẽ́berã Bed̶ea", "Ẽ́berã Beɗea" ], "region": "PA", "regionname": "Panama", "regions": [ "CO" ], @@ -31241,13 +32700,28 @@ "tags": [ "emp-Latn", "emp-PA" ], "windows": "emp-Latn" }, + { + "full": "emq-Zyyy-CN", + "iana": [ "Eastern Minyag" ], + "iso639_3": "emq", + "name": "Minyag, Eastern", + "names": [ "Eastern Minyag", "Munya", "Muya", "Shimian Minyag", "Shimian Munya", "Shimian Muya" ], + "region": "CN", + "regionname": "China", + "script": "Zyyy", + "sldr": false, + "tag": "emq", + "tags": [ "emq-CN", "emq-Zyyy" ], + "unwritten": true, + "windows": "emq-Zyyy" + }, { "full": "ems-Latn-US", "iana": [ "Pacific Gulf Yupik" ], "iso639_3": "ems", "localnames": [ "Sugpiaq" ], "name": "Yupik, Pacific Gulf", - "names": [ "Aleut", "Alutiiq", "Chugach Eskimo", "Koniag-Chugach", "Pacific Gulf Yupik", "Pacific Yupik", "South Alaska Eskimo", "Sugcestun", "Sugpiak Eskimo", "Sugpiaq", "Sugpiaq Eskimo", "Suk" ], + "names": [ "Aleut", "Alutiiq", "Alutiit’stun", "Chugach Eskimo", "Koniag-Chugach", "Pacific Gulf Yupik", "Pacific Yupik", "South Alaska Eskimo", "Sugcestun", "Sugpiak Eskimo", "Sugpiaq", "Sugpiaq Eskimo", "Sugt’stun", "Suk" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -31261,7 +32735,7 @@ "iana": [ "Pacific Gulf Yupik" ], "iso639_3": "ems", "name": "Yupik, Pacific Gulf", - "names": [ "Aleut", "Alutiiq", "Chugach Eskimo", "Koniag-Chugach", "Pacific Gulf Yupik", "Pacific Yupik", "South Alaska Eskimo", "Sugcestun", "Sugpiak Eskimo", "Sugpiaq", "Sugpiaq Eskimo", "Suk" ], + "names": [ "Aleut", "Alutiiq", "Alutiit’stun", "Chugach Eskimo", "Koniag-Chugach", "Pacific Gulf Yupik", "Pacific Yupik", "South Alaska Eskimo", "Sugcestun", "Sugpiak Eskimo", "Sugpiaq", "Sugpiaq Eskimo", "Sugt’stun", "Suk" ], "nophonvars": true, "obsolete": true, "region": "US", @@ -31316,18 +32790,32 @@ "windows": "emx-Latn" }, { - "full": "emy-Zyyy-MX", + "full": "emy-Maya-MX", "iana": [ "Epigraphic Mayan" ], "iso639_3": "emy", "name": "Epigraphic Mayan", "region": "MX", "regionname": "Mexico", - "script": "Zyyy", + "script": "Maya", "sldr": false, "tag": "emy", - "tags": [ "emy-MX", "emy-Zyyy" ], + "tags": [ "emy-MX", "emy-Maya" ], "unwritten": true, - "windows": "emy-Zyyy" + "windows": "emy-Maya" + }, + { + "full": "emz-Latn-CM", + "iana": [ "Mbessa" ], + "iso639_3": "emz", + "name": "Mbessa", + "names": [ "Itangimbesa", "Iteanghe-a-Mbessa", "Mbesa", "Mbizenaku" ], + "region": "CM", + "regionname": "Cameroon", + "script": "Latn", + "sldr": false, + "tag": "emz", + "tags": [ "emz-CM", "emz-Latn" ], + "windows": "emz-Latn" }, { "full": "en-Latn-US", @@ -31339,14 +32827,14 @@ "names": [ "Anglais", "Angleščina", "Anglisy", "Angličtina", "Anglų", "Angol", "Angļu", "Engels", "Engelsk", "Engelska", "Engelski", "Englaisa", "Englanti", "Englesch", "Engleză", "Englisch", "Ingilizce", "Inglese", "Ingliż", "Inglés", "Inglês", "Język angielski", "Kiingereza", "anglais" ], "region": "US", "regionname": "United States", - "regions": [ "AD", "AR", "AS", "AW", "BD", "BG", "BH", "BL", "BN", "BQ", "BT", "CL", "CN", "CO", "CW", "CY", "CZ", "DO", "EC", "EE", "ES", "ET", "FM", "FR", "GQ", "GR", "HN", "HR", "HU", "ID", "IT", "JP", "KH", "KR", "LB", "LK", "LT", "LU", "LV", "LY", "MC", "MF", "MV", "MX", "NO", "NP", "OM", "PL", "PM", "PR", "PT", "RO", "RU", "SA", "SK", "SO", "SR", "ST", "TC", "TH", "UM", "VE", "VG", "VI" ], + "regions": [ "AD", "AF", "AR", "AS", "AW", "BD", "BG", "BH", "BL", "BN", "BQ", "BT", "BY", "CL", "CN", "CO", "CR", "CW", "CY", "CZ", "DO", "EC", "EE", "ES", "ET", "FM", "FR", "GQ", "GR", "GW", "HN", "HR", "HU", "ID", "IS", "IT", "JP", "KH", "KR", "KW", "LB", "LK", "LT", "LU", "LV", "LY", "MC", "ME", "MF", "MX", "NO", "NP", "OM", "PA", "PL", "PM", "PR", "PT", "RO", "RS", "RU", "SA", "SK", "SO", "SR", "ST", "SV", "TC", "TH", "TN", "TW", "UA", "UM", "UY", "VE", "VG", "VI" ], "script": "Latn", "sldr": true, "suppress": true, "tag": "en", "tags": [ "en-Latn", "en-US" ], "variants": [ "basiceng", "boont", "cornu", "emodeng", "oxendict", "scotland", "scouse", "spanglis", "unifon" ], - "windows": "en" + "windows": "en-US" }, { "full": "en-Latn-001", @@ -31359,9 +32847,8 @@ "regionname": "World", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-001", - "windows": "en-001" + "windows": "en-Latn-001" }, { "full": "en-Latn-150", @@ -31374,9 +32861,8 @@ "regionname": "Europe", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-150", - "windows": "en-150" + "windows": "en-Latn-150" }, { "full": "en-Latn-AE", @@ -31389,9 +32875,8 @@ "regionname": "United Arab Emirates", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-AE", - "windows": "en-AE" + "windows": "en-Latn-AE" }, { "full": "en-Latn-AG", @@ -31404,9 +32889,8 @@ "regionname": "Antigua and Barbuda", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-AG", - "windows": "en-AG" + "windows": "en-Latn-AG" }, { "full": "en-Latn-AI", @@ -31419,9 +32903,8 @@ "regionname": "Anguilla", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-AI", - "windows": "en-AI" + "windows": "en-Latn-AI" }, { "full": "en-Latn-AT", @@ -31434,9 +32917,8 @@ "regionname": "Austria", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-AT", - "windows": "en-AT" + "windows": "en-Latn-AT" }, { "full": "en-Latn-AU", @@ -31449,9 +32931,8 @@ "regionname": "Australia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-AU", - "windows": "en-AU" + "windows": "en-Latn-AU" }, { "full": "en-Latn-BB", @@ -31464,9 +32945,8 @@ "regionname": "Barbados", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BB", - "windows": "en-BB" + "windows": "en-Latn-BB" }, { "full": "en-Latn-BE", @@ -31479,9 +32959,8 @@ "regionname": "Belgium", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BE", - "windows": "en-BE" + "windows": "en-Latn-BE" }, { "full": "en-Latn-BI", @@ -31494,9 +32973,8 @@ "regionname": "Burundi", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BI", - "windows": "en-BI" + "windows": "en-Latn-BI" }, { "full": "en-Latn-BM", @@ -31509,9 +32987,8 @@ "regionname": "Bermuda", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BM", - "windows": "en-BM" + "windows": "en-Latn-BM" }, { "full": "en-Latn-BS", @@ -31524,9 +33001,8 @@ "regionname": "Bahamas", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BS", - "windows": "en-BS" + "windows": "en-Latn-BS" }, { "full": "en-Latn-BW", @@ -31539,9 +33015,8 @@ "regionname": "Botswana", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BW", - "windows": "en-BW" + "windows": "en-Latn-BW" }, { "full": "en-Latn-BZ", @@ -31554,24 +33029,23 @@ "regionname": "Belize", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-BZ", - "windows": "en-BZ" + "windows": "en-Latn-BZ" }, { "full": "en-Brai-GB", "iana": [ "English" ], "iso639_3": "eng", "name": "English", - "names": [ "Anglais", "Angleščina", "Anglisy", "Angličtina", "Anglų", "Angol", "Angļu", "Engels", "Engelsk", "Engelska", "Englaisa", "Englanti", "Englesch", "Engleză", "Englisch", "Ingilizce", "Inglese", "Ingliż", "Inglés", "Inglês", "Język angielski", "Kiingereza", "anglais" ], + "names": [ "Anglais", "Angleščina", "Anglisy", "Angličtina", "Anglų", "Angol", "Angļu", "Engels", "Engelsk", "Engelska", "Engelski", "Englaisa", "Englanti", "Englesch", "Engleză", "Englisch", "Ingilizce", "Inglese", "Ingliż", "Inglés", "Inglês", "Język angielski", "Kiingereza", "anglais" ], "nophonvars": true, "region": "GB", "regionname": "United Kingdom", - "regions": [ "AD", "AE", "AG", "AI", "AS", "AT", "AU", "AW", "BB", "BD", "BE", "BG", "BH", "BL", "BM", "BN", "BQ", "BS", "BT", "BW", "BZ", "CA", "CH", "CK", "CL", "CM", "CW", "CY", "CZ", "DE", "DK", "DM", "DO", "EC", "EE", "ER", "ES", "ET", "FI", "FJ", "FM", "FR", "GD", "GG", "GH", "GI", "GM", "GQ", "GR", "GU", "GY", "HK", "HN", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IT", "JE", "JM", "JP", "KE", "KH", "KI", "KR", "KY", "LB", "LC", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MF", "MG", "MH", "MO", "MP", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "NA", "NF", "NG", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "RO", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SI", "SK", "SL", "SO", "SR", "SS", "ST", "SX", "SZ", "TC", "TH", "TK", "TO", "TT", "TV", "TZ", "UG", "US", "VC", "VE", "VG", "VI", "VU", "WS", "ZA", "ZM", "ZW" ], + "regions": [ "AD", "AE", "AF", "AG", "AI", "AR", "AS", "AT", "AU", "AW", "BB", "BD", "BE", "BG", "BH", "BI", "BL", "BM", "BN", "BQ", "BS", "BT", "BW", "BY", "BZ", "CA", "CC", "CH", "CK", "CL", "CM", "CN", "CO", "CR", "CW", "CX", "CY", "CZ", "DE", "DG", "DK", "DM", "DO", "EC", "EE", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FR", "GD", "GG", "GH", "GI", "GM", "GQ", "GR", "GU", "GW", "GY", "HK", "HN", "HR", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IS", "IT", "JE", "JM", "JP", "KE", "KH", "KI", "KN", "KR", "KW", "KY", "LB", "LC", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MC", "ME", "MF", "MG", "MH", "MO", "MP", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "NA", "NF", "NG", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SK", "SL", "SO", "SR", "SS", "ST", "SV", "SX", "SZ", "TC", "TH", "TK", "TN", "TO", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "VC", "VE", "VG", "VI", "VU", "WS", "ZA", "ZM", "ZW" ], "script": "Brai", "sldr": false, "tag": "en-Brai", - "windows": "en-Brai" + "windows": "en-Brai-GB" }, { "full": "en-Latn-CA", @@ -31585,10 +33059,9 @@ "regionname": "Canada", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-CA", "variants": [ "newfound" ], - "windows": "en-CA" + "windows": "en-Latn-CA" }, { "full": "en-Latn-CC", @@ -31601,9 +33074,8 @@ "regionname": "Cocos (Keeling) Islands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-CC", - "windows": "en-CC" + "windows": "en-Latn-CC" }, { "full": "en-Latn-CH", @@ -31616,9 +33088,8 @@ "regionname": "Switzerland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-CH", - "windows": "en-CH" + "windows": "en-Latn-CH" }, { "full": "en-Latn-CK", @@ -31631,9 +33102,8 @@ "regionname": "Cook Islands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-CK", - "windows": "en-CK" + "windows": "en-Latn-CK" }, { "full": "en-Latn-CM", @@ -31646,9 +33116,8 @@ "regionname": "Cameroon", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-CM", - "windows": "en-CM" + "windows": "en-Latn-CM" }, { "full": "en-Latn-CX", @@ -31661,9 +33130,8 @@ "regionname": "Christmas Island", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-CX", - "windows": "en-CX" + "windows": "en-Latn-CX" }, { "full": "en-Latn-DE", @@ -31676,9 +33144,8 @@ "regionname": "Germany", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-DE", - "windows": "en-DE" + "windows": "en-Latn-DE" }, { "full": "en-Latn-DG", @@ -31691,9 +33158,8 @@ "regionname": "Diego Garcia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-DG", - "windows": "en-DG" + "windows": "en-Latn-DG" }, { "full": "en-Latn-DK", @@ -31706,9 +33172,8 @@ "regionname": "Denmark", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-DK", - "windows": "en-DK" + "windows": "en-Latn-DK" }, { "full": "en-Latn-DM", @@ -31721,9 +33186,8 @@ "regionname": "Dominica", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-DM", - "windows": "en-DM" + "windows": "en-Latn-DM" }, { "full": "en-Dsrt-US", @@ -31734,11 +33198,11 @@ "nophonvars": true, "region": "US", "regionname": "United States", - "regions": [ "AC", "AD", "AE", "AG", "AI", "AR", "AS", "AT", "AU", "AW", "BA", "BB", "BD", "BE", "BG", "BH", "BL", "BM", "BN", "BQ", "BR", "BS", "BT", "BW", "BZ", "CA", "CH", "CK", "CL", "CM", "CW", "CY", "CZ", "DE", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FM", "FR", "GB", "GD", "GG", "GH", "GI", "GM", "GQ", "GR", "GU", "GY", "HK", "HN", "HR", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IT", "JE", "JM", "JO", "JP", "KE", "KH", "KI", "KR", "KY", "KZ", "LB", "LC", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MF", "MG", "MH", "MO", "MP", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "NA", "NF", "NG", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "RO", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SI", "SK", "SL", "SO", "SR", "SS", "ST", "SX", "SZ", "TA", "TC", "TH", "TK", "TO", "TR", "TT", "TV", "TZ", "UG", "VC", "VE", "VG", "VI", "VU", "WS", "YE", "ZA", "ZM", "ZW" ], + "regions": [ "AC", "AD", "AE", "AF", "AG", "AI", "AR", "AS", "AT", "AU", "AW", "BA", "BB", "BD", "BE", "BG", "BH", "BI", "BL", "BM", "BN", "BQ", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CC", "CH", "CK", "CL", "CM", "CN", "CO", "CR", "CW", "CX", "CY", "CZ", "DE", "DG", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FR", "GB", "GD", "GG", "GH", "GI", "GM", "GQ", "GR", "GU", "GW", "GY", "HK", "HN", "HR", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KH", "KI", "KN", "KR", "KW", "KY", "KZ", "LB", "LC", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "ME", "MF", "MG", "MH", "MO", "MP", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "NA", "NF", "NG", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SK", "SL", "SO", "SR", "SS", "ST", "SV", "SX", "SZ", "TA", "TC", "TH", "TK", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "UY", "VC", "VE", "VG", "VI", "VU", "WS", "YE", "ZA", "ZM", "ZW" ], "script": "Dsrt", "sldr": false, "tag": "en-Dsrt", - "windows": "en-Dsrt" + "windows": "en-Dsrt-US" }, { "full": "en-Dupl-US", @@ -31751,7 +33215,7 @@ "script": "Dupl", "sldr": false, "tag": "en-Dupl", - "windows": "en-Dupl" + "windows": "en-Dupl-US" }, { "full": "en-Latn-ER", @@ -31764,9 +33228,8 @@ "regionname": "Eritrea", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-ER", - "windows": "en-ER" + "windows": "en-Latn-ER" }, { "full": "en-Latn-FI", @@ -31779,9 +33242,8 @@ "regionname": "Finland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-FI", - "windows": "en-FI" + "windows": "en-Latn-FI" }, { "full": "en-Latn-FJ", @@ -31794,9 +33256,8 @@ "regionname": "Fiji", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-FJ", - "windows": "en-FJ" + "windows": "en-Latn-FJ" }, { "full": "en-Latn-FK", @@ -31809,9 +33270,8 @@ "regionname": "Falkland Islands (Malvinas)", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-FK", - "windows": "en-FK" + "windows": "en-Latn-FK" }, { "full": "en-Latn-GB", @@ -31824,9 +33284,8 @@ "regionname": "United Kingdom", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GB", - "windows": "en-GB" + "windows": "en-Latn-GB" }, { "full": "en-Latn-GD", @@ -31839,9 +33298,8 @@ "regionname": "Grenada", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GD", - "windows": "en-GD" + "windows": "en-Latn-GD" }, { "full": "en-Latn-GG", @@ -31854,9 +33312,8 @@ "regionname": "Guernsey", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GG", - "windows": "en-GG" + "windows": "en-Latn-GG" }, { "full": "en-Latn-GH", @@ -31869,9 +33326,8 @@ "regionname": "Ghana", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GH", - "windows": "en-GH" + "windows": "en-Latn-GH" }, { "full": "en-Latn-GI", @@ -31884,9 +33340,8 @@ "regionname": "Gibraltar", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GI", - "windows": "en-GI" + "windows": "en-Latn-GI" }, { "full": "en-Latn-GM", @@ -31899,9 +33354,8 @@ "regionname": "Gambia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GM", - "windows": "en-GM" + "windows": "en-Latn-GM" }, { "full": "en-Latn-GU", @@ -31914,9 +33368,8 @@ "regionname": "Guam", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GU", - "windows": "en-GU" + "windows": "en-Latn-GU" }, { "full": "en-Latn-GY", @@ -31929,9 +33382,8 @@ "regionname": "Guyana", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-GY", - "windows": "en-GY" + "windows": "en-Latn-GY" }, { "full": "en-Latn-HK", @@ -31944,9 +33396,8 @@ "regionname": "Hong Kong", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-HK", - "windows": "en-HK" + "windows": "en-Latn-HK" }, { "full": "en-Latn-IE", @@ -31959,9 +33410,8 @@ "regionname": "Ireland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-IE", - "windows": "en-IE" + "windows": "en-Latn-IE" }, { "full": "en-Latn-IL", @@ -31974,9 +33424,8 @@ "regionname": "Israel", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-IL", - "windows": "en-IL" + "windows": "en-Latn-IL" }, { "full": "en-Latn-IM", @@ -31989,9 +33438,8 @@ "regionname": "Isle of Man", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-IM", - "windows": "en-IM" + "windows": "en-Latn-IM" }, { "full": "en-Latn-IN", @@ -32004,9 +33452,8 @@ "regionname": "India", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-IN", - "windows": "en-IN" + "windows": "en-Latn-IN" }, { "full": "en-Latn-IO", @@ -32019,9 +33466,8 @@ "regionname": "British Indian Ocean Territory", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-IO", - "windows": "en-IO" + "windows": "en-Latn-IO" }, { "full": "en-Latn-JE", @@ -32034,9 +33480,8 @@ "regionname": "Jersey", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-JE", - "windows": "en-JE" + "windows": "en-Latn-JE" }, { "full": "en-Latn-JM", @@ -32049,9 +33494,8 @@ "regionname": "Jamaica", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-JM", - "windows": "en-JM" + "windows": "en-Latn-JM" }, { "full": "en-Latn-KE", @@ -32064,9 +33508,8 @@ "regionname": "Kenya", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-KE", - "windows": "en-KE" + "windows": "en-Latn-KE" }, { "full": "en-Latn-KI", @@ -32079,9 +33522,8 @@ "regionname": "Kiribati", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-KI", - "windows": "en-KI" + "windows": "en-Latn-KI" }, { "full": "en-Latn-KN", @@ -32094,9 +33536,8 @@ "regionname": "Saint Kitts and Nevis", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-KN", - "windows": "en-KN" + "windows": "en-Latn-KN" }, { "full": "en-Latn-KY", @@ -32109,9 +33550,8 @@ "regionname": "Cayman Islands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-KY", - "windows": "en-KY" + "windows": "en-Latn-KY" }, { "full": "en-Latn-LC", @@ -32124,9 +33564,8 @@ "regionname": "Saint Lucia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-LC", - "windows": "en-LC" + "windows": "en-Latn-LC" }, { "full": "en-Latn-LR", @@ -32139,9 +33578,8 @@ "regionname": "Liberia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-LR", - "windows": "en-LR" + "windows": "en-Latn-LR" }, { "full": "en-Latn-LS", @@ -32154,9 +33592,8 @@ "regionname": "Lesotho", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-LS", - "windows": "en-LS" + "windows": "en-Latn-LS" }, { "full": "en-Latn-MG", @@ -32169,9 +33606,8 @@ "regionname": "Madagascar", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MG", - "windows": "en-MG" + "windows": "en-Latn-MG" }, { "full": "en-Latn-MH", @@ -32184,9 +33620,8 @@ "regionname": "Marshall Islands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MH", - "windows": "en-MH" + "windows": "en-Latn-MH" }, { "full": "en-Latn-MO", @@ -32199,9 +33634,8 @@ "regionname": "Macao", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MO", - "windows": "en-MO" + "windows": "en-Latn-MO" }, { "full": "en-Latn-MP", @@ -32214,9 +33648,8 @@ "regionname": "Northern Mariana Islands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MP", - "windows": "en-MP" + "windows": "en-Latn-MP" }, { "full": "en-Latn-MS", @@ -32229,9 +33662,8 @@ "regionname": "Montserrat", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MS", - "windows": "en-MS" + "windows": "en-Latn-MS" }, { "full": "en-Latn-MT", @@ -32244,9 +33676,8 @@ "regionname": "Malta", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MT", - "windows": "en-MT" + "windows": "en-Latn-MT" }, { "full": "en-Latn-MU", @@ -32259,9 +33690,22 @@ "regionname": "Mauritius", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MU", - "windows": "en-MU" + "windows": "en-Latn-MU" + }, + { + "full": "en-Latn-MV", + "iana": [ "English" ], + "iso639_3": "eng", + "localname": "English", + "localnames": [ "English" ], + "name": "English", + "region": "MV", + "regionname": "Maldives", + "script": "Latn", + "sldr": true, + "tag": "en-MV", + "windows": "en-Latn-MV" }, { "full": "en-Latn-MW", @@ -32274,9 +33718,8 @@ "regionname": "Malawi", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MW", - "windows": "en-MW" + "windows": "en-Latn-MW" }, { "full": "en-Latn-MY", @@ -32289,9 +33732,8 @@ "regionname": "Malaysia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-MY", - "windows": "en-MY" + "windows": "en-Latn-MY" }, { "full": "en-Latn-NA", @@ -32304,9 +33746,8 @@ "regionname": "Namibia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NA", - "windows": "en-NA" + "windows": "en-Latn-NA" }, { "full": "en-Latn-NF", @@ -32319,9 +33760,8 @@ "regionname": "Norfolk Island", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NF", - "windows": "en-NF" + "windows": "en-Latn-NF" }, { "full": "en-Latn-NG", @@ -32334,9 +33774,8 @@ "regionname": "Nigeria", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NG", - "windows": "en-NG" + "windows": "en-Latn-NG" }, { "full": "en-Latn-NL", @@ -32349,9 +33788,8 @@ "regionname": "Netherlands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NL", - "windows": "en-NL" + "windows": "en-Latn-NL" }, { "full": "en-Latn-NR", @@ -32364,9 +33802,8 @@ "regionname": "Nauru", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NR", - "windows": "en-NR" + "windows": "en-Latn-NR" }, { "full": "en-Latn-NU", @@ -32379,9 +33816,8 @@ "regionname": "Niue", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NU", - "windows": "en-NU" + "windows": "en-Latn-NU" }, { "full": "en-Latn-NZ", @@ -32394,9 +33830,8 @@ "regionname": "New Zealand", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-NZ", - "windows": "en-NZ" + "windows": "en-Latn-NZ" }, { "full": "en-Latn-PG", @@ -32409,9 +33844,8 @@ "regionname": "Papua New Guinea", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-PG", - "windows": "en-PG" + "windows": "en-Latn-PG" }, { "full": "en-Latn-PH", @@ -32424,9 +33858,8 @@ "regionname": "Philippines", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-PH", - "windows": "en-PH" + "windows": "en-Latn-PH" }, { "full": "en-Latn-PK", @@ -32439,9 +33872,8 @@ "regionname": "Pakistan", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-PK", - "windows": "en-PK" + "windows": "en-Latn-PK" }, { "full": "en-Latn-PN", @@ -32454,9 +33886,8 @@ "regionname": "Pitcairn", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-PN", - "windows": "en-PN" + "windows": "en-Latn-PN" }, { "full": "en-Latn-PW", @@ -32469,9 +33900,8 @@ "regionname": "Palau", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-PW", - "windows": "en-PW" + "windows": "en-Latn-PW" }, { "full": "en-Latn-RW", @@ -32484,9 +33914,21 @@ "regionname": "Rwanda", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-RW", - "windows": "en-RW" + "windows": "en-Latn-RW" + }, + { + "full": "en-Runr-001", + "iana": [ "English" ], + "iso639_3": "eng", + "name": "English", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "en-Runr", + "windows": "en-Runr-001" }, { "full": "en-Latn-SB", @@ -32499,9 +33941,8 @@ "regionname": "Solomon Islands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SB", - "windows": "en-SB" + "windows": "en-Latn-SB" }, { "full": "en-Latn-SC", @@ -32514,9 +33955,8 @@ "regionname": "Seychelles", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SC", - "windows": "en-SC" + "windows": "en-Latn-SC" }, { "full": "en-Latn-SD", @@ -32529,9 +33969,8 @@ "regionname": "Sudan", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SD", - "windows": "en-SD" + "windows": "en-Latn-SD" }, { "full": "en-Latn-SE", @@ -32544,9 +33983,8 @@ "regionname": "Sweden", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SE", - "windows": "en-SE" + "windows": "en-Latn-SE" }, { "full": "en-Latn-SG", @@ -32559,9 +33997,8 @@ "regionname": "Singapore", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SG", - "windows": "en-SG" + "windows": "en-Latn-SG" }, { "full": "en-Latn-SH", @@ -32574,9 +34011,8 @@ "regionname": "Saint Helena, Ascension and Tristan da Cunha", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SH", - "windows": "en-SH" + "windows": "en-Latn-SH" }, { "full": "en-Latn-SI", @@ -32589,9 +34025,8 @@ "regionname": "Slovenia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SI", - "windows": "en-SI" + "windows": "en-Latn-SI" }, { "full": "en-Latn-SL", @@ -32604,9 +34039,8 @@ "regionname": "Sierra Leone", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SL", - "windows": "en-SL" + "windows": "en-Latn-SL" }, { "full": "en-Latn-SS", @@ -32619,9 +34053,8 @@ "regionname": "South Sudan", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SS", - "windows": "en-SS" + "windows": "en-Latn-SS" }, { "full": "en-Latn-SX", @@ -32634,9 +34067,8 @@ "regionname": "Sint Maarten (Dutch part)", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SX", - "windows": "en-SX" + "windows": "en-Latn-SX" }, { "full": "en-Latn-SZ", @@ -32649,9 +34081,8 @@ "regionname": "Swaziland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-SZ", - "windows": "en-SZ" + "windows": "en-Latn-SZ" }, { "full": "en-Shaw-GB", @@ -32663,11 +34094,11 @@ "obsolete": true, "region": "GB", "regionname": "United Kingdom", - "regions": [ "AC", "AD", "AE", "AG", "AI", "AR", "AS", "AT", "AU", "AW", "BA", "BB", "BD", "BE", "BG", "BH", "BL", "BM", "BN", "BQ", "BR", "BS", "BT", "BW", "BZ", "CA", "CH", "CK", "CL", "CM", "CW", "CY", "CZ", "DE", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FM", "FR", "GD", "GG", "GH", "GI", "GM", "GQ", "GR", "GU", "GY", "HK", "HN", "HR", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IT", "JE", "JM", "JO", "JP", "KE", "KH", "KI", "KR", "KY", "KZ", "LB", "LC", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MF", "MG", "MH", "MO", "MP", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "NA", "NF", "NG", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "RO", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SI", "SK", "SL", "SO", "SR", "SS", "ST", "SX", "SZ", "TA", "TC", "TH", "TK", "TO", "TR", "TT", "TV", "TZ", "UG", "US", "VC", "VE", "VG", "VI", "VU", "WS", "YE", "ZA", "ZM", "ZW" ], + "regions": [ "AC", "AD", "AE", "AF", "AG", "AI", "AR", "AS", "AT", "AU", "AW", "BA", "BB", "BD", "BE", "BG", "BH", "BI", "BL", "BM", "BN", "BQ", "BR", "BS", "BT", "BW", "BY", "BZ", "CA", "CC", "CH", "CK", "CL", "CM", "CN", "CO", "CR", "CW", "CX", "CY", "CZ", "DE", "DG", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FR", "GD", "GG", "GH", "GI", "GM", "GQ", "GR", "GU", "GW", "GY", "HK", "HN", "HR", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KH", "KI", "KN", "KR", "KW", "KY", "KZ", "LB", "LC", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "ME", "MF", "MG", "MH", "MO", "MP", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "NA", "NF", "NG", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PT", "PW", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SK", "SL", "SO", "SR", "SS", "ST", "SV", "SX", "SZ", "TA", "TC", "TH", "TK", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "VC", "VE", "VG", "VI", "VU", "WS", "YE", "ZA", "ZM", "ZW" ], "script": "Shaw", "sldr": false, "tag": "en-Shaw", - "windows": "en-Shaw" + "windows": "en-Shaw-GB" }, { "full": "en-Latn-TK", @@ -32680,9 +34111,8 @@ "regionname": "Tokelau", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-TK", - "windows": "en-TK" + "windows": "en-Latn-TK" }, { "full": "en-Latn-TO", @@ -32695,9 +34125,8 @@ "regionname": "Tonga", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-TO", - "windows": "en-TO" + "windows": "en-Latn-TO" }, { "full": "en-Latn-TT", @@ -32710,9 +34139,8 @@ "regionname": "Trinidad and Tobago", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-TT", - "windows": "en-TT" + "windows": "en-Latn-TT" }, { "full": "en-Latn-TV", @@ -32725,9 +34153,8 @@ "regionname": "Tuvalu", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-TV", - "windows": "en-TV" + "windows": "en-Latn-TV" }, { "full": "en-Latn-TZ", @@ -32740,9 +34167,8 @@ "regionname": "United Republic of Tanzania", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-TZ", - "windows": "en-TZ" + "windows": "en-Latn-TZ" }, { "full": "en-Latn-UG", @@ -32755,9 +34181,8 @@ "regionname": "Uganda", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-UG", - "windows": "en-UG" + "windows": "en-Latn-UG" }, { "full": "en-Latn-VC", @@ -32770,9 +34195,8 @@ "regionname": "Saint Vincent and the Grenadines", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-VC", - "windows": "en-VC" + "windows": "en-Latn-VC" }, { "full": "en-Latn-VU", @@ -32785,9 +34209,8 @@ "regionname": "Vanuatu", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-VU", - "windows": "en-VU" + "windows": "en-Latn-VU" }, { "full": "en-Latn-WS", @@ -32800,9 +34223,8 @@ "regionname": "Samoa", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-WS", - "windows": "en-WS" + "windows": "en-Latn-WS" }, { "full": "en-Latn-ZA", @@ -32815,9 +34237,8 @@ "regionname": "South Africa", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-ZA", - "windows": "en-ZA" + "windows": "en-Latn-ZA" }, { "full": "en-Latn-ZM", @@ -32830,9 +34251,8 @@ "regionname": "Zambia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-ZM", - "windows": "en-ZM" + "windows": "en-Latn-ZM" }, { "full": "en-Latn-ZW", @@ -32845,9 +34265,8 @@ "regionname": "Zimbabwe", "script": "Latn", "sldr": true, - "suppress": true, "tag": "en-ZW", - "windows": "en-ZW" + "windows": "en-Latn-ZW" }, { "full": "ena-Latn-PG", @@ -32869,6 +34288,7 @@ "iana": [ "Markweeta" ], "iso639_3": "enb", "localnames": [ "Markweta" ], + "macrolang": "kln", "name": "Markweeta", "names": [ "Marakuet", "Marakwet", "Markweta" ], "region": "KE", @@ -32965,12 +34385,27 @@ "sldr": false, "tag": "enm", "tags": [ "enm-GB", "enm-Latn" ], - "windows": "enm-Latn" + "windows": "enm-Latn-GB" + }, + { + "full": "enm-Runr-001", + "iana": [ "Middle English (1100-1500)" ], + "iso639_3": "enm", + "name": "Middle English (1100-1500)", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "enm-Runr", + "tags": [ "enm-001" ], + "windows": "enm-Runr-001" }, { "full": "enn-Latn-NG", "iana": [ "Engenni" ], "iso639_3": "enn", + "localname": "Egene", "localnames": [ "Egene" ], "name": "Engenni", "names": [ "Egene", "Ngene" ], @@ -33044,10 +34479,10 @@ }, { "full": "env-Latn-NG", - "iana": [ "Enwan (Edu State)" ], + "iana": [ "Enwan (Edo State)" ], "iso639_3": "env", "name": "Enwan", - "names": [ "Enwan (Edu State)" ], + "names": [ "Enwan (Edo State)" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -33135,19 +34570,19 @@ "windows": "epi-Latn" }, { - "full": "era-Zyyy-IN", + "full": "era-Taml-IN", "iana": [ "Eravallan" ], "iso639_3": "era", "name": "Eravallan", "names": [ "Ambuvilluvedar", "Villu Vedan", "Vilvedan" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Taml", "sldr": false, "tag": "era", - "tags": [ "era-IN", "era-Zyyy" ], + "tags": [ "era-IN", "era-Taml" ], "unwritten": true, - "windows": "era-Zyyy" + "windows": "era-Taml" }, { "full": "erg-Latn-VU", @@ -33240,20 +34675,35 @@ "unwritten": true, "windows": "err-Latn" }, + { + "full": "ers-Latn-CN", + "iana": [ "Ersu" ], + "iso639_3": "ers", + "name": "Ersu", + "names": [ "Bu’erci", "Bu’erzi", "Bu’erzi Ersu", "Doxu", "Duoxu", "Erhsu", "Lizu", "Lusu", "T’osu" ], + "region": "CN", + "regionname": "China", + "script": "Latn", + "sldr": false, + "tag": "ers", + "tags": [ "ers-CN", "ers-Latn" ], + "windows": "ers-Latn" + }, { "full": "ers-Zzzz-CN-x-ersushab", "iana": [ "Ersu" ], "iso639_3": "ers", "name": "Ersu", "names": [ "Bu’erci", "Bu’erzi", "Bu’erzi Ersu", "Doxu", "Duoxu", "Erhsu", "Lizu", "Lusu", "T’osu" ], + "nophonvars": true, "obsolete": true, "region": "CN", "regionname": "China", "script": "Zzzz", "sldr": false, - "tag": "ers", - "tags": [ "ers-CN", "ers-CN-x-ersushab", "ers-Zzzz", "ers-Zzzz-CN", "ers-Zzzz-x-ersushab", "ers-x-ersushab" ], - "windows": "ers-Zzzz" + "tag": "ers-Zzzz-x-ersushab", + "tags": [ "ers-CN-x-ersushab" ], + "windows": "ers-Zzzz-x-ersushab" }, { "full": "ert-Latn-ID", @@ -33294,14 +34744,14 @@ "names": [ "Castellano", "Castilian", "Castillan", "Espagnol", "Español" ], "region": "ES", "regionname": "Spain", - "regions": [ "AD", "AT", "AU", "AW", "BE", "BG", "BQ", "CA", "CH", "CI", "CW", "CZ", "DE", "DK", "DZ", "EA", "EH", "FI", "FR", "GI", "GU", "HR", "IC", "IE", "IL", "IS", "IT", "JM", "KY", "LU", "MA", "MT", "NL", "NO", "NZ", "PL", "PT", "RO", "RU", "SE", "SR", "SX", "TR", "TT", "VI" ], + "regions": [ "AD", "AT", "AU", "AW", "BE", "BG", "BQ", "CA", "CH", "CI", "CW", "CZ", "DE", "DK", "DZ", "EA", "EH", "FI", "FR", "GB", "GI", "GU", "GW", "HR", "IC", "IE", "IL", "IS", "IT", "JM", "KY", "LU", "MA", "MT", "NL", "NO", "NZ", "PL", "PT", "RO", "RU", "SE", "SR", "SX", "TR", "TT", "VG", "VI" ], "script": "Latn", "sldr": true, "suppress": true, "tag": "es", "tags": [ "es-ES", "es-Latn" ], "variants": [ "spanglis" ], - "windows": "es" + "windows": "es-ES" }, { "full": "es-Latn-419", @@ -33315,9 +34765,8 @@ "regionname": "Latin America and the Caribbean", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-419", - "windows": "es-419" + "windows": "es-Latn-419" }, { "full": "es-Latn-AR", @@ -33331,9 +34780,8 @@ "regionname": "Argentina", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-AR", - "windows": "es-AR" + "windows": "es-Latn-AR" }, { "full": "es-Latn-BO", @@ -33347,9 +34795,8 @@ "regionname": "Bolivia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-BO", - "windows": "es-BO" + "windows": "es-Latn-BO" }, { "full": "es-Latn-BR", @@ -33363,9 +34810,8 @@ "regionname": "Brazil", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-BR", - "windows": "es-BR" + "windows": "es-Latn-BR" }, { "full": "es-Latn-BZ", @@ -33379,9 +34825,8 @@ "regionname": "Belize", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-BZ", - "windows": "es-BZ" + "windows": "es-Latn-BZ" }, { "full": "es-Brai-ES", @@ -33396,7 +34841,7 @@ "script": "Brai", "sldr": false, "tag": "es-Brai", - "windows": "es-Brai" + "windows": "es-Brai-ES" }, { "full": "es-Latn-CL", @@ -33410,9 +34855,8 @@ "regionname": "Chile", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-CL", - "windows": "es-CL" + "windows": "es-Latn-CL" }, { "full": "es-Latn-CO", @@ -33426,9 +34870,8 @@ "regionname": "Colombia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-CO", - "windows": "es-CO" + "windows": "es-Latn-CO" }, { "full": "es-Latn-CR", @@ -33442,9 +34885,8 @@ "regionname": "Costa Rica", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-CR", - "windows": "es-CR" + "windows": "es-Latn-CR" }, { "full": "es-Latn-CU", @@ -33458,9 +34900,8 @@ "regionname": "Cuba", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-CU", - "windows": "es-CU" + "windows": "es-Latn-CU" }, { "full": "es-Latn-DO", @@ -33474,9 +34915,8 @@ "regionname": "Dominican Republic", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-DO", - "windows": "es-DO" + "windows": "es-Latn-DO" }, { "full": "es-Dupl-ES", @@ -33491,7 +34931,7 @@ "script": "Dupl", "sldr": false, "tag": "es-Dupl", - "windows": "es-Dupl" + "windows": "es-Dupl-ES" }, { "full": "es-Latn-EC", @@ -33505,9 +34945,8 @@ "regionname": "Ecuador", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-EC", - "windows": "es-EC" + "windows": "es-Latn-EC" }, { "full": "es-Latn-GQ", @@ -33521,9 +34960,8 @@ "regionname": "Equatorial Guinea", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-GQ", - "windows": "es-GQ" + "windows": "es-Latn-GQ" }, { "full": "es-Latn-GT", @@ -33537,9 +34975,8 @@ "regionname": "Guatemala", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-GT", - "windows": "es-GT" + "windows": "es-Latn-GT" }, { "full": "es-Latn-HN", @@ -33553,9 +34990,8 @@ "regionname": "Honduras", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-HN", - "windows": "es-HN" + "windows": "es-Latn-HN" }, { "full": "es-Latn-MX", @@ -33569,9 +35005,8 @@ "regionname": "Mexico", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-MX", - "windows": "es-MX" + "windows": "es-Latn-MX" }, { "full": "es-Latn-NI", @@ -33585,9 +35020,8 @@ "regionname": "Nicaragua", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-NI", - "windows": "es-NI" + "windows": "es-Latn-NI" }, { "full": "es-Latn-PA", @@ -33601,9 +35035,8 @@ "regionname": "Panama", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-PA", - "windows": "es-PA" + "windows": "es-Latn-PA" }, { "full": "es-Latn-PE", @@ -33617,9 +35050,8 @@ "regionname": "Peru", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-PE", - "windows": "es-PE" + "windows": "es-Latn-PE" }, { "full": "es-Latn-PH", @@ -33633,9 +35065,8 @@ "regionname": "Philippines", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-PH", - "windows": "es-PH" + "windows": "es-Latn-PH" }, { "full": "es-Latn-PR", @@ -33649,9 +35080,8 @@ "regionname": "Puerto Rico", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-PR", - "windows": "es-PR" + "windows": "es-Latn-PR" }, { "full": "es-Latn-PY", @@ -33665,9 +35095,8 @@ "regionname": "Paraguay", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-PY", - "windows": "es-PY" + "windows": "es-Latn-PY" }, { "full": "es-Latn-SV", @@ -33681,9 +35110,8 @@ "regionname": "El Salvador", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-SV", - "windows": "es-SV" + "windows": "es-Latn-SV" }, { "full": "es-Latn-US", @@ -33697,9 +35125,8 @@ "regionname": "United States", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-US", - "windows": "es-US" + "windows": "es-Latn-US" }, { "full": "es-Latn-UY", @@ -33713,9 +35140,8 @@ "regionname": "Uruguay", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-UY", - "windows": "es-UY" + "windows": "es-Latn-UY" }, { "full": "es-Latn-VE", @@ -33729,15 +35155,14 @@ "regionname": "Venezuela", "script": "Latn", "sldr": true, - "suppress": true, "tag": "es-VE", - "windows": "es-VE" + "windows": "es-Latn-VE" }, { "full": "ese-Latn-BO", "iana": [ "Ese Ejja" ], "iso639_3": "ese", - "localname": "Ese'ejja", + "localname": "Eseꞌejja", "localnames": [ "Ese'ejja" ], "name": "Ese Ejja", "names": [ "Ese Eja", "Ese Exa", "Eseejja", "Ese’eha", "Ese’ejja", "Essejja", "Guarayo", "Huarayo", "Tambopata-Guarayo", "Tiatinagua" ], @@ -33751,39 +35176,42 @@ "windows": "ese-Latn" }, { - "full": "esg-Deva-IN", + "full": "esg-Gonm-IN", "iana": [ "Aheri Gondi" ], "iso639_3": "esg", - "latnnames": [ "Koyam", "Gondi" ], - "localnames": [ "कोयम", "गोंडी" ], + "macrolang": "gon", "name": "Gondi, Aheri", "names": [ "Aheri Gondi", "Koyam", "Raj Gond" ], "region": "IN", "regionname": "India", - "script": "Deva", - "sldr": false, + "script": "Gonm", + "sldr": true, "tag": "esg", - "tags": [ "esg-Deva", "esg-IN" ], - "windows": "esg-Deva" + "tags": [ "esg-Gonm", "esg-IN" ], + "windows": "esg-Gonm" }, { - "full": "esg-Gonm-IN", + "full": "esg-Deva-IN", "iana": [ "Aheri Gondi" ], "iso639_3": "esg", + "latnnames": [ "Koyam", "Gondi" ], + "localnames": [ "कोयम", "गोंडी" ], + "macrolang": "gon", "name": "Gondi, Aheri", "names": [ "Aheri Gondi", "Koyam", "Raj Gond" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Gonm", + "script": "Deva", "sldr": false, - "tag": "esg-Gonm", - "windows": "esg-Gonm" + "tag": "esg-Deva", + "windows": "esg-Deva" }, { "full": "esg-Telu-IN", "iana": [ "Aheri Gondi" ], "iso639_3": "esg", + "macrolang": "gon", "name": "Gondi, Aheri", "names": [ "Aheri Gondi", "Koyam", "Raj Gond" ], "nophonvars": true, @@ -33795,23 +35223,24 @@ "windows": "esg-Telu" }, { - "full": "esh-Zyyy-IR", + "full": "esh-Arab-IR", "iana": [ "Eshtehardi" ], "iso639_3": "esh", "name": "Eshtehardi", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "esh", - "tags": [ "esh-IR", "esh-Zyyy" ], - "windows": "esh-Zyyy" + "tags": [ "esh-Arab", "esh-IR" ], + "windows": "esh-Arab" }, { "full": "esi-Latn-US", "iana": [ "North Alaskan Inupiatun" ], "iso639_3": "esi", "localnames": [ "Iñupiatun" ], + "macrolang": "ik", "name": "Inupiatun, North Alaskan", "names": [ "Eskimo", "Ieupiaq", "Inuktitut, North Alaskan", "Inupiak", "Inupiat", "Iñupiaq", "Iñupiatun", "North Alaskan Inuktitut", "North Alaskan Inupiaq", "North Alaskan Inupiat", "North Alaskan Inupiatun", "North Alaskan Iñupiaq", "Uqabniqput Ieupiatun" ], "region": "US", @@ -33842,6 +35271,7 @@ "iana": [ "Esuma" ], "iso639_3": "esm", "name": "Esuma", + "names": [ "Essouma" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -33856,7 +35286,7 @@ "iana": [ "Salvadoran Sign Language" ], "iso639_3": "esn", "name": "Salvadoran Sign Language", - "names": [ "El Salvadoran Sign Language", "LESA", "LESSA", "Lengua de señas salvadoreñas" ], + "names": [ "El Salvadoran Sign Language", "LESA", "LESSA", "Lengua de señas salvadoreña", "Lengua de señas salvadoreñas" ], "region": "SV", "regionname": "El Salvador", "script": "Zxxx", @@ -33898,14 +35328,14 @@ "full": "ess-Latn-US", "iana": [ "Central Siberian Yupik" ], "iso639_3": "ess", - "localnames": [ "Yupik" ], + "localnames": [ "Yupigestun" ], "name": "Yupik, Saint Lawrence Island", - "names": [ "Asiatic Yupik", "Bering Strait", "Bering Strait Yupik", "Central Siberian Yupik", "Saint Lawrence Island Eskimo", "Siberian Yupik", "Sivuqaghmiistun", "Yoit", "Yuit", "Yuitsky", "Yuk", "Yupik", "Yupik, Central Siberian" ], + "names": [ "Akuzipigestun", "Asiatic Yupik", "Bering Strait", "Bering Strait Yupik", "Central Siberian Yupik", "Saint Lawrence Island Eskimo", "Siberian Yupik", "Sivuqaghmiistun", "Yoit", "Yuit", "Yuitsky", "Yuk", "Yupigestun", "Yupik", "Yupik, Central Siberian" ], "region": "US", "regionname": "United States", "regions": [ "RU" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ess", "tags": [ "ess-Latn", "ess-US" ], "windows": "ess-Latn" @@ -33915,7 +35345,7 @@ "iana": [ "Central Siberian Yupik" ], "iso639_3": "ess", "name": "Yupik, Saint Lawrence Island", - "names": [ "Asiatic Yupik", "Bering Strait", "Bering Strait Yupik", "Central Siberian Yupik", "Saint Lawrence Island Eskimo", "Siberian Yupik", "Sivuqaghmiistun", "Yoit", "Yuit", "Yuitsky", "Yuk", "Yupik", "Yupik, Central Siberian" ], + "names": [ "Akuzipigestun", "Asiatic Yupik", "Bering Strait", "Bering Strait Yupik", "Central Siberian Yupik", "Saint Lawrence Island Eskimo", "Siberian Yupik", "Sivuqaghmiistun", "Yoit", "Yuit", "Yuitsky", "Yuk", "Yupigestun", "Yupik", "Yupik, Central Siberian" ], "nophonvars": true, "region": "US", "regionname": "United States", @@ -33929,9 +35359,9 @@ "full": "esu-Latn-US", "iana": [ "Central Yupik" ], "iso639_3": "esu", - "localnames": [ "Yugtun" ], + "localnames": [ "Cugtun", "Yugtun" ], "name": "Yupik, Central", - "names": [ "Central Alaskan Yupik", "Central Yupik", "Kuskokwim Eskimo", "Yugtun", "Yupik", "Yupʼik" ], + "names": [ "Central Alaskan Yupik", "Central Yupik", "Cugtun", "Kuskokwim Eskimo", "Yugtun", "Yupik", "Yupʼik" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -33974,9 +35404,9 @@ "iana": [ "Estonian" ], "iso639_3": "est", "localname": "eesti", - "localnames": [ "Eesti Keel", "Eesti Kirjakeel" ], - "name": "Estonian, Standard", - "names": [ "Eesti", "Eesti Keel", "Eesti Kirjakeel", "Estonian" ], + "localnames": [ "Eesti Keel" ], + "name": "Estonian", + "names": [ "Eesti", "Eesti Keel", "Eesti Kirjakeel", "Estonian, Standard" ], "region": "EE", "regionname": "Estonia", "regions": [ "AU", "CA", "DE", "FI", "KZ", "LV", "RU", "SE", "US" ], @@ -34091,6 +35521,7 @@ "full": "ets-Latn-NG", "iana": [ "Yekhee" ], "iso639_3": "ets", + "localnames": [ "Etsako" ], "name": "Etsako", "names": [ "Afenmai", "Etsakor", "Iyekhee", "Yekhee" ], "region": "NG", @@ -34132,8 +35563,9 @@ "full": "etu-Latn-NG", "iana": [ "Ejagham" ], "iso639_3": "etu", + "localnames": [ "Éjághám" ], "name": "Ejagham", - "names": [ "Edjagam", "Ejagam", "Ejaham", "Ejwe", "Ekoi", "Etung", "Keaka", "Kwa", "Obang" ], + "names": [ "Edjagam", "Ejagam", "Ejaham", "Ejwe", "Ejághá", "Ekoi", "Etung", "Jagham", "Keaka", "Kwa", "Obang", "Éjághám" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -34148,9 +35580,8 @@ "iana": [ "Eten" ], "iso639_3": "etx", "localname": "Iten", - "localnames": [ "Etien", "Iten", "Ten" ], "name": "Iten", - "names": [ "Aten", "Eten", "Etien", "Ganawuri", "Niten", "Ten" ], + "names": [ "Aten", "Eten", "Etien", "Ganawuri", "Niten", "Ten", "ìtên" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -34179,9 +35610,9 @@ "iana": [ "Basque" ], "iso639_3": "eus", "localname": "euskara", - "localnames": [ "Euskara" ], + "localnames": [ "Euskera" ], "name": "Basque", - "names": [ "Euska", "Euskara", "Euskera", "Euskerie" ], + "names": [ "Euska", "Euskara", "Euskera", "Euskerie", "Vasco" ], "region": "ES", "regionname": "Spain", "regions": [ "AR", "FR", "MX", "PH", "US" ], @@ -34193,6 +35624,20 @@ "variants": [ "biscayan" ], "windows": "eu" }, + { + "full": "eud-Latn-MX", + "iso639_3": "eud", + "name": "Eudeve", + "names": [ "Dóhmenerít" ], + "region": "MX", + "regionname": "Mexico", + "script": "Latn", + "sldr": false, + "tag": "eud", + "tags": [ "eud-Latn", "eud-MX" ], + "unwritten": true, + "windows": "eud-Latn" + }, { "full": "eve-Cyrl-RU", "iana": [ "Even" ], @@ -34209,6 +35654,21 @@ "tags": [ "eve-Cyrl", "eve-RU" ], "windows": "eve-Cyrl" }, + { + "full": "eve-Cyrs-RU", + "iana": [ "Even" ], + "iso639_3": "eve", + "name": "Even", + "names": [ "Eben", "Ewen", "Ilqan", "Lamut", "Orich" ], + "nophonvars": true, + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "script": "Cyrs", + "sldr": false, + "tag": "eve-Cyrs", + "windows": "eve-Cyrs" + }, { "full": "evh-Latn-NG", "iana": [ "Uvbie" ], @@ -34237,7 +35697,7 @@ "sldr": false, "tag": "evn", "tags": [ "evn-Cyrl", "evn-RU" ], - "windows": "evn-Cyrl" + "windows": "evn-Cyrl-RU" }, { "full": "evn-Latn-CN", @@ -34251,7 +35711,7 @@ "script": "Latn", "sldr": false, "tag": "evn-Latn", - "windows": "evn-Latn" + "windows": "evn-Latn-CN" }, { "full": "evn-Mong-CN", @@ -34266,7 +35726,7 @@ "script": "Mong", "sldr": false, "tag": "evn-Mong", - "windows": "evn-Mong" + "windows": "evn-Mong-CN" }, { "full": "ewo-Latn-CM", @@ -34301,23 +35761,25 @@ "windows": "ext-Latn" }, { - "full": "eya-Zyyy-US", + "full": "eya-Latn-US", "iana": [ "Eyak" ], "iso639_3": "eya", + "localnames": [ "dAxhunhyuuga’" ], "name": "Eyak", + "names": [ "dAxhunhyuuga’" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "eya", - "tags": [ "eya-US", "eya-Zyyy" ], - "unwritten": true, - "windows": "eya-Zyyy" + "tags": [ "eya-Latn", "eya-US" ], + "windows": "eya-Latn" }, { "full": "eyo-Latn-KE", "iana": [ "Keiyo" ], "iso639_3": "eyo", + "macrolang": "kln", "name": "Keiyo", "names": [ "Elgeyo", "Keyo" ], "region": "KE", @@ -34367,8 +35829,8 @@ "latnnames": [ "Fārsi" ], "localname": "فارسی", "localnames": [ "فارسی" ], - "name": "Persian, Iranian", - "names": [ "Ajamic", "Farsi", "New Persian", "Parsi", "Persian", "West Persian", "Western Farsi" ], + "name": "Persian", + "names": [ "Ajamic", "Farsi", "New Persian", "Parsi", "Persian, Iranian", "West Persian", "Western Farsi" ], "region": "IR", "regionname": "Islamic Republic of Iran", "regions": [ "AE", "AT", "AU", "AZ", "BH", "CA", "DE", "DK", "ES", "FI", "FR", "GR", "IL", "IN", "IQ", "NL", "OM", "PK", "QA", "RU", "SE", "SY", "TJ", "TM", "TR", "US", "UZ" ], @@ -34377,7 +35839,7 @@ "suppress": true, "tag": "fa", "tags": [ "fa-Arab", "fa-IR", "pes-Arab", "pes-Arab-IR" ], - "windows": "fa" + "windows": "fa-IR" }, { "full": "fa-Arab-AF", @@ -34389,18 +35851,17 @@ "regionname": "Afghanistan", "script": "Arab", "sldr": true, - "suppress": true, "tag": "fa-AF", - "windows": "fa-AF" + "windows": "fa-Arab-AF" }, { "full": "faa-Latn-PG", "iana": [ "Fasu" ], "iso639_3": "faa", "localname": "Namo Me", - "localnames": [ "Namo Me" ], + "localnames": [ "Nāmo Mé" ], "name": "Fasu", - "names": [ "Namo Me", "Namome" ], + "names": [ "Namo Aporo", "Namome", "Nāmo Mé" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -34459,8 +35920,9 @@ "full": "fag-Latn-PG", "iana": [ "Finongan" ], "iso639_3": "fag", + "localnames": [ "Finongan man" ], "name": "Finongan", - "names": [ "Finungwa", "Finungwan" ], + "names": [ "Finongan man", "Finungwa", "Finungwan" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -34594,6 +36056,8 @@ "full": "far-Latn-SB", "iana": [ "Fataleka" ], "iso639_3": "far", + "localname": "Fataleka", + "localnames": [ "Fataleka" ], "name": "Fataleka", "region": "SB", "regionname": "Solomon Islands", @@ -34665,6 +36129,7 @@ "full": "fbl-Latn-PH", "iana": [ "West Albay Bikol" ], "iso639_3": "fbl", + "macrolang": "bik", "name": "Bikol, West Albay", "names": [ "Bicol", "Bikol", "Ligaoeño", "Oasnun", "Oasnün", "Polanguinon", "West Albay Bikol" ], "region": "PH", @@ -34709,9 +36174,9 @@ "iana": [ "Fulah" ], "iso639_3": "ful", "localname": "Pulaar", - "localnames": [ "Pulaar", "Pël" ], - "name": "Pulaar", - "names": [ "Fulah", "Fulani", "Fulbe Jeeri", "Fulfulde Pulaar", "Fulfulde-Pulaar", "Haalpulaar", "Peuhl", "Peul", "Peulh", "Pulaar Fulfulde", "Pël" ], + "localnames": [ "Pulaar" ], + "name": "Fulah", + "names": [ "Fula", "Fulacunda", "Fulado", "Fulani", "Fulbe Jeeri", "Fulfulde Pulaar", "Fulfulde-Pulaar", "Haalpulaar", "Peuhl", "Peul", "Peulh", "Pulaar", "Pulaar Fulfulde", "Pël" ], "region": "SN", "regionname": "Senegal", "regions": [ "BF", "BJ", "CM", "GW", "ML", "NE", "TD" ], @@ -34719,29 +36184,173 @@ "sldr": true, "tag": "ff", "tags": [ "ff-Latn", "ff-SN", "fuc", "fuc-Latn", "fuc-Latn-SN", "fuc-SN" ], - "windows": "ff-Latn" + "windows": "ff-Latn-SN" }, { "full": "ff-Adlm-GN", "iana": [ "Fulah" ], "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", "name": "Fulah", "nophonvars": true, "region": "GN", "regionname": "Guinea", - "regions": [ "BJ", "CM", "ML", "NE", "NG", "SN", "TD" ], + "regions": [ "BJ", "ML", "TD" ], "script": "Adlm", - "sldr": false, + "sldr": true, "tag": "ff-Adlm", - "windows": "ff-Adlm" + "windows": "ff-Adlm-GN" + }, + { + "full": "ff-Adlm-BF", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "BF", + "regionname": "Burkina Faso", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-BF", + "windows": "ff-Adlm-BF" + }, + { + "full": "ff-Adlm-CM", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "CM", + "regionname": "Cameroon", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-CM", + "windows": "ff-Adlm-CM" + }, + { + "full": "ff-Adlm-GH", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "GH", + "regionname": "Ghana", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-GH", + "windows": "ff-Adlm-GH" + }, + { + "full": "ff-Adlm-GM", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "GM", + "regionname": "Gambia", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-GM", + "windows": "ff-Adlm-GM" + }, + { + "full": "ff-Adlm-GW", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "GW", + "regionname": "Guinea-Bissau", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-GW", + "windows": "ff-Adlm-GW" + }, + { + "full": "ff-Adlm-LR", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "LR", + "regionname": "Liberia", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-LR", + "windows": "ff-Adlm-LR" + }, + { + "full": "ff-Adlm-MR", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "Pulaar", + "name": "Fulah", + "region": "MR", + "regionname": "Mauritania", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-MR", + "windows": "ff-Adlm-MR" + }, + { + "full": "ff-Adlm-NE", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "NE", + "regionname": "Niger", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-NE", + "windows": "ff-Adlm-NE" + }, + { + "full": "ff-Adlm-NG", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "NG", + "regionname": "Nigeria", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-NG", + "windows": "ff-Adlm-NG" + }, + { + "full": "ff-Adlm-SL", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "SL", + "regionname": "Sierra Leone", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-SL", + "windows": "ff-Adlm-SL" + }, + { + "full": "ff-Adlm-SN", + "iana": [ "Fulah" ], + "iso639_3": "ful", + "localname": "𞤆𞤵𞤤𞤢𞤪", + "name": "Fulah", + "region": "SN", + "regionname": "Senegal", + "script": "Adlm", + "sldr": true, + "tag": "ff-Adlm-SN", + "windows": "ff-Adlm-SN" }, { "full": "ff-Arab-SN", "iana": [ "Fulah" ], "iso639_3": "ful", "localname": "Pulaar", - "name": "Pulaar", - "names": [ "Fulah", "Fulani", "Fulbe Jeeri", "Fulfulde Pulaar", "Fulfulde-Pulaar", "Haalpulaar", "Peuhl", "Peul", "Peulh", "Pulaar Fulfulde", "Pël" ], + "name": "Fulah", + "names": [ "Fula", "Fulacunda", "Fulado", "Fulani", "Fulbe Jeeri", "Fulfulde Pulaar", "Fulfulde-Pulaar", "Haalpulaar", "Peuhl", "Peul", "Peulh", "Pulaar", "Pulaar Fulfulde", "Pël" ], "nophonvars": true, "region": "SN", "regionname": "Senegal", @@ -34750,7 +36359,7 @@ "sldr": true, "tag": "ff-Arab", "tags": [ "fuc-Arab", "fuc-Arab-SN" ], - "windows": "ff-Arab" + "windows": "ff-Arab-SN" }, { "full": "ff-Latn-GH", @@ -34865,6 +36474,7 @@ "iso639_3": "ffm", "localname": "Fulfulde", "localnames": [ "Fulfulde", "Maasinankoore" ], + "macrolang": "ff", "name": "Fulfulde, Maasina", "names": [ "Fulani", "Fulbe", "Fulfulde", "Maacina", "Maasina Fulfulde", "Maasinankoore", "Macina", "Peul", "Toucouleur" ], "region": "ML", @@ -34881,6 +36491,7 @@ "iana": [ "Maasina Fulfulde" ], "iso639_3": "ffm", "localname": "Fulfulde", + "macrolang": "ff", "name": "Fulfulde, Maasina", "names": [ "Fulani", "Fulbe", "Fulfulde", "Maacina", "Maasina Fulfulde", "Maasinankoore", "Macina", "Peul", "Toucouleur" ], "nophonvars": true, @@ -34966,10 +36577,10 @@ "regionname": "Sudan", "regions": [ "EG" ], "script": "Copt", - "sldr": false, + "sldr": true, "tag": "fia-Copt", "tags": [ "fia-Copt-SD", "fia-Copt-x-olnubian", "fia-SD-x-olnubian" ], - "windows": "fia-Copt" + "windows": "fia-Copt-x-olnubian" }, { "full": "fia-Latn-SD", @@ -34990,7 +36601,7 @@ "iana": [ "Fyer" ], "iso639_3": "fie", "name": "Fyer", - "names": [ "Fier" ], + "names": [ "Fer", "Fier" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -35000,6 +36611,20 @@ "unwritten": true, "windows": "fie-Latn" }, + { + "full": "fif-Latn-SA", + "iana": [ "Faifi" ], + "iso639_3": "fif", + "name": "Faifi", + "names": [ "Al-fayfiyah", "Al-fifiyah", "Fifi" ], + "region": "SA", + "regionname": "Saudi Arabia", + "script": "Latn", + "sldr": false, + "tag": "fif", + "tags": [ "fif-Latn", "fif-SA" ], + "windows": "fif-Latn" + }, { "full": "fil-Latn-PH", "iana": [ "Filipino", "Pilipino" ], @@ -35065,11 +36690,11 @@ }, { "full": "fit-Latn-SE", - "iana": [ "Tornedalen Finnish" ], + "iana": [ "Tornedalen Finnish", "Meänkieli" ], "iso639_3": "fit", "localnames": [ "Meänkieli" ], - "name": "Finnish, Tornedalen", - "names": [ "Meänkieli", "North Finnish", "Torne Valley Finnish", "Tornedalen Finnish", "Tornedalsfinska" ], + "name": "Meänkieli", + "names": [ "North Finnish", "Torne Valley Finnish", "Tornedalen Finnish", "Tornedalsfinska" ], "region": "SE", "regionname": "Sweden", "regions": [ "FI" ], @@ -35100,7 +36725,7 @@ "iso639_3": "fij", "localnames": [ "Na Vosa Vakaviti" ], "name": "Fijian", - "names": [ "Boumaa Fijian", "Eastern Fijian", "Fiji", "Na Vosa Vakaviti", "Standard Fijian", "Vakaviti" ], + "names": [ "Boumaa Fijian", "Eastern Fijian", "Fiji", "Na Vosa Vakaviti", "Standard Fijian", "Vakaviti", "iTaukei" ], "region": "FJ", "regionname": "Fiji", "regions": [ "NR", "NZ", "US", "VU" ], @@ -35117,7 +36742,7 @@ "iso639_3": "fkk", "localnames": [ "myá Kákíryà", "myá Kónzə̀l" ], "name": "Kirya-Konzel", - "names": [ "Fali of Kiria", "Fali of Kiriya", "Fali of Kirya", "Fali of Mijilu", "Karya", "Kirya", "Kirya-Konzəl", "Konzal", "myá Kákíryà", "myá Kónzə̀l" ], + "names": [ "Karya", "Kirya", "Kirya-Konzəl", "Konzal", "myá Kákíryà", "myá Kónzə̀l" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -35153,7 +36778,6 @@ "sldr": false, "tag": "fla", "tags": [ "fla-Latn", "fla-US" ], - "unwritten": true, "windows": "fla-Latn" }, { @@ -35168,15 +36792,14 @@ "sldr": false, "tag": "flh", "tags": [ "flh-ID", "flh-Latn" ], - "unwritten": true, "windows": "flh-Latn" }, { "full": "fli-Latn-NG", "iana": [ "Fali" ], "iso639_3": "fli", - "name": "Fali", - "names": [ "Fali of Mubi", "Fali of Muchella", "Vimtim", "Yimtim" ], + "name": "Fali Muchella", + "names": [ "Fali", "Fali of Mubi", "Fali of Muchella", "Vimtim", "Yimtim" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -35251,10 +36874,10 @@ "full": "fmp-Latn-CM", "iana": [ "Fe'fe'" ], "iso639_3": "fmp", - "localname": "Fe'efe'e", - "localnames": [ "Fe'efe'e", "Nufi" ], + "localname": "fèʼéfěʼè", + "localnames": [ "fè'éfě'è", "nùfī" ], "name": "Fe’fe’", - "names": [ "Bafang", "Bamileke-Fe’fe’", "Fe'fe'", "Feefee", "Fefe", "Fe’efe’e", "Fotouni", "Fè-fèè", "Nufi", "fe’fe’" ], + "names": [ "Bafang", "Bamileke-Fe’fe’", "Fe'fe'", "Feefee", "Fefe", "Fe’efe’e", "Fotouni", "Fè-fèè", "Nufi", "fè’éfě’è", "nùfī" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -35275,7 +36898,6 @@ "sldr": false, "tag": "fmu", "tags": [ "fmu-Deva", "fmu-IN" ], - "unwritten": true, "windows": "fmu-Deva" }, { @@ -35340,7 +36962,7 @@ "suppress": true, "tag": "fo", "tags": [ "fo-FO", "fo-Latn" ], - "windows": "fo" + "windows": "fo-FO" }, { "full": "fo-Latn-DK", @@ -35353,9 +36975,22 @@ "regionname": "Denmark", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fo-DK", - "windows": "fo-DK" + "windows": "fo-Latn-DK" + }, + { + "full": "fo-Runr-001", + "iana": [ "Faroese" ], + "iso639_3": "fao", + "name": "Faroese", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "fo-Runr", + "tags": [ "fo-001" ], + "windows": "fo-Runr-001" }, { "full": "fod-Latn-BJ", @@ -35378,6 +37013,7 @@ "full": "foi-Latn-PG", "iana": [ "Foi" ], "iso639_3": "foi", + "localnames": [ "Foe" ], "name": "Foi", "names": [ "Foe", "Mubi River" ], "region": "PG", @@ -35407,7 +37043,7 @@ "full": "fon-Latn-BJ", "iana": [ "Fon" ], "iso639_3": "fon", - "localname": "Fongbe", + "localname": "Fɔngbè", "localnames": [ "Fɔngbè" ], "name": "Fon", "names": [ "Dahomeen", "Djedji", "Fo", "Fogbe", "Fon-Gbe", "Fongbe", "Fonnu", "Fɔngbè" ], @@ -35468,8 +37104,8 @@ "full": "fqs-Latn-PG", "iana": [ "Fas" ], "iso639_3": "fqs", - "name": "Fas", - "names": [ "Bembi", "Momu" ], + "name": "Momu", + "names": [ "Bembi", "Fas" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -35488,14 +37124,14 @@ "names": [ "Barang", "Francese", "Francès", "Francés", "Frans", "Frantsay", "Französisch", "Français", "Inglês", "français" ], "region": "FR", "regionname": "France", - "regions": [ "AD", "AM", "AT", "AU", "BF", "BG", "BJ", "BL", "CF", "CG", "CV", "DE", "DK", "DM", "DO", "EE", "EG", "ES", "FI", "GA", "GG", "GP", "GQ", "GR", "GW", "HR", "HU", "IE", "IL", "IN", "IT", "JE", "KH", "LA", "LB", "LC", "LT", "LV", "LY", "MC", "MD", "ME", "MF", "MK", "MQ", "MT", "NC", "NE", "NL", "NO", "NZ", "PF", "PL", "PM", "PR", "PT", "QA", "RO", "RS", "SE", "SI", "SK", "ST", "TG", "US", "VI", "WF", "YT" ], + "regions": [ "AD", "AE", "AL", "AM", "AT", "AU", "BA", "BF", "BG", "BJ", "BL", "CF", "CG", "CV", "CZ", "DE", "DK", "DM", "DO", "EE", "EG", "ES", "FI", "GA", "GE", "GG", "GH", "GP", "GQ", "GR", "GW", "HR", "HU", "IE", "IL", "IN", "IT", "JE", "KH", "LA", "LB", "LC", "LT", "LV", "LY", "MC", "MD", "ME", "MF", "MK", "MQ", "MT", "MX", "MZ", "NC", "NE", "NL", "NO", "NZ", "PF", "PL", "PM", "PR", "PT", "QA", "RO", "RS", "RU", "SE", "SI", "SK", "ST", "TG", "UA", "US", "VI", "WF", "YT" ], "script": "Latn", "sldr": true, "suppress": true, "tag": "fr", "tags": [ "fr-FR", "fr-Latn" ], "variants": [ "1694acad" ], - "windows": "fr" + "windows": "fr-FR" }, { "full": "fr-Latn-BE", @@ -35508,9 +37144,8 @@ "regionname": "Belgium", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-BE", - "windows": "fr-BE" + "windows": "fr-Latn-BE" }, { "full": "fr-Latn-BI", @@ -35523,9 +37158,8 @@ "regionname": "Burundi", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-BI", - "windows": "fr-BI" + "windows": "fr-Latn-BI" }, { "full": "fr-Brai-FR", @@ -35540,7 +37174,7 @@ "script": "Brai", "sldr": false, "tag": "fr-Brai", - "windows": "fr-Brai" + "windows": "fr-Brai-FR" }, { "full": "fr-Latn-CA", @@ -35553,9 +37187,8 @@ "regionname": "Canada", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-CA", - "windows": "fr-CA" + "windows": "fr-Latn-CA" }, { "full": "fr-Latn-CD", @@ -35568,9 +37201,8 @@ "regionname": "The Democratic Republic of the Congo", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-CD", - "windows": "fr-CD" + "windows": "fr-Latn-CD" }, { "full": "fr-Latn-CH", @@ -35583,9 +37215,8 @@ "regionname": "Switzerland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-CH", - "windows": "fr-CH" + "windows": "fr-Latn-CH" }, { "full": "fr-Latn-CI", @@ -35598,9 +37229,8 @@ "regionname": "Côte d'Ivoire", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-CI", - "windows": "fr-CI" + "windows": "fr-Latn-CI" }, { "full": "fr-Latn-CM", @@ -35613,9 +37243,8 @@ "regionname": "Cameroon", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-CM", - "windows": "fr-CM" + "windows": "fr-Latn-CM" }, { "full": "fr-Latn-DJ", @@ -35628,9 +37257,8 @@ "regionname": "Djibouti", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-DJ", - "windows": "fr-DJ" + "windows": "fr-Latn-DJ" }, { "full": "fr-Latn-DZ", @@ -35643,9 +37271,8 @@ "regionname": "Algeria", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-DZ", - "windows": "fr-DZ" + "windows": "fr-Latn-DZ" }, { "full": "fr-Dupl-FR", @@ -35660,7 +37287,7 @@ "script": "Dupl", "sldr": false, "tag": "fr-Dupl", - "windows": "fr-Dupl" + "windows": "fr-Dupl-FR" }, { "full": "fr-Latn-GF", @@ -35673,9 +37300,8 @@ "regionname": "French Guiana", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-GF", - "windows": "fr-GF" + "windows": "fr-Latn-GF" }, { "full": "fr-Latn-GN", @@ -35688,9 +37314,8 @@ "regionname": "Guinea", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-GN", - "windows": "fr-GN" + "windows": "fr-Latn-GN" }, { "full": "fr-Latn-HT", @@ -35703,9 +37328,8 @@ "regionname": "Haiti", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-HT", - "windows": "fr-HT" + "windows": "fr-Latn-HT" }, { "full": "fr-Latn-KM", @@ -35718,9 +37342,8 @@ "regionname": "Comoros", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-KM", - "windows": "fr-KM" + "windows": "fr-Latn-KM" }, { "full": "fr-Latn-LU", @@ -35733,9 +37356,8 @@ "regionname": "Luxembourg", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-LU", - "windows": "fr-LU" + "windows": "fr-Latn-LU" }, { "full": "fr-Latn-MA", @@ -35748,9 +37370,8 @@ "regionname": "Morocco", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-MA", - "windows": "fr-MA" + "windows": "fr-Latn-MA" }, { "full": "fr-Latn-MG", @@ -35763,9 +37384,8 @@ "regionname": "Madagascar", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-MG", - "windows": "fr-MG" + "windows": "fr-Latn-MG" }, { "full": "fr-Latn-ML", @@ -35778,9 +37398,8 @@ "regionname": "Mali", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-ML", - "windows": "fr-ML" + "windows": "fr-Latn-ML" }, { "full": "fr-Latn-MR", @@ -35793,9 +37412,8 @@ "regionname": "Mauritania", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-MR", - "windows": "fr-MR" + "windows": "fr-Latn-MR" }, { "full": "fr-Latn-MU", @@ -35808,9 +37426,8 @@ "regionname": "Mauritius", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-MU", - "windows": "fr-MU" + "windows": "fr-Latn-MU" }, { "full": "fr-Latn-RE", @@ -35823,9 +37440,8 @@ "regionname": "Réunion", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-RE", - "windows": "fr-RE" + "windows": "fr-Latn-RE" }, { "full": "fr-Latn-RW", @@ -35838,9 +37454,8 @@ "regionname": "Rwanda", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-RW", - "windows": "fr-RW" + "windows": "fr-Latn-RW" }, { "full": "fr-Latn-SC", @@ -35853,9 +37468,8 @@ "regionname": "Seychelles", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-SC", - "windows": "fr-SC" + "windows": "fr-Latn-SC" }, { "full": "fr-Latn-SN", @@ -35868,9 +37482,8 @@ "regionname": "Senegal", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-SN", - "windows": "fr-SN" + "windows": "fr-Latn-SN" }, { "full": "fr-Latn-SY", @@ -35883,9 +37496,8 @@ "regionname": "Syrian Arab Republic", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-SY", - "windows": "fr-SY" + "windows": "fr-Latn-SY" }, { "full": "fr-Latn-TD", @@ -35898,9 +37510,8 @@ "regionname": "Chad", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-TD", - "windows": "fr-TD" + "windows": "fr-Latn-TD" }, { "full": "fr-Latn-TN", @@ -35913,9 +37524,8 @@ "regionname": "Tunisia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-TN", - "windows": "fr-TN" + "windows": "fr-Latn-TN" }, { "full": "fr-Latn-VU", @@ -35928,9 +37538,8 @@ "regionname": "Vanuatu", "script": "Latn", "sldr": true, - "suppress": true, "tag": "fr-VU", - "windows": "fr-VU" + "windows": "fr-Latn-VU" }, { "full": "frc-Latn-US", @@ -35951,6 +37560,7 @@ "full": "frd-Latn-ID", "iana": [ "Fordata" ], "iso639_3": "frd", + "localnames": [ "Vaidida" ], "name": "Fordata", "names": [ "Larat", "Vai Fordata", "Vai Sera-Larat-Fordata", "Vai Tnebar", "Vaidida" ], "region": "ID", @@ -36023,27 +37633,29 @@ "full": "frq-Latn-PG", "iana": [ "Forak" ], "iso639_3": "frq", + "localnames": [ "Forak" ], "name": "Forak", + "names": [ "Mamgak", "Porak" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "frq", "tags": [ "frq-Latn", "frq-PG" ], - "unwritten": true, "windows": "frq-Latn" }, { "full": "frr-Latn-DE", "iana": [ "Northern Frisian" ], "iso639_3": "frr", + "localname": "Nordfriisk", "localnames": [ "Friisk" ], "name": "Frisian, Northern", "names": [ "Frasch", "Freesch", "Freesk", "Friisk", "Fräisch", "Nordfriesisch", "Northern Frisian" ], "region": "DE", "regionname": "Germany", "script": "Latn", - "sldr": false, + "sldr": true, "suppress": true, "tag": "frr", "tags": [ "frr-DE", "frr-Latn" ], @@ -36128,6 +37740,10 @@ "full": "fub-Arab-CM", "iana": [ "Adamawa Fulfulde" ], "iso639_3": "fub", + "latnnames": [ "Fulfulde" ], + "localname": "ڢُلْڢُلْدٜ", + "localnames": [ "ڢُلْڢُلْدٜ" ], + "macrolang": "ff", "name": "Fulfulde, Adamawa", "names": [ "Adamawa Fulani", "Adamawa Fulfulde", "Biira", "Boulbe", "Domona", "Dzemay", "Eastern Fulani", "Eastern Fulfulde", "Fellata", "Fillanci", "Foulfoulde", "Ful", "Fula", "Fula Fulbe", "Fulani", "Fulata", "Fulatanchi", "Fulbe", "Fulfulde", "Mbororo", "Nagapelta", "Palata", "Paldena", "Paldida", "Pelta Hay", "Peul", "Peulh", "Pladina", "Pule", "Pullo", "Sanyo", "Taareyo", "Zaakosa", "Zemay" ], "region": "CM", @@ -36143,7 +37759,9 @@ "full": "fub-Latn-CM", "iana": [ "Adamawa Fulfulde" ], "iso639_3": "fub", + "localname": "Fulfulde", "localnames": [ "Fulfulde" ], + "macrolang": "ff", "name": "Fulfulde, Adamawa", "names": [ "Adamawa Fulani", "Adamawa Fulfulde", "Biira", "Boulbe", "Domona", "Dzemay", "Eastern Fulani", "Eastern Fulfulde", "Fellata", "Fillanci", "Foulfoulde", "Ful", "Fula", "Fula Fulbe", "Fulani", "Fulata", "Fulatanchi", "Fulbe", "Fulfulde", "Mbororo", "Nagapelta", "Palata", "Paldena", "Paldida", "Pelta Hay", "Peul", "Peulh", "Pladina", "Pule", "Pullo", "Sanyo", "Taareyo", "Zaakosa", "Zemay" ], "region": "CM", @@ -36174,7 +37792,9 @@ "full": "fue-Latn-BJ", "iana": [ "Borgu Fulfulde" ], "iso639_3": "fue", + "localname": "Fulfulde", "localnames": [ "Fulfulde" ], + "macrolang": "ff", "name": "Fulfulde, Borgu", "names": [ "Benin-Togo Fulfulde", "Borgu Fulfulde", "Fulani", "Fulbe-Borgu", "Fulfulde", "Fulfulde, Benin-Togo", "Peul", "Peulh" ], "region": "BJ", @@ -36192,6 +37812,7 @@ "iso639_3": "fuf", "localname": "Pular", "localnames": [ "Pular" ], + "macrolang": "ff", "name": "Pular", "names": [ "Foula Fouta", "Fouta Djallon", "Fouta Dyalon", "Fula", "Fulbe", "Fulfulde Jalon", "Fullo Fuuta", "Futa", "Futa Fula", "Futa Jallon", "Fuuta Jalon", "Jalon", "Poular" ], "region": "GN", @@ -36207,6 +37828,7 @@ "full": "fuf-Adlm-GN", "iana": [ "Pular" ], "iso639_3": "fuf", + "macrolang": "ff", "name": "Pular", "names": [ "Foula Fouta", "Fouta Djallon", "Fouta Dyalon", "Fula", "Fulbe", "Fulfulde Jalon", "Fullo Fuuta", "Futa", "Futa Fula", "Futa Jallon", "Fuuta Jalon", "Jalon", "Poular" ], "nophonvars": true, @@ -36223,6 +37845,7 @@ "iana": [ "Pular" ], "iso639_3": "fuf", "localname": "Pular", + "macrolang": "ff", "name": "Pular", "names": [ "Foula Fouta", "Fouta Djallon", "Fouta Dyalon", "Fula", "Fulbe", "Fulfulde Jalon", "Fullo Fuuta", "Futa", "Futa Fula", "Futa Jallon", "Fuuta Jalon", "Jalon", "Poular" ], "nophonvars": true, @@ -36234,10 +37857,31 @@ "tag": "fuf-Arab", "windows": "fuf-Arab" }, + { + "full": "fuh-Latn-NE", + "iana": [ "Western Niger Fulfulde" ], + "iso639_3": "fuh", + "localname": "Fulfulde", + "localnames": [ "Fulfulde" ], + "macrolang": "ff", + "name": "Fulfulde, Western Niger", + "names": [ "Fula", "Fulani", "Fulbe", "Fulfulde", "Fulfulde, Gorgal", "Fulfulde, Northeastern Burkina Faso", "Gorgal Fulfulde", "Northeastern Burkina Faso Fulfulde", "Peul", "Peulh", "Western Niger Fulfulde" ], + "region": "NE", + "regionname": "Niger", + "regions": [ "BJ" ], + "script": "Latn", + "sldr": true, + "tag": "fuh", + "tags": [ "fuh-Latn", "fuh-NE" ], + "windows": "fuh-Latn-NE" + }, { "full": "fuh-Arab-NE", "iana": [ "Western Niger Fulfulde" ], "iso639_3": "fuh", + "latnnames": [ "Fulfulde" ], + "localnames": [ "ڢُلْڢُلْدٜ" ], + "macrolang": "ff", "name": "Fulfulde, Western Niger", "names": [ "Fula", "Fulani", "Fulbe", "Fulfulde", "Fulfulde, Gorgal", "Fulfulde, Northeastern Burkina Faso", "Gorgal Fulfulde", "Northeastern Burkina Faso Fulfulde", "Peul", "Peulh", "Western Niger Fulfulde" ], "nophonvars": true, @@ -36247,13 +37891,14 @@ "script": "Arab", "sldr": false, "tag": "fuh-Arab", - "windows": "fuh-Arab" + "windows": "fuh-Arab-NE" }, { "full": "fuh-Latn-BF", "iana": [ "Western Niger Fulfulde" ], "iso639_3": "fuh", "localnames": [ "Fulfulde" ], + "macrolang": "ff", "name": "Fulfulde, Northeastern Burkina Faso", "names": [ "Fula", "Fulani", "Fulbe", "Fulfulde", "Fulfulde, Gorgal", "Fulfulde, Western Niger", "Gorgal Fulfulde", "Northeastern Burkina Faso Fulfulde", "Peul", "Peulh", "Western Niger Fulfulde" ], "region": "BF", @@ -36264,25 +37909,11 @@ "tag": "fuh-BF", "windows": "fuh-Latn-BF" }, - { - "full": "fuh-Latn-NE", - "iana": [ "Western Niger Fulfulde" ], - "iso639_3": "fuh", - "localnames": [ "Fulfulde" ], - "name": "Fulfulde, Western Niger", - "names": [ "Fula", "Fulani", "Fulbe", "Fulfulde", "Fulfulde, Gorgal", "Fulfulde, Northeastern Burkina Faso", "Gorgal Fulfulde", "Northeastern Burkina Faso Fulfulde", "Peul", "Peulh", "Western Niger Fulfulde" ], - "region": "NE", - "regionname": "Niger", - "regions": [ "BJ" ], - "script": "Latn", - "sldr": true, - "tag": "fuh-Latn", - "windows": "fuh-Latn" - }, { "full": "fui-Latn-TD", "iana": [ "Bagirmi Fulfulde" ], "iso639_3": "fui", + "macrolang": "ff", "name": "Fulfulde, Bagirmi", "names": [ "Baghirmi Peul", "Bagirmi Fula", "Bagirmi Fulfulde" ], "region": "TD", @@ -36343,8 +37974,9 @@ "full": "fuq-Latn-NE", "iana": [ "Central-Eastern Niger Fulfulde" ], "iso639_3": "fuq", - "localname": "Fulfulde, Central-Eastern Niger", + "localname": "Fulfulde", "localnames": [ "Fulfulde" ], + "macrolang": "ff", "name": "Fulfulde, Central-Eastern Niger", "names": [ "Central-Eastern Niger Fulfulde", "Fula", "Fulani", "Fulbe", "Fulfulde", "Peul", "Peulh" ], "region": "NE", @@ -36376,7 +38008,7 @@ "iana": [ "Futuna-Aniwa" ], "iso639_3": "fut", "name": "Futuna-Aniwa", - "names": [ "Erronan", "West Futuna-Aniwa" ], + "names": [ "Erronan", "Fotuna-Anewa", "West Futuna-Aniwa", "West Futunan" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -36390,7 +38022,7 @@ "iana": [ "Furu" ], "iso639_3": "fuu", "name": "Furu", - "names": [ "Bagero", "Bagiro", "Baguero", "Baguiro" ], + "names": [ "Bagero", "Bagiro", "Baguero", "Baguiro", "Fulu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "CF" ], @@ -36407,8 +38039,9 @@ "iso639_3": "fuv", "localname": "Fulfulde", "localnames": [ "Fulfulde" ], + "macrolang": "ff", "name": "Fulfulde, Nigerian", - "names": [ "Fulbe", "Fulfulde", "Fulfulde, Mbororo", "Hausa States Fulfulde", "Nigerian Fulfulde", "Peul" ], + "names": [ "Fulani", "Fulatanci", "Fulbe", "Fulfulde", "Fulfulde, Mbororo", "Fulɓe", "Hausa States Fulfulde", "Nigerian Fulfulde", "Peul" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM", "TD" ], @@ -36422,9 +38055,12 @@ "full": "fuv-Arab-NG", "iana": [ "Nigerian Fulfulde" ], "iso639_3": "fuv", + "latnnames": [ "fulْfulْdٜ" ], "localname": "Fulfulde Caka Nigeria", + "localnames": [ "فُلْفُلْدٜ" ], + "macrolang": "ff", "name": "Fulfulde, Nigerian", - "names": [ "Fulbe", "Fulfulde", "Fulfulde, Mbororo", "Hausa States Fulfulde", "Nigerian Fulfulde", "Peul" ], + "names": [ "Fulani", "Fulatanci", "Fulbe", "Fulfulde", "Fulfulde, Mbororo", "Fulɓe", "Hausa States Fulfulde", "Nigerian Fulfulde", "Peul" ], "nophonvars": true, "region": "NG", "regionname": "Nigeria", @@ -36438,6 +38074,7 @@ "full": "fuy-Latn-PG", "iana": [ "Fuyug" ], "iso639_3": "fuy", + "localnames": [ "Fuyug" ], "name": "Fuyug", "names": [ "Fujuge", "Fuyuge", "Fuyughe", "Mafufu", "Mafulu" ], "region": "PG", @@ -36483,7 +38120,9 @@ "full": "fwe-Latn-NA", "iana": [ "Fwe" ], "iso639_3": "fwe", + "localnames": [ "Cìfwè" ], "name": "Fwe", + "names": [ "Cìfwè", "Mafwe", "Sifwe" ], "region": "NA", "regionname": "Namibia", "regions": [ "ZM" ], @@ -36554,7 +38193,7 @@ "obsolete": true, "region": "IE", "regionname": "Ireland", - "regions": [ "GB" ], + "regions": [ "001", "GB" ], "script": "Ogam", "sldr": false, "tag": "ga-Ogam", @@ -36606,19 +38245,34 @@ "windows": "gab-Latn" }, { - "full": "gac-Zyyy-IN", + "full": "gac-Latn-IN", "iana": [ "Mixed Great Andamanese" ], "iso639_3": "gac", "name": "Great Andamanese, Mixed", "names": [ "Andamese", "Jeru", "Mixed Great Andamanese" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "gac", - "tags": [ "gac-IN", "gac-Zyyy" ], + "tags": [ "gac-IN", "gac-Latn" ], + "unwritten": true, + "windows": "gac-Latn" + }, + { + "full": "gac-Deva-IN", + "iana": [ "Mixed Great Andamanese" ], + "iso639_3": "gac", + "name": "Great Andamanese, Mixed", + "names": [ "Andamese", "Jeru", "Mixed Great Andamanese" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "gac-Deva", "unwritten": true, - "windows": "gac-Zyyy" + "windows": "gac-Deva" }, { "full": "gad-Latn-PH", @@ -36716,8 +38370,9 @@ "full": "gah-Latn-PG", "iana": [ "Alekano" ], "iso639_3": "gah", + "localnames": [ "Alekano" ], "name": "Alekano", - "names": [ "Gafuku", "Gahuku", "Gahuku-Asaro", "Gahuku-Gama" ], + "names": [ "Gafuku", "Gahuku", "Gahuku-Asaro", "Gahuku-Gama", "Gahukuq" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -36746,7 +38401,9 @@ "full": "gaj-Latn-PG", "iana": [ "Gadsup" ], "iso639_3": "gaj", + "localnames": [ "Kasufu wayani" ], "name": "Gadsup", + "names": [ "Gadzub", "Kasufu wayani" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -36802,6 +38459,7 @@ "full": "gan-Hans-CN", "iana": [ "Gan Chinese" ], "iso639_3": "gan", + "macrolang": "zh", "name": "Chinese, Gan", "names": [ "Gan Chinese", "Jiangxi", "Jiangxi hua", "Jiangxinese", "Kan", "Xi" ], "region": "CN", @@ -36816,6 +38474,7 @@ "full": "gan-Hant-CN", "iana": [ "Gan Chinese" ], "iso639_3": "gan", + "macrolang": "zh", "name": "Chinese, Gan", "names": [ "Gan Chinese", "Jiangxi", "Jiangxi hua", "Jiangxinese", "Kan", "Xi" ], "nophonvars": true, @@ -36831,6 +38490,7 @@ "iana": [ "Gan Chinese" ], "iso639_3": "gan", "localnames": [ "Jiangxi hua" ], + "macrolang": "zh", "name": "Chinese, Gan", "names": [ "Gan Chinese", "Jiangxi", "Jiangxi hua", "Jiangxinese", "Kan", "Xi" ], "obsolete": true, @@ -36904,8 +38564,10 @@ "full": "gas-Gujr-IN", "iana": [ "Adiwasi Garasia" ], "iso639_3": "gas", + "latnnames": [ "Garāsiyā", "Ādivāsī Garāsiyā" ], + "localnames": [ "ગરાસિયા", "ગરાસિયા આદિવાસી" ], "name": "Garasia, Adiwasi", - "names": [ "Adiwasi Garasia", "Adiwasi Girasia", "Adiwasi Gujarati", "Girasia" ], + "names": [ "Adiwasi Garasia", "Adiwasi Girasia", "Adiwasi Gujarati", "Garasia", "Girasia" ], "region": "IN", "regionname": "India", "script": "Gujr", @@ -36918,8 +38580,9 @@ "full": "gat-Latn-PG", "iana": [ "Kenati" ], "iso639_3": "gat", - "name": "Kenati", - "names": [ "Aziana", "Ganati", "Kenathi" ], + "localnames": [ "Aziana" ], + "name": "Aziana", + "names": [ "Ganati", "Kenathi", "Kenati" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -36949,7 +38612,7 @@ "localname": "Nobonob", "localnames": [ "Nobonob" ], "name": "Nobonob", - "names": [ "Butelkud-Guntabak", "Garuh", "Nobanob", "Nobnob", "Nupanob" ], + "names": [ "Butelkud-Guntabak", "Garuh", "Halopa", "Nobanob", "Nobnob", "Nupanob" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -36963,6 +38626,7 @@ "iana": [ "Borana-Arsi-Guji Oromo" ], "iso639_3": "gax", "localnames": [ "Afaan Oromoo" ], + "macrolang": "om", "name": "Oromo, Borana-Arsi-Guji", "names": [ "Afaani Borana", "Booran", "Boraan", "Boraana", "Boran", "Borana-Arsi-Guji Oromo", "Oromo" ], "region": "ET", @@ -36978,6 +38642,7 @@ "full": "gax-Ethi-ET", "iana": [ "Borana-Arsi-Guji Oromo" ], "iso639_3": "gax", + "macrolang": "om", "name": "Oromo, Borana-Arsi-Guji", "names": [ "Afaani Borana", "Booran", "Boraan", "Boraana", "Boran", "Borana-Arsi-Guji Oromo", "Oromo" ], "nophonvars": true, @@ -36994,6 +38659,7 @@ "full": "gax-Ethi-ET-x-borana", "iana": [ "Oromo, Borana-Arsi-Guji - Borana" ], "iso639_3": "gax", + "macrolang": "om", "name": "Oromo, Borana-Arsi-Guji", "names": [ "Oromo, Borana-Arsi-Guji - Borana" ], "nophonvars": true, @@ -37010,6 +38676,7 @@ "full": "gax-Ethi-ET-x-guji", "iana": [ "Oromo, Borana-Arsi-Guji - Guji" ], "iso639_3": "gax", + "macrolang": "om", "name": "Oromo, Borana-Arsi-Guji", "names": [ "Oromo, Borana-Arsi-Guji - Guji" ], "nophonvars": true, @@ -37040,9 +38707,9 @@ "full": "gba-Latn-CF", "iana": [ "Gbaya (Central African Republic)" ], "iso639_3": "gba", - "localnames": [ "Nu Gbaya" ], - "name": "Gbaya, Northwest", - "names": [ "Baya", "Gbaya", "Gbaya (Central African Republic)", "Gbaya Nord-Ouest", "Northwest Gbaya" ], + "localname": "Nu Gbaya", + "name": "Gbaya (Central African Republic)", + "names": [ "Baya", "Gbaya", "Gbaya Nord-Ouest", "Gbaya, Northwest", "Northwest Gbaya" ], "region": "CF", "regionname": "Central African Republic", "regions": [ "CG", "CM", "NG" ], @@ -37177,7 +38844,7 @@ "iana": [ "Bodo Gadaba" ], "iso639_3": "gbj", "name": "Gadaba, Bodo", - "names": [ "Bodo Gadaba", "Boi Gadaba", "Gadba", "Gadwa", "Godwa", "Gudwa", "Gutob", "Gutop" ], + "names": [ "Bodo Gadaba", "Boi Gadaba", "Gadba", "Gadwa", "Godwa", "Gudwa", "Gutab", "Gutob", "Gutob-Gadaba", "Gutop" ], "region": "IN", "regionname": "India", "script": "Orya", @@ -37190,6 +38857,8 @@ "full": "gbk-Deva-IN", "iana": [ "Gaddi" ], "iso639_3": "gbk", + "latnnames": [ "Gadi" ], + "localnames": [ "गदी" ], "name": "Gaddi", "names": [ "Bharmauri Bhadi", "Gaddyali", "Gadi", "Gadiali", "Pahari Bharmauri", "Panchi Brahmauri Rajput" ], "region": "IN", @@ -37281,6 +38950,7 @@ "iana": [ "Gbaya-Bossangoa" ], "iso639_3": "gbp", "localnames": [ "Gbɛya" ], + "macrolang": "gba", "name": "Gbaya-Bossangoa", "names": [ "Gbaya", "Gbaya de Bosangoa", "Gbaya of Borro", "Gbaya of Bossangoa", "Gbaya-Borro", "Gbea", "Gbeya", "Gbäyä Bossangoa", "Gbɛa", "Gbɛya", "Gbɛ́yá" ], "region": "CF", @@ -37295,8 +38965,9 @@ "full": "gbq-Latn-CF", "iana": [ "Gbaya-Bozoum" ], "iso639_3": "gbq", + "macrolang": "gba", "name": "Gbaya-Bozoum", - "names": [ "Bozom", "Gbaya de Bozoum" ], + "names": [ "Bozom", "Gbaya de Bozoum", "Ɓòzôm" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -37400,6 +39071,8 @@ "full": "gby-Latn-NG", "iana": [ "Gbari" ], "iso639_3": "gby", + "localname": "Gbari", + "localnames": [ "Gbari" ], "name": "Gbari", "names": [ "Gbari Yamma", "Gwari Yamma", "Nkwa", "West Gwari" ], "region": "NG", @@ -37429,6 +39102,7 @@ "full": "gcc-Latn-PG", "iana": [ "Mali" ], "iso639_3": "gcc", + "localnames": [ "Mali", "Mali-Baining" ], "name": "Mali", "names": [ "Baining", "Gaktai", "Mali-Baining", "Neu-Pommern" ], "region": "PG", @@ -37472,11 +39146,11 @@ "full": "gcf-Latn-GP", "iana": [ "Guadeloupean Creole French" ], "iso639_3": "gcf", - "name": "Guadeloupean Creole French", - "names": [ "Guadeloupean Creole", "Kreyol", "Martinican Creole", "Martiniquan Creole French", "Patois", "Patwa", "Saint Barthélemy Creole French" ], + "name": "Guadeloupean French Creole", + "names": [ "Guadeloupean Creole", "Guadeloupean Creole French", "Kreyol", "Patois", "Patwa", "Saint Barthélemy French Creole" ], "region": "GP", "regionname": "Guadeloupe", - "regions": [ "BL", "MQ" ], + "regions": [ "BL", "MF", "MQ" ], "script": "Latn", "sldr": false, "tag": "gcf", @@ -37488,7 +39162,8 @@ "full": "gcl-Latn-GD", "iana": [ "Grenadian Creole English" ], "iso639_3": "gcl", - "name": "Grenadian Creole English", + "name": "Grenadian English Creole", + "names": [ "Grenadian Creole English" ], "region": "GD", "regionname": "Grenada", "script": "Latn", @@ -37515,8 +39190,8 @@ "full": "gcr-Latn-GF", "iana": [ "Guianese Creole French" ], "iso639_3": "gcr", - "name": "Guianese Creole French", - "names": [ "French Guianese Creole French", "Guyanais", "Guyane", "Guyane Creole", "Patois", "Patwa" ], + "name": "Guianese French Creole", + "names": [ "French Guianese French Creole", "Guianese Creole French", "Guyanais", "Guyane", "Guyane Creole", "Patois", "Patwa" ], "region": "GF", "regionname": "French Guiana", "script": "Latn", @@ -37554,12 +39229,28 @@ "sldr": true, "tag": "gd", "tags": [ "gd-GB", "gd-Latn" ], - "windows": "gd-Latn" + "windows": "gd-Latn-GB" + }, + { + "full": "gd-Ogam-001", + "iana": [ "Scottish Gaelic", "Gaelic" ], + "iso639_3": "gla", + "name": "Scottish Gaelic", + "names": [ "Gaelic", "Gàidhlig", "Gàidhlig Albannach", "Gàidhlig na h-Alba", "Scots Gaelic" ], + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Ogam", + "sldr": false, + "tag": "gd-Ogam", + "tags": [ "gd-001" ], + "windows": "gd-Ogam-001" }, { "full": "gda-Zyyy-IN", "iana": [ "Gade Lohar" ], "iso639_3": "gda", + "macrolang": "raj", "name": "Lohar, Gade", "names": [ "Bagri Lohar", "Belani", "Bhubaliya Lohar", "Chitodi Lohar", "Chittoriya Lohar", "Dhunkuria", "Domba", "Dombiali", "Gade Lohar", "Gadia Lohar", "Gaduliya Lohar", "Gara", "Kanwar Khati", "Lohari", "Lohpitta", "Panchal Lohar", "Rajput Lohar" ], "region": "IN", @@ -37665,7 +39356,7 @@ "full": "gdg-Latn-PH", "iana": [ "Ga'dang" ], "iso639_3": "gdg", - "localname": "Ga’dang", + "localname": "Gaꞌdang", "localnames": [ "Ga'dang" ], "name": "Ga’dang", "names": [ "Baliwon", "Ga'dang", "Gaddang", "Ginabwal" ], @@ -37679,7 +39370,7 @@ }, { "full": "gdh-Latn-AU", - "iana": [ "Gadjerawang" ], + "iana": [ "Gadjerawang", "Gajirrabeng" ], "iso639_3": "gdh", "name": "Gadjerawang", "names": [ "Gadjerong", "Gadjerrong", "Gadyerong", "Gajerrong", "Gajirrabeng", "Gajirrawoong", "Kadjerawang", "Kadjeroen", "Kadjerong", "Kadpjeroen", "Kajirrawong", "Kajirrawung" ], @@ -37832,7 +39523,7 @@ "localname": "Wipi", "localnames": [ "Wipi" ], "name": "Wipi", - "names": [ "Gidra", "Jibu", "Oriomo" ], + "names": [ "Jibu", "Oriomo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -37885,18 +39576,18 @@ "windows": "gdu-Latn" }, { - "full": "gdx-Zyyy-IN", + "full": "gdx-Deva-IN", "iana": [ "Godwari" ], "iso639_3": "gdx", "name": "Godwari", "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "gdx", - "tags": [ "gdx-IN", "gdx-Zyyy" ], + "tags": [ "gdx-Deva", "gdx-IN" ], "unwritten": true, - "windows": "gdx-Zyyy" + "windows": "gdx-Deva" }, { "full": "gea-Latn-NG", @@ -37932,6 +39623,7 @@ "full": "gec-Latn-LR", "iana": [ "Gboloo Grebo" ], "iso639_3": "gec", + "macrolang": "grb", "name": "Grebo, Gboloo", "names": [ "Gblou Grebo", "Gboloo", "Gboloo Grebo" ], "region": "LR", @@ -37960,6 +39652,7 @@ }, { "full": "gef-Latn-ID", + "iana": [ "Gerai" ], "iso639_3": "gef", "localnames": [ "Gorai" ], "name": "Gerai", @@ -38115,7 +39808,7 @@ "iana": [ "Gera" ], "iso639_3": "gew", "name": "Gera", - "names": [ "Fyandigere", "Fyandigeri", "Gerawa", "Rawam" ], + "names": [ "Fyandigere", "Fyandigeri", "Geranci", "Gerawa", "Gere", "Rawam" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -38126,18 +39819,19 @@ "windows": "gew-Latn" }, { - "full": "gex-Zyyy-SO", + "full": "gex-Latn-SO", "iana": [ "Garre" ], "iso639_3": "gex", "name": "Garre", - "names": [ "Af-Garre", "Karre" ], + "names": [ "Af-Garre", "Garreh", "Gurreh", "Karre" ], "region": "SO", "regionname": "Somalia", - "script": "Zyyy", + "regions": [ "KE" ], + "script": "Latn", "sldr": false, "tag": "gex", - "tags": [ "gex-SO", "gex-Zyyy" ], - "windows": "gex-Zyyy" + "tags": [ "gex-Latn", "gex-SO" ], + "windows": "gex-Latn" }, { "full": "gey-Latn-CD", @@ -38159,7 +39853,7 @@ "iana": [ "Geez" ], "iso639_3": "gez", "latnnames": [ "Gəʿəz" ], - "localname": "ግዕዝኛ", + "localname": "ግዕዝ", "localnames": [ "ግዕዝ" ], "name": "Geez", "region": "ET", @@ -38347,7 +40041,7 @@ "windows": "ggw-Latn" }, { - "full": "gha-Zyyy-LY", + "full": "gha-Arab-LY", "iana": [ "Ghadamès" ], "iso639_3": "gha", "name": "Ghadamès", @@ -38355,12 +40049,41 @@ "region": "LY", "regionname": "Libya", "regions": [ "TN" ], - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "gha", - "tags": [ "gha-LY", "gha-Zyyy" ], + "tags": [ "gha-Arab", "gha-LY" ], "unwritten": true, - "windows": "gha-Zyyy" + "windows": "gha-Arab" + }, + { + "full": "gha-Latn-LY", + "iana": [ "Ghadamès" ], + "iso639_3": "gha", + "name": "Ghadamès", + "names": [ "Berbère", "Ghadames", "Ghadāmis" ], + "region": "LY", + "regionname": "Libya", + "regions": [ "TN" ], + "script": "Latn", + "sldr": false, + "tag": "gha-Latn", + "windows": "gha-Latn" + }, + { + "full": "gha-Tfng-LY", + "iana": [ "Ghadamès" ], + "iso639_3": "gha", + "name": "Ghadamès", + "names": [ "Berbère", "Ghadames", "Ghadāmis" ], + "nophonvars": true, + "region": "LY", + "regionname": "Libya", + "regions": [ "TN" ], + "script": "Tfng", + "sldr": false, + "tag": "gha-Tfng", + "windows": "gha-Tfng" }, { "full": "ghc-Latn-GB", @@ -38613,6 +40336,20 @@ "unwritten": true, "windows": "gih-Latn" }, + { + "full": "gii-Zyyy-ET", + "iana": [ "Girirra" ], + "iso639_3": "gii", + "name": "Girirra", + "region": "ET", + "regionname": "Ethiopia", + "script": "Zyyy", + "sldr": false, + "tag": "gii", + "tags": [ "gii-ET", "gii-Zyyy" ], + "unwritten": true, + "windows": "gii-Zyyy" + }, { "full": "gil-Latn-KI", "iana": [ "Gilbertese" ], @@ -38664,6 +40401,7 @@ "full": "gip-Latn-PG", "iana": [ "Gimi (West New Britain)" ], "iso639_3": "gip", + "localnames": [ "Gimi" ], "name": "Gimi", "names": [ "Gimi (West New Britain)", "Loko" ], "region": "PG", @@ -38810,20 +40548,6 @@ "tags": [ "giz-CM", "giz-Latn" ], "windows": "giz-Latn" }, - { - "full": "gji-Latn-NG", - "iana": [ "Geji" ], - "iso639_3": "gji", - "name": "Geji", - "names": [ "Gejawa", "Gezawa", "Kaiyorawa", "Kayauri" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "gji", - "tags": [ "gji-Latn", "gji-NG" ], - "windows": "gji-Latn" - }, { "full": "gjk-Arab-PK", "iana": [ "Kachi Koli" ], @@ -38876,9 +40600,9 @@ "iana": [ "Gonja" ], "iso639_3": "gjn", "localname": "Ngbanyito", - "localnames": [ "Ngbanyito" ], + "localnames": [ "Ŋgbanyɛto" ], "name": "Gonja", - "names": [ "Guang", "Ngbanyito" ], + "names": [ "Guang", "Ngbanya", "Ngbanyito", "Ŋgbanyɛto" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -38892,7 +40616,7 @@ "iana": [ "Gonja" ], "iso639_3": "gjn", "name": "Gonja", - "names": [ "Guang", "Ngbanyito" ], + "names": [ "Guang", "Ngbanya", "Ngbanyito", "Ŋgbanyɛto" ], "nophonvars": true, "region": "GH", "regionname": "Ghana", @@ -38923,6 +40647,7 @@ "latnnames": [ "Gojri" ], "localname": "گوجری", "localnames": [ "گوجری" ], + "macrolang": "raj", "name": "Gujari", "names": [ "Gogri", "Gojari", "Gojri", "Gujar", "Gujer", "Gujjari", "Gujuri", "Gujuri Rajasthani", "Gurjar", "Hindki", "Kashmir Gujuri", "Parimu", "Rajasthani Gujuri" ], "region": "PK", @@ -38932,12 +40657,13 @@ "sldr": true, "tag": "gju", "tags": [ "gju-Arab", "gju-PK" ], - "windows": "gju-Arab" + "windows": "gju-Arab-PK" }, { "full": "gju-Deva-IN", "iana": [ "Gujari" ], "iso639_3": "gju", + "macrolang": "raj", "name": "Gujari", "names": [ "Gogri", "Gojari", "Gojri", "Gujar", "Gujer", "Gujjari", "Gujuri", "Gujuri Rajasthani", "Gurjar", "Hindki", "Kashmir Gujuri", "Parimu", "Rajasthani Gujuri" ], "nophonvars": true, @@ -38948,7 +40674,7 @@ "sldr": false, "tag": "gju-Deva", "tags": [ "gju-IN" ], - "windows": "gju-Deva" + "windows": "gju-Deva-IN" }, { "full": "gka-Latn-PG", @@ -39030,8 +40756,9 @@ "iso639_3": "gkp", "localname": "Kpɛlɛwoo", "localnames": [ "Kpɛlɛwoo" ], + "macrolang": "kpe", "name": "Kpelle, Guinea", - "names": [ "Akpese", "Gbese", "Gerse", "Gerze", "Guerze", "Guinea Kpelle", "Kpele", "Kpelese", "Kpelesetina", "Kperese", "Kpese", "Kpwessi", "Kpɛlɛwoo", "Northern Kpele", "Pessa", "Pessy" ], + "names": [ "Akpese", "Gbese", "Gerse", "Gerze", "Guerzé", "Guinea Kpelle", "Kpele", "Kpelese", "Kpelesetina", "Kperese", "Kpese", "Kpwessi", "Kpɛlɛwoo", "Northern Kpele", "Pessa", "Pessy" ], "region": "GN", "regionname": "Guinea", "script": "Latn", @@ -39065,7 +40792,7 @@ "names": [ "Galego", "Gallego" ], "region": "ES", "regionname": "Spain", - "regions": [ "PT", "UY" ], + "regions": [ "AR", "PT", "UY" ], "script": "Latn", "sldr": true, "suppress": true, @@ -39074,13 +40801,28 @@ "variants": [ "ao1990" ], "windows": "gl" }, + { + "full": "glb-Latn-NG", + "iana": [ "Belning" ], + "iso639_3": "glb", + "name": "Belning", + "names": [ "Balning", "Belnəng" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "glb", + "tags": [ "glb-Latn", "glb-NG" ], + "unwritten": true, + "windows": "glb-Latn" + }, { "full": "glc-Latn-TD", "iana": [ "Bon Gula" ], "iso639_3": "glc", - "localnames": [ "Êeni" ], + "localnames": [ "Êêni" ], "name": "Bon Gula", - "names": [ "Bon", "Bon Goula", "Bun", "Gula Guera", "Poun", "Taataal", "Êeni" ], + "names": [ "Bon", "Bon Goula", "Bun", "Gula Guera", "Poun", "Taataal", "Êêni" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -39185,8 +40927,8 @@ "full": "glo-Latn-NG", "iana": [ "Galambu" ], "iso639_3": "glo", - "name": "Galambu", - "names": [ "Galambe", "Galambi", "Galembi" ], + "name": "Galambi", + "names": [ "Galambanchi", "Galambawa", "Galambe", "Galambu", "Galembi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -39291,7 +41033,7 @@ "iana": [ "Mághdì" ], "iso639_3": "gmd", "name": "Mághdì", - "names": [ "Tala", "Widala" ], + "names": [ "Lee Mághdì", "Tala", "Widala" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -39348,8 +41090,9 @@ "full": "gmm-Latn-CM", "iana": [ "Gbaya-Mbodomo" ], "iso639_3": "gmm", + "macrolang": "gba", "name": "Gbaya-Mbodomo", - "names": [ "Mbodomo" ], + "names": [ "Mbodomo", "Mbódɔ̀mɔ̀" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -39375,6 +41118,7 @@ }, { "full": "gmr-Latn-AU", + "iana": [ "Mirning", "Mirniny" ], "iso639_3": "gmr", "name": "Mirning", "names": [ "Mirniny" ], @@ -39417,6 +41161,7 @@ "full": "gmv-Latn-ET", "iana": [ "Gamo" ], "iso639_3": "gmv", + "localname": "Gamotso", "localnames": [ "Gamotso" ], "name": "Gamo", "region": "ET", @@ -39442,17 +41187,17 @@ "windows": "gmx-Latn" }, { - "full": "gmy-Zyyy-GR", + "full": "gmy-Linb-GR", "iana": [ "Mycenaean Greek" ], "iso639_3": "gmy", "name": "Mycenaean Greek", "region": "GR", "regionname": "Greece", - "script": "Zyyy", + "script": "Linb", "sldr": false, "tag": "gmy", - "tags": [ "gmy-GR", "gmy-Zyyy" ], - "windows": "gmy-Zyyy" + "tags": [ "gmy-GR", "gmy-Linb" ], + "windows": "gmy-Linb" }, { "full": "gmz-Latn-NG", @@ -39474,8 +41219,8 @@ "iana": [ "Guarani" ], "iso639_3": "grn", "localnames": [ "Avañe'ẽ" ], - "name": "Guaraní, Paraguayan", - "names": [ "Avanye’e", "Avañee", "Avañe’ẽ", "Correntino Guaraní", "Guarani", "Guaraní", "Guaraní paraguaio" ], + "name": "Guarani", + "names": [ "Avanye’e", "Avañee", "Avañe’ẽ", "Guaraní", "Guaraní paraguaio", "Guaraní, Paraguayan" ], "region": "PY", "regionname": "Paraguay", "regions": [ "AR", "BO", "BR" ], @@ -39506,6 +41251,7 @@ "full": "gnb-Latn-IN", "iana": [ "Gangte" ], "iso639_3": "gnb", + "localnames": [ "Gangte" ], "name": "Gangte", "names": [ "Gante" ], "region": "IN", @@ -39714,8 +41460,8 @@ "full": "gnt-Latn-PG", "iana": [ "Guntai" ], "iso639_3": "gnt", - "name": "Guntai", - "names": [ "Kan", "Warta Thuntai" ], + "name": "Wartha Thuntai", + "names": [ "Guntai", "Kan", "Thuntai", "Warta Thuntai", "Wartha" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -39744,8 +41490,10 @@ "iana": [ "Western Bolivian Guaraní" ], "iso639_3": "gnw", "localname": "Simba", + "localnames": [ "Guaraní" ], + "macrolang": "gn", "name": "Guaraní, Western Bolivian", - "names": [ "Simba", "Simba Guaraní", "Western Bolivian Guaraní" ], + "names": [ "Guaraní", "Simba", "Simba Guaraní", "Western Bolivian Guaraní" ], "region": "BO", "regionname": "Bolivia", "script": "Latn", @@ -39810,14 +41558,16 @@ "sldr": false, "tag": "goc", "tags": [ "goc-Latn", "goc-PG" ], + "unwritten": true, "windows": "goc-Latn" }, { "full": "god-Latn-CI", "iana": [ "Godié" ], "iso639_3": "god", + "localnames": [ "-gwëjewälɩɩ'" ], "name": "Godié", - "names": [ "Dadjriwalé", "Godye" ], + "names": [ "-gwëjewälɩɩ’", "-gwödyéwale", "Betɩgwʋʋ", "Dadjriwalé", "Godye" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -39827,19 +41577,19 @@ "windows": "god-Latn" }, { - "full": "goe-Zyyy-BT", + "full": "goe-Tibt-BT", "iana": [ "Gongduk" ], "iso639_3": "goe", "name": "Gongduk", "names": [ "Gongdubikha" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "goe", - "tags": [ "goe-BT", "goe-Zyyy" ], + "tags": [ "goe-BT", "goe-Tibt" ], "unwritten": true, - "windows": "goe-Zyyy" + "windows": "goe-Tibt" }, { "full": "gof-Ethi-ET", @@ -39858,6 +41608,7 @@ "full": "gof-Latn-ET", "iana": [ "Gofa" ], "iso639_3": "gof", + "localname": "Goofatho", "localnames": [ "Goofatho" ], "name": "Gofa", "region": "ET", @@ -39900,8 +41651,8 @@ "full": "goi-Latn-PG", "iana": [ "Gobasi" ], "iso639_3": "goi", - "name": "Gobasi", - "names": [ "Gebusi", "Nomad" ], + "name": "Gebusi", + "names": [ "Gobasi", "Nomad" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -39911,23 +41662,25 @@ "windows": "goi-Latn" }, { - "full": "goj-Zyyy-IN", + "full": "goj-Deva-IN", "iana": [ "Gowlan" ], "iso639_3": "goj", "name": "Gowlan", "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "goj", - "tags": [ "goj-IN", "goj-Zyyy" ], + "tags": [ "goj-Deva", "goj-IN" ], "unwritten": true, - "windows": "goj-Zyyy" + "windows": "goj-Deva" }, { "full": "gok-Deva-IN", "iana": [ "Gowli" ], "iso639_3": "gok", + "latnnames": [ "Gaulī" ], + "localnames": [ "गौली" ], "name": "Gowli", "names": [ "Nand" ], "region": "IN", @@ -39959,6 +41712,7 @@ "iso639_3": "gom", "latnnames": [ "Konknni" ], "localnames": [ "कोंकणी" ], + "macrolang": "kok", "name": "Konkani, Goan", "names": [ "Goan", "Goan Konkani", "Goanese Konkani", "Gomataki", "Konkani", "Konknni", "Southern Kanara" ], "region": "IN", @@ -39974,6 +41728,7 @@ "full": "gom-Knda-IN", "iana": [ "Goan Konkani" ], "iso639_3": "gom", + "macrolang": "kok", "name": "Konkani, Goan", "names": [ "Goan", "Goan Konkani", "Goanese Konkani", "Gomataki", "Konkani", "Konknni", "Southern Kanara" ], "nophonvars": true, @@ -39990,6 +41745,7 @@ "iana": [ "Goan Konkani" ], "iso639_3": "gom", "localnames": [ "Konknni" ], + "macrolang": "kok", "name": "Konkani, Goan", "names": [ "Goan", "Goan Konkani", "Goanese Konkani", "Gomataki", "Konkani", "Konknni", "Southern Kanara" ], "region": "IN", @@ -40017,8 +41773,10 @@ "full": "gon-Deva-IN", "iana": [ "Gondi" ], "iso639_3": "gon", - "name": "Gondi, Northern", - "names": [ "Gaudi", "Ghond", "Godi", "Gondi", "Gondiva", "Gondu", "Gondwadi", "Goondile", "Goudi", "Goudwal" ], + "latnnames": [ "Gondi" ], + "localnames": [ "गोंडी" ], + "name": "Gondi", + "names": [ "Gaudi", "Ghond", "Godi", "Gondi, Northern", "Gondiva", "Gondu", "Gondwadi", "Goondile", "Goudi", "Goudwal" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -40028,12 +41786,25 @@ "tags": [ "gno", "gno-Deva", "gno-Deva-IN", "gno-IN" ], "windows": "gon-Deva" }, + { + "full": "gon-Gong-IN", + "iana": [ "Gondi" ], + "iso639_3": "gon", + "name": "Gondi", + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Gong", + "sldr": false, + "tag": "gon-Gong", + "windows": "gon-Gong" + }, { "full": "gon-Gonm-IN", "iana": [ "Gondi" ], "iso639_3": "gon", - "name": "Gondi, Northern", - "names": [ "Gaudi", "Ghond", "Godi", "Gondi", "Gondiva", "Gondu", "Gondwadi", "Goondile", "Goudi", "Goudwal" ], + "name": "Gondi", + "names": [ "Gaudi", "Ghond", "Godi", "Gondi, Northern", "Gondiva", "Gondu", "Gondwadi", "Goondile", "Goudi", "Goudwal" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -40106,7 +41877,7 @@ "full": "gos-Latn-NL", "iana": [ "Gronings" ], "iso639_3": "gos", - "localnames": [ "Grunnegs", "Grönnegs" ], + "localnames": [ "Grunnegs" ], "name": "Gronings", "names": [ "Groningen", "Grunnegs", "Grunnings", "Grönnegs" ], "region": "NL", @@ -40146,6 +41917,21 @@ "tags": [ "gou-CM", "gou-Latn" ], "windows": "gou-Latn" }, + { + "full": "gov-Latn-CI", + "iana": [ "Goo" ], + "iso639_3": "gov", + "name": "Goo", + "names": [ "Goh", "Guse", "Guu", "Gụ̏ụ̏" ], + "region": "CI", + "regionname": "Côte d'Ivoire", + "script": "Latn", + "sldr": false, + "tag": "gov", + "tags": [ "gov-CI", "gov-Latn" ], + "unwritten": true, + "windows": "gov-Latn" + }, { "full": "gow-Latn-TZ", "iana": [ "Gorowa" ], @@ -40239,8 +42025,8 @@ "full": "gpn-Latn-PG", "iana": [ "Taiap" ], "iso639_3": "gpn", - "name": "Taiap", - "names": [ "Gapun" ], + "name": "Tayap", + "names": [ "Gapun", "Taiap" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -40332,12 +42118,12 @@ "localnames": [ "गरसिया" ], "name": "Garasia, Rajput", "names": [ "Dhungri Garasia", "Dungari Garasia", "Dungri Grasia", "Girasia", "Grasia", "Nyar", "Rajput Garasia" ], - "nophonvars": true, "region": "IN", "regionname": "India", "script": "Deva", "sldr": false, - "tag": "gra-Deva", + "tag": "gra", + "tags": [ "gra-Deva", "gra-IN" ], "windows": "gra-Deva" }, { @@ -40360,8 +42146,8 @@ "iso639_3": "grb", "localname": "Klépo", "localnames": [ "Klépo" ], - "name": "Grebo, Northern", - "names": [ "Grebo", "Klépo" ], + "name": "Grebo", + "names": [ "Grebo, Northern", "Klépo" ], "region": "LR", "regionname": "Liberia", "script": "Latn", @@ -40383,7 +42169,7 @@ "sldr": false, "tag": "grc", "tags": [ "grc-CY", "grc-Cprt" ], - "windows": "grc-Cprt" + "windows": "grc-Cprt-CY" }, { "full": "grc-Grek-GR", @@ -40399,7 +42185,7 @@ "script": "Grek", "sldr": false, "tag": "grc-Grek", - "windows": "grc-Grek" + "windows": "grc-Grek-GR" }, { "full": "grc-Linb-GR", @@ -40413,14 +42199,14 @@ "script": "Linb", "sldr": false, "tag": "grc-Linb", - "windows": "grc-Linb" + "windows": "grc-Linb-GR" }, { "full": "grd-Latn-NG", "iana": [ "Guruntum-Mbaaru" ], "iso639_3": "grd", "name": "Guruntum-Mbaaru", - "names": [ "Gurdung", "Guruntum" ], + "names": [ "Gurdun", "Gurdung", "Guruntum" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -40448,6 +42234,7 @@ "full": "grh-Latn-NG", "iana": [ "Gbiri-Niragu" ], "iso639_3": "grh", + "localnames": [ "Tugbiri", "Tugbiri-Niragu" ], "name": "Tugbiri-Niragu", "names": [ "Gbiri-Niragu", "Gure-Kahugu", "Tugbiri" ], "region": "NG", @@ -40467,7 +42254,7 @@ "region": "SB", "regionname": "Solomon Islands", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "gri", "tags": [ "gri-Latn", "gri-SB" ], "windows": "gri-Latn" @@ -40476,6 +42263,7 @@ "full": "grj-Latn-LR", "iana": [ "Southern Grebo" ], "iso639_3": "grj", + "macrolang": "grb", "name": "Grebo, Southern", "names": [ "Southern Grebo" ], "region": "LR", @@ -40533,20 +42321,36 @@ "windows": "grq-Latn" }, { - "full": "grr-Zyyy-DZ", + "full": "grr-Arab-DZ", "iana": [ "Taznatit" ], "iso639_3": "grr", "name": "Taznatit", "names": [ "Sud Oranais-Gourara", "Zenatiyya" ], + "nophonvars": true, "region": "DZ", "regionname": "Algeria", "regions": [ "MA" ], - "script": "Zyyy", + "script": "Arab", + "sldr": false, + "tag": "grr-Arab", + "unwritten": true, + "windows": "grr-Arab" + }, + { + "full": "grr-Tfng-DZ", + "iana": [ "Taznatit" ], + "iso639_3": "grr", + "name": "Taznatit", + "names": [ "Sud Oranais-Gourara", "Zenatiyya" ], + "nophonvars": true, + "region": "DZ", + "regionname": "Algeria", + "regions": [ "MA" ], + "script": "Tfng", "sldr": false, - "tag": "grr", - "tags": [ "grr-DZ", "grr-Zyyy" ], + "tag": "grr-Tfng", "unwritten": true, - "windows": "grr-Zyyy" + "windows": "grr-Tfng" }, { "full": "grs-Latn-ID", @@ -40637,6 +42441,7 @@ "full": "grv-Latn-LR", "iana": [ "Central Grebo" ], "iso639_3": "grv", + "macrolang": "grb", "name": "Grebo, Central", "names": [ "Central Grebo" ], "region": "LR", @@ -40652,6 +42457,7 @@ "full": "grw-Latn-PG", "iana": [ "Gweda" ], "iso639_3": "grw", + "localname": "Gweda", "localnames": [ "Gweda" ], "name": "Gweda", "names": [ "Garuwahi" ], @@ -40667,8 +42473,8 @@ "full": "grx-Latn-PG", "iana": [ "Guriaso" ], "iso639_3": "grx", - "name": "Guriaso", - "names": [ "Muno" ], + "name": "Muno", + "names": [ "Guriaso" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -40682,6 +42488,7 @@ "full": "gry-Latn-LR", "iana": [ "Barclayville Grebo" ], "iso639_3": "gry", + "macrolang": "grb", "name": "Grebo, Barclayville", "names": [ "Barclayville Grebo", "Wedebo Grebo" ], "region": "LR", @@ -40729,6 +42536,7 @@ "names": [ "DGS", "Deutsche Gebärdensprache" ], "region": "DE", "regionname": "Germany", + "regions": [ "LU" ], "script": "Zxxx", "sldr": false, "tag": "gsg", @@ -40739,6 +42547,7 @@ "full": "gsl-Latn-SN", "iana": [ "Gusilay" ], "iso639_3": "gsl", + "localnames": [ "Gusiilaay" ], "name": "Gusilay", "names": [ "Gusiilaay", "Gusiilay", "Gusilaay", "Kusiilaay", "Kusilay" ], "region": "SN", @@ -40767,6 +42576,7 @@ "full": "gsn-Latn-PG", "iana": [ "Nema", "Gusan" ], "iso639_3": "gsn", + "localnames": [ "Nema" ], "name": "Nema", "names": [ "Gusan" ], "region": "PG", @@ -40775,7 +42585,6 @@ "sldr": false, "tag": "gsn", "tags": [ "gsn-Latn", "gsn-PG" ], - "unwritten": true, "windows": "gsn-Latn" }, { @@ -40783,8 +42592,9 @@ "iana": [ "Southwest Gbaya" ], "iso639_3": "gso", "localnames": [ "Nu Gbaya" ], + "macrolang": "gba", "name": "Gbaya, Southwest", - "names": [ "Gbaya", "Gbaya Sud-Ouest", "Southwest Gbaya" ], + "names": [ "Gbaya", "Gbaya Sud-Ouest", "Nu Gbaya", "Southwest Gbaya" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -40798,7 +42608,7 @@ "iana": [ "Wasembo" ], "iso639_3": "gsp", "name": "Wasembo", - "names": [ "Biapim", "Gusap", "Yankowan" ], + "names": [ "Biapim", "Gusap", "Horokoi", "Yankowan" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -40880,12 +42690,12 @@ "names": [ "Gujerathi", "Gujerati", "Gujrathi" ], "region": "IN", "regionname": "India", - "regions": [ "AU", "BD", "BH", "BW", "CA", "FJ", "GB", "KE", "LK", "MU", "MW", "NZ", "PK", "SG", "TZ", "US", "ZM", "ZW" ], + "regions": [ "AU", "BD", "BH", "BW", "CA", "CN", "FJ", "GB", "KE", "LK", "MU", "MW", "NZ", "PK", "SG", "TZ", "US", "ZM", "ZW" ], "script": "Gujr", "sldr": true, "suppress": true, "tag": "gu", - "tags": [ "gu-Gujr", "gu-IN" ], + "tags": [ "gu-Gujr", "gu-IN", "prp", "prp-Gujr", "prp-Gujr-IN", "prp-IN" ], "windows": "gu" }, { @@ -40956,7 +42766,7 @@ "localname": "Wayuunaiki", "localnames": [ "Wayuunaiki" ], "name": "Wayuu", - "names": [ "Goajiro", "Guajira", "Guajiro", "Uáira", "Wahiro", "Waiu", "Wayu", "Wayuunaiki", "Wayúu" ], + "names": [ "Goajiro", "Guajira", "Guajiro", "Uáira", "Wahiro", "Waiu", "Wayu", "Wayuunaiki", "Wayuunaiqui", "Wayúu" ], "region": "CO", "regionname": "Colombia", "regions": [ "VE" ], @@ -41031,8 +42841,10 @@ "full": "gui-Latn-BO", "iana": [ "Eastern Bolivian Guaraní" ], "iso639_3": "gui", + "localnames": [ "Guaraní" ], + "macrolang": "gn", "name": "Guaraní, Eastern Bolivian", - "names": [ "Eastern Bolivian Guaraní", "Guaraní", "Guaraní Occidental", "Guaraní Pe", "Guaraní, Western Argentine", "Guarayo", "Guasurango", "Western Argentine Guaraní" ], + "names": [ "Eastern Bolivian Guaraní", "Guaraní", "Guaraní Occidental", "Guaraní, Western Argentine", "Guarayo", "Guasurango", "Western Argentine Guaraní", "Ñandeva" ], "region": "BO", "regionname": "Bolivia", "regions": [ "AR", "PY" ], @@ -41049,7 +42861,7 @@ "localname": "Sa-Gumuz", "localnames": [ "Sa-Gumuz" ], "name": "Gumuz", - "names": [ "Deguba", "Shanqilla" ], + "names": [ "Deguba", "Kadallu" ], "region": "ET", "regionname": "Ethiopia", "regions": [ "SD" ], @@ -41064,7 +42876,7 @@ "iana": [ "Gumuz" ], "iso639_3": "guk", "name": "Gumuz", - "names": [ "Deguba", "Shanqilla" ], + "names": [ "Deguba", "Kadallu" ], "nophonvars": true, "obsolete": true, "region": "ET", @@ -41081,8 +42893,8 @@ "iso639_3": "gul", "localname": "Gullah", "localnames": [ "Geechee", "Gullah" ], - "name": "Sea Island Creole English", - "names": [ "Geechee", "Gullah" ], + "name": "Sea Island English Creole", + "names": [ "Geechee", "Gullah", "Sea Island Creole English" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -41097,8 +42909,8 @@ "iso639_3": "gum", "localname": "Namdrik", "localnames": [ "Namdrik" ], - "name": "Guambiano", - "names": [ "Coconuco", "Guambia", "Guanaca", "Misak", "Moguex", "Nam Trik", "Namdrik" ], + "name": "Misak", + "names": [ "Coconuco", "Guambia", "Guambiano", "Guanaca", "Moguex", "Nam Trik", "Namdrik" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -41112,9 +42924,10 @@ "iana": [ "Mbyá Guaraní" ], "iso639_3": "gun", "localname": "Mbya", - "localnames": [ "Mbya" ], + "localnames": [ "Nhandeayvu" ], + "macrolang": "gn", "name": "Guaraní, Mbyá", - "names": [ "Bugre", "Eastern Argentina Guaraní", "Ka’yngua", "Mbiá", "Mbua", "Mbya", "Mbya-apytere", "Mbyá", "Mbyá Guaraní" ], + "names": [ "Bugre", "Eastern Argentina Guaraní", "Ka’yngua", "Mbiá", "Mbua", "Mbya", "Mbya-apytere", "Mbyá", "Mbyá Guaraní", "Nhandeayvu" ], "region": "BR", "regionname": "Brazil", "regions": [ "AR", "PY", "UY" ], @@ -41128,8 +42941,9 @@ "full": "guo-Latn-CO", "iana": [ "Guayabero" ], "iso639_3": "guo", + "localnames": [ "Jiwjame" ], "name": "Guayabero", - "names": [ "Cunimía", "Jiw", "Mítua", "Mítus" ], + "names": [ "Cunimía", "Jiw", "Jiwjame", "Mítua", "Mítus" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -41142,8 +42956,9 @@ "full": "gup-Latn-AU", "iana": [ "Gunwinggu" ], "iso639_3": "gup", + "localnames": [ "Bininj Kunwok", "Kunwinjku" ], "name": "Gunwinggu", - "names": [ "Bininj Gun-Wok", "Gunawitji", "Gunwinjgu", "Gunwinygu", "Kuninjku", "Kunwinjku" ], + "names": [ "Bininj Gun-Wok", "Bininj Gunwok", "Bininj Kunwok", "Gunawitji", "Gunwinjgu", "Gunwinygu", "Kuninjku", "Kunwinjku", "Kunwinjkuan", "Kunwok" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -41172,9 +42987,9 @@ "iana": [ "Farefare" ], "iso639_3": "gur", "localname": "Farefare", - "localnames": [ "Farefare", "Gurenɛ" ], + "localnames": [ "Fãrfãre" ], "name": "Farefare", - "names": [ "Frafra", "Gurene", "Gurenne", "Gurenɛ", "Gurne", "Gurune", "Nankani", "Ninkare", "Ninkarsé", "fãrfãrɛ", "gʋrnɛ Ninkãrɛ", "nankam", "nankana", "ninkãrɛ" ], + "names": [ "Frafra", "Fãrfãre", "Gurene", "Gurenne", "Gurenɛ", "Gurne", "Gurune", "Gãrfãrɛ", "Gʋrnɛ Ninkãrɛ", "Nankam", "Nankana", "Nankani", "Ninkare", "Ninkarsé", "Ninkãrɛ" ], "region": "GH", "regionname": "Ghana", "regions": [ "BF" ], @@ -41218,7 +43033,7 @@ "iso639_3": "guu", "localnames": [ "Yanomamɨ" ], "name": "Yanomamö", - "names": [ "Cobari Kobali", "Cobariwa", "Guaharibo", "Guaica", "Guajaribo", "Ianomami", "Shaathari", "Shamatari", "Shamatri", "Yanoama", "Yanoamae", "Yanoamï", "Yanomae", "Yanomam", "Yanomame", "Yanomami", "Yanomamï", "Yanomamɨ" ], + "names": [ "Guaharibo", "Guaica", "Guajaribo", "Ianomami", "Shaathari", "Shamatari", "Shamatri", "Xamatari", "Yanoama", "Yanoamae", "Yanoamï", "Yanomae", "Yanomam", "Yanomame", "Yanomami", "Yanomamï", "Yanomamɨ" ], "region": "VE", "regionname": "Venezuela", "regions": [ "BR" ], @@ -41250,9 +43065,9 @@ "iana": [ "Gourmanchéma" ], "iso639_3": "gux", "localname": "Gourma", - "localnames": [ "Gourma" ], + "localnames": [ "Gùlmàncéma" ], "name": "Gourmanchéma", - "names": [ "Goulimancema", "Goulmacema", "Goulmancema", "Gourma", "Gourmantche", "Gulimancema", "Gulmancema", "Gurma", "Migulimancema" ], + "names": [ "Goulimancema", "Goulmacema", "Goulmancema", "Gourma", "Gourmantche", "Gulimancema", "Gulimanchema", "Gulmancema", "Gurma", "Gùlmàncéma", "Migulimancema" ], "region": "BF", "regionname": "Burkina Faso", "regions": [ "BJ", "NE", "TG" ], @@ -41269,7 +43084,7 @@ "localname": "Ekegusii", "localnames": [ "Ekegusii" ], "name": "Ekegusii", - "names": [ "EkeGusii", "Gusii", "Guzii", "Kisii", "Kosova" ], + "names": [ "Gusii", "Guzii", "Kisii", "Kosova" ], "region": "KE", "regionname": "Kenya", "regions": [ "TZ" ], @@ -41294,7 +43109,22 @@ "suppress": true, "tag": "gv", "tags": [ "gv-IM", "gv-Latn" ], - "windows": "gv" + "windows": "gv-IM" + }, + { + "full": "gv-Ogam-001", + "iana": [ "Manx" ], + "iso639_3": "glv", + "name": "Manx", + "names": [ "Gaelg", "Gailck", "Manx Gaelic" ], + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Ogam", + "sldr": false, + "tag": "gv-Ogam", + "tags": [ "gv-001" ], + "windows": "gv-Ogam-001" }, { "full": "gva-Latn-PY", @@ -41315,9 +43145,10 @@ "full": "gvc-Latn-BR", "iana": [ "Guanano" ], "iso639_3": "gvc", + "localname": "Kotiria", "localnames": [ "Kotiria" ], "name": "Wanano", - "names": [ "Anana", "Guanano", "Kotiria", "Kótedia", "Kótirya", "Uanana", "Uanano", "Wanana", "Wanana-Pirá", "Wanâna" ], + "names": [ "Anana", "Guanano", "Kootiria", "Kotiria", "Kótedia", "Kótirya", "Uanana", "Uanano", "Wanana", "Wanana-Pirá", "Wanâna" ], "region": "BR", "regionname": "Brazil", "regions": [ "CO" ], @@ -41421,7 +43252,7 @@ "iana": [ "Gavião Do Jiparaná" ], "iso639_3": "gvo", "name": "Gavião do Jiparaná", - "names": [ "Digüt", "Gavião Do Jiparaná", "Gavião do Rondônia", "Ikolen", "Ikõleey", "Ikõlej", "Ikõro" ], + "names": [ "Digüt", "Gavião - RO", "Gavião Do Jiparaná", "Gavião do Rondônia", "Ikolen", "Ikólóéhj", "Ikõleey", "Ikõlej", "Ikõro" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -41449,16 +43280,18 @@ "full": "gvr-Deva-NP", "iana": [ "Gurung" ], "iso639_3": "gvr", + "latnnames": [ "Tamu Kyi" ], + "localnames": [ "तमु क्यी" ], "name": "Gurung", "names": [ "Daduwa", "Gurung Kura", "Tamu Kyi", "Western Gurung" ], "region": "NP", "regionname": "Nepal", "regions": [ "BT", "IN" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "gvr", "tags": [ "ggn", "ggn-Deva", "ggn-Deva-NP", "ggn-NP", "gvr-Deva", "gvr-NP" ], - "windows": "gvr-Deva" + "windows": "gvr-Deva-NP" }, { "full": "gvr-Latn-IN", @@ -41472,7 +43305,7 @@ "sldr": false, "tag": "gvr-Latn", "tags": [ "ggn-IN", "ggn-Latn", "ggn-Latn-IN", "gvr-IN" ], - "windows": "gvr-Latn" + "windows": "gvr-Latn-IN" }, { "full": "gvs-Latn-PG", @@ -41536,8 +43369,11 @@ }, { "full": "gwc-Arab-PK", - "iana": [ "Kalami" ], + "iana": [ "Gawri", "Kalami" ], "iso639_3": "gwc", + "latnnames": [ "Gauri" ], + "localname": "گاؤری", + "localnames": [ "گاؤری" ], "name": "Gawri", "names": [ "Bashgharik", "Bashkarik", "Baskarik", "Dir Kohistani", "Diri", "Dirwali", "Gaawro", "Garwa", "Garwi", "Gowri", "Kalam Kohistani", "Kalami", "Kohistana", "Kohistani" ], "region": "PK", @@ -41552,8 +43388,7 @@ "full": "gwd-Latn-ET", "iana": [ "Gawwada" ], "iso639_3": "gwd", - "latnnames": [ "Ale" ], - "localnames": [ "Ale" ], + "localnames": [ "Qale" ], "name": "Ale", "region": "ET", "regionname": "Ethiopia", @@ -41611,10 +43446,10 @@ "full": "gwi-Latn-CA", "iana": [ "Gwichʼin" ], "iso639_3": "gwi", - "localname": "Gwich’in", - "localnames": [ "Dinju Zhuh K'yuu" ], + "localname": "Dinju Zhuh K'yuu", + "localnames": [ "Dinjii Zhuh Ginjik" ], "name": "Gwich’in", - "names": [ "Dinju Zhuh K’yuu", "Gwichʼin", "Kutchin", "Loucheux", "Takudh", "Tukudh" ], + "names": [ "Dinjii Zhuh Ginjik", "Dinjii Zhuh K’yaa", "Dinju Zhuh K’yuu", "Gwichʼin", "Kutchin", "Loucheux", "Takudh", "Tukkuthkutchin", "Tukudh" ], "region": "CA", "regionname": "Canada", "regions": [ "US" ], @@ -41676,7 +43511,7 @@ "localname": "Lugwere", "localnames": [ "Lugwere" ], "name": "Gwere", - "names": [ "Bagwere", "Lugwere", "Olugwere", "oLugwere" ], + "names": [ "Bagwere", "Lugwere", "oLugwere" ], "region": "UG", "regionname": "Uganda", "script": "Latn", @@ -41750,7 +43585,6 @@ "full": "gxx-Latn-CI", "iana": [ "Wè Southern" ], "iso639_3": "gxx", - "localnames": [ "Wèè" ], "name": "Wè Southern", "names": [ "Central Guéré", "Gere", "Guéré", "Wèè" ], "region": "CI", @@ -41840,6 +43674,8 @@ "full": "gyi-Latn-CM", "iana": [ "Gyele" ], "iso639_3": "gyi", + "localname": "Gyele", + "localnames": [ "Gyele" ], "name": "Gyele", "names": [ "Babinga", "Bagiele", "Bagyele", "Bajele", "Bajeli", "Bako", "Bakola", "Bakuele", "Bekoe", "Bogyel", "Bogyeli", "Bondjiel", "Giele", "Gieli", "Gyeli", "Likoya" ], "region": "CM", @@ -41852,18 +43688,30 @@ "windows": "gyi-Latn" }, { - "full": "gyl-Zyyy-ET", + "full": "gyl-Latn-ET", "iana": [ "Gayil" ], "iso639_3": "gyl", "name": "Gayil", "region": "ET", "regionname": "Ethiopia", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "gyl", - "tags": [ "gyl-ET", "gyl-Zyyy" ], - "unwritten": true, - "windows": "gyl-Zyyy" + "tags": [ "gyl-ET", "gyl-Latn" ], + "windows": "gyl-Latn" + }, + { + "full": "gyl-Ethi-ET", + "iana": [ "Gayil" ], + "iso639_3": "gyl", + "name": "Gayil", + "nophonvars": true, + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": false, + "tag": "gyl-Ethi", + "windows": "gyl-Ethi" }, { "full": "gym-Latn-PA", @@ -41886,9 +43734,9 @@ "full": "gyn-Latn-GY", "iana": [ "Guyanese Creole English" ], "iso639_3": "gyn", - "localnames": [ "Creolese" ], - "name": "Guyanese Creole English", - "names": [ "Creolese", "Guyanese Creole" ], + "localnames": [ "Gaiyniiz" ], + "name": "Guyanese English Creole", + "names": [ "Creolese", "Gaiyniiz", "Guyanese", "Guyanese Creole", "Guyanese Creole English", "Kriiyliiz", "Kriiyoliiz" ], "region": "GY", "regionname": "Guyana", "regions": [ "SR", "US" ], @@ -41899,25 +43747,26 @@ "windows": "gyn-Latn" }, { - "full": "gyo-Zyyy-NP", + "full": "gyo-Deva-NP", "iana": [ "Gyalsumdo" ], "iso639_3": "gyo", "name": "Gyalsumdo", "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "gyo", - "tags": [ "gyo-NP", "gyo-Zyyy" ], + "tags": [ "gyo-Deva", "gyo-NP" ], "unwritten": true, - "windows": "gyo-Zyyy" + "windows": "gyo-Deva" }, { "full": "gyr-Latn-BO", "iana": [ "Guarayu" ], "iso639_3": "gyr", + "localnames": [ "Gwarayu" ], "name": "Guarayu", - "names": [ "Chapacura", "Guapore", "Huachi", "Iten", "Maré", "Tapacura", "Ñañañe" ], + "names": [ "Chapacura", "Guapore", "Gwarayu", "Huachi", "Iten", "Maré", "Tapacura", "Ñañañe" ], "region": "BO", "regionname": "Bolivia", "script": "Latn", @@ -41941,6 +43790,21 @@ "unwritten": true, "windows": "gyy-Latn" }, + { + "full": "gyz-Latn-NG", + "iana": [ "Geji", "Gyaazi" ], + "iso639_3": "gyz", + "localnames": [ "Ka̱ biGyazi" ], + "name": "Gyaazi", + "names": [ "Bagba", "Gaejawa", "Gejawa", "Geji", "Gezawa", "Ka̱ biGyazi" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "gyz", + "tags": [ "gji", "gji-Latn", "gji-Latn-NG", "gji-NG", "gyz-Latn", "gyz-NG" ], + "windows": "gyz-Latn" + }, { "full": "gza-Latn-SD", "iana": [ "Ganza" ], @@ -41992,7 +43856,7 @@ "localname": "Hausa", "localnames": [ "Hausa" ], "name": "Hausa", - "names": [ "Abakwariga", "Habe", "Haoussa", "Hausawa", "Haussa", "Hawsa", "Kado", "Mgbakpa" ], + "names": [ "Abakwariga", "Haoussa", "Hausawa", "Haussa", "Hawsa", "Mgbakpa" ], "region": "NG", "regionname": "Nigeria", "regions": [ "BF", "BJ", "CF", "CG", "CI", "CM", "DE", "ER", "NE", "SD", "TD", "TG" ], @@ -42000,14 +43864,14 @@ "sldr": true, "tag": "ha", "tags": [ "ha-Latn", "ha-NG" ], - "windows": "ha-Latn" + "windows": "ha-Latn-NG" }, { "full": "ha-Arab-NG", "iana": [ "Hausa" ], "iso639_3": "hau", "name": "Hausa", - "names": [ "Abakwariga", "Habe", "Haoussa", "Hausawa", "Haussa", "Hawsa", "Kado", "Mgbakpa" ], + "names": [ "Abakwariga", "Haoussa", "Hausawa", "Haussa", "Hawsa", "Mgbakpa" ], "nophonvars": true, "region": "NG", "regionname": "Nigeria", @@ -42015,14 +43879,14 @@ "script": "Arab", "sldr": true, "tag": "ha-Arab", - "windows": "ha-Arab" + "windows": "ha-Arab-NG" }, { "full": "ha-Brai-NG", "iana": [ "Hausa" ], "iso639_3": "hau", "name": "Hausa", - "names": [ "Abakwariga", "Habe", "Haoussa", "Hausawa", "Haussa", "Hawsa", "Kado", "Mgbakpa" ], + "names": [ "Abakwariga", "Haoussa", "Hausawa", "Haussa", "Hawsa", "Mgbakpa" ], "nophonvars": true, "region": "NG", "regionname": "Nigeria", @@ -42030,7 +43894,7 @@ "script": "Brai", "sldr": false, "tag": "ha-Brai", - "windows": "ha-Brai" + "windows": "ha-Brai-NG" }, { "full": "ha-Arab-CM", @@ -42089,9 +43953,9 @@ "full": "haa-Latn-US", "iana": [ "Han" ], "iso639_3": "haa", - "localnames": [ "Tr’ondëk Hwëch’in" ], + "localnames": [ "Häł Gołan", "Tr’ondëk Hwëch’in" ], "name": "Han", - "names": [ "Dawson", "Han-Kutchin", "Hän", "Moosehide", "Tr’ondëk Hwëch’in" ], + "names": [ "Dawson", "Han-Kutchin", "Hän", "Häł Gołan", "Moosehide", "Tr’ondëk Hwëch’in" ], "region": "US", "regionname": "United States", "regions": [ "CA" ], @@ -42149,6 +44013,7 @@ "iana": [ "Eastern Oromo" ], "iso639_3": "hae", "localnames": [ "Afaan Oromoo" ], + "macrolang": "om", "name": "Oromo, Eastern", "names": [ "Eastern Oromo" ], "region": "ET", @@ -42177,8 +44042,10 @@ "full": "hag-Latn-GH", "iana": [ "Hanga" ], "iso639_3": "hag", + "localname": "Haŋa", + "localnames": [ "Haŋatiri" ], "name": "Hanga", - "names": [ "Anga" ], + "names": [ "Anga", "Haŋa", "Haŋatiri" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -42191,8 +44058,9 @@ "full": "hah-Latn-PG", "iana": [ "Hahon" ], "iso639_3": "hah", + "localnames": [ "Rautsuiiri" ], "name": "Hahon", - "names": [ "Hanon" ], + "names": [ "Hanon", "Rautsuiiri" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -42205,9 +44073,9 @@ "full": "hai-Latn-CA", "iana": [ "Haida" ], "iso639_3": "hai", - "localnames": [ "Xaad Kil" ], - "name": "Haida, Northern", - "names": [ "Haida", "Masset", "Xaad Kil", "Xaat Kíl" ], + "localnames": [ "X̱aat Kíl" ], + "name": "Haida", + "names": [ "Haida, Northern", "Masset", "Xaad Kil", "Xaat Kíl", "X̱aad Kíl", "X̱aat Kíl" ], "region": "CA", "regionname": "Canada", "regions": [ "US" ], @@ -42254,11 +44122,12 @@ "full": "hak-Hans-CN", "iana": [ "Hakka Chinese" ], "iso639_3": "hak", + "macrolang": "zh", "name": "Chinese, Hakka", - "names": [ "Hakka Chinese", "Hokka", "Ke", "Kechia", "Kehia", "Kejia", "Kejiahua", "Kek", "Khek", "Majiahua", "Tu Guangdonghua", "Xinminhua" ], + "names": [ "Hakka Chinese", "Hokka", "Ke", "Kechia", "Kehia", "Kejia", "Kejiahua", "Kek", "Khek", "Majiahua", "Ngái", "Người Ngái", "Tu Guangdonghua", "Xinminhua" ], "region": "CN", "regionname": "China", - "regions": [ "BN", "GF", "HK", "ID", "KH", "MY", "PA", "PF", "SG", "SR", "TH", "TW" ], + "regions": [ "BN", "CA", "GF", "HK", "ID", "KH", "MY", "PA", "PF", "SG", "SR", "TH", "TW", "US", "VN" ], "script": "Hans", "sldr": false, "tag": "hak", @@ -42269,12 +44138,13 @@ "full": "hak-Hant-CN", "iana": [ "Hakka Chinese" ], "iso639_3": "hak", + "macrolang": "zh", "name": "Chinese, Hakka", - "names": [ "Hakka Chinese", "Hokka", "Ke", "Kechia", "Kehia", "Kejia", "Kejiahua", "Kek", "Khek", "Majiahua", "Tu Guangdonghua", "Xinminhua" ], + "names": [ "Hakka Chinese", "Hokka", "Ke", "Kechia", "Kehia", "Kejia", "Kejiahua", "Kek", "Khek", "Majiahua", "Ngái", "Người Ngái", "Tu Guangdonghua", "Xinminhua" ], "nophonvars": true, "region": "CN", "regionname": "China", - "regions": [ "BN", "CA", "GF", "HK", "ID", "KH", "MY", "PA", "PF", "SG", "SR", "TH", "TW", "US" ], + "regions": [ "BN", "CA", "GF", "HK", "ID", "KH", "MY", "PA", "PF", "SG", "SR", "TH", "TW", "US", "VN" ], "script": "Hant", "sldr": false, "tag": "hak-Hant", @@ -42284,11 +44154,12 @@ "full": "hak-Latn-CN", "iana": [ "Hakka Chinese" ], "iso639_3": "hak", + "macrolang": "zh", "name": "Chinese, Hakka", - "names": [ "Hakka Chinese", "Hokka", "Ke", "Kechia", "Kehia", "Kejia", "Kejiahua", "Kek", "Khek", "Majiahua", "Tu Guangdonghua", "Xinminhua" ], + "names": [ "Hakka Chinese", "Hokka", "Ke", "Kechia", "Kehia", "Kejia", "Kejiahua", "Kek", "Khek", "Majiahua", "Ngái", "Người Ngái", "Tu Guangdonghua", "Xinminhua" ], "region": "CN", "regionname": "China", - "regions": [ "BN", "CA", "GF", "HK", "ID", "KH", "MY", "PA", "PF", "SG", "SR", "TH", "TW", "US" ], + "regions": [ "BN", "CA", "GF", "HK", "ID", "KH", "MY", "PA", "PF", "SG", "SR", "TH", "TW", "US", "VN" ], "script": "Latn", "sldr": false, "tag": "hak-Latn", @@ -42314,7 +44185,7 @@ "iana": [ "Hewa" ], "iso639_3": "ham", "name": "Hewa", - "names": [ "Sisimin", "Yoliapi" ], + "names": [ "Sisimin", "Taduaguay", "Yoliapi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -42329,7 +44200,7 @@ "iso639_3": "han", "localnames": [ "Igihangaza" ], "name": "Hangaza", - "names": [ "Kihangaza" ], + "names": [ "Igihangaza", "Kihangaza" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -42342,6 +44213,7 @@ "full": "hao-Latn-PG", "iana": [ "Hakö" ], "iso639_3": "hao", + "localnames": [ "Hakö" ], "name": "Hakö", "names": [ "Haku" ], "region": "PG", @@ -42440,15 +44312,15 @@ "full": "hav-Latn-CD", "iana": [ "Havu" ], "iso639_3": "hav", + "localnames": [ "Gihavu" ], "name": "Havu", - "names": [ "Haavu", "Kihavu" ], + "names": [ "Gihavu", "Haavu", "Kihavu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", "sldr": false, "tag": "hav", "tags": [ "hav-CD", "hav-Latn" ], - "unwritten": true, "windows": "hav-Latn" }, { @@ -42471,6 +44343,7 @@ "full": "hax-Latn-CA", "iana": [ "Southern Haida" ], "iso639_3": "hax", + "macrolang": "hai", "name": "Haida, Southern", "names": [ "Southern Haida", "Xaaydaa Kil" ], "region": "CA", @@ -42501,7 +44374,7 @@ "full": "haz-Arab-AF", "iana": [ "Hazaragi" ], "iso639_3": "haz", - "latnnames": [ "Azoragi" ], + "latnnames": [ "Āzaragi" ], "localnames": [ "آزرگی‎" ], "name": "Hazaragi", "names": [ "Azargi", "Hazara", "Hazaraghi", "Hezareh", "Hezare’i" ], @@ -42533,8 +44406,9 @@ "iana": [ "Huba" ], "iso639_3": "hbb", "localname": "Nya Huba", + "localnames": [ "Huba" ], "name": "Nya Huba", - "names": [ "Chobba", "Huba", "Kilba" ], + "names": [ "Chobba", "Huba", "Hәba", "Kilba" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -42591,8 +44465,8 @@ "full": "hca-Zyyy-IN", "iana": [ "Andaman Creole Hindi" ], "iso639_3": "hca", - "name": "Andaman Creole Hindi", - "names": [ "Andaman Hindi" ], + "name": "Andaman Hindi Creole", + "names": [ "Andaman Creole Hindi", "Andaman Hindi" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -42607,9 +44481,9 @@ "iana": [ "Huichol" ], "iso639_3": "hch", "localname": "Wixárika", - "localnames": [ "Wixárika" ], + "localnames": [ "Wixárika", "Wixárika Niukiyari" ], "name": "Huichol", - "names": [ "Vixaritari Vaniuqui", "Vizaritari Vaniuki", "Wixárika" ], + "names": [ "Viixáarica", "Vixaricacü", "Vixaritari", "Vixaritari Vaniuqui", "Vizaritari Vaniuki", "Wixárika", "Wixárika Niukiyari", "ꞌüquisicayari" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -42623,7 +44497,7 @@ "iana": [ "Honduras Sign Language" ], "iso639_3": "hds", "name": "Honduran Sign Language", - "names": [ "Honduras Sign Language", "LESHO", "Lengua de Señas Hondureñas" ], + "names": [ "Honduras Sign Language", "LESHO", "Lengua de Señas Hondureñas", "Lengua de señas Hondureña" ], "region": "HN", "regionname": "Honduras", "script": "Zxxx", @@ -42662,12 +44536,13 @@ "full": "he-Hebr-IL", "iana": [ "Hebrew" ], "iso639_3": "heb", - "latnnames": [ "Ivrit", "ivrít ḥadašá[h]" + "latnnames": [ "ivrít ḥadašá[h]", + "Ivrit" ], "localname": "עברית", - "localnames": [ "עברית", "עברית חדשה" ], + "localnames": [ "עִבְרִית חֲדָשָׁה", "עברית" ], "name": "Hebrew", - "names": [ "Israeli" ], + "names": [ "Contemporary Hebrew", "Israeli", "Israeli Hebrew", "Modern Hebrew" ], "region": "IL", "regionname": "Israel", "regions": [ "AO", "CA", "DE", "PA", "PS", "RU", "US" ], @@ -42683,7 +44558,7 @@ "iana": [ "Hebrew" ], "iso639_3": "heb", "name": "Hebrew", - "names": [ "Israeli" ], + "names": [ "Contemporary Hebrew", "Israeli", "Israeli Hebrew", "Modern Hebrew" ], "nophonvars": true, "region": "IL", "regionname": "Israel", @@ -42697,8 +44572,9 @@ "full": "hed-Latn-TD", "iana": [ "Herdé" ], "iso639_3": "hed", + "localnames": [ "Zime Pala" ], "name": "Herdé", - "names": [ "He’dé", "Ka’do Herdé", "Zime", "Zime of Pala" ], + "names": [ "He’dé", "Ka’do Herdé", "Zime", "Zime Pala", "Zime of Pala" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -42773,7 +44649,7 @@ "iana": [ "Haiǁom" ], "iso639_3": "hgm", "name": "Hai|ǁom", - "names": [ "Haikom", "Haikum", "Hai||om", "Haiǁom", "Heikom", "Heikom Bushman", "Heikum", "Hei|ǁom", "Oshikwankala Haiǁom", "Xwaga" ], + "names": [ "Haikom", "Haikum", "Haiǁom", "Heikom", "Heikom Bushman", "Heikum", "Hei|ǁom", "Kaikom", "Oshikwankala Haiǁom", "Xwaga" ], "region": "NA", "regionname": "Namibia", "regions": [ "BW", "ZA", "ZM" ], @@ -42846,14 +44722,14 @@ "full": "hi-Deva-IN", "iana": [ "Hindi" ], "iso639_3": "hin", - "latnnames": [ "Mānak Hindī", " Hindī" ], + "latnnames": [ "Mānak Hindī", "Hindī" ], "localname": "हिन्दी", "localnames": [ "मानक हिन्दी", "हिन्दी" ], "name": "Hindi", "names": [ "Dakhini", "Hindi-Urdu", "Hindustani", "Modern Standard Hindi" ], "region": "IN", "regionname": "India", - "regions": [ "AE", "AU", "BD", "BT", "BZ", "CA", "DE", "DJ", "FI", "GB", "GQ", "GY", "KE", "LK", "MU", "NP", "NZ", "PH", "PR", "RU", "SG", "SX", "TH", "US", "YE", "ZA", "ZM" ], + "regions": [ "AE", "AU", "BD", "BT", "BZ", "CA", "DE", "DJ", "FI", "GB", "GQ", "GY", "KE", "KW", "LK", "MU", "NP", "NZ", "PH", "PR", "RU", "SG", "SX", "TH", "US", "YE", "ZA", "ZM" ], "script": "Deva", "sldr": true, "suppress": true, @@ -42876,6 +44752,21 @@ "tag": "hi-Brai", "windows": "hi-Brai" }, + { + "full": "hi-Latn-IN", + "iana": [ "Hindi" ], + "iso639_3": "hin", + "localname": "Hindi", + "name": "Hindi", + "names": [ "Dakhini", "Hindi-Urdu", "Hindustani", "Modern Standard Hindi" ], + "region": "IN", + "regionname": "India", + "regions": [ "AE", "AU", "BD", "BT", "BZ", "CA", "DE", "DJ", "FI", "FJ", "GB", "GQ", "GY", "KE", "LK", "MU", "NP", "NZ", "PH", "PR", "RU", "SG", "SX", "TH", "US", "YE", "ZA", "ZM" ], + "script": "Latn", + "sldr": true, + "tag": "hi-Latn", + "windows": "hi-Latn" + }, { "full": "hi-Mahj-IN", "iana": [ "Hindi" ], @@ -42955,8 +44846,8 @@ "iana": [ "Fiji Hindi" ], "iso639_3": "hif", "localnames": [ "Fiji Baat" ], - "name": "Hindi, Fiji", - "names": [ "Fiji Baat", "Fiji Hindi", "Fiji Hindustani" ], + "name": "Fiji Hindi", + "names": [ "Fiji Baat", "Fiji Hindustani" ], "region": "FJ", "regionname": "Fiji", "regions": [ "US" ], @@ -42970,8 +44861,8 @@ "full": "hif-Deva-FJ", "iana": [ "Fiji Hindi" ], "iso639_3": "hif", - "name": "Hindi, Fiji", - "names": [ "Fiji Baat", "Fiji Hindi", "Fiji Hindustani" ], + "name": "Fiji Hindi", + "names": [ "Fiji Baat", "Fiji Hindustani" ], "nophonvars": true, "region": "FJ", "regionname": "Fiji", @@ -42985,10 +44876,10 @@ "full": "hig-Latn-NG", "iana": [ "Kamwe" ], "iso639_3": "hig", - "localname": "Kamwǝ", - "localnames": [ "Kamwǝ", "Vәcәmwe" ], + "localname": "Kamwə", + "localnames": [ "Kamwə", "Vәcәmwe" ], "name": "Kamwe", - "names": [ "Hiji", "Kamwǝ", "Vacamwe" ], + "names": [ "Hiji", "Kamwə", "Vәcәmwe" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -43004,7 +44895,7 @@ "localname": "Pamosu", "localnames": [ "Pamosu" ], "name": "Pamosu", - "names": [ "Hinihon" ], + "names": [ "Ambon", "Hinihon" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -43014,18 +44905,32 @@ "windows": "hih-Latn" }, { - "full": "hii-Zyyy-IN", + "full": "hii-Takr-IN", "iana": [ "Hinduri" ], "iso639_3": "hii", "name": "Hinduri", "names": [ "Handuri" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Takr", "sldr": false, "tag": "hii", - "tags": [ "hii-IN", "hii-Zyyy" ], - "windows": "hii-Zyyy" + "tags": [ "hii-IN", "hii-Takr" ], + "windows": "hii-Takr" + }, + { + "full": "hii-Deva-IN", + "iana": [ "Hinduri" ], + "iso639_3": "hii", + "name": "Hinduri", + "names": [ "Handuri" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "hii-Deva", + "windows": "hii-Deva" }, { "full": "hij-Latn-CM", @@ -43092,7 +44997,7 @@ "iana": [ "Tsoa" ], "iso639_3": "hio", "name": "Tshuwau", - "names": [ "Amasili", "Bakhwa", "Basarwa", "Chirechire", "Chuwau", "Chware", "Gabake-Ntshori", "Gǁabake", "Haitshuari", "Haitshuwau", "Hiechware", "Hietshware", "Hiochuwau", "Hiotshuwau", "Kalahari Khoe", "Khoisan", "Kwe", "Kwe-Etshori Kwee", "Kwe-Tshori", "Masarwa", "Sesarwa", "Tati", "Tati Bushman", "Tshikwa", "Tshwa", "Tshwao", "Tshwawo", "Tsoa", "Tsua" ], + "names": [ "Amasili", "Bakhwa", "Basarwa", "Chirechire", "Chuwau", "Chware", "Gabake-Ntshori", "Gǁabake", "Haitshuari", "Haitshuwau", "Hiechware", "Hietshware", "Hiochuwau", "Hiotshuwau", "Kalahari Khoe", "Khoisan", "Koi-san", "Koisan", "Kwe", "Kwe-Etshori Kwee", "Kwe-Tshori", "Masarwa", "Sesarwa", "Tati", "Tati Bushman", "Tjwao", "Tshikwa", "Tshwa", "Tshwao", "Tshwawo", "Tsoa", "Tsua" ], "region": "BW", "regionname": "Botswana", "regions": [ "ZW" ], @@ -43165,8 +45070,9 @@ "full": "hji-Latn-ID", "iana": [ "Haji" ], "iso639_3": "hji", + "macrolang": "ms", "name": "Haji", - "names": [ "Aji" ], + "names": [ "Aji", "Malayisch" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -43205,6 +45111,48 @@ "tags": [ "hke-CD", "hke-Latn" ], "windows": "hke-Latn" }, + { + "full": "hkh-Arab-IN", + "iana": [ "Khah", "Poguli" ], + "iso639_3": "hkh", + "name": "Khah", + "names": [ "Banhali", "Khahistani", "Khash", "Khasha", "Khashali", "Kohistani", "Panchali", "Parbati", "Paristani", "Poguli" ], + "region": "IN", + "regionname": "India", + "script": "Arab", + "sldr": false, + "tag": "hkh", + "tags": [ "hkh-Arab", "hkh-IN" ], + "windows": "hkh-Arab" + }, + { + "full": "hkh-Deva-IN", + "iana": [ "Khah", "Poguli" ], + "iso639_3": "hkh", + "name": "Khah", + "names": [ "Banhali", "Khahistani", "Khash", "Khasha", "Khashali", "Kohistani", "Panchali", "Parbati", "Paristani", "Poguli" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "hkh-Deva", + "windows": "hkh-Deva" + }, + { + "full": "hkh-Latn-IN", + "iana": [ "Khah", "Poguli" ], + "iso639_3": "hkh", + "localnames": [ "Khah" ], + "name": "Khah", + "names": [ "Banhali", "Khahistani", "Khash", "Khasha", "Khashali", "Kohistani", "Panchali", "Parbati", "Paristani", "Poguli" ], + "region": "IN", + "regionname": "India", + "script": "Latn", + "sldr": false, + "tag": "hkh-Latn", + "windows": "hkh-Latn" + }, { "full": "hkk-Latn-PG", "iana": [ "Hunjara-Kaina Ke" ], @@ -43251,6 +45199,7 @@ "full": "hla-Latn-PG", "iana": [ "Halia" ], "iso639_3": "hla", + "localnames": [ "Halia" ], "name": "Halia", "names": [ "Tasi" ], "region": "PG", @@ -43312,9 +45261,9 @@ "full": "hlt-Latn-MM", "iana": [ "Matu Chin" ], "iso639_3": "hlt", - "localnames": [ "Matu Chin" ], + "localnames": [ "Matu", "Matu Chin" ], "name": "Chin, Matu", - "names": [ "Matu Chin", "Matupi", "Nga La" ], + "names": [ "Matu", "Matu Chin", "Matupi", "Nga La" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -43338,18 +45287,19 @@ "windows": "hlu-Hluw" }, { - "full": "hma-Zyyy-CN", + "full": "hma-Latn-CN", "iana": [ "Southern Mashan Hmong", "Southern Mashan Miao" ], "iso639_3": "hma", + "macrolang": "hmn", "name": "Miao, Southern Mashan", - "names": [ "Southern Mashan Hmong", "Southern Mashan Miao" ], + "names": [ "Hsiung", "Hunan Miao", "Southern Mashan Hmong", "Southern Mashan Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "hma", - "tags": [ "hma-CN", "hma-Zyyy" ], - "windows": "hma-Zyyy" + "tags": [ "hma-CN", "hma-Latn" ], + "windows": "hma-Latn" }, { "full": "hmb-Latn-ML", @@ -43370,8 +45320,9 @@ "full": "hmc-Zyyy-CN", "iana": [ "Central Huishui Hmong", "Central Huishui Miao" ], "iso639_3": "hmc", + "macrolang": "hmn", "name": "Miao, Central Huishui", - "names": [ "Central Huishui Hmong", "Central Huishui Miao" ], + "names": [ "Central Huishui Hmong", "Central Huishui Miao", "Hmo", "Hsiung", "Hunan Miao" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -43384,6 +45335,7 @@ "full": "hmd-Plrd-CN", "iana": [ "Large Flowery Miao", "A-hmaos", "Da-Hua Miao" ], "iso639_3": "hmd", + "macrolang": "hmn", "name": "Miao, Large Flowery", "names": [ "A-Hmao", "A-Hmaos", "A-hmaos", "Ad Hmaob lul", "Big Flowery Miao", "Da Hua Bei Miao", "Da Hua Miao", "Da-Hua Miao", "Diandongbei Miao", "Flowery Miao", "Great Flowery Tribe", "Hua Miao", "Hwa Miao", "Large Flowery Miao", "Northeastern Dian Miao", "Northeastern Yunnan Miao", "Ta Hwa Miao" ], "region": "CN", @@ -43398,6 +45350,7 @@ "full": "hmd-Hmng-CN", "iana": [ "Large Flowery Miao", "A-hmaos", "Da-Hua Miao" ], "iso639_3": "hmd", + "macrolang": "hmn", "name": "Miao, Large Flowery", "names": [ "A-Hmao", "A-Hmaos", "A-hmaos", "Ad Hmaob lul", "Big Flowery Miao", "Da Hua Bei Miao", "Da Hua Miao", "Da-Hua Miao", "Diandongbei Miao", "Flowery Miao", "Great Flowery Tribe", "Hua Miao", "Hwa Miao", "Large Flowery Miao", "Northeastern Dian Miao", "Northeastern Yunnan Miao", "Ta Hwa Miao" ], "nophonvars": true, @@ -43413,6 +45366,7 @@ "iana": [ "Large Flowery Miao", "A-hmaos", "Da-Hua Miao" ], "iso639_3": "hmd", "localnames": [ "A-Hmao" ], + "macrolang": "hmn", "name": "Miao, Large Flowery", "names": [ "A-Hmao", "A-Hmaos", "A-hmaos", "Ad Hmaob lul", "Big Flowery Miao", "Da Hua Bei Miao", "Da Hua Miao", "Da-Hua Miao", "Diandongbei Miao", "Flowery Miao", "Great Flowery Tribe", "Hua Miao", "Hwa Miao", "Large Flowery Miao", "Northeastern Dian Miao", "Northeastern Yunnan Miao", "Ta Hwa Miao" ], "region": "CN", @@ -43422,10 +45376,26 @@ "tag": "hmd-Latn", "windows": "hmd-Latn" }, + { + "full": "hmd-Plrd-CN-x-normal", + "iana": [ "Miao, Large Flowery, Normalised orthography" ], + "iso639_3": "hmd", + "macrolang": "hmn", + "name": "Miao, Large Flowery", + "names": [ "A-Hmao", "A-Hmaos", "Ad Hmaob lul", "Big Flowery Miao", "Da Hua Bei Miao", "Da Hua Miao", "Diandongbei Miao", "Flowery Miao", "Great Flowery Tribe", "Hua Miao", "Hwa Miao", "Miao, Large Flowery, Normalised orthography", "Northeastern Dian Miao", "Northeastern Yunnan Miao", "Ta Hwa Miao" ], + "region": "CN", + "regionname": "China", + "script": "Plrd", + "sldr": false, + "tag": "hmd-x-normal", + "tags": [ "hmd-CN-x-normal", "hmd-Plrd-x-normal" ], + "windows": "hmd-Plrd-x-normal" + }, { "full": "hme-Zyyy-CN", "iana": [ "Eastern Huishui Hmong", "Eastern Huishui Miao" ], "iso639_3": "hme", + "macrolang": "hmn", "name": "Miao, Eastern Huishui", "names": [ "Eastern Huishui Hmong", "Eastern Huishui Miao" ], "region": "CN", @@ -43441,6 +45411,7 @@ "iana": [ "Hmong Don" ], "iso639_3": "hmf", "name": "Hmong Don", + "names": [ "Hua Binh", "Hunan Miao", "Nghia Lo", "YenBai" ], "region": "VN", "regionname": "Viet Nam", "script": "Latn", @@ -43454,6 +45425,7 @@ "full": "hmg-Zyyy-CN", "iana": [ "Southwestern Guiyang Hmong" ], "iso639_3": "hmg", + "macrolang": "hmn", "name": "Miao, Southwestern Guiyang", "names": [ "Southwestern Guiyang Hmong" ], "region": "CN", @@ -43468,6 +45440,7 @@ "full": "hmh-Zyyy-CN", "iana": [ "Southwestern Huishui Hmong", "Southwestern Huishui Miao" ], "iso639_3": "hmh", + "macrolang": "hmn", "name": "Miao, Southwestern Huishui", "names": [ "Miao", "Southwestern Huishui Hmong", "Southwestern Huishui Miao" ], "region": "CN", @@ -43482,8 +45455,9 @@ "full": "hmi-Zyyy-CN", "iana": [ "Northern Huishui Hmong", "Northern Huishui Miao" ], "iso639_3": "hmi", + "macrolang": "hmn", "name": "Miao, Northern Huishui", - "names": [ "Northern Huishui Hmong", "Northern Huishui Miao" ], + "names": [ "Gaopo", "Kwei-chu", "Kwei-chu Miao", "Northern Huishui Hmong", "Northern Huishui Miao" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -43496,6 +45470,7 @@ "full": "hmj-Bopo-CN", "iana": [ "Ge", "Gejia" ], "iso639_3": "hmj", + "macrolang": "hmn", "name": "Ge", "names": [ "Chonganjiang Miao", "Ge Jia", "Ge-Mong", "Gedang", "Gedong", "Gedou", "Gedou Miao", "Gedoudiu", "Gedu", "Gejia", "Gho-mhon", "Keh Deo" ], "obsolete": true, @@ -43524,6 +45499,7 @@ "full": "hml-Zyyy-CN", "iana": [ "Luopohe Hmong", "Luopohe Miao" ], "iso639_3": "hml", + "macrolang": "hmn", "name": "Miao, Luopohe", "names": [ "Lobohe Miao", "Luobo River Miao", "Luobohe Hmong", "Luopohe Hmong", "Luopohe Miao", "Xi", "Xijia Miao", "Ximahe Miao" ], "region": "CN", @@ -43535,18 +45511,19 @@ "windows": "hml-Zyyy" }, { - "full": "hmm-Zyyy-CN", + "full": "hmm-Latn-CN", "iana": [ "Central Mashan Hmong", "Central Mashan Miao" ], "iso639_3": "hmm", + "macrolang": "hmn", "name": "Miao, Central Mashan", "names": [ "Central Mashan Hmong", "Central Mashan Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "hmm", - "tags": [ "hmm-CN", "hmm-Zyyy" ], - "windows": "hmm-Zyyy" + "tags": [ "hmm-CN", "hmm-Latn" ], + "windows": "hmm-Latn" }, { "full": "hmn-Latn-CN", @@ -43566,8 +45543,8 @@ "full": "hmn-Bopo-CN", "iana": [ "Hmong", "Mong" ], "iso639_3": "hmn", - "name": "Miao, Northern Qiandong", - "names": [ "Black Miao", "Central Miao", "Chientung Miao", "East Guizhou Miao", "Eastern Guizhou Hmu", "Gha Ne", "Gha Ne Dlai", "Heh Miao", "Hei Miao", "Hmong", "Hmu", "Kuv tsis has lug Moob tau", "Mong", "Northern East Guizhou Miao", "Northern Hmu", "Qián-Dōng fāngyán", "m̥hu˧", "m̥ə˧" ], + "name": "Hmong", + "names": [ "Black Miao", "Central Miao", "Chientung Miao", "East Guizhou Miao", "Eastern Guizhou Hmu", "Gha Ne", "Gha Ne Dlai", "Heh Miao", "Hei Miao", "Hmu", "Kuv tsis has lug Moob tau", "Miao, Northern Qiandong", "Mong", "Northern East Guizhou Miao", "Northern Hmu", "Qián-Dōng fāngyán", "m̥hu˧", "m̥ə˧" ], "nophonvars": true, "obsolete": true, "region": "CN", @@ -43594,23 +45571,25 @@ "windows": "hmn-Hmng" }, { - "full": "hmp-Zyyy-CN", + "full": "hmp-Latn-CN", "iana": [ "Northern Mashan Hmong", "Northern Mashan Miao" ], "iso639_3": "hmp", + "macrolang": "hmn", "name": "Miao, Northern Mashan", "names": [ "Northern Mashan Hmong", "Northern Mashan Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "hmp", - "tags": [ "hmp-CN", "hmp-Zyyy" ], - "windows": "hmp-Zyyy" + "tags": [ "hmp-CN", "hmp-Latn" ], + "windows": "hmp-Latn" }, { "full": "hmq-Bopo-CN", "iana": [ "Eastern Qiandong Miao" ], "iso639_3": "hmq", + "macrolang": "hmn", "name": "Miao, Eastern Qiandong", "names": [ "Black Miao", "Central Miao", "Chientung Miao", "Eastern East-Guizhou Miao", "Eastern Hmu", "Eastern Qiandong Miao", "Hei Miao", "Hmu", "mo˧" ], "obsolete": true, @@ -43641,6 +45620,7 @@ "full": "hms-Latn-CN", "iana": [ "Southern Qiandong Miao" ], "iso639_3": "hms", + "macrolang": "hmn", "name": "Miao, Southern Qiandong", "names": [ "Black Miao", "Central Miao", "Chientung Miao", "Hei Miao", "Hmu", "Southern East-Guizhou Miao", "Southern Hmu", "Southern Qiandong Miao" ], "region": "CN", @@ -43655,6 +45635,8 @@ "full": "hmt-Latn-PG", "iana": [ "Hamtai" ], "iso639_3": "hmt", + "localname": "Hamtai", + "localnames": [ "Hamtai" ], "name": "Hamtai", "names": [ "Hamday", "Kamea", "Kapau", "Watut" ], "region": "PG", @@ -43694,39 +45676,42 @@ "windows": "hmv-Latn" }, { - "full": "hmw-Zyyy-CN", + "full": "hmw-Latn-CN", "iana": [ "Western Mashan Hmong", "Western Mashan Miao" ], "iso639_3": "hmw", + "macrolang": "hmn", "name": "Miao, Western Mashan", "names": [ "Western Mashan Hmong", "Western Mashan Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "hmw", - "tags": [ "hmw-CN", "hmw-Zyyy" ], + "tags": [ "hmw-CN", "hmw-Latn" ], "unwritten": true, - "windows": "hmw-Zyyy" + "windows": "hmw-Latn" }, { - "full": "hmy-Zyyy-CN", + "full": "hmy-Latn-CN", "iana": [ "Southern Guiyang Hmong", "Southern Guiyang Miao" ], "iso639_3": "hmy", + "macrolang": "hmn", "name": "Miao, Southern Guiyang", "names": [ "Southern Guiyang Hmong", "Southern Guiyang Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "hmy", - "tags": [ "hmy-CN", "hmy-Zyyy" ], - "windows": "hmy-Zyyy" + "tags": [ "hmy-CN", "hmy-Latn" ], + "windows": "hmy-Latn" }, { "full": "hmz-Latn-CN", "iana": [ "Hmong Shua", "Sinicized Miao" ], "iso639_3": "hmz", "localnames": [ "Hmongb Shuat" ], + "macrolang": "hmn", "name": "Sinicized Miao", "names": [ "Biantou Miao", "Changshu Miao", "Curved Comb Miao", "Flat Head Miao", "Han Miao", "Hmong Nzi", "Hmong Sa", "Hmong Shua", "Hmong Sua", "Hmongb Shuat", "Long Comb Miao", "Lopsided Comb Miao", "Mushu Maio", "Piān Miáo", "Shuixi Miao", "Sinicised Hmong", "Waishu Miao", "Water Miao", "West of the Water Miao", "Wooden Comb Miao" ], "region": "CN", @@ -43734,13 +45719,15 @@ "regions": [ "VN" ], "script": "Latn", "sldr": false, - "tag": "hmz-Latn", + "tag": "hmz", + "tags": [ "hmz-CN", "hmz-Latn" ], "windows": "hmz-Latn" }, { "full": "hmz-Plrd-CN", "iana": [ "Hmong Shua", "Sinicized Miao" ], "iso639_3": "hmz", + "macrolang": "hmn", "name": "Sinicized Miao", "names": [ "Biantou Miao", "Changshu Miao", "Curved Comb Miao", "Flat Head Miao", "Han Miao", "Hmong Nzi", "Hmong Sa", "Hmong Shua", "Hmong Sua", "Hmongb Shuat", "Long Comb Miao", "Lopsided Comb Miao", "Mushu Maio", "Piān Miáo", "Shuixi Miao", "Sinicised Hmong", "Waishu Miao", "Water Miao", "West of the Water Miao", "Wooden Comb Miao" ], "nophonvars": true, @@ -43773,6 +45760,7 @@ "iso639_3": "hnd", "latnnames": [ "Hindko" ], "localnames": [ "ہندکو" ], + "macrolang": "lah", "name": "Hindko, Southern", "names": [ "Hindko", "Southern Hindko" ], "region": "PK", @@ -43801,6 +45789,7 @@ }, { "full": "hng-Latn-AO", + "iana": [ "Hungu" ], "iso639_3": "hng", "name": "Hungu", "names": [ "Bahungu", "Dihungu", "Kihungu" ], @@ -43844,26 +45833,45 @@ "tags": [ "hni-CN", "hni-Latn" ], "windows": "hni-Latn" }, + { + "full": "hnj-Hmnp-LA", + "iana": [ "Hmong Njua", "Mong Leng", "Mong Njua" ], + "iso639_3": "hnj", + "macrolang": "hmn", + "name": "Hmong Njua", + "names": [ "Blue Hmong", "Ching Miao", "Green Hmong", "Hmong Leng", "Hmong Nzhua", "Hmongb Nzhuab", "Hmoob Leeg", "Lu Man Zi", "Lu Miao", "Miao Tsi", "Mong Leng", "Mong Njua", "Mong Ntsua", "Moob Leeg", "Qing Miao", "Tak Miao" ], + "region": "LA", + "regionname": "Lao People's Democratic Republic", + "regions": [ "CA", "CN", "GF", "MM", "TH", "US", "VN" ], + "script": "Hmnp", + "sldr": false, + "tag": "hnj", + "tags": [ "hnj-Hmnp", "hnj-LA" ], + "variants": [ "pahawh2", "pahawh3", "pahawh4" ], + "windows": "hnj-Hmnp-LA" + }, { "full": "hnj-Hmng-LA", "iana": [ "Hmong Njua", "Mong Leng", "Mong Njua" ], "iso639_3": "hnj", + "macrolang": "hmn", "name": "Hmong Njua", "names": [ "Blue Hmong", "Ching Miao", "Green Hmong", "Hmong Leng", "Hmong Nzhua", "Hmongb Nzhuab", "Hmoob Leeg", "Lu Man Zi", "Lu Miao", "Miao Tsi", "Mong Leng", "Mong Njua", "Mong Ntsua", "Moob Leeg", "Qing Miao", "Tak Miao" ], + "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", "regions": [ "CA", "CN", "GF", "MM", "TH", "US", "VN" ], "script": "Hmng", "sldr": false, - "tag": "hnj", - "tags": [ "hnj-Hmng", "hnj-LA" ], + "tag": "hnj-Hmng", "variants": [ "pahawh2", "pahawh3", "pahawh4" ], - "windows": "hnj-Hmng" + "windows": "hnj-Hmng-LA" }, { "full": "hnj-Laoo-LA", "iana": [ "Hmong Njua", "Mong Leng", "Mong Njua" ], "iso639_3": "hnj", + "macrolang": "hmn", "name": "Hmong Njua", "names": [ "Blue Hmong", "Ching Miao", "Green Hmong", "Hmong Leng", "Hmong Nzhua", "Hmongb Nzhuab", "Hmoob Leeg", "Lu Man Zi", "Lu Miao", "Miao Tsi", "Mong Leng", "Mong Njua", "Mong Ntsua", "Moob Leeg", "Qing Miao", "Tak Miao" ], "nophonvars": true, @@ -43873,13 +45881,14 @@ "script": "Laoo", "sldr": false, "tag": "hnj-Laoo", - "windows": "hnj-Laoo" + "windows": "hnj-Laoo-LA" }, { "full": "hnj-Latn-LA", "iana": [ "Hmong Njua", "Mong Leng", "Mong Njua" ], "iso639_3": "hnj", "localnames": [ "Hmongb Nzhuab", "Moob Leeg" ], + "macrolang": "hmn", "name": "Hmong Njua", "names": [ "Blue Hmong", "Ching Miao", "Green Hmong", "Hmong Leng", "Hmong Nzhua", "Hmongb Nzhuab", "Hmoob Leeg", "Lu Man Zi", "Lu Miao", "Miao Tsi", "Mong Leng", "Mong Njua", "Mong Ntsua", "Moob Leeg", "Qing Miao", "Tak Miao" ], "region": "LA", @@ -43888,12 +45897,13 @@ "script": "Latn", "sldr": false, "tag": "hnj-Latn", - "windows": "hnj-Latn" + "windows": "hnj-Latn-LA" }, { "full": "hnj-Plrd-CN", "iana": [ "Hmong Njua", "Mong Leng", "Mong Njua" ], "iso639_3": "hnj", + "macrolang": "hmn", "name": "Hmong Njua", "names": [ "Blue Hmong", "Blue Meo", "Ching Miao", "Green Hmong", "Green Meo", "Hmong Leng", "Hmong Nzhua", "Hmongb Nzhuab", "Hmoob Leeg", "Lu Man Zi", "Lu Miao", "Meo Dam", "Meo Lai", "Miao Tsi", "Mong Leng", "Mong Njua", "Mong Ntsua", "Moob Leeg", "Qing Miao", "Tak Miao" ], "nophonvars": true, @@ -43904,12 +45914,13 @@ "sldr": false, "tag": "hnj-Plrd", "tags": [ "hnj-CN" ], - "windows": "hnj-Plrd" + "windows": "hnj-Plrd-CN" }, { "full": "hnj-Thai-TH", "iana": [ "Hmong Njua", "Mong Leng", "Mong Njua" ], "iso639_3": "hnj", + "macrolang": "hmn", "name": "Hmong Njua", "names": [ "Blue Hmong", "Ching Miao", "Green Hmong", "Hmong Leng", "Hmong Nzhua", "Hmongb Nzhuab", "Hmoob Leeg", "Lu Man Zi", "Lu Miao", "Miao Tsi", "Mong Leng", "Mong Njua", "Mong Ntsua", "Moob Leeg", "Qing Miao", "Tak Miao" ], "nophonvars": true, @@ -43920,14 +45931,14 @@ "sldr": false, "tag": "hnj-Thai", "tags": [ "hnj-TH" ], - "windows": "hnj-Thai" + "windows": "hnj-Thai-TH" }, { "full": "hnn-Latn-PH", "iana": [ "Hanunoo" ], "iso639_3": "hnn", "name": "Hanunoo", - "names": [ "Hanonoo", "Hanunoo-Mangyan", "Mangyan", "minaŋyan" ], + "names": [ "Hanonoo", "Hanunoo-Mangyan", "Hanunó’o", "Mangyan", "minaŋyan" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -43941,7 +45952,7 @@ "iana": [ "Hanunoo" ], "iso639_3": "hnn", "name": "Hanunoo", - "names": [ "Hanonoo", "Hanunoo-Mangyan", "Mangyan", "minaŋyan" ], + "names": [ "Hanonoo", "Hanunoo-Mangyan", "Hanunó’o", "Mangyan", "minaŋyan" ], "nophonvars": true, "region": "PH", "regionname": "Philippines", @@ -43955,8 +45966,9 @@ "iana": [ "Northern Hindko" ], "iso639_3": "hno", "latnnames": [ "Hindko" ], - "localname": "ہندکو‬", - "localnames": [ "ہندکو‬" ], + "localname": "ہندکو", + "localnames": [ "ہندکو" ], + "macrolang": "lah", "name": "Hindko, Northern", "names": [ "Hazara Hindko", "Hindko", "Kagani", "Kaghani", "Northern Hindko" ], "region": "PK", @@ -43971,7 +45983,7 @@ "full": "hns-Latn-SR", "iana": [ "Caribbean Hindustani" ], "iso639_3": "hns", - "localname": "Hindustani, Caribbean", + "localname": "Sarnaanie Hiendoestaanie", "localnames": [ "Sarnaanie Hiendoestaanie" ], "name": "Hindustani, Sarnami", "names": [ "Aili Gaili", "Caribbean Bharatiya", "Caribbean Hindi", "Caribbean Hindustani", "Caribbean Urdu", "Guyanese Hindustani", "Sarnaanie Hiendoestaanie", "Sarnami Hindi", "Sarnami Hindoestani", "Trinidad Bhojpuri", "Trinidadian Bhojpuri", "Trinidadian Hindustani" ], @@ -44159,7 +46171,7 @@ "windows": "hoe-Latn" }, { - "full": "hoh-Zyyy-OM", + "full": "hoh-Arab-OM", "iana": [ "Hobyót" ], "iso639_3": "hoh", "name": "Hobyót", @@ -44167,18 +46179,18 @@ "region": "OM", "regionname": "Oman", "regions": [ "YE" ], - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "hoh", - "tags": [ "hoh-OM", "hoh-Zyyy" ], - "windows": "hoh-Zyyy" + "tags": [ "hoh-Arab", "hoh-OM" ], + "windows": "hoh-Arab" }, { "full": "hoi-Latn-US", "iana": [ "Holikachuk" ], "iso639_3": "hoi", "name": "Holikachuk", - "names": [ "Innoko" ], + "names": [ "Doogh Qinag", "Innoko" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -44194,6 +46206,7 @@ "iso639_3": "hoj", "latnnames": [ "Haroti" ], "localnames": [ "हाड़ौती" ], + "macrolang": "raj", "name": "Haroti", "names": [ "Hadauti", "Hadothi", "Hadoti", "Hara", "Harauti" ], "region": "IN", @@ -44251,6 +46264,7 @@ "full": "hop-Latn-US", "iana": [ "Hopi" ], "iso639_3": "hop", + "localname": "Hopílavayi", "localnames": [ "Hopílavayi" ], "name": "Hopi", "names": [ "Hopílavayi" ], @@ -44295,10 +46309,10 @@ "full": "hot-Latn-PG", "iana": [ "Hote", "Malê" ], "iso639_3": "hot", - "localname": "Malê", - "localnames": [ "Malê" ], - "name": "Hote", - "names": [ "Hotec", "Ho’tei", "Malei", "Malei-Hote", "Malê" ], + "localname": "Malei", + "localnames": [ "Malei" ], + "name": "Malei", + "names": [ "Hote", "Hotec", "Ho’tei", "Malei-Hote", "Malê" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -44366,26 +46380,26 @@ "windows": "hoz-Zyyy" }, { - "full": "hpo-Zyyy-MM", + "full": "hpo-Mymr-MM", "iana": [ "Hpon" ], "iso639_3": "hpo", "name": "Hpon", - "names": [ "Hpön", "Megyaw", "Phon", "Phun", "Phön", "Samong" ], + "names": [ "Hpun", "Hpön", "Megyaw", "Phon", "Phun", "Phön", "Samong" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "hpo", - "tags": [ "hpo-MM", "hpo-Zyyy" ], + "tags": [ "hpo-MM", "hpo-Mymr" ], "unwritten": true, - "windows": "hpo-Zyyy" + "windows": "hpo-Mymr" }, { "full": "hps-Zxxx-US", "iana": [ "Hawai'i Sign Language (HSL)", "Hawai'i Pidgin Sign Language" ], "iso639_3": "hps", - "name": "Hawaii Sign Language", - "names": [ "HPS", "HSL", "Hawai'i Pidgin Sign Language", "Hawai'i Sign Language (HSL)", "Hawaiian Sign Language", "Hawai’i Pidgin Sign Language" ], + "name": "Hawai‘i Sign Language", + "names": [ "HPS", "HSL", "Hawai'i Pidgin Sign Language", "Hawai'i Sign Language (HSL)", "Hawaii Sign Language", "Hawaiian Sign Language", "Hawai‘i Pidgin Sign Language" ], "region": "US", "regionname": "United States", "script": "Zxxx", @@ -44400,6 +46414,7 @@ "iso639_3": "hrv", "localname": "hrvatski", "localnames": [ "Hrvatski" ], + "macrolang": "sh", "name": "Croatian", "names": [ "Hrvatski", "Serbo-Croatian" ], "region": "HR", @@ -44410,7 +46425,7 @@ "suppress": true, "tag": "hr", "tags": [ "hr-HR", "hr-Latn" ], - "windows": "hr" + "windows": "hr-HR" }, { "full": "hr-Latn-BA", @@ -44418,19 +46433,20 @@ "iso639_3": "hrv", "localname": "hrvatski", "localnames": [ "Hrvatski" ], + "macrolang": "sh", "name": "Croatian", "region": "BA", "regionname": "Bosnia and Herzegovina", "script": "Latn", "sldr": true, - "suppress": true, "tag": "hr-BA", - "windows": "hr-BA" + "windows": "hr-Latn-BA" }, { "full": "hr-Brai-HR", "iana": [ "Croatian" ], "iso639_3": "hrv", + "macrolang": "sh", "name": "Croatian", "names": [ "Hrvatski", "Serbo-Croatian" ], "nophonvars": true, @@ -44440,7 +46456,7 @@ "script": "Brai", "sldr": false, "tag": "hr-Brai", - "windows": "hr-Brai" + "windows": "hr-Brai-HR" }, { "full": "hra-Latn-IN", @@ -44461,7 +46477,7 @@ "iana": [ "Niwer Mil" ], "iso639_3": "hrc", "name": "Niwer Mil", - "names": [ "Tanga", "Tangga" ], + "names": [ "Tanga", "Tangga", "Tuak" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -44480,7 +46496,7 @@ "region": "VN", "regionname": "Viet Nam", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "hre", "tags": [ "hre-Latn", "hre-Latn-VN-x-1968", "hre-Latn-x-1968", "hre-VN", "hre-VN-x-1968", "hre-x-1968" ], "windows": "hre-Latn" @@ -44515,19 +46531,36 @@ "windows": "hrk-Latn" }, { - "full": "hrm-Zyyy-CN", + "full": "hrm-Latn-CN", "iana": [ "Horned Miao" ], "iso639_3": "hrm", + "macrolang": "hmn", "name": "Miao, Horned", "names": [ "A-Hmo", "Bai Miao", "Changjiao Miao", "Forest Miao", "Hmo", "Hmong Khua Shua Ndrang", "Hmong Ndong", "Hmong Ndou", "Hmong Sou", "Horned Miao", "Jiao Miao", "Jiaojiao Miao", "Kha-Nzi", "Longhorn Miao", "Qing Miao", "White Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "hrm", - "tags": [ "hrm-CN", "hrm-Zyyy" ], + "tags": [ "hrm-CN", "hrm-Latn" ], + "unwritten": true, + "windows": "hrm-Latn" + }, + { + "full": "hrm-Hmng-CN", + "iana": [ "Horned Miao" ], + "iso639_3": "hrm", + "macrolang": "hmn", + "name": "Miao, Horned", + "names": [ "A-Hmo", "Bai Miao", "Changjiao Miao", "Forest Miao", "Hmo", "Hmong Khua Shua Ndrang", "Hmong Ndong", "Hmong Ndou", "Hmong Sou", "Horned Miao", "Jiao Miao", "Jiaojiao Miao", "Kha-Nzi", "Longhorn Miao", "Qing Miao", "White Miao" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Hmng", + "sldr": false, + "tag": "hrm-Hmng", "unwritten": true, - "windows": "hrm-Zyyy" + "windows": "hrm-Hmng" }, { "full": "hro-Latn-VN", @@ -44593,7 +46626,7 @@ "iana": [ "Warwar Feni" ], "iso639_3": "hrw", "name": "Warwar Feni", - "names": [ "Anir", "Feni", "Tangga" ], + "names": [ "Anir", "Feni", "Tangga", "Tuak" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -44623,6 +46656,7 @@ "iana": [ "Harzani" ], "iso639_3": "hrz", "name": "Harzani", + "names": [ "Harzandi" ], "region": "IR", "regionname": "Islamic Republic of Iran", "script": "Arab", @@ -44682,6 +46716,7 @@ "full": "hsn-Hans-CN", "iana": [ "Xiang Chinese" ], "iso639_3": "hsn", + "macrolang": "zh", "name": "Chinese, Xiang", "names": [ "Hsiang", "Hunan", "Hunanese", "Xiang Chinese" ], "region": "CN", @@ -44697,6 +46732,7 @@ "full": "hsn-Hant-CN", "iana": [ "Xiang Chinese" ], "iso639_3": "hsn", + "macrolang": "zh", "name": "Chinese, Xiang", "names": [ "Hsiang", "Hunan", "Hunanese", "Xiang Chinese" ], "nophonvars": true, @@ -44709,33 +46745,18 @@ "windows": "hsn-Hant" }, { - "full": "hsn-Nshu-CN", - "iana": [ "Xiang Chinese" ], - "iso639_3": "hsn", - "name": "Chinese, Xiang", - "names": [ "Hsiang", "Hunan", "Hunanese", "Xiang Chinese" ], - "nophonvars": true, - "region": "CN", - "regionname": "China", - "regions": [ "US" ], - "script": "Nshu", - "sldr": false, - "tag": "hsn-Nshu", - "windows": "hsn-Nshu" - }, - { - "full": "hss-Zyyy-OM", + "full": "hss-Arab-OM", "iana": [ "Harsusi" ], "iso639_3": "hss", "name": "Harsusi", "names": [ "Harsi ’Aforit", "Hersyet", "South Arabian" ], "region": "OM", "regionname": "Oman", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "hss", - "tags": [ "hss-OM", "hss-Zyyy" ], - "windows": "hss-Zyyy" + "tags": [ "hss-Arab", "hss-OM" ], + "windows": "hss-Arab" }, { "full": "ht-Latn-HT", @@ -44772,6 +46793,7 @@ "full": "hto-Latn-CO", "iana": [ "Minica Huitoto" ], "iso639_3": "hto", + "localname": "Huitoto Mɨnɨca", "localnames": [ "Huitoto Mɨnɨca" ], "name": "Witoto, Minika", "names": [ "Huitoto Meneca", "Huitoto Mɨnɨca", "Meneca", "Meneka", "Minica Huitoto", "Minika", "Mɨnɨca", "Mɨnɨca Huitoto", "Noaiko-Muína", "Southern Witoto", "Witoto Meneca" ], @@ -44813,17 +46835,17 @@ "windows": "htu-Latn" }, { - "full": "htx-Zyyy-TR", + "full": "htx-Xsux-TR", "iana": [ "Middle Hittite" ], "iso639_3": "htx", "name": "Middle Hittite", "region": "TR", "regionname": "Turkey", - "script": "Zyyy", + "script": "Xsux", "sldr": false, "tag": "htx", - "tags": [ "htx-TR", "htx-Zyyy" ], - "windows": "htx-Zyyy" + "tags": [ "htx-TR", "htx-Xsux" ], + "windows": "htx-Xsux" }, { "full": "hu-Latn-HU", @@ -44876,10 +46898,10 @@ }, { "full": "huc-Latn-BW", - "iana": [ "ǂHua" ], + "iana": [ "ǂHua", "ǂʼAmkhoe" ], "iso639_3": "huc", "name": "ǂ’Amkhoe", - "names": [ "Eastern ‡Hoan", "ǀHua", "ǀHû", "ǂHua", "‡Hoa", "‡Hoan", "‡Hoã", "‡Hua", "‡Hua-Owani" ], + "names": [ "Eastern ‡Hoan", "ǀHua", "ǀHû", "ǂHua", "ǂʼAmkhoe", "‡Hoa", "‡Hoan", "‡Hoã", "‡Hua", "‡Hua-Owani" ], "region": "BW", "regionname": "Botswana", "script": "Latn", @@ -44953,7 +46975,7 @@ "iana": [ "Huilliche" ], "iso639_3": "huh", "name": "Huilliche", - "names": [ "Huiliche", "Veliche" ], + "names": [ "Huilche", "Huiliche", "Veliche" ], "region": "CL", "regionname": "Chile", "script": "Latn", @@ -44983,6 +47005,7 @@ "full": "huj-Zyyy-CN", "iana": [ "Northern Guiyang Hmong", "Northern Guiyang Miao" ], "iso639_3": "huj", + "macrolang": "hmn", "name": "Miao, Northern Guiyang", "names": [ "Northern Guiyang Hmong", "Northern Guiyang Miao" ], "region": "CN", @@ -45012,8 +47035,8 @@ "iana": [ "Hula" ], "iso639_3": "hul", "localnames": [ "Vula'a" ], - "name": "Hula", - "names": [ "Vulaa", "Vula’a" ], + "name": "Vula’a", + "names": [ "Bula’a", "Hula", "Vulaa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -45027,7 +47050,7 @@ "iana": [ "Hungana" ], "iso639_3": "hum", "name": "Hungana", - "names": [ "Huana", "Hungaan", "Hungan", "Hunganna", "Kihungana" ], + "names": [ "Huana", "Hungaan", "Hungan", "Hunganna", "Kihungan", "Kihungana" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -45041,6 +47064,7 @@ "iana": [ "Hu" ], "iso639_3": "huo", "name": "Hu", + "names": [ "Angku", "Āng-kú" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -45055,7 +47079,7 @@ "iana": [ "Hupa" ], "iso639_3": "hup", "name": "Hupa", - "names": [ "Hoopa", "Na:tinixwe Mixine:whe’" ], + "names": [ "Hoopa", "Hoopa-Chilula", "Hupa-Chilula-Whilkut", "Na:tinixwe Mixine:whe’" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -45114,26 +47138,56 @@ "windows": "hus-Latn" }, { - "full": "hut-Zyyy-NP", + "full": "hus-Latn-MX-x-central", + "iana": [ "Huastec" ], + "iso639_3": "hus", + "localnames": [ "Teenek" ], + "name": "Huastec", + "names": [ "Teenek", "Tenek", "Tének" ], + "region": "MX", + "regionname": "Mexico", + "rod": "4681", + "script": "Latn", + "sldr": false, + "tag": "hus-x-central", + "tags": [ "hus-Latn-x-central", "hus-MX-x-central" ], + "windows": "hus-Latn-x-central" + }, + { + "full": "hut-Deva-NP", "iana": [ "Humla" ], "iso639_3": "hut", "name": "Humla", - "names": [ "Dangali", "Humli Khyampa", "Phoke" ], + "names": [ "Bhote", "Humla Tibetan", "Humli Lama", "Kham", "Phoke" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "hut", - "tags": [ "hut-NP", "hut-Zyyy" ], + "tags": [ "hut-Deva", "hut-NP" ], "unwritten": true, - "windows": "hut-Zyyy" + "windows": "hut-Deva" + }, + { + "full": "hut-Tibt-NP", + "iana": [ "Humla" ], + "iso639_3": "hut", + "name": "Humla", + "names": [ "Bhote", "Humla Tibetan", "Humli Lama", "Kham", "Phoke" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Tibt", + "sldr": false, + "tag": "hut-Tibt", + "windows": "hut-Tibt" }, { "full": "huu-Latn-PE", "iana": [ "Murui Huitoto" ], "iso639_3": "huu", "localname": "Murui", - "localnames": [ "Huitoto Murui", "Murui" ], + "localnames": [ "Bue", "Murui" ], "name": "Witoto, Murui", "names": [ "Bue", "Central Witoto proper", "Huitoto", "Huitoto Murai", "Huitoto Murui", "Komïne", "Murai", "Murai Huitoto", "Murui", "Murui Huitoto", "Uitoto", "Witoto" ], "region": "PE", @@ -45149,7 +47203,7 @@ "full": "huv-Latn-MX", "iana": [ "San Mateo Del Mar Huave" ], "iso639_3": "huv", - "localname": "Huave, San Mateo del Mar", + "localname": "Ombeayiüts", "localnames": [ "Ombeayiüts" ], "name": "Huave, San Mateo del Mar", "names": [ "Huave del Oeste", "Ombeayiüts", "San Mateo Del Mar Huave" ], @@ -45318,10 +47372,10 @@ "full": "hwc-Latn-US", "iana": [ "Hawai'i Creole English", "Hawai'i Pidgin" ], "iso639_3": "hwc", - "localname": "Hawai’i Pidgin", - "localnames": [ "Pidgin" ], - "name": "Hawai’i Pidgin", - "names": [ "HCE", "Hawai'i Creole English", "Hawai'i Pidgin", "Hawaiian Creole", "Hawaiian Creole English", "Hawai’i Creole", "Hawai’i Creole English", "Pidgin", "ōlelo paʻi ʻai" ], + "localname": "Pidgin", + "localnames": [ "Hawaii Pidgin", "Pidgin" ], + "name": "Hawaii Pidgin", + "names": [ "HCE", "Hawai'i Creole English", "Hawai'i Pidgin", "Hawaiian Creole", "Hawaiian Creole English", "Hawaiian Pidgin", "Hawai’i Creole", "Hawai’i Creole English", "Pidgin", "ōlelo paʻi ʻai" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -45349,11 +47403,11 @@ "full": "hy-Armn-AM", "iana": [ "Armenian" ], "iso639_3": "hye", - "latnnames": [ "hayeren" ], + "latnnames": [ "Hayeren" ], "localname": "հայերեն", - "localnames": [ "հայերէն/հայերեն" ], + "localnames": [ "հայերեն" ], "name": "Armenian", - "names": [ "Armani", "Armjanski", "Armjanski Yazyk", "Eastern Armenian", "Ena", "Ermeni Dili", "Ermenice", "Erâmani", "Haieren", "Somekhuri", "Somkhuri" ], + "names": [ "Armani", "Armjanski", "Armjanski Yazyk", "Eastern Armenian", "Ermeni Dili", "Ermenice", "Erâmani", "Haieren", "Somekhuri", "Somkhuri ena" ], "region": "AM", "regionname": "Armenia", "regions": [ "AZ", "BG", "CA", "CY", "DE", "EE", "EG", "FR", "GE", "GR", "HN", "HU", "IL", "IN", "IQ", "IR", "JO", "KG", "KZ", "LB", "PL", "PS", "RO", "RU", "SY", "TJ", "TM", "TR", "UA", "US", "UZ" ], @@ -45370,7 +47424,7 @@ "iana": [ "Armenian" ], "iso639_3": "hye", "name": "Armenian", - "names": [ "Armani", "Armjanski", "Armjanski Yazyk", "Eastern Armenian", "Ena", "Ermeni Dili", "Ermenice", "Erâmani", "Haieren", "Somekhuri", "Somkhuri" ], + "names": [ "Armani", "Armjanski", "Armjanski Yazyk", "Eastern Armenian", "Ermeni Dili", "Ermenice", "Erâmani", "Haieren", "Somekhuri", "Somkhuri ena" ], "nophonvars": true, "region": "AM", "regionname": "Armenia", @@ -45406,7 +47460,7 @@ "names": [ "West Armenian", "Western Armenian" ], "region": "AM", "regionname": "Armenia", - "regions": [ "BG", "CA", "CY", "EG", "FR", "GE", "GR", "HU", "IL", "IQ", "JO", "LB", "PL", "PS", "RO", "RU", "SY", "TM", "TR", "UA", "US", "UZ" ], + "regions": [ "BG", "CA", "CY", "EG", "FR", "GE", "GR", "HU", "IL", "IQ", "JO", "KW", "LB", "PL", "PS", "RO", "RU", "SY", "TM", "TR", "UA", "US", "UZ" ], "script": "Armn", "sldr": false, "tag": "hyw", @@ -45419,7 +47473,7 @@ "iso639_3": "her", "localnames": [ "Otjiherero" ], "name": "Herero", - "names": [ "Himba", "Ochiherero", "Oluherero", "Otjiherero", "Otshiherero", "Ovaherero" ], + "names": [ "Himba", "Ochiherero", "Otjiherero", "Otjihimba", "Otshiherero", "Ovaherero" ], "region": "NA", "regionname": "Namibia", "regions": [ "AO", "BW" ], @@ -45430,18 +47484,17 @@ "windows": "hz-Latn" }, { - "full": "ia-Latn-FR", + "full": "ia-Latn-001", "iana": [ "Interlingua (International Auxiliary Language" ], "iso639_3": "ina", "localname": "interlingua", "name": "Interlingua (International Auxiliary Language", - "region": "FR", - "regionname": "France", - "regions": [ "BR" ], + "region": "001", + "regionname": "World", "script": "Latn", "sldr": true, "tag": "ia", - "tags": [ "ia-FR", "ia-Latn" ], + "tags": [ "ia-001", "ia-Latn" ], "windows": "ia-Latn" }, { @@ -45492,6 +47545,7 @@ "full": "iar-Latn-PG", "iana": [ "Purari" ], "iso639_3": "iar", + "localnames": [ "Koriki" ], "name": "Purari", "names": [ "Evorra", "Iai", "Koriki", "Maipua" ], "region": "PG", @@ -45515,7 +47569,7 @@ "script": "Latn", "sldr": false, "tag": "iba", - "tags": [ "iba-Latn", "iba-MY" ], + "tags": [ "blg", "blg-Latn", "blg-Latn-MY", "blg-MY", "iba-Latn", "iba-MY", "snb", "snb-Latn", "snb-Latn-MY", "snb-MY" ], "windows": "iba-Latn" }, { @@ -45537,8 +47591,9 @@ "full": "ibd-Latn-AU", "iana": [ "Iwaidja" ], "iso639_3": "ibd", + "localnames": [ "Manganawal Iwaja" ], "name": "Iwaidja", - "names": [ "Eiwaja", "Ibadjo", "Iwaidji", "Iwaydja", "Jiwadja", "Karadjee", "Limba", "Yiwaidja" ], + "names": [ "Eiwaja", "Ibadjo", "Iwaidja Inyman", "Iwaidji", "Iwaja yinyman", "Iwaydja", "Jiwadja", "Karadjee", "Limba", "Manganawal Iwaja", "Yibadjdja", "Yiwaidja" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -45669,8 +47724,9 @@ "full": "iby-Latn-NG", "iana": [ "Ibani" ], "iso639_3": "iby", + "localnames": [ "Ịḅanị" ], "name": "Ibani", - "names": [ "Bonny", "Obani", "Okoloba", "Okuloma", "Ubani" ], + "names": [ "Bonny", "Ịḅanị", "Obani", "Okoloba", "Okuloma", "Ubani" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -45684,7 +47740,7 @@ "iana": [ "Ede Ica" ], "iso639_3": "ica", "name": "Ede Ica", - "names": [ "Ica", "Itcha" ], + "names": [ "Ica", "Itcha", "Ìtsà" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -45697,6 +47753,7 @@ "full": "ich-Latn-NG", "iana": [ "Etkywan" ], "iso639_3": "ich", + "localname": "Kentu", "localnames": [ "Kentu", "Kyãtõ" ], "name": "Etkywan", "names": [ "Etekwe", "Icen", "Ichen", "Itchen", "Kentu", "Kyanton", "Kyãtõ", "Nyidu" ], @@ -45727,8 +47784,8 @@ "iana": [ "Islander Creole English" ], "iso639_3": "icr", "localname": "Islander, San Andrés Creole", - "name": "Islander Creole English", - "names": [ "Bende", "San Andrés Creole" ], + "name": "Islander English Creole", + "names": [ "Bende", "Islander Creole English", "San Andrés Creole" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -45743,11 +47800,12 @@ "iso639_3": "ind", "localname": "Indonesia", "localnames": [ "Bahasa Indonesia" ], + "macrolang": "ms", "name": "Indonesian", "names": [ "Bahasa Indonesia" ], "region": "ID", "regionname": "Indonesia", - "regions": [ "AU", "HK", "NL", "PH", "SG", "SR", "SX", "US" ], + "regions": [ "AU", "HK", "NL", "PH", "SG", "SR", "SX", "TL", "US" ], "script": "Latn", "sldr": true, "suppress": true, @@ -45759,6 +47817,7 @@ "full": "id-Arab-ID", "iana": [ "Indonesian" ], "iso639_3": "ind", + "macrolang": "ms", "name": "Indonesian", "names": [ "Bahasa Indonesia" ], "nophonvars": true, @@ -45774,6 +47833,7 @@ "full": "id-Brai-ID", "iana": [ "Indonesian" ], "iso639_3": "ind", + "macrolang": "ms", "name": "Indonesian", "names": [ "Bahasa Indonesia" ], "nophonvars": true, @@ -45790,6 +47850,7 @@ "iana": [ "Idakho-Isukha-Tiriki", "Luidakho-Luisukha-Lutirichi" ], "iso639_3": "ida", "localnames": [ "Luidakho-Luisukha-Lutirichi" ], + "macrolang": "luy", "name": "Luidakho-Luisukha-Lutirichi", "names": [ "Idakho-Isukha-Tiriki", "Idaxo-Isuxa-Tiriki" ], "region": "KE", @@ -45805,6 +47866,7 @@ "iana": [ "Indo-Portuguese" ], "iso639_3": "idb", "name": "Indo-Portuguese", + "names": [ "Daman-Diu Portuguese" ], "region": "IN", "regionname": "India", "regions": [ "LK" ], @@ -45836,7 +47898,7 @@ "localname": "Ìdàáshà", "localnames": [ "Ìdàáshà" ], "name": "Ede Idaca", - "names": [ "Idaaca", "Idaasa", "Idaasha", "Idaatcha", "Idaca", "Idáìtsà", "Ìdàáshà" ], + "names": [ "Dasa", "Idaaca", "Idaasa", "Idaatcha", "Idaca", "Idaṣa", "Idáìtsà", "Ìdàáshà" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -45949,8 +48011,9 @@ "iana": [ "Amganad Ifugao" ], "iso639_3": "ifa", "localname": "Ifugao, Amganad", + "localnames": [ "Ipugaw" ], "name": "Ifugao, Amganad", - "names": [ "Amganad", "Amganad Ifugao", "Central Ifugao", "Ifugao", "Ifugao Mun’alyon", "Ifugaw" ], + "names": [ "Amganad", "Amganad Ifugao", "Amgode Ifugao", "Central Ifugao", "Ifugao", "Ifugaw", "Ipugaw" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -45963,8 +48026,9 @@ "full": "ifb-Latn-PH", "iana": [ "Batad Ifugao", "Ayangan Ifugao" ], "iso639_3": "ifb", + "localnames": [ "Ipugaw" ], "name": "Ifugao, Batad", - "names": [ "Ayangan Ifugao", "Batad", "Batad Ifugao", "Ifugaw" ], + "names": [ "Ayangan", "Ayangan Ifugao", "Batad", "Batad Ifugao", "Eastern Ifugao", "Ifugaw", "Ipugaw" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -45978,25 +48042,25 @@ "iana": [ "Ifè" ], "iso639_3": "ife", "localname": "Ifè", - "localnames": [ "Ifè" ], + "localnames": [ "Ifɛ̀" ], "name": "Ifè", - "names": [ "Ana", "Ana-Ife", "Ana-Ifé", "Anago", "Baate", "Ede Ife" ], + "names": [ "Ana", "Ana-Ife", "Ana-Ifé", "Anago", "Baate", "Ede Ife", "Ifɛ̀" ], "region": "TG", "regionname": "Togo", "script": "Latn", "sldr": true, "tag": "ife", "tags": [ "ife-Latn", "ife-TG" ], - "windows": "ife-Latn" + "windows": "ife-Latn-TG" }, { "full": "ife-Latn-BJ", "iana": [ "Ifè" ], "iso639_3": "ife", "localname": "Ifè", - "localnames": [ "Ifè" ], + "localnames": [ "Ifɛ̀" ], "name": "Ifè", - "names": [ "Ana", "Ana-Ife", "Anago", "Baate", "Ede Ife" ], + "names": [ "Ana", "Ana-Ife", "Anago", "Baate", "Ede Ife", "Ifɛ̀" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -46024,9 +48088,9 @@ "iana": [ "Tuwali Ifugao" ], "iso639_3": "ifk", "localname": "Tuwali", - "localnames": [ "Tuwali" ], + "localnames": [ "Ipugaw", "Tuwali" ], "name": "Ifugao, Tuwali", - "names": [ "Gilipanes", "Ifugaw", "Kiangan Ifugao", "Quiangan", "Tuwali", "Tuwali Ifugao" ], + "names": [ "Gilipanes", "Ifugaw", "Ipugaw", "Kiangan Ifugao", "Quiangan", "Tuwali", "Tuwali Ifugao" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -46039,8 +48103,9 @@ "full": "ifm-Latn-CG", "iana": [ "Teke-Fuumu" ], "iso639_3": "ifm", - "name": "Teke-Fuumu", - "names": [ "Fumu", "Fuumu", "Kiteke", "South Central Teke", "Teke du Pool" ], + "localnames": [ "Ifumu" ], + "name": "Teke-Wuumu", + "names": [ "Fumu", "Fuumu", "Ifumu", "Kiteke", "South Central Teke", "Teke du Pool", "Teke-Fuumu" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -46054,9 +48119,9 @@ "iana": [ "Mayoyao Ifugao" ], "iso639_3": "ifu", "localname": "Mayayao Ifugao", - "localnames": [ "Mayayao Ifugao" ], + "localnames": [ "Ipugaw" ], "name": "Ifugao, Mayoyao", - "names": [ "Ifugaw", "Mayaoyaw", "Mayayao Ifugao", "Mayoyao", "Mayoyao Ifugao" ], + "names": [ "Ifugaw", "Ipugaw", "Mayaoyaw", "Mayayao Ifugao", "Mayoyao", "Mayoyao Ifugao" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -46070,7 +48135,6 @@ "iana": [ "Keley-I Kallahan" ], "iso639_3": "ify", "localname": "Antipolo Ifugao", - "localnames": [ "Antipolo Ifugao" ], "name": "Kallahan, Keley-i", "names": [ "Antipolo Ifugao", "Hanalulo", "Keley-I Kallahan", "Keley-i", "Keley-i Kalanguya", "Keleyqiq Ifugao" ], "region": "PH", @@ -46132,6 +48196,7 @@ "iana": [ "Igana" ], "iso639_3": "igg", "name": "Igana", + "names": [ "Iski" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -46175,7 +48240,7 @@ "full": "ign-Latn-BO", "iana": [ "Ignaciano" ], "iso639_3": "ign", - "localname": "Moxos", + "localname": "Inaciànurupi", "localnames": [ "Inaciànurupi" ], "name": "Ignaciano", "names": [ "Ignaciano Mojo", "Inaciànurupi", "Moja", "Mojeño", "Mojo", "Moxo" ], @@ -46203,17 +48268,30 @@ "windows": "igo-Latn" }, { - "full": "igs-Zyyy-001", + "full": "igs-Latn-001", "iana": [ "Interglossa" ], "iso639_3": "igs", "name": "Interglossa", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "igs", - "tags": [ "igs-001", "igs-Zyyy" ], - "windows": "igs-Zyyy" + "tags": [ "igs-001", "igs-Latn" ], + "windows": "igs-Latn" + }, + { + "full": "igs-Grek-001", + "iana": [ "Interglossa" ], + "iso639_3": "igs", + "name": "Interglossa", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Grek", + "sldr": false, + "tag": "igs-Grek", + "windows": "igs-Grek" }, { "full": "igw-Latn-NG", @@ -46277,6 +48355,7 @@ "iana": [ "Bidhawal" ], "iso639_3": "ihw", "name": "Bidhawal", + "names": [ "Beddiwell", "Bidawal", "Bidewel", "Biduelli", "Bidwell", "Bidwelli", "Bidwill", "Birdawal", "Birdhawal", "Birtowall" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -46294,7 +48373,7 @@ "localname": "ꆈꌠꉙ", "localnames": [ "ꆈꌠꉙ" ], "name": "Nuosu", - "names": [ "Black Yi", "Liangshan Yi", "Liangshan Yi-Nuosu", "Northern Yi", "Nosu", "Nosu Yi", "Sichuan Yi", "Yi-Nuosu" ], + "names": [ "Black Yi", "Liangshan Yi", "Liangshan Yi-Nuosu", "Lolo", "Northern Yi", "Nosu", "Nosu Yi", "Sichuan Yi", "Yi-Nuosu" ], "region": "CN", "regionname": "China", "script": "Yiii", @@ -46309,7 +48388,7 @@ "iso639_3": "iii", "localnames": [ "Nuosu" ], "name": "Nuosu", - "names": [ "Black Yi", "Liangshan Yi", "Liangshan Yi-Nuosu", "Northern Yi", "Nosu", "Nosu Yi", "Sichuan Yi", "Yi-Nuosu" ], + "names": [ "Black Yi", "Liangshan Yi", "Liangshan Yi-Nuosu", "Lolo", "Northern Yi", "Nosu", "Nosu Yi", "Sichuan Yi", "Yi-Nuosu" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -46336,8 +48415,9 @@ "full": "ijc-Latn-NG", "iana": [ "Izon" ], "iso639_3": "ijc", + "localnames": [ "Ịzọn" ], "name": "Izon", - "names": [ "Central-Western Ijo", "Ijaw", "Ijo", "Izo", "Uzo" ], + "names": [ "Central-Western Ijo", "Ezon", "Ijaw", "Ijo", "Ijon", "Izo", "Uzo", "Ịzọn" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -46366,7 +48446,7 @@ "iana": [ "Ede Ije" ], "iso639_3": "ijj", "name": "Ede Ije", - "names": [ "Holi", "Holli-Djé", "Ije" ], + "names": [ "Ede-Ijε", "Holi", "Holli-Djé", "Hɔlí", "Ije", "Ìjὲ", "Ọhọ̀rí" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -46410,8 +48490,8 @@ "iso639_3": "ipk", "localname": "Inupiatun, Northwest Alaska", "localnames": [ "Iñupiatun" ], - "name": "Inupiatun, Northwest Alaska", - "names": [ "Eskimo", "Ieupiaq", "Inupiaq", "Iñupiaq", "Iñupiatun", "Northwest Alaska Inupiat", "Seward Peninsula Inupiaq" ], + "name": "Inupiaq", + "names": [ "Eskimo", "Ieupiaq", "Inupiatun, Northwest Alaska", "Iñupiaq", "Iñupiatun", "Northwest Alaska Inupiat", "Seward Peninsula Inupiaq" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -46424,8 +48504,9 @@ "full": "ike-Latn-US", "iana": [ "Eastern Canadian Inuktitut" ], "iso639_3": "ike", + "macrolang": "iu", "name": "Inuktitut, Eastern Canadian", - "names": [ "Canadian Inuit", "Eastern Canadian Inuktitut", "Inuit", "Inuit of Quebec" ], + "names": [ "Canadian Inuit", "Eastern Canadian Inuktitut", "Inuit", "Inuit of Quebec", "Inuktut" ], "region": "US", "regionname": "United States", "regions": [ "CA" ], @@ -46433,7 +48514,20 @@ "sldr": false, "tag": "ike-Latn", "tags": [ "ike-US" ], - "windows": "ike-Latn" + "windows": "ike-Latn-US" + }, + { + "full": "ikh-Latn-NG", + "iso639_3": "ikh", + "name": "Ikhin-Aokho", + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "ikh", + "tags": [ "ikh-Latn", "ikh-NG" ], + "unwritten": true, + "windows": "ikh-Latn" }, { "full": "iki-Latn-NG", @@ -46469,7 +48563,7 @@ "iana": [ "Ikulu" ], "iso639_3": "ikl", "name": "Ikulu", - "names": [ "Ankulu", "Ikolu", "Kulu" ], + "names": [ "Ankulu", "Bekulu", "Ikolu", "Kulu" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -46491,7 +48585,6 @@ "sldr": false, "tag": "iko", "tags": [ "iko-Latn", "iko-NG" ], - "unwritten": true, "windows": "iko-Latn" }, { @@ -46543,6 +48636,7 @@ "iana": [ "Inuinnaqtun", "Western Canadian Inuktitut" ], "iso639_3": "ikt", "localnames": [ "Inuinnaqtun" ], + "macrolang": "iu", "name": "Inuinnaqtun", "names": [ "Western Canada Inuktun", "Western Canadian Inuit", "Western Canadian Inuktitut" ], "region": "CA", @@ -46559,6 +48653,7 @@ "iso639_3": "ikt", "latnnames": [ "Inuvialuktun" ], "localnames": [ "ᐃᓄᐃᓐᓇᖅᑐᓐ" ], + "macrolang": "iu", "name": "Inuinnaqtun", "names": [ "Western Canada Inuktun", "Western Canadian Inuit", "Western Canadian Inuktitut" ], "nophonvars": true, @@ -46591,7 +48686,7 @@ "localname": "Ikwerre", "localnames": [ "Ikwerre" ], "name": "Ikwere", - "names": [ "Ikwerre", "Ikwerri" ], + "names": [ "Ikwerre", "Ikwerri", "Ìwhnuruòhnà" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -46620,8 +48715,9 @@ "full": "ikz-Latn-TZ", "iana": [ "Ikizu" ], "iso639_3": "ikz", + "localnames": [ "Ikiikiizʉ" ], "name": "Ikizu", - "names": [ "Ikiikiizu", "Ikikizo", "Ikikizu", "Kiikizu", "Kizu" ], + "names": [ "Ikiikiizu", "Ikiikiizʉ", "Ikikizo", "Ikikizu", "Kiikizu", "Kizu" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -46675,7 +48771,7 @@ "windows": "ilg-Latn" }, { - "full": "ili-Zyyy-CN", + "full": "ili-Latn-CN", "iana": [ "Ili Turki" ], "iso639_3": "ili", "name": "Ili Turki", @@ -46683,11 +48779,42 @@ "region": "CN", "regionname": "China", "regions": [ "KZ" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ili", - "tags": [ "ili-CN", "ili-Zyyy" ], - "windows": "ili-Zyyy" + "tags": [ "ili-CN", "ili-Latn" ], + "windows": "ili-Latn-CN" + }, + { + "full": "ili-Arab-CN", + "iana": [ "Ili Turki" ], + "iso639_3": "ili", + "name": "Ili Turki", + "names": [ "Ili Turk", "Taranchi", "Tuerke", "Tu’erke", "T’urk" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "regions": [ "KZ" ], + "script": "Arab", + "sldr": false, + "tag": "ili-Arab", + "windows": "ili-Arab-CN" + }, + { + "full": "ili-Cyrl-KZ", + "iana": [ "Ili Turki" ], + "iso639_3": "ili", + "name": "Ili Turki", + "names": [ "Ili Turk", "Taranchi", "Tuerke", "Tu’erke", "T’urk" ], + "nophonvars": true, + "region": "KZ", + "regionname": "Kazakhstan", + "regions": [ "CN" ], + "script": "Cyrl", + "sldr": false, + "tag": "ili-Cyrl", + "tags": [ "ili-KZ" ], + "windows": "ili-Cyrl-KZ" }, { "full": "ilk-Latn-PH", @@ -46708,6 +48835,7 @@ "full": "ilm-Latn-MY", "iana": [ "Iranun (Malaysia)" ], "iso639_3": "ilm", + "localnames": [ "Iranun" ], "name": "Iranun", "names": [ "Ilanun", "Illanoan", "Illanoon", "Illanos", "Illanun", "Iranon Maranao", "Iranum", "Iranun (Malaysia)", "Lanoon", "Lanun", "Ylanos" ], "region": "MY", @@ -46785,7 +48913,7 @@ "iana": [ "International Sign" ], "iso639_3": "ils", "name": "International Sign", - "names": [ "Gestuno", "IS", "International Sign Language" ], + "names": [ "Gestuno", "IS", "International Sign Language", "International gestures" ], "region": "IT", "regionname": "Italy", "script": "Zxxx", @@ -46843,8 +48971,8 @@ "full": "imi-Latn-PG", "iana": [ "Anamgura" ], "iso639_3": "imi", - "name": "Anamgura", - "names": [ "Anamuxra", "Ikundun", "Mindivi" ], + "name": "Anamuxra", + "names": [ "Anamgura", "Ikundun", "Mindivi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -46855,18 +48983,19 @@ "windows": "imi-Latn" }, { - "full": "iml-Zyyy-US", + "full": "iml-Latn-US", "iana": [ "Miluk" ], "iso639_3": "iml", "name": "Miluk", + "names": [ "Lower Coquille" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "iml", - "tags": [ "iml-US", "iml-Zyyy" ], + "tags": [ "iml-Latn", "iml-US" ], "unwritten": true, - "windows": "iml-Zyyy" + "windows": "iml-Latn" }, { "full": "imn-Latn-PG", @@ -46926,17 +49055,31 @@ "windows": "ims-Latn" }, { - "full": "imy-Zyyy-TR", + "full": "imt-Latn-SS", + "iana": [ "Imotong" ], + "iso639_3": "imt", + "name": "Imotong", + "region": "SS", + "regionname": "South Sudan", + "script": "Latn", + "sldr": false, + "tag": "imt", + "tags": [ "imt-Latn", "imt-SS" ], + "unwritten": true, + "windows": "imt-Latn" + }, + { + "full": "imy-Lyci-TR", "iana": [ "Milyan" ], "iso639_3": "imy", "name": "Milyan", "region": "TR", "regionname": "Turkey", - "script": "Zyyy", + "script": "Lyci", "sldr": false, "tag": "imy", - "tags": [ "imy-TR", "imy-Zyyy" ], - "windows": "imy-Zyyy" + "tags": [ "imy-Lyci", "imy-TR" ], + "windows": "imy-Lyci" }, { "full": "inb-Latn-CO", @@ -46958,8 +49101,8 @@ "iana": [ "Degexit'an" ], "iso639_3": "ing", "localnames": [ "Deg Xinag" ], - "name": "Degexit’an", - "names": [ "Deg Xinag", "Deg Xit’an", "Degexit'an" ], + "name": "Deg Xinag", + "names": [ "Deg Hit’an", "Deg Xit’an", "Degexit'an", "Degexit’an" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -46992,6 +49135,7 @@ "name": "Ingush", "names": [ "Galgay", "Ghalghaj", "Ghalghay", "Ingus", "Ingushi", "Kisti", "Kistin" ], "nophonvars": true, + "obsolete": true, "region": "RU", "regionname": "Russian Federation", "regions": [ "KZ", "UZ" ], @@ -47034,7 +49178,7 @@ "iana": [ "Indonesian Sign Language" ], "iso639_3": "inl", "name": "Indonesian Sign Language", - "names": [ "ISL" ], + "names": [ "BISINDO", "Bahasa Isyarat Indonesia", "ISL" ], "region": "ID", "regionname": "Indonesia", "script": "Zxxx", @@ -47159,25 +49303,27 @@ "windows": "io-Latn" }, { - "full": "ior-Zyyy-ET", + "full": "ior-Ethi-ET", "iana": [ "Inor" ], "iso639_3": "ior", + "latnnames": [ "Inori" ], + "localnames": [ "ኢኖር" ], "name": "Inor", "region": "ET", "regionname": "Ethiopia", - "script": "Zyyy", + "script": "Ethi", "sldr": false, "tag": "ior", - "tags": [ "ior-ET", "ior-Zyyy" ], - "unwritten": true, - "windows": "ior-Zyyy" + "tags": [ "ior-ET", "ior-Ethi" ], + "windows": "ior-Ethi" }, { "full": "iou-Latn-PG", "iana": [ "Tuma-Irumu" ], "iso639_3": "iou", + "localnames": [ "Tuma Man" ], "name": "Tuma-Irumu", - "names": [ "Irumu", "Tatuma-Irumu", "Tuma", "Upper Irumu" ], + "names": [ "Irumu", "Tatuma-Irumu", "Tuma", "Tuma Man", "Upper Irumu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -47187,19 +49333,19 @@ "windows": "iou-Latn" }, { - "full": "iow-Zyyy-US", + "full": "iow-Latn-US", "iana": [ "Iowa-Oto" ], "iso639_3": "iow", "name": "Iowa-Oto", "names": [ "Iowa-Oto-Missouria" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "iow", - "tags": [ "iow-US", "iow-Zyyy" ], + "tags": [ "iow-Latn", "iow-US" ], "unwritten": true, - "windows": "iow-Zyyy" + "windows": "iow-Latn" }, { "full": "ipi-Latn-PG", @@ -47424,7 +49570,21 @@ "suppress": true, "tag": "is", "tags": [ "is-IS", "is-Latn" ], - "windows": "is" + "windows": "is-IS" + }, + { + "full": "is-Runr-001", + "iana": [ "Icelandic" ], + "iso639_3": "isl", + "name": "Icelandic", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "is-Runr", + "tags": [ "is-001" ], + "windows": "is-Runr-001" }, { "full": "isa-Latn-PG", @@ -47446,7 +49606,7 @@ "iana": [ "Isconahua" ], "iso639_3": "isc", "name": "Isconahua", - "names": [ "Iscobaquebu", "Iskobákebo" ], + "names": [ "Iscobaquebu", "Iskobákebo", "Iskonawa" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -47477,7 +49637,7 @@ "iana": [ "Italian Sign Language" ], "iso639_3": "ise", "name": "Italian Sign Language", - "names": [ "LIS", "Lingua Italiana Dei Segni" ], + "names": [ "LIS", "Lingua Italiana Dei Segni", "Lingua dei Segni Italiana" ], "region": "IT", "regionname": "Italy", "script": "Zxxx", @@ -47544,7 +49704,22 @@ "tag": "isk", "tags": [ "isk-AF", "isk-Arab" ], "unwritten": true, - "windows": "isk-Arab" + "windows": "isk-Arab-AF" + }, + { + "full": "isk-Cyrl-TJ", + "iana": [ "Ishkashimi" ], + "iso639_3": "isk", + "name": "Ishkashimi", + "names": [ "Ishkashim" ], + "nophonvars": true, + "region": "TJ", + "regionname": "Tajikistan", + "script": "Cyrl", + "sldr": false, + "tag": "isk-Cyrl", + "tags": [ "isk-TJ" ], + "windows": "isk-Cyrl-TJ" }, { "full": "ism-Latn-ID", @@ -47608,7 +49783,7 @@ "iana": [ "Istriot" ], "iso639_3": "ist", "name": "Istriot", - "names": [ "Istro-Romance" ], + "names": [ "Istriano", "Istro-Romance" ], "region": "HR", "regionname": "Croatia", "script": "Latn", @@ -47648,7 +49823,7 @@ "suppress": true, "tag": "it", "tags": [ "it-IT", "it-Latn" ], - "windows": "it" + "windows": "it-IT" }, { "full": "it-Brai-IT", @@ -47663,7 +49838,7 @@ "script": "Brai", "sldr": false, "tag": "it-Brai", - "windows": "it-Brai" + "windows": "it-Brai-IT" }, { "full": "it-Latn-CH", @@ -47676,9 +49851,8 @@ "regionname": "Switzerland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "it-CH", - "windows": "it-CH" + "windows": "it-Latn-CH" }, { "full": "itb-Latn-PH", @@ -47715,7 +49889,7 @@ "iana": [ "Itene" ], "iso639_3": "ite", "name": "Itene", - "names": [ "Guarayo", "Iten", "Iteneo", "Iténez", "Maré", "More", "Moré" ], + "names": [ "Guarayo", "Iten", "Iteneo", "Iténez", "Maré", "Moré" ], "region": "BO", "regionname": "Bolivia", "regions": [ "BR" ], @@ -47729,8 +49903,8 @@ "full": "iti-Latn-PH", "iana": [ "Inlaod Itneg" ], "iso639_3": "iti", - "name": "Itneg, Inlaod", - "names": [ "Inlaod", "Inlaod Itneg", "Tinggian", "Tinguian" ], + "name": "Itneg, Inlaud", + "names": [ "Inlaod", "Inlaod Itneg", "Inlaud Itneg", "Tinggian", "Tinguian" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -47745,7 +49919,7 @@ "iana": [ "Judeo-Italian" ], "iso639_3": "itk", "name": "Judeo-Italian", - "names": [ "Italkian", "La‘az", "giudeo-italiano" ], + "names": [ "Italkian", "Judaico-Romanesco", "Judeo-Venetian", "La‘az", "giudeo-italiano" ], "region": "IT", "regionname": "Italy", "regions": [ "GR" ], @@ -47804,8 +49978,8 @@ "full": "itr-Latn-PG", "iana": [ "Iteri" ], "iso639_3": "itr", - "name": "Iteri", - "names": [ "Alowiemino", "Iyo", "Laro", "Rocky Peak", "Yinibu" ], + "name": "Yawuno Teneyo", + "names": [ "Alowiemino", "Iteri", "Iyo", "Laro", "Rocky Peak", "Yawuno", "Yinibu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -47820,7 +49994,7 @@ "iso639_3": "its", "localnames": [ "Iṣekiri" ], "name": "Isekiri", - "names": [ "Chekiri", "Irhobo", "Iselema-Otu", "Ishekiri", "Itsekiri", "Iwere", "Iṣekiri", "Jekri", "Selemo", "Shekiri", "Warri" ], + "names": [ "Chekiri", "Dsekiri", "Irhobo", "Iselema-Otu", "Ishekiri", "Itsekiri", "Iwere", "Izekíri", "Iṣekiri", "Jekri", "Selemo", "Shekiri", "Tshekeri", "Warri" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -47924,8 +50098,8 @@ "latnnames": [ "Inuktitut" ], "localname": "ᐃᓄᒃᑎᑐᑦ", "localnames": [ "ᐃᓄᒃᑎᑐᑦ" ], - "name": "Inuktitut, Eastern Canadian", - "names": [ "Canadian Inuit", "Inuit", "Inuit of Quebec", "Inuktitut" ], + "name": "Inuktitut", + "names": [ "Canadian Inuit", "Inuit", "Inuit of Quebec", "Inuktitut, Eastern Canadian", "Inuktut" ], "region": "CA", "regionname": "Canada", "script": "Cans", @@ -47960,7 +50134,7 @@ "sldr": false, "tag": "ium", "tags": [ "ium-CN", "ium-Latn" ], - "windows": "ium-Latn" + "windows": "ium-Latn-CN" }, { "full": "ium-Hani-CN", @@ -47975,7 +50149,7 @@ "script": "Hani", "sldr": false, "tag": "ium-Hani", - "windows": "ium-Hani" + "windows": "ium-Hani-CN" }, { "full": "ium-Laoo-LA", @@ -47991,7 +50165,7 @@ "sldr": false, "tag": "ium-Laoo", "tags": [ "ium-LA" ], - "windows": "ium-Laoo" + "windows": "ium-Laoo-LA" }, { "full": "ium-Thai-TH", @@ -48007,7 +50181,7 @@ "sldr": false, "tag": "ium-Thai", "tags": [ "ium-TH" ], - "windows": "ium-Thai" + "windows": "ium-Thai-TH" }, { "full": "ium-Latn-CN-x-1954", @@ -48023,7 +50197,7 @@ "sldr": false, "tag": "ium-x-1954", "tags": [ "ium-CN-x-1954", "ium-Latn-x-1954" ], - "windows": "ium-Latn-x-1954" + "windows": "ium-Latn-CN-x-1954" }, { "full": "ivb-Latn-PH", @@ -48138,6 +50312,7 @@ "localname": "Ixil", "localnames": [ "Ixil" ], "name": "Ixil", + "names": [ "Ixhil", "Ixhil Mayan", "Ixil Maya" ], "region": "GT", "regionname": "Guatemala", "script": "Latn", @@ -48146,6 +50321,22 @@ "tags": [ "ixl-GT", "ixl-Latn" ], "windows": "ixl-Latn" }, + { + "full": "ixl-Latn-GT-x-cotzal", + "iana": [ "Ixil" ], + "iso639_3": "ixl", + "localnames": [ "Ixil" ], + "name": "Ixil", + "names": [ "Ixhil", "Ixhil Mayan", "Ixil Maya" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4781", + "script": "Latn", + "sldr": true, + "tag": "ixl-x-cotzal", + "tags": [ "ixl-GT-x-cotzal", "ixl-Latn-x-cotzal" ], + "windows": "ixl-Latn-x-cotzal" + }, { "full": "iya-Latn-NG", "iana": [ "Iyayu" ], @@ -48181,12 +50372,12 @@ "iana": [ "Yaka (Congo)" ], "iso639_3": "iyx", "name": "Yaka", - "names": [ "Iyaka", "West Teke", "Yaa", "Yaka (Congo)" ], + "names": [ "Iyaa", "Iyaka", "West Teke", "Yaa", "Yaka (Congo)" ], "obsolete": true, "region": "CG", "regionname": "Congo", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "iyx", "tags": [ "iyx-CG", "iyx-Latn" ], "windows": "iyx-Latn" @@ -48197,7 +50388,7 @@ "iso639_3": "izh", "localnames": [ "Ižoran keel" ], "name": "Ingrian", - "names": [ "Ingermanlandian", "Inkeroisen", "Izhor", "Izhorian", "Ižoran", "Ižoran keel", "Ižoran keeli" ], + "names": [ "Ingermanlandian", "Inkeroisen", "Isuri keel", "Izhor", "Izhorian", "Ižoran", "Ižoran keel", "Ižoran keeli" ], "obsolete": true, "region": "RU", "regionname": "Russian Federation", @@ -48207,6 +50398,20 @@ "tags": [ "izh-Latn", "izh-RU" ], "windows": "izh-Latn" }, + { + "full": "izm-Latn-NG", + "iso639_3": "izm", + "name": "Kizamani", + "names": [ "Azaman", "Azamani", "Kuzamani" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "izm", + "tags": [ "izm-Latn", "izm-NG", "ksa", "ksa-Latn", "ksa-Latn-NG", "ksa-NG" ], + "unwritten": true, + "windows": "izm-Latn" + }, { "full": "izr-Latn-NG", "iana": [ "Izere" ], @@ -48214,7 +50419,7 @@ "localname": "Izere", "localnames": [ "Izere" ], "name": "Izere", - "names": [ "Afizare", "Afizarek", "Afizere", "Afudelek", "Afusare", "Feserek", "Fezere", "Fizere", "Hill Jarawa", "Izarek", "Izer", "Jarawa", "Jarawan Dutse", "Jari", "Jos-Zarazon" ], + "names": [ "Afizare", "Afizarek", "Afizere", "Afudelek", "Afusare", "Feserek", "Fezere", "Fizere", "Hill Jarawa", "Izarek", "Izer", "Jarawa", "Jarawan Dutse", "Jari", "Jos-Zarazon", "Zarek" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -48230,7 +50435,7 @@ "localname": "Izii", "localnames": [ "Izii" ], "name": "Izii", - "names": [ "Izhi", "Izi", "Izzi" ], + "names": [ "Ezzi", "Izhi", "Izi", "Izzi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -48286,6 +50491,8 @@ "full": "jaa-Latn-BR", "iana": [ "Jamamadí" ], "iso639_3": "jaa", + "localname": "Jamamadí", + "localnames": [ "Jamamadí" ], "name": "Jamamadí", "names": [ "Canamanti", "Kanamanti", "Kapaná", "Madi", "Yamadi", "Yamamadí", "Yamamandi" ], "region": "BR", @@ -48296,10 +50503,43 @@ "tags": [ "jaa-BR", "jaa-Latn" ], "windows": "jaa-Latn" }, + { + "full": "jaa-Latn-BR-x-banawa", + "iana": [ "Jamamadí" ], + "iso639_3": "jaa", + "localnames": [ "Jamamadí" ], + "name": "Jamamadí", + "names": [ "Canamanti", "Kanamanti", "Kapaná", "Madi", "Yamadi", "Yamamadí", "Yamamandi" ], + "region": "BR", + "regionname": "Brazil", + "rod": "7657", + "script": "Latn", + "sldr": false, + "tag": "jaa-x-banawa", + "tags": [ "jaa-BR-x-banawa", "jaa-Latn-x-banawa" ], + "windows": "jaa-Latn-x-banawa" + }, + { + "full": "jaa-Latn-BR-x-jarawara", + "iana": [ "Jamamadí" ], + "iso639_3": "jaa", + "localnames": [ "Jamamadí" ], + "name": "Jamamadí", + "names": [ "Canamanti", "Kanamanti", "Kapaná", "Madi", "Yamadi", "Yamamadí", "Yamamandi" ], + "region": "BR", + "regionname": "Brazil", + "rod": "3816", + "script": "Latn", + "sldr": true, + "tag": "jaa-x-jarawara", + "tags": [ "jaa-BR-x-jarawara", "jaa-Latn-x-jarawara" ], + "windows": "jaa-Latn-x-jarawara" + }, { "full": "jab-Latn-NG", "iana": [ "Hyam" ], "iso639_3": "jab", + "localname": "Jaba", "localnames": [ "Jaba" ], "name": "Hyam", "names": [ "Ham", "Hum", "Hyamhum", "Jaba", "Jabba", "Jeba" ], @@ -48315,10 +50555,10 @@ "full": "jac-Latn-GT", "iana": [ "Popti'", "Jakalteko" ], "iso639_3": "jac", - "localname": "Jakalteko-Popti'", - "localnames": [ "Jakalteko-Popti'" ], + "localname": "Abxubal", + "localnames": [ "Abxubal", "Abꞌxubꞌal Poptiꞌ" ], "name": "Jakalteko", - "names": [ "Jakalteko del Oeste", "Jakalteko-Popti’", "Popti'", "Popti’" ], + "names": [ "Abxubal", "Abꞌxubꞌal Poptiꞌ", "Jabxubal", "Jabʼ xubʼal", "Jakalteko del Oeste", "Jakalteko-Popti’", "Popti'", "Popti’" ], "region": "GT", "regionname": "Guatemala", "regions": [ "MX" ], @@ -48333,7 +50573,7 @@ "iana": [ "Jahanka" ], "iso639_3": "jad", "name": "Jahanka", - "names": [ "Diakhanke", "Diakkanke", "Dyakanke", "Jahanque", "Jahonque", "Jakankalou", "Jaxanka" ], + "names": [ "Diakanka", "Diakhanke", "Diakkanke", "Djakanka", "Dyakanke", "Jahanque", "Jahonque", "Jakankalou", "Jaxanka" ], "region": "GN", "regionname": "Guinea", "regions": [ "GW", "ML" ], @@ -48406,6 +50646,7 @@ "full": "jak-Latn-MY", "iana": [ "Jakun" ], "iso639_3": "jak", + "macrolang": "ms", "name": "Jakun", "names": [ "Djakun", "Jakoon", "Jakud’n", "Jaku’d", "Orang Hulu" ], "region": "MY", @@ -48422,7 +50663,7 @@ "iana": [ "Yalahatan" ], "iso639_3": "jal", "name": "Yalahatan", - "names": [ "Atamanu", "Awaiya", "Jahalatan", "Jahalatane" ], + "names": [ "Atamanu", "Awaiya", "Jahalatan", "Jahalatane", "Yalahatan-Haruru-Awaiya" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -48436,8 +50677,8 @@ "iana": [ "Jamaican Creole English" ], "iso639_3": "jam", "localnames": [ "Jamiekan" ], - "name": "Jamaican Creole English", - "names": [ "Bocas del Toro Patois", "Bongo Talk", "Jamaican Patois", "Jamiekan", "Jimiekn", "Jimiekn Langwij", "Jumieka Kruyol", "Jumieka Languij", "Jumieka Langwij", "Jumieka Taak", "Jumiekan", "Jumiekan Kriol", "Jumiekan languij", "Langui Jumieka", "Limonese Creole", "Limón Creole English", "Panamanian Creole English", "Panamanian Patois English", "Patois", "Patua", "Patwa", "Patwah", "Southwestern Caribbean Creole English", "Western Caribbean Creole" ], + "name": "Jamaican English Creole", + "names": [ "Bocas del Toro Patois", "Bongo Talk", "Jamaican Creole English", "Jamaican Patois", "Jamiekan", "Jimiekn", "Jimiekn Langwij", "Jumieka Kruyol", "Jumieka Languij", "Jumieka Langwij", "Jumieka Taak", "Jumiekan", "Jumiekan Kriol", "Jumiekan languij", "Langui Jumieka", "Limonese Creole", "Limón Creole English", "Panamanian Creole English", "Panamanian Patois English", "Patois", "Patua", "Patwa", "Patwah", "Southwestern Caribbean English Creole" ], "region": "JM", "regionname": "Jamaica", "regions": [ "CA", "CR", "DO", "GB", "PA", "US" ], @@ -48466,6 +50707,7 @@ "full": "jao-Latn-AU", "iana": [ "Yanyuwa" ], "iso639_3": "jao", + "localnames": [ "Yanyuwa" ], "name": "Yanyuwa", "names": [ "Aniula", "Anula", "Anyula", "Janjula", "Leeanuwa", "Wadiri", "Yanula", "Yanuwa", "Yanyula" ], "region": "AU", @@ -48510,6 +50752,7 @@ "full": "jat-Arab-AF", "iana": [ "Jakati" ], "iso639_3": "jat", + "macrolang": "lah", "name": "Inku", "names": [ "Enku", "Jakati", "Jat", "Jataki", "Jati", "Jatu", "Kayani", "Musali" ], "region": "AF", @@ -48540,6 +50783,7 @@ "full": "jax-Latn-ID", "iana": [ "Jambi Malay" ], "iso639_3": "jax", + "macrolang": "ms", "name": "Malay, Jambi", "names": [ "Bahasa Melayu Jambi", "Bahaso Daerah", "Bahaso Dusun", "Baso Jambi", "Batin", "Djambi", "Jambi", "Jambi Malay" ], "region": "ID", @@ -48637,6 +50881,21 @@ "unwritten": true, "windows": "jbk-Latn" }, + { + "full": "jbm-Latn-NG", + "iana": [ "Bijim" ], + "iso639_3": "jbm", + "name": "Bijim", + "names": [ "Bijimawa", "Vaghat", "Vaghat-Ya-Bijim-Legeri" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "jbm", + "tags": [ "bij", "bij-Latn", "bij-Latn-NG", "bij-NG", "jbm-Latn", "jbm-NG" ], + "unwritten": true, + "windows": "jbm-Latn" + }, { "full": "jbn-Arab-LY", "iana": [ "Nafusi" ], @@ -48701,7 +50960,7 @@ "localname": "Njikum", "localnames": [ "Njikum" ], "name": "Jukun Takum", - "names": [ "Diyi", "Diyu", "Jukun", "Njikum" ], + "names": [ "Diyi", "Diyu", "Jukun", "Njikum", "Takum-Donga" ], "region": "CM", "regionname": "Cameroon", "regions": [ "NG" ], @@ -48771,18 +51030,18 @@ "windows": "jct-Latn" }, { - "full": "jda-Zyyy-IN", + "full": "jda-Tibt-IN", "iana": [ "Jad" ], "iso639_3": "jda", "name": "Jad", "names": [ "Bhotia", "Dzad", "Rongba" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "jda", - "tags": [ "jda-IN", "jda-Zyyy" ], - "windows": "jda-Zyyy" + "tags": [ "jda-IN", "jda-Tibt" ], + "windows": "jda-Tibt" }, { "full": "jdg-Arab-PK", @@ -48812,7 +51071,7 @@ "sldr": false, "tag": "jdt", "tags": [ "jdt-Cyrl", "jdt-RU" ], - "windows": "jdt-Cyrl" + "windows": "jdt-Cyrl-RU" }, { "full": "jdt-Hebr-RU", @@ -48827,7 +51086,7 @@ "script": "Hebr", "sldr": false, "tag": "jdt-Hebr", - "windows": "jdt-Hebr" + "windows": "jdt-Hebr-RU" }, { "full": "jdt-Latn-AZ", @@ -48843,7 +51102,7 @@ "sldr": false, "tag": "jdt-Latn", "tags": [ "jdt-AZ" ], - "windows": "jdt-Latn" + "windows": "jdt-Latn-AZ" }, { "full": "jeb-Latn-PE", @@ -48888,7 +51147,7 @@ "sldr": false, "tag": "jeh", "tags": [ "jeh-Latn", "jeh-VN" ], - "windows": "jeh-Latn" + "windows": "jeh-Latn-VN" }, { "full": "jeh-Laoo-LA", @@ -48904,7 +51163,7 @@ "sldr": false, "tag": "jeh-Laoo", "tags": [ "jeh-LA" ], - "windows": "jeh-Laoo" + "windows": "jeh-Laoo-LA" }, { "full": "jei-Latn-ID", @@ -48955,7 +51214,7 @@ "full": "jen-Latn-NG", "iana": [ "Dza" ], "iso639_3": "jen", - "localname": "Ngwa Idza", + "localname": "nnwa’ Dzâ", "localnames": [ "nnwa’ Dzâ" ], "name": "Dza", "names": [ "Ja", "Janjo", "Jen", "Jenjo", "nnwa’ Dzâ", "Èédzá" ], @@ -49029,18 +51288,32 @@ "windows": "jgb-Latn" }, { - "full": "jge-Zyyy-IL", + "full": "jge-Geor-GE", "iana": [ "Judeo-Georgian" ], "iso639_3": "jge", "name": "Judeo-Georgian", + "region": "GE", + "regionname": "Georgia", + "regions": [ "IL" ], + "script": "Geor", + "sldr": false, + "tag": "jge", + "tags": [ "jge-GE", "jge-Geor" ], + "windows": "jge-Geor-GE" + }, + { + "full": "jge-Hebr-IL", + "iana": [ "Judeo-Georgian" ], + "iso639_3": "jge", + "name": "Judeo-Georgian", + "nophonvars": true, "region": "IL", "regionname": "Israel", - "regions": [ "GE" ], - "script": "Zyyy", + "script": "Hebr", "sldr": false, - "tag": "jge", - "tags": [ "jge-IL", "jge-Zyyy" ], - "windows": "jge-Zyyy" + "tag": "jge-Hebr", + "tags": [ "jge-IL" ], + "windows": "jge-Hebr-IL" }, { "full": "jgk-Latn-NG", @@ -49152,10 +51425,10 @@ }, { "full": "jid-Latn-NG", - "iana": [ "Bu" ], + "iana": [ "Bu (Kaduna State)" ], "iso639_3": "jid", "name": "Bu", - "names": [ "Ibut", "Jida", "Jida-Abu", "Jidda-Abu", "Nakare" ], + "names": [ "Bu (Kaduna State)", "Ibut", "Jida", "Jida-Abu", "Jidda-Abu", "Nakare" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -49170,7 +51443,7 @@ "iana": [ "Jilbe" ], "iso639_3": "jie", "name": "Jilbe", - "names": [ "Zoulbou" ], + "names": [ "Yilbe", "Zoulbou" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -49200,7 +51473,7 @@ "iana": [ "sTodsde", "Shangzhai" ], "iso639_3": "jih", "name": "sTodsde", - "names": [ "Shangzhai", "Western Jiarong" ], + "names": [ "Shangzhai" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -49314,9 +51587,9 @@ "iana": [ "Shuar" ], "iso639_3": "jiv", "localname": "Shuar Chicham", - "localnames": [ "Shuar Chicham" ], + "localnames": [ "Shuar chicham" ], "name": "Shuar", - "names": [ "Chicham", "Chiwaro", "Jivaro", "Shuar Chicham", "Shuara", "Siurra", "Siwora", "Xivaro" ], + "names": [ "Chicham", "Shiwar chicham", "Shuar chicham", "Shuara", "Siurra", "Siwora" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -49384,19 +51657,46 @@ "windows": "jka-Latn" }, { - "full": "jkm-Zyyy-MM", + "full": "jkm-Mymr-MM", "iana": [ "Mobwa Karen" ], "iso639_3": "jkm", "name": "Karen, Mobwa", "names": [ "Bilichi", "Blimaw", "Dermuha", "Maleh", "Mobwa Karen", "Monpwa", "Mopwa", "Pa-Le-Chi", "Palachi", "Palakhi", "Palaychi" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "jkm", - "tags": [ "jkm-MM", "jkm-Zyyy" ], + "tags": [ "jkm-MM", "jkm-Mymr" ], "unwritten": true, - "windows": "jkm-Zyyy" + "windows": "jkm-Mymr" + }, + { + "full": "jkm-Brai-MM", + "iana": [ "Mobwa Karen" ], + "iso639_3": "jkm", + "name": "Karen, Mobwa", + "names": [ "Bilichi", "Blimaw", "Dermuha", "Maleh", "Mobwa Karen", "Monpwa", "Mopwa", "Pa-Le-Chi", "Palachi", "Palakhi", "Palaychi" ], + "nophonvars": true, + "region": "MM", + "regionname": "Myanmar", + "script": "Brai", + "sldr": false, + "tag": "jkm-Brai", + "windows": "jkm-Brai" + }, + { + "full": "jkm-Latn-MM", + "iana": [ "Mobwa Karen" ], + "iso639_3": "jkm", + "name": "Karen, Mobwa", + "names": [ "Bilichi", "Blimaw", "Dermuha", "Maleh", "Mobwa Karen", "Monpwa", "Mopwa", "Pa-Le-Chi", "Palachi", "Palakhi", "Palaychi" ], + "region": "MM", + "regionname": "Myanmar", + "script": "Latn", + "sldr": false, + "tag": "jkm-Latn", + "windows": "jkm-Latn" }, { "full": "jko-Latn-PG", @@ -49441,12 +51741,26 @@ "unwritten": true, "windows": "jkr-Zyyy" }, + { + "full": "jks-Zxxx-JP", + "iana": [ "Amami Koniya Sign Language" ], + "iso639_3": "jks", + "name": "Amami Koniya Sign Language", + "names": [ "Amami-Oshima Shuwa" ], + "region": "JP", + "regionname": "Japan", + "script": "Zxxx", + "sldr": false, + "tag": "jks", + "tags": [ "jks-JP", "jks-Zxxx", "sgn-jks", "sgn-jks-JP" ], + "windows": "jks-Zxxx" + }, { "full": "jku-Latn-NG", "iana": [ "Labir" ], "iso639_3": "jku", "name": "Labir", - "names": [ "Jakanci", "Jaku", "Jakun", "Labur" ], + "names": [ "Jaaku", "Jakanci", "Jaku", "Jakun", "Labur" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -49475,7 +51789,7 @@ "iana": [ "Jamaican Sign Language" ], "iso639_3": "jls", "name": "Jamaican Sign Language", - "names": [ "JSL" ], + "names": [ "JSL", "Jamaica Sign Language" ], "region": "JM", "regionname": "Jamaica", "script": "Zxxx", @@ -49489,7 +51803,7 @@ "iana": [ "Dima" ], "iso639_3": "jma", "name": "Dima", - "names": [ "Bayu Dimadima", "Dimadima", "Jimajima" ], + "names": [ "Bayu Dimadima", "Dimadima", "Jimajima", "Jimjam" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -49504,7 +51818,7 @@ "iana": [ "Zumbun" ], "iso639_3": "jmb", "name": "Zumbun", - "names": [ "Jimbin", "Jimbinawa" ], + "names": [ "Jimbin", "Jimbinawa", "Zibinju" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -49534,9 +51848,9 @@ "full": "jmd-Latn-ID", "iana": [ "Yamdena" ], "iso639_3": "jmd", - "localnames": [ "Tanempar Liriny" ], + "localnames": [ "Tanempar", "Tanempar Liriny" ], "name": "Yamdena", - "names": [ "Jamden", "Jamdena", "Tanempar Liriny", "Tanemprar lirin" ], + "names": [ "Jamden", "Jamdena", "Tanempar", "Tanempar Liriny", "Tanemprar lirin" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -49595,6 +51909,7 @@ "iana": [ "Kamara" ], "iso639_3": "jmr", "name": "Kamara", + "names": [ "Mara" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -49637,32 +51952,32 @@ "full": "jmx-Latn-MX", "iana": [ "Western Juxtlahuaca Mixtec" ], "iso639_3": "jmx", - "localnames": [ "To'on Savi" ], + "localnames": [ "Tu̱'un-yó" ], "name": "Mixtec, Western Juxtlahuaca", - "names": [ "Coicoyán Mixtec", "Mixteco del Oeste de Juxtlahuaca", "To’on Savi", "Western Juxtlahuaca Mixtec" ], + "names": [ "Coicoyán Mixtec", "Mixteco del Oeste Alto", "Mixteco del Oeste de Juxtlahuaca", "Tu̱’un davi", "Tu̱’un savi", "Tu̱’un-yó", "Western Juxtlahuaca Mixtec" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "jmx", "tags": [ "jmx-Latn", "jmx-MX" ], "windows": "jmx-Latn" }, { - "full": "jna-Zyyy-IN", + "full": "jna-Takr-IN", "iana": [ "Jangshung" ], "iso639_3": "jna", "name": "Jangshung", "names": [ "Central Kinnauri", "Jangiam", "Jangrami", "Thebarskad", "Thebor", "Thebör Skadd", "Zangram", "Zhang-Zhung" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Takr", "sldr": false, "tag": "jna", - "tags": [ "jna-IN", "jna-Zyyy" ], + "tags": [ "jna-IN", "jna-Takr" ], "unwritten": true, - "windows": "jna-Zyyy" + "windows": "jna-Takr" }, { "full": "jnd-Arab-PK", @@ -49684,7 +51999,7 @@ "iana": [ "Yangman" ], "iso639_3": "jng", "name": "Yangman", - "names": [ "Dagoman", "Jungman" ], + "names": [ "Dagoman", "Jangman", "Jongman", "Jungman", "Yung munni", "Yungman", "Yungmanni", "Yungmun", "Yungmunee", "Yungmunnee", "Yungmunni" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -49713,7 +52028,7 @@ "full": "jnj-Latn-ET", "iana": [ "Yemsa" ], "iso639_3": "jnj", - "localnames": [ "Yemsa" ], + "localnames": [ "Yèmsa" ], "name": "Yemsa", "region": "ET", "regionname": "Ethiopia", @@ -49723,6 +52038,21 @@ "tags": [ "jnj-ET", "jnj-Latn" ], "windows": "jnj-Latn" }, + { + "full": "jnj-Ethi-ET", + "iana": [ "Yemsa" ], + "iso639_3": "jnj", + "localname": "Yemsa", + "name": "Yemsa", + "nophonvars": true, + "obsolete": true, + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": true, + "tag": "jnj-Ethi", + "windows": "jnj-Ethi" + }, { "full": "jnl-Deva-IN", "iana": [ "Rawat" ], @@ -49784,15 +52114,14 @@ "full": "job-Latn-CD", "iana": [ "Joba" ], "iso639_3": "job", - "name": "Joba", - "names": [ "Kijoba", "Kivira", "Loba", "Vira" ], + "name": "Kiviila", + "names": [ "Baviila", "Joba", "Kijoba", "Kivira", "Loba", "Vira" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", "sldr": false, "tag": "job", "tags": [ "job-CD", "job-Latn" ], - "unwritten": true, "windows": "job-Latn" }, { @@ -49844,7 +52173,7 @@ "iana": [ "Jordanian Sign Language" ], "iso639_3": "jos", "name": "Jordanian Sign Language", - "names": [ "LIU", "Lughat il-Ishaarah il-Urduniyyah" ], + "names": [ "LIU", "Lughat al-Ishāra al-Urdunia", "Lughat il-Ishaarah il-Urduniyyah" ], "region": "JO", "regionname": "Jordan", "script": "Zxxx", @@ -49870,17 +52199,17 @@ "windows": "jow-Latn" }, { - "full": "jpa-Zyyy-PS", + "full": "jpa-Hebr-PS", "iana": [ "Jewish Palestinian Aramaic" ], "iso639_3": "jpa", "name": "Jewish Palestinian Aramaic", "region": "PS", "regionname": "State of Palestine", - "script": "Zyyy", + "script": "Hebr", "sldr": false, "tag": "jpa", - "tags": [ "jpa-PS", "jpa-Zyyy" ], - "windows": "jpa-Zyyy" + "tags": [ "jpa-Hebr", "jpa-PS" ], + "windows": "jpa-Hebr" }, { "full": "jpr-Hebr-IL", @@ -49919,7 +52248,7 @@ "localname": "Jơrai", "localnames": [ "Jơrai" ], "name": "Jarai", - "names": [ "Cho Rai", "Cho-Rai", "Chor", "Chrai", "Djarai", "Drai", "Gia Rai", "Gia-Rai", "Gio Rai", "Gio-Rai", "Jorai", "Jơrai", "Mthur", "Zrai" ], + "names": [ "Cho Rai", "Cho-Rai", "Chor", "Chrai", "Djarai", "Drai", "Gia Rai", "Gia-Rai", "Gio Rai", "Gio-Rai", "Jo’rai", "Jơrai", "Mthur", "Zrai" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "KH", "US" ], @@ -49927,14 +52256,14 @@ "sldr": true, "tag": "jra", "tags": [ "jra-Latn", "jra-VN" ], - "windows": "jra-Latn" + "windows": "jra-Latn-VN" }, { "full": "jra-Khmr-KH", "iana": [ "Jarai" ], "iso639_3": "jra", "name": "Jarai", - "names": [ "Cho Rai", "Cho-Rai", "Chor", "Chrai", "Djarai", "Drai", "Gia Rai", "Gia-Rai", "Gio Rai", "Gio-Rai", "Jorai", "Jơrai", "Mthur", "Zrai" ], + "names": [ "Cho Rai", "Cho-Rai", "Chor", "Chrai", "Djarai", "Drai", "Gia Rai", "Gia-Rai", "Gio Rai", "Gio-Rai", "Jo’rai", "Jơrai", "Mthur", "Zrai" ], "nophonvars": true, "region": "KH", "regionname": "Cambodia", @@ -49943,14 +52272,14 @@ "sldr": false, "tag": "jra-Khmr", "tags": [ "jra-KH" ], - "windows": "jra-Khmr" + "windows": "jra-Khmr-KH" }, { "full": "jrb-Hebr-IL", "iana": [ "Judeo-Arabic" ], "iso639_3": "jrb", - "name": "Arabic, Judeo-Moroccan", - "names": [ "Judeo-Arabic", "il‘arabiyya dyalna" ], + "name": "Judeo-Arabic", + "names": [ "Arabic, Judeo-Moroccan", "il‘arabiyya dyalna" ], "region": "IL", "regionname": "Israel", "regions": [ "CA", "MA" ], @@ -49958,7 +52287,23 @@ "sldr": false, "tag": "jrb", "tags": [ "aju-Hebr", "aju-Hebr-IL", "jrb-Hebr", "jrb-IL" ], - "windows": "jrb-Hebr" + "windows": "jrb-Hebr-IL" + }, + { + "full": "jrb-Arab-MA", + "iana": [ "Judeo-Arabic" ], + "iso639_3": "aju", + "macrolang": "jrb", + "name": "Arabic, Judeo-Moroccan", + "names": [ "Judeo-Arabic", "il‘arabiyya dyalna" ], + "region": "MA", + "regionname": "Morocco", + "regions": [ "CA", "IL" ], + "script": "Arab", + "sldr": false, + "tag": "jrb-Arab-MA", + "tags": [ "aju-Arab", "aju-Arab-MA" ], + "windows": "jrb-Arab-MA" }, { "full": "jrr-Latn-NG", @@ -49977,7 +52322,7 @@ }, { "full": "jrt-Latn-NG", - "iana": [ "Jorto" ], + "iana": [ "Jakattoe" ], "iso639_3": "jrt", "name": "Jakattoe", "names": [ "Chakato", "Jakato", "Jorto" ], @@ -50025,7 +52370,7 @@ "iso639_3": "jua", "localnames": [ "Kagwahiva" ], "name": "Júma", - "names": [ "Arara", "Kagwahibm", "Kagwahiph", "Kagwahiv", "Kagwahiva", "Kavahiva", "Kawahip", "Kawaib", "Yumá" ], + "names": [ "Arara", "Içuã Tupi", "Kagwahibm", "Kagwahiph", "Kagwahiv", "Kagwahiva", "Kavahiva", "Kawahip", "Kawaib", "Yumá" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -50066,15 +52411,15 @@ "full": "jud-Latn-CI", "iana": [ "Worodougou" ], "iso639_3": "jud", + "localnames": [ "Wüödughukakan" ], "name": "Worodougou", - "names": [ "Bakokan", "Ouorodougou", "Worodougou Jula", "Worodougoukakan", "Worodugu" ], + "names": [ "Bakokan", "Ouorodougou", "Worodougou Jula", "Worodougoukakan", "Worodugu", "Wüödughukakan" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", "sldr": false, "tag": "jud", "tags": [ "jud-CI", "jud-Latn" ], - "unwritten": true, "windows": "jud-Latn" }, { @@ -50125,6 +52470,8 @@ "full": "jul-Deva-NP", "iana": [ "Jirel" ], "iso639_3": "jul", + "latnnames": [ "jirel" ], + "localnames": [ "जिरेल" ], "name": "Jirel", "names": [ "Jiri", "Jirial", "Ziral" ], "region": "NP", @@ -50139,8 +52486,9 @@ "full": "jum-Latn-SD", "iana": [ "Jumjum" ], "iso639_3": "jum", + "localnames": [ "Tüŋ Mäbaan" ], "name": "Jumjum", - "names": [ "Berin", "Olga", "Wadega", "Wadkai" ], + "names": [ "Berin", "Jiik Mäbaan", "Olga", "Tüŋ Mäbaan", "Wadega", "Wadkai" ], "region": "SD", "regionname": "Sudan", "regions": [ "SS" ], @@ -50184,9 +52532,9 @@ "full": "jup-Latn-BR", "iana": [ "Hupdë" ], "iso639_3": "jup", - "localnames": [ "Hupdah" ], + "localnames": [ "Hup" ], "name": "Hupdë", - "names": [ "Hup", "Hupda", "Hupdah", "Hupdé", "Jupde", "Ubdé" ], + "names": [ "Hup", "Hupda", "Hupdah", "Hupdé", "Húpd’äh", "Jupde", "Ubdé" ], "region": "BR", "regionname": "Brazil", "regions": [ "CO" ], @@ -50256,7 +52604,7 @@ "iana": [ "Wãpha" ], "iso639_3": "juw", "name": "Wãpha", - "names": [ "Wase", "Wase Tofa" ], + "names": [ "Jukun Wase", "Wase", "Wase Tofa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -50267,17 +52615,17 @@ "windows": "juw-Latn" }, { - "full": "juy-Zyyy-IN", + "full": "juy-Orya-IN", "iana": [ "Juray" ], "iso639_3": "juy", "name": "Juray", "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Orya", "sldr": false, "tag": "juy", - "tags": [ "juy-IN", "juy-Zyyy" ], - "windows": "juy-Zyyy" + "tags": [ "juy-IN", "juy-Orya" ], + "windows": "juy-Orya" }, { "full": "jv-Latn-ID", @@ -50296,6 +52644,22 @@ "tags": [ "jv-ID", "jv-Latn", "jw", "jw-ID", "jw-Latn", "jw-Latn-ID" ], "windows": "jv-Latn" }, + { + "full": "jv-Arab-ID", + "iana": [ "Javanese" ], + "iso639_3": "jav", + "name": "Javanese", + "names": [ "Djawa", "Jawa" ], + "nophonvars": true, + "obsolete": true, + "region": "ID", + "regionname": "Indonesia", + "regions": [ "MY", "NL", "SG", "US" ], + "script": "Arab", + "sldr": false, + "tag": "jv-Arab", + "windows": "jv-Arab" + }, { "full": "jv-Java-ID", "iana": [ "Javanese" ], @@ -50376,6 +52740,7 @@ "full": "jye-Hebr-IL", "iana": [ "Judeo-Yemeni Arabic" ], "iso639_3": "jye", + "macrolang": "jrb", "name": "Arabic, Judeo-Yemeni", "names": [ "Judeo-Yemeni", "Judeo-Yemeni Arabic", "Yemenite", "Yemenite Judeo-Arabic", "Yeminite" ], "region": "IL", @@ -50392,6 +52757,7 @@ "iana": [ "Jaya" ], "iso639_3": "jyy", "name": "Jaya", + "names": [ "Yaya" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -50453,7 +52819,7 @@ }, { "full": "kaa-Cyrl-UZ", - "iana": [ "Kara-Kalpak" ], + "iana": [ "Kara-Kalpak", "Karakalpak" ], "iso639_3": "kaa", "latnnames": [ "Qaraqalpaq tili" ], "localnames": [ "Қарақалпақ тили" ], @@ -50470,7 +52836,7 @@ }, { "full": "kaa-Arab-UZ", - "iana": [ "Kara-Kalpak" ], + "iana": [ "Kara-Kalpak", "Karakalpak" ], "iso639_3": "kaa", "name": "Karakalpak", "names": [ "Kara-Kalpak", "Karaklobuk", "Klobouki", "Qaraqalpaq tili", "Tchorny" ], @@ -50485,7 +52851,7 @@ }, { "full": "kaa-Latn-UZ", - "iana": [ "Kara-Kalpak" ], + "iana": [ "Kara-Kalpak", "Karakalpak" ], "iso639_3": "kaa", "localnames": [ "Qaraqalpaq tili" ], "name": "Karakalpak", @@ -50504,8 +52870,8 @@ "iso639_3": "kab", "localname": "Taqbaylit", "localnames": [ "Taqbaylit", "Tazwawt" ], - "name": "Kabyle", - "names": [ "Amazigh", "Kabyl", "Kabylia", "Tamazight", "Taqbaylit", "Tazwawt" ], + "name": "Amazigh", + "names": [ "Kabyl", "Kabyle", "Kabylia", "Tamazight", "Taqbaylit", "Tazwawt" ], "region": "DZ", "regionname": "Algeria", "regions": [ "BE", "FR" ], @@ -50519,8 +52885,8 @@ "full": "kab-Arab-DZ", "iana": [ "Kabyle" ], "iso639_3": "kab", - "name": "Kabyle", - "names": [ "Amazigh", "Kabyl", "Kabylia", "Tamazight", "Taqbaylit", "Tazwawt" ], + "name": "Amazigh", + "names": [ "Kabyl", "Kabyle", "Kabylia", "Tamazight", "Taqbaylit", "Tazwawt" ], "nophonvars": true, "region": "DZ", "regionname": "Algeria", @@ -50534,8 +52900,8 @@ "full": "kab-Tfng-DZ", "iana": [ "Kabyle" ], "iso639_3": "kab", - "name": "Kabyle", - "names": [ "Amazigh", "Kabyl", "Kabylia", "Tamazight", "Taqbaylit", "Tazwawt" ], + "name": "Amazigh", + "names": [ "Kabyl", "Kabyle", "Kabylia", "Tamazight", "Taqbaylit", "Tazwawt" ], "nophonvars": true, "region": "DZ", "regionname": "Algeria", @@ -50565,6 +52931,7 @@ "full": "kad-Latn-NG", "iana": [ "Adara" ], "iso639_3": "kad", + "localname": "Ànda", "localnames": [ "Ànda" ], "name": "Adara", "names": [ "Kadara", "Ànda", "Èdà" ], @@ -50581,7 +52948,7 @@ "iana": [ "Ketangalan" ], "iso639_3": "kae", "name": "Ketangalan", - "names": [ "Basay", "Ketagalan", "Sprache von Formosa", "Tangalan" ], + "names": [ "Ketagalan", "Ketangalano", "Sprache von Formosa", "Tangalan" ], "region": "TW", "regionname": "China-Taiwan", "script": "Zyyy", @@ -50610,7 +52977,7 @@ "iana": [ "Kajaman" ], "iso639_3": "kag", "name": "Kajaman", - "names": [ "Kayaman", "Kejaman" ], + "names": [ "Kajaman-Sekapan", "Kayaman", "Kejaman" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -50640,7 +53007,7 @@ "iana": [ "Karekare" ], "iso639_3": "kai", "name": "Karekare", - "names": [ "Karai Karai", "Karai-karai", "Karaikarai", "Kerekere", "Kerrikerri" ], + "names": [ "Karai Karai", "Karai-karai", "Karaikarai", "Kerekere", "Kerikeri", "Kәrekәre" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -50656,7 +53023,7 @@ "localname": "Jju", "localnames": [ "Jju", "Kәjju" ], "name": "Jju", - "names": [ "Kache", "Kaje", "Kajji" ], + "names": [ "Bajju", "Baju", "Kache", "Kaje", "Kajji", "Kәjju" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -50671,7 +53038,7 @@ "iso639_3": "kak", "localnames": [ "Kalanguya" ], "name": "Kalanguya", - "names": [ "Ikalahan", "Kalangoya", "Kalangoya-Ikalahan", "Kayapa Kallahan" ], + "names": [ "Ahin-Kayapa Kalanguya", "Ikalahan", "Kalangoya", "Kalangoya-Ikalahan", "Kayapa Kallahan" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -50685,9 +53052,9 @@ "iana": [ "Kamba (Kenya)" ], "iso639_3": "kam", "localname": "Kikamba", - "localnames": [ "Kĩĩkamba" ], + "localnames": [ "Kĩkamba" ], "name": "Kamba", - "names": [ "Akamba", "Kamba (Kenya)", "Kekamba", "Kiikamba", "Kikamba", "Kĩĩkamba" ], + "names": [ "Akamba", "Kamba (Kenya)", "Kiikamba", "Kikamba", "Kĩkamba", "Kĩĩkamba" ], "region": "KE", "regionname": "Kenya", "regions": [ "TZ" ], @@ -50737,7 +53104,7 @@ "localname": "Capa Baquebo", "localnames": [ "Capa Baquebo" ], "name": "Capanahua", - "names": [ "Capa Baquebo", "Capabaquebo", "Capacho", "Capanawa", "Kapanawa", "Nuquencaibo" ], + "names": [ "Capa Baquebo", "Capabaquebo", "Capanawa", "Kapanawa", "Nuquencaibo" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -50761,18 +53128,34 @@ "unwritten": true, "windows": "kav-Latn" }, + { + "full": "kaw-Kawi-ID", + "iana": [ "Kawi" ], + "iso639_3": "kaw", + "name": "Kawi", + "region": "ID", + "regionname": "Indonesia", + "regions": [ "MY" ], + "script": "Kawi", + "sldr": false, + "tag": "kaw", + "tags": [ "kaw-ID", "kaw-Kawi" ], + "windows": "kaw-Kawi-ID" + }, { "full": "kaw-Bali-MY", "iana": [ "Kawi" ], "iso639_3": "kaw", "name": "Kawi", + "nophonvars": true, "region": "MY", "regionname": "Malaysia", + "regions": [ "ID" ], "script": "Bali", "sldr": false, - "tag": "kaw", - "tags": [ "kaw-Bali", "kaw-MY" ], - "windows": "kaw-Bali" + "tag": "kaw-Bali", + "tags": [ "kaw-MY" ], + "windows": "kaw-Bali-MY" }, { "full": "kax-Latn-ID", @@ -50808,7 +53191,7 @@ "iana": [ "Kalarko" ], "iso639_3": "kba", "name": "Kalarko", - "names": [ "Galaagu", "Kalaako", "Kalaaku", "Kalako", "Kalakul", "Malba", "Malpa" ], + "names": [ "Galaagu", "Kalaako", "Kalaaku", "Kalako", "Kalakul", "Kalarku", "Malba", "Malpa" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -50837,8 +53220,10 @@ "full": "kbc-Latn-BR", "iana": [ "Kadiwéu" ], "iso639_3": "kbc", + "localname": "Goniwoladi ejiwajegi", + "localnames": [ "Goniwoladi ejiwajegi" ], "name": "Kadiwéu", - "names": [ "Cadiuéu", "Caduvéo", "Ediu-Adig", "Ejiwajigi", "Kadiweo", "Mbaya-Guaikuru" ], + "names": [ "Cadiuéu", "Caduvéo", "Ediu-Adig", "Ejiwajigi", "Eyiguayegi", "Goniwoladi ejiwajegi", "Kadiweo", "Mbaya-Guaikuru" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -50880,28 +53265,27 @@ "windows": "kbe-Latn" }, { - "full": "kbg-Tibt-IN-x-hingna", + "full": "kbg-Tibt-IN", "iana": [ "Khamba" ], "iso639_3": "kbg", "name": "Khamba", "names": [ "Khamba Khaadi" ], - "nophonvars": true, "region": "IN", "regionname": "India", "script": "Tibt", "sldr": false, - "tag": "kbg-Tibt", - "tags": [ "kbg-IN-x-hingna", "kbg-Tibt-IN", "kbg-Tibt-x-hingna" ], + "tag": "kbg", + "tags": [ "kbg-IN", "kbg-Tibt" ], "windows": "kbg-Tibt" }, { "full": "kbh-Latn-CO", "iana": [ "Camsá" ], "iso639_3": "kbh", - "localname": "Camënts̈á", - "localnames": [ "Camënts̈á" ], + "localname": "Camëntsá", + "localnames": [ "Camëntsá" ], "name": "Camsá", - "names": [ "Camënts̈á", "Camëntxá", "Coche", "Kamemtxa", "Kamsa", "Kamse", "Kamëntsa", "Sibundoy", "Sibundoy-Gaché" ], + "names": [ "Camëntsá", "Camëntxá", "Coche", "Kamemtxa", "Kamsa", "Kamse", "Kamëntsa", "Sibundoy", "Sibundoy-Gaché" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -50944,15 +53328,15 @@ "full": "kbk-Latn-PG", "iana": [ "Grass Koiari" ], "iso639_3": "kbk", - "name": "Koiari, Grass", - "names": [ "Grass Koiari", "Koiari" ], + "localnames": [ "Koiari" ], + "name": "Koiari", + "names": [ "Grass Koiari" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "kbk", "tags": [ "kbk-Latn", "kbk-PG" ], - "unwritten": true, "windows": "kbk-Latn" }, { @@ -50990,9 +53374,8 @@ "full": "kbn-Latn-CF", "iana": [ "Kare (Central African Republic)" ], "iso639_3": "kbn", - "localnames": [ "Kare" ], "name": "Kare", - "names": [ "Kali", "Kare (Central African Republic)", "Kareng", "Kari", "Karré", "Kãrɛ̃" ], + "names": [ "Kali", "Kare (Central African Republic)", "Kareng", "Kari", "Karré", "Kãrɛ̃", "Labi-Kare", "nzáà kã́rĩ́" ], "region": "CF", "regionname": "Central African Republic", "regions": [ "CM" ], @@ -51023,6 +53406,7 @@ "full": "kbp-Latn-TG", "iana": [ "Kabiyè" ], "iso639_3": "kbp", + "localname": "Kabɩyɛ", "localnames": [ "Kabɩyɛ", "Kabɩyɛ Tɔm" ], "name": "Kabiyè", "names": [ "Cabrai", "Cabrais", "Kabire", "Kabre", "Kabure", "Kabye", "Kabɩyɛ", "Kabɩyɛ Tɔm" ], @@ -51084,12 +53468,13 @@ "full": "kbr-Ethi-ET", "iana": [ "Kafa" ], "iso639_3": "kbr", + "localname": "Kafa", "name": "Kafa", "nophonvars": true, "region": "ET", "regionname": "Ethiopia", "script": "Ethi", - "sldr": false, + "sldr": true, "tag": "kbr-Ethi", "windows": "kbr-Ethi" }, @@ -51098,7 +53483,7 @@ "iana": [ "Kande" ], "iso639_3": "kbs", "name": "Kande", - "names": [ "Kanda", "Okande" ], + "names": [ "Kaande", "Kanda", "Okande" ], "region": "GA", "regionname": "Gabon", "script": "Latn", @@ -51142,8 +53527,9 @@ "full": "kbv-Latn-ID", "iana": [ "Dera (Indonesia)" ], "iso639_3": "kbv", - "name": "Dera", - "names": [ "Dera (Indonesia)", "Dla", "Dra", "Kamberataro", "Kamberatoro", "Komberatoro", "Mangguar" ], + "localnames": [ "Dla" ], + "name": "Dla", + "names": [ "Awe", "Dera", "Dera (Indonesia)", "Dra", "Kamberataro", "Kamberatoro", "Komberatoro", "Mangguar" ], "region": "ID", "regionname": "Indonesia", "regions": [ "PG" ], @@ -51186,8 +53572,9 @@ "full": "kby-Arab-NE", "iana": [ "Manga Kanuri" ], "iso639_3": "kby", + "macrolang": "kr", "name": "Kanuri, Manga", - "names": [ "Kanouri", "Kanoury", "Kanuri", "Manga", "Manga Kanuri", "kànòrí" ], + "names": [ "Kanouri", "Kanoury", "Kanuri", "Kànòrí", "Manga", "Manga Kanuri" ], "region": "NE", "regionname": "Niger", "regions": [ "NG" ], @@ -51201,9 +53588,11 @@ "full": "kby-Latn-NE", "iana": [ "Manga Kanuri" ], "iso639_3": "kby", - "localnames": [ "kànòrí" ], + "localname": "Manga", + "localnames": [ "Manga" ], + "macrolang": "kr", "name": "Kanuri, Manga", - "names": [ "Kanouri", "Kanoury", "Kanuri", "Manga", "Manga Kanuri", "kànòrí" ], + "names": [ "Kanouri", "Kanoury", "Kanuri", "Kànòrí", "Manga", "Manga Kanuri" ], "region": "NE", "regionname": "Niger", "regions": [ "NG" ], @@ -51234,7 +53623,7 @@ "latnnames": [ "hantĩ jasaň" ], "localnames": [ "ханты ясаң" ], "name": "Khanty", - "names": [ "Hanty", "Khant", "Khanti", "Ostjak", "Ostyak", "Xanty" ], + "names": [ "Hanty", "Kazym-Berezover-Suryskarer Khanty", "Khant", "Khanti", "Ostjak", "Ostyak", "Xanty" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -51278,7 +53667,7 @@ "iana": [ "Ngkâlmpw Kanum" ], "iso639_3": "kcd", "name": "Kanum, Ngkâlmpw", - "names": [ "Enkelembu", "Kanum", "Kenume", "Knwne", "Ngkâlmpw Kanum" ], + "names": [ "Enkelembu", "Kanum", "Kenume", "Knwne", "Ngkolmpu", "Ngkontar Ngkolmpu", "Ngkâlmpw Kanum" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -51308,7 +53697,7 @@ "iana": [ "Ukaan" ], "iso639_3": "kcf", "name": "Ukaan", - "names": [ "AIKA (Ayanran-Ishe-Kakumo-Auga)", "Anyaran", "Auga", "Ikan", "Kakumo" ], + "names": [ "AIKA (Ayanran-Ishe-Kakumo-Auga)", "Anyaran", "Ikaan", "Ikani", "Ìkàn", "Ìkã", "Ùkãã" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -51322,6 +53711,8 @@ "full": "kcg-Latn-NG", "iana": [ "Tyap" ], "iso639_3": "kcg", + "localname": "Tyap", + "localnames": [ "Tyap" ], "name": "Tyap", "names": [ "Katab", "Kataf" ], "region": "NG", @@ -51337,7 +53728,7 @@ "iana": [ "Vono" ], "iso639_3": "kch", "name": "Vono", - "names": [ "Kiballo", "Kiwollo" ], + "names": [ "Avɔnɔ", "Kiballo", "Kibolo", "Kivɔnɔ", "Kiwollo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -51367,7 +53758,7 @@ "iana": [ "Kobiana" ], "iso639_3": "kcj", "name": "Kobiana", - "names": [ "Buy", "Cobiana", "Guboy", "Uboi" ], + "names": [ "Buy", "Cobiana", "Gubhoi", "Guboy", "Uboi" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "SN" ], @@ -51382,9 +53773,9 @@ "full": "kck-Latn-ZW", "iana": [ "Kalanga" ], "iso639_3": "kck", - "localnames": [ "Ikalanga", "Tjikalanga" ], + "localnames": [ "Tjikalanga" ], "name": "Kalanga", - "names": [ "Bakaa", "Chikalanga", "Ikalanga", "Kalagan", "Kalaka", "Kalana", "Kanana", "Makalaka", "Sekalaka", "Sekalaña", "Tjikalanga", "Wakalanga", "Western Shona" ], + "names": [ "Bakaa", "Chikalanga", "Ikalanga", "Kalagan", "Kalaka", "Kalana", "Kanana", "Makalaka", "Sekalaka", "Sekalanga", "Sekalaña", "Tjikalanga", "Wakalanga", "Western Shona" ], "region": "ZW", "regionname": "Zimbabwe", "regions": [ "BW" ], @@ -51421,15 +53812,15 @@ "sldr": false, "tag": "kcm", "tags": [ "kcm-CF", "kcm-Latn" ], - "unwritten": true, "windows": "kcm-Latn" }, { "full": "kcn-Latn-UG", "iana": [ "Nubi" ], "iso639_3": "kcn", + "localnames": [ "Rutan Nubi" ], "name": "Nubi", - "names": [ "Ki-Nubi", "Kinubi" ], + "names": [ "Ki-Nubi", "Kinubi", "Rutan Nubi" ], "region": "UG", "regionname": "Uganda", "regions": [ "KE", "US" ], @@ -51530,7 +53921,7 @@ "iana": [ "Kami (Tanzania)" ], "iso639_3": "kcu", "name": "Kami", - "names": [ "Kami (Tanzania)", "Kikami" ], + "names": [ "Kami (Tanzania)", "Kikami", "Wakami" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -51585,19 +53976,19 @@ "windows": "kcx-Zyyy" }, { - "full": "kcy-Zyyy-DZ", + "full": "kcy-Arab-DZ", "iana": [ "Korandje" ], "iso639_3": "kcy", "name": "Korandje", - "names": [ "Belbalis", "Kwarandzyey" ], + "names": [ "Belbalis", "Kwarandzyey", "Tabelbala", "lbəlbaliyya" ], "region": "DZ", "regionname": "Algeria", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "kcy", - "tags": [ "kcy-DZ", "kcy-Zyyy" ], + "tags": [ "kcy-Arab", "kcy-DZ" ], "unwritten": true, - "windows": "kcy-Zyyy" + "windows": "kcy-Arab" }, { "full": "kcz-Latn-TZ", @@ -51666,7 +54057,7 @@ "localname": "Chimakonde", "localnames": [ "Chimakonde" ], "name": "Makonde", - "names": [ "Chimakonde", "Chinimakonde", "Cimakonde", "Kimakonde", "Konde", "Maconde", "Makonda", "Matambe", "Matambwe", "Mekwengo", "Shimakonde", "Simakonde" ], + "names": [ "Chimakonde", "Chinimakonde", "Cimakonde", "Kimakonde", "Konde", "Maconde", "Makonda", "Matambwe", "Mekwengo", "Shimakonde", "Simakonde", "Símákòòndè" ], "region": "TZ", "regionname": "United Republic of Tanzania", "regions": [ "MZ" ], @@ -51680,6 +54071,7 @@ "full": "kdf-Latn-PG", "iana": [ "Mamusi" ], "iso639_3": "kdf", + "localnames": [ "Mamusi" ], "name": "Mamusi", "names": [ "Kakuna" ], "region": "PG", @@ -51705,35 +54097,52 @@ "unwritten": true, "windows": "kdg-Latn" }, + { + "full": "kdh-Latn-TG", + "iana": [ "Tem" ], + "iso639_3": "kdh", + "localname": "Tem", + "localnames": [ "Tem" ], + "name": "Tem", + "names": [ "Cotocoli", "Kotokoli", "Temba", "Tembe", "Tim", "Timu" ], + "region": "TG", + "regionname": "Togo", + "regions": [ "BJ", "GH" ], + "script": "Latn", + "sldr": true, + "tag": "kdh", + "tags": [ "kdh-Latn", "kdh-TG" ], + "windows": "kdh-Latn" + }, { "full": "kdh-Arab-TG", "iana": [ "Tem" ], "iso639_3": "kdh", "name": "Tem", "names": [ "Cotocoli", "Kotokoli", "Temba", "Tembe", "Tim", "Timu" ], + "nophonvars": true, "region": "TG", "regionname": "Togo", "regions": [ "BJ", "GH" ], "script": "Arab", "sldr": true, - "tag": "kdh", - "tags": [ "kdh-Arab", "kdh-TG" ], + "tag": "kdh-Arab", "windows": "kdh-Arab" }, { - "full": "kdh-Latn-TG", + "full": "kdh-Brai-TG", "iana": [ "Tem" ], "iso639_3": "kdh", - "localnames": [ "Tem" ], "name": "Tem", "names": [ "Cotocoli", "Kotokoli", "Temba", "Tembe", "Tim", "Timu" ], + "nophonvars": true, "region": "TG", "regionname": "Togo", "regions": [ "BJ", "GH" ], - "script": "Latn", - "sldr": true, - "tag": "kdh-Latn", - "windows": "kdh-Latn" + "script": "Brai", + "sldr": false, + "tag": "kdh-Brai", + "windows": "kdh-Brai" }, { "full": "kdi-Latn-UG", @@ -51787,7 +54196,7 @@ "localname": "Tsikimba", "localnames": [ "Tsikimba" ], "name": "Tsikimba", - "names": [ "Akimba", "Auna", "Kambari", "Kamberchi", "Kamberri", "Kimba" ], + "names": [ "Akimba", "Auna", "Kambari", "Kamberchi", "Kamberri", "Kimba", "Wara" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -51815,9 +54224,9 @@ "full": "kdn-Latn-ZW", "iana": [ "Kunda" ], "iso639_3": "kdn", - "localnames": [ "Chikunda" ], + "localnames": [ "Mu Chikunda" ], "name": "Kunda", - "names": [ "Achikunda", "Chicunda", "Chikunda", "Cikunda" ], + "names": [ "Achikunda", "Chicunda", "Chikunda", "Cikunda", "Mu Chikunda" ], "region": "ZW", "regionname": "Zimbabwe", "regions": [ "MZ", "ZM" ], @@ -51839,7 +54248,6 @@ "sldr": false, "tag": "kdp", "tags": [ "kdp-Latn", "kdp-NG" ], - "unwritten": true, "windows": "kdp-Latn" }, { @@ -51871,7 +54279,7 @@ "sldr": false, "tag": "kdr", "tags": [ "kdr-LT", "kdr-Latn" ], - "windows": "kdr-Latn" + "windows": "kdr-Latn-LT" }, { "full": "kdr-Cyrl-UA", @@ -51887,7 +54295,7 @@ "sldr": false, "tag": "kdr-Cyrl", "tags": [ "kdr-UA" ], - "windows": "kdr-Cyrl" + "windows": "kdr-Cyrl-UA" }, { "full": "kdt-Thai-TH", @@ -51904,7 +54312,7 @@ "sldr": false, "tag": "kdt", "tags": [ "kdt-TH", "kdt-Thai" ], - "windows": "kdt-Thai" + "windows": "kdt-Thai-TH" }, { "full": "kdt-Khmr-KH", @@ -51920,7 +54328,7 @@ "sldr": false, "tag": "kdt-Khmr", "tags": [ "kdt-KH" ], - "windows": "kdt-Khmr" + "windows": "kdt-Khmr-KH" }, { "full": "kdt-Laoo-LA", @@ -51936,7 +54344,7 @@ "sldr": false, "tag": "kdt-Laoo", "tags": [ "kdt-LA" ], - "windows": "kdt-Laoo" + "windows": "kdt-Laoo-LA" }, { "full": "kdu-Zyyy-SD", @@ -52049,7 +54457,7 @@ "iana": [ "Keiga" ], "iso639_3": "kec", "name": "Keiga", - "names": [ "Kayigang", "Kayikang", "Keiga-Al-Kheil", "Keiga-Timero", "Sani ma-yɛgaŋ", "Yega" ], + "names": [ "Deiga", "Kayigang", "Kayikang", "Keiga-Al-Kheil", "Keiga-Timero", "Sani ma-yɛgaŋ", "Yega" ], "region": "SD", "regionname": "Sudan", "script": "Latn", @@ -52106,7 +54514,7 @@ "iana": [ "Tese" ], "iso639_3": "keg", "name": "Tese", - "names": [ "Keiga Girru", "Keiga Jirru", "Teis-Umm-Danab" ], + "names": [ "Keiga Girru", "Keiga Jirru", "Teis-Umm-Danab", "These" ], "region": "SD", "regionname": "Sudan", "script": "Latn", @@ -52149,7 +54557,7 @@ "iana": [ "Kadar" ], "iso639_3": "kej", "name": "Kadar", - "names": [ "Kada" ], + "names": [ "Kada", "Kadir" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -52163,10 +54571,10 @@ "full": "kek-Latn-GT", "iana": [ "Kekchí" ], "iso639_3": "kek", - "localname": "Q’eqchi’", + "localname": "Qꞌeqchiꞌ", "localnames": [ "Q'eqchi'" ], "name": "Q’eqchi’", - "names": [ "Cacche’", "Cacché", "Kekchi’", "Kekchí", "Ketchi", "Ketchi’", "Kʼekchiʼ", "Quecchi’", "Quecchí" ], + "names": [ "Cacche’", "Cacché", "Kekchi’", "Kekchí", "Ketchi", "Ketchi’", "Kʼekchiʼ", "Kʼeqchi’", "Quecchi’", "Quecchí" ], "region": "GT", "regionname": "Guatemala", "regions": [ "BZ" ], @@ -52180,6 +54588,7 @@ "full": "kel-Latn-CD", "iana": [ "Kela (Democratic Republic of Congo)" ], "iso639_3": "kel", + "localnames": [ "Ikela" ], "name": "Kela", "names": [ "Ikela", "Kela (Democratic Republic of Congo)", "Lemba", "Okela" ], "region": "CD", @@ -52271,7 +54680,7 @@ "iso639_3": "ker", "localnames": [ "Keera" ], "name": "Kera", - "names": [ "Keera" ], + "names": [ "Keera", "Kere" ], "region": "TD", "regionname": "Chad", "regions": [ "CM" ], @@ -52327,26 +54736,41 @@ "windows": "keu-Latn" }, { - "full": "kev-Zyyy-IN", + "full": "kev-Mlym-IN", "iana": [ "Kanikkaran" ], "iso639_3": "kev", "name": "Kanikkaran", "names": [ "Kanikkar", "Kannikan", "Kannikaran", "Kannikharan", "Malampashi" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Mlym", "sldr": false, "tag": "kev", - "tags": [ "kev-IN", "kev-Zyyy" ], - "windows": "kev-Zyyy" + "tags": [ "kev-IN", "kev-Mlym" ], + "windows": "kev-Mlym" + }, + { + "full": "kev-Taml-IN", + "iana": [ "Kanikkaran" ], + "iso639_3": "kev", + "name": "Kanikkaran", + "names": [ "Kanikkar", "Kannikan", "Kannikaran", "Kannikharan", "Malampashi" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Taml", + "sldr": false, + "tag": "kev-Taml", + "windows": "kev-Taml" }, { "full": "kew-Latn-PG", "iana": [ "West Kewa" ], "iso639_3": "kew", "localname": "adaa agaa", - "name": "Kewa, West", - "names": [ "Pasuma", "West Kewa", "adaa agaa", "adaa agale" ], + "localnames": [ "Adaa Agaa" ], + "name": "Kewapi, West", + "names": [ "Adaa Agaa", "Adaa Agale", "Pasuma", "West Kewa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -52403,9 +54827,10 @@ "full": "kez-Latn-NG", "iana": [ "Kukele" ], "iso639_3": "kez", + "localname": "Kukele", "localnames": [ "Kukele" ], "name": "Kukele", - "names": [ "Bakele", "Ukele" ], + "names": [ "Bakele", "Ukele", "Ukelle" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -52465,18 +54890,18 @@ "windows": "kfc-Telu" }, { - "full": "kfd-Zyyy-IN", + "full": "kfd-Knda-IN", "iana": [ "Korra Koraga" ], "iso639_3": "kfd", "name": "Koraga, Korra", "names": [ "Koragar", "Koragara", "Korangi", "Korra", "Korra Koraga" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Knda", "sldr": false, "tag": "kfd", - "tags": [ "kfd-IN", "kfd-Zyyy" ], - "windows": "kfd-Zyyy" + "tags": [ "kfd-IN", "kfd-Knda" ], + "windows": "kfd-Knda" }, { "full": "kfe-Taml-IN", @@ -52493,34 +54918,35 @@ "windows": "kfe-Taml" }, { - "full": "kff-Deva-IN", + "full": "kff-Latn-IN", "iana": [ "Koya" ], "iso639_3": "kff", - "latnnames": [ "Koya" ], - "localnames": [ "कोया" ], + "localnames": [ "Koya" ], "name": "Koya", "names": [ "Kavor", "Kaya", "Koa", "Koi", "Koi Gondi", "Koitar", "Koyato", "Koyi", "Raj Koya" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Latn", "sldr": false, - "tag": "kff-Deva", - "windows": "kff-Deva" + "tag": "kff", + "tags": [ "kff-IN", "kff-Latn" ], + "windows": "kff-Latn" }, { - "full": "kff-Latn-IN", + "full": "kff-Deva-IN", "iana": [ "Koya" ], "iso639_3": "kff", - "localnames": [ "Koya" ], + "latnnames": [ "Koya" ], + "localnames": [ "कोया" ], "name": "Koya", "names": [ "Kavor", "Kaya", "Koa", "Koi", "Koi Gondi", "Koitar", "Koyato", "Koyi", "Raj Koya" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Deva", "sldr": false, - "tag": "kff-Latn", - "windows": "kff-Latn" + "tag": "kff-Deva", + "windows": "kff-Deva" }, { "full": "kff-Orya-IN", @@ -52557,13 +54983,13 @@ "iana": [ "Kudiya" ], "iso639_3": "kfg", "name": "Kudiya", - "names": [ "Male Kudiya" ], - "nophonvars": true, + "names": [ "Kurku", "Male Kudiya" ], "region": "IN", "regionname": "India", "script": "Knda", "sldr": false, - "tag": "kfg-Knda", + "tag": "kfg", + "tags": [ "kfg-IN", "kfg-Knda" ], "windows": "kfg-Knda" }, { @@ -52571,7 +54997,7 @@ "iana": [ "Kudiya" ], "iso639_3": "kfg", "name": "Kudiya", - "names": [ "Male Kudiya" ], + "names": [ "Kurku", "Male Kudiya" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -52674,6 +55100,7 @@ "iana": [ "Kung" ], "iso639_3": "kfl", "name": "Kung", + "names": [ "Cungo" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -52688,6 +55115,7 @@ "iana": [ "Khunsari" ], "iso639_3": "kfm", "name": "Khunsari", + "names": [ "Khunsaric", "Kuk" ], "region": "IR", "regionname": "Islamic Republic of Iran", "script": "Arab", @@ -52728,8 +55156,10 @@ "full": "kfp-Deva-IN", "iana": [ "Korwa" ], "iso639_3": "kfp", + "latnnames": [ "Kodava" ], + "localnames": [ "कोड़वा" ], "name": "Korwa", - "names": [ "Ernga", "Singli" ], + "names": [ "Ernga", "Erngga", "Korowa", "Singli" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -52767,12 +55197,29 @@ "sldr": false, "tag": "kfr", "tags": [ "kfr-Deva", "kfr-IN" ], - "windows": "kfr-Deva" + "windows": "kfr-Deva-IN" + }, + { + "full": "kfr-Arab-PK", + "iana": [ "Kachhi", "Kutchi" ], + "iso639_3": "kfr", + "name": "Kacchi", + "names": [ "Cuchi", "Cutch", "Kachchhi", "Kachchi", "Kachhi", "Kachi", "Katch", "Katchi", "Kautchy", "Kutchchi", "Kutchi", "Kutchie" ], + "nophonvars": true, + "region": "PK", + "regionname": "Pakistan", + "script": "Arab", + "sldr": false, + "tag": "kfr-Arab", + "tags": [ "kfr-PK" ], + "windows": "kfr-Arab-PK" }, { "full": "kfr-Gujr-IN", "iana": [ "Kachhi", "Kutchi" ], "iso639_3": "kfr", + "latnnames": [ "Kacchi" ], + "localnames": [ "કચ્છી" ], "name": "Kacchi", "names": [ "Cuchi", "Cutch", "Cutchi", "Kachchhi", "Kachchi", "Kachhi", "Kachi", "Katch", "Katchi", "Kautchy", "Kutchchi", "Kutchi", "Kutchie" ], "nophonvars": true, @@ -52782,14 +55229,16 @@ "script": "Gujr", "sldr": false, "tag": "kfr-Gujr", - "windows": "kfr-Gujr" + "windows": "kfr-Gujr-IN" }, { "full": "kfs-Deva-IN", "iana": [ "Bilaspuri" ], "iso639_3": "kfs", + "latnnames": [ "Bilāsapūrī" ], + "localnames": [ "बिलासपूरी" ], "name": "Bilaspuri", - "names": [ "Bilaspuri Pahari", "Kahluri", "Kehloori Pahari", "Kehluri", "Pacchmi" ], + "names": [ "Bilaspuri Pahari", "Himachali", "Kahluri", "Kehloori Pahari", "Kehluri", "Pacchmi", "Pahari" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -52883,6 +55332,36 @@ "tag": "kfx-Takr", "windows": "kfx-Takr" }, + { + "full": "kfx-Deva-IN-x-inner", + "iana": [ "Kullu Pahari" ], + "iso639_3": "kfx", + "name": "Pahari, Kullu", + "names": [ "Kauli", "Kullu Pahari", "Kullui", "Kulu Boli", "Kulu Pahari", "Kului", "Kulvi", "Kulwali", "Pahari", "Pahari Kullu", "Phari Kulu" ], + "region": "IN", + "regionname": "India", + "rod": "3313", + "script": "Deva", + "sldr": false, + "tag": "kfx-x-inner", + "tags": [ "kfx-Deva-x-inner", "kfx-IN-x-inner" ], + "windows": "kfx-Deva-x-inner" + }, + { + "full": "kfx-Deva-IN-x-mandi", + "iana": [ "Kullu Pahari" ], + "iso639_3": "kfx", + "name": "Pahari, Kullu", + "names": [ "Kauli", "Kullu Pahari", "Kullui", "Kulu Boli", "Kulu Pahari", "Kului", "Kulvi", "Kulwali", "Pahari", "Pahari Kullu", "Phari Kulu" ], + "region": "IN", + "regionname": "India", + "rod": "3372", + "script": "Deva", + "sldr": false, + "tag": "kfx-x-mandi", + "tags": [ "kfx-Deva-x-mandi", "kfx-IN-x-mandi" ], + "windows": "kfx-Deva-x-mandi" + }, { "full": "kfy-Deva-IN", "iana": [ "Kumaoni" ], @@ -52899,13 +55378,47 @@ "tags": [ "kfy-Deva", "kfy-IN" ], "windows": "kfy-Deva" }, + { + "full": "kfy-Deva-IN-x-pash", + "iana": [ "Kumaoni" ], + "iso639_3": "kfy", + "latnnames": [ "Kumaaũũnii" ], + "localnames": [ "कुमाँऊनी" ], + "name": "Kumaoni", + "names": [ "Kamaoni", "Kumau", "Kumauni", "Kumawani", "Kumgoni", "Kumman", "Kunayaoni" ], + "region": "IN", + "regionname": "India", + "rod": "12469", + "script": "Deva", + "sldr": false, + "tag": "kfy-x-pash", + "tags": [ "kfy-Deva-x-pash", "kfy-IN-x-pash" ], + "windows": "kfy-Deva-x-pash" + }, + { + "full": "kfy-Deva-IN-x-soriyali", + "iana": [ "Kumaoni" ], + "iso639_3": "kfy", + "latnnames": [ "Kumaaũũnii" ], + "localnames": [ "कुमाँऊनी" ], + "name": "Kumaoni", + "names": [ "Kamaoni", "Kumau", "Kumauni", "Kumawani", "Kumgoni", "Kumman", "Kunayaoni" ], + "region": "IN", + "regionname": "India", + "rod": "12473", + "script": "Deva", + "sldr": false, + "tag": "kfy-x-soriyali", + "tags": [ "kfy-Deva-x-soriyali", "kfy-IN-x-soriyali" ], + "windows": "kfy-Deva-x-soriyali" + }, { "full": "kfz-Latn-BF", "iana": [ "Koromfé" ], "iso639_3": "kfz", - "localnames": [ "Koromba" ], + "localnames": [ "Koromfe" ], "name": "Koromfé", - "names": [ "Foula", "Foulse", "Fula", "Fulse", "Koromba", "Kouroumba", "Kurum-Korey", "Kuruma", "Kurumba", "Kurumfe", "Tellem" ], + "names": [ "Foula", "Foulse", "Fula", "Fulse", "Koromba", "Koromfe", "Kouroumba", "Kurum-Korey", "Kuruma", "Kurumba", "Kurumfe", "Tellem" ], "region": "BF", "regionname": "Burkina Faso", "regions": [ "ML" ], @@ -52920,8 +55433,8 @@ "iana": [ "Kongo" ], "iso639_3": "kon", "localnames": [ "Kikongo" ], - "name": "Koongo", - "names": [ "Congo", "Kikongo", "Kikoongo", "Kongo", "Kongo-Nseke" ], + "name": "Kongo", + "names": [ "Congo", "Kikongo", "Kikoongo", "Kongo-Nseke", "Koongo" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "AO", "CG" ], @@ -52980,7 +55493,7 @@ "iana": [ "Kube" ], "iso639_3": "kgf", "name": "Kube", - "names": [ "Hube", "Mongi" ], + "names": [ "Hube", "Kulungtfu-Yuanggeng-Tobo", "Mongi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -53022,8 +55535,10 @@ "full": "kgj-Deva-NP", "iana": [ "Gamale Kham" ], "iso639_3": "kgj", - "name": "Kham, Gamale", - "names": [ "Gamale", "Gamale Kham" ], + "latnnames": [ "gamale kham" ], + "localnames": [ "गामाले खाम" ], + "name": "Kham, Gamal", + "names": [ "Gamale", "Gamale Kham", "Kham", "Kham-Magar", "Khamkura", "Magar Kham" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -53037,7 +55552,7 @@ "iana": [ "Kaiwá" ], "iso639_3": "kgk", "name": "Kaiwá", - "names": [ "Caingua", "Caiua", "Caiwa", "Cayua", "Kaingwá", "Kaiova", "Kaiowá", "Kayova", "Paï-Tavyterã" ], + "names": [ "Caingua", "Caiua", "Caiwa", "Cayua", "Guarani-kaiowá", "Kaingwá", "Kaiova", "Kaiowá", "Kayova", "Paï-Tavyterã" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -53061,27 +55576,12 @@ "unwritten": true, "windows": "kgl-Latn" }, - { - "full": "kgm-Latn-BR", - "iana": [ "Karipúna" ], - "iso639_3": "kgm", - "name": "Karipúna", - "names": [ "Karipuna do Amapá", "Karipuna do Uaçá" ], - "region": "BR", - "regionname": "Brazil", - "script": "Latn", - "sldr": false, - "tag": "kgm", - "tags": [ "kgm-BR", "kgm-Latn" ], - "unwritten": true, - "windows": "kgm-Latn" - }, { "full": "kgn-Zyyy-IR", "iana": [ "Karingani" ], "iso639_3": "kgn", "name": "Karingani", - "names": [ "Keringani" ], + "names": [ "Karingani-Kalasuri-Khoynarudi", "Keringani" ], "region": "IR", "regionname": "Islamic Republic of Iran", "script": "Zyyy", @@ -53109,6 +55609,7 @@ "full": "kgp-Latn-BR", "iana": [ "Kaingang" ], "iso639_3": "kgp", + "localname": "kanhgág", "name": "Kaingang", "names": [ "Bugre", "Caingang", "Caingangue", "Coroado", "Coroados", "Kanhgág" ], "region": "BR", @@ -53169,7 +55670,7 @@ "iana": [ "Somyev" ], "iso639_3": "kgt", "name": "Somyev", - "names": [ "Kila", "Somyewe" ], + "names": [ "Kila", "Somyewe", "Zuzun" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -53183,8 +55684,8 @@ "full": "kgu-Latn-PG", "iana": [ "Kobol" ], "iso639_3": "kgu", - "name": "Kobol", - "names": [ "Koguman" ], + "name": "Kovol", + "names": [ "Kobol", "Koguman", "Kogumar" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -53199,6 +55700,7 @@ "iana": [ "Karas" ], "iso639_3": "kgv", "name": "Karas", + "names": [ "Kalamang" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -53238,20 +55740,20 @@ "windows": "kgx-Latn" }, { - "full": "kgy-Zyyy-CN", + "full": "kgy-Deva-NP", "iana": [ "Kyerung" ], "iso639_3": "kgy", - "name": "Kyerung", - "names": [ "Gyirong", "Kyerong", "Kyirong", "Kyirong kai", "Kyirong-nga", "Kyirung" ], - "region": "CN", - "regionname": "China", - "regions": [ "NP" ], - "script": "Zyyy", + "name": "Kyirong", + "names": [ "Gyirong", "Kyerong", "Kyerung", "Kyirong kai", "Kyirong-nga", "Kyirung" ], + "region": "NP", + "regionname": "Nepal", + "regions": [ "CN" ], + "script": "Deva", "sldr": false, "tag": "kgy", - "tags": [ "kgy-CN", "kgy-Zyyy" ], + "tags": [ "kgy-Deva", "kgy-NP" ], "unwritten": true, - "windows": "kgy-Zyyy" + "windows": "kgy-Deva" }, { "full": "kha-Latn-IN", @@ -53289,11 +55791,11 @@ "full": "khb-Talu-CN", "iana": [ "Lü" ], "iso639_3": "khb", - "latnnames": [ "Kam Tai Lue" ], + "latnnames": [ "Kam Tai Lue", "Dai Lue" ], "localname": "ᦅᧄᦺᦑᦟᦹᧉ", - "localnames": [ "ᦅᧄᦺᦑᦟᦹᧉ" ], + "localnames": [ "ᦅᧄᦺᦑᦟᦹᧉ", "ᦺᦑ ᦟᦹᧉ" ], "name": "Lü", - "names": [ "Dai", "Dai Le", "Duon", "Lu", "Lue", "Ly", "Nhuon", "Pai-I", "Pai-Yi", "Pai’i’", "Shu-Ai-I", "Shui-Pai-I", "Sipsongpanna Dai", "Tai Lu", "Tai Lue", "Thai Lu", "Xishuangbanna Dai" ], + "names": [ "Dai", "Dai Le", "Dai Lue", "Duon", "Lu", "Lue", "Ly", "Nhuon", "Pai-I", "Pai-Yi", "Pai’i’", "Shu-Ai-I", "Shui-Pai-I", "Sipsongpanna Dai", "Tai Lu", "Tai Lue", "Thai Lu", "Xishuangbanna Dai" ], "region": "CN", "regionname": "China", "regions": [ "LA", "MM", "TH", "VN" ], @@ -53308,7 +55810,7 @@ "iana": [ "Lü" ], "iso639_3": "khb", "name": "Lü", - "names": [ "Dai", "Dai Le", "Duon", "Lu", "Lue", "Ly", "Nhuon", "Pai-I", "Pai-Yi", "Pai’i’", "Shu-Ai-I", "Shui-Pai-I", "Sipsongpanna Dai", "Tai Lu", "Tai Lue", "Thai Lu", "Xishuangbanna Dai" ], + "names": [ "Dai", "Dai Le", "Dai Lue", "Duon", "Lu", "Lue", "Ly", "Nhuon", "Pai-I", "Pai-Yi", "Pai’i’", "Shu-Ai-I", "Shui-Pai-I", "Sipsongpanna Dai", "Tai Lu", "Tai Lue", "Thai Lu", "Xishuangbanna Dai" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -53353,8 +55855,9 @@ "full": "khe-Latn-ID", "iana": [ "Korowai" ], "iso639_3": "khe", + "localnames": [ "Kolufo Aup" ], "name": "Korowai", - "names": [ "Kolufaup" ], + "names": [ "Kolufaup", "Kolufo Aup" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -53453,18 +55956,32 @@ "windows": "khn-Deva" }, { - "full": "kho-Zyyy-IR", + "full": "kho-Brah-IR", "iana": [ "Khotanese", "Sakan" ], "iso639_3": "kho", "name": "Khotanese", "names": [ "Sakan" ], "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Brah", "sldr": false, "tag": "kho", - "tags": [ "kho-IR", "kho-Zyyy" ], - "windows": "kho-Zyyy" + "tags": [ "kho-Brah", "kho-IR" ], + "windows": "kho-Brah" + }, + { + "full": "kho-Khar-IR", + "iana": [ "Khotanese", "Sakan" ], + "iso639_3": "kho", + "name": "Khotanese", + "names": [ "Sakan" ], + "nophonvars": true, + "region": "IR", + "regionname": "Islamic Republic of Iran", + "script": "Khar", + "sldr": false, + "tag": "kho-Khar", + "windows": "kho-Khar" }, { "full": "khp-Latn-ID", @@ -53531,6 +56048,8 @@ "full": "khs-Latn-PG", "iana": [ "Kasua" ], "iso639_3": "khs", + "localname": "Kasua", + "localnames": [ "Kasua" ], "name": "Kasua", "region": "PG", "regionname": "Papua New Guinea", @@ -53562,8 +56081,8 @@ "iana": [ "Nkhumbi" ], "iso639_3": "khu", "localnames": [ "Ocinkumbi" ], - "name": "Muhumbi", - "names": [ "Khumbi", "Ngumbi", "Nkhumbi", "Nkumbi", "Ocinkumbi", "Olungkhumbi", "Otjingumbi" ], + "name": "Nkumbi", + "names": [ "Khumbi", "Muhumbi", "Ngumbi", "Nkhumbi", "Ocinkumbi", "Olungkhumbi", "Otjingumbi" ], "region": "AO", "regionname": "Angola", "script": "Latn", @@ -53577,7 +56096,7 @@ "iana": [ "Khvarshi" ], "iso639_3": "khv", "name": "Khvarshi", - "names": [ "Atl’ilqo", "Khvarsh", "Khvarshin", "Xvarshi", "Xvarshik" ], + "names": [ "Atl’ilqo", "Khvarsh", "Khvarshin", "Khwarshi-Inkhoqwari", "Xvarshi", "Xvarshik" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -53598,7 +56117,7 @@ "names": [ "Arniya", "Chitrali", "Chitrari", "Citrali", "Kashkari", "Khawar", "Patu", "Qashqari" ], "region": "PK", "regionname": "Pakistan", - "regions": [ "IN" ], + "regions": [ "AF", "IN" ], "script": "Arab", "sldr": true, "tag": "khw", @@ -53655,7 +56174,7 @@ "localname": "Gikuyu", "localnames": [ "Gĩgĩkũyũ" ], "name": "Gikuyu", - "names": [ "Gekoyo", "Gĩgĩkũyũ", "Kikuyu" ], + "names": [ "Gekoyo", "Gĩgĩkũyũ", "Gĩkũyũ", "Kikuyu" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -53679,6 +56198,54 @@ "tags": [ "kia-Latn", "kia-TD" ], "windows": "kia-Latn" }, + { + "full": "kia-Latn-TD-x-gerep", + "iana": [ "Kim" ], + "iso639_3": "kia", + "localnames": [ "Garab" ], + "name": "Kim", + "names": [ "Garab", "Masa" ], + "region": "TD", + "regionname": "Chad", + "rod": "1641", + "script": "Latn", + "sldr": false, + "tag": "kia-x-gerep", + "tags": [ "kia-Latn-x-gerep", "kia-TD-x-gerep" ], + "windows": "kia-Latn-x-gerep" + }, + { + "full": "kia-Latn-TD-x-kolop", + "iana": [ "Kim" ], + "iso639_3": "kia", + "localnames": [ "Garab" ], + "name": "Kim", + "names": [ "Garab", "Masa" ], + "region": "TD", + "regionname": "Chad", + "rod": "12002", + "script": "Latn", + "sldr": false, + "tag": "kia-x-kolop", + "tags": [ "kia-Latn-x-kolop", "kia-TD-x-kolop" ], + "windows": "kia-Latn-x-kolop" + }, + { + "full": "kia-Latn-TD-x-kosop", + "iana": [ "Kim" ], + "iso639_3": "kia", + "localnames": [ "Garab" ], + "name": "Kim", + "names": [ "Garab", "Masa" ], + "region": "TD", + "regionname": "Chad", + "rod": "12003", + "script": "Latn", + "sldr": false, + "tag": "kia-x-kosop", + "tags": [ "kia-Latn-x-kosop", "kia-TD-x-kosop" ], + "windows": "kia-Latn-x-kosop" + }, { "full": "kib-Latn-SD", "iana": [ "Koalib" ], @@ -53741,19 +56308,19 @@ "windows": "kie-Latn" }, { - "full": "kif-Zyyy-NP", + "full": "kif-Deva-NP", "iana": [ "Eastern Parbate Kham" ], "iso639_3": "kif", "name": "Kham, Eastern Parbate", - "names": [ "Eastern Parbate", "Eastern Parbate Kham", "Nisel", "Nishel Kham", "Nisi", "Nisi Kham" ], + "names": [ "Eastern Parbate", "Eastern Parbate Kham", "Kham", "Kham-Magar", "Khamkura", "Magar Kham", "Nisel", "Nishel Kham", "Nisi", "Nisi Kham" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "kif", - "tags": [ "kif-NP", "kif-Zyyy" ], + "tags": [ "kif-Deva", "kif-NP" ], "unwritten": true, - "windows": "kif-Zyyy" + "windows": "kif-Deva" }, { "full": "kig-Latn-ID", @@ -53818,7 +56385,7 @@ "iana": [ "Kariya" ], "iso639_3": "kil", "name": "Kariya", - "names": [ "Kariyu", "Kauyawa", "Lipkawa", "Vinahe", "Wihe" ], + "names": [ "Kariyu", "Kauyawa", "Keriya", "Lipkawa", "Vinahә", "Wihә" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -53835,7 +56402,7 @@ "latnnames": [ "tofa dyl" ], "localnames": [ "тоъфа дыл" ], "name": "Karagas", - "names": [ "Kamas", "Karagass", "Sayan Samoyed", "Tofa", "Tofalar" ], + "names": [ "Kamas", "Karagass", "Karagassisch", "Karagasy", "Khotowci", "Taiga Sayan Turkic", "Tofa", "Tofalar" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -53863,8 +56430,10 @@ "full": "kip-Deva-NP", "iana": [ "Sheshi Kham" ], "iso639_3": "kip", + "latnnames": [ "Sheshi Paang" ], + "localnames": [ "शेषी पाङ" ], "name": "Kham, Sheshi", - "names": [ "Sheshi", "Sheshi Kham" ], + "names": [ "Kham", "Kham-Magar", "Khamkura", "Magar Kham", "Sheshi Kham" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -53907,7 +56476,7 @@ "iana": [ "Agob" ], "iso639_3": "kit", "name": "Agob", - "names": [ "Dabu" ], + "names": [ "Agob-Ende-Kawam", "Dabu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -53920,9 +56489,10 @@ "full": "kiu-Latn-TR", "iana": [ "Kirmanjki (individual language)" ], "iso639_3": "kiu", - "localnames": [ "Shar Ma" ], + "localnames": [ "Kırmancki", "So-Bê", "Zonê Ma" ], + "macrolang": "zza", "name": "Zazaki, Northern", - "names": [ "Alevica", "Dersimki", "Dimilki", "Kirmanjki", "Kirmanjki (individual language)", "Northern Zaza", "Shar Ma", "So-Bê", "Zaza", "Zonê Ma" ], + "names": [ "Alevica", "Alevija", "Dersimki", "Dersımki", "Dimilki", "Dımılki", "Kirmanjki", "Kirmanjki (individual language)", "Kırmancki", "Northern Zaza", "Shar Ma", "So-Bê", "Zaza", "Zazaca", "Zazaja", "Zazaki", "Zonê Ma" ], "region": "TR", "regionname": "Turkey", "regions": [ "AT", "CH", "DE", "NL" ], @@ -53961,6 +56531,51 @@ "tags": [ "kiw-Latn", "kiw-PG" ], "windows": "kiw-Latn" }, + { + "full": "kiw-Latn-PG-x-gibaio", + "iana": [ "Kiwai, Northeast" ], + "iso639_3": "kiw", + "name": "Kiwai, Northeast", + "names": [ "Urama-Kope" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "5023", + "script": "Latn", + "sldr": false, + "tag": "kiw-x-gibaio", + "tags": [ "kiw-Latn-x-gibaio", "kiw-PG-x-gibaio" ], + "windows": "kiw-Latn-x-gibaio" + }, + { + "full": "kiw-Latn-PG-x-kope", + "iana": [ "Kiwai, Northeast" ], + "iso639_3": "kiw", + "name": "Kiwai, Northeast", + "names": [ "Urama-Kope" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "1222", + "script": "Latn", + "sldr": false, + "tag": "kiw-x-kope", + "tags": [ "kiw-Latn-x-kope", "kiw-PG-x-kope" ], + "windows": "kiw-Latn-x-kope" + }, + { + "full": "kiw-Latn-PG-x-urama", + "iana": [ "Kiwai, Northeast" ], + "iso639_3": "kiw", + "name": "Kiwai, Northeast", + "names": [ "Urama-Kope" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "5025", + "script": "Latn", + "sldr": false, + "tag": "kiw-x-urama", + "tags": [ "kiw-Latn-x-urama", "kiw-PG-x-urama" ], + "windows": "kiw-Latn-x-urama" + }, { "full": "kix-Latn-IN", "iana": [ "Khiamniungan Naga" ], @@ -53996,8 +56611,9 @@ "full": "kiz-Latn-TZ", "iana": [ "Kisi" ], "iso639_3": "kiz", + "localnames": [ "Kikɨsɨ" ], "name": "Kisi", - "names": [ "Kese", "Kikisi" ], + "names": [ "Kese", "Kikisi", "Kikɨsɨ" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -54012,7 +56628,7 @@ "iso639_3": "kua", "localnames": [ "Oshikwanyama", "Oshiwambo" ], "name": "Kwanyama", - "names": [ "Cuanhama", "Humba", "Kuanjama", "Kuanyama", "Kwancama", "Kwanjama", "Ochikwanyama", "Oshikuanjama", "Oshikwanyama", "Oshiwambo", "Otjiwambo", "Ovambo", "Owambo", "Oxikuanyama", "Wambo" ], + "names": [ "Cuanhama", "Humba", "Kuanjama", "Kuanyama", "Kwancama", "Kwanjama", "Ochikwanyama", "Oshikuanjama", "Oshikwanyama", "Oshiwambo", "Osikuanjama", "Otjiwambo", "Ovambo", "Owambo", "Oxikuanyama", "Wambo" ], "region": "NA", "regionname": "Namibia", "regions": [ "AO" ], @@ -54020,14 +56636,14 @@ "sldr": false, "tag": "kj", "tags": [ "kj-Latn", "kj-NA" ], - "windows": "kj-Latn" + "windows": "kj-Latn-NA" }, { "full": "kj-Brai-AO", "iana": [ "Kuanyama", "Kwanyama" ], "iso639_3": "kua", "name": "Oshiwambo", - "names": [ "Cuanhama", "Humba", "Kuanjama", "Kuanyama", "Kwancama", "Kwanjama", "Kwanyama", "Ochikwanyama", "Oshikuanjama", "Oshikwanyama", "Otjiwambo", "Ovambo", "Owambo", "Oxikuanyama", "Wambo" ], + "names": [ "Cuanhama", "Humba", "Kuanjama", "Kuanyama", "Kwancama", "Kwanjama", "Kwanyama", "Ochikwanyama", "Oshikuanjama", "Oshikwanyama", "Osikuanjama", "Otjiwambo", "Ovambo", "Owambo", "Oxikuanyama", "Wambo" ], "nophonvars": true, "region": "AO", "regionname": "Angola", @@ -54036,7 +56652,7 @@ "sldr": false, "tag": "kj-Brai", "tags": [ "kj-AO" ], - "windows": "kj-Brai" + "windows": "kj-Brai-AO" }, { "full": "kja-Latn-ID", @@ -54115,9 +56731,10 @@ }, { "full": "kjf-Zyyy-AZ", - "iana": [ "Khalaj" ], + "iana": [ "Khalaj [Indo-Iranian]" + ], "iso639_3": "kjf", - "name": "Khalaj", + "name": "Khalaj [Indo-Iranian]", "region": "AZ", "regionname": "Azerbaijan", "regions": [ "IR" ], @@ -54142,7 +56759,7 @@ "sldr": false, "tag": "kjg", "tags": [ "kjg-LA", "kjg-Laoo" ], - "windows": "kjg-Laoo" + "windows": "kjg-Laoo-LA" }, { "full": "kjg-Latn-LA", @@ -54156,7 +56773,7 @@ "script": "Latn", "sldr": false, "tag": "kjg-Latn", - "windows": "kjg-Latn" + "windows": "kjg-Latn-LA" }, { "full": "kjg-Thai-TH", @@ -54172,7 +56789,7 @@ "sldr": false, "tag": "kjg-Thai", "tags": [ "kjg-TH" ], - "windows": "kjg-Thai" + "windows": "kjg-Thai-TH" }, { "full": "kjh-Cyrl-RU", @@ -54188,7 +56805,7 @@ "script": "Cyrl", "sldr": false, "tag": "kjh", - "tags": [ "kjh-Cyrl", "kjh-RU" ], + "tags": [ "kjh-Cyrl", "kjh-RU", "zkb", "zkb-Cyrl", "zkb-Cyrl-RU", "zkb-RU" ], "windows": "kjh-Cyrl" }, { @@ -54196,7 +56813,7 @@ "iana": [ "Zabana" ], "iso639_3": "kji", "name": "Zabana", - "names": [ "Kia" ], + "names": [ "Jabana", "Kia" ], "region": "SB", "regionname": "Solomon Islands", "script": "Latn", @@ -54209,15 +56826,15 @@ "full": "kjj-Latn-AZ", "iana": [ "Khinalugh" ], "iso639_3": "kjj", + "localnames": [ "Kətşi Miż" ], "name": "Khinalugh", - "names": [ "Khinalug", "Khinalugi", "Xinalug" ], + "names": [ "Khinalug", "Khinalugi", "Kətşi Miż", "Xinaliq", "Xinalug", "Xınalıq dili" ], "region": "AZ", "regionname": "Azerbaijan", "script": "Latn", "sldr": false, "tag": "kjj", "tags": [ "kjj-AZ", "kjj-Latn" ], - "unwritten": true, "windows": "kjj-Latn" }, { @@ -54239,12 +56856,14 @@ "full": "kjl-Deva-NP", "iana": [ "Western Parbate Kham" ], "iso639_3": "kjl", + "latnnames": [ "kham" ], + "localnames": [ "खाम" ], "name": "Kham, Western Parbate", - "names": [ "Kham", "Kham-Magar", "Takale", "Takale Kham", "Western Parbate", "Western Parbate Kham" ], + "names": [ "Kham", "Kham-Magar", "Khamkura", "Magar Kham", "Takale", "Takale Kham", "Western Parbate", "Western Parbate Kham" ], "region": "NP", "regionname": "Nepal", "script": "Deva", - "sldr": false, + "sldr": true, "tag": "kjl", "tags": [ "kjl-Deva", "kjl-NP" ], "windows": "kjl-Deva" @@ -54283,8 +56902,10 @@ "full": "kjo-Deva-IN", "iana": [ "Harijan Kinnauri" ], "iso639_3": "kjo", + "latnnames": [ "Kinauri Phaadei" ], + "localnames": [ "किनौरी पहाड़ी" ], "name": "Kinnauri, Pahari", - "names": [ "Boli", "Chamang Boli", "Lower Kinnauri", "Orasi", "Ores", "Sonar Boli" ], + "names": [ "Boli", "Chamang Boli", "Himachali", "Kinnauri Pahari", "Lower Kinnauri", "Oras Boli", "Orasi", "Ores", "Pahari", "Sonar Boli" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -54297,6 +56918,8 @@ "full": "kjp-Mymr-MM", "iana": [ "Pwo Eastern Karen" ], "iso639_3": "kjp", + "latnnames": [ "Phlou" ], + "localnames": [ "ဖၠုံ" ], "name": "Karen, Pwo Eastern", "names": [ "Karen, Pwo West-Central Thailand", "Moulmein Pwo Karen", "Phlong", "Phlou", "Pho", "Pwo Eastern Karen", "Southern Pwo Karen" ], "region": "MM", @@ -54306,7 +56929,7 @@ "sldr": false, "tag": "kjp", "tags": [ "kjp-MM", "kjp-Mymr" ], - "windows": "kjp-Mymr" + "windows": "kjp-Mymr-MM" }, { "full": "kjp-Leke-MM", @@ -54321,12 +56944,14 @@ "script": "Leke", "sldr": false, "tag": "kjp-Leke", - "windows": "kjp-Leke" + "windows": "kjp-Leke-MM" }, { "full": "kjp-Mymr-MM-x-chekhi", "iana": [ "Karen, Pwo Eastern" ], "iso639_3": "kjp", + "latnnames": [ "Phlou" ], + "localnames": [ "ဖၠုံ" ], "name": "Karen, Pwo Eastern", "names": [ "Karen, Pwo West-Central Thailand", "Moulmein Pwo Karen", "Phlong", "Phlou", "Pho", "Southern Pwo Karen" ], "nophonvars": true, @@ -54337,12 +56962,14 @@ "sldr": false, "tag": "kjp-Mymr-x-chekhi", "tags": [ "kjp-MM-x-chekhi" ], - "windows": "kjp-Mymr-x-chekhi" + "windows": "kjp-Mymr-MM-x-chekhi" }, { "full": "kjp-Mymr-MM-x-phlouyu", "iana": [ "Karen, Pwo Eastern" ], "iso639_3": "kjp", + "latnnames": [ "Phlou" ], + "localnames": [ "ဖၠုံ" ], "name": "Karen, Pwo Eastern", "names": [ "Karen, Pwo West-Central Thailand", "Moulmein Pwo Karen", "Phlong", "Phlou", "Pho", "Southern Pwo Karen" ], "nophonvars": true, @@ -54353,12 +56980,14 @@ "sldr": false, "tag": "kjp-Mymr-x-phlouyu", "tags": [ "kjp-MM-x-phlouyu" ], - "windows": "kjp-Mymr-x-phlouyu" + "windows": "kjp-Mymr-MM-x-phlouyu" }, { "full": "kjp-Mymr-MM-x-thiyon", "iana": [ "Karen, Pwo Eastern" ], "iso639_3": "kjp", + "latnnames": [ "Phlou" ], + "localnames": [ "ဖၠုံ" ], "name": "Karen, Pwo Eastern", "names": [ "Karen, Pwo West-Central Thailand", "Moulmein Pwo Karen", "Phlong", "Phlou", "Pho", "Southern Pwo Karen" ], "nophonvars": true, @@ -54369,7 +56998,7 @@ "sldr": false, "tag": "kjp-Mymr-x-thiyon", "tags": [ "kjp-MM-x-thiyon" ], - "windows": "kjp-Mymr-x-thiyon" + "windows": "kjp-Mymr-MM-x-thiyon" }, { "full": "kjp-Thai-TH", @@ -54385,7 +57014,7 @@ "sldr": false, "tag": "kjp-Thai", "tags": [ "kjp-TH" ], - "windows": "kjp-Thai" + "windows": "kjp-Thai-TH" }, { "full": "kjq-Latn-US", @@ -54421,8 +57050,8 @@ "iso639_3": "kjs", "localname": "adaa agale", "localnames": [ "adaa agale" ], - "name": "Kewa, East", - "names": [ "East Kewa", "adaa agale" ], + "name": "Kewapi, East", + "names": [ "East Kewa", "Kewa", "Kewapi", "adaa agale" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -54491,8 +57120,8 @@ "full": "kjy-Latn-PG", "iana": [ "Erave" ], "iso639_3": "kjy", - "name": "Erave", - "names": [ "Kewa South", "Pole", "South Kewa" ], + "name": "Pole", + "names": [ "Erave", "Kewa South", "South Kewa", "South Kewapi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -54502,18 +57131,17 @@ "windows": "kjy-Latn" }, { - "full": "kjz-Tibt-BT-x-uchen", + "full": "kjz-Tibt-BT", "iana": [ "Bumthangkha" ], "iso639_3": "kjz", "name": "Bumthangkha", "names": [ "Bhumtam", "Bumtang", "Bumtangkha", "Bumtanp", "Bumthang", "Bumthapkha", "Kebumtamp" ], - "nophonvars": true, "region": "BT", "regionname": "Bhutan", "script": "Tibt", "sldr": false, - "tag": "kjz-Tibt", - "tags": [ "kjz-BT-x-uchen", "kjz-Tibt-BT", "kjz-Tibt-x-uchen" ], + "tag": "kjz", + "tags": [ "kjz-BT", "kjz-Tibt" ], "windows": "kjz-Tibt" }, { @@ -54533,7 +57161,7 @@ "suppress": true, "tag": "kk", "tags": [ "kk-Cyrl", "kk-KZ" ], - "windows": "kk" + "windows": "kk-KZ" }, { "full": "kk-Arab-AF", @@ -54560,7 +57188,7 @@ "script": "Brai", "sldr": false, "tag": "kk-Brai", - "windows": "kk-Brai" + "windows": "kk-Brai-KZ" }, { "full": "kk-Arab-CN", @@ -54605,7 +57233,7 @@ "tag": "kk-Latn", "tags": [ "kk-TR" ], "variants": [ "baku1926" ], - "windows": "kk-Latn" + "windows": "kk-Latn-TR" }, { "full": "kk-Arab-MN", @@ -54671,7 +57299,7 @@ "iana": [ "Kinuku" ], "iso639_3": "kkd", "name": "Kinuku", - "names": [ "Kinugu", "Kinuka" ], + "names": [ "Anu", "Kinugu", "Kinuka", "Nu", "Tinu" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -54711,19 +57339,19 @@ "windows": "kke-Arab" }, { - "full": "kkf-Zyyy-IN", + "full": "kkf-Tibt-IN", "iana": [ "Kalaktang Monpa" ], "iso639_3": "kkf", "name": "Monpa, Kalaktang", "names": [ "Kalaktang Monpa", "Sharpa-lo", "Southern Monpa", "Tsangla Monpa" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "kkf", - "tags": [ "kkf-IN", "kkf-Zyyy" ], + "tags": [ "kkf-IN", "kkf-Tibt" ], "unwritten": true, - "windows": "kkf-Zyyy" + "windows": "kkf-Tibt" }, { "full": "kkg-Latn-PH", @@ -54745,7 +57373,7 @@ "iana": [ "Khün" ], "iso639_3": "kkh", "name": "Khün", - "names": [ "Gon Shan", "Hkun", "Khuen", "Khun Shan", "Khyn", "Tai Khun", "Tai-Khuen" ], + "names": [ "Gon Shan", "Hkun", "Kengtung Dai", "Khuen", "Khun Shan", "Khyn", "Tai Khün", "Tai-Khuen" ], "region": "MM", "regionname": "Myanmar", "regions": [ "LA", "TH" ], @@ -54785,13 +57413,13 @@ "sldr": true, "tag": "kkj", "tags": [ "kkj-CM", "kkj-Latn" ], - "windows": "kkj-Latn" + "windows": "kkj-Latn-CM" }, { "full": "kkj-Latn-TD", "iana": [ "Kako" ], "iso639_3": "kkj", - "localname": "kakɔ", + "localname": "Kakɔ", "localnames": [ "Kakɔ" ], "name": "Kako", "region": "TD", @@ -54820,8 +57448,9 @@ "full": "kkl-Latn-ID", "iana": [ "Kosarek Yale" ], "iso639_3": "kkl", + "localnames": [ "Yale yupu" ], "name": "Yale, Kosarek", - "names": [ "In-lom", "Kosarek", "Kosarek Yale", "Mek Kosarek", "Wanam", "Yale", "Yale yubu", "Yale-Kosarek", "Yali", "Yali Kosarek" ], + "names": [ "In-lom", "Kosarek", "Kosarek Yale", "Mek Kosarek", "Wanam", "Yale", "Yale yubu", "Yale yupu", "Yale-Kosarek", "Yali", "Yali Kosarek" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -54835,7 +57464,7 @@ "iana": [ "Kiong" ], "iso639_3": "kkm", "name": "Kiong", - "names": [ "Akayon", "Akoiyang", "Iyoniyong", "Okonyong", "Okoyong", "Äkäyön~" ], + "names": [ "Akayon", "Akoiyang", "Iyoniyong", "Okonyong", "Okoyong", "Äkäyöñ", "Ökönyöñ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -54892,7 +57521,7 @@ }, { "full": "kkq-Latn-CD", - "iana": [ "Kaiku" ], + "iana": [ "Kaeku" ], "iso639_3": "kkq", "name": "Kaiku", "names": [ "Ekaeku", "Ikaiku", "Kaeku", "Kaiko" ], @@ -54925,7 +57554,7 @@ "iana": [ "Giiwo" ], "iso639_3": "kks", "name": "Giiwo", - "names": [ "Bu Giiwo", "Kirfi", "Kirifawa", "Kirifi", "Ma Giiwo" ], + "names": [ "Bu Giiwo", "Kirfanci", "Kirfawa", "Kirfi", "Kirifi", "Ma Giiwo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -54939,8 +57568,8 @@ "full": "kkt-Deva-NP", "iana": [ "Koi" ], "iso639_3": "kkt", - "name": "Koi", - "names": [ "Kohi", "Koi Ba’a", "Koyee", "Koyi", "Koyu" ], + "name": "Koyee", + "names": [ "Kohi", "Koi", "Koi Ba’a", "Koyi", "Koyi Rai", "Koyu" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -54969,6 +57598,7 @@ "iana": [ "Kangean" ], "iso639_3": "kkv", "name": "Kangean", + "names": [ "Bĕsa Kangéan", "Kangeanese" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -54982,7 +57612,7 @@ "iana": [ "Teke-Kukuya" ], "iso639_3": "kkw", "name": "Teke-Kukuya", - "names": [ "Chikuya", "Cikuya", "Kikuwa", "Kikuya", "Koukouya", "Kukua", "Kukuya", "Kukwa", "Southern Teke" ], + "names": [ "Chikuya", "Cikuya", "Kikukuya", "Kikuwa", "Kikuya", "Koukouya", "Kukua", "Kukuya", "Kukwa", "Kukẅa", "Southern Teke" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -55055,19 +57685,19 @@ "windows": "kl" }, { - "full": "kla-Zyyy-US", + "full": "kla-Latn-US", "iana": [ "Klamath-Modoc" ], "iso639_3": "kla", "name": "Klamath-Modoc", - "names": [ "Klamath" ], + "names": [ "E-ukshikni", "Klamath", "Lutuami", "Lutuamí-modoco", "Modoc" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "kla", - "tags": [ "kla-US", "kla-Zyyy" ], + "tags": [ "kla-Latn", "kla-US" ], "unwritten": true, - "windows": "kla-Zyyy" + "windows": "kla-Latn" }, { "full": "klb-Latn-MX", @@ -55103,7 +57733,7 @@ "iana": [ "Gamilaraay" ], "iso639_3": "kld", "name": "Gamilaraay", - "names": [ "Camileroi", "Gamilaroi", "Kamilaroi" ], + "names": [ "Cam ell eri", "Camel Duahi", "Camileroi", "Camleroy", "Cammealroy", "Comleroy", "Cum milroy", "Cumilri", "Cummeroy", "Euahlayi", "Euhahlayi", "Gamilaroi", "Gamilray", "Gamilroi", "Ghummilarai", "Gomeroi", "Gumilori", "Gumilray", "Gumilroi", "Guminilroi", "Gummilray", "Gummilroi", "Gunilroi", "Gunnilaroi", "Guyinbaray", "Juwalarai", "Kaameelarrai", "Kahmilaharoy", "Kahmilari", "Kahml Duhai", "Kakmilari", "Kamalarai", "Kamil", "Kamilaori", "Kamilarai", "Kamilari", "Kamilaroi", "Kamilary", "Kamileroi", "Kamilrai", "Kamilroi", "Kamu", "Kamularoi", "Karmil", "Kimilari", "Komleroy", "Koomilroi", "Northern Gamilaraay", "Peel River language", "Tjake", "Tyake", "Ualarai", "Walarai", "Weraerai", "Wirajarai", "Yauan" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -55118,7 +57748,7 @@ "iana": [ "Kulung (Nepal)" ], "iso639_3": "kle", "name": "Kulung", - "names": [ "Kholung", "Khulung", "Khulunge Rai", "Kulu Ring", "Kulung (Nepal)", "Kulunge" ], + "names": [ "Jindā", "Kholung", "Khulung", "Khulunge Rai", "Kulu Ring", "Kulung (Nepal)", "Kulunge", "Kulungo" ], "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], @@ -55133,7 +57763,7 @@ "iana": [ "Kendeje" ], "iso639_3": "klf", "name": "Kendeje", - "names": [ "Yaali" ], + "names": [ "Kedendje", "Kendeye", "Kindeje", "Yaali" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -55189,25 +57819,25 @@ "windows": "kli-Latn" }, { - "full": "klj-Zyyy-IR", - "iana": [ "Turkic Khalaj" ], + "full": "klj-Arab-IR", + "iana": [ "Khalaj" ], "iso639_3": "klj", "name": "Khalaj", - "names": [ "Chaladsch", "Turkic Khalaj" ], + "names": [ "Arghu", "Chaladsch", "Jalái", "Turkic Khalaj" ], "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "klj", - "tags": [ "klj-IR", "klj-Zyyy" ], - "windows": "klj-Zyyy" + "tags": [ "klj-Arab", "klj-IR" ], + "windows": "klj-Arab" }, { "full": "klk-Latn-NG", "iana": [ "Kono (Nigeria)" ], "iso639_3": "klk", "name": "Kono", - "names": [ "Kono (Nigeria)", "Konu", "Kwono" ], + "names": [ "Kono (Nigeria)", "Konu", "Kowono", "Kwono" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -55250,10 +57880,10 @@ "full": "kln-Latn-KE", "iana": [ "Kalenjin" ], "iso639_3": "kln", - "localname": "Kalenjin", + "localname": "Sābāwōōt", "localnames": [ "Sābāwōōt" ], - "name": "Sabaot", - "names": [ "Kalenjin", "Sebei", "Sābāwōōt" ], + "name": "Kalenjin", + "names": [ "Sabaot", "Sebei", "Sābāwōōt" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -55310,8 +57940,10 @@ "full": "klr-Deva-NP", "iana": [ "Khaling" ], "iso639_3": "klr", + "latnnames": [ "Khaael braa" ], + "localnames": [ "खा़ल ब्रा" ], "name": "Khaling", - "names": [ "Khael Baat", "Khael Bra", "Khael Braa", "Khaling Kura", "Khalinge Rai" ], + "names": [ "Kaling", "Khael Baat", "Khael Bra", "Khael Braa", "Khaling Kura", "Khalinge Rai" ], "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], @@ -55327,7 +57959,7 @@ "iso639_3": "kls", "localnames": [ "Kal'as'amon" ], "name": "Kalasha", - "names": [ "Kalash", "Kalashamon", "Kalashamond", "Kalashamondr", "Kalashi", "Kal’as’amon" ], + "names": [ "Calasa", "Dardu", "Kalasa", "Kalash", "Kalashamon", "Kalashamond", "Kalashamondr", "Kalashi", "Kal’as’amon" ], "region": "PK", "regionname": "Pakistan", "script": "Latn", @@ -55341,7 +57973,7 @@ "iana": [ "Kalasha" ], "iso639_3": "kls", "name": "Kalasha", - "names": [ "Kalash", "Kalashamon", "Kalashamond", "Kalashamondr", "Kalashi", "Kal’as’amon" ], + "names": [ "Calasa", "Dardu", "Kalasa", "Kalash", "Kalashamon", "Kalashamond", "Kalashamondr", "Kalashi", "Kal’as’amon" ], "nophonvars": true, "region": "PK", "regionname": "Pakistan", @@ -55388,7 +58020,7 @@ "localname": "nasorun ta Uluveu", "localnames": [ "nasorun ta Uluveu" ], "name": "Maskelynes", - "names": [ "Kuliviu", "Maskelyne Islands", "Uliveo", "Uluveu", "nasorun ta Uluveu" ], + "names": [ "Kuliviu", "Mallicolo", "Maskelyne Islands", "Uliveo", "Uluveu", "nasorun ta Uluveu" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -55403,7 +58035,7 @@ "iso639_3": "klw", "localnames": [ "Tado" ], "name": "Tado", - "names": [ "Lindoe", "Lindu", "Linduan" ], + "names": [ "Lindoe", "Lindu", "Linduan", "Tado-Lindu" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -55416,6 +58048,8 @@ "full": "klx-Latn-PG", "iana": [ "Koluwawa" ], "iso639_3": "klx", + "localname": "Koluwawa", + "localnames": [ "Koluwawa" ], "name": "Koluwawa", "names": [ "Kalokalo" ], "region": "PG", @@ -55496,21 +58130,23 @@ "iso639_3": "kmb", "localnames": [ "Kimbundu" ], "name": "Kimbundu", - "names": [ "Kimbundo", "Loanda Mbundu", "Loande", "Luanda", "Lunda", "Mbundu", "Nbundu", "North Mbundu", "N’bundo" ], + "names": [ "Kimbundo", "Loanda Mbundu", "Loande", "Luanda", "Lunda", "Mbundu", "Nbundu", "North Mbundu", "N’bundo", "Tonga" ], "region": "AO", "regionname": "Angola", + "regions": [ "ST" ], "script": "Latn", "sldr": false, "tag": "kmb", - "tags": [ "kmb-AO", "kmb-Latn" ], + "tags": [ "kmb-AO", "kmb-Latn", "smd", "smd-AO", "smd-Latn", "smd-Latn-AO" ], "windows": "kmb-Latn" }, { "full": "kmc-Latn-CN", "iana": [ "Southern Dong" ], "iso639_3": "kmc", + "localnames": [ "Lix Gaeml" ], "name": "Dong, Southern", - "names": [ "Gam", "Kam", "Southern Dong", "Tong", "Tung", "Tung-Chia" ], + "names": [ "Gam", "Kam", "Leec Gaeml", "Lix Gaeml", "Southern Dong", "Tong", "Tung", "Tung-Chia" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -55524,7 +58160,7 @@ "iana": [ "Southern Dong" ], "iso639_3": "kmc", "name": "Dong, Southern", - "names": [ "Gam", "Kam", "Southern Dong", "Tong", "Tung", "Tung-Chia" ], + "names": [ "Gam", "Kam", "Leec Gaeml", "Lix Gaeml", "Southern Dong", "Tong", "Tung", "Tung-Chia" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -55623,25 +58259,25 @@ "windows": "kmi-Latn" }, { - "full": "kmj-Zyyy-IN", + "full": "kmj-Deva-IN", "iana": [ "Kumarbhag Paharia" ], "iso639_3": "kmj", "name": "Kumarbhag Paharia", "names": [ "Kumar", "Mad", "Mal", "Maler", "Malti", "Malto", "Maltu", "Paharia", "Pahariya" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "kmj", - "tags": [ "kmj-IN", "kmj-Zyyy" ], + "tags": [ "kmj-Deva", "kmj-IN" ], "unwritten": true, - "windows": "kmj-Zyyy" + "windows": "kmj-Deva" }, { "full": "kmk-Latn-PH", "iana": [ "Limos Kalinga" ], "iso639_3": "kmk", - "localname": "Kalinga, Limos", + "localname": "Linimos", "localnames": [ "Linimos" ], "name": "Kalinga, Limos", "names": [ "Limos Kalinga", "Limos-Liwan Kalinga", "Linimos", "Northern Kalinga" ], @@ -55701,8 +58337,9 @@ "full": "kmo-Latn-PG", "iana": [ "Kwoma" ], "iso639_3": "kmo", + "localnames": [ "Washkuk" ], "name": "Kwoma", - "names": [ "Washkuk" ], + "names": [ "Kuome", "Washkuk" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -55744,6 +58381,7 @@ "full": "kms-Latn-PG", "iana": [ "Kamasau" ], "iso639_3": "kms", + "localnames": [ "Wand Tuan" ], "name": "Kamasau", "names": [ "Wand Tuan" ], "region": "PG", @@ -55787,8 +58425,9 @@ "full": "kmv-Latn-BR", "iana": [ "Karipúna Creole French" ], "iso639_3": "kmv", - "name": "Karipuna Creole French", - "names": [ "Crioulo", "Karipúna Creole French" ], + "localnames": [ "Lanc-Patuá" ], + "name": "Karipuna French Creole", + "names": [ "Amapá French Creole", "Crioulo", "Crioulo karipúna", "Karipúna Creole French", "Lanc-Patuá", "Uaçá Creole French" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -55801,6 +58440,7 @@ "full": "kmw-Latn-CD", "iana": [ "Komo (Democratic Republic of Congo)" ], "iso639_3": "kmw", + "localnames": [ "Kikomo" ], "name": "Komo", "names": [ "Kikomo", "Kikumo", "Kikumu", "Kikuumu", "Komo (Democratic Republic of Congo)", "Kumo", "Kumu", "Kuumu" ], "region": "CD", @@ -55910,7 +58550,7 @@ "iana": [ "Dera (Nigeria)" ], "iso639_3": "kna", "name": "Dera", - "names": [ "Dera (Nigeria)", "Kanakuru" ], + "names": [ "Bo Dera", "Dera (Nigeria)", "Kanakuru" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -55924,9 +58564,9 @@ "iana": [ "Lubuagan Kalinga" ], "iso639_3": "knb", "localname": "Lubuagan", - "localnames": [ "Lubuagan" ], + "localnames": [ "Lilubuagen" ], "name": "Kalinga, Lubuagan", - "names": [ "Lubuagan", "Lubuagan Kalinga" ], + "names": [ "Lilubuagen", "Lubuagan", "Lubuagan Kalinga" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -55955,9 +58595,9 @@ "iana": [ "Kankanaey" ], "iso639_3": "kne", "localname": "Kankanaey", - "localnames": [ "Kankanaey" ], + "localnames": [ "Kankanáey" ], "name": "Kankanaey", - "names": [ "Central Kankanaey", "Kankanai", "Kankanay" ], + "names": [ "Central Kankanaey", "Kankana-ey", "Kankanai", "Kankanay", "Kankanáey", "Katawán" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -55973,7 +58613,7 @@ "localname": "Uhula", "localnames": [ "Uhula" ], "name": "Mankanya", - "names": [ "Bola", "Mancagne", "Mancang", "Mancanha", "Mankaañ", "Mankanha", "Uhula" ], + "names": [ "Bola", "Bramis", "Mancagne", "Mancang", "Mancanha", "Mankaañ", "Mankanha", "Uhula" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "GM", "SN" ], @@ -55988,7 +58628,7 @@ "iana": [ "Kanufi" ], "iso639_3": "kni", "name": "Kanufi", - "names": [ "Karshi" ], + "names": [ "Anib", "Aninib", "Karshi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -56020,7 +58660,7 @@ "iana": [ "Kuranko" ], "iso639_3": "knk", "name": "Kuranko", - "names": [ "Koranko" ], + "names": [ "Koranko", "Kurankokole" ], "region": "SL", "regionname": "Sierra Leone", "regions": [ "GN" ], @@ -56035,7 +58675,7 @@ "iana": [ "Kuranko" ], "iso639_3": "knk", "name": "Kuranko", - "names": [ "Koranko" ], + "names": [ "Koranko", "Kurankokole" ], "nophonvars": true, "region": "SL", "regionname": "Sierra Leone", @@ -56075,37 +58715,6 @@ "unwritten": true, "windows": "knm-Latn" }, - { - "full": "knn-Knda-IN", - "iana": [ "Konkani (individual language)" ], - "iso639_3": "knn", - "name": "Konkani", - "names": [ "Amchigela", "Bankoti", "Central Konkan", "Concorinum", "Cugani", "Kathodi", "Katvadi", "Konkan Standard", "Konkanese", "Konkani (individual language)", "Konkani Mangalorean", "Kunabi", "North Konkan" ], - "nophonvars": true, - "obsolete": true, - "region": "IN", - "regionname": "India", - "regions": [ "CA" ], - "script": "Knda", - "sldr": false, - "tag": "knn-Knda", - "windows": "knn-Knda" - }, - { - "full": "knn-Latn-IN", - "iana": [ "Konkani (individual language)" ], - "iso639_3": "knn", - "name": "Konkani", - "names": [ "Amchigela", "Bankoti", "Central Konkan", "Concorinum", "Cugani", "Kathodi", "Katvadi", "Konkan Standard", "Konkanese", "Konkani (individual language)", "Konkani Mangalorean", "Kunabi", "North Konkan" ], - "obsolete": true, - "region": "IN", - "regionname": "India", - "regions": [ "CA" ], - "script": "Latn", - "sldr": false, - "tag": "knn-Latn", - "windows": "knn-Latn" - }, { "full": "kno-Latn-SL", "iana": [ "Kono (Sierra Leone)" ], @@ -56182,7 +58791,7 @@ "sldr": false, "tag": "kns", "tags": [ "kns-Latn", "kns-MY" ], - "windows": "kns-Latn" + "windows": "kns-Latn-MY" }, { "full": "kns-Thai-TH", @@ -56199,7 +58808,7 @@ "sldr": false, "tag": "kns-Thai", "tags": [ "kns-TH" ], - "windows": "kns-Thai" + "windows": "kns-Thai-TH" }, { "full": "knt-Latn-BR", @@ -56247,6 +58856,38 @@ "tags": [ "knv-Latn", "knv-PG" ], "windows": "knv-Latn" }, + { + "full": "knv-Latn-PG-x-aramia", + "iana": [ "Tabo" ], + "iso639_3": "knv", + "localnames": [ "Tabo" ], + "name": "Tabo", + "names": [ "Hibaradai", "Hiwi", "Waia", "Waya" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "17012", + "script": "Latn", + "sldr": false, + "tag": "knv-x-aramia", + "tags": [ "knv-Latn-x-aramia", "knv-PG-x-aramia" ], + "windows": "knv-Latn-x-aramia" + }, + { + "full": "knv-Latn-PG-x-flyriver", + "iana": [ "Tabo" ], + "iso639_3": "knv", + "localnames": [ "Tabo" ], + "name": "Tabo", + "names": [ "Hibaradai", "Hiwi", "Waia", "Waya" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "17013", + "script": "Latn", + "sldr": true, + "tag": "knv-x-flyriver", + "tags": [ "knv-Latn-x-flyriver", "knv-PG-x-flyriver" ], + "windows": "knv-Latn-x-flyriver" + }, { "full": "knw-Latn-NA", "iana": [ "Kung-Ekoka" ], @@ -56316,7 +58957,7 @@ "iso639_3": "kor", "localname": "한국어", "name": "Korean", - "names": [ "Chaoxian", "Chaoxianyu", "Chaoyu", "Goryeomal", "Hangouyu", "Hanguohua", "Hanyu", "Koryomal", "Zanichi Korean" ], + "names": [ "Chaoxian", "Chaoxianyu", "Chaoyu", "Goryeomal", "Hangouyu", "Hanguohua", "Hanyu", "Koryomal", "Zainichi Korean" ], "region": "KR", "regionname": "Republic of Korea", "regions": [ "AS", "AU", "AZ", "BH", "BN", "BR", "BY", "CA", "CN", "DE", "GU", "JP", "KG", "KZ", "LY", "MN", "MP", "MZ", "NZ", "PA", "PH", "PY", "RU", "SG", "SR", "TJ", "TM", "UA", "US", "UZ" ], @@ -56325,14 +58966,14 @@ "suppress": true, "tag": "ko", "tags": [ "ko-KR", "ko-Kore" ], - "windows": "ko" + "windows": "ko-KR" }, { "full": "ko-Brai-KR", "iana": [ "Korean" ], "iso639_3": "kor", "name": "Korean", - "names": [ "Chaoxian", "Chaoxianyu", "Chaoyu", "Goryeomal", "Hangouyu", "Hanguohua", "Hanyu", "Koryomal", "Zanichi Korean" ], + "names": [ "Chaoxian", "Chaoxianyu", "Chaoyu", "Goryeomal", "Hangouyu", "Hanguohua", "Hanyu", "Koryomal", "Zainichi Korean" ], "nophonvars": true, "region": "KR", "regionname": "Republic of Korea", @@ -56340,7 +58981,7 @@ "script": "Brai", "sldr": false, "tag": "ko-Brai", - "windows": "ko-Brai" + "windows": "ko-Brai-KR" }, { "full": "ko-Kore-KP", @@ -56352,23 +58993,22 @@ "regionname": "Democratic People's Republic of Korea", "script": "Kore", "sldr": true, - "suppress": true, "tag": "ko-KP", - "windows": "ko-KP" + "windows": "ko-Kore-KP" }, { "full": "ko-Latn-KR", "iana": [ "Korean" ], "iso639_3": "kor", "name": "Korean", - "names": [ "Chaoxian", "Chaoxianyu", "Chaoyu", "Goryeomal", "Hangouyu", "Hanguohua", "Hanyu", "Koryomal", "Zanichi Korean" ], + "names": [ "Chaoxian", "Chaoxianyu", "Chaoyu", "Goryeomal", "Hangouyu", "Hanguohua", "Hanyu", "Koryomal", "Zainichi Korean" ], "region": "KR", "regionname": "Republic of Korea", "regions": [ "AS", "AU", "AZ", "BH", "BN", "BR", "BY", "CA", "CN", "DE", "GU", "JP", "KG", "KP", "KZ", "LY", "MN", "MP", "MZ", "NZ", "PA", "PH", "PY", "RU", "SG", "SR", "TJ", "TM", "UA", "US", "UZ" ], "script": "Latn", "sldr": false, "tag": "ko-Latn", - "windows": "ko-Latn" + "windows": "ko-Latn-KR" }, { "full": "koa-Latn-PG", @@ -56411,16 +59051,15 @@ "sldr": false, "tag": "kod", "tags": [ "kod-ID", "kod-Latn" ], - "unwritten": true, "windows": "kod-Latn" }, { "full": "koe-Latn-SS", - "iana": [ "Kacipo-Balesi" ], + "iana": [ "Kacipo-Bale Suri" ], "iso639_3": "koe", - "localnames": [ "Kacipo" ], - "name": "Kacipo-Balesi", - "names": [ "Baale", "Kacipo" ], + "localnames": [ "Suri" ], + "name": "Suri, Kacipo-Bale", + "names": [ "Kachepo", "Kachipo", "Kacipo", "Kacipo-Bale Suri", "Kacipo-Balesi", "Suri" ], "region": "SS", "regionname": "South Sudan", "regions": [ "ET" ], @@ -56449,6 +59088,7 @@ "full": "kog-Latn-CO", "iana": [ "Cogui", "Kogi" ], "iso639_3": "kog", + "localname": "Kaugiañ", "localnames": [ "Kaugiañ" ], "name": "Kogi", "names": [ "Cagaba", "Coghui", "Cogui", "Kagaba", "Kaggaba", "Kaugiañ", "Kawgian", "Kogui" ], @@ -56479,8 +59119,9 @@ "full": "koi-Cyrl-RU", "iana": [ "Komi-Permyak" ], "iso639_3": "koi", + "macrolang": "kv", "name": "Komi-Permyak", - "names": [ "Kama Permyak", "Komi-Perm", "Komi-Permyaki", "Komi-Permyat", "Permian", "Permyak" ], + "names": [ "Kama Permyak", "Komi-Perm", "Komi-Permian", "Komi-Permyaki", "Komi-Permyan", "Komi-Permyat", "Permian", "Permyak" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -56493,9 +59134,9 @@ "full": "koi-Latn-RU", "iana": [ "Komi-Permyak" ], "iso639_3": "koi", + "macrolang": "kv", "name": "Komi-Permyak", - "names": [ "Kama Permyak", "Komi-Perm", "Komi-Permyaki", "Komi-Permyat", "Permian", "Permyak" ], - "obsolete": true, + "names": [ "Kama Permyak", "Komi-Perm", "Komi-Permian", "Komi-Permyaki", "Komi-Permyan", "Komi-Permyat", "Permian", "Permyak" ], "region": "RU", "regionname": "Russian Federation", "script": "Latn", @@ -56507,8 +59148,9 @@ "full": "koi-Perm-RU", "iana": [ "Komi-Permyak" ], "iso639_3": "koi", + "macrolang": "kv", "name": "Komi-Permyak", - "names": [ "Kama Permyak", "Komi-Perm", "Komi-Permyaki", "Komi-Permyat", "Permian", "Permyak" ], + "names": [ "Kama Permyak", "Komi-Perm", "Komi-Permian", "Komi-Permyaki", "Komi-Permyan", "Komi-Permyat", "Permian", "Permyak" ], "nophonvars": true, "obsolete": true, "region": "RU", @@ -56525,8 +59167,8 @@ "latnnames": [ "Konknni" ], "localname": "कोंकणी", "localnames": [ "कोंकणी" ], - "name": "Konkani", - "names": [ "Amchigela", "Bankoti", "Central Konkan", "Concorinum", "Cugani", "Kathodi", "Katvadi", "Konkan Standard", "Konkanese", "Konkani (macrolanguage)", "Konkani Mangalorean", "Kunabi", "North Konkan" ], + "name": "Konkani (macrolanguage)", + "names": [ "Amchigela", "Central Konkan", "Concorinum", "Cugani", "Kathodi", "Katvadi", "Konkan Standard", "Konkanese", "Konkani", "Konkani Mangalorean", "Kunabi", "North Konkan" ], "region": "IN", "regionname": "India", "regions": [ "CA" ], @@ -56537,6 +59179,39 @@ "tags": [ "knn", "knn-Deva", "knn-Deva-IN", "knn-IN", "kok-Deva", "kok-IN", "kok-knn", "kok-knn-IN" ], "windows": "kok" }, + { + "full": "kok-Knda-IN", + "iana": [ "Konkani (macrolanguage)" ], + "iso639_3": "kok", + "name": "Konkani (macrolanguage)", + "names": [ "Amchigela", "Central Konkan", "Concorinum", "Cugani", "Kathodi", "Katvadi", "Konkan Standard", "Konkanese", "Konkani", "Konkani Mangalorean", "Kunabi", "North Konkan" ], + "nophonvars": true, + "obsolete": true, + "region": "IN", + "regionname": "India", + "regions": [ "CA" ], + "script": "Knda", + "sldr": false, + "tag": "kok-Knda", + "tags": [ "knn-Knda", "knn-Knda-IN" ], + "windows": "kok-Knda" + }, + { + "full": "kok-Latn-IN", + "iana": [ "Konkani (macrolanguage)" ], + "iso639_3": "kok", + "name": "Konkani (macrolanguage)", + "names": [ "Amchigela", "Central Konkan", "Concorinum", "Cugani", "Kathodi", "Katvadi", "Konkan Standard", "Konkanese", "Konkani", "Konkani Mangalorean", "Kunabi", "North Konkan" ], + "obsolete": true, + "region": "IN", + "regionname": "India", + "regions": [ "CA" ], + "script": "Latn", + "sldr": false, + "tag": "kok-Latn", + "tags": [ "knn-Latn", "knn-Latn-IN" ], + "windows": "kok-Latn" + }, { "full": "kol-Latn-PG", "iana": [ "Kol (Papua New Guinea)" ], @@ -56617,13 +59292,14 @@ "full": "kot-Latn-CM", "iana": [ "Lagwan" ], "iso639_3": "kot", + "localnames": [ "Lagwan" ], "name": "Lagwan", "names": [ "Kotoko-Logone", "Lagouane", "Lagwane", "Logone" ], "region": "CM", "regionname": "Cameroon", "regions": [ "TD" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "kot", "tags": [ "kot-CM", "kot-Latn" ], "windows": "kot-Latn" @@ -56633,7 +59309,7 @@ "iana": [ "Koke" ], "iso639_3": "kou", "name": "Koke", - "names": [ "Khoke" ], + "names": [ "Khoke", "Koké" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -56677,9 +59353,9 @@ "full": "koy-Latn-US", "iana": [ "Koyukon" ], "iso639_3": "koy", - "localnames": [ "Denaakk'e" ], + "localnames": [ "Denaakk'e", "Kenaakk’enaage" ], "name": "Koyukon", - "names": [ "Denaakk’e", "Ten’a" ], + "names": [ "Denaakk’e", "Kenaakk’enaage", "Ten’a" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -56692,8 +59368,10 @@ "full": "koz-Latn-PG", "iana": [ "Korak" ], "iso639_3": "koz", - "name": "Korak", - "names": [ "Amako" ], + "localname": "Amako", + "localnames": [ "Amako" ], + "name": "Amako", + "names": [ "Korak" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -56736,13 +59414,14 @@ "full": "kpc-Latn-CO", "iana": [ "Curripaco" ], "iso639_3": "kpc", + "localnames": [ "Kurripako" ], "name": "Curripaco", "names": [ "Baniva del Isana", "Baniva-Kurripako", "Baniwa del Isana", "Coripaco", "Cumata", "Curipaco", "Curripaco-Baniva", "Karrupaku", "Koripako", "Korispaso", "Kuripaco", "Kuripako", "Kurripaco", "Kurripako", "Kúrrim", "Pacu", "Palioariene", "Payualiene", "Payuliene", "Wakuénai", "Waquenia" ], "region": "CO", "regionname": "Colombia", "regions": [ "BR", "VE" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "kpc", "tags": [ "kpc-CO", "kpc-Latn" ], "windows": "kpc-Latn" @@ -56752,6 +59431,7 @@ "iana": [ "Koba" ], "iso639_3": "kpd", "name": "Koba", + "names": [ "Aru" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -56767,8 +59447,8 @@ "iso639_3": "kpe", "localname": "Kpɛlɛwoo", "localnames": [ "Kpɛlɛwoo" ], - "name": "Kpelle, Liberia", - "names": [ "Gbese", "Kpele", "Kpelle", "Kpwessi", "Kpɛlɛwoo", "Pessa", "Pessy" ], + "name": "Kpelle", + "names": [ "Gbese", "Kpele", "Kpelle, Liberia", "Kpese", "Kpwessi", "Kpɛlɛwoo", "Pessa", "Pessy" ], "region": "LR", "regionname": "Liberia", "script": "Latn", @@ -56781,8 +59461,9 @@ "full": "kpe-Kpel-LR", "iana": [ "Kpelle" ], "iso639_3": "xpe", + "macrolang": "kpe", "name": "Kpelle, Liberia", - "names": [ "Gbese", "Kpele", "Kpelle", "Kpwessi", "Kpɛlɛwoo", "Pessa", "Pessy" ], + "names": [ "Gbese", "Kpele", "Kpelle", "Kpese", "Kpwessi", "Kpɛlɛwoo", "Pessa", "Pessy" ], "nophonvars": true, "region": "LR", "regionname": "Liberia", @@ -56798,7 +59479,7 @@ "iso639_3": "kpf", "localname": "Likɔɔnl", "name": "Komba", - "names": [ "Neng Den" ], + "names": [ "Neng Den", "Niiŋ Den" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -56828,7 +59509,7 @@ "iana": [ "Kplang" ], "iso639_3": "kph", "name": "Kplang", - "names": [ "Prang" ], + "names": [ "Guang", "Prang" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -56843,6 +59524,7 @@ "iana": [ "Kofei" ], "iso639_3": "kpi", "name": "Kofei", + "names": [ "Kifei" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -56978,8 +59660,9 @@ "full": "kps-Latn-ID", "iana": [ "Tehit" ], "iso639_3": "kps", + "localnames": [ "Salo Tkoin", "Tehit" ], "name": "Tehit", - "names": [ "Kaibus", "Tahit", "Tehid", "Teminabuan" ], + "names": [ "Kaibus", "Salo Tkoin", "Tahit", "Tehid", "Teminabuan" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -57113,9 +59796,9 @@ "iana": [ "Doromu-Koki" ], "iso639_3": "kqc", "localname": "Doromu-Koki", - "localnames": [ "Doromu-Koki" ], + "localnames": [ "Doromu ago", "Doromu-Koki" ], "name": "Doromu-Koki", - "names": [ "Dorom", "Doromu", "Koki" ], + "names": [ "Dorom", "Doromu", "Doromu ago", "Koki" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -57143,9 +59826,9 @@ "iana": [ "Kalagan" ], "iso639_3": "kqe", "localname": "Kalagan", - "localnames": [ "Kalagan" ], + "localnames": [ "Kagan", "Kalagan" ], "name": "Kalagan", - "names": [ "Kaagan", "Kinalagan", "Minuslim Kalagan" ], + "names": [ "Kaagan", "Kagan", "Kinalagan", "Minuslim Kalagan" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -57158,8 +59841,9 @@ "full": "kqf-Latn-PG", "iana": [ "Kakabai" ], "iso639_3": "kqf", + "localnames": [ "Kakabai" ], "name": "Kakabai", - "names": [ "Igora" ], + "names": [ "Igola", "Igora" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -57202,15 +59886,14 @@ "full": "kqi-Latn-PG", "iana": [ "Koitabu" ], "iso639_3": "kqi", - "name": "Koitabu", - "names": [ "Koita" ], + "localnames": [ "Koita" ], + "name": "Koita", "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "kqi", "tags": [ "kqi-Latn", "kqi-PG" ], - "unwritten": true, "windows": "kqi-Latn" }, { @@ -57294,7 +59977,7 @@ "iana": [ "Eastern Krahn" ], "iso639_3": "kqo", "name": "Krahn, Eastern", - "names": [ "Eastern Krahn", "Eastern Kran", "Kran" ], + "names": [ "Eastern Krahn", "Eastern Kran", "Konobo-Eastern Krahn", "Kran" ], "region": "LR", "regionname": "Liberia", "regions": [ "CI" ], @@ -57308,6 +59991,8 @@ "full": "kqp-Latn-TD", "iana": [ "Kimré" ], "iso639_3": "kqp", + "localname": "Gawra", + "localnames": [ "Gawra" ], "name": "Kimré", "names": [ "Gabri-Kimré", "Gawra" ], "region": "TD", @@ -57355,7 +60040,7 @@ "localname": "Kisi", "localnames": [ "Kisi" ], "name": "Kissi, Northern", - "names": [ "Gizi", "Kisi", "Kisie", "Kissien", "Northern Kissi" ], + "names": [ "Gizi", "Kisi", "Kisie", "Kissien", "Northern Kissi", "kìsìéí" ], "region": "GN", "regionname": "Guinea", "regions": [ "SL" ], @@ -57408,7 +60093,6 @@ "sldr": false, "tag": "kqv", "tags": [ "kqv-ID", "kqv-Latn" ], - "unwritten": true, "windows": "kqv-Latn" }, { @@ -57459,7 +60143,7 @@ "full": "kqy-Latn-ET", "iana": [ "Koorete" ], "iso639_3": "kqy", - "localnames": [ "Koorete", "Koori Nuuna" ], + "localnames": [ "Koorete" ], "name": "Koorete", "region": "ET", "regionname": "Ethiopia", @@ -57473,7 +60157,7 @@ "iana": [ "Korana" ], "iso639_3": "kqz", "name": "Korana", - "names": [ "!Kora", "!Ora", "Gorachouqua", "Koranna", "Koraqua" ], + "names": [ "!Gora", "!Kora", "!Ora", "Gorachouqua", "Kora", "Koranna", "Koraqua" ], "obsolete": true, "region": "ZA", "regionname": "South Africa", @@ -57487,9 +60171,10 @@ "full": "kr-Latn-NG", "iana": [ "Kanuri" ], "iso639_3": "kau", + "localname": "Kanuri", "localnames": [ "Kanuri", "Yerwa Kanuri" ], - "name": "Kanuri, Yerwa", - "names": [ "Aga", "Bornouan", "Bornouans", "Bornu", "Central Kanuri", "Kanouri", "Kanoury", "Kanuri", "Kole", "Kolere", "Sirata", "Yerwa Kanuri" ], + "name": "Kanuri", + "names": [ "Aga", "Borno", "Bornouan", "Bornouans", "Bornu", "Central Kanuri", "Kanouri", "Kanoury", "Kanuri, Yerwa", "Kole", "Kolere", "Sirata", "Yerwa Kanuri" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM", "ER", "NE", "SD", "TD" ], @@ -57503,8 +60188,9 @@ "full": "kr-Arab-NG", "iana": [ "Kanuri" ], "iso639_3": "knc", + "macrolang": "kr", "name": "Kanuri, Yerwa", - "names": [ "Aga", "Bornouan", "Bornouans", "Bornu", "Central Kanuri", "Kanouri", "Kanoury", "Kanuri", "Kole", "Kolere", "Sirata", "Yerwa Kanuri" ], + "names": [ "Aga", "Borno", "Bornouan", "Bornouans", "Bornu", "Central Kanuri", "Kanouri", "Kanoury", "Kanuri", "Kole", "Kolere", "Sirata", "Yerwa Kanuri" ], "nophonvars": true, "region": "NG", "regionname": "Nigeria", @@ -57530,18 +60216,19 @@ "windows": "kra-Deva" }, { - "full": "krb-Zyyy-US", + "full": "krb-Latn-US", "iana": [ "Karkin" ], "iso639_3": "krb", "name": "Karkin", + "names": [ "Carquin", "Los Carquines" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "krb", - "tags": [ "krb-US", "krb-Zyyy" ], + "tags": [ "krb-Latn", "krb-US" ], "unwritten": true, - "windows": "krb-Zyyy" + "windows": "krb-Latn" }, { "full": "krc-Cyrl-RU", @@ -57579,8 +60266,9 @@ "full": "kre-Latn-BR", "iana": [ "Panará" ], "iso639_3": "kre", + "localnames": [ "Panãra pẽẽ" ], "name": "Panará", - "names": [ "Indios Gigantes", "Kreen Akarore", "Kren Akarore", "Krenakarore", "Krenakore", "Krenhakarore" ], + "names": [ "Indios Gigantes", "Kreen Akarore", "Kren Akarore", "Krenakarore", "Krenakore", "Krenhakarore", "Panãra pẽẽ" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -57594,7 +60282,7 @@ "iana": [ "Koro (Vanuatu)" ], "iso639_3": "krf", "name": "Koro", - "names": [ "Koro (Vanuatu)" ], + "names": [ "Banks-Inseln", "Koro (Vanuatu)", "Koro-Olrat" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -57714,8 +60402,8 @@ "full": "krp-Latn-NG", "iana": [ "Korop" ], "iso639_3": "krp", - "name": "Korop", - "names": [ "Durop", "Dyurop", "Erorup", "Kurop", "Ododop" ], + "name": "Durop", + "names": [ "Dyurop", "Erorup", "Korop", "Kurop", "Ododop" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -57730,6 +60418,8 @@ "full": "krr-Khmr-KH", "iana": [ "Krung" ], "iso639_3": "krr", + "latnnames": [ "kreung" ], + "localnames": [ "គ្រឹង" ], "name": "Krung", "names": [ "Brao Krung", "Kreung", "Kru’ng" ], "region": "KH", @@ -57745,9 +60435,9 @@ "iana": [ "Gbaya (Sudan)" ], "iso639_3": "krs", "localname": "gbäyä", - "localnames": [ "gbäyä" ], + "localnames": [ "Gbäyä" ], "name": "Gbaya", - "names": [ "Gbaya (Sudan)", "Kpala", "Kpara", "Kparla", "Kredj", "Kreich", "Kreish", "Kresh", "gbäyä" ], + "names": [ "Gbaya (Sudan)", "Gbäyä", "Kpala", "Kpara", "Kparla", "Kredj", "Kreich", "Kreish", "Kresh" ], "region": "SS", "regionname": "South Sudan", "regions": [ "CF", "SD" ], @@ -57761,6 +60451,7 @@ "full": "krt-Latn-NE", "iana": [ "Tumari Kanuri" ], "iso639_3": "krt", + "macrolang": "kr", "name": "Kanuri, Tumari", "names": [ "Kanambu", "Kanembu", "Tumari Kanuri" ], "region": "NE", @@ -57778,31 +60469,16 @@ "latnnames": [ "Kudukh" ], "localnames": [ "कुड़ुख़" ], "name": "Kurux", - "names": [ "Kadukali", "Kurka", "Kurukh", "Oraoan", "Urang", "Uraon" ], + "names": [ "Kadukali", "Kurka", "Kurukh", "Kurukha", "Nepali Kurux", "Oraoan", "Oraon", "Orau", "Urang", "Uranw", "Uraon", "Urau", "Uraw" ], "region": "IN", "regionname": "India", - "regions": [ "BD", "BT" ], + "regions": [ "BD", "BT", "NP" ], "script": "Deva", "sldr": false, "tag": "kru", - "tags": [ "kru-Deva", "kru-IN" ], + "tags": [ "kru-Deva", "kru-IN", "kxl", "kxl-Deva", "kxl-Deva-IN", "kxl-Deva-NP", "kxl-IN", "kxl-NP" ], "windows": "kru-Deva" }, - { - "full": "kru-Deva-NP", - "iana": [ "Kurukh" ], - "latnnames": [ "Kudukh" ], - "localnames": [ "कुड़ुख़" ], - "name": "Kurux", - "names": [ "Kurukh", "Kurukha", "Nepali Kurux", "Oraon", "Orau", "Uranw", "Uraon", "Urau", "Uraw" ], - "region": "NP", - "regionname": "Nepal", - "script": "Deva", - "sldr": false, - "tag": "kru-Deva-NP", - "tags": [ "kxl", "kxl-Deva", "kxl-Deva-NP", "kxl-NP" ], - "windows": "kru-Deva-NP" - }, { "full": "krv-Khmr-KH", "iana": [ "Kavet" ], @@ -57902,6 +60578,7 @@ "iana": [ "Kashmiri" ], "iso639_3": "kas", "latnnames": [ "Koshur" ], + "localname": "कॉशुर", "localnames": [ "कॉशुर" ], "name": "Kashmiri", "names": [ "Cashmeeree", "Cashmiri", "Kacmiri", "Kaschemiri", "Keshur", "Keshuri", "Koshur" ], @@ -57910,7 +60587,7 @@ "regionname": "India", "regions": [ "PK", "US" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "ks-Deva", "windows": "ks-Deva" }, @@ -57944,21 +60621,6 @@ "tag": "ks-Shrd", "windows": "ks-Shrd" }, - { - "full": "ksa-Latn-NG", - "iana": [ "Shuwa-Zamani" ], - "iso639_3": "ksa", - "name": "Shuwa-Zamani", - "names": [ "Kauru", "Kuzamani", "Rishuwa" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "ksa", - "tags": [ "ksa-Latn", "ksa-NG" ], - "unwritten": true, - "windows": "ksa-Latn" - }, { "full": "ksb-Latn-TZ", "iana": [ "Shambala" ], @@ -57979,7 +60641,7 @@ "full": "ksc-Latn-PH", "iana": [ "Southern Kalinga" ], "iso639_3": "ksc", - "localname": "Kalinga, Southern", + "localname": "Kinalingka", "localnames": [ "Kinalingka" ], "name": "Kalinga, Southern", "names": [ "Kinalingga", "Kinalingka", "Southern Kalinga" ], @@ -57998,7 +60660,7 @@ "localname": "Tinata Tuna", "localnames": [ "Tinata Tuna" ], "name": "Kuanua", - "names": [ "Blanche Bay", "Gunantuna", "New Britain Language", "Tinata Tuna", "Tolai", "Tuna" ], + "names": [ "Blanche Bay", "Gunantuna", "Neu-Pommern", "New Britain Language", "Tinata Tuna", "Tolai", "Tuna" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -58041,7 +60703,7 @@ "iana": [ "Kusaghe" ], "iso639_3": "ksg", "name": "Kusaghe", - "names": [ "Kusage", "Kushage" ], + "names": [ "Kusage", "Kusaghe-Njela", "Kushage" ], "region": "SB", "regionname": "Solomon Islands", "script": "Latn", @@ -58071,8 +60733,8 @@ "full": "ksi-Latn-PG", "iana": [ "Krisa", "I'saka" ], "iso639_3": "ksi", - "name": "Isaka", - "names": [ "I'saka", "Krisa" ], + "name": "I’saka", + "names": [ "I'saka", "Isaka", "Krisa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -58125,6 +60787,7 @@ "sldr": false, "tag": "ksl", "tags": [ "ksl-Latn", "ksl-PG" ], + "unwritten": true, "windows": "ksl-Latn" }, { @@ -58162,7 +60825,7 @@ "iana": [ "Kofa" ], "iso639_3": "kso", "name": "Kofa", - "names": [ "Kota" ], + "names": [ "Koffa", "Kota", "Muleng" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -58179,14 +60842,14 @@ "localname": "Kabba", "localnames": [ "Kabba" ], "name": "Kabba", - "names": [ "Kaba", "Kaba de Baibokoum", "Kaba de Goré", "Kaba de Paoua", "Sara", "Western Kaba" ], + "names": [ "Godogodo", "Kaba", "Kaba Laka", "Kaba de Baibokoum", "Kaba de Goré", "Kaba de Paoua", "Laga", "Laka", "Laka of Lau", "Lakka", "Lau", "Lau Haabe", "Sara", "Western Kaba" ], "region": "CF", "regionname": "Central African Republic", - "regions": [ "TD" ], + "regions": [ "NG", "TD" ], "script": "Latn", "sldr": true, "tag": "ksp", - "tags": [ "ksp-CF", "ksp-Latn" ], + "tags": [ "ksp-CF", "ksp-Latn", "lak", "lak-CF", "lak-Latn", "lak-Latn-CF" ], "windows": "ksp-Latn" }, { @@ -58225,7 +60888,7 @@ "iana": [ "Southern Kisi" ], "iso639_3": "kss", "name": "Kisi, Southern", - "names": [ "Gisi", "Gissi", "Gizi", "Kisi", "Kissi", "Kissien", "Southern Kisi" ], + "names": [ "Gisi", "Gissi", "Gizi", "Kisi", "Kissi", "Kissien", "Southern Kisi", "kìsìéí" ], "region": "LR", "regionname": "Liberia", "regions": [ "SL" ], @@ -58239,8 +60902,9 @@ "full": "kst-Latn-BF", "iana": [ "Winyé" ], "iso639_3": "kst", + "localnames": [ "Winɩ́ɛ" ], "name": "Winyé", - "names": [ "Kols", "Kolsi", "Kõ" ], + "names": [ "Kols", "Kolsi", "Kõ", "Winɩ́ɛ" ], "region": "BF", "regionname": "Burkina Faso", "script": "Latn", @@ -58250,18 +60914,18 @@ "windows": "kst-Latn" }, { - "full": "ksu-Zyyy-IN", + "full": "ksu-Mymr-IN", "iana": [ "Khamyang" ], "iso639_3": "ksu", "name": "Khamyang", "names": [ "Khamiyang", "Khamjang", "Shyam", "Tai Khamyang", "Tai Nora" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "ksu", - "tags": [ "ksu-IN", "ksu-Zyyy" ], - "windows": "ksu-Zyyy" + "tags": [ "ksu-IN", "ksu-Mymr" ], + "windows": "ksu-Mymr" }, { "full": "ksv-Latn-CD", @@ -58282,22 +60946,22 @@ "full": "ksw-Mymr-MM", "iana": [ "S'gaw Karen" ], "iso639_3": "ksw", + "latnnames": [ "Sgaw" ], + "localnames": [ "ပှၤကညီဖိ" ], "name": "Karen, S’gaw", - "names": [ "Burmese Karen", "Kanyaw", "Karen", "Kayinpyu", "Kyetho", "Paganyaw", "Pchcknya", "Pwakanyaw", "Pwakenyaw", "S'gaw Karen", "S’gau", "S’gaw", "S’gaw Kayin", "White", "White Karen", "Yang Khao" ], + "names": [ "Burmese Karen", "Kanyaw", "Kayinpyu", "Kyetho", "Paganyaw", "Pchcknya", "Pwakenyaw", "S'gaw Karen", "S’gau", "S’gaw", "S’gaw Kayin", "White Karen", "Yang Khao" ], "region": "MM", "regionname": "Myanmar", - "regions": [ "TH" ], "script": "Mymr", "sldr": false, "tag": "ksw", "tags": [ "ksw-MM", "ksw-Mymr" ], - "windows": "ksw-Mymr" + "windows": "ksw-Mymr-MM" }, { "full": "ksw-Latn-MM", "iana": [ "S'gaw Karen" ], "iso639_3": "ksw", - "localnames": [ "S'gaw Kayin" ], "name": "Karen, S’gaw", "names": [ "Burmese Karen", "Kanyaw", "Karen", "Kayinpyu", "Kyetho", "Paganyaw", "Pchcknya", "Pwakanyaw", "Pwakenyaw", "S'gaw Karen", "S’gau", "S’gaw", "S’gaw Kayin", "White", "White Karen", "Yang Khao" ], "obsolete": true, @@ -58307,7 +60971,37 @@ "script": "Latn", "sldr": false, "tag": "ksw-Latn", - "windows": "ksw-Latn" + "windows": "ksw-Latn-MM" + }, + { + "full": "ksw-Mymr-TH", + "iana": [ "S'gaw Karen" ], + "iso639_3": "ksw", + "latnnames": [ "Sgaw" ], + "localnames": [ "ပှၤကညီဖိ" ], + "name": "Karen, S’gaw", + "names": [ "Burmese Karen", "Kanyaw", "Karen", "Paganyaw", "Pwakanyaw", "S'gaw Karen", "S’gau", "S’gaw", "S’gaw Kayin", "White", "Yang Khao" ], + "region": "TH", + "regionname": "Thailand", + "script": "Mymr", + "sldr": false, + "tag": "ksw-TH", + "windows": "ksw-Mymr-TH" + }, + { + "full": "ksw-Thai-TH", + "iana": [ "S'gaw Karen" ], + "iso639_3": "ksw", + "name": "Karen, S’gaw", + "names": [ "Burmese Karen", "Kanyaw", "Karen", "Kayinpyu", "Kyetho", "Paganyaw", "Pchcknya", "Pwakanyaw", "Pwakenyaw", "S'gaw Karen", "S’gau", "S’gaw", "S’gaw Kayin", "White", "White Karen", "Yang Khao" ], + "nophonvars": true, + "region": "TH", + "regionname": "Thailand", + "regions": [ "MM" ], + "script": "Thai", + "sldr": false, + "tag": "ksw-Thai", + "windows": "ksw-Thai-TH" }, { "full": "ksx-Latn-ID", @@ -58342,6 +61036,8 @@ "full": "ksz-Deva-IN", "iana": [ "Kodaku" ], "iso639_3": "ksz", + "latnnames": [ "Kodaku" ], + "localnames": [ "कोड़ाकु" ], "name": "Kodaku", "names": [ "Koraku", "Korwa" ], "region": "IN", @@ -58424,19 +61120,34 @@ "windows": "ktd-Latn" }, { - "full": "kte-Zyyy-NP", + "full": "kte-Deva-NP", "iana": [ "Nubri" ], "iso639_3": "kte", "name": "Nubri", "names": [ "Bhote", "Bhotia", "Kutang", "Kutang Bhotia", "Larkye" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "kte", - "tags": [ "kte-NP", "kte-Zyyy" ], + "tags": [ "kte-Deva", "kte-NP" ], + "unwritten": true, + "windows": "kte-Deva" + }, + { + "full": "kte-Tibt-NP", + "iana": [ "Nubri" ], + "iso639_3": "kte", + "name": "Nubri", + "names": [ "Bhote", "Bhotia", "Kutang", "Kutang Bhotia", "Larkye" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Tibt", + "sldr": false, + "tag": "kte-Tibt", "unwritten": true, - "windows": "kte-Zyyy" + "windows": "kte-Tibt" }, { "full": "ktf-Latn-CD", @@ -58473,7 +61184,7 @@ "iana": [ "Karanga" ], "iso639_3": "kth", "name": "Karanga", - "names": [ "Kurunga" ], + "names": [ "Karango", "Kurunga" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -58503,8 +61214,9 @@ "iana": [ "Plapo Krumen" ], "iso639_3": "ktj", "localname": "Krumen, Plapo", + "localnames": [ "Plaawin" ], "name": "Krumen, Plapo", - "names": [ "Plapo", "Plapo Krumen" ], + "names": [ "Kroumen", "Kroumen Plaapo", "Kru Plapo", "Krumen", "Krumen Plapo", "Plaapo Kroumen", "Plaawin", "Plapo", "Plapo Krumen" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -58528,17 +61240,17 @@ "windows": "ktk-Latn" }, { - "full": "ktl-Zyyy-IR", + "full": "ktl-Arab-IR", "iana": [ "Koroshi" ], "iso639_3": "ktl", "name": "Koroshi", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "ktl", - "tags": [ "ktl-IR", "ktl-Zyyy" ], - "windows": "ktl-Zyyy" + "tags": [ "ktl-Arab", "ktl-IR" ], + "windows": "ktl-Arab" }, { "full": "ktm-Latn-PG", @@ -58547,7 +61259,7 @@ "localname": "Kurti", "localnames": [ "Kurti" ], "name": "Kurti", - "names": [ "Kuruti", "Kuruti-Pare", "Ndrugul" ], + "names": [ "Kuruti", "Kuruti-Pare", "Ndrugul", "Sohou Kurti" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -58561,7 +61273,7 @@ "iana": [ "Karitiâna" ], "iso639_3": "ktn", "name": "Karitiâna", - "names": [ "Caritiana" ], + "names": [ "Caritiana", "Yjxa" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -58676,19 +61388,19 @@ "windows": "ktv-Latn" }, { - "full": "ktw-Zyyy-US", + "full": "ktw-Latn-US", "iana": [ "Kato" ], "iso639_3": "ktw", "name": "Kato", "names": [ "Batem-Da-Kai-Ee", "Cahto", "Kai Po-Mo", "Tlokeang" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ktw", - "tags": [ "ktw-US", "ktw-Zyyy" ], + "tags": [ "ktw-Latn", "ktw-US" ], "unwritten": true, - "windows": "ktw-Zyyy" + "windows": "ktw-Latn" }, { "full": "ktx-Latn-BR", @@ -58726,7 +61438,7 @@ "iso639_3": "ktz", "localnames": [ "Juǀ'hoansi" ], "name": "Juǀ’hoansi", - "names": [ "!Xo", "!Xun", "Dobe Kung", "Dzu’oasi", "Juǀʼhoan", "Juǀʼhoansi", "Juǀ’hoan", "Ju’oasi", "Kung", "Kung-Tsumkwe", "Tshumkwe", "Tsumkwe", "Xaixai", "Xû", "Zhu’oasi" ], + "names": [ "!Xo", "!Xun", "Dobe Kung", "Dzu’oasi", "Juǀʼhoan", "Juǀʼhoansi", "Juǀ’hoan", "Ju’oasi", "Kung", "Kung-Tsumkwe", "South-Eastern Ju", "Tshumkwe", "Tsumkwe", "Xaixai", "Xû", "Zhu’oasi" ], "region": "NA", "regionname": "Namibia", "regions": [ "BW" ], @@ -58742,8 +61454,8 @@ "iso639_3": "kur", "localname": "kurdî", "localnames": [ "Kurdî-Kurmancî", "Kurmancî" ], - "name": "Kurdish, Northern", - "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish", "Kurdish Kurmanji", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], + "name": "Kurdish", + "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish Kurmanji", "Kurdish, Northern", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], "region": "TR", "regionname": "Turkey", "regions": [ "AM", "AZ", "BE", "BH", "CA", "CH", "DE", "DK", "FI", "GE", "GR", "IQ", "IR", "IT", "KG", "KW", "KZ", "LB", "NL", "NO", "RU", "SE", "SY", "TM", "US" ], @@ -58751,7 +61463,7 @@ "sldr": true, "tag": "ku", "tags": [ "kmr-Latn", "kmr-Latn-TR", "ku-Latn", "ku-TR" ], - "windows": "ku-Latn" + "windows": "ku-Latn-TR" }, { "full": "ku-Arab-IQ", @@ -58766,14 +61478,14 @@ "script": "Arab", "sldr": true, "tag": "ku-Arab", - "windows": "ku-Arab" + "windows": "ku-Arab-IQ" }, { "full": "ku-Arab-TR", "iana": [ "Kurdish" ], "localname": "kurdî", - "name": "Kurdish, Northern", - "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish", "Kurdish Kurmanji", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], + "name": "Kurdish", + "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish Kurmanji", "Kurdish, Northern", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], "region": "TR", "regionname": "Turkey", "regions": [ "AM", "AZ", "BE", "BH", "CA", "CH", "DE", "DK", "FI", "GE", "GR", "IR", "IT", "KG", "KW", "KZ", "NL", "NO", "RU", "SE", "SY", "TM", "US" ], @@ -58787,8 +61499,8 @@ "full": "ku-Armn-AZ", "iana": [ "Kurdish" ], "iso639_3": "kur", - "name": "Kurdish, Northern", - "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish", "Kurdish Kurmanji", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], + "name": "Kurdish", + "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish Kurmanji", "Kurdish, Northern", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], "nophonvars": true, "region": "AZ", "regionname": "Azerbaijan", @@ -58797,7 +61509,7 @@ "sldr": false, "tag": "ku-Armn", "tags": [ "kmr-Armn", "kmr-Armn-AZ" ], - "windows": "ku-Armn" + "windows": "ku-Armn-AZ" }, { "full": "ku-Cyrl-IQ", @@ -58811,11 +61523,13 @@ "script": "Cyrl", "sldr": false, "tag": "ku-Cyrl", - "windows": "ku-Cyrl" + "windows": "ku-Cyrl-IQ" }, { "full": "ku-Cyrl-AZ", "iana": [ "Kurdish" ], + "iso639_3": "kmr", + "macrolang": "ku", "name": "Kurdish, Northern", "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish", "Kurdish Kurmanji", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], "region": "AZ", @@ -58842,6 +61556,8 @@ { "full": "ku-Yezi-GE", "iana": [ "Kurdish" ], + "iso639_3": "kmr", + "macrolang": "ku", "name": "Kurdish, Northern", "names": [ "Badinani", "Badînî", "Bahdini", "Behdînî", "Eastern Kurmanji", "Ezdiki", "Kermancî", "Khorasani", "Khorasani Kurmanji", "Kirmanciya Jori", "Kirmancî", "Kordi", "Kordi Kormanji", "Kurdi", "Kurdish", "Kurdish Kurmanji", "Kurdiya jorîn", "Kurdmancî", "Kurdî", "Kurdî-Kurmancî", "Kurmancî", "Kurmanji", "Kurmanji Kurdish", "Kurmanjî", "Red Kurmanji" ], "region": "GE", @@ -58860,7 +61576,7 @@ "localname": "Kuteb", "localnames": [ "Kuteb" ], "name": "Kutep", - "names": [ "Ati", "Kuteb", "Kutev", "Mbarike", "Zumper" ], + "names": [ "Ati", "Kuteb", "Kutev", "Mbarike" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -58889,7 +61605,7 @@ "full": "kud-Latn-PG", "iana": [ "'Auhelawa" ], "iso639_3": "kud", - "localname": "’Auhelawa", + "localname": "'Auhelawa", "localnames": [ "'Auhelawa" ], "name": "’Auhelawa", "names": [ "'Auhelawa", "Kurada", "Nuakata", "Ulada", "’Urada" ], @@ -58952,7 +61668,7 @@ "iso639_3": "kuh", "localnames": [ "Fo Goji" ], "name": "Kushi", - "names": [ "Chong’e", "Goji", "Kushe" ], + "names": [ "Chonge", "Chongee", "Chong’e", "Fo Goji", "Goji", "Kushe", "Memme Goji" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -58963,7 +61679,7 @@ }, { "full": "kui-Latn-BR", - "iana": [ "Kuikúro-Kalapálo" ], + "iana": [ "Kuikúro-Kalapálo", "Kalapalo" ], "iso639_3": "kui", "name": "Kuikúro-Kalapálo", "names": [ "Apalakiri", "Apalaquiri", "Calapalu", "Cuicuro", "Cuicutl", "Guicurú", "Kalapalo", "Kuikuru", "Kurkuro" ], @@ -58997,7 +61713,7 @@ "iana": [ "Kepo'" ], "iso639_3": "kuk", "name": "Kepo’", - "names": [ "Kepo'", "Kepoq" ], + "names": [ "Ba’i", "Kepo", "Kepo'", "Kepoq" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -59073,6 +61789,7 @@ "full": "kun-Latn-ER", "iana": [ "Kunama" ], "iso639_3": "kun", + "localname": "Kunama", "localnames": [ "Kunama" ], "name": "Kunama", "names": [ "Baada", "Baaden", "Baaza", "Baazayn", "Baazen", "Bada", "Baden", "Baza", "Bazen", "Cunama", "Diila" ], @@ -59132,10 +61849,10 @@ "full": "kus-Latn-GH", "iana": [ "Kusaal" ], "iso639_3": "kus", - "localname": "Kusaas", - "localnames": [ "Kusaas" ], + "localname": "Kʋsaal", + "localnames": [ "Kʋsaal" ], "name": "Kusaal", - "names": [ "Koussassé", "Kusaas", "Kusaasi", "Kusale", "Kusasi" ], + "names": [ "Koussassé", "Kusaas", "Kusaasi", "Kusale", "Kusasi", "Kʋsaal" ], "region": "GH", "regionname": "Ghana", "regions": [ "BF", "TG" ], @@ -59171,7 +61888,7 @@ "region": "US", "regionname": "United States", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "kuu", "tags": [ "kuu-Latn", "kuu-US" ], "windows": "kuu-Latn" @@ -59254,8 +61971,10 @@ "full": "kv-Cyrl-RU", "iana": [ "Komi" ], "iso639_3": "kom", - "name": "Komi-Zyrian", - "names": [ "Komi", "Komi-Zyryan", "Zyrian" ], + "latnnames": [ "Komi kyv" ], + "localnames": [ "Коми кыв" ], + "name": "Komi", + "names": [ "Komi-Syrjänisch", "Komi-Zyrian", "Komi-Zyryan", "Syrjänisch", "Syränisch", "Yazva", "Ziryene", "Zyrian", "Zyriène", "Zyryan" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -59264,16 +61983,33 @@ "tags": [ "kpv", "kpv-Cyrl", "kpv-Cyrl-RU", "kpv-RU", "kv-Cyrl", "kv-RU" ], "windows": "kv-Cyrl" }, + { + "full": "kv-Cyrs-RU", + "iana": [ "Komi" ], + "iso639_3": "kpv", + "macrolang": "kv", + "name": "Komi-Zyrian", + "names": [ "Komi", "Komi-Syrjänisch", "Komi-Zyryan", "Syrjänisch", "Syränisch", "Yazva", "Ziryene", "Zyrian", "Zyriène", "Zyryan" ], + "region": "RU", + "regionname": "Russian Federation", + "script": "Cyrs", + "sldr": false, + "tag": "kv-Cyrs-RU", + "tags": [ "kpv-Cyrs", "kpv-Cyrs-RU" ], + "windows": "kv-Cyrs-RU" + }, { "full": "kv-Latn-RU", "iana": [ "Komi" ], "iso639_3": "kom", "name": "Komi", + "names": [ "Komi-Syrjänisch", "Komi-Zyrian", "Komi-Zyryan", "Syrjänisch", "Syränisch", "Yazva", "Ziryene", "Zyrian", "Zyriène", "Zyryan" ], "region": "RU", "regionname": "Russian Federation", "script": "Latn", "sldr": false, "tag": "kv-Latn", + "tags": [ "kpv-Latn", "kpv-Latn-RU" ], "windows": "kv-Latn" }, { @@ -59309,6 +62045,7 @@ "full": "kvb-Latn-ID", "iana": [ "Kubu" ], "iso639_3": "kvb", + "macrolang": "ms", "name": "Kubu", "names": [ "Anak Dalam", "Orang Hutan", "Orang Rimba" ], "obsolete": true, @@ -59338,7 +62075,7 @@ "iana": [ "Kui (Indonesia)" ], "iso639_3": "kvd", "name": "Kui", - "names": [ "Kui (Indonesia)", "Lerabain", "Masin-Lak" ], + "names": [ "Kui (Indonesia)", "Lerabaing", "Masin-Lak" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -59353,7 +62090,7 @@ "iana": [ "Kalabakan" ], "iso639_3": "kve", "name": "Murut, Kalabakan", - "names": [ "Kalabakan" ], + "names": [ "Kalabakan", "Tawau Murut", "Tidung" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -59382,6 +62119,8 @@ "full": "kvg-Latn-PG", "iana": [ "Kuni-Boazi" ], "iso639_3": "kvg", + "localname": "Kuni-Boazi", + "localnames": [ "Kuni-Boazi" ], "name": "Kuni-Boazi", "names": [ "Boadji", "Boazi", "Bwadji", "Kuini", "Kuni" ], "region": "PG", @@ -59444,6 +62183,7 @@ "names": [ "Hanguk Sueo", "Hanguk Suhwa Eoneo", "KSL" ], "region": "KR", "regionname": "Republic of Korea", + "regions": [ "KP" ], "script": "Zxxx", "sldr": false, "tag": "kvk", @@ -59484,6 +62224,7 @@ "full": "kvn-Latn-CO", "iana": [ "Border Kuna" ], "iso639_3": "kvn", + "localnames": [ "Tule" ], "name": "Kuna, Border", "names": [ "Border Kuna", "Caiman Nuevo", "Colombia Cuna", "Cuna", "Guna", "Kuna de la Frontera", "Long Hair Cuna", "Paya Kuna", "Paya-Pucuro", "Paya-Pucuro Kuna", "Tule" ], "region": "CO", @@ -59501,7 +62242,7 @@ "iso639_3": "kvo", "localnames": [ "Dobel" ], "name": "Dobel", - "names": [ "Doibel", "Kobroor", "Kobro’or", "Sersifar Tannin" ], + "names": [ "Aru", "Doibel", "Kobroor", "Kobro’or", "Sersifar Tannin" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -59515,7 +62256,7 @@ "iana": [ "Kompane" ], "iso639_3": "kvp", "name": "Kompane", - "names": [ "Komfana", "Kongampani" ], + "names": [ "Aru", "Komfana", "Kongampani" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -59526,36 +62267,37 @@ "windows": "kvp-Latn" }, { - "full": "kvq-Latn-MM", + "full": "kvq-Mymr-MM", "iana": [ "Geba Karen" ], "iso639_3": "kvq", "name": "Karen, Geba", "names": [ "Daneh", "Geba", "Geba Karen", "Kaba", "Karenbyu", "Kayinbyu", "Kayinpyu", "Kebar", "Kono", "Koo-ong", "Northern Bwe", "White Karen" ], "region": "MM", "regionname": "Myanmar", - "script": "Latn", + "script": "Mymr", "sldr": false, - "tag": "kvq-Latn", - "windows": "kvq-Latn" + "tag": "kvq", + "tags": [ "kvq-MM", "kvq-Mymr" ], + "windows": "kvq-Mymr" }, { - "full": "kvq-Mymr-MM", + "full": "kvq-Latn-MM", "iana": [ "Geba Karen" ], "iso639_3": "kvq", "name": "Karen, Geba", "names": [ "Daneh", "Geba", "Geba Karen", "Kaba", "Karenbyu", "Kayinbyu", "Kayinpyu", "Kebar", "Kono", "Koo-ong", "Northern Bwe", "White Karen" ], - "nophonvars": true, "region": "MM", "regionname": "Myanmar", - "script": "Mymr", + "script": "Latn", "sldr": false, - "tag": "kvq-Mymr", - "windows": "kvq-Mymr" + "tag": "kvq-Latn", + "windows": "kvq-Latn" }, { "full": "kvr-Latn-ID", "iana": [ "Kerinci" ], "iso639_3": "kvr", + "macrolang": "ms", "name": "Kerinci", "names": [ "Kerinchi", "Kinchai" ], "region": "ID", @@ -59570,6 +62312,7 @@ "full": "kvr-Rjng-ID", "iana": [ "Kerinci" ], "iso639_3": "kvr", + "macrolang": "ms", "name": "Kerinci", "names": [ "Kerinchi", "Kinchai" ], "nophonvars": true, @@ -59582,19 +62325,19 @@ "windows": "kvr-Rjng" }, { - "full": "kvt-Zyyy-MM", + "full": "kvt-Mymr-MM", "iana": [ "Lahta Karen", "Lahta" ], "iso639_3": "kvt", "name": "Lahta", "names": [ "Kayan Lahta", "Khahta", "Lahta Karen", "Peu", "Taru", "Tarulakhi", "Yan Wohta" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "kvt", - "tags": [ "kvt-MM", "kvt-Zyyy" ], + "tags": [ "kvt-MM", "kvt-Mymr" ], "unwritten": true, - "windows": "kvt-Zyyy" + "windows": "kvt-Mymr" }, { "full": "kvu-Zyyy-MM", @@ -59658,26 +62401,25 @@ "windows": "kvx-Arab" }, { - "full": "kvy-Zyyy-MM", + "full": "kvy-Kali-MM", "iana": [ "Yintale Karen", "Yintale" ], "iso639_3": "kvy", "name": "Yintale", - "names": [ "Taliak", "Yangatalet", "Yangtadai", "Yintale Karen", "Yintalet" ], + "names": [ "Tali", "Taliak", "Yangatalet", "Yangtadai", "Yintale Karen", "Yintalet" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Kali", "sldr": false, "tag": "kvy", - "tags": [ "kvy-MM", "kvy-Zyyy" ], - "unwritten": true, - "windows": "kvy-Zyyy" + "tags": [ "kvy-Kali", "kvy-MM" ], + "windows": "kvy-Kali" }, { "full": "kvz-Latn-ID", "iana": [ "Tsakwambo", "Tsaukambo" ], "iso639_3": "kvz", "name": "Tsaukambo", - "names": [ "Kotogüt", "Tsakwambo", "Tsokwambo" ], + "names": [ "Kotogut", "Kotogüt", "Tsakwambo", "Tsokwambo" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -59702,7 +62444,22 @@ "tag": "kw", "tags": [ "kw-GB", "kw-Latn" ], "variants": [ "kkcor", "kscor", "uccor", "ucrcor" ], - "windows": "kw-Latn" + "windows": "kw-Latn-GB" + }, + { + "full": "kw-Ogam-001", + "iana": [ "Cornish" ], + "iso639_3": "cor", + "name": "Cornish", + "names": [ "Curnoack", "Kernewek", "Kernowek" ], + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Ogam", + "sldr": false, + "tag": "kw-Ogam", + "tags": [ "kw-001" ], + "windows": "kw-Ogam-001" }, { "full": "kwa-Latn-BR", @@ -59724,7 +62481,7 @@ "iana": [ "Kwa" ], "iso639_3": "kwb", "name": "Kwa", - "names": [ "Baa", "Kwah" ], + "names": [ "Baa", "Kwah", "nyaa Báà" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -59847,6 +62604,8 @@ "full": "kwj-Latn-PG", "iana": [ "Kwanga" ], "iso639_3": "kwj", + "localname": "Kwanga", + "localnames": [ "Kwanga" ], "name": "Kwanga", "names": [ "Gawanga", "Kawanga" ], "region": "PG", @@ -59863,7 +62622,7 @@ "iso639_3": "kwk", "localnames": [ "Kwakwala" ], "name": "Kwakwala", - "names": [ "Kwagiutl", "Kwakiutl", "Kwakwaka’wakw", "Kwakwalla", "Kwak̓wala" ], + "names": [ "Kwagiutl", "Kwakiutl", "Kwakwaka’wakw" ], "region": "CA", "regionname": "Canada", "regions": [ "US" ], @@ -59878,7 +62637,7 @@ "iana": [ "Kofyar" ], "iso639_3": "kwl", "name": "Kofyar", - "names": [ "Koffiar" ], + "names": [ "Koffiar", "Pan" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -59923,6 +62682,7 @@ "full": "kwo-Latn-PG", "iana": [ "Kwomtari" ], "iso639_3": "kwo", + "localname": "Glefe", "localnames": [ "Glefe" ], "name": "Kwomtari", "names": [ "Glefe", "Kefe", "Kisofe", "Klefe" ], @@ -59983,6 +62743,7 @@ "iana": [ "Kwesten" ], "iso639_3": "kwt", "name": "Kwesten", + "names": [ "Kwalhioqua", "Kwalhioqua-Clatskanie" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -59996,8 +62757,9 @@ "full": "kwu-Latn-CM", "iana": [ "Kwakum" ], "iso639_3": "kwu", + "localnames": [ "Kwakum" ], "name": "Kwakum", - "names": [ "Abakoum", "Abakum", "Akpwakum", "Bakum", "Kpakum", "Pakum" ], + "names": [ "Abakoum", "Abakum", "Akpwakum", "Bakoum", "Bakum", "Kpakum", "Pakum" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -60042,7 +62804,7 @@ "iana": [ "Khirwar" ], "iso639_3": "kwx", "name": "Khirwar", - "names": [ "Kalari", "Kherwari", "Khirwara" ], + "names": [ "Kalari", "Kharwâr", "Kherwari", "Khirwara" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -60055,6 +62817,8 @@ "full": "kwy-Latn-CD", "iana": [ "San Salvador Kongo" ], "iso639_3": "kwy", + "localnames": [ "Kikongo" ], + "macrolang": "kg", "name": "Kongo, San Salvador", "names": [ "Congo", "Iwoyo", "Kikongo", "Kikoongo", "Kisikongo", "San Salvador Kongo" ], "region": "CD", @@ -60111,40 +62875,44 @@ "windows": "kxb-Latn" }, { - "full": "kxc-Ethi-ET", + "full": "kxc-Latn-ET", "iana": [ "Konso", "Khonso" ], "iso639_3": "kxc", + "localnames": [ "Afaa Xonso" ], "name": "Konso", "region": "ET", "regionname": "Ethiopia", "regions": [ "KE" ], - "script": "Ethi", - "sldr": true, + "script": "Latn", + "sldr": false, "tag": "kxc", - "tags": [ "kxc-ET", "kxc-Ethi" ], - "windows": "kxc-Ethi" + "tags": [ "kxc-ET", "kxc-Latn" ], + "windows": "kxc-Latn" }, { - "full": "kxc-Latn-ET", + "full": "kxc-Ethi-ET", "iana": [ "Konso", "Khonso" ], "iso639_3": "kxc", - "localnames": [ "Khonso" ], + "latnnames": [ "äfä honəso" ], + "localnames": [ "አፈ ኾንሶ" ], "name": "Konso", + "nophonvars": true, "region": "ET", "regionname": "Ethiopia", "regions": [ "KE" ], - "script": "Latn", - "sldr": false, - "tag": "kxc-Latn", - "windows": "kxc-Latn" + "script": "Ethi", + "sldr": true, + "tag": "kxc-Ethi", + "windows": "kxc-Ethi" }, { "full": "kxd-Latn-BN", "iana": [ "Brunei" ], "iso639_3": "kxd", - "localnames": [ "Bahasa Melayu Brunei" ], + "localnames": [ "Brunei", "Melayu Brunei" ], + "macrolang": "ms", "name": "Brunei", - "names": [ "Bahasa Melayu Brunei", "Brunai", "Brunei-Kadayan", "Orang Bukit" ], + "names": [ "Brunai", "Brunei-Kadayan", "Melayu Brunei" ], "region": "BN", "regionname": "Brunei Darussalam", "regions": [ "MY" ], @@ -60158,10 +62926,11 @@ "full": "kxd-Arab-BN", "iana": [ "Brunei" ], "iso639_3": "kxd", - "latnnames": [ "Bahasa Melayu Brunei" ], - "localnames": [ "بهاس ملايو بروني" ], + "latnnames": [ "Melayu Brunei" ], + "localnames": [ "ملايو بروني" ], + "macrolang": "ms", "name": "Brunei", - "names": [ "Bahasa Melayu Brunei", "Brunai", "Brunei-Kadayan", "Orang Bukit" ], + "names": [ "Brunai", "Brunei-Kadayan", "Melayu Brunei" ], "nophonvars": true, "region": "BN", "regionname": "Brunei Darussalam", @@ -60176,7 +62945,7 @@ "iana": [ "Manumanaw Karen", "Manumanaw" ], "iso639_3": "kxf", "name": "Kawyaw", - "names": [ "Kayah-Munu", "Kayàw", "Manaw", "Manu", "Manu Manaw", "Manumanaw", "Manumanaw Karen", "Manö", "Monu" ], + "names": [ "Kayah Monu", "Kayah-Munu", "Kayàw", "Manaw", "Manu", "Manu Manaw", "Manumanaw", "Manumanaw Karen", "Manö", "Monu" ], "region": "MM", "regionname": "Myanmar", "script": "Mymr", @@ -60191,7 +62960,7 @@ "iso639_3": "kxf", "localnames": [ "Kawyaw" ], "name": "Kawyaw", - "names": [ "Kayah-Munu", "Kayàw", "Manaw", "Manu", "Manu Manaw", "Manumanaw", "Manumanaw Karen", "Manö", "Monu" ], + "names": [ "Kayah Monu", "Kayah-Munu", "Kayàw", "Manaw", "Manu", "Manu Manaw", "Manumanaw", "Manumanaw Karen", "Manö", "Monu" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -60205,7 +62974,7 @@ "iso639_3": "kxf", "localnames": [ "Kawyaw" ], "name": "Kawyaw", - "names": [ "Kayah-Munu", "Kayàw", "Manaw", "Manu", "Manu Manaw", "Manu and Manaw dialects", "Manumanaw", "Manumanaw Karen", "Manö", "Monu" ], + "names": [ "Kayah Monu", "Kayah-Munu", "Kayàw", "Manaw", "Manu", "Manu Manaw", "Manu and Manaw dialects", "Manumanaw", "Manumanaw Karen", "Manö", "Monu" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -60260,19 +63029,19 @@ "windows": "kxj-Latn" }, { - "full": "kxk-Zyyy-MM", + "full": "kxk-Mymr-MM", "iana": [ "Zayein Karen" ], "iso639_3": "kxk", "name": "Zayein", - "names": [ "Gaungtou", "Khaungtou", "Zayein Karen" ], + "names": [ "Gaungtou", "Khaungtou", "Lahta-Zayein Karen", "Zayein Karen" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "kxk", - "tags": [ "kxk-MM", "kxk-Zyyy" ], + "tags": [ "kxk-MM", "kxk-Mymr" ], "unwritten": true, - "windows": "kxk-Zyyy" + "windows": "kxk-Mymr" }, { "full": "kxm-Thai-TH", @@ -60347,7 +63116,7 @@ "sldr": true, "tag": "kxp", "tags": [ "kxp-Arab", "kxp-PK" ], - "windows": "kxp-Arab" + "windows": "kxp-Arab-PK" }, { "full": "kxp-Gujr-IN", @@ -60363,7 +63132,7 @@ "sldr": false, "tag": "kxp-Gujr", "tags": [ "kxp-IN" ], - "windows": "kxp-Gujr" + "windows": "kxp-Gujr-IN" }, { "full": "kxp-Arab-PK-x-thr", @@ -60378,7 +63147,7 @@ "sldr": false, "tag": "kxp-x-thr", "tags": [ "kxp-Arab-x-thr", "kxp-PK-x-thr" ], - "windows": "kxp-Arab-x-thr" + "windows": "kxp-Arab-PK-x-thr" }, { "full": "kxq-Latn-ID", @@ -60406,7 +63175,7 @@ "script": "Latn", "sldr": false, "tag": "kxr", - "tags": [ "kxr-Latn", "kxr-PG" ], + "tags": [ "kxr-Latn", "kxr-PG", "pat", "pat-Latn", "pat-Latn-PG", "pat-PG" ], "unwritten": true, "windows": "kxr-Latn" }, @@ -60430,6 +63199,7 @@ "iana": [ "Koiwat" ], "iso639_3": "kxt", "name": "Koiwat", + "names": [ "Sawos" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -60457,7 +63227,7 @@ "iana": [ "Kuvi" ], "iso639_3": "kxv", "name": "Kuvi", - "names": [ "Jatapu", "Khondh", "Khondi", "Kond", "Kuvi Kond", "Kuvinga", "Kuwi" ], + "names": [ "Khod", "Khondh", "Khondi", "Kodu", "Kond", "Kuvi Kond", "Kuvinga", "Kuwi" ], "region": "IN", "regionname": "India", "script": "Orya", @@ -60466,6 +63236,35 @@ "tags": [ "kxv-IN", "kxv-Orya" ], "windows": "kxv-Orya" }, + { + "full": "kxv-Latn-IN", + "iana": [ "Kuvi" ], + "iso639_3": "kxv", + "name": "Kuvi", + "names": [ "Khod", "Khondh", "Khondi", "Kodu", "Kond", "Kuvi Kond", "Kuvinga", "Kuwi" ], + "region": "IN", + "regionname": "India", + "script": "Latn", + "sldr": false, + "tag": "kxv-Latn", + "windows": "kxv-Latn" + }, + { + "full": "kxv-Telu-IN", + "iana": [ "Kuvi" ], + "iso639_3": "kxv", + "latnnames": [ "Kuvi" ], + "localnames": [ "కువి" ], + "name": "Kuvi", + "names": [ "Khod", "Khondh", "Khondi", "Kodu", "Kond", "Kuvi Kond", "Kuvinga", "Kuwi" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Telu", + "sldr": false, + "tag": "kxv-Telu", + "windows": "kxv-Telu" + }, { "full": "kxw-Latn-PG", "iana": [ "Konai" ], @@ -60487,7 +63286,7 @@ "iana": [ "Likuba" ], "iso639_3": "kxx", "name": "Likuba", - "names": [ "Kuba" ], + "names": [ "Kuba", "Kyba" ], "obsolete": true, "region": "CG", "regionname": "Congo", @@ -60502,7 +63301,7 @@ "iana": [ "Kayong" ], "iso639_3": "kxy", "name": "Kayong", - "names": [ "Ca Giong", "Kagiuong", "Katang" ], + "names": [ "Ca Giong", "Cagiuong", "Kagiuong", "Katang" ], "region": "VN", "regionname": "Viet Nam", "script": "Latn", @@ -60543,7 +63342,7 @@ "sldr": true, "tag": "ky", "tags": [ "ky-Cyrl", "ky-KG" ], - "windows": "ky-Cyrl" + "windows": "ky-Cyrl-KG" }, { "full": "ky-Arab-CN", @@ -60583,11 +63382,11 @@ "iso639_3": "kya", "localnames": [ "Ekikwaya" ], "name": "Kwaya", - "names": [ "Kikwaya" ], + "names": [ "Ekikwaya", "Kikwaya" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "kya", "tags": [ "kya-Latn", "kya-TZ" ], "windows": "kya-Latn" @@ -60612,8 +63411,9 @@ "full": "kyc-Latn-PG", "iana": [ "Kyaka" ], "iso639_3": "kyc", + "localnames": [ "Kyaka" ], "name": "Kyaka", - "names": [ "Baiyer", "Enga-Kyaka", "Kyaka Enga" ], + "names": [ "Baiyer", "Enga-Kyaka", "Kyaka Enga", "Kyaka Eŋa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -60641,10 +63441,10 @@ "full": "kye-Latn-GH", "iana": [ "Krache" ], "iso639_3": "kye", - "localname": "Kaakyi", - "localnames": [ "Kaakyi" ], + "localname": "Kaakye", + "localnames": [ "Kaakye" ], "name": "Krache", - "names": [ "Kaakyi", "Krachi", "Krakye" ], + "names": [ "Kaache", "Kaakye", "Kaakyi", "Kaakyɩ", "Krachi", "Krakye" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -60688,7 +63488,7 @@ "iana": [ "Karok" ], "iso639_3": "kyh", "name": "Karok", - "names": [ "Karuk" ], + "names": [ "Charoc", "Karuk", "Quoratean", "Quoratem" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -60732,15 +63532,15 @@ "full": "kyk-Latn-PH", "iana": [ "Kamayo" ], "iso639_3": "kyk", + "localnames": [ "Kinamayo" ], "name": "Kamayo", - "names": [ "Davawenyo", "Davaweño", "Kadi", "Kinadi", "Kinamayo", "Mandaya" ], + "names": [ "Camayo", "Davawenyo", "Davaweño", "Kadi", "Kinadi", "Kinamayo", "Mandaya" ], "region": "PH", "regionname": "Philippines", "script": "Latn", "sldr": false, "tag": "kyk", "tags": [ "kyk-Latn", "kyk-PH" ], - "unwritten": true, "windows": "kyk-Latn" }, { @@ -60869,7 +63669,7 @@ "iana": [ "Kayagar" ], "iso639_3": "kyt", "name": "Kayagar", - "names": [ "Kajagar", "Kaygi", "Kaygir", "Wiyagar" ], + "names": [ "Kajagar", "Kajakaja", "Kaygi", "Kaygir", "Wiyagar" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -60885,7 +63685,7 @@ "iso639_3": "kyu", "localname": "ꤊꤢ꤬ꤛꤢ꤭ꤜꤟꤤ꤬", "name": "Kayah, Western", - "names": [ "Karenni", "Karennyi", "Karieng Daeng", "Kayah", "Kayah Li", "Red Karen", "Western Kayah", "Yang Daeng" ], + "names": [ "Karenni", "Karennyi", "Karieng Daeng", "Kayah", "Kayah Li", "Kayahli", "Red Karen", "Western Kayah", "Yang Daeng" ], "region": "MM", "regionname": "Myanmar", "script": "Kali", @@ -60899,11 +63699,11 @@ "iana": [ "Western Kayah" ], "iso639_3": "kyu", "name": "Kayah, Western", - "names": [ "Karenni", "Karennyi", "Karieng Daeng", "Kayah", "Kayah Li", "Red Karen", "Western Kayah", "Yang Daeng" ], + "names": [ "Karenni", "Karennyi", "Karieng Daeng", "Kayah", "Kayah Li", "Kayahli", "Red Karen", "Western Kayah", "Yang Daeng" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "kyu-Latn", "windows": "kyu-Latn" }, @@ -60911,8 +63711,10 @@ "full": "kyu-Mymr-MM", "iana": [ "Western Kayah" ], "iso639_3": "kyu", + "latnnames": [ "Kayahli" ], + "localnames": [ "ကယားလီ" ], "name": "Kayah, Western", - "names": [ "Karenni", "Karennyi", "Karieng Daeng", "Kayah", "Kayah Li", "Red Karen", "Western Kayah", "Yang Daeng" ], + "names": [ "Karenni", "Karennyi", "Karieng Daeng", "Kayah", "Kayah Li", "Kayahli", "Red Karen", "Western Kayah", "Yang Daeng" ], "nophonvars": true, "region": "MM", "regionname": "Myanmar", @@ -60922,19 +63724,33 @@ "windows": "kyu-Mymr" }, { - "full": "kyv-Zyyy-NP", + "full": "kyv-Deva-NP", "iana": [ "Kayort" ], "iso639_3": "kyv", "name": "Kewat", "names": [ "Kayort" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "kyv", - "tags": [ "kyv-NP", "kyv-Zyyy" ], + "tags": [ "kyv-Deva", "kyv-NP" ], "unwritten": true, - "windows": "kyv-Zyyy" + "windows": "kyv-Deva" + }, + { + "full": "kyw-Deva-IN", + "iana": [ "Kudmali" ], + "iso639_3": "kyw", + "name": "Kudmali", + "names": [ "Bedia", "Dharua", "Khotta", "Kurmali", "Kurmali Thar", "Kurumali" ], + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "kyw", + "tags": [ "kyw-Deva", "kyw-IN" ], + "windows": "kyw-Deva" }, { "full": "kyw-Beng-IN", @@ -60951,7 +63767,7 @@ "windows": "kyw-Beng" }, { - "full": "kyw-Deva-IN", + "full": "kyw-Orya-IN", "iana": [ "Kudmali" ], "iso639_3": "kyw", "name": "Kudmali", @@ -60959,13 +63775,13 @@ "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Orya", "sldr": false, - "tag": "kyw-Deva", - "windows": "kyw-Deva" + "tag": "kyw-Orya", + "windows": "kyw-Orya" }, { - "full": "kyw-Orya-IN", + "full": "kyw-Zzzz-IN-x-chisoi", "iana": [ "Kudmali" ], "iso639_3": "kyw", "name": "Kudmali", @@ -60973,15 +63789,17 @@ "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Orya", + "script": "Zzzz", "sldr": false, - "tag": "kyw-Orya", - "windows": "kyw-Orya" + "tag": "kyw-Zzzz-x-chisoi", + "tags": [ "kyw-IN-x-chisoi" ], + "windows": "kyw-Zzzz-x-chisoi" }, { "full": "kyx-Latn-PG", "iana": [ "Rapoisi" ], "iso639_3": "kyx", + "localnames": [ "Rapoisi" ], "name": "Rapoisi", "names": [ "Konua", "Kunua" ], "region": "PG", @@ -60996,8 +63814,8 @@ "full": "kyy-Latn-PG", "iana": [ "Kambaira" ], "iso639_3": "kyy", - "name": "Kambaira", - "names": [ "Asa’a" ], + "name": "Asa’a", + "names": [ "Kambaira" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -61055,6 +63873,7 @@ "full": "kzc-Latn-CI", "iana": [ "Bondoukou Kulango" ], "iso639_3": "kzc", + "localnames": [ "Kolango" ], "name": "Kulango, Bondoukou", "names": [ "Bondoukou", "Bondoukou Kulango", "Bonduku", "Kolango", "Koulango", "Kulange", "Kulango", "Nkuraeng", "Nkurange" ], "region": "CI", @@ -61128,6 +63947,7 @@ "full": "kzi-Latn-MY", "iana": [ "Kelabit" ], "iso639_3": "kzi", + "localnames": [ "Kelabit" ], "name": "Kelabit", "names": [ "Apo Duat", "Kalabit", "Kerabit" ], "region": "MY", @@ -61173,7 +63993,7 @@ "iana": [ "Kais" ], "iso639_3": "kzm", "name": "Kais", - "names": [ "Aiso", "Atori", "Kampung Baru", "Mintamani" ], + "names": [ "Aiso", "Atori", "Kampong Baru", "Kampung Baru", "Mintamani" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -61187,8 +64007,9 @@ "full": "kzn-Latn-MW", "iana": [ "Kokola" ], "iso639_3": "kzn", + "localnames": [ "iKokola" ], "name": "Kokola", - "names": [ "Kokhola" ], + "names": [ "Ekokhola", "Kokhola", "iKokola" ], "region": "MW", "regionname": "Malawi", "regions": [ "MZ" ], @@ -61233,7 +64054,7 @@ "iana": [ "Kaike" ], "iso639_3": "kzq", "name": "Kaike", - "names": [ "Tarali Kham" ], + "names": [ "Magar Kaike", "Tarali Kham" ], "region": "NP", "regionname": "Nepal", "script": "Zyyy", @@ -61248,9 +64069,9 @@ "iana": [ "Karang" ], "iso639_3": "kzr", "localname": "Karaŋ", - "localnames": [ "Karaŋ" ], + "localnames": [ "Karaŋ", "Nzak karaŋ" ], "name": "Karang", - "names": [ "Eastern Mbum", "Karaŋ", "Kareng", "Laka", "Lakka", "Lakka Mbum", "Mbum", "Mbum Bakal", "Mbum-East", "Nzák Kàráng" ], + "names": [ "Eastern Mbum", "Karaŋ", "Kareng", "Laka", "Lakka", "Lakka Mbum", "Mbum", "Mbum Bakal", "Mbum-East", "Nzak karaŋ", "Nzák Kàráng" ], "region": "CM", "regionname": "Cameroon", "regions": [ "TD" ], @@ -61280,7 +64101,7 @@ "iana": [ "Kayupulau" ], "iso639_3": "kzu", "name": "Kayupulau", - "names": [ "Kajupulau" ], + "names": [ "Kajupulau", "Kayapulau", "Kayu Pulo" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -61309,7 +64130,7 @@ "iana": [ "Karirí-Xocó" ], "iso639_3": "kzw", "name": "Karirí-Xocó", - "names": [ "Cariri", "Dzubukuá", "Kariri Xucó", "Karirí", "Kipeá", "Kiriri", "Tumbulalá", "Xocó", "Xokó", "Xokó-Karirí", "Xukuru Kariri", "Xukurú" ], + "names": [ "Cariri", "Dzubukuá", "Kariri Xucó", "Kariri-Xoc", "Karirí", "Kiriri", "Quirirí", "Tumbulalá", "Xocó", "Xokó", "Xokó-Karirí", "Xukuru Kariri", "Xukurú" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -61354,7 +64175,7 @@ "iana": [ "Kalabra" ], "iso639_3": "kzz", "name": "Kalabra", - "names": [ "Beraur" ], + "names": [ "Beraur", "Klabra", "Salo Tkoin" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -61416,7 +64237,7 @@ "localname": "Jach-t'aan", "localnames": [ "Jach-t'aan" ], "name": "Lacandon", - "names": [ "Jach-t’aan", "Lacandón", "Lakantún" ], + "names": [ "Hach tʼan", "Jach-t’aan", "Lacandón", "Lakantún", "Maya Lacandón" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -61440,7 +64261,7 @@ "sldr": false, "tag": "lad", "tags": [ "lad-Hebr", "lad-IL" ], - "windows": "lad-Hebr" + "windows": "lad-Hebr-IL" }, { "full": "lad-Cyrl-TR", @@ -61455,7 +64276,7 @@ "script": "Cyrl", "sldr": false, "tag": "lad-Cyrl", - "windows": "lad-Cyrl" + "windows": "lad-Cyrl-TR" }, { "full": "lad-Latn-TR", @@ -61469,7 +64290,7 @@ "script": "Latn", "sldr": false, "tag": "lad-Latn", - "windows": "lad-Latn" + "windows": "lad-Latn-TR" }, { "full": "lae-Deva-IN", @@ -61505,7 +64326,7 @@ "iana": [ "Lafofa" ], "iso639_3": "laf", "name": "Lafofa", - "names": [ "Kidie Lafofa" ], + "names": [ "Kidie", "Kidie Lafofa" ], "region": "SD", "regionname": "Sudan", "script": "Zyyy", @@ -61520,9 +64341,9 @@ "iana": [ "Langi" ], "iso639_3": "lag", "localname": "Kɨlaangi", - "localnames": [ "Kɨlaangi", "Langi" ], - "name": "Langi", - "names": [ "Irangi", "Kelangi", "Kilaangi", "Kilangi", "Kirangi", "Kɨlaangi", "Rangi", "Valaangi" ], + "localnames": [ "Kɨlaangi" ], + "name": "Rangi", + "names": [ "Irangi", "Kelangi", "Kilaangi", "Kilangi", "Kirangi", "Kɨlaangi", "Langi", "Valaangi" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -61537,8 +64358,8 @@ "iso639_3": "lah", "latnnames": [ "Panjābī" ], "localnames": [ "پنجابی" ], - "name": "Punjabi, Western", - "names": [ "Jangli", "Lahanda", "Lahnda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjapi", "Shahmukhi", "Western Panjabi" ], + "name": "Lahnda", + "names": [ "Jangli", "Lahanda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjabi, Western", "Punjapi", "Shahmukhi", "Western Panjabi" ], "region": "PK", "regionname": "Pakistan", "regions": [ "AE", "CA", "GB", "IN", "LY", "US" ], @@ -61546,14 +64367,14 @@ "sldr": false, "tag": "lah", "tags": [ "lah-Arab", "lah-PK", "pnb", "pnb-Arab", "pnb-Arab-PK", "pnb-PK" ], - "windows": "lah-Arab" + "windows": "lah-Arab-PK" }, { "full": "lah-Khoj-PK", "iana": [ "Lahnda" ], "iso639_3": "lah", - "name": "Punjabi, Western", - "names": [ "Jangli", "Lahanda", "Lahnda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjapi", "Shahmukhi", "Western Panjabi" ], + "name": "Lahnda", + "names": [ "Jangli", "Lahanda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjabi, Western", "Punjapi", "Shahmukhi", "Western Panjabi" ], "nophonvars": true, "region": "PK", "regionname": "Pakistan", @@ -61562,14 +64383,14 @@ "sldr": false, "tag": "lah-Khoj", "tags": [ "pnb-Khoj", "pnb-Khoj-PK" ], - "windows": "lah-Khoj" + "windows": "lah-Khoj-PK" }, { "full": "lah-Latn-PK", "iana": [ "Lahnda" ], "iso639_3": "lah", - "name": "Punjabi, Western", - "names": [ "Jangli", "Lahanda", "Lahnda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjapi", "Shahmukhi", "Western Panjabi" ], + "name": "Lahnda", + "names": [ "Jangli", "Lahanda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjabi, Western", "Punjapi", "Shahmukhi", "Western Panjabi" ], "region": "PK", "regionname": "Pakistan", "regions": [ "GB", "IN" ], @@ -61577,11 +64398,13 @@ "sldr": false, "tag": "lah-Latn", "tags": [ "pnb-Latn", "pnb-Latn-PK" ], - "windows": "lah-Latn" + "windows": "lah-Latn-PK" }, { "full": "lah-Mahj-IN", "iana": [ "Lahnda" ], + "iso639_3": "pnb", + "macrolang": "lah", "name": "Punjabi, Western", "names": [ "Lahanda", "Lahnda", "Lahndi", "Western Panjabi" ], "region": "IN", @@ -61595,6 +64418,8 @@ { "full": "lah-Zzzz-PK", "iana": [ "Lahnda" ], + "iso639_3": "pnb", + "macrolang": "lah", "name": "Punjabi, Western", "names": [ "Jangli", "Lahanda", "Lahnda", "Lahndi", "Panjabi", "Panjabi Proper", "Punjabi", "Punjapi", "Shahmukhi", "Western Panjabi" ], "region": "PK", @@ -61612,7 +64437,7 @@ "iso639_3": "lai", "localnames": [ "ChiLambya" ], "name": "Lambya", - "names": [ "Ichilambya", "Icilambya", "Icirambia", "Iramba", "Kilambya", "Lambia", "Lambwa", "Rambia" ], + "names": [ "ChiLambya", "Ichilambya", "Icilambya", "Icirambia", "Iramba", "Kilambya", "Lambia", "Lambwa", "Rambia", "iLyamba" ], "region": "MW", "regionname": "Malawi", "regions": [ "TZ", "ZM" ], @@ -61637,21 +64462,6 @@ "tags": [ "laj-Latn", "laj-UG" ], "windows": "laj-Latn" }, - { - "full": "lak-Latn-NG", - "iana": [ "Laka (Nigeria)" ], - "iso639_3": "lak", - "name": "Laka", - "names": [ "Godogodo", "Laka (Nigeria)", "Lakka", "Lao Habe", "Lau" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "lak", - "tags": [ "lak-Latn", "lak-NG" ], - "unwritten": true, - "windows": "lak-Latn" - }, { "full": "lal-Latn-CD", "iana": [ "Lalia" ], @@ -61702,7 +64512,7 @@ "iana": [ "Laka (Chad)" ], "iso639_3": "lap", "name": "Laka", - "names": [ "Kabba Laka", "Laka (Chad)" ], + "names": [ "Kabba Laka", "Laga", "Laka (Chad)", "Làgà" ], "region": "TD", "regionname": "Chad", "regions": [ "CF" ], @@ -61749,7 +64559,7 @@ "localname": "Lama", "localnames": [ "Lama" ], "name": "Lama", - "names": [ "Lama (Togo)", "Lamba", "Losso" ], + "names": [ "Lama (Togo)", "Lamba", "Lamʋ", "Losso" ], "region": "TG", "regionname": "Togo", "regions": [ "BJ", "GH" ], @@ -61789,38 +64599,39 @@ "windows": "law-Latn" }, { - "full": "lax-Beng-IN", + "full": "lax-Latn-IN", "iana": [ "Tiwa" ], "iso639_3": "lax", "name": "Tiwa", "names": [ "Dowyan", "Lalung" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "lax-Beng", - "windows": "lax-Beng" + "tag": "lax", + "tags": [ "lax-IN", "lax-Latn" ], + "windows": "lax-Latn" }, { - "full": "lax-Latn-IN", + "full": "lax-Beng-IN", "iana": [ "Tiwa" ], "iso639_3": "lax", "name": "Tiwa", "names": [ "Dowyan", "Lalung" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "lax-Latn", - "windows": "lax-Latn" + "tag": "lax-Beng", + "windows": "lax-Beng" }, { "full": "lay-Zyyy-CN", "iana": [ "Lama Bai" ], "iso639_3": "lay", "name": "Bai, Lama", - "names": [ "Lama", "Lama Bai", "Lan-Bi Bai", "Nama", "Northern Bai" ], + "names": [ "Lama", "Lama Bai", "Lan-Bi Bai", "Lanping", "Nama", "Northern Bai" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -61834,7 +64645,7 @@ "iana": [ "Aribwatsa" ], "iso639_3": "laz", "name": "Aribwatsa", - "names": [ "Lae", "Lahe" ], + "names": [ "Aribuacha", "Lae", "Lahe" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -61851,10 +64662,10 @@ "localname": "Lëtzebuergesch", "localnames": [ "Lëtzebuergesch" ], "name": "Luxembourgish", - "names": [ "Frankish", "Letzburgisch", "Letzeburgesch", "Luxembourgeois", "Luxemburgian", "Luxemburgish", "Lëtzebuergesch", "Moselle Franconian", "Platt" ], + "names": [ "Frankish", "Letzburgisch", "Letzeburgesch", "Letzeburgisch", "Limba săsească", "Luxembourgeois", "Luxemburgian", "Luxemburgish", "Lëtzebuergesch", "Moselle Franconian", "Platt", "Siweberjesch Såksesch", "Transylvanian Saxon" ], "region": "LU", "regionname": "Luxembourg", - "regions": [ "BE", "DE", "FR", "US" ], + "regions": [ "BE", "DE", "FR", "RO", "US" ], "script": "Latn", "sldr": true, "suppress": true, @@ -61879,6 +64690,7 @@ "full": "lbb-Latn-PG", "iana": [ "Label" ], "iso639_3": "lbb", + "localnames": [ "Label" ], "name": "Label", "region": "PG", "regionname": "Papua New Guinea", @@ -61920,21 +64732,51 @@ "tags": [ "lbe-Cyrl", "lbe-RU" ], "windows": "lbe-Cyrl" }, + { + "full": "lbe-Arab-RU", + "iana": [ "Lak" ], + "iso639_3": "lbe", + "name": "Lak", + "names": [ "Kazikumukhtsy", "Lakh", "Laki" ], + "nophonvars": true, + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "AZ", "KG", "KZ", "TJ", "TM", "UA", "UZ" ], + "script": "Arab", + "sldr": false, + "tag": "lbe-Arab", + "windows": "lbe-Arab" + }, + { + "full": "lbe-Latn-RU", + "iana": [ "Lak" ], + "iso639_3": "lbe", + "name": "Lak", + "names": [ "Kazikumukhtsy", "Lakh", "Laki" ], + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "AZ", "KG", "KZ", "TJ", "TM", "UA", "UZ" ], + "script": "Latn", + "sldr": false, + "tag": "lbe-Latn", + "windows": "lbe-Latn" + }, { "full": "lbf-Deva-IN", "iana": [ "Tinani" ], "iso639_3": "lbf", "name": "Tinani", "names": [ "Bhotia of Lahul", "Gondhla", "Gondla", "Lahauli", "Lahouli", "Lahuli", "Lahuli Tinan", "Rangloi", "Teenan", "Tinan Lahuli" ], - "nophonvars": true, "region": "IN", "regionname": "India", "regions": [ "CN" ], "script": "Deva", "sldr": false, - "tag": "lbf-Deva", - "tags": [ "lbf-IN" ], - "windows": "lbf-Deva" + "tag": "lbf", + "tags": [ "lbf-Deva", "lbf-IN" ], + "windows": "lbf-Deva-IN" }, { "full": "lbf-Tibt-CN", @@ -61950,7 +64792,7 @@ "sldr": false, "tag": "lbf-Tibt", "tags": [ "lbf-CN" ], - "windows": "lbf-Tibt" + "windows": "lbf-Tibt-CN" }, { "full": "lbg-Zyyy-LA", @@ -61986,7 +64828,7 @@ "iana": [ "Ladakhi" ], "iso639_3": "lbj", "latnnames": [ "Ladaks skat" ], - "localname": "ladakhspa", + "localname": "ལ་དྭགས་ཀྱི་སྐད།", "localnames": [ "ལ་དྭགས་ཀྱི་སྐད།" ], "name": "Ladakhi", "names": [ "Ladak", "Ladakh", "Ladakh Skat", "Ladaphi", "Ladhakhi", "Ladwags" ], @@ -61999,10 +64841,25 @@ "tags": [ "lbj-IN", "lbj-Tibt" ], "windows": "lbj-Tibt" }, + { + "full": "lbj-Arab-IN", + "iana": [ "Ladakhi" ], + "iso639_3": "lbj", + "name": "Ladakhi", + "names": [ "Ladak", "Ladakh", "Ladakh Skat", "Ladaphi", "Ladhakhi", "Ladwags" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Arab", + "sldr": false, + "tag": "lbj-Arab", + "windows": "lbj-Arab" + }, { "full": "lbl-Latn-PH", "iana": [ "Libon Bikol" ], "iso639_3": "lbl", + "macrolang": "bik", "name": "Bikol, Libon", "names": [ "Libon Bikol", "Libongeño" ], "region": "PH", @@ -62029,37 +64886,38 @@ "windows": "lbm-Deva" }, { - "full": "lbn-Laoo-LA", + "full": "lbn-Latn-LA", "iana": [ "Rmeet" ], "iso639_3": "lbn", + "localnames": [ "Rmeet" ], "name": "Rmeet", "names": [ "Kha Lamet", "Khamed", "Khamet", "Lamed", "Lamet", "Lemet" ], - "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", "regions": [ "US" ], - "script": "Laoo", + "script": "Latn", "sldr": false, - "tag": "lbn-Laoo", - "windows": "lbn-Laoo" + "tag": "lbn", + "tags": [ "lbn-LA", "lbn-Latn" ], + "windows": "lbn-Latn" }, { - "full": "lbn-Latn-LA", + "full": "lbn-Laoo-LA", "iana": [ "Rmeet" ], "iso639_3": "lbn", - "localnames": [ "Rmeet" ], "name": "Rmeet", "names": [ "Kha Lamet", "Khamed", "Khamet", "Lamed", "Lamet", "Lemet" ], + "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", "regions": [ "US" ], - "script": "Latn", + "script": "Laoo", "sldr": false, - "tag": "lbn-Latn", - "windows": "lbn-Latn" + "tag": "lbn-Laoo", + "windows": "lbn-Laoo" }, { - "full": "lbo-Zyyy-LA", + "full": "lbo-Laoo-LA", "iana": [ "Laven" ], "iso639_3": "lbo", "name": "Laven", @@ -62067,19 +64925,35 @@ "region": "LA", "regionname": "Lao People's Democratic Republic", "regions": [ "US" ], - "script": "Zyyy", + "script": "Laoo", "sldr": false, "tag": "lbo", - "tags": [ "lbo-LA", "lbo-Zyyy" ], + "tags": [ "lbo-LA", "lbo-Laoo" ], "unwritten": true, - "windows": "lbo-Zyyy" + "windows": "lbo-Laoo-LA" + }, + { + "full": "lbo-Latn-US", + "iana": [ "Laven" ], + "iso639_3": "lbo", + "name": "Laven", + "names": [ "Boloven", "Boriwen", "Jaru", "Jru", "Jruq", "Jru’", "Laweenjru", "Loven", "Yrou" ], + "region": "US", + "regionname": "United States", + "regions": [ "LA" ], + "script": "Latn", + "sldr": false, + "tag": "lbo-Latn", + "tags": [ "lbo-US" ], + "windows": "lbo-Latn-US" }, { "full": "lbq-Latn-PG", "iana": [ "Wampar" ], "iso639_3": "lbq", + "localnames": [ "Dzob Wampar", "Wampar" ], "name": "Wampar", - "names": [ "Laewamba", "Laewomba", "Laiwomba" ], + "names": [ "Dzob Wampar", "Laewamba", "Laewomba", "Laiwomba", "Ngaeng Wampar" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -62157,7 +65031,7 @@ "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "lbv", "tags": [ "lbv-Latn", "lbv-PG" ], "unwritten": true, @@ -62213,7 +65087,7 @@ "iana": [ "Lardil" ], "iso639_3": "lbz", "name": "Lardil", - "names": [ "Kunana", "Ladil", "Laierdila", "Lardill" ], + "names": [ "Damin", "Demin", "Gunana", "Kare wa", "Kunana", "Kuna’na", "La:dil", "Ladil", "Laierdila", "Lardi:i", "Lardiil", "Lardill", "Layardilda", "Leedil", "Leerdil", "Mornington Island language", "Nemarang" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -62243,6 +65117,7 @@ "iana": [ "Lola" ], "iso639_3": "lcd", "name": "Lola", + "names": [ "Aru" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -62256,6 +65131,7 @@ "full": "lce-Latn-ID", "iana": [ "Loncong", "Sekak" ], "iso639_3": "lce", + "macrolang": "ms", "name": "Sekak", "names": [ "Lonchong", "Loncong", "Lontjong", "Orang Laut", "Orang Sawang", "Sawang", "Seka", "Sekah" ], "region": "ID", @@ -62271,7 +65147,9 @@ "full": "lcf-Latn-ID", "iana": [ "Lubu" ], "iso639_3": "lcf", + "macrolang": "ms", "name": "Lubu", + "names": [ "Siladang" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -62316,6 +65194,8 @@ "full": "lcm-Latn-PG", "iana": [ "Tungag" ], "iso639_3": "lcm", + "localname": "Tungag", + "localnames": [ "Tungag" ], "name": "Tungag", "names": [ "Dang", "Lavangai", "Lavongai", "Toangai", "Tungak" ], "region": "PG", @@ -62330,8 +65210,10 @@ "full": "lcp-Thai-CN", "iana": [ "Western Lawa" ], "iso639_3": "lcp", + "latnnames": [ "Lawa" ], + "localnames": [ "ละเวือะ" ], "name": "Lawa, Western", - "names": [ "Lava", "Lavua", "Lavüa", "L’wa", "Mae Hong Son Lawa", "Mountain Lawa", "Omphai Lawa", "Western Lawa" ], + "names": [ "Lava", "Lavua", "Lavüa", "L’wa", "Mae Hong Son Lawa", "Mountain Lawa", "Western Lawa" ], "region": "CN", "regionname": "China", "regions": [ "TH" ], @@ -62391,9 +65273,10 @@ "full": "ldb-Latn-NG", "iana": [ "Dũya" ], "iso639_3": "ldb", + "localname": "Idṹ", "localnames": [ "Idṹ" ], "name": "Duya", - "names": [ "Adong", "Adṹ", "Dũya", "Idun", "Idṹ", "Jaba Duya", "Jaba Lunga", "Lungu", "Ungu" ], + "names": [ "Adong", "Adṹ", "Duhnya", "Dũya", "Idun", "Idṹ", "Jaba Duya", "Jaba Lunga", "Lungu", "Ungu" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -62407,7 +65290,7 @@ "iana": [ "Luri" ], "iso639_3": "ldd", "name": "Luri", - "names": [ "Lúr" ], + "names": [ "Lur", "Lurí", "Lúr" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -62450,6 +65333,7 @@ "full": "ldi-Latn-CG", "iana": [ "Laari" ], "iso639_3": "ldi", + "macrolang": "kg", "name": "Laari", "names": [ "Kilari", "Laadi", "Ladi", "Lali", "Lari" ], "region": "CG", @@ -62511,7 +65395,7 @@ "iana": [ "Landoma" ], "iso639_3": "ldm", "name": "Landoma", - "names": [ "Cocoli", "Landouman", "Landuma", "Tiapi", "Tyapi", "Tyopi" ], + "names": [ "Cocoli", "Landouman", "Landuma", "Tiapi", "Tyapi", "Tyop", "Tyopi" ], "region": "GN", "regionname": "Guinea", "script": "Latn", @@ -62521,17 +65405,17 @@ "windows": "ldm-Latn" }, { - "full": "ldn-Zyyy-001", + "full": "ldn-Latn-001", "iana": [ "Láadan" ], "iso639_3": "ldn", "name": "Láadan", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ldn", - "tags": [ "ldn-001", "ldn-Zyyy" ], - "windows": "ldn-Zyyy" + "tags": [ "ldn-001", "ldn-Latn" ], + "windows": "ldn-Latn" }, { "full": "ldo-Latn-NG", @@ -62553,7 +65437,7 @@ "iana": [ "Tso" ], "iso639_3": "ldp", "name": "Tso", - "names": [ "Cibbo", "Cuyi Tsó", "Kitta", "Lotsu-Piri", "Pire", "Piri", "Tsóbó" ], + "names": [ "Cibbo", "Cuyi Tsó", "Fire", "Kitta", "Lotsu-Piri", "Pire", "Piri", "Tsóbó", "nyi Tsó" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -62568,6 +65452,7 @@ "iana": [ "Lufu" ], "iso639_3": "ldq", "name": "Lufu", + "names": [ "Lufueg" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -62582,7 +65467,7 @@ "iana": [ "Lega-Shabunda" ], "iso639_3": "lea", "name": "Lega-Shabunda", - "names": [ "Igonzabale", "Ileka-Igonzabale", "Kilega", "Kirega", "Lega", "Lega-Malinga", "Leka-Igonzabale", "Rega" ], + "names": [ "Igonzabale", "Ileka-Igonzabale", "Kilega", "Kirega", "Lega", "Lega Shubanda", "Lega-Malinga", "Leka-Igonzabale", "Rega", "Shabunda Lega" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -62626,6 +65511,8 @@ "full": "led-Latn-CD", "iana": [ "Lendu" ], "iso639_3": "led", + "localname": "Bbaledha", + "localnames": [ "Bbaledha" ], "name": "Lendu", "names": [ "Bale", "Baledha", "Balendru", "Baletha", "Batha", "Bbadha", "Bbaledha", "Hema-Nord", "Kihema-Nord", "Kilendu" ], "region": "CD", @@ -62657,8 +65544,10 @@ "full": "lef-Latn-GH", "iana": [ "Lelemi" ], "iso639_3": "lef", + "localname": "Lɛlɛmi", + "localnames": [ "Lɛlɛmi" ], "name": "Lelemi", - "names": [ "Buem", "Lafana", "Lefana" ], + "names": [ "Buem", "Lafana", "Lefana", "Lɛfana", "Lɛlɛmi" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -62731,7 +65620,7 @@ "iana": [ "Lele (Democratic Republic of Congo)" ], "iso639_3": "lel", "name": "Lele", - "names": [ "Bashilele", "Lele (Democratic Republic of Congo)", "Usilele" ], + "names": [ "Bashilele", "Kishilele", "Lele (Democratic Republic of Congo)", "Usilele" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -62748,7 +65637,7 @@ "localname": "Nɔmaándɛ́", "localnames": [ "Nɔmaándɛ́" ], "name": "Nomaande", - "names": [ "Lemande", "Mande", "Mandi", "Noomaante", "Numaand", "Numand", "Nɔmaándɛ́", "Nɔmaántɛ́", "Pimenc" ], + "names": [ "Lemande", "Maande", "Mande", "Mandi", "Noomaante", "Numaand", "Numand", "Nɔmaándɛ́", "Nɔmaántɛ́", "Pimenc" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -62762,6 +65651,7 @@ "iana": [ "Lenca" ], "iso639_3": "len", "name": "Lenca", + "names": [ "Honduran Lenca", "Lenka" ], "region": "HN", "regionname": "Honduras", "script": "Latn", @@ -62939,7 +65829,7 @@ "iana": [ "Lemolang" ], "iso639_3": "ley", "name": "Limola", - "names": [ "Baebunta", "Lemolang", "Limolang" ], + "names": [ "Baebunta", "Lemolang", "Lemolango", "Limolang" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -62964,7 +65854,7 @@ "sldr": false, "tag": "lez", "tags": [ "lez-Cyrl", "lez-RU" ], - "windows": "lez-Cyrl" + "windows": "lez-Cyrl-RU" }, { "full": "lez-Aghb-AZ", @@ -62980,14 +65870,15 @@ "sldr": false, "tag": "lez-Aghb", "tags": [ "lez-AZ" ], - "windows": "lez-Aghb" + "windows": "lez-Aghb-AZ" }, { "full": "lfa-Latn-CM", "iana": [ "Lefa" ], "iso639_3": "lfa", + "localnames": [ "Lɨfa’" ], "name": "Lefa", - "names": [ "Balom", "Fak", "Fa’", "Lefa’" ], + "names": [ "Balom", "Fak", "Fa’", "Lefa’", "Lɨfa’" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -63086,10 +65977,10 @@ "full": "lgg-Latn-UG", "iana": [ "Lugbara" ], "iso639_3": "lgg", - "localname": "Lugbarati", - "localnames": [ "Lugbarati" ], + "localname": "Lʉgbara tɨ", + "localnames": [ "Lʉgbara tɨ" ], "name": "Lugbara", - "names": [ "High Lugbara", "Logbara", "Lubarati", "Lugbarati", "Western Lugbara" ], + "names": [ "High Lugbara", "Logbara", "Lubarati", "Lugbarati", "Lʉgbara tɨ", "Western Lugbara" ], "region": "UG", "regionname": "Uganda", "regions": [ "CD" ], @@ -63104,7 +65995,7 @@ "iana": [ "Lugbara" ], "iso639_3": "lgg", "name": "Lugbara", - "names": [ "High Lugbara", "Logbara", "Lubarati", "Lugbarati", "Western Lugbara" ], + "names": [ "High Lugbara", "Logbara", "Lubarati", "Lugbarati", "Lʉgbara tɨ", "Western Lugbara" ], "nophonvars": true, "region": "UG", "regionname": "Uganda", @@ -63119,7 +66010,7 @@ "iana": [ "Laghuu" ], "iso639_3": "lgh", "name": "Laghuu", - "names": [ "Laopa", "Xá Phó" ], + "names": [ "Laopa", "Laopán", "Lolo", "Xá Phó", "Yi" ], "region": "VN", "regionname": "Viet Nam", "script": "Latn", @@ -63134,6 +66025,7 @@ "iana": [ "Lengilu" ], "iso639_3": "lgi", "name": "Lengilu", + "names": [ "Lengilo" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -63179,7 +66071,7 @@ "iana": [ "Lega-Mwenga" ], "iso639_3": "lgm", "name": "Lega-Mwenga", - "names": [ "Ileka Ishile", "Ishile", "Isile", "Kilega", "Kirega", "Lega-Ntara", "Leka-Shile", "Leka-Sile", "Rega", "Shile" ], + "names": [ "Ileka Ishile", "Ishile", "Isile", "Kilega", "Kirega", "Lega-Ntara", "Leka-Shile", "Leka-Sile", "Mwenga Lega", "Rega", "Shile" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -63192,8 +66084,8 @@ "full": "lgn-Latn-ET", "iana": [ "T'apo", "Opuuo" ], "iso639_3": "lgn", - "localnames": [ "T'apo" ], - "name": "T’apo", + "localnames": [ "T'apo", "T'azita" ], + "name": "Opo", "names": [ "Pur", "Shitta", "T'apo" ], "region": "ET", "regionname": "Ethiopia", @@ -63204,10 +66096,26 @@ "tags": [ "lgn-ET", "lgn-Latn" ], "windows": "lgn-Latn" }, + { + "full": "lgo-Latn-SS", + "iana": [ "Lango (South Sudan)" ], + "iso639_3": "lgo", + "localnames": [ "Lango" ], + "name": "Lango", + "names": [ "Langgo", "Lango (South Sudan)", "Lokwa" ], + "region": "SS", + "regionname": "South Sudan", + "script": "Latn", + "sldr": false, + "tag": "lgo", + "tags": [ "lgo-Latn", "lgo-SS", "lno", "lno-Latn", "lno-Latn-SS", "lno-SS" ], + "windows": "lgo-Latn" + }, { "full": "lgq-Latn-GH", "iana": [ "Logba" ], "iso639_3": "lgq", + "localnames": [ "Ikpana" ], "name": "Logba", "names": [ "Ikpana" ], "region": "GH", @@ -63216,7 +66124,6 @@ "sldr": false, "tag": "lgq", "tags": [ "lgq-GH", "lgq-Latn" ], - "unwritten": true, "windows": "lgq-Latn" }, { @@ -63224,7 +66131,7 @@ "iana": [ "Lengo" ], "iso639_3": "lgr", "name": "Lengo", - "names": [ "Doku", "Ruavatu", "Tadhimboko" ], + "names": [ "Doku", "Ruavatu", "Tadhimboko", "Tasemboko" ], "region": "SB", "regionname": "Solomon Islands", "script": "Latn", @@ -63233,12 +66140,25 @@ "tags": [ "lgr-Latn", "lgr-SB" ], "windows": "lgr-Latn" }, + { + "full": "lgs-Zxxx-GW", + "iso639_3": "lgs", + "name": "Guinea-Bissau Sign Language", + "names": [ "LGG", "Língua Gestual Guineense" ], + "region": "GW", + "regionname": "Guinea-Bissau", + "script": "Zxxx", + "sldr": false, + "tag": "lgs", + "tags": [ "lgs-GW", "lgs-Zxxx" ], + "windows": "lgs-Zxxx" + }, { "full": "lgt-Latn-PG", "iana": [ "Pahi" ], "iso639_3": "lgt", "name": "Pahi", - "names": [ "Lugitama", "Riahoma", "Wansum" ], + "names": [ "Lugitama", "Riahoma", "Riahorna", "Wansum" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -63260,7 +66180,6 @@ "sldr": false, "tag": "lgu", "tags": [ "lgu-Latn", "lgu-SB" ], - "unwritten": true, "windows": "lgu-Latn" }, { @@ -63312,7 +66231,7 @@ "full": "lhi-Latn-CN", "iana": [ "Lahu Shi" ], "iso639_3": "lhi", - "localname": "Lahu Si", + "localname": "Lad hur Si", "localnames": [ "Lad hur Si" ], "name": "Lahu Shi", "names": [ "Kui", "Kui Lung", "Kui Sang", "Kur", "Kwi", "Lad hur Si", "Lahu Si", "Lahu Xi", "Lahu-Xi", "Lahu-shi", "Lahusi", "Shi", "Yellow Lahu" ], @@ -63376,7 +66295,7 @@ "iana": [ "Lhokpu" ], "iso639_3": "lhp", "name": "Lhokpu", - "names": [ "Lhobikha", "Taba-Damey-Bikha" ], + "names": [ "Lhobikha", "Lokpu", "Taba-Damey-Bikha" ], "region": "BT", "regionname": "Bhutan", "script": "Zyyy", @@ -63391,7 +66310,7 @@ "iana": [ "Mlahsö" ], "iso639_3": "lhs", "name": "Mlahsö", - "names": [ "Suryoyo" ], + "names": [ "Melasó", "Mlahso", "Mlaḥso", "Mlaḥsô", "Suryoyo" ], "region": "SY", "regionname": "Syrian Arab Republic", "script": "Syrc", @@ -63525,6 +66444,9 @@ "full": "lif-Deva-NP", "iana": [ "Limbu" ], "iso639_3": "lif", + "latnnames": [ "Yakthungpan" ], + "localname": "यक्थुङ्पन", + "localnames": [ "यक्थुङ्पन" ], "name": "Limbu", "names": [ "Limbo", "Limboo", "Lumbu", "Yakthung Pan", "Yakthungpan" ], "region": "NP", @@ -63534,7 +66456,7 @@ "sldr": true, "tag": "lif", "tags": [ "lif-Deva", "lif-NP" ], - "windows": "lif-Deva" + "windows": "lif-Deva-NP" }, { "full": "lif-Limb-IN", @@ -63550,7 +66472,41 @@ "sldr": true, "tag": "lif-Limb", "tags": [ "lif-IN" ], - "windows": "lif-Limb" + "windows": "lif-Limb-IN" + }, + { + "full": "lif-Deva-NP-x-chathare", + "iana": [ "Limbu" ], + "iso639_3": "lif", + "latnnames": [ "Yakthungpan" ], + "localnames": [ "यक्थुङ्पन" ], + "name": "Limbu", + "names": [ "Yakthung Pan", "Yakthungpan" ], + "region": "NP", + "regionname": "Nepal", + "rod": "12960", + "script": "Deva", + "sldr": false, + "tag": "lif-x-chathare", + "tags": [ "lif-Deva-x-chathare", "lif-NP-x-chathare" ], + "windows": "lif-Deva-NP-x-chathare" + }, + { + "full": "lif-Deva-NP-x-phedappe", + "iana": [ "Limbu" ], + "iso639_3": "lif", + "latnnames": [ "Yakthungpan" ], + "localnames": [ "यक्थुङ्पन" ], + "name": "Limbu", + "names": [ "Yakthung Pan", "Yakthungpan" ], + "region": "NP", + "regionname": "Nepal", + "rod": "12962", + "script": "Deva", + "sldr": false, + "tag": "lif-x-phedappe", + "tags": [ "lif-Deva-x-phedappe", "lif-NP-x-phedappe" ], + "windows": "lif-Deva-NP-x-phedappe" }, { "full": "lig-Latn-GH", @@ -63590,7 +66546,7 @@ "iso639_3": "lij", "localnames": [ "Ligure" ], "name": "Ligurian", - "names": [ "Ligure", "Monégasque", "Munegascu", "Zenéize" ], + "names": [ "Ligure", "Monégasque", "Munegascu", "Zeneize" ], "region": "IT", "regionname": "Italy", "regions": [ "FR", "MC" ], @@ -63695,7 +66651,7 @@ "iana": [ "Liberian English" ], "iso639_3": "lir", "name": "Liberian English", - "names": [ "Kreyol", "Liberian Pidgin English" ], + "names": [ "Colocwa", "Kolokwa", "Koloqua", "Kru Pidgin English", "Liberian Kolokwa English", "Liberian Kreyol", "Liberian Pidgin English" ], "region": "LR", "regionname": "Liberia", "script": "Latn", @@ -63709,6 +66665,9 @@ "full": "lis-Lisu-CN", "iana": [ "Lisu" ], "iso639_3": "lis", + "latnnames": [ "Li-su" ], + "localname": "ꓡꓲ-ꓢꓴ", + "localnames": [ "ꓡꓲ-ꓢꓴ" ], "name": "Lisu", "names": [ "Central Lisu", "Chedi", "Cheli", "Chung", "Khae", "Leisu", "Leshuoopa", "Lesuo", "Li", "Li-Hsaw", "Li-Shaw", "Lip’a", "Lisaw", "Lishu", "Liso", "Lissu", "Loisu", "Lu-Tzu", "Lusu", "Southern Lisu", "Yao Yen", "Yaw Yin", "Yaw-Yen", "Yeh-Jeh", "Yeh-Jen" ], "region": "CN", @@ -63724,8 +66683,7 @@ "full": "lis-Latn-CN", "iana": [ "Lisu" ], "iso639_3": "lis", - "latnnames": [ "Li-su", "" ], - "localnames": [ "LI-SU", "Lisu" ], + "localnames": [ "Lisu" ], "name": "Lisu", "names": [ "Central Lisu", "Chedi", "Cheli", "Chung", "Khae", "Leisu", "Leshuoopa", "Lesuo", "Li", "Li-Hsaw", "Li-Shaw", "Lip’a", "Lisaw", "Lishu", "Liso", "Lissu", "Loisu", "Lu-Tzu", "Lusu", "Southern Lisu", "Yao Yen", "Yaw Yin", "Yaw-Yen", "Yeh-Jeh", "Yeh-Jen" ], "region": "CN", @@ -63741,7 +66699,7 @@ "iana": [ "Logorik" ], "iso639_3": "liu", "name": "Logorik", - "names": [ "Liguri" ], + "names": [ "Laggori", "Liguri", "Logori" ], "region": "SD", "regionname": "Sudan", "script": "Latn", @@ -63754,9 +66712,9 @@ "full": "liv-Latn-LV", "iana": [ "Liv" ], "iso639_3": "liv", - "localnames": [ "Livõ kel" ], + "localnames": [ "Livõ kēļ", "Rāndakēļ" ], "name": "Liv", - "names": [ "Livonian", "Livõ kel" ], + "names": [ "Livonian", "Livõ kēļ", "Rāndakēļ" ], "region": "LV", "regionname": "Latvia", "script": "Latn", @@ -63769,6 +66727,7 @@ "full": "liw-Latn-ID", "iana": [ "Col" ], "iso639_3": "liw", + "macrolang": "ms", "name": "Col", "names": [ "Cul", "Sindang" ], "region": "ID", @@ -63785,6 +66744,7 @@ "iana": [ "Liabuku" ], "iso639_3": "lix", "name": "Liabuku", + "names": [ "Liabuka" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -63798,9 +66758,9 @@ "full": "liy-Latn-CF", "iana": [ "Banda-Bambari" ], "iso639_3": "liy", - "localnames": [ "Linda" ], + "localnames": [ "Banda" ], "name": "Banda-Bambari", - "names": [ "Banda of Bambari", "Banda-Linda", "Linda" ], + "names": [ "Banda", "Banda of Bambari", "Banda-Linda", "Linda" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -63829,7 +66789,7 @@ "iana": [ "Golpa" ], "iso639_3": "lja", "name": "Golpa", - "names": [ "Golba", "Golbu", "Gorlba", "Gorlpa", "Kolpa" ], + "names": [ "Girrkirr", "Golba", "Golbu", "Gorlba", "Gorlbu", "Gorlpa", "Guba", "Gulba", "Kolpa", "Korlpa", "Miliwuru", "Nango" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -63916,7 +66876,7 @@ "iana": [ "Yirandali" ], "iso639_3": "ljw", "name": "Yirandali", - "names": [ "Dal-leyburra", "Dalebura", "Dalleburra", "Irendely", "Jirandali", "Pooroga", "Yerrundulli", "Yerrunthully", "Yirandhali" ], + "names": [ "Dal leyburra", "Dal-leyburra", "Dalebura", "Dalleburra", "Dalleyburra", "Irendely", "Jirandali", "Pooroga", "Yerrundulli", "Yerrunthully", "Yirandhali", "Yirendali" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -63959,6 +66919,7 @@ "iana": [ "Kabras", "Lukabaras" ], "iso639_3": "lkb", "localnames": [ "Lukabaras" ], + "macrolang": "luy", "name": "Lukabaras", "names": [ "Kabarasi", "Kabras" ], "region": "KE", @@ -63990,6 +66951,7 @@ "iana": [ "Lakondê" ], "iso639_3": "lkd", "name": "Lakondê", + "names": [ "Laconde", "Lacondê", "Lakonde" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -64015,19 +66977,19 @@ "windows": "lke-Latn" }, { - "full": "lkh-Zyyy-BT", + "full": "lkh-Tibt-BT", "iana": [ "Lakha" ], "iso639_3": "lkh", "name": "Lakha", "names": [ "Tshangkha" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "lkh", - "tags": [ "lkh-BT", "lkh-Zyyy" ], + "tags": [ "lkh-BT", "lkh-Tibt" ], "unwritten": true, - "windows": "lkh-Zyyy" + "windows": "lkh-Tibt" }, { "full": "lki-Arab-IR", @@ -64063,7 +67025,7 @@ "iana": [ "Laeko-Libuat" ], "iso639_3": "lkl", "name": "Laeko-Libuat", - "names": [ "Laeko", "Laeko-Limbuat" ], + "names": [ "Laeko", "Laeko-Limbuat", "Waitalu", "Watalu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -64108,8 +67070,9 @@ "iana": [ "Khayo", "Olukhayo" ], "iso639_3": "lko", "localnames": [ "Olukhayo" ], + "macrolang": "luy", "name": "Olukhayo", - "names": [ "Khayo", "Xaayo" ], + "names": [ "Bakhayo", "Khayo", "Xaayo" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -64124,7 +67087,7 @@ "iso639_3": "lkr", "localnames": [ "Dhi-Pari" ], "name": "Päri", - "names": [ "Dhi-Pari", "Lokoro", "Pari" ], + "names": [ "Beri", "Berri", "Dhi-Pari", "Jo-Pari", "Ojwan-Boii", "Pari" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -64138,8 +67101,9 @@ "iana": [ "Kisa", "Olushisa" ], "iso639_3": "lks", "localnames": [ "Olushisa" ], + "macrolang": "luy", "name": "Olushisa", - "names": [ "Kisa" ], + "names": [ "Kisa", "Lushisa", "Shisa" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -64170,7 +67134,7 @@ "iana": [ "Kungkari" ], "iso639_3": "lku", "name": "Kungkari", - "names": [ "Koongerri", "Koonkerri", "Kungeri", "Kunggari", "Mokaburra", "Tarawalla", "Torra-burri", "Yangeeberra", "Yankibura" ], + "names": [ "Gungari", "Gunggari", "Guunggari", "Koongerri", "Koonkerri", "Kungeri", "Kunggari", "Kuungkari", "Mokaburra", "Tarawalla", "Torra-burri", "Yangeberra", "Yangeeberra", "Yankibura" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -64185,7 +67149,7 @@ "iana": [ "Lokoya" ], "iso639_3": "lky", "name": "Lokoya", - "names": [ "Ellyria", "Koyo", "Loirya", "Lokoiya", "Lokoja", "Loquia", "Lowoi", "Oirya", "Owoi", "Oxoriok" ], + "names": [ "Ellyria", "Koyo", "Loirya", "Lokoiya", "Lokoja", "Loquia", "Lowoi", "Oirya", "Owoi", "Oxoriok", "Oxoryok" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -64200,7 +67164,7 @@ "iana": [ "Lala-Roba" ], "iso639_3": "lla", "name": "Lala-Roba", - "names": [ "Gworam" ], + "names": [ "Gworam", "Ɓәna" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -64213,8 +67177,9 @@ "full": "llb-Latn-MZ", "iana": [ "Lolo" ], "iso639_3": "llb", + "localnames": [ "iLolo" ], "name": "Lolo", - "names": [ "Ilolo" ], + "names": [ "iLolo" ], "region": "MZ", "regionname": "Mozambique", "script": "Latn", @@ -64285,8 +67250,9 @@ "full": "llg-Latn-ID", "iana": [ "Lole" ], "iso639_3": "llg", + "localnames": [ "Rote Lole" ], "name": "Lole", - "names": [ "Baä", "Ba’a", "Central Rote", "Loleh", "Rote", "Rote Tengah", "Roti", "Rotinese" ], + "names": [ "Central Rote", "Loleh", "Rote", "Rote Lole", "Rote Tengah", "Roti", "Rotinese" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -64314,7 +67280,7 @@ "iana": [ "Teke-Laali" ], "iso639_3": "lli", "name": "Teke-Laali", - "names": [ "Ilaali", "Laali", "West Teke" ], + "names": [ "Ilaali", "Ilaalɛ", "Laali", "West Teke" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -64401,8 +67367,9 @@ "full": "llp-Latn-VU", "iana": [ "North Efate" ], "iso639_3": "llp", + "localnames": [ "Nakanamanga" ], "name": "Efate, North", - "names": [ "Nakanamanga", "North Efate" ], + "names": [ "Nakanamanga", "Nguna", "North Efate" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -64430,7 +67397,7 @@ "iana": [ "Lithuanian Sign Language" ], "iso639_3": "lls", "name": "Lithuanian Sign Language", - "names": [ "LGK", "Lietuvių gestų kalba", "LtSL" ], + "names": [ "LGK", "Lietuviu gestu kalba", "Lietuvių gestų kalba", "LtSL" ], "region": "LT", "regionname": "Lithuania", "script": "Zxxx", @@ -64448,7 +67415,7 @@ "region": "SB", "regionname": "Solomon Islands", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "llu", "tags": [ "llu-Latn", "llu-SB" ], "windows": "llu-Latn" @@ -64503,7 +67470,7 @@ "iana": [ "Limilngan" ], "iso639_3": "lmc", "name": "Limilngan", - "names": [ "Manadja", "Manidja", "Minitji" ], + "names": [ "Baneidja", "Bani:dja", "Banidja", "Banindja", "Beriguruk", "Buneidja", "Buneidya", "Buneitja", "Lemil", "Limil", "Limit", "Manadja", "Manasi", "Manidja", "Manidya", "Manitja", "Menassie", "Minidja", "Minitja", "Minitjaa", "Minitji", "Minitjja", "Minityi", "Minnitji", "Norweilemil", "Peneitja", "Punaka", "Puneitja", "Punuurlu", "naWinjmil" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -64523,7 +67490,7 @@ "region": "SD", "regionname": "Sudan", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "lmd", "tags": [ "lmd-Latn", "lmd-SD" ], "windows": "lmd-Latn" @@ -64533,7 +67500,7 @@ "iana": [ "Pévé" ], "iso639_3": "lme", "name": "Pévé", - "names": [ "Ka’do", "Ka’do Pevé", "Lamé", "Zime" ], + "names": [ "Lamé", "Peve", "Zime" ], "region": "TD", "regionname": "Chad", "regions": [ "CM" ], @@ -64548,7 +67515,7 @@ "iana": [ "South Lembata" ], "iso639_3": "lmf", "name": "Lembata, South", - "names": [ "South Lembata" ], + "names": [ "Bahasa Atadei", "Eastern Atadai", "Painara", "South Lembata" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -64573,19 +67540,19 @@ "windows": "lmg-Latn" }, { - "full": "lmh-Zyyy-NP", + "full": "lmh-Deva-NP", "iana": [ "Lambichhong" ], "iso639_3": "lmh", "name": "Mugali", "names": [ "Lambicchhong", "Lambicchong", "Lambichhong", "Lambichong", "Lambitshong", "Mugali-Ring", "Muga·Yakkha" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "lmh", - "tags": [ "lmh-NP", "lmh-Zyyy" ], + "tags": [ "lmh-Deva", "lmh-NP" ], "unwritten": true, - "windows": "lmh-Zyyy" + "windows": "lmh-Deva" }, { "full": "lmi-Latn-CD", @@ -64758,7 +67725,7 @@ "iana": [ "Lamalera" ], "iso639_3": "lmr", "name": "Lamalera", - "names": [ "Kawela", "Lebatukan", "Mulan" ], + "names": [ "Kawela", "Lebatukan", "Mulan", "Peripheral Lembata" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -64802,7 +67769,7 @@ "iana": [ "Lake Miwok" ], "iso639_3": "lmw", "name": "Miwok, Lake", - "names": [ "Lake Miwok" ], + "names": [ "Lake Miwok", "Miwok du lac", "Miwokano del lago" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -64831,6 +67798,7 @@ "iana": [ "Lamboya" ], "iso639_3": "lmy", "name": "Lamboya", + "names": [ "Laboya" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -64870,7 +67838,7 @@ "suppress": true, "tag": "ln", "tags": [ "ln-CD", "ln-Latn" ], - "windows": "ln" + "windows": "ln-CD" }, { "full": "ln-Latn-AO", @@ -64883,9 +67851,8 @@ "regionname": "Angola", "script": "Latn", "sldr": true, - "suppress": true, "tag": "ln-AO", - "windows": "ln-AO" + "windows": "ln-Latn-AO" }, { "full": "lna-Latn-CF", @@ -64908,7 +67875,7 @@ "iana": [ "Mbalanhu" ], "iso639_3": "lnb", "name": "Mbalanhu", - "names": [ "Mbaanhu", "Mbalantu", "Mbaluntu" ], + "names": [ "Central Wambo", "Mbaanhu", "Mbalantu", "Mbaluntu" ], "region": "NA", "regionname": "Namibia", "script": "Latn", @@ -64924,7 +67891,7 @@ "iso639_3": "lnd", "localnames": [ "Buri' Lun Bawang", "Buri' Lun Dayeh" ], "name": "Lundayeh", - "names": [ "Buri tau", "Buri’ Lun Bawang", "Buri’ Lun Dayeh", "Lun Bawang", "Lun Daya", "Lun Dayah", "Lun Daye", "Lun Dayeh", "Lun Dayoh", "Lun Lod", "Lundaya", "Lundaya Putuk", "Lundayu", "Southern Murut" ], + "names": [ "Brunei Murut", "Buri tau", "Buri’ Lun Bawang", "Buri’ Lun Dayeh", "Lun Bawang", "Lun Daya", "Lun Dayah", "Lun Daye", "Lun Dayeh", "Lun Dayoh", "Lun Lod", "Lundaya", "Lundaya Putuk", "Lundayu", "Murut", "Southern Murut" ], "region": "ID", "regionname": "Indonesia", "regions": [ "BN", "MY" ], @@ -64935,25 +67902,39 @@ "windows": "lnd-Latn" }, { - "full": "lng-Zyyy-HU", + "full": "lng-Latn-HU", "iana": [ "Langobardic" ], "iso639_3": "lng", "name": "Langobardic", "region": "HU", "regionname": "Hungary", "regions": [ "IT" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "lng", - "tags": [ "lng-HU", "lng-Zyyy" ], - "windows": "lng-Zyyy" + "tags": [ "lng-HU", "lng-Latn" ], + "windows": "lng-Latn" + }, + { + "full": "lng-Runr-HU", + "iana": [ "Langobardic" ], + "iso639_3": "lng", + "name": "Langobardic", + "nophonvars": true, + "region": "HU", + "regionname": "Hungary", + "regions": [ "001", "IT" ], + "script": "Runr", + "sldr": false, + "tag": "lng-Runr", + "windows": "lng-Runr" }, { "full": "lnh-Latn-MY", "iana": [ "Lanoh" ], "iso639_3": "lnh", "name": "Lanoh", - "names": [ "Jengjeng" ], + "names": [ "Jengjeng", "Lanoh Jengjeng" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -64983,7 +67964,7 @@ "iana": [ "Leningitij" ], "iso639_3": "lnj", "name": "Leningitij", - "names": [ "Linngithig", "Linngithigh" ], + "names": [ "Andyinit", "Cabati", "Ladamngid", "Laini ngitti", "Laini-ngitti", "Latangeti", "Latarngit", "Leiningidi", "Leningit", "Leningiti", "Lenngeti", "Lenngidgh", "Linginiti", "Lingitir", "Linngidhigh", "Linngithig", "Linngithigh", "Linngiti", "Linŋit̯iγ", "Onnatangnit", "Umngnity", "Winda Winda", "Winduwinda" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -65003,7 +67984,7 @@ "regionname": "Central African Republic", "regions": [ "CD" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "lnl", "tags": [ "lnl-CF", "lnl-Latn" ], "windows": "lnl-Latn" @@ -65012,7 +67993,8 @@ "full": "lnm-Latn-PG", "iana": [ "Langam" ], "iso639_3": "lnm", - "name": "Langam", + "name": "Pondi", + "names": [ "Langam", "Mwa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -65027,6 +68009,7 @@ "iana": [ "Lorediakarkar" ], "iso639_3": "lnn", "name": "Lorediakarkar", + "names": [ "Nethalp" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -65036,29 +68019,14 @@ "unwritten": true, "windows": "lnn-Latn" }, - { - "full": "lno-Latn-SS", - "iana": [ "Lango (South Sudan)" ], - "iso639_3": "lno", - "name": "Lango", - "names": [ "Langgo", "Lango (South Sudan)" ], - "region": "SS", - "regionname": "South Sudan", - "script": "Latn", - "sldr": false, - "tag": "lno", - "tags": [ "lno-Latn", "lno-SS" ], - "unwritten": true, - "windows": "lno-Latn" - }, { "full": "lns-Latn-CM", "iana": [ "Lamnso'" ], "iso639_3": "lns", - "localname": "Lamnso'", + "localname": "Lamnsoʼ", "localnames": [ "Lamnso'" ], "name": "Lamnso’", - "names": [ "Bansaw", "Banso", "Banso’", "Lamnso'", "Lamnsok", "Lamso", "Nsaw", "Nsho’", "Nso", "Nso’", "Panso" ], + "names": [ "Bansaw", "Banso", "Banso’", "Lamnso", "Lamnso'", "Lamnsok", "Lamso", "Nsaw", "Nsho’", "Nso", "Nso’", "Panso" ], "region": "CM", "regionname": "Cameroon", "regions": [ "NG" ], @@ -65072,9 +68040,10 @@ "full": "lnu-Latn-NG", "iana": [ "Longuda" ], "iso639_3": "lnu", + "localname": "Nungurama Nyuar", "localnames": [ "Nungurama Nyuar" ], "name": "Longuda", - "names": [ "Languda", "Longura", "Nunguda", "Nungura", "Nunguraba" ], + "names": [ "Languda", "Longura", "Nunguda", "Nungura", "Nunguraba", "Nungurama Nyuar" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -65103,6 +68072,7 @@ "iana": [ "Lonzo" ], "iso639_3": "lnz", "name": "Lonzo", + "names": [ "Kilonzo" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -65150,9 +68120,9 @@ "full": "lob-Latn-BF", "iana": [ "Lobi" ], "iso639_3": "lob", - "localnames": [ "'Lobiiri" ], + "localnames": [ "'Lobiire" ], "name": "Lobi", - "names": [ "Lobiri", "Miwa", "’Lobiir", "’Lobiiri" ], + "names": [ "Lobiri", "Miwa", "’Lobiire" ], "region": "BF", "regionname": "Burkina Faso", "regions": [ "CI", "GH" ], @@ -65167,7 +68137,7 @@ "iana": [ "Inonhan" ], "iso639_3": "loc", "name": "Inonhan", - "names": [ "Bisaya-Inunhan", "Loocnon", "Looknon", "Ohnhan" ], + "names": [ "Bisaya-Inunhan", "Loocnon", "Looknon", "Onhan" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -65209,7 +68179,7 @@ "full": "log-Latn-CD", "iana": [ "Logo" ], "iso639_3": "log", - "localname": "Logo", + "localname": "Lògòti", "localnames": [ "Lògòti" ], "name": "Logo", "names": [ "Logoti", "Lògòti" ], @@ -65225,9 +68195,9 @@ "full": "loh-Latn-SS", "iana": [ "Narim" ], "iso639_3": "loh", - "localnames": [ "Boya" ], - "name": "Narim", - "names": [ "Boya", "Lariim", "Larim", "Lariminit", "Larimo", "Longarim", "Nariim" ], + "localnames": [ "Laarim" ], + "name": "Laarim", + "names": [ "Boya", "Lariim", "Larim", "Lariminit", "Larimo", "Longarim", "Nariim", "Narim" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -65300,8 +68270,9 @@ "full": "lom-Latn-LR", "iana": [ "Loma (Liberia)" ], "iso639_3": "lom", + "localnames": [ "Löghömàgòòi", "Löömàgòòi" ], "name": "Loma", - "names": [ "Loghoma", "Logoma", "Loma (Liberia)", "Looma", "Lorma", "Toa" ], + "names": [ "Loghoma", "Logoma", "Loma (Liberia)", "Looma", "Lorma", "Löghömàgòòi", "Löömàgòòi", "Toa" ], "region": "LR", "regionname": "Liberia", "script": "Latn", @@ -65315,7 +68286,7 @@ "iana": [ "Loma (Liberia)" ], "iso639_3": "lom", "name": "Loma", - "names": [ "Loghoma", "Logoma", "Loma (Liberia)", "Looma", "Lorma", "Toa" ], + "names": [ "Loghoma", "Logoma", "Loma (Liberia)", "Looma", "Lorma", "Löghömàgòòi", "Löömàgòòi", "Toa" ], "nophonvars": true, "region": "LR", "regionname": "Liberia", @@ -65329,7 +68300,7 @@ "iana": [ "Malawi Lomwe" ], "iso639_3": "lon", "name": "Lomwe, Malawi", - "names": [ "Anguru", "Elhomwe", "Malawi Lomwe", "Nguru" ], + "names": [ "Anguru", "Elhomwe", "Ellomwe", "Elomwe", "Lhomwe", "Malawi Lomwe", "Nguru" ], "region": "MW", "regionname": "Malawi", "script": "Latn", @@ -65358,7 +68329,7 @@ "iana": [ "Lopa" ], "iso639_3": "lop", "name": "Lopa", - "names": [ "Djiri", "Kirikjir", "Lopawa", "Lupa", "Ollaran" ], + "names": [ "Djiri", "Kirikjir", "Lopanic", "Lopawa", "Lupa", "Ollaran", "Rop", "Tsupamini" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -65372,7 +68343,10 @@ "full": "loq-Latn-CD", "iana": [ "Lobala" ], "iso639_3": "loq", + "localname": "Loɓala", + "localnames": [ "Loɓala" ], "name": "Lobala", + "names": [ "Loɓala" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -65446,15 +68420,15 @@ "full": "lou-Latn-US", "iana": [ "Louisiana Creole" ], "iso639_3": "lou", + "localnames": [ "Kouri-Vini" ], "name": "Louisiana Creole", - "names": [ "Creole", "Kréyol", "Louisiana Creole French", "Louisiana French Creole", "créole louisianais", "kréyol la lwizyàn" ], + "names": [ "Creole", "Fransé", "Kouri-Vini", "Kréyol", "Louisiana Creole French", "Louisiana French Creole", "créole louisianais", "kréyol la lwizyàn" ], "region": "US", "regionname": "United States", "script": "Latn", "sldr": false, "tag": "lou", "tags": [ "lou-Latn", "lou-US" ], - "unwritten": true, "windows": "lou-Latn" }, { @@ -65503,8 +68477,9 @@ "full": "loy-Deva-NP", "iana": [ "Loke" ], "iso639_3": "loy", - "latnnames": [ "Lhowa" ], - "localnames": [ "ल्होवा" ], + "latnnames": [ "ᴴlokek" ], + "localname": "ॱलोकेक", + "localnames": [ "ॱलोकेक" ], "name": "Lhowa", "names": [ "Glo Skad", "Lhopa", "Lo Montang", "Loba", "Loke", "Loket", "Lopa", "Lowa", "Loyu", "Mustangi" ], "region": "NP", @@ -65622,24 +68597,38 @@ "full": "lpx-Latn-SS", "iana": [ "Lopit" ], "iso639_3": "lpx", - "localnames": [ "Lopit" ], + "localnames": [ "Hutuk na Lopit" ], "name": "Lopit", - "names": [ "Lafiit", "Lafit", "Lafite", "Lofit", "Lopid", "Loppit" ], + "names": [ "Hutuk na Lopit", "Lafiit", "Lafit", "Lafite", "Lofit", "Lopid", "Loppit" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "lpx", "tags": [ "lpx-Latn", "lpx-SS" ], "windows": "lpx-Latn" }, + { + "full": "lqr-Latn-SS", + "iana": [ "Logir" ], + "iso639_3": "lqr", + "name": "Logir", + "region": "SS", + "regionname": "South Sudan", + "script": "Latn", + "sldr": false, + "tag": "lqr", + "tags": [ "lqr-Latn", "lqr-SS" ], + "unwritten": true, + "windows": "lqr-Latn" + }, { "full": "lra-Latn-MY", "iana": [ "Rara Bakati'" ], "iso639_3": "lra", "localnames": [ "Rara Bakati'" ], "name": "Bakati’, Rara", - "names": [ "Bekatiq", "Bekati’ Kendayan", "Bekati’ Nyam-Pelayo", "Lara’", "Luru", "Rara Bakati", "Rara Bakati'" ], + "names": [ "Bekatiq", "Bekati’ Kendayan", "Bekati’ Nyam-Pelayo", "Lara’", "Luru", "Rara Bakati'", "Rara Bakati’" ], "region": "MY", "regionname": "Malaysia", "regions": [ "ID" ], @@ -65664,7 +68653,7 @@ "sldr": true, "tag": "lrc", "tags": [ "lrc-Arab", "lrc-IR" ], - "windows": "lrc-Arab" + "windows": "lrc-Arab-IR" }, { "full": "lrc-Arab-IQ", @@ -65686,6 +68675,7 @@ "iana": [ "Laurentian" ], "iso639_3": "lre", "name": "Laurentian", + "names": [ "St. Lawrence Iroquoian" ], "region": "US", "regionname": "United States", "regions": [ "CA" ], @@ -65700,15 +68690,15 @@ "full": "lrg-Latn-AU", "iana": [ "Laragia" ], "iso639_3": "lrg", + "localnames": [ "Gulumoerrgin" ], "name": "Laragia", - "names": [ "Gulumirrgin", "Laragiya", "Larakia", "Larakiya", "Larrakia", "Larrakiya", "Ngandalagarak" ], + "names": [ "Gulumirrgin", "Gulumoerrgin", "Laragiya", "Larakia", "Larakiya", "Larrakia", "Larrakiya", "Ngandalagarak" ], "region": "AU", "regionname": "Australia", "script": "Latn", "sldr": false, "tag": "lrg", "tags": [ "lrg-AU", "lrg-Latn" ], - "unwritten": true, "windows": "lrg-Latn" }, { @@ -65716,6 +68706,7 @@ "iana": [ "Marachi", "Olumarachi" ], "iso639_3": "lri", "localnames": [ "Olumarachi" ], + "macrolang": "luy", "name": "Olumarachi", "names": [ "Marachi" ], "region": "KE", @@ -65759,6 +68750,7 @@ "iana": [ "Marama", "Olumarama" ], "iso639_3": "lrm", "localnames": [ "Olumarama" ], + "macrolang": "luy", "name": "Olumarama", "names": [ "Marama" ], "region": "KE", @@ -65872,6 +68864,34 @@ "tags": [ "lsa-Arab", "lsa-IR" ], "windows": "lsa-Arab" }, + { + "full": "lsb-Zxxx-BI", + "iana": [ "Burundian Sign Language", "Langue des Signes Burundaise" ], + "iso639_3": "lsb", + "name": "Burundian Sign Language", + "names": [ "LSB", "Langue des Signes Burundaise" ], + "region": "BI", + "regionname": "Burundi", + "script": "Zxxx", + "sldr": false, + "tag": "lsb", + "tags": [ "lsb-BI", "lsb-Zxxx", "sgn-lsb", "sgn-lsb-BI" ], + "windows": "lsb-Zxxx" + }, + { + "full": "lsc-Zxxx-MX", + "iana": [ "Albarradas Sign Language", "Lengua de señas Albarradas" ], + "iso639_3": "lsc", + "name": "Albarradas Sign Language", + "names": [ "LSAlb", "Lengua de señas Albarradas" ], + "region": "MX", + "regionname": "Mexico", + "script": "Zxxx", + "sldr": false, + "tag": "lsc", + "tags": [ "lsc-MX", "lsc-Zxxx", "sgn-lsc", "sgn-lsc-MX" ], + "windows": "lsc-Zxxx" + }, { "full": "lsd-Hebr-IL", "iana": [ "Lishana Deni" ], @@ -65963,8 +68983,9 @@ "iana": [ "Saamia", "Olusamia" ], "iso639_3": "lsm", "localnames": [ "Lusaamya-Lugwe", "Ólusaamya" ], + "macrolang": "luy", "name": "Saamya-Gwe", - "names": [ "Bagwe", "Basamia", "Lusaamia", "Lusaamya-Lugwe", "Olusamia", "Saamia", "Ólusaamya" ], + "names": [ "Bagwe", "Basamia", "Lusaamia", "Lusaamya-Lugwe", "Olusamia", "Saamia", "Samia", "Ólusaamya" ], "region": "UG", "regionname": "Uganda", "regions": [ "KE" ], @@ -66072,6 +69093,20 @@ "tags": [ "lsv-PE", "lsv-Zxxx", "sgn-lsv", "sgn-lsv-PE" ], "windows": "lsv-Zxxx" }, + { + "full": "lsw-Zxxx-SC", + "iana": [ "Seychelles Sign Language", "Lalang Siny Seselwa", "Langue des Signes Seychelloise" ], + "iso639_3": "lsw", + "name": "Seychelles Sign Language", + "names": [ "Lalang Siny Seselwa", "Langue des Signes Seychelloise", "SSL", "Seychellois Sign Language" ], + "region": "SC", + "regionname": "Seychelles", + "script": "Zxxx", + "sldr": false, + "tag": "lsw", + "tags": [ "lsw-SC", "lsw-Zxxx", "sgn-lsw", "sgn-lsw-SC" ], + "windows": "lsw-Zxxx" + }, { "full": "lsy-Zxxx-MU", "iana": [ "Mauritian Sign Language" ], @@ -66121,23 +69156,37 @@ "windows": "lt-Latf" }, { - "full": "ltc-Zyyy-CN", + "full": "ltc-Hant-CN", "iana": [ "Late Middle Chinese" ], "iso639_3": "ltc", "name": "Late Middle Chinese", "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Hant", "sldr": false, "tag": "ltc", - "tags": [ "ltc-CN", "ltc-Zyyy" ], - "windows": "ltc-Zyyy" + "tags": [ "ltc-CN", "ltc-Hant" ], + "windows": "ltc-Hant" + }, + { + "full": "ltc-Hans-CN", + "iana": [ "Late Middle Chinese" ], + "iso639_3": "ltc", + "name": "Late Middle Chinese", + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Hans", + "sldr": false, + "tag": "ltc-Hans", + "windows": "ltc-Hans" }, { "full": "ltg-Latn-LV", "iana": [ "Latgalian" ], "iso639_3": "ltg", "localnames": [ "Latgališu" ], + "macrolang": "lv", "name": "Latgalian", "names": [ "Latgaliešu", "Latgališu" ], "region": "LV", @@ -66199,8 +69248,9 @@ "iana": [ "Tsotso", "Olutsotso" ], "iso639_3": "lto", "localnames": [ "Olutsotso" ], + "macrolang": "luy", "name": "Olutsotso", - "names": [ "Tsotso" ], + "names": [ "Batsotso", "Tsotso" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -66214,6 +69264,7 @@ "iana": [ "Tachoni", "Lutachoni" ], "iso639_3": "lts", "localnames": [ "Lutachoni" ], + "macrolang": "luy", "name": "Lutachoni", "names": [ "Tachon", "Tachoni", "Tatsoni" ], "region": "KE", @@ -66258,9 +69309,9 @@ "full": "lua-Latn-CD", "iana": [ "Luba-Lulua" ], "iso639_3": "lua", - "localnames": [ "Tshiluba" ], + "localnames": [ "Ciluba", "Tshiluba" ], "name": "Luba-Kasai", - "names": [ "Bena-Lulua", "Ciluba", "Luba-Lulua", "Luva", "Tshiluba", "Western Luba" ], + "names": [ "Bapemba", "Bena-Lulua", "Ciluba", "Kanyuka", "Luba-Lulua", "Luva", "Tshiluba" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "AO" ], @@ -66274,10 +69325,10 @@ "full": "luc-Latn-UG", "iana": [ "Aringa" ], "iso639_3": "luc", - "localname": "Ãrị̃ngã", - "localnames": [ "Ãrị̃ngã" ], + "localname": "Ãrị̃ngã tị", + "localnames": [ "Ãrị̃ngã tị" ], "name": "Aringa", - "names": [ "Low Lugbara", "Ãrị̃ngã" ], + "names": [ "Aringa Ti", "Low Lugbara", "Ãrị̃ngã tị" ], "region": "UG", "regionname": "Uganda", "script": "Latn", @@ -66362,18 +69413,18 @@ "windows": "luj-Latn" }, { - "full": "luk-Zyyy-BT", + "full": "luk-Tibt-BT", "iana": [ "Lunanakha" ], "iso639_3": "luk", "name": "Lunanakha", "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "luk", - "tags": [ "luk-BT", "luk-Zyyy" ], + "tags": [ "luk-BT", "luk-Tibt" ], "unwritten": true, - "windows": "luk-Zyyy" + "windows": "luk-Tibt" }, { "full": "lul-Latn-SS", @@ -66381,7 +69432,7 @@ "iso639_3": "lul", "localnames": [ "Ọ́lụ́ꞌbọ́ Tị" ], "name": "Olu’bo", - "names": [ "Lolubo", "Luluba", "Lulubo", "Olu'bo", "Olubogo", "Oluboti", "Ondoe" ], + "names": [ "Lolubo", "Luluba", "Lulubo", "Olu'bo", "Olubogo", "Oluboti", "Olu’boti", "Ondoe", "Ọ́lụ́ꞌbọ́ Tị" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -66427,7 +69478,7 @@ "localname": "Dholuo", "localnames": [ "Dholuo" ], "name": "Dholuo", - "names": [ "Doluo", "Kavirondo", "Kidjaluo", "Luo", "Luo (Kenya and Tanzania)" ], + "names": [ "Doluo", "Kidjaluo", "Luo", "Luo (Kenya and Tanzania)" ], "region": "KE", "regionname": "Kenya", "regions": [ "TZ" ], @@ -66471,7 +69522,7 @@ "full": "lur-Latn-ID", "iana": [ "Laura" ], "iso639_3": "lur", - "name": "Laura", + "name": "Loura", "names": [ "Laora" ], "region": "ID", "regionname": "Indonesia", @@ -66488,7 +69539,7 @@ "iso639_3": "lus", "localnames": [ "Mizo" ], "name": "Mizo", - "names": [ "Chin, Mizo", "Duhlian Twang", "Dulien", "Hualngo", "Ka-Lin-Kaw", "Le", "Lei", "Lukhai", "Lusago", "Lusai", "Lusei", "Lushai", "Lushai-Mizo", "Lushay", "Lushei", "Mizo ṭawng", "Sailau", "Whelngo" ], + "names": [ "Chin, Mizo", "Duhlian Twang", "Dulien", "Hualngo", "Ka-Lin-Kaw", "Le", "Lei", "Lukhai", "Lusago", "Lusai", "Lusei", "Lushai", "Lushai-Mizo", "Lushay", "Lushei", "Mizo Chin", "Mizo ṭawng", "Sailau", "Whelngo" ], "region": "IN", "regionname": "India", "regions": [ "BD", "MM" ], @@ -66496,14 +69547,14 @@ "sldr": false, "tag": "lus", "tags": [ "lus-IN", "lus-Latn" ], - "windows": "lus-Latn" + "windows": "lus-Latn-IN" }, { "full": "lus-Beng-BD", "iana": [ "Lushai" ], "iso639_3": "lus", "name": "Mizo", - "names": [ "Chin, Mizo", "Duhlian Twang", "Dulien", "Hualngo", "Ka-Lin-Kaw", "Le", "Lei", "Lukhai", "Lusago", "Lusai", "Lusei", "Lushai", "Lushai-Mizo", "Lushay", "Lushei", "Mizo ṭawng", "Sailau", "Whelngo" ], + "names": [ "Chin, Mizo", "Duhlian Twang", "Dulien", "Hualngo", "Ka-Lin-Kaw", "Le", "Lei", "Lukhai", "Lusago", "Lusai", "Lusei", "Lushai", "Lushai-Mizo", "Lushay", "Lushei", "Mizo Chin", "Mizo ṭawng", "Sailau", "Whelngo" ], "nophonvars": true, "region": "BD", "regionname": "Bangladesh", @@ -66512,14 +69563,14 @@ "sldr": false, "tag": "lus-Beng", "tags": [ "lus-BD" ], - "windows": "lus-Beng" + "windows": "lus-Beng-BD" }, { "full": "lus-Brai-IN", "iana": [ "Lushai" ], "iso639_3": "lus", "name": "Mizo", - "names": [ "Chin, Mizo", "Duhlian Twang", "Dulien", "Hualngo", "Ka-Lin-Kaw", "Le", "Lei", "Lukhai", "Lusago", "Lusai", "Lusei", "Lushai", "Lushai-Mizo", "Lushay", "Lushei", "Mizo ṭawng", "Sailau", "Whelngo" ], + "names": [ "Chin, Mizo", "Duhlian Twang", "Dulien", "Hualngo", "Ka-Lin-Kaw", "Le", "Lei", "Lukhai", "Lusago", "Lusai", "Lusei", "Lushai", "Lushai-Mizo", "Lushay", "Lushei", "Mizo Chin", "Mizo ṭawng", "Sailau", "Whelngo" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -66527,7 +69578,7 @@ "script": "Brai", "sldr": false, "tag": "lus-Brai", - "windows": "lus-Brai" + "windows": "lus-Brai-IN" }, { "full": "lut-Latn-US", @@ -66545,33 +69596,33 @@ "windows": "lut-Latn" }, { - "full": "luu-Zyyy-NP", + "full": "luu-Deva-NP", "iana": [ "Lumba-Yakkha" ], "iso639_3": "luu", "name": "Yakkha, Chhathare", "names": [ "Lumba-Yakkha", "Yakkhaba Cea" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "luu", - "tags": [ "luu-NP", "luu-Zyyy" ], + "tags": [ "luu-Deva", "luu-NP" ], "unwritten": true, - "windows": "luu-Zyyy" + "windows": "luu-Deva" }, { - "full": "luv-Zyyy-OM", + "full": "luv-Arab-OM", "iana": [ "Luwati" ], "iso639_3": "luv", "name": "Luwati", "names": [ "Hyderabadi", "Khoja", "Khojki", "Lawatiya" ], "region": "OM", "regionname": "Oman", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "luv", - "tags": [ "luv-OM", "luv-Zyyy" ], - "windows": "luv-Zyyy" + "tags": [ "luv-Arab", "luv-OM" ], + "windows": "luv-Arab" }, { "full": "luw-Latn-CM", @@ -66594,8 +69645,8 @@ "iso639_3": "luy", "localname": "Luluhia", "localnames": [ "Lubukusu" ], - "name": "Bukusu", - "names": [ "Babukusu", "Lubukusu", "Luyia", "Oluluyia" ], + "name": "Luyia", + "names": [ "Babukusu", "Bukusu", "Lubukusu", "Oluluyia" ], "region": "KE", "regionname": "Kenya", "regions": [ "UG" ], @@ -66627,8 +69678,8 @@ "iso639_3": "lav", "localname": "latviešu", "localnames": [ "Latviešu valoda", "Latviski" ], - "name": "Latvian, Standard", - "names": [ "Latvian", "Latviešu valoda", "Latviski" ], + "name": "Latvian", + "names": [ "Latvian, Standard", "Latviešu valoda", "Latviski" ], "region": "LV", "regionname": "Latvia", "regions": [ "BR", "BY", "CA", "DE", "EE", "IE", "KZ", "LT", "RU", "UA", "US", "VE" ], @@ -66667,18 +69718,19 @@ "windows": "lva-Latn" }, { - "full": "lvi-Zyyy-LA", + "full": "lvi-Latn-LA", "iana": [ "Lavi" ], "iso639_3": "lvi", "name": "Lavi", "names": [ "Lavy", "Lawi", "Salwoeng", "Saveung", "Swoeng" ], "region": "LA", "regionname": "Lao People's Democratic Republic", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "lvi", - "tags": [ "lvi-LA", "lvi-Zyyy" ], - "windows": "lvi-Zyyy" + "tags": [ "lvi-LA", "lvi-Latn" ], + "unwritten": true, + "windows": "lvi-Latn" }, { "full": "lvk-Latn-SB", @@ -66694,12 +69746,26 @@ "tags": [ "lvk-Latn", "lvk-SB" ], "windows": "lvk-Latn" }, + { + "full": "lvl-Latn-CD", + "iso639_3": "lvl", + "name": "Lwel", + "names": [ "Lori" ], + "region": "CD", + "regionname": "The Democratic Republic of the Congo", + "script": "Latn", + "sldr": false, + "tag": "lvl", + "tags": [ "lvl-CD", "lvl-Latn" ], + "unwritten": true, + "windows": "lvl-Latn" + }, { "full": "lvu-Latn-ID", "iana": [ "Levuka" ], "iso639_3": "lvu", "name": "Levuka", - "names": [ "Lembata", "Lewokukun", "Lewuka", "Painara" ], + "names": [ "Lembata", "Lewokukun", "Lewuka" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -66714,7 +69780,7 @@ "iana": [ "Lwalu" ], "iso639_3": "lwa", "name": "Lwalu", - "names": [ "Khongo", "Lwalwa" ], + "names": [ "Khongo", "Lwalwa", "Ukhongo" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "AO" ], @@ -66729,7 +69795,8 @@ "full": "lwe-Latn-ID", "iana": [ "Lewo Eleng" ], "iso639_3": "lwe", - "name": "Lewo Eleng", + "name": "Lewoeleng", + "names": [ "Lewo Eleng" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -66744,6 +69811,7 @@ "iana": [ "Wanga", "Oluwanga" ], "iso639_3": "lwg", "localnames": [ "Oluwanga" ], + "macrolang": "luy", "name": "Oluwanga", "names": [ "Hanga", "Kawanga", "Luhanga", "Luwanga", "Oluhanga", "Wanga" ], "region": "KE", @@ -66790,7 +69858,7 @@ "iana": [ "Laomian" ], "iso639_3": "lwm", "name": "Laomian", - "names": [ "Bisu", "Guba", "Lawa", "Lawmeh", "Lua", "Mbi", "Mibisu", "Misu", "Pin" ], + "names": [ "Bisu", "Guba", "Lawa", "Lawmeh", "Lua", "Mbi", "Mbisu", "Mibisu", "Misu", "Pin" ], "region": "CN", "regionname": "China", "script": "Thai", @@ -66813,7 +69881,7 @@ "sldr": true, "tag": "lwo", "tags": [ "lwo-Latn", "lwo-SS" ], - "windows": "lwo-Latn" + "windows": "lwo-Latn-SS" }, { "full": "lwo-Latn-ZA", @@ -66889,18 +69957,33 @@ "windows": "lww-Latn" }, { - "full": "lya-Zyyy-BT", + "full": "lxm-Latn-PG", + "iana": [ "Lakurumau" ], + "iso639_3": "lxm", + "name": "Lakurumau", + "names": [ "Lakuramau", "Laxudamau", "Loxodumau" ], + "region": "PG", + "regionname": "Papua New Guinea", + "script": "Latn", + "sldr": false, + "tag": "lxm", + "tags": [ "lxm-Latn", "lxm-PG" ], + "unwritten": true, + "windows": "lxm-Latn" + }, + { + "full": "lya-Tibt-BT", "iana": [ "Layakha" ], "iso639_3": "lya", "name": "Layakha", "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "lya", - "tags": [ "lya-BT", "lya-Zyyy" ], + "tags": [ "lya-BT", "lya-Tibt" ], "unwritten": true, - "windows": "lya-Zyyy" + "windows": "lya-Tibt" }, { "full": "lyg-Zyyy-IN", @@ -66922,7 +70005,7 @@ "iana": [ "Luyana" ], "iso639_3": "lyn", "name": "Luyana", - "names": [ "Aluyi", "Ca-Luiana", "Esiluyana", "Louyi", "Lui", "Luyi", "Rouyi", "Si-Luyana" ], + "names": [ "Aluyi", "Ca-Luiana", "Esiluyana", "Louyi", "Lui", "Luyaana", "Luyi", "Rouyi", "Si-Luyana" ], "region": "ZM", "regionname": "Zambia", "regions": [ "BW", "NA" ], @@ -66936,7 +70019,9 @@ "full": "lzh-Hans-CN", "iana": [ "Literary Chinese" ], "iso639_3": "lzh", - "name": "Literary Chinese", + "macrolang": "zh", + "name": "Chinese, Classical", + "names": [ "Literary Chinese" ], "region": "CN", "regionname": "China", "regions": [ "JP", "KR", "VN" ], @@ -66950,7 +70035,9 @@ "full": "lzh-Hant-CN", "iana": [ "Literary Chinese" ], "iso639_3": "lzh", - "name": "Literary Chinese", + "macrolang": "zh", + "name": "Chinese, Classical", + "names": [ "Literary Chinese" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -66998,34 +70085,48 @@ "names": [ "Chan", "Chanuri", "Chanzan", "Laze", "Lazuri", "Zan" ], "region": "TR", "regionname": "Turkey", - "regions": [ "BE", "DE", "GE", "US" ], + "regions": [ "BE", "DE", "US" ], "script": "Latn", "sldr": false, "tag": "lzz", "tags": [ "lzz-Latn", "lzz-TR" ], - "windows": "lzz-Latn" + "windows": "lzz-Latn-TR" }, { - "full": "lzz-Geor-TR", + "full": "lzz-Geor-GE", "iana": [ "Laz" ], "iso639_3": "lzz", "name": "Laz", "names": [ "Chan", "Chanuri", "Chanzan", "Laze", "Lazuri", "Zan" ], "nophonvars": true, - "region": "TR", - "regionname": "Turkey", - "regions": [ "BE", "DE", "GE", "US" ], + "region": "GE", + "regionname": "Georgia", "script": "Geor", "sldr": false, "tag": "lzz-Geor", - "windows": "lzz-Geor" + "windows": "lzz-Geor-GE" + }, + { + "full": "lzz-Latn-GE", + "iana": [ "Laz" ], + "iso639_3": "lzz", + "localnames": [ "Lazuri" ], + "name": "Laz", + "names": [ "Chan", "Chanuri", "Chanzan", "Laze", "Lazuri", "Zan" ], + "region": "GE", + "regionname": "Georgia", + "script": "Latn", + "sldr": false, + "tag": "lzz-Latn-GE", + "windows": "lzz-Latn-GE" }, { "full": "maa-Latn-MX", "iana": [ "San Jerónimo Tecóatl Mazatec" ], "iso639_3": "maa", + "localnames": [ "ꞌÉn-ná" ], "name": "Mazatec, San Jerónimo Tecóatl", - "names": [ "Mazateco de San Antonio Eloxochitlán", "Mazateco de San Jerónimo Tecóatl", "Mazateco del Oeste", "Northern Highland Mazatec", "San Jerónimo Tecóatl Mazatec" ], + "names": [ "Enna", "Mazateco de San Jerónimo Tecóatl", "Mazateco de Tecóatl", "Mazateco del Oeste", "Northern Highland Mazatec", "San Jerónimo Tecóatl Mazatec", "ꞌÉn-ná" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -67038,7 +70139,6 @@ "full": "mab-Latn-MX", "iana": [ "Yutanduchi Mixtec" ], "iso639_3": "mab", - "localnames": [ "Tu'un savi" ], "name": "Mixtec, Yutanduchi", "names": [ "Mixteco de Yutanduchi", "Mixteco de Yutanduchi de Guerrero", "Southern Nochixtlan Mixtec", "Tu’un savi", "Yutanduchi Mixtec" ], "region": "MX", @@ -67065,6 +70165,21 @@ "tags": [ "mad-ID", "mad-Latn" ], "windows": "mad-Latn" }, + { + "full": "mad-Arab-ID", + "iana": [ "Madurese" ], + "iso639_3": "mad", + "name": "Madura", + "names": [ "Basa Mathura", "Madhura", "Madurese" ], + "nophonvars": true, + "region": "ID", + "regionname": "Indonesia", + "regions": [ "SG" ], + "script": "Arab", + "sldr": false, + "tag": "mad-Arab", + "windows": "mad-Arab" + }, { "full": "mae-Latn-NG", "iana": [ "Bo-Rukul" ], @@ -67185,9 +70300,9 @@ "iana": [ "Jalapa De Díaz Mazatec" ], "iso639_3": "maj", "localname": "Ntaxjo", - "localnames": [ "Ntaxjo" ], + "localnames": [ "Kinꞌekꞌatiya", "Ntaxjo̱" ], "name": "Mazatec, Jalapa de Díaz", - "names": [ "Jalapa De Díaz Mazatec", "Lowland Mazatec", "Mazateco de San Felipe Jalapa de Díaz", "Mazateco del Este Bajo", "Ntaxjo" ], + "names": [ "Jalapa De Díaz Mazatec", "Jalapa Mazatec", "Kinꞌekꞌatiya", "Lowland Mazatec", "Mazateco de San Felipe Jalapa de Díaz", "Mazateco del Este Bajo", "Ntaxjo̱" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -67218,7 +70333,6 @@ "name": "Makasar", "names": [ "Basa Mangkasara’", "Goa", "Macassar", "Macassarese", "Makassa", "Makassaarsche", "Makassar", "Makassarese", "Mangasara", "Mengkasara", "Taena", "Tena" ], "nophonvars": true, - "obsolete": true, "region": "ID", "regionname": "Indonesia", "script": "Bugi", @@ -67245,10 +70359,10 @@ "full": "mam-Latn-GT", "iana": [ "Mam" ], "iso639_3": "mam", - "localname": "Mam", - "localnames": [ "B'anax Mam", "Qyool Mam" ], + "localname": "B'anax Mam", + "localnames": [ "Qyol Mam", "Yol Mam" ], "name": "Mam", - "names": [ "B’anax Mam", "Huehuetenango Mam", "Kyol", "Qyol", "Qyol Mam", "Qyool", "Qyool Mam" ], + "names": [ "B’anax Mam", "Kyol", "Qyol", "Qyol Mam", "Qyool", "Qyool Mam", "Yol Mam", "toj qyol" ], "region": "GT", "regionname": "Guatemala", "regions": [ "MX" ], @@ -67258,14 +70372,78 @@ "tags": [ "mam-GT", "mam-Latn" ], "windows": "mam-Latn" }, + { + "full": "mam-Latn-GT-x-neastern", + "iana": [ "Mam" ], + "iso639_3": "mam", + "localnames": [ "Qyol Mam", "Yol Mam" ], + "name": "Mam", + "names": [ "B’anax Mam", "Kyol", "Qyol", "Qyol Mam", "Qyool", "Qyool Mam", "Yol Mam", "toj qyol" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "3470", + "script": "Latn", + "sldr": false, + "tag": "mam-x-neastern", + "tags": [ "mam-GT-x-neastern", "mam-Latn-x-neastern" ], + "windows": "mam-Latn-x-neastern" + }, + { + "full": "mam-Latn-GT-x-northern", + "iana": [ "Mam" ], + "iso639_3": "mam", + "localnames": [ "Qyol Mam", "Yol Mam" ], + "name": "Mam", + "names": [ "B’anax Mam", "Kyol", "Qyol", "Qyol Mam", "Qyool", "Qyool Mam", "Yol Mam", "toj qyol" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "2116", + "script": "Latn", + "sldr": false, + "tag": "mam-x-northern", + "tags": [ "mam-GT-x-northern", "mam-Latn-x-northern" ], + "windows": "mam-Latn-x-northern" + }, + { + "full": "mam-Latn-GT-x-southern", + "iana": [ "Mam" ], + "iso639_3": "mam", + "localnames": [ "Qyol Mam", "Yol Mam" ], + "name": "Mam", + "names": [ "B’anax Mam", "Kyol", "Qyol", "Qyol Mam", "Qyool", "Qyool Mam", "Yol Mam", "toj qyol" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "36", + "script": "Latn", + "sldr": false, + "tag": "mam-x-southern", + "tags": [ "mam-GT-x-southern", "mam-Latn-x-southern" ], + "windows": "mam-Latn-x-southern" + }, + { + "full": "mam-Latn-GT-x-western", + "iana": [ "Mam" ], + "iso639_3": "mam", + "localnames": [ "Qyol Mam", "Yol Mam" ], + "name": "Mam", + "names": [ "B’anax Mam", "Kyol", "Qyol", "Qyol Mam", "Qyool", "Qyool Mam", "Yol Mam", "toj qyol" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4742", + "script": "Latn", + "sldr": false, + "tag": "mam-x-western", + "tags": [ "mam-GT-x-western", "mam-Latn-x-western" ], + "windows": "mam-Latn-x-western" + }, { "full": "man-Latn-GM", "iana": [ "Mandingo", "Manding" ], "iso639_3": "man", "localname": "Mandi'nka kango", "localnames": [ "Mandi'nka kango" ], - "name": "Mandinka", - "names": [ "Mande", "Manding", "Mandinga", "Mandingo", "Mandingue", "Mandinque", "Mandi’nka kango", "Mandé", "Socé" ], + "name": "Mandingo", + "names": [ "Mande", "Mandinca", "Manding", "Mandinga", "Mandingue", "Mandinka", "Mandinque", "Mandi’nka kango", "Mandé", "Socé" ], "region": "GM", "regionname": "Gambia", "regions": [ "GW", "SN" ], @@ -67273,15 +70451,15 @@ "sldr": true, "tag": "man", "tags": [ "man-GM", "man-Latn", "mnk", "mnk-GM", "mnk-Latn", "mnk-Latn-GM", "myq", "myq-GM", "myq-Latn", "myq-Latn-GM" ], - "windows": "man-Latn" + "windows": "man-Latn-GM" }, { "full": "man-Arab-GN", "iana": [ "Mandingo", "Manding" ], "iso639_3": "man", "localname": "Mandi'nka kango", - "name": "Maninkakan, Eastern", - "names": [ "Eastern Malinke", "Kankan Maninka", "Madingo", "Malinké", "Mande", "Manding", "Mandinga", "Mandingo", "Mandingue", "Mandinka", "Mandinque", "Mandi’nka kango", "Mandé", "Maninga", "Maninka", "Maninka-Mori", "Maninkakan", "Maninkaxan", "Socé", "Southern Malinke", "Southern Maninka" ], + "name": "Mandingo", + "names": [ "Eastern Malinke", "Kankan Maninka", "Madingo", "Malinké", "Mande", "Mandinca", "Manding", "Mandinga", "Mandingue", "Mandinka", "Mandinque", "Mandi’nka kango", "Mandé", "Maninga", "Maninka", "Maninka-Mori", "Maninkakan", "Maninkakan, Eastern", "Maninkaxan", "Socé", "Southern Malinke", "Southern Maninka" ], "nophonvars": true, "region": "GN", "regionname": "Guinea", @@ -67290,14 +70468,14 @@ "sldr": true, "tag": "man-Arab", "tags": [ "emk-Arab", "emk-Arab-GN", "mnk-Arab", "mnk-Arab-GN" ], - "windows": "man-Arab" + "windows": "man-Arab-GN" }, { "full": "man-Nkoo-GN", "iana": [ "Mandingo", "Manding" ], "iso639_3": "man", - "name": "Maninkakan, Eastern", - "names": [ "Eastern Malinke", "Kankan Maninka", "Madingo", "Malinké", "Mande", "Manding", "Mandingo", "Maninga", "Maninka", "Maninka-Mori", "Maninkakan", "Maninkaxan", "Southern Malinke", "Southern Maninka" ], + "name": "Mandingo", + "names": [ "Eastern Malinke", "Kankan Maninka", "Madingo", "Malinké", "Mande", "Manding", "Maninga", "Maninka", "Maninka-Mori", "Maninkakan", "Maninkakan, Eastern", "Maninkaxan", "Southern Malinke", "Southern Maninka" ], "region": "GN", "regionname": "Guinea", "regions": [ "LR", "ML", "SL" ], @@ -67310,10 +70488,10 @@ { "full": "man-Latn-GN", "iana": [ "Mandingo", "Manding" ], - "localname": "Mandi'nka kango", + "localname": "Maninka", "localnames": [ "Maninka" ], - "name": "Maninkakan, Eastern", - "names": [ "Eastern Malinke", "Kankan Maninka", "Madingo", "Malinké", "Mande", "Manding", "Mandingo", "Maninga", "Maninka", "Maninka-Mori", "Maninkakan", "Maninkaxan", "Southern Malinke", "Southern Maninka" ], + "name": "Mandingo", + "names": [ "Eastern Malinke", "Kankan Maninka", "Madingo", "Malinké", "Mande", "Manding", "Maninga", "Maninka", "Maninka-Mori", "Maninkakan", "Maninkakan, Eastern", "Maninkaxan", "Southern Malinke", "Southern Maninka" ], "region": "GN", "regionname": "Guinea", "regions": [ "ML", "SL" ], @@ -67328,9 +70506,9 @@ "iana": [ "Chiquihuitlán Mazatec" ], "iso639_3": "maq", "localname": "Nne nangui ngaxni", - "localnames": [ "Nne nangui ngaxni" ], + "localnames": [ "Hne naja", "Nne nangui ngaxni" ], "name": "Mazatec, Chiquihuitlán", - "names": [ "Chiquihuitlán Mazatec", "Mazateco de San Juan Chiquihuitlán", "Mazateco del Sur", "Nne nangui ngaxni" ], + "names": [ "Chiquihuitlán Mazatec", "Hne naja", "Mazateco de San Juan Chiquihuitlán", "Mazateco del Sur", "Nne nangui ngaxni" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -67344,7 +70522,7 @@ "iana": [ "Masai" ], "iso639_3": "mas", "localname": "Maa", - "localnames": [ "Maa" ], + "localnames": [ "Maa", "ɔl Maa" ], "name": "Maasai", "names": [ "Kimaasai", "Lumbwa", "Maa", "Masai", "Massai", "ɔl Maa" ], "region": "KE", @@ -67353,16 +70531,16 @@ "sldr": true, "tag": "mas", "tags": [ "mas-KE", "mas-Latn" ], - "windows": "mas-Latn" + "windows": "mas-Latn-KE" }, { "full": "mas-Latn-TZ", "iana": [ "Masai" ], "iso639_3": "mas", "localname": "Maa", - "localnames": [ "Maa" ], + "localnames": [ "Maa", "ɔl Maa" ], "name": "Maasai", - "names": [ "Kimaasai", "Lumbwa", "Maa", "Masai", "Massai" ], + "names": [ "Kimaasai", "Lumbwa", "Maa", "Masai", "Massai", "ɔl Maa" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -67390,9 +70568,9 @@ "iana": [ "Huautla Mazatec" ], "iso639_3": "mau", "localname": "Enna", - "localnames": [ "Enna" ], + "localnames": [ "Én‑ná" ], "name": "Mazatec, Huautla", - "names": [ "Enna", "Highland Mazatec", "Huautla Mazatec", "Mazateco de Huautla de Jimenez", "Mazateco de la Sierra", "Mazateco del Centro" ], + "names": [ "Enna", "Highland Mazatec", "Huautla Mazatec", "Mazateco de Huautla de Jimenez", "Mazateco de la Sierra", "Mazateco del Centro", "Én‑ná" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -67421,8 +70599,10 @@ "full": "maw-Latn-GH", "iana": [ "Mampruli" ], "iso639_3": "maw", + "localname": "Ŋmampulli", + "localnames": [ "Ŋmampulli", "Ŋmampurili" ], "name": "Mampruli", - "names": [ "Mampelle", "Mamprule", "Mamprusi", "Mamprussi", "Manpelle", "Ngmamperli", "Nmampurli", "Ŋmampəlli" ], + "names": [ "Mampelle", "Mamprule", "Mamprusi", "Mamprussi", "Manpelle", "Ngmamperli", "Nmampurli", "Ŋmampulli", "Ŋmampurili", "Ŋmampəlli" ], "region": "GH", "regionname": "Ghana", "regions": [ "TG" ], @@ -67437,7 +70617,7 @@ "iana": [ "Mampruli" ], "iso639_3": "maw", "name": "Mampruli", - "names": [ "Mampelle", "Mamprule", "Mamprusi", "Mamprussi", "Manpelle", "Ngmamperli", "Nmampurli", "Ŋmampəlli" ], + "names": [ "Mampelle", "Mamprule", "Mamprusi", "Mamprussi", "Manpelle", "Ngmamperli", "Nmampurli", "Ŋmampulli", "Ŋmampurili", "Ŋmampəlli" ], "nophonvars": true, "region": "GH", "regionname": "Ghana", @@ -67452,8 +70632,9 @@ "iana": [ "North Moluccan Malay" ], "iso639_3": "max", "localnames": [ "Bahasa Pasar" ], + "macrolang": "ms", "name": "Malay, North Moluccan", - "names": [ "Bahasa Pasar", "North Moluccan Malay", "Ternate Malay" ], + "names": [ "Bahasa Pasar", "Melayu Maluku Utara", "North Maluku Malay", "North Moluccan Malay", "Ternate Malay" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -67467,9 +70648,9 @@ "iana": [ "Central Mazahua" ], "iso639_3": "maz", "localname": "Jnatrjo", - "localnames": [ "Jnatrjo" ], + "localnames": [ "Jñatjo" ], "name": "Mazahua, Central", - "names": [ "Central Mazahua", "Jnatrjo", "Masawa", "Mazahua de oriente" ], + "names": [ "Central Mazahua", "Jñatjo", "Jñatrjo", "Masawa", "Mazahua de oriente" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -67497,6 +70678,7 @@ "full": "mbb-Latn-PH", "iana": [ "Western Bukidnon Manobo" ], "iso639_3": "mbb", + "localnames": [ "Menuvù" ], "name": "Manobo, Western Bukidnon", "names": [ "Manobo", "Menuvù", "Western Bukidnon", "Western Bukidnon Manobo" ], "region": "PH", @@ -67511,8 +70693,10 @@ "full": "mbc-Latn-BR", "iana": [ "Macushi" ], "iso639_3": "mbc", + "localname": "Makusi", + "localnames": [ "Makusi" ], "name": "Macushi", - "names": [ "Macusi", "Macussi", "Macuxi", "Makuchi", "Makushi", "Makusi", "Makuxi", "Teueia", "Teweya" ], + "names": [ "Macushi Maimu", "Macusi", "Macussi", "Macuxi", "Makuchi", "Makushi", "Makusi", "Makuxi", "Teueia", "Teweya" ], "region": "BR", "regionname": "Brazil", "regions": [ "GY", "VE" ], @@ -67526,7 +70710,7 @@ "full": "mbd-Latn-PH", "iana": [ "Dibabawon Manobo" ], "iso639_3": "mbd", - "localname": "Manobo, Dibabawon", + "localname": "Dibabawon", "localnames": [ "Dibabawon" ], "name": "Manobo, Dibabawon", "names": [ "Debabaon", "Dibabaon", "Dibabauon", "Dibabawon", "Dibabawon Manobo", "Mandaya" ], @@ -67590,6 +70774,7 @@ "iana": [ "Ilianen Manobo" ], "iso639_3": "mbi", "localname": "Manobo, Ilianen", + "localnames": [ "Menuvù" ], "name": "Manobo, Ilianen", "names": [ "Ilianen", "Ilianen Manobo", "Menuvù" ], "region": "PH", @@ -67786,7 +70971,7 @@ "iana": [ "Mbulungish" ], "iso639_3": "mbv", "name": "Mbulungish", - "names": [ "Baga Foré", "Baga Monson", "Black Baga", "Bulunits", "Longich", "Monchon", "Monshon" ], + "names": [ "Baga Foré", "Baga Monson", "Black Baga", "Bulunits", "Longich", "Mbulongish", "Monchon", "Monshon" ], "region": "GN", "regionname": "Guinea", "script": "Latn", @@ -67907,7 +71092,7 @@ "localname": "Sharanahuan tsain", "localnames": [ "Sharanahuan tsain" ], "name": "Sharanahua", - "names": [ "Acre Arara", "Arara Shawãdawa", "Arara do Acre", "Sharanahuan tsain", "Sharanawa", "Shawanauá", "Shawã" ], + "names": [ "Acre Arara", "Arara", "Arara Pano", "Arara Shawãdawa", "Arara do Acre", "Sharanahuan tsain", "Sharanawa", "Shawadawa", "Shawanauá", "Shawã" ], "region": "PE", "regionname": "Peru", "regions": [ "BR" ], @@ -67975,7 +71160,7 @@ "regionname": "Venezuela", "regions": [ "BR" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mch", "tags": [ "mch-Latn", "mch-VE" ], "windows": "mch-Latn" @@ -67984,6 +71169,7 @@ "full": "mci-Latn-PG", "iana": [ "Mese" ], "iso639_3": "mci", + "localnames": [ "Mese" ], "name": "Mesem", "names": [ "Mese" ], "region": "PG", @@ -68045,8 +71231,8 @@ "iana": [ "Malaccan Creole Portuguese" ], "iso639_3": "mcm", "localnames": [ "Kristang" ], - "name": "Malaccan Creole Portuguese", - "names": [ "Bahasa Geragau", "Bahasa Serani", "Kristang", "Luso-Malay", "Malacca Creole", "Malaccan", "Malaqueiro", "Malaquenho", "Malaquense", "Malaquês", "Malayo-Portuguese", "Malaysian Creole Portuguese", "Papia Cristao", "Papia Kristang", "Portuguese Patois", "Português de Malaca", "Serani" ], + "name": "Malaccan Portuguese Creole", + "names": [ "Bahasa Geragau", "Bahasa Serani", "Kristang", "Luso-Malay", "Malacca Creole", "Malaccan", "Malaccan Creole Portuguese", "Malaqueiro", "Malaquenho", "Malaquense", "Malaquês", "Malayo-Portuguese", "Malaysian Portuguese Creole", "Papia Cristao", "Papia Kristang", "Portuguese Patois", "Português de Malaca", "Serani" ], "obsolete": true, "region": "MY", "regionname": "Malaysia", @@ -68077,9 +71263,9 @@ "iana": [ "Coatlán Mixe" ], "iso639_3": "mco", "localname": "Ayuk", - "localnames": [ "Ayuk" ], + "localnames": [ "Ayuc" ], "name": "Mixe, Coatlán", - "names": [ "Ayuk", "Coatlán Mixe", "Southeastern Mixe" ], + "names": [ "Ayuc", "Ayuk", "Ayuuc", "Coatlán Mixe", "Lowland Mixe", "Mixe Bajo", "Southeastern Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -68092,10 +71278,10 @@ "full": "mcp-Latn-CM", "iana": [ "Makaa" ], "iso639_3": "mcp", - "localname": "Mǝkaá", - "localnames": [ "Mǝkaá" ], + "localname": "Məkaá", + "localnames": [ "Məkaá" ], "name": "Makaa", - "names": [ "Maka", "Mekaa", "Mǝkaá", "South Makaa", "South Mekaa" ], + "names": [ "Maka", "Mekaa", "Məkaá", "South Makaa", "South Mekaa" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -68177,20 +71363,6 @@ "tags": [ "mcu-CM", "mcu-Latn" ], "windows": "mcu-Latn" }, - { - "full": "mcu-Laoo-CM", - "iana": [ "Cameroon Mambila" ], - "iso639_3": "mcu", - "localname": "Jù Bà", - "name": "Mambila, Cameroon", - "names": [ "Bang", "Bea", "Ble", "Cameroon Mambila", "Juli", "Jù Bà", "Lagubi", "Mambere", "Mambilla", "Nor", "Tagbo", "Tongbo" ], - "region": "CM", - "regionname": "Cameroon", - "script": "Laoo", - "sldr": true, - "tag": "mcu-Laoo-CM", - "windows": "mcu-Laoo-CM" - }, { "full": "mcv-Latn-PG", "iana": [ "Minanibai" ], @@ -68225,8 +71397,9 @@ "full": "mcx-Latn-CF", "iana": [ "Mpiemo" ], "iso639_3": "mcx", + "localnames": [ "Mpyemɔ" ], "name": "Mpiemo", - "names": [ "Bimu", "Mbimou", "Mbimu", "Mbyemo", "Mpo", "Mpyemo" ], + "names": [ "Bimu", "Mbimou", "Mbimu", "Mbyemo", "Mpo", "Mpyemo", "Mpyemɔ" ], "region": "CF", "regionname": "Central African Republic", "regions": [ "CG", "CM" ], @@ -68255,6 +71428,7 @@ "full": "mcz-Latn-PG", "iana": [ "Mawan" ], "iso639_3": "mcz", + "localnames": [ "Mawan" ], "name": "Mawan", "region": "PG", "regionname": "Papua New Guinea", @@ -68262,7 +71436,6 @@ "sldr": false, "tag": "mcz", "tags": [ "mcz-Latn", "mcz-PG" ], - "unwritten": true, "windows": "mcz-Latn" }, { @@ -68299,14 +71472,15 @@ "full": "mdc-Latn-PG", "iana": [ "Male (Papua New Guinea)" ], "iso639_3": "mdc", - "name": "Male", - "names": [ "Koliku", "Male (Papua New Guinea)" ], + "name": "Soq", + "names": [ "Koliku", "Male", "Male (Papua New Guinea)" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "mdc", "tags": [ "mdc-Latn", "mdc-PG" ], + "unwritten": true, "windows": "mdc-Latn" }, { @@ -68357,13 +71531,14 @@ "iana": [ "Moksha" ], "iso639_3": "mdf", "latnnames": [ "Mokšenj kälj" ], + "localname": "мокшень кяль", "localnames": [ "Мокшень кяль" ], "name": "Moksha", - "names": [ "Moksha Mordvin", "Mokshan", "Mordoff", "Mordov", "Mordvin-Moksha" ], + "names": [ "Moksha Mordvin", "Mokshan", "Mordoff", "Mordov", "Mordovian", "Mordvin-Moksha" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", - "sldr": false, + "sldr": true, "tag": "mdf", "tags": [ "mdf-Cyrl", "mdf-RU" ], "windows": "mdf-Cyrl" @@ -68467,7 +71642,7 @@ "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mdm", "tags": [ "mdm-CD", "mdm-Latn" ], "windows": "mdm-Latn" @@ -68490,8 +71665,9 @@ "full": "mdp-Latn-CD", "iana": [ "Mbala" ], "iso639_3": "mdp", + "localnames": [ "Gimbala" ], "name": "Mbala", - "names": [ "Gimbala", "Rumbala" ], + "names": [ "Gimbala", "Kimbala", "Rumbala" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -68594,8 +71770,9 @@ "full": "mdv-Latn-MX", "iana": [ "Santa Lucía Monteverde Mixtec" ], "iso639_3": "mdv", + "localnames": [ "Sasau" ], "name": "Mixtec, Santa Lucía Monteverde", - "names": [ "Mixteco de Santa Lucía Monteverde", "Mixteco de Yosonotú", "Santa Lucía Monteverde Mixtec" ], + "names": [ "Mixteco de Santa Lucía Monteverde", "Mixteco de Yosonotú", "Santa Lucía Monteverde Mixtec", "Sasau" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -68637,7 +71814,7 @@ "full": "mdx-Latn-ET", "iana": [ "Dizin" ], "iso639_3": "mdx", - "localnames": [ "Diizi-Noog", "Diizin" ], + "localnames": [ "Diizi-nuog", "Diizin" ], "name": "Dizin", "region": "ET", "regionname": "Ethiopia", @@ -68729,7 +71906,6 @@ "sldr": false, "tag": "mec", "tags": [ "mec-AU", "mec-Latn" ], - "unwritten": true, "windows": "mec-Latn" }, { @@ -68783,7 +71959,7 @@ "iso639_3": "meh", "localnames": [ "Tu̱ꞌun káꞌánꞌ kájí" ], "name": "Mixtec, Southwestern Tlaxiaco", - "names": [ "Mixteco de Santiago Nuyoo", "Mixteco del Suroeste de Tlaxiaco", "Mixteco del suroeste del distrito de Tlaxiaco", "Nuyoo Mixtec", "Southeastern Ocotepec Mixtec", "Southwestern Tlaxiaco Mixtec", "Tu̱ꞌun káꞌánꞌ kájí" ], + "names": [ "Mixteco de Santiago Nuyoo", "Mixteco del Suroeste", "Mixteco del Suroeste de Tlaxiaco", "Mixteco del suroeste del distrito de Tlaxiaco", "Nuyoo Mixtec", "Southeastern Ocotepec Mixtec", "Southwestern Tlaxiaco Mixtec", "Tu̱ꞌun káꞌánꞌ kájí", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -68849,7 +72025,6 @@ "sldr": false, "tag": "mel", "tags": [ "mel-Latn", "mel-MY" ], - "unwritten": true, "windows": "mel-Latn" }, { @@ -68905,6 +72080,7 @@ "iana": [ "Kedah Malay" ], "iso639_3": "meo", "localnames": [ "Pelat Utagha" ], + "macrolang": "ms", "name": "Malay, Kedah", "names": [ "Kedah Malay", "Kedahan Malay", "Malay, Satun", "Pelat Utagha", "Satun Malay" ], "region": "MY", @@ -68920,6 +72096,7 @@ "full": "meo-Arab-MY", "iana": [ "Kedah Malay" ], "iso639_3": "meo", + "macrolang": "ms", "name": "Malay, Kedah", "names": [ "Kedah Malay", "Kedahan Malay", "Pelat Utagha", "Satun Malay" ], "nophonvars": true, @@ -69010,6 +72187,8 @@ "full": "meu-Latn-PG", "iana": [ "Motu" ], "iso639_3": "meu", + "localname": "Motu", + "localnames": [ "Motu" ], "name": "Motu", "names": [ "Pure Motu", "True Motu" ], "region": "PG", @@ -69026,7 +72205,7 @@ "iso639_3": "mev", "localnames": [ "Mã́ã́" ], "name": "Maan", - "names": [ "Maa", "Mah", "Mann", "Mano", "Manon", "Mawe", "Mã́ã́" ], + "names": [ "Maa", "Mah", "Mann", "Mano", "Manon", "Mawe", "Máá", "Mã́ã́" ], "region": "LR", "regionname": "Liberia", "regions": [ "GN" ], @@ -69103,6 +72282,7 @@ "full": "mfa-Arab-TH", "iana": [ "Pattani Malay" ], "iso639_3": "mfa", + "macrolang": "ms", "name": "Malay, Pattani", "names": [ "Jawi", "Jawi-Malay", "Pattani Malay", "Yawi", "oré Jawi" ], "region": "TH", @@ -69119,6 +72299,7 @@ "iso639_3": "mfa", "latnnames": [ "Baso Jawi" ], "localnames": [ "ภาษายาวี" ], + "macrolang": "ms", "name": "Malay, Pattani", "names": [ "Jawi", "Jawi-Malay", "Pattani Malay", "Yawi", "oré Jawi" ], "nophonvars": true, @@ -69133,6 +72314,7 @@ "full": "mfb-Latn-ID", "iana": [ "Bangka" ], "iso639_3": "mfb", + "macrolang": "ms", "name": "Bangka", "region": "ID", "regionname": "Indonesia", @@ -69147,15 +72329,15 @@ "full": "mfc-Latn-CD", "iana": [ "Mba" ], "iso639_3": "mfc", + "localnames": [ "Mbanɨ" ], "name": "Mba", - "names": [ "Kimanga", "Kimbanga", "Manga", "Mbane" ], + "names": [ "Kimanga", "Kimbanga", "Manga", "Mbane", "Mbanɨ" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", "sldr": false, "tag": "mfc", "tags": [ "mfc-CD", "mfc-Latn" ], - "unwritten": true, "windows": "mfc-Latn" }, { @@ -69205,18 +72387,33 @@ "unwritten": true, "windows": "mff-Latn" }, + { + "full": "mfg-Latn-GN", + "iana": [ "Mogofin" ], + "iso639_3": "mfg", + "localnames": [ "Mɔgɔfin" ], + "name": "Mogofin", + "names": [ "Mikifore", "Mixifore", "Mɔgɔfin" ], + "region": "GN", + "regionname": "Guinea", + "script": "Latn", + "sldr": false, + "tag": "mfg", + "tags": [ "mfg-GN", "mfg-Latn" ], + "windows": "mfg-Latn" + }, { "full": "mfg-Arab-GN", "iana": [ "Mogofin" ], "iso639_3": "mfg", "name": "Mogofin", - "names": [ "Mikifore", "Mixifore" ], + "names": [ "Mikifore", "Mixifore", "Mɔgɔfin" ], + "nophonvars": true, "region": "GN", "regionname": "Guinea", "script": "Arab", "sldr": false, - "tag": "mfg", - "tags": [ "mfg-Arab", "mfg-GN" ], + "tag": "mfg-Arab", "windows": "mfg-Arab" }, { @@ -69254,6 +72451,7 @@ "full": "mfi-Latn-CM", "iana": [ "Wandala" ], "iso639_3": "mfi", + "localname": "Wandala", "localnames": [ "Wandala" ], "name": "Wandala", "names": [ "Mandara", "Mandara Montagnard", "Ndara" ], @@ -69315,7 +72513,7 @@ "iso639_3": "mfm", "localnames": [ "Margi" ], "name": "Marghi South", - "names": [ "Margi" ], + "names": [ "Margi", "Margi ti ntәm" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -69419,8 +72617,8 @@ "full": "mft-Latn-PG", "iana": [ "Mokerang" ], "iso639_3": "mft", - "name": "Mokerang", - "names": [ "Mokareng", "Mokoreng" ], + "name": "Idio", + "names": [ "Idien", "Mokareng", "Mokerang", "Mokoreng" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -69450,7 +72648,7 @@ "iso639_3": "mfv", "localnames": [ "Manjáku" ], "name": "Mandjak", - "names": [ "Kanyop", "Majak", "Mandjaque", "Mandyak", "Manjaaku", "Manjaca", "Manjack", "Manjaco", "Manjacu", "Manjago", "Manjak", "Manjaku", "Manjanku", "Manjiak", "Manjáku", "Mendyako", "Ndjak", "Ndyak" ], + "names": [ "Kanyop", "Majak", "Mandjako-bé", "Mandjaque", "Mandyak", "Manjaaku", "Manjaca", "Manjack", "Manjaco", "Manjacu", "Manjago", "Manjak", "Manjanku", "Manjiak", "Manjáku", "Mendyako", "Ndiagó", "Ndjak", "Ndyak" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "GM", "SN" ], @@ -69475,18 +72673,30 @@ "windows": "mfw-Latn" }, { - "full": "mfx-Zyyy-ET", + "full": "mfx-Latn-ET", "iana": [ "Melo" ], "iso639_3": "mfx", "name": "Melo", "region": "ET", "regionname": "Ethiopia", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "mfx", - "tags": [ "mfx-ET", "mfx-Zyyy" ], - "unwritten": true, - "windows": "mfx-Zyyy" + "tags": [ "mfx-ET", "mfx-Latn" ], + "windows": "mfx-Latn" + }, + { + "full": "mfx-Ethi-ET", + "iana": [ "Melo" ], + "iso639_3": "mfx", + "name": "Melo", + "nophonvars": true, + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": false, + "tag": "mfx-Ethi", + "windows": "mfx-Ethi" }, { "full": "mfy-Latn-MX", @@ -69524,9 +72734,9 @@ "iana": [ "Malagasy" ], "iso639_3": "mlg", "localname": "Malagasy", - "localnames": [ "Malagasy" ], - "name": "Malagasy, Southern Betsimisaraka", - "names": [ "Ambaniandro", "Antatsimo", "Betsimisaraka", "Borizany", "Fiteny Malagasy", "Hova", "Malagasy", "Malagasy fiteny", "Malagasy, Plateau", "Malgache", "Official Malagasy", "Standard Malagasy", "Teny ofisialy", "Teny ofisiealy" ], + "localnames": [ "Malagasy", "Malagasy ôfisialy" ], + "name": "Malagasy", + "names": [ "Ambaniandro", "Antatsimo", "Betsimisaraka", "Borizano", "Borizany", "Fiteny Malagasy", "Hova", "Malagasy fiteny", "Malagasy ôfisialy", "Malagasy, Merina", "Malagasy, Southern Betsimisaraka", "Malgache", "Official Malagasy", "Plateau Malagasy", "Standard Malagasy", "Teny ôfisialy" ], "region": "MG", "regionname": "Madagascar", "regions": [ "KM", "MU", "RE", "US", "YT" ], @@ -69554,8 +72764,8 @@ "full": "mg-Brai-MG", "iana": [ "Malagasy" ], "iso639_3": "mlg", - "name": "Malagasy, Plateau", - "names": [ "Ambaniandro", "Borizany", "Fiteny Malagasy", "Hova", "Malagasy", "Malagasy fiteny", "Malgache", "Official Malagasy", "Standard Malagasy", "Teny ofisialy", "Teny ofisiealy" ], + "name": "Malagasy", + "names": [ "Ambaniandro", "Borizano", "Borizany", "Fiteny Malagasy", "Hova", "Malagasy fiteny", "Malagasy ôfisialy", "Malagasy, Merina", "Malgache", "Official Malagasy", "Plateau Malagasy", "Standard Malagasy", "Teny ôfisialy" ], "nophonvars": true, "region": "MG", "regionname": "Madagascar", @@ -69599,9 +72809,10 @@ "full": "mgc-Latn-SS", "iana": [ "Morokodo" ], "iso639_3": "mgc", - "localnames": [ "Morokodo" ], + "localname": "Hɔ Morokodo", + "localnames": [ "Hᴐ Morokodo" ], "name": "Morokodo", - "names": [ "Ma’di" ], + "names": [ "Hᴐ Morokodo", "Ma’di", "Vïdï Morokodo" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -69643,12 +72854,13 @@ "full": "mge-Latn-TD", "iana": [ "Mango" ], "iso639_3": "mge", + "localnames": [ "Mangɔ" ], "name": "Mango", - "names": [ "Doba", "Mbay Doba", "Mongo" ], + "names": [ "Doba", "Mangɔ", "Mbay Doba", "Mongo" ], "region": "TD", "regionname": "Chad", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mge", "tags": [ "mge-Latn", "mge-TD" ], "windows": "mge-Latn" @@ -69672,8 +72884,9 @@ "full": "mgg-Latn-CM", "iana": [ "Mpumpong" ], "iso639_3": "mgg", + "localnames": [ "Mpumpoŋ" ], "name": "Mpumpong", - "names": [ "Bombo", "Mbombo", "Mpompo", "Mpompon", "Mpongmpong", "Mpopo", "Mpumpoo", "Pongpong" ], + "names": [ "Bombo", "Mbombo", "Mpompo", "Mpompon", "Mpongmpong", "Mpopo", "Mpumpoo", "Mpumpoŋ", "Pongpong" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -69692,7 +72905,7 @@ "names": [ "Chimakua", "Emakhua", "Emakhuwa", "Emeto", "Imakua", "Imeetto", "Kimakua", "Macua", "Makhua", "Makoa", "Makua", "Makuwa", "Maquoua", "Mato", "Medo", "Meetto", "Meto", "Metto", "Mêto" ], "region": "MZ", "regionname": "Mozambique", - "regions": [ "TZ" ], + "regions": [ "TZ", "YT" ], "script": "Latn", "sldr": true, "tag": "mgh", @@ -69704,8 +72917,8 @@ "iana": [ "Lijili" ], "iso639_3": "mgi", "localnames": [ "Lijili" ], - "name": "Lijili", - "names": [ "Jili", "Koro Lafia", "Koro of Lafia", "Ligili", "Megili", "Migili", "Mijili" ], + "name": "Migili", + "names": [ "Jili", "Koro Lafia", "Koro of Lafia", "Ligili", "Lijili", "Megili", "Mijili" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -69726,7 +72939,6 @@ "sldr": false, "tag": "mgj", "tags": [ "mgj-Latn", "mgj-NG" ], - "unwritten": true, "windows": "mgj-Latn" }, { @@ -69748,7 +72960,7 @@ "iana": [ "Maleu-Kilenge" ], "iso639_3": "mgl", "name": "Maleu-Kilenge", - "names": [ "Idne" ], + "names": [ "Idne", "Lolo", "Maleu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -69839,13 +73051,13 @@ "full": "mgq-Latn-TZ", "iana": [ "Malila" ], "iso639_3": "mgq", - "localnames": [ "Bamalila" ], + "localnames": [ "Shɨmalɨla" ], "name": "Malila", - "names": [ "Bamalila", "Ishimalilia", "Kimalila", "Malilia", "Shimalilia" ], + "names": [ "Bamalila", "Ishimalilia", "Kimalila", "Malilia", "Shimalilia", "Shɨmalɨla" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mgq", "tags": [ "mgq-Latn", "mgq-TZ" ], "windows": "mgq-Latn" @@ -69870,6 +73082,7 @@ "full": "mgs-Latn-TZ", "iana": [ "Manda (Tanzania)" ], "iso639_3": "mgs", + "localnames": [ "Kimanda" ], "name": "Manda", "names": [ "Kimanda", "Kinyasa", "Manda (Tanzania)", "Manda-Matumba", "Nyasa" ], "region": "TZ", @@ -69884,7 +73097,8 @@ "full": "mgt-Latn-PG", "iana": [ "Mongol" ], "iso639_3": "mgt", - "name": "Mongol", + "name": "Mwakai", + "names": [ "Mongol", "Mwa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -69921,7 +73135,6 @@ "sldr": false, "tag": "mgv", "tags": [ "mgv-Latn", "mgv-TZ" ], - "unwritten": true, "windows": "mgv-Latn" }, { @@ -69936,7 +73149,6 @@ "sldr": false, "tag": "mgw", "tags": [ "mgw-Latn", "mgw-TZ" ], - "unwritten": true, "windows": "mgw-Latn" }, { @@ -70062,8 +73274,8 @@ "full": "mhf-Latn-PG", "iana": [ "Mamaa" ], "iso639_3": "mhf", - "name": "Mamaa", - "names": [ "Doloman", "Mama" ], + "name": "Doloman", + "names": [ "Mama", "Mamaa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -70095,7 +73307,7 @@ "localname": "Mãꞌdí", "localnames": [ "Mãꞌdí" ], "name": "Ma’di", - "names": [ "Ma'di", "Madi", "Ma’adi", "Ma’aditi", "Ma’di Ti", "Ma’di ti", "Ma’diti", "Mãꞌdí" ], + "names": [ "Ma'di", "Madi", "Ma’adi", "Ma’aditi", "Ma’di ti", "Ma’diti", "Mãꞌdí" ], "region": "UG", "regionname": "Uganda", "regions": [ "SS" ], @@ -70124,8 +73336,9 @@ "full": "mhk-Latn-CM", "iana": [ "Mungaka" ], "iso639_3": "mhk", + "localnames": [ "Mʉ̀ngâkà" ], "name": "Mungaka", - "names": [ "Bali", "Li", "Munga’ka", "Ngaaka", "Nga’ka" ], + "names": [ "Bali", "Li", "Munga’ka", "Mʉ̀ngâkà", "Ngaaka", "Nga’ka" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -70273,7 +73486,7 @@ "iso639_3": "mhw", "localnames": [ "Thimbukushu" ], "name": "Mbukushu", - "names": [ "Cusso", "Gova", "Hambukushu", "Kuso", "Kusso", "Mambukush", "Mampukush", "Mbukuhu", "Mbukushi", "Sempukushu", "Thimbukushu", "Thimukushu" ], + "names": [ "Cusso", "Gova", "Hambukushu", "Kuso", "Kusso", "Mambukush", "Mampukush", "Mbukuhu", "Mbukushi", "Sembukushu", "Sempukushu", "Thimbukushu", "Thimukushu" ], "region": "BW", "regionname": "Botswana", "regions": [ "AO", "NA", "ZM" ], @@ -70287,7 +73500,7 @@ "full": "mhx-Latn-MM", "iana": [ "Maru", "Lhaovo" ], "iso639_3": "mhx", - "localnames": [ "Lhao Vo" ], + "localnames": [ "Lhaovo" ], "name": "Lhao Vo", "names": [ "Diso", "Lang", "Langsu", "Langwa", "Lang’e", "Laungaw", "Laungwaw", "Lawgore", "Lawng", "Lhaovo", "Liangsu", "Lovo", "Malu", "Matu", "Mulu", "Nyky", "Zi" ], "region": "MM", @@ -70365,7 +73578,7 @@ "iana": [ "Atatláhuca Mixtec" ], "iso639_3": "mib", "name": "Mixtec, Atatlahuca", - "names": [ "Atatláhuca Mixtec", "Mixteco de San Esteban Atatlahuca", "San Esteban Atatlahuca Mixteco", "South Central Tlaxiaco Mixtec" ], + "names": [ "Atatláhuca Mixtec", "Mixteco de San Esteban Atatlahuca", "Mixteco del Sur Medio", "San Esteban Atatlahuca Mixteco", "Sasavú", "South Central Tlaxiaco Mixtec", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70381,7 +73594,7 @@ "localname": "L'nui'simk", "localnames": [ "L'nui'simk", "Míkmawísimk" ], "name": "Mi’kmaq", - "names": [ "L’nui’simk", "Mi'kmaq", "Micmac", "Miigmao", "Mi’gmaq", "Mi’gmaw", "Mi’gmawi’simg", "Mi’kmaw", "Míkmawísimk", "Restigouche" ], + "names": [ "L’nui’simk", "Mi'kmaq", "Micmac", "Miigmao", "Mikmak", "Mikmaw", "Mi’gmaq", "Mi’gmaw", "Mi’gmawi’simg", "Mi’kmaw", "Mìgmaq", "Mìkmaq", "Míkmawísimk" ], "region": "CA", "regionname": "Canada", "regions": [ "US" ], @@ -70396,7 +73609,7 @@ "iana": [ "Mi'kmaq" ], "iso639_3": "mic", "name": "Mi’kmaq", - "names": [ "L’nui’simk", "Mi'kmaq", "Micmac", "Miigmao", "Mi’gmaq", "Mi’gmaw", "Mi’gmawi’simg", "Mi’kmaw", "Míkmawísimk", "Restigouche" ], + "names": [ "L’nui’simk", "Mi'kmaq", "Micmac", "Miigmao", "Mikmak", "Mikmaw", "Mi’gmaq", "Mi’gmaw", "Mi’gmawi’simg", "Mi’kmaw", "Mìgmaq", "Mìkmaq", "Míkmawísimk" ], "nophonvars": true, "obsolete": true, "region": "CA", @@ -70428,9 +73641,9 @@ "iana": [ "Ocotepec Mixtec" ], "iso639_3": "mie", "localname": "Tu'un savi", - "localnames": [ "Tu'un savi" ], + "localnames": [ "Tūhun sāsávī" ], "name": "Mixtec, Ocotepec", - "names": [ "Mixteco de Santo Tomás Ocotepec", "Mixteco de Sierra Sur Noroeste", "Ocotepec Mixtec", "Santo Tomás Ocotepec Mixtec", "Tu’un savi" ], + "names": [ "Mixteco de Santo Tomás Ocotepec", "Mixteco de Sierra Sur Noroeste", "Ocotepec Mixtec", "Santo Tomás Ocotepec Mixtec", "Tu’un savi", "Tūhun sāsávī" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70459,8 +73672,9 @@ "full": "mig-Latn-MX", "iana": [ "San Miguel El Grande Mixtec" ], "iso639_3": "mig", + "localnames": [ "Sahin sau" ], "name": "Mixtec, San Miguel el Grande", - "names": [ "Chalcatongo Mixtec", "Mixteco de San Pedro Molinos", "Mixteco del Sur Bajo", "San Miguel El Grande Mixtec" ], + "names": [ "Mixteco de San Pedro Molinos", "Mixteco del Sur Bajo", "Sahin sau", "San Miguel El Grande Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70474,9 +73688,9 @@ "iana": [ "Chayuco Mixtec" ], "iso639_3": "mih", "localname": "Tu'un savi", - "localnames": [ "Tu'un savi" ], + "localnames": [ "Tuhun savi" ], "name": "Mixtec, Chayuco", - "names": [ "Chayuco Mixtec", "Eastern Jamiltepec-Chayuco Mixtec", "Mixteco de Chayucu", "Tu’un savi" ], + "names": [ "Chayuco Mixtec", "Eastern Jamiltepec-Chayuco Mixtec", "Mixteco de Chayucu", "Mixteco de Oaxaca de la Costa Central", "Tuhun savi", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70489,9 +73703,8 @@ "full": "mii-Latn-MX", "iana": [ "Chigmecatitlán Mixtec" ], "iso639_3": "mii", - "localnames": [ "Da'an davi" ], "name": "Mixtec, Chigmecatitlán", - "names": [ "Central Puebla Mixtec", "Chigmecatitlán Mixtec", "Da’an davi", "Mixteco de Santa María Chigmecatitlán" ], + "names": [ "Central Puebla Mixtec", "Chigmecatitlán Mixtec", "Da’an davi", "Dehe dau", "De’e dau", "Mixteco de Santa María Chigmecatitlán", "Mixteco del Suroeste de Puebla" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70533,10 +73746,10 @@ "full": "mil-Latn-MX", "iana": [ "Peñoles Mixtec" ], "iso639_3": "mil", - "localname": "Mixtec, Peñoles", - "localnames": [ "Tu'un savi" ], + "localname": "Tu'un savi", + "localnames": [ "Tnúhu dàú" ], "name": "Mixtec, Peñoles", - "names": [ "Eastern Mixtec", "Mixteco de Santa María Peñoles", "Peñoles Mixtec", "Tu’un savi" ], + "names": [ "Eastern Mixtec", "Mixteco de Santa María Peñoles", "Peñoles Mixtec", "Tnúhu dàú", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -70550,10 +73763,10 @@ "full": "mim-Latn-MX", "iana": [ "Alacatlatzala Mixtec" ], "iso639_3": "mim", - "localname": "To'on Savi", - "localnames": [ "To'on Savi" ], + "localname": "Toꞌon Savi", + "localnames": [ "To'on savi" ], "name": "Mixtec, Alacatlatzala", - "names": [ "Alacatlatzala Mixtec", "Central Misteko", "Highland Guerrero Mixtec", "Mixteco de Alacatlatzala", "To’on Savi" ], + "names": [ "Alacatlatzala Mixtec", "Central Misteko", "Highland Guerrero Mixtec", "Mixteco de Alacatlatzala", "To’on savi", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -70567,6 +73780,7 @@ "full": "min-Latn-ID", "iana": [ "Minangkabau" ], "iso639_3": "min", + "macrolang": "ms", "name": "Minangkabau", "names": [ "Minang", "Padang" ], "region": "ID", @@ -70581,6 +73795,7 @@ "full": "min-Arab-ID", "iana": [ "Minangkabau" ], "iso639_3": "min", + "macrolang": "ms", "name": "Minangkabau", "names": [ "Minang", "Padang" ], "nophonvars": true, @@ -70595,8 +73810,9 @@ "full": "mio-Latn-MX", "iana": [ "Pinotepa Nacional Mixtec" ], "iso639_3": "mio", + "localnames": [ "Tu’un savi" ], "name": "Mixtec, Pinotepa Nacional", - "names": [ "Coastal Mixtec", "Jicaltepec Mixtec", "Lowland Jicaltepec Mixtec", "Mixteco de Pinotepa Nacional", "Pinotepa Nacional Mixtec", "Western Jamiltepec Mixtec" ], + "names": [ "Coastal Mixtec", "Jicaltepec Mixtec", "Lowland Jicaltepec Mixtec", "Mechoacán Mixtec", "Mixteco de Oaxaca de la Costa Oeste Central", "Mixteco de Pinotepa Nacional", "Mixteco del Oeste de la Costa", "Pinotepa Nacional Mixtec", "Se’en savi", "Tu’un savi", "Western Jamiltepec Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70609,8 +73825,9 @@ "full": "mip-Latn-MX", "iana": [ "Apasco-Apoala Mixtec" ], "iso639_3": "mip", + "localnames": [ "Da'an davi" ], "name": "Mixtec, Apasco-Apoala", - "names": [ "Apasco Mixtec", "Apasco-Apoala Mixtec", "Apoala Mixtec", "Mixteco de Santiago Apoala", "Northern Nochixtlán Mixtec" ], + "names": [ "Apasco Mixtec", "Apasco-Apoala Mixtec", "Apoala Mixtec", "Da’an davi", "Mixteco de Santiago Apoala", "Mixteco del Noreste", "Northern Nochixtlán Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70640,9 +73857,9 @@ "iana": [ "Isthmus Mixe" ], "iso639_3": "mir", "localname": "hagunaax", - "localnames": [ "hagunaax", "hayuuc" ], + "localnames": [ "Ayuuk" ], "name": "Mixe, Isthmus", - "names": [ "Ayuk", "Eastern Mixe", "Guichicovi Mixe", "Isthmus Mixe", "Mixe del Istmo", "Mixe medio del este", "hagunaax", "hayuuc" ], + "names": [ "Ayuk", "Ayuuc", "Ayuuk", "Eastern Mixe", "Guichicovi Mixe", "Isthmus Mixe", "Lowland Mixe", "Mixe Bajo", "Mixe del Istmo", "Mixe medio del este", "hagunaax", "hayuuc" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70652,19 +73869,30 @@ "windows": "mir-Latn" }, { - "full": "mis-Hatr-IQ", + "full": "mis-Hatr-001", "iana": [ "Uncoded languages" ], "iso639_3": "mis", "name": "Uncoded languages", - "region": "IQ", - "regionname": "Iraq", - "regions": [ "IR", "US" ], + "region": "001", + "regionname": "World", "script": "Hatr", "sldr": false, "tag": "mis", - "tags": [ "mis-Hatr", "mis-IQ" ], + "tags": [ "mis-001", "mis-Hatr" ], "windows": "mis-Hatr" }, + { + "full": "mis-Latn-001", + "iana": [ "Uncoded languages" ], + "iso639_3": "mis", + "name": "Uncoded languages", + "region": "001", + "regionname": "World", + "script": "Latn", + "sldr": false, + "tag": "mis-Latn", + "windows": "mis-Latn" + }, { "full": "mit-Latn-MX", "iana": [ "Southern Puebla Mixtec" ], @@ -70672,7 +73900,7 @@ "localname": "Da'an davi", "localnames": [ "Da'an davi" ], "name": "Mixtec, Southern Puebla", - "names": [ "Acatlán Mixtec", "Da’an davi", "Mixteco de la Frontera Puebla-Oaxaca", "Mixteco del Sur de Puebla", "Southern Puebla Mixtec", "Xayacatlán de Bravo" ], + "names": [ "Acatlán Mixtec", "Dahan davi", "Da’an davi", "Mixteco de la Frontera Puebla-Oaxaca", "Mixteco del Sur de Puebla", "Mixteco del Suroeste de Puebla", "Southern Puebla Mixtec", "Xayacatlán de Bravo" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70686,7 +73914,7 @@ "iana": [ "Cacaloxtepec Mixtec" ], "iso639_3": "miu", "name": "Mixtec, Cacaloxtepec", - "names": [ "Cacaloxtepec Mixtec", "Huajuapan Mixtec", "Mixteco de Cacaloxtepec" ], + "names": [ "Cacaloxtepec Mixtec", "Huajuapan Mixtec", "Mixteco de Cacaloxtepec", "Mixteco del Oeste Alto", "Tu’un davi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70715,8 +73943,9 @@ "full": "mix-Latn-MX", "iana": [ "Mixtepec Mixtec" ], "iso639_3": "mix", + "localnames": [ "Sa'an Savi Snuviko", "Sa'an savi" ], "name": "Mixtec, Mixtepec", - "names": [ "Eastern Juxtlahuaca Mixtec", "Mixteco de Oeste Central", "Mixteco de San Juan Mixtepec", "Mixtepec Mixtec" ], + "names": [ "Eastern Juxtlahuaca Mixtec", "Mixteco de Mixtepec", "Mixteco de Oeste Central", "Mixteco de San Juan Mixtepec", "Mixtepec Mixtec", "Sahan lin savi", "San Juan Mixtepec", "Sa’an Savi Snuviko", "Sa’an ntavi", "Sa’an savi", "Saꞌan Ntavi Xnuviko" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -70730,10 +73959,10 @@ "full": "miy-Latn-MX", "iana": [ "Ayutla Mixtec" ], "iso639_3": "miy", - "localname": "Mixtec, Ayutla", - "localnames": [ "Tu'un savi" ], + "localname": "Tu̱n savi̱ꞌ Tu̱tyoo̱ꞌ", + "localnames": [ "Tu̱n savi̱ꞌ Tu̱tyoo̱ꞌ" ], "name": "Mixtec, Ayutla", - "names": [ "Ayutla Mixtec", "Coastal Guerrero Mixtec", "Mixteco de Ayutla", "Tu’un savi" ], + "names": [ "Ayutla Mixtec", "Coastal Guerrero Mixtec", "Libres Mixtec", "Mixteco de Ayutla", "Mixteco de Ayutla de los Libres", "Tu̱n savi̱ꞌ Tu̱tyoo̱ꞌ", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70746,10 +73975,10 @@ "full": "miz-Latn-MX", "iana": [ "Coatzospan Mixtec" ], "iso639_3": "miz", - "localname": "Tu'un davi", + "localname": "Tuꞌun davi", "localnames": [ "Tu'un davi" ], "name": "Mixtec, Coatzospan", - "names": [ "Coatzospan Mixtec", "Mixteco de Coatzóspan", "Mixteco de San Juan Coatzospan", "Teotitlán Mixtec", "Tu’un davi" ], + "names": [ "Coatzospan Mixtec", "Mixteco de Coatzóspan", "Mixteco de San Juan Coatzospan", "Teotitlán Mixtec", "Tuhun davi", "Tu’un davi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -70823,7 +74052,7 @@ "iana": [ "Tu" ], "iso639_3": "mjg", "name": "Tu", - "names": [ "Mongor", "Mongour", "Monguor", "Qighaan Mongghul" ], + "names": [ "Mihe Tu", "Mongor", "Mongour", "Monguor", "Qighaan Mongghul" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -70832,12 +74061,27 @@ "tags": [ "mjg-CN", "mjg-Latn" ], "windows": "mjg-Latn" }, + { + "full": "mjg-Latn-CN-x-minhe", + "iana": [ "Tu" ], + "iso639_3": "mjg", + "name": "Tu", + "names": [ "Mihe Tu", "Mongor", "Mongour", "Monguor", "Qighaan Mongghul" ], + "region": "CN", + "regionname": "China", + "rod": "4946", + "script": "Latn", + "sldr": false, + "tag": "mjg-x-minhe", + "tags": [ "mjg-CN-x-minhe", "mjg-Latn-x-minhe" ], + "windows": "mjg-Latn-x-minhe" + }, { "full": "mjh-Latn-TZ", "iana": [ "Mwera (Nyasa)" ], "iso639_3": "mjh", "name": "Mwera", - "names": [ "Kimwera", "Kinyasa", "Mwera (Nyasa)", "Nyanza", "Nyasa" ], + "names": [ "Kimwera", "Kinyasa", "Mbamba Bay", "Mwera (Nyasa)", "Nyanza", "Nyasa" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -70850,7 +74094,7 @@ "full": "mji-Latn-CN", "iana": [ "Kim Mun" ], "iso639_3": "mji", - "localnames": [ "Kem di mun" ], + "localnames": [ "Kem di mun", "Kim Mun" ], "name": "Kim Mun", "names": [ "Chasan Yao", "Coc Mun", "Dao Ao Dai", "Dao Lam Dinh", "Dao Quan Trang", "Dao Thanh Y", "Gem Mun", "Great Tunic Yao", "Hainan Miao", "Jim Mun", "Jinmen", "Kem Mun", "Kem di mun", "Kimmun", "Lan Ten", "Lan Tin", "Lanten", "Lantin", "Lowland Yao", "Man Lan-Tien", "Man Lantien", "Men", "Mun", "Red Trouser Yao", "San Chi", "Shanzi Yao" ], "region": "CN", @@ -70880,8 +74124,8 @@ "full": "mjk-Latn-PG", "iana": [ "Matukar" ], "iso639_3": "mjk", - "name": "Matukar", - "names": [ "Matugar", "Matukar Panau" ], + "name": "Matukar Panau", + "names": [ "Matugar", "Matukar" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -70979,34 +74223,34 @@ "windows": "mjp-Zyyy" }, { - "full": "mjq-Zyyy-IN", + "full": "mjq-Mlym-IN", "iana": [ "Malaryan" ], "iso639_3": "mjq", "name": "Malaryan", "names": [ "Arayans", "Karingal", "Malai Arayan", "Malayarayan", "Malayarayar", "Male Arayans", "Maley Arayan", "Vazhiyammar" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Mlym", "sldr": false, "tag": "mjq", - "tags": [ "mjq-IN", "mjq-Zyyy" ], + "tags": [ "mjq-IN", "mjq-Mlym" ], "unwritten": true, - "windows": "mjq-Zyyy" + "windows": "mjq-Mlym" }, { - "full": "mjr-Zyyy-IN", + "full": "mjr-Mlym-IN", "iana": [ "Malavedan" ], "iso639_3": "mjr", "name": "Malavedan", "names": [ "Malai Vedan", "Malavetan", "Towetan", "Veda Bhasha", "Vedans", "Vettava Bhasha" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Mlym", "sldr": false, "tag": "mjr", - "tags": [ "mjr-IN", "mjr-Zyyy" ], + "tags": [ "mjr-IN", "mjr-Mlym" ], "unwritten": true, - "windows": "mjr-Zyyy" + "windows": "mjr-Mlym" }, { "full": "mjs-Latn-NG", @@ -71038,7 +74282,7 @@ "sldr": false, "tag": "mjt", "tags": [ "mjt-Deva", "mjt-IN" ], - "windows": "mjt-Deva" + "windows": "mjt-Deva-IN" }, { "full": "mjt-Beng-BD", @@ -71054,7 +74298,7 @@ "sldr": false, "tag": "mjt-Beng", "tags": [ "mjt-BD" ], - "windows": "mjt-Beng" + "windows": "mjt-Beng-BD" }, { "full": "mju-Telu-IN", @@ -71114,23 +74358,41 @@ "windows": "mjx-Latn" }, { - "full": "mjy-Zyyy-US", + "full": "mjx-Beng-BD", + "iana": [ "Mahali" ], + "iso639_3": "mjx", + "name": "Mahali", + "names": [ "Mahle" ], + "nophonvars": true, + "region": "BD", + "regionname": "Bangladesh", + "script": "Beng", + "sldr": false, + "tag": "mjx-Beng", + "windows": "mjx-Beng" + }, + { + "full": "mjy-Latn-US", "iana": [ "Mahican" ], "iso639_3": "mjy", + "localnames": [ "Mã’eekaneeweexthowãakan" ], "name": "Mahican", + "names": [ "Mohican", "Mã’eekaneeweexthowãakan" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "mjy", - "tags": [ "mjy-US", "mjy-Zyyy" ], + "tags": [ "mjy-Latn", "mjy-US" ], "unwritten": true, - "windows": "mjy-Zyyy" + "windows": "mjy-Latn" }, { "full": "mjz-Deva-NP", "iana": [ "Majhi" ], "iso639_3": "mjz", + "latnnames": [ "Majhi" ], + "localnames": [ "माझी" ], "name": "Majhi", "names": [ "Manjhi" ], "region": "NP", @@ -71150,10 +74412,10 @@ "localname": "македонски", "localnames": [ "македонски", "македонски јазик" ], "name": "Macedonian", - "names": [ "Macedonian Slavic", "Slavic" ], + "names": [ "Macedoneana", "Macedonian Slavic", "Slavic" ], "region": "MK", "regionname": "North Macedonia", - "regions": [ "AL", "AU", "BG", "CA", "DE", "GR", "HR", "ME", "RS", "SI", "US" ], + "regions": [ "AL", "AU", "BG", "CA", "DE", "GR", "HR", "ME", "RO", "RS", "SI", "US" ], "script": "Cyrl", "sldr": true, "suppress": true, @@ -71224,7 +74486,7 @@ "iana": [ "Mawchi" ], "iso639_3": "mke", "name": "Mawchi", - "names": [ "Mauchi", "Mavchi", "Mawachi", "Mawchi Bhil", "Mowchi" ], + "names": [ "Mauchi", "Mavchi", "Mawachi", "Mawchi Bhil", "Mowchi", "Māwəchī" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -71266,6 +74528,8 @@ "full": "mki-Arab-PK", "iana": [ "Dhatki" ], "iso639_3": "mki", + "latnnames": [ "Dhatki" ], + "localnames": [ "ڍاٽڪِي" ], "name": "Dhatki", "names": [ "Dhati", "Thar", "Thari" ], "region": "PK", @@ -71275,7 +74539,22 @@ "sldr": true, "tag": "mki", "tags": [ "mki-Arab", "mki-PK" ], - "windows": "mki-Arab" + "windows": "mki-Arab-PK" + }, + { + "full": "mki-Deva-IN", + "iana": [ "Dhatki" ], + "iso639_3": "mki", + "name": "Dhatki", + "names": [ "Thar" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "mki-Deva", + "tags": [ "mki-IN" ], + "windows": "mki-Deva-IN" }, { "full": "mkj-Latn-FM", @@ -71315,7 +74594,7 @@ "localname": "Ede-Mɔkɔle", "localnames": [ "Ede-Mɔkɔle" ], "name": "Mokole", - "names": [ "Ede-Mɔkɔle", "Féri", "Mokollé", "Mokolé", "Mokwale", "Monkole", "Mɔ̄kɔ́lé" ], + "names": [ "Ede-Mɔkɔle", "Féri", "Mokollé", "Mokolé", "Mokwale", "Monkolé", "Mɔ̄kɔ́lé" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -71359,7 +74638,7 @@ "iana": [ "Mingang Doso" ], "iso639_3": "mko", "name": "Mingang Doso", - "names": [ "Doso", "Munga Doso", "Ngwai Mungàn" ], + "names": [ "Doso", "Munga", "Munga Doso", "Ngwai Mungàn" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -71388,6 +74667,7 @@ "iana": [ "Bay Miwok" ], "iso639_3": "mkq", "name": "Bay Miwok", + "names": [ "Saclan", "Saklan" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -71401,7 +74681,8 @@ "full": "mkr-Latn-PG", "iana": [ "Malas" ], "iso639_3": "mkr", - "name": "Malas", + "name": "Manep", + "names": [ "Malas" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -71416,9 +74697,9 @@ "iana": [ "Silacayoapan Mixtec" ], "iso639_3": "mks", "localname": "tu̱hun ndáhví", - "localnames": [ "tu̱hun ndáhví" ], + "localnames": [ "Tu̱hun ndáhví" ], "name": "Mixtec, Silacayoapan", - "names": [ "Silacayoapan Mixtec", "tu̱hun ndáhví" ], + "names": [ "Mixteco de Santo Domingo Tonalá", "Mixteco del Oeste Alto", "Silacayoapan Mixtec", "To’on nda’vi", "Tu̱hun ndáhví", "Tu’un davi" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -71447,8 +74728,9 @@ "full": "mku-Latn-GN", "iana": [ "Konyanka Maninka" ], "iso639_3": "mku", + "macrolang": "myq", "name": "Maninka, Konyanka", - "names": [ "Konya", "Konyakakan", "Konyanka", "Konyanka Maninka", "Maninya" ], + "names": [ "Koniaka", "Konya", "Konyakakan", "Konyanka", "Konyanka Maninka", "Maninya" ], "region": "GN", "regionname": "Guinea", "regions": [ "LR" ], @@ -71720,7 +75002,7 @@ "iso639_3": "mlk", "localnames": [ "Kiwilwana" ], "name": "Kiwilwana", - "names": [ "Elwana", "Ilwana" ], + "names": [ "Elwana", "Ilwana", "Walwana" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -71804,33 +75086,36 @@ "windows": "mlp-Latn" }, { - "full": "mlq-Arab-SN", + "full": "mlq-Latn-SN", "iana": [ "Western Maninkakan" ], "iso639_3": "mlq", + "macrolang": "myq", "name": "Maninkakan, Western", "names": [ "Malinka", "Malinke", "Malinké", "Maninga", "Maninka", "Maninka-Western", "Maninkakan", "Maninkaxan", "Northwestern Maninka", "Western Malinke", "Western Maninkakan" ], - "nophonvars": true, "region": "SN", "regionname": "Senegal", "regions": [ "GM", "GW", "ML" ], - "script": "Arab", + "script": "Latn", "sldr": false, - "tag": "mlq-Arab", - "windows": "mlq-Arab" + "tag": "mlq", + "tags": [ "mlq-Latn", "mlq-SN" ], + "windows": "mlq-Latn" }, { - "full": "mlq-Latn-SN", + "full": "mlq-Arab-SN", "iana": [ "Western Maninkakan" ], "iso639_3": "mlq", + "macrolang": "myq", "name": "Maninkakan, Western", "names": [ "Malinka", "Malinke", "Malinké", "Maninga", "Maninka", "Maninka-Western", "Maninkakan", "Maninkaxan", "Northwestern Maninka", "Western Malinke", "Western Maninkakan" ], + "nophonvars": true, "region": "SN", "regionname": "Senegal", "regions": [ "GM", "GW", "ML" ], - "script": "Latn", + "script": "Arab", "sldr": false, - "tag": "mlq-Latn", - "windows": "mlq-Latn" + "tag": "mlq-Arab", + "windows": "mlq-Arab" }, { "full": "mlr-Latn-CM", @@ -71842,7 +75127,7 @@ "region": "CM", "regionname": "Cameroon", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mlr", "tags": [ "mlr-CM", "mlr-Latn" ], "windows": "mlr-Latn" @@ -71851,9 +75136,9 @@ "full": "mls-Latn-SD", "iana": [ "Masalit" ], "iso639_3": "mls", - "localnames": [ "Kana Masaraka" ], + "localnames": [ "Kanaa Masarak", "Masarak" ], "name": "Masalit", - "names": [ "Kaana Masala", "Kana Masaraka", "Masale", "Masara", "Masaraka", "Massalit", "Massolit", "Mesalit" ], + "names": [ "Kaana Masala", "Kana Masalaka", "Kana Masaraka", "Kanaa Masarak", "Kanaa Masaraka", "Masale", "Masara", "Masarak", "Masaraka", "Massalit", "Massolit", "Mesalit" ], "region": "SD", "regionname": "Sudan", "regions": [ "TD" ], @@ -71968,9 +75253,9 @@ "full": "mmc-Latn-MX", "iana": [ "Michoacán Mazahua" ], "iso639_3": "mmc", - "localnames": [ "Jnatjo" ], + "localnames": [ "Jñatjo" ], "name": "Mazahua, Michoacán", - "names": [ "Jnatjo", "Mazahua de occidente", "Michoacán Mazahua" ], + "names": [ "Jñatjo", "Mazahua de occidente", "Michoacán Mazahua" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -72040,7 +75325,8 @@ "full": "mmf-Latn-NG", "iana": [ "Mundat" ], "iso639_3": "mmf", - "name": "Mundat", + "name": "Mindat", + "names": [ "Mundat" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -72054,8 +75340,9 @@ "full": "mmg-Latn-VU", "iana": [ "North Ambrym" ], "iso639_3": "mmg", + "localnames": [ "Rral", "Tolongken" ], "name": "Ambrym, North", - "names": [ "North Ambrym" ], + "names": [ "North Ambrym", "Rral", "Tolongken" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -72084,15 +75371,15 @@ "full": "mmi-Latn-PG", "iana": [ "Musar" ], "iso639_3": "mmi", + "localnames": [ "Hember Avu" ], "name": "Musar", - "names": [ "Amben", "Aregerek" ], + "names": [ "Amben", "Aregerek", "Hember Avu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "mmi", "tags": [ "mmi-Latn", "mmi-PG" ], - "unwritten": true, "windows": "mmi-Latn" }, { @@ -72159,8 +75446,8 @@ "iso639_3": "mmn", "localname": "Minamanwa", "localnames": [ "Minamanwa" ], - "name": "Mamanwa", - "names": [ "Mamanwa Negrito", "Minamanwa" ], + "name": "Minamanwa", + "names": [ "Mamanwa", "Mamanwa Negrito" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -72216,8 +75503,9 @@ "full": "mmr-Latn-CN", "iana": [ "Western Xiangxi Miao" ], "iso639_3": "mmr", + "macrolang": "hmn", "name": "Miao, Western Xiangxi", - "names": [ "Eastern Miao", "Ghao-Xong", "Hsianghsi Miao", "Huayuan Miao", "Meo Do", "Northern Miao", "Red Meo", "Red Miao", "West Hunan Miao", "Western Ghao-Xong", "Western West-Hunan Miao", "Western Xiangxi Miao" ], + "names": [ "Eastern Miao", "Ghao-Xong", "Hsianghsi Miao", "Huayuan Miao", "Meo Do", "Northern Miao", "Red Meo", "Red Miao", "West Hunan Miao", "Western Ghao-Xong", "Western West-Hunan Miao", "Western Xiangxi Miao", "Xong" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -72231,7 +75519,7 @@ "iana": [ "Malalamai" ], "iso639_3": "mmt", "name": "Mala", - "names": [ "Bonga", "Garpunei", "Malalamai" ], + "names": [ "Bonga", "Garingei", "Garpunei", "Malalamai" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -72246,9 +75534,9 @@ "iana": [ "Mmaala" ], "iso639_3": "mmu", "localname": "Numaala", - "localnames": [ "Numaala" ], + "localnames": [ "Numála" ], "name": "Mmaala", - "names": [ "Central Yambassa", "Mmala", "Numaala", "Numala", "Yambassa" ], + "names": [ "Central Yambassa", "Mmahla", "Mmala", "Numaala", "Numala", "Nummáalá", "Numála", "Yambassa" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -72307,11 +75595,11 @@ "iso639_3": "mmy", "localnames": [ "Migaama" ], "name": "Migaama", - "names": [ "Dionkor", "Djonkor", "Dyongor", "Jongor", "Jonkor", "Migama" ], + "names": [ "Migama" ], "region": "TD", "regionname": "Chad", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mmy", "tags": [ "mmy-Latn", "mmy-TD" ], "windows": "mmy-Latn" @@ -72338,8 +75626,8 @@ "latnnames": [ "Mongol khel" ], "localname": "монгол", "localnames": [ "Монгол хэл" ], - "name": "Mongolian, Halh", - "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian" ], + "name": "Mongolian", + "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian, Halh" ], "region": "MN", "regionname": "Mongolia", "regions": [ "CZ", "KG", "RU", "TW", "US" ], @@ -72347,14 +75635,14 @@ "sldr": true, "tag": "mn", "tags": [ "drh", "drh-Cyrl", "drh-Cyrl-MN", "drh-MN", "khk", "khk-Cyrl", "khk-Cyrl-MN", "khk-MN", "mn-Cyrl", "mn-MN" ], - "windows": "mn-Cyrl" + "windows": "mn-Cyrl-MN" }, { "full": "mn-Brai-MN", "iana": [ "Mongolian" ], "iso639_3": "mon", - "name": "Mongolian, Halh", - "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian" ], + "name": "Mongolian", + "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian, Halh" ], "nophonvars": true, "region": "MN", "regionname": "Mongolia", @@ -72363,7 +75651,7 @@ "sldr": false, "tag": "mn-Brai", "tags": [ "khk-Brai", "khk-Brai-MN" ], - "windows": "mn-Brai" + "windows": "mn-Brai-MN" }, { "full": "mn-Mong-CN", @@ -72371,28 +75659,16 @@ "iso639_3": "mon", "localname": "монгол", "name": "Mongolian", + "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian, Halh" ], "region": "CN", "regionname": "China", + "regions": [ "CZ", "KG", "MN", "RU", "TW", "US" ], "script": "Mong", "sldr": true, "tag": "mn-CN", - "tags": [ "mn-Mong" ], + "tags": [ "drh-Mong", "drh-Mong-CN", "khk-Mong", "khk-Mong-CN", "mn-Mong" ], "windows": "mn-Mong-CN" }, - { - "full": "mn-Mong-MN", - "iana": [ "Mongolian" ], - "name": "Mongolian, Halh", - "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian" ], - "region": "MN", - "regionname": "Mongolia", - "regions": [ "CZ", "KG", "RU", "TW", "US" ], - "script": "Mong", - "sldr": false, - "tag": "mn-Mong-MN", - "tags": [ "khk-Mong", "khk-Mong-MN" ], - "windows": "mn-Mong-MN" - }, { "full": "mn-Phag-CN", "iana": [ "Mongolian" ], @@ -72406,13 +75682,15 @@ "script": "Phag", "sldr": false, "tag": "mn-Phag", - "windows": "mn-Phag" + "windows": "mn-Phag-CN" }, { "full": "mn-Phag-MN", "iana": [ "Mongolian" ], + "iso639_3": "khk", + "macrolang": "mn", "name": "Mongolian, Halh", - "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian" ], + "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian" ], "region": "MN", "regionname": "Mongolia", "regions": [ "CZ", "KG", "RU", "TW", "US" ], @@ -72426,8 +75704,8 @@ "full": "mn-Tibt-CN", "iana": [ "Mongolian" ], "iso639_3": "mon", - "name": "Mongolian, Halh", - "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian" ], + "name": "Mongolian", + "names": [ "Central Mongolian", "Halh", "Halha", "Kalkh", "Khalkh", "Khalkha", "Khalkha Mongolian", "Mongol", "Mongolian, Halh" ], "nophonvars": true, "obsolete": true, "region": "CN", @@ -72437,7 +75715,7 @@ "sldr": false, "tag": "mn-Tibt", "tags": [ "khk-Tibt", "khk-Tibt-CN" ], - "windows": "mn-Tibt" + "windows": "mn-Tibt-CN" }, { "full": "mna-Latn-PG", @@ -72476,46 +75754,14 @@ "iso639_3": "mnc", "name": "Manchu", "names": [ "Man", "Manju Gisun" ], - "nophonvars": true, "region": "CN", "regionname": "China", "script": "Mong", "sldr": false, - "tag": "mnc-Mong", - "tags": [ "mnc-CN" ], + "tag": "mnc", + "tags": [ "mnc-CN", "mnc-Mong" ], "windows": "mnc-Mong" }, - { - "full": "mnc-Mong-CN-x-manchu", - "iana": [ "Manchu" ], - "iso639_3": "mnc", - "name": "Manchu", - "names": [ "Man", "Manju Gisun" ], - "nophonvars": true, - "region": "CN", - "regionname": "China", - "script": "Mong", - "sldr": false, - "tag": "mnc-Mong-x-manchu", - "tags": [ "mnc-CN-x-manchu" ], - "windows": "mnc-Mong-x-manchu" - }, - { - "full": "mnc-Mong-CN-x-olmanchu", - "iana": [ "Manchu" ], - "iso639_3": "mnc", - "name": "Manchu", - "names": [ "Man", "Manju Gisun" ], - "nophonvars": true, - "obsolete": true, - "region": "CN", - "regionname": "China", - "script": "Mong", - "sldr": false, - "tag": "mnc-Mong-x-olmanchu", - "tags": [ "mnc-CN-x-olmanchu" ], - "windows": "mnc-Mong-x-olmanchu" - }, { "full": "mnd-Latn-BR", "iana": [ "Mondé" ], @@ -72542,7 +75788,6 @@ "sldr": false, "tag": "mne", "tags": [ "mne-Latn", "mne-TD" ], - "unwritten": true, "windows": "mne-Latn" }, { @@ -72580,12 +75825,13 @@ "full": "mnh-Latn-CD", "iana": [ "Mono (Democratic Republic of Congo)" ], "iso639_3": "mnh", + "localnames": [ "Mɔnɔ" ], "name": "Mono", - "names": [ "Amono", "Mono (Democratic Republic of Congo)" ], + "names": [ "Amono", "Mono (Democratic Republic of Congo)", "Mɔnɔ" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mnh", "tags": [ "mnh-CD", "mnh-Latn" ], "windows": "mnh-Latn" @@ -72595,6 +75841,7 @@ "iana": [ "Manipuri" ], "iso639_3": "mni", "latnnames": [ "Meiteilon" ], + "localname": "মৈতৈলোন্", "localnames": [ "মৈতৈলোন্" ], "name": "Meitei", "names": [ "Kathe", "Kathi", "Manipuri", "Meetei", "Meeteilon", "Meitei Manipuri", "Meiteilon", "Meiteiron", "Meithe", "Meithei", "Menipuri", "Mitei", "Mithe", "Ponna" ], @@ -72602,7 +75849,7 @@ "regionname": "India", "regions": [ "BD" ], "script": "Beng", - "sldr": false, + "sldr": true, "tag": "mni", "tags": [ "mni-Beng", "mni-IN" ], "windows": "mni-Beng" @@ -72645,13 +75892,12 @@ "localnames": [ "منجانی" ], "name": "Munji", "names": [ "Munjani", "Munjigi", "Munjiwar" ], - "nophonvars": true, "region": "AF", "regionname": "Afghanistan", "script": "Arab", "sldr": false, - "tag": "mnj-Arab", - "tags": [ "mnj-AF" ], + "tag": "mnj", + "tags": [ "mnj-AF", "mnj-Arab" ], "windows": "mnj-Arab" }, { @@ -72701,12 +75947,13 @@ "full": "mnp-Latn-CN", "iana": [ "Min Bei Chinese" ], "iso639_3": "mnp", + "macrolang": "zh", "name": "Chinese, Min Bei", "names": [ "Min Bei Chinese", "Min Pei", "Northern Fujian", "Northern Min" ], "obsolete": true, "region": "CN", "regionname": "China", - "regions": [ "SG" ], + "regions": [ "SG", "TH" ], "script": "Latn", "sldr": false, "tag": "mnp", @@ -72784,7 +76031,7 @@ "region": "SB", "regionname": "Solomon Islands", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mnv", "tags": [ "mnv-Latn", "mnv-SB" ], "windows": "mnv-Latn" @@ -72793,6 +76040,8 @@ "full": "mnw-Mymr-MM", "iana": [ "Mon" ], "iso639_3": "mnw", + "latnnames": [ "bharsar maan" ], + "localnames": [ "ဘာသာ မန်" ], "name": "Mon", "names": [ "Aleng", "Mou", "Mun", "Peguan", "Raman", "Rman", "Rmen", "Takanoon", "Talaing", "Taleng", "Teguan" ], "region": "MM", @@ -72878,22 +76127,23 @@ "windows": "moc-Latn" }, { - "full": "mod-Zyyy-US", + "full": "mod-Latn-US", "iana": [ "Mobilian" ], "iso639_3": "mod", "name": "Mobilian", + "names": [ "Chickasaw-Choctaw Trade Language", "Mobilian Jargon", "Mobilian Trade Jargon", "Mobilian Trade Language", "Yamá" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "mod", - "tags": [ "mod-US", "mod-Zyyy" ], + "tags": [ "mod-Latn", "mod-US" ], "unwritten": true, - "windows": "mod-Zyyy" + "windows": "mod-Latn" }, { "full": "moe-Latn-CA", - "iana": [ "Montagnais" ], + "iana": [ "Innu", "Montagnais" ], "iso639_3": "moe", "localnames": [ "Innu", "Innu Aimun" ], "name": "Innu", @@ -73064,8 +76314,8 @@ "iana": [ "Mossi" ], "iso639_3": "mos", "localname": "Mòoré", - "localnames": [ "Mòoré" ], - "name": "Mòoré", + "localnames": [ "Moore" ], + "name": "Moore", "names": [ "Mole", "Moose", "More", "Moré", "Moshi", "Mossi" ], "region": "BF", "regionname": "Burkina Faso", @@ -73080,7 +76330,7 @@ "full": "mos-Brai-BF", "iana": [ "Mossi" ], "iso639_3": "mos", - "name": "Mòoré", + "name": "Moore", "names": [ "Mole", "Moose", "More", "Moré", "Moshi", "Mossi" ], "nophonvars": true, "region": "BF", @@ -73096,12 +76346,12 @@ "iana": [ "Barí" ], "iso639_3": "mot", "name": "Barí", - "names": [ "Bari", "Barira", "Cunausaya", "Dobocubi", "Motilone", "Motilón" ], + "names": [ "Baria", "Barira", "Cunausaya", "Motilone", "Motilón" ], "region": "CO", "regionname": "Colombia", "regions": [ "VE" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mot", "tags": [ "mot-CO", "mot-Latn" ], "unwritten": true, @@ -73111,8 +76361,9 @@ "full": "mou-Latn-TD", "iana": [ "Mogum" ], "iso639_3": "mou", + "localnames": [ "Mogume" ], "name": "Mogum", - "names": [ "Mogoum" ], + "names": [ "Mogoum", "Mogume" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -73181,6 +76432,21 @@ "tags": [ "moy-ET", "moy-Latn" ], "windows": "moy-Latn" }, + { + "full": "moy-Ethi-ET", + "iana": [ "Shekkacho" ], + "iso639_3": "moy", + "localname": "Shekkacho", + "name": "Shekkacho", + "nophonvars": true, + "obsolete": true, + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": true, + "tag": "moy-Ethi", + "windows": "moy-Ethi" + }, { "full": "moz-Latn-TD", "iana": [ "Mukulu", "Gergiko" ], @@ -73203,7 +76469,7 @@ "iso639_3": "mpa", "localnames": [ "Chimpoto" ], "name": "Mpoto", - "names": [ "Chimpoto", "Cimpoto", "Kimpoto", "Kinyasa", "Nyasa" ], + "names": [ "ChiNyasa", "Chimpoto", "Cimpoto", "Kimpoto", "Kinyasa", "Nyasa" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -73378,7 +76644,7 @@ "iana": [ "Yosondúa Mixtec" ], "iso639_3": "mpm", "name": "Mixtec, Yosondúa", - "names": [ "Mixteco de Santiago Yosondúa", "Southern Tlaxiaco Mixtec", "Yosondúa Mixtec" ], + "names": [ "Mixteco de Santiago Yosondúa", "Mixteco de Yosondúa", "Sa’an sau", "Southern Tlaxiaco Mixtec", "Yosondúa Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -73440,7 +76706,6 @@ "sldr": false, "tag": "mpq", "tags": [ "mpq-BR", "mpq-Latn" ], - "unwritten": true, "windows": "mpq-Latn" }, { @@ -73454,7 +76719,6 @@ "sldr": false, "tag": "mpr", "tags": [ "mpr-Latn", "mpr-SB" ], - "unwritten": true, "windows": "mpr-Latn" }, { @@ -73480,7 +76744,7 @@ "localname": "Mian weng", "localnames": [ "Mian weng" ], "name": "Mian", - "names": [ "Mian weng", "Mianmin" ], + "names": [ "Mian weng", "Mianmin", "Miyanmin" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -73570,6 +76834,8 @@ "full": "mpz-Thai-TH", "iana": [ "Mpi" ], "iso639_3": "mpz", + "latnnames": [ "Kaw", "Mpi" ], + "localnames": [ "ก้อ", "มปี้" ], "name": "Mpi", "names": [ "Kaw" ], "region": "TH", @@ -73658,6 +76924,7 @@ "full": "mqg-Latn-ID", "iana": [ "Kota Bangun Kutai Malay" ], "iso639_3": "mqg", + "macrolang": "ms", "name": "Malay, Kota Bangun Kutai", "names": [ "Kota Bangun Kutai Malay" ], "region": "ID", @@ -73734,8 +77001,9 @@ "full": "mql-Latn-BJ", "iana": [ "Mbelime" ], "iso639_3": "mql", + "localnames": [ "Mbɛlímɛ̀" ], "name": "Mbelime", - "names": [ "Bebelibe", "Bèbèdibè", "Mbilme", "Mbèlimè", "Oubièlo", "Ubielo" ], + "names": [ "Bebelibe", "Bèbèdibè", "Mbilme", "Mbèlimè", "Mbɛlímɛ̀", "M̄ bɛ̄dímɛ", "Oubièlo", "Ubielo" ], "region": "BJ", "regionname": "Benin", "regions": [ "TG" ], @@ -73869,7 +77137,7 @@ "iso639_3": "mqu", "localnames": [ "Mundari" ], "name": "Mandari", - "names": [ "Chir", "Kir", "Mondari", "Mundari", "Shir" ], + "names": [ "Chir", "Kir", "Kútúk nà mùndárì", "Mondari", "Mundari", "Shir" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -74080,7 +77348,8 @@ "full": "mre-Zxxx-US", "iana": [ "Martha's Vineyard Sign Language" ], "iso639_3": "mre", - "name": "Martha's Vineyard Sign Language", + "name": "Martha’s Vineyard Sign Language", + "names": [ "Martha's Vineyard Sign Language" ], "region": "US", "regionname": "United States", "regions": [ "GB" ], @@ -74152,9 +77421,9 @@ "full": "mrh-Latn-IN", "iana": [ "Mara Chin" ], "iso639_3": "mrh", - "localnames": [ "Mara" ], + "localnames": [ "Mara", "Mara reih" ], "name": "Chin, Mara", - "names": [ "Lakher", "Mara", "Mara Chin", "Maram", "Mira", "Miram", "Zao" ], + "names": [ "Lakher", "Mara", "Mara Chin", "Mara reih", "Maram", "Mira", "Miram", "Zao" ], "region": "IN", "regionname": "India", "regions": [ "MM" ], @@ -74170,6 +77439,7 @@ "iso639_3": "mrj", "latnnames": [ "Kyryk mary jÿlmÿ", "Kyryk mary" ], "localnames": [ "Кырык мары йӹлмӹ", "кырык мары" ], + "macrolang": "chm", "name": "Mari, Hill", "names": [ "Cheremis", "Gorno-Mariy", "High Mari", "Highland Mari", "Mari-Hills", "Western Mari" ], "region": "RU", @@ -74258,6 +77528,7 @@ "full": "mro-Latn-BD", "iana": [ "Mru" ], "iso639_3": "mro", + "localnames": [ "Mru" ], "name": "Mru", "names": [ "Dak", "Launghu", "Maru", "Mro", "Mrucha", "Mrung", "Mrusa", "Murung", "Niopheng", "Niopreng", "Taung Mru" ], "region": "BD", @@ -74273,7 +77544,7 @@ "iana": [ "Morouas" ], "iso639_3": "mrp", "name": "Morouas", - "names": [ "Moruas" ], + "names": [ "Ande", "Moruas" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -74303,8 +77574,10 @@ "full": "mrr-Deva-IN", "iana": [ "Maria (India)" ], "iso639_3": "mrr", + "latnnames": [ "Maadiya Gond" ], + "localnames": [ "माडिया गोंड" ], "name": "Maria", - "names": [ "Hill Maria", "Madi", "Madia", "Madiya", "Maria (India)", "Modh", "Modi" ], + "names": [ "Hill Madia", "Hill Maria", "Madi", "Madia", "Madia Gond", "Madiya", "Maria (India)", "Modh", "Modi" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -74388,6 +77661,20 @@ "tags": [ "mrw-Latn", "mrw-PH" ], "windows": "mrw-Latn" }, + { + "full": "mrw-Arab-PH", + "iana": [ "Maranao" ], + "iso639_3": "mrw", + "name": "Maranao", + "names": [ "Maranaw", "Mëranaw", "Ranao" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "script": "Arab", + "sldr": false, + "tag": "mrw-Arab", + "windows": "mrw-Arab" + }, { "full": "mrx-Latn-ID", "iana": [ "Maremgi", "Dineor" ], @@ -74438,8 +77725,8 @@ "iso639_3": "msa", "localname": "Melayu", "localnames": [ "Bahasa Malaysia" ], - "name": "Malay, Standard", - "names": [ "Bahasa Malaysia", "Bahasa Malayu", "Formal Malay", "Malay", "Malay (macrolanguage)", "Malayu", "Melayu", "Melayu Baku" ], + "name": "Malay (macrolanguage)", + "names": [ "Bahasa Malaysia", "Bahasa Malayu", "Bahasa Melayu Malaysia", "Formal Malay", "Malay", "Malay, Standard", "Malaysian Malay", "Malayu", "Melayu", "Melayu Baku" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -74447,15 +77734,17 @@ "suppress": true, "tag": "ms", "tags": [ "ms-Latn", "ms-MY", "ms-zsm", "ms-zsm-MY", "zsm", "zsm-Latn", "zsm-Latn-MY", "zsm-MY" ], - "windows": "ms" + "windows": "ms-MY" }, { "full": "ms-Arab-MY", "iana": [ "Malay (macrolanguage)" ], "iso639_3": "msa", - "localname": "بهاس ملايو", - "name": "Malay, Standard", - "names": [ "Bahasa Malaysia", "Bahasa Malayu", "Formal Malay", "Malay", "Malay (macrolanguage)", "Malayu", "Melayu", "Melayu Baku" ], + "latnnames": [ "Bahasa Malaysia" ], + "localname": "بهاس مليسيا", + "localnames": [ "بهاس مليسيا" ], + "name": "Malay (macrolanguage)", + "names": [ "Bahasa Malaysia", "Bahasa Malayu", "Bahasa Melayu Malaysia", "Formal Malay", "Malay", "Malay, Standard", "Malaysian Malay", "Malayu", "Melayu", "Melayu Baku" ], "nophonvars": true, "region": "MY", "regionname": "Malaysia", @@ -74464,7 +77753,7 @@ "sldr": true, "tag": "ms-Arab", "tags": [ "zsm-Arab", "zsm-Arab-MY" ], - "windows": "ms-Arab" + "windows": "ms-Arab-MY" }, { "full": "ms-Latn-BN", @@ -74476,16 +77765,16 @@ "regionname": "Brunei Darussalam", "script": "Latn", "sldr": true, - "suppress": true, "tag": "ms-BN", - "windows": "ms-BN" + "windows": "ms-Latn-BN" }, { "full": "ms-Brai-MY", "iana": [ "Malay (macrolanguage)" ], "iso639_3": "zsm", + "macrolang": "ms", "name": "Malay, Standard", - "names": [ "Bahasa Malaysia", "Bahasa Malayu", "Formal Malay", "Malay", "Malay (macrolanguage)", "Malayu", "Melayu", "Melayu Baku" ], + "names": [ "Bahasa Malaysia", "Bahasa Malayu", "Bahasa Melayu Malaysia", "Formal Malay", "Malay", "Malay (macrolanguage)", "Malaysian Malay", "Malayu", "Melayu", "Melayu Baku" ], "nophonvars": true, "region": "MY", "regionname": "Malaysia", @@ -74494,13 +77783,14 @@ "sldr": false, "tag": "ms-Brai", "tags": [ "zsm-Brai", "zsm-Brai-MY" ], - "windows": "ms-Brai" + "windows": "ms-Brai-MY" }, { "full": "ms-Arab-CC", "iana": [ "Malay (macrolanguage)" ], "iso639_3": "msa", "name": "Malay (macrolanguage)", + "obsolete": true, "region": "CC", "regionname": "Cocos (Keeling) Islands", "script": "Arab", @@ -74513,10 +77803,11 @@ "iana": [ "Malay (macrolanguage)" ], "iso639_3": "msa", "name": "Malay (macrolanguage)", + "obsolete": true, "region": "ID", "regionname": "Indonesia", "script": "Arab", - "sldr": false, + "sldr": true, "tag": "ms-ID", "windows": "ms-Arab-ID" }, @@ -74530,16 +77821,15 @@ "regionname": "Singapore", "script": "Latn", "sldr": true, - "suppress": true, "tag": "ms-SG", - "windows": "ms-SG" + "windows": "ms-Latn-SG" }, { "full": "msb-Latn-PH", "iana": [ "Masbatenyo" ], "iso639_3": "msb", "localname": "Masbatenyo", - "localnames": [ "Masbatenyo" ], + "localnames": [ "Masbatenyo", "Minasbate" ], "name": "Masbatenyo", "names": [ "Masbateño", "Minasbate" ], "region": "PH", @@ -74555,8 +77845,10 @@ "iana": [ "Sankaran Maninka" ], "iso639_3": "msc", "localname": "Sankaran", + "localnames": [ "Sankaran" ], + "macrolang": "myq", "name": "Maninka, Sankaran", - "names": [ "Faranah", "Sankaran Maninka", "Sankarankan" ], + "names": [ "Faranah", "Sankaran", "Sankaran Maninka", "Sankarankan" ], "region": "GN", "regionname": "Guinea", "script": "Latn", @@ -74570,7 +77862,7 @@ "iana": [ "Yucatec Maya Sign Language" ], "iso639_3": "msd", "name": "Yucatec Maya Sign Language", - "names": [ "Chican Sign Language", "LSChicana", "Lengua de Señas Chicana", "Lenguaje Manual Maya", "MSL", "Maya Sign Language", "Mayan Sign Language", "Nohya Sign Language" ], + "names": [ "Chican Sign Language", "LSChicana", "LSMY", "Lengua de Señas Chicana", "Lengua de Señas Maya Yucateca", "Lenguaje Manual Maya", "MSL", "Maya Sign Language", "Mayan Sign Language", "Nohya Sign Language", "YMSL" ], "region": "MX", "regionname": "Mexico", "script": "Zxxx", @@ -74629,6 +77921,7 @@ "iana": [ "Masikoro Malagasy" ], "iso639_3": "msh", "localnames": [ "Masikoro Malagasy" ], + "macrolang": "mg", "name": "Malagasy, Masikoro", "names": [ "Masikoro Malagasy" ], "region": "MG", @@ -74644,6 +77937,7 @@ "iana": [ "Sabah Malay" ], "iso639_3": "msi", "localnames": [ "Bahasa Sabah" ], + "macrolang": "ms", "name": "Malay, Sabah", "names": [ "Bahasa Sabah", "Bazaar Malay", "Pasar Malay", "Sabah Malay", "Sabah Malay Dialect" ], "region": "MY", @@ -74781,6 +78075,7 @@ "iana": [ "Mongolian Sign Language" ], "iso639_3": "msr", "name": "Mongolian Sign Language", + "names": [ "MSL" ], "region": "MN", "regionname": "Mongolia", "script": "Zxxx", @@ -74838,8 +78133,9 @@ "full": "msw-Latn-GW", "iana": [ "Mansoanka" ], "iso639_3": "msw", + "localnames": [ "Suwa" ], "name": "Mansoanka", - "names": [ "Kunant", "Kunante", "Mansoanca", "Maswanka", "Sua" ], + "names": [ "Kunant", "Kunante", "Mansoanca", "Mansonka", "Maswanka", "Sua", "Suwa" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "GM" ], @@ -74869,8 +78165,9 @@ "iana": [ "Aruamu" ], "iso639_3": "msy", "localname": "Aruamu", + "localnames": [ "Aruamɨn Akam" ], "name": "Aruamu", - "names": [ "Ariawiai", "Makarub", "Makarup", "Mikarew", "Mikarew-Ariaw", "Mikarup" ], + "names": [ "Ariawiai", "Aruamɨn Akam", "Makarub", "Makarup", "Mikarew", "Mikarew-Ariaw", "Mikarup" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -74948,9 +78245,9 @@ "iana": [ "Cotabato Manobo" ], "iso639_3": "mta", "localname": "Menubù", - "localnames": [ "Menubù" ], + "localnames": [ "Kagi Cotabato Menubù", "Kagi Menubù" ], "name": "Manobo, Cotabato", - "names": [ "Cotabato Manobo", "Dulangan Manobo", "Menubù" ], + "names": [ "Cotabato Manobo", "Dulangan Manobo", "Kagi Cotabato Menubù", "Kagi Menubù", "Menubù" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -75110,8 +78407,8 @@ "full": "mtl-Latn-NG", "iana": [ "Montol" ], "iso639_3": "mtl", - "name": "Montol", - "names": [ "Baltap", "Montal", "Teel", "Tehl" ], + "name": "Tehl", + "names": [ "Baltap", "Montal", "Teel", "Tel" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -75126,6 +78423,7 @@ "iana": [ "Mator" ], "iso639_3": "mtm", "name": "Mator", + "names": [ "Mator-Taigi-Karagas", "Motor" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -75155,9 +78453,9 @@ "iana": [ "Totontepec Mixe" ], "iso639_3": "mto", "localname": "Ayuk", - "localnames": [ "Ayuk" ], + "localnames": [ "Ayöök" ], "name": "Mixe, Totontepec", - "names": [ "Ayuk", "Mixe Alto del Norte", "Northwestern Mixe", "Totontepec Mixe" ], + "names": [ "Ayooc", "Ayuk", "Ayuujk", "Ayuuk", "Ayuuk-Mixe", "Ayöök", "Mixe Alto del Norte", "North Highland Mixe", "Northwestern Mixe", "Totontepec Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -75171,8 +78469,8 @@ "iana": [ "Wichí Lhamtés Nocten" ], "iso639_3": "mtp", "localnames": [ "'weenhayeklhayhi'", "'weenhayeklhàmet" ], - "name": "Wichí Lhamtés Nocten", - "names": [ "Bolivian", "Nocten", "Noctenes", "Noctén", "Oktenai", "Weenhayek", "’weenhayeklhayhi’", "’weenhayeklhàmet" ], + "name": "Weenhayek", + "names": [ "Bolivian", "Noctenes", "Noctén", "Oktenai", "Wichí Lhamtés Nocten", "’weenhayeklhayhi’", "’weenhayeklhàmet" ], "region": "BO", "regionname": "Bolivia", "regions": [ "AR" ], @@ -75201,6 +78499,7 @@ "iso639_3": "mtr", "latnnames": [ "Mewadi" ], "localnames": [ "मेवाड़ी" ], + "macrolang": "mwr", "name": "Mewari", "names": [ "Mewadi" ], "region": "IN", @@ -75220,7 +78519,7 @@ "region": "PE", "regionname": "Peru", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mts", "tags": [ "mts-Latn", "mts-PE" ], "windows": "mts-Latn" @@ -75242,7 +78541,6 @@ "full": "mtu-Latn-MX", "iana": [ "Tututepec Mixtec" ], "iso639_3": "mtu", - "localnames": [ "Tu'un savi" ], "name": "Mixtec, Tututepec", "names": [ "Mixteco de San Pedro Tututepec", "Mixteco de Villa de Tututepec", "Tututepec Mixtec", "Tu’un savi" ], "region": "MX", @@ -75257,8 +78555,9 @@ "full": "mtv-Latn-PG", "iana": [ "Asaro'o" ], "iso639_3": "mtv", + "localnames": [ "Asarɨ’o" ], "name": "Asaro’o", - "names": [ "Asaro'o", "Morafa" ], + "names": [ "Asaro'o", "Asarɨ’o", "Morafa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -75286,7 +78585,6 @@ "full": "mtx-Latn-MX", "iana": [ "Tidaá Mixtec" ], "iso639_3": "mtx", - "localnames": [ "Tnu'un dawi" ], "name": "Mixtec, Tidaá", "names": [ "Mixteco de San Pedro Tidaá", "Mixteco de Tidaá", "North Central Nochixtlán Mixtec", "Tidaá Mixtec", "Tnu’un dawi" ], "region": "MX", @@ -75425,15 +78723,16 @@ "full": "mui-Latn-ID", "iana": [ "Musi" ], "iso639_3": "mui", + "localnames": [ "Baso Palembang" ], + "macrolang": "ms", "name": "Musi", - "names": [ "Baso Palembang", "Palembang", "Palembangnese", "Sekayu" ], + "names": [ "Baso Palembang", "Baso Pelembang", "Palembang", "Palembang Malay", "Palembangese", "Sekayu" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", "sldr": false, "tag": "mui", "tags": [ "ms-mui", "ms-mui-ID", "mui-ID", "mui-Latn" ], - "unwritten": true, "windows": "mui-Latn" }, { @@ -75454,8 +78753,8 @@ "full": "muk-Tibt-NP", "iana": [ "Mugom" ], "iso639_3": "muk", - "name": "Mugom", - "names": [ "Mugali", "Mugu", "Mugum" ], + "name": "Mugom-Karmarong", + "names": [ "Mugom", "Mugu Tibetan", "Mugum" ], "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], @@ -75482,7 +78781,9 @@ "full": "mum-Latn-PG", "iana": [ "Maiwala" ], "iso639_3": "mum", + "localnames": [ "Maiwalei" ], "name": "Maiwala", + "names": [ "Maiwalei" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -75496,7 +78797,7 @@ "iana": [ "Nyong" ], "iso639_3": "muo", "name": "Mubako", - "names": [ "Chukkol", "Daganonga", "Daganyonga", "Mumbake", "Ndagam", "Nyoking", "Nyong", "Nyongnepa", "Peti", "Samba Bali", "Teteka", "Yapeli" ], + "names": [ "Chukkol", "Daganonga", "Daganyonga", "Mumbake", "Ndagam", "Nyoking", "Nyong", "Nyongnepa", "Nyɔŋ", "Peti", "Samba Bali", "Teteka", "Yapeli" ], "region": "CM", "regionname": "Cameroon", "regions": [ "NG" ], @@ -75508,18 +78809,19 @@ "windows": "muo-Latn" }, { - "full": "muq-Zyyy-CN", + "full": "muq-Latn-CN", "iana": [ "Eastern Xiangxi Miao" ], "iso639_3": "muq", + "macrolang": "hmn", "name": "Miao, Eastern Xiangxi", "names": [ "Eastern Ghao-Xong", "Eastern Miao", "Eastern West-Hunan Miao", "Eastern Xiangxi Miao", "Ghao-Xong", "Hsianghsi Miao", "Meo Do", "Northern Miao", "Red Meo", "Red Miao" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "muq", - "tags": [ "muq-CN", "muq-Zyyy" ], - "windows": "muq-Zyyy" + "tags": [ "muq-CN", "muq-Latn" ], + "windows": "muq-Latn" }, { "full": "mur-Latn-SS", @@ -75645,6 +78947,8 @@ "full": "muz-Ethi-ET", "iana": [ "Mursi" ], "iso639_3": "muz", + "latnnames": [ "munɛn" ], + "localnames": [ "ሙነን" ], "name": "Mursi", "region": "ET", "regionname": "Ethiopia", @@ -75712,43 +79016,27 @@ "windows": "mvd-Latn" }, { - "full": "mve-Arab-PK-x-sindhi", - "iana": [ "Marwari" ], + "full": "mve-Arab-PK", + "iana": [ "Marwari (Pakistan)" ], "iso639_3": "mve", "latnnames": [ "Mārwāṛī" ], "localnames": [ "مارواڑی" ], + "macrolang": "mwr", "name": "Marwari", - "names": [ "Jaiselmer", "Marawar", "Merwari", "Rajasthani" ], - "nophonvars": true, + "names": [ "Jaiselmer", "Marawar", "Marwari (Pakistan)", "Merwari", "Rajasthani" ], "region": "PK", "regionname": "Pakistan", "script": "Arab", "sldr": false, - "tag": "mve-Arab", - "tags": [ "mve-Arab-PK", "mve-Arab-x-sindhi", "mve-PK-x-sindhi" ], + "tag": "mve", + "tags": [ "mve-Arab", "mve-PK" ], "windows": "mve-Arab" }, - { - "full": "mve-Arab-PK-x-urdu", - "iana": [ "Marwari" ], - "iso639_3": "mve", - "latnnames": [ "Mārwāṛī" ], - "localnames": [ "مارواڑی" ], - "name": "Marwari", - "names": [ "Jaiselmer", "Marawar", "Merwari", "Rajasthani" ], - "nophonvars": true, - "region": "PK", - "regionname": "Pakistan", - "script": "Arab", - "sldr": false, - "tag": "mve-Arab-x-urdu", - "tags": [ "mve-PK-x-urdu" ], - "windows": "mve-Arab-x-urdu" - }, { "full": "mvf-Mong-CN", "iana": [ "Peripheral Mongolian" ], "iso639_3": "mvf", + "macrolang": "mn", "name": "Mongolian, Peripheral", "names": [ "Inner Mongolian", "Menggu", "Monggol", "Mongol", "Peripheral Mongolian", "Southern-Eastern Mongolian" ], "region": "CN", @@ -75764,6 +79052,7 @@ "full": "mvf-Phag-CN", "iana": [ "Peripheral Mongolian" ], "iso639_3": "mvf", + "macrolang": "mn", "name": "Mongolian, Peripheral", "names": [ "Inner Mongolian", "Menggu", "Monggol", "Mongol", "Peripheral Mongolian", "Southern-Eastern Mongolian" ], "nophonvars": true, @@ -75780,7 +79069,6 @@ "full": "mvg-Latn-MX", "iana": [ "Yucuañe Mixtec" ], "iso639_3": "mvg", - "localnames": [ "Tnu'u savi" ], "name": "Mixtec, Yucuañe", "names": [ "Mixteco de San Bartolomé Yucuañe", "Mixteco del Sureste Central", "Tnu’u savi", "Yucuañe Mixtec" ], "region": "MX", @@ -75811,7 +79099,7 @@ "iana": [ "Miyako" ], "iso639_3": "mvi", "name": "Miyako", - "names": [ "Miyako-Ikema" ], + "names": [ "Miyako-Ikema", "Miyakoan" ], "region": "JP", "regionname": "Japan", "script": "Zyyy", @@ -75849,20 +79137,6 @@ "unwritten": true, "windows": "mvl-Latn" }, - { - "full": "mvm-Zyyy-CN", - "iana": [ "Muya" ], - "iso639_3": "mvm", - "name": "Muya", - "names": [ "Boba", "Manyak", "Menya", "Minyag", "Minyak", "Miyao", "Munya" ], - "region": "CN", - "regionname": "China", - "script": "Zyyy", - "sldr": false, - "tag": "mvm", - "tags": [ "mvm-CN", "mvm-Zyyy" ], - "windows": "mvm-Zyyy" - }, { "full": "mvn-Latn-PG", "iana": [ "Minaveha" ], @@ -75903,7 +79177,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "mvp", "tags": [ "mvp-ID", "mvp-Latn" ], "windows": "mvp-Latn" @@ -76039,6 +79313,19 @@ "tags": [ "mvy-Arab", "mvy-PK" ], "windows": "mvy-Arab" }, + { + "full": "mvz-Ethi-ET", + "iana": [ "Mesqan" ], + "iso639_3": "mvz", + "name": "Mesqan", + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": false, + "tag": "mvz", + "tags": [ "mvz-ET", "mvz-Ethi" ], + "windows": "mvz-Ethi" + }, { "full": "mvz-Arab-ET", "iana": [ "Mesqan" ], @@ -76050,7 +79337,6 @@ "script": "Arab", "sldr": false, "tag": "mvz-Arab", - "tags": [ "mvz-ET" ], "unwritten": true, "windows": "mvz-Arab" }, @@ -76072,8 +79358,8 @@ "full": "mwb-Latn-PG", "iana": [ "Juwal" ], "iso639_3": "mwb", - "name": "Juwal", - "names": [ "Juwar" ], + "name": "Juwar", + "names": [ "Juwal" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -76087,6 +79373,7 @@ "full": "mwc-Latn-PG", "iana": [ "Are" ], "iso639_3": "mwc", + "localnames": [ "Are" ], "name": "Are", "names": [ "Mukawa" ], "region": "PG", @@ -76138,6 +79425,7 @@ "sldr": false, "tag": "mwg", "tags": [ "mwg-Latn", "mwg-PG" ], + "unwritten": true, "windows": "mwg-Latn" }, { @@ -76172,6 +79460,7 @@ "full": "mwk-Latn-ML", "iana": [ "Kita Maninkakan" ], "iso639_3": "mwk", + "macrolang": "myq", "name": "Maninkakan, Kita", "names": [ "Central Malinke", "Kita Maninka", "Kita Maninkakan", "Malinke" ], "region": "ML", @@ -76201,9 +79490,9 @@ "full": "mwm-Latn-TD", "iana": [ "Sar" ], "iso639_3": "mwm", - "localnames": [ "Majngany" ], + "localnames": [ "tà Sàᶉ̄" ], "name": "Sar", - "names": [ "Madjingay", "Majngany", "Sara", "Sara Madjingay", "Sara Madjingaye" ], + "names": [ "Sara", "Sara Madjingay", "Sàᶉ̄", "tà Sàᶉ̄" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -76278,8 +79567,8 @@ "iso639_3": "mwr", "latnnames": [ "Dhundhari" ], "localnames": [ "धुन्धारी" ], - "name": "Dhundari", - "names": [ "Dhundari-Marwari", "Dhundhari", "Jaipuri", "Jhar-sahi boli", "Kai kui ki boli", "Marwari" ], + "name": "Marwari", + "names": [ "Dhundari", "Dhundari-Marwari", "Dhundhari", "Jaipuri", "Jhar-sahi boli", "Kai kui ki boli" ], "region": "IN", "regionname": "India", "regions": [ "PK" ], @@ -76313,7 +79602,6 @@ "sldr": false, "tag": "mws", "tags": [ "mws-KE", "mws-Latn" ], - "unwritten": true, "windows": "mws-Latn" }, { @@ -76329,7 +79617,7 @@ "sldr": false, "tag": "mwt", "tags": [ "mwt-MM", "mwt-Mymr" ], - "windows": "mwt-Mymr" + "windows": "mwt-Mymr-MM" }, { "full": "mwt-Thai-TH", @@ -76345,7 +79633,7 @@ "sldr": false, "tag": "mwt-Thai", "tags": [ "mwt-TH" ], - "windows": "mwt-Thai" + "windows": "mwt-Thai-TH" }, { "full": "mwu-Latn-SS", @@ -76378,25 +79666,26 @@ "windows": "mwv-Latn" }, { - "full": "mww-Latn-CN", + "full": "mww-Hmnp-CN", "iana": [ "Hmong Daw" ], "iso639_3": "mww", - "localnames": [ "Hmongb Dleub" ], + "macrolang": "hmn", "name": "Hmong Daw", "names": [ "Bai Miao", "Banded Arm Hmong", "Chuan Miao", "Forest Miao", "Hmong Dao", "Hmong Der", "Hmong Dleu", "Hmong Qua Mpa", "Hmong Rongd", "Hmongb Dleub", "Hmoob Dawb", "Meo Do", "Meo Kao", "Mong Do", "Mong Trang", "Mán Tráng", "Pe Miao", "Peh Miao", "Striped Arm Hmong", "Striped Hmong", "White Hmong", "White Lum", "White Meo", "White Miao" ], "region": "CN", "regionname": "China", "regions": [ "FR", "LA", "TH", "US", "VN" ], - "script": "Latn", + "script": "Hmnp", "sldr": false, "tag": "mww", - "tags": [ "mww-CN", "mww-Latn" ], - "windows": "mww-Latn" + "tags": [ "mww-CN", "mww-Hmnp" ], + "windows": "mww-Hmnp-CN" }, { "full": "mww-Hmng-CN", "iana": [ "Hmong Daw" ], "iso639_3": "mww", + "macrolang": "hmn", "name": "Hmong Daw", "names": [ "Bai Miao", "Banded Arm Hmong", "Chuan Miao", "Forest Miao", "Hmong Dao", "Hmong Der", "Hmong Dleu", "Hmong Qua Mpa", "Hmong Rongd", "Hmongb Dleub", "Hmoob Dawb", "Meo Do", "Meo Kao", "Mong Do", "Mong Trang", "Mán Tráng", "Pe Miao", "Peh Miao", "Striped Arm Hmong", "Striped Hmong", "White Hmong", "White Lum", "White Meo", "White Miao" ], "nophonvars": true, @@ -76406,12 +79695,13 @@ "script": "Hmng", "sldr": false, "tag": "mww-Hmng", - "windows": "mww-Hmng" + "windows": "mww-Hmng-CN" }, { "full": "mww-Laoo-LA", "iana": [ "Hmong Daw" ], "iso639_3": "mww", + "macrolang": "hmn", "name": "Hmong Daw", "names": [ "Bai Miao", "Banded Arm Hmong", "Chuan Miao", "Forest Miao", "Hmong Dao", "Hmong Der", "Hmong Dleu", "Hmong Qua Mpa", "Hmong Rongd", "Hmongb Dleub", "Hmoob Dawb", "Meo Do", "Mong Do", "Mong Trang", "Mán Tráng", "Pe Miao", "Peh Miao", "Striped Arm Hmong", "Striped Hmong", "White Hmong", "White Lum", "White Miao" ], "nophonvars": true, @@ -76422,12 +79712,29 @@ "sldr": false, "tag": "mww-Laoo", "tags": [ "mww-LA" ], - "windows": "mww-Laoo" + "windows": "mww-Laoo-LA" + }, + { + "full": "mww-Latn-CN", + "iana": [ "Hmong Daw" ], + "iso639_3": "mww", + "localnames": [ "Hmongb Dleub" ], + "macrolang": "hmn", + "name": "Hmong Daw", + "names": [ "Bai Miao", "Banded Arm Hmong", "Chuan Miao", "Forest Miao", "Hmong Dao", "Hmong Der", "Hmong Dleu", "Hmong Qua Mpa", "Hmong Rongd", "Hmongb Dleub", "Hmoob Dawb", "Meo Do", "Meo Kao", "Mong Do", "Mong Trang", "Mán Tráng", "Pe Miao", "Peh Miao", "Striped Arm Hmong", "Striped Hmong", "White Hmong", "White Lum", "White Meo", "White Miao" ], + "region": "CN", + "regionname": "China", + "regions": [ "FR", "LA", "TH", "US", "VN" ], + "script": "Latn", + "sldr": false, + "tag": "mww-Latn", + "windows": "mww-Latn-CN" }, { "full": "mww-Plrd-CN", "iana": [ "Hmong Daw" ], "iso639_3": "mww", + "macrolang": "hmn", "name": "Hmong Daw", "names": [ "Bai Miao", "Banded Arm Hmong", "Chuan Miao", "Forest Miao", "Hmong Dao", "Hmong Der", "Hmong Dleu", "Hmong Qua Mpa", "Hmong Rongd", "Hmongb Dleub", "Hmoob Dawb", "Meo Do", "Meo Kao", "Mong Do", "Mong Trang", "Mán Tráng", "Pe Miao", "Peh Miao", "Striped Arm Hmong", "Striped Hmong", "White Hmong", "White Lum", "White Meo", "White Miao" ], "nophonvars": true, @@ -76438,12 +79745,13 @@ "script": "Plrd", "sldr": false, "tag": "mww-Plrd", - "windows": "mww-Plrd" + "windows": "mww-Plrd-CN" }, { "full": "mww-Thai-TH", "iana": [ "Hmong Daw" ], "iso639_3": "mww", + "macrolang": "hmn", "name": "Hmong Daw", "names": [ "Bai Miao", "Banded Arm Hmong", "Chuan Miao", "Forest Miao", "Hmong Dao", "Hmong Der", "Hmong Dleu", "Hmong Qua Mpa", "Hmong Rongd", "Hmongb Dleub", "Hmoob Dawb", "Meo Do", "Mong Do", "Mong Trang", "Mán Tráng", "Pe Miao", "Peh Miao", "Striped Arm Hmong", "Striped Hmong", "White Hmong", "White Lum", "White Miao" ], "nophonvars": true, @@ -76454,7 +79762,7 @@ "sldr": false, "tag": "mww-Thai", "tags": [ "mww-TH" ], - "windows": "mww-Thai" + "windows": "mww-Thai-TH" }, { "full": "mwx-Latn-TZ", @@ -76502,9 +79810,8 @@ "full": "mxa-Latn-MX", "iana": [ "Northwest Oaxaca Mixtec" ], "iso639_3": "mxa", - "localnames": [ "Tu'un sav" ], "name": "Mixtec, Northwest Oaxaca", - "names": [ "Mixteco de Yucuná", "Mixteco del Noroeste", "Mixteco del Noroeste de Oaxaca", "Northwest Oaxaca Mixtec", "Tu’un sav" ], + "names": [ "Mixteco de Yucuná", "Mixteco del Noroeste", "Mixteco del Noroeste de Oaxaca", "Northwest Oaxaca Mixtec", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76517,10 +79824,10 @@ "full": "mxb-Latn-MX", "iana": [ "Tezoatlán Mixtec" ], "iso639_3": "mxb", - "localname": "Tu'un nda'i", - "localnames": [ "Tu'un nda'i" ], + "localname": "Tuꞌun ndaꞌi", + "localnames": [ "To̱ꞌon nda’í" ], "name": "Mixtec, Tezoatlán", - "names": [ "Mixteco de Tezoatlán de Segura y Luna", "Tezoatlán Mixtec", "Tu’un nda’i" ], + "names": [ "Mixteco de Tezoatlán de Segura y Luna", "Mixteco del Municipio de Tezoatlán", "Mixteco del Noroeste Central Alto", "Mixteco del Noroeste Central Bajo", "Mixteco del Noroeste Medio", "Mixteco del Oeste Alto", "Tezoatlán Mixtec", "To̱ꞌon nda’í", "Tu’un davi", "Tu’un djavi", "Tu’un javi", "Tu’un nda’i" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76534,7 +79841,7 @@ "iana": [ "Manyika" ], "iso639_3": "mxc", "name": "Manyika", - "names": [ "Bamanyeka", "Chimanyika", "Manika", "Wamanyika", "Wanyika" ], + "names": [ "Bamanyeka", "Chimanyika", "Manika", "Manyika Shona", "Wamanyika", "Wanyika" ], "region": "ZW", "regionname": "Zimbabwe", "regions": [ "MZ" ], @@ -76592,7 +79899,7 @@ "iana": [ "Mbangala" ], "iso639_3": "mxg", "name": "Mbangala", - "names": [ "Bangala", "Cimbangala" ], + "names": [ "Bangala", "Cimbangala", "Umbangala" ], "region": "AO", "regionname": "Angola", "script": "Latn", @@ -76634,7 +79941,6 @@ "full": "mxj-Latn-IN", "iana": [ "Miju-Mishmi", "Geman Deng" ], "iso639_3": "mxj", - "localnames": [ "Kman" ], "name": "Miju-Mishmi", "names": [ "Eastern Mishmi", "Geman Dend", "Geman Deng", "Kaman", "Keman", "Kman", "Miji", "Miju", "Mishmi" ], "region": "IN", @@ -76651,6 +79957,7 @@ "iana": [ "Monumbo" ], "iso639_3": "mxk", "name": "Monumbo", + "names": [ "Mambuwan" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -76680,6 +79987,8 @@ "full": "mxm-Latn-PG", "iana": [ "Meramera" ], "iso639_3": "mxm", + "localname": "Meramera", + "localnames": [ "Meramera" ], "name": "Meramera", "names": [ "Melamela", "Ubili" ], "region": "PG", @@ -76694,8 +80003,8 @@ "full": "mxn-Latn-ID", "iana": [ "Moi (Indonesia)" ], "iso639_3": "mxn", - "name": "Moi", - "names": [ "Mekwei", "Moi (Indonesia)", "Mooi", "Mosana", "Waipu" ], + "name": "Moi Kelim", + "names": [ "Moi", "Moi (Indonesia)", "Mooi", "Mosana", "Waipu" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -76724,8 +80033,9 @@ "full": "mxp-Latn-MX", "iana": [ "Tlahuitoltepec Mixe" ], "iso639_3": "mxp", + "localnames": [ "Ayuujk" ], "name": "Mixe, Tlahuitoltepec", - "names": [ "Mixe Alto del Centro", "Tlahuitoltepec Mixe", "West Central Mixe" ], + "names": [ "Ayuujk", "Mixe Alto del Centro", "South Highland Mixe", "Tlahuitoltepec Mixe", "West Central Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76741,7 +80051,7 @@ "localname": "Ayuk", "localnames": [ "Ayuk" ], "name": "Mixe, Juquila", - "names": [ "Ayuk", "Juquila Mixe", "South Central Mixe" ], + "names": [ "Ayuk", "Ayuuk", "Juquila Mixe", "Midland Mixe", "Mixe Medio del Este", "South Central Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76769,9 +80079,8 @@ "full": "mxs-Latn-MX", "iana": [ "Huitepec Mixtec" ], "iso639_3": "mxs", - "localnames": [ "Tu'un sav" ], "name": "Mixtec, Huitepec", - "names": [ "Huitepec Mixtec", "Mixteco de Huitepec", "Mixteco de San Antonio Huitepec", "Mixteco de Zaachila", "Tu’un sav" ], + "names": [ "Huitepec Mixtec", "Mixteco de Huitepec", "Mixteco de San Antonio Huitepec", "Mixteco de Zaachila", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76785,7 +80094,7 @@ "iana": [ "Jamiltepec Mixtec" ], "iso639_3": "mxt", "name": "Mixtec, Jamiltepec", - "names": [ "Eastern Jamiltepec-San Cristobal Mixtec", "Jamiltepec Mixtec", "Mixteco de Jamiltepec", "Mixteco de Oaxaca de Costa Central Baja", "Mixteco de Santa María Huazolotitlán" ], + "names": [ "Eastern Jamiltepec-San Cristobal Mixtec", "Jamiltepec Mixtec", "Mixteco de Jamiltepec", "Mixteco de Oaxaca de Costa Central Baja", "Mixteco de Santa María Huazolotitlán", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76813,8 +80122,10 @@ "full": "mxv-Latn-MX", "iana": [ "Metlatónoc Mixtec" ], "iso639_3": "mxv", + "localname": "Tuꞌun Sávi", + "localnames": [ "Tu'un Sávi", "Tuꞌun Savi de Metlatónoc y Cochoapa el Grande" ], "name": "Mixtec, Metlatónoc", - "names": [ "Metlatónoc Mixtec" ], + "names": [ "Guerrero Mixteco", "Isavi", "Metlatónoc Mixtec", "Mixteco de Chochoapa", "Mixteco de Guerrero del Este Medio", "Mixteco de Metlatónoc", "Tu’un Sávi", "Tu’un isasi", "Tu’un isavi", "Tuꞌun Savi de Metlatónoc y Cochoapa el Grande" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76827,6 +80138,7 @@ "full": "mxw-Latn-PG", "iana": [ "Namo" ], "iso639_3": "mxw", + "localnames": [ "Namo" ], "name": "Namo", "names": [ "Dorro", "Mari", "Nä" ], "region": "PG", @@ -76856,8 +80168,9 @@ "full": "mxy-Latn-MX", "iana": [ "Southeastern Nochixtlán Mixtec" ], "iso639_3": "mxy", + "localnames": [ "Tu'un davi" ], "name": "Mixtec, Southeastern Nochixtlán", - "names": [ "Mixteco de Santo Domingo Nuxaá", "Mixteco del Sureste de Nochixtlán", "Southeastern Nochixtlán Mixtec" ], + "names": [ "Mixteco de Santo Domingo Nuxaá", "Mixteco del Este", "Mixteco del Sureste de Nochixtlán", "Nuxaá Mixtec", "Southeastern Nochixtlán Mixtec", "Tu’un davi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -76885,9 +80198,9 @@ "full": "my-Mymr-MM", "iana": [ "Burmese" ], "iso639_3": "mya", - "latnnames": [ "Mranma", "Mranmabhasa" ], + "latnnames": [ "Mranmar", "Mranmar sakar" ], "localname": "မြန်မာ", - "localnames": [ "မြန်မာစ", "မြန်မာစကား" ], + "localnames": [ "မြန်မာ", "မြန်မာစကား" ], "name": "Burmese", "names": [ "Bama", "Bama Saka", "Bamachaka", "Myamasa", "Myamma Saka", "Myanma", "Myanmar", "Myen" ], "region": "MM", @@ -77000,7 +80313,6 @@ "sldr": false, "tag": "myh", "tags": [ "myh-Latn", "myh-US" ], - "unwritten": true, "windows": "myh-Latn" }, { @@ -77035,7 +80347,7 @@ "full": "myk-Latn-ML", "iana": [ "Mamara Senoufo" ], "iso639_3": "myk", - "localname": "Sénoufo, Mamara", + "localname": "Mamaara", "localnames": [ "Mamaara" ], "name": "Sénoufo, Mamara", "names": [ "Bamaraga", "Mamaara", "Mamara", "Mamara Senoufo", "Mianka", "Minianka", "Miniyanka", "Minya", "Minyanka", "Tupiire" ], @@ -77051,8 +80363,9 @@ "full": "myl-Latn-ID", "iana": [ "Moma" ], "iso639_3": "myl", + "localnames": [ "Baha Moma-Kulawi", "Moma" ], "name": "Moma", - "names": [ "Kulawi", "Moma-Kulawi" ], + "names": [ "Baha Moma-Kulawi", "Kulawi", "Moma-Kulawi" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -77081,6 +80394,7 @@ "iana": [ "Me'en" ], "iso639_3": "mym", "latnnames": [ "Me'enen" ], + "localname": "Mɛʼɛnɛn", "localnames": [ "Mɛʼɛnɛn" ], "name": "Me’en", "names": [ "Me'en" ], @@ -77152,8 +80466,10 @@ "full": "myu-Latn-BR", "iana": [ "Mundurukú" ], "iso639_3": "myu", + "localname": "Mõnjoroko", + "localnames": [ "Mõnjoroko" ], "name": "Mundurukú", - "names": [ "Caras-Pretas", "Monjoroku", "Mundurucu", "Paiquize", "Pari", "Weidyenye" ], + "names": [ "Caras-Pretas", "Monjoroku", "Mundurucu", "Mõnjoroko", "Paiquize", "Pari", "Weidyenye" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -77214,6 +80530,7 @@ "full": "myy-Latn-CO", "iana": [ "Macuna" ], "iso639_3": "myy", + "localname": "Masa ye", "localnames": [ "Masa ye" ], "name": "Macuna", "names": [ "Baigana", "Buhagana", "Emoa", "Ide", "Jepa-Matsi", "Makuna", "Makuna-Erulia", "Masa ye", "Paneroa", "Roea", "Suroa", "Tabotiro Jejea", "Umua", "Wuhána", "Yeba", "Yeba-Masã", "Yebamasã", "Yehpá Majsá", "Yepá Maxsã", "Yepá-Mahsá" ], @@ -77245,10 +80562,10 @@ "full": "mza-Latn-MX", "iana": [ "Santa María Zacatepec Mixtec" ], "iso639_3": "mza", - "localname": "Mixtec, Santa María Zacatepec", - "localnames": [ "Tu'un Va'a" ], + "localname": "Tu'un Va'a", + "localnames": [ "Tu'un va'a" ], "name": "Mixtec, Santa María Zacatepec", - "names": [ "Mixteco de Santa María Zacatepec", "Santa María Zacatepec Mixtec", "Southern Putla Mixtec", "Tu’un Va’a", "Zacatepec Mixtec" ], + "names": [ "Mixteco de Santa María Zacatepec", "Mixteco de Sierra Sur Oeste", "Santa María Zacatepec Mixtec", "Southern Putla Mixtec", "Tu’un va’a", "Zacatepec Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -77302,8 +80619,8 @@ "full": "mzc-Zxxx-MG", "iana": [ "Madagascar Sign Language" ], "iso639_3": "mzc", - "name": "Madagascar Sign Language", - "names": [ "Malagasy Sign Language" ], + "name": "Malagasy Sign Language", + "names": [ "Madagascar Sign Language" ], "region": "MG", "regionname": "Madagascar", "script": "Zxxx", @@ -77359,8 +80676,8 @@ "iana": [ "Wichí Lhamtés Güisnay" ], "iso639_3": "mzh", "localnames": [ "Wichí Lhamtés" ], - "name": "Wichí Lhamtés Güisnay", - "names": [ "Güisnay", "Wichí", "Wichí Lhamtés" ], + "name": "Pilcomayo Wichí", + "names": [ "Güisnay", "Wichí", "Wichí Lhamtés", "Wichí Lhamtés Güisnay" ], "region": "AR", "regionname": "Argentina", "script": "Latn", @@ -77375,7 +80692,7 @@ "iso639_3": "mzi", "localnames": [ "En ningotsie" ], "name": "Mazatec, Ixcatlán", - "names": [ "En ningotsie", "Ixcatlán Mazatec", "Mazateco de San Pedro Ixcatlán", "Mazateco de presa bajo" ], + "names": [ "En ningotsie", "Ixcatlán Mazatec", "Mazateco de Presa Bajo", "Mazateco de San Pedro Ixcatlán" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -77404,9 +80721,10 @@ "full": "mzk-Latn-NG", "iana": [ "Nigeria Mambila" ], "iso639_3": "mzk", + "localname": "Ju Nɔri", "localnames": [ "Ju Nɔri" ], "name": "Mambila, Nigeria", - "names": [ "Bang", "Lagubi", "Mabila", "Mambere", "Mambilla", "Nigeria Mambila", "Nor", "Nor Tagbo", "Tongbo" ], + "names": [ "Bang", "Ju Nɔri", "Lagubi", "Mabila", "Mambere", "Mambilla", "Nigeria Mambila", "Nor", "Nor Tagbo", "Tongbo", "Western Mambila" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -77421,7 +80739,7 @@ "iso639_3": "mzl", "localnames": [ "Ayuuk" ], "name": "Mixe, Mazatlán", - "names": [ "Ayuuk", "East Central Mixe", "Mazatlán Mixe", "Tutla Mixe" ], + "names": [ "Ayuk", "Ayuuk", "East Central Mixe", "Lowland Mixe", "Mazatlán Mixe", "Mixe Bajo", "Tutla Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -77480,6 +80798,7 @@ "full": "mzp-Latn-BO", "iana": [ "Movima" ], "iso639_3": "mzp", + "localname": "Chosineɬ diꞌ mowi:maj", "localnames": [ "Chosineɬ di' mowi:maj" ], "name": "Movima", "names": [ "Chosineɬ di’ mowi:maj" ], @@ -77509,8 +80828,9 @@ "full": "mzr-Latn-BR", "iana": [ "Marúbo" ], "iso639_3": "mzr", + "localnames": [ "Yora" ], "name": "Marubo", - "names": [ "Kaniuá", "Marova", "Maruba", "Marúbo" ], + "names": [ "Kaniuá", "Marova", "Maruba", "Marúbo", "Yora" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -77553,8 +80873,8 @@ "full": "mzu-Latn-PG", "iana": [ "Inapang" ], "iso639_3": "mzu", + "localnames": [ "Inapang" ], "name": "Inapang", - "names": [ "Midsivindi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -77567,9 +80887,9 @@ "full": "mzv-Latn-CF", "iana": [ "Manza" ], "iso639_3": "mzv", - "localnames": [ "Mänzä" ], + "localnames": [ "Mandja" ], "name": "Mandja", - "names": [ "Manja", "Manza" ], + "names": [ "Manja", "Manza", "Mänzä" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -77582,8 +80902,10 @@ "full": "mzw-Latn-GH", "iana": [ "Deg" ], "iso639_3": "mzw", + "localname": "Dɛg", + "localnames": [ "Dɛg" ], "name": "Deg", - "names": [ "Aculo", "Buro", "Buru", "Degha", "Janela", "Mmfo", "Mo" ], + "names": [ "Aculo", "Buro", "Buru", "Degha", "Dɛg", "Janela", "Mmfo", "Mo" ], "region": "GH", "regionname": "Ghana", "regions": [ "CI" ], @@ -77626,8 +80948,10 @@ "full": "mzz-Latn-PG", "iana": [ "Maiadomu" ], "iso639_3": "mzz", + "localname": "Maiadom", + "localnames": [ "Maiadomu" ], "name": "Maiadomu", - "names": [ "Maiadom", "Maiadomeqa" ], + "names": [ "Maiadom" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -77671,7 +80995,7 @@ "iana": [ "Southern Nambikuára" ], "iso639_3": "nab", "name": "Nambikuára, Southern", - "names": [ "Nambicuara", "Nambikwara", "Nambikwara do Sul", "Nambiquara", "Southern Nambikuára", "Southern Nambikwara" ], + "names": [ "Nambicuara", "Nambikuára", "Nambikwara", "Nambikwara do Sul", "Nambiquara", "Southern Nambikuára", "Southern Nambikwara" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -77684,6 +81008,8 @@ "full": "nac-Latn-PG", "iana": [ "Narak" ], "iso639_3": "nac", + "localname": "Narak", + "localnames": [ "Narak" ], "name": "Narak", "names": [ "Ganja" ], "region": "PG", @@ -77713,6 +81039,7 @@ "full": "naf-Latn-PG", "iana": [ "Nabak" ], "iso639_3": "naf", + "localnames": [ "Nabak" ], "name": "Nabak", "names": [ "Naba", "Napa", "Wain" ], "region": "PG", @@ -77728,8 +81055,8 @@ "iana": [ "Naga Pidgin" ], "iso639_3": "nag", "localnames": [ "Nagamiz" ], - "name": "Naga Pidgin", - "names": [ "Bodo", "Kachari Bengali", "Naga Creole Assamese", "Naga-Assamese", "Nagamese", "Nagamiz" ], + "name": "Nagamese", + "names": [ "Bodo", "Kachari Bengali", "Naga Creole Assamese", "Naga Pidgin", "Naga-Assamese", "Nagamese Creole", "Nagamiz" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -77743,7 +81070,7 @@ "iana": [ "Nalu" ], "iso639_3": "naj", "name": "Nalu", - "names": [ "Nalou" ], + "names": [ "Babelké", "Nalou", "Ntché Blék" ], "region": "GN", "regionname": "Guinea", "regions": [ "GW" ], @@ -77801,8 +81128,9 @@ "full": "nan-Hans-CN", "iana": [ "Min Nan Chinese" ], "iso639_3": "nan", + "macrolang": "zh", "name": "Chinese, Min Nan", - "names": [ "Banlamgi", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], + "names": [ "Banlamgi", "Banlamgu", "Banlamue", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], "region": "CN", "regionname": "China", "regions": [ "BN", "HK", "ID", "KH", "MO", "MY", "PH", "SG", "TH", "TW" ], @@ -77810,14 +81138,15 @@ "sldr": false, "tag": "nan", "tags": [ "nan-CN", "nan-Hans", "zh-nan", "zh-nan-CN" ], - "windows": "nan-Hans" + "windows": "nan-Hans-CN" }, { "full": "nan-Bopo-TW", "iana": [ "Min Nan Chinese" ], "iso639_3": "nan", + "macrolang": "zh", "name": "Chinese, Min Nan", - "names": [ "Banlamgi", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], + "names": [ "Banlamgi", "Banlamgu", "Banlamue", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], "nophonvars": true, "region": "TW", "regionname": "China-Taiwan", @@ -77825,14 +81154,15 @@ "script": "Bopo", "sldr": false, "tag": "nan-Bopo", - "windows": "nan-Bopo" + "windows": "nan-Bopo-TW" }, { "full": "nan-Kana-TW", "iana": [ "Min Nan Chinese" ], "iso639_3": "nan", + "macrolang": "zh", "name": "Chinese, Min Nan", - "names": [ "Banlamgi", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], + "names": [ "Banlamgi", "Banlamgu", "Banlamue", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], "nophonvars": true, "obsolete": true, "region": "TW", @@ -77841,28 +81171,30 @@ "script": "Kana", "sldr": false, "tag": "nan-Kana", - "windows": "nan-Kana" + "windows": "nan-Kana-TW" }, { "full": "nan-Latn-TW", "iana": [ "Min Nan Chinese" ], "iso639_3": "nan", + "macrolang": "zh", "name": "Chinese, Min Nan", - "names": [ "Banlamgi", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], + "names": [ "Banlamgi", "Banlamgu", "Banlamue", "Bân-lâm-gú", "Min Nam", "Min Nan", "Min Nan Chinese", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], "region": "TW", "regionname": "China-Taiwan", "regions": [ "AU", "BN", "CA", "CN", "HK", "ID", "KH", "MO", "MY", "PH", "SG", "TH", "US" ], "script": "Latn", "sldr": false, "tag": "nan-Latn", - "windows": "nan-Latn" + "windows": "nan-Latn-TW" }, { "full": "nan-Latn-TW-x-transpoj", "iana": [ "Chinese, Min Nan" ], "iso639_3": "nan", + "macrolang": "zh", "name": "Chinese, Min Nan", - "names": [ "Banlamgi", "Bân-lâm-gú", "Min Nam", "Min Nan", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], + "names": [ "Banlamgi", "Banlamgu", "Banlamue", "Bân-lâm-gú", "Min Nam", "Min Nan", "Minnan", "Southern Fujian", "Southern Min", "Taiwanese" ], "region": "TW", "regionname": "China-Taiwan", "regions": [ "AU", "BN", "CA", "CN", "HK", "ID", "KH", "MO", "MY", "PH", "SG", "TH", "US" ], @@ -77870,22 +81202,23 @@ "sldr": false, "tag": "nan-Latn-x-transpoj", "tags": [ "nan-TW-x-transpoj", "zh-nan-TW-x-transpoj" ], - "windows": "nan-Latn-x-transpoj" + "windows": "nan-Latn-TW-x-transpoj" }, { - "full": "nao-Zyyy-NP", + "full": "nao-Deva-NP", "iana": [ "Naaba" ], "iso639_3": "nao", "name": "Naaba", "names": [ "Naapa", "Naapaa", "Naba", "Nawa Sherpa" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "regions": [ "CN" ], + "script": "Deva", "sldr": false, "tag": "nao", - "tags": [ "nao-NP", "nao-Zyyy" ], + "tags": [ "nao-Deva", "nao-NP" ], "unwritten": true, - "windows": "nao-Zyyy" + "windows": "nao-Deva" }, { "full": "nap-Latn-IT", @@ -77907,7 +81240,7 @@ "iana": [ "Khoekhoe", "Nama (Namibia)" ], "iso639_3": "naq", "localname": "Khoekhoegowab", - "localnames": [ "Khoekhoe", "Khoekhoegowab" ], + "localnames": [ "Khoekhoegowab" ], "name": "Khoekhoe", "names": [ "Bergdamara", "Dama", "Damaqua", "Damara", "Hai||’om", "Khoekhoegowab", "Khoekhoegowap", "Khoi", "Maqua", "Nama", "Nama (Namibia)", "Namagowab", "Namakwa", "Naman", "Namaqua", "Tama", "Tamakwa", "Tamma" ], "region": "NA", @@ -77924,7 +81257,7 @@ "iana": [ "Iguta" ], "iso639_3": "nar", "name": "Iguta", - "names": [ "Anaguta", "Naraguta" ], + "names": [ "Anaguta", "Eguta", "Guta", "Naraguta", "Narkuta" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -77939,9 +81272,9 @@ "iana": [ "Naasioi" ], "iso639_3": "nas", "localname": "Naasioi", - "localnames": [ "Naasioi" ], + "localnames": [ "Naasioina'" ], "name": "Naasioi", - "names": [ "Aunge", "Kieta", "Kieta Talk", "Nasioi" ], + "names": [ "Aunge", "Kieta", "Kieta Talk", "Naasioina’", "Nasioi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -77954,6 +81287,7 @@ "full": "nat-Latn-NG", "iana": [ "Ca̱hungwa̱rya̱", "Hungworo" ], "iso639_3": "nat", + "localname": "Cәhungwә̀ryə̀", "localnames": [ "Cәhungwә̀ryə̀" ], "name": "Cahungwarya", "names": [ "Ca̱hungwa̱rya̱", "Cәhungwә̀ryə̀", "Hungworo", "Ingwe", "Ingwo", "Ngwai", "Ngwe", "Ngwoi", "Nkwoi", "Ungwai", "Ungwe" ], @@ -77969,6 +81303,8 @@ "full": "naw-Latn-GH", "iana": [ "Nawuri" ], "iso639_3": "naw", + "localname": "Nawuri", + "localnames": [ "Nawuri" ], "name": "Nawuri", "region": "GH", "regionname": "Ghana", @@ -77989,6 +81325,7 @@ "sldr": false, "tag": "nax", "tags": [ "nax-Latn", "nax-PG" ], + "unwritten": true, "windows": "nax-Latn" }, { @@ -78012,7 +81349,7 @@ "iana": [ "Coatepec Nahuatl" ], "iso639_3": "naz", "name": "Nahuatl, Coatepec", - "names": [ "Coatepec Aztec", "Coatepec Nahuatl", "Náhuatl de Coatepec" ], + "names": [ "Coatepec Aztec", "Coatepec Nahuatl", "Mexicano", "Mexicano de Guerrero", "Náhuatl de Coatepec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -78027,23 +81364,36 @@ "iana": [ "Norwegian Bokmål" ], "iso639_3": "nob", "localname": "norsk bokmål", + "macrolang": "no", "name": "Norwegian Bokmål", "region": "NO", "regionname": "Norway", - "regions": [ "SJ" ], "script": "Latn", "sldr": true, "suppress": true, "tag": "nb", "tags": [ "nb-Latn", "nb-NO" ], - "windows": "nb" + "windows": "nb-NO" + }, + { + "full": "nb-Latn-SJ", + "iana": [ "Norwegian Bokmål" ], + "iso639_3": "nob", + "macrolang": "no", + "name": "Norwegian Bokmål", + "region": "SJ", + "regionname": "Svalbard and Jan Mayen", + "script": "Latn", + "sldr": true, + "tag": "nb-SJ", + "windows": "nb-Latn-SJ" }, { "full": "nba-Latn-AO", "iana": [ "Nyemba" ], "iso639_3": "nba", "name": "Nyemba", - "names": [ "Gangela", "Ganguela", "Ganguella", "Ngangela", "Nhemba" ], + "names": [ "Gangela", "Ganguela", "Ganguella", "Ngangela", "Nganguela", "Nhemba" ], "region": "AO", "regionname": "Angola", "regions": [ "NA", "ZM" ], @@ -78071,6 +81421,7 @@ "full": "nbc-Latn-IN", "iana": [ "Chang Naga" ], "iso639_3": "nbc", + "localnames": [ "Mochungrr" ], "name": "Naga, Chang", "names": [ "Chang", "Chang Naga", "Changyanguh", "Machongrr", "Mochumi", "Mochungrr", "Mojung" ], "region": "IN", @@ -78291,29 +81642,30 @@ "windows": "nbs-Zxxx" }, { - "full": "nbt-Deva-IN", + "full": "nbt-Latn-IN", "iana": [ "Na" ], "iso639_3": "nbt", "name": "Na", - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Latn", "sldr": false, - "tag": "nbt-Deva", - "windows": "nbt-Deva" + "tag": "nbt", + "tags": [ "nbt-IN", "nbt-Latn" ], + "windows": "nbt-Latn" }, { - "full": "nbt-Latn-IN", + "full": "nbt-Deva-IN", "iana": [ "Na" ], "iso639_3": "nbt", "name": "Na", + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Deva", "sldr": false, - "tag": "nbt-Latn", - "windows": "nbt-Latn" + "tag": "nbt-Deva", + "windows": "nbt-Deva" }, { "full": "nbu-Latn-IN", @@ -78342,13 +81694,13 @@ "sldr": false, "tag": "nbv", "tags": [ "nbv-CM", "nbv-Latn" ], - "unwritten": true, "windows": "nbv-Latn" }, { "full": "nbw-Latn-CD", "iana": [ "Southern Ngbandi" ], "iso639_3": "nbw", + "localnames": [ "Ngbandi-Ngiri" ], "name": "Ngbandi, Southern", "names": [ "Mbati", "Mongbandi", "Mongwandi", "Ngbandi-Ngiri", "Ngbandi-Sud", "Ngwandi", "Southern Ngbandi" ], "region": "CD", @@ -78391,38 +81743,40 @@ "windows": "nca-Latn" }, { - "full": "ncb-Deva-IN", + "full": "ncb-Latn-IN", "iana": [ "Central Nicobarese" ], "iso639_3": "ncb", + "localnames": [ "Mūöt" ], "name": "Nicobarese, Central", "names": [ "Central Nicobarese", "Mūöt", "Nicobar" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Latn", "sldr": false, - "tag": "ncb-Deva", - "windows": "ncb-Deva" + "tag": "ncb", + "tags": [ "ncb-IN", "ncb-Latn" ], + "windows": "ncb-Latn" }, { - "full": "ncb-Latn-IN", + "full": "ncb-Deva-IN", "iana": [ "Central Nicobarese" ], "iso639_3": "ncb", - "localnames": [ "Mūöt" ], "name": "Nicobarese, Central", "names": [ "Central Nicobarese", "Mūöt", "Nicobar" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Deva", "sldr": false, - "tag": "ncb-Latn", - "windows": "ncb-Latn" + "tag": "ncb-Deva", + "windows": "ncb-Deva" }, { "full": "ncc-Latn-PG", "iana": [ "Ponam" ], "iso639_3": "ncc", "name": "Ponam", + "names": [ "Rahau Ponam" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -78436,8 +81790,8 @@ "full": "ncd-Deva-NP", "iana": [ "Nachering" ], "iso639_3": "ncd", - "name": "Nachering", - "names": [ "Bangdale", "Bangdel Tûm", "Bangdile", "Mathsereng", "Nacchhering", "Nacering Ra", "Nachering Tûm", "Nachiring", "Nasring", "Nasru Bhra" ], + "name": "Nachiring", + "names": [ "Bangdale", "Bangdel Tûm", "Bangdile", "Mathsereng", "Nacchhering", "Nacering Ra", "Nachering", "Nachering Tûm", "Nasring", "Nasru Bhra" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -78497,7 +81851,7 @@ "full": "nch-Latn-MX", "iana": [ "Central Huasteca Nahuatl" ], "iso639_3": "nch", - "localname": "nāhuatlahtōlli", + "localname": "Nāhuatlahtōlli", "localnames": [ "Nāhuatlahtōlli" ], "name": "Nahuatl, Central Huasteca", "names": [ "Central Huasteca Nahuatl", "Mexicano", "Mexicano de la Huasteca Hidalguense", "Náhuat", "Náhuat de la Huasteca Central", "Náhuatl", "Náhuatl de la Huasteca", "Nāhuatlahtōlli" ], @@ -78526,9 +81880,10 @@ "full": "ncj-Latn-MX", "iana": [ "Northern Puebla Nahuatl" ], "iso639_3": "ncj", - "localnames": [ "Masehual tla’tol" ], + "localname": "Masehual tla’tol", + "localnames": [ "Mexicatl" ], "name": "Nahuatl, Northern Puebla", - "names": [ "Masehual tla’tol", "North Puebla Aztec", "North Puebla Sierra Nahuatl", "Northern Puebla Nahuatl", "Náhuatl del Norte de Puebla" ], + "names": [ "Maseual tla’tol", "Mexicatl", "North Puebla Aztec", "North Puebla Sierra Nahuatl", "Northern Puebla Nahuatl", "Náhuatl del Noroeste Central", "Náhuatl del Norte de Puebla" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -78573,7 +81928,7 @@ "iana": [ "Nambo" ], "iso639_3": "ncm", "name": "Nambo", - "names": [ "Arufe", "Nambu", "Namna" ], + "names": [ "Arufe", "Kerake", "Keraki", "Nambu", "Namna", "Nmbo" ], "obsolete": true, "region": "PG", "regionname": "Papua New Guinea", @@ -78670,31 +82025,32 @@ "windows": "ncs-Zxxx" }, { - "full": "nct-Beng-IN", + "full": "nct-Latn-IN", "iana": [ "Chothe Naga" ], "iso639_3": "nct", "name": "Naga, Chothe", "names": [ "Chawte", "Chote", "Chothe", "Chothe Naga", "Chowte" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "nct-Beng", - "windows": "nct-Beng" + "tag": "nct", + "tags": [ "nct-IN", "nct-Latn" ], + "windows": "nct-Latn" }, { - "full": "nct-Latn-IN", + "full": "nct-Beng-IN", "iana": [ "Chothe Naga" ], "iso639_3": "nct", "name": "Naga, Chothe", "names": [ "Chawte", "Chote", "Chothe", "Chothe Naga", "Chowte" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "nct-Latn", - "windows": "nct-Latn" + "tag": "nct-Beng", + "windows": "nct-Beng" }, { "full": "ncu-Latn-GH", @@ -78727,18 +82083,18 @@ "windows": "ncx-Latn" }, { - "full": "ncz-Zyyy-US", + "full": "ncz-Latn-US", "iana": [ "Natchez" ], "iso639_3": "ncz", "name": "Natchez", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ncz", - "tags": [ "ncz-US", "ncz-Zyyy" ], + "tags": [ "ncz-Latn", "ncz-US" ], "unwritten": true, - "windows": "ncz-Zyyy" + "windows": "ncz-Latn" }, { "full": "nd-Latn-ZW", @@ -78816,7 +82172,6 @@ "sldr": false, "tag": "ndd", "tags": [ "ndd-Latn", "ndd-NG" ], - "unwritten": true, "windows": "ndd-Latn" }, { @@ -78860,7 +82215,7 @@ "regionname": "United Republic of Tanzania", "regions": [ "MW" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ndh", "tags": [ "ndh-Latn", "ndh-TZ" ], "windows": "ndh-Latn" @@ -78869,8 +82224,9 @@ "full": "ndi-Latn-NG", "iana": [ "Samba Leko" ], "iso639_3": "ndi", + "localnames": [ "Sama" ], "name": "Samba Leko", - "names": [ "Chamba Leeko", "Chamba Leko", "Lego", "Leko", "Lekon", "Ndi", "Samba", "Samba Leeko", "Suntai" ], + "names": [ "Chamba Leeko", "Chamba Leko", "Lego", "Leko", "Lekon", "Ndi", "Sama", "Samba", "Samba Leeko", "Suntai" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -78959,8 +82315,8 @@ "full": "ndp-Latn-UG", "iana": [ "Ndo" ], "iso639_3": "ndp", - "name": "Ndo", - "names": [ "Kebu", "Kebutu", "Kebu’tu", "Ke’bu", "Ndu", "Okebu", "Oke’bu" ], + "name": "Kebu", + "names": [ "Kebutu", "Kebu’tu", "Ke’bu", "Ndo", "Ndu", "Okebu", "Oke’bu" ], "region": "UG", "regionname": "Uganda", "regions": [ "CD" ], @@ -79017,7 +82373,7 @@ "suppress": true, "tag": "nds", "tags": [ "nds-DE", "nds-Latn" ], - "windows": "nds" + "windows": "nds-DE" }, { "full": "nds-Latn-NL", @@ -79031,9 +82387,23 @@ "regionname": "Netherlands", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nds-NL", - "windows": "nds-NL" + "windows": "nds-Latn-NL" + }, + { + "full": "nds-Runr-001", + "iana": [ "Low German", "Low Saxon" ], + "iso639_3": "nds", + "name": "Low German", + "names": [ "Low Saxon" ], + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "nds-Runr", + "tags": [ "nds-001" ], + "windows": "nds-Runr-001" }, { "full": "ndt-Latn-CD", @@ -79068,13 +82438,13 @@ "full": "ndv-Latn-SN", "iana": [ "Ndut" ], "iso639_3": "ndv", - "localnames": [ "Ndut" ], + "localnames": [ "Ndút" ], "name": "Ndut", - "names": [ "Ndoute" ], + "names": [ "Ndoute", "Ndút" ], "region": "SN", "regionname": "Senegal", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ndv", "tags": [ "ndv-Latn", "ndv-SN" ], "windows": "ndv-Latn" @@ -79112,22 +82482,24 @@ "full": "ndy-Latn-CF", "iana": [ "Lutos" ], "iso639_3": "ndy", - "name": "Lutos", - "names": [ "Rito", "Ruto" ], + "localnames": [ "Luto", "Lutos" ], + "name": "Luto", + "names": [ "Lutos", "Rito", "Rito-Lutos", "Ruto" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", "sldr": false, "tag": "ndy", "tags": [ "ndy-CF", "ndy-Latn" ], - "windows": "ndy-Latn" + "windows": "ndy-Latn-CF" }, { "full": "ndy-Latn-TD", "iana": [ "Lutos" ], "iso639_3": "ndy", - "name": "Lutos", - "names": [ "Rito", "Ruto" ], + "localnames": [ "Luto", "Lutos" ], + "name": "Luto", + "names": [ "Lutos", "Rito", "Rito-Lutos", "Ruto" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -79158,8 +82530,8 @@ "latnnames": [ "Nepālī" ], "localname": "नेपाली", "localnames": [ "नेपाली" ], - "name": "Nepali", - "names": [ "Eastern Pahadi", "Eastern Pahari", "Gorkhali", "Gurkhali", "Khaskura", "Lhotshammikha", "Nepalese", "Nepali (macrolanguage)", "Parbate", "Parbatiya" ], + "name": "Nepali (macrolanguage)", + "names": [ "Eastern Pahadi", "Eastern Pahari", "Gorkhali", "Gurkhali", "Khaskura", "Lhotshammikha", "Nepalese", "Nepali", "Parbate", "Parbatiya" ], "region": "NP", "regionname": "Nepal", "regions": [ "AU", "BN", "BT", "FI", "GB", "US" ], @@ -79168,14 +82540,14 @@ "suppress": true, "tag": "ne", "tags": [ "ne-Deva", "ne-NP", "npi", "npi-Deva", "npi-Deva-NP", "npi-NP" ], - "windows": "ne" + "windows": "ne-NP" }, { "full": "ne-Brai-NP", "iana": [ "Nepali (macrolanguage)" ], "iso639_3": "nep", - "name": "Nepali", - "names": [ "Eastern Pahadi", "Eastern Pahari", "Gorkhali", "Gurkhali", "Khaskura", "Lhotshammikha", "Nepalese", "Nepali (macrolanguage)", "Parbate", "Parbatiya" ], + "name": "Nepali (macrolanguage)", + "names": [ "Eastern Pahadi", "Eastern Pahari", "Gorkhali", "Gurkhali", "Khaskura", "Lhotshammikha", "Nepalese", "Nepali", "Parbate", "Parbatiya" ], "nophonvars": true, "region": "NP", "regionname": "Nepal", @@ -79184,7 +82556,7 @@ "sldr": false, "tag": "ne-Brai", "tags": [ "npi-Brai", "npi-Brai-NP" ], - "windows": "ne-Brai" + "windows": "ne-Brai-NP" }, { "full": "ne-Deva-IN", @@ -79196,9 +82568,8 @@ "regionname": "India", "script": "Deva", "sldr": true, - "suppress": true, "tag": "ne-IN", - "windows": "ne-IN" + "windows": "ne-Deva-IN" }, { "full": "ne-Newa-NP", @@ -79211,7 +82582,7 @@ "script": "Newa", "sldr": false, "tag": "ne-Newa", - "windows": "ne-Newa" + "windows": "ne-Newa-NP" }, { "full": "nea-Latn-ID", @@ -79319,32 +82690,32 @@ "windows": "neg-Cyrl" }, { - "full": "neh-Zyyy-BT", + "full": "neh-Tibt-BT", "iana": [ "Nyenkha" ], "iso639_3": "neh", "name": "Nyenkha", - "names": [ "Henkha", "Lap", "Mangdhika", "Mangsdekha" ], + "names": [ "Henkha", "Lap", "Mangdhika", "Mangsdekha", "Upper Mangdep" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "neh", - "tags": [ "neh-BT", "neh-Zyyy" ], + "tags": [ "neh-BT", "neh-Tibt" ], "unwritten": true, - "windows": "neh-Zyyy" + "windows": "neh-Tibt" }, { - "full": "nei-Zyyy-TR", + "full": "nei-Xsux-TR", "iana": [ "Neo-Hittite" ], "iso639_3": "nei", "name": "Neo-Hittite", "region": "TR", "regionname": "Turkey", - "script": "Zyyy", + "script": "Xsux", "sldr": false, "tag": "nei", - "tags": [ "nei-TR", "nei-Zyyy" ], - "windows": "nei-Zyyy" + "tags": [ "nei-TR", "nei-Xsux" ], + "windows": "nei-Xsux" }, { "full": "nej-Latn-PG", @@ -79424,11 +82795,11 @@ "iso639_3": "neq", "localnames": [ "Ayuuk" ], "name": "Mixe, North Central", - "names": [ "Ayuuk", "Hayuuk", "North Central Mixe", "Northeastern Mixe" ], + "names": [ "Ayuuk", "Hayuuk", "Midland Mixe", "Mixe Medio del Este", "North Central Mixe", "Northeastern Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "neq", "tags": [ "neq-Latn", "neq-MX" ], "windows": "neq-Latn" @@ -79509,7 +82880,7 @@ "full": "new-Deva-NP", "iana": [ "Newari", "Nepal Bhasa" ], "iso639_3": "new", - "latnnames": [ "Nepal Bhasa", "Newah Bhaaye" ], + "latnnames": [ "Nepal Bhasa", "Newah Bhay" ], "localnames": [ "नेपाल भाषा", "नेवाः भाय्" ], "name": "Newar", "names": [ "Nepal Bhasa", "Newa Bhaye", "Newaah Bhaae", "Newaah Bhaaye", "Newah", "Newal Bhaye" ], @@ -79517,7 +82888,7 @@ "regionname": "Nepal", "regions": [ "IN" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "new", "tags": [ "new-Deva", "new-NP" ], "windows": "new-Deva" @@ -79538,21 +82909,20 @@ "windows": "new-Newa" }, { - "full": "new-Zzzz-NP-x-ranjana", - "iana": [ "Newar" ], + "full": "new-Ranj-NP", + "iana": [ "Newari", "Nepal Bhasa" ], "iso639_3": "new", "name": "Newar", - "names": [ "Newa Bhaye", "Newaah Bhaae", "Newaah Bhaaye", "Newah", "Newal Bhaye" ], + "names": [ "Nepal Bhasa", "Newa Bhaye", "Newaah Bhaae", "Newaah Bhaaye", "Newah", "Newal Bhaye" ], "nophonvars": true, "obsolete": true, "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], - "script": "Zzzz", + "script": "Ranj", "sldr": false, - "tag": "new-Zzzz-x-ranjana", - "tags": [ "new-NP-x-ranjana" ], - "windows": "new-Zzzz-x-ranjana" + "tag": "new-Ranj", + "windows": "new-Ranj" }, { "full": "nex-Latn-PG", @@ -79646,8 +83016,10 @@ "full": "nfr-Latn-GH", "iana": [ "Nafaanra" ], "iso639_3": "nfr", + "localname": "Nafaanra", + "localnames": [ "Nafaanra" ], "name": "Nafaanra", - "names": [ "Banafo", "Banda", "Dzama", "Gambo", "Nafaara", "Nafana", "Pantera-Fantera" ], + "names": [ "Banafo", "Banda", "Dzama", "Gambo", "Nafaara", "Nafana", "Nafana yolo" ], "region": "GH", "regionname": "Ghana", "regions": [ "CI" ], @@ -79661,8 +83033,9 @@ "full": "nfu-Latn-CM", "iana": [ "Mfumte" ], "iso639_3": "nfu", + "localnames": [ "Mfɨ́ti" ], "name": "Mfumte", - "names": [ "Nfumte" ], + "names": [ "Mfɨ́ti", "Nfumte" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -79691,9 +83064,9 @@ "iana": [ "Ngbaka" ], "iso639_3": "nga", "localname": "Ngbaka", - "localnames": [ "Ngbaka" ], + "localnames": [ "Ngbàkà" ], "name": "Ngbaka", - "names": [ "Ngbaka Gbaya", "Ngbaka Minagende" ], + "names": [ "Ngbaka Gbaya", "Ngbaka Minagende", "Ngbàkà" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "CF", "CG" ], @@ -79707,6 +83080,7 @@ "full": "ngb-Latn-CD", "iana": [ "Northern Ngbandi" ], "iso639_3": "ngb", + "localnames": [ "Ngbandi" ], "name": "Ngbandi, Northern", "names": [ "Baza", "Mongwandi", "Ngbandi", "Ngbandi du Nord", "Ngwandi", "Northern Ngbandi" ], "region": "CD", @@ -79758,7 +83132,6 @@ "sldr": false, "tag": "nge", "tags": [ "nge-CM", "nge-Latn" ], - "unwritten": true, "windows": "nge-Latn" }, { @@ -79807,6 +83180,7 @@ "full": "ngj-Latn-CM", "iana": [ "Ngie" ], "iso639_3": "ngj", + "localnames": [ "Ungie" ], "name": "Ngie", "names": [ "Angie", "Baminge", "Baninge", "Mingi", "Ngi", "Ugie", "Ungie" ], "region": "CM", @@ -79836,13 +83210,13 @@ "full": "ngl-Latn-MZ", "iana": [ "Lomwe" ], "iso639_3": "ngl", - "localnames": [ "Elhomwe" ], + "localnames": [ "Elomwe" ], "name": "Lomwe", - "names": [ "Acilowe", "Alomwe", "Chilowe", "Cilowe", "Elhomwe", "Elomwe", "Ilomwe", "Ingulu", "Lomue", "Mihavane", "Mihavani", "Mihawani", "Ngulu", "Nguru", "Walomwe", "West Makhuwa", "Western Makua" ], + "names": [ "Acilowe", "Alomwe", "Chilowe", "Cilowe", "Elomwe", "Ilomwe", "Ingulu", "Lomue", "Ngulu", "Nguru", "Walomwe", "West Makhuwa", "Western Makua" ], "region": "MZ", "regionname": "Mozambique", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ngl", "tags": [ "ngl-Latn", "ngl-MZ" ], "windows": "ngl-Latn" @@ -79866,39 +83240,24 @@ "full": "ngn-Latn-CM", "iana": [ "Ngwo" ], "iso639_3": "ngn", + "localnames": [ "atsó’ Ngwo" ], "name": "Ngwo", - "names": [ "Engwo", "Ngwaw" ], + "names": [ "Engwo", "Ngwaw", "atsó’ Ngwo" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", "sldr": false, "tag": "ngn", "tags": [ "ngn-CM", "ngn-Latn" ], - "unwritten": true, "windows": "ngn-Latn" }, - { - "full": "ngo-Latn-TZ", - "iana": [ "Ngoni" ], - "iso639_3": "ngo", - "localnames": [ "Xingoni" ], - "name": "Ngoni", - "names": [ "Angoni", "Chingoni", "Cingoni", "Kingoni", "Kisutu", "Sutu", "Xingoni" ], - "region": "TZ", - "regionname": "United Republic of Tanzania", - "regions": [ "MZ" ], - "script": "Latn", - "sldr": false, - "tag": "ngo", - "tags": [ "ngo-Latn", "ngo-TZ" ], - "windows": "ngo-Latn" - }, { "full": "ngp-Latn-TZ", "iana": [ "Ngulu" ], "iso639_3": "ngp", + "localname": "Ngulu", "name": "Ngulu", - "names": [ "Geja", "Kingulu", "Nguru", "Nguu", "Wayomba" ], + "names": [ "Geja", "Kingulu", "Kinguu", "Nguru", "Nguu", "Wayomba" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -79913,7 +83272,7 @@ "iso639_3": "ngq", "localnames": [ "Kengoreme" ], "name": "Ngoreme", - "names": [ "Dengurume", "Ikingurimi", "Kingereme", "Kingoreme", "Ngruimi", "Ngurimi", "Nguruimi" ], + "names": [ "Dengurume", "Ekengoreme", "Ikingurimi", "Kengoreme", "Kingereme", "Kingoreme", "Ngruimi", "Ngurimi", "Nguruimi" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -79971,8 +83330,9 @@ "iana": [ "Guerrero Nahuatl" ], "iso639_3": "ngu", "localname": "Nahuatl, Guerrero", + "localnames": [ "Nauatlajtojli" ], "name": "Nahuatl, Guerrero", - "names": [ "Guerrero Aztec", "Guerrero Nahuatl", "Mexicano de Guerrero", "Náhuatl de Guerrero", "Xalitla Nahuatl" ], + "names": [ "Guerrero Aztec", "Guerrero Nahuatl", "Mexicano", "Mexicano de Guerrero", "Nauatlajtojli", "Nawatlajtoli", "Náhuatl de Guerrero", "Xalitla Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80045,8 +83405,9 @@ "full": "ngz-Latn-CG", "iana": [ "Ngungwel" ], "iso639_3": "ngz", + "localnames": [ "Engungwel" ], "name": "Ngungwel", - "names": [ "Engungwel", "Ngangoulou", "Ngangulu", "Ngungulu", "Northeastern Teke" ], + "names": [ "Engungwel", "Gangoulou", "Gangulu", "Ngangoulou", "Ngangulu", "Ngungulu", "Northeastern Teke" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -80075,7 +83436,7 @@ "iana": [ "Beng" ], "iso639_3": "nhb", "name": "Beng", - "names": [ "Ben" ], + "names": [ "Ben", "Gan", "Ngain", "Ngan", "Ngen" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -80089,7 +83450,7 @@ "iana": [ "Tabasco Nahuatl" ], "iso639_3": "nhc", "name": "Nahuatl, Tabasco", - "names": [ "Tabasco Aztec", "Tabasco Nahuatl" ], + "names": [ "Mexicano", "Mexicano del Oriente", "Tabasco Aztec", "Tabasco Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80104,6 +83465,7 @@ "iana": [ "Chiripá", "Ava Guaraní" ], "iso639_3": "nhd", "localnames": [ "Ava Guaraní" ], + "macrolang": "gn", "name": "Guaraní, Ava", "names": [ "Apytare", "Ava", "Ava Guaraní", "Ava-Chiripá", "Ava-katu-ete", "Chiripá", "Chiripá-Guaraní", "Guaraní", "Nandeva", "Nhandeva", "Tsiripá", "Txiripá", "Ñandeva" ], "region": "PY", @@ -80120,8 +83482,9 @@ "iana": [ "Eastern Huasteca Nahuatl" ], "iso639_3": "nhe", "localname": "Nahuatl, Eastern Huasteca", + "localnames": [ "Mexkatl" ], "name": "Nahuatl, Eastern Huasteca", - "names": [ "Eastern Huasteca Aztec", "Eastern Huasteca Nahuatl", "Huasteca Nahuatl", "Mexicano", "Náhuatl de Hidalgo", "Náhuatl de la Huasteca Oriental", "nāhuatlahtōlli" ], + "names": [ "Eastern Huasteca Aztec", "Eastern Huasteca Nahuatl", "Huasteca Nahuatl", "Mexicano", "Mexkatl", "Náhuatl de Hidalgo", "Náhuatl de la Huasteca Oriental", "nāhuatlahtōlli" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80148,8 +83511,9 @@ "full": "nhg-Latn-MX", "iana": [ "Tetelcingo Nahuatl" ], "iso639_3": "nhg", + "localnames": [ "Mösiehuali̱" ], "name": "Nahuatl, Tetelcingo", - "names": [ "Tetelcingo Aztec", "Tetelcingo Nahuatl" ], + "names": [ "Mösiehuali̱", "Tetelcingo Aztec", "Tetelcingo Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80176,9 +83540,10 @@ "full": "nhi-Latn-MX", "iana": [ "Zacatlán-Ahuacatlán-Tepetzintla Nahuatl" ], "iso639_3": "nhi", - "localnames": [ "Mehcanohtlahtol" ], + "localname": "Mehcanohtlahtol", + "localnames": [ "Mehcanohtlahtol", "Mehcanohtlahtol de Zacatlán-Ahuacatlán uan Tepetzintla" ], "name": "Nahuatl, Zacatlán-Ahuacatlán-Tepetzintla", - "names": [ "Ahuacatlán and Tepetzintla", "Aztec of Zacatlán", "Mehcanohtlahtol", "Mejicano", "Mexicano", "Tenango Nahuatl", "Zacatlán-Ahuacatlán-Tepetzintla Nahuatl" ], + "names": [ "Ahuacatlán and Tepetzintla", "Aztec of Zacatlán", "Masehual tla’tol", "Mehcanohtlahtol", "Mehcanohtlahtol de Zacatlán-Ahuacatlán uan Tepetzintla", "Mejicano", "Mexicano", "Mexicanohtlahtol", "Náhuatl de la Sierra Oeste de Puebla", "Tenango Nahuatl", "Zacatlán-Ahuacatlán-Tepetzintla Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80192,7 +83557,7 @@ "iana": [ "Isthmus-Cosoleacaque Nahuatl" ], "iso639_3": "nhk", "name": "Nahuatl, Isthmus-Cosoleacaque", - "names": [ "Cosoleacaque Aztec", "Isthmus-Cosoleacaque Nahuatl", "Náhuatl del Istmo-Cosoleacaque" ], + "names": [ "Cosoleacaque Aztec", "Isthmus-Cosoleacaque Nahuatl", "Náhuatl", "Náhuatl del Istmo", "Náhuatl del Istmo-Cosoleacaque" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80206,8 +83571,9 @@ "full": "nhm-Latn-MX", "iana": [ "Morelos Nahuatl" ], "iso639_3": "nhm", + "localnames": [ "Ika mejikano", "Ika nauatl" ], "name": "Nahuatl, Morelos", - "names": [ "Morelos Nahuatl", "Náhuatl de Cuentepec" ], + "names": [ "Ika mejikano", "Ika nauatl", "Mexicano", "Mexicano de Temixco", "Morelos Nahuatl", "Náhuatl de Cuentepec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80235,6 +83601,7 @@ "iana": [ "Takuu" ], "iso639_3": "nho", "localname": "Takuu", + "localnames": [ "Takuu" ], "name": "Takuu", "names": [ "Mortlock", "Taku", "Tau", "Tauu" ], "region": "PG", @@ -80250,7 +83617,7 @@ "iana": [ "Isthmus-Pajapan Nahuatl" ], "iso639_3": "nhp", "name": "Nahuatl, Isthmus-Pajapan", - "names": [ "Isthmus-Pajapan Nahuatl", "Nahuatl del Istmo bajo", "Náhuat de Pajapan", "Pajapan Nahuatl" ], + "names": [ "Isthmus-Pajapan Nahuatl", "Nahuatl del Istmo bajo", "Náhuat de Pajapan", "Náhuatl", "Náhuatl del Istmo", "Pajapan Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80325,7 +83692,7 @@ "iana": [ "Temascaltepec Nahuatl" ], "iso639_3": "nhv", "name": "Nahuatl, Temascaltepec", - "names": [ "Almomoloa Náhuatl", "Temascaltepec Aztec", "Temascaltepec Nahuatl" ], + "names": [ "Almomoloa Náhuatl", "Mexicano", "Mexicano del Centro Bajo", "San Mateo Almomoloa", "Temascaltepec Aztec", "Temascaltepec Nahuatl", "Temazcaltepēc nāhuatlahtōlli" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80353,10 +83720,10 @@ "full": "nhx-Latn-MX", "iana": [ "Isthmus-Mecayapan Nahuatl" ], "iso639_3": "nhx", - "localname": "Nahuatl, Isthmus-Mecayapan", + "localname": "Melaꞌtájto̱l", "localnames": [ "Melaꞌtájto̱l" ], "name": "Nahuatl, Isthmus-Mecayapan", - "names": [ "Isthmus Aztec-Mecayapan", "Isthmus Nahuat", "Isthmus Nahuatl", "Isthmus-Mecayapan Nahuatl", "Mecayapan Isthmus Nahuatl", "Mejicano del Istmo", "Melaꞌtájto̱l", "Mexicano", "Nahuatl del Istmo", "Náhuat de Mecayapan", "Náhuatl", "Náhuatl sur de Veracruz", "Tatahuicapan Nahuatl" ], + "names": [ "Isthmus Aztec-Mecayapan", "Isthmus Nahuat", "Isthmus Nahuatl", "Isthmus-Mecayapan Nahuatl", "Mecayapan Isthmus Nahuatl", "Mejicano", "Mejicano de Mecayapan", "Mejicano del Istmo", "Melaꞌtájto̱l", "Mexicano", "Mexicano de Mecayapan", "Nahuatl del Istmo", "Náhuat de Mecayapan", "Náhuatl", "Náhuatl sur de Veracruz", "Tatahuicapan Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80369,7 +83736,7 @@ "full": "nhy-Latn-MX", "iana": [ "Northern Oaxaca Nahuatl" ], "iso639_3": "nhy", - "localname": "nāhuatlahtōlli", + "localname": "Nāhuatlahtōlli", "localnames": [ "Nāhuatlahtōlli" ], "name": "Nahuatl, Northern Oaxaca", "names": [ "Mexicano", "Nahuatl de Oaxaca", "Northern Oaxaca Nahuatl", "Náhuatl", "Náhuatl del Norte de Oaxaca", "Nāhuatlahtōlli" ], @@ -80386,7 +83753,7 @@ "iana": [ "Santa María La Alta Nahuatl" ], "iso639_3": "nhz", "name": "Nahuatl, Santa María la Alta", - "names": [ "Náhuatl de Santa María la Alta", "Santa María La Alta Nahuatl" ], + "names": [ "Mexicano", "Náhuatl", "Náhuatl de Santa María la Alta", "Náhuatl de la Sierra Negra Norte", "Santa María La Alta Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -80445,7 +83812,7 @@ "iana": [ "Niellim" ], "iso639_3": "nie", "name": "Niellim", - "names": [ "Lua", "Mjillem", "Nielim", "Nyilem" ], + "names": [ "Lua", "Lwaà", "Mjillem", "Nielim", "Nyilem" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -80565,7 +83932,7 @@ "iso639_3": "nim", "localnames": [ "Kĩnilyamba" ], "name": "Nilamba", - "names": [ "Ikinilamba", "Ikiniramba", "Ilamba", "Ilinilamba", "Iramba", "Kinilamba", "Kiniramba", "Nilyamba", "Niramba", "Nyilamba", "Nyiramba" ], + "names": [ "Ikinilamba", "Ikiniramba", "Ilamba", "Ilinilamba", "Iramba", "Kinilamba", "Kiniramba", "Kĩnilyamba", "Nilyamba", "Niramba", "Nyilamba", "Nyiramba" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -80578,8 +83945,9 @@ "full": "nin-Latn-NG", "iana": [ "Ninzo" ], "iso639_3": "nin", + "localnames": [ "Ninzo" ], "name": "Ninzo", - "names": [ "Akiza", "Amar Tita", "Ancha", "Fadan Wate", "Gbhu", "Gbhu D Amar Randfa", "Hate", "Incha", "Kwasu", "Ninzam", "Nunzo" ], + "names": [ "Gbhu", "Gbhu D Amar Randfa", "Ninzam", "Ninzom", "Nunzo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -80608,6 +83976,7 @@ "full": "niq-Latn-KE", "iana": [ "Nandi" ], "iso639_3": "niq", + "macrolang": "kln", "name": "Nandi", "names": [ "Cemual", "Kalenjin Nandi", "Naandi" ], "region": "KE", @@ -80637,7 +84006,8 @@ "full": "nis-Latn-PG", "iana": [ "Nimi" ], "iso639_3": "nis", - "name": "Nimi", + "name": "Sama", + "names": [ "Nimi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -80742,9 +84112,9 @@ "iana": [ "Ngiti" ], "iso639_3": "niy", "localname": "Ndruna", - "localnames": [ "Ndruna" ], + "localnames": [ "Ndrûna" ], "name": "Ngiti", - "names": [ "Bindi", "Dru", "Druna", "Kingeti", "Kingiti", "Lendu-Sud", "Ndruna", "Ngeti" ], + "names": [ "Bindi", "Dru", "Druna", "Kingeti", "Kingiti", "Lendu-Sud", "Ndruna", "Ndrûna", "Ngeti" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -80771,8 +84141,9 @@ "full": "nja-Latn-NG", "iana": [ "Nzanyi" ], "iso639_3": "nja", + "localnames": [ "Wur Nzanyi" ], "name": "Nzanyi", - "names": [ "Jeng", "Jenge", "Kobochi", "Kobotshi", "Mzangyim", "Njai", "Njanyi", "Njegn", "Njei", "Njeing", "Njeng", "Njeny", "Nzangi", "Zani", "Zany" ], + "names": [ "Jeng", "Jenge", "Kobochi", "Kobotshi", "Mzangyim", "Njai", "Njanyi", "Njegn", "Njei", "Njeing", "Njeng", "Njeny", "Nzangi", "Wur Nzanyi", "Zani", "Zany" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -80890,9 +84261,9 @@ "full": "njn-Latn-IN", "iana": [ "Liangmai Naga" ], "iso639_3": "njn", - "localnames": [ "Lianglad" ], + "localnames": [ "Liangkhon", "Lianglad" ], "name": "Naga, Liangmai", - "names": [ "Kacha", "Lianglad", "Liangmai", "Liangmai Naga", "Liangmei", "Liyang", "Lyangmay", "Lyengmai", "Zeliang" ], + "names": [ "Kacha Naga", "Liangkhon", "Lianglad", "Liangmai", "Liangmai Naga", "Liangmei", "Liyang", "Lyangmay", "Lyangmei", "Lyengmai", "Quoireng", "Zeliang" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -80980,7 +84351,7 @@ "iana": [ "Kunyi" ], "iso639_3": "njx", "name": "Kunyi", - "names": [ "Kikunyi", "Kugni" ], + "names": [ "Kikuni", "Kikunyi", "Kugni", "Kuni" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -81007,34 +84378,34 @@ "windows": "njy-Latn" }, { - "full": "njz-Beng-IN", + "full": "njz-Latn-IN", "iana": [ "Nyishi" ], "iso639_3": "njz", + "localnames": [ "Nyishi" ], "name": "Nyishi", "names": [ "Bangni", "Dafla", "Daphla", "Lel", "Nil", "Nishi", "Nisi", "Nissi", "Nyising" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "njz-Beng", - "tags": [ "dap-Beng", "dap-Beng-IN" ], - "windows": "njz-Beng" + "tag": "njz", + "tags": [ "dap-Latn", "dap-Latn-IN", "njz-IN", "njz-Latn" ], + "windows": "njz-Latn" }, { - "full": "njz-Latn-IN", + "full": "njz-Beng-IN", "iana": [ "Nyishi" ], "iso639_3": "njz", - "localnames": [ "Nil" ], "name": "Nyishi", "names": [ "Bangni", "Dafla", "Daphla", "Lel", "Nil", "Nishi", "Nisi", "Nissi", "Nyising" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "njz-Latn", - "tags": [ "dap-Latn", "dap-Latn-IN" ], - "windows": "njz-Latn" + "tag": "njz-Beng", + "tags": [ "dap-Beng", "dap-Beng-IN" ], + "windows": "njz-Beng" }, { "full": "nka-Latn-ZM", @@ -81085,7 +84456,7 @@ "iana": [ "Koireng" ], "iso639_3": "nkd", "name": "Koireng", - "names": [ "Koirng", "Kolren", "Koren", "Kwoireng", "Liangmai", "Liangmei", "Liyang", "Liyangmai", "Lyengmai", "Quoireng" ], + "names": [ "Koirng", "Kolren", "Koren" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -81098,8 +84469,8 @@ "full": "nke-Latn-SB", "iana": [ "Duke" ], "iso639_3": "nke", - "name": "Duke", - "names": [ "Kolombangara", "Ndughore", "Nduke" ], + "name": "Dughore", + "names": [ "Duke", "Kolombangara", "Ndughore", "Nduke" ], "region": "SB", "regionname": "Solomon Islands", "script": "Latn", @@ -81156,32 +84527,33 @@ "windows": "nkh-Latn" }, { - "full": "nki-Beng-IN", + "full": "nki-Latn-IN", "iana": [ "Thangal Naga" ], "iso639_3": "nki", + "localnames": [ "Thangkhal Haam" ], "name": "Naga, Thangal", "names": [ "Khoirao", "Khoirao Naga", "Koirao", "Kolya", "Mayangkhang", "Miyang-Khang", "Ngari", "Thangal", "Thangal Naga", "Thanggal", "Thangkhal Haam", "Tukaimi" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "nki-Beng", - "windows": "nki-Beng" + "tag": "nki", + "tags": [ "nki-IN", "nki-Latn" ], + "windows": "nki-Latn" }, { - "full": "nki-Latn-IN", + "full": "nki-Beng-IN", "iana": [ "Thangal Naga" ], "iso639_3": "nki", - "localnames": [ "Thangkhal Haam" ], "name": "Naga, Thangal", "names": [ "Khoirao", "Khoirao Naga", "Koirao", "Kolya", "Mayangkhang", "Miyang-Khang", "Ngari", "Thangal", "Thangal Naga", "Thanggal", "Thangkhal Haam", "Tukaimi" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "nki-Latn", - "windows": "nki-Latn" + "tag": "nki-Beng", + "windows": "nki-Beng" }, { "full": "nkj-Latn-ID", @@ -81245,10 +84617,10 @@ "full": "nko-Latn-GH", "iana": [ "Nkonya" ], "iso639_3": "nko", - "localname": "Nkunya", - "localnames": [ "Nkunya" ], + "localname": "Nkonya", + "localnames": [ "Nkonya" ], "name": "Nkonya", - "names": [ "Nkunya", "Nkunyá" ], + "names": [ "Nkunya" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -81268,6 +84640,7 @@ "sldr": false, "tag": "nkp", "tags": [ "nkp-TO", "nkp-Zyyy" ], + "unwritten": true, "windows": "nkp-Zyyy" }, { @@ -81334,6 +84707,7 @@ "full": "nku-Latn-CI", "iana": [ "Bouna Kulango" ], "iso639_3": "nku", + "localnames": [ "Bouna Kulango" ], "name": "Kulango, Bouna", "names": [ "Bouna Koulango", "Bouna Kulango", "Buna Kulango", "Koulango", "Kulange", "Nkuraeng", "Nkurange" ], "region": "CI", @@ -81365,8 +84739,9 @@ "full": "nkw-Latn-CD", "iana": [ "Nkutu" ], "iso639_3": "nkw", + "localnames": [ "Lonkfukfutu" ], "name": "Nkutu", - "names": [ "Bankutu", "Nkuchu", "Nkucu", "Nkutshu" ], + "names": [ "Bankutu", "Lonkfukfutu", "Nkuchu", "Nkucu", "Nkutshu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -81409,9 +84784,9 @@ "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localnames": [ "Nederlands" ], "name": "Dutch", - "names": [ "Flemish", "Hollands", "Nederlands", "Vlaams" ], + "names": [ "Flemish", "Nederlands", "Vlaams" ], "region": "NL", "regionname": "Netherlands", "regions": [ "AU", "BR", "CA", "CH", "DE", "FI", "LU", "US", "ZA" ], @@ -81420,62 +84795,59 @@ "suppress": true, "tag": "nl", "tags": [ "nl-Latn", "nl-NL" ], - "windows": "nl" + "windows": "nl-NL" }, { "full": "nl-Latn-AW", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localnames": [ "Nederlands" ], "name": "Dutch", "names": [ "Flemish" ], "region": "AW", "regionname": "Aruba", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nl-AW", - "windows": "nl-AW" + "windows": "nl-Latn-AW" }, { "full": "nl-Latn-BE", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", - "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localname": "Vlaams", + "localnames": [ "Nederlands" ], "name": "Dutch", "names": [ "Flemish" ], "region": "BE", "regionname": "Belgium", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nl-BE", - "windows": "nl-BE" + "windows": "nl-Latn-BE" }, { "full": "nl-Latn-BQ", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localnames": [ "Nederlands" ], "name": "Dutch", "names": [ "Flemish" ], "region": "BQ", "regionname": "Bonaire, Sint Eustatius and Saba", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nl-BQ", - "windows": "nl-BQ" + "windows": "nl-Latn-BQ" }, { "full": "nl-Brai-NL", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "name": "Dutch", - "names": [ "Flemish", "Hollands", "Nederlands", "Vlaams" ], + "names": [ "Flemish", "Nederlands", "Vlaams" ], "nophonvars": true, "region": "NL", "regionname": "Netherlands", @@ -81483,55 +84855,52 @@ "script": "Brai", "sldr": false, "tag": "nl-Brai", - "windows": "nl-Brai" + "windows": "nl-Brai-NL" }, { "full": "nl-Latn-CW", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localnames": [ "Nederlands" ], "name": "Dutch", "names": [ "Flemish" ], "region": "CW", "regionname": "Curaçao", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nl-CW", - "windows": "nl-CW" + "windows": "nl-Latn-CW" }, { "full": "nl-Latn-SR", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localnames": [ "Nederlands" ], "name": "Dutch", "names": [ "Flemish" ], "region": "SR", "regionname": "Suriname", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nl-SR", - "windows": "nl-SR" + "windows": "nl-Latn-SR" }, { "full": "nl-Latn-SX", "iana": [ "Dutch", "Flemish" ], "iso639_3": "nld", "localname": "Nederlands", - "localnames": [ "Hollands", "Nederlands" ], + "localnames": [ "Nederlands" ], "name": "Dutch", "names": [ "Flemish" ], "region": "SX", "regionname": "Sint Maarten (Dutch part)", "script": "Latn", "sldr": true, - "suppress": true, "tag": "nl-SX", - "windows": "nl-SX" + "windows": "nl-Latn-SX" }, { "full": "nla-Latn-CM", @@ -81568,8 +84937,9 @@ "iana": [ "East Nyala" ], "iso639_3": "nle", "localnames": [ "Lunyala" ], + "macrolang": "luy", "name": "Nyala", - "names": [ "East Nyala", "LuNyala", "Lunyala", "OluNyala", "Olunyala" ], + "names": [ "Banyala", "East Nyala", "LuNyala", "Lunyala", "OluNyala", "Olunyala" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -81672,8 +85042,8 @@ "full": "nlo-Latn-CD", "iana": [ "Ngul" ], "iso639_3": "nlo", - "name": "Ngul", - "names": [ "Ingul", "Ngoli", "Nguli", "Ngulu", "Ngwii" ], + "name": "Ngwii", + "names": [ "Engwii", "Ingul", "Ngoli", "Ngul", "Nguli", "Ngulu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -81716,9 +85086,9 @@ "full": "nlv-Latn-MX", "iana": [ "Orizaba Nahuatl" ], "iso639_3": "nlv", - "localnames": [ "Āwillisāpan Nāwatl" ], + "localnames": [ "Nawatl" ], "name": "Nahuatl, Orizaba", - "names": [ "Nawatl de Orizaba", "Náhuatl de Orizaba", "Náhuatl de la Sierra de Zongolica", "Nāwatl", "Orizaba Aztec", "Orizaba Nahuatl", "Āwillisāpan Nāwatl" ], + "names": [ "Mexicano", "Nahuatl", "Nawatl", "Nawatl de Orizaba", "Náhuatl Central de Veracruz", "Náhuatl de Orizaba", "Náhuatl de la Sierra de Zongolica", "Orizaba Aztec", "Orizaba Nahuatl", "Āwillisāpan Nāwatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -81743,19 +85113,18 @@ "windows": "nlw-Latn" }, { - "full": "nlx-Zyyy-IN", + "full": "nlx-Deva-IN", "iana": [ "Nahali" ], "iso639_3": "nlx", "name": "Nahali", "names": [ "Kalto", "Nahal", "Nahale", "Nahalia" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "nlx", - "tags": [ "nlx-IN", "nlx-Zyyy" ], - "unwritten": true, - "windows": "nlx-Zyyy" + "tags": [ "nlx-Deva", "nlx-IN" ], + "windows": "nlx-Deva" }, { "full": "nly-Latn-AU", @@ -81822,7 +85191,7 @@ "iana": [ "Ngam" ], "iso639_3": "nmc", "name": "Ngam", - "names": [ "Ngahm", "Ngama", "Ngamh", "Sarngam" ], + "names": [ "Ngahm", "Ngama", "Ngamh", "Ngàm̄", "Sarngam" ], "region": "TD", "regionname": "Chad", "regions": [ "CF" ], @@ -81890,7 +85259,7 @@ "sldr": true, "tag": "nmg", "tags": [ "nmg-CM", "nmg-Latn" ], - "windows": "nmg-Latn" + "windows": "nmg-Latn-CM" }, { "full": "nmg-Latn-GQ", @@ -81957,7 +85326,7 @@ "iana": [ "Namakura" ], "iso639_3": "nmk", "name": "Namakura", - "names": [ "Makura", "Namakir" ], + "names": [ "Makura", "Namakir", "Namakuran" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -82028,32 +85397,33 @@ "windows": "nmn-Latn" }, { - "full": "nmo-Beng-IN", + "full": "nmo-Latn-IN", "iana": [ "Moyon Naga" ], "iso639_3": "nmo", + "localnames": [ "Bujuurchong" ], "name": "Naga, Moyon", "names": [ "Bujuurchong", "Mayol", "Mayon Naga", "Moyon", "Moyon Naga" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "nmo-Beng", - "windows": "nmo-Beng" + "tag": "nmo", + "tags": [ "nmo-IN", "nmo-Latn" ], + "windows": "nmo-Latn" }, { - "full": "nmo-Latn-IN", + "full": "nmo-Beng-IN", "iana": [ "Moyon Naga" ], "iso639_3": "nmo", - "localnames": [ "Bujuurchong" ], "name": "Naga, Moyon", "names": [ "Bujuurchong", "Mayol", "Mayon Naga", "Moyon", "Moyon Naga" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "nmo-Latn", - "windows": "nmo-Latn" + "tag": "nmo-Beng", + "windows": "nmo-Beng" }, { "full": "nmp-Latn-AU", @@ -82165,8 +85535,10 @@ "full": "nmw-Latn-PG", "iana": [ "Nimoa", "Rifao" ], "iso639_3": "nmw", + "localname": "Rifao", + "localnames": [ "Rifao" ], "name": "Rifao", - "names": [ "Nimoa", "Nimowa" ], + "names": [ "Nimoa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -82225,7 +85597,8 @@ "full": "nn-Latn-NO", "iana": [ "Norwegian Nynorsk" ], "iso639_3": "nno", - "localname": "nynorsk", + "localname": "norsk nynorsk", + "macrolang": "no", "name": "Norwegian Nynorsk", "region": "NO", "regionname": "Norway", @@ -82270,6 +85643,7 @@ "full": "nnc-Latn-TD", "iana": [ "Nancere" ], "iso639_3": "nnc", + "localnames": [ "Nangjere" ], "name": "Nancere", "names": [ "Nanchere", "Nangcere", "Nangjere", "Nangtchere", "Nanjeri", "Nantcere" ], "region": "TD", @@ -82299,10 +85673,11 @@ "full": "nne-Latn-AO", "iana": [ "Ngandyera" ], "iso639_3": "nne", - "name": "Ngandyera", - "names": [ "Ngandjera" ], + "name": "Ngandjera", + "names": [ "Ngandyera", "Oshingandjera" ], "region": "AO", "regionname": "Angola", + "regions": [ "NA" ], "script": "Latn", "sldr": false, "tag": "nne", @@ -82327,31 +85702,33 @@ "windows": "nnf-Latn" }, { - "full": "nng-Beng-IN", + "full": "nng-Latn-IN", "iana": [ "Maring Naga" ], "iso639_3": "nng", + "localnames": [ "Maringa Tlou" ], "name": "Naga, Maring", - "names": [ "Maring", "Maring Naga" ], - "nophonvars": true, + "names": [ "Maring", "Maring Naga", "Maringa Tlou" ], "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "nng-Beng", - "windows": "nng-Beng" + "tag": "nng", + "tags": [ "nng-IN", "nng-Latn" ], + "windows": "nng-Latn" }, { - "full": "nng-Latn-IN", + "full": "nng-Beng-IN", "iana": [ "Maring Naga" ], "iso639_3": "nng", "name": "Naga, Maring", - "names": [ "Maring", "Maring Naga" ], + "names": [ "Maring", "Maring Naga", "Maringa Tlou" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "nng-Latn", - "windows": "nng-Latn" + "tag": "nng-Beng", + "windows": "nng-Beng" }, { "full": "nnh-Latn-CM", @@ -82381,13 +85758,13 @@ "sldr": false, "tag": "nni", "tags": [ "nni-ID", "nni-Latn" ], - "unwritten": true, "windows": "nni-Latn" }, { "full": "nnj-Latn-ET", "iana": [ "Nyangatom" ], "iso639_3": "nnj", + "localnames": [ "Nga-Nyangatom" ], "name": "Nyangatom", "region": "ET", "regionname": "Ethiopia", @@ -82401,7 +85778,9 @@ "full": "nnk-Latn-PG", "iana": [ "Nankina" ], "iso639_3": "nnk", + "localnames": [ "Nankinawam" ], "name": "Nankina", + "names": [ "Nankinawam" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -82414,6 +85793,7 @@ "full": "nnl-Latn-IN", "iana": [ "Northern Rengma Naga" ], "iso639_3": "nnl", + "localnames": [ "Ntenyi" ], "name": "Naga, Northern Rengma", "names": [ "Northern Rengma", "Northern Rengma Naga", "Ntenyi", "Ntenyi Naga", "Nthenyi" ], "region": "IN", @@ -82450,15 +85830,30 @@ "sldr": false, "tag": "nnn", "tags": [ "nnn-Latn", "nnn-TD" ], - "unwritten": true, "windows": "nnn-Latn" }, + { + "full": "nnp-Wcho-IN", + "iana": [ "Wancho Naga" ], + "iso639_3": "nnp", + "name": "Naga, Wancho", + "names": [ "Banpara Naga", "Joboka", "Jokoba", "Naga Wancho", "Wancho", "Wancho Naga", "Wanchu" ], + "region": "IN", + "regionname": "India", + "script": "Wcho", + "sldr": false, + "tag": "nnp", + "tags": [ "nnp-IN", "nnp-Wcho" ], + "windows": "nnp-Wcho" + }, { "full": "nnp-Deva-IN", "iana": [ "Wancho Naga" ], "iso639_3": "nnp", + "latnnames": [ "Vaanko" ], + "localnames": [ "वांचो" ], "name": "Naga, Wancho", - "names": [ "Banpara Naga", "Joboka", "Jokoba", "Naga Wancho", "Wancho", "Wancho Naga" ], + "names": [ "Banpara Naga", "Joboka", "Jokoba", "Naga Wancho", "Wancho", "Wancho Naga", "Wanchu" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -82472,7 +85867,7 @@ "iana": [ "Wancho Naga" ], "iso639_3": "nnp", "name": "Naga, Wancho", - "names": [ "Banpara Naga", "Joboka", "Jokoba", "Naga Wancho", "Wancho", "Wancho Naga" ], + "names": [ "Banpara Naga", "Joboka", "Jokoba", "Naga Wancho", "Wancho", "Wancho Naga", "Wanchu" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -82480,20 +85875,6 @@ "tag": "nnp-Latn", "windows": "nnp-Latn" }, - { - "full": "nnp-Wcho-IN", - "iana": [ "Wancho Naga" ], - "iso639_3": "nnp", - "name": "Naga, Wancho", - "names": [ "Banpara Naga", "Joboka", "Jokoba", "Naga Wancho", "Wancho", "Wancho Naga" ], - "nophonvars": true, - "region": "IN", - "regionname": "India", - "script": "Wcho", - "sldr": false, - "tag": "nnp-Wcho", - "windows": "nnp-Wcho" - }, { "full": "nnq-Latn-TZ", "iana": [ "Ngindo" ], @@ -82525,18 +85906,18 @@ "windows": "nnr-Latn" }, { - "full": "nnt-Zyyy-US", + "full": "nnt-Latn-US", "iana": [ "Nanticoke" ], "iso639_3": "nnt", "name": "Nanticoke", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "nnt", - "tags": [ "nnt-US", "nnt-Zyyy" ], + "tags": [ "nnt-Latn", "nnt-US" ], "unwritten": true, - "windows": "nnt-Zyyy" + "windows": "nnt-Latn" }, { "full": "nnu-Latn-GH", @@ -82572,7 +85953,7 @@ "full": "nnw-Latn-BF", "iana": [ "Southern Nuni" ], "iso639_3": "nnw", - "localname": "Nuni, Southern", + "localname": "Nʋnɩ", "localnames": [ "Nʋnɩ" ], "name": "Nuni, Southern", "names": [ "Nibulu", "Nouni", "Nounouma", "Nuna", "Nune", "Nuni", "Nunuma", "Nuruma", "Nʋnɩ", "Southern Nuni" ], @@ -82618,25 +85999,40 @@ "full": "no-Latn-NO", "iana": [ "Norwegian" ], "iso639_3": "nor", + "localname": "norsk", "localnames": [ "Norsk" ], "name": "Norwegian", "names": [ "Norsk" ], "region": "NO", "regionname": "Norway", - "regions": [ "CA", "DE", "RU", "SE", "US" ], + "regions": [ "CA", "DE", "RU", "SE", "SJ", "US" ], "script": "Latn", - "sldr": false, + "sldr": true, "suppress": true, "tag": "no", "tags": [ "no-Latn", "no-NO" ], - "windows": "no" + "windows": "no-NO" + }, + { + "full": "no-Runr-001", + "iana": [ "Norwegian" ], + "iso639_3": "nor", + "name": "Norwegian", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "no-Runr", + "tags": [ "no-001" ], + "windows": "no-Runr-001" }, { "full": "noa-Latn-CO", "iana": [ "Woun Meu" ], "iso639_3": "noa", "localname": "Woun Meu", - "localnames": [ "Woun Meu" ], + "localnames": [ "Woun Meu", "Wounaan Meu" ], "name": "Woun Meu", "names": [ "Chanco", "Chocama", "Chocó", "Noanama", "Noenama", "Nonama", "Waumeo", "Waun Meo", "Waunana", "Waunméu", "Waunán", "Woun Meo", "Wounaan", "Wounaan Meu", "Wounmeu" ], "region": "CO", @@ -82681,6 +86077,7 @@ "iana": [ "Northern Thai" ], "iso639_3": "nod", "latnnames": [ "Kam Mueang" ], + "localname": "คำเมือง", "localnames": [ "คำเมือง" ], "name": "Thai, Northern", "names": [ "Kam Mueang", "Kam Mu’ang", "Kammyang", "Kammüang", "Khon", "Khon Mueang", "Khon Myang", "Khonmuang", "La Nya", "Lan Na", "Lanatai", "Lanna", "Lannatai", "Muang", "Mueang", "Mung", "Mu’ang", "Myang", "Northern Thai", "Payap", "Phayap", "Phyap", "Tai Nya", "Western Laotian" ], @@ -82744,6 +86141,7 @@ "iana": [ "Nomu" ], "iso639_3": "noh", "name": "Nomu", + "names": [ "Mida" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -82757,8 +86155,10 @@ "full": "noi-Deva-IN", "iana": [ "Noiri" ], "iso639_3": "noi", + "latnnames": [ "Noiri" ], + "localnames": [ "नोइरि" ], "name": "Noiri", - "names": [ "Mathwadi Bhilori" ], + "names": [ "Bhilori", "Mathwadi Bhilori" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -82784,19 +86184,19 @@ "windows": "noj-Latn" }, { - "full": "nok-Zyyy-US", + "full": "nok-Latn-US", "iana": [ "Nooksack" ], "iso639_3": "nok", "name": "Nooksack", "names": [ "Lhéchelesem", "Nootsack" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "nok", - "tags": [ "nok-US", "nok-Zyyy" ], + "tags": [ "nok-Latn", "nok-US" ], "unwritten": true, - "windows": "nok-Zyyy" + "windows": "nok-Latn" }, { "full": "nol-Zyyy-US", @@ -82813,21 +86213,6 @@ "unwritten": true, "windows": "nol-Zyyy" }, - { - "full": "nom-Latn-PE", - "iana": [ "Nocamán" ], - "iso639_3": "nom", - "name": "Nocamán", - "region": "PE", - "regionname": "Peru", - "regions": [ "BR" ], - "script": "Latn", - "sldr": false, - "tag": "nom", - "tags": [ "nom-Latn", "nom-PE" ], - "unwritten": true, - "windows": "nom-Latn" - }, { "full": "non-Runr-SE", "iana": [ "Old Norse" ], @@ -82835,6 +86220,7 @@ "name": "Old Norse", "region": "SE", "regionname": "Sweden", + "regions": [ "001" ], "script": "Runr", "sldr": false, "tag": "non", @@ -82845,8 +86231,9 @@ "full": "nop-Latn-PG", "iana": [ "Numanggang" ], "iso639_3": "nop", + "localnames": [ "Numaŋgaŋ" ], "name": "Numanggang", - "names": [ "Boana", "Kai", "Manggang", "Ngain", "Numangan", "Numangang", "Sugu" ], + "names": [ "Boana", "Kai", "Manggang", "Ngain", "Numangan", "Numangang", "Numaŋgaŋ", "Sugu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -82860,7 +86247,7 @@ "iana": [ "Ngongo" ], "iso639_3": "noq", "name": "Ngongo", - "names": [ "Ngong" ], + "names": [ "Gingooŋ", "Kingongo", "Ngong" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -82905,6 +86292,7 @@ "iana": [ "Ewage-Notu" ], "iso639_3": "nou", "name": "Ewage-Notu", + "names": [ "Notu-Ewage" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -82986,19 +86374,19 @@ "windows": "npa-Zyyy" }, { - "full": "npb-Zyyy-BT", + "full": "npb-Tibt-BT", "iana": [ "Nupbikha" ], "iso639_3": "npb", "name": "Nupbikha", - "names": [ "Trongsakha" ], + "names": [ "Nupbi", "Trongsakha" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "npb", - "tags": [ "npb-BT", "npb-Zyyy" ], + "tags": [ "npb-BT", "npb-Tibt" ], "unwritten": true, - "windows": "npb-Zyyy" + "windows": "npb-Tibt" }, { "full": "npg-Latn-MM", @@ -83035,7 +86423,7 @@ "iana": [ "Southeastern Puebla Nahuatl" ], "iso639_3": "npl", "name": "Nahuatl, Southeastern Puebla", - "names": [ "Mexicano", "Náhuatl del Sureste de Puebla", "Náhuatl del Valle de Tehuacán", "Southeastern Puebla Nahuatl", "Tehuacán Náhuatl" ], + "names": [ "Mexicano", "Náhuatl", "Náhuatl de la Sierra Negra Sur", "Náhuatl del Sureste de Puebla", "Náhuatl del Valle de Tehuacán", "Southeastern Puebla Nahuatl", "Tehuacán Náhuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -83049,6 +86437,7 @@ "iana": [ "Mondropolon" ], "iso639_3": "npn", "name": "Mondropolon", + "names": [ "Nawe Mondropolon" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -83120,8 +86509,9 @@ "full": "npy-Latn-ID", "iana": [ "Napu" ], "iso639_3": "npy", + "localnames": [ "Pekurehua" ], "name": "Napu", - "names": [ "Pekurehua" ], + "names": [ "Basa Napu", "Pekurehua" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -83207,8 +86597,8 @@ "full": "nqo-Nkoo-GN", "iana": [ "N'Ko", "N’Ko" ], "iso639_3": "nqo", - "name": "N’ko", - "names": [ "N'Ko", "N’Ko" ], + "name": "N’Ko", + "names": [ "N'Ko" ], "region": "GN", "regionname": "Guinea", "regions": [ "BF", "CI", "GM", "LR", "ML", "SL", "SN" ], @@ -83234,6 +86624,21 @@ "unwritten": true, "windows": "nqq-Latn" }, + { + "full": "nqt-Latn-NG", + "iana": [ "Nteng" ], + "iso639_3": "nqt", + "name": "Nteng", + "names": [ "Chasoekoeh", "Jasikit" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "nqt", + "tags": [ "nqt-Latn", "nqt-NG" ], + "unwritten": true, + "windows": "nqt-Latn" + }, { "full": "nqy-Latn-MM", "iana": [ "Akyaung Ari Naga" ], @@ -83300,6 +86705,7 @@ "full": "nrb-Latn-ER", "iana": [ "Nara" ], "iso639_3": "nrb", + "localname": "Nara", "localnames": [ "Nara" ], "name": "Nara", "names": [ "Nara bana", "Nera" ], @@ -83328,8 +86734,9 @@ "full": "nre-Latn-IN", "iana": [ "Southern Rengma Naga" ], "iso639_3": "nre", + "localnames": [ "Terüpvunyu", "Tseminyu" ], "name": "Naga, Southern Rengma", - "names": [ "Injang", "Moiyui", "Mon", "Mozhumi", "Nzong", "Nzonyu", "Rengma", "Rengma Naga", "Southern Rengma", "Southern Rengma Naga", "Unza", "Western Rengma" ], + "names": [ "Injang", "Moiyui", "Mon", "Mozhumi", "Nzong", "Nzonyu", "Rengma", "Rengma Naga", "Southern Rengma", "Southern Rengma Naga", "Terüpvunyu", "Tseminyu", "Unza", "Western Rengma" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -83340,11 +86747,11 @@ }, { "full": "nrf-Latn-JE", - "iana": [ "Jèrriais", "Guernésiais" ], + "iana": [ "Jèrriais", "Guernésiais", "Sercquiais" ], "iso639_3": "nrf", - "localnames": [ "Jèrriais" ], + "localnames": [ "Dgernesiais" ], "name": "Jèrriais", - "names": [ "Dgernesiais", "Guernsey French", "Guernésiais", "Jersey French", "Jersey Norman", "Norman French", "Normand", "Normaund" ], + "names": [ "Dgernesiais", "Guernsey French", "Guernésiais", "Jersey French", "Jersey Norman", "Norman French", "Normand", "Normaund", "Sercquiais" ], "region": "JE", "regionname": "Jersey", "regions": [ "FR", "GG" ], @@ -83359,6 +86766,7 @@ "iana": [ "Narango" ], "iso639_3": "nrg", "name": "Narango", + "names": [ "Farsaf", "Farsav", "Nambel" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -83372,7 +86780,7 @@ "iana": [ "Chokri Naga" ], "iso639_3": "nri", "name": "Naga, Chokri", - "names": [ "Chakhesang", "Chakrima Naga", "Chakru", "Chokri", "Chokri Naga", "Eastern Angami" ], + "names": [ "Chakhesang", "Chakhesang Chokri", "Chakrima Naga", "Chakru", "Chokri", "Chokri Chakhesang", "Chokri Naga", "Eastern Angami" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -83427,18 +86835,31 @@ "windows": "nrm-Latn" }, { - "full": "nrn-Zyyy-GB", + "full": "nrn-Runr-GB", "iana": [ "Norn" ], "iso639_3": "nrn", "name": "Norn", "region": "GB", "regionname": "United Kingdom", - "regions": [ "FR" ], - "script": "Zyyy", + "regions": [ "001", "FR" ], + "script": "Runr", "sldr": false, "tag": "nrn", - "tags": [ "nrn-GB", "nrn-Zyyy" ], - "windows": "nrn-Zyyy" + "tags": [ "nrn-GB", "nrn-Runr" ], + "windows": "nrn-Runr" + }, + { + "full": "nrn-Latn-GB", + "iana": [ "Norn" ], + "iso639_3": "nrn", + "name": "Norn", + "region": "GB", + "regionname": "United Kingdom", + "regions": [ "FR" ], + "script": "Latn", + "sldr": false, + "tag": "nrn-Latn", + "windows": "nrn-Latn" }, { "full": "nrp-Latn-IT", @@ -83483,19 +86904,77 @@ "windows": "nrt-Zyyy" }, { - "full": "nru-Zyyy-CN", + "full": "nru-Latn-CN", "iana": [ "Narua" ], "iso639_3": "nru", "name": "Narua", "names": [ "Eastern Naxi", "Meng yu", "Moso", "Mosso", "Mosuo", "Musuo yu", "Na", "Naru", "Nazu", "Yongning Na" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "nru", - "tags": [ "nru-CN", "nru-Zyyy" ], + "tags": [ "nru-CN", "nru-Latn" ], "unwritten": true, - "windows": "nru-Zyyy" + "windows": "nru-Latn" + }, + { + "full": "nru-Hans-CN", + "iana": [ "Narua" ], + "iso639_3": "nru", + "name": "Narua", + "names": [ "Eastern Naxi", "Meng yu", "Moso", "Mosso", "Mosuo", "Musuo yu", "Na", "Naru", "Nazu", "Yongning Na" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Hans", + "sldr": false, + "tag": "nru-Hans", + "windows": "nru-Hans" + }, + { + "full": "nru-Hant-CN", + "iana": [ "Narua" ], + "iso639_3": "nru", + "name": "Narua", + "names": [ "Eastern Naxi", "Meng yu", "Moso", "Mosso", "Mosuo", "Musuo yu", "Na", "Naru", "Nazu", "Yongning Na" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Hant", + "sldr": false, + "tag": "nru-Hant", + "windows": "nru-Hant" + }, + { + "full": "nru-Nkdb-CN", + "iana": [ "Narua" ], + "iso639_3": "nru", + "name": "Narua", + "names": [ "Eastern Naxi", "Meng yu", "Moso", "Mosso", "Mosuo", "Musuo yu", "Na", "Naru", "Nazu", "Yongning Na" ], + "nophonvars": true, + "obsolete": true, + "region": "CN", + "regionname": "China", + "script": "Nkdb", + "sldr": false, + "tag": "nru-Nkdb", + "windows": "nru-Nkdb" + }, + { + "full": "nru-Nkgb-CN", + "iana": [ "Narua" ], + "iso639_3": "nru", + "name": "Narua", + "names": [ "Eastern Naxi", "Meng yu", "Moso", "Mosso", "Mosuo", "Musuo yu", "Na", "Naru", "Nazu", "Yongning Na" ], + "nophonvars": true, + "obsolete": true, + "region": "CN", + "regionname": "China", + "script": "Nkgb", + "sldr": false, + "tag": "nru-Nkgb", + "windows": "nru-Nkgb" }, { "full": "nrx-Latn-AU", @@ -83516,7 +86995,6 @@ "full": "nrz-Latn-PG", "iana": [ "Lala" ], "iso639_3": "nrz", - "localnames": [ "Ala'ala" ], "name": "Lala", "names": [ "Ala’ala", "Nala", "Nara", "Pokau" ], "region": "PG", @@ -83544,11 +87022,13 @@ }, { "full": "nsb-Latn-ZA", + "iana": [ "Lower Nossob" ], "iso639_3": "nsb", "name": "Lower Nossob", "names": [ "Lower-Nosop" ], "region": "ZA", "regionname": "South Africa", + "regions": [ "BW" ], "script": "Latn", "sldr": false, "tag": "nsb", @@ -83587,6 +87067,7 @@ "full": "nse-Latn-ZM", "iana": [ "Nsenga" ], "iso639_3": "nse", + "localnames": [ "Chinsenga" ], "name": "Nsenga", "names": [ "Chinsenga", "Cinsenga", "Senga" ], "region": "ZM", @@ -83599,19 +87080,19 @@ "windows": "nse-Latn" }, { - "full": "nsf-Zyyy-CN", + "full": "nsf-Yiii-CN", "iana": [ "Northwestern Nisu" ], "iso639_3": "nsf", "name": "Nisu, Northwestern", "names": [ "Nisu", "Northwestern Nisu" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Yiii", "sldr": false, "tag": "nsf", - "tags": [ "nsf-CN", "nsf-Zyyy" ], + "tags": [ "nsf-CN", "nsf-Yiii" ], "unwritten": true, - "windows": "nsf-Zyyy" + "windows": "nsf-Yiii" }, { "full": "nsg-Latn-TZ", @@ -83633,7 +87114,7 @@ "iana": [ "Ngoshie" ], "iso639_3": "nsh", "name": "Ngoshie", - "names": [ "Ngishe", "Oshie" ], + "names": [ "Mise", "Ngishe", "Oshie" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -83668,7 +87149,7 @@ "region": "CA", "regionname": "Canada", "script": "Cans", - "sldr": false, + "sldr": true, "tag": "nsk", "tags": [ "nsk-CA", "nsk-Cans" ], "windows": "nsk-Cans" @@ -83824,21 +87305,36 @@ "tags": [ "nss-Latn", "nss-PG" ], "windows": "nss-Latn" }, + { + "full": "nst-Tnsa-IN", + "iana": [ "Tase Naga" ], + "iso639_3": "nst", + "name": "Naga, Tangsa", + "names": [ "Cham Chang", "Haimi", "Hawa", "Heimi", "Kuwa", "Naga, Tangshang", "Pangmi", "Pangwa", "Rangpan", "Rangpang", "Tangsa", "Tangsa Naga", "Tangshang", "Tangshang Naga", "Tangwa", "Tase", "Tase Naga", "Tasey" ], + "region": "IN", + "regionname": "India", + "regions": [ "MM" ], + "script": "Tnsa", + "sldr": false, + "tag": "nst", + "tags": [ "nst-IN", "nst-Tnsa" ], + "windows": "nst-Tnsa-IN" + }, { "full": "nst-Latn-MM", "iana": [ "Tase Naga" ], "iso639_3": "nst", "localnames": [ "Tangshang Naga" ], "name": "Naga, Tangshang", - "names": [ "Cham Chang", "Haimi", "Hawa", "Heimi", "Kuwa", "Naga, Tangsa", "Pangmi", "Pangwa", "Rangpan", "Rangpang", "Tangsa", "Tangshang", "Tangwa", "Tase", "Tase Naga", "Tasey" ], + "names": [ "Cham Chang", "Haimi", "Hawa", "Heimi", "Kuwa", "Naga, Tangsa", "Pangmi", "Pangwa", "Rangpan", "Rangpang", "Tangsa", "Tangsa Naga", "Tangshang", "Tangshang Naga", "Tangwa", "Tase", "Tase Naga", "Tasey" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], "script": "Latn", "sldr": false, - "tag": "nst", - "tags": [ "nst-Latn", "nst-MM" ], - "windows": "nst-Latn" + "tag": "nst-Latn", + "tags": [ "nst-MM" ], + "windows": "nst-Latn-MM" }, { "full": "nst-Latn-MM-x-moshang", @@ -83846,7 +87342,7 @@ "iso639_3": "nst", "localnames": [ "Tangshang Naga" ], "name": "Naga, Tangshang", - "names": [ "Cham Chang", "Haimi", "Hawa", "Heimi", "Kuwa", "Naga, Tangsa", "Naga, Tangshang - Moshang Hawa", "Pangmi", "Pangwa", "Rangpan", "Rangpang", "Tangsa", "Tangshang", "Tangwa", "Tase", "Tase Naga", "Tasey" ], + "names": [ "Cham Chang", "Haimi", "Hawa", "Heimi", "Kuwa", "Naga, Tangsa", "Naga, Tangshang - Moshang Hawa", "Pangmi", "Pangwa", "Rangpan", "Rangpang", "Tangsa", "Tangsa Naga", "Tangshang", "Tangshang Naga", "Tangwa", "Tase", "Tase Naga", "Tasey" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -83854,15 +87350,16 @@ "sldr": false, "tag": "nst-x-moshang", "tags": [ "nst-Latn-x-moshang", "nst-MM-x-moshang" ], - "windows": "nst-Latn-x-moshang" + "windows": "nst-Latn-MM-x-moshang" }, { "full": "nsu-Latn-MX", "iana": [ "Sierra Negra Nahuatl" ], "iso639_3": "nsu", "localname": "Nahuatl", + "localnames": [ "Nahuatl", "Nahuatlajtoli" ], "name": "Nahuatl, Sierra Negra", - "names": [ "Náhuatl de la Sierra Negra", "Sierra Negra Nahuatl" ], + "names": [ "Mexicano", "Mexicatl", "Nahuatl", "Nahuatlajtoli", "Náhuatl de la Sierra Negra", "Náhuatl de la Sierra Negra Norte", "Sierra Negra Nahuatl", "Totlajtol tlen ik titlapovah Alcomunga" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -83962,7 +87459,7 @@ "iana": [ "Nathembo" ], "iso639_3": "nte", "name": "Nathembo", - "names": [ "Esakaji", "Sakaji", "Sakati", "Sanagage", "Sangaji", "Sankaji", "Theithei" ], + "names": [ "Enatthembo", "Esakaji", "Esangaje", "Esangaji", "Eshangáaji", "Sakaji", "Sakati", "Sanagage", "Sangaje", "Sangaji", "Sankaji" ], "region": "MZ", "regionname": "Mozambique", "script": "Latn", @@ -84022,7 +87519,7 @@ "iso639_3": "ntk", "localnames": [ "Eghiikoma" ], "name": "Ikoma-Nata-Isenye", - "names": [ "Eghiikoma", "Kiikoma" ], + "names": [ "Eghiikoma" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -84052,7 +87549,7 @@ "iana": [ "Ntomba" ], "iso639_3": "nto", "name": "Ntomba", - "names": [ "Lontomba", "Luntumba", "Ntomba-Bolia", "Ntumba" ], + "names": [ "Lia-Ntomba", "Lontomba", "Luntumba", "Ntomba-Bolia", "Ntumba" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -84065,10 +87562,10 @@ "full": "ntp-Latn-MX", "iana": [ "Northern Tepehuan" ], "iso639_3": "ntp", - "localname": "O'otham", - "localnames": [ "O'otham" ], + "localname": "Oodamicʌdʌ", + "localnames": [ "Oodamicʌdʌ" ], "name": "Tepehuan, Northern", - "names": [ "Northern Tepehuan", "O’otham", "Tepehuán del Norte" ], + "names": [ "Northern Tepehuan", "Oodamicʌdʌ", "O’otham", "Tepehuán del Norte" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -84081,6 +87578,8 @@ "full": "ntr-Latn-GH", "iana": [ "Delo" ], "iso639_3": "ntr", + "localname": "Delo", + "localnames": [ "Delo" ], "name": "Delo", "names": [ "Ntribou", "Ntribu", "Ntrubo" ], "region": "GH", @@ -84197,8 +87696,8 @@ "full": "nud-Latn-PG", "iana": [ "Ngala" ], "iso639_3": "nud", - "name": "Ngala", - "names": [ "Kara", "Sogap", "Swagup" ], + "name": "Gala", + "names": [ "Kara", "Ngala", "Sogap", "Swagup" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -84232,7 +87731,7 @@ "regionname": "China", "regions": [ "MM" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "nuf", "tags": [ "nuf-CN", "nuf-Latn" ], "windows": "nuf-Latn" @@ -84270,6 +87769,8 @@ "full": "nui-Latn-GQ", "iana": [ "Ngumbi" ], "iso639_3": "nui", + "localname": "Kombe", + "localnames": [ "Kombe" ], "name": "Kombe", "names": [ "Combe", "Ndowe", "Ngumbi" ], "region": "GQ", @@ -84285,9 +87786,9 @@ "iana": [ "Nyole" ], "iso639_3": "nuj", "localname": "Lunyole", - "localnames": [ "Lunyole" ], + "localnames": [ "Lunyole", "Olunyole" ], "name": "Nyole", - "names": [ "LoNyole", "Lunyole", "Nyule", "Nyuli" ], + "names": [ "LoNyole", "Lunyole", "Nyule", "Nyuli", "Olunyole" ], "region": "UG", "regionname": "Uganda", "script": "Latn", @@ -84321,8 +87822,8 @@ "regionname": "Indonesia", "script": "Latn", "sldr": false, - "tag": "nul", - "tags": [ "nul-ID", "nul-Latn" ], + "tag": "nul-Latn", + "tags": [ "nul", "nul-ID" ], "unwritten": true, "windows": "nul-Latn" }, @@ -84413,7 +87914,7 @@ "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "nuq", "tags": [ "nuq-Latn", "nuq-PG" ], "windows": "nuq-Latn" @@ -84437,9 +87938,9 @@ "iana": [ "Nuer" ], "iso639_3": "nus", "localname": "Thok Nath", - "localnames": [ "Naath" ], + "localnames": [ "Thok Nath" ], "name": "Nuer", - "names": [ "Naadh", "Naath" ], + "names": [ "Naadh", "Naath", "Nuaar", "Thok Naath", "Thok Nath", "Thok Nuerä", "Thok Nuäärä" ], "region": "SS", "regionname": "South Sudan", "regions": [ "ET", "SD" ], @@ -84453,8 +87954,9 @@ "full": "nut-Latn-VN", "iana": [ "Nung (Viet Nam)" ], "iso639_3": "nut", + "localnames": [ "Nohng" ], "name": "Nung", - "names": [ "Bu-Nong", "Highland Nung", "Lungchow", "Nong", "Nung (Viet Nam)", "Tai Nung", "Tay", "Tày Nùng" ], + "names": [ "Bu-Nong", "Highland Nung", "Lungchow", "Nohng", "Nong", "Nung (Viet Nam)", "Tai Nung", "Tay", "Tày Nùng" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "CA", "LA", "US" ], @@ -84511,7 +88013,7 @@ "iana": [ "Mehek" ], "iso639_3": "nux", "name": "Mehek", - "names": [ "Driafleisuma", "Indinogosima", "Me’ek", "Nuku" ], + "names": [ "Driafleisuma", "Indinogosima", "Mehek Suma", "Me’ek", "Nuku" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -84540,8 +88042,9 @@ "full": "nuz-Latn-MX", "iana": [ "Tlamacazapa Nahuatl" ], "iso639_3": "nuz", + "localnames": [ "Nawatlajtoli" ], "name": "Nahuatl, Tlamacazapa", - "names": [ "Tlamacazapa Nahuatl" ], + "names": [ "Mexicano", "Mexicano de Guerrero", "Nawatlajtoli", "Tlamacazapa Nahuatl" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -84554,7 +88057,7 @@ "full": "nv-Latn-US", "iana": [ "Navajo", "Navaho" ], "iso639_3": "nav", - "localnames": [ "Diné Bizaad", "Naabeehó bizaad" ], + "localnames": [ "Diné Bizaad" ], "name": "Navajo", "names": [ "Diné", "Diné Bizaad", "Naabeehó bizaad", "Navaho" ], "region": "US", @@ -84641,18 +88144,74 @@ "windows": "nwb-Latn" }, { - "full": "nwc-Zyyy-NP", + "full": "nwc-Newa-NP", "iana": [ "Classical Newari", "Classical Nepal Bhasa", "Old Newari" ], "iso639_3": "nwc", "name": "Classical Newari", "names": [ "Classical Nepal Bhasa", "Old Newari" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Newa", "sldr": false, "tag": "nwc", - "tags": [ "nwc-NP", "nwc-Zyyy" ], - "windows": "nwc-Zyyy" + "tags": [ "nwc-NP", "nwc-Newa" ], + "windows": "nwc-Newa" + }, + { + "full": "nwc-Brah-NP", + "iana": [ "Classical Newari", "Classical Nepal Bhasa", "Old Newari" ], + "iso639_3": "nwc", + "name": "Classical Newari", + "names": [ "Classical Nepal Bhasa", "Old Newari" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Brah", + "sldr": false, + "tag": "nwc-Brah", + "windows": "nwc-Brah" + }, + { + "full": "nwc-Deva-NP", + "iana": [ "Classical Newari", "Classical Nepal Bhasa", "Old Newari" ], + "iso639_3": "nwc", + "name": "Classical Newari", + "names": [ "Classical Nepal Bhasa", "Old Newari" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Deva", + "sldr": false, + "tag": "nwc-Deva", + "windows": "nwc-Deva" + }, + { + "full": "nwc-Ranj-NP", + "iana": [ "Classical Newari", "Classical Nepal Bhasa", "Old Newari" ], + "iso639_3": "nwc", + "name": "Classical Newari", + "names": [ "Classical Nepal Bhasa", "Old Newari" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Ranj", + "sldr": false, + "tag": "nwc-Ranj", + "windows": "nwc-Ranj" + }, + { + "full": "nwc-Sidd-NP", + "iana": [ "Classical Newari", "Classical Nepal Bhasa", "Old Newari" ], + "iso639_3": "nwc", + "name": "Classical Newari", + "names": [ "Classical Nepal Bhasa", "Old Newari" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Sidd", + "sldr": false, + "tag": "nwc-Sidd", + "windows": "nwc-Sidd" }, { "full": "nwe-Latn-CM", @@ -84744,6 +88303,20 @@ "unwritten": true, "windows": "nwr-Latn" }, + { + "full": "nww-Latn-TZ", + "iana": [ "Ndwewe" ], + "iso639_3": "nww", + "name": "Ndwewe", + "names": [ "Kindwewe" ], + "region": "TZ", + "regionname": "United Republic of Tanzania", + "script": "Latn", + "sldr": false, + "tag": "nww", + "tags": [ "nww-Latn", "nww-TZ" ], + "windows": "nww-Latn" + }, { "full": "nwx-Deva-NP", "iana": [ "Middle Newar" ], @@ -84762,6 +88335,7 @@ "iana": [ "Nottoway-Meherrin" ], "iso639_3": "nwy", "name": "Nottoway-Meherrin", + "names": [ "Cheroenhaka" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -84804,14 +88378,13 @@ "iana": [ "Nage" ], "iso639_3": "nxe", "name": "Nage", - "names": [ "Nage-Keo", "Nagé" ], + "names": [ "Nage-Keo" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", "sldr": false, "tag": "nxe", "tags": [ "nxe-ID", "nxe-Latn" ], - "unwritten": true, "windows": "nxe-Latn" }, { @@ -84849,7 +88422,7 @@ "iana": [ "Koki Naga" ], "iso639_3": "nxk", "name": "Naga, Kokak", - "names": [ "Kokak", "Koki", "Koki Naga", "Konke" ], + "names": [ "Goga", "Kokak", "Koki", "Koki Naga", "Konke" ], "region": "MM", "regionname": "Myanmar", "script": "Zyyy", @@ -84874,16 +88447,16 @@ "windows": "nxl-Latn" }, { - "full": "nxm-Zyyy-XX", + "full": "nxm-Tfng-XX", "iana": [ "Numidian" ], "iso639_3": "nxm", "name": "Numidian", "region": "XX", - "script": "Zyyy", + "script": "Tfng", "sldr": false, "tag": "nxm", - "tags": [ "nxm-XX", "nxm-Zyyy" ], - "windows": "nxm-Zyyy" + "tags": [ "nxm-Tfng", "nxm-XX" ], + "windows": "nxm-Tfng" }, { "full": "nxn-Latn-AU", @@ -84928,6 +88501,34 @@ "tags": [ "nbf", "nbf-CN", "nbf-Latn", "nbf-Latn-CN", "nxq-CN", "nxq-Latn" ], "windows": "nxq-Latn" }, + { + "full": "nxq-Hans-CN", + "iana": [ "Naxi" ], + "iso639_3": "nxq", + "name": "Naxi", + "names": [ "Lomi", "Mu", "Nahsi", "Nakhi", "Naqxi", "Nasi" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Hans", + "sldr": false, + "tag": "nxq-Hans", + "windows": "nxq-Hans" + }, + { + "full": "nxq-Hant-CN", + "iana": [ "Naxi" ], + "iso639_3": "nxq", + "name": "Naxi", + "names": [ "Lomi", "Mu", "Nahsi", "Nakhi", "Naqxi", "Nasi" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Hant", + "sldr": false, + "tag": "nxq-Hant", + "windows": "nxq-Hant" + }, { "full": "nxq-Lisu-CN", "iana": [ "Naxi" ], @@ -84942,6 +88543,21 @@ "tag": "nxq-Lisu", "windows": "nxq-Lisu" }, + { + "full": "nxq-Nkdb-CN", + "iana": [ "Naxi" ], + "iso639_3": "nxq", + "name": "Naxi", + "names": [ "Lomi", "Mu", "Nahsi", "Nakhi", "Naqxi", "Nasi" ], + "nophonvars": true, + "obsolete": true, + "region": "CN", + "regionname": "China", + "script": "Nkdb", + "sldr": false, + "tag": "nxq-Nkdb", + "windows": "nxq-Nkdb" + }, { "full": "nxq-Nkgb-CN", "iana": [ "Naxi" ], @@ -84970,6 +88586,7 @@ "sldr": true, "tag": "nxr", "tags": [ "nxr-Latn", "nxr-PG" ], + "unwritten": true, "windows": "nxr-Latn" }, { @@ -84993,7 +88610,7 @@ "localname": "Chicheŵa", "localnames": [ "Chicheŵa" ], "name": "Chichewa", - "names": [ "Chewa", "Chicheŵa", "Chinjanja", "Chinyanja", "Cinianja", "Cinyanja", "Nyanja", "Nyanja-Chewa" ], + "names": [ "Chewa", "Chicheŵa", "Chinyanja", "CiNyanja", "Cinianja", "Cinyanja", "Nyanja", "Nyanja-Chewa" ], "region": "MW", "regionname": "Malawi", "regions": [ "BW", "MZ", "SZ", "ZM", "ZW" ], @@ -85009,7 +88626,7 @@ "iana": [ "Nyanja", "Chewa", "Chichewa" ], "iso639_3": "nya", "name": "Chichewa", - "names": [ "Chewa", "Chicheŵa", "Chinjanja", "Chinyanja", "Cinianja", "Cinyanja", "Nyanja", "Nyanja-Chewa" ], + "names": [ "Chewa", "Chicheŵa", "Chinyanja", "CiNyanja", "Cinianja", "Cinyanja", "Nyanja", "Nyanja-Chewa" ], "nophonvars": true, "region": "MW", "regionname": "Malawi", @@ -85023,12 +88640,13 @@ "full": "nyb-Latn-GH", "iana": [ "Nyangbo" ], "iso639_3": "nyb", - "name": "Nyangbo", - "names": [ "Tutrugbu" ], + "localnames": [ "Tutrugbu" ], + "name": "Nyagbo", + "names": [ "Nyangbo", "Tutrugbu" ], "region": "GH", "regionname": "Ghana", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "nyb", "tags": [ "nyb-GH", "nyb-Latn" ], "unwritten": true, @@ -85053,9 +88671,10 @@ "full": "nyd-Latn-KE", "iana": [ "Nyore", "Olunyole" ], "iso639_3": "nyd", - "localnames": [ "Olunyole" ], + "localnames": [ "Lunyole", "Olunyole" ], + "macrolang": "luy", "name": "Olunyole", - "names": [ "Lunyole", "Lunyore", "Nyole", "Nyoole", "Nyore", "Olunyore" ], + "names": [ "Banyole", "Banyore", "Lunyole", "Lunyore", "Nyole", "Nyoole", "Nyore", "Olunyore" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -85086,7 +88705,7 @@ "localname": "Kigiryama", "localnames": [ "Kigiryama" ], "name": "Kigiryama", - "names": [ "Agiryama", "Giriama", "Giryama", "Kigiriama", "Kinyika", "Nika", "Nyika" ], + "names": [ "Agiryama", "Giriama", "Giryama", "Kigiriama" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -85099,7 +88718,9 @@ "full": "nyg-Latn-CD", "iana": [ "Nyindu" ], "iso639_3": "nyg", + "localnames": [ "Kinyindu" ], "name": "Nyindu", + "names": [ "Kinyindu" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -85157,9 +88778,9 @@ "full": "nyk-Latn-AO", "iana": [ "Nyaneka" ], "iso639_3": "nyk", - "localnames": [ "Olunyaneka" ], + "localnames": [ "Oluñaneka" ], "name": "Nyaneka", - "names": [ "Lunyaneka", "Nhaneca", "Nhaneka", "Olunyaneka" ], + "names": [ "Lunyaneka", "Nhaneca", "Nhaneka", "Olunyaneka", "Oluñaneka" ], "region": "AO", "regionname": "Angola", "script": "Latn", @@ -85169,19 +88790,19 @@ "windows": "nyk-Latn" }, { - "full": "nyl-Zyyy-TH", + "full": "nyl-Thai-TH", "iana": [ "Nyeu" ], "iso639_3": "nyl", "name": "Nyeu", "names": [ "Yeu", "Yoe" ], "region": "TH", "regionname": "Thailand", - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "nyl", - "tags": [ "nyl-TH", "nyl-Zyyy" ], + "tags": [ "nyl-TH", "nyl-Thai" ], "unwritten": true, - "windows": "nyl-Zyyy" + "windows": "nyl-Thai" }, { "full": "nym-Latn-TZ", @@ -85189,7 +88810,7 @@ "iso639_3": "nym", "localnames": [ "Kinyamwezi" ], "name": "Nyamwezi", - "names": [ "Kinyamuesi", "Kinyamwesi", "Kinyamwezi", "Manjamuesi", "Mwezi", "Namwezi", "Nyamwesi", "Nyawezi" ], + "names": [ "KiNyamweezi", "Kinyamuesi", "Kinyamwesi", "Kinyamwezi", "Manjamuesi", "Mwezi", "Namwezi", "Nyamwesi", "Nyawezi" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -85205,9 +88826,10 @@ "localname": "Runyankore", "localnames": [ "Runyankore" ], "name": "Nyankore", - "names": [ "Nkole", "Nyankole", "Olunyankole", "Runyankole", "Runyankore", "Ulunyankole", "Ulunyankore" ], + "names": [ "Hima", "Nkole", "Nyankole", "Olunyankole", "Runyankole", "Runyankore", "Ulunyankole", "Ulunyankore" ], "region": "UG", "regionname": "Uganda", + "regions": [ "RW" ], "script": "Latn", "sldr": true, "tag": "nyn", @@ -85223,6 +88845,7 @@ "names": [ "Kyopi", "Lunyoro", "Runyoro" ], "region": "UG", "regionname": "Uganda", + "regions": [ "RW" ], "script": "Latn", "sldr": false, "tag": "nyo", @@ -85332,18 +88955,46 @@ "windows": "nyv-Latn" }, { - "full": "nyw-Zyyy-TH", + "full": "nyw-Thai-TH", "iana": [ "Nyaw" ], "iso639_3": "nyw", "name": "Nyaw", "names": [ "Jo", "Nyo", "Nyoh", "Yo" ], "region": "TH", "regionname": "Thailand", - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "nyw", - "tags": [ "nyw-TH", "nyw-Zyyy" ], - "windows": "nyw-Zyyy" + "tags": [ "nyw-TH", "nyw-Thai" ], + "windows": "nyw-Thai" + }, + { + "full": "nyw-Latn-TH", + "iana": [ "Nyaw" ], + "iso639_3": "nyw", + "name": "Nyaw", + "names": [ "Jo", "Nyo", "Nyoh", "Yo" ], + "region": "TH", + "regionname": "Thailand", + "script": "Latn", + "sldr": false, + "tag": "nyw-Latn", + "windows": "nyw-Latn" + }, + { + "full": "nyw-Zzzz-TH-x-taiyo", + "iana": [ "Nyaw" ], + "iso639_3": "nyw", + "name": "Nyaw", + "names": [ "Jo", "Nyo", "Nyoh", "Yo" ], + "nophonvars": true, + "region": "TH", + "regionname": "Thailand", + "script": "Zzzz", + "sldr": false, + "tag": "nyw-Zzzz-x-taiyo", + "tags": [ "nyw-TH-x-taiyo" ], + "windows": "nyw-Zzzz-x-taiyo" }, { "full": "nyx-Latn-AU", @@ -85384,7 +89035,7 @@ "localname": "Mbémbe", "localnames": [ "Mbémbe" ], "name": "Mbembe, Tigon", - "names": [ "Akonto", "Akwanto", "Mbémbe", "Noale", "Tigim", "Tigon", "Tigon Mbembe", "Tigong", "Tigum", "Tigun", "Tikun", "Tukun", "cha Mbémbe", "chya Mbembe", "cya Mbembe" ], + "names": [ "Akonto", "Akwanto", "Mbémbe", "Noale", "Tigon", "Tigon Mbembe", "Tigong", "Tigum", "Tigun", "Tikun", "Tukun", "cha Mbémbe", "chya Mbembe", "cya Mbembe" ], "region": "CM", "regionname": "Cameroon", "regions": [ "NG" ], @@ -85428,6 +89079,7 @@ "full": "nzi-Latn-GH", "iana": [ "Nzima" ], "iso639_3": "nzi", + "localnames": [ "Nzema" ], "name": "Nzema", "names": [ "Appolo", "Nzima" ], "region": "GH", @@ -85443,8 +89095,9 @@ "full": "nzk-Latn-CF", "iana": [ "Nzakara" ], "iso639_3": "nzk", + "localnames": [ "Nzakala" ], "name": "Nzakara", - "names": [ "Ansakara", "N’sakara", "Sakara", "Zakara" ], + "names": [ "Ansakara", "Nzakala", "N’sakara", "Sakara", "Zakara" ], "region": "CF", "regionname": "Central African Republic", "regions": [ "CD" ], @@ -85469,6 +89122,18 @@ "tags": [ "nzm-IN", "nzm-Latn" ], "windows": "nzm-Latn" }, + { + "full": "nzr-Latn-NG", + "iso639_3": "nzr", + "name": "Dir-Nyamzak-Mbarimi", + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "nzr", + "tags": [ "nzr-Latn", "nzr-NG", "plj", "plj-Latn", "plj-Latn-NG", "plj-NG" ], + "windows": "nzr-Latn" + }, { "full": "nzs-Zxxx-NZ", "iana": [ "New Zealand Sign Language" ], @@ -85488,7 +89153,7 @@ "iana": [ "Teke-Nzikou" ], "iso639_3": "nzu", "name": "Teke-Nzikou", - "names": [ "Central Teke", "Ndzindziu", "Njiunjiu", "Njyunjyu" ], + "names": [ "Central Teke", "Ityɔɔ", "Ndzindziu", "Njiunjiu", "Njyunjyu", "Téké des Plateaux" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -85588,23 +89253,24 @@ "windows": "oav-Geor" }, { - "full": "obi-Zyyy-US", + "full": "obi-Latn-US", "iana": [ "Obispeño" ], "iso639_3": "obi", "name": "Obispeño", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "obi", - "tags": [ "obi-US", "obi-Zyyy" ], + "tags": [ "obi-Latn", "obi-US" ], "unwritten": true, - "windows": "obi-Zyyy" + "windows": "obi-Latn" }, { "full": "obk-Latn-PH", "iana": [ "Southern Bontok" ], "iso639_3": "obk", + "macrolang": "bkb", "name": "Bontok, Southern", "names": [ "Bontoc", "Southern Bontoc", "Southern Bontok" ], "region": "PH", @@ -85648,9 +89314,9 @@ "iana": [ "Obo Manobo" ], "iso639_3": "obo", "localname": "Manobo", - "localnames": [ "Manobo" ], + "localnames": [ "Minonuvu", "Monuvu'" ], "name": "Manobo, Obo", - "names": [ "Bagobo", "Kidapawan Manobo", "Manobo", "Manuvu’", "Obo Bagobo", "Obo Manobo" ], + "names": [ "Bagobo", "Manobo", "Manuvu’", "Minonuvu", "Monuvu’", "Obo Bagobo", "Obo Manobo" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -85706,17 +89372,32 @@ "iana": [ "Occitan (post 1500)" ], "iso639_3": "oci", "localname": "occitan", + "localnames": [ "Lenga d'oc", "Lengo d'o" ], "name": "Occitan", - "names": [ "Aranese", "Aranés", "Aranés Occitán", "Arnais", "Gascon, Aranese", "Gascón", "Occitan (post 1500)", "Occitani", "Provenzale", "Provençal" ], + "names": [ "Aranais", "Aranese", "Aranés", "Aranés Occitán", "Gascon Aranese", "Langue d’oc", "Lenga d’oc", "Lengo d’o", "Occitan (post 1500)", "Provenzale", "Provençal" ], "region": "FR", "regionname": "France", - "regions": [ "ES", "IT", "MC" ], + "regions": [ "IT", "MC" ], "script": "Latn", "sldr": true, "tag": "oc", "tags": [ "oc-FR", "oc-Latn" ], "variants": [ "aranes", "auvern", "cisaup", "creiss", "gascon", "grclass", "grital", "grmistr", "lemosin", "lengadoc", "nicard", "provenc", "vivaraup" ], - "windows": "oc-Latn" + "windows": "oc-Latn-FR" + }, + { + "full": "oc-Latn-ES", + "iana": [ "Occitan (post 1500)" ], + "iso639_3": "oci", + "localname": "occitan", + "localnames": [ "Lenga d'oc", "Lengo d'o" ], + "name": "Occitan (post 1500)", + "region": "ES", + "regionname": "Spain", + "script": "Latn", + "sldr": true, + "tag": "oc-ES", + "windows": "oc-Latn-ES" }, { "full": "oca-Latn-PE", @@ -85747,6 +89428,18 @@ "tags": [ "och-CN", "och-Zyyy" ], "windows": "och-Zyyy" }, + { + "full": "ocm-Zyyy-XX", + "iana": [ "Old Cham" ], + "iso639_3": "ocm", + "name": "Old Cham", + "region": "XX", + "script": "Zyyy", + "sldr": false, + "tag": "ocm", + "tags": [ "ocm-XX", "ocm-Zyyy" ], + "windows": "ocm-Zyyy" + }, { "full": "oco-Latn-GB", "iana": [ "Old Cornish" ], @@ -85849,18 +89542,32 @@ "windows": "ofo-Zyyy" }, { - "full": "ofs-Zyyy-NL", + "full": "ofs-Latn-NL", "iana": [ "Old Frisian" ], "iso639_3": "ofs", "name": "Old Frisian", "region": "NL", "regionname": "Netherlands", "regions": [ "DE" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ofs", - "tags": [ "ofs-NL", "ofs-Zyyy" ], - "windows": "ofs-Zyyy" + "tags": [ "ofs-Latn", "ofs-NL" ], + "windows": "ofs-Latn" + }, + { + "full": "ofs-Runr-NL", + "iana": [ "Old Frisian" ], + "iso639_3": "ofs", + "name": "Old Frisian", + "nophonvars": true, + "region": "NL", + "regionname": "Netherlands", + "regions": [ "001", "DE" ], + "script": "Runr", + "sldr": false, + "tag": "ofs-Runr", + "windows": "ofs-Runr" }, { "full": "ofu-Latn-NG", @@ -85874,7 +89581,6 @@ "sldr": false, "tag": "ofu", "tags": [ "ofu-Latn", "ofu-NG" ], - "unwritten": true, "windows": "ofu-Latn" }, { @@ -85954,7 +89660,7 @@ "iana": [ "Ogbronuagum" ], "iso639_3": "ogu", "name": "Ogbronuagum", - "names": [ "Bukuma", "Obronuagum" ], + "names": [ "Agum", "Bukuma", "Obronuagum" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -85965,42 +89671,43 @@ "windows": "ogu-Latn" }, { - "full": "oht-Zyyy-TR", + "full": "oht-Xsux-TR", "iana": [ "Old Hittite" ], "iso639_3": "oht", "name": "Old Hittite", "region": "TR", "regionname": "Turkey", - "script": "Zyyy", + "script": "Xsux", "sldr": false, "tag": "oht", - "tags": [ "oht-TR", "oht-Zyyy" ], - "windows": "oht-Zyyy" + "tags": [ "oht-TR", "oht-Xsux" ], + "windows": "oht-Xsux" }, { - "full": "ohu-Hung-HU", + "full": "ohu-Latn-HU", "iana": [ "Old Hungarian" ], "iso639_3": "ohu", "name": "Old Hungarian", - "nophonvars": true, "region": "HU", "regionname": "Hungary", - "script": "Hung", + "script": "Latn", "sldr": false, - "tag": "ohu-Hung", - "windows": "ohu-Hung" + "tag": "ohu", + "tags": [ "ohu-HU", "ohu-Latn" ], + "windows": "ohu-Latn" }, { - "full": "ohu-Latn-HU", + "full": "ohu-Hung-HU", "iana": [ "Old Hungarian" ], "iso639_3": "ohu", "name": "Old Hungarian", + "nophonvars": true, "region": "HU", "regionname": "Hungary", - "script": "Latn", + "script": "Hung", "sldr": false, - "tag": "ohu-Latn", - "windows": "ohu-Latn" + "tag": "ohu-Hung", + "windows": "ohu-Hung" }, { "full": "oia-Latn-ID", @@ -86017,6 +89724,20 @@ "unwritten": true, "windows": "oia-Latn" }, + { + "full": "oie-Latn-SS", + "iana": [ "Okolie" ], + "iso639_3": "oie", + "name": "Okolie", + "region": "SS", + "regionname": "South Sudan", + "script": "Latn", + "sldr": false, + "tag": "oie", + "tags": [ "oie-Latn", "oie-SS" ], + "unwritten": true, + "windows": "oie-Latn" + }, { "full": "oin-Latn-PG", "iana": [ "Inebu One" ], @@ -86033,40 +89754,40 @@ "windows": "oin-Latn" }, { - "full": "oj-Latn-CA", + "full": "oj-Cans-CA", "iana": [ "Ojibwa" ], "iso639_3": "oji", - "localnames": [ "Jibwemwin", "Nishnaabemwin" ], - "name": "Ojibwa, Eastern", - "names": [ "Jibwemwin", "Nishnaabemwin", "Ojibwa", "Ojibway", "Ojibwe" ], + "name": "Ojibwa", + "names": [ "Jibwemwin", "Nishnaabemwin", "Ojibwa, Eastern", "Ojibway", "Ojibwe" ], "region": "CA", "regionname": "Canada", - "script": "Latn", + "script": "Cans", "sldr": false, "tag": "oj", - "tags": [ "oj-CA", "oj-Latn", "ojg", "ojg-CA", "ojg-Latn", "ojg-Latn-CA" ], - "windows": "oj-Latn" + "tags": [ "oj-CA", "oj-Cans", "ojg-Cans", "ojg-Cans-CA" ], + "windows": "oj-Cans" }, { - "full": "oj-Cans-CA", + "full": "oj-Latn-CA", "iana": [ "Ojibwa" ], "iso639_3": "oji", - "name": "Ojibwa, Eastern", - "names": [ "Jibwemwin", "Nishnaabemwin", "Ojibwa", "Ojibway", "Ojibwe" ], - "nophonvars": true, + "localnames": [ "Jibwemwin", "Nishnaabemwin" ], + "name": "Ojibwa", + "names": [ "Jibwemwin", "Nishnaabemwin", "Ojibwa, Eastern", "Ojibway", "Ojibwe" ], "region": "CA", "regionname": "Canada", - "script": "Cans", + "script": "Latn", "sldr": false, - "tag": "oj-Cans", - "tags": [ "ojg-Cans", "ojg-Cans-CA" ], - "windows": "oj-Cans" + "tag": "oj-Latn", + "tags": [ "ojg", "ojg-CA", "ojg-Latn", "ojg-Latn-CA" ], + "windows": "oj-Latn" }, { "full": "ojb-Latn-CA", "iana": [ "Northwestern Ojibwa" ], "iso639_3": "ojb", "localnames": [ "Nakawēmowin" ], + "macrolang": "oj", "name": "Ojibwa, Northwestern", "names": [ "Nakawēmowin", "Northern Ojibwa", "Northwestern Ojibwa", "Ojibway", "Ojibwe" ], "region": "CA", @@ -86081,6 +89802,7 @@ "full": "ojb-Cans-CA", "iana": [ "Northwestern Ojibwa" ], "iso639_3": "ojb", + "macrolang": "oj", "name": "Ojibwa, Northwestern", "names": [ "Nakawēmowin", "Northern Ojibwa", "Northwestern Ojibwa", "Ojibway", "Ojibwe" ], "nophonvars": true, @@ -86096,6 +89818,7 @@ "full": "ojc-Latn-CA", "iana": [ "Central Ojibwa" ], "iso639_3": "ojc", + "macrolang": "oj", "name": "Ojibwa, Central", "names": [ "Central Ojibwa", "Central Ojibwe", "Ojibway", "Ojibwe" ], "region": "CA", @@ -86126,6 +89849,7 @@ "iso639_3": "ojs", "latnnames": [ "Anishininiimowin" ], "localnames": [ "ᐊᓂᐦᔑᓂᓃᒧᐏᐣ" ], + "macrolang": "oj", "name": "Oji-Cree", "names": [ "Anishinaabemowin", "Cree", "Northern Ojibwa", "Ojibway", "Ojibwe", "Ojicree", "Severn Ojibwa" ], "region": "CA", @@ -86140,6 +89864,7 @@ "full": "ojs-Latn-CA", "iana": [ "Severn Ojibwa" ], "iso639_3": "ojs", + "macrolang": "oj", "name": "Oji-Cree", "names": [ "Anishinaabemowin", "Cree", "Northern Ojibwa", "Ojibway", "Ojibwe", "Ojicree", "Severn Ojibwa" ], "region": "CA", @@ -86169,6 +89894,7 @@ "iana": [ "Western Ojibwa" ], "iso639_3": "ojw", "localnames": [ "Anishnaubemowin", "Nakawēmowin" ], + "macrolang": "oj", "name": "Ojibwa, Western", "names": [ "Anishnaubemowin", "Nahkawēwin", "Nakawēmowin", "Ojibway", "Ojibwe", "Plains Ojibway", "Plains Ojibwe", "Saulteau", "Saulteaux", "Saulteaux Ojibwe", "Western Ojibwa", "Western Ojibwe" ], "region": "CA", @@ -86183,6 +89909,7 @@ "full": "ojw-Cans-CA", "iana": [ "Western Ojibwa" ], "iso639_3": "ojw", + "macrolang": "oj", "name": "Ojibwa, Western", "names": [ "Anishnaubemowin", "Nahkawēwin", "Nakawēmowin", "Ojibway", "Ojibwe", "Plains Ojibway", "Plains Ojibwe", "Saulteau", "Saulteaux", "Saulteaux Ojibwe", "Western Ojibwa", "Western Ojibwe" ], "nophonvars": true, @@ -86239,6 +89966,21 @@ "unwritten": true, "windows": "okb-Latn" }, + { + "full": "okc-Latn-CD", + "iana": [ "Kobo" ], + "iso639_3": "okc", + "name": "Kobo", + "names": [ "Ikobo" ], + "region": "CD", + "regionname": "The Democratic Republic of the Congo", + "script": "Latn", + "sldr": false, + "tag": "okc", + "tags": [ "okc-CD", "okc-Latn" ], + "unwritten": true, + "windows": "okc-Latn" + }, { "full": "okd-Latn-NG", "iana": [ "Okodia" ], @@ -86259,7 +90001,7 @@ "iana": [ "Okpe (Southwestern Edo)" ], "iso639_3": "oke", "name": "Okpe", - "names": [ "Okpe (Southwestern Edo)" ], + "names": [ "Okpe (Southwestern Edo)", "Ukpɛ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -86301,6 +90043,7 @@ "full": "oki-Latn-KE", "iana": [ "Okiek" ], "iso639_3": "oki", + "macrolang": "kln", "name": "Okiek", "names": [ "Akie", "Akiek", "Kinare", "Ogiek" ], "region": "KE", @@ -86355,17 +90098,17 @@ "windows": "okl-Zxxx" }, { - "full": "okm-Zyyy-KR", + "full": "okm-Hang-KR", "iana": [ "Middle Korean (10th-16th cent.)" ], "iso639_3": "okm", "name": "Middle Korean (10th-16th cent.)", "region": "KR", "regionname": "Republic of Korea", - "script": "Zyyy", + "script": "Hang", "sldr": false, "tag": "okm", - "tags": [ "okm-KR", "okm-Zyyy" ], - "windows": "okm-Zyyy" + "tags": [ "okm-Hang", "okm-KR" ], + "windows": "okm-Hang" }, { "full": "okn-Zyyy-JP", @@ -86381,17 +90124,17 @@ "windows": "okn-Zyyy" }, { - "full": "oko-Zyyy-KR", + "full": "oko-Hani-KR", "iana": [ "Old Korean (3rd-9th cent.)" ], "iso639_3": "oko", "name": "Old Korean (3rd-9th cent.)", "region": "KR", "regionname": "Republic of Korea", - "script": "Zyyy", + "script": "Hani", "sldr": false, "tag": "oko", - "tags": [ "oko-KR", "oko-Zyyy" ], - "windows": "oko-Zyyy" + "tags": [ "oko-Hani", "oko-KR" ], + "windows": "oko-Hani" }, { "full": "okr-Latn-NG", @@ -86468,7 +90211,21 @@ "windows": "okx-Latn" }, { - "full": "ola-Tibt-NP", + "full": "okz-Khmr-KH", + "iana": [ "Old Khmer" ], + "iso639_3": "okz", + "name": "Old Khmer", + "region": "KH", + "regionname": "Cambodia", + "regions": [ "LA", "TH", "VN" ], + "script": "Khmr", + "sldr": false, + "tag": "okz", + "tags": [ "okz-KH", "okz-Khmr" ], + "windows": "okz-Khmr" + }, + { + "full": "ola-Deva-NP", "iana": [ "Walungge" ], "iso639_3": "ola", "name": "Walungge", @@ -86476,11 +90233,27 @@ "region": "NP", "regionname": "Nepal", "regions": [ "CN", "IN" ], - "script": "Tibt", + "script": "Deva", "sldr": false, "tag": "ola", - "tags": [ "ola-NP", "ola-Tibt" ], - "windows": "ola-Tibt" + "tags": [ "ola-Deva", "ola-NP", "thw", "thw-Deva", "thw-Deva-NP", "thw-NP" ], + "windows": "ola-Deva-NP" + }, + { + "full": "ola-Tibt-CN", + "iana": [ "Walungge" ], + "iso639_3": "ola", + "name": "Walungge", + "names": [ "Halung", "Halungge", "Olangchung Gola", "Waling", "Walongchung Gola", "Walung", "Walungchung Gola", "Walunggi Keccya" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "regions": [ "IN", "NP" ], + "script": "Tibt", + "sldr": false, + "tag": "ola-Tibt", + "tags": [ "ola-CN", "thw-CN" ], + "windows": "ola-Tibt-CN" }, { "full": "old-Latn-TZ", @@ -86497,19 +90270,19 @@ "windows": "old-Latn" }, { - "full": "ole-Zyyy-BT", + "full": "ole-Tibt-BT", "iana": [ "Olekha" ], "iso639_3": "ole", "name": "Olekha", "names": [ "Black Mountain Language", "Monkha", "Monpa", "Ole Mönpa" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "ole", - "tags": [ "ole-BT", "ole-Zyyy" ], + "tags": [ "ole-BT", "ole-Tibt" ], "unwritten": true, - "windows": "ole-Zyyy" + "windows": "ole-Tibt" }, { "full": "olk-Latn-AU", @@ -86546,7 +90319,7 @@ "iso639_3": "olo", "localnames": [ "Livvin kieli" ], "name": "Livvi-Karelian", - "names": [ "Aunus", "Karel", "Karelian", "Livvi", "Livvikovian", "Livvikovskij Jazyk", "Livvin kieli", "Olonets", "Olonetsian", "Southern Olonetsian" ], + "names": [ "Aunus", "Aunuse Karelian", "Karel", "Karelian", "Livvi", "Livvikovian", "Livvikovskij Jazyk", "Livvin kieli", "Olonets", "Olonetsian", "Southern Olonetsian" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "FI" ], @@ -86605,8 +90378,8 @@ "iso639_3": "orm", "localname": "Oromoo", "localnames": [ "Afaan Oromoo" ], - "name": "Oromo, West Central", - "names": [ "Oromo" ], + "name": "Oromo", + "names": [ "Oromo, West Central" ], "region": "ET", "regionname": "Ethiopia", "script": "Latn", @@ -86614,7 +90387,7 @@ "suppress": true, "tag": "om", "tags": [ "gaz", "gaz-ET", "gaz-Latn", "gaz-Latn-ET", "om-ET", "om-Latn" ], - "windows": "om" + "windows": "om-ET" }, { "full": "om-Arab-ET", @@ -86627,15 +90400,15 @@ "script": "Arab", "sldr": false, "tag": "om-Arab", - "windows": "om-Arab" + "windows": "om-Arab-ET" }, { "full": "om-Ethi-ET", "iana": [ "Oromo" ], "iso639_3": "orm", "localname": "Oromoo", - "name": "Oromo, West Central", - "names": [ "Oromo" ], + "name": "Oromo", + "names": [ "Oromo, West Central" ], "nophonvars": true, "region": "ET", "regionname": "Ethiopia", @@ -86643,7 +90416,7 @@ "sldr": true, "tag": "om-Ethi", "tags": [ "gaz-Ethi", "gaz-Ethi-ET" ], - "windows": "om-Ethi" + "windows": "om-Ethi-ET" }, { "full": "om-Latn-KE", @@ -86655,9 +90428,8 @@ "regionname": "Kenya", "script": "Latn", "sldr": true, - "suppress": true, "tag": "om-KE", - "windows": "om-KE" + "windows": "om-Latn-KE" }, { "full": "oma-Latn-US", @@ -86694,6 +90466,7 @@ "iana": [ "Mochica" ], "iso639_3": "omc", "name": "Mochica", + "names": [ "Chimu", "Chimú", "Mochika", "Muchic", "Muchik", "Yunca", "Yunga" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -86723,8 +90496,9 @@ "full": "omi-Latn-CD", "iana": [ "Omi" ], "iso639_3": "omi", + "localnames": [ "Òmíti" ], "name": "Omi", - "names": [ "Kaliko-Omi" ], + "names": [ "Kaliko-Omi", "Òmíti" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -86738,6 +90512,7 @@ "iana": [ "Omok" ], "iso639_3": "omk", "name": "Omok", + "names": [ "Malyj Anjuj Omok" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -86779,7 +90554,7 @@ "iana": [ "Utarmbung" ], "iso639_3": "omo", "name": "Utarmbung", - "names": [ "Osum" ], + "names": [ "Namnd", "Osum", "Utarmbu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -86862,16 +90637,30 @@ "windows": "omw-Latn" }, { - "full": "omx-Zyyy-XX", + "full": "omx-Mymr-MM", "iana": [ "Old Mon" ], "iso639_3": "omx", "name": "Old Mon", - "region": "XX", - "script": "Zyyy", + "region": "MM", + "regionname": "Myanmar", + "script": "Mymr", "sldr": false, "tag": "omx", - "tags": [ "omx-XX", "omx-Zyyy" ], - "windows": "omx-Zyyy" + "tags": [ "omx-MM", "omx-Mymr" ], + "windows": "omx-Mymr" + }, + { + "full": "omy-Zyyy-MY", + "iana": [ "Old Malay" ], + "iso639_3": "omy", + "name": "Old Malay", + "region": "MY", + "regionname": "Malaysia", + "script": "Zyyy", + "sldr": false, + "tag": "omy", + "tags": [ "omy-MY", "omy-Zyyy" ], + "windows": "omy-Zyyy" }, { "full": "ona-Latn-AR", @@ -86952,7 +90741,7 @@ "iana": [ "Onjob" ], "iso639_3": "onj", "name": "Onjob", - "names": [ "Onjab" ], + "names": [ "Onjab", "Onjou" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -86981,6 +90770,8 @@ "full": "onn-Latn-PG", "iana": [ "Onobasulu" ], "iso639_3": "onn", + "localname": "Onobasulu", + "localnames": [ "Onobasulu" ], "name": "Onobasulu", "names": [ "Onabasulu" ], "region": "PG", @@ -87008,32 +90799,33 @@ "windows": "ono-Latn" }, { - "full": "onp-Deva-IN", + "full": "onp-Latn-IN", "iana": [ "Sartang" ], "iso639_3": "onp", + "localnames": [ "Sartang" ], "name": "Sartang", "names": [ "Bootpa", "But Monpa", "But Pa", "Matchopa" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Latn", "sldr": false, - "tag": "onp-Deva", - "windows": "onp-Deva" + "tag": "onp", + "tags": [ "onp-IN", "onp-Latn" ], + "windows": "onp-Latn" }, { - "full": "onp-Latn-IN", + "full": "onp-Deva-IN", "iana": [ "Sartang" ], "iso639_3": "onp", - "localnames": [ "Sartang" ], "name": "Sartang", "names": [ "Bootpa", "But Monpa", "But Pa", "Matchopa" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Deva", "sldr": false, - "tag": "onp-Latn", - "windows": "onp-Latn" + "tag": "onp-Deva", + "windows": "onp-Deva" }, { "full": "onr-Latn-PG", @@ -87106,7 +90898,7 @@ "sldr": false, "tag": "onw", "tags": [ "onw-Copt", "onw-Copt-SD", "onw-Copt-x-olnubian", "onw-SD", "onw-SD-x-olnubian", "onw-x-olnubian" ], - "windows": "onw-Copt" + "windows": "onw-Copt-x-olnubian" }, { "full": "onx-Latn-ID", @@ -87126,10 +90918,10 @@ "full": "ood-Latn-US", "iana": [ "Tohono O'odham" ], "iso639_3": "ood", - "localname": "Tohono O’odham", - "localnames": [ "Tohono O'otham" ], + "localname": "Oʼodham ha-ñeʼokĭ", + "localnames": [ "Oʼodham ha-ñeʼokĭ", "Oʼodham ñiok" ], "name": "Tohono O’odham", - "names": [ "Nebome", "Nevome", "Oʼodham ñiok", "O’odham", "O’othham", "Papago-Pima", "Pápago", "Tohono O'odham", "Tohono O’otham", "Upper Piman" ], + "names": [ "Nebome", "Nevome", "Oʼodham ha-ñeʼokĭ", "Oʼodham ñiok", "O’odham", "O’othham", "Papago-Pima", "Pápago", "Tohono O'odham", "Tohono O’otham", "Upper Piman" ], "region": "US", "regionname": "United States", "regions": [ "MX" ], @@ -87139,6 +90931,22 @@ "tags": [ "ood-Latn", "ood-US" ], "windows": "ood-Latn" }, + { + "full": "ood-Latn-US-x-pima", + "iana": [ "Tohono O'odham" ], + "iso639_3": "ood", + "localnames": [ "Oʼodham ha-ñeʼokĭ", "Oʼodham ñiok" ], + "name": "Tohono O’odham", + "names": [ "Nebome", "Nevome", "Oʼodham ha-ñeʼokĭ", "Oʼodham ñiok", "O’odham", "O’othham", "Papago-Pima", "Tohono O'odham", "Tohono O’otham", "Upper Piman" ], + "region": "US", + "regionname": "United States", + "rod": "237", + "script": "Latn", + "sldr": false, + "tag": "ood-x-pima", + "tags": [ "ood-Latn-x-pima", "ood-US-x-pima" ], + "windows": "ood-Latn-x-pima" + }, { "full": "oog-Zyyy-LA", "iana": [ "Ong" ], @@ -87155,19 +90963,19 @@ "windows": "oog-Zyyy" }, { - "full": "oon-Zyyy-IN", + "full": "oon-Deva-IN", "iana": [ "Önge" ], "iso639_3": "oon", "name": "Öñge", "names": [ "Ong", "Önge" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "oon", - "tags": [ "oon-IN", "oon-Zyyy" ], + "tags": [ "oon-Deva", "oon-IN" ], "unwritten": true, - "windows": "oon-Zyyy" + "windows": "oon-Deva" }, { "full": "oor-Latn-ZA", @@ -87257,7 +91065,7 @@ "iana": [ "Opata" ], "iso639_3": "opt", "name": "Opata", - "names": [ "Dohema", "Eudeve", "Heve", "Teguima" ], + "names": [ "Teguima" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -87289,8 +91097,8 @@ "latnnames": [ "Oḍiā" ], "localname": "ଓଡ଼ିଆ", "localnames": [ "ଓଡ଼ିଆ" ], - "name": "Odia", - "names": [ "Odia (macrolanguage)", "Odisha", "Odri", "Odrum", "Oliya", "Oriya", "Oriya (macrolanguage)", "Uriya", "Utkali", "Vadiya", "Yudhia" ], + "name": "Oriya (macrolanguage)", + "names": [ "Odia", "Odia (macrolanguage)", "Odisha", "Odri", "Odrum", "Oliya", "Oriya", "Uriya", "Utkali", "Vadiya", "Yudhia" ], "region": "IN", "regionname": "India", "regions": [ "BD", "NP", "US" ], @@ -87305,8 +91113,8 @@ "full": "or-Brai-IN", "iana": [ "Oriya (macrolanguage)", "Odia (macrolanguage)" ], "iso639_3": "ori", - "name": "Odia", - "names": [ "Odia (macrolanguage)", "Odisha", "Odri", "Odrum", "Oliya", "Oriya", "Oriya (macrolanguage)", "Uriya", "Utkali", "Vadiya", "Yudhia" ], + "name": "Oriya (macrolanguage)", + "names": [ "Odia", "Odia (macrolanguage)", "Odisha", "Odri", "Odrum", "Oliya", "Oriya", "Uriya", "Utkali", "Vadiya", "Yudhia" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -87337,6 +91145,7 @@ "iana": [ "Orma" ], "iso639_3": "orc", "localnames": [ "Orma" ], + "macrolang": "om", "name": "Orma", "region": "KE", "regionname": "Kenya", @@ -87394,6 +91203,7 @@ "full": "orn-Latn-MY", "iana": [ "Orang Kanaq" ], "iso639_3": "orn", + "macrolang": "ms", "name": "Orang Kanaq", "names": [ "Kanaq" ], "region": "MY", @@ -87424,6 +91234,7 @@ "iana": [ "Oruma" ], "iso639_3": "orr", "name": "Oruma", + "names": [ "Tugbeni", "Tugbeni Kạạmạ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -87437,6 +91248,7 @@ "full": "ors-Latn-MY", "iana": [ "Orang Seletar" ], "iso639_3": "ors", + "macrolang": "ms", "name": "Orang Seletar", "names": [ "Orang Laut" ], "region": "MY", @@ -87453,6 +91265,8 @@ "full": "ort-Telu-IN", "iana": [ "Adivasi Oriya" ], "iso639_3": "ort", + "latnnames": [ "Adivasi Oriya" ], + "localnames": [ "ఆదివాసి ఒరియ" ], "name": "Oriya, Adivasi", "names": [ "Adivasi", "Adivasi Oriya", "Adiwasi Oriya", "Desiya", "Kotia", "Kotia Oriya", "Kotiya", "Tribal Oriya" ], "region": "IN", @@ -87468,7 +91282,7 @@ "iana": [ "Ormuri" ], "iso639_3": "oru", "name": "Ormuri", - "names": [ "Baraki", "Baraks", "Bargista", "Oormuri", "Ormui", "Ormur", "Urmuri" ], + "names": [ "Baraki", "Baraks", "Bargista", "Berki", "Birki", "Brakee", "Braki", "Burki", "Oormuri", "Ormar", "Ormari", "Ormui", "Ormur", "Urmar", "Urmuri", "Warmaro" ], "region": "PK", "regionname": "Pakistan", "regions": [ "AF" ], @@ -87552,7 +91366,7 @@ "sldr": true, "tag": "os", "tags": [ "os-Cyrl", "os-GE" ], - "windows": "os-Cyrl" + "windows": "os-Cyrl-GE" }, { "full": "os-Geor-RU", @@ -87568,7 +91382,7 @@ "script": "Geor", "sldr": false, "tag": "os-Geor", - "windows": "os-Geor" + "windows": "os-Geor-RU" }, { "full": "os-Latn-RU", @@ -87583,7 +91397,7 @@ "script": "Latn", "sldr": false, "tag": "os-Latn", - "windows": "os-Latn" + "windows": "os-Latn-RU" }, { "full": "os-Cyrl-RU", @@ -87669,6 +91483,19 @@ "tags": [ "osi-ID", "osi-Java" ], "windows": "osi-Java" }, + { + "full": "osn-Sund-XX", + "iana": [ "Old Sundanese" ], + "iso639_3": "osn", + "name": "Old Sundanese", + "obsolete": true, + "region": "XX", + "script": "Sund", + "sldr": false, + "tag": "osn", + "tags": [ "osn-Sund", "osn-XX" ], + "windows": "osn-Sund" + }, { "full": "oso-Latn-NG", "iana": [ "Ososo" ], @@ -87783,9 +91610,9 @@ "iana": [ "Mezquital Otomi" ], "iso639_3": "ote", "localname": "Hñahñu", - "localnames": [ "Hñahñu" ], + "localnames": [ "Hñähñu" ], "name": "Otomi, Mezquital", - "names": [ "Hñahñu", "Mezquital Otomi", "Otomí del Valle del Mezquital" ], + "names": [ "Hñähñu", "Mezquital Otomi", "Otomi", "Otomí del Valle del Mezquital" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -87843,9 +91670,9 @@ "iana": [ "Eastern Highland Otomi" ], "iso639_3": "otm", "localname": "Yųhų", - "localnames": [ "Yųhų" ], + "localnames": [ "Nyuhu" ], "name": "Otomi, Eastern Highland", - "names": [ "Eastern Highland Otomi", "Eastern Otomi", "Otomí de Huehuetla", "Otomí de la Sierra", "Otomí de la Sierra Madre Oriental", "Otomí de la Sierra Oriental", "Otomí del Oriente", "Sierra Oriental Otomi", "Sierra Otomi", "Yųhų" ], + "names": [ "Eastern Highland Otomi", "Eastern Otomi", "Nyuhu", "Otomí de Huehuetla", "Otomí de la Sierra", "Otomí de la Sierra Madre Oriental", "Otomí de la Sierra Oriental", "Otomí del Oriente", "Sierra Oriental Otomi", "Sierra Otomi", "Yuhu", "Yųhų" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -87874,10 +91701,10 @@ "full": "otq-Latn-MX", "iana": [ "Querétaro Otomi" ], "iso639_3": "otq", - "localname": "Hñohño", - "localnames": [ "ñhöñhö" ], + "localname": "ñhöñhö", + "localnames": [ "Ñhöñhö" ], "name": "Otomi, Querétaro", - "names": [ "Northwestern Otomi", "Otomí de Querétaro", "Querétaro Otomi", "Santiago Mexquititlán Otomi", "Western Otomi", "hñohño", "ñhöñhö" ], + "names": [ "Hñohño", "Hñąñho", "Northwestern Otomi", "Otomi", "Otomí de Querétaro", "Querétaro Otomi", "Santiago Mexquititlán Otomi", "Western Otomi", "Ñañhų", "Ñhöñhö", "Ñǫthǫ" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -87952,6 +91779,7 @@ "iana": [ "Ottawa" ], "iso639_3": "otw", "localnames": [ "Daawaamwin", "Nishnaabemwin" ], + "macrolang": "oj", "name": "Ottawa", "names": [ "Chippewa", "Daawaamwin", "Eastern Ojibwa", "Nishnaabemwin", "Odawa", "Ojibway", "Ojibwe" ], "region": "CA", @@ -88052,16 +91880,18 @@ "windows": "oue-Latn" }, { - "full": "oui-Zyyy-XX", + "full": "oui-Ougr-143", "iana": [ "Old Uighur" ], "iso639_3": "oui", "name": "Old Uighur", - "region": "XX", - "script": "Zyyy", + "obsolete": true, + "region": "143", + "regionname": "Central Asia", + "script": "Ougr", "sldr": false, "tag": "oui", - "tags": [ "oui-XX", "oui-Zyyy" ], - "windows": "oui-Zyyy" + "tags": [ "oui-143", "oui-Ougr" ], + "windows": "oui-Ougr-143" }, { "full": "oui-Mong-XX", @@ -88073,7 +91903,8 @@ "script": "Mong", "sldr": false, "tag": "oui-Mong", - "windows": "oui-Mong" + "tags": [ "oui-XX" ], + "windows": "oui-Mong-XX" }, { "full": "oum-Latn-PG", @@ -88093,8 +91924,9 @@ "full": "ovd-Latn-SE", "iana": [ "Elfdalian", "Övdalian" ], "iso639_3": "ovd", + "localnames": [ "Övkallmå̢leð" ], "name": "Övdalian", - "names": [ "Dalska", "Elfdalian", "Elvdalian", "Älvdalska", "Älvdalsmålet", "Övdalska" ], + "names": [ "Dalska", "Elfdalian", "Elvdalian", "Älvdalska", "Älvdalsmålet", "Övdalsk", "Övdalska", "Övkallmå̢leð", "Övkallmǫleð" ], "region": "SE", "regionname": "Sweden", "script": "Latn", @@ -88148,7 +91980,6 @@ "full": "oyd-Latn-ET", "iana": [ "Oyda" ], "iso639_3": "oyd", - "localnames": [ "Oyda" ], "name": "Oyda", "region": "ET", "regionname": "Ethiopia", @@ -88165,7 +91996,7 @@ "localname": "Wajãpi", "localnames": [ "Wajãpi" ], "name": "Wayampi", - "names": [ "Guaiapi", "Guayapi", "Oiampí", "Oiumpian", "Oyampí", "Oyampík", "Oyanpík", "Oyapí", "Waiampi", "Waiãpi", "Wajapae", "Wajapi", "Wajapuku", "Wajãpi", "Wayapae", "Wayapi", "Wayãpi", "Wayãpy" ], + "names": [ "Guaiapi", "Guayapi", "Oiampí", "Oiumpian", "Oyampí", "Oyampík", "Oyanpík", "Oyapí", "Waiampi", "Waiãpi", "Wajapae", "Wajapuku", "Wajãpi", "Wayapae", "Wayapi", "Wayãpi", "Wayãpy", "Wãyãpĩ" ], "region": "BR", "regionname": "Brazil", "regions": [ "GF" ], @@ -88217,13 +92048,13 @@ "names": [ "Eastern Panjabi", "Gurmukhi", "Gurumukhi", "Panjabi", "Punjabi" ], "region": "IN", "regionname": "India", - "regions": [ "AE", "AU", "BD", "CA", "FJ", "GB", "KE", "MY", "SG", "US" ], + "regions": [ "AE", "AU", "BD", "CA", "FJ", "GB", "KE", "MU", "MY", "SG", "US" ], "script": "Guru", "sldr": true, "suppress": true, "tag": "pa", "tags": [ "pa-Guru", "pa-IN" ], - "windows": "pa" + "windows": "pa-IN" }, { "full": "pa-Khoj-IN", @@ -88238,7 +92069,7 @@ "script": "Khoj", "sldr": false, "tag": "pa-Khoj", - "windows": "pa-Khoj" + "windows": "pa-Khoj-IN" }, { "full": "pa-Mahj-IN", @@ -88252,7 +92083,7 @@ "script": "Mahj", "sldr": false, "tag": "pa-Mahj", - "windows": "pa-Mahj" + "windows": "pa-Mahj-IN" }, { "full": "pa-Arab-PK", @@ -88290,13 +92121,14 @@ "full": "pac-Latn-VN", "iana": [ "Pacoh" ], "iso639_3": "pac", + "localnames": [ "Cang Pacóuh" ], "name": "Pacoh", - "names": [ "Bo River Van Kieu", "Ganai", "Pa Co", "Paco", "Pako", "Poko", "Pokoh" ], + "names": [ "Bo River Van Kieu", "Cang Pacóuh", "Ganai", "Pa Co", "Paco", "Pako", "Poko", "Pokoh" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "LA" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "pac", "tags": [ "pac-Latn", "pac-VN" ], "windows": "pac-Latn" @@ -88306,7 +92138,7 @@ "iana": [ "Pacoh (Cado dialect)" ], "iso639_3": "pac", "name": "Pacoh", - "names": [ "Bo River Van Kieu", "Ganai", "Pa Co", "Paco", "Pacoh (Cado dialect)", "Pako", "Poko", "Pokoh" ], + "names": [ "Bo River Van Kieu", "Cang Pacóuh", "Ganai", "Pa Co", "Paco", "Pacoh (Cado dialect)", "Pako", "Poko", "Pokoh" ], "nophonvars": true, "region": "LA", "regionname": "Lao People's Democratic Republic", @@ -88321,8 +92153,9 @@ "full": "pac-Latn-VN-x-diacado", "iana": [ "Pacoh (Cado dialect)" ], "iso639_3": "pac", + "localnames": [ "Cang Pacóuh" ], "name": "Pacoh", - "names": [ "Bo River Van Kieu", "Ganai", "Pa Co", "Paco", "Pacoh (Cado dialect)", "Pako", "Poko", "Pokoh" ], + "names": [ "Bo River Van Kieu", "Cang Pacóuh", "Ganai", "Pa Co", "Paco", "Pacoh (Cado dialect)", "Pako", "Poko", "Pokoh" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "LA" ], @@ -88336,6 +92169,7 @@ "full": "pad-Latn-BR", "iana": [ "Paumarí" ], "iso639_3": "pad", + "localname": "Pamoari", "localnames": [ "Pamoari" ], "name": "Paumarí", "names": [ "Palmari", "Pamoari", "Purupurú" ], @@ -88351,8 +92185,9 @@ "full": "pae-Latn-CD", "iana": [ "Pagibete" ], "iso639_3": "pae", + "localnames": [ "Pagabete" ], "name": "Pagibete", - "names": [ "Apagibete", "Apagibeti", "Apakabeti", "Apakibeti", "Pagabete" ], + "names": [ "Apagibete", "Apagibeti", "Apakabeti", "Apakibeti", "Pagabete", "Pakabete" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -88441,7 +92276,7 @@ "iana": [ "Parakanã" ], "iso639_3": "pak", "name": "Parakanã", - "names": [ "Apiterewa", "Awaeté", "Parakanân", "Parocana" ], + "names": [ "Apiterewa", "Awaeté", "Paracana", "Parakanân", "Parocana" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -88461,7 +92296,7 @@ "sldr": false, "tag": "pal", "tags": [ "pal-IR", "pal-Phli" ], - "windows": "pal-Phli" + "windows": "pal-Phli-IR" }, { "full": "pal-Phlp-CN", @@ -88475,7 +92310,7 @@ "sldr": false, "tag": "pal-Phlp", "tags": [ "pal-CN" ], - "windows": "pal-Phlp" + "windows": "pal-Phlp-CN" }, { "full": "pam-Latn-PH", @@ -88512,6 +92347,7 @@ "full": "pao-Latn-US", "iana": [ "Northern Paiute" ], "iso639_3": "pao", + "localname": "Numu", "localnames": [ "Numu" ], "name": "Paiute, Northern", "names": [ "Northern Paiute", "Numu", "Paviotso" ], @@ -88529,7 +92365,7 @@ "iso639_3": "pap", "localnames": [ "Papiamentu" ], "name": "Papiamento", - "names": [ "Curassese", "Curaçoleño", "Papiam", "Papiamen", "Papiamentoe", "Papiamentu", "Papiments" ], + "names": [ "Curassese", "Curaçoleño", "Papiam", "Papiamen", "Papiamentoe", "Papiamentu" ], "region": "AW", "regionname": "Aruba", "regions": [ "BQ", "CW", "NL", "PR", "SX", "US", "VI" ], @@ -88540,7 +92376,7 @@ "windows": "pap-Latn" }, { - "full": "paq-Zyyy-TJ", + "full": "paq-Cyrl-TJ", "iana": [ "Parya" ], "iso639_3": "paq", "name": "Parya", @@ -88548,12 +92384,12 @@ "region": "TJ", "regionname": "Tajikistan", "regions": [ "AF", "UZ" ], - "script": "Zyyy", + "script": "Cyrl", "sldr": false, "tag": "paq", - "tags": [ "paq-TJ", "paq-Zyyy" ], + "tags": [ "paq-Cyrl", "paq-TJ" ], "unwritten": true, - "windows": "paq-Zyyy" + "windows": "paq-Cyrl" }, { "full": "par-Latn-US", @@ -88584,20 +92420,6 @@ "unwritten": true, "windows": "pas-Latn" }, - { - "full": "pat-Latn-PG", - "iana": [ "Papitalai" ], - "iso639_3": "pat", - "name": "Papitalai", - "region": "PG", - "regionname": "Papua New Guinea", - "script": "Latn", - "sldr": false, - "tag": "pat", - "tags": [ "pat-Latn", "pat-PG" ], - "unwritten": true, - "windows": "pat-Latn" - }, { "full": "pau-Latn-PW", "iana": [ "Palauan" ], @@ -88619,7 +92441,7 @@ "iana": [ "Pakaásnovos" ], "iso639_3": "pav", "name": "Pakaásnovos", - "names": [ "Jaru", "Oro Wari", "Orowari", "Pacaas-Novos", "Pacahanovo", "Pacaás Novos", "Pakaa Nova", "Pakaanova", "Pakaanovas", "Uomo", "Wari’" ], + "names": [ "Jaru", "Oro Wari", "Orowari", "Pacahanovo", "Pacaás Novos", "Pakaa Nova", "Pakaanova", "Pakaanovas", "Uomo", "Wari’" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -88693,8 +92515,8 @@ "iso639_3": "pbb", "localname": "Nasa Yuwe", "localnames": [ "Nasa Yuwe" ], - "name": "Páez", - "names": [ "Nasa", "Nasa Yuwe", "Nasayuwe", "Paes" ], + "name": "Nasa", + "names": [ "Nasa Yuwe", "Nasayuwe" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -88723,7 +92545,7 @@ "iana": [ "Mezontla Popoloca" ], "iso639_3": "pbe", "name": "Popoloca, Mezontla", - "names": [ "Los Reyes Metzontla Popoloca", "Mezontla Popoloca", "Ngiba", "Ngigua", "Ngiwa", "Popoloca de Mezontla", "Southern Popoloca" ], + "names": [ "Los Reyes Metzontla Popoloca", "Mezontla Popoloca", "Ngiba", "Ngigua", "Ngiwa", "Popoloca de Mezontla", "Popoloca de Oriente", "Southern Popoloca" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -88738,7 +92560,7 @@ "iana": [ "Coyotepec Popoloca" ], "iso639_3": "pbf", "name": "Popoloca, Coyotepec", - "names": [ "Coyotepec Popoloca", "Ngiba", "Ngigua", "Ngiwa", "San Vicente Coyotepec Popoloca" ], + "names": [ "Coyotepec Popoloca", "Ngiba", "Ngigua", "Ngiwa", "Popoloca de Oriente", "San Vicente Coyotepec Popoloca" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -88782,10 +92604,10 @@ "full": "pbi-Latn-CM", "iana": [ "Parkwa" ], "iso639_3": "pbi", - "localname": "Gwaɗi Parǝkwa", - "localnames": [ "Gwaɗi Parǝkwa" ], + "localname": "Gwaɗi Parəkwa", + "localnames": [ "Gwaɗi Parəkwa" ], "name": "Parkwa", - "names": [ "Gwadi Parekwa", "Gwaɗi Parǝkwa", "Kudala", "Padogo", "Padoko", "Padokwa", "Paduko", "Parekwa", "Pawdawkwa", "Podogo", "Podoko", "Podokwa", "Podokwo" ], + "names": [ "Gwadi Parekwa", "Gwaɗi Parəkwa", "Kudala", "Padogo", "Padoko", "Padokwa", "Paduko", "Parekwa", "Pawdawkwa", "Podogo", "Podoko", "Podokwa", "Podokwo" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -88814,7 +92636,7 @@ "iana": [ "Puebla Mazatec" ], "iso639_3": "pbm", "name": "Mazatec, Puebla and Northeastern", - "names": [ "Mazateco de Puebla y del Noroeste", "Puebla Mazatec", "’an Xo’boó" ], + "names": [ "An xo’boo", "Mazateco de Puebla", "Mazateco de Puebla y del Noroeste", "Puebla Mazatec", "’an Xo’boó" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -88843,8 +92665,9 @@ "full": "pbo-Latn-GW", "iana": [ "Papel" ], "iso639_3": "pbo", + "localnames": [ "Oyum" ], "name": "Papel", - "names": [ "Moium", "Oium", "Papei", "Pepel" ], + "names": [ "Moium", "Oium", "Oyum", "Papei", "Pepel" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "SN" ], @@ -88873,15 +92696,15 @@ "full": "pbr-Latn-TZ", "iana": [ "Pangwa" ], "iso639_3": "pbr", + "localnames": [ "Khipangwa" ], "name": "Pangwa", - "names": [ "Ekipangwa", "Kipangwa" ], + "names": [ "Ekipangwa", "Khipangwa", "Kipangwa" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", "sldr": false, "tag": "pbr", "tags": [ "pbr-Latn", "pbr-TZ" ], - "unwritten": true, "windows": "pbr-Latn" }, { @@ -88905,8 +92728,9 @@ "iso639_3": "pbt", "latnnames": [ "Pax̌tō" ], "localnames": [ "پښتو‎" ], + "macrolang": "ps", "name": "Pashto, Southern", - "names": [ "Kandahar Pashto", "Paktu", "Pashtu", "Pushto", "Pushtu", "Qandahar Pashto", "Quetta-Kandahar Pashto", "Southern Pashto", "Southwestern Pashto" ], + "names": [ "Kandahari Pashto", "Paktu", "Pashtu", "Pushto", "Pushtu", "Qandahari Pashto", "Quetta-Kandahari Pashto", "Southern Pashto", "Southwestern Pashto" ], "region": "AF", "regionname": "Afghanistan", "regions": [ "AE", "IR", "PK", "TJ", "TM" ], @@ -88961,19 +92785,34 @@ "windows": "pca-Latn" }, { - "full": "pcb-Zyyy-KH", + "full": "pcb-Khmr-KH", "iana": [ "Pear" ], "iso639_3": "pcb", "name": "Pear", "names": [ "Por" ], "region": "KH", "regionname": "Cambodia", - "script": "Zyyy", + "script": "Khmr", "sldr": false, "tag": "pcb", - "tags": [ "pcb-KH", "pcb-Zyyy" ], + "tags": [ "pcb-KH", "pcb-Khmr" ], "unwritten": true, - "windows": "pcb-Zyyy" + "windows": "pcb-Khmr" + }, + { + "full": "pcc-Latn-CN", + "iana": [ "Bouyei" ], + "iso639_3": "pcc", + "name": "Bouyei", + "names": [ "Bo-I", "Bo-Y", "Bui", "Buyei", "Buyi", "Buyui", "Chang Chá", "Chung Cha", "Chung-Chia", "Cùi Chu", "Dang", "Dioi", "Giai", "Giang", "Giay", "Giáy", "Nhaang", "Nhang", "Niang", "Nyang", "Pau Thin", "Pu-I", "Pu-Jui", "Pu-Nam", "Pui", "Pujai", "Puyi", "Puyoi", "Pú Nà", "Sa", "Shuihu", "Trong Ggia", "Tu-Dìn", "Tujia", "Xa Chung Chá", "Yai", "Yang", "Yay", "Zhongjia" ], + "region": "CN", + "regionname": "China", + "regions": [ "LA", "VN" ], + "script": "Latn", + "sldr": false, + "tag": "pcc", + "tags": [ "pcc-CN", "pcc-Latn" ], + "windows": "pcc-Latn" }, { "full": "pcc-Hani-CN", @@ -88991,18 +92830,49 @@ "windows": "pcc-Hani" }, { - "full": "pcc-Latn-CN", + "full": "pcc-Latn-CN-x-central", "iana": [ "Bouyei" ], "iso639_3": "pcc", "name": "Bouyei", - "names": [ "Bo-I", "Bo-Y", "Bui", "Buyei", "Buyi", "Buyui", "Chang Chá", "Chung Cha", "Chung-Chia", "Cùi Chu", "Dang", "Dioi", "Giai", "Giang", "Giay", "Giáy", "Nhaang", "Nhang", "Niang", "Nyang", "Pau Thin", "Pu-I", "Pu-Jui", "Pu-Nam", "Pui", "Pujai", "Puyi", "Puyoi", "Pú Nà", "Sa", "Shuihu", "Trong Ggia", "Tu-Dìn", "Tujia", "Xa Chung Chá", "Yai", "Yang", "Yay", "Zhongjia" ], + "names": [ "Bo-I", "Bui", "Buyei", "Buyi", "Buyui", "Chung-Chia", "Dioi", "Giay", "Pu-I", "Pu-Jui", "Pui", "Pujai", "Puyi", "Puyoi", "Shuihu", "Tujia", "Yay", "Zhongjia" ], "region": "CN", "regionname": "China", - "regions": [ "LA", "VN" ], + "rod": "6089", "script": "Latn", "sldr": false, - "tag": "pcc-Latn", - "windows": "pcc-Latn" + "tag": "pcc-x-central", + "tags": [ "pcc-CN-x-central", "pcc-Latn-x-central" ], + "windows": "pcc-Latn-x-central" + }, + { + "full": "pcc-Latn-CN-x-southern", + "iana": [ "Bouyei" ], + "iso639_3": "pcc", + "name": "Bouyei", + "names": [ "Bo-I", "Bui", "Buyei", "Buyi", "Buyui", "Chung-Chia", "Dioi", "Giay", "Pu-I", "Pu-Jui", "Pui", "Pujai", "Puyi", "Puyoi", "Shuihu", "Tujia", "Yay", "Zhongjia" ], + "region": "CN", + "regionname": "China", + "rod": "6163", + "script": "Latn", + "sldr": false, + "tag": "pcc-x-southern", + "tags": [ "pcc-CN-x-southern", "pcc-Latn-x-southern" ], + "windows": "pcc-Latn-x-southern" + }, + { + "full": "pcc-Latn-CN-x-western", + "iana": [ "Bouyei" ], + "iso639_3": "pcc", + "name": "Bouyei", + "names": [ "Bo-I", "Bui", "Buyei", "Buyi", "Buyui", "Chung-Chia", "Dioi", "Giay", "Pu-I", "Pu-Jui", "Pui", "Pujai", "Puyi", "Puyoi", "Shuihu", "Tujia", "Yay", "Zhongjia" ], + "region": "CN", + "regionname": "China", + "rod": "6090", + "script": "Latn", + "sldr": false, + "tag": "pcc-x-western", + "tags": [ "pcc-CN-x-western", "pcc-Latn-x-western" ], + "windows": "pcc-Latn-x-western" }, { "full": "pcd-Latn-FR", @@ -89024,66 +92894,84 @@ "full": "pce-Mymr-MM", "iana": [ "Ruching Palaung" ], "iso639_3": "pce", + "latnnames": [ "Daraarin" ], + "localnames": [ "ဒါအာင်" ], "name": "Palaung, Ruching", - "names": [ "Bulai", "Bulei", "Da’ang", "Di-Ang", "Dlang", "Ngwe Palaung", "Palaung, Pale", "Palay", "Pale", "Pale Palaung", "Pulei", "Ruching Palaung", "Silver Palaung", "Southern Palaung", "Southern Ta’ang", "Ta-Ang" ], + "names": [ "Da’ang", "De’ang", "Di-Ang", "Dlang", "Ngwe Palaung", "Palay", "Pale", "Pale Palaung", "Ru-Jing Palaung", "Ruching Palaung", "Silver Palaung", "Southern Ta’ang" ], "region": "MM", "regionname": "Myanmar", - "regions": [ "CN", "TH" ], + "regions": [ "CN" ], "script": "Mymr", "sldr": false, "tag": "pce", "tags": [ "pce-MM", "pce-Mymr" ], - "windows": "pce-Mymr" + "windows": "pce-Mymr-MM" }, { - "full": "pcf-Zyyy-IN", + "full": "pce-Thai-TH", + "iana": [ "Ruching Palaung" ], + "iso639_3": "pce", + "name": "Palaung, Pale", + "names": [ "Di-Ang", "Ngwe Palaung", "Palay", "Pale", "Ruching Palaung", "Silver Palaung", "Southern Palaung", "Ta-Ang" ], + "nophonvars": true, + "region": "TH", + "regionname": "Thailand", + "script": "Thai", + "sldr": false, + "tag": "pce-Thai", + "tags": [ "pce-TH" ], + "windows": "pce-Thai-TH" + }, + { + "full": "pcf-Mlym-IN", "iana": [ "Paliyan" ], "iso639_3": "pcf", "name": "Paliyan", "names": [ "Makkal", "Malai Paliyar", "Palani", "Palaya", "Palayan", "Paliyar", "Palleyan", "Palliyar", "Poliyar", "Seramar", "Tamil" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Mlym", "sldr": false, "tag": "pcf", - "tags": [ "pcf-IN", "pcf-Zyyy" ], - "windows": "pcf-Zyyy" + "tags": [ "pcf-IN", "pcf-Mlym" ], + "windows": "pcf-Mlym" }, { - "full": "pcg-Knda-IN", + "full": "pcg-Mlym-IN", "iana": [ "Paniya" ], "iso639_3": "pcg", + "latnnames": [ "Paniya" ], + "localnames": [ "പാണിയ" ], "name": "Paniya", "names": [ "Nil", "Pania", "Paniyan", "Panyah" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Knda", + "script": "Mlym", "sldr": false, - "tag": "pcg-Knda", - "windows": "pcg-Knda" + "tag": "pcg", + "tags": [ "pcg-IN", "pcg-Mlym" ], + "windows": "pcg-Mlym" }, { - "full": "pcg-Mlym-IN", + "full": "pcg-Knda-IN", "iana": [ "Paniya" ], "iso639_3": "pcg", - "latnnames": [ "Paniya" ], - "localnames": [ "പാണിയ" ], "name": "Paniya", "names": [ "Nil", "Pania", "Paniyan", "Panyah" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Mlym", + "script": "Knda", "sldr": false, - "tag": "pcg-Mlym", - "windows": "pcg-Mlym" + "tag": "pcg-Knda", + "windows": "pcg-Knda" }, { "full": "pcg-Taml-IN", "iana": [ "Paniya" ], "iso639_3": "pcg", "latnnames": [ "Paniya" ], + "localname": "பணிய", "localnames": [ "பணிய" ], "name": "Paniya", "names": [ "Nil", "Pania", "Paniyan", "Panyah" ], @@ -89096,32 +92984,34 @@ "windows": "pcg-Taml" }, { - "full": "pch-Zyyy-IN", + "full": "pch-Deva-IN", "iana": [ "Pardhan" ], "iso639_3": "pch", "name": "Pardhan", "names": [ "Pradhan", "Pradhani" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "pch", - "tags": [ "pch-IN", "pch-Zyyy" ], + "tags": [ "pch-Deva", "pch-IN" ], "unwritten": true, - "windows": "pch-Zyyy" + "windows": "pch-Deva" }, { "full": "pci-Deva-IN", "iana": [ "Duruwa" ], "iso639_3": "pci", + "latnnames": [ "Durvaa" ], + "localnames": [ "दुरवा" ], "name": "Duruwa", "names": [ "Dhruva", "Dhurwa", "Durva", "Paraja", "Parajhi", "Parjhi", "Parji", "Tagara", "Thakara", "Tugara" ], - "nophonvars": true, "region": "IN", "regionname": "India", "script": "Deva", "sldr": false, - "tag": "pci-Deva", + "tag": "pci", + "tags": [ "pci-Deva", "pci-IN" ], "windows": "pci-Deva" }, { @@ -89158,7 +93048,7 @@ "iso639_3": "pck", "localnames": [ "Paite Chin" ], "name": "Chin, Paite", - "names": [ "Haithe", "Paite", "Paite Chin", "Paithe", "Parte", "Vuite", "Zomi", "Zoukam" ], + "names": [ "Haithe", "Paite", "Paite Chin", "Paithe", "Parte", "Vuite", "Zome", "Zomi", "Zoukam" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -89188,7 +93078,7 @@ "localname": "Naijá", "localnames": [ "Naijá" ], "name": "Pidgin, Nigerian", - "names": [ "Anglo-Nigerian Pidgin", "Broken English", "Brokin", "Brokun", "Naijá", "Nigerian Creole English", "Nigerian Pidgin", "Nigerian Pidgin English", "Pidgin" ], + "names": [ "Anglo-Nigerian Pidgin", "Broken English", "Brokin", "Brokun", "Naijá", "Nigerian Creole English", "Nigerian Pidgin", "Nigerian Pidgin English", "Pidgin", "Pijin" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -89339,9 +93229,9 @@ "full": "pdu-Latn-MM", "iana": [ "Kayan" ], "iso639_3": "pdu", - "localnames": [ "Kayan" ], + "localnames": [ "Kayăn", "Kayǎn tamangaò̌" ], "name": "Kayan", - "names": [ "Ka-Yun", "Kayang", "Lahwi" ], + "names": [ "Ka-Yun", "Kayang", "Kayăn", "Kayǎn tamangaò̌", "Lahwi" ], "region": "MM", "regionname": "Myanmar", "regions": [ "TH" ], @@ -89356,7 +93246,7 @@ "iana": [ "Kayan" ], "iso639_3": "pdu", "name": "Kayan", - "names": [ "Ka-Yun", "Kayang", "Lahwi" ], + "names": [ "Ka-Yun", "Kayang", "Kayăn", "Kayǎn tamangaò̌", "Lahwi" ], "nophonvars": true, "region": "MM", "regionname": "Myanmar", @@ -89382,19 +93272,19 @@ "windows": "pea-Latn" }, { - "full": "peb-Zyyy-US", + "full": "peb-Latn-US", "iana": [ "Eastern Pomo" ], "iso639_3": "peb", "name": "Pomo, Eastern", "names": [ "Clear Lake Pomo", "Eastern Pomo" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "peb", - "tags": [ "peb-US", "peb-Zyyy" ], + "tags": [ "peb-Latn", "peb-US" ], "unwritten": true, - "windows": "peb-Zyyy" + "windows": "peb-Latn" }, { "full": "ped-Latn-PG", @@ -89461,7 +93351,7 @@ "iana": [ "Bonan" ], "iso639_3": "peh", "name": "Bonan", - "names": [ "Baonan", "Bao’an", "Boan", "Manikacha", "Paoan", "Paongan" ], + "names": [ "Baonan", "Bao’an", "Boan", "Manikacha", "Paoan", "Paonang" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -89519,6 +93409,7 @@ "full": "pel-Latn-ID", "iana": [ "Pekal" ], "iso639_3": "pel", + "macrolang": "ms", "name": "Pekal", "region": "ID", "regionname": "Indonesia", @@ -89560,8 +93451,9 @@ "full": "pep-Latn-PG", "iana": [ "Kunja" ], "iso639_3": "pep", - "name": "Kunja", - "names": [ "Kánchá", "Kénzä", "Lower Morehead", "Peremka", "Thundai-Kanza" ], + "localnames": [ "Kanja" ], + "name": "Kanja", + "names": [ "Kunja", "Kánchá", "Kénzä", "Lower Morehead", "Peremka", "Thundai-Kanza" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -89659,7 +93551,7 @@ }, { "full": "pfe-Latn-CM", - "iana": [ "Peere" ], + "iana": [ "Pere" ], "iso639_3": "pfe", "localnames": [ "Pɛrɛ" ], "name": "Pere", @@ -89693,9 +93585,10 @@ "full": "pga-Latn-SS", "iana": [ "Sudanese Creole Arabic" ], "iso639_3": "pga", - "localnames": [ "Arabi Juba" ], - "name": "Arabic, Sudanese Creole", - "names": [ "Arabi Juba", "Juba Arabic", "Pidgin Arabic", "Southern Sudan Arabic", "Sudanese Creole Arabic" ], + "localnames": [ "Arabi", "Arabi Juba" ], + "macrolang": "ar", + "name": "Juba Arabic", + "names": [ "Arabi", "Arabi Juba", "Pidgin Arabic", "Southern Sudan Arabic", "Sudanese Arabic Creole", "Sudanese Creole Arabic" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -89849,18 +93742,18 @@ "windows": "pha-Zyyy" }, { - "full": "phd-Zyyy-IN", + "full": "phd-Deva-IN", "iana": [ "Phudagi" ], "iso639_3": "phd", "name": "Phudagi", "names": [ "Vadval" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "phd", - "tags": [ "phd-IN", "phd-Zyyy" ], - "windows": "phd-Zyyy" + "tags": [ "phd-Deva", "phd-IN" ], + "windows": "phd-Deva" }, { "full": "phg-Latn-VN", @@ -89893,6 +93786,21 @@ "unwritten": true, "windows": "phh-Latn" }, + { + "full": "phj-Zyyy-NP", + "iana": [ "Pahari" ], + "iso639_3": "phj", + "name": "Pahari", + "names": [ "Pahara", "Pahi", "Pahibhāe", "Pihi", "Pihi bhya" ], + "region": "NP", + "regionname": "Nepal", + "script": "Zyyy", + "sldr": false, + "tag": "phj", + "tags": [ "phj-NP", "phj-Zyyy" ], + "unwritten": true, + "windows": "phj-Zyyy" + }, { "full": "phk-Mymr-IN", "iana": [ "Phake" ], @@ -89982,6 +93890,7 @@ "full": "phr-Arab-PK", "iana": [ "Pahari-Potwari" ], "iso639_3": "phr", + "macrolang": "lah", "name": "Pahari-Potwari", "names": [ "Mirpur", "Mirpur Panjabi", "Mirpur Punjabi", "Mirpuri", "Pahari", "Pahari-Pothowari", "Pothohari", "Potohari", "Potwari" ], "region": "PK", @@ -89994,23 +93903,24 @@ "windows": "phr-Arab" }, { - "full": "pht-Zyyy-TH", + "full": "pht-Thai-TH", "iana": [ "Phu Thai" ], "iso639_3": "pht", + "latnnames": [ "Phu Thai or Phuu Thai", "Phu Thai or Phuu Thai" ], + "localnames": [ "ผู้ไท", "ภูไท" ], "name": "Phu Thai", "names": [ "Phutai", "Phuu Thai", "Poutai", "Putai", "Puthai", "Puthay" ], "region": "TH", "regionname": "Thailand", "regions": [ "LA", "US", "VN" ], - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "pht", - "tags": [ "pht-TH", "pht-Zyyy" ], - "unwritten": true, - "windows": "pht-Zyyy" + "tags": [ "pht-TH", "pht-Thai" ], + "windows": "pht-Thai" }, { - "full": "phu-Zyyy-TH", + "full": "phu-Thai-TH", "iana": [ "Phuan" ], "iso639_3": "phu", "name": "Phuan", @@ -90018,12 +93928,42 @@ "region": "TH", "regionname": "Thailand", "regions": [ "KH", "LA" ], - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "phu", - "tags": [ "phu-TH", "phu-Zyyy" ], + "tags": [ "phu-TH", "phu-Thai" ], "unwritten": true, - "windows": "phu-Zyyy" + "windows": "phu-Thai-TH" + }, + { + "full": "phu-Lana-TH", + "iana": [ "Phuan" ], + "iso639_3": "phu", + "name": "Phuan", + "names": [ "Lao Phuan", "Lao Phuon", "Phoan", "Phu Un", "Phu-uen", "Phuon", "Poan", "Puan" ], + "nophonvars": true, + "region": "TH", + "regionname": "Thailand", + "regions": [ "KH", "LA" ], + "script": "Lana", + "sldr": false, + "tag": "phu-Lana", + "windows": "phu-Lana-TH" + }, + { + "full": "phu-Laoo-LA", + "iana": [ "Phuan" ], + "iso639_3": "phu", + "name": "Phuan", + "names": [ "Lao Phuan", "Phu Un" ], + "nophonvars": true, + "region": "LA", + "regionname": "Lao People's Democratic Republic", + "script": "Laoo", + "sldr": false, + "tag": "phu-Laoo", + "tags": [ "phu-LA" ], + "windows": "phu-Laoo-LA" }, { "full": "phv-Arab-AF", @@ -90040,19 +93980,49 @@ "windows": "phv-Arab" }, { - "full": "phw-Zyyy-NP", + "full": "phw-Deva-NP", "iana": [ "Phangduwali" ], "iso639_3": "phw", "name": "Phangduwali", "names": [ "Phangduvali", "Phangduwali Poti" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "phw", - "tags": [ "phw-NP", "phw-Zyyy" ], + "tags": [ "phw-Deva", "phw-NP" ], "unwritten": true, - "windows": "phw-Zyyy" + "windows": "phw-Deva" + }, + { + "full": "pi-Sinh-IN", + "iana": [ "Pali" ], + "iso639_3": "pli", + "name": "Pali", + "names": [ "Magadhan" ], + "region": "IN", + "regionname": "India", + "regions": [ "MM" ], + "script": "Sinh", + "sldr": false, + "tag": "pi", + "tags": [ "pi-IN", "pi-Sinh" ], + "windows": "pi-Sinh" + }, + { + "full": "pi-Brah-IN", + "iana": [ "Pali" ], + "iso639_3": "pli", + "name": "Pali", + "names": [ "Magadhan" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "MM" ], + "script": "Brah", + "sldr": false, + "tag": "pi-Brah", + "windows": "pi-Brah" }, { "full": "pi-Deva-IN", @@ -90072,7 +94042,7 @@ "windows": "pi-Deva" }, { - "full": "pi-Mymr-IN", + "full": "pi-Khar-IN", "iana": [ "Pali" ], "iso639_3": "pli", "name": "Pali", @@ -90081,13 +94051,13 @@ "region": "IN", "regionname": "India", "regions": [ "MM" ], - "script": "Mymr", + "script": "Khar", "sldr": false, - "tag": "pi-Mymr", - "windows": "pi-Mymr" + "tag": "pi-Khar", + "windows": "pi-Khar" }, { - "full": "pi-Sinh-IN", + "full": "pi-Khmr-IN", "iana": [ "Pali" ], "iso639_3": "pli", "name": "Pali", @@ -90096,10 +94066,25 @@ "region": "IN", "regionname": "India", "regions": [ "MM" ], - "script": "Sinh", + "script": "Khmr", "sldr": false, - "tag": "pi-Sinh", - "windows": "pi-Sinh" + "tag": "pi-Khmr", + "windows": "pi-Khmr" + }, + { + "full": "pi-Mymr-IN", + "iana": [ "Pali" ], + "iso639_3": "pli", + "name": "Pali", + "names": [ "Magadhan" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "MM" ], + "script": "Mymr", + "sldr": false, + "tag": "pi-Mymr", + "windows": "pi-Mymr" }, { "full": "pi-Thai-IN", @@ -90136,7 +94121,7 @@ "iana": [ "Yine" ], "iso639_3": "pib", "localname": "Yine", - "localnames": [ "Yine", "Yineri tokani" ], + "localnames": [ "Yine", "Yineru tokanu" ], "name": "Yine", "names": [ "Apurinã", "Chichineri", "Chontaquiro", "Contaquiro", "Pira", "Simiranch", "Tokanï", "Yineri tokani", "Yineru tokanu", "Yinerï" ], "region": "PE", @@ -90183,6 +94168,7 @@ "iana": [ "Piro" ], "iso639_3": "pie", "name": "Piro", + "names": [ "Atziqui", "Tihue" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -90244,7 +94230,6 @@ "iana": [ "Pini" ], "iso639_3": "pii", "name": "Pini", - "names": [ "Bandjanu", "Banjanu", "Bini", "Biniridjara", "Birni", "Birniridjara", "Buranudjara", "Jabura", "Madutjara", "Nana", "Nangaritjara", "Pandjanu", "Piniiri", "Piniridjara", "Piniritjara", "Pirniritjara", "Tjalkadjara", "Tjubun", "Tjupan", "Wordako" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -90286,19 +94271,19 @@ "windows": "pil-Latn" }, { - "full": "pim-Zyyy-US", + "full": "pim-Latn-US", "iana": [ "Powhatan" ], "iso639_3": "pim", "name": "Powhatan", "names": [ "Virginia Algonkian", "Virginia Algonquian" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "pim", - "tags": [ "pim-US", "pim-Zyyy" ], + "tags": [ "pim-Latn", "pim-US" ], "unwritten": true, - "windows": "pim-Zyyy" + "windows": "pim-Latn" }, { "full": "pin-Latn-PG", @@ -90336,8 +94321,9 @@ "full": "pip-Latn-NG", "iana": [ "Pero" ], "iso639_3": "pip", + "localnames": [ "Péerò" ], "name": "Pero", - "names": [ "Filiya", "Pipero" ], + "names": [ "Filiya", "Péerò", "Pìpéerò", "Walo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -90365,13 +94351,14 @@ "full": "pis-Latn-SB", "iana": [ "Pijin" ], "iso639_3": "pis", + "localname": "Pijin", "localnames": [ "Pijin" ], "name": "Pijin", "names": [ "Neo-Solomonic", "Solomons Pidgin" ], "region": "SB", "regionname": "Solomon Islands", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "pis", "tags": [ "pis-Latn", "pis-SB" ], "windows": "pis-Latn" @@ -90423,9 +94410,9 @@ "full": "piw-Latn-TZ", "iana": [ "Pimbwe" ], "iso639_3": "piw", - "localnames": [ "Kipimbwe" ], + "localnames": [ "Chɨpɨmbwe", "ƗChɨpɨmbwe" ], "name": "Pimbwe", - "names": [ "Cipimbwe", "Ichipimbwe", "Icipimbwe", "Kipimbwe" ], + "names": [ "Chɨpɨmbwe", "Cipimbwe", "Ichipimbwe", "Icipimbwe", "Kipimbwe", "ƗChɨpɨmbwe" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -90512,9 +94499,9 @@ "iana": [ "Pokomo", "Kipfokomo" ], "iso639_3": "pkb", "localname": "Kipfokomo", - "localnames": [ "Kipfokomo" ], - "name": "Kipfokomo", - "names": [ "Kipokomo", "Lower Pokomo", "Malachini", "Pfokomo", "Pokomo", "Upper Pokomo" ], + "localnames": [ "Kipfokomu" ], + "name": "Kipfokomu", + "names": [ "Kimpokomu", "Kipfokomo", "Lower Pokomo", "Milanchini", "Mpokomu", "Pfokomo", "Pokomo", "Upper Pokomo", "Wapfokomu" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -90551,33 +94538,34 @@ "windows": "pkg-Latn" }, { - "full": "pkh-Deva-BD", + "full": "pkh-Latn-BD", "iana": [ "Pankhu" ], "iso639_3": "pkh", "name": "Pangkhua", "names": [ "Paang", "Pang", "Pang Khua", "Pangkhu", "Pankho", "Pankhu", "Pankhua", "Panko", "Pankua" ], - "nophonvars": true, "region": "BD", "regionname": "Bangladesh", "regions": [ "IN" ], - "script": "Deva", + "script": "Latn", "sldr": false, - "tag": "pkh-Deva", - "windows": "pkh-Deva" + "tag": "pkh", + "tags": [ "pkh-BD", "pkh-Latn" ], + "windows": "pkh-Latn" }, { - "full": "pkh-Latn-BD", + "full": "pkh-Deva-BD", "iana": [ "Pankhu" ], "iso639_3": "pkh", "name": "Pangkhua", "names": [ "Paang", "Pang", "Pang Khua", "Pangkhu", "Pankho", "Pankhu", "Pankhua", "Panko", "Pankua" ], + "nophonvars": true, "region": "BD", "regionname": "Bangladesh", "regions": [ "IN" ], - "script": "Latn", + "script": "Deva", "sldr": false, - "tag": "pkh-Latn", - "windows": "pkh-Latn" + "tag": "pkh-Deva", + "windows": "pkh-Deva" }, { "full": "pkn-Latn-AU", @@ -90599,6 +94587,7 @@ "iana": [ "Pökoot" ], "iso639_3": "pko", "localnames": [ "ngala Pökot" ], + "macrolang": "kln", "name": "Pökoot", "names": [ "Pakot", "Pokot", "Pökot", "Suk", "ngala Pökot", "ng’ala Pokot" ], "region": "KE", @@ -90630,6 +94619,8 @@ "full": "pkr-Mlym-IN", "iana": [ "Attapady Kurumba" ], "iso639_3": "pkr", + "latnnames": [ "Pāl Kuṟumpa" ], + "localnames": [ "പാല് കുറുമ്പ" ], "name": "Kurumba, Attapady", "names": [ "Attapady Kurumba", "Kurumba", "Pal Kurumba" ], "region": "IN", @@ -90726,7 +94717,7 @@ "localname": "Miani", "localnames": [ "Miani" ], "name": "Miani", - "names": [ "Bonaputa-Mopu", "Miani North", "Tani" ], + "names": [ "Banar", "Bonaputa-Mopu", "Kenege", "Miani North", "Tani" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -90820,20 +94811,6 @@ "unwritten": true, "windows": "plh-Latn" }, - { - "full": "plj-Latn-NG", - "iana": [ "Polci" ], - "iso639_3": "plj", - "name": "Polci", - "names": [ "Palchi", "Palci", "Polchi" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "plj", - "tags": [ "plj-Latn", "plj-NG" ], - "windows": "plj-Latn" - }, { "full": "plk-Arab-PK", "iana": [ "Kohistani Shina" ], @@ -90852,8 +94829,10 @@ "full": "pll-Mymr-MM", "iana": [ "Shwe Palaung" ], "iso639_3": "pll", + "latnnames": [ "Saam Loong" ], + "localnames": [ "ဆာမ်လိုင်" ], "name": "Palaung, Shwe", - "names": [ "Golden Palaung", "Liang", "Liang Palaung", "Red Da’ang", "Shwe", "Shwe Palaung", "Ta-Ang Palaung", "Ta’ang Samlung" ], + "names": [ "Flowery De’ang", "Golden Palaung", "Liang", "Liang Palaung", "Red Da’ang", "Saam-Loong", "Shwe", "Shwe Palaung", "Ta-Ang Palaung", "Ta-ang", "Ta’ang Samlung" ], "region": "MM", "regionname": "Myanmar", "regions": [ "CN" ], @@ -90924,7 +94903,7 @@ "iana": [ "Palaka Senoufo" ], "iso639_3": "plr", "name": "Sénoufo, Palaka", - "names": [ "Kpalagha", "Palaka", "Palaka Senoufo", "Palara", "Pallakha", "Pilara" ], + "names": [ "Kpalaga", "Kpalagha", "Palaka", "Palaka Senoufo", "Palara", "Pallakha", "Pilara" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -90938,7 +94917,7 @@ "full": "pls-Latn-MX", "iana": [ "San Marcos Tlacoyalco Popoloca", "San Marcos Tlalcoyalco Popoloca" ], "iso639_3": "pls", - "localname": "Popoloca, San Marcos Tlalcoyalco", + "localname": "Ngigua", "localnames": [ "Ngigua" ], "name": "Popoloca, San Marcos Tlacoyalco", "names": [ "Ngigua", "Ngiwa", "Northern Popoloca", "Popoloca de San Marcos Tlalcoyalco", "Popoluca del Norte", "San Marcos Tlacoyalco Popoloca", "San Marcos Tlalcoyalco Popoloca" ], @@ -90964,7 +94943,7 @@ "script": "Latn", "sldr": true, "tag": "plu", - "tags": [ "plu-BR", "plu-Latn" ], + "tags": [ "kgm", "kgm-BR", "kgm-Latn", "kgm-Latn-BR", "plu-BR", "plu-Latn" ], "windows": "plu-Latn" }, { @@ -90985,8 +94964,8 @@ "full": "plw-Latn-PH", "iana": [ "Brooke's Point Palawano" ], "iso639_3": "plw", - "localname": "Palawano, Brooke’s Point", - "localnames": [ "Palawano", "Pelaꞌwan" ], + "localname": "Palawano", + "localnames": [ "Palawano" ], "name": "Palawano, Brooke’s Point", "names": [ "Brooke's Point Palawano", "Brooke’s Point Palawan", "Palawan", "Palawano", "Palawanun", "Palaweño", "Pelaꞌwan", "Pelewan", "Pinalawan", "Pinelawan" ], "region": "PH", @@ -91002,7 +94981,7 @@ "iana": [ "Bolyu" ], "iso639_3": "ply", "name": "Bolyu", - "names": [ "Baliu", "Lai", "Lailai", "Paliu", "Palju", "Palyu", "Polyu" ], + "names": [ "Baliu", "Buliu", "Lai", "Lai Yu", "Lailai", "Paliu", "Palju", "Palyu", "Polyu" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -91103,76 +95082,62 @@ "windows": "pmf-Latn" }, { - "full": "pmh-Zyyy-IN", + "full": "pmh-Brah-IN", "iana": [ "Māhārāṣṭri Prākrit" ], "iso639_3": "pmh", "name": "Māhārāṣṭri Prākrit", "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Brah", "sldr": false, "tag": "pmh", - "tags": [ "pmh-IN", "pmh-Zyyy" ], - "windows": "pmh-Zyyy" + "tags": [ "pmh-Brah", "pmh-IN" ], + "windows": "pmh-Brah" }, { - "full": "pmi-Zyyy-CN", + "full": "pmi-Latn-CN", "iana": [ "Northern Pumi" ], "iso639_3": "pmi", "name": "Pumi, Northern", "names": [ "Ch’rame", "Northern Pumi", "Pimi", "Primmi", "Pruumi", "P’rome", "P’umi", "P’ömi" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "pmi", - "tags": [ "pmi-CN", "pmi-Zyyy" ], + "tags": [ "pmi-CN", "pmi-Latn" ], "unwritten": true, - "windows": "pmi-Zyyy" + "windows": "pmi-Latn" }, { - "full": "pmj-Zyyy-CN", + "full": "pmj-Latn-CN", "iana": [ "Southern Pumi" ], "iso639_3": "pmj", "name": "Pumi, Southern", "names": [ "Pimi", "Primmi", "Prinmi", "Pruumi", "P’rome", "P’umi", "P’ömi", "Southern Pumi" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "pmj", - "tags": [ "pmj-CN", "pmj-Zyyy" ], + "tags": [ "pmj-CN", "pmj-Latn" ], "unwritten": true, - "windows": "pmj-Zyyy" + "windows": "pmj-Latn" }, { - "full": "pmk-Zyyy-US", - "iana": [ "Pamlico" ], - "iso639_3": "pmk", - "name": "Pamlico", - "region": "US", - "regionname": "United States", - "script": "Zyyy", - "sldr": false, - "tag": "pmk", - "tags": [ "pmk-US", "pmk-Zyyy" ], - "unwritten": true, - "windows": "pmk-Zyyy" - }, - { - "full": "pml-Zyyy-TN", + "full": "pml-Latn-TN", "iana": [ "Lingua Franca" ], "iso639_3": "pml", "name": "Lingua Franca", "names": [ "Aljamia", "Ferenghi", "Petit Mauresque", "Sabir", "’Ajnabi" ], "region": "TN", "regionname": "Tunisia", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "pml", - "tags": [ "pml-TN", "pml-Zyyy" ], + "tags": [ "pml-Latn", "pml-TN" ], "unwritten": true, - "windows": "pml-Zyyy" + "windows": "pml-Latn" }, { "full": "pmm-Latn-CM", @@ -91300,7 +95265,7 @@ "iso639_3": "pmx", "localnames": [ "Poula" ], "name": "Naga, Poumai", - "names": [ "Paumei", "Pomai", "Pome", "Poula", "Poumai", "Poumei", "Poumei Naga" ], + "names": [ "Paola", "Paumei", "Pomai", "Pome", "Poula", "Poumai", "Poumei", "Poumei Naga" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -91319,7 +95284,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "pmy", "tags": [ "pmy-ID", "pmy-Latn" ], "windows": "pmy-Latn" @@ -91343,8 +95308,8 @@ "full": "pna-Latn-MY", "iana": [ "Punan Bah-Biau" ], "iso639_3": "pna", - "name": "Penan, Bah-Biau", - "names": [ "Bah-Biau", "Punan", "Punan Bah-Biau" ], + "name": "Punan Bah-Biau", + "names": [ "Bah-Biau", "Bah-Biau Penan", "Punan" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -91359,7 +95324,7 @@ "iana": [ "Pannei" ], "iso639_3": "pnc", "name": "Pannei", - "names": [ "Tapango" ], + "names": [ "Panei", "Tapango" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -91399,11 +95364,12 @@ }, { "full": "png-Latn-NG", - "iana": [ "Pongu" ], + "iana": [ "Pangu", "Pongu" ], "iso639_3": "png", - "localnames": [ "Tarin" ], + "localname": "Ta̱rin", + "localnames": [ "Ta̱rin" ], "name": "Pangu", - "names": [ "Akwa", "Arringeu", "Pongo", "Pongu", "Tarin" ], + "names": [ "Akwa", "Arringeu", "Arĩ", "Panganci", "Pongo", "Pongu", "Rin", "Tara", "Tarin", "Tariya", "Ta̱rin", "Tə-rĩ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -91605,7 +95571,7 @@ "sldr": false, "tag": "pnt", "tags": [ "pnt-GR", "pnt-Grek" ], - "windows": "pnt-Grek" + "windows": "pnt-Grek-GR" }, { "full": "pnt-Cyrl-GR", @@ -91620,7 +95586,7 @@ "script": "Cyrl", "sldr": false, "tag": "pnt-Cyrl", - "windows": "pnt-Cyrl" + "windows": "pnt-Cyrl-GR" }, { "full": "pnt-Latn-TR", @@ -91635,7 +95601,7 @@ "sldr": false, "tag": "pnt-Latn", "tags": [ "pnt-TR" ], - "windows": "pnt-Latn" + "windows": "pnt-Latn-TR" }, { "full": "pnu-Zyyy-CN", @@ -91649,6 +95615,7 @@ "sldr": false, "tag": "pnu", "tags": [ "pnu-CN", "pnu-Zyyy" ], + "unwritten": true, "windows": "pnu-Zyyy" }, { @@ -91746,10 +95713,10 @@ "full": "poe-Latn-MX", "iana": [ "San Juan Atzingo Popoloca" ], "iso639_3": "poe", - "localname": "Popoloca, San Juan Atzingo", - "localnames": [ "Ngigua" ], + "localname": "Ngigua", + "localnames": [ "Ngiva" ], "name": "Popoloca, San Juan Atzingo", - "names": [ "Atzingo Popoloca", "Eastern Popoloca", "Ngiba", "Ngigua", "Ngiwa", "Popoloca de San Juan Atzingo", "Popoloca del Oriente", "San Juan Atzingo Popoloca", "Southern Popoloca" ], + "names": [ "Atzingo Popoloca", "Eastern Popoloca", "Ngiba", "Ngigua", "Ngiva", "Ngiwa", "Nkiva", "Nquiva", "Popoloca de Oriente", "Popoloca de San Juan Atzingo", "Popoloca del Oriente", "San Juan Atzingo Popoloca", "Southern Popoloca" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -91793,9 +95760,9 @@ "iana": [ "Poqomchi'" ], "iso639_3": "poh", "localname": "Poqonchi", - "localnames": [ "Poqonchi" ], + "localnames": [ "Poqon", "Poqonchi" ], "name": "Poqomchi’", - "names": [ "Pocomchí", "Poconchí", "Pokomchí", "Pokonchí", "Poqomchi'", "Poqonchi", "Tactic Pokomchí" ], + "names": [ "Pocomchí", "Poconchí", "Pokomchí", "Pokon", "Pokonchi’", "Pokonchí", "Poqomchi'", "Poqon", "Poqonchi", "Tactic Pokomchí" ], "region": "GT", "regionname": "Guatemala", "script": "Latn", @@ -91804,6 +95771,22 @@ "tags": [ "poh-GT", "poh-Latn" ], "windows": "poh-Latn" }, + { + "full": "poh-Latn-GT-x-western", + "iana": [ "Poqomchi'" ], + "iso639_3": "poh", + "localnames": [ "Poqon", "Poqonchi" ], + "name": "Poqomchi’", + "names": [ "Pocomchí", "Poconchí", "Pokomchí", "Pokon", "Pokonchi’", "Pokonchí", "Poqomchi'", "Poqon", "Poqonchi", "Tactic Pokomchí" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4744", + "script": "Latn", + "sldr": false, + "tag": "poh-x-western", + "tags": [ "poh-GT-x-western", "poh-Latn-x-western" ], + "windows": "poh-Latn-x-western" + }, { "full": "poi-Latn-MX", "iana": [ "Highland Popoluca" ], @@ -91947,7 +95930,7 @@ "iso639_3": "pov", "localnames": [ "Kiriol" ], "name": "Guinea-Bissau Creole", - "names": [ "Casamance Creole", "Casamancese Creole", "Casamançais", "Crioulo", "Crioulo de Guinea-Bissau", "Créole", "Créole afro-portugais de Casamance", "Créole casamançais", "Créole de Casamance", "Guinea-Bissau Kriyol", "Guineense", "Kiriol", "Kriulo", "Kriyol", "Portuguese Creole", "Upper Guinea Crioulo" ], + "names": [ "Casamance Creole", "Casamancese Creole", "Casamançais", "Crioulo", "Crioulo de Guinea-Bissau", "Créole", "Créole afro-portugais de Casamance", "Créole casamançais", "Créole de Casamance", "Guinea-Bissau Kriyol", "Guineense", "Kiriol", "Kriol", "Kriulo", "Kriyol", "Portuguese Creole", "Upper Guinea Crioulo" ], "region": "GW", "regionname": "Guinea-Bissau", "regions": [ "GM", "PT", "SN", "US" ], @@ -91961,9 +95944,9 @@ "full": "pow-Latn-MX", "iana": [ "San Felipe Otlaltepec Popoloca" ], "iso639_3": "pow", - "localnames": [ "Ngiba" ], + "localnames": [ "Nguiva" ], "name": "Popoloca, San Felipe Otlaltepec", - "names": [ "Ngiba", "Popoloca de San Felipe Otlaltepec", "Popoloca del Poniente", "San Felipe Otlaltepec Popoloca", "Western Popoloca" ], + "names": [ "Ngiba", "Ngiwa", "Nguiva", "Popoloca de San Felipe Otlaltepec", "Popoloca del Poniente", "San Felipe Otlaltepec Popoloca", "Western Popoloca" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -91989,6 +95972,7 @@ "full": "poy-Latn-TZ", "iana": [ "Pogolo" ], "iso639_3": "poy", + "localname": "Pogolo", "name": "Pogolo", "names": [ "Chipogolo", "Chipogoro", "Cipogolo", "Pogolu", "Pogora", "Pogoro", "Shipogolu" ], "region": "TZ", @@ -92003,8 +95987,8 @@ "full": "ppe-Latn-PG", "iana": [ "Papi" ], "iso639_3": "ppe", - "name": "Papi", - "names": [ "Paupe" ], + "name": "Baiyamo", + "names": [ "Papi", "Paupe" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -92033,9 +96017,9 @@ "full": "ppk-Latn-ID", "iana": [ "Uma" ], "iso639_3": "ppk", - "localnames": [ "Uma" ], + "localnames": [ "Basa Uma", "Uma" ], "name": "Uma", - "names": [ "Koro", "Oema", "Pipikoro" ], + "names": [ "Basa Uma", "Koro", "Oema", "Pipikoro" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -92078,6 +96062,7 @@ "iana": [ "Papapana" ], "iso639_3": "ppn", "name": "Papapana", + "names": [ "Auta", "Numanuma", "Papapa", "Teperoi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -92108,6 +96093,7 @@ "iana": [ "Pelende" ], "iso639_3": "ppp", "name": "Pelende", + "names": [ "Kipelende" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -92121,8 +96107,8 @@ "full": "ppq-Latn-PG", "iana": [ "Pei" ], "iso639_3": "ppq", - "name": "Pei", - "names": [ "Pai" ], + "name": "Pefiyahe", + "names": [ "Pai", "Pei" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -92136,10 +96122,10 @@ "full": "pps-Latn-MX", "iana": [ "San Luís Temalacayuca Popoloca" ], "iso639_3": "pps", - "localname": "Popoloca, San Luís Temalacayuca", - "localnames": [ "Ngigua" ], + "localname": "Ngigua", + "localnames": [ "Ngigua", "Ngigua Temalacayuca" ], "name": "Popoloca, San Luís Temalacayuca", - "names": [ "Ngiba", "Ngigua", "Ngiwa", "Popoloca de San Luis Temalacayuca", "San Luís Temalacayuca Popoloca" ], + "names": [ "Ngiba", "Ngigua", "Ngigua Temalacayuca", "Ngiwa", "Popoloca de San Luis Temalacayuca", "San Luís Temalacayuca Popoloca" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -92152,8 +96138,8 @@ "full": "ppt-Latn-PG", "iana": [ "Pare" ], "iso639_3": "ppt", - "name": "Pare", - "names": [ "Akium-Pare", "Pa" ], + "name": "Pa", + "names": [ "Akium-Pare", "Pare" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -92181,7 +96167,7 @@ "iana": [ "Pa'a" ], "iso639_3": "pqa", "name": "Pa’a", - "names": [ "Afa", "Afanci", "Afawa", "Fa’awa", "Foni", "Fucaka", "Fuucəka", "Pa'a", "Pala", "Pa’anci", "Pa’awa" ], + "names": [ "Afa", "Afanci", "Afawa", "Fa’awa", "Foni", "Fucaka", "Fuucəka", "Pa'a", "Paha", "Pala", "Pa’anci", "Pa’awa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -92265,8 +96251,9 @@ "full": "pre-Latn-ST", "iana": [ "Principense" ], "iso639_3": "pre", + "localnames": [ "Lung'Ie" ], "name": "Principense", - "names": [ "Lun’gwiye" ], + "names": [ "Dialeto", "Lung’Ie", "Lun’gwiye" ], "region": "ST", "regionname": "Sao Tome and Principe", "script": "Latn", @@ -92392,8 +96379,8 @@ "regionname": "Afghanistan", "script": "Arab", "sldr": false, - "tag": "prn", - "tags": [ "prn-AF", "prn-Arab" ], + "tag": "prn-Arab", + "tags": [ "prn", "prn-AF" ], "unwritten": true, "windows": "prn-Arab" }, @@ -92411,21 +96398,6 @@ "tags": [ "pro-FR", "pro-Latn" ], "windows": "pro-Latn" }, - { - "full": "prp-Zyyy-IN", - "iana": [ "Parsi" ], - "iso639_3": "prp", - "name": "Parsi", - "names": [ "Parsee" ], - "region": "IN", - "regionname": "India", - "regions": [ "CN", "PK", "US" ], - "script": "Zyyy", - "sldr": false, - "tag": "prp", - "tags": [ "prp-IN", "prp-Zyyy" ], - "windows": "prp-Zyyy" - }, { "full": "prq-Latn-PE", "iana": [ "Ashéninka Perené" ], @@ -92461,6 +96433,7 @@ "iso639_3": "prs", "latnnames": [ "Farsi" ], "localnames": [ "فارسی" ], + "macrolang": "fa", "name": "Dari", "names": [ "Afghan Persian", "Badakhshi", "Eastern Farsi", "Madaglashti", "Parsi", "Persian" ], "region": "AF", @@ -92476,6 +96449,8 @@ "full": "prt-Thai-TH", "iana": [ "Phai" ], "iso639_3": "prt", + "latnnames": [ "Prai" ], + "localnames": [ "ไปร" ], "name": "Prai", "names": [ "Lao Mai", "Lao Prai", "Lua Prai", "Lua’", "Phai", "Pray", "Thin" ], "region": "TH", @@ -92521,8 +96496,8 @@ "full": "prx-Arab-IN", "iana": [ "Purik" ], "iso639_3": "prx", - "name": "Purik", - "names": [ "Burig", "Burigskat", "Purig", "Purig-pa", "Purigskad", "Purik Bhotia", "Purkhi", "Purki" ], + "name": "Purig", + "names": [ "Burig", "Burigskat", "Purig-pa", "Purigi", "Purigskad", "Purik", "Purik Bhotia", "Purik-Sham-Nubra", "Purkhi", "Purki" ], "region": "IN", "regionname": "India", "script": "Arab", @@ -92535,8 +96510,8 @@ "full": "prx-Tibt-IN", "iana": [ "Purik" ], "iso639_3": "prx", - "name": "Purik", - "names": [ "Burig", "Burigskat", "Purig", "Purig-pa", "Purigskad", "Purik Bhotia", "Purkhi", "Purki" ], + "name": "Purig", + "names": [ "Burig", "Burigskat", "Purig-pa", "Purigi", "Purigskad", "Purik", "Purik Bhotia", "Purik-Sham-Nubra", "Purkhi", "Purki" ], "nophonvars": true, "obsolete": true, "region": "IN", @@ -92551,6 +96526,7 @@ "iana": [ "Providencia Sign Language" ], "iso639_3": "prz", "name": "Providencia Sign Language", + "names": [ "PISL", "Providence Island Sign Language" ], "region": "CO", "regionname": "Colombia", "script": "Zxxx", @@ -92566,8 +96542,8 @@ "latnnames": [ "Pashto" ], "localname": "پښتو", "localnames": [ "پښتو" ], - "name": "Pashto, Northern", - "names": [ "Afghan", "Eastern Afghan Pashto", "Northwestern Pakhto", "Pakhto", "Pakhtoo", "Pakhtoon", "Pakhtun", "Paktu", "Pashto", "Pashtoon", "Pashtu", "Passtoo", "Pushto", "Pusto", "Sharqi", "Yousafzai Pashto", "Yusufzai Pashto" ], + "name": "Pushto", + "names": [ "Afghan", "Eastern Afghan Pashto", "Northwestern Pakhto", "Pakhto", "Pakhtoo", "Pakhtoon", "Pakhtun", "Paktu", "Pashto", "Pashto, Northern", "Pashtoon", "Pashtu", "Passtoo", "Pusto", "Sharqi", "Yousafzai Pashto", "Yusufzai Pashto" ], "region": "AF", "regionname": "Afghanistan", "regions": [ "AE", "CA", "IN", "US" ], @@ -92576,7 +96552,7 @@ "suppress": true, "tag": "ps", "tags": [ "pbu", "pbu-AF", "pbu-Arab", "pbu-Arab-AF", "ps-AF", "ps-Arab" ], - "windows": "ps" + "windows": "ps-AF" }, { "full": "ps-Arab-PK", @@ -92589,9 +96565,8 @@ "regionname": "Pakistan", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ps-PK", - "windows": "ps-PK" + "windows": "ps-Arab-PK" }, { "full": "psa-Latn-ID", @@ -92609,7 +96584,7 @@ }, { "full": "psc-Zxxx-IR", - "iana": [ "Persian Sign Language" ], + "iana": [ "Iranian Sign Language", "Persian Sign Language" ], "iso639_3": "psc", "name": "Iranian Sign Language", "names": [ "Persian Sign Language", "ZEI", "Zaban Eshareh Irani" ], @@ -92626,7 +96601,7 @@ "iana": [ "Plains Indian Sign Language" ], "iso639_3": "psd", "name": "Plains Indian Sign Language", - "names": [ "First Nation Sign Language", "Hand Talk", "Indian Language of Signs", "Indian Sign Language", "NAISL", "North American Indian Sign Language", "PISL", "PST", "Plains Sign Language", "Plains Sign Talk", "Sign Talk" ], + "names": [ "AISL", "American Indian Sign Language", "First Nation Sign Language", "Hand Talk", "Indian Language of Signs", "Indian Sign Language", "NAISL", "North American Indian Sign Language", "PISL", "PST", "Plains Sign Language", "Plains Sign Talk", "Sign Talk" ], "region": "US", "regionname": "United States", "regions": [ "CA" ], @@ -92640,6 +96615,7 @@ "full": "pse-Latn-ID", "iana": [ "Central Malay" ], "iso639_3": "pse", + "macrolang": "ms", "name": "Malay, Central", "names": [ "Bengkulu", "Besemah", "Central Malay", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], "region": "ID", @@ -92650,6 +96626,118 @@ "tags": [ "ms-pse", "ms-pse-ID", "pse-ID", "pse-Latn" ], "windows": "pse-Latn" }, + { + "full": "pse-Latn-ID-x-enim", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "9634", + "script": "Latn", + "sldr": false, + "tag": "pse-x-enim", + "tags": [ "ms-pse-ID-x-enim", "ms-pse-x-enim", "pse-ID-x-enim", "pse-Latn-x-enim" ], + "windows": "pse-Latn-x-enim" + }, + { + "full": "pse-Latn-ID-x-lintang", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "12976", + "script": "Latn", + "sldr": false, + "tag": "pse-x-lintang", + "tags": [ "ms-pse-ID-x-lintang", "ms-pse-x-lintang", "pse-ID-x-lintang", "pse-Latn-x-lintang" ], + "windows": "pse-Latn-x-lintang" + }, + { + "full": "pse-Latn-ID-x-manak", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "20054", + "script": "Latn", + "sldr": false, + "tag": "pse-x-manak", + "tags": [ "ms-pse-ID-x-manak", "ms-pse-x-manak", "pse-ID-x-manak", "pse-Latn-x-manak" ], + "windows": "pse-Latn-x-manak" + }, + { + "full": "pse-Latn-ID-x-ogan", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "15233", + "script": "Latn", + "sldr": false, + "tag": "pse-x-ogan", + "tags": [ "ms-pse-ID-x-ogan", "ms-pse-x-ogan", "pse-ID-x-ogan", "pse-Latn-x-ogan" ], + "windows": "pse-Latn-x-ogan" + }, + { + "full": "pse-Latn-ID-x-pasemah", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "1181", + "script": "Latn", + "sldr": false, + "tag": "pse-x-pasemah", + "tags": [ "ms-pse-ID-x-pasemah", "ms-pse-x-pasemah", "pse-ID-x-pasemah", "pse-Latn-x-pasemah" ], + "windows": "pse-Latn-x-pasemah" + }, + { + "full": "pse-Latn-ID-x-semenda", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "5070", + "script": "Latn", + "sldr": false, + "tag": "pse-x-semenda", + "tags": [ "ms-pse-ID-x-semenda", "ms-pse-x-semenda", "pse-ID-x-semenda", "pse-Latn-x-semenda" ], + "windows": "pse-Latn-x-semenda" + }, + { + "full": "pse-Latn-ID-x-serawai", + "iana": [ "Malay, Central" ], + "iso639_3": "pse", + "macrolang": "ms", + "name": "Malay, Central", + "names": [ "Bengkulu", "Besemah", "Enim", "Lintang", "Melayu Tengah", "Midden Maleisch", "Ogan", "Pasemah", "Semendo", "Serawai" ], + "region": "ID", + "regionname": "Indonesia", + "rod": "3807", + "script": "Latn", + "sldr": false, + "tag": "pse-x-serawai", + "tags": [ "ms-pse-ID-x-serawai", "ms-pse-x-serawai", "pse-ID-x-serawai", "pse-Latn-x-serawai" ], + "windows": "pse-Latn-x-serawai" + }, { "full": "psg-Zxxx-MY", "iana": [ "Penang Sign Language" ], @@ -92682,6 +96770,8 @@ "full": "psi-Arab-AF", "iana": [ "Southeast Pashai", "Southeast Pashayi" ], "iso639_3": "psi", + "latnnames": [ "Pashai" ], + "localnames": [ "پَشَي" ], "name": "Pashai, Southeast", "names": [ "Pashai", "Southeast Pashai", "Southeast Pashayi" ], "region": "AF", @@ -92711,7 +96801,7 @@ "iana": [ "Pauserna" ], "iso639_3": "psm", "name": "Pauserna", - "names": [ "Guarayu-Ta", "Paucerne", "Pauserna-Guarasugwé" ], + "names": [ "Guarasugwé", "Guarasú’we", "Guarayu-Ta", "Paucerne", "Pauserna-Guarasugwé", "Warázu" ], "region": "BO", "regionname": "Bolivia", "script": "Latn", @@ -92768,6 +96858,7 @@ "full": "psq-Latn-PG", "iana": [ "Pasi" ], "iso639_3": "psq", + "localnames": [ "Pasi" ], "name": "Pasi", "names": [ "Besi" ], "region": "PG", @@ -92815,6 +96906,7 @@ "iso639_3": "pst", "latnnames": [ "Pashto" ], "localnames": [ "پښتو" ], + "macrolang": "ps", "name": "Pashto, Central", "names": [ "Central Pashto", "Mahsudi" ], "region": "PK", @@ -92830,12 +96922,12 @@ "iana": [ "Sauraseni Prākrit" ], "iso639_3": "psu", "name": "Sauraseni Prākrit", - "nophonvars": true, "region": "IN", "regionname": "India", "script": "Brah", "sldr": false, - "tag": "psu-Brah", + "tag": "psu", + "tags": [ "psu-Brah", "psu-IN" ], "windows": "psu-Brah" }, { @@ -92888,7 +96980,7 @@ "localname": "português", "localnames": [ "Português" ], "name": "Portuguese", - "names": [ "Portugais", "Portugués", "Portugués del Uruguay", "Português", "Portunhol", "Portuñol", "Purtagaalee", "Uruguayan Portuguese" ], + "names": [ "Portugais", "Portugués", "Portugués del Uruguay", "Português", "Portunhol", "Portuñol", "Uruguayan Portuguese" ], "region": "BR", "regionname": "Brazil", "regions": [ "AD", "AG", "AU", "BE", "BM", "CA", "CG", "CH", "CW", "DE", "ES", "FI", "FR", "GG", "GQ", "GY", "IN", "JE", "JM", "MW", "PY", "RU", "SN", "SR", "US", "UY", "VC", "VE", "ZA", "ZM" ], @@ -92898,7 +96990,7 @@ "tag": "pt", "tags": [ "pt-BR", "pt-Latn" ], "variants": [ "abl1943", "ai1990", "colb1945" ], - "windows": "pt" + "windows": "pt-BR" }, { "full": "pt-Latn-AO", @@ -92911,16 +97003,15 @@ "regionname": "Angola", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-AO", - "windows": "pt-AO" + "windows": "pt-Latn-AO" }, { "full": "pt-Brai-PT", "iana": [ "Portuguese" ], "iso639_3": "por", "name": "Portuguese", - "names": [ "Portugais", "Portugués del Uruguay", "Português", "Portunhol", "Portuñol", "Purtagaalee", "Uruguayan Portuguese" ], + "names": [ "Portugais", "Portugués del Uruguay", "Português", "Portunhol", "Portuñol", "Uruguayan Portuguese" ], "nophonvars": true, "region": "PT", "regionname": "Portugal", @@ -92928,7 +97019,7 @@ "script": "Brai", "sldr": false, "tag": "pt-Brai", - "windows": "pt-Brai" + "windows": "pt-Brai-PT" }, { "full": "pt-Latn-CV", @@ -92941,9 +97032,8 @@ "regionname": "Cape Verde", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-CV", - "windows": "pt-CV" + "windows": "pt-Latn-CV" }, { "full": "pt-Latn-GW", @@ -92956,9 +97046,8 @@ "regionname": "Guinea-Bissau", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-GW", - "windows": "pt-GW" + "windows": "pt-Latn-GW" }, { "full": "pt-Latn-LU", @@ -92971,9 +97060,8 @@ "regionname": "Luxembourg", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-LU", - "windows": "pt-LU" + "windows": "pt-Latn-LU" }, { "full": "pt-Latn-MO", @@ -92986,9 +97074,8 @@ "regionname": "Macao", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-MO", - "windows": "pt-MO" + "windows": "pt-Latn-MO" }, { "full": "pt-Latn-MZ", @@ -93001,9 +97088,8 @@ "regionname": "Mozambique", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-MZ", - "windows": "pt-MZ" + "windows": "pt-Latn-MZ" }, { "full": "pt-Latn-PT", @@ -93016,9 +97102,8 @@ "regionname": "Portugal", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-PT", - "windows": "pt-PT" + "windows": "pt-Latn-PT" }, { "full": "pt-Latn-ST", @@ -93031,9 +97116,8 @@ "regionname": "Sao Tome and Principe", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-ST", - "windows": "pt-ST" + "windows": "pt-Latn-ST" }, { "full": "pt-Latn-TL", @@ -93046,9 +97130,8 @@ "regionname": "Timor-Leste", "script": "Latn", "sldr": true, - "suppress": true, "tag": "pt-TL", - "windows": "pt-TL" + "windows": "pt-Latn-TL" }, { "full": "pta-Latn-PY", @@ -93192,7 +97275,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ptu", "tags": [ "ptu-ID", "ptu-Latn" ], "windows": "ptu-Latn" @@ -93217,6 +97300,7 @@ "iana": [ "Pentlatch" ], "iso639_3": "ptw", "name": "Pentlatch", + "names": [ "Puntlatch", "Puntledge" ], "region": "CA", "regionname": "Canada", "script": "Zyyy", @@ -93304,7 +97388,7 @@ "iana": [ "Puelche" ], "iso639_3": "pue", "name": "Puelche", - "names": [ "Gennaken", "Günün Yajich", "Gününa Küne", "Northern Tehuelche", "Pampa" ], + "names": [ "Gennaken", "Günün Yajich", "Gününa Küne", "Gününa Yajich", "Northern Tehuelche", "Pampa" ], "region": "AR", "regionname": "Argentina", "script": "Latn", @@ -93332,9 +97416,9 @@ "full": "pug-Latn-BF", "iana": [ "Phuie" ], "iso639_3": "pug", - "localnames": [ "Phuie" ], + "localnames": [ "Phuieŋ", "Phuĩé" ], "name": "Phuie", - "names": [ "Buguli", "Buguri", "Phuien", "Phuó", "Pougouli", "Puguli", "Pwa", "Pwe", "Pwie", "Pwien", "Pwo" ], + "names": [ "Buguli", "Buguri", "Phuien", "Phuieŋ", "Phuó", "Phuĩé", "Phwĩe", "PhwĩƗ", "Pougouli", "Puguli", "Pwa", "Pwe", "Pwie", "Pwien", "Pwo" ], "region": "BF", "regionname": "Burkina Faso", "script": "Latn", @@ -93353,7 +97437,7 @@ "regionname": "Colombia", "regions": [ "VE" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "pui", "tags": [ "pui-CO", "pui-Latn" ], "windows": "pui-Latn" @@ -93436,6 +97520,7 @@ "iana": [ "Puquina" ], "iso639_3": "puq", "name": "Puquina", + "names": [ "Pukina" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -93479,7 +97564,7 @@ "full": "puu-Latn-GA", "iana": [ "Punu" ], "iso639_3": "puu", - "localnames": [ "yiPunu" ], + "localnames": [ "Yipunu" ], "name": "Punu", "names": [ "Ipounou", "Ipunu", "Pouno", "Pounou", "Puno", "Yipounou", "Yipunu", "yi-punu", "yiPunu" ], "region": "GA", @@ -93522,26 +97607,26 @@ "windows": "pux-Latn" }, { - "full": "puy-Zyyy-US", + "full": "puy-Latn-US", "iana": [ "Purisimeño" ], "iso639_3": "puy", "name": "Purisimeño", "names": [ "Chumash", "Chumasha Proper", "La Purisima", "Purismeño", "Purísima", "Santa Rosa" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "puy", - "tags": [ "puy-US", "puy-Zyyy" ], + "tags": [ "puy-Latn", "puy-US" ], "unwritten": true, - "windows": "puy-Zyyy" + "windows": "puy-Latn" }, { "full": "pwa-Latn-PG", "iana": [ "Pawaia" ], "iso639_3": "pwa", "name": "Pawaia", - "names": [ "Aurama", "Pavaia", "Pawaian", "Sira", "Tudahwe", "Yasa" ], + "names": [ "Pauaia", "Pavaia", "Pawaian", "Tudahwe", "Yasa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -93555,7 +97640,7 @@ "iana": [ "Panawa" ], "iso639_3": "pwb", "name": "Panawa", - "names": [ "Bugel", "Bujiye" ], + "names": [ "Bugel", "Bujiyel", "anaPanawa", "iPanawa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -93642,19 +97727,19 @@ "windows": "pwo-Mymr" }, { - "full": "pwr-Zyyy-IN", + "full": "pwr-Deva-IN", "iana": [ "Powari" ], "iso639_3": "pwr", "name": "Powari", "names": [ "Pwari" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "pwr", - "tags": [ "pwr-IN", "pwr-Zyyy" ], + "tags": [ "pwr-Deva", "pwr-IN" ], "unwritten": true, - "windows": "pwr-Zyyy" + "windows": "pwr-Deva" }, { "full": "pww-Thai-TH", @@ -93674,8 +97759,9 @@ "full": "pxm-Latn-MX", "iana": [ "Quetzaltepec Mixe" ], "iso639_3": "pxm", + "localnames": [ "Ayuk", "Kuna'tsmʉdʉ Ayuk" ], "name": "Mixe, Quetzaltepec", - "names": [ "Central Mixe", "Chuxnabán Mixe", "Midland Mixe", "Mixe Alto del Sur", "Quetzaltepec Mixe" ], + "names": [ "Ayuk", "Ayuuk", "Central Mixe", "Chuxnabán Mixe", "Kuna’tsmʉdʉ Ayuk", "Midland Mixe", "Mixe Alto del Sur", "Mixe Medio del Este", "Quetzaltepec Mixe" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -93688,15 +97774,15 @@ "full": "pye-Latn-CI", "iana": [ "Pye Krumen" ], "iso639_3": "pye", + "localnames": [ "Pɩɛwin" ], "name": "Krumen, Pye", - "names": [ "Kroumen", "Northeastern Krumen", "Pye Krumen", "Southeastern Krumen" ], + "names": [ "Kroumen", "Kroumen Piè", "Krumen", "Krumen Piè", "Krumen Pye", "Northeastern Krumen", "Pye Krumen", "Pɩɛwin", "Southeastern Krumen" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", "sldr": false, "tag": "pye", "tags": [ "pye-CI", "pye-Latn" ], - "unwritten": true, "windows": "pye-Latn" }, { @@ -93750,7 +97836,7 @@ "iso639_3": "pyu", "localnames": [ "Pinuyumayan" ], "name": "Puyuma", - "names": [ "Kadas", "Panapanayan", "Pelam", "Pilam", "Pinuyumayan", "Piyuma", "Pyuma", "Tipun" ], + "names": [ "Beinan", "Kadas", "Panapanayan", "Pelam", "Pilam", "Pinuyumayan", "Piyuma", "Pyuma", "Tipun" ], "region": "TW", "regionname": "China-Taiwan", "script": "Latn", @@ -93764,7 +97850,7 @@ "iana": [ "Puyuma" ], "iso639_3": "pyu", "name": "Puyuma", - "names": [ "Kadas", "Panapanayan", "Pelam", "Pilam", "Pinuyumayan", "Piyuma", "Pyuma", "Tipun" ], + "names": [ "Beinan", "Kadas", "Panapanayan", "Pelam", "Pilam", "Pinuyumayan", "Piyuma", "Pyuma", "Tipun" ], "nophonvars": true, "obsolete": true, "region": "TW", @@ -93775,17 +97861,17 @@ "windows": "pyu-Hani" }, { - "full": "pyx-Zyyy-MM", + "full": "pyx-Mymr-MM", "iana": [ "Pyu (Myanmar)" ], "iso639_3": "pyx", "name": "Pyu (Myanmar)", "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "pyx", - "tags": [ "pyx-MM", "pyx-Zyyy" ], - "windows": "pyx-Zyyy" + "tags": [ "pyx-MM", "pyx-Mymr" ], + "windows": "pyx-Mymr" }, { "full": "pyy-Latn-MM", @@ -93801,13 +97887,42 @@ "tags": [ "pyy-Latn", "pyy-MM" ], "windows": "pyy-Latn" }, + { + "full": "pze-Latn-NG", + "iso639_3": "pze", + "name": "Pesse", + "names": [ "Palchi", "Palci", "Palsawa", "Palshwa", "Pisse", "Polchi", "Polsawa", "Polshi", "Posse", "Posə", "Posɨ", "Pəlci" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "pze", + "tags": [ "pze-Latn", "pze-NG" ], + "unwritten": true, + "windows": "pze-Latn" + }, + { + "full": "pzh-Latn-TW", + "iana": [ "Pazeh" ], + "iso639_3": "pzh", + "localnames": [ "Pazeh" ], + "name": "Pazeh", + "names": [ "Bazehai", "Kulon-Pazeh", "Pazeh-Kahabu", "Pazeh-Kaxabu", "Pazehe", "Pazih", "Pazzehe", "Sprache von Formosa" ], + "region": "TW", + "regionname": "China-Taiwan", + "script": "Latn", + "sldr": false, + "tag": "pzh", + "tags": [ "pzh-Latn", "pzh-TW", "uun", "uun-Latn", "uun-Latn-TW", "uun-TW" ], + "windows": "pzh-Latn" + }, { "full": "pzn-Latn-MM", - "iana": [ "Para Naga" ], + "iana": [ "Jejara Naga", "Para Naga" ], "iso639_3": "pzn", "localnames": [ "Naga" ], "name": "Naga, Jejara", - "names": [ "Bara Naga", "Naga", "Para Naga", "Parasar Naga" ], + "names": [ "Bara Naga", "Jejara Naga", "Naga", "Para Naga", "Parasar Naga" ], "region": "MM", "regionname": "Myanmar", "script": "Latn", @@ -93822,8 +97937,8 @@ "iso639_3": "que", "localname": "Runasimi", "localnames": [ "Runasimi", "Runasimi Qusqu Qullaw" ], - "name": "Quechua, Cusco", - "names": [ "Cuzco", "Cuzco Quechua", "Qheswa", "Qheswasimi", "Quechua", "Quechua Cusco", "Quechua Qosqo-Qollaw", "Quechua de Cusco-Collao", "Qusqu qhichwa simi", "Qusqu runasimi", "Runasimi", "Runasimi Qusqu Qullaw" ], + "name": "Quechua", + "names": [ "Cuzco", "Cuzco Quechua", "Qheswa", "Qheswasimi", "Quechua Cusco", "Quechua Qosqo-Qollaw", "Quechua de Cusco-Collao", "Quechua, Cusco", "Qusqu qhichwa simi", "Qusqu runasimi", "Runasimi", "Runasimi Qusqu Qullaw" ], "region": "PE", "regionname": "Peru", "regions": [ "AR" ], @@ -93832,24 +97947,23 @@ "suppress": true, "tag": "qu", "tags": [ "qu-Latn", "qu-PE", "quz", "quz-Latn", "quz-Latn-PE", "quz-PE" ], - "windows": "qu" + "windows": "qu-PE" }, { "full": "qu-Latn-BO", "iana": [ "Quechua" ], "localname": "Uralan Buliwya runasimi", - "localnames": [ "Runasimi" ], - "name": "Quechua, South Bolivian", - "names": [ "Central Bolivian Quechua", "Cochabamba Quechua", "Quechua", "Quechua Boliviano", "Runasimi" ], + "localnames": [ "Qhichwa" ], + "name": "Quechua", + "names": [ "Central Bolivian Quechua", "Cochabamba Quechua", "Qhichwa", "Qhishwa", "Quechua Boliviano", "Quechua, South Bolivian", "Uralan Buliwya runasimi" ], "region": "BO", "regionname": "Bolivia", "regions": [ "AR", "CL" ], "script": "Latn", "sldr": true, - "suppress": true, "tag": "qu-BO", "tags": [ "cqu", "cqu-BO", "cqu-Latn", "cqu-Latn-BO", "quh", "quh-BO", "quh-Latn", "quh-Latn-BO" ], - "windows": "qu-BO" + "windows": "qu-Latn-BO" }, { "full": "qu-Latn-EC", @@ -93861,9 +97975,8 @@ "regionname": "Ecuador", "script": "Latn", "sldr": true, - "suppress": true, "tag": "qu-EC", - "windows": "qu-EC" + "windows": "qu-Latn-EC" }, { "full": "qua-Latn-US", @@ -93886,6 +97999,7 @@ "iso639_3": "qub", "localname": "Runa shimi", "localnames": [ "Runa shimi" ], + "macrolang": "qu", "name": "Quechua, Huallaga", "names": [ "Alto Huallaga", "Huallaga Huánuco Quechua", "Quechua Huallaguino", "Quechua de Huánuco", "Quechua de Llacón", "Quechua de Matihuaca", "Runa shimi" ], "region": "PE", @@ -93900,8 +98014,8 @@ "full": "quc-Latn-GT", "iana": [ "K'iche'", "Quiché" ], "iso639_3": "quc", - "localname": "K’iche’", - "localnames": [ "Qach'abel" ], + "localname": "Qach'abel", + "localnames": [ "K'iche'", "Qach'abel" ], "name": "K’iche’", "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], "region": "GT", @@ -93912,13 +98026,110 @@ "tags": [ "quc-GT", "quc-Latn" ], "windows": "quc-Latn" }, + { + "full": "quc-Latn-GT-x-andres", + "iana": [ "K'iche'" ], + "iso639_3": "quc", + "localnames": [ "K'iche'", "Qach'abel" ], + "name": "K’iche’", + "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4815", + "script": "Latn", + "sldr": false, + "tag": "quc-x-andres", + "tags": [ "quc-GT-x-andres", "quc-Latn-x-andres" ], + "windows": "quc-Latn-x-andres" + }, + { + "full": "quc-Latn-GT-x-eastern", + "iana": [ "K'iche'" ], + "iso639_3": "quc", + "localnames": [ "K'iche'", "Qach'abel" ], + "name": "K’iche’", + "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "3473", + "script": "Latn", + "sldr": false, + "tag": "quc-x-eastern", + "tags": [ "quc-GT-x-eastern", "quc-Latn-x-eastern" ], + "windows": "quc-Latn-x-eastern" + }, + { + "full": "quc-Latn-GT-x-ecentral", + "iana": [ "K'iche'" ], + "iso639_3": "quc", + "localnames": [ "K'iche'", "Qach'abel" ], + "name": "K’iche’", + "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4796", + "script": "Latn", + "sldr": false, + "tag": "quc-x-ecentral", + "tags": [ "quc-GT-x-ecentral", "quc-Latn-x-ecentral" ], + "windows": "quc-Latn-x-ecentral" + }, + { + "full": "quc-Latn-GT-x-northern", + "iana": [ "K'iche'" ], + "iso639_3": "quc", + "localnames": [ "K'iche'", "Qach'abel" ], + "name": "K’iche’", + "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4968", + "script": "Latn", + "sldr": false, + "tag": "quc-x-northern", + "tags": [ "quc-GT-x-northern", "quc-Latn-x-northern" ], + "windows": "quc-Latn-x-northern" + }, + { + "full": "quc-Latn-GT-x-wcentral", + "iana": [ "K'iche'" ], + "iso639_3": "quc", + "localnames": [ "K'iche'", "Qach'abel" ], + "name": "K’iche’", + "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "4782", + "script": "Latn", + "sldr": false, + "tag": "quc-x-wcentral", + "tags": [ "quc-GT-x-wcentral", "quc-Latn-x-wcentral" ], + "windows": "quc-Latn-x-wcentral" + }, + { + "full": "quc-Latn-GT-x-western", + "iana": [ "K'iche'" ], + "iso639_3": "quc", + "localnames": [ "K'iche'", "Qach'abel" ], + "name": "K’iche’", + "names": [ "Central K’iche’", "Central Quiché", "Chiquel", "K'iche'", "Qach’abel", "Quiché" ], + "region": "GT", + "regionname": "Guatemala", + "rod": "27187", + "script": "Latn", + "sldr": false, + "tag": "quc-x-western", + "tags": [ "quc-GT-x-western", "quc-Latn-x-western" ], + "windows": "quc-Latn-x-western" + }, { "full": "qud-Latn-EC", "iana": [ "Calderón Highland Quichua" ], "iso639_3": "qud", - "localnames": [ "Kichwa Shimi", "Runa Shimi" ], + "localnames": [ "Kichwa Shimi" ], + "macrolang": "qu", "name": "Quichua, Calderón Highland", - "names": [ "Calderón Highland Quichua", "Calderón Quichua", "Cayambe Quichua", "Kichwa", "Kichwa Shimi", "Pichincha Quichua", "Quichua de la Sierra Cayambe", "Runa Shimi" ], + "names": [ "Calderón Highland Quichua", "Calderón Quichua", "Cayambe Quichua", "Kichwa", "Kichwa Shimi", "Pichincha Quichua", "Quichua de la Sierra Cayambe", "Salcedo Quichua" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -93933,8 +98144,9 @@ "iso639_3": "quf", "localname": "Linwaras", "localnames": [ "Linwaras" ], + "macrolang": "qu", "name": "Quechua, Lambayeque", - "names": [ "Ferreñafe", "Inkawasi-Kañaris Runashimi", "Lambayeque Quechua", "Linwaras", "Quechua Inkawasi-Kañaris", "Quechua de Lambayeque", "Quechua of Incahuasi-Cañaris", "Runa Shimi" ], + "names": [ "Ferreñafe", "Inkawasi-Kañaris Runashimi", "Lambayeque Quechua", "Linwaras", "Quechua Inkawasi-Kañaris", "Quechua de Lambayeque", "Quechua of Incahuasi-Cañaris" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -93947,9 +98159,10 @@ "full": "qug-Latn-EC", "iana": [ "Chimborazo Highland Quichua" ], "iso639_3": "qug", - "localnames": [ "Kichwa Shimi", "Runa Shimi" ], + "localnames": [ "Kichwa Shimi" ], + "macrolang": "qu", "name": "Quichua, Chimborazo Highland", - "names": [ "Chimborazo Highland Quichua", "Ecuadorean Quechua", "Kichwa", "Kichwa Shimi", "Kichwa de la Sierra", "Quichua de Chimborazo", "Quichua de la Sierra", "Quichua de la Sierra Chimborazo", "Runa Shimi" ], + "names": [ "Central Highland Quichua", "Chimborazo Highland Quichua", "Chimborazo Quichua", "Ecuadorean Quechua", "Kichwa", "Kichwa Shimi", "Kichwa de la Sierra", "Quichua Serrano Central", "Quichua de Chimborazo", "Quichua de la Sierra", "Quichua de la Sierra Chimborazo" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -93978,6 +98191,7 @@ "iana": [ "Chachapoyas Quechua" ], "iso639_3": "quk", "localnames": [ "Llakwash" ], + "macrolang": "qu", "name": "Quechua, Chachapoyas", "names": [ "Amazonas", "Amazonas Quechua", "Chachapoyas Quechua", "Llakwash" ], "region": "PE", @@ -93994,6 +98208,7 @@ "iso639_3": "qul", "localname": "Quechua", "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, North Bolivian", "names": [ "North Bolivian Quechua", "North La Paz Quechua", "Quechua" ], "region": "BO", @@ -94040,8 +98255,9 @@ "iso639_3": "qup", "localname": "Inka", "localnames": [ "Inka" ], + "macrolang": "qu", "name": "Quechua, Southern Pastaza", - "names": [ "Inga", "Inka", "Quechua del Pastaza", "Runa shimi", "Southern Pastaza Quechua" ], + "names": [ "Inga", "Inka", "Quechua del Pastaza", "Southern Pastaza Quechua" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94054,8 +98270,7 @@ "full": "quq-Latn-ES", "iana": [ "Quinqui" ], "iso639_3": "quq", - "name": "Quinqui", - "names": [ "Mercheros" ], + "name": "Mercheros", "region": "ES", "regionname": "Spain", "script": "Latn", @@ -94069,9 +98284,9 @@ "full": "qur-Latn-PE", "iana": [ "Yanahuanca Pasco Quechua" ], "iso639_3": "qur", - "localnames": [ "Runa Simi" ], + "macrolang": "qu", "name": "Quechua, Chaupihuaranga", - "names": [ "Daniel Carrion", "Runa Simi", "Yanahuanca Pasco Quechua" ], + "names": [ "Yanahuanca Pasco Quechua", "Yaru Quechua" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94085,6 +98300,7 @@ "iana": [ "Santiago del Estero Quichua" ], "iso639_3": "qus", "localnames": [ "Quichua" ], + "macrolang": "qu", "name": "Quichua, Santiago del Estero", "names": [ "Quichua", "Santiago del Estero Quichua", "Santiagueño Quichua" ], "region": "AR", @@ -94115,8 +98331,9 @@ "iana": [ "Tena Lowland Quichua" ], "iso639_3": "quw", "localnames": [ "Runa Shimi" ], + "macrolang": "qu", "name": "Quichua, Tena Lowland", - "names": [ "Napo Kichwa", "Quichua de Tena", "Quichua del Oriente", "Quijo", "Quixo", "Runa Shimi", "Tena Lowland Quichua", "Yumbo" ], + "names": [ "Napo Kichwa", "Quichua de Tena", "Quichua del Oriente", "Quijo", "Quixo", "Runa Shimi", "Tena Kichwa", "Tena Lowland Quichua", "Yumbo" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -94129,6 +98346,7 @@ "full": "qux-Latn-PE", "iana": [ "Yauyos Quechua" ], "iso639_3": "qux", + "macrolang": "qu", "name": "Quechua, Yauyos", "names": [ "Yauyos Quechua" ], "region": "PE", @@ -94145,6 +98363,7 @@ "iana": [ "Ayacucho Quechua" ], "iso639_3": "quy", "localnames": [ "Chanka runasimi", "Runasimi" ], + "macrolang": "qu", "name": "Quechua, Ayacucho", "names": [ "Ayacucho Quechua", "Chanca", "Chanka", "Chanka runasimi", "Runasimi" ], "region": "PE", @@ -94159,6 +98378,7 @@ "full": "qva-Latn-PE", "iana": [ "Ambo-Pasco Quechua" ], "iso639_3": "qva", + "macrolang": "qu", "name": "Quechua, Ambo-Pasco", "names": [ "Ambo-Pasco Quechua", "San Rafael-Huariaca Quechua" ], "region": "PE", @@ -94175,8 +98395,9 @@ "iso639_3": "qvc", "localname": "Kichwa", "localnames": [ "Kichwa" ], + "macrolang": "qu", "name": "Quechua, Cajamarca", - "names": [ "Cajamarca Quechua", "Kichwa", "Kichwa Lingwa", "Lingwa" ], + "names": [ "Cajamarca Quechua", "Kashamarka Runashimi", "Kashamarka qichwa", "Kichwa", "Kichwa Lingwa", "Lingwa" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94189,10 +98410,11 @@ "full": "qve-Latn-PE", "iana": [ "Eastern Apurímac Quechua" ], "iso639_3": "qve", - "localname": "Runa Simi", - "localnames": [ "Runasimi" ], + "localname": "Runasimi", + "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, Eastern Apurímac", - "names": [ "Apurímac Quechua", "Eastern Apurímac Quechua", "Quechua del Este de Apurímac", "Runasimi" ], + "names": [ "Apurímac Quechua", "Eastern Apurímac Quechua", "Quechua", "Quechua del Este de Apurímac" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94206,9 +98428,9 @@ "iana": [ "Huamalíes-Dos de Mayo Huánuco Quechua" ], "iso639_3": "qvh", "localname": "Runa Simi", - "localnames": [ "Runa Simi" ], + "macrolang": "qu", "name": "Quechua, Huamalíes-Dos de Mayo Huánuco", - "names": [ "Huamalíes-Dos de Mayo Huánuco Quechua", "Runa Simi" ], + "names": [ "Huamalíes-Dos de Mayo Huánuco Quechua" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94221,9 +98443,10 @@ "full": "qvi-Latn-EC", "iana": [ "Imbabura Highland Quichua" ], "iso639_3": "qvi", - "localnames": [ "Kichwa Shimi", "Runa Shimi" ], + "localnames": [ "Kichwa Shimi" ], + "macrolang": "qu", "name": "Quichua, Imbabura Highland", - "names": [ "Imbabura Highland Quichua", "Imbabura Quichua", "Kichwa", "Kichwa Shimi", "Kichwa de la Sierra", "Otavalo Quichua", "Quichua de la Sierra Imbabura", "Runa Shimi" ], + "names": [ "Imbabura Highland Quichua", "Imbabura Quichua", "Kichwa", "Kichwa Shimi", "Kichwa de la Sierra", "Otavalo Quichua", "Quichua de la Sierra Imbabura" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -94236,9 +98459,9 @@ "full": "qvj-Latn-EC", "iana": [ "Loja Highland Quichua" ], "iso639_3": "qvj", - "localnames": [ "Runa Shimi" ], + "macrolang": "qu", "name": "Quichua, Loja Highland", - "names": [ "Loja Highland Quichua", "Loja Quichua", "Runa Shimi", "Saraguro Quichua" ], + "names": [ "Azuay Quichua", "Loja Highland Quichua", "Loja Quichua", "Saraguro Quichua" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -94251,6 +98474,7 @@ "full": "qvl-Latn-PE", "iana": [ "Cajatambo North Lima Quechua" ], "iso639_3": "qvl", + "macrolang": "qu", "name": "Quechua, Cajatambo North Lima", "names": [ "Cajatambo North Lima Quechua" ], "region": "PE", @@ -94267,9 +98491,10 @@ "iana": [ "Margos-Yarowilca-Lauricocha Quechua" ], "iso639_3": "qvm", "localname": "Quechua", - "localnames": [ "Quechua", "Runa Shimi" ], + "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, Margos-Yarowilca-Lauricocha", - "names": [ "Dos de Mayo Quechua", "Ketchwa", "Margos-Yarowilca-Lauricocha Quechua", "Quechua", "Quechua Margos", "Quechua de Margos", "Quechua de Margos-Cauri-Chavinillo Huanuco", "Runa Shimi" ], + "names": [ "Dos de Mayo Quechua", "Ketchwa", "Margos-Yarowilca-Lauricocha Quechua", "Quechua", "Quechua Margos", "Quechua de Margos", "Quechua de Margos-Cauri-Chavinillo Huanuco" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94282,9 +98507,11 @@ "full": "qvn-Latn-PE", "iana": [ "North Junín Quechua" ], "iso639_3": "qvn", + "localname": "Quechua", "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, North Junín", - "names": [ "Junín Quechua", "North Junín Quechua", "Quechua", "Quechua del Norte de Junín", "Runa shimi", "Tarma Quechua", "Tarma-Junín Quechua" ], + "names": [ "Junín Quechua", "North Junín Quechua", "Quechua", "Quechua del Norte de Junín", "Tarma Quechua", "Tarma-Junín Quechua" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94297,14 +98524,15 @@ "full": "qvo-Latn-PE", "iana": [ "Napo Lowland Quechua" ], "iso639_3": "qvo", - "localnames": [ "Napo Kichwa shimi", "Runa Shimi" ], + "localnames": [ "Napo Kichwa shimi" ], + "macrolang": "qu", "name": "Quichua, Napo", - "names": [ "Ecuador Quichua", "Ingano", "Kicho", "Kichua", "Lowland Napo Quechua", "Lowland Napo Quichua", "Napo", "Napo Kichua", "Napo Kichwa", "Napo Kichwa shimi", "Napo Lowland Quechua", "Napo Lowland Quichua", "Napo Quichua", "Quichua del Napo", "Quichua del Oriente", "Quichua, Napo Lowland", "Runa Shimi", "Santa Rosa Quechua", "Santarrosino", "Yumbo" ], + "names": [ "Ecuador Quichua", "Ingano", "Kicho", "Kichua", "Kichwa", "Kichwa Shimi", "Lowland Napo Quechua", "Lowland Napo Quichua", "Napo", "Napo Kichua", "Napo Kichwa", "Napo Kichwa shimi", "Napo Lowland Quechua", "Napo Lowland Quichua", "Napo Quichua", "Quichua del Napo", "Quichua del Oriente", "Quichua, Napo Lowland", "Runa Shimi", "Santa Rosa Quechua", "Santarrosino", "Yumbo" ], "region": "PE", "regionname": "Peru", "regions": [ "CO", "EC" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "qvo", "tags": [ "qvo-Latn", "qvo-PE" ], "windows": "qvo-Latn" @@ -94313,6 +98541,7 @@ "full": "qvp-Latn-PE", "iana": [ "Pacaraos Quechua" ], "iso639_3": "qvp", + "macrolang": "qu", "name": "Quechua, Pacaraos", "names": [ "Pacaraos Quechua" ], "region": "PE", @@ -94328,10 +98557,11 @@ "full": "qvs-Latn-PE", "iana": [ "San Martín Quechua" ], "iso639_3": "qvs", - "localname": "Llakwash Quechua", - "localnames": [ "Llakwash", "San Martin Kechwa" ], + "localname": "Llakwash", + "localnames": [ "Kechwa", "San Martin Kechwa" ], + "macrolang": "qu", "name": "Quechua, San Martín", - "names": [ "Kechwa de San Martin", "Lama", "Lamano", "Lamas Kichwa", "Lamas Quechua", "Lamista", "Lamisto", "Llakwash", "Llakwash Quechua", "Llakwash Runashimi", "Motilón", "San Martin Kechwa", "San Martín Quechua", "Ucayali" ], + "names": [ "Kechwa", "Kechwa de San Martin", "Lama", "Lamano", "Lamas Kichwa", "Lamas Quechua", "Lamista", "Lamisto", "Llakwash", "Llakwash Quechua", "Llakwash Runashimi", "Motilón", "San Martin Kechwa", "San Martín Quechua", "Ucayali" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94345,9 +98575,10 @@ "iana": [ "Huaylla Wanca Quechua" ], "iso639_3": "qvw", "localname": "Runa Simi", - "localnames": [ "Runa Simi", "Wanca Nunashimi" ], + "localnames": [ "Kichwa", "Wanca Nunashimi" ], + "macrolang": "qu", "name": "Quechua, Huaylla Wanca", - "names": [ "Huanca Huaylla Quechua", "Huaylla Wanca Quechua", "Quechua Huanca", "Quechua Wanca", "Quechua Wanka", "Runa Simi", "Southern Huancayo Quechua", "Wanca", "Wanca Nunashimi", "Wanka Quechua" ], + "names": [ "Huanca Huaylla Quechua", "Huaylla Wanca Quechua", "Kichwa", "Quechua Huanca", "Quechua Wanca", "Quechua Wanka", "Southern Huancayo Quechua", "Wanca", "Wanca Nunashimi", "Wanka Quechua" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94375,9 +98606,9 @@ "iana": [ "Northern Pastaza Quichua" ], "iso639_3": "qvz", "localname": "Runa Shimi", - "localnames": [ "Runa Shimi" ], + "macrolang": "qu", "name": "Quichua, Northern Pastaza", - "names": [ "Alama", "Bobonaza", "Bobonaza Quichua", "Canelos Quichua", "Northern Pastaza Quichua", "Pastaza Quichua", "Quichua del Oriente", "Runa Shimi", "Sarayacu Quichua", "Tigre Quechua" ], + "names": [ "Alama", "Bobonaza", "Bobonaza Quichua", "Canelos Quichua", "Northern Pastaza Quichua", "Pastaza Quichua", "Quichua del Oriente", "Sarayacu Quichua", "Tigre Quechua" ], "region": "EC", "regionname": "Ecuador", "regions": [ "PE" ], @@ -94392,8 +98623,9 @@ "iana": [ "Corongo Ancash Quechua" ], "iso639_3": "qwa", "localnames": [ "Llaqwash" ], + "macrolang": "qu", "name": "Quechua, Corongo Ancash", - "names": [ "Corongo Ancash Quechua", "Corongo Quechua", "Llaqwash", "Quechua de Corongo", "Runa Simi" ], + "names": [ "Corongo Ancash Quechua", "Corongo Quechua", "Llaqwash", "Quechua de Corongo" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94406,6 +98638,7 @@ "full": "qwc-Latn-PE", "iana": [ "Classical Quechua" ], "iso639_3": "qwc", + "macrolang": "qu", "name": "Classical Quechua", "region": "PE", "regionname": "Peru", @@ -94420,9 +98653,11 @@ "full": "qwh-Latn-PE", "iana": [ "Huaylas Ancash Quechua" ], "iso639_3": "qwh", - "localname": "Quechua", + "localname": "Anqas Qichwa", + "localnames": [ "Anqas Qichwa", "Qichwa" ], + "macrolang": "qu", "name": "Quechua, Huaylas Ancash", - "names": [ "Huaylas Ancash Quechua", "Qichwa", "Runa Shimi" ], + "names": [ "Anqas Qichwa", "Huaylas Ancash Quechua", "Qichwa" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94431,25 +98666,53 @@ "tags": [ "qwh-Latn", "qwh-PE" ], "windows": "qwh-Latn" }, + { + "full": "qwm-Latn-RU", + "iana": [ "Kuman (Russia)" ], + "iso639_3": "qwm", + "name": "Kuman (Russia)", + "region": "RU", + "regionname": "Russian Federation", + "script": "Latn", + "sldr": false, + "tag": "qwm", + "tags": [ "qwm-Latn", "qwm-RU" ], + "windows": "qwm-Latn" + }, { "full": "qwm-Cyrl-RU", "iana": [ "Kuman (Russia)" ], "iso639_3": "qwm", "name": "Kuman (Russia)", + "nophonvars": true, "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", "sldr": false, - "tag": "qwm", - "tags": [ "qwm-Cyrl", "qwm-RU" ], + "tag": "qwm-Cyrl", "unwritten": true, "windows": "qwm-Cyrl" }, + { + "full": "qwm-Runr-RU", + "iana": [ "Kuman (Russia)" ], + "iso639_3": "qwm", + "name": "Kuman (Russia)", + "nophonvars": true, + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "001" ], + "script": "Runr", + "sldr": false, + "tag": "qwm-Runr", + "windows": "qwm-Runr" + }, { "full": "qws-Latn-PE", "iana": [ "Sihuas Ancash Quechua" ], "iso639_3": "qws", "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, Sihuas Ancash", "names": [ "Quechua", "Quechua de Sihuas", "Sihuas Ancash Quechua" ], "region": "PE", @@ -94461,23 +98724,24 @@ "windows": "qws-Latn" }, { - "full": "qwt-Zyyy-US", + "full": "qwt-Latn-US", "iana": [ "Kwalhioqua-Tlatskanai" ], "iso639_3": "qwt", "name": "Kwalhioqua-Tlatskanai", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "qwt", - "tags": [ "qwt-US", "qwt-Zyyy" ], + "tags": [ "qwt-Latn", "qwt-US" ], "unwritten": true, - "windows": "qwt-Zyyy" + "windows": "qwt-Latn" }, { "full": "qxa-Latn-PE", "iana": [ "Chiquián Ancash Quechua" ], "iso639_3": "qxa", + "macrolang": "qu", "name": "Quechua, Chiquián", "names": [ "Ancash Quechua", "Chiquián Ancash Quechua" ], "region": "PE", @@ -94493,6 +98757,7 @@ "full": "qxc-Latn-PE", "iana": [ "Chincha Quechua" ], "iso639_3": "qxc", + "macrolang": "qu", "name": "Quechua, Chincha", "names": [ "Chincha Quechua" ], "region": "PE", @@ -94508,10 +98773,11 @@ "full": "qxh-Latn-PE", "iana": [ "Panao Huánuco Quechua" ], "iso639_3": "qxh", - "localname": "Panao runacuna", - "localnames": [ "Runashimi" ], + "localname": "Runa shimi", + "localnames": [ "Panao Quechua", "Runa shimi" ], + "macrolang": "qu", "name": "Quechua, Panao", - "names": [ "Pachitea Quechua", "Panao Huánuco Quechua", "Panao runacuna", "Quechua de Pachitea", "Runashimi" ], + "names": [ "Pachitea Quechua", "Panao Huánuco Quechua", "Panao Quechua", "Panao runacuna", "Quechua de Pachitea", "Runa shimi" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94524,10 +98790,11 @@ "full": "qxl-Latn-EC", "iana": [ "Salasaca Highland Quichua" ], "iso639_3": "qxl", - "localname": "Salasakaguna", - "localnames": [ "Runa Shimibi", "Salasaca Shimibi" ], + "localname": "Runa Shimibi", + "localnames": [ "Salasaca Shimibi", "Salasaka shimibi" ], + "macrolang": "qu", "name": "Quichua, Salasaca Highland", - "names": [ "Kichwa", "Runa Shimibi", "Salasaca Highland Quichua", "Salasaca Quichua", "Salasaca Shimibi", "Tungurahua Highland Quichua", "Tungurahua Quichua" ], + "names": [ "Kichwa", "Runa Shimibi", "Runa shimi", "Salasaca Highland Quichua", "Salasaca Quichua", "Salasaca Shimibi", "Salasaka", "Salasaka Quichua", "Salasaka shimibi", "Salasakamunda Kichwa", "Tungurahua Highland Quichua", "Tungurahua Quichua" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -94540,10 +98807,11 @@ "full": "qxn-Latn-PE", "iana": [ "Northern Conchucos Ancash Quechua" ], "iso639_3": "qxn", - "localname": "Huaylay", + "localname": "Quechua", "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, Northern Conchucos Ancash", - "names": [ "Conchucos Quechua", "Northern Conchucos Ancash Quechua", "Northern Conchucos Quechua", "Quechua", "Quechua de Ancash", "Quechua de Pomabamba (Ancash)", "Quechua del Norte del Callejon de Conchucos", "Runa Simi" ], + "names": [ "Conchucos Quechua", "Northern Conchucos Ancash Quechua", "Northern Conchucos Quechua", "Quechua", "Quechua de Ancash", "Quechua de Pomabamba (Ancash)", "Quechua del Norte del Callejon de Conchucos" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94558,8 +98826,9 @@ "iso639_3": "qxo", "localname": "Quechua", "localnames": [ "Quechua" ], + "macrolang": "qu", "name": "Quechua, Southern Conchucos", - "names": [ "Conchucos Quechua", "Quechua", "Quechua de Conchucos", "Quechua del Callejón de Huaylas", "Quechua del sur de Conchucos", "Quechwa", "Runa simi", "South Conchucos Quechua", "Southern Conchucos Ancash Quechua" ], + "names": [ "Conchucos Quechua", "Quechua", "Quechua de Conchucos", "Quechua del Callejón de Huaylas", "Quechua del sur de Conchucos", "Quechwa", "South Conchucos Quechua", "Southern Conchucos Ancash Quechua" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94572,9 +98841,10 @@ "full": "qxp-Latn-PE", "iana": [ "Puno Quechua" ], "iso639_3": "qxp", - "localnames": [ "Runa Simi" ], + "localnames": [ "Qhichwa simi" ], + "macrolang": "qu", "name": "Quechua, Puno", - "names": [ "Puno Quechua", "Quechua Collao", "Quechua Qollaw", "Runa Simi" ], + "names": [ "Colla Quechua", "Puno Quechua", "Qhichwa simi", "Qhishwa simi", "Quechua Qollaw", "Quechua of Collao" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94603,9 +98873,10 @@ "full": "qxr-Latn-EC", "iana": [ "Cañar Highland Quichua" ], "iso639_3": "qxr", - "localnames": [ "Kichwa Shimi", "Runa Shimi" ], + "localnames": [ "Kichwa Shimi" ], + "macrolang": "qu", "name": "Quichua, Cañar Highland", - "names": [ "Cañar Highland Quichua", "Kichwa", "Kichwa Shimi", "Quichua de la provincia de Cañar", "Runa Shimi" ], + "names": [ "Cañar Highland Quichua", "Cañar Quichua", "Kichwa", "Kichwa Shimi", "Quichua de la provincia de Cañar" ], "region": "EC", "regionname": "Ecuador", "script": "Latn", @@ -94632,6 +98903,7 @@ "full": "qxt-Latn-PE", "iana": [ "Santa Ana de Tusi Pasco Quechua" ], "iso639_3": "qxt", + "macrolang": "qu", "name": "Quechua, Santa Ana de Tusi Pasco", "names": [ "Santa Ana de Tusi Pasco Quechua" ], "region": "PE", @@ -94647,9 +98919,9 @@ "full": "qxu-Latn-PE", "iana": [ "Arequipa-La Unión Quechua" ], "iso639_3": "qxu", - "localnames": [ "Runasimi" ], + "macrolang": "qu", "name": "Quechua, Arequipa-La Unión", - "names": [ "Arequipa Quechua", "Arequipa-La Unión Quechua", "Cotahuasi Quechua", "Quechua de Arequipa", "Quechua de La Unión", "Runasimi" ], + "names": [ "Arequipa Quechua", "Arequipa-La Unión Quechua", "Cotahuasi Quechua", "Quechua de Arequipa", "Quechua de La Unión" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -94662,6 +98934,7 @@ "full": "qxw-Latn-PE", "iana": [ "Jauja Wanca Quechua" ], "iso639_3": "qxw", + "macrolang": "qu", "name": "Quechua, Jauja Wanca", "names": [ "Huanca Jauja Quechua", "Jauja Wanca Quechua", "Quechua shausha huanca", "Shausha Wanka Quechua" ], "region": "PE", @@ -94726,18 +98999,19 @@ "windows": "qya-Teng" }, { - "full": "qyp-Zyyy-US", + "full": "qyp-Latn-US", "iana": [ "Quiripi" ], "iso639_3": "qyp", "name": "Quiripi", + "names": [ "Mattabesic", "Quiripi-Naugatuck", "Quiripi-Unquachog", "Wampano" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "qyp", - "tags": [ "qyp-US", "qyp-Zyyy" ], + "tags": [ "qyp-Latn", "qyp-US" ], "unwritten": true, - "windows": "qyp-Zyyy" + "windows": "qyp-Latn" }, { "full": "raa-Deva-NP", @@ -94757,6 +99031,8 @@ "full": "rab-Deva-NP", "iana": [ "Camling" ], "iso639_3": "rab", + "latnnames": [ "Caamliṅa" ], + "localnames": [ "चाम्लिङ" ], "name": "Chamling", "names": [ "Camling", "Chamlinge Rai", "Rodong" ], "region": "NP", @@ -94817,6 +99093,7 @@ "iana": [ "Logooli", "Lulogooli" ], "iso639_3": "rag", "localnames": [ "Lulogooli" ], + "macrolang": "luy", "name": "Lulogooli", "names": [ "Llogole", "Llugule", "Logooli", "Lugooli", "Luragoli", "Maragoli", "Maragooli", "Ragoli", "Uluragooli" ], "region": "KE", @@ -94878,13 +99155,14 @@ "iana": [ "Rajasthani" ], "iso639_3": "raj", "latnnames": [ "Malvi" ], + "localname": "राजस्थानी", "localnames": [ "मालवी" ], - "name": "Malvi", - "names": [ "Malavi", "Mallow", "Malwada", "Malwi", "Rajasthani" ], + "name": "Rajasthani", + "names": [ "Malavi", "Mallow", "Malvi", "Malwada", "Malwi" ], "region": "IN", "regionname": "India", "script": "Deva", - "sldr": false, + "sldr": true, "tag": "raj", "tags": [ "mup", "mup-Deva", "mup-Deva-IN", "mup-IN", "raj-Deva", "raj-IN" ], "windows": "raj-Deva" @@ -94939,7 +99217,7 @@ "region": "BR", "regionname": "Brazil", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ram", "tags": [ "ram-BR", "ram-Latn" ], "windows": "ram-Latn" @@ -95010,7 +99288,7 @@ "iso639_3": "rar", "localnames": [ "Te Reo Maori" ], "name": "Cook Islands Maori", - "names": [ "Cook Island", "Cook Islands Māori", "Kuki Airani", "Maori", "Māori Kūki ’Āirani", "Rarotongan", "Te Reo Maori" ], + "names": [ "Cook Island", "Kuki Airani", "Maori", "Māori Kūki ’Āirani", "Rarotongan", "Te Reo Maori" ], "region": "CK", "regionname": "Cook Islands", "regions": [ "AU", "NZ", "PF", "US" ], @@ -95081,9 +99359,9 @@ "full": "raw-Latn-MM", "iana": [ "Rawang" ], "iso639_3": "raw", - "localnames": [ "Nung", "Rawang" ], + "localnames": [ "Rvwang" ], "name": "Rawang", - "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Taron" ], + "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rvwang", "Rvwangka", "Taron" ], "region": "MM", "regionname": "Myanmar", "regions": [ "CN", "IN" ], @@ -95097,9 +99375,9 @@ "full": "raw-Latn-MM-x-daruzerw", "iana": [ "Rawang, Daru-Zerwang" ], "iso639_3": "raw", - "localnames": [ "Nung", "Rawang" ], + "localnames": [ "Rvwang" ], "name": "Rawang", - "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Daru-Zerwang", "Taron" ], + "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Daru-Zerwang", "Rvwang", "Rvwangka", "Taron" ], "region": "MM", "regionname": "Myanmar", "regions": [ "CN", "IN" ], @@ -95113,9 +99391,9 @@ "full": "raw-Latn-MM-x-longmin", "iana": [ "Rawang, Longmi North Mashang Dangraq" ], "iso639_3": "raw", - "localnames": [ "Nung", "Rawang" ], + "localnames": [ "Rvwang" ], "name": "Rawang", - "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Longmi North Mashang Dangraq", "Taron" ], + "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Longmi North Mashang Dangraq", "Rvwang", "Rvwangka", "Taron" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -95129,9 +99407,9 @@ "full": "raw-Latn-MM-x-longmis", "iana": [ "Rawang, Longmi South" ], "iso639_3": "raw", - "localnames": [ "Nung", "Rawang" ], + "localnames": [ "Rvwang" ], "name": "Rawang", - "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Longmi South", "Taron" ], + "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Longmi South", "Rvwang", "Rvwangka", "Taron" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -95145,9 +99423,9 @@ "full": "raw-Latn-MM-x-tangsare", "iana": [ "Rawang, Tangsarr East - Marangdong, Jigong" ], "iso639_3": "raw", - "localnames": [ "Nung", "Rawang" ], + "localnames": [ "Rvwang" ], "name": "Rawang", - "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Tangsarr East - Marangdong, Jigong", "Taron" ], + "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Tangsarr East - Marangdong, Jigong", "Rvwang", "Rvwangka", "Taron" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -95161,9 +99439,9 @@ "full": "raw-Latn-MM-x-tangsarw", "iana": [ "Rawang, Tangsarr West Dakhum" ], "iso639_3": "raw", - "localnames": [ "Nung", "Rawang" ], + "localnames": [ "Rvwang" ], "name": "Rawang", - "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Tangsarr West Dakhum", "Taron" ], + "names": [ "Chiutse", "Ch’opa", "Ganung-Rawang", "Kiutze", "Krangku", "Nung", "Nung Rawang", "Qiuze", "Rawang, Tangsarr West Dakhum", "Rvwang", "Rvwangka", "Taron" ], "region": "MM", "regionname": "Myanmar", "regions": [ "IN" ], @@ -95222,7 +99500,7 @@ "iana": [ "Rumai Palaung" ], "iso639_3": "rbb", "name": "Palaung, Rumai", - "names": [ "Humai", "Ngwe Palaung", "Rumai", "Rumai Humai", "Rumai Palaung", "Shan Rumai", "Silver Palaung", "Tai Rumai", "Ta’ang", "Ta’ang Rumai" ], + "names": [ "Black De’ang", "Humai", "Ngwe Palaung", "Ru-Mai Palaung", "Rumai", "Rumai Humai", "Rumai Palaung", "Shan Rumai", "Silver Palaung", "Tai Rumai", "Ta’ang", "Ta’ang Rumai" ], "region": "MM", "regionname": "Myanmar", "regions": [ "CN" ], @@ -95236,6 +99514,7 @@ "full": "rbk-Latn-PH", "iana": [ "Northern Bontok" ], "iso639_3": "rbk", + "macrolang": "bkb", "name": "Bontok, Northern", "names": [ "Bontoc", "Central Bontoc", "Northern Bontoc", "Northern Bontok" ], "region": "PH", @@ -95251,6 +99530,7 @@ "full": "rbl-Latn-PH", "iana": [ "Miraya Bikol" ], "iso639_3": "rbl", + "macrolang": "bik", "name": "Bikol, Miraya", "names": [ "Bikol", "Daraga", "Miraya", "Miraya Bikol" ], "region": "PH", @@ -95281,7 +99561,9 @@ "full": "rcf-Latn-RE", "iana": [ "Réunion Creole French" ], "iso639_3": "rcf", - "name": "Réunion Creole French", + "localnames": [ "Kréol rénioné" ], + "name": "Réunion French Creole", + "names": [ "Créole réunionnais", "Kréol rénioné", "Réunion Creole French" ], "region": "RE", "regionname": "Réunion", "regions": [ "MG" ], @@ -95363,18 +99645,32 @@ "windows": "reg-Latn" }, { - "full": "rei-Zyyy-IN", + "full": "rei-Orya-IN", "iana": [ "Reli" ], "iso639_3": "rei", "name": "Reli", "names": [ "Relli" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Orya", "sldr": false, "tag": "rei", - "tags": [ "rei-IN", "rei-Zyyy" ], - "windows": "rei-Zyyy" + "tags": [ "rei-IN", "rei-Orya" ], + "windows": "rei-Orya" + }, + { + "full": "rei-Telu-IN", + "iana": [ "Reli" ], + "iso639_3": "rei", + "name": "Reli", + "names": [ "Relli" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Telu", + "sldr": false, + "tag": "rei-Telu", + "windows": "rei-Telu" }, { "full": "rej-Latn-ID", @@ -95408,8 +99704,10 @@ "full": "rel-Latn-KE", "iana": [ "Rendille" ], "iso639_3": "rel", + "localname": "Afi Renꞌdille", + "localnames": [ "Afi Renꞌdille" ], "name": "Rendille", - "names": [ "Randile", "Rendile" ], + "names": [ "Afi Renꞌdille", "Randile", "Rendile" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -95437,8 +99735,9 @@ "full": "ren-Latn-VN", "iana": [ "Rengao" ], "iso639_3": "ren", + "localnames": [ "Rơngao" ], "name": "Rengao", - "names": [ "Ro-Ngao" ], + "names": [ "Ro-Ngao", "Rơngao" ], "region": "VN", "regionname": "Viet Nam", "script": "Latn", @@ -95593,13 +99892,13 @@ "sldr": false, "tag": "rgs", "tags": [ "rgs-Latn", "rgs-VN" ], - "unwritten": true, "windows": "rgs-Latn" }, { "full": "rgu-Latn-ID", "iana": [ "Ringgou" ], "iso639_3": "rgu", + "localnames": [ "Rikou" ], "name": "Rikou", "names": [ "Eastern Rote", "Ringgou", "Rote", "Rote Timur", "Roti", "Rotinese" ], "region": "ID", @@ -95610,6 +99909,21 @@ "tags": [ "rgu-ID", "rgu-Latn" ], "windows": "rgu-Latn" }, + { + "full": "rhg-Rohg-MM", + "iana": [ "Rohingya" ], + "iso639_3": "rhg", + "name": "Rohingya", + "names": [ "Rohinga", "Rohinja", "Ruwainggya" ], + "region": "MM", + "regionname": "Myanmar", + "regions": [ "BD", "CN", "MY", "PK", "SA", "TH" ], + "script": "Rohg", + "sldr": false, + "tag": "rhg", + "tags": [ "rhg-MM", "rhg-Rohg" ], + "windows": "rhg-Rohg" + }, { "full": "rhg-Arab-MM", "iana": [ "Rohingya" ], @@ -95640,21 +99954,6 @@ "tag": "rhg-Latn", "windows": "rhg-Latn" }, - { - "full": "rhg-Rohg-MM", - "iana": [ "Rohingya" ], - "iso639_3": "rhg", - "name": "Rohingya", - "names": [ "Rohinga", "Rohinja", "Ruwainggya" ], - "nophonvars": true, - "region": "MM", - "regionname": "Myanmar", - "regions": [ "BD", "CN", "MY", "PK", "SA", "TH" ], - "script": "Rohg", - "sldr": false, - "tag": "rhg-Rohg", - "windows": "rhg-Rohg" - }, { "full": "rhp-Latn-PG", "iana": [ "Yahang" ], @@ -95686,6 +99985,19 @@ "tags": [ "ria-IN", "ria-Latn" ], "windows": "ria-Latn" }, + { + "full": "rib-Zxxx-CR", + "iana": [ "Bribri Sign Language" ], + "iso639_3": "rib", + "name": "Bribri Sign Language", + "region": "CR", + "regionname": "Costa Rica", + "script": "Zxxx", + "sldr": false, + "tag": "rib", + "tags": [ "rib-CR", "rib-Zxxx", "sgn-rib", "sgn-rib-CR" ], + "windows": "rib-Zxxx" + }, { "full": "rie-Zyyy-XX", "iana": [ "Rien" ], @@ -95699,21 +100011,20 @@ "windows": "rie-Zyyy" }, { - "full": "rif-Tfng-MA", + "full": "rif-Latn-MA", "iana": [ "Tarifit" ], "iso639_3": "rif", - "latnnames": [ "Tarifit" ], - "localnames": [ "ⵜⴰⵔⵉⴼⵉⵜ" ], + "localnames": [ "Tarifit" ], "name": "Tarifit", "names": [ "Arrif", "Fifia", "Northern Shilha", "Rif", "Rif Berber", "Rifeño", "Riff", "Rifi", "Rifia", "Rifiya", "Ruafa", "Shilha", "Tamazight", "Tamazight n Arrif", "Tarifiyt", "Tarifyt Berber", "Tirifie", "Tmazight", "Tmaziɣt" ], "region": "MA", "regionname": "Morocco", "regions": [ "BE", "DE", "DZ", "ES", "NL" ], - "script": "Tfng", - "sldr": false, + "script": "Latn", + "sldr": true, "tag": "rif", - "tags": [ "rif-MA", "rif-Tfng" ], - "windows": "rif-Tfng" + "tags": [ "rif-Latn", "rif-MA" ], + "windows": "rif-Latn" }, { "full": "rif-Arab-MA", @@ -95733,34 +100044,21 @@ "windows": "rif-Arab" }, { - "full": "rif-Latn-MA", + "full": "rif-Tfng-MA", "iana": [ "Tarifit" ], "iso639_3": "rif", - "localnames": [ "Tarifit" ], + "latnnames": [ "Tarifit" ], + "localnames": [ "ⵜⴰⵔⵉⴼⵉⵜ" ], "name": "Tarifit", "names": [ "Arrif", "Fifia", "Northern Shilha", "Rif", "Rif Berber", "Rifeño", "Riff", "Rifi", "Rifia", "Rifiya", "Ruafa", "Shilha", "Tamazight", "Tamazight n Arrif", "Tarifiyt", "Tarifyt Berber", "Tirifie", "Tmazight", "Tmaziɣt" ], + "nophonvars": true, "region": "MA", "regionname": "Morocco", - "regions": [ "BE", "DE", "DZ", "ES" ], - "script": "Latn", - "sldr": false, - "tag": "rif-Latn", - "windows": "rif-Latn" - }, - { - "full": "rif-Latn-NL", - "iana": [ "Tarifit" ], - "iso639_3": "rif", - "localnames": [ "Tarifit" ], - "name": "Tarifit", - "names": [ "Arrif", "Fifia", "Northern Shilha", "Rif", "Rif Berber", "Rifeño", "Riff", "Rifi", "Rifia", "Rifiya", "Ruafa", "Shilha", "Tamazight", "Tamazight n Arrif", "Tarifiyt", "Tarifyt Berber", "Tirifie", "Tmazight", "Tmaziɣt" ], - "region": "NL", - "regionname": "Netherlands", - "regions": [ "BE", "DE", "DZ", "ES" ], - "script": "Latn", + "regions": [ "BE", "DE", "DZ", "ES", "NL" ], + "script": "Tfng", "sldr": false, - "tag": "rif-NL", - "windows": "rif-Latn-NL" + "tag": "rif-Tfng", + "windows": "rif-Tfng" }, { "full": "ril-Latn-MM", @@ -95798,7 +100096,7 @@ "iana": [ "Nungu" ], "iso639_3": "rin", "name": "Nungu", - "names": [ "Lindiri", "Rendre", "Rindiri", "Rindre" ], + "names": [ "Lindiri", "Rendre", "Rindiri", "Rindre", "Wamba" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -95888,7 +100186,7 @@ "latnnames": [ "Rājbanshi" ], "localnames": [ "राजबंशी" ], "name": "Rajbanshi", - "names": [ "Gangai", "Koch", "Koche", "Rajbangsi", "Rajbansi", "Tajpuria" ], + "names": [ "Deshi Bhasa", "Gangai", "Koch", "Koche", "Rajbangsi", "Rajbansi", "Tajpuria" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -95898,23 +100196,24 @@ "windows": "rjs-Deva" }, { - "full": "rka-Zyyy-KH", + "full": "rka-Khmr-KH", "iana": [ "Kraol" ], "iso639_3": "rka", "name": "Kraol", "region": "KH", "regionname": "Cambodia", - "script": "Zyyy", + "script": "Khmr", "sldr": false, "tag": "rka", - "tags": [ "rka-KH", "rka-Zyyy" ], + "tags": [ "rka-KH", "rka-Khmr" ], "unwritten": true, - "windows": "rka-Zyyy" + "windows": "rka-Khmr" }, { "full": "rkb-Latn-BR", "iana": [ "Rikbaktsa" ], "iso639_3": "rkb", + "localname": "Rikbaktsa", "localnames": [ "Rikbaktsa" ], "name": "Rikbaktsa", "names": [ "Aripaktsá", "Canoeiro", "Erigbaagtsá", "Erigpactsá", "Erigpaktsá", "Erikbaktsá", "Erikbatsá", "Erikpatsá", "Orelhas de Pau", "Rikpakcá", "Rikpaktsá" ], @@ -95978,8 +100277,10 @@ "full": "rkt-Beng-BD", "iana": [ "Rangpuri", "Kamta" ], "iso639_3": "rkt", + "latnnames": [ "Rangpuri" ], + "localnames": [ "রংপুরী" ], "name": "Rangpuri", - "names": [ "Anchalit Bangla", "Bahe Bangla", "Kamta", "Kamtapuri", "Koch Rajbanshi", "Polia", "Rajbangsi", "Rajbanshi", "Rajbansi", "Rajbongshi" ], + "names": [ "Anchalit Bangla", "Bahe", "Bahe Bangla", "Deshi Bhasa", "Dhekia", "Dhekri", "Goalparia", "Kamata", "Kamta", "Kamtapuri", "Koch Rajbanshi", "Polia", "Rajbangsi", "Rajbanshi", "Rajbansi", "Rajbongshi" ], "region": "BD", "regionname": "Bangladesh", "regions": [ "IN" ], @@ -96057,8 +100358,9 @@ "iana": [ "Carpathian Romani" ], "iso639_3": "rmc", "localnames": [ "Romanes" ], + "macrolang": "rom", "name": "Romani, Carpathian", - "names": [ "Bashaldo", "Burgenland Romani", "Carpathian Romani", "Central Romani", "Hungarian-Slovak Romani", "Karpacki Roma", "Romanes", "Romungro", "South Polish Romani", "Sárvika Romá", "Ungrike Romá" ], + "names": [ "Bashaldo", "Burgenland Romani", "Carpathian Romani", "Central Romani", "Hungarian-Slovak Romani", "Karpacki Roma", "Romanes", "Romani", "Romungro", "Servika Roma", "Slovak Roma", "South Polish Romani", "Sárvika Romá", "Ungrike Romá" ], "region": "SK", "regionname": "Slovakia", "regions": [ "AT", "CZ", "HU", "PL", "RO", "SI", "UA" ], @@ -96104,6 +100406,7 @@ "iana": [ "Kalo Finnish Romani" ], "iso639_3": "rmf", "localnames": [ "Kaalengo tšibbaha" ], + "macrolang": "rom", "name": "Romani, Kalo Finnish", "names": [ "Fíntika Rómma", "Kaalengo tšibbaha", "Kaalo", "Kalo Finnish Romani", "Kalé", "Roma" ], "region": "FI", @@ -96116,18 +100419,18 @@ "windows": "rmf-Latn" }, { - "full": "rmg-Zyyy-NO", + "full": "rmg-Latn-NO", "iana": [ "Traveller Norwegian" ], "iso639_3": "rmg", "name": "Norwegian, Traveller", "names": [ "Norwegian Traveller", "Rodi", "Scandoromani", "Traveller Norwegian" ], "region": "NO", "regionname": "Norway", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "rmg", - "tags": [ "rmg-NO", "rmg-Zyyy" ], - "windows": "rmg-Zyyy" + "tags": [ "rmg-Latn", "rmg-NO" ], + "windows": "rmg-Latn" }, { "full": "rmh-Latn-ID", @@ -96144,27 +100447,26 @@ "windows": "rmh-Latn" }, { - "full": "rmi-Zyyy-AM", + "full": "rmi-Armn-AM", "iana": [ "Lomavren" ], "iso639_3": "rmi", "name": "Lomavren", - "names": [ "Armenian Bosa", "Armenian Bosha", "Arnebuab Bisa", "Bosa", "Bosha", "Boša", "Lom", "Posha" ], + "names": [ "Armenian Bosa", "Armenian Bosha", "Bosha", "Boša", "Lom", "Posha" ], "region": "AM", "regionname": "Armenia", "regions": [ "AZ", "RU", "SY" ], - "script": "Zyyy", + "script": "Armn", "sldr": false, "tag": "rmi", - "tags": [ "rmi-AM", "rmi-Zyyy" ], - "unwritten": true, - "windows": "rmi-Zyyy" + "tags": [ "rmi-AM", "rmi-Armn" ], + "windows": "rmi-Armn" }, { "full": "rmk-Latn-PG", "iana": [ "Romkun" ], "iso639_3": "rmk", "name": "Romkun", - "names": [ "Olvɨeŋgɨ In’ambreŋgɨ", "Romaken", "Romkuin" ], + "names": [ "Iski", "Olvɨeŋgɨ In’ambreŋgɨ", "Romaken", "Romkuin" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -96179,6 +100481,7 @@ "iana": [ "Baltic Romani" ], "iso639_3": "rml", "localnames": [ "Romani" ], + "macrolang": "rom", "name": "Romani, Baltic", "names": [ "Balt Romani", "Balt Slavic Romani", "Baltic Romani", "Baltic Slavic Romani", "Roma", "Romani" ], "region": "PL", @@ -96188,12 +100491,13 @@ "sldr": false, "tag": "rml", "tags": [ "rml-Latn", "rml-PL" ], - "windows": "rml-Latn" + "windows": "rml-Latn-PL" }, { "full": "rml-Cyrl-BY", "iana": [ "Baltic Romani" ], "iso639_3": "rml", + "macrolang": "rom", "name": "Romani, Baltic", "names": [ "Balt Romani", "Balt Slavic Romani", "Baltic Romani", "Baltic Slavic Romani", "Roma", "Romani" ], "nophonvars": true, @@ -96204,7 +100508,7 @@ "sldr": false, "tag": "rml-Cyrl", "tags": [ "rml-BY" ], - "windows": "rml-Cyrl" + "windows": "rml-Cyrl-BY" }, { "full": "rmm-Latn-ID", @@ -96226,6 +100530,7 @@ "iana": [ "Balkan Romani" ], "iso639_3": "rmn", "localnames": [ "Romani", "Romani ćhib" ], + "macrolang": "rom", "name": "Romani, Balkan", "names": [ "Balkan Romani", "Gypsy", "Roma", "Romani", "Romani ćhib" ], "region": "RS", @@ -96235,12 +100540,13 @@ "sldr": false, "tag": "rmn", "tags": [ "rmn-Latn", "rmn-RS" ], - "windows": "rmn-Latn" + "windows": "rmn-Latn-RS" }, { "full": "rmn-Cyrl-BG", "iana": [ "Balkan Romani" ], "iso639_3": "rmn", + "macrolang": "rom", "name": "Romani, Balkan", "names": [ "Balkan Romani", "Gypsy", "Roma", "Romani", "Romani ćhib" ], "nophonvars": true, @@ -96251,12 +100557,13 @@ "sldr": false, "tag": "rmn-Cyrl", "tags": [ "rmn-BG" ], - "windows": "rmn-Cyrl" + "windows": "rmn-Cyrl-BG" }, { "full": "rmn-Grek-GR", "iana": [ "Balkan Romani" ], "iso639_3": "rmn", + "macrolang": "rom", "name": "Romani, Balkan", "names": [ "Balkan Romani", "Gypsy", "Roma", "Romani", "Romani ćhib" ], "nophonvars": true, @@ -96267,13 +100574,14 @@ "sldr": false, "tag": "rmn-Grek", "tags": [ "rmn-GR" ], - "windows": "rmn-Grek" + "windows": "rmn-Grek-GR" }, { "full": "rmo-Latn-CH", "iana": [ "Sinte Romani" ], "iso639_3": "rmo", "localnames": [ "Romanes" ], + "macrolang": "rom", "name": "Romani, Sinte", "names": [ "Manouche", "Manuche", "Manuš", "Romanes", "Sasítka Romá", "Sinte", "Sinte Romani", "Sinti", "Sintitikes", "Sinto-Manush", "Tsigane", "Zigeuner" ], "region": "CH", @@ -96289,8 +100597,9 @@ "full": "rmp-Latn-PG", "iana": [ "Rempi" ], "iso639_3": "rmp", + "localnames": [ "Aic" ], "name": "Rempi", - "names": [ "Aic", "A’e", "Erempi", "Rempin" ], + "names": [ "Aic", "Ai’", "A’e", "Erempi", "Rempin" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -96305,10 +100614,10 @@ "iso639_3": "rmq", "localnames": [ "Caló" ], "name": "Caló", - "names": [ "Calão", "Caló Romani", "Chibi", "Gitano", "Hispanoromani", "Iberian Romani", "Romanó" ], + "names": [ "Calão", "Caló Romani", "Chibi", "Gitano", "Hispanoromani", "Iberian Romani", "Roma", "Romanó" ], "region": "ES", "regionname": "Spain", - "regions": [ "BR", "FR", "PT" ], + "regions": [ "BR", "CO", "FR", "PT" ], "script": "Latn", "sldr": false, "tag": "rmq", @@ -96320,7 +100629,7 @@ "iana": [ "Romanian Sign Language" ], "iso639_3": "rms", "name": "Romanian Sign Language", - "names": [ "LMGR", "LSR", "Limbaj Mimico-Gestual Romanesc", "Limbajul Semenelor Romanesc", "Limbajului mimico-gestual" ], + "names": [ "LMGR", "LSR", "Limba semnelor române", "Limbajul mimico-gestual romanesc", "Limbajul semenelor romanesc" ], "region": "RO", "regionname": "Romania", "regions": [ "MD" ], @@ -96378,6 +100687,7 @@ "iana": [ "Welsh Romani" ], "iso639_3": "rmw", "localnames": [ "Romnimus" ], + "macrolang": "rom", "name": "Romani, Welsh", "names": [ "Kååle", "Roma", "Romani", "Romnimus", "Voshanange Kalá", "Walsenenge Kale", "Welsh Romani" ], "region": "GB", @@ -96453,6 +100763,19 @@ "tag": "rn-Brai", "windows": "rn-Brai" }, + { + "full": "rnb-Zxxx-CR", + "iana": [ "Brunca Sign Language" ], + "iso639_3": "rnb", + "name": "Brunca Sign Language", + "region": "CR", + "regionname": "Costa Rica", + "script": "Zxxx", + "sldr": false, + "tag": "rnb", + "tags": [ "rnb-CR", "rnb-Zxxx", "sgn-rnb", "sgn-rnb-CR" ], + "windows": "rnb-Zxxx" + }, { "full": "rnd-Latn-CD", "iana": [ "Ruund" ], @@ -96575,7 +100898,7 @@ "suppress": true, "tag": "ro", "tags": [ "mo", "mo-Latn", "mo-Latn-RO", "mo-RO", "ro-Latn", "ro-RO" ], - "windows": "ro" + "windows": "ro-RO" }, { "full": "ro-Brai-RO", @@ -96590,7 +100913,7 @@ "script": "Brai", "sldr": false, "tag": "ro-Brai", - "windows": "ro-Brai" + "windows": "ro-Brai-RO" }, { "full": "ro-Cyrl-MD", @@ -96605,7 +100928,23 @@ "script": "Cyrl", "sldr": false, "tag": "ro-Cyrl", - "windows": "ro-Cyrl" + "windows": "ro-Cyrl-MD" + }, + { + "full": "ro-Cyrs-RO", + "iana": [ "Romanian", "Moldavian", "Moldovan" ], + "iso639_3": "ron", + "name": "Romanian", + "names": [ "Daco-Romanian", "Daco-Rumanian", "Limba română", "Moldavian", "Moldovan", "Româneşte", "Română", "Roumanian", "Rumanian" ], + "nophonvars": true, + "obsolete": true, + "region": "RO", + "regionname": "Romania", + "regions": [ "HU", "IL", "MD", "RS" ], + "script": "Cyrs", + "sldr": false, + "tag": "ro-Cyrs", + "windows": "ro-Cyrs-RO" }, { "full": "ro-Dupl-RO", @@ -96619,7 +100958,7 @@ "script": "Dupl", "sldr": false, "tag": "ro-Dupl", - "windows": "ro-Dupl" + "windows": "ro-Dupl-RO" }, { "full": "ro-Latn-MD", @@ -96633,10 +100972,9 @@ "regionname": "Moldova", "script": "Latn", "sldr": true, - "suppress": true, "tag": "ro-MD", "tags": [ "mo-Latn-MD", "mo-MD" ], - "windows": "ro-MD" + "windows": "ro-Latn-MD" }, { "full": "rob-Latn-ID", @@ -96673,7 +101011,7 @@ "iana": [ "Rogo" ], "iso639_3": "rod", "name": "Rogo", - "names": [ "Burogo", "Shiyabe", "Toshiyabe", "Ucanja Kamuku", "Urogo", "toRogo" ], + "names": [ "Burogo", "Shiyabe", "Toshiyabe", "TɔRɔgɔ", "Ucanja Kamuku", "Urogo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -96687,8 +101025,8 @@ "full": "roe-Latn-PG", "iana": [ "Ronji" ], "iso639_3": "roe", - "name": "Ronji", - "names": [ "Gali", "Roinji" ], + "name": "Uma", + "names": [ "Gali", "Roinji", "Ronji" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -96750,16 +101088,16 @@ "iana": [ "Romany" ], "iso639_3": "rom", "localnames": [ "Romani" ], - "name": "Romani, Vlax", - "names": [ "Beás", "Cigány", "Danubian", "Gurbéti", "Gypsy", "Rom", "Roma", "Romanes", "Romanese", "Romani", "Romany", "Romanés", "Tsigane", "Tsigene", "Tsingani", "Vlax", "Vlax Romany" ], + "name": "Romany", + "names": [ "Beás", "Cigány", "Danubian", "Gurbéti", "Gypsy", "Rom", "Roma", "Romanese", "Romani", "Romani, Vlax", "Romanés", "Tsigene", "Tsingani", "Vlax", "Vlax Romany" ], "region": "RO", "regionname": "Romania", "regions": [ "AL", "AR", "AT", "BA", "BG", "BR", "CA", "CL", "CO", "DE", "ES", "FR", "GR", "HU", "IT", "MD", "MK", "MX", "NL", "NO", "PL", "PT", "RS", "RU", "SE", "SK", "UA" ], "script": "Latn", "sldr": false, "tag": "rom", - "tags": [ "rmy-Latn", "rmy-Latn-RO", "rom-Latn", "rom-RO" ], - "windows": "rom-Latn" + "tags": [ "rmy", "rmy-Latn", "rmy-Latn-RO", "rmy-RO", "rom-Latn", "rom-RO" ], + "windows": "rom-Latn-RO" }, { "full": "rom-Cyrl-RO", @@ -96772,11 +101110,13 @@ "script": "Cyrl", "sldr": false, "tag": "rom-Cyrl", - "windows": "rom-Cyrl" + "windows": "rom-Cyrl-RO" }, { "full": "rom-Cyrl-BG", "iana": [ "Romany" ], + "iso639_3": "rmy", + "macrolang": "rom", "name": "Romani, Vlax", "names": [ "Beás", "Cigány", "Danubian", "Gurbéti", "Gypsy", "Rom", "Roma", "Romanese", "Romani", "Romany", "Romanés", "Tsigene", "Tsingani", "Vlax", "Vlax Romany" ], "region": "BG", @@ -96854,8 +101194,9 @@ "full": "row-Latn-ID", "iana": [ "Dela-Oenale" ], "iso639_3": "row", + "localnames": [ "Rote Dela" ], "name": "Dela-Oenale", - "names": [ "Dela", "Delha", "Oe Nale", "Rote", "Rote Barat", "Roti", "Rotinese", "Western Rote" ], + "names": [ "Dela", "Delha", "Oe Nale", "Rote", "Rote Barat", "Rote Dela", "Roti", "Rotinese", "Western Rote" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -96910,6 +101251,7 @@ "full": "rro-Latn-PG", "iana": [ "Waima" ], "iso639_3": "rro", + "localnames": [ "Waima" ], "name": "Waima", "names": [ "Roro" ], "region": "PG", @@ -96925,6 +101267,7 @@ "iana": [ "Arritinngithigh" ], "iso639_3": "rrt", "name": "Arritinngithigh", + "names": [ "Arangit", "Arariniti", "Aratingiti", "Aratiniti", "Aretinngit", "Aridingidigh", "Aridinngidhigh", "Aritingiti", "Aritinngayth", "Aritinngithig", "Aritinŋayt", "Aritinŋit̯iγ", "Arraythinngith", "Arreythinwum", "Arrithinngayth" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -96962,15 +101305,32 @@ "tags": [ "rsi-SB", "rsi-Zxxx", "sgn-rsi", "sgn-rsi-SB" ], "windows": "rsi-Zxxx" }, + { + "full": "rsk-Cyrl-RS", + "iana": [ "Ruthenian", "Rusyn" ], + "iso639_3": "rsk", + "latnnames": [ "Ruski" ], + "localnames": [ "руски" ], + "name": "Ruthenian", + "names": [ "Pannonian Rusyn", "Rusnak", "Rusnatski", "Rusyn" ], + "region": "RS", + "regionname": "Serbia", + "regions": [ "HR" ], + "script": "Cyrl", + "sldr": false, + "tag": "rsk", + "tags": [ "rsk-Cyrl", "rsk-RS" ], + "windows": "rsk-Cyrl" + }, { "full": "rsl-Zxxx-RU", "iana": [ "Russian Sign Language" ], "iso639_3": "rsl", "name": "Russian Sign Language", - "names": [ "RSL" ], + "names": [ "RSL", "TSL", "Tajik Sign Language" ], "region": "RU", "regionname": "Russian Federation", - "regions": [ "EE", "IL", "MD" ], + "regions": [ "AM", "BY", "EE", "IL", "KG", "KZ", "MD", "TJ", "TM", "UZ" ], "script": "Zxxx", "sldr": false, "tag": "rsl", @@ -96991,6 +101351,33 @@ "tags": [ "rsm-AU", "rsm-Zxxx", "sgn-rsm", "sgn-rsm-AU" ], "windows": "rsm-Zxxx" }, + { + "full": "rsn-Zxxx-RW", + "iana": [ "Rwandan Sign Language" ], + "iso639_3": "rsn", + "name": "Rwandan Sign Language", + "names": [ "Amarenga yo mu Rwanda", "Amarenga y’Ikinyarwanda", "RSL", "Rwanda Sign Language" ], + "region": "RW", + "regionname": "Rwanda", + "script": "Zxxx", + "sldr": false, + "tag": "rsn", + "tags": [ "rsn-RW", "rsn-Zxxx", "sgn-rsn", "sgn-rsn-RW" ], + "windows": "rsn-Zxxx" + }, + { + "full": "rsw-Latn-NG", + "iso639_3": "rsw", + "name": "Rishiwa", + "names": [ "Ashiwa", "Kishiwa", "Rishua", "Rishuwa" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "rsw", + "tags": [ "rsw-Latn", "rsw-NG" ], + "windows": "rsw-Latn" + }, { "full": "rtc-Latn-MM", "iana": [ "Rungtu Chin" ], @@ -97035,31 +101422,32 @@ "windows": "rtm-Latn" }, { - "full": "rts-Zyyy-XX", + "full": "rts-Zyyy-RU", "iana": [ "Yurats" ], "iso639_3": "rts", "name": "Yurats", - "region": "XX", + "names": [ "Yurak" ], + "region": "RU", + "regionname": "Russian Federation", "script": "Zyyy", "sldr": false, "tag": "rts", - "tags": [ "rts-XX", "rts-Zyyy" ], + "tags": [ "rts-RU", "rts-Zyyy" ], "windows": "rts-Zyyy" }, { - "full": "rtw-Zyyy-IN", + "full": "rtw-Deva-IN", "iana": [ "Rathawi" ], "iso639_3": "rtw", "name": "Rathawi", "names": [ "Bal-La", "Kohelia", "Rathwi" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "rtw", - "tags": [ "rtw-IN", "rtw-Zyyy" ], - "unwritten": true, - "windows": "rtw-Zyyy" + "tags": [ "rtw-Deva", "rtw-IN" ], + "windows": "rtw-Deva" }, { "full": "ru-Cyrl-RU", @@ -97079,7 +101467,7 @@ "tag": "ru", "tags": [ "ru-Cyrl", "ru-RU" ], "variants": [ "luna1918", "petr1708" ], - "windows": "ru" + "windows": "ru-RU" }, { "full": "ru-Cyrl-BY", @@ -97093,9 +101481,8 @@ "regionname": "Belarus", "script": "Cyrl", "sldr": true, - "suppress": true, "tag": "ru-BY", - "windows": "ru-BY" + "windows": "ru-Cyrl-BY" }, { "full": "ru-Brai-RU", @@ -97110,7 +101497,7 @@ "script": "Brai", "sldr": false, "tag": "ru-Brai", - "windows": "ru-Brai" + "windows": "ru-Brai-RU" }, { "full": "ru-Cyrl-KG", @@ -97124,9 +101511,8 @@ "regionname": "Kyrgyzstan", "script": "Cyrl", "sldr": true, - "suppress": true, "tag": "ru-KG", - "windows": "ru-KG" + "windows": "ru-Cyrl-KG" }, { "full": "ru-Cyrl-KZ", @@ -97140,9 +101526,8 @@ "regionname": "Kazakhstan", "script": "Cyrl", "sldr": true, - "suppress": true, "tag": "ru-KZ", - "windows": "ru-KZ" + "windows": "ru-Cyrl-KZ" }, { "full": "ru-Cyrl-MD", @@ -97156,9 +101541,8 @@ "regionname": "Moldova", "script": "Cyrl", "sldr": true, - "suppress": true, "tag": "ru-MD", - "windows": "ru-MD" + "windows": "ru-Cyrl-MD" }, { "full": "ru-Cyrl-UA", @@ -97172,9 +101556,8 @@ "regionname": "Ukraine", "script": "Cyrl", "sldr": true, - "suppress": true, "tag": "ru-UA", - "windows": "ru-UA" + "windows": "ru-Cyrl-UA" }, { "full": "rub-Latn-UG", @@ -97214,7 +101597,7 @@ "latnnames": [ "Rusynska mova" ], "localnames": [ "русиньскый язык" ], "name": "Rusyn", - "names": [ "Carpathian", "Carpatho-Rusyn", "Lemko", "Rusinskiy yazyk", "Rusynski", "Ruthenian" ], + "names": [ "Carpathian", "Carpatho-Rusyn", "Lemko", "Rusinskiy yazyk", "Ruthenian" ], "region": "UA", "regionname": "Ukraine", "regions": [ "CZ", "HR", "HU", "PL", "RS", "SK" ], @@ -97222,14 +101605,14 @@ "sldr": false, "tag": "rue", "tags": [ "rue-Cyrl", "rue-UA" ], - "windows": "rue-Cyrl" + "windows": "rue-Cyrl-UA" }, { "full": "rue-Latn-SK", "iana": [ "Rusyn" ], "iso639_3": "rue", "name": "Rusyn", - "names": [ "Carpathian", "Carpatho-Rusyn", "Lemko", "Rusinskiy yazyk", "Rusynski", "Ruthenian" ], + "names": [ "Carpathian", "Carpatho-Rusyn", "Lemko", "Rusinskiy yazyk", "Ruthenian" ], "region": "SK", "regionname": "Slovakia", "regions": [ "CZ", "HR", "HU", "PL", "RS", "UA" ], @@ -97237,12 +101620,13 @@ "sldr": false, "tag": "rue-Latn", "tags": [ "rue-SK" ], - "windows": "rue-Latn" + "windows": "rue-Latn-SK" }, { "full": "ruf-Latn-TZ", "iana": [ "Luguru" ], "iso639_3": "ruf", + "localname": "Luguru", "name": "Luguru", "names": [ "Cilugulu", "Guru", "Ikiruguru", "Kiluguru", "Kiruguru", "Lughuru", "Lugulu", "Ruguru" ], "region": "TZ", @@ -97303,7 +101687,7 @@ "iso639_3": "ruk", "localnames": [ "Kuche" ], "name": "Kuce", - "names": [ "Bace", "Bache", "Che", "Inchazi", "Kuche", "Rukuba", "Sale" ], + "names": [ "Bace", "Bache", "Ce", "Che", "Inchazi", "Kuche", "Rukuba", "Sale" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -97341,7 +101725,7 @@ "sldr": false, "tag": "rup", "tags": [ "rup-Latn", "rup-RO" ], - "windows": "rup-Latn" + "windows": "rup-Latn-RO" }, { "full": "rup-Grek-GR", @@ -97357,7 +101741,7 @@ "sldr": false, "tag": "rup-Grek", "tags": [ "rup-GR" ], - "windows": "rup-Grek" + "windows": "rup-Grek-GR" }, { "full": "ruq-Latn-GR", @@ -97381,7 +101765,7 @@ "latnnames": [ "Myḥabišdy", "Myḥabišdy č̣el" ], "localnames": [ "мыхӏабишды", "мыхӏабишды чӏел" ], "name": "Rutul", - "names": [ "Chal", "Knovtsy", "Mukhad", "Myhadbyr", "Myhynnynydy-ch’al", "Rutal", "Rutultsy", "Rutuly" ], + "names": [ "Chal", "Knovtsy", "Mukhad", "Myhadbyr", "Myhynnynydy-ch’al", "Myxʼabišdy čʼel", "Rutal", "Rutultsy", "Rutuly" ], "region": "RU", "regionname": "Russian Federation", "regions": [ "AZ" ], @@ -97389,14 +101773,14 @@ "sldr": false, "tag": "rut", "tags": [ "rut-Cyrl", "rut-RU" ], - "windows": "rut-Cyrl" + "windows": "rut-Cyrl-RU" }, { "full": "rut-Latn-AZ", "iana": [ "Rutul" ], "iso639_3": "rut", "name": "Rutul", - "names": [ "Chal", "Knovtsy", "Mukhad", "Myhadbyr", "Myhynnynydy-ch’al", "Rutal", "Rutultsy", "Rutuly" ], + "names": [ "Chal", "Knovtsy", "Mukhad", "Myhadbyr", "Myhynnynydy-ch’al", "Myxʼabišdy čʼel", "Rutal", "Rutultsy", "Rutuly" ], "region": "AZ", "regionname": "Azerbaijan", "regions": [ "RU" ], @@ -97404,7 +101788,7 @@ "sldr": false, "tag": "rut-Latn", "tags": [ "rut-AZ" ], - "windows": "rut-Latn" + "windows": "rut-Latn-AZ" }, { "full": "ruu-Latn-MY", @@ -97441,7 +101825,7 @@ "iana": [ "Ruma" ], "iso639_3": "ruz", "name": "Ruma", - "names": [ "Bagwama", "Rurama", "Ruruma", "Turama" ], + "names": [ "Arumaruma", "Bagwama", "Rurama", "Ruruma", "Turama", "Turuma" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -97456,7 +101840,7 @@ "iana": [ "Kinyarwanda" ], "iso639_3": "kin", "localname": "Kinyarwanda", - "localnames": [ "Kinyarwanda" ], + "localnames": [ "Ikinyarwanda" ], "name": "Kinyarwanda", "names": [ "Bafumbira", "Banyarwanda", "Fumbira", "Hima", "Ikinyarwanda", "Orunyarwanda", "Ruanda", "Rufumbira", "Runyarwanda", "Rwanda", "Rwandan", "Urunyaruanda" ], "region": "RW", @@ -97513,6 +101897,21 @@ "tags": [ "rwk-Latn", "rwk-TZ" ], "windows": "rwk-Latn" }, + { + "full": "rwl-Latn-TZ", + "iana": [ "Ruwila" ], + "iso639_3": "rwl", + "name": "Ruwila", + "names": [ "Kiruwila", "Luwila", "Luwira", "Lwila", "Lwira", "Rwila", "Waruwila" ], + "region": "TZ", + "regionname": "United Republic of Tanzania", + "script": "Latn", + "sldr": false, + "tag": "rwl", + "tags": [ "rwl-Latn", "rwl-TZ" ], + "unwritten": true, + "windows": "rwl-Latn" + }, { "full": "rwm-Latn-UG", "iana": [ "Amba (Uganda)" ], @@ -97567,6 +101966,7 @@ "iso639_3": "rwr", "latnnames": [ "Mārwāṛī" ], "localnames": [ "मारवाड़ी" ], + "macrolang": "mwr", "name": "Marwari", "names": [ "Marrubhasha", "Marvadi", "Marvari", "Marwadi", "Marwari (India)", "Rajasthani", "Rajsthani" ], "region": "IN", @@ -97671,7 +102071,7 @@ "iana": [ "Sanskrit" ], "iso639_3": "san", "latnnames": [ "Saṃskṛtam" ], - "localname": "संस्कृत भाषा", + "localname": "संस्कृतम्", "localnames": [ "संस्कृतम्" ], "name": "Sanskrit", "names": [ "Deva Bhasha", "Deva vani", "Sanskrit bhasha" ], @@ -97685,6 +102085,21 @@ "variants": [ "bauddha", "itihasa", "laukika", "vaidika" ], "windows": "sa-Deva" }, + { + "full": "sa-Bhks-IN", + "iana": [ "Sanskrit" ], + "iso639_3": "san", + "name": "Sanskrit", + "names": [ "Deva Bhasha", "Deva vani", "Sanskrit bhasha" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "NP" ], + "script": "Bhks", + "sldr": false, + "tag": "sa-Bhks", + "windows": "sa-Bhks" + }, { "full": "sa-Gran-IN", "iana": [ "Sanskrit" ], @@ -97700,6 +102115,36 @@ "tag": "sa-Gran", "windows": "sa-Gran" }, + { + "full": "sa-Kawi-IN", + "iana": [ "Sanskrit" ], + "iso639_3": "san", + "name": "Sanskrit", + "names": [ "Deva Bhasha", "Deva vani", "Sanskrit bhasha" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "NP" ], + "script": "Kawi", + "sldr": false, + "tag": "sa-Kawi", + "windows": "sa-Kawi" + }, + { + "full": "sa-Khar-IN", + "iana": [ "Sanskrit" ], + "iso639_3": "san", + "name": "Sanskrit", + "names": [ "Deva Bhasha", "Deva vani", "Sanskrit bhasha" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "NP" ], + "script": "Khar", + "sldr": false, + "tag": "sa-Khar", + "windows": "sa-Khar" + }, { "full": "sa-Mymr-IN", "iana": [ "Sanskrit" ], @@ -97795,12 +102240,28 @@ "variants": [ "bauddha", "itihasa", "laukika", "vaidika" ], "windows": "sa-Sinh" }, + { + "full": "sa-Tirh-IN", + "iana": [ "Sanskrit" ], + "iso639_3": "san", + "name": "Sanskrit", + "names": [ "Deva Bhasha", "Deva vani", "Sanskrit bhasha" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "NP" ], + "script": "Tirh", + "sldr": false, + "tag": "sa-Tirh", + "windows": "sa-Tirh" + }, { "full": "saa-Latn-TD", "iana": [ "Saba" ], "iso639_3": "saa", + "localnames": [ "Saɓɓoŋ" ], "name": "Saba", - "names": [ "Jelkung" ], + "names": [ "Jelkung", "Saba Sorki", "Saɓɓoŋ" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -97844,9 +102305,9 @@ "full": "sad-Latn-TZ", "iana": [ "Sandawe" ], "iso639_3": "sad", - "localnames": [ "Sandawso" ], + "localnames": [ "Sandawegki" ], "name": "Sandawe", - "names": [ "Kisandawe", "Kissandaui", "Sandaui", "Sandaweeki", "Sandawi", "Sandawso", "Sandwe" ], + "names": [ "Kisandawe", "Kissandaui", "Sandaui", "Sandaweeki", "Sandawegki", "Sandawi", "Sandawso", "Sandwe" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -97903,6 +102364,35 @@ "tags": [ "sah-Cyrl", "sah-RU" ], "windows": "sah-Cyrl" }, + { + "full": "sah-Cyrs-RU", + "iana": [ "Yakut" ], + "iso639_3": "sah", + "name": "Yakut", + "names": [ "Sakha", "Yakut-Sakha" ], + "nophonvars": true, + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "script": "Cyrs", + "sldr": false, + "tag": "sah-Cyrs", + "windows": "sah-Cyrs" + }, + { + "full": "sah-Latn-RU", + "iana": [ "Yakut" ], + "iso639_3": "sah", + "name": "Yakut", + "names": [ "Sakha", "Yakut-Sakha" ], + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "script": "Latn", + "sldr": false, + "tag": "sah-Latn", + "windows": "sah-Latn" + }, { "full": "saj-Latn-ID", "iana": [ "Sahu" ], @@ -98054,20 +102544,20 @@ "windows": "sas-Bali" }, { - "full": "sat-Latn-IN", + "full": "sat-Olck-IN", "iana": [ "Santali" ], "iso639_3": "sat", - "localnames": [ "Har Rar" ], + "localname": "ᱥᱟᱱᱛᱟᱲᱤ", "name": "Santhali", "names": [ "Har", "Har Rar", "Hor", "Sainti", "Samtali", "Sandal", "Sangtal", "Santal", "Santali", "Santhal", "Santhiali", "Satar", "Sentali", "Sonthal" ], "region": "IN", "regionname": "India", "regions": [ "BD", "BT", "NP" ], - "script": "Latn", - "sldr": false, + "script": "Olck", + "sldr": true, "tag": "sat", - "tags": [ "sat-IN", "sat-Latn" ], - "windows": "sat-Latn" + "tags": [ "sat-IN", "sat-Olck" ], + "windows": "sat-Olck-IN" }, { "full": "sat-Beng-IN", @@ -98082,7 +102572,7 @@ "script": "Beng", "sldr": false, "tag": "sat-Beng", - "windows": "sat-Beng" + "windows": "sat-Beng-IN" }, { "full": "sat-Deva-NP", @@ -98098,22 +102588,22 @@ "sldr": false, "tag": "sat-Deva", "tags": [ "sat-NP" ], - "windows": "sat-Deva" + "windows": "sat-Deva-NP" }, { - "full": "sat-Olck-IN", + "full": "sat-Latn-IN", "iana": [ "Santali" ], "iso639_3": "sat", + "localnames": [ "Har Rar" ], "name": "Santhali", "names": [ "Har", "Har Rar", "Hor", "Sainti", "Samtali", "Sandal", "Sangtal", "Santal", "Santali", "Santhal", "Santhiali", "Satar", "Sentali", "Sonthal" ], - "nophonvars": true, "region": "IN", "regionname": "India", "regions": [ "BD", "BT", "NP" ], - "script": "Olck", + "script": "Latn", "sldr": false, - "tag": "sat-Olck", - "windows": "sat-Olck" + "tag": "sat-Latn", + "windows": "sat-Latn-IN" }, { "full": "sat-Orya-IN", @@ -98128,7 +102618,7 @@ "script": "Orya", "sldr": false, "tag": "sat-Orya", - "windows": "sat-Orya" + "windows": "sat-Orya-IN" }, { "full": "sau-Latn-ID", @@ -98142,7 +102632,6 @@ "sldr": false, "tag": "sau", "tags": [ "sau-ID", "sau-Latn" ], - "unwritten": true, "windows": "sau-Latn" }, { @@ -98209,8 +102698,9 @@ "full": "say-Latn-NG", "iana": [ "Saya" ], "iso639_3": "say", + "localnames": [ "Vigzar", "Vìk Zaar" ], "name": "Saya", - "names": [ "Sayanci", "Sayara", "Sayawa", "Seiyara", "Seya", "Seyawa", "Vigzar", "Vikzar", "Zaar" ], + "names": [ "Sayanci", "Sayara", "Sayawa", "Seiyara", "Seya", "Seyawa", "Vigzar", "Vìk Zaar", "Zaar", "Zar", "Za’r" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -98223,8 +102713,8 @@ "full": "saz-Saur-IN", "iana": [ "Saurashtra" ], "iso639_3": "saz", - "name": "Saurashtra", - "names": [ "Palkar", "Patnuli", "Saurashtri", "Sourashtra", "Sowrashtra" ], + "name": "Sourashtra", + "names": [ "Palkar", "Patnuli", "Saurashtra", "Saurashtri", "Sowrashtra" ], "region": "IN", "regionname": "India", "script": "Saur", @@ -98237,8 +102727,8 @@ "full": "saz-Deva-IN", "iana": [ "Saurashtra" ], "iso639_3": "saz", - "name": "Saurashtra", - "names": [ "Palkar", "Patnuli", "Saurashtri", "Sourashtra", "Sowrashtra" ], + "name": "Sourashtra", + "names": [ "Palkar", "Patnuli", "Saurashtra", "Saurashtri", "Sowrashtra" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -98253,8 +102743,8 @@ "iso639_3": "saz", "latnnames": [ "Caurāṭṭira moḻi" ], "localnames": [ "சௌராட்டிர மொழி" ], - "name": "Saurashtra", - "names": [ "Palkar", "Patnuli", "Saurashtri", "Sourashtra", "Sowrashtra" ], + "name": "Sourashtra", + "names": [ "Palkar", "Patnuli", "Saurashtra", "Saurashtri", "Sowrashtra" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -98267,8 +102757,8 @@ "full": "saz-Telu-IN", "iana": [ "Saurashtra" ], "iso639_3": "saz", - "name": "Saurashtra", - "names": [ "Palkar", "Patnuli", "Saurashtri", "Sourashtra", "Sowrashtra" ], + "name": "Sourashtra", + "names": [ "Palkar", "Patnuli", "Saurashtra", "Saurashtri", "Sowrashtra" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -98329,7 +102819,7 @@ "iso639_3": "sbd", "localnames": [ "San" ], "name": "Samo, Southern", - "names": [ "Sa", "Samo", "San", "Sane", "Southern Samo" ], + "names": [ "Sa", "Samo", "San", "San Maka", "Sane", "Southern Samo" ], "region": "BF", "regionname": "Burkina Faso", "script": "Latn", @@ -98345,6 +102835,7 @@ "localname": "Saliba", "localnames": [ "Saliba" ], "name": "Saliba", + "names": [ "Saliba-Logea", "Sariba" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -98371,7 +102862,8 @@ "full": "sbg-Latn-ID", "iana": [ "Seget" ], "iso639_3": "sbg", - "name": "Seget", + "name": "Moi Lemas", + "names": [ "Moi", "Seget" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -98428,7 +102920,7 @@ "full": "sbk-Latn-TZ", "iana": [ "Safwa" ], "iso639_3": "sbk", - "localnames": [ "Ishisafwa" ], + "localnames": [ "Shisafwa" ], "name": "Safwa", "names": [ "Cisafwa", "Ishisafwa", "Kisafwa", "Shisafwa" ], "region": "TZ", @@ -98443,10 +102935,10 @@ "full": "sbl-Latn-PH", "iana": [ "Botolan Sambal" ], "iso639_3": "sbl", - "localname": "Sambal, Botolan", - "localnames": [ "Hambali" ], + "localname": "Hambali", + "localnames": [ "Sambal Botolan" ], "name": "Sambal, Botolan", - "names": [ "Aeta Negrito", "Ayta Hambali", "Botolan Sambal", "Botolan Zambal", "Hambali" ], + "names": [ "Aeta Negrito", "Aeta Sambal", "Botolan", "Botolan Sambal", "Botolan Zambal", "Sambal Botolan" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -98475,6 +102967,7 @@ "iana": [ "Sindhi Bhil" ], "iso639_3": "sbn", "name": "Sindhi Bhil", + "names": [ "Meghwar Bhil" ], "region": "PK", "regionname": "Pakistan", "script": "Arab", @@ -98503,7 +102996,7 @@ "iana": [ "Sangu (Tanzania)" ], "iso639_3": "sbp", "localname": "Ishisangu", - "localnames": [ "Shisango" ], + "localnames": [ "Shisangu" ], "name": "Sangu", "names": [ "Ishisango", "Ishisangu", "Kisangu", "Lori", "Rori", "Sangi", "Sango", "Sangu (Tanzania)", "Shisango", "Shisangu" ], "region": "TZ", @@ -98577,21 +103070,21 @@ "windows": "sbt-Latn" }, { - "full": "sbu-Deva-IN", + "full": "sbu-Tibt-IN", "iana": [ "Stod Bhoti" ], "iso639_3": "sbu", "name": "Stod Bhoti", "names": [ "Lahul Bhoti", "Stod", "Stod-Kad", "Tod", "Tod-Kad" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Tibt", "sldr": false, - "tag": "sbu-Deva", - "windows": "sbu-Deva" + "tag": "sbu", + "tags": [ "sbu-IN", "sbu-Tibt" ], + "windows": "sbu-Tibt" }, { - "full": "sbu-Tibt-IN", + "full": "sbu-Deva-IN", "iana": [ "Stod Bhoti" ], "iso639_3": "sbu", "name": "Stod Bhoti", @@ -98599,22 +103092,24 @@ "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Tibt", + "script": "Deva", "sldr": false, - "tag": "sbu-Tibt", - "windows": "sbu-Tibt" + "tag": "sbu-Deva", + "windows": "sbu-Deva" }, { - "full": "sbv-Zyyy-XX", + "full": "sbv-Latn-IT", "iana": [ "Sabine" ], "iso639_3": "sbv", "name": "Sabine", - "region": "XX", - "script": "Zyyy", + "region": "IT", + "regionname": "Italy", + "script": "Latn", "sldr": false, "tag": "sbv", - "tags": [ "sbv-XX", "sbv-Zyyy" ], - "windows": "sbv-Zyyy" + "tags": [ "sbv-IT", "sbv-Latn" ], + "unwritten": true, + "windows": "sbv-Latn" }, { "full": "sbw-Latn-GA", @@ -98678,12 +103173,13 @@ "full": "sc-Latn-IT", "iana": [ "Sardinian" ], "iso639_3": "srd", - "name": "Sardinian, Logudorese", - "names": [ "Central Sardinian", "Logudorese", "Sard", "Sardarese", "Sardinian" ], + "localname": "sardu", + "name": "Sardinian", + "names": [ "Central Sardinian", "Logudorese", "Sard", "Sardarese", "Sardinian, Logudorese" ], "region": "IT", "regionname": "Italy", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "sc", "tags": [ "sc-IT", "sc-Latn", "src", "src-IT", "src-Latn", "src-Latn-IT" ], "windows": "sc-Latn" @@ -98737,7 +103233,8 @@ "full": "scf-Latn-PA", "iana": [ "San Miguel Creole French" ], "iso639_3": "scf", - "name": "San Miguel Creole French", + "name": "San Miguel French Creole", + "names": [ "San Miguel Creole French" ], "region": "PA", "regionname": "Panama", "script": "Latn", @@ -98779,8 +103276,8 @@ "full": "sci-Latn-LK", "iana": [ "Sri Lankan Creole Malay" ], "iso639_3": "sci", - "name": "Sri Lankan Creole Malay", - "names": [ "Java Jati", "Melayu Bahasa", "Sri Lankan Malay" ], + "name": "Sri Lankan Malay Creole", + "names": [ "Java Jati", "Melayu Bahasa", "Sri Lankan Creole Malay", "Sri Lankan Malay" ], "region": "LK", "regionname": "Sri Lanka", "script": "Latn", @@ -98836,7 +103333,7 @@ "sldr": true, "tag": "scl", "tags": [ "scl-Arab", "scl-PK" ], - "windows": "scl-Arab" + "windows": "scl-Arab-PK" }, { "full": "scl-Arab-IN", @@ -98883,12 +103380,28 @@ "tag": "sco", "tags": [ "sco-GB", "sco-Latn" ], "variants": [ "ulster" ], - "windows": "sco-Latn" + "windows": "sco-Latn-GB" + }, + { + "full": "sco-Runr-001", + "iana": [ "Scots" ], + "iso639_3": "sco", + "name": "Scots", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "sco-Runr", + "tags": [ "sco-001" ], + "windows": "sco-Runr-001" }, { "full": "scp-Deva-NP", "iana": [ "Hyolmo", "Helambu Sherpa" ], "iso639_3": "scp", + "latnnames": [ "Hyolmo" ], + "localnames": [ "ह्योल्मो" ], "name": "Hyolmo", "names": [ "Helambu Sherpa", "Hyolmo Tam", "Yholmo", "Yohlmo", "Yolmo" ], "region": "NP", @@ -98918,9 +103431,10 @@ "full": "scs-Latn-CA", "iana": [ "North Slavey" ], "iso639_3": "scs", - "localnames": [ "Satúotine Yatí" ], + "localnames": [ "Sahtúot’ı̨nę Yatı̨́" ], + "macrolang": "den", "name": "Slavey, North", - "names": [ "Dene", "Dené", "Esclave du Nord", "Mackenzian", "North Slavey", "Satúotine Yatí" ], + "names": [ "Dene", "Esclave du Nord", "Mackenzian", "North Slavey", "Sahtúot’ı̨nę Yatı̨́", "Satúotine Yatí" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -98933,8 +103447,9 @@ "full": "scs-Cans-CA", "iana": [ "North Slavey" ], "iso639_3": "scs", + "macrolang": "den", "name": "Slavey, North", - "names": [ "Dene", "Dené", "Esclave du Nord", "Mackenzian", "North Slavey", "Satúotine Yatí" ], + "names": [ "Dene", "Esclave du Nord", "Mackenzian", "North Slavey", "Sahtúot’ı̨nę Yatı̨́", "Satúotine Yatí" ], "nophonvars": true, "obsolete": true, "region": "CA", @@ -98960,32 +103475,32 @@ "windows": "sct-Laoo" }, { - "full": "scu-Zyyy-IN", + "full": "scu-Takr-IN", "iana": [ "Shumcho" ], "iso639_3": "scu", "name": "Shumcho", "names": [ "Central Kinnauri", "Shumcu", "Sumcho", "Sumchu", "Sumtsu", "Thebarskad", "Thebor", "Thebör Skadd" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Takr", "sldr": false, "tag": "scu", - "tags": [ "scu-IN", "scu-Zyyy" ], + "tags": [ "scu-IN", "scu-Takr" ], "unwritten": true, - "windows": "scu-Zyyy" + "windows": "scu-Takr" }, { "full": "scv-Latn-NG", "iana": [ "Sheni" ], "iso639_3": "scv", "name": "Sheni", - "names": [ "Seni", "Shaini", "Shani", "Shenanci", "Shenne", "tiSeni" ], + "names": [ "Seni", "Shaini", "Shani", "Shenanci", "Shenne", "anaSeni", "tiSeni" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", "sldr": false, "tag": "scv", - "tags": [ "scv-Latn", "scv-NG" ], + "tags": [ "scv-Latn", "scv-NG", "zir", "zir-Latn", "zir-Latn-NG", "zir-NG" ], "unwritten": true, "windows": "scv-Latn" }, @@ -98993,7 +103508,8 @@ "full": "scw-Latn-NG", "iana": [ "Sha" ], "iso639_3": "scw", - "name": "Sha", + "name": "Sya", + "names": [ "Sha" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -99004,16 +103520,17 @@ "windows": "scw-Latn" }, { - "full": "scx-Zyyy-XX", + "full": "scx-Grek-IT", "iana": [ "Sicel" ], "iso639_3": "scx", "name": "Sicel", - "region": "XX", - "script": "Zyyy", + "region": "IT", + "regionname": "Italy", + "script": "Grek", "sldr": false, "tag": "scx", - "tags": [ "scx-XX", "scx-Zyyy" ], - "windows": "scx-Zyyy" + "tags": [ "scx-Grek", "scx-IT" ], + "windows": "scx-Grek" }, { "full": "sd-Arab-PK", @@ -99030,10 +103547,10 @@ "sldr": true, "tag": "sd", "tags": [ "sd-Arab", "sd-PK" ], - "windows": "sd-Arab" + "windows": "sd-Arab-PK" }, { - "full": "sd-Deva-IN", + "full": "sd-Guru-IN", "iana": [ "Sindhi" ], "iso639_3": "snd", "name": "Sindhi", @@ -99041,24 +103558,25 @@ "region": "IN", "regionname": "India", "regions": [ "BZ", "CA", "NP", "PH", "PK", "SG", "US" ], - "script": "Deva", + "script": "Guru", "sldr": false, - "tag": "sd-Deva", - "windows": "sd-Deva" + "tag": "sd-Guru", + "windows": "sd-Guru-IN" }, { - "full": "sd-Guru-IN", + "full": "sd-Deva-IN", "iana": [ "Sindhi" ], "iso639_3": "snd", + "localname": "सिन्धी", "name": "Sindhi", - "nophonvars": true, "region": "IN", "regionname": "India", "regions": [ "BZ", "CA", "NP", "PH", "PK", "SG", "US" ], - "script": "Guru", - "sldr": false, - "tag": "sd-Guru", - "windows": "sd-Guru" + "script": "Deva", + "sldr": true, + "tag": "sd-IN", + "tags": [ "sd-Deva" ], + "windows": "sd-Deva-IN" }, { "full": "sd-Khoj-IN", @@ -99071,7 +103589,7 @@ "script": "Khoj", "sldr": false, "tag": "sd-Khoj", - "windows": "sd-Khoj" + "windows": "sd-Khoj-IN" }, { "full": "sd-Khoj-PK", @@ -99098,7 +103616,7 @@ "script": "Sind", "sldr": false, "tag": "sd-Sind", - "windows": "sd-Sind" + "windows": "sd-Sind-IN" }, { "full": "sda-Latn-ID", @@ -99106,7 +103624,7 @@ "iso639_3": "sda", "localnames": [ "Basa Tora'a" ], "name": "Toraja-Sa’dan", - "names": [ "Basa Toraya", "Basa Tora’a", "Sada", "Sadan", "Sadang", "Sa’dan", "Sa’dansche", "South Toraja", "Taeq", "Tae’", "Tator", "Toradja", "Toraja", "Toraja-Sa'dan" ], + "names": [ "Basa Toraya", "Basa Tora’a", "Sada", "Sadan", "Sadang", "Sa’dan", "Sa’dansche", "South Toraja", "Taeq", "Tae’", "Toradja", "Toraja", "Toraja-Sa'dan" ], "region": "ID", "regionname": "Indonesia", "regions": [ "MY" ], @@ -99117,22 +103635,24 @@ "windows": "sda-Latn" }, { - "full": "sdb-Zyyy-IQ", + "full": "sdb-Arab-IQ", "iana": [ "Shabak" ], "iso639_3": "sdb", "name": "Shabak", "region": "IQ", "regionname": "Iraq", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "sdb", - "tags": [ "sdb-IQ", "sdb-Zyyy" ], - "windows": "sdb-Zyyy" + "tags": [ "sdb-Arab", "sdb-IQ" ], + "unwritten": true, + "windows": "sdb-Arab" }, { "full": "sdc-Latn-IT", "iana": [ "Sassarese Sardinian" ], "iso639_3": "sdc", + "macrolang": "sc", "name": "Sardinian, Sassarese", "names": [ "Northwestern Sardinian", "Sassarese", "Sassarese Sardinian" ], "region": "IT", @@ -99159,18 +103679,18 @@ "windows": "sde-Latn" }, { - "full": "sdf-Zyyy-IQ", + "full": "sdf-Arab-IQ", "iana": [ "Sarli" ], "iso639_3": "sdf", "name": "Sarli", "names": [ "Sarliya" ], "region": "IQ", "regionname": "Iraq", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "sdf", - "tags": [ "sdf-IQ", "sdf-Zyyy" ], - "windows": "sdf-Zyyy" + "tags": [ "sdf-Arab", "sdf-IQ" ], + "windows": "sdf-Arab" }, { "full": "sdg-Arab-AF", @@ -99194,6 +103714,7 @@ "iso639_3": "sdh", "latnnames": [ "Kurdî xwarg", "Kurdîyi başûrî" ], "localnames": [ "کوردی خوارگ", "کوردیی باشووری" ], + "macrolang": "ku", "name": "Kurdish, Southern", "names": [ "Kurdî Xwarîn", "Pehlewaní", "Southern Kurdish" ], "region": "IR", @@ -99265,6 +103786,7 @@ "full": "sdn-Latn-IT", "iana": [ "Gallurese Sardinian" ], "iso639_3": "sdn", + "macrolang": "sc", "name": "Sardinian, Gallurese", "names": [ "Gallurese", "Gallurese Sardinian", "Northeastern Sardinian" ], "region": "IT", @@ -99308,6 +103830,7 @@ }, { "full": "sdq-Latn-ID", + "iana": [ "Semandang" ], "iso639_3": "sdq", "localnames": [ "Semandang" ], "name": "Semandang", @@ -99326,12 +103849,12 @@ "iso639_3": "sdr", "name": "Sadri, Oraon", "names": [ "Oraon Sadri", "Pahan" ], - "nophonvars": true, "region": "BD", "regionname": "Bangladesh", "script": "Beng", "sldr": false, - "tag": "sdr-Beng", + "tag": "sdr", + "tags": [ "sdr-BD", "sdr-Beng" ], "windows": "sdr-Beng" }, { @@ -99362,18 +103885,18 @@ "windows": "sdr-Latn" }, { - "full": "sds-Zyyy-TN", + "full": "sds-Arab-TN", "iana": [ "Sened" ], "iso639_3": "sds", "name": "Sened", "region": "TN", "regionname": "Tunisia", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "sds", - "tags": [ "sds-TN", "sds-Zyyy" ], + "tags": [ "sds-Arab", "sds-TN" ], "unwritten": true, - "windows": "sds-Zyyy" + "windows": "sds-Arab" }, { "full": "sdt-Zyyy-FR", @@ -99438,23 +103961,23 @@ "iana": [ "Northern Sami" ], "iso639_3": "sme", "localname": "davvisámegiella", - "localnames": [ "Sámegiella" ], + "localnames": [ "Davvisámegiella", "Sámegiella" ], "name": "Saami, North", - "names": [ "Davvin", "North Sámi", "Northern Lapp", "Northern Saami", "Northern Sami", "Norwegian Saami", "Saame", "Saami", "Same", "Samic", "Sámegiella" ], + "names": [ "Davvin", "Davvisámegiella", "North Sámi", "Northern Lapp", "Northern Saami", "Northern Sami", "Norwegian Saami", "Saame", "Saami", "Same", "Samic", "Sámegiella" ], "region": "NO", "regionname": "Norway", "script": "Latn", "sldr": true, "tag": "se", "tags": [ "se-Latn", "se-NO" ], - "windows": "se-Latn" + "windows": "se-Latn-NO" }, { "full": "se-Cyrl-NO", "iana": [ "Northern Sami" ], "iso639_3": "sme", "name": "Saami, North", - "names": [ "Davvin", "North Sámi", "Northern Lapp", "Northern Saami", "Northern Sami", "Norwegian Saami", "Saame", "Saami", "Same", "Samic", "Sámegiella" ], + "names": [ "Davvin", "Davvisámegiella", "North Sámi", "Northern Lapp", "Northern Saami", "Northern Sami", "Norwegian Saami", "Saame", "Saami", "Same", "Samic", "Sámegiella" ], "nophonvars": true, "region": "NO", "regionname": "Norway", @@ -99462,14 +103985,14 @@ "script": "Cyrl", "sldr": false, "tag": "se-Cyrl", - "windows": "se-Cyrl" + "windows": "se-Cyrl-NO" }, { "full": "se-Latn-FI", "iana": [ "Northern Sami" ], "iso639_3": "sme", "localname": "davvisámegiella", - "localnames": [ "Sámegiella" ], + "localnames": [ "Davvisámegiella", "Sámegiella" ], "name": "Northern Sami", "region": "FI", "regionname": "Finland", @@ -99483,7 +104006,7 @@ "iana": [ "Northern Sami" ], "iso639_3": "sme", "localname": "davvisámegiella", - "localnames": [ "Sámegiella" ], + "localnames": [ "Davvisámegiella", "Sámegiella" ], "name": "Northern Sami", "region": "SE", "regionname": "Sweden", @@ -99615,6 +104138,22 @@ "tags": [ "seh-Latn", "seh-MZ" ], "windows": "seh-Latn" }, + { + "full": "seh-Latn-MZ-x-podzu", + "iana": [ "Sena" ], + "iso639_3": "seh", + "localnames": [ "Cisena" ], + "name": "Sena", + "names": [ "Chisena", "Cisena" ], + "region": "MZ", + "regionname": "Mozambique", + "rod": "16484", + "script": "Latn", + "sldr": false, + "tag": "seh-x-podzu", + "tags": [ "seh-Latn-x-podzu", "seh-MZ-x-podzu" ], + "windows": "seh-Latn-x-podzu" + }, { "full": "sei-Latn-MX", "iana": [ "Seri" ], @@ -99648,9 +104187,9 @@ "full": "sek-Latn-CA", "iana": [ "Sekani" ], "iso639_3": "sek", - "localnames": [ "Tse'khene" ], + "localnames": [ "Tsek'ene" ], "name": "Sekani", - "names": [ "Tsek’ehne", "Tsek’hene", "Tse’khene" ], + "names": [ "Tsek’ehne", "Tsek’ene", "Tsek’hene", "Tse’khene" ], "region": "CA", "regionname": "Canada", "script": "Latn", @@ -99664,7 +104203,7 @@ "iana": [ "Sekani" ], "iso639_3": "sek", "name": "Sekani", - "names": [ "Tsek’ehne", "Tsek’hene", "Tse’khene" ], + "names": [ "Tsek’ehne", "Tsek’ene", "Tsek’hene", "Tse’khene" ], "nophonvars": true, "obsolete": true, "region": "CA", @@ -99709,8 +104248,8 @@ "full": "seo-Latn-PG", "iana": [ "Suarmin" ], "iso639_3": "seo", - "name": "Suarmin", - "names": [ "Akiapmin", "Duranmin" ], + "name": "Asaba", + "names": [ "Akiapmin", "Asabano", "Duranmin", "Suarmin" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -99790,7 +104329,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "set", "tags": [ "set-ID", "set-Latn" ], "windows": "set-Latn" @@ -99814,12 +104353,13 @@ "full": "sev-Latn-CI", "iana": [ "Nyarafolo Senoufo" ], "iso639_3": "sev", + "localnames": [ "Nyaʔafolido" ], "name": "Sénoufo, Nyarafolo", - "names": [ "Niarafolo", "Nyarafolo Senoufo" ], + "names": [ "Niafolo", "Niarafolo", "Nyarafolo Senoufo", "Nyaʔafolido" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "sev", "tags": [ "sev-CI", "sev-Latn" ], "windows": "sev-Latn" @@ -99843,10 +104383,10 @@ "full": "sey-Latn-EC", "iana": [ "Secoya" ], "iso639_3": "sey", - "localname": "Paikoka", - "localnames": [ "Pai Coca" ], - "name": "Secoya", - "names": [ "Aido Pãi", "Encabellao", "Pai Coca", "Pai Koka", "Paikoka", "Pãi kokua", "Siona-Secoya" ], + "localname": "Pai Coca", + "localnames": [ "Paicoca" ], + "name": "Paicoca", + "names": [ "Aido Pãi", "Encabellao", "Pai Coca", "Pai Koka", "Paikoka", "Pãi kokua", "Secoya", "Siona-Secoya" ], "region": "EC", "regionname": "Ecuador", "regions": [ "PE" ], @@ -99904,6 +104444,7 @@ "full": "sfm-Plrd-CN", "iana": [ "Small Flowery Miao" ], "iso639_3": "sfm", + "macrolang": "hmn", "name": "Miao, Small Flowery", "names": [ "Atse", "Gha-Mu", "Ghab-Mvb Ghab-Svd", "Ghuab-Hmongb Ghuab-Soud", "Hsiao Hwa Miao", "Small Flowery Miao", "Xiao Hua Miao" ], "obsolete": true, @@ -99933,8 +104474,9 @@ "full": "sfw-Latn-GH", "iana": [ "Sehwi" ], "iso639_3": "sfw", - "name": "Sehwi", - "names": [ "Asahyue", "Sefwi" ], + "localnames": [ "Esahie" ], + "name": "Esahie", + "names": [ "Asahyue", "Sefwi", "Sehwi" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -99968,7 +104510,7 @@ "name": "Old Irish (to 900)", "region": "IE", "regionname": "Ireland", - "regions": [ "GB" ], + "regions": [ "001", "GB" ], "script": "Ogam", "sldr": false, "tag": "sga", @@ -100008,8 +104550,9 @@ "full": "sgc-Latn-KE", "iana": [ "Kipsigis" ], "iso639_3": "sgc", + "macrolang": "kln", "name": "Kipsigis", - "names": [ "Kipsiikis", "Kipsikiis", "Kipsikis" ], + "names": [ "Kalenjin", "Kipsiikis", "Kipsikiis", "Kipsikis" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -100024,7 +104567,7 @@ "iso639_3": "sgd", "localnames": [ "Surigaonon" ], "name": "Surigaonon", - "names": [ "Jaun-Jaun", "Sinurigao", "Waya-Waya" ], + "names": [ "Cantilangnon", "Jaun-Jaun", "Sinurigao", "Waya-Waya" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -100053,7 +104596,7 @@ "iana": [ "Swiss-German Sign Language" ], "iso639_3": "sgg", "name": "Swiss-German Sign Language", - "names": [ "DGS", "DSGS", "Deutschschweizer Gebärdensprache", "Deutschschweizerische Gebärdensprache", "Natürliche Gebärde" ], + "names": [ "DSGS", "Deutschschweizer Gebärdensprache", "Deutschschweizerische Gebärdensprache", "Natürliche Gebärde" ], "region": "CH", "regionname": "Switzerland", "regions": [ "LI" ], @@ -100063,6 +104606,21 @@ "tags": [ "sgg-CH", "sgg-Zxxx", "sgn-sgg", "sgn-sgg-CH" ], "windows": "sgg-Zxxx" }, + { + "full": "sgh-Cyrl-TJ", + "iana": [ "Shughni" ], + "iso639_3": "sgh", + "name": "Shughni", + "names": [ "Khughnani", "Khughni", "Khughnoni", "Khugnone", "Shighnani", "Shighni", "Shughnani", "Shughnoni", "Shugnan-Rushan", "Shugni" ], + "region": "TJ", + "regionname": "Tajikistan", + "regions": [ "AF" ], + "script": "Cyrl", + "sldr": false, + "tag": "sgh", + "tags": [ "sgh-Cyrl", "sgh-TJ" ], + "windows": "sgh-Cyrl-TJ" + }, { "full": "sgh-Arab-AF", "iana": [ "Shughni" ], @@ -100077,22 +104635,7 @@ "sldr": true, "tag": "sgh-Arab", "tags": [ "sgh-AF" ], - "windows": "sgh-Arab" - }, - { - "full": "sgh-Cyrl-TJ", - "iana": [ "Shughni" ], - "iso639_3": "sgh", - "name": "Shughni", - "names": [ "Khughnani", "Khughni", "Khughnoni", "Khugnone", "Shighnani", "Shighni", "Shughnani", "Shughnoni", "Shugnan-Rushan", "Shugni" ], - "nophonvars": true, - "region": "TJ", - "regionname": "Tajikistan", - "regions": [ "AF" ], - "script": "Cyrl", - "sldr": false, - "tag": "sgh-Cyrl", - "windows": "sgh-Cyrl" + "windows": "sgh-Arab-AF" }, { "full": "sgh-Latn-TJ", @@ -100107,7 +104650,7 @@ "script": "Latn", "sldr": false, "tag": "sgh-Latn", - "windows": "sgh-Latn" + "windows": "sgh-Latn-TJ" }, { "full": "sgi-Latn-CM", @@ -100127,6 +104670,8 @@ "full": "sgj-Deva-IN", "iana": [ "Surgujia" ], "iso639_3": "sgj", + "latnnames": [ "Surgujia" ], + "localnames": [ "सरगुजिया" ], "name": "Surgujia", "names": [ "Suraji", "Surguja", "Surgujia-Chhattisgarhi", "Surjugia" ], "region": "IN", @@ -100184,6 +104729,7 @@ "full": "sgp-Latn-IN", "iana": [ "Singpho" ], "iso639_3": "sgp", + "localnames": [ "Singpho" ], "name": "Singpho", "names": [ "Jingphaw", "Kachin", "Sing-Fo" ], "region": "IN", @@ -100226,19 +104772,19 @@ "windows": "sgs-Latn" }, { - "full": "sgt-Zyyy-BT", + "full": "sgt-Tibt-BT", "iana": [ "Brokpake" ], "iso639_3": "sgt", "name": "Brokpake", "names": [ "Brokpa", "Dakpa", "Damilo", "Dap", "Drokpakay", "Jobikha", "Mera Sagtengpa", "Meragsagstengkha", "Mira Sagtengpa", "Sagtengpa" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "sgt", - "tags": [ "sgt-BT", "sgt-Zyyy" ], + "tags": [ "sgt-BT", "sgt-Tibt" ], "unwritten": true, - "windows": "sgt-Zyyy" + "windows": "sgt-Tibt" }, { "full": "sgu-Latn-ID", @@ -100346,7 +104892,7 @@ "iana": [ "Ninam" ], "iso639_3": "shb", "name": "Ninam", - "names": [ "Shiriana", "Xirianá", "Xirixana", "Yanam" ], + "names": [ "Crichana", "Jawaperi", "Jawari", "Kasrapai", "Shiriana", "Shiriana Casapare", "Xirianá", "Xirixana", "Yanam", "Yanam–Ninam", "Yanomami Ninam" ], "obsolete": true, "region": "BR", "regionname": "Brazil", @@ -100362,7 +104908,7 @@ "iana": [ "Sonde" ], "iso639_3": "shc", "name": "Sonde", - "names": [ "Kilua", "Kisonde", "Kisoonde", "Soonde" ], + "names": [ "Kilua", "Kisonde", "Kisondi", "Kisoonde", "Soonde" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -100391,7 +104937,8 @@ "full": "she-Latn-ET", "iana": [ "Sheko" ], "iso639_3": "she", - "localnames": [ "S'oku noogu" ], + "localname": "S'oku noogu", + "localnames": [ "S’oku noogu" ], "name": "Sheko", "region": "ET", "regionname": "Ethiopia", @@ -100436,12 +104983,14 @@ "full": "shi-Tfng-MA", "iana": [ "Tachelhit" ], "iso639_3": "shi", + "latnnames": [ "taclhiyt" ], "localname": "ⵜⴰⵛⵍⵃⵉⵜ", + "localnames": [ "ⵜⴰⵛⵍⵃⵉⵢⵜ" ], "name": "Tachelhit", - "names": [ "Shilha", "Soussiya", "Southern Shilha", "Susiya", "Tachelheit", "Tachilhit", "Tashelhait", "Tashelhayt", "Tashelheit", "Tashelheyt", "Tashelhit", "Tashilheet", "Tashlhiyt", "Tasoussit" ], + "names": [ "Chleuh", "Shilha", "Soussiya", "Southern Shilha", "Susiya", "Tachelheit", "Tachilhit", "Tashelhait", "Tashelhayt", "Tashelheit", "Tashelheyt", "Tashelhit", "Tashilheet", "Tashlhiyt", "Tasoussit", "Tašlḥiyt" ], "region": "MA", "regionname": "Morocco", - "regions": [ "BE", "DZ", "NL" ], + "regions": [ "BE", "DZ", "EH", "NL" ], "script": "Tfng", "sldr": true, "tag": "shi", @@ -100452,8 +105001,10 @@ "full": "shi-Arab-MA", "iana": [ "Tachelhit" ], "iso639_3": "shi", + "latnnames": [ "tšlḥyt" ], + "localnames": [ "تشلحيت" ], "name": "Tachelhit", - "names": [ "Shilha", "Soussiya", "Southern Shilha", "Susiya", "Tachelheit", "Tachilhit", "Tashelhait", "Tashelhayt", "Tashelheit", "Tashelheyt", "Tashelhit", "Tashilheet", "Tashlhiyt", "Tasoussit" ], + "names": [ "Chleuh", "Shilha", "Soussiya", "Southern Shilha", "Susiya", "Tachelheit", "Tachilhit", "Tashelhait", "Tashelhayt", "Tashelheit", "Tashelheyt", "Tashelhit", "Tashilheet", "Tashlhiyt", "Tasoussit", "Tašlḥiyt" ], "nophonvars": true, "region": "MA", "regionname": "Morocco", @@ -100468,9 +105019,9 @@ "iana": [ "Tachelhit" ], "iso639_3": "shi", "localname": "Tashelḥiyt", - "localnames": [ "Tachelhit" ], + "localnames": [ "Tašlḥiyt" ], "name": "Tachelhit", - "names": [ "Shilha", "Soussiya", "Southern Shilha", "Susiya", "Tachelheit", "Tachilhit", "Tashelhait", "Tashelhayt", "Tashelheit", "Tashelheyt", "Tashelhit", "Tashilheet", "Tashlhiyt", "Tasoussit" ], + "names": [ "Chleuh", "Shilha", "Soussiya", "Southern Shilha", "Susiya", "Tachelheit", "Tachilhit", "Tashelhait", "Tashelhayt", "Tashelheit", "Tashelheyt", "Tashelhit", "Tashilheet", "Tashlhiyt", "Tasoussit", "Tašlḥiyt" ], "region": "MA", "regionname": "Morocco", "regions": [ "BE", "DZ", "NL" ], @@ -100483,9 +105034,9 @@ "full": "shj-Latn-SD", "iana": [ "Shatt" ], "iso639_3": "shj", - "localnames": [ "Caning" ], + "localnames": [ "Ikä Caning" ], "name": "Shatt", - "names": [ "Caning" ], + "names": [ "Caning", "Ikä Caning" ], "region": "SD", "regionname": "Sudan", "script": "Latn", @@ -100499,9 +105050,9 @@ "iana": [ "Shilluk" ], "iso639_3": "shk", "localname": "Colo", - "localnames": [ "Colo" ], + "localnames": [ "Dhøg Cøllø" ], "name": "Shilluk", - "names": [ "Chulla", "Colo", "Dhocolo", "Shulla" ], + "names": [ "Chulla", "Colo", "Cøllø", "Dhocolo", "Dhog Collo", "Dhøg Cøllø", "Shulla" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -100515,7 +105066,7 @@ "iana": [ "Shilluk" ], "iso639_3": "shk", "name": "Shilluk", - "names": [ "Chulla", "Colo", "Dhocolo", "Shulla" ], + "names": [ "Chulla", "Colo", "Cøllø", "Dhocolo", "Dhog Collo", "Dhøg Cøllø", "Shulla" ], "nophonvars": true, "region": "SS", "regionname": "South Sudan", @@ -100568,7 +105119,7 @@ "sldr": true, "tag": "shn", "tags": [ "shn-MM", "shn-Mymr" ], - "windows": "shn-Mymr" + "windows": "shn-Mymr-MM" }, { "full": "shn-Mymr-MM-x-taimau", @@ -100586,7 +105137,7 @@ "sldr": false, "tag": "shn-Mymr-x-taimau", "tags": [ "shn-MM-x-taimau" ], - "windows": "shn-Mymr-x-taimau" + "windows": "shn-Mymr-MM-x-taimau" }, { "full": "shn-Tale-MM", @@ -100601,7 +105152,7 @@ "script": "Tale", "sldr": false, "tag": "shn-Tale", - "windows": "shn-Tale" + "windows": "shn-Tale-MM" }, { "full": "shn-Thai-TH", @@ -100617,7 +105168,7 @@ "sldr": false, "tag": "shn-Thai", "tags": [ "shn-TH" ], - "windows": "shn-Thai" + "windows": "shn-Thai-TH" }, { "full": "sho-Latn-NG", @@ -100693,43 +105244,48 @@ "windows": "shs-Latn" }, { - "full": "sht-Zyyy-US", + "full": "sht-Latn-US", "iana": [ "Shasta" ], "iso639_3": "sht", "name": "Shasta", "names": [ "Sastean", "Shastan" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sht", - "tags": [ "sht-US", "sht-Zyyy" ], + "tags": [ "sht-Latn", "sht-US" ], "unwritten": true, - "windows": "sht-Zyyy" + "windows": "sht-Latn" }, { "full": "shu-Arab-TD", "iana": [ "Chadian Arabic" ], "iso639_3": "shu", - "latnnames": [ "alearabia" ], - "localnames": [ "العربية" ], + "latnnames": [ "alearabia", "alearabiat alttshadia" ], + "localname": "عربي هنا تشاد", + "localnames": [ "العربية", "العربية التشادية" ], + "macrolang": "ar", "name": "Arabic, Chadian Spoken", - "names": [ "Arabe Choa", "Arabic, Shuwa", "Chad Arabic", "Chadian Arabic", "Chadian Spoken Arabic", "Chadic Arabic", "Choa", "Chowa", "L’arabe du Tchad", "Shoa", "Shoa Arabic", "Shua", "Shua Arabic", "Shuwa", "Shuwa Arabic", "Suwa", "Western Sudanic Arabic" ], - "nophonvars": true, + "names": [ "Al-arabi al-Tachaadi", "Arabe Choa", "Arabic, Shuwa", "Baggara Arabic", "Chad Arabic", "Chadian Arabic", "Chadian Spoken Arabic", "Chadic Arabic", "Choa", "Chowa", "L’arabe du Tchad", "Shoa", "Shoa Arabic", "Shua", "Shua Arabic", "Shuwa", "Shuwa Arabic", "Suwa", "West Sudanic Arabic", "Western Sudanic Arabic" ], "region": "TD", "regionname": "Chad", "regions": [ "CF", "CM", "NE", "NG" ], "script": "Arab", "sldr": true, - "tag": "shu-Arab", + "tag": "shu", + "tags": [ "ar-shu", "ar-shu-TD", "shu-Arab", "shu-TD" ], "windows": "shu-Arab" }, { "full": "shu-Latn-TD", "iana": [ "Chadian Arabic" ], "iso639_3": "shu", + "localname": "arabi hana Tchad", + "localnames": [ "Al-arabi al-Tachaadi" ], + "macrolang": "ar", "name": "Arabic, Chadian Spoken", - "names": [ "Arabe Choa", "Arabic, Shuwa", "Chad Arabic", "Chadian Arabic", "Chadian Spoken Arabic", "Chadic Arabic", "Choa", "Chowa", "L’arabe du Tchad", "Shoa", "Shoa Arabic", "Shua", "Shua Arabic", "Shuwa", "Shuwa Arabic", "Suwa", "Western Sudanic Arabic" ], + "names": [ "Al-arabi al-Tachaadi", "Arabe Choa", "Arabic, Shuwa", "Baggara Arabic", "Chad Arabic", "Chadian Arabic", "Chadian Spoken Arabic", "Chadic Arabic", "Choa", "Chowa", "L’arabe du Tchad", "Shoa", "Shoa Arabic", "Shua", "Shua Arabic", "Shuwa", "Shuwa Arabic", "Suwa", "West Sudanic Arabic", "Western Sudanic Arabic" ], "region": "TD", "regionname": "Chad", "regions": [ "CF", "CM", "NE", "NG" ], @@ -100739,7 +105295,7 @@ "windows": "shu-Latn" }, { - "full": "shv-Zyyy-OM", + "full": "shv-Arab-OM", "iana": [ "Shehri" ], "iso639_3": "shv", "name": "Shehri", @@ -100747,11 +105303,11 @@ "region": "OM", "regionname": "Oman", "regions": [ "SA" ], - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "shv", - "tags": [ "shv-OM", "shv-Zyyy" ], - "windows": "shv-Zyyy" + "tags": [ "shv-Arab", "shv-OM" ], + "windows": "shv-Arab" }, { "full": "shw-Latn-SD", @@ -100783,32 +105339,33 @@ "windows": "shx-Zyyy" }, { - "full": "shy-Arab-DZ", + "full": "shy-Latn-DZ", "iana": [ "Tachawit" ], "iso639_3": "shy", "name": "Tachawit", "names": [ "Aurès", "Awras", "Chaouia", "Chawi", "Shawia", "Shawiya", "Tacawit" ], - "nophonvars": true, "region": "DZ", "regionname": "Algeria", - "regions": [ "TN" ], - "script": "Arab", + "script": "Latn", "sldr": false, - "tag": "shy-Arab", - "windows": "shy-Arab" + "tag": "shy", + "tags": [ "shy-DZ", "shy-Latn" ], + "windows": "shy-Latn" }, { - "full": "shy-Latn-DZ", + "full": "shy-Arab-DZ", "iana": [ "Tachawit" ], "iso639_3": "shy", "name": "Tachawit", "names": [ "Aurès", "Awras", "Chaouia", "Chawi", "Shawia", "Shawiya", "Tacawit" ], + "nophonvars": true, "region": "DZ", "regionname": "Algeria", - "script": "Latn", + "regions": [ "TN" ], + "script": "Arab", "sldr": false, - "tag": "shy-Latn", - "windows": "shy-Latn" + "tag": "shy-Arab", + "windows": "shy-Arab" }, { "full": "shy-Tfng-DZ", @@ -100877,7 +105434,7 @@ "iana": [ "Akkala Sami" ], "iso639_3": "sia", "name": "Saami, Akkala", - "names": [ "Ahkkil", "Akkala Sami", "Babino", "Babinsk" ], + "names": [ "Ahkkil", "Ah’kel’ kill", "Akg’-jav’r kill", "Akkala Sami", "Babino", "Babinsk", "Äh’k’el’ k’ell", "Äh’k’el’säm’ k’ell", "Äh’k’el’säm’la" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -100906,7 +105463,7 @@ "full": "sid-Latn-ET", "iana": [ "Sidamo" ], "iso639_3": "sid", - "localname": "Sidaamu Afo", + "localname": "Sidaamu Afoo", "localnames": [ "Sidaamu Afoo" ], "name": "Sidamo", "region": "ET", @@ -100964,8 +105521,10 @@ "full": "sig-Latn-GH", "iana": [ "Paasaal" ], "iso639_3": "sig", + "localname": "Pasaal", + "localnames": [ "Pasaal" ], "name": "Paasaal", - "names": [ "Funsile", "Pasaale", "Pasaale Sisaala", "Southern Sisaala" ], + "names": [ "Funsile", "Pasaal", "Pasaale", "Pasaale Sisaala", "Sisaala-Pasaale", "Southern Sisaala" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -100990,19 +105549,19 @@ "windows": "sih-Latn" }, { - "full": "sii-Zyyy-IN", + "full": "sii-Latn-IN", "iana": [ "Shom Peng" ], "iso639_3": "sii", "name": "Shom Peng", "names": [ "Shobang", "Shom Pen", "Shompen", "Shompeng" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sii", - "tags": [ "sii-IN", "sii-Zyyy" ], + "tags": [ "sii-IN", "sii-Latn" ], "unwritten": true, - "windows": "sii-Zyyy" + "windows": "sii-Latn" }, { "full": "sij-Latn-PG", @@ -101039,8 +105598,9 @@ "full": "sil-Latn-GH", "iana": [ "Tumulung Sisaala" ], "iso639_3": "sil", + "localnames": [ "Isaalɩŋ" ], "name": "Sisaala, Tumulung", - "names": [ "Hissala", "Isaalang", "Isaalung", "Issala", "Sisai", "Sisala Tumu", "Tumulung Sisaala" ], + "names": [ "Hissala", "Isaalang", "Isaalung", "Isaalɩŋ", "Issala", "Sisai", "Sisala Tumu", "Tumulung Sisaala" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -101072,7 +105632,7 @@ "latnnames": [ "'bras ljongs skad" ], "localnames": [ "འབྲས་ལྗོངས་སྐད" ], "name": "Sikkimese", - "names": [ "Bhutia", "Dandzongka", "Danjongka", "Danyouka", "Denjong", "Denjong-skad", "Denjongkha", "Denjongkä", "Denjongpa", "Denjonka", "Denjonke", "Denzong Ke", "Denzongke", "Denzongpe Ke", "Denzongpeke", "Dranjoke", "Dranjongke", "Drendzongké", "Lachengpa", "Lachungpa", "Sikami", "Sikkim Bhotia", "Sikkim Bhutia" ], + "names": [ "Bhutia", "Dandzongka", "Danjongka", "Danyouka", "Denjong", "Denjong-skad", "Denjongke", "Denjongkha", "Denjongkä", "Denjongpa", "Denjonka", "Denjonke", "Denzong Ke", "Denzongke", "Denzongpe Ke", "Denzongpeke", "Dranjoke", "Dranjongke", "Drendzongké", "Lachengpa", "Lachungpa", "Lhoke", "Sikami", "Sikkim Bhotia", "Sikkim Bhutia", "Sikkimese Bhutia" ], "region": "IN", "regionname": "India", "script": "Tibt", @@ -101100,7 +105660,7 @@ "iana": [ "Siri" ], "iso639_3": "sir", "name": "Siri", - "names": [ "Sirawa" ], + "names": [ "Sirawa", "Siryanci" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -101111,19 +105671,19 @@ "windows": "sir-Latn" }, { - "full": "sis-Zyyy-US", + "full": "sis-Latn-US", "iana": [ "Siuslaw" ], "iso639_3": "sis", "name": "Siuslaw", "names": [ "Lower Umpqua", "Siuslawan" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sis", - "tags": [ "sis-US", "sis-Zyyy" ], + "tags": [ "sis-Latn", "sis-US" ], "unwritten": true, - "windows": "sis-Zyyy" + "windows": "sis-Latn" }, { "full": "siu-Latn-PG", @@ -101145,7 +105705,7 @@ "iana": [ "Sumariup" ], "iso639_3": "siv", "name": "Sumariup", - "names": [ "Latoma", "Sogoba" ], + "names": [ "Latoma", "Smajof", "Sogoba", "Sohoba", "Togoba" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -101160,8 +105720,8 @@ "iana": [ "Siwai" ], "iso639_3": "siw", "localnames": [ "Motuna" ], - "name": "Siwai", - "names": [ "Motuna", "Siuai" ], + "name": "Motuna", + "names": [ "Siuai", "Siwai" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -101185,17 +105745,17 @@ "windows": "six-Latn" }, { - "full": "siy-Zyyy-IR", + "full": "siy-Arab-IR", "iana": [ "Sivandi" ], "iso639_3": "siy", "name": "Sivandi", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "siy", - "tags": [ "siy-IR", "siy-Zyyy" ], - "windows": "siy-Zyyy" + "tags": [ "siy-Arab", "siy-IR" ], + "windows": "siy-Arab" }, { "full": "siz-Arab-EG", @@ -101218,9 +105778,9 @@ "iana": [ "Epena" ], "iso639_3": "sja", "localname": "Epéna Pedée", - "localnames": [ "Epéna Pedée" ], + "localnames": [ "Eperã pedee" ], "name": "Epena", - "names": [ "Cholo", "Embena", "Embera", "Emberá", "Emberá Chami", "Emberá del Sur", "Emberá-Saija", "Epena Saija", "Epená", "Epená Saija", "Epera Pedede", "Epéna Pedée", "Saija", "Siapedie", "Southern Embera", "Southern Empera", "Southern Epena", "Êpera" ], + "names": [ "Cholo", "Embena", "Embera", "Emberá", "Emberá Chami", "Emberá del Sur", "Emberá-Saija", "Epena Pedee Saija", "Epena Saija", "Epená Saija", "Epera Pedede", "Eperara", "Eperã pedee", "Epéna Pedée", "Saija", "Siapedie", "Southern Embera", "Southern Empera", "Southern Epena", "Êpera" ], "region": "CO", "regionname": "Colombia", "regions": [ "EC", "PA" ], @@ -101262,19 +105822,20 @@ "windows": "sjd-Cyrl" }, { - "full": "sje-Zyyy-SE", + "full": "sje-Latn-SE", "iana": [ "Pite Sami" ], "iso639_3": "sje", + "localnames": [ "Bidumsáme", "Bidumsámegiella" ], "name": "Saami, Pite", - "names": [ "Arjeplog Saami", "Pite", "Pite Sami", "Saami" ], + "names": [ "Arjeplog Saami", "Bidumsáme", "Bidumsámegiella", "Pite", "Pite Sami", "Saami" ], "region": "SE", "regionname": "Sweden", "regions": [ "NO" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sje", - "tags": [ "sje-SE", "sje-Zyyy" ], - "windows": "sje-Zyyy" + "tags": [ "sje-Latn", "sje-SE" ], + "windows": "sje-Latn" }, { "full": "sjg-Latn-TD", @@ -101292,16 +105853,16 @@ "windows": "sjg-Latn" }, { - "full": "sjk-Zyyy-XX", + "full": "sjk-Latn-XX", "iana": [ "Kemi Sami" ], "iso639_3": "sjk", "name": "Kemi Sami", "region": "XX", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sjk", - "tags": [ "sjk-XX", "sjk-Zyyy" ], - "windows": "sjk-Zyyy" + "tags": [ "sjk-Latn", "sjk-XX" ], + "windows": "sjk-Latn" }, { "full": "sjl-Latn-IN", @@ -101321,9 +105882,9 @@ "full": "sjm-Latn-PH", "iana": [ "Mapun" ], "iso639_3": "sjm", - "localnames": [ "Pullun Mapun" ], + "localnames": [ "Pallun Mapun" ], "name": "Mapun", - "names": [ "Bajau Kagayan", "Cagayan", "Cagayan de Sulu", "Cagayanen", "Cagayano", "Cagayanon", "Jama Mapun", "Kagayan", "Orang", "Orang Cagayan", "Pellun Mapun", "Pullun Mapun", "Sama Mapun" ], + "names": [ "Bajau Kagayan", "Cagayan", "Cagayan de Sulu", "Cagayanen", "Cagayano", "Cagayanon", "Jama Mapun", "Kagayan", "Orang", "Orang Cagayan", "Pallun Mapun", "Pellun Mapun", "Pullun Mapun", "Sama Mapun" ], "region": "PH", "regionname": "Philippines", "regions": [ "MY" ], @@ -101334,17 +105895,30 @@ "windows": "sjm-Latn" }, { - "full": "sjn-Zyyy-001", + "full": "sjn-Teng-001", "iana": [ "Sindarin" ], "iso639_3": "sjn", "name": "Sindarin", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Teng", "sldr": false, "tag": "sjn", - "tags": [ "sjn-001", "sjn-Zyyy" ], - "windows": "sjn-Zyyy" + "tags": [ "sjn-001", "sjn-Teng" ], + "windows": "sjn-Teng" + }, + { + "full": "sjn-Cirt-001", + "iana": [ "Sindarin" ], + "iso639_3": "sjn", + "name": "Sindarin", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Cirt", + "sldr": false, + "tag": "sjn-Cirt", + "windows": "sjn-Cirt" }, { "full": "sjo-Mong-CN-x-sibe", @@ -101358,7 +105932,7 @@ "sldr": false, "tag": "sjo", "tags": [ "sjo-CN", "sjo-CN-x-sibe", "sjo-Mong", "sjo-Mong-CN", "sjo-Mong-x-sibe", "sjo-x-sibe" ], - "windows": "sjo-Mong" + "windows": "sjo-Mong-x-sibe" }, { "full": "sjo-Cyrl-CN", @@ -101408,8 +105982,10 @@ "full": "sjp-Deva-IN", "iana": [ "Surjapuri" ], "iso639_3": "sjp", + "latnnames": [ "Surjapuri" ], + "localnames": [ "सुरजापुरी" ], "name": "Surjapuri", - "names": [ "Sura" ], + "names": [ "Deshi Bhasa", "Sura", "Surajpuri" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -101418,6 +105994,20 @@ "tags": [ "sjp-Deva", "sjp-IN" ], "windows": "sjp-Deva" }, + { + "full": "sjp-Beng-IN", + "iana": [ "Surjapuri" ], + "iso639_3": "sjp", + "name": "Surjapuri", + "names": [ "Deshi Bhasa", "Sura", "Surajpuri" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Beng", + "sldr": false, + "tag": "sjp-Beng", + "windows": "sjp-Beng" + }, { "full": "sjr-Latn-PG", "iana": [ "Siar-Lak" ], @@ -101544,25 +106134,26 @@ "windows": "ska-Latn" }, { - "full": "skb-Zyyy-LA", + "full": "skb-Thai-TH", "iana": [ "Saek" ], "iso639_3": "skb", "name": "Saek", "names": [ "Seak", "Sek", "Set", "Tai Sek", "Xaek", "Xec", "Xek" ], - "region": "LA", - "regionname": "Lao People's Democratic Republic", - "regions": [ "TH" ], - "script": "Zyyy", + "region": "TH", + "regionname": "Thailand", + "regions": [ "LA" ], + "script": "Thai", "sldr": false, "tag": "skb", - "tags": [ "skb-LA", "skb-Zyyy" ], + "tags": [ "skb-TH", "skb-Thai" ], "unwritten": true, - "windows": "skb-Zyyy" + "windows": "skb-Thai" }, { "full": "skc-Latn-PG", "iana": [ "Ma Manda" ], "iso639_3": "skc", + "localname": "Ma Manda", "localnames": [ "Ma Manda" ], "name": "Ma Manda", "names": [ "Sauk" ], @@ -101623,6 +106214,7 @@ "full": "skg-Latn-MG", "iana": [ "Sakalava Malagasy" ], "iso639_3": "skg", + "macrolang": "mg", "name": "Malagasy, Sakalava", "names": [ "Sakalava Malagasy" ], "region": "MG", @@ -101665,25 +106257,25 @@ "windows": "ski-Latn" }, { - "full": "skj-Zyyy-NP", + "full": "skj-Deva-NP", "iana": [ "Seke (Nepal)" ], "iso639_3": "skj", "name": "Seke", "names": [ "Seke (Nepal)" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "skj", - "tags": [ "skj-NP", "skj-Zyyy" ], + "tags": [ "skj-Deva", "skj-NP" ], "unwritten": true, - "windows": "skj-Zyyy" + "windows": "skj-Deva" }, { "full": "skm-Latn-PG", "iana": [ "Kutong" ], "iso639_3": "skm", - "localnames": [ "Kutong gin" ], + "localnames": [ "Kutong", "Kutong gin" ], "name": "Kutong", "names": [ "Dinangat", "Kutong gin", "Sakam" ], "region": "PG", @@ -101760,6 +106352,7 @@ "latnnames": [ "Saraiki" ], "localname": "سرائیکی", "localnames": [ "سرائیکی" ], + "macrolang": "lah", "name": "Saraiki", "names": [ "Belochki", "Multani", "Mutani", "Seraiki", "Siraiki" ], "region": "PK", @@ -101769,12 +106362,13 @@ "sldr": true, "tag": "skr", "tags": [ "skr-Arab", "skr-PK" ], - "windows": "skr-Arab" + "windows": "skr-Arab-PK" }, { "full": "skr-Deva-PK", "iana": [ "Saraiki", "Seraiki" ], "iso639_3": "skr", + "macrolang": "lah", "name": "Saraiki", "names": [ "Belochki", "Multani", "Mutani", "Seraiki", "Siraiki" ], "nophonvars": true, @@ -101785,12 +106379,13 @@ "script": "Deva", "sldr": false, "tag": "skr-Deva", - "windows": "skr-Deva" + "windows": "skr-Deva-PK" }, { "full": "skr-Guru-PK", "iana": [ "Saraiki", "Seraiki" ], "iso639_3": "skr", + "macrolang": "lah", "name": "Saraiki", "names": [ "Belochki", "Multani", "Mutani", "Seraiki", "Siraiki" ], "nophonvars": true, @@ -101801,12 +106396,13 @@ "script": "Guru", "sldr": false, "tag": "skr-Guru", - "windows": "skr-Guru" + "windows": "skr-Guru-PK" }, { "full": "skr-Khoj-PK", "iana": [ "Saraiki", "Seraiki" ], "iso639_3": "skr", + "macrolang": "lah", "name": "Saraiki", "names": [ "Belochki", "Multani", "Mutani", "Seraiki", "Siraiki" ], "nophonvars": true, @@ -101816,7 +106412,25 @@ "script": "Khoj", "sldr": false, "tag": "skr-Khoj", - "windows": "skr-Khoj" + "windows": "skr-Khoj-PK" + }, + { + "full": "skr-Mult-IN", + "iana": [ "Saraiki", "Seraiki" ], + "iso639_3": "skr", + "macrolang": "lah", + "name": "Saraiki", + "names": [ "Belochki", "Multani", "Mutani", "Seraiki", "Siraiki" ], + "nophonvars": true, + "obsolete": true, + "region": "IN", + "regionname": "India", + "regions": [ "PK" ], + "script": "Mult", + "sldr": false, + "tag": "skr-Mult", + "tags": [ "skr-IN" ], + "windows": "skr-Mult-IN" }, { "full": "sks-Latn-PG", @@ -101880,7 +106494,8 @@ "full": "skw-Latn-GY", "iana": [ "Skepi Creole Dutch" ], "iso639_3": "skw", - "name": "Skepi Creole Dutch", + "name": "Skepi Dutch Creole", + "names": [ "Skepi Creole Dutch" ], "region": "GY", "regionname": "Guyana", "script": "Latn", @@ -101894,9 +106509,9 @@ "full": "skx-Latn-ID", "iana": [ "Seko Padang" ], "iso639_3": "skx", - "localnames": [ "Sua Tu Padang" ], + "localnames": [ "sua: tu Padang" ], "name": "Seko Padang", - "names": [ "Seko", "Sua Tu Padang", "Wono" ], + "names": [ "Seko", "Wono", "sua: tu Padang" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -101917,7 +106532,6 @@ "sldr": false, "tag": "sky", "tags": [ "sky-Latn", "sky-SB" ], - "unwritten": true, "windows": "sky-Latn" }, { @@ -101942,7 +106556,7 @@ "localname": "slovenščina", "localnames": [ "Slovenski jezik", "Slovenščina" ], "name": "Slovene", - "names": [ "Slovenian", "Slovenski jezik", "Slovenščina", "Vintak" ], + "names": [ "Slovenian", "Slovenski jezik", "Slovenščina" ], "region": "SI", "regionname": "Slovenia", "regions": [ "AR", "AT", "CA", "DE", "HR", "HU", "IT", "RS", "RU", "US" ], @@ -101959,7 +106573,7 @@ "iana": [ "Slovenian" ], "iso639_3": "slv", "name": "Slovene", - "names": [ "Slovenian", "Slovenski jezik", "Slovenščina", "Vintak" ], + "names": [ "Slovenian", "Slovenski jezik", "Slovenščina" ], "nophonvars": true, "region": "SI", "regionname": "Slovenia", @@ -102005,7 +106619,7 @@ "iana": [ "Sholaga" ], "iso639_3": "sle", "name": "Sholaga", - "names": [ "Kadu Sholigar", "Sholanayika", "Sholiga", "Sholigar", "Solaga", "Solagaru mattu", "Solanayakkans", "Solega", "Soliga", "Soligar" ], + "names": [ "Kadu Sholigar", "Sholanayika", "Sholiga", "Sholigar", "Sikiligar", "Solaga", "Solagaru mattu", "Solanayakkans", "Solega", "Soliga", "Soligar" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -102064,7 +106678,7 @@ "iana": [ "Lower Silesian" ], "iso639_3": "sli", "name": "Silesian, Lower", - "names": [ "Lower Silesian", "Upper Schlesisch" ], + "names": [ "Lower Silesian", "Schlesisch", "Schlesische Sprache", "Schläsche Sproache", "Silesian German", "Upper Schlesisch" ], "region": "PL", "regionname": "Poland", "regions": [ "CZ" ], @@ -102094,8 +106708,8 @@ "iso639_3": "sll", "localname": "Yui", "localnames": [ "Yui" ], - "name": "Salt-Yui", - "names": [ "Iui", "Salt", "Salt-Iui", "Yui" ], + "name": "Yui", + "names": [ "Iui", "Salt", "Salt-Iui", "Salt-Yui" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -102121,25 +106735,25 @@ "windows": "slm-Latn" }, { - "full": "sln-Zyyy-US", + "full": "sln-Latn-US", "iana": [ "Salinan" ], "iso639_3": "sln", "name": "Salinan", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sln", - "tags": [ "sln-US", "sln-Zyyy" ], + "tags": [ "sln-Latn", "sln-US" ], "unwritten": true, - "windows": "sln-Zyyy" + "windows": "sln-Latn" }, { "full": "slp-Latn-ID", "iana": [ "Lamaholot" ], "iso639_3": "slp", "name": "Lamaholot", - "names": [ "Florenese", "Om", "Orang Timur", "Solor", "Solorese", "Timorese" ], + "names": [ "Solor", "Solorese" ], "region": "ID", "regionname": "Indonesia", "regions": [ "MY" ], @@ -102151,33 +106765,33 @@ "windows": "slp-Latn" }, { - "full": "slq-Zyyy-IR", + "full": "slq-Arab-IR", "iana": [ "Salchuq" ], "iso639_3": "slq", "name": "Salchuq", - "names": [ "Seljuq", "Seljūq" ], + "obsolete": true, "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "slq", - "tags": [ "slq-IR", "slq-Zyyy" ], - "windows": "slq-Zyyy" + "tags": [ "slq-Arab", "slq-IR" ], + "windows": "slq-Arab" }, { - "full": "slr-Zyyy-CN", + "full": "slr-Latn-CN", "iana": [ "Salar" ], "iso639_3": "slr", "name": "Salar", "names": [ "Sala" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "slr", - "tags": [ "slr-CN", "slr-Zyyy" ], + "tags": [ "slr-CN", "slr-Latn" ], "unwritten": true, - "windows": "slr-Zyyy" + "windows": "slr-Latn" }, { "full": "sls-Zxxx-SG", @@ -102218,7 +106832,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "slu", "tags": [ "slu-ID", "slu-Latn" ], "windows": "slu-Latn" @@ -102317,6 +106931,7 @@ "full": "smb-Latn-PG", "iana": [ "Simbari" ], "iso639_3": "smb", + "localnames": [ "Simbari" ], "name": "Simbari", "names": [ "Chimbari" ], "region": "PG", @@ -102331,8 +106946,8 @@ "full": "smc-Latn-PG", "iana": [ "Som" ], "iso639_3": "smc", - "name": "Som", - "names": [ "Dinanget", "Somm", "Tayatuk" ], + "name": "Tayatuk", + "names": [ "Dinanget", "Som", "Somm" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -102342,21 +106957,6 @@ "unwritten": true, "windows": "smc-Latn" }, - { - "full": "smd-Latn-AO", - "iana": [ "Sama" ], - "iso639_3": "smd", - "name": "Sama", - "names": [ "Kissama", "Quissama" ], - "region": "AO", - "regionname": "Angola", - "script": "Latn", - "sldr": false, - "tag": "smd", - "tags": [ "smd-AO", "smd-Latn" ], - "unwritten": true, - "windows": "smd-Latn" - }, { "full": "smf-Latn-PG", "iana": [ "Auwe" ], @@ -102421,8 +107021,8 @@ "full": "smk-Latn-PH", "iana": [ "Bolinao" ], "iso639_3": "smk", - "localname": "Binu-Bolinao", - "localnames": [ "Binobolinao" ], + "localname": "Binobolinao", + "localnames": [ "Binubolinao", "Bolinao" ], "name": "Bolinao", "names": [ "Bino-Bolinao", "Binobolinao", "Binubolinao", "Binubulinao", "Boliano", "Bolinao Sambal", "Bolinao Zambal", "Bulinaw", "Sambal Bolinao" ], "region": "PH", @@ -102470,9 +107070,9 @@ "iana": [ "Inari Sami" ], "iso639_3": "smn", "localname": "anarâškielâ", - "localnames": [ "Anarâškielâ" ], + "localnames": [ "Aanaar Saami", "Anarâškielâ" ], "name": "Saami, Inari", - "names": [ "Anar", "Anarâškielâ", "Inari Sami", "Saam", "Saame", "Samic", "Sámi" ], + "names": [ "Aanaar Saami", "Anar", "Anarâškielâ", "Inari Sami", "Saam", "Saame", "Samic", "Sámi" ], "region": "FI", "regionname": "Finland", "script": "Latn", @@ -102494,7 +107094,7 @@ "sldr": false, "tag": "smp", "tags": [ "smp-IL", "smp-Samr" ], - "windows": "smp-Samr" + "windows": "smp-Samr-IL" }, { "full": "smp-Arab-PS", @@ -102509,7 +107109,7 @@ "script": "Arab", "sldr": false, "tag": "smp-Arab", - "windows": "smp-Arab" + "windows": "smp-Arab-PS" }, { "full": "smp-Hebr-PS", @@ -102525,7 +107125,7 @@ "script": "Hebr", "sldr": false, "tag": "smp-Hebr", - "windows": "smp-Hebr" + "windows": "smp-Hebr-PS" }, { "full": "smq-Latn-PG", @@ -102562,17 +107162,35 @@ "full": "sms-Latn-FI", "iana": [ "Skolt Sami" ], "iso639_3": "sms", + "localname": "sääʹmǩiõll", "localnames": [ "Sääʹmǩiõll" ], "name": "Saami, Skolt", - "names": [ "Kolta", "Koltta", "Lopar", "Nuõrttsääʹmǩiõll", "Russian Lapp", "Saam", "Saame", "Saami", "Same", "Skolt", "Skolt Sami", "Sääʹmǩiõll" ], + "names": [ "Kolta", "Koltta", "Nuõrttsääʹmǩiõll", "Saam", "Saame", "Saami", "Same", "Skolt", "Skolt Sami", "Sääʹmǩiõll" ], "region": "FI", "regionname": "Finland", - "regions": [ "RU" ], + "regions": [ "NO", "RU" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "sms", "tags": [ "sms-FI", "sms-Latn" ], - "windows": "sms-Latn" + "windows": "sms-Latn-FI" + }, + { + "full": "sms-Cyrl-RU", + "iana": [ "Skolt Sami" ], + "iso639_3": "sms", + "name": "Saami, Skolt", + "names": [ "Kolta", "Koltta", "Nuõrttsääʹmǩiõll", "Saam", "Saame", "Saami", "Same", "Skolt", "Skolt Sami", "Sääʹmǩiõll" ], + "nophonvars": true, + "obsolete": true, + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "FI", "NO" ], + "script": "Cyrl", + "sldr": false, + "tag": "sms-Cyrl", + "tags": [ "sms-RU" ], + "windows": "sms-Cyrl-RU" }, { "full": "smt-Latn-IN", @@ -102589,23 +107207,24 @@ "windows": "smt-Latn" }, { - "full": "smu-Zyyy-KH", + "full": "smu-Khmr-KH", "iana": [ "Somray" ], "iso639_3": "smu", "name": "Somray", "region": "KH", "regionname": "Cambodia", - "script": "Zyyy", + "script": "Khmr", "sldr": false, "tag": "smu", - "tags": [ "smu-KH", "smu-Zyyy" ], - "windows": "smu-Zyyy" + "tags": [ "smu-KH", "smu-Khmr" ], + "windows": "smu-Khmr" }, { "full": "smv-Zyyy-IN", "iana": [ "Samvedi" ], "iso639_3": "smv", "name": "Samvedi", + "names": [ "Kadodi" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -102634,7 +107253,7 @@ "iana": [ "Samba" ], "iso639_3": "smx", "name": "Samba", - "names": [ "Saamba", "Shankadi", "Tsaam", "Tsamba", "Usamba" ], + "names": [ "Ntsambaan", "Saamba", "Sambaan", "Shankadi", "Tsaam", "Tsamba", "Usamba" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -102705,21 +107324,6 @@ "tag": "sn-Brai", "windows": "sn-Brai" }, - { - "full": "snb-Latn-MY", - "iana": [ "Sebuyau" ], - "iso639_3": "snb", - "name": "Sebuyau", - "names": [ "Sabuyan", "Sabuyau", "Sibuian", "Sibuyan", "Sibuyau" ], - "region": "MY", - "regionname": "Malaysia", - "script": "Latn", - "sldr": false, - "tag": "snb", - "tags": [ "snb-Latn", "snb-MY" ], - "unwritten": true, - "windows": "snb-Latn" - }, { "full": "snc-Latn-PG", "iana": [ "Sinaugoro" ], @@ -102744,6 +107348,7 @@ "names": [ "Bau Bidayuh", "Bau-Jagoi", "Jaggoi", "Jagoi", "Sarawak Dayak" ], "region": "MY", "regionname": "Malaysia", + "regions": [ "ID" ], "script": "Latn", "sldr": false, "tag": "sne", @@ -102755,9 +107360,9 @@ "iana": [ "Noon" ], "iso639_3": "snf", "localname": "Noon", - "localnames": [ "Noon" ], + "localnames": [ "Kinoon" ], "name": "Noon", - "names": [ "Non", "None", "Serer", "Serer-Noon" ], + "names": [ "Kinoon", "Non", "None", "Serer", "Serer-Noon" ], "region": "SN", "regionname": "Senegal", "script": "Latn", @@ -102844,7 +107449,7 @@ "localname": "Sooninkanxanne", "localnames": [ "Sooninkanxanne" ], "name": "Soninke", - "names": [ "Aswanek", "Aswanik", "Azer", "Ceddo", "Cheddo", "Gangara", "Genger", "Kwara", "Maraka", "Marka", "Markaajo", "Markakan", "Sarakole", "Sarakolle", "Sarakolé", "Sarakule", "Sarakulle", "Sarangkole", "Sarangkolle", "Saraxuli", "Sebbe", "Serahule", "Serecole", "Soninkanxanne", "Sooninkanxanne", "Sooninke", "Wakkore", "Wankara" ], + "names": [ "Aswanek", "Aswanik", "Azer", "Ceddo", "Cheddo", "Gangara", "Genger", "Kwara", "Maraka", "Marka", "Markaajo", "Markakan", "Sarahulanxanne", "Sarakole", "Sarakolle", "Sarakolé", "Sarakule", "Sarakulle", "Sarangkole", "Sarangkolle", "Saraxuli", "Sebbe", "Serahule", "Serecole", "Soninkanxanne", "Sooninkanxanne", "Sooninke", "Wakkore", "Wankara" ], "region": "ML", "regionname": "Mali", "regions": [ "CI", "GA", "GM", "GN", "GW", "MR", "SN", "US" ], @@ -102860,7 +107465,7 @@ "iso639_3": "snk", "localname": "Sooninkanxanne", "name": "Soninke", - "names": [ "Aswanek", "Aswanik", "Azer", "Ceddo", "Cheddo", "Gangara", "Genger", "Kwara", "Maraka", "Marka", "Markaajo", "Markakan", "Sarakole", "Sarakolle", "Sarakule", "Sarakulle", "Sarangkole", "Sarangkolle", "Saraxuli", "Sebbe", "Serahule", "Serecole", "Soninkanxanne", "Sooninkanxanne", "Sooninke", "Wakkore", "Wankara" ], + "names": [ "Aswanek", "Aswanik", "Azer", "Ceddo", "Cheddo", "Gangara", "Genger", "Kwara", "Maraka", "Marka", "Markaajo", "Markakan", "Sarahulanxanne", "Sarakole", "Sarakolle", "Sarakule", "Sarakulle", "Sarangkole", "Sarangkolle", "Saraxuli", "Sebbe", "Serahule", "Serecole", "Soninkanxanne", "Sooninkanxanne", "Sooninke", "Wakkore", "Wankara" ], "nophonvars": true, "region": "ML", "regionname": "Mali", @@ -102889,8 +107494,9 @@ "full": "snm-Latn-UG", "iana": [ "Southern Ma'di" ], "iso639_3": "snm", + "localnames": [ "Maꞌdi Urúléti" ], "name": "Ma’di, Southern", - "names": [ "Southern Ma'di" ], + "names": [ "Ma’di Urule", "Maꞌdi Urúléti", "Southern Ma'di" ], "region": "UG", "regionname": "Uganda", "script": "Latn", @@ -102903,6 +107509,7 @@ "full": "snn-Latn-CO", "iana": [ "Siona" ], "iso639_3": "snn", + "localname": "Gantëya coca", "localnames": [ "Gantëya coca" ], "name": "Siona", "names": [ "Baicoca", "Ceona", "Ganteya", "Ganteya bain", "Gantëya coca", "Kanú", "Koka", "Kokakañú", "Pai Coca", "Pioche-Sioni", "Piohé", "Pioje", "Pioje-Sioni", "Siona-Secoya", "Sioni", "Zeona" ], @@ -102943,6 +107550,66 @@ "tags": [ "snp-Latn", "snp-PG" ], "windows": "snp-Latn" }, + { + "full": "snp-Latn-PG-x-alango", + "iana": [ "Siane" ], + "iso639_3": "snp", + "name": "Siane", + "names": [ "Siani", "Siyane" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "20085", + "script": "Latn", + "sldr": false, + "tag": "snp-x-alango", + "tags": [ "snp-Latn-x-alango", "snp-PG-x-alango" ], + "windows": "snp-Latn-x-alango" + }, + { + "full": "snp-Latn-PG-x-galeka", + "iana": [ "Siane" ], + "iso639_3": "snp", + "name": "Siane", + "names": [ "Siani", "Siyane" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "20081", + "script": "Latn", + "sldr": false, + "tag": "snp-x-galeka", + "tags": [ "snp-Latn-x-galeka", "snp-PG-x-galeka" ], + "windows": "snp-Latn-x-galeka" + }, + { + "full": "snp-Latn-PG-x-keto", + "iana": [ "Siane" ], + "iso639_3": "snp", + "name": "Siane", + "names": [ "Siani", "Siyane" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "20084", + "script": "Latn", + "sldr": false, + "tag": "snp-x-keto", + "tags": [ "snp-Latn-x-keto", "snp-PG-x-keto" ], + "windows": "snp-Latn-x-keto" + }, + { + "full": "snp-Latn-PG-x-kolepa", + "iana": [ "Siane" ], + "iso639_3": "snp", + "name": "Siane", + "names": [ "Siani", "Siyane" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "20079", + "script": "Latn", + "sldr": false, + "tag": "snp-x-kolepa", + "tags": [ "snp-Latn-x-kolepa", "snp-PG-x-kolepa" ], + "windows": "snp-Latn-x-kolepa" + }, { "full": "snp-Latn-PG-x-lambau", "iana": [ "Siane" ], @@ -102957,6 +107624,21 @@ "tags": [ "snp-Latn-x-lambau", "snp-PG-x-lambau" ], "windows": "snp-Latn-x-lambau" }, + { + "full": "snp-Latn-PG-x-ona", + "iana": [ "Siane" ], + "iso639_3": "snp", + "name": "Siane", + "names": [ "Siani", "Siyane" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "20083", + "script": "Latn", + "sldr": false, + "tag": "snp-x-ona", + "tags": [ "snp-Latn-x-ona", "snp-PG-x-ona" ], + "windows": "snp-Latn-x-ona" + }, { "full": "snq-Latn-GA", "iana": [ "Sangu (Gabon)" ], @@ -103067,9 +107749,9 @@ "iana": [ "Saniyo-Hiyewe" ], "iso639_3": "sny", "localname": "Hiyewe", - "localnames": [ "Hiyewe" ], + "localnames": [ "Saniyo-Hiyewe" ], "name": "Saniyo-Hiyewe", - "names": [ "Hiyewe", "Sanio-Hiowe" ], + "names": [ "Sanio-Hiowe" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -103109,7 +107791,7 @@ "suppress": true, "tag": "so", "tags": [ "so-Latn", "so-SO" ], - "windows": "so" + "windows": "so-SO" }, { "full": "so-Arab-SO", @@ -103126,7 +107808,7 @@ "script": "Arab", "sldr": true, "tag": "so-Arab", - "windows": "so-Arab" + "windows": "so-Arab-SO" }, { "full": "so-Latn-DJ", @@ -103139,9 +107821,8 @@ "regionname": "Djibouti", "script": "Latn", "sldr": true, - "suppress": true, "tag": "so-DJ", - "windows": "so-DJ" + "windows": "so-Latn-DJ" }, { "full": "so-Latn-ET", @@ -103154,9 +107835,8 @@ "regionname": "Ethiopia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "so-ET", - "windows": "so-ET" + "windows": "so-Latn-ET" }, { "full": "so-Latn-KE", @@ -103169,9 +107849,8 @@ "regionname": "Kenya", "script": "Latn", "sldr": true, - "suppress": true, "tag": "so-KE", - "windows": "so-KE" + "windows": "so-Latn-KE" }, { "full": "so-Osma-SO", @@ -103187,7 +107866,7 @@ "script": "Osma", "sldr": false, "tag": "so-Osma", - "windows": "so-Osma" + "windows": "so-Osma-SO" }, { "full": "soa-Tavt-TH", @@ -103264,8 +107943,8 @@ "full": "soe-Latn-CD", "iana": [ "Songomeno" ], "iso639_3": "soe", - "name": "Songomeno", - "names": [ "Hendo", "Lohendo" ], + "name": "Ohendo", + "names": [ "Hendo", "Lohendo", "Songomeno" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -103276,28 +107955,29 @@ "windows": "soe-Latn" }, { - "full": "sog-Mani-XX", + "full": "sog-Sogd-UZ", "iana": [ "Sogdian" ], "iso639_3": "sog", "name": "Sogdian", - "nophonvars": true, - "region": "XX", - "script": "Mani", + "region": "UZ", + "regionname": "Uzbekistan", + "script": "Sogd", "sldr": false, - "tag": "sog-Mani", - "windows": "sog-Mani" + "tag": "sog", + "tags": [ "sog-Sogd", "sog-UZ" ], + "windows": "sog-Sogd-UZ" }, { - "full": "sog-Sogd-XX", + "full": "sog-Mani-XX", "iana": [ "Sogdian" ], "iso639_3": "sog", "name": "Sogdian", "nophonvars": true, "region": "XX", - "script": "Sogd", + "script": "Mani", "sldr": false, - "tag": "sog-Sogd", - "windows": "sog-Sogd" + "tag": "sog-Mani", + "windows": "sog-Mani-XX" }, { "full": "sog-Sogo-XX", @@ -103309,7 +107989,7 @@ "script": "Sogo", "sldr": false, "tag": "sog-Sogo", - "windows": "sog-Sogo" + "windows": "sog-Sogo-XX" }, { "full": "sog-Syrc-XX", @@ -103321,14 +108001,14 @@ "script": "Syrc", "sldr": false, "tag": "sog-Syrc", - "windows": "sog-Syrc" + "windows": "sog-Syrc-XX" }, { "full": "soh-Zyyy-SD", "iana": [ "Aka" ], "iso639_3": "soh", "name": "Aka", - "names": [ "Fa-C-Aka", "Jebel Silak", "Jebels Sillok", "Sillok" ], + "names": [ "Fa-c-aka", "Jebel Silak", "Jebels Sillok", "Sillok" ], "region": "SD", "regionname": "Sudan", "script": "Zyyy", @@ -103339,19 +108019,19 @@ "windows": "soh-Zyyy" }, { - "full": "soi-Zyyy-NP", + "full": "soi-Deva-NP", "iana": [ "Sonha" ], "iso639_3": "soi", "name": "Sonha", "names": [ "Sonaha", "Sonahaa", "Sunah", "Sunha" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "soi", - "tags": [ "soi-NP", "soi-Zyyy" ], + "tags": [ "soi-Deva", "soi-NP" ], "unwritten": true, - "windows": "soi-Zyyy" + "windows": "soi-Deva" }, { "full": "soj-Zyyy-IR", @@ -103370,7 +108050,10 @@ "full": "sok-Latn-TD", "iana": [ "Sokoro" ], "iso639_3": "sok", + "localname": "Soŋoroŋ", + "localnames": [ "Soŋoroŋ" ], "name": "Sokoro", + "names": [ "Soŋoroŋ" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -103399,14 +108082,13 @@ "iana": [ "Songo" ], "iso639_3": "soo", "name": "Songo", - "names": [ "Itsong", "Kisongo", "Nsong", "Ntsuo", "Tsong" ], + "names": [ "Itsong", "Kisongo", "Nsong", "Ntsong", "Ntsuo", "Tsong" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", "sldr": false, "tag": "soo", "tags": [ "soo-CD", "soo-Latn" ], - "unwritten": true, "windows": "soo-Latn" }, { @@ -103427,6 +108109,7 @@ "full": "soq-Latn-PG", "iana": [ "Kanasi" ], "iso639_3": "soq", + "localnames": [ "Kanasi" ], "name": "Kanasi", "names": [ "Sona" ], "region": "PG", @@ -103441,13 +108124,12 @@ "full": "sor-Latn-TD", "iana": [ "Somrai" ], "iso639_3": "sor", - "localnames": [ "Shibne" ], - "name": "Somrai", - "names": [ "Shibne", "Sibine", "Somray", "Somre", "Somrei", "Soumrai", "Soumray", "Sounrai", "Sumrai" ], + "name": "Soumraye", + "names": [ "Chibne", "Chibné", "Shibne", "Sibine", "Somrai", "Somray", "Somre", "Somrei", "Soumrai", "Soumray", "Sounrai", "Sumrai" ], "region": "TD", "regionname": "Chad", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "sor", "tags": [ "sor-Latn", "sor-TD" ], "windows": "sor-Latn" @@ -103487,8 +108169,9 @@ "full": "sov-Latn-PW", "iana": [ "Sonsorol" ], "iso639_3": "sov", + "localnames": [ "Ramari Dongosaro" ], "name": "Sonsorolese", - "names": [ "Sonsorol", "Sonsorol-Tobi" ], + "names": [ "Ramari Dongosaro", "Sonsorol", "Sonsorol-Tobi" ], "region": "PW", "regionname": "Palau", "script": "Latn", @@ -103517,8 +108200,9 @@ "full": "sox-Latn-CM", "iana": [ "Swo" ], "iso639_3": "sox", + "localnames": [ "Sùə̀" ], "name": "Swo", - "names": [ "Fo", "Shwo", "Sso", "Sô" ], + "names": [ "Fo", "Shwo", "Sso", "Sùə̀", "Sô" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -103531,8 +108215,10 @@ "full": "soy-Latn-BJ", "iana": [ "Miyobe" ], "iso639_3": "soy", + "localname": "Mɛyɔ́pɛ", + "localnames": [ "Mɛyɔ́pɛ" ], "name": "Miyobe", - "names": [ "Bijobe", "Biyobe", "Kayobe", "Kuyobe", "Kyobe", "Meyobe", "Sola", "Solamba", "Solla", "Sorouba", "Soruba", "Uyobe" ], + "names": [ "Bijobe", "Biyobe", "Kayobe", "Kuyobe", "Kyobe", "Meyobe", "Mɛyɔ́pɛ", "Sola", "Solamba", "Solla", "Sorouba", "Soruba", "Uyobe" ], "region": "BJ", "regionname": "Benin", "regions": [ "TG" ], @@ -103591,6 +108277,7 @@ "full": "spd-Latn-PG", "iana": [ "Saep" ], "iso639_3": "spd", + "localnames": [ "Saep" ], "name": "Saep", "region": "PG", "regionname": "Papua New Guinea", @@ -103678,9 +108365,10 @@ "full": "spm-Latn-PG", "iana": [ "Akukem" ], "iso639_3": "spm", - "localname": "Akɨghɨman Akam", + "localname": "Akɨghɨm", + "localnames": [ "Akɨghɨm" ], "name": "Akukem", - "names": [ "Sepen" ], + "names": [ "Akɨghɨm", "Sepen" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -103723,7 +108411,7 @@ "full": "spp-Latn-ML", "iana": [ "Supyire Senoufo" ], "iso639_3": "spp", - "localname": "Sénoufo, Supyire", + "localname": "Supyire", "localnames": [ "Supyire" ], "name": "Sénoufo, Supyire", "names": [ "Suppire", "Supyire", "Supyire Senoufo", "Sup’ide" ], @@ -103740,7 +108428,7 @@ "iana": [ "Loreto-Ucayali Spanish" ], "iso639_3": "spq", "name": "Spanish, Charapa", - "names": [ "Castellano Sharapa", "Jungle Spanish", "Loreto-Ucayali Spanish" ], + "names": [ "Amazonic Spanish", "Castellano Sharapa", "Jungle Spanish", "Loreto-Ucayali Spanish" ], "region": "PE", "regionname": "Peru", "script": "Latn", @@ -103815,6 +108503,7 @@ "iso639_3": "spv", "latnnames": [ "Koshali" ], "localnames": [ "କୋଶଳୀ" ], + "macrolang": "or", "name": "Sambalpuri", "names": [ "Dom", "Kosali", "Koshal", "Koshali", "Kosli", "Oriya", "Western Oriya" ], "region": "IN", @@ -103843,8 +108532,8 @@ "iso639_3": "sqi", "localname": "shqip", "localnames": [ "Shqip" ], - "name": "Albanian, Tosk", - "names": [ "Albanian", "Arnaut", "Arvanitika", "Camerija", "Shkip", "Shqip", "Skchip", "Tosk", "Zhgabe" ], + "name": "Albanian", + "names": [ "Albanian, Tosk", "Arnaut", "Arvanitika", "Camerija", "Shkip", "Shqip", "Skchip", "Tosk", "Zhgabe" ], "region": "AL", "regionname": "Albania", "regions": [ "AT", "AU", "BE", "DE", "GR", "HR", "IT", "MN", "RS", "SE", "TR", "UA", "US" ], @@ -103853,7 +108542,7 @@ "suppress": true, "tag": "sq", "tags": [ "als", "als-AL", "als-Latn", "als-Latn-AL", "sq-AL", "sq-Latn" ], - "windows": "sq" + "windows": "sq-AL" }, { "full": "sq-Brai-AL", @@ -103866,14 +108555,14 @@ "script": "Brai", "sldr": false, "tag": "sq-Brai", - "windows": "sq-Brai" + "windows": "sq-Brai-AL" }, { "full": "sq-Elba-AL", "iana": [ "Albanian" ], "iso639_3": "sqi", - "name": "Albanian, Tosk", - "names": [ "Albanian", "Arnaut", "Arvanitika", "Camerija", "Shkip", "Shqip", "Skchip", "Tosk", "Zhgabe" ], + "name": "Albanian", + "names": [ "Albanian, Tosk", "Arnaut", "Arvanitika", "Camerija", "Shkip", "Shqip", "Skchip", "Tosk", "Zhgabe" ], "nophonvars": true, "region": "AL", "regionname": "Albania", @@ -103882,14 +108571,14 @@ "sldr": false, "tag": "sq-Elba", "tags": [ "als-Elba", "als-Elba-AL" ], - "windows": "sq-Elba" + "windows": "sq-Elba-AL" }, { "full": "sq-Grek-GR", "iana": [ "Albanian" ], "iso639_3": "sqi", - "name": "Albanian, Tosk", - "names": [ "Albanian", "Arnaut", "Arvanitika", "Camerija", "Shkip", "Shqip", "Skchip", "Tosk", "Zhgabe" ], + "name": "Albanian", + "names": [ "Albanian, Tosk", "Arnaut", "Arvanitika", "Camerija", "Shkip", "Shqip", "Skchip", "Tosk", "Zhgabe" ], "nophonvars": true, "region": "GR", "regionname": "Greece", @@ -103898,7 +108587,7 @@ "sldr": false, "tag": "sq-Grek", "tags": [ "als-Grek", "als-Grek-GR", "sq-GR" ], - "windows": "sq-Grek" + "windows": "sq-Grek-GR" }, { "full": "sq-Latn-MK", @@ -103910,9 +108599,22 @@ "regionname": "North Macedonia", "script": "Latn", "sldr": true, - "suppress": true, "tag": "sq-MK", - "windows": "sq-MK" + "windows": "sq-Latn-MK" + }, + { + "full": "sq-Vith-AL", + "iana": [ "Albanian" ], + "iso639_3": "sqi", + "name": "Albanian", + "nophonvars": true, + "obsolete": true, + "region": "AL", + "regionname": "Albania", + "script": "Vith", + "sldr": false, + "tag": "sq-Vith", + "windows": "sq-Vith-AL" }, { "full": "sq-Latn-XK", @@ -103923,9 +108625,8 @@ "region": "XK", "script": "Latn", "sldr": true, - "suppress": true, "tag": "sq-XK", - "windows": "sq-XK" + "windows": "sq-Latn-XK" }, { "full": "sqa-Latn-NG", @@ -103998,44 +108699,44 @@ "windows": "sqn-Zyyy" }, { - "full": "sqo-Zyyy-IR", + "full": "sqo-Arab-IR", "iana": [ "Sorkhei" ], "iso639_3": "sqo", "name": "Sorkhei", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "sqo", - "tags": [ "sqo-IR", "sqo-Zyyy" ], - "windows": "sqo-Zyyy" + "tags": [ "sqo-Arab", "sqo-IR" ], + "windows": "sqo-Arab" }, { - "full": "sqq-Zyyy-LA", + "full": "sqq-Laoo-LA", "iana": [ "Sou" ], "iso639_3": "sqq", "name": "Sou", "names": [ "Sawk", "Souk", "Su", "Suq", "Su’" ], "region": "LA", "regionname": "Lao People's Democratic Republic", - "script": "Zyyy", + "script": "Laoo", "sldr": false, "tag": "sqq", - "tags": [ "sqq-LA", "sqq-Zyyy" ], + "tags": [ "sqq-LA", "sqq-Laoo" ], "unwritten": true, - "windows": "sqq-Zyyy" + "windows": "sqq-Laoo" }, { - "full": "sqr-Zyyy-XX", + "full": "sqr-Arab-XX", "iana": [ "Siculo Arabic" ], "iso639_3": "sqr", "name": "Siculo Arabic", "region": "XX", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "sqr", - "tags": [ "sqr-XX", "sqr-Zyyy" ], - "windows": "sqr-Zyyy" + "tags": [ "sqr-Arab", "sqr-XX" ], + "windows": "sqr-Arab" }, { "full": "sqs-Zxxx-LK", @@ -104094,6 +108795,20 @@ "tags": [ "squ-CA", "squ-Latn" ], "windows": "squ-Latn" }, + { + "full": "sqx-Zxxx-IL", + "iana": [ "Kufr Qassem Sign Language (KQSL)" ], + "iso639_3": "sqx", + "name": "Kufr Qassem Sign Language", + "names": [ "KQSL", "Kafr Qassem Sign Language", "Kafr Qassim Sign Language", "Kfar Qassem Sign Language", "Kfar Qassim Sign Language", "Kufr Qassem Sign Language (KQSL)" ], + "region": "IL", + "regionname": "Israel", + "script": "Zxxx", + "sldr": false, + "tag": "sqx", + "tags": [ "sgn-sqx", "sgn-sqx-IL", "sqx-IL", "sqx-Zxxx" ], + "windows": "sqx-Zxxx" + }, { "full": "sr-Cyrl-RS", "iana": [ "Serbian" ], @@ -104101,6 +108816,7 @@ "latnnames": [ "Srpski" ], "localname": "српски", "localnames": [ "српски" ], + "macrolang": "sh", "name": "Serbian", "names": [ "Montenegrin", "Serbo-Croatian" ], "region": "RS", @@ -104111,7 +108827,7 @@ "tag": "sr", "tags": [ "sr-Cyrl", "sr-RS" ], "variants": [ "ekavsk", "ijekavsk" ], - "windows": "sr-Cyrl" + "windows": "sr-Cyrl-RS" }, { "full": "sr-Cyrl-BA", @@ -104120,6 +108836,7 @@ "latnnames": [ "Srpski" ], "localname": "српски", "localnames": [ "српски" ], + "macrolang": "sh", "name": "Serbian", "region": "BA", "regionname": "Bosnia and Herzegovina", @@ -104132,6 +108849,7 @@ "full": "sr-Brai-RS", "iana": [ "Serbian" ], "iso639_3": "srp", + "macrolang": "sh", "name": "Serbian", "names": [ "Montenegrin", "Serbo-Croatian" ], "nophonvars": true, @@ -104141,7 +108859,7 @@ "script": "Brai", "sldr": false, "tag": "sr-Brai", - "windows": "sr-Brai" + "windows": "sr-Brai-RS" }, { "full": "sr-Cyrl-ME", @@ -104150,6 +108868,7 @@ "latnnames": [ "Srpski" ], "localname": "српски", "localnames": [ "српски" ], + "macrolang": "sh", "name": "Serbian", "region": "ME", "regionname": "Montenegro", @@ -104163,6 +108882,7 @@ "iana": [ "Serbian" ], "iso639_3": "srp", "localname": "srpski", + "macrolang": "sh", "name": "Serbian", "names": [ "Montenegrin", "Serbo-Croatian" ], "region": "RS", @@ -104172,13 +108892,14 @@ "sldr": true, "tag": "sr-Latn", "variants": [ "ekavsk", "ijekavsk" ], - "windows": "sr-Latn" + "windows": "sr-Latn-RS" }, { "full": "sr-Latn-BA", "iana": [ "Serbian" ], "iso639_3": "srp", "localname": "srpski", + "macrolang": "sh", "name": "Serbian", "region": "BA", "regionname": "Bosnia and Herzegovina", @@ -104192,6 +108913,7 @@ "iana": [ "Serbian" ], "iso639_3": "srp", "localname": "srpski", + "macrolang": "sh", "name": "Serbian", "region": "XK", "script": "Latn", @@ -104204,6 +108926,7 @@ "iana": [ "Serbian" ], "iso639_3": "srp", "localname": "srpski", + "macrolang": "sh", "name": "Serbian", "names": [ "Serbo-Croatian" ], "region": "ME", @@ -104217,6 +108940,7 @@ "full": "sr-Latn-RO", "iana": [ "Serbian" ], "iso639_3": "srp", + "macrolang": "sh", "name": "Serbian", "names": [ "Serbo-Croatian" ], "region": "RO", @@ -104230,6 +108954,7 @@ "full": "sr-Latn-RU", "iana": [ "Serbian" ], "iso639_3": "srp", + "macrolang": "sh", "name": "Serbian", "names": [ "Serbo-Croatian" ], "region": "RU", @@ -104243,6 +108968,7 @@ "full": "sr-Latn-TR", "iana": [ "Serbian" ], "iso639_3": "srp", + "macrolang": "sh", "name": "Serbian", "names": [ "Serbo-Croatian" ], "region": "TR", @@ -104259,6 +108985,7 @@ "latnnames": [ "Srpski" ], "localname": "српски", "localnames": [ "српски" ], + "macrolang": "sh", "name": "Serbian", "region": "XK", "script": "Cyrl", @@ -104284,6 +109011,8 @@ "full": "srb-Sora-IN", "iana": [ "Sora" ], "iso639_3": "srb", + "latnnames": [ "Savara" ], + "localnames": [ "𑃐𑃚𑃝" ], "name": "Sora", "names": [ "Sabar", "Sabara", "Saonras", "Saora", "Saura", "Savara", "Sawaria", "Shabari", "Soura", "Swara" ], "region": "IN", @@ -104311,6 +109040,8 @@ "full": "srb-Orya-IN", "iana": [ "Sora" ], "iso639_3": "srb", + "latnnames": [ "Savara" ], + "localnames": [ "ସଉରା" ], "name": "Sora", "names": [ "Sabar", "Sabara", "Saonras", "Saora", "Saura", "Savara", "Sawaria", "Shabari", "Soura", "Swara" ], "nophonvars": true, @@ -104380,25 +109111,27 @@ "windows": "srg-Latn" }, { - "full": "srh-Zyyy-CN", + "full": "srh-Arab-CN", "iana": [ "Sarikoli" ], "iso639_3": "srh", "name": "Sarikoli", - "names": [ "Salikur", "Saliku’er", "Sarikuli", "Sariqul", "Sarykoly", "Tadzik", "Tajik", "Tajiki" ], + "names": [ "Salikur", "Saliku’er", "Sarikuli", "Sariqul", "Sariquli", "Sarykoly", "Tadzik", "Tajik", "Tajiki" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "regions": [ "PK" ], + "script": "Arab", "sldr": false, "tag": "srh", - "tags": [ "srh-CN", "srh-Zyyy" ], - "windows": "srh-Zyyy" + "tags": [ "srh-Arab", "srh-CN" ], + "windows": "srh-Arab" }, { "full": "sri-Latn-CO", "iana": [ "Siriano" ], "iso639_3": "sri", + "localnames": [ "Sʉraya" ], "name": "Siriano", - "names": [ "Chiranga", "Cirnga", "Desana-Siriana", "Sarirá", "Si-Ra", "Siria-Masã", "Siriana", "Siriane", "Sura Masa", "Surianá", "Surirá", "Suryana" ], + "names": [ "Chiranga", "Cirnga", "Desana-Siriana", "Sarirá", "Si-Ra", "Siria-Masã", "Siriana", "Siriane", "Sura Masa", "Surianá", "Surirá", "Suryana", "Sʉraya" ], "region": "CO", "regionname": "Colombia", "regions": [ "BR" ], @@ -104427,8 +109160,9 @@ "full": "srl-Latn-ID", "iana": [ "Isirawa" ], "iso639_3": "srl", + "localnames": [ "Isirawa rarara" ], "name": "Isirawa", - "names": [ "Okwasar", "Saberi", "Saweri" ], + "names": [ "Isirawa rarara", "Okwasar", "Saberi", "Saweri" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -104475,6 +109209,7 @@ "full": "sro-Latn-IT", "iana": [ "Campidanese Sardinian" ], "iso639_3": "sro", + "macrolang": "sc", "name": "Sardinian, Campidanese", "names": [ "Campidanese", "Campidanese Sardinian", "Campidese", "Sardu", "South Sardinian" ], "region": "IT", @@ -104504,9 +109239,9 @@ "iana": [ "Serer" ], "iso639_3": "srr", "localname": "Seereer", - "localnames": [ "Seereer" ], + "localnames": [ "seereer" ], "name": "Serer-Sine", - "names": [ "Seereer", "Seex", "Sereer", "Serer", "Serer-Sin", "Serrer", "Sine-Saloum", "Sine-Sine", "Sérère-Sine" ], + "names": [ "Seereer", "Seex", "Sereer", "Serer", "Serer-Sin", "Serrer", "Sine-Saloum", "Sine-Sine", "Sérère-Sine", "seereer" ], "region": "SN", "regionname": "Senegal", "regions": [ "GM" ], @@ -104522,7 +109257,7 @@ "iso639_3": "srr", "localname": "Seereer", "name": "Serer-Sine", - "names": [ "Seereer", "Seex", "Sereer", "Serer", "Serer-Sin", "Serrer", "Sine-Saloum", "Sine-Sine", "Sérère-Sine" ], + "names": [ "Seereer", "Seex", "Sereer", "Serer", "Serer-Sin", "Serrer", "Sine-Saloum", "Sine-Sine", "Sérère-Sine", "seereer" ], "nophonvars": true, "region": "SN", "regionname": "Senegal", @@ -104609,6 +109344,8 @@ "full": "srx-Deva-IN", "iana": [ "Sirmauri" ], "iso639_3": "srx", + "latnnames": [ "Sirmauri" ], + "localnames": [ "सिरमौरी" ], "name": "Sirmauri", "names": [ "Himachali", "Pahari", "Sirmouri", "Sirmuri" ], "region": "IN", @@ -104637,8 +109374,9 @@ "full": "sry-Latn-PG", "iana": [ "Sera" ], "iso639_3": "sry", + "localnames": [ "Ser" ], "name": "Sera", - "names": [ "Serra", "Ssia" ], + "names": [ "Ser", "Serra", "Ssia" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -104665,9 +109403,9 @@ "iana": [ "Swati" ], "iso639_3": "ssw", "localname": "Siswati", - "localnames": [ "Siswati" ], + "localnames": [ "siSwati" ], "name": "Swati", - "names": [ "Isiswazi", "Ngwane", "Phuthi", "Sephuthi", "Siswati", "Siswazi", "Swazi", "Tekela", "Tekeza", "Thithiza", "Yeyeza", "siSwati" ], + "names": [ "Isiswazi", "Ngwane", "Phuthi", "Sephuthi", "Siswati", "Siswazi", "Swazi", "Síphùthì", "Tekela", "Tekeza", "Thithiza", "Yeyeza", "siSwati" ], "region": "ZA", "regionname": "South Africa", "regions": [ "LS", "MZ", "SZ" ], @@ -104698,13 +109436,13 @@ "full": "ssc-Latn-TZ", "iana": [ "Suba-Simbiti" ], "iso639_3": "ssc", - "localnames": [ "Suba-Simbiti" ], + "localnames": [ "Ikisimbëtë" ], "name": "Suba-Simbiti", - "names": [ "Kisimbiti", "Kisuba", "Simbiti" ], + "names": [ "Ikisimbëtë", "Kisimbiti", "Kisuba", "Simbiti" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "ssc", "tags": [ "ssc-Latn", "ssc-TZ" ], "windows": "ssc-Latn" @@ -104776,9 +109514,9 @@ "iana": [ "Seimat" ], "iso639_3": "ssg", "localname": "Seimat", - "localnames": [ "Seimat" ], + "localnames": [ "Kakai Seimat" ], "name": "Seimat", - "names": [ "Ninigo" ], + "names": [ "Kakai Seimat", "Ninigo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -104791,6 +109529,7 @@ "full": "ssh-Arab-AE", "iana": [ "Shihhi Arabic" ], "iso639_3": "ssh", + "macrolang": "ar", "name": "Arabic, Shihhi Spoken", "names": [ "Al-Shihuh", "Shihhi", "Shihhi Arabic", "Shihu", "Shihuh" ], "region": "AE", @@ -104817,7 +109556,23 @@ "tag": "ssi", "tags": [ "ssi-IN", "ssi-Zyyy" ], "unwritten": true, - "windows": "ssi-Zyyy" + "windows": "ssi-Zyyy-IN" + }, + { + "full": "ssi-Arab-PK", + "iana": [ "Sansi" ], + "iso639_3": "ssi", + "name": "Sansi", + "names": [ "Bhilki" ], + "nophonvars": true, + "region": "PK", + "regionname": "Pakistan", + "script": "Arab", + "sldr": false, + "tag": "ssi-Arab", + "tags": [ "ssi-PK" ], + "unwritten": true, + "windows": "ssi-Arab-PK" }, { "full": "ssj-Latn-PG", @@ -104854,7 +109609,7 @@ "iana": [ "Western Sisaala" ], "iso639_3": "ssl", "name": "Sisaala, Western", - "names": [ "Busillu Sisala", "Hissala", "Issala", "Sisai", "Western Sisaala" ], + "names": [ "Busillu Sisala", "Hissala", "Isaalo", "Issala", "Sisai", "Western Sisaala" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -104881,23 +109636,24 @@ "full": "ssn-Latn-KE", "iana": [ "Waata" ], "iso639_3": "ssn", + "localnames": [ "Waata" ], "name": "Waata", - "names": [ "Ariangulu", "Langulo", "Sanya", "Sanye", "Waat", "Wasanye" ], + "names": [ "Ariangulu", "Langulo", "Sanya", "Sanye", "Waat", "Waatah", "Wasanye" ], "region": "KE", "regionname": "Kenya", "script": "Latn", "sldr": false, "tag": "ssn", "tags": [ "ssn-KE", "ssn-Latn" ], - "unwritten": true, "windows": "ssn-Latn" }, { "full": "sso-Latn-PG", "iana": [ "Sissano" ], "iso639_3": "sso", - "name": "Sissano", - "names": [ "Sinama", "Sinano", "Sisano" ], + "localnames": [ "Essono" ], + "name": "Essono", + "names": [ "Sinama", "Sinano", "Sisano", "Sissano" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -104963,7 +109719,7 @@ "sldr": false, "tag": "sss", "tags": [ "sss-LA", "sss-Laoo" ], - "windows": "sss-Laoo" + "windows": "sss-Laoo-LA" }, { "full": "sss-Thai-TH", @@ -104979,7 +109735,7 @@ "sldr": false, "tag": "sss-Thai", "tags": [ "sss-TH" ], - "windows": "sss-Thai" + "windows": "sss-Thai-TH" }, { "full": "sst-Latn-PG", @@ -105042,9 +109798,9 @@ "full": "ssy-Latn-ER", "iana": [ "Saho" ], "iso639_3": "ssy", - "localnames": [ "Saho" ], + "localnames": [ "Saaho" ], "name": "Saho", - "names": [ "Sao", "Shaho", "Shiho", "Shoho" ], + "names": [ "Saaho", "Sao", "Shaho", "Shiho", "Shoho" ], "region": "ER", "regionname": "Eritrea", "regions": [ "ET" ], @@ -105052,13 +109808,14 @@ "sldr": true, "tag": "ssy", "tags": [ "ssy-ER", "ssy-Latn" ], - "windows": "ssy-Latn" + "windows": "ssy-Latn-ER" }, { "full": "ssy-Ethi-ET", "iana": [ "Saho" ], "iso639_3": "ssy", "name": "Saho", + "names": [ "Saaho" ], "nophonvars": true, "region": "ET", "regionname": "Ethiopia", @@ -105067,7 +109824,7 @@ "sldr": false, "tag": "ssy-Ethi", "tags": [ "ssy-ET" ], - "windows": "ssy-Ethi" + "windows": "ssy-Ethi-ET" }, { "full": "ssz-Latn-PG", @@ -105099,7 +109856,7 @@ "suppress": true, "tag": "st", "tags": [ "st-Latn", "st-ZA" ], - "windows": "st" + "windows": "st-ZA" }, { "full": "st-Latn-LS", @@ -105112,9 +109869,8 @@ "regionname": "Lesotho", "script": "Latn", "sldr": true, - "suppress": true, "tag": "st-LS", - "windows": "st-LS" + "windows": "st-Latn-LS" }, { "full": "sta-Latn-ZM", @@ -105140,7 +109896,7 @@ "region": "PH", "regionname": "Philippines", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "stb", "tags": [ "stb-Latn", "stb-PH" ], "windows": "stb-Latn" @@ -105224,22 +109980,24 @@ "full": "sti-Latn-VN", "iana": [ "Bulo Stieng" ], "iso639_3": "sti", + "localnames": [ "Troong Sdiêng" ], "name": "Stieng, Bulo", - "names": [ "Budíp", "Bulo Stieng", "Kajiang", "Northern Stieng", "Rangah", "Upper Stieng", "Xa-Dieng", "Xtieng" ], + "names": [ "Budíp", "Bulo Stieng", "Kajiang", "Northern Stieng", "Rangah", "Troong Sdiêng", "Upper Stieng", "Xa-Dieng", "Xtieng" ], "region": "VN", "regionname": "Viet Nam", "script": "Latn", "sldr": false, "tag": "sti", "tags": [ "sti-Latn", "sti-VN" ], - "windows": "sti-Latn" + "windows": "sti-Latn-VN" }, { "full": "sti-Latn-KH", "iana": [ "Bulo Stieng" ], "iso639_3": "sti", + "localnames": [ "Troong Sdiêng" ], "name": "Stieng, Bulo", - "names": [ "Bulo Stieng", "Kajiang" ], + "names": [ "Bulo Stieng", "Kajiang", "Troong Sdiêng" ], "region": "KH", "regionname": "Cambodia", "script": "Latn", @@ -105252,7 +110010,7 @@ "iana": [ "Matya Samo" ], "iso639_3": "stj", "name": "Samo, Matya", - "names": [ "Matya Samo", "Northwestern Samo", "Samogho", "San", "Sane", "Sànán", "Sã", "Tougan", "West Central Goe" ], + "names": [ "Matya Samo", "Northern San Macaa", "Northwestern Samo", "Samogho", "San", "San Ma Caa", "San Macaa", "San Matya", "Sane", "Sànán", "Sã", "Tougan", "West Central Goe" ], "region": "BF", "regionname": "Burkina Faso", "regions": [ "ML" ], @@ -105268,7 +110026,7 @@ "iana": [ "Arammba" ], "iso639_3": "stk", "localname": "Arammba", - "localnames": [ "Arammba" ], + "localnames": [ "Aramba" ], "name": "Aramba", "names": [ "Arammba", "Serki", "Serkisetavi", "Upper Morehead" ], "region": "PG", @@ -105312,9 +110070,9 @@ "full": "stn-Latn-SB", "iana": [ "Owa" ], "iso639_3": "stn", - "localnames": [ "Owa" ], + "localnames": [ "Owa", "Tamasinga iamami" ], "name": "Owa", - "names": [ "Anganiwai", "Anganiwei", "Narihua", "Santa Anna", "Wanoni" ], + "names": [ "Anganiwai", "Anganiwei", "Narihua", "Santa Anna", "Tamasinga iamami", "Wanoni" ], "region": "SB", "regionname": "Solomon Islands", "script": "Latn", @@ -105342,7 +110100,7 @@ "full": "stp-Latn-MX", "iana": [ "Southeastern Tepehuan" ], "iso639_3": "stp", - "localname": "O'dam", + "localname": "Oꞌdam", "localnames": [ "O'dam" ], "name": "Tepehuan, Southeastern", "names": [ "O’dam", "Southeastern Tepehuan", "Tepehuano", "Tepehuán del Sureste" ], @@ -105372,8 +110130,8 @@ "full": "str-Latn-CA", "iana": [ "Straits Salish" ], "iso639_3": "str", - "name": "Salish, Straits", - "names": [ "Lkwungen", "Malchosen", "Northern Straits Salish", "Senčoten", "Sooke", "Straits", "Straits Salish", "T’Sou-ke" ], + "name": "Northern Straits Salish", + "names": [ "North Straits Salish", "SENĆOŦEN", "Straits", "Straits Salish" ], "region": "CA", "regionname": "Canada", "regions": [ "US" ], @@ -105414,33 +110172,51 @@ "windows": "stt-Latn" }, { - "full": "stu-Zyyy-MM", + "full": "stu-Lana-MM", "iana": [ "Samtao" ], "iso639_3": "stu", "name": "Samtao", "names": [ "Samtau", "Samtuan" ], + "nophonvars": true, "region": "MM", "regionname": "Myanmar", "regions": [ "CN", "LA" ], - "script": "Zyyy", + "script": "Lana", "sldr": false, - "tag": "stu", - "tags": [ "stu-MM", "stu-Zyyy" ], + "tag": "stu-Lana", + "tags": [ "stu-MM" ], "unwritten": true, - "windows": "stu-Zyyy" + "windows": "stu-Lana-MM" + }, + { + "full": "stu-Tale-CN", + "iana": [ "Samtao" ], + "iso639_3": "stu", + "name": "Samtao", + "names": [ "Samtau", "Samtuan" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "regions": [ "LA", "MM" ], + "script": "Tale", + "sldr": false, + "tag": "stu-Tale", + "tags": [ "stu-CN" ], + "windows": "stu-Tale-CN" }, { "full": "stv-Ethi-ET", "iana": [ "Silt'e" ], "iso639_3": "stv", "latnnames": [ "Yesiltʼe" ], + "localname": "የስልጤ", "localnames": [ "የስልጤ" ], "name": "Silt’e", "names": [ "Silt'e" ], "region": "ET", "regionname": "Ethiopia", "script": "Ethi", - "sldr": false, + "sldr": true, "tag": "stv", "tags": [ "stv-ET", "stv-Ethi" ], "windows": "stv-Ethi" @@ -105478,28 +110254,30 @@ "full": "sty-Cyrl-RU", "iana": [ "Siberian Tatar" ], "iso639_3": "sty", - "name": "Tatar, Siberian", - "names": [ "Eastern Tatar", "Siberian Tatar" ], + "latnnames": [ "Seber tel", "Sebertatartsa", "Sybyr tel", "Sybyrtsa" ], + "localnames": [ "Себер тел", "Себертатарца", "Сыбыр тел", "Сыбырца" ], + "name": "Siberian Tatar", + "names": [ "Eastern Tatar", "Seber", "Sebertatar", "Siberian Turkish", "Sybyr" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", "sldr": false, "tag": "sty", "tags": [ "sty-Cyrl", "sty-RU" ], - "unwritten": true, "windows": "sty-Cyrl" }, { "full": "su-Latn-ID", "iana": [ "Sundanese" ], "iso639_3": "sun", + "localname": "Basa Sunda", "localnames": [ "Basa Sunda" ], "name": "Sunda", "names": [ "Basa Gumung", "Basa Sunda", "Priangan", "Sundanese" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "su", "tags": [ "su-ID", "su-Latn" ], "windows": "su-Latn" @@ -105570,7 +110348,7 @@ "iso639_3": "sub", "localnames": [ "Kisuku" ], "name": "Suku", - "names": [ "Kisuku" ], + "names": [ "Kisuku", "Musuku" ], "obsolete": true, "region": "CD", "regionname": "The Democratic Republic of the Congo", @@ -105585,7 +110363,7 @@ "full": "suc-Latn-PH", "iana": [ "Western Subanon" ], "iso639_3": "suc", - "localname": "Subanon, Western", + "localname": "Subanon", "localnames": [ "Subanon" ], "name": "Subanon, Western", "names": [ "Siocon", "Subanon", "Western Subanon" ], @@ -105672,13 +110450,28 @@ "tags": [ "suk-Latn", "suk-TZ" ], "windows": "suk-Latn" }, + { + "full": "suo-Latn-PG", + "iana": [ "Bouni" ], + "iso639_3": "suo", + "name": "Bouni", + "names": [ "Bouni-Bobe", "Sumo" ], + "region": "PG", + "regionname": "Papua New Guinea", + "script": "Latn", + "sldr": false, + "tag": "suo", + "tags": [ "suo-Latn", "suo-PG" ], + "windows": "suo-Latn" + }, { "full": "suq-Latn-ET", - "iana": [ "Suri" ], + "iana": [ "Tirmaga-Chai Suri", "Suri" ], "iso639_3": "suq", "localname": "Surichɛn", "localnames": [ "Surichɛn" ], - "name": "Suri", + "name": "Suri, Tirmaga-Chai", + "names": [ "Tirmaga-Chai Suri" ], "region": "ET", "regionname": "Ethiopia", "script": "Latn", @@ -105689,10 +110482,11 @@ }, { "full": "suq-Ethi-ET", - "iana": [ "Suri" ], + "iana": [ "Tirmaga-Chai Suri", "Suri" ], "iso639_3": "suq", "localname": "Surichɛn", - "name": "Suri", + "name": "Suri, Tirmaga-Chai", + "names": [ "Tirmaga-Chai Suri" ], "nophonvars": true, "region": "ET", "regionname": "Ethiopia", @@ -105708,7 +110502,7 @@ "localname": "Mwaghavul", "localnames": [ "Mwaghavul" ], "name": "Mwaghavul", - "names": [ "Maghavul", "Sura" ], + "names": [ "Mwahavul", "Sura", "pòo Mwaghavul" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -105721,10 +110515,10 @@ "full": "sus-Latn-GN", "iana": [ "Susu" ], "iso639_3": "sus", - "localname": "Susu", + "localname": "Sosoxui", "localnames": [ "Sosoxui" ], "name": "Susu", - "names": [ "Sose", "Soso", "Sosoxui", "Soussou", "Susoo" ], + "names": [ "Sose", "Soso", "Sosoxui", "Sosso", "Soussou", "Susoo", "Sussu" ], "region": "GN", "regionname": "Guinea", "regions": [ "GW", "SL", "SN" ], @@ -105738,9 +110532,9 @@ "full": "sus-Arab-GN", "iana": [ "Susu" ], "iso639_3": "sus", - "localname": "Susu", + "localname": "سٌسٌ", "name": "Susu", - "names": [ "Sose", "Soso", "Sosoxui", "Soussou", "Susoo" ], + "names": [ "Sose", "Soso", "Sosoxui", "Sosso", "Soussou", "Susoo", "Sussu" ], "nophonvars": true, "region": "GN", "regionname": "Guinea", @@ -105823,16 +110617,16 @@ "windows": "suw-Latn" }, { - "full": "sux-Zyyy-XX", + "full": "sux-Xsux-XX", "iana": [ "Sumerian" ], "iso639_3": "sux", "name": "Sumerian", "region": "XX", - "script": "Zyyy", + "script": "Xsux", "sldr": false, "tag": "sux", - "tags": [ "sux-XX", "sux-Zyyy" ], - "windows": "sux-Zyyy" + "tags": [ "sux-XX", "sux-Xsux" ], + "windows": "sux-Xsux" }, { "full": "suy-Latn-BR", @@ -105852,8 +110646,10 @@ "full": "suz-Deva-NP", "iana": [ "Sunwar" ], "iso639_3": "suz", + "latnnames": [ "kõicə" ], + "localnames": [ "कोँइच" ], "name": "Sunwar", - "names": [ "Bhujuwar", "Kiranti-Kõits Lo", "Kirati-Koits", "Koits Lo", "Mukhiya", "Pirthwar", "Sunuwar", "Sunwari" ], + "names": [ "Bhujuwar", "Kiranti-Kõits", "Kiranti-Kõits Lo", "Kirati-Koits", "Koic", "Kõits Lo", "Mukhiya", "Pirthwar", "Sunuwar", "Sunwari" ], "region": "NP", "regionname": "Nepal", "script": "Deva", @@ -105862,6 +110658,20 @@ "tags": [ "suz-Deva", "suz-NP" ], "windows": "suz-Deva" }, + { + "full": "suz-Sunu-NP", + "iana": [ "Sunwar" ], + "iso639_3": "suz", + "name": "Sunwar", + "names": [ "Bhujuwar", "Kiranti-Kõits", "Kiranti-Kõits Lo", "Kirati-Koits", "Koic", "Kõits Lo", "Mukhiya", "Pirthwar", "Sunuwar", "Sunwari" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "script": "Sunu", + "sldr": false, + "tag": "suz-Sunu", + "windows": "suz-Sunu" + }, { "full": "sv-Latn-SE", "iana": [ "Swedish" ], @@ -105878,7 +110688,7 @@ "suppress": true, "tag": "sv", "tags": [ "sv-Latn", "sv-SE" ], - "windows": "sv" + "windows": "sv-SE" }, { "full": "sv-Brai-SE", @@ -105893,7 +110703,7 @@ "script": "Brai", "sldr": false, "tag": "sv-Brai", - "windows": "sv-Brai" + "windows": "sv-Brai-SE" }, { "full": "sv-Latn-FI", @@ -105906,9 +110716,22 @@ "regionname": "Finland", "script": "Latn", "sldr": true, - "suppress": true, "tag": "sv-FI", - "windows": "sv-FI" + "windows": "sv-Latn-FI" + }, + { + "full": "sv-Runr-001", + "iana": [ "Swedish" ], + "iso639_3": "swe", + "name": "Swedish", + "nophonvars": true, + "region": "001", + "regionname": "World", + "script": "Runr", + "sldr": false, + "tag": "sv-Runr", + "tags": [ "sv-001" ], + "windows": "sv-Runr-001" }, { "full": "sva-Geor-GE", @@ -105917,7 +110740,7 @@ "latnnames": [ "lušnu nin" ], "localnames": [ "ლუშნუ ნინ" ], "name": "Svan", - "names": [ "Lushnu", "Svanuri" ], + "names": [ "Svanuri", "Šwan-är" ], "region": "GE", "regionname": "Georgia", "script": "Geor", @@ -105931,7 +110754,7 @@ "iana": [ "Svan" ], "iso639_3": "sva", "name": "Svan", - "names": [ "Lushnu", "Svanuri" ], + "names": [ "Lushnu", "Svanuri", "Šwan-är" ], "nophonvars": true, "region": "GE", "regionname": "Georgia", @@ -105946,7 +110769,7 @@ "iana": [ "Svan" ], "iso639_3": "sva", "name": "Svan", - "names": [ "Lushnu", "Svanuri" ], + "names": [ "Svanuri", "Šwan-är" ], "region": "GE", "regionname": "Georgia", "script": "Latn", @@ -105959,7 +110782,7 @@ "iana": [ "Ulau-Suain" ], "iso639_3": "svb", "name": "Ulau-Suain", - "names": [ "Suain" ], + "names": [ "Ainiayn", "Suain" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -105974,8 +110797,8 @@ "iana": [ "Vincentian Creole English" ], "iso639_3": "svc", "localnames": [ "Vincy Twang" ], - "name": "Vincentian Creole English", - "names": [ "Vincy Twang" ], + "name": "Vincentian English Creole", + "names": [ "Vincentian Creole English", "Vincy Twang" ], "region": "VC", "regionname": "Saint Vincent and the Grenadines", "script": "Latn", @@ -106039,7 +110862,6 @@ "sldr": false, "tag": "svs", "tags": [ "svs-Latn", "svs-SB" ], - "unwritten": true, "windows": "svs-Latn" }, { @@ -106060,8 +110882,8 @@ "iso639_3": "swa", "localname": "Kiswahili", "localnames": [ "Kiswahili" ], - "name": "Swahili", - "names": [ "Arab-Swahili", "Kisuaheli", "Kisuahili", "Kiswaheli", "Kiswahili", "Suahili", "Swahili (macrolanguage)" ], + "name": "Swahili (macrolanguage)", + "names": [ "Arab-Swahili", "Kisuaheli", "Kisuahili", "Kiswaheli", "Kiswahili", "Suahili", "Swahili" ], "region": "TZ", "regionname": "United Republic of Tanzania", "regions": [ "AE", "BI", "CA", "MZ", "OM", "RW", "SA", "SO", "US", "YT", "ZA" ], @@ -106070,7 +110892,7 @@ "suppress": true, "tag": "sw", "tags": [ "sw-Latn", "sw-TZ", "sw-swh", "sw-swh-TZ", "swh", "swh-Latn", "swh-Latn-TZ", "swh-TZ" ], - "windows": "sw" + "windows": "sw-TZ" }, { "full": "sw-Arab-TZ", @@ -106085,7 +110907,7 @@ "script": "Arab", "sldr": true, "tag": "sw-Arab", - "windows": "sw-Arab" + "windows": "sw-Arab-TZ" }, { "full": "sw-Latn-CD", @@ -106097,9 +110919,8 @@ "regionname": "The Democratic Republic of the Congo", "script": "Latn", "sldr": true, - "suppress": true, "tag": "sw-CD", - "windows": "sw-CD" + "windows": "sw-Latn-CD" }, { "full": "sw-Latn-KE", @@ -106111,9 +110932,8 @@ "regionname": "Kenya", "script": "Latn", "sldr": true, - "suppress": true, "tag": "sw-KE", - "windows": "sw-KE" + "windows": "sw-Latn-KE" }, { "full": "sw-Latn-UG", @@ -106125,9 +110945,8 @@ "regionname": "Uganda", "script": "Latn", "sldr": true, - "suppress": true, "tag": "sw-UG", - "windows": "sw-UG" + "windows": "sw-Latn-UG" }, { "full": "swb-Arab-YT", @@ -106164,6 +110983,7 @@ "iana": [ "Congo Swahili" ], "iso639_3": "swc", "localnames": [ "Kiswahili" ], + "macrolang": "sw", "name": "Swahili, Congo", "names": [ "Congo Swahili", "Kiswahili", "Zaïre Swahili" ], "region": "CD", @@ -106179,6 +110999,7 @@ "full": "swc-Arab-CD", "iana": [ "Congo Swahili" ], "iso639_3": "swc", + "macrolang": "sw", "name": "Swahili, Congo", "names": [ "Congo Swahili", "Kiswahili", "Zaïre Swahili" ], "nophonvars": true, @@ -106226,6 +111047,7 @@ "full": "swh-Arab-TZ", "iana": [ "Swahili (individual language)", "Kiswahili" ], "iso639_3": "swh", + "macrolang": "sw", "name": "Swahili", "names": [ "Arab-Swahili", "Kisuaheli", "Kisuahili", "Kiswaheli", "Kiswahili", "Suahili", "Swahili (individual language)" ], "nophonvars": true, @@ -106241,6 +111063,7 @@ "full": "swh-Brai-TZ", "iana": [ "Swahili (individual language)", "Kiswahili" ], "iso639_3": "swh", + "macrolang": "sw", "name": "Swahili", "names": [ "Arab-Swahili", "Kisuaheli", "Kisuahili", "Kiswaheli", "Kiswahili", "Suahili", "Swahili (individual language)" ], "nophonvars": true, @@ -106300,7 +111123,7 @@ "iana": [ "Swedish Sign Language" ], "iso639_3": "swl", "name": "Swedish Sign Language", - "names": [ "STS", "Svenskt teckenspråk", "SwedSL" ], + "names": [ "SSL", "STS", "Svenskt Teckenspråk", "SwedSL" ], "region": "SE", "regionname": "Sweden", "script": "Zxxx", @@ -106418,7 +111241,7 @@ "iana": [ "Sawila" ], "iso639_3": "swt", "name": "Sawila", - "names": [ "Tanglapui", "Tanlapui" ], + "names": [ "Manata", "Sawali", "Tanglapui", "Tanlapui", "Wullili" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -106447,6 +111270,9 @@ "full": "swv-Deva-IN", "iana": [ "Shekhawati" ], "iso639_3": "swv", + "latnnames": [ "Shekhaavaatee" ], + "localnames": [ "शेखावाटी" ], + "macrolang": "mwr", "name": "Shekhawati", "names": [ "Shekhawati-Marwari" ], "region": "IN", @@ -106518,16 +111344,16 @@ "windows": "sxb-Latn" }, { - "full": "sxc-Zyyy-XX", + "full": "sxc-Grek-XX", "iana": [ "Sicanian" ], "iso639_3": "sxc", "name": "Sicanian", "region": "XX", - "script": "Zyyy", + "script": "Grek", "sldr": false, "tag": "sxc", - "tags": [ "sxc-XX", "sxc-Zyyy" ], - "windows": "sxc-Zyyy" + "tags": [ "sxc-Grek", "sxc-XX" ], + "windows": "sxc-Grek" }, { "full": "sxe-Latn-GA", @@ -106627,18 +111453,18 @@ "windows": "sxo-Zyyy" }, { - "full": "sxr-Zyyy-TW", + "full": "sxr-Latn-TW", "iana": [ "Saaroa" ], "iso639_3": "sxr", "name": "Saaroa", - "names": [ "Hla’alua", "La’alua", "La’arua", "Lha’alua", "Pachien", "Paichien", "Rarua", "Saarua", "Saroa", "Shishaban", "Sisyaban" ], + "names": [ "Hla’alua", "La Aluwa", "La’alua", "La’arua", "Lha’alua", "Pachien", "Paichien", "Rarua", "Saarua", "Saroa", "Shishaban", "Sisyaban" ], "region": "TW", "regionname": "China-Taiwan", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "sxr", - "tags": [ "sxr-TW", "sxr-Zyyy" ], - "windows": "sxr-Zyyy" + "tags": [ "sxr-Latn", "sxr-TW" ], + "windows": "sxr-Latn" }, { "full": "sxs-Latn-NG", @@ -106654,6 +111480,21 @@ "unwritten": true, "windows": "sxs-Latn" }, + { + "full": "sxu-Runr-DE", + "iana": [ "Upper Saxon" ], + "iso639_3": "sxu", + "name": "Saxon, Upper", + "names": [ "Meissenish", "Meißnisch", "Obersächsisch", "Sächsisch", "Upper Saxon" ], + "region": "DE", + "regionname": "Germany", + "regions": [ "001" ], + "script": "Runr", + "sldr": false, + "tag": "sxu", + "tags": [ "sxu-DE", "sxu-Runr" ], + "windows": "sxu-Runr" + }, { "full": "sxu-Latn-DE", "iana": [ "Upper Saxon" ], @@ -106665,8 +111506,7 @@ "regionname": "Germany", "script": "Latn", "sldr": false, - "tag": "sxu", - "tags": [ "sxu-DE", "sxu-Latn" ], + "tag": "sxu-Latn", "windows": "sxu-Latn" }, { @@ -106704,7 +111544,7 @@ "full": "syb-Latn-PH", "iana": [ "Central Subanen" ], "iso639_3": "syb", - "localname": "Subanen, Central", + "localname": "Sinubaanen", "localnames": [ "Sinubaanen", "Subanen" ], "name": "Subanen, Central", "names": [ "Central Subanen", "Sindangan Subanun", "Sinubaanen", "Subaanen", "Subanen" ], @@ -106752,8 +111592,9 @@ "full": "syk-Latn-NG", "iana": [ "Sukur" ], "iso639_3": "syk", + "localnames": [ "Gemasakun", "Sakun" ], "name": "Sukur", - "names": [ "Adikimmu Sukur", "Gemasakun", "Sakul", "Sugur" ], + "names": [ "Adikummu Sukur", "Gemasakun", "Sakun", "Sugur" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -106784,7 +111625,7 @@ "full": "syl-Latn-BD", "iana": [ "Sylheti" ], "iso639_3": "syl", - "localname": "ছিলটী", + "localname": "Sileti", "localnames": [ "Sileti" ], "name": "Sylheti", "names": [ "Bengali of Cachar", "Sileti", "Siloti", "Srihattia", "Sylheti Bangla", "Sylheti Bengali", "Sylhetti", "Sylhetti Bangla", "Syloti", "Syloty" ], @@ -106800,7 +111641,6 @@ "full": "syl-Sylo-BD", "iana": [ "Sylheti" ], "iso639_3": "syl", - "localname": "ছিলটী", "name": "Sylheti", "names": [ "Bengali of Cachar", "Sileti", "Siloti", "Srihattia", "Sylheti Bangla", "Sylheti Bengali", "Sylhetti", "Sylhetti Bangla", "Syloti", "Syloty" ], "nophonvars": true, @@ -106818,7 +111658,7 @@ "iana": [ "Maya Samo" ], "iso639_3": "sym", "name": "Samo, Maya", - "names": [ "Maya Samo", "Northeastern Samo", "Sa", "San" ], + "names": [ "Leburu", "Maya Samo", "Northeastern Samo", "Northern San Mayaa", "Sa", "San", "San Maya", "San Mayaa" ], "region": "BF", "regionname": "Burkina Faso", "script": "Latn", @@ -106863,8 +111703,8 @@ "iana": [ "Syriac" ], "iso639_3": "syr", "localname": "ܣܘܪܝܝܐ", - "name": "Chaldean Neo-Aramaic", - "names": [ "Chaldean", "Fallani", "Fellihi", "Kaldaya", "Kildani", "Lishana Kaldaya", "Modern Chaldean", "Neo-Chaldean", "Soorath", "Soorith", "Suras", "Sureth", "Syriac" ], + "name": "Syriac", + "names": [ "Chaldean", "Chaldean Neo-Aramaic", "Fallani", "Fellihi", "Kaldaya", "Kildani", "Lishana Kaldaya", "Modern Chaldean", "Neo-Chaldean", "Soorath", "Soorith", "Suras", "Sureth" ], "region": "IQ", "regionname": "Iraq", "regions": [ "BE", "CA", "DE", "LB", "NL", "SY", "TR", "US" ], @@ -106978,20 +111818,6 @@ "unwritten": true, "windows": "szc-Latn" }, - { - "full": "szd-Latn-MY", - "iana": [ "Seru" ], - "iso639_3": "szd", - "name": "Seru", - "region": "MY", - "regionname": "Malaysia", - "script": "Latn", - "sldr": false, - "tag": "szd", - "tags": [ "szd-Latn", "szd-MY" ], - "unwritten": true, - "windows": "szd-Latn" - }, { "full": "sze-Zyyy-ET", "iana": [ "Seze" ], @@ -107010,8 +111836,9 @@ "full": "szg-Latn-CD", "iana": [ "Sengele" ], "iso639_3": "szg", + "localnames": [ "Kisengele" ], "name": "Sengele", - "names": [ "Kesengele", "Sengere" ], + "names": [ "Kesengele", "Kisengele", "Sengere" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -107065,33 +111892,18 @@ "unwritten": true, "windows": "szp-Latn" }, - { - "full": "szs-Latn-SB", - "iana": [ "Solomon Islands Sign Language" ], - "iso639_3": "szs", - "name": "Solomon Islands Sign Language", - "names": [ "SISL" ], - "region": "SB", - "regionname": "Solomon Islands", - "script": "Latn", - "sldr": false, - "tag": "szs", - "tags": [ "sgn-szs", "sgn-szs-SB", "szs-Latn", "szs-SB" ], - "unwritten": true, - "windows": "szs-Latn" - }, { "full": "szs-Zxxx-SB", "iana": [ "Solomon Islands Sign Language" ], "iso639_3": "szs", "name": "Solomon Islands Sign Language", "names": [ "SISL" ], - "nophonvars": true, "region": "SB", "regionname": "Solomon Islands", "script": "Zxxx", "sldr": false, - "tag": "szs-Zxxx", + "tag": "szs", + "tags": [ "sgn-szs", "sgn-szs-SB", "szs-SB", "szs-Zxxx" ], "windows": "szs-Zxxx" }, { @@ -107112,6 +111924,7 @@ "full": "szw-Latn-ID", "iana": [ "Sawai" ], "iso639_3": "szw", + "localnames": [ "Sawai" ], "name": "Sawai", "names": [ "Weda", "Weda-Sawai", "Were" ], "region": "ID", @@ -107148,13 +111961,28 @@ "names": [ "Damulian", "Tamal", "Tamalsan", "Tambul", "Tamili" ], "region": "IN", "regionname": "India", - "regions": [ "AE", "AU", "BH", "BZ", "CA", "DE", "DK", "FJ", "GB", "MU", "NL", "QA", "RE", "TH", "US", "ZA" ], + "regions": [ "AE", "AU", "BH", "BZ", "CA", "DE", "DK", "FJ", "GB", "MU", "NL", "PK", "QA", "RE", "TH", "US", "ZA" ], "script": "Taml", "sldr": true, "suppress": true, "tag": "ta", "tags": [ "ta-IN", "ta-Taml" ], - "windows": "ta" + "windows": "ta-IN" + }, + { + "full": "ta-Arab-IN", + "iana": [ "Tamil" ], + "iso639_3": "tam", + "name": "Tamil", + "names": [ "Damulian", "Tamal", "Tamalsan", "Tambul", "Tamili" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "regions": [ "AE", "AU", "BH", "BZ", "CA", "DE", "DK", "FJ", "GB", "LK", "MU", "MY", "NL", "QA", "RE", "SG", "TH", "US", "ZA" ], + "script": "Arab", + "sldr": false, + "tag": "ta-Arab", + "windows": "ta-Arab-IN" }, { "full": "ta-Brai-IN", @@ -107169,7 +111997,7 @@ "script": "Brai", "sldr": false, "tag": "ta-Brai", - "windows": "ta-Brai" + "windows": "ta-Brai-IN" }, { "full": "ta-Taml-LK", @@ -107183,9 +112011,8 @@ "regionname": "Sri Lanka", "script": "Taml", "sldr": true, - "suppress": true, "tag": "ta-LK", - "windows": "ta-LK" + "windows": "ta-Taml-LK" }, { "full": "ta-Taml-MY", @@ -107199,9 +112026,8 @@ "regionname": "Malaysia", "script": "Taml", "sldr": true, - "suppress": true, "tag": "ta-MY", - "windows": "ta-MY" + "windows": "ta-Taml-MY" }, { "full": "ta-Taml-SG", @@ -107215,17 +112041,16 @@ "regionname": "Singapore", "script": "Taml", "sldr": true, - "suppress": true, "tag": "ta-SG", - "windows": "ta-SG" + "windows": "ta-Taml-SG" }, { "full": "taa-Latn-US", "iana": [ "Lower Tanana" ], "iso639_3": "taa", - "localnames": [ "Menhti Kenaga" ], + "localnames": [ "Benhti Kokhwt’ana Kenaga’", "Menhti Kenaga" ], "name": "Tanana, Lower", - "names": [ "Lower Tanana", "Menhti Kenaga", "Minto", "Tanana", "Tanana Athabaskan" ], + "names": [ "Benhti Kokhwt’ana Kenaga’", "Lower Tanana", "Menhti Kenaga", "Minto", "Tanana", "Tanana Athabaskan" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -107279,7 +112104,6 @@ "sldr": false, "tag": "tad", "tags": [ "tad-ID", "tad-Latn" ], - "unwritten": true, "windows": "tad-Latn" }, { @@ -107314,19 +112138,19 @@ "windows": "taf-Latn" }, { - "full": "tag-Zyyy-SD", + "full": "tag-Latn-SD", "iana": [ "Tagoi" ], "iso639_3": "tag", "name": "Tagoi", "names": [ "Tagoy" ], "region": "SD", "regionname": "Sudan", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tag", - "tags": [ "tag-SD", "tag-Zyyy" ], + "tags": [ "tag-Latn", "tag-SD" ], "unwritten": true, - "windows": "tag-Zyyy" + "windows": "tag-Latn" }, { "full": "taj-Deva-NP", @@ -107344,13 +112168,13 @@ "sldr": true, "tag": "taj", "tags": [ "taj-Deva", "taj-NP", "tsf", "tsf-Deva", "tsf-Deva-NP", "tsf-NP" ], - "windows": "taj-Deva" + "windows": "taj-Deva-NP" }, { "full": "taj-Tibt-BT", "iana": [ "Eastern Tamang" ], "iso639_3": "taj", - "latnnames": [ "Tamang" ], + "latnnames": [ "tāmāng" ], "localnames": [ "རྟ་དམག" ], "name": "Tamang, Eastern", "names": [ "Eastern Tamang", "Ishang", "Murmi", "Sei", "Tamang" ], @@ -107362,13 +112186,13 @@ "sldr": false, "tag": "taj-Tibt", "tags": [ "taj-BT", "tsf-BT" ], - "windows": "taj-Tibt" + "windows": "taj-Tibt-BT" }, { "full": "taj-Tibt-BT-x-tamhig", "iana": [ "Tamang, Eastern" ], "iso639_3": "taj", - "latnnames": [ "Tamang" ], + "latnnames": [ "tāmāng" ], "localnames": [ "རྟ་དམག" ], "name": "Tamang, Eastern", "names": [ "Ishang", "Murmi", "Sei", "Tamang" ], @@ -107380,7 +112204,7 @@ "sldr": false, "tag": "taj-Tibt-x-tamhig", "tags": [ "taj-BT-x-tamhig", "tsf-BT-x-tamhig" ], - "windows": "taj-Tibt-x-tamhig" + "windows": "taj-Tibt-BT-x-tamhig" }, { "full": "tak-Latn-NG", @@ -107403,7 +112227,7 @@ "localname": "Tal", "localnames": [ "Tal" ], "name": "Tal", - "names": [ "Tal mwa" ], + "names": [ "Amtul", "Hampul", "Kwabzak", "Tal mwa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -107416,8 +112240,9 @@ "full": "tan-Latn-NG", "iana": [ "Tangale" ], "iso639_3": "tan", + "localnames": [ "Táŋlɛ" ], "name": "Tangale", - "names": [ "Tangle" ], + "names": [ "Tangle", "Táŋlɛ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -107430,9 +112255,9 @@ "full": "tao-Latn-TW", "iana": [ "Yami" ], "iso639_3": "tao", - "localnames": [ "Pongso no Tao" ], + "localnames": [ "Ciriciring no tao", "Pongso no Tao" ], "name": "Yami", - "names": [ "Botel Tabago", "Botel Tobago", "Lanyu", "Pongso no Tao", "Tao", "Tawu" ], + "names": [ "Botel Tabago", "Botel Tobago", "Ciriciring no tao", "Dawu", "Lanyu", "Pongso no Tao", "Tao", "Tawu" ], "region": "TW", "regionname": "China-Taiwan", "script": "Latn", @@ -107460,23 +112285,27 @@ "full": "taq-Latn-ML", "iana": [ "Tamasheq" ], "iso639_3": "taq", - "localnames": [ "Tǝmajǝq" ], + "localname": "Təmajəq", + "localnames": [ "Təmajəq" ], + "macrolang": "tmh", "name": "Tamasheq", - "names": [ "Tamachen", "Tamashek", "Tamashekin", "Tamasheq, Kidal", "Timbuktu", "Tomacheck", "Tǝmajǝq", "tamachèque", "tamasagt" ], + "names": [ "Tamachen", "Tamashek", "Tamashekin", "Tamasheq, Kidal", "Timbuktu", "Tomacheck", "Təmajəq", "tamachèque", "tamasagt" ], "region": "ML", "regionname": "Mali", "regions": [ "BF", "BG", "DZ" ], "script": "Latn", "sldr": true, - "tag": "taq-Latn", + "tag": "taq", + "tags": [ "taq-Latn", "taq-ML" ], "windows": "taq-Latn" }, { "full": "taq-Tfng-ML", "iana": [ "Tamasheq" ], "iso639_3": "taq", + "macrolang": "tmh", "name": "Tamasheq", - "names": [ "Tamachen", "Tamashek", "Tamashekin", "Tamasheq, Kidal", "Timbuktu", "Tomacheck", "Tǝmajǝq", "tamachèque", "tamasagt" ], + "names": [ "Tamachen", "Tamashek", "Tamashekin", "Tamasheq, Kidal", "Timbuktu", "Tomacheck", "Təmajəq", "tamachèque", "tamasagt" ], "nophonvars": true, "region": "ML", "regionname": "Mali", @@ -107552,9 +112381,10 @@ "full": "taw-Latn-PG", "iana": [ "Tai" ], "iso639_3": "taw", - "localname": "Tai", - "name": "Tai", - "names": [ "Tay", "Ti" ], + "localname": "Tay", + "localnames": [ "Tay" ], + "name": "Tay", + "names": [ "Tai", "Tai mnm", "Tay mnm", "Ti" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -107759,9 +112589,9 @@ "full": "tbi-Latn-SD", "iana": [ "Gaam" ], "iso639_3": "tbi", - "localnames": [ "Gaahmg" ], + "localnames": [ "Gəəməndən" ], "name": "Gaam", - "names": [ "Gaahmg", "Ingassana", "Ingessana", "Kamanidi", "Mamedja", "Mamidza", "Metabi", "Muntabi", "Tabi" ], + "names": [ "Gaahmg", "Gəəməndən", "Ingassana", "Ingessana", "Kamanidi", "Kor é Gəəmg", "Mamedja", "Mamidza", "Metabi", "Muntabi", "Tabi" ], "region": "SD", "regionname": "Sudan", "script": "Latn", @@ -107799,10 +112629,25 @@ "tags": [ "tbk-PH", "tbk-Tagb" ], "windows": "tbk-Tagb" }, + { + "full": "tbk-Hano-PH", + "iana": [ "Calamian Tagbanwa" ], + "iso639_3": "tbk", + "name": "Tagbanwa, Calamian", + "names": [ "Calamian Tagbanwa", "Kalamian Tagbanwa", "Kalamianon", "Tinagbanwa" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "script": "Hano", + "sldr": false, + "tag": "tbk-Hano", + "windows": "tbk-Hano" + }, { "full": "tbk-Latn-PH", "iana": [ "Calamian Tagbanwa" ], "iso639_3": "tbk", + "localname": "Tinagbanwa", "localnames": [ "Tinagbanwa" ], "name": "Tagbanwa, Calamian", "names": [ "Calamian Tagbanwa", "Kalamian Tagbanwa", "Kalamianon", "Tinagbanwa" ], @@ -107908,6 +112753,7 @@ "full": "tbs-Latn-PG", "iana": [ "Tanguat" ], "iso639_3": "tbs", + "localnames": [ "Tanguat" ], "name": "Tanguat", "region": "PG", "regionname": "Papua New Guinea", @@ -107915,7 +112761,6 @@ "sldr": false, "tag": "tbs", "tags": [ "tbs-Latn", "tbs-PG" ], - "unwritten": true, "windows": "tbs-Latn" }, { @@ -107977,6 +112822,20 @@ "tags": [ "tbw-Latn", "tbw-PH" ], "windows": "tbw-Latn" }, + { + "full": "tbw-Hano-PH", + "iana": [ "Tagbanwa" ], + "iso639_3": "tbw", + "name": "Tagbanwa", + "names": [ "Aborlan Tagbanwa", "Apurawnon", "Tagbanua" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "script": "Hano", + "sldr": false, + "tag": "tbw-Hano", + "windows": "tbw-Hano" + }, { "full": "tbw-Tagb-PH", "iana": [ "Tagbanwa" ], @@ -107995,8 +112854,9 @@ "full": "tbx-Latn-PG", "iana": [ "Kapin" ], "iso639_3": "tbx", + "localnames": [ "Nambei nyon" ], "name": "Kapin", - "names": [ "Katumene", "Sambio", "Taiak", "Tayek" ], + "names": [ "Katumene", "Nambei nyon", "Sambio", "Taiak", "Tayek" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -108042,9 +112902,9 @@ "iana": [ "Ticuna" ], "iso639_3": "tca", "localname": "ticunagaxũ", - "localnames": [ "ticunagaxũ" ], + "localnames": [ "Ticunaga" ], "name": "Ticuna", - "names": [ "Du-ûgü", "Duuxugu", "Duüxügu", "Magüa", "Tikuna", "Tucuna", "Tukuna", "Tukúna", "ticunagaxũ" ], + "names": [ "Du-ûgü", "Duuxugu", "Duüxügu", "Magüa", "Ticunaga", "Tikuna", "Tucuna", "Tukuna", "Tukúna" ], "region": "BR", "regionname": "Brazil", "regions": [ "CO", "PE" ], @@ -108058,9 +112918,9 @@ "full": "tcb-Latn-US", "iana": [ "Tanacross" ], "iso639_3": "tcb", - "localnames": [ "Nee'anděg'" ], + "localnames": [ "Dihthaad Xt’een Iin Aanděeg’", "Nee'anděg'" ], "name": "Tanacross", - "names": [ "Nee’anděg’" ], + "names": [ "Dihthaad Xt’een Iin Aanděeg’", "Nee’anděg’" ], "region": "US", "regionname": "United States", "script": "Latn", @@ -108087,15 +112947,15 @@ "full": "tcd-Latn-GH", "iana": [ "Tafi" ], "iso639_3": "tcd", + "localnames": [ "Tɛgbɔ" ], "name": "Tafi", - "names": [ "Tegbo" ], + "names": [ "Tegbo", "Tɛgbɔ" ], "region": "GH", "regionname": "Ghana", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tcd", "tags": [ "tcd-GH", "tcd-Latn" ], - "unwritten": true, "windows": "tcd-Latn" }, { @@ -108147,7 +113007,7 @@ "full": "tch-Latn-TC", "iana": [ "Turks And Caicos Creole English" ], "iso639_3": "tch", - "name": "Turks and Caicos Creole English", + "name": "Turks and Caicos English Creole", "names": [ "Turks And Caicos Creole English" ], "region": "TC", "regionname": "Turks and Caicos Islands", @@ -108218,34 +113078,34 @@ "windows": "tcm-Latn" }, { - "full": "tcn-Zyyy-NP", + "full": "tcn-Tibt-NP", "iana": [ "Tichurong" ], "iso639_3": "tcn", "name": "Tichurong", "names": [ "Ticherong" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "tcn", - "tags": [ "tcn-NP", "tcn-Zyyy" ], + "tags": [ "tcn-NP", "tcn-Tibt" ], "unwritten": true, - "windows": "tcn-Zyyy" + "windows": "tcn-Tibt" }, { - "full": "tco-Zyyy-MM", + "full": "tco-Mymr-MM", "iana": [ "Taungyo" ], "iso639_3": "tco", "name": "Taungyo", "names": [ "Taru", "Taung Yoe", "Taung-yo", "Toru", "Twing Reu", "Twinreu" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "tco", - "tags": [ "tco-MM", "tco-Zyyy" ], + "tags": [ "tco-MM", "tco-Mymr" ], "unwritten": true, - "windows": "tco-Zyyy" + "windows": "tco-Mymr" }, { "full": "tcp-Latn-MM", @@ -108325,9 +113185,9 @@ "full": "tcw-Latn-MX", "iana": [ "Tecpatlán Totonac" ], "iso639_3": "tcw", - "localnames": [ "Totonaca" ], + "localnames": [ "Tachiwiin", "Tutunakuj" ], "name": "Totonac, Tecpatlán", - "names": [ "Tecpatlán Totonac", "Totonaca" ], + "names": [ "Tachaqawaxti", "Tachiwiin", "Tecpatlán Totonac", "Totonaca", "Totonaco Central del Norte", "Tutunakuj" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -108398,45 +113258,45 @@ "windows": "tcz-Latn" }, { - "full": "tda-Arab-NE", + "full": "tda-Tfng-NE", "iana": [ "Tagdal" ], "iso639_3": "tda", "name": "Tagdal", "names": [ "Igalan" ], - "nophonvars": true, "region": "NE", "regionname": "Niger", - "script": "Arab", + "script": "Tfng", "sldr": false, - "tag": "tda-Arab", - "windows": "tda-Arab" + "tag": "tda", + "tags": [ "tda-NE", "tda-Tfng" ], + "windows": "tda-Tfng" }, { - "full": "tda-Latn-NE", + "full": "tda-Arab-NE", "iana": [ "Tagdal" ], "iso639_3": "tda", "name": "Tagdal", "names": [ "Igalan" ], + "nophonvars": true, "region": "NE", "regionname": "Niger", - "script": "Latn", + "script": "Arab", "sldr": false, - "tag": "tda-Latn", - "windows": "tda-Latn" + "tag": "tda-Arab", + "windows": "tda-Arab" }, { - "full": "tda-Tfng-NE", + "full": "tda-Latn-NE", "iana": [ "Tagdal" ], "iso639_3": "tda", "name": "Tagdal", "names": [ "Igalan" ], - "nophonvars": true, "region": "NE", "regionname": "Niger", - "script": "Tfng", + "script": "Latn", "sldr": false, - "tag": "tda-Tfng", - "windows": "tda-Tfng" + "tag": "tda-Latn", + "windows": "tda-Latn" }, { "full": "tdb-Deva-IN", @@ -108488,12 +113348,13 @@ "full": "tdc-Latn-CO", "iana": [ "Emberá-Tadó" ], "iso639_3": "tdc", - "name": "Emberá-Tadó", - "names": [ "Cholo", "Embena", "Embera", "Epena", "Katío", "Êpêra" ], + "localnames": [ "Ẽpẽra Pedea" ], + "name": "Embera Tadó", + "names": [ "Cholo", "Embena", "Embera", "Embera Pedea Tadó", "Emberá-Tadó", "Epena", "Katío", "Tadó", "Êpêra", "Ẽpẽra Pedea" ], "region": "CO", "regionname": "Colombia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tdc", "tags": [ "tdc-CO", "tdc-Latn" ], "windows": "tdc-Latn" @@ -108514,7 +113375,7 @@ "sldr": true, "tag": "tdd", "tags": [ "tdd-CN", "tdd-Tale" ], - "windows": "tdd-Tale" + "windows": "tdd-Tale-CN" }, { "full": "tdd-Tale-MM", @@ -108566,6 +113427,8 @@ "full": "tdg-Deva-NP", "iana": [ "Western Tamang" ], "iso639_3": "tdg", + "latnnames": [ "Pakṣcimī tāmāṅga" ], + "localnames": [ "पक्ष्चिमी तामाङ्ग" ], "name": "Tamang, Western", "names": [ "Murmi", "Sain", "Tamang Gyot", "Tamang Tam", "Western Tamang" ], "region": "NP", @@ -108573,7 +113436,7 @@ "script": "Deva", "sldr": false, "tag": "tdg", - "tags": [ "tdg-Deva", "tdg-NP" ], + "tags": [ "tdg-Deva", "tdg-NP", "tmk", "tmk-Deva", "tmk-Deva-NP", "tmk-NP" ], "windows": "tdg-Deva" }, { @@ -108602,7 +113465,7 @@ "regionname": "Nepal", "regions": [ "IN" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "tdh", "tags": [ "tdh-Deva", "tdh-NP" ], "windows": "tdh-Deva" @@ -108633,7 +113496,6 @@ "sldr": false, "tag": "tdj", "tags": [ "tdj-ID", "tdj-Latn" ], - "unwritten": true, "windows": "tdj-Latn" }, { @@ -108656,7 +113518,7 @@ "iana": [ "Sur" ], "iso639_3": "tdl", "name": "Kusur-Myet", - "names": [ "Ansur", "Dishili", "Naas-Dishili", "Nsur", "Sur", "Suru", "Tapshin", "Tapshinawa" ], + "names": [ "Dishili", "Naas-Dishili", "Sur", "Suru", "Tapshin", "Tapshinawa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -108702,7 +113564,7 @@ "iana": [ "Teme" ], "iso639_3": "tdo", "name": "Teme", - "names": [ "Tema" ], + "names": [ "Tema", "Temme" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -108776,8 +113638,8 @@ "full": "tdv-Latn-NG", "iana": [ "Toro" ], "iso639_3": "tdv", - "name": "Toro", - "names": [ "Turkwam" ], + "name": "Atoro", + "names": [ "Toro", "Turkpa", "Turkpan", "Turkwan" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -108792,8 +113654,9 @@ "iana": [ "Tandroy-Mahafaly Malagasy" ], "iso639_3": "tdx", "localnames": [ "Antandroy" ], + "macrolang": "mg", "name": "Malagasy, Tandroy-Mahafaly", - "names": [ "Antandroy", "Mahafaly", "Tandroy", "Tandroy-Mahafaly Malagasy" ], + "names": [ "Antandroy", "Mahafaly", "Ntandroy", "Tandroy", "Tandroy-Mahafaly Malagasy" ], "region": "MG", "regionname": "Madagascar", "script": "Latn", @@ -108825,10 +113688,10 @@ "localname": "తెలుగు", "localnames": [ "తెలుగు" ], "name": "Telugu", - "names": [ "Andhra", "Tailangi", "Telangire", "Telegu", "Telgi", "Tengu", "Terangi", "Tolangan" ], + "names": [ "Andhra", "Tailangi", "Telangire", "Telegu", "Telgi", "Telinga", "Teloogoo", "Telungu", "Tengu", "Tenungu", "Terangi", "Tolangan", "Trilinga" ], "region": "IN", "regionname": "India", - "regions": [ "AE", "AU", "BH", "CA", "FJ", "LK", "MU", "MY", "SG", "US" ], + "regions": [ "AE", "AU", "BH", "CA", "FJ", "LK", "MU", "MY", "SG", "US", "ZA" ], "script": "Telu", "sldr": true, "suppress": true, @@ -108841,7 +113704,7 @@ "iana": [ "Telugu" ], "iso639_3": "tel", "name": "Telugu", - "names": [ "Andhra", "Tailangi", "Telangire", "Telegu", "Telgi", "Tengu", "Terangi", "Tolangan" ], + "names": [ "Andhra", "Tailangi", "Telangire", "Telegu", "Telgi", "Telinga", "Teloogoo", "Telungu", "Tengu", "Tenungu", "Terangi", "Tolangan", "Trilinga" ], "nophonvars": true, "region": "IN", "regionname": "India", @@ -108856,7 +113719,7 @@ "iana": [ "Temiar" ], "iso639_3": "tea", "name": "Temiar", - "names": [ "Northern Sakai", "Pie", "Seroq", "Temer" ], + "names": [ "Northern Sakai", "Nèhtuh Temèèr", "Pie", "Seroq", "Temer" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -108884,6 +113747,7 @@ "full": "tec-Latn-KE", "iana": [ "Terik" ], "iso639_3": "tec", + "macrolang": "kln", "name": "Terik", "names": [ "Nyang’ori" ], "region": "KE", @@ -108899,10 +113763,10 @@ "full": "ted-Latn-CI", "iana": [ "Tepo Krumen" ], "iso639_3": "ted", - "localname": "Krumen, Tepo", - "localnames": [ "-tepowin" ], + "localname": "-tepowin", + "localnames": [ "Tepowin" ], "name": "Krumen, Tepo", - "names": [ "-tepowin", "Krou tepo", "Kroumen", "Kroumen tépo", "Kru", "Krumen", "Southern Krumen", "Southwestern Kroumen", "Tepo Krou", "Tepo Krumen" ], + "names": [ "Krou tepo", "Kroumen", "Kroumen tépo", "Kru", "Kru tepo", "Krumen", "Krumen tepo", "Southern Krumen", "Southwestern Kroumen", "Tepo Krou", "Tepo Krumen", "Tepowin" ], "region": "CI", "regionname": "Côte d'Ivoire", "regions": [ "LR" ], @@ -108916,8 +113780,9 @@ "full": "tee-Latn-MX", "iana": [ "Huehuetla Tepehua" ], "iso639_3": "tee", + "localnames": [ "Lhima'alh'ama'" ], "name": "Tepehua, Huehuetla", - "names": [ "Huehuetla Tepehua", "Tepehua de Hidalgo", "Tepehua de Huehuetla" ], + "names": [ "Huehuetla Tepehua", "Lhiimaqalhqama’", "Lhimakalhkama’", "Lhima’alh’ama’", "Lhīmakꞌalhkāmaꞌ", "Tepehua de Hidalgo", "Tepehua de Huehuetla" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -108960,7 +113825,7 @@ "iana": [ "Tehuelche" ], "iso639_3": "teh", "name": "Tehuelche", - "names": [ "Aoniken", "Gunua-Kena", "Gununa-Kena", "Gününa Küne", "Gününa Yajich", "Inaquen", "Tewelche" ], + "names": [ "Aonek’enk", "Aoniken", "Gunua-Kena", "Gununa-Kena", "Inaquen", "Patagón", "Tewelche" ], "region": "AR", "regionname": "Argentina", "script": "Latn", @@ -108974,8 +113839,8 @@ "full": "tei-Latn-PG", "iana": [ "Torricelli" ], "iso639_3": "tei", - "name": "Torricelli", - "names": [ "Anamagi", "Lou" ], + "name": "Aro", + "names": [ "Anamagi", "Lou", "Torricelli" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -109038,15 +113903,15 @@ "iso639_3": "teo", "localname": "Kiteso", "localnames": [ "Atεsᴐ" ], - "name": "Teso", - "names": [ "Ateso", "Atεsᴐ", "Bakedi", "Bakidi", "Elgumi", "Etossio", "Ikumama", "Iteso", "Wamia" ], + "name": "Ateso", + "names": [ "Atεsᴐ", "Bakedi", "Bakidi", "Elgumi", "Etossio", "Ikumama", "Iteso", "Teso", "Wamia" ], "region": "UG", "regionname": "Uganda", "script": "Latn", "sldr": true, "tag": "teo", "tags": [ "teo-Latn", "teo-UG" ], - "windows": "teo-Latn" + "windows": "teo-Latn-UG" }, { "full": "teo-Latn-KE", @@ -109054,8 +113919,8 @@ "iso639_3": "teo", "localname": "Kiteso", "localnames": [ "Atεsᴐ" ], - "name": "Teso", - "names": [ "Ateso" ], + "name": "Ateso", + "names": [ "Atεsᴐ", "Iteso", "Teso" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -109095,6 +113960,7 @@ "full": "ter-Latn-BR", "iana": [ "Tereno" ], "iso639_3": "ter", + "localname": "Emo'u xâne", "localnames": [ "Emo'u xâne" ], "name": "Terêna", "names": [ "Emo’u xâne", "Etelena", "Tereno" ], @@ -109127,7 +113993,7 @@ "localname": "Tetun", "localnames": [ "Tetun" ], "name": "Tetun", - "names": [ "Belo", "Belu", "Fehan", "Teto", "Tettum", "Tetu", "Tetum", "Tetun Belu", "Tetun Loos", "Tetun Terik", "Tetung" ], + "names": [ "Belo", "Belu", "Fehan", "Teto", "Tettum", "Tetu", "Tetum", "Tetun Belu", "Tetun Terik", "Tetung" ], "region": "TL", "regionname": "Timor-Leste", "regions": [ "ID" ], @@ -109220,9 +114086,9 @@ "iana": [ "Tennet" ], "iso639_3": "tex", "localname": "Tennette", - "localnames": [ "Tennette" ], + "localnames": [ "Te̱nne̱tte̱" ], "name": "Tennet", - "names": [ "Tenet", "Tennette" ], + "names": [ "Tenet", "Tennette", "Te̱nne̱tte̱" ], "region": "SS", "regionname": "South Sudan", "script": "Latn", @@ -109255,7 +114121,8 @@ "regionname": "Niger", "script": "Latn", "sldr": false, - "tag": "tez-Latn", + "tag": "tez", + "tags": [ "tez-Latn", "tez-NE" ], "unwritten": true, "windows": "tez-Latn" }, @@ -109279,7 +114146,7 @@ "iana": [ "Tofin Gbe" ], "iso639_3": "tfi", "name": "Gbe, Tofin", - "names": [ "Tofi", "Tofin", "Tofin Gbe", "Tofingbe" ], + "names": [ "Tofi", "Tofin", "Tofin Gbe", "Tofingbe", "Tɔfin", "Tɔfingbe" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -109365,7 +114232,7 @@ "sldr": true, "tag": "tg", "tags": [ "tg-Cyrl", "tg-TJ" ], - "windows": "tg-Cyrl" + "windows": "tg-Cyrl-TJ" }, { "full": "tg-Arab-PK", @@ -109381,7 +114248,7 @@ "sldr": false, "tag": "tg-Arab", "tags": [ "tg-PK" ], - "windows": "tg-Arab" + "windows": "tg-Arab-PK" }, { "full": "tg-Hebr-UZ", @@ -109396,7 +114263,7 @@ "sldr": false, "tag": "tg-Hebr", "tags": [ "tg-UZ" ], - "windows": "tg-Hebr" + "windows": "tg-Hebr-UZ" }, { "full": "tg-Latn-TJ", @@ -109409,7 +114276,7 @@ "script": "Latn", "sldr": false, "tag": "tg-Latn", - "windows": "tg-Latn" + "windows": "tg-Latn-TJ" }, { "full": "tga-Latn-KE", @@ -109473,41 +114340,41 @@ "windows": "tgd-Latn" }, { - "full": "tge-Zyyy-NP", + "full": "tge-Deva-NP", "iana": [ "Eastern Gorkha Tamang" ], "iso639_3": "tge", "name": "Tamang, Eastern Gorkha", "names": [ "Eastern Gorkha Tamang" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "tge", - "tags": [ "tge-NP", "tge-Zyyy" ], + "tags": [ "tge-Deva", "tge-NP" ], "unwritten": true, - "windows": "tge-Zyyy" + "windows": "tge-Deva" }, { - "full": "tgf-Zyyy-BT", + "full": "tgf-Tibt-BT", "iana": [ "Chalikha" ], "iso639_3": "tgf", "name": "Chalikha", "names": [ "Chali", "Chalipkha", "Tshali", "Tshalingpa" ], "region": "BT", "regionname": "Bhutan", - "script": "Zyyy", + "script": "Tibt", "sldr": false, "tag": "tgf", - "tags": [ "tgf-BT", "tgf-Zyyy" ], + "tags": [ "tgf-BT", "tgf-Tibt" ], "unwritten": true, - "windows": "tgf-Zyyy" + "windows": "tgf-Tibt" }, { "full": "tgh-Latn-TT", "iana": [ "Tobagonian Creole English" ], "iso639_3": "tgh", - "name": "Tobagonian Creole English", - "names": [ "Tobagonian Dialect" ], + "name": "Tobagonian English Creole", + "names": [ "Tobagonian Creole English", "Tobagonian Dialect" ], "region": "TT", "regionname": "Trinidad and Tobago", "script": "Latn", @@ -109536,8 +114403,9 @@ "full": "tgj-Latn-IN", "iana": [ "Tagin" ], "iso639_3": "tgj", + "localnames": [ "Tagin" ], "name": "Tagin", - "names": [ "Nil" ], + "names": [ "Nil", "Tagen" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -109579,8 +114447,9 @@ "full": "tgp-Latn-VU", "iana": [ "Tangoa" ], "iso639_3": "tgp", + "localnames": [ "Leon Tatangoa" ], "name": "Tangoa", - "names": [ "Movono", "Santo", "South Santo" ], + "names": [ "Leon Tatagoa", "Leon Tatangoa", "Mara Tatagoa", "Movono", "Santo", "South Santo" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -109645,6 +114514,34 @@ "tags": [ "tgt-Latn", "tgt-PH" ], "windows": "tgt-Latn" }, + { + "full": "tgt-Hano-PH", + "iana": [ "Central Tagbanwa" ], + "iso639_3": "tgt", + "name": "Tagbanwa, Central", + "names": [ "Central Tagbanwa", "Tagbanwa" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "script": "Hano", + "sldr": false, + "tag": "tgt-Hano", + "windows": "tgt-Hano" + }, + { + "full": "tgt-Tagb-PH", + "iana": [ "Central Tagbanwa" ], + "iso639_3": "tgt", + "name": "Tagbanwa, Central", + "names": [ "Central Tagbanwa", "Tagbanwa" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "script": "Tagb", + "sldr": false, + "tag": "tgt-Tagb", + "windows": "tgt-Tagb" + }, { "full": "tgu-Latn-PG", "iana": [ "Tanggu" ], @@ -109784,10 +114681,8 @@ "full": "the-Deva-NP", "iana": [ "Chitwania Tharu" ], "iso639_3": "the", - "latnnames": [ "Madhya Ksetriya Tharu" ], - "localnames": [ "मध्य क्षेत्रीय थारू" ], - "name": "Tharu, Madhya Ksetriya", - "names": [ "Chitwania Tharu", "Tharu, Chitwania" ], + "name": "Tharu, Central", + "names": [ "Chhetriya Tharu", "Chitawan Tharu", "Chitoniya Tharu", "Chitwan Tharu", "Chitwania Tharu", "Madhya Chhetriya Tharu", "Madhya Ksetriya Tharu", "Tharu, Chitwania" ], "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], @@ -109801,6 +114696,8 @@ "full": "thf-Deva-NP", "iana": [ "Thangmi" ], "iso639_3": "thf", + "latnnames": [ "thaangmi khaam" ], + "localnames": [ "थाङ्मी खाम" ], "name": "Thangmi", "names": [ "Dolakha", "Thami", "Thangmi Kham", "Thangmi Wakhe", "Thani" ], "region": "NP", @@ -109828,26 +114725,26 @@ "windows": "thh-Latn" }, { - "full": "thi-Zyyy-LA", + "full": "thi-Tale-LA", "iana": [ "Tai Long" ], "iso639_3": "thi", "name": "Tai Long", "region": "LA", "regionname": "Lao People's Democratic Republic", - "script": "Zyyy", + "script": "Tale", "sldr": false, "tag": "thi", - "tags": [ "thi-LA", "thi-Zyyy" ], - "windows": "thi-Zyyy" + "tags": [ "thi-LA", "thi-Tale" ], + "windows": "thi-Tale" }, { "full": "thk-Latn-KE", "iana": [ "Tharaka", "Kitharaka" ], "iso639_3": "thk", - "localname": "Kitharaka", - "localnames": [ "Kitharaka" ], + "localname": "Kîîtharaka", + "localnames": [ "Kîîtharaka" ], "name": "Kitharaka", - "names": [ "Atharaka", "Saraka", "Sharoka", "Tharaka" ], + "names": [ "Atharaka", "Kîîtharaka", "Saraka", "Sharoka", "Tharaka" ], "region": "KE", "regionname": "Kenya", "script": "Latn", @@ -109860,19 +114757,21 @@ "full": "thl-Deva-NP", "iana": [ "Dangaura Tharu" ], "iso639_3": "thl", + "latnnames": [ "thaaru" ], + "localnames": [ "थारु" ], "name": "Tharu, Dangaura", "names": [ "Chaudary", "Chaudhari", "Chaudhuri", "Dang", "Dangaha", "Dangali", "Dangauli", "Dangaura Tharu", "Dangora", "Dangura", "Tharu" ], "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], "script": "Deva", - "sldr": false, + "sldr": true, "tag": "thl", "tags": [ "thl-Deva", "thl-NP" ], "windows": "thl-Deva" }, { - "full": "thm-Zyyy-TH", + "full": "thm-Thai-TH", "iana": [ "Aheu" ], "iso639_3": "thm", "name": "Aheu", @@ -109880,12 +114779,12 @@ "region": "TH", "regionname": "Thailand", "regions": [ "LA" ], - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "thm", - "tags": [ "thm-TH", "thm-Zyyy" ], + "tags": [ "thm-TH", "thm-Thai" ], "unwritten": true, - "windows": "thm-Zyyy" + "windows": "thm-Thai" }, { "full": "thn-Zyyy-IN", @@ -109935,10 +114834,10 @@ "full": "thq-Deva-NP", "iana": [ "Kochila Tharu" ], "iso639_3": "thq", - "latnnames": [ "Madhya-Purbiya Tharu" ], + "latnnames": [ "madhya-pūrviyā thārū" ], "localnames": [ "मध्य-पूर्विया थारू" ], - "name": "Tharu, Madhya-Purbiya", - "names": [ "Kochila Tharu", "Mid-Eastern Tharu", "Saptari", "Tharu, Kochila" ], + "name": "Tharu, Mid-Eastern", + "names": [ "Kochila Tharu", "Madhya-Purbiya Tharu", "Saptari", "Tharu, Kochila" ], "region": "NP", "regionname": "Nepal", "regions": [ "IN" ], @@ -109952,6 +114851,8 @@ "full": "thr-Deva-NP", "iana": [ "Rana Tharu" ], "iso639_3": "thr", + "latnnames": [ "raanaa thaaru" ], + "localnames": [ "राना थारू" ], "name": "Tharu, Rana", "names": [ "Rana Thakur", "Rana Tharu", "Tharu", "Tharuwa" ], "region": "NP", @@ -110007,37 +114908,45 @@ "windows": "thu-Latn" }, { - "full": "thv-Arab-DZ", + "full": "thv-Latn-DZ", "iana": [ "Tahaggart Tamahaq" ], "iso639_3": "thv", + "localnames": [ "Tamahaq" ], + "macrolang": "tmh", "name": "Tamahaq, Tahaggart", "names": [ "Tahaggart Tamahaq", "Tamachek", "Tamachek’", "Tamahaq", "Tamashekin", "Tamasheq", "Tomachek", "Touareg", "Tourage", "Toureg", "Tuareg" ], - "nophonvars": true, "region": "DZ", "regionname": "Algeria", "regions": [ "LY", "NE" ], - "script": "Arab", - "sldr": false, - "tag": "thv-Arab", - "windows": "thv-Arab" + "script": "Latn", + "sldr": true, + "tag": "thv", + "tags": [ "thv-DZ", "thv-Latn" ], + "windows": "thv-Latn" }, { - "full": "thv-Latn-DZ", + "full": "thv-Arab-DZ", "iana": [ "Tahaggart Tamahaq" ], "iso639_3": "thv", + "macrolang": "tmh", "name": "Tamahaq, Tahaggart", "names": [ "Tahaggart Tamahaq", "Tamachek", "Tamachek’", "Tamahaq", "Tamashekin", "Tamasheq", "Tomachek", "Touareg", "Tourage", "Toureg", "Tuareg" ], + "nophonvars": true, "region": "DZ", "regionname": "Algeria", "regions": [ "LY", "NE" ], - "script": "Latn", + "script": "Arab", "sldr": false, - "tag": "thv-Latn", - "windows": "thv-Latn" + "tag": "thv-Arab", + "windows": "thv-Arab" }, { "full": "thv-Latn-DZ-x-ahaggar", "iana": [ "Tahaggart Tamahaq" ], + "iso639_3": "thv", + "localname": "Tamahaq", + "localnames": [ "Tamahaq" ], + "macrolang": "tmh", "name": "Tamahaq, Tahaggart", "names": [ "Tahaggart Tamahaq", "Tamachek", "Tamachek’", "Tamahaq", "Tamashekin", "Tamasheq", "Tomachek", "Touareg", "Tourage", "Tuareg" ], "region": "DZ", @@ -110051,6 +114960,7 @@ "full": "thv-Tfng-DZ", "iana": [ "Tahaggart Tamahaq" ], "iso639_3": "thv", + "macrolang": "tmh", "name": "Tamahaq, Tahaggart", "names": [ "Tahaggart Tamahaq", "Tamachek", "Tamachek’", "Tamahaq", "Tamashekin", "Tamasheq", "Tomachek", "Touareg", "Tourage", "Toureg", "Tuareg" ], "nophonvars": true, @@ -110062,20 +114972,6 @@ "tag": "thv-Tfng", "windows": "thv-Tfng" }, - { - "full": "thw-Zyyy-NP", - "iana": [ "Thudam" ], - "iso639_3": "thw", - "name": "Thudam", - "region": "NP", - "regionname": "Nepal", - "script": "Zyyy", - "sldr": false, - "tag": "thw", - "tags": [ "thw-NP", "thw-Zyyy" ], - "unwritten": true, - "windows": "thw-Zyyy" - }, { "full": "thy-Latn-NG", "iana": [ "Tha" ], @@ -110096,19 +114992,22 @@ "iana": [ "Tayart Tamajeq" ], "iso639_3": "thz", "localnames": [ "Tamajeq" ], + "macrolang": "tmh", "name": "Tamajeq, Tayart", "names": [ "Amazigh", "Tamachek", "Tamajeq", "Tayart Tamajeq", "Tomacheck", "Touareg", "Tuareg" ], "region": "NE", "regionname": "Niger", "script": "Latn", "sldr": false, - "tag": "thz-Latn", + "tag": "thz", + "tags": [ "thz-Latn", "thz-NE" ], "windows": "thz-Latn" }, { "full": "thz-Tfng-NE", "iana": [ "Tayart Tamajeq" ], "iso639_3": "thz", + "macrolang": "tmh", "name": "Tamajeq, Tayart", "names": [ "Amazigh", "Tamachek", "Tamajeq", "Tayart Tamajeq", "Tomacheck", "Touareg", "Tuareg" ], "nophonvars": true, @@ -110136,7 +115035,7 @@ "suppress": true, "tag": "ti", "tags": [ "ti-ET", "ti-Ethi" ], - "windows": "ti" + "windows": "ti-ET" }, { "full": "ti-Arab-ET", @@ -110151,7 +115050,7 @@ "script": "Arab", "sldr": false, "tag": "ti-Arab", - "windows": "ti-Arab" + "windows": "ti-Arab-ET" }, { "full": "ti-Ethi-ER", @@ -110166,9 +115065,8 @@ "regionname": "Eritrea", "script": "Ethi", "sldr": true, - "suppress": true, "tag": "ti-ER", - "windows": "ti-ER" + "windows": "ti-Ethi-ER" }, { "full": "tia-Zyyy-DZ", @@ -110221,7 +115119,7 @@ "iana": [ "Tigre" ], "iso639_3": "tig", "latnnames": [ "Tigrayit/Tigraayit" ], - "localname": "ትግረ", + "localname": "ትግራይት", "localnames": [ "ትግራይት" ], "name": "Tigré", "names": [ "Tigre" ], @@ -110232,7 +115130,22 @@ "sldr": true, "tag": "tig", "tags": [ "tig-ER", "tig-Ethi" ], - "windows": "tig-Ethi" + "windows": "tig-Ethi-ER" + }, + { + "full": "tig-Arab-ER", + "iana": [ "Tigre" ], + "iso639_3": "tig", + "name": "Tigré", + "names": [ "Tigre" ], + "nophonvars": true, + "region": "ER", + "regionname": "Eritrea", + "regions": [ "SD" ], + "script": "Arab", + "sldr": false, + "tag": "tig-Arab", + "windows": "tig-Arab-ER" }, { "full": "tig-Latn-SD", @@ -110310,18 +115223,18 @@ "windows": "tik-Latn" }, { - "full": "til-Zyyy-US", + "full": "til-Latn-US", "iana": [ "Tillamook" ], "iso639_3": "til", "name": "Tillamook", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "til", - "tags": [ "til-US", "til-Zyyy" ], + "tags": [ "til-Latn", "til-US" ], "unwritten": true, - "windows": "til-Zyyy" + "windows": "til-Latn" }, { "full": "tim-Latn-PG", @@ -110428,7 +115341,7 @@ "iana": [ "Adasen" ], "iso639_3": "tiu", "name": "Adasen", - "names": [ "Addasen", "Addasen Tinguian", "Itneg Adasen" ], + "names": [ "Addasen", "Addasen Tinguian", "Addassen", "Itneg Adasen" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -110444,6 +115357,7 @@ "localname": "Tiv", "localnames": [ "Tiv" ], "name": "Tiv", + "names": [ "Tivi" ], "region": "NG", "regionname": "Nigeria", "regions": [ "CM" ], @@ -110538,22 +115452,21 @@ "sldr": false, "tag": "tjg", "tags": [ "tjg-ID", "tjg-Latn" ], - "unwritten": true, "windows": "tjg-Latn" }, { - "full": "tji-Zyyy-CN", + "full": "tji-Latn-CN", "iana": [ "Northern Tujia" ], "iso639_3": "tji", "name": "Tujia, Northern", "names": [ "Northern Tujia", "Tuchia", "Tudja", "pi tsi kha" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tji", - "tags": [ "tji-CN", "tji-Zyyy" ], - "windows": "tji-Zyyy" + "tags": [ "tji-CN", "tji-Latn" ], + "windows": "tji-Latn" }, { "full": "tjj-Latn-AU", @@ -110584,16 +115497,16 @@ "windows": "tjl-Mymr" }, { - "full": "tjm-Zyyy-XX", + "full": "tjm-Latn-XX", "iana": [ "Timucua" ], "iso639_3": "tjm", "name": "Timucua", "region": "XX", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tjm", - "tags": [ "tjm-XX", "tjm-Zyyy" ], - "windows": "tjm-Zyyy" + "tags": [ "tjm-Latn", "tjm-XX" ], + "windows": "tjm-Latn" }, { "full": "tjn-Latn-CI", @@ -110610,19 +115523,19 @@ "windows": "tjn-Latn" }, { - "full": "tjo-Zyyy-DZ", + "full": "tjo-Arab-DZ", "iana": [ "Temacine Tamazight" ], "iso639_3": "tjo", "name": "Tamazight, Temacine", "names": [ "Temacine Tamazight", "Touggourt", "Tougourt", "Tugurt" ], "region": "DZ", "regionname": "Algeria", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "tjo", - "tags": [ "tjo-DZ", "tjo-Zyyy" ], + "tags": [ "tjo-Arab", "tjo-DZ" ], "unwritten": true, - "windows": "tjo-Zyyy" + "windows": "tjo-Arab" }, { "full": "tjp-Latn-AU", @@ -110640,18 +115553,18 @@ "windows": "tjp-Latn" }, { - "full": "tjs-Zyyy-CN", + "full": "tjs-Latn-CN", "iana": [ "Southern Tujia" ], "iso639_3": "tjs", "name": "Tujia, Southern", "names": [ "Mong Tsi", "Southern Tujia", "Tuchia" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tjs", - "tags": [ "tjs-CN", "tjs-Zyyy" ], - "windows": "tjs-Zyyy" + "tags": [ "tjs-CN", "tjs-Latn" ], + "windows": "tjs-Latn" }, { "full": "tju-Latn-AU", @@ -110699,7 +115612,7 @@ "tag": "tk", "tags": [ "tk-Latn", "tk-TM" ], "variants": [ "baku1926" ], - "windows": "tk-Latn" + "windows": "tk-Latn-TM" }, { "full": "tk-Arab-AF", @@ -110716,7 +115629,7 @@ "sldr": true, "tag": "tk-Arab", "tags": [ "tk-AF" ], - "windows": "tk-Arab" + "windows": "tk-Arab-AF" }, { "full": "tk-Arab-IR", @@ -110736,6 +115649,7 @@ "iana": [ "Turkmen" ], "iso639_3": "tuk", "latnnames": [ "Türkmençe" ], + "localname": "түркменче", "localnames": [ "түркменче" ], "name": "Turkmen", "names": [ "Torkomani", "Trukhmen", "Trukhmeny", "Turkman", "Turkmani", "Turkmanian", "Turkmencha", "Turkmeni", "Turkmenian", "Turkmenler", "Turkomans", "Turkpen", "Türkmen dili", "Türkmençe" ], @@ -110744,9 +115658,9 @@ "regionname": "Turkmenistan", "regions": [ "AF", "DE", "IQ", "IR", "KG", "KZ", "PK", "RU", "SY", "TJ", "TR", "US", "UZ" ], "script": "Cyrl", - "sldr": false, + "sldr": true, "tag": "tk-Cyrl", - "windows": "tk-Cyrl" + "windows": "tk-Cyrl-TM" }, { "full": "tka-Latn-BR", @@ -110767,6 +115681,7 @@ "iana": [ "Buksa" ], "iso639_3": "tkb", "name": "Buksa", + "names": [ "Bhoksha", "Bhuksa", "Bukas", "Buxa" ], "region": "IN", "regionname": "India", "script": "Deva", @@ -110779,8 +115694,9 @@ "full": "tkd-Latn-TL", "iana": [ "Tukudede" ], "iso639_3": "tkd", + "localnames": [ "Tokodede" ], "name": "Tukudede", - "names": [ "Tocod", "Tokodede", "Tokodé", "Tukude" ], + "names": [ "Tocod", "Tocodede", "Tokodede", "Tokodé", "Tukude" ], "region": "TL", "regionname": "Timor-Leste", "script": "Latn", @@ -110793,9 +115709,10 @@ "full": "tke-Latn-MZ", "iana": [ "Takwane" ], "iso639_3": "tke", - "localnames": [ "Etakwani" ], + "localname": "Etakwani", + "localnames": [ "ETakwani" ], "name": "Takwane", - "names": [ "Thakwani" ], + "names": [ "ETakwani", "Etakwane", "Thakwani" ], "region": "MZ", "regionname": "Mozambique", "script": "Latn", @@ -110824,6 +115741,7 @@ "iana": [ "Tesaka Malagasy" ], "iso639_3": "tkg", "localnames": [ "Tesaka Malagasy" ], + "macrolang": "mg", "name": "Malagasy, Tesaka", "names": [ "Antaisaka", "Antesaka", "Atesaka", "Tesaka Malagasy" ], "region": "MG", @@ -110894,8 +115812,8 @@ "full": "tkq-Latn-NG", "iana": [ "Tee" ], "iso639_3": "tkq", - "name": "Tee", - "names": [ "Tai" ], + "name": "Tèẹ̀ ̣", + "names": [ "Tai", "Tee" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -110919,7 +115837,7 @@ "sldr": false, "tag": "tkr", "tags": [ "tkr-AZ", "tkr-Latn" ], - "windows": "tkr-Latn" + "windows": "tkr-Latn-AZ" }, { "full": "tkr-Cyrl-RU", @@ -110935,7 +115853,7 @@ "sldr": false, "tag": "tkr-Cyrl", "tags": [ "tkr-RU" ], - "windows": "tkr-Cyrl" + "windows": "tkr-Cyrl-RU" }, { "full": "tks-Arab-IR", @@ -110955,6 +115873,8 @@ "full": "tkt-Deva-NP", "iana": [ "Kathoriya Tharu" ], "iso639_3": "tkt", + "latnnames": [ "thaaru" ], + "localnames": [ "थारु" ], "name": "Tharu, Kathariya", "names": [ "Kathariya", "Kathoriya Tharu", "Khatima Tharu", "Tharu" ], "region": "NP", @@ -110971,9 +115891,9 @@ "iana": [ "Upper Necaxa Totonac" ], "iso639_3": "tku", "localname": "Totonaca", - "localnames": [ "Totonaca" ], + "localnames": [ "Totonaco", "Totonaco xalac Patla" ], "name": "Totonac, Upper Necaxa", - "names": [ "Totonaca", "Totonaco del río Necaxa", "Upper Necaxa Totonac" ], + "names": [ "Totonaca", "Totonaco", "Totonaco del río Necaxa", "Totonaco xalac Patla", "Upper Necaxa Totonac" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -111046,10 +115966,10 @@ "iso639_3": "tgl", "localnames": [ "Tagalog" ], "name": "Tagalog", - "names": [ "Filipino", "Wikang Tagalog" ], + "names": [ "Wikang Tagalog" ], "region": "PH", "regionname": "Philippines", - "regions": [ "AE", "AS", "AU", "BH", "CA", "CY", "DE", "FI", "GU", "HK", "IE", "KY", "LY", "MO", "MP", "NZ", "OM", "PW", "QA", "SA", "US" ], + "regions": [ "AE", "AS", "AU", "BH", "CA", "CY", "DE", "FI", "GU", "HK", "IE", "KY", "LY", "MO", "MP", "NZ", "OM", "PW", "QA", "SA", "SE", "US" ], "script": "Latn", "sldr": false, "suppress": true, @@ -111062,7 +115982,7 @@ "iana": [ "Tagalog" ], "iso639_3": "tgl", "name": "Tagalog", - "names": [ "Filipino", "Wikang Tagalog" ], + "names": [ "Wikang Tagalog" ], "nophonvars": true, "region": "PH", "regionname": "Philippines", @@ -111072,12 +115992,42 @@ "tag": "tl-Brai", "windows": "tl-Brai" }, + { + "full": "tl-Buhd-PH", + "iana": [ "Tagalog" ], + "iso639_3": "tgl", + "name": "Tagalog", + "names": [ "Wikang Tagalog" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "regions": [ "AE", "AS", "AU", "BH", "CA", "CY", "DE", "FI", "GU", "HK", "IE", "KY", "LY", "MO", "MP", "NZ", "OM", "PW", "QA", "SA", "US" ], + "script": "Buhd", + "sldr": false, + "tag": "tl-Buhd", + "windows": "tl-Buhd" + }, + { + "full": "tl-Hano-PH", + "iana": [ "Tagalog" ], + "iso639_3": "tgl", + "name": "Tagalog", + "names": [ "Wikang Tagalog" ], + "nophonvars": true, + "region": "PH", + "regionname": "Philippines", + "regions": [ "AE", "AS", "AU", "BH", "CA", "CY", "DE", "FI", "GU", "HK", "IE", "KY", "LY", "MO", "MP", "NZ", "OM", "PW", "QA", "SA", "US" ], + "script": "Hano", + "sldr": false, + "tag": "tl-Hano", + "windows": "tl-Hano" + }, { "full": "tl-Tglg-PH", "iana": [ "Tagalog" ], "iso639_3": "tgl", "name": "Tagalog", - "names": [ "Filipino", "Wikang Tagalog" ], + "names": [ "Wikang Tagalog" ], "nophonvars": true, "obsolete": true, "region": "PH", @@ -111121,7 +116071,7 @@ "iana": [ "Yecuatla Totonac" ], "iso639_3": "tlc", "name": "Totonac, Yecuatla", - "names": [ "Misantla Totonac", "Yecuatla Totonac" ], + "names": [ "Laakanaachiwíin", "Misantla Totonac", "Yecuatla Totonac" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -111207,9 +116157,9 @@ "full": "tli-Latn-US", "iana": [ "Tlingit" ], "iso639_3": "tli", - "localnames": [ "Łingít" ], + "localnames": [ "Lingít", "Łingít" ], "name": "Tlingit", - "names": [ "Inland Tlingit", "Kolosch", "Kolosh", "Thlinget", "Tlinkit", "Łingít" ], + "names": [ "Inland Tlingit", "Kolosch", "Kolosh", "Lingít", "Thlinget", "Tlinkit", "Łingít" ], "region": "US", "regionname": "United States", "regions": [ "CA" ], @@ -111224,7 +116174,7 @@ "iana": [ "Tlingit" ], "iso639_3": "tli", "name": "Tlingit", - "names": [ "Inland Tlingit", "Kolosch", "Kolosh", "Thlinget", "Tlinkit", "Łingít" ], + "names": [ "Inland Tlingit", "Kolosch", "Kolosh", "Lingít", "Thlinget", "Tlinkit", "Łingít" ], "nophonvars": true, "region": "US", "regionname": "United States", @@ -111238,8 +116188,8 @@ "full": "tlj-Latn-UG", "iana": [ "Talinga-Bwisi" ], "iso639_3": "tlj", - "localname": "Bwisi", - "localnames": [ "Bwisi" ], + "localname": "Lubwisi", + "localnames": [ "Lubwisi" ], "name": "Talinga-Bwisi", "names": [ "Bwisi", "Bwissi", "Kitalinga", "Lubwisi", "Lubwissi", "Mawissi", "Olubwisi", "Talinga", "Talinge" ], "region": "UG", @@ -111251,20 +116201,6 @@ "tags": [ "tlj-Latn", "tlj-UG" ], "windows": "tlj-Latn" }, - { - "full": "tlj-Latn-GH", - "iana": [ "Talinga-Bwisi" ], - "iso639_3": "tlj", - "localname": "Bwisi", - "localnames": [ "Bwisi" ], - "name": "Talinga-Bwisi", - "region": "GH", - "regionname": "Ghana", - "script": "Latn", - "sldr": true, - "tag": "tlj-GH", - "windows": "tlj-Latn-GH" - }, { "full": "tlk-Latn-ID", "iana": [ "Taloki" ], @@ -111284,8 +116220,9 @@ "full": "tll-Latn-CD", "iana": [ "Tetela" ], "iso639_3": "tll", + "localnames": [ "Otetela", "Ɔtɛtɛla" ], "name": "Tetela", - "names": [ "Otetela", "Sungu" ], + "names": [ "Kikitatela", "Kitetela", "Otetela", "Sungu", "Ɔtɛtɛla" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -111343,7 +116280,7 @@ "iana": [ "Filomena Mata-Coahuitlán Totonac" ], "iso639_3": "tlp", "name": "Totonac, Filomena Mata-Coahuitlán", - "names": [ "Filomena Mata-Coahuitlán Totonac", "Santo Domingo Totonac", "Totonaco central del norte", "Totonaco de Filomena Mata-Coahuitlán" ], + "names": [ "Filomena Mata-Coahuitlán Totonac", "Liisantutumínku", "Santo Domingo Totonac", "Totonaco Central Alto", "Totonaco central del norte", "Totonaco de Filomena Mata-Coahuitlán", "Tutunáku" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -111357,7 +116294,7 @@ "iana": [ "Tai Loi" ], "iso639_3": "tlq", "name": "Tai Loi", - "names": [ "Aw-aak", "Bulang-Su", "Doi", "Khabe", "Kon Doi", "Kon Loi", "Lao Doi", "Loi", "Monglwe", "Tai Doi", "Tai-Loi", "Tailoi", "Wakut" ], + "names": [ "Aw-aak", "Bulang-Su", "Doi", "Khabe", "Kon Doi", "Kon Loi", "Lao Doi", "Loi", "Mong Lue", "Monglwe", "Tai Doi", "Tai-Loi", "Tailoi", "Wakut" ], "region": "MM", "regionname": "Myanmar", "regions": [ "LA" ], @@ -111537,7 +116474,7 @@ "iana": [ "Tumak" ], "iso639_3": "tmc", "name": "Tumak", - "names": [ "Dije", "Sara Toumak", "Toumak", "Tumac", "Tumag", "Tummok" ], + "names": [ "Bə̀g ɗə təmàgə́ə̀n", "Dije", "Sara Toumak", "Toumak", "Tumac", "Tumag", "Tummok" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -111581,8 +116518,9 @@ "full": "tmf-Latn-PY", "iana": [ "Toba-Maskoy" ], "iso639_3": "tmf", + "localnames": [ "Enenlhet" ], "name": "Toba-Maskoy", - "names": [ "Cabanatit", "Enenxet", "Machicui", "Quilyilhrayrom", "Toba of Paraguay", "Toba-Enenlhet" ], + "names": [ "Cabanatit", "Enenlhet", "Enenxet", "Machicui", "Quilyilhrayrom", "Toba of Paraguay", "Toba-Enenlhet" ], "region": "PY", "regionname": "Paraguay", "script": "Latn", @@ -111611,9 +116549,9 @@ "iana": [ "Tamashek" ], "iso639_3": "tmh", "localname": "Tamajaq, Tawallammat", - "localnames": [ "Tamajaq" ], - "name": "Tamajaq, Tawallammat", - "names": [ "Amazigh", "Azbinawa", "Buzu", "Tahoua", "Tahoua Tamajeq", "Tajag", "Tamachek", "Tamajaq", "Tamajeq", "Tamashek", "Tamashekin", "Tamasheq", "Tewellemet", "Tomacheck", "Touareg", "Tourage", "Tuareg" ], + "localnames": [ "Təmajəq" ], + "name": "Tamashek", + "names": [ "Amazigh", "Azbinawa", "Bugaje", "Buzu", "Tahoua", "Tahoua Tamajeq", "Tajag", "Tamachek", "Tamajaq", "Tamajaq, Tawallammat", "Tamajeq", "Tamashekin", "Tamasheq", "Tewellemet", "Tomacheck", "Touareg", "Tourage", "Tuareg", "Təmajəq" ], "region": "NE", "regionname": "Niger", "regions": [ "DZ", "ML", "NG" ], @@ -111628,15 +116566,17 @@ "full": "tmh-Arab-NE", "iana": [ "Tamashek" ], "iso639_3": "ttq", - "localname": "Tamajaq, Tawallammat", + "latnnames": [ "tamāğiq" ], + "localnames": [ "تَمَاجِقْ" ], + "macrolang": "tmh", "name": "Tamajaq, Tawallammat", - "names": [ "Amazigh", "Azbinawa", "Buzu", "Tahoua", "Tahoua Tamajeq", "Tajag", "Tamachek", "Tamajaq", "Tamajeq", "Tamashek", "Tamashekin", "Tamasheq", "Tewellemet", "Tomacheck", "Touareg", "Tourage", "Tuareg" ], + "names": [ "Amazigh", "Azbinawa", "Bugaje", "Buzu", "Tahoua", "Tahoua Tamajeq", "Tajag", "Tamachek", "Tamajaq", "Tamajeq", "Tamashek", "Tamashekin", "Tamasheq", "Tewellemet", "Tomacheck", "Touareg", "Tourage", "Tuareg", "Təmajəq" ], "nophonvars": true, "region": "NE", "regionname": "Niger", "regions": [ "ML", "NG" ], "script": "Arab", - "sldr": true, + "sldr": false, "tag": "tmh-Arab", "tags": [ "ttq-Arab", "ttq-Arab-NE" ], "windows": "tmh-Arab" @@ -111645,15 +116585,17 @@ "full": "tmh-Tfng-NE", "iana": [ "Tamashek" ], "iso639_3": "ttq", - "localname": "Tamajaq, Tawallammat", + "latnnames": [ "Tamajaq" ], + "localnames": [ "ⵜⵎⵌⵆ" ], + "macrolang": "tmh", "name": "Tamajaq, Tawallammat", - "names": [ "Amazigh", "Azbinawa", "Buzu", "Tahoua", "Tahoua Tamajeq", "Tajag", "Tamachek", "Tamajaq", "Tamajeq", "Tamashek", "Tamashekin", "Tamasheq", "Tewellemet", "Tomacheck", "Touareg", "Tourage", "Tuareg" ], + "names": [ "Amazigh", "Azbinawa", "Bugaje", "Buzu", "Tahoua", "Tahoua Tamajeq", "Tajag", "Tamachek", "Tamajaq", "Tamajeq", "Tamashek", "Tamashekin", "Tamasheq", "Tewellemet", "Tomacheck", "Touareg", "Tourage", "Tuareg", "Təmajəq" ], "nophonvars": true, "region": "NE", "regionname": "Niger", "regions": [ "ML", "NG" ], "script": "Tfng", - "sldr": true, + "sldr": false, "tag": "tmh-Tfng", "tags": [ "ttq-Tfng", "ttq-Tfng-NE" ], "windows": "tmh-Tfng" @@ -111687,33 +116629,19 @@ "unwritten": true, "windows": "tmj-Latn" }, - { - "full": "tmk-Deva-NP", - "iana": [ "Northwestern Tamang" ], - "iso639_3": "tmk", - "name": "Tamang, Northwestern", - "names": [ "Kath-Bhotiya", "Lama Bhote", "Murmi", "Northwestern Tamang", "Rongba", "Sain", "Tamang Gyoi", "Tamang Gyot", "Tamang Lengmo", "Tamang Tam" ], - "region": "NP", - "regionname": "Nepal", - "script": "Deva", - "sldr": false, - "tag": "tmk", - "tags": [ "tmk-Deva", "tmk-NP" ], - "windows": "tmk-Deva" - }, { "full": "tml-Latn-ID", "iana": [ "Tamnim Citak" ], "iso639_3": "tml", + "localnames": [ "Kau Adakam" ], "name": "Citak, Tamnim", - "names": [ "Asmat Darat", "Tamnim", "Tamnim Citak" ], + "names": [ "Asmat Darat", "Kau Adakam", "Tamnim", "Tamnim Citak" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", "sldr": false, "tag": "tml", "tags": [ "tml-ID", "tml-Latn" ], - "unwritten": true, "windows": "tml-Latn" }, { @@ -111851,6 +116779,7 @@ "full": "tmw-Latn-MY", "iana": [ "Temuan" ], "iso639_3": "tmw", + "macrolang": "ms", "name": "Temuan", "names": [ "Benua", "Niap" ], "region": "MY", @@ -111895,7 +116824,7 @@ "iana": [ "Tswana" ], "iso639_3": "tsn", "localname": "Setswana", - "localnames": [ "Setswana", "Tswana" ], + "localnames": [ "Setswana" ], "name": "Setswana", "names": [ "Bechuana", "Beetjuans", "Chuana", "Chwana", "Coana", "Cuana", "Sechuana", "Secoana", "Secwana", "Tshwana", "Tsiwaha", "Tswana" ], "region": "ZA", @@ -111906,22 +116835,21 @@ "suppress": true, "tag": "tn", "tags": [ "tn-Latn", "tn-ZA" ], - "windows": "tn" + "windows": "tn-ZA" }, { "full": "tn-Latn-BW", "iana": [ "Tswana" ], "iso639_3": "tsn", "localname": "Setswana", - "localnames": [ "Setswana", "Tswana" ], + "localnames": [ "Setswana" ], "name": "Tswana", "region": "BW", "regionname": "Botswana", "script": "Latn", "sldr": true, - "suppress": true, "tag": "tn-BW", - "windows": "tn-BW" + "windows": "tn-Latn-BW" }, { "full": "tna-Latn-BO", @@ -111956,8 +116884,9 @@ "full": "tnc-Latn-CO", "iana": [ "Tanimuca-Retuarã" ], "iso639_3": "tnc", - "name": "Tanimuca-Retuarã", - "names": [ "Letuama", "Letuhama", "Ohañara", "Opaina", "Retuama", "Retuarã", "Tanimboka", "Tanimuca-Letuama", "Uairã", "Ufaina" ], + "localnames": [ "Wejeñememaja" ], + "name": "Tanimuca-Letuama", + "names": [ "Letuama", "Letuhama", "Ohañara", "Opaina", "Retuama", "Retuarã", "Tanimboka", "Tanimuca-Retuarã", "Uairã", "Ufaina", "Wejeñememaja" ], "region": "CO", "regionname": "Colombia", "script": "Latn", @@ -112029,10 +116958,10 @@ "full": "tnk-Latn-VU", "iana": [ "Kwamera" ], "iso639_3": "tnk", - "localname": "Nəfe", + "localname": "Nɨfe", "localnames": [ "Nɨfe", "Nɨninɨfe" ], "name": "Kwamera", - "names": [ "Nafe", "Ncfe", "Neninafe", "Nxnincfe", "Nəfe", "Nɨfe", "Nɨninɨfe", "Southeast Tanna" ], + "names": [ "Nafe", "Ncfe", "Neninafe", "Nəfe", "Nɨfe", "Nɨninɨfe", "Southeast Tanna" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -112074,7 +117003,7 @@ "full": "tnn-Latn-VU", "iana": [ "North Tanna" ], "iso639_3": "tnn", - "localname": "Tanna, North", + "localname": "Nən-naka", "localnames": [ "Nən-naka" ], "name": "Tanna, North", "names": [ "North Tanna", "Not Tana", "Nən-naka" ], @@ -112286,9 +117215,9 @@ "full": "tob-Latn-AR", "iana": [ "Toba" ], "iso639_3": "tob", - "localnames": [ "Qom" ], + "localnames": [ "Qom l’aqtac" ], "name": "Toba", - "names": [ "Chaco Sur", "Emok-Lik", "Namqom", "Qom", "Qom-Lik", "Qoml’ek", "Takshika", "Toba Qom", "Toba Sur", "Toba-Qom" ], + "names": [ "Chaco Sur", "Emok-Lik", "Namqom", "Qom", "Qom l’aqtac", "Qom-Lik", "Qoml’ek", "Takshika", "Toba Qom", "Toba Sur" ], "region": "AR", "regionname": "Argentina", "regions": [ "BO", "PY" ], @@ -112305,7 +117234,7 @@ "localname": "Tutunakú", "localnames": [ "Tutunakú" ], "name": "Totonac, Coyutla", - "names": [ "Coyutla Totonac", "Totonaco", "Totonaco de Coyutla", "Tutunakú" ], + "names": [ "Coyutla Totonac", "Totonaco", "Totonaco Central del Sur", "Totonaco de Coyutla", "Tutunakú" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -112396,7 +117325,7 @@ "iana": [ "Tojolabal" ], "iso639_3": "toj", "localname": "Tojolabal", - "localnames": [ "Tojolabal" ], + "localnames": [ "Tojol-ab'al" ], "name": "Tojolabal", "names": [ "Chañabal", "Comiteco", "Tojol-ab’al" ], "region": "MX", @@ -112407,6 +117336,20 @@ "tags": [ "toj-Latn", "toj-MX" ], "windows": "toj-Latn" }, + { + "full": "tok-Latn-001", + "iana": [ "Toki Pona" ], + "iso639_3": "tok", + "localname": "Toki Pona", + "name": "Toki Pona", + "region": "001", + "regionname": "World", + "script": "Latn", + "sldr": true, + "tag": "tok", + "tags": [ "tok-001", "tok-Latn" ], + "windows": "tok-Latn" + }, { "full": "tol-Latn-US", "iana": [ "Tolowa" ], @@ -112441,8 +117384,10 @@ "full": "too-Latn-MX", "iana": [ "Xicotepec De Juárez Totonac" ], "iso639_3": "too", + "localname": "Tachihuin", + "localnames": [ "Tachihuin" ], "name": "Totonac, Xicotepec de Juárez", - "names": [ "Apapantilla Totonac", "Northern Totonac", "Totonaca", "Totonaco", "Totonaco de Villa Juárez", "Totonaco del Norte", "Totonako", "Xicotepec De Juárez Totonac", "Xicotepec Totonac" ], + "names": [ "Apapantilla Totonac", "Northern Totonac", "Tachihuin", "Tachiwiin", "Totonaca", "Totonaco", "Totonaco Central del Norte", "Totonaco de Villa Juárez", "Totonaco del Norte", "Totonako", "Xicotepec De Juárez Totonac", "Xicotepec Totonac" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -112456,9 +117401,9 @@ "iana": [ "Papantla Totonac" ], "iso639_3": "top", "localname": "Tutunakú", - "localnames": [ "Tutunakú" ], + "localnames": [ "Lichiwin Tutunaku", "Tutunakú" ], "name": "Totonac, Papantla", - "names": [ "Lowland Totonaca", "Papantla Totonac", "Totonaco", "Totonaco de Papantla", "Tutunakú" ], + "names": [ "Lichiwin Tutunaku", "Lowland Totonaca", "Papantla Totonac", "Totonaco", "Totonaco de Papantla", "Totonaco de la Costa", "Tutunakú" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -112504,9 +117449,9 @@ "iana": [ "Highland Totonac" ], "iso639_3": "tos", "localname": "Tutunakú", - "localnames": [ "Tutunakú" ], + "localnames": [ "Totonaco", "Tutunakú" ], "name": "Totonac, Highland", - "names": [ "Highland Totonac", "Sierra Totonac", "Totonaco", "Totonaco de la Sierra", "Tutunakú" ], + "names": [ "Highland Totonac", "Sierra Totonac", "Totonaco", "Totonaco Central Alto", "Totonaco Central del Sur", "Totonaco de la Sierra", "Tutunakú" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -112531,18 +117476,18 @@ "windows": "tou-Latn" }, { - "full": "tov-Zyyy-IR", + "full": "tov-Arab-IR", "iana": [ "Upper Taromi" ], "iso639_3": "tov", "name": "Taromi, Upper", "names": [ "Upper Taromi" ], "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "tov", - "tags": [ "tov-IR", "tov-Zyyy" ], - "windows": "tov-Zyyy" + "tags": [ "tov-Arab", "tov-IR" ], + "windows": "tov-Arab" }, { "full": "tow-Latn-US", @@ -112562,8 +117507,9 @@ "full": "tox-Latn-PW", "iana": [ "Tobian" ], "iso639_3": "tox", + "localnames": [ "Hatohobei", "Ramari Hatohobei" ], "name": "Tobian", - "names": [ "Hatohobei", "Tobi" ], + "names": [ "Hatohobei", "Ramari Hatohobei", "Tobi" ], "region": "PW", "regionname": "Palau", "script": "Latn", @@ -112605,6 +117551,7 @@ "full": "tpa-Latn-PG", "iana": [ "Taupota" ], "iso639_3": "tpa", + "localnames": [ "Taupota" ], "name": "Taupota", "region": "PG", "regionname": "Papua New Guinea", @@ -112630,31 +117577,32 @@ "windows": "tpc-Latn" }, { - "full": "tpe-Beng-BD", + "full": "tpe-Latn-BD", "iana": [ "Tippera" ], "iso639_3": "tpe", "name": "Tippera", - "names": [ "Kok Borok", "Tipperah", "Tippurah", "Tipra", "Tipura", "Triperah", "Tripura" ], - "nophonvars": true, + "names": [ "Hill Tippera", "Kok Borok", "Tipperah", "Tippurah", "Tipra", "Tipura", "Triperah", "Tripura" ], "region": "BD", "regionname": "Bangladesh", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "tpe-Beng", - "windows": "tpe-Beng" + "tag": "tpe", + "tags": [ "tpe-BD", "tpe-Latn" ], + "windows": "tpe-Latn" }, { - "full": "tpe-Latn-BD", + "full": "tpe-Beng-BD", "iana": [ "Tippera" ], "iso639_3": "tpe", "name": "Tippera", - "names": [ "Kok Borok", "Tipperah", "Tippurah", "Tipra", "Tipura", "Triperah", "Tripura" ], + "names": [ "Hill Tippera", "Kok Borok", "Tipperah", "Tippurah", "Tipra", "Tipura", "Triperah", "Tripura" ], + "nophonvars": true, "region": "BD", "regionname": "Bangladesh", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "tpe-Latn", - "windows": "tpe-Latn" + "tag": "tpe-Beng", + "windows": "tpe-Beng" }, { "full": "tpf-Latn-ID", @@ -112689,13 +117637,14 @@ "full": "tpi-Latn-PG", "iana": [ "Tok Pisin" ], "iso639_3": "tpi", + "localname": "Tok Pisin", "localnames": [ "Tok Pisin" ], "name": "Tok Pisin", "names": [ "Melanesian English", "Neomelanesian", "New Guinea Pidgin English", "Pidgin", "Pisin" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", - "sldr": false, + "sldr": true, "suppress": true, "tag": "tpi", "tags": [ "tpi-Latn", "tpi-PG" ], @@ -112706,7 +117655,7 @@ "iana": [ "Tapieté" ], "iso639_3": "tpj", "name": "Ñandeva", - "names": [ "Guarayo", "Guarayú", "Guasurango", "Guasurangue", "Nandeva", "Tapiete", "Tapieté", "Tirumbae", "Yanaigua", "Ñanagua" ], + "names": [ "Guasurango", "Guasurangue", "Nandeva", "Tapiete", "Tapieté", "Tirumbae", "Yanaigua", "Ñanagua" ], "region": "PY", "regionname": "Paraguay", "regions": [ "AR", "BO" ], @@ -112751,6 +117700,8 @@ "full": "tpm-Latn-GH", "iana": [ "Tampulma" ], "iso639_3": "tpm", + "localname": "Tampulma", + "localnames": [ "Tampulma" ], "name": "Tampulma", "names": [ "Tampele", "Tamplima", "Tampole", "Tampolem", "Tampolense", "Tamprusi" ], "region": "GH", @@ -112765,18 +117716,32 @@ "full": "tpn-Latn-BR", "iana": [ "Tupinambá" ], "iso639_3": "tpn", + "localnames": [ "Abáñeenga", "ñeendyba" ], "name": "Tupinambá", + "names": [ "Abáñeenga", "Classical Tupí", "Língua Brasílica", "Língua Geral", "Old Tupí", "Tupi Antigo", "Tupí", "ñeendyba" ], "region": "BR", "regionname": "Brazil", "script": "Latn", "sldr": false, "tag": "tpn", "tags": [ "tpn-BR", "tpn-Latn" ], - "unwritten": true, - "windows": "tpn-Latn" + "windows": "tpn-Latn-BR" + }, + { + "full": "tpn-Latn-XX", + "iana": [ "Tupinambá" ], + "iso639_3": "tpw", + "localnames": [ "Abáñeenga", "ñeendyba" ], + "name": "Tupinambá", + "region": "XX", + "script": "Latn", + "sldr": false, + "tag": "tpn-Latn-XX", + "tags": [ "tpw", "tpw-Latn", "tpw-Latn-XX", "tpw-XX" ], + "windows": "tpn-Latn-XX" }, { - "full": "tpo-Zyyy-LA", + "full": "tpo-Zzzz-LA", "iana": [ "Tai Pao" ], "iso639_3": "tpo", "name": "Tai Pao", @@ -112784,20 +117749,36 @@ "region": "LA", "regionname": "Lao People's Democratic Republic", "regions": [ "VN" ], - "script": "Zyyy", + "script": "Zzzz", "sldr": false, - "tag": "tpo", - "tags": [ "thc", "thc-LA", "thc-Zyyy", "thc-Zyyy-LA", "tpo-LA", "tpo-Zyyy" ], - "unwritten": true, - "windows": "tpo-Zyyy" + "tag": "tpo-Zzzz-LA", + "tags": [ "thc-Zzzz-LA-x-laipao", "thc-Zzzz-x-laipao" ], + "windows": "tpo-Zzzz-LA" + }, + { + "full": "tpo-Zzzz-LA-x-laipao", + "iana": [ "Tai Pao" ], + "iso639_3": "tpo", + "name": "Tai Pao", + "names": [ "Hàng Tong", "Tai Hang Tong", "Tai Paw", "Thai Muong", "Tày Muòng" ], + "nophonvars": true, + "region": "LA", + "regionname": "Lao People's Democratic Republic", + "regions": [ "VN" ], + "script": "Zzzz", + "sldr": false, + "tag": "tpo-Zzzz-x-laipao", + "tags": [ "thc-LA-x-laipao", "tpo-LA-x-laipao" ], + "windows": "tpo-Zzzz-x-laipao" }, { "full": "tpp-Latn-MX", "iana": [ "Pisaflores Tepehua" ], "iso639_3": "tpp", "localname": "Tepehua, Pisaflores", + "localnames": [ "Llhichiwin" ], "name": "Tepehua, Pisaflores", - "names": [ "Pisaflores Tepehua" ], + "names": [ "Llhichiwin", "North Tepehua", "Pisaflores Tepehua" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -112810,8 +117791,8 @@ "full": "tpq-Zyyy-IN", "iana": [ "Tukpa" ], "iso639_3": "tpq", - "name": "Tukpa", - "names": [ "Nesang", "Nyam-Kad", "Nyam-kat", "Nyamkad" ], + "name": "Kinnauri, Chhoyul", + "names": [ "Nesang", "Nyam-Kad", "Nyam-kat", "Nyamkad", "Tukpa" ], "region": "IN", "regionname": "India", "script": "Zyyy", @@ -112839,8 +117820,9 @@ "full": "tpt-Latn-MX", "iana": [ "Tlachichilco Tepehua" ], "iso639_3": "tpt", + "localnames": [ "Lhimasipijni" ], "name": "Tepehua, Tlachichilco", - "names": [ "Tlachichilco Tepehua" ], + "names": [ "Lhiimaasipijni", "Lhimasipijni", "Liimaasipijni", "Tlachichilco Tepehua", "West Tepehua" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -112853,12 +117835,14 @@ "full": "tpu-Khmr-KH", "iana": [ "Tampuan" ], "iso639_3": "tpu", + "latnnames": [ "Tampuon" ], + "localnames": [ "តំពួន" ], "name": "Tampuan", "names": [ "Campuon", "Proon", "Proons", "Tamphuan", "Tampuen", "Tampuon" ], "region": "KH", "regionname": "Cambodia", "script": "Khmr", - "sldr": false, + "sldr": true, "tag": "tpu", "tags": [ "tpu-KH", "tpu-Khmr" ], "windows": "tpu-Khmr" @@ -112878,18 +117862,6 @@ "unwritten": true, "windows": "tpv-Latn" }, - { - "full": "tpw-Zyyy-XX", - "iana": [ "Tupí" ], - "iso639_3": "tpw", - "name": "Tupí", - "region": "XX", - "script": "Zyyy", - "sldr": false, - "tag": "tpw", - "tags": [ "tpw-XX", "tpw-Zyyy" ], - "windows": "tpw-Zyyy" - }, { "full": "tpx-Latn-MX", "iana": [ "Acatepec Me'phaa", "Acatepec Tlapanec" ], @@ -112900,7 +117872,7 @@ "region": "MX", "regionname": "Mexico", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tpx", "tags": [ "tpx-Latn", "tpx-MX" ], "windows": "tpx-Latn" @@ -113052,7 +118024,7 @@ "iso639_3": "tqt", "localnames": [ "Xinulajgsípij Totonaco" ], "name": "Totonaco del cerro Xinolatépetl", - "names": [ "Ozumatlán Totonac", "Totonaco Norte de Huauchinango", "Totonaco de Ozomatlán", "Western Totonac", "Xinolatépetl Totonac", "Xinulajgsípij Totonaco" ], + "names": [ "Kintachiuinkan", "Ozumatlán Totonac", "Totonaco Norte de Huauchinango", "Totonaco de Ozomatlán", "Western Totonac", "Xinolatépetl Totonac", "Xinulajgsípij Totonaco" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -113077,18 +118049,18 @@ "windows": "tqu-Latn" }, { - "full": "tqw-Zyyy-US", + "full": "tqw-Latn-US", "iana": [ "Tonkawa" ], "iso639_3": "tqw", "name": "Tonkawa", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tqw", - "tags": [ "tqw-US", "tqw-Zyyy" ], + "tags": [ "tqw-Latn", "tqw-US" ], "unwritten": true, - "windows": "tqw-Zyyy" + "windows": "tqw-Latn" }, { "full": "tr-Latn-TR", @@ -113106,7 +118078,7 @@ "suppress": true, "tag": "tr", "tags": [ "tr-Latn", "tr-TR" ], - "windows": "tr" + "windows": "tr-TR" }, { "full": "tr-Arab-TR", @@ -113122,7 +118094,7 @@ "script": "Arab", "sldr": false, "tag": "tr-Arab", - "windows": "tr-Arab" + "windows": "tr-Arab-TR" }, { "full": "tr-Brai-TR", @@ -113137,7 +118109,7 @@ "script": "Brai", "sldr": false, "tag": "tr-Brai", - "windows": "tr-Brai" + "windows": "tr-Brai-TR" }, { "full": "tr-Latn-CY", @@ -113150,9 +118122,8 @@ "regionname": "Cyprus", "script": "Latn", "sldr": true, - "suppress": true, "tag": "tr-CY", - "windows": "tr-CY" + "windows": "tr-Latn-CY" }, { "full": "tr-Cyrl-BG", @@ -113167,7 +118138,7 @@ "sldr": false, "tag": "tr-Cyrl", "tags": [ "tr-BG" ], - "windows": "tr-Cyrl" + "windows": "tr-Cyrl-BG" }, { "full": "tr-Grek-TR", @@ -113183,7 +118154,7 @@ "script": "Grek", "sldr": false, "tag": "tr-Grek", - "windows": "tr-Grek" + "windows": "tr-Grek-TR" }, { "full": "tra-Arab-AF", @@ -113218,10 +118189,10 @@ "full": "trc-Latn-MX", "iana": [ "Copala Triqui" ], "iso639_3": "trc", - "localname": "xnaꞌánj nu̱ꞌ", - "localnames": [ "xnaꞌánj nu̱ꞌ" ], + "localname": "Xnaꞌánj nu̱ꞌ", + "localnames": [ "Xnaꞌánj nu̱ꞌ" ], "name": "Triqui, Copala", - "names": [ "Copala Trique", "Copala Triqui", "Copala-Trique", "Trike", "Trique", "Triqui Bajo", "Triqui de San Juan Copala", "xnaꞌánj nu̱ꞌ" ], + "names": [ "Copala Trique", "Copala Triqui", "Copala-Trique", "Trike", "Trique", "Triqui Bajo", "Triqui de San Juan Copala", "Xnaꞌánj nu̱ꞌ" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -113263,7 +118234,8 @@ "full": "trf-Latn-TT", "iana": [ "Trinidadian Creole English" ], "iso639_3": "trf", - "name": "Trinidadian Creole English", + "name": "Trinidadian English Creole", + "names": [ "Trinidadian Creole English" ], "region": "TT", "regionname": "Trinidad and Tobago", "script": "Latn", @@ -113336,7 +118308,7 @@ "windows": "trj-Latn" }, { - "full": "trl-Zyyy-GB", + "full": "trl-Latn-GB", "iana": [ "Traveller Scottish" ], "iso639_3": "trl", "name": "Traveller Scottish", @@ -113344,12 +118316,12 @@ "region": "GB", "regionname": "United Kingdom", "regions": [ "US" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "trl", - "tags": [ "trl-GB", "trl-Zyyy" ], + "tags": [ "trl-GB", "trl-Latn" ], "unwritten": true, - "windows": "trl-Zyyy" + "windows": "trl-Latn" }, { "full": "trm-Arab-AF", @@ -113371,7 +118343,7 @@ "iana": [ "Trinitario" ], "iso639_3": "trn", "name": "Trinitario", - "names": [ "Mojeño", "Mojeño Trinitario", "Mojos", "Moxo", "Moxos" ], + "names": [ "Mojeño", "Mojeño Trinitario", "Mojo", "Mojos", "Moxeño", "Moxo", "Moxos" ], "region": "BO", "regionname": "Bolivia", "script": "Latn", @@ -113395,45 +118367,46 @@ "windows": "tro-Latn" }, { - "full": "trp-Beng-IN", + "full": "trp-Latn-IN", "iana": [ "Kok Borok" ], "iso639_3": "trp", - "latnnames": [ "Kok borok" ], - "localnames": [ "ককবরক" ], + "localnames": [ "Kok Borok" ], "name": "Kok Borok", "names": [ "Debbarma", "Halam", "Hill Tippera", "Kakbarak", "Kohkborohk", "Kokbarak", "Kokborok", "Tipura", "Tripura", "Tripuri", "Usipi Mrung" ], - "nophonvars": true, "region": "IN", "regionname": "India", "regions": [ "BD" ], - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "trp-Beng", - "windows": "trp-Beng" + "tag": "trp", + "tags": [ "trp-IN", "trp-Latn" ], + "windows": "trp-Latn" }, { - "full": "trp-Latn-IN", + "full": "trp-Beng-IN", "iana": [ "Kok Borok" ], "iso639_3": "trp", - "localnames": [ "Kok Borok" ], + "latnnames": [ "Kok borok" ], + "localnames": [ "ককবরক" ], "name": "Kok Borok", "names": [ "Debbarma", "Halam", "Hill Tippera", "Kakbarak", "Kohkborohk", "Kokbarak", "Kokborok", "Tipura", "Tripura", "Tripuri", "Usipi Mrung" ], + "nophonvars": true, "region": "IN", "regionname": "India", "regions": [ "BD" ], - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "trp-Latn", - "windows": "trp-Latn" + "tag": "trp-Beng", + "windows": "trp-Beng" }, { "full": "trq-Latn-MX", "iana": [ "San Martín Itunyoso Triqui" ], "iso639_3": "trq", - "localname": "sná'ánj nì'", - "localnames": [ "sná'ánj nì'" ], + "localname": "snáꞌánj nìꞌ", + "localnames": [ "snáhánj nìh" ], "name": "Triqui, San Martín Itunyoso", - "names": [ "San Martín Itunyoso Trique", "San Martín Itunyoso Triqui", "Triqui Alto", "Triqui de San Martín Itunyoso", "sná’ánj nì’" ], + "names": [ "San Martín Itunyoso Trique", "San Martín Itunyoso Triqui", "Triqui Alto", "Triqui de San Martín Itunyoso", "snáhánj nìh" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -113492,7 +118465,7 @@ "iso639_3": "tru", "localnames": [ "Surayt" ], "name": "Turoyo", - "names": [ "Surayt", "Suryoyo", "Syryoyo", "Süryani", "Turani" ], + "names": [ "Surayt", "Suryoyo", "Syryoyo", "Süryani", "Turani", "Western Neo-Syriac", "Ṭūr ‘Abdinian Aramaic" ], "region": "TR", "regionname": "Turkey", "regions": [ "AR", "BE", "BR", "CA", "DE", "IQ", "LB", "NL", "SY", "US" ], @@ -113507,7 +118480,7 @@ "iana": [ "Turoyo", "Surayt" ], "iso639_3": "tru", "name": "Turoyo", - "names": [ "Surayt", "Suryoyo", "Syryoyo", "Süryani", "Turani" ], + "names": [ "Surayt", "Suryoyo", "Syryoyo", "Süryani", "Turani", "Western Neo-Syriac", "Ṭūr ‘Abdinian Aramaic" ], "nophonvars": true, "region": "TR", "regionname": "Turkey", @@ -113519,12 +118492,12 @@ }, { "full": "trv-Latn-TW", - "iana": [ "Taroko" ], + "iana": [ "Sediq", "Seediq", "Taroko" ], "iso639_3": "trv", "localname": "Seediq", "localnames": [ "Seediq" ], - "name": "Seediq", - "names": [ "Bu-Hwan", "Che-Hwan", "Daiya-Ataiyal", "Hogo", "Iboho", "Saediq", "Sazek", "Sedek", "Sedeq", "Sediakk", "Sedik", "Sediq", "Seedakka", "Seedek", "Seedeq", "Seedik", "Sejiq", "Shedekka", "Taroko", "Taruku", "Toda", "Toroko" ], + "name": "Sediq", + "names": [ "Bu-Hwan", "Che-Hwan", "Daiya-Ataiyal", "Hogo", "Iboho", "Saediq", "Sazek", "Sedek", "Sedeq", "Sediakk", "Sedik", "Seedakka", "Seedek", "Seedeq", "Seedik", "Seediq", "Sejiq", "Shedekka", "Taroko", "Taruku", "Toda", "Toroko" ], "region": "TW", "regionname": "China-Taiwan", "script": "Latn", @@ -113537,8 +118510,11 @@ "full": "trw-Arab-PK", "iana": [ "Torwali" ], "iso639_3": "trw", + "latnnames": [ "Torwali" ], + "localname": "توروالی", + "localnames": [ "توروالی" ], "name": "Torwali", - "names": [ "Torwalak", "Torwali Kohistani", "Turvali" ], + "names": [ "Kohistani", "Torwalak", "Torwalik", "Turvali" ], "region": "PK", "regionname": "Pakistan", "script": "Arab", @@ -113702,8 +118678,10 @@ "full": "tsg-Latn-PH", "iana": [ "Tausug" ], "iso639_3": "tsg", + "localname": "Bahasa Sūg", + "localnames": [ "Bahasa Sūg", "Sinūg" ], "name": "Tausug", - "names": [ "Joloano", "Joloano Sulu", "Jolohano", "Moro Joloano", "Pamung Sug", "Sinug", "Sinug Tausug", "Sooloo", "Sug", "Sulu", "Suluk", "Taosug", "Tausog", "Taw Sug" ], + "names": [ "Bahasa Sūg", "Joloano", "Joloano Sulu", "Jolohano", "Moro", "Moro Joloano", "Pamung Sug", "Sinug", "Sinug Tausug", "Sinūg", "Sooloo", "Sug", "Sulu", "Suluk", "Taosug", "Tausog", "Taw Sug" ], "region": "PH", "regionname": "Philippines", "regions": [ "ID", "MY" ], @@ -113718,15 +118696,16 @@ "iana": [ "Tausug" ], "iso639_3": "tsg", "latnnames": [ "Bahasa Sūg" ], + "localname": "بَهَسَ سُوگ", "localnames": [ "بَهَسَ سُوگ" ], "name": "Tausug", - "names": [ "Joloano", "Joloano Sulu", "Jolohano", "Moro Joloano", "Pamung Sug", "Sinug", "Sinug Tausug", "Sooloo", "Sug", "Sulu", "Suluk", "Taosug", "Tausog", "Taw Sug" ], + "names": [ "Bahasa Sūg", "Joloano", "Joloano Sulu", "Jolohano", "Moro", "Moro Joloano", "Pamung Sug", "Sinug", "Sinug Tausug", "Sinūg", "Sooloo", "Sug", "Sulu", "Suluk", "Taosug", "Tausog", "Taw Sug" ], "nophonvars": true, "region": "PH", "regionname": "Philippines", "regions": [ "ID", "MY" ], "script": "Arab", - "sldr": false, + "sldr": true, "tag": "tsg-Arab", "windows": "tsg-Arab" }, @@ -113870,7 +118849,7 @@ "iana": [ "Taiwan Sign Language" ], "iso639_3": "tss", "name": "Taiwan Sign Language", - "names": [ "Taiwan Ziran Shouyu" ], + "names": [ "Taiwan Shouyu", "Taiwan Ziran Shouyu" ], "region": "TW", "regionname": "China-Taiwan", "script": "Zxxx", @@ -113943,7 +118922,7 @@ "iana": [ "Mubami" ], "iso639_3": "tsx", "name": "Mubami", - "names": [ "Dausame", "Dausuami Mubami", "Ta", "Tao-Suamato", "Tao-Suame" ], + "names": [ "Mubami Ta", "Ta" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -113999,6 +118978,20 @@ "tags": [ "tt-Cyrl", "tt-RU" ], "windows": "tt-Cyrl" }, + { + "full": "tt-Arab-RU", + "iana": [ "Tatar" ], + "iso639_3": "tat", + "name": "Tatar", + "names": [ "Tartar" ], + "nophonvars": true, + "region": "RU", + "regionname": "Russian Federation", + "script": "Arab", + "sldr": false, + "tag": "tt-Arab", + "windows": "tt-Arab" + }, { "full": "tt-Latn-RU", "iana": [ "Tatar" ], @@ -114015,15 +119008,17 @@ "windows": "tt-Latn" }, { - "full": "tta-Zyyy-XX", + "full": "tta-Zyyy-CA", "iana": [ "Tutelo" ], "iso639_3": "tta", "name": "Tutelo", - "region": "XX", + "names": [ "Saponi", "Tutelo-Saponi" ], + "region": "CA", + "regionname": "Canada", "script": "Zyyy", "sldr": false, "tag": "tta", - "tags": [ "tta-XX", "tta-Zyyy" ], + "tags": [ "tta-CA", "tta-Zyyy" ], "windows": "tta-Zyyy" }, { @@ -114063,7 +119058,7 @@ "iana": [ "Tauade" ], "iso639_3": "ttd", "name": "Tauade", - "names": [ "Tauata" ], + "names": [ "Goilala", "Tauata" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -114121,6 +119116,8 @@ "full": "tth-Laoo-LA", "iana": [ "Upper Ta'oih" ], "iso639_3": "tth", + "latnnames": [ "Ta'oiq" ], + "localnames": [ "ຕາໂອ້ຍອ໌" ], "name": "Ta’oih, Upper", "names": [ "Kantua", "Katang", "T-Oy", "Ta Hoi", "Ta-Oi", "Ta-Oy", "Ta-oiq", "Tau Oi", "Ta’oiq", "Toi-Oi", "Tà-Oi", "Upper Ta'oih" ], "region": "LA", @@ -114293,7 +119290,7 @@ "sldr": false, "tag": "ttt", "tags": [ "ttt-AZ", "ttt-Latn" ], - "windows": "ttt-Latn" + "windows": "ttt-Latn-AZ" }, { "full": "ttt-Arab-IR", @@ -114308,7 +119305,7 @@ "sldr": false, "tag": "ttt-Arab", "tags": [ "ttt-IR" ], - "windows": "ttt-Arab" + "windows": "ttt-Arab-IR" }, { "full": "ttt-Cyrl-AZ", @@ -114323,7 +119320,7 @@ "script": "Cyrl", "sldr": false, "tag": "ttt-Cyrl", - "windows": "ttt-Cyrl" + "windows": "ttt-Cyrl-AZ" }, { "full": "ttu-Latn-PG", @@ -114384,19 +119381,19 @@ "windows": "tty-Latn" }, { - "full": "ttz-Zyyy-NP", + "full": "ttz-Deva-NP", "iana": [ "Tsum" ], "iso639_3": "ttz", "name": "Tsum", - "names": [ "Tsumba", "Tsumge" ], + "names": [ "Tsumba", "Tsumge", "Tsumke" ], "region": "NP", "regionname": "Nepal", - "script": "Zyyy", + "script": "Deva", "sldr": false, "tag": "ttz", - "tags": [ "ttz-NP", "ttz-Zyyy" ], + "tags": [ "ttz-Deva", "ttz-NP" ], "unwritten": true, - "windows": "ttz-Zyyy" + "windows": "ttz-Deva" }, { "full": "tua-Latn-PG", @@ -114448,6 +119445,7 @@ "full": "tuc-Latn-PG-x-oov", "iana": [ "Mutu - Saveeng-Oov" ], "iso639_3": "tuc", + "localname": "Saveeng", "localnames": [ "Saveeng" ], "name": "Mutu", "names": [ "Mutu - Saveeng-Oov", "Saveeng", "Sisi", "Tuam", "Tuam-Mutu", "Tuom" ], @@ -114455,7 +119453,7 @@ "regionname": "Papua New Guinea", "rod": "21173", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tuc-x-oov", "tags": [ "tuc-Latn-x-oov", "tuc-PG-x-oov" ], "windows": "tuc-Latn-x-oov" @@ -114464,6 +119462,7 @@ "full": "tuc-Latn-PG-x-tuam", "iana": [ "Mutu - Saveeng-Tuam" ], "iso639_3": "tuc", + "localname": "Saveeng", "localnames": [ "Saveeng" ], "name": "Mutu", "names": [ "Mutu - Saveeng-Tuam", "Saveeng", "Sisi", "Tuam", "Tuam-Mutu", "Tuom" ], @@ -114471,7 +119470,7 @@ "regionname": "Papua New Guinea", "rod": "21172", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tuc-x-tuam", "tags": [ "tuc-Latn-x-tuam", "tuc-PG-x-tuam" ], "windows": "tuc-Latn-x-tuam" @@ -114542,8 +119541,8 @@ "full": "tuh-Latn-PG", "iana": [ "Taulil" ], "iso639_3": "tuh", - "name": "Taulil", - "names": [ "Tulil" ], + "name": "Tulil", + "names": [ "Taulil" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -114572,7 +119571,9 @@ "full": "tuj-Latn-ID", "iana": [ "Tugutil" ], "iso639_3": "tuj", + "localnames": [ "O Totobeloho" ], "name": "Tugutil", + "names": [ "O Totobeloho", "Tobelo Boeng", "Togutil" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -114586,10 +119587,10 @@ "full": "tul-Latn-NG", "iana": [ "Tula" ], "iso639_3": "tul", - "localname": "Ki̱tule", - "localnames": [ "KiTulɛ" ], + "localname": "KiTulɛ", + "localnames": [ "yii Kitule" ], "name": "Tula", - "names": [ "KiTulɛ", "Kotule", "Kutule" ], + "names": [ "KiTule", "Kotule", "Kutule", "Ture", "yii Kitule" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -114615,19 +119616,20 @@ "windows": "tum-Latn" }, { - "full": "tun-Zyyy-US", + "full": "tun-Latn-US", "iana": [ "Tunica" ], "iso639_3": "tun", + "localnames": [ "Luhchi Yoroni" ], "name": "Tunica", "names": [ "Luhchi Yoroni", "Tonica", "Yuron" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tun", - "tags": [ "tun-US", "tun-Zyyy" ], + "tags": [ "tun-Latn", "tun-US" ], "unwritten": true, - "windows": "tun-Zyyy" + "windows": "tun-Latn" }, { "full": "tuo-Latn-BR", @@ -114650,6 +119652,8 @@ "full": "tuq-Latn-TD", "iana": [ "Tedaga" ], "iso639_3": "tuq", + "localname": "Tudaga", + "localnames": [ "Tudaga" ], "name": "Tedaga", "names": [ "Tebou", "Tebu", "Teda", "Teda-Daza", "Tibbu", "Toda", "Todaga", "Todga", "Toubou", "Tubu", "Tuda", "Tudaga" ], "region": "TD", @@ -114727,6 +119731,7 @@ "iana": [ "Tugen" ], "iso639_3": "tuy", "localnames": [ "Tukēn" ], + "macrolang": "kln", "name": "Tugen", "names": [ "Tukēn" ], "region": "KE", @@ -114741,8 +119746,9 @@ "full": "tuz-Latn-BF", "iana": [ "Turka" ], "iso639_3": "tuz", + "localnames": [ "Cuurammã" ], "name": "Turka", - "names": [ "Curama", "Cuuramã", "Tchourama", "Tourka", "Turuka", "Tyurama" ], + "names": [ "Curama", "Cuurammã", "Cuuramã", "Tchourama", "Tourka", "Turuka", "Tyurama" ], "region": "BF", "regionname": "Burkina Faso", "regions": [ "CI" ], @@ -114771,8 +119777,9 @@ "full": "tvd-Latn-NG", "iana": [ "Tsuvadi" ], "iso639_3": "tvd", + "localnames": [ "TsuVaɗi" ], "name": "Tsuvadi", - "names": [ "Abadi", "Avadi", "Evadi", "Ibeto", "Kamberi" ], + "names": [ "Abadi", "Avadi", "Evadi", "Ibeto", "Kamberi", "TsuVaɗi", "Vadi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -114796,6 +119803,20 @@ "unwritten": true, "windows": "tve-Latn" }, + { + "full": "tvi-Latn-NG", + "iso639_3": "tvi", + "name": "Tulai", + "names": [ "Tulawa", "Tule" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "tvi", + "tags": [ "tvi-Latn", "tvi-NG" ], + "unwritten": true, + "windows": "tvi-Latn" + }, { "full": "tvk-Latn-VU", "iana": [ "Southeast Ambrym" ], @@ -114845,19 +119866,19 @@ "windows": "tvm-Latn" }, { - "full": "tvn-Zyyy-MM", + "full": "tvn-Mymr-MM", "iana": [ "Tavoyan" ], "iso639_3": "tvn", "name": "Tavoyan", "names": [ "Dawai", "Dawe", "Dawei", "Tavoya", "Tawe-Tavoy" ], "region": "MM", "regionname": "Myanmar", - "script": "Zyyy", + "script": "Mymr", "sldr": false, "tag": "tvn", - "tags": [ "tvn-MM", "tvn-Zyyy" ], + "tags": [ "tvn-MM", "tvn-Mymr" ], "unwritten": true, - "windows": "tvn-Zyyy" + "windows": "tvn-Mymr" }, { "full": "tvo-Latn-ID", @@ -114962,26 +119983,26 @@ "windows": "tvy-Zyyy" }, { - "full": "twa-Zyyy-US", + "full": "twa-Latn-US", "iana": [ "Twana" ], "iso639_3": "twa", "name": "Twana", "names": [ "Skokomish", "Ti’tuwaduqut’sid", "Tuwa’duqx" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "twa", - "tags": [ "twa-US", "twa-Zyyy" ], + "tags": [ "twa-Latn", "twa-US" ], "unwritten": true, - "windows": "twa-Zyyy" + "windows": "twa-Latn" }, { "full": "twb-Latn-PH", "iana": [ "Western Tawbuid" ], "iso639_3": "twb", "name": "Tawbuid", - "names": [ "Batangan Taubuid", "Fanawbuid", "Western Taubuid", "Western Tawbuid" ], + "names": [ "Batangan Taubuid", "Fanawbuid", "Kanlurang bahagi Tawbuid", "Occidental Tawbuid", "Taubuid", "Western Taubuid", "Western Tawbuid" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -115064,8 +120085,9 @@ "full": "twh-Latn-VN", "iana": [ "Tai Dón" ], "iso639_3": "twh", + "localnames": [ "Tãy Ðón" ], "name": "Tai Dón", - "names": [ "Bai Dai", "Tai Blanc", "Tai Jinping", "Tai Kao", "Tai Lai", "Thái Tráng", "Táy Khao", "White Tai" ], + "names": [ "Bai Dai", "Tai Blanc", "Tai Jinping", "Tai Kao", "Tai Khao", "Tai Lai", "Thái Tráng", "Táy Khao", "Tãy Ðón", "White Tai" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "CN", "LA" ], @@ -115080,7 +120102,7 @@ "iana": [ "Tai Dón" ], "iso639_3": "twh", "name": "Tai Dón", - "names": [ "Bai Dai", "Tai Blanc", "Tai Jinping", "Tai Kao", "Tai Lai", "Thái Tráng", "Táy Khao", "White Tai" ], + "names": [ "Bai Dai", "Tai Blanc", "Tai Jinping", "Tai Kao", "Tai Khao", "Tai Lai", "Thái Tráng", "Táy Khao", "Tãy Ðón", "White Tai" ], "nophonvars": true, "region": "VN", "regionname": "Viet Nam", @@ -115153,8 +120175,9 @@ "full": "twp-Latn-PG", "iana": [ "Ere" ], "iso639_3": "twp", + "localnames": [ "Ede" ], "name": "Ere", - "names": [ "Londru", "Nane" ], + "names": [ "Ede", "Londru" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -115170,7 +120193,7 @@ "localname": "Tasawaq senni", "localnames": [ "Ingalkoyyu'" ], "name": "Tasawaq", - "names": [ "Ingalkoyyu’", "Ingelshi" ], + "names": [ "Ingalkoyyu’", "Ingelshi", "Tassawaq" ], "region": "NE", "regionname": "Niger", "script": "Latn", @@ -115242,7 +120265,7 @@ "iana": [ "Tewe" ], "iso639_3": "twx", "name": "Tewe", - "names": [ "Chiute", "Ciute", "Teve", "Utee", "Vateve", "Wateve" ], + "names": [ "Chitewe", "Chiute", "Ciute", "Teve", "Utee", "Vateve", "Wateve" ], "region": "MZ", "regionname": "Mozambique", "script": "Latn", @@ -115292,6 +120315,18 @@ "tags": [ "txb-XX", "txb-Zyyy" ], "windows": "txb-Zyyy" }, + { + "full": "txb-Mani-XX", + "iana": [ "Tokharian B" ], + "iso639_3": "txb", + "name": "Tokharian B", + "nophonvars": true, + "region": "XX", + "script": "Mani", + "sldr": false, + "tag": "txb-Mani", + "windows": "txb-Mani" + }, { "full": "txc-Zyyy-XX", "iana": [ "Tsetsaut" ], @@ -115324,26 +120359,25 @@ "iana": [ "Tangut" ], "iso639_3": "txg", "name": "Tangut", - "nophonvars": true, "region": "CN", "regionname": "China", "script": "Tang", "sldr": false, - "tag": "txg-Tang", - "tags": [ "txg-CN" ], + "tag": "txg", + "tags": [ "txg-CN", "txg-Tang" ], "windows": "txg-Tang" }, { - "full": "txh-Zyyy-XX", + "full": "txh-Grek-XX", "iana": [ "Thracian" ], "iso639_3": "txh", "name": "Thracian", "region": "XX", - "script": "Zyyy", + "script": "Grek", "sldr": false, "tag": "txh", - "tags": [ "txh-XX", "txh-Zyyy" ], - "windows": "txh-Zyyy" + "tags": [ "txh-Grek", "txh-XX" ], + "windows": "txh-Grek" }, { "full": "txi-Latn-BR", @@ -115406,43 +120440,43 @@ "windows": "txn-Latn" }, { - "full": "txo-Beng-IN", + "full": "txo-Toto-IN", "iana": [ "Toto" ], "iso639_3": "txo", "name": "Toto", - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Beng", + "script": "Toto", "sldr": false, - "tag": "txo-Beng", - "windows": "txo-Beng" + "tag": "txo", + "tags": [ "txo-IN", "txo-Toto" ], + "windows": "txo-Toto" }, { - "full": "txo-Deva-IN", + "full": "txo-Beng-IN", "iana": [ "Toto" ], "iso639_3": "txo", "name": "Toto", "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Beng", "sldr": false, - "tag": "txo-Deva", - "windows": "txo-Deva" + "tag": "txo-Beng", + "windows": "txo-Beng" }, { - "full": "txo-Toto-IN", + "full": "txo-Deva-IN", "iana": [ "Toto" ], "iso639_3": "txo", "name": "Toto", "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Toto", + "script": "Deva", "sldr": false, - "tag": "txo-Toto", - "windows": "txo-Toto" + "tag": "txo-Deva", + "windows": "txo-Deva" }, { "full": "txq-Latn-ID", @@ -115488,8 +120522,9 @@ "full": "txt-Latn-ID", "iana": [ "Citak" ], "iso639_3": "txt", + "localnames": [ "Kau Atakum" ], "name": "Citak", - "names": [ "Asmat Darat", "Cicak", "Kaunak", "Tjitak", "Tjitjak" ], + "names": [ "Asmat Darat", "Cicak", "Kau Adagum", "Kau Atakum", "Kaunak", "Tjitak", "Tjitjak" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -115502,8 +120537,9 @@ "full": "txu-Latn-BR", "iana": [ "Kayapó" ], "iso639_3": "txu", + "localnames": [ "Mẽbêngôkre kabẽn" ], "name": "Kayapó", - "names": [ "Cayapo", "Kokraimoro", "Mebêngokrê", "Put Karot" ], + "names": [ "Cayapo", "Kokraimoro", "Mebêngokrê", "Mẽbêngôkre kabẽn", "Put Karot" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -115531,6 +120567,7 @@ "full": "txy-Latn-MG", "iana": [ "Tanosy Malagasy" ], "iso639_3": "txy", + "macrolang": "mg", "name": "Malagasy, Tanosy", "names": [ "Tanosy Malagasy" ], "region": "MG", @@ -115561,8 +120598,9 @@ "full": "tya-Latn-PG", "iana": [ "Tauya" ], "iso639_3": "tya", + "localnames": [ "Foʔu Po" ], "name": "Tauya", - "names": [ "Inafosa" ], + "names": [ "Foʔu", "Foʔu Po", "Inafosa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -115577,7 +120615,7 @@ "iso639_3": "tye", "localnames": [ "Kyangganya" ], "name": "Kyanga", - "names": [ "Canga", "Cenka", "Changa", "Kenga", "Kyangawa", "Kyenga", "Kã", "Tienga", "Tyanga", "Tyenga" ], + "names": [ "Canga", "Cenka", "Changa", "Kenga", "Kyangawa", "Kyanggana", "Kyangganya", "Kyenga", "Kã", "Tienga", "Tyanga", "Tyenga" ], "region": "NG", "regionname": "Nigeria", "regions": [ "BJ" ], @@ -115622,7 +120660,7 @@ "iana": [ "Tai Do", "Tai Yo" ], "iso639_3": "tyj", "name": "Tai Yo", - "names": [ "Tai Do", "Tai Maen", "Tai Man", "Tai Men", "Tai Mene", "Tai Mène", "Tai Mènè", "Tai-Maen", "Tay Muoi", "Tay Quy Chau", "Tay Yo", "Tay-Jo", "Thai Muong" ], + "names": [ "Jò", "Tai Do", "Tai Maen", "Tai Man", "Tai Men", "Tai Mene", "Tai Muong", "Tai Mène", "Tai Mènè", "Tai-Maen", "Tay Quy Chau", "Tay Yo", "Tay-Jo", "Thai Muong", "Thái Do", "Tày Dọ’" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "LA" ], @@ -115637,7 +120675,7 @@ "iana": [ "Tai Yo" ], "iso639_3": "tyj", "name": "Tai Yo", - "names": [ "Tai Do", "Tai Maen", "Tai Man", "Tai Men", "Tai Mene", "Tai Mène", "Tai Mènè", "Tai-Maen", "Tay Muoi", "Tay Quy Chau", "Tay Yo", "Tay-Jo", "Thai Muong" ], + "names": [ "Jò", "Tai Do", "Tai Maen", "Tai Man", "Tai Men", "Tai Mene", "Tai Muong", "Tai Mène", "Tai Mènè", "Tai-Maen", "Tay Quy Chau", "Tay Yo", "Tay-Jo", "Thai Muong", "Thái Do", "Tày Dọ’" ], "nophonvars": true, "obsolete": true, "region": "VN", @@ -115697,7 +120735,7 @@ "iana": [ "Tai Daeng" ], "iso639_3": "tyr", "name": "Tai Daeng", - "names": [ "Daeng", "Man Thanh", "Môc-Châu", "Red Tai", "Red Thai", "Tai Deng", "Tai Muoi", "Tai Nhai", "Tai Rouge", "Thai Dang", "Thai Do", "Táy-Môc-Châu" ], + "names": [ "Daeng", "Deng", "Laeng", "Leng", "Lèng", "Man Thanh", "Red Tai", "Red Thai", "Tai Deng", "Tai Laeng", "Tai Nhai", "Tai Rouge", "Thai Dang", "Thai Do", "Thái Đỏ", "Đanh" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "LA", "TH", "US" ], @@ -115732,7 +120770,8 @@ "regionname": "Viet Nam", "script": "Latn", "sldr": false, - "tag": "tyt-Latn", + "tag": "tyt", + "tags": [ "tyt-Latn", "tyt-VN" ], "windows": "tyt-Latn" }, { @@ -115786,7 +120825,7 @@ "iana": [ "Teke-Tyee" ], "iso639_3": "tyx", "name": "Teke-Tyee", - "names": [ "Kwe", "Tee", "Tyee", "West Teke" ], + "names": [ "Etyee", "Etyeghe", "Kwe", "Tee", "Teke du sud-ouest", "Tyee", "West Teke", "Ɛtyɛɛ" ], "region": "CG", "regionname": "Congo", "script": "Latn", @@ -115795,6 +120834,21 @@ "tags": [ "tyx-CG", "tyx-Latn" ], "windows": "tyx-Latn" }, + { + "full": "tyy-Latn-NG", + "iana": [ "Tiyaa" ], + "iso639_3": "tyy", + "localnames": [ "Tiyaa" ], + "name": "Tiyaa", + "names": [ "Ayaa", "Boi", "Boyawa", "Kaye", "Tiya", "Ya" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "tyy", + "tags": [ "tyy-Latn", "tyy-NG" ], + "windows": "tyy-Latn" + }, { "full": "tyz-Latn-VN", "iana": [ "Tày" ], @@ -115828,9 +120882,9 @@ "full": "tzh-Latn-MX", "iana": [ "Tzeltal" ], "iso639_3": "tzh", - "localnames": [ "Bats'il k'op" ], + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], "name": "Tzeltal", - "names": [ "Bats’il k’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -115839,11 +120893,107 @@ "tags": [ "tzh-Latn", "tzh-MX" ], "windows": "tzh-Latn" }, + { + "full": "tzh-Latn-MX-x-amate", + "iana": [ "Tzeltal" ], + "iso639_3": "tzh", + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], + "name": "Tzeltal", + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "region": "MX", + "regionname": "Mexico", + "rod": "2775", + "script": "Latn", + "sldr": false, + "tag": "tzh-x-amate", + "tags": [ "tzh-Latn-x-amate", "tzh-MX-x-amate" ], + "windows": "tzh-Latn-x-amate" + }, + { + "full": "tzh-Latn-MX-x-bachajon", + "iana": [ "Tzeltal" ], + "iso639_3": "tzh", + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], + "name": "Tzeltal", + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "region": "MX", + "regionname": "Mexico", + "rod": "859", + "script": "Latn", + "sldr": false, + "tag": "tzh-x-bachajon", + "tags": [ "tzh-Latn-x-bachajon", "tzh-MX-x-bachajon" ], + "windows": "tzh-Latn-x-bachajon" + }, + { + "full": "tzh-Latn-MX-x-delplano", + "iana": [ "Tzeltal" ], + "iso639_3": "tzh", + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], + "name": "Tzeltal", + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "region": "MX", + "regionname": "Mexico", + "rod": "862", + "script": "Latn", + "sldr": false, + "tag": "tzh-x-delplano", + "tags": [ "tzh-Latn-x-delplano", "tzh-MX-x-delplano" ], + "windows": "tzh-Latn-x-delplano" + }, + { + "full": "tzh-Latn-MX-x-sibal", + "iana": [ "Tzeltal" ], + "iso639_3": "tzh", + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], + "name": "Tzeltal", + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "region": "MX", + "regionname": "Mexico", + "rod": "1796", + "script": "Latn", + "sldr": false, + "tag": "tzh-x-sibal", + "tags": [ "tzh-Latn-x-sibal", "tzh-MX-x-sibal" ], + "windows": "tzh-Latn-x-sibal" + }, + { + "full": "tzh-Latn-MX-x-tenango", + "iana": [ "Tzeltal" ], + "iso639_3": "tzh", + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], + "name": "Tzeltal", + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "region": "MX", + "regionname": "Mexico", + "rod": "2454", + "script": "Latn", + "sldr": false, + "tag": "tzh-x-tenango", + "tags": [ "tzh-Latn-x-tenango", "tzh-MX-x-tenango" ], + "windows": "tzh-Latn-x-tenango" + }, + { + "full": "tzh-Latn-MX-x-tenejapa", + "iana": [ "Tzeltal" ], + "iso639_3": "tzh", + "localnames": [ "Bats'il K'op Tseltal", "Bats'il k'op" ], + "name": "Tzeltal", + "names": [ "Bats’il K’op Tseltal", "Bats’il k’op", "Batz’il K’op", "Cancuc", "Chanal", "Highland Tzeltal", "Oxchuc Tzeltal", "Tenango", "Tenejapa", "Tenejapa Tzeltal", "Tseltal" ], + "region": "MX", + "regionname": "Mexico", + "rod": "2455", + "script": "Latn", + "sldr": false, + "tag": "tzh-x-tenejapa", + "tags": [ "tzh-Latn-x-tenejapa", "tzh-MX-x-tenejapa" ], + "windows": "tzh-Latn-x-tenejapa" + }, { "full": "tzj-Latn-GT", "iana": [ "Tz'utujil" ], "iso639_3": "tzj", - "localname": "Tz’utujil", + "localname": "Tz'utujil", "localnames": [ "Tz'utujil" ], "name": "Tz’utujil", "names": [ "Eastern Tzutujil", "Santiago Atitlán Tzutujil", "Tz'utujil", "Tzutuhil", "Tzutujil", "Tzutujil Oriental" ], @@ -115860,6 +121010,7 @@ "full": "tzj-Latn-GT-x-western", "iana": [ "Tz'utujil - Western" ], "iso639_3": "tzj", + "localname": "Tz'utujil", "localnames": [ "Tz'utujil" ], "name": "Tz’utujil", "names": [ "Eastern Tzutujil", "Santiago Atitlán Tzutujil", "Tz'utujil - Western", "Tzutuhil", "Tzutujil", "Tzutujil Oriental" ], @@ -115867,23 +121018,23 @@ "regionname": "Guatemala", "rod": "00374", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tzj-x-western", "tags": [ "tzj-GT-x-western", "tzj-Latn-x-western" ], "windows": "tzj-Latn-x-western" }, { - "full": "tzl-Zyyy-001", + "full": "tzl-Latn-001", "iana": [ "Talossan" ], "iso639_3": "tzl", "name": "Talossan", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "tzl", - "tags": [ "tzl-001", "tzl-Zyyy" ], - "windows": "tzl-Zyyy" + "tags": [ "tzl-001", "tzl-Latn" ], + "windows": "tzl-Latn" }, { "full": "tzm-Latn-MA", @@ -115956,7 +121107,7 @@ "localname": "Bats'i k'op", "localnames": [ "Bats'i k'op" ], "name": "Tzotzil", - "names": [ "Bats’i k’op", "San Bartolomé Venustiano Carranza Tzotzil", "Tsotsil" ], + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -115965,18 +121116,51 @@ "tags": [ "tzo-Latn", "tzo-MX" ], "windows": "tzo-Latn" }, + { + "full": "tzo-Latn-MX-x-chamula", + "iana": [ "Tzotzil" ], + "iso639_3": "tzo", + "localnames": [ "Bats'i k'op" ], + "name": "Tzotzil", + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil" ], + "region": "MX", + "regionname": "Mexico", + "rod": "2641", + "script": "Latn", + "sldr": false, + "tag": "tzo-x-chamula", + "tags": [ "tzo-Latn-x-chamula", "tzo-MX-x-chamula" ], + "windows": "tzo-Latn-x-chamula" + }, + { + "full": "tzo-Latn-MX-x-chenalho", + "iana": [ "Tzotzil" ], + "iso639_3": "tzo", + "localnames": [ "Bats'i k'op" ], + "name": "Tzotzil", + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil" ], + "region": "MX", + "regionname": "Mexico", + "rod": "2681", + "script": "Latn", + "sldr": false, + "tag": "tzo-x-chenalho", + "tags": [ "tzo-Latn-x-chenalho", "tzo-MX-x-chenalho" ], + "windows": "tzo-Latn-x-chenalho" + }, { "full": "tzo-Latn-MX-x-huixtan", "iana": [ "Tzotzil - Huixtan" ], "iso639_3": "tzo", + "localname": "Bats'i k'op", "localnames": [ "Bats'i k'op" ], "name": "Tzotzil", - "names": [ "Bats’i k’op", "San Bartolomé Venustiano Carranza Tzotzil", "Tsotsil", "Tzotzil - Huixtan" ], + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil", "Tzotzil - Huixtan" ], "region": "MX", "regionname": "Mexico", "rod": "01204", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tzo-x-huixtan", "tags": [ "tzo-Latn-x-huixtan", "tzo-MX-x-huixtan" ], "windows": "tzo-Latn-x-huixtan" @@ -115985,30 +121169,48 @@ "full": "tzo-Latn-MX-x-sanandre", "iana": [ "Tzotzil - San Andres" ], "iso639_3": "tzo", + "localname": "Bats'i k'op", "localnames": [ "Bats'i k'op" ], "name": "Tzotzil", - "names": [ "Bats’i k’op", "San Bartolomé Venustiano Carranza Tzotzil", "Tsotsil", "Tzotzil - San Andres" ], + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil", "Tzotzil - San Andres" ], "region": "MX", "regionname": "Mexico", "rod": "02443", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tzo-x-sanandre", "tags": [ "tzo-Latn-x-sanandre", "tzo-MX-x-sanandre" ], "windows": "tzo-Latn-x-sanandre" }, + { + "full": "tzo-Latn-MX-x-venus", + "iana": [ "Tzotzil" ], + "iso639_3": "tzo", + "localnames": [ "Bats'i k'op" ], + "name": "Tzotzil", + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil" ], + "region": "MX", + "regionname": "Mexico", + "rod": "2739", + "script": "Latn", + "sldr": false, + "tag": "tzo-x-venus", + "tags": [ "tzo-Latn-x-venus", "tzo-MX-x-venus" ], + "windows": "tzo-Latn-x-venus" + }, { "full": "tzo-Latn-MX-x-zinacntn", "iana": [ "Tzotzil - Zinacantan" ], "iso639_3": "tzo", + "localname": "Bats'i k'op", "localnames": [ "Bats'i k'op" ], "name": "Tzotzil", - "names": [ "Bats’i k’op", "San Bartolomé Venustiano Carranza Tzotzil", "Tsotsil", "Tzotzil - Zinacantan" ], + "names": [ "Bats’i c’op", "Bats’i k’op", "Tsotsil", "Tzotzil - Zinacantan" ], "region": "MX", "regionname": "Mexico", "rod": "00060", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "tzo-x-zinacntn", "tags": [ "tzo-Latn-x-zinacntn", "tzo-MX-x-zinacntn" ], "windows": "tzo-Latn-x-zinacntn" @@ -116017,8 +121219,8 @@ "full": "tzx-Latn-PG", "iana": [ "Tabriak" ], "iso639_3": "tzx", - "name": "Tabriak", - "names": [ "Karawari" ], + "name": "Karawari", + "names": [ "Ambonwari", "Tabriak" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -116077,6 +121279,7 @@ "iana": [ "Ubang" ], "iso639_3": "uba", "name": "Ubang", + "names": [ "Ùbâŋ" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -116090,8 +121293,9 @@ "full": "ubi-Latn-TD", "iana": [ "Ubi" ], "iso639_3": "ubi", + "localnames": [ "Ubu" ], "name": "Ubi", - "names": [ "Oubi" ], + "names": [ "Oubi", "Ubu" ], "region": "TD", "regionname": "Chad", "script": "Latn", @@ -116105,6 +121309,7 @@ "iana": [ "Buhi'non Bikol" ], "iso639_3": "ubl", "localnames": [ "Bikol-Boînën", "Boînën" ], + "macrolang": "bik", "name": "Bikol, Buhi’non", "names": [ "Bikol Buhi", "Bikol-Boînën", "Boie’nen", "Boînan", "Boînën", "Buhi", "Buhi'non Bikol", "Buhi-non", "Buhi’non" ], "region": "PH", @@ -116145,6 +121350,22 @@ "tags": [ "ubu-Latn", "ubu-PG" ], "windows": "ubu-Latn" }, + { + "full": "ubu-Latn-PG-x-andelale", + "iana": [ "Umbu-Ungu" ], + "iso639_3": "ubu", + "localnames": [ "Umbu-Ungu" ], + "name": "Umbu-Ungu", + "names": [ "Gawigl", "Gawil", "Hagen", "Imbo-Ungu", "Kakoli", "Kaugel", "Kauil", "Ubu Ugu", "Umbongu" ], + "region": "PG", + "regionname": "Papua New Guinea", + "rod": "17940", + "script": "Latn", + "sldr": false, + "tag": "ubu-x-andelale", + "tags": [ "ubu-Latn-x-andelale", "ubu-PG-x-andelale" ], + "windows": "ubu-Latn-x-andelale" + }, { "full": "ubu-Latn-PG-x-kala", "iana": [ "Umbu-Ungu" ], @@ -116178,19 +121399,34 @@ "windows": "ubu-Latn-x-nopenge" }, { - "full": "uby-Zyyy-TR", + "full": "uby-Latn-TR", "iana": [ "Ubykh" ], "iso639_3": "uby", "name": "Ubykh", "names": [ "Oubykh", "Pekhi", "Ubyx" ], "region": "TR", "regionname": "Turkey", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "uby", - "tags": [ "uby-TR", "uby-Zyyy" ], + "tags": [ "uby-Latn", "uby-TR" ], "unwritten": true, - "windows": "uby-Zyyy" + "windows": "uby-Latn" + }, + { + "full": "uby-Cyrl-TR", + "iana": [ "Ubykh" ], + "iso639_3": "uby", + "name": "Ubykh", + "names": [ "Oubykh", "Pekhi", "Ubyx" ], + "nophonvars": true, + "region": "TR", + "regionname": "Turkey", + "script": "Cyrl", + "sldr": false, + "tag": "uby-Cyrl", + "unwritten": true, + "windows": "uby-Cyrl" }, { "full": "uda-Latn-NG", @@ -116226,6 +121462,8 @@ "full": "udg-Mlym-IN", "iana": [ "Muduga" ], "iso639_3": "udg", + "latnnames": [ "Muduga" ], + "localnames": [ "മുഡുഗ" ], "name": "Muduga", "names": [ "Mudugar" ], "region": "IN", @@ -116237,19 +121475,52 @@ "windows": "udg-Mlym" }, { - "full": "udi-Cyrl-AZ", + "full": "udi-Aghb-RU", + "iana": [ "Udi" ], + "iso639_3": "udi", + "name": "Udi", + "names": [ "Udin", "Udin muz", "Uti" ], + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "AZ", "GE", "TM" ], + "script": "Aghb", + "sldr": false, + "tag": "udi", + "tags": [ "udi-Aghb", "udi-RU" ], + "windows": "udi-Aghb-RU" + }, + { + "full": "udi-Cyrl-RU", + "iana": [ "Udi" ], + "iso639_3": "udi", + "latnnames": [ "udin muz" ], + "localnames": [ "удин муз" ], + "name": "Udi", + "names": [ "Udin", "Udin muz", "Uti" ], + "nophonvars": true, + "region": "RU", + "regionname": "Russian Federation", + "regions": [ "AZ", "GE", "TM" ], + "script": "Cyrl", + "sldr": false, + "tag": "udi-Cyrl", + "windows": "udi-Cyrl-RU" + }, + { + "full": "udi-Latn-AZ", "iana": [ "Udi" ], "iso639_3": "udi", + "localnames": [ "Udin", "Udin muz" ], "name": "Udi", - "names": [ "Udin", "Uti", "udin muz" ], + "names": [ "Udin", "Udin muz", "Uti" ], "region": "AZ", "regionname": "Azerbaijan", "regions": [ "GE", "RU", "TM" ], - "script": "Cyrl", + "script": "Latn", "sldr": true, - "tag": "udi", - "tags": [ "udi-AZ", "udi-Cyrl" ], - "windows": "udi-Cyrl" + "tag": "udi-Latn", + "tags": [ "udi-AZ" ], + "windows": "udi-Latn-AZ" }, { "full": "udj-Latn-ID", @@ -116318,7 +121589,7 @@ "iso639_3": "udu", "localnames": [ "Twampa" ], "name": "Uduk", - "names": [ "Kebeirka", "Korara", "Kumus", "Kwanim Pa", "Othan", "Twampa" ], + "names": [ "Cai", "Kamus", "Kebeirka", "Korara", "Kumus", "Kwanim Pa", "Othan", "Tam", "Twampa" ], "region": "SD", "regionname": "Sudan", "regions": [ "SS" ], @@ -116372,7 +121643,7 @@ "sldr": true, "tag": "ug", "tags": [ "ug-Arab", "ug-CN" ], - "windows": "ug-Arab" + "windows": "ug-Arab-CN" }, { "full": "ug-Cyrl-KZ", @@ -116401,7 +121672,7 @@ "script": "Latn", "sldr": false, "tag": "ug-Latn", - "windows": "ug-Latn" + "windows": "ug-Latn-CN" }, { "full": "ug-Cyrl-MN", @@ -116459,12 +121730,28 @@ "unwritten": true, "windows": "uge-Latn" }, + { + "full": "ugh-Cyrl-RU", + "iana": [ "Kubachi" ], + "iso639_3": "ugh", + "latnnames": [ "Ghug’bugan" ], + "localnames": [ "Гӏугъбуган" ], + "name": "Kubachi", + "names": [ "Arbuk", "Kubachi-Ashty", "Kubachin", "Kubachinskiy", "Kubachintsy", "Kubaci", "Ughbug", "Zirexgeran" ], + "region": "RU", + "regionname": "Russian Federation", + "script": "Cyrl", + "sldr": false, + "tag": "ugh", + "tags": [ "ugh-Cyrl", "ugh-RU" ], + "windows": "ugh-Cyrl" + }, { "full": "ugn-Zxxx-UG", "iana": [ "Ugandan Sign Language" ], "iso639_3": "ugn", "name": "Ugandan Sign Language", - "names": [ "USL" ], + "names": [ "Lugha ya Alama ya Uganda", "Olulimi Iwobubonelo e Uganda", "UGANDA SIGN", "USL", "UgSL" ], "region": "UG", "regionname": "Uganda", "script": "Zxxx", @@ -116474,19 +121761,19 @@ "windows": "ugn-Zxxx" }, { - "full": "ugo-Zyyy-TH", + "full": "ugo-Thai-TH", "iana": [ "Ugong" ], "iso639_3": "ugo", "name": "Ugong", "names": [ "Gong", "Lawa", "Ugawng" ], "region": "TH", "regionname": "Thailand", - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "ugo", - "tags": [ "ugo-TH", "ugo-Zyyy" ], + "tags": [ "ugo-TH", "ugo-Thai" ], "unwritten": true, - "windows": "ugo-Zyyy" + "windows": "ugo-Thai" }, { "full": "ugy-Zxxx-UY", @@ -116507,7 +121794,7 @@ "iana": [ "Uhami" ], "iso639_3": "uha", "name": "Uhami", - "names": [ "Ishua" ], + "names": [ "Isua" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -116535,6 +121822,7 @@ "full": "uis-Latn-PG", "iana": [ "Uisai" ], "iso639_3": "uis", + "localnames": [ "Uisai" ], "name": "Uisai", "region": "PG", "regionname": "Papua New Guinea", @@ -116564,7 +121852,7 @@ "iana": [ "Tanjijili" ], "iso639_3": "uji", "name": "Tanjijili", - "names": [ "Jijili", "Koro", "Koro Funtu of Kafin Koro", "Koro Funtu of Minna", "Koro Funtu of Shakoyi", "Ujijili" ], + "names": [ "Jijili", "Koro", "Koro Funtu of Kafin Koro", "Koro Funtu of Minna", "Koro Funtu of Shakoyi", "Rjili", "Ujijili" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -116593,6 +121881,21 @@ "tags": [ "uk-Cyrl", "uk-UA" ], "windows": "uk" }, + { + "full": "uk-Latn-UA", + "iana": [ "Ukrainian" ], + "iso639_3": "ukr", + "name": "Ukrainian", + "names": [ "Ucrainean", "Ukraiński", "Ukrán" ], + "obsolete": true, + "region": "UA", + "regionname": "Ukraine", + "regions": [ "AM", "AR", "AZ", "BA", "BG", "BR", "BY", "CA", "CZ", "DE", "EE", "FI", "GE", "HR", "HU", "KG", "KZ", "LT", "LV", "MD", "PL", "PT", "RO", "RS", "RU", "SK", "TJ", "TM", "US", "UZ" ], + "script": "Latn", + "sldr": false, + "tag": "uk-Latn", + "windows": "uk-Latn" + }, { "full": "uka-Latn-ID", "iana": [ "Kaburi" ], @@ -116638,9 +121941,12 @@ }, { "full": "uki-Orya-IN", + "iana": [ "Kui (India)" ], "iso639_3": "uki", + "latnnames": [ "Kui", "Kutia" ], + "localnames": [ "କୁଇ", "କୁଟିଆ" ], "name": "Kui", - "names": [ "Kanda", "Kandh", "Khond", "Khondi", "Khondo", "Kodu", "Kodulu", "Kuinga", "Kuy" ], + "names": [ "Kanda", "Kandh", "Khond", "Khondi", "Khondo", "Kodu", "Kodulu", "Kui (India)", "Kuinga", "Kutia", "Kuy" ], "region": "IN", "regionname": "India", "script": "Orya", @@ -116670,7 +121976,7 @@ "iana": [ "Ukrainian Sign Language" ], "iso639_3": "ukl", "name": "Ukrainian Sign Language", - "names": [ "USL", "Ukrayinska Mova Zhestiv", "Ukrayinska Zhestova Mova", "Ukrayinskoyu Zhestivoyu Movoyu" ], + "names": [ "USL", "Ukrayinska Mova Zhestiv", "Ukrayinska Zhestova Mova" ], "region": "UA", "regionname": "Ukraine", "script": "Zxxx", @@ -116713,7 +122019,7 @@ "iana": [ "Urubú-Kaapor Sign Language", "Kaapor Sign Language" ], "iso639_3": "uks", "name": "Kaapor Sign Language", - "names": [ "Ka’apor Sign Language", "LSKB", "Língua de Sinais Kaapor Brasileira", "Urubú-Kaapor Sign Language" ], + "names": [ "Ka’apor Sign Language", "LSK", "LSKB", "Lingua de sinais ka’apor", "Lingua dos Sinais Kaapor Brasileira", "Língua de Sinais Kaapor Brasileira", "Urubú-Kaapor Sign Language" ], "region": "BR", "regionname": "Brazil", "script": "Zxxx", @@ -116737,6 +122043,21 @@ "unwritten": true, "windows": "uku-Latn" }, + { + "full": "ukv-Latn-SS", + "iana": [ "Kuku" ], + "iso639_3": "ukv", + "name": "Kuku", + "names": [ "Kukuu" ], + "region": "SS", + "regionname": "South Sudan", + "regions": [ "UG" ], + "script": "Latn", + "sldr": false, + "tag": "ukv", + "tags": [ "ukv-Latn", "ukv-SS" ], + "windows": "ukv-Latn" + }, { "full": "ukw-Latn-NG", "iana": [ "Ukwuani-Aboh-Ndoni" ], @@ -116748,7 +122069,6 @@ "sldr": false, "tag": "ukw", "tags": [ "ukw-Latn", "ukw-NG" ], - "unwritten": true, "windows": "ukw-Latn" }, { @@ -116771,7 +122091,7 @@ "iana": [ "Fungwa" ], "iso639_3": "ula", "name": "Fungwa", - "names": [ "Afungwa", "Tufungwa", "Ula", "Ura" ], + "names": [ "Afungwa", "Ayungo", "Chi-Fungwa", "Cifungwa", "Tufungwa", "Ula", "Ura", "Yungo" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -116786,6 +122106,7 @@ "iana": [ "Ulukwumi" ], "iso639_3": "ulb", "name": "Ulukwumi", + "names": [ "Olukumi", "Olukwumi", "Unukwumi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -116845,6 +122166,7 @@ "iana": [ "Ulithian" ], "iso639_3": "uli", "name": "Ulithian", + "names": [ "Yulidiy" ], "region": "FM", "regionname": "Federated States of Micronesia", "regions": [ "US" ], @@ -116888,7 +122210,7 @@ "iana": [ "Ulumanda'" ], "iso639_3": "ulm", "name": "Ulumanda’", - "names": [ "Awo-Sumakuyu", "Botteng-Tappalang", "Kado", "Oeloemanda", "Tubbi", "Ulumanda'", "Ulumandak", "Ulunda" ], + "names": [ "Awo-Sumakuyu", "Botteng-Tappalang", "Kado", "Oeloemanda", "Paʔnnei", "Tubbi", "Ulumanda'", "Ulumandak", "Ulunda" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -116943,6 +122265,20 @@ "unwritten": true, "windows": "ulw-Latn" }, + { + "full": "uly-Latn-NG", + "iso639_3": "uly", + "name": "Buli", + "names": [ "Bili", "Bulawa", "Bələ", "Bɨlɨ", "Ɓuli" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "uly", + "tags": [ "uly-Latn", "uly-NG" ], + "unwritten": true, + "windows": "uly-Latn" + }, { "full": "uma-Latn-US", "iana": [ "Umatilla" ], @@ -116974,16 +122310,16 @@ "windows": "umb-Latn" }, { - "full": "umc-Zyyy-XX", + "full": "umc-Ital-XX", "iana": [ "Marrucinian" ], "iso639_3": "umc", "name": "Marrucinian", "region": "XX", - "script": "Zyyy", + "script": "Ital", "sldr": false, "tag": "umc", - "tags": [ "umc-XX", "umc-Zyyy" ], - "windows": "umc-Zyyy" + "tags": [ "umc-Ital", "umc-XX" ], + "windows": "umc-Ital" }, { "full": "umd-Latn-AU", @@ -117001,18 +122337,19 @@ "windows": "umd-Latn" }, { - "full": "umg-Qaax-AU", + "full": "umg-Latn-AU", "iana": [ "Morrobalama", "Umbuygamu" ], "iso639_3": "umg", "name": "Morrobalama", "names": [ "Baka", "Banjigam", "Barungguan", "Barunguan", "Ganganda", "Jintjingga", "Kuuku-yani", "Moroba-Lama", "Morrabalama", "Morrobolam", "Njindingga", "Umbuygamu", "Umbuykamu", "Umpuykumu", "Yintjingga" ], "region": "AU", "regionname": "Australia", - "script": "Qaax", + "script": "Latn", "sldr": false, "tag": "umg", - "tags": [ "umg-AU", "umg-Qaax" ], - "windows": "umg-Qaax" + "tags": [ "umg-AU", "umg-Latn" ], + "unwritten": true, + "windows": "umg-Latn" }, { "full": "umi-Latn-MY", @@ -117024,7 +122361,7 @@ "script": "Latn", "sldr": false, "tag": "umi", - "tags": [ "umi-Latn", "umi-MY" ], + "tags": [ "szd", "szd-Latn", "szd-Latn-MY", "szd-MY", "umi-Latn", "umi-MY" ], "unwritten": true, "windows": "umi-Latn" }, @@ -117174,6 +122511,20 @@ "unwritten": true, "windows": "ung-Latn" }, + { + "full": "uni-Latn-PG", + "iana": [ "Uni" ], + "iso639_3": "uni", + "name": "Uni", + "names": [ "Ramo" ], + "region": "PG", + "regionname": "Papua New Guinea", + "script": "Latn", + "sldr": false, + "tag": "uni", + "tags": [ "uni-Latn", "uni-PG" ], + "windows": "uni-Latn" + }, { "full": "unk-Latn-BR", "iana": [ "Enawené-Nawé" ], @@ -117190,19 +122541,20 @@ "windows": "unk-Latn" }, { - "full": "unm-Zyyy-US", + "full": "unm-Latn-US", "iana": [ "Unami" ], "iso639_3": "unm", + "macrolang": "del", "name": "Unami", "names": [ "Delaware", "Lenape", "Lenni-Lenape" ], "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "unm", - "tags": [ "unm-US", "unm-Zyyy" ], + "tags": [ "unm-Latn", "unm-US" ], "unwritten": true, - "windows": "unm-Zyyy" + "windows": "unm-Latn" }, { "full": "unn-Latn-AU", @@ -117234,7 +122586,7 @@ "sldr": false, "tag": "unr", "tags": [ "unr-Beng", "unr-IN" ], - "windows": "unr-Beng" + "windows": "unr-Beng-IN" }, { "full": "unr-Latn-IN", @@ -117249,7 +122601,7 @@ "script": "Latn", "sldr": false, "tag": "unr-Latn", - "windows": "unr-Latn" + "windows": "unr-Latn-IN" }, { "full": "unr-Deva-NP", @@ -117268,6 +122620,21 @@ "tags": [ "unr-Deva" ], "windows": "unr-Deva-NP" }, + { + "full": "unr-Nagm-NP", + "iana": [ "Mundari" ], + "iso639_3": "unr", + "name": "Mundari", + "names": [ "Colh", "Horo", "Kolh", "Mandari", "Mondari", "Munari", "Munda", "Muṇḍari", "Santhali", "Satar" ], + "nophonvars": true, + "region": "NP", + "regionname": "Nepal", + "regions": [ "BD", "IN" ], + "script": "Nagm", + "sldr": false, + "tag": "unr-Nagm", + "windows": "unr-Nagm-NP" + }, { "full": "unr-Orya-IN", "iana": [ "Mundari" ], @@ -117283,7 +122650,7 @@ "script": "Orya", "sldr": false, "tag": "unr-Orya", - "windows": "unr-Orya" + "windows": "unr-Orya-IN" }, { "full": "unu-Latn-PG", @@ -117330,6 +122697,20 @@ "tag": "unx-Deva", "windows": "unx-Deva" }, + { + "full": "unx-Orya-IN", + "iana": [ "Munda" ], + "iso639_3": "unx", + "name": "Munda", + "names": [ "Heriki", "Killi", "hoɽo jagar", "muɳɖa jagar" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Orya", + "sldr": false, + "tag": "unx-Orya", + "windows": "unx-Orya" + }, { "full": "unz-Latn-ID", "iana": [ "Unde Kaili" ], @@ -117345,6 +122726,21 @@ "unwritten": true, "windows": "unz-Latn" }, + { + "full": "uon-Latn-TW", + "iana": [ "Kulon" ], + "iso639_3": "uon", + "name": "Kulon", + "names": [ "Kulun" ], + "region": "TW", + "regionname": "China-Taiwan", + "script": "Latn", + "sldr": false, + "tag": "uon", + "tags": [ "uon-Latn", "uon-TW" ], + "unwritten": true, + "windows": "uon-Latn" + }, { "full": "upi-Latn-PG", "iana": [ "Umeda" ], @@ -117375,6 +122771,70 @@ "tags": [ "upv-Latn", "upv-VU" ], "windows": "upv-Latn" }, + { + "full": "upv-Latn-VU-x-atchin", + "iana": [ "Uripiv-Wala-Rano-Atchin" ], + "iso639_3": "upv", + "localnames": [ "Tirax" ], + "name": "Uripiv-Wala-Rano-Atchin", + "names": [ "Northeast Malakula", "Tirax" ], + "region": "VU", + "regionname": "Vanuatu", + "rod": "4705", + "script": "Latn", + "sldr": false, + "tag": "upv-x-atchin", + "tags": [ "upv-Latn-x-atchin", "upv-VU-x-atchin" ], + "windows": "upv-Latn-x-atchin" + }, + { + "full": "upv-Latn-VU-x-eastern", + "iana": [ "Uripiv-Wala-Rano-Atchin" ], + "iso639_3": "upv", + "localnames": [ "Tirax" ], + "name": "Uripiv-Wala-Rano-Atchin", + "names": [ "Northeast Malakula", "Tirax" ], + "region": "VU", + "regionname": "Vanuatu", + "rod": "4714", + "script": "Latn", + "sldr": false, + "tag": "upv-x-eastern", + "tags": [ "upv-Latn-x-eastern", "upv-VU-x-eastern" ], + "windows": "upv-Latn-x-eastern" + }, + { + "full": "upv-Latn-VU-x-tautu", + "iana": [ "Uripiv-Wala-Rano-Atchin" ], + "iso639_3": "upv", + "localnames": [ "Tirax" ], + "name": "Uripiv-Wala-Rano-Atchin", + "names": [ "Northeast Malakula", "Tirax" ], + "region": "VU", + "regionname": "Vanuatu", + "rod": "4573", + "script": "Latn", + "sldr": false, + "tag": "upv-x-tautu", + "tags": [ "upv-Latn-x-tautu", "upv-VU-x-tautu" ], + "windows": "upv-Latn-x-tautu" + }, + { + "full": "upv-Latn-VU-x-wala", + "iana": [ "Uripiv-Wala-Rano-Atchin" ], + "iso639_3": "upv", + "localnames": [ "Tirax" ], + "name": "Uripiv-Wala-Rano-Atchin", + "names": [ "Northeast Malakula", "Tirax" ], + "region": "VU", + "regionname": "Vanuatu", + "rod": "17968", + "script": "Latn", + "sldr": false, + "tag": "upv-x-wala", + "tags": [ "upv-Latn-x-wala", "upv-VU-x-wala" ], + "windows": "upv-Latn-x-wala" + }, { "full": "ur-Arab-PK", "iana": [ "Urdu" ], @@ -117392,7 +122852,7 @@ "suppress": true, "tag": "ur", "tags": [ "ur-Arab", "ur-PK" ], - "windows": "ur" + "windows": "ur-PK" }, { "full": "ur-Brai-PK", @@ -117407,7 +122867,21 @@ "script": "Brai", "sldr": false, "tag": "ur-Brai", - "windows": "ur-Brai" + "windows": "ur-Brai-PK" + }, + { + "full": "ur-Deva-IN", + "iana": [ "Urdu" ], + "iso639_3": "urd", + "name": "Urdu", + "names": [ "Islami", "Undri", "Urudu" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Deva", + "sldr": false, + "tag": "ur-Deva", + "windows": "ur-Deva-IN" }, { "full": "ur-Arab-IN", @@ -117421,9 +122895,8 @@ "regionname": "India", "script": "Arab", "sldr": true, - "suppress": true, "tag": "ur-IN", - "windows": "ur-IN" + "windows": "ur-Arab-IN" }, { "full": "ur-Latn-PK", @@ -117437,7 +122910,7 @@ "script": "Latn", "sldr": false, "tag": "ur-Latn", - "windows": "ur-Latn" + "windows": "ur-Latn-PK" }, { "full": "ura-Latn-PE", @@ -117566,6 +123039,7 @@ "full": "urk-Thai-TH", "iana": [ "Urak Lawoi'" ], "iso639_3": "urk", + "macrolang": "ms", "name": "Urak Lawoi’", "names": [ "Chaw Talay", "Lawoi", "Lawta", "Lumoh Lawoi’", "Olang Lawta", "Orak Lawoi’", "Urak Lawoi'" ], "region": "TH", @@ -117596,6 +123070,7 @@ "iana": [ "Urapmin" ], "iso639_3": "urm", "name": "Urapmin", + "names": [ "Urabmin", "Urap" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -117668,6 +123143,8 @@ "full": "urt-Latn-PG", "iana": [ "Urat" ], "iso639_3": "urt", + "localname": "Urat", + "localnames": [ "Urat" ], "name": "Urat", "region": "PG", "regionname": "Papua New Guinea", @@ -117710,8 +123187,9 @@ "full": "urw-Latn-PG", "iana": [ "Sop" ], "iso639_3": "urw", - "name": "Sop", - "names": [ "Kari", "Usino" ], + "localnames": [ "Sob" ], + "name": "Sob", + "names": [ "Kari", "Sop", "Usino" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -117794,32 +123272,33 @@ "windows": "ush-Arab" }, { - "full": "usi-Beng-BD", + "full": "usi-Latn-BD", "iana": [ "Usui" ], "iso639_3": "usi", + "localnames": [ "Kaobrung" ], "name": "Usoi", "names": [ "Kaobrung", "Kau Brung", "Unshoi", "Unsuiy", "Ushoi", "Usui" ], - "nophonvars": true, "region": "BD", "regionname": "Bangladesh", - "script": "Beng", + "script": "Latn", "sldr": false, - "tag": "usi-Beng", - "windows": "usi-Beng" + "tag": "usi", + "tags": [ "usi-BD", "usi-Latn" ], + "windows": "usi-Latn" }, { - "full": "usi-Latn-BD", + "full": "usi-Beng-BD", "iana": [ "Usui" ], "iso639_3": "usi", - "localnames": [ "Kaobrung" ], "name": "Usoi", "names": [ "Kaobrung", "Kau Brung", "Unshoi", "Unsuiy", "Ushoi", "Usui" ], + "nophonvars": true, "region": "BD", "regionname": "Bangladesh", - "script": "Latn", + "script": "Beng", "sldr": false, - "tag": "usi-Latn", - "windows": "usi-Latn" + "tag": "usi-Beng", + "windows": "usi-Beng" }, { "full": "usk-Latn-CM", @@ -117858,7 +123337,7 @@ "iana": [ "us-Saare" ], "iso639_3": "uss", "name": "us-Saare", - "names": [ "Western Duka", "Western Dukawa", "es-Saare", "əs-Saare" ], + "names": [ "Western Duka", "Western Dukawa", "es-Saare", "sSaare", "əs-Saare" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -117887,8 +123366,8 @@ "full": "uta-Latn-NG", "iana": [ "Otank" ], "iso639_3": "uta", - "name": "Otank", - "names": [ "Itang", "Itank", "Otang", "Otanga", "Utanga", "Utange", "Utank" ], + "name": "Itang", + "names": [ "Itank", "Otang", "Otanga", "Otank", "Utanga", "Utange", "Utank" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -117917,9 +123396,9 @@ "iana": [ "ut-Hun" ], "iso639_3": "uth", "localname": "u̱t-Hun", - "localnames": [ "it-Hun" ], + "localnames": [ "u̱t-Hun" ], "name": "ut-Hun", - "names": [ "Eastern Duka", "Eastern Dukawa", "Hun-ne", "Hune", "et-Hun", "it-Hun", "ət-Hun" ], + "names": [ "Eastern Duka", "Eastern Dukawa", "Ethun", "Hun-ne", "Hune", "et-Hun", "it-Hun", "tHun", "u̱t-Hun", "ət-Hun" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -117946,6 +123425,8 @@ "full": "utr-Latn-NG", "iana": [ "Etulo" ], "iso639_3": "utr", + "localname": "Etulo", + "localnames": [ "Etulo" ], "name": "Etulo", "names": [ "Eturo", "Turumawa", "Utur" ], "region": "NG", @@ -117998,21 +123479,6 @@ "tag": "uum-Cyrl", "windows": "uum-Cyrl" }, - { - "full": "uun-Latn-TW", - "iana": [ "Kulon-Pazeh" ], - "iso639_3": "uun", - "localnames": [ "Pazih" ], - "name": "Kulon-Pazeh", - "names": [ "Kulon", "Kulon-Pazih", "Kulun", "Pazeh", "Pazeh-Kahabu", "Pazehe", "Pazih", "Pazzehe", "Sprache von Formosa" ], - "region": "TW", - "regionname": "China-Taiwan", - "script": "Latn", - "sldr": false, - "tag": "uun", - "tags": [ "uun-Latn", "uun-TW" ], - "windows": "uun-Latn" - }, { "full": "uur-Latn-VU", "iana": [ "Ura (Vanuatu)" ], @@ -118033,7 +123499,7 @@ "iana": [ "U" ], "iso639_3": "uuu", "name": "U", - "names": [ "Awa Blang", "A’erwa", "Puman", "P’uman", "Wa", "Wu", "Wu Blang" ], + "names": [ "Awa", "Awa Blang", "A’erwa", "Puman", "P’uman", "Wa", "Wu", "Wu Blang" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -118123,8 +123589,8 @@ "iso639_3": "uzb", "localname": "o‘zbek", "localnames": [ "oʻzbek tili" ], - "name": "Uzbek, Northern", - "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], + "name": "Uzbek", + "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek, Northern", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], "region": "UZ", "regionname": "Uzbekistan", "regions": [ "AF", "CN", "IL", "KG", "KZ", "RU", "TJ", "TM", "TR", "UA", "US" ], @@ -118133,15 +123599,15 @@ "tag": "uz", "tags": [ "uz-Latn", "uz-UZ", "uz-uzn", "uz-uzn-UZ", "uzn", "uzn-Latn", "uzn-Latn-UZ", "uzn-UZ" ], "variants": [ "baku1926" ], - "windows": "uz-Latn" + "windows": "uz-Latn-UZ" }, { "full": "uz-Arab-AF", "iana": [ "Uzbek" ], "iso639_3": "uzb", "localname": "اوزبیک", - "name": "Uzbek, Northern", - "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], + "name": "Uzbek", + "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek, Northern", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], "region": "AF", "regionname": "Afghanistan", "regions": [ "CN", "IL", "KG", "KZ", "RU", "TJ", "TM", "TR", "UA", "US", "UZ" ], @@ -118155,8 +123621,8 @@ "full": "uz-Brai-UZ", "iana": [ "Uzbek" ], "iso639_3": "uzb", - "name": "Uzbek, Northern", - "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], + "name": "Uzbek", + "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek, Northern", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], "nophonvars": true, "region": "UZ", "regionname": "Uzbekistan", @@ -118165,7 +123631,7 @@ "sldr": false, "tag": "uz-Brai", "tags": [ "uzn-Brai", "uzn-Brai-UZ" ], - "windows": "uz-Brai" + "windows": "uz-Brai-UZ" }, { "full": "uz-Cyrl-CN", @@ -118186,8 +123652,8 @@ "latnnames": [ "oʻzbek tili" ], "localname": "ўзбекча", "localnames": [ "ўзбек тили" ], - "name": "Uzbek, Northern", - "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], + "name": "Uzbek", + "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek, Northern", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], "nophonvars": true, "region": "UZ", "regionname": "Uzbekistan", @@ -118196,14 +123662,14 @@ "sldr": true, "tag": "uz-Cyrl", "tags": [ "uzn-Cyrl", "uzn-Cyrl-UZ" ], - "windows": "uz-Cyrl" + "windows": "uz-Cyrl-UZ" }, { "full": "uz-Sogd-CN", "iana": [ "Uzbek" ], "iso639_3": "uzb", - "name": "Uzbek, Northern", - "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], + "name": "Uzbek", + "names": [ "Ouzbek", "O’zbek", "Usbaki", "Usbeki", "Uzbek, Northern", "oʻzbek", "oʻzbek tili", "oʻzbekcha", "Özbek" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -118212,7 +123678,7 @@ "sldr": false, "tag": "uz-Sogd", "tags": [ "uzn-Sogd", "uzn-Sogd-CN" ], - "windows": "uz-Sogd" + "windows": "uz-Sogd-CN" }, { "full": "uzs-Arab-AF", @@ -118220,6 +123686,7 @@ "iso639_3": "uzs", "latnnames": [ "o'zbek" ], "localnames": [ "ﯣزبېک" ], + "macrolang": "uz", "name": "Uzbek, Southern", "names": [ "O’zbek", "Southern Uzbek", "Usbeki", "Uzbak", "Uzbeki" ], "region": "AF", @@ -118276,6 +123743,8 @@ "full": "vag-Latn-GH", "iana": [ "Vagla" ], "iso639_3": "vag", + "localname": "Vagla", + "localnames": [ "Vagla" ], "name": "Vagla", "names": [ "Konosarola", "Paxala", "Vagala" ], "region": "GH", @@ -118306,7 +123775,9 @@ "full": "vai-Vaii-LR", "iana": [ "Vai" ], "iso639_3": "vai", + "latnnames": [ "Vai" ], "localname": "ꕙꔤ", + "localnames": [ "ꕙꔤ" ], "name": "Vai", "names": [ "Gallinas", "Gallines", "Vei", "Vy" ], "region": "LR", @@ -118368,8 +123839,9 @@ "full": "vam-Latn-PG", "iana": [ "Vanimo" ], "iso639_3": "vam", - "name": "Vanimo", - "names": [ "Dumo", "Duso", "Manimo", "Wanimo" ], + "localnames": [ "Dumo" ], + "name": "Dumo", + "names": [ "Duso", "Manimo", "Vanimo", "Wanimo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -118382,8 +123854,8 @@ "full": "van-Latn-PG", "iana": [ "Valman" ], "iso639_3": "van", - "name": "Valman", - "names": [ "Koroko", "Koruon", "Walman" ], + "name": "Walman", + "names": [ "Koroko", "Koruon", "Valman" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -118413,7 +123885,7 @@ "iso639_3": "vap", "localnames": [ "Vaiphei" ], "name": "Vaiphei", - "names": [ "Bhaipei", "Vaipei", "Veiphei", "Zomi" ], + "names": [ "Bhaipei", "Vaipei", "Veiphei", "Zome", "Zomi" ], "region": "IN", "regionname": "India", "script": "Latn", @@ -118426,7 +123898,7 @@ "full": "var-Latn-MX", "iana": [ "Huarijio" ], "iso639_3": "var", - "localname": "Huarijío", + "localname": "Warijio", "localnames": [ "Warijio" ], "name": "Huarijío", "names": [ "Guarijío", "Huarijio", "Maculái", "Macurawe", "Macurái", "Makurawe", "Varihío", "Varijío", "Varohio", "Vorijío", "Warihó", "Warijio" ], @@ -118472,15 +123944,15 @@ "full": "vau-Latn-CD", "iana": [ "Vanuma" ], "iso639_3": "vau", + "localname": "Lɨvanʉma", "name": "Vanuma", "names": [ "Bambutuku", "Bvanuma", "Livanuma", "Nyali-Tchabi", "South Nyali" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "vau", "tags": [ "vau-CD", "vau-Latn" ], - "unwritten": true, "windows": "vau-Latn" }, { @@ -118546,6 +124018,7 @@ "full": "vbk-Latn-PH", "iana": [ "Southwestern Bontok" ], "iso639_3": "vbk", + "macrolang": "bkb", "name": "Bontok, Southwestern", "names": [ "Bontoc", "Southwestern Bontok" ], "region": "PH", @@ -118598,7 +124071,7 @@ "localname": "Veneto", "localnames": [ "Veneto" ], "name": "Venetian", - "names": [ "Chipileño", "Talian", "Taliano", "Venet", "Veneto", "Vèneto" ], + "names": [ "Chipileño", "Talian", "Taliano", "Venet", "Veneto", "Vêneto Brasileiro" ], "region": "IT", "regionname": "Italy", "regions": [ "AL", "BR", "GR", "HR", "ME", "MX", "SI" ], @@ -118651,26 +124124,26 @@ "windows": "vem-Latn" }, { - "full": "veo-Zyyy-US", + "full": "veo-Latn-US", "iana": [ "Ventureño" ], "iso639_3": "veo", "name": "Ventureño", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "veo", - "tags": [ "veo-US", "veo-Zyyy" ], + "tags": [ "veo-Latn", "veo-US" ], "unwritten": true, - "windows": "veo-Zyyy" + "windows": "veo-Latn" }, { "full": "vep-Latn-RU", "iana": [ "Veps" ], "iso639_3": "vep", - "localnames": [ "Vepsäks", "Vepsän kelʹ" ], + "localnames": [ "Vepsä", "Vepsän kelʹ" ], "name": "Veps", - "names": [ "Vepsian", "Vepsish", "Vepsäks", "Vepsän keli", "Vepsän kelʹ" ], + "names": [ "Vepsian", "Vepsish", "Vepsä", "Vepsän keli", "Vepsän kelʹ" ], "region": "RU", "regionname": "Russian Federation", "script": "Latn", @@ -118771,12 +124244,156 @@ "tag": "vi-Hani", "windows": "vi-Hani" }, + { + "full": "vi-Latn-VN-x-central", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "757", + "script": "Latn", + "sldr": false, + "tag": "vi-x-central", + "tags": [ "vi-Latn-x-central", "vi-VN-x-central" ], + "windows": "vi-Latn-x-central" + }, + { + "full": "vi-Latn-VN-x-ncentral", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "22375", + "script": "Latn", + "sldr": false, + "tag": "vi-x-ncentral", + "tags": [ "vi-Latn-x-ncentral", "vi-VN-x-ncentral" ], + "windows": "vi-Latn-x-ncentral" + }, + { + "full": "vi-Latn-VN-x-southern", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "2", + "script": "Latn", + "sldr": false, + "tag": "vi-x-southern", + "tags": [ "vi-Latn-x-southern", "vi-VN-x-southern" ], + "windows": "vi-Latn-x-southern" + }, + { + "full": "vi-Latn-VN-x-sub25", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "24048", + "script": "Latn", + "sldr": false, + "tag": "vi-x-sub25", + "tags": [ "vi-Latn-x-sub25", "vi-VN-x-sub25" ], + "windows": "vi-Latn-x-sub25" + }, + { + "full": "vi-Latn-VN-x-sub31", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "24043", + "script": "Latn", + "sldr": false, + "tag": "vi-x-sub31", + "tags": [ "vi-Latn-x-sub31", "vi-VN-x-sub31" ], + "windows": "vi-Latn-x-sub31" + }, + { + "full": "vi-Latn-VN-x-sub39", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "24046", + "script": "Latn", + "sldr": false, + "tag": "vi-x-sub39", + "tags": [ "vi-Latn-x-sub39", "vi-VN-x-sub39" ], + "windows": "vi-Latn-x-sub39" + }, + { + "full": "vi-Latn-VN-x-sub49", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "24050", + "script": "Latn", + "sldr": false, + "tag": "vi-x-sub49", + "tags": [ "vi-Latn-x-sub49", "vi-VN-x-sub49" ], + "windows": "vi-Latn-x-sub49" + }, + { + "full": "vi-Latn-VN-x-sub67", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "24045", + "script": "Latn", + "sldr": false, + "tag": "vi-x-sub67", + "tags": [ "vi-Latn-x-sub67", "vi-VN-x-sub67" ], + "windows": "vi-Latn-x-sub67" + }, + { + "full": "vi-Latn-VN-x-subdn", + "iana": [ "Vietnamese" ], + "iso639_3": "vie", + "localnames": [ "Tiếng Việt" ], + "name": "Vietnamese", + "names": [ "Annamese", "Ching", "Gin", "Jing", "Kinh", "Tiếng Việt", "Viet" ], + "region": "VN", + "regionname": "Viet Nam", + "rod": "24047", + "script": "Latn", + "sldr": false, + "tag": "vi-x-subdn", + "tags": [ "vi-Latn-x-subdn", "vi-VN-x-subdn" ], + "windows": "vi-Latn-x-subdn" + }, { "full": "vic-Latn-SX", "iana": [ "Virgin Islands Creole English" ], "iso639_3": "vic", - "name": "Netherlands Antilles Creole English", - "names": [ "Virgin Islands Creole English" ], + "name": "Virgin Islands English Creole", + "names": [ "Netherlands Antilles Creole English", "Virgin Islands Creole English" ], "region": "SX", "regionname": "Sint Maarten (Dutch part)", "regions": [ "BQ", "VG", "VI" ], @@ -118790,7 +124407,7 @@ "full": "vid-Latn-TZ", "iana": [ "Vidunda" ], "iso639_3": "vid", - "localname": "Vidunda", + "localname": "Chividunda", "localnames": [ "Chividunda" ], "name": "Vidunda", "names": [ "Chividunda", "Kividunda", "Ndunda" ], @@ -118806,13 +124423,14 @@ "full": "vif-Latn-CG", "iana": [ "Vili" ], "iso639_3": "vif", + "localnames": [ "Civili" ], "name": "Vili", "names": [ "Bavili", "Civili", "Fiot", "Fiote", "Tshivili", "Tsivili" ], "region": "CG", "regionname": "Congo", "regions": [ "GA" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "vif", "tags": [ "vif-CG", "vif-Latn" ], "windows": "vif-Latn" @@ -118821,15 +124439,15 @@ "full": "vig-Latn-BF", "iana": [ "Viemo" ], "iso639_3": "vig", + "localnames": [ "Viemɔ̃" ], "name": "Viemo", - "names": [ "Vige", "Vigué", "Vigye" ], + "names": [ "Viemon", "Viemɔ̃", "Vige", "Vigué", "Vigye" ], "region": "BF", "regionname": "Burkina Faso", "script": "Latn", "sldr": false, "tag": "vig", "tags": [ "vig-BF", "vig-Latn" ], - "unwritten": true, "windows": "vig-Latn" }, { @@ -118906,6 +124524,22 @@ "tags": [ "viv-Latn", "viv-PG" ], "windows": "viv-Latn" }, + { + "full": "vjk-Deva-IN", + "iso639_3": "vjk", + "latnnames": [ "Bajjika" ], + "localnames": [ "बज्जिका" ], + "name": "Bajjika", + "names": [ "Brajjika", "Brijjika", "Vaishali ki boli", "Vajjika", "Vrijjika", "Western Maithili" ], + "region": "IN", + "regionname": "India", + "regions": [ "NP" ], + "script": "Deva", + "sldr": false, + "tag": "vjk", + "tags": [ "vjk-Deva", "vjk-IN" ], + "windows": "vjk-Deva" + }, { "full": "vka-Latn-AU", "iana": [ "Kariyarra" ], @@ -118921,21 +124555,6 @@ "unwritten": true, "windows": "vka-Latn" }, - { - "full": "vki-Latn-NG", - "iana": [ "Ija-Zuba" ], - "iso639_3": "vki", - "name": "Ija-Zuba", - "names": [ "Koro Afiki", "Koro Ija", "Koro Zuba" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "vki", - "tags": [ "vki-Latn", "vki-NG" ], - "unwritten": true, - "windows": "vki-Latn" - }, { "full": "vkj-Latn-TD", "iana": [ "Kujarge" ], @@ -118954,6 +124573,7 @@ "full": "vkk-Latn-ID", "iana": [ "Kaur" ], "iso639_3": "vkk", + "macrolang": "ms", "name": "Kaur", "names": [ "Bintuhan", "Ka’ur", "Mulak" ], "region": "ID", @@ -118995,6 +124615,21 @@ "unwritten": true, "windows": "vkm-Latn" }, + { + "full": "vkn-Latn-NG", + "iana": [ "Koro Nulu" ], + "iso639_3": "vkn", + "name": "Koro Nulu", + "names": [ "Ija Koro", "Koro Afiki", "Koro Ija" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "vkn", + "tags": [ "vki", "vki-Latn", "vki-Latn-NG", "vki-NG", "vkn-Latn", "vkn-NG" ], + "unwritten": true, + "windows": "vkn-Latn" + }, { "full": "vko-Latn-ID", "iana": [ "Kodeoha" ], @@ -119011,36 +124646,39 @@ "windows": "vko-Latn" }, { - "full": "vkp-Deva-IN", + "full": "vkp-Latn-IN", "iana": [ "Korlai Creole Portuguese" ], "iso639_3": "vkp", - "name": "Korlai Creole Portuguese", - "names": [ "Creole Portuguese" ], - "nophonvars": true, + "localnames": [ "Nou Ling" ], + "name": "Korlai Portuguese Creole", + "names": [ "Creole Portuguese", "Korlai Creole Portuguese", "Kristi", "No Ling", "Nou Ling", "Nɔ Ling" ], "region": "IN", "regionname": "India", - "script": "Deva", + "script": "Latn", "sldr": false, - "tag": "vkp-Deva", - "windows": "vkp-Deva" + "tag": "vkp", + "tags": [ "vkp-IN", "vkp-Latn" ], + "windows": "vkp-Latn" }, { - "full": "vkp-Latn-IN", + "full": "vkp-Deva-IN", "iana": [ "Korlai Creole Portuguese" ], "iso639_3": "vkp", - "name": "Korlai Creole Portuguese", - "names": [ "Creole Portuguese" ], + "name": "Korlai Portuguese Creole", + "names": [ "Creole Portuguese", "Korlai Creole Portuguese", "Kristi", "No Ling", "Nou Ling", "Nɔ Ling" ], + "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Latn", + "script": "Deva", "sldr": false, - "tag": "vkp-Latn", - "windows": "vkp-Latn" + "tag": "vkp-Deva", + "windows": "vkp-Deva" }, { "full": "vkt-Latn-ID", "iana": [ "Tenggarong Kutai Malay" ], "iso639_3": "vkt", + "macrolang": "ms", "name": "Malay, Tenggarong Kutai", "names": [ "Kutai", "Tenggarong", "Tenggarong Kutai Malay" ], "region": "ID", @@ -119067,6 +124705,21 @@ "unwritten": true, "windows": "vku-Latn" }, + { + "full": "vkz-Latn-NG", + "iana": [ "Koro Zuba" ], + "iso639_3": "vkz", + "name": "Koro Zuba", + "names": [ "Koro Ganagana", "Zuba Koro" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "vkz", + "tags": [ "vkz-Latn", "vkz-NG" ], + "unwritten": true, + "windows": "vkz-Latn" + }, { "full": "vlp-Latn-VU", "iana": [ "Valpei" ], @@ -119086,8 +124739,9 @@ "full": "vls-Latn-BE", "iana": [ "Vlaams" ], "iso639_3": "vls", - "name": "Vlaams", - "names": [ "Flamand", "Frans Vlaams", "Vlaemsch", "Vlamingen", "West Flemish", "West Vlaams", "Western Flemish" ], + "localnames": [ "West-Vlams" ], + "name": "West Flemish", + "names": [ "Flamand", "Frans Vlaams", "Vlaams", "Vlaemsch", "Vlamingen", "West Vlaams", "West-Vlams", "West-Vloams", "Western Flemish", "Westvlams", "flamand occidental" ], "region": "BE", "regionname": "Belgium", "regions": [ "FR", "NL", "US" ], @@ -119132,7 +124786,7 @@ "iana": [ "Juxtlahuaca Mixtec" ], "iso639_3": "vmc", "name": "Mixtec, Juxtlahuaca", - "names": [ "Central Juxtlahuaca Mixtec", "Juxtlahuaca Mixtec", "Mixteco de Juxtlahuaca" ], + "names": [ "Central Juxtlahuaca Mixtec", "Juxtlahuaca Mixtec", "Mixteco de Juxtlahuaca", "Mixteco del Oeste Alto", "Tu’un davi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -119142,18 +124796,18 @@ "windows": "vmc-Latn" }, { - "full": "vmd-Zyyy-IN", + "full": "vmd-Knda-IN", "iana": [ "Mudu Koraga" ], "iso639_3": "vmd", "name": "Koraga, Mudu", "names": [ "Mudu Koraga", "Muudu" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Knda", "sldr": false, "tag": "vmd", - "tags": [ "vmd-IN", "vmd-Zyyy" ], - "windows": "vmd-Zyyy" + "tags": [ "vmd-IN", "vmd-Knda" ], + "windows": "vmd-Knda" }, { "full": "vme-Latn-ID", @@ -119189,8 +124843,9 @@ "full": "vmg-Latn-PG", "iana": [ "Lungalunga" ], "iso639_3": "vmg", - "name": "Lungalunga", - "names": [ "Minigir", "Tavue", "Vinitiri" ], + "localnames": [ "Vinitiri", "Vinitiri Lungalungá" ], + "name": "Vinitiri", + "names": [ "Lungalunga", "Minigir", "Minigiri", "Tavue", "Vinitiri Lungalungá", "Vinitiri Tuna" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -119200,17 +124855,17 @@ "windows": "vmg-Latn" }, { - "full": "vmh-Zyyy-IR", + "full": "vmh-Arab-IR", "iana": [ "Maraghei" ], "iso639_3": "vmh", "name": "Maraghei", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "vmh", - "tags": [ "vmh-IR", "vmh-Zyyy" ], - "windows": "vmh-Zyyy" + "tags": [ "vmh-Arab", "vmh-IR" ], + "windows": "vmh-Arab" }, { "full": "vmi-Latn-AU", @@ -119233,7 +124888,7 @@ "iso639_3": "vmj", "localnames": [ "Tu'un savi" ], "name": "Mixtec, Ixtayutla", - "names": [ "Ixtayutla Mixtec", "Mixteco de Santiago Ixtayutla", "Northeastern Jamiltepec Mixtec", "Tu’un savi" ], + "names": [ "Ixtayutla Mixtec", "Mixteco de Ixtayutla", "Mixteco de Santiago Ixtayutla", "Northeastern Jamiltepec Mixtec", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -119278,7 +124933,7 @@ "iso639_3": "vmm", "localnames": [ "Jnu'u lavi" ], "name": "Mixtec, Mitlatongo", - "names": [ "Jnu’u lavi", "Mitlatongo Mixtec", "Mixteco de Mitlatongo" ], + "names": [ "Jnu’u lavi", "Mitlatongo Mixtec", "Mixteco de Mitlatongo", "Tu’un lavi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -119307,7 +124962,7 @@ "iana": [ "Soyaltepec Mixtec" ], "iso639_3": "vmq", "name": "Mixtec, Soyaltepec", - "names": [ "Mixteco de San Bartolo Soyaltepec", "Mixteco del Noreste Bajo", "Soyaltepec Mixtec", "Tu’un savi" ], + "names": [ "Mixteco de San Bartolo Soyaltepec", "Mixteco del Noreste Bajo", "San Bartolo Soyaltepec", "Soyaltepec Mixtec", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -119386,6 +125041,7 @@ "names": [ "Central Makhuwa", "Emakhuwa", "Emakhuwa-Makhuwana", "Emakua", "Macua", "Makhuwa-Makhuwana", "Makhuwwa of Nampula", "Makoane", "Makua", "Maquoua" ], "region": "MZ", "regionname": "Mozambique", + "regions": [ "KM" ], "script": "Latn", "sldr": false, "tag": "vmw", @@ -119411,7 +125067,7 @@ "iana": [ "Tamazola Mixtec" ], "iso639_3": "vmx", "name": "Mixtec, Tamazola", - "names": [ "Mixteco de San Juan Tamazola", "Tamazola Mixtec" ], + "names": [ "Mixteco de San Juan Tamazola", "Tamazola Mixtec", "Tnu’un davi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -119424,7 +125080,7 @@ "full": "vmy-Latn-MX", "iana": [ "Ayautla Mazatec" ], "iso639_3": "vmy", - "localname": "Mazatec, Ayautla", + "localname": "Enre naxinanda nguifi", "localnames": [ "Enre naxinanda nguifi" ], "name": "Mazatec, Ayautla", "names": [ "Ayautla Mazatec", "Enre naxinanda nguifi", "Mazateco del Sureste" ], @@ -119442,7 +125098,7 @@ "iso639_3": "vmz", "localnames": [ "Ienra naxinandana nnandia" ], "name": "Mazatec, Mazatlán", - "names": [ "Ienra naxinandana nnandia", "Mazateco de Mazatlán Villa de Flores", "Mazateco del Suroeste", "Mazatlán Mazatec" ], + "names": [ "Ienra naxinandana nnandia", "Mazateco de Mazatlán Villa de Flores", "Mazateco del Suroeste", "Mazatlán Mazatec", "ꞌíénná" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -119515,7 +125171,7 @@ "iana": [ "Voro" ], "iso639_3": "vor", "name": "Voro", - "names": [ "Bena", "Buna", "Ebina", "Ebuna", "Woro", "Yungur" ], + "names": [ "Bena", "Buna", "Ebina", "Ebәna", "Woro", "Yungur" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -119566,7 +125222,6 @@ "sldr": false, "tag": "vra", "tags": [ "vra-Latn", "vra-VU" ], - "unwritten": true, "windows": "vra-Latn" }, { @@ -119574,10 +125229,12 @@ "iana": [ "Võro" ], "iso639_3": "vro", "localnames": [ "Võro kiil" ], + "macrolang": "et", "name": "Võro", - "names": [ "Voro", "Voru", "Võro kiil", "Võro-Seto", "Võru" ], + "names": [ "Seto", "Setu", "Voro", "Voru", "Võro kiil", "Võro-Seto", "Võru" ], "region": "EE", "regionname": "Estonia", + "regions": [ "LV", "RU" ], "script": "Latn", "sldr": false, "tag": "vro", @@ -119716,19 +125373,33 @@ "windows": "vut-Latn" }, { - "full": "vwa-Zyyy-CN", + "full": "vwa-Latn-CN", "iana": [ "Awa (China)" ], "iso639_3": "vwa", "name": "Awa", "names": [ "Ava", "Awa (China)", "Awa Wa", "Va" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "vwa", - "tags": [ "vwa-CN", "vwa-Zyyy" ], + "tags": [ "vwa-CN", "vwa-Latn" ], "unwritten": true, - "windows": "vwa-Zyyy" + "windows": "vwa-Latn" + }, + { + "full": "vwa-Mymr-CN", + "iana": [ "Awa (China)" ], + "iso639_3": "vwa", + "name": "Awa", + "names": [ "Ava", "Awa (China)", "Awa Wa", "Va" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Mymr", + "sldr": false, + "tag": "vwa-Mymr", + "windows": "vwa-Mymr" }, { "full": "wa-Latn-BE", @@ -119765,15 +125436,15 @@ "full": "wab-Latn-PG", "iana": [ "Wab" ], "iso639_3": "wab", - "name": "Wab", - "names": [ "Som" ], + "localnames": [ "Yote" ], + "name": "Yote", + "names": [ "Som", "Wab" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "wab", "tags": [ "wab-Latn", "wab-PG" ], - "unwritten": true, "windows": "wab-Latn" }, { @@ -119793,11 +125464,11 @@ }, { "full": "wad-Latn-ID", - "iana": [ "Wandamen" ], + "iana": [ "Wamesa", "Wondama" ], "iso639_3": "wad", "localnames": [ "Wamesa", "Wondama" ], - "name": "Wandamen", - "names": [ "Wamesa", "Wandamen-Windesi", "Wondama" ], + "name": "Wamesa", + "names": [ "Wandamen", "Wandamen-Windesi", "Wondama" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -119827,6 +125498,7 @@ "iana": [ "Wakoná" ], "iso639_3": "waf", "name": "Wakoná", + "names": [ "Aconan" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -119841,7 +125513,7 @@ "iana": [ "Wa'ema" ], "iso639_3": "wag", "name": "Wa’ema", - "names": [ "Wa'ema", "Waiema" ], + "names": [ "Wa'ema", "Waema", "Waiema" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -119885,6 +125557,7 @@ "iana": [ "Waffa" ], "iso639_3": "waj", "name": "Waffa", + "names": [ "Vuafaaqaa" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -119911,7 +125584,7 @@ "full": "wal-Latn-ET", "iana": [ "Wolaytta", "Wolaitta" ], "iso639_3": "wal", - "localname": "ወላይታቱ", + "localname": "Wolaytta", "localnames": [ "Wolaytta" ], "name": "Wolaytta", "region": "ET", @@ -119968,8 +125641,9 @@ "full": "wap-Latn-GY", "iana": [ "Wapishana" ], "iso639_3": "wap", + "localnames": [ "Wapichan", "Wapichan paradan" ], "name": "Wapishana", - "names": [ "Aruma", "Uapixana", "Vapidiana", "Wapichan", "Wapichana", "Wapisana", "Wapishiana", "Wapishshiana", "Wapisiana", "Wapitxana", "Wapixana", "Wapixiana", "Wapixiána", "Wapixána" ], + "names": [ "Aruma", "Uapixana", "Vapidiana", "Wapichan", "Wapichan paradan", "Wapichana", "Wapisana", "Wapishiana", "Wapishshiana", "Wapisiana", "Wapitxana", "Wapixana", "Wapixiana", "Wapixiána", "Wapixána" ], "region": "GY", "regionname": "Guyana", "regions": [ "BR" ], @@ -120000,7 +125674,7 @@ "iso639_3": "war", "localnames": [ "Waray", "Waray-Waray" ], "name": "Waray-Waray", - "names": [ "Binisaya", "Samaran", "Samarenyo", "Samareño", "Waray", "Waray (Philippines)", "Winaray" ], + "names": [ "Binisaya", "Lineyte-Samarnon", "Samaran", "Samarenyo", "Samareño", "Waray", "Waray (Philippines)", "Winaray" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -120014,7 +125688,7 @@ "iana": [ "Waray (Philippines)" ], "iso639_3": "war", "name": "Waray-Waray", - "names": [ "Binisaya", "Samaran", "Samarenyo", "Samareño", "Waray", "Waray (Philippines)", "Winaray" ], + "names": [ "Binisaya", "Lineyte-Samarnon", "Samaran", "Samarenyo", "Samareño", "Waray", "Waray (Philippines)", "Winaray" ], "nophonvars": true, "region": "PH", "regionname": "Philippines", @@ -120042,6 +125716,8 @@ "full": "wat-Latn-PG", "iana": [ "Kaninuwa" ], "iso639_3": "wat", + "localname": "Kaninuwa", + "localnames": [ "Kaninuwa" ], "name": "Kaninuwa", "names": [ "Kaokao", "Wataluma" ], "region": "PG", @@ -120152,7 +125828,7 @@ "regionname": "Venezuela", "regions": [ "GY", "SR" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "wba", "tags": [ "wba-Latn", "wba-VE" ], "windows": "wba-Latn" @@ -120220,10 +125896,10 @@ "full": "wbi-Latn-TZ", "iana": [ "Vwanji" ], "iso639_3": "wbi", - "localname": "Bavwanji", - "localnames": [ "Bavwanji" ], + "localname": "Kɨvwanji", + "localnames": [ "Kɨvwanji" ], "name": "Vwanji", - "names": [ "Bavwanji", "Kivwanji", "Kiwanji", "Wanji" ], + "names": [ "Bavwanji", "Kivwanji", "Kiwanji", "Kɨvwanji", "Wanji" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -120275,7 +125951,7 @@ "sldr": false, "tag": "wbl", "tags": [ "wbl-Latn", "wbl-PK" ], - "windows": "wbl-Latn" + "windows": "wbl-Latn-PK" }, { "full": "wbl-Arab-AF", @@ -120291,7 +125967,7 @@ "sldr": false, "tag": "wbl-Arab", "tags": [ "wbl-AF" ], - "windows": "wbl-Arab" + "windows": "wbl-Arab-AF" }, { "full": "wbl-Cyrl-TJ", @@ -120307,7 +125983,7 @@ "sldr": false, "tag": "wbl-Cyrl", "tags": [ "wbl-TJ" ], - "windows": "wbl-Cyrl" + "windows": "wbl-Cyrl-TJ" }, { "full": "wbm-Latn-CN", @@ -120355,12 +126031,27 @@ "tags": [ "wbq-IN", "wbq-Telu" ], "windows": "wbq-Telu" }, + { + "full": "wbq-Arab-IN", + "iana": [ "Waddar" ], + "iso639_3": "wbq", + "name": "Waddar", + "names": [ "Od", "Orh", "Vadari", "Vadda Beldar", "Werders", "Wodde" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Arab", + "sldr": false, + "tag": "wbq-Arab", + "windows": "wbq-Arab" + }, { "full": "wbr-Deva-IN", "iana": [ "Wagdi" ], "iso639_3": "wbr", "latnnames": [ "Vāgdī" ], "localnames": [ "वाग्दी" ], + "macrolang": "raj", "name": "Wagdi", "names": [ "Bhili", "Bhilodi", "Mina Bhil", "Vagadi", "Vagari", "Vagdi", "Vaged", "Vageri", "Vagi", "Wagadi", "Wagari", "Waghari", "Wagholi", "Wagri" ], "region": "IN", @@ -120519,6 +126210,20 @@ "unwritten": true, "windows": "wdk-Latn" }, + { + "full": "wdt-Latn-CA", + "iana": [ "Wendat" ], + "iso639_3": "wdt", + "name": "Wendat", + "names": [ "Huron", "Huron-Wendat" ], + "region": "CA", + "regionname": "Canada", + "script": "Latn", + "sldr": false, + "tag": "wdt", + "tags": [ "wdt-CA", "wdt-Latn" ], + "windows": "wdt-Latn" + }, { "full": "wdu-Latn-AU", "iana": [ "Wadjigu" ], @@ -120568,7 +126273,6 @@ "full": "wec-Latn-CI", "iana": [ "Wè Western" ], "iso639_3": "wec", - "localnames": [ "Wèè" ], "name": "Wè Western", "names": [ "Gere", "Guéré", "Neyo", "Wèè" ], "region": "CI", @@ -120583,8 +126287,9 @@ "full": "wed-Latn-PG", "iana": [ "Wedau" ], "iso639_3": "wed", + "localnames": [ "Pona Wedau" ], "name": "Wedau", - "names": [ "Wamira", "Wedaun", "Wedawan" ], + "names": [ "Pona Wedau", "Wamira", "Wedaun", "Wedawan" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -120612,7 +126317,9 @@ "full": "weh-Latn-CM", "iana": [ "Weh" ], "iso639_3": "weh", + "localnames": [ "Ghɔ̂mə̀ kə́wí", "Kə́wí" ], "name": "Weh", + "names": [ "Ghɔ̂mə̀ kə́wí", "Kə́wí", "Kɨ́wí", "Wi" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -120832,7 +126539,7 @@ "iana": [ "Waigeo" ], "iso639_3": "wgo", "name": "Ambel", - "names": [ "Amber", "Amberi", "Waigeo", "Waigiu" ], + "names": [ "Amber", "Amberi", "Siam", "Waigeo", "Waigiu" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -120891,8 +126598,8 @@ "full": "whg-Latn-PG", "iana": [ "North Wahgi" ], "iso639_3": "whg", - "name": "Wahgi, North", - "names": [ "Banz", "Jiwaka", "North Wahgi", "Yuwe" ], + "name": "Yuwei", + "names": [ "Banz", "Jiwaka", "North Wahgi", "Yu We", "Yu Wei", "Yuwe" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -120936,9 +126643,10 @@ "full": "wib-Latn-BF", "iana": [ "Southern Toussian" ], "iso639_3": "wib", - "localname": "Toussian, Southern", + "localname": "Wĩ́nwɛ́y", + "localnames": [ "Wĩ́nwɛ́y" ], "name": "Toussian, Southern", - "names": [ "Southern Toussian", "Tusia", "Tusian", "Tusya", "Tusyang", "Win" ], + "names": [ "Southern Toussian", "Tusia", "Tusian", "Tusya", "Tusyang", "Win", "Wĩ́nwɛ́y" ], "region": "BF", "regionname": "Burkina Faso", "regions": [ "CI" ], @@ -121133,8 +126841,8 @@ "iso639_3": "wiu", "localname": "Witu", "localnames": [ "Witu" ], - "name": "Wiru", - "names": [ "Witu" ], + "name": "Witu", + "names": [ "Wiru" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -121178,8 +126886,9 @@ "full": "wja-Latn-NG", "iana": [ "Waja" ], "iso639_3": "wja", + "localnames": [ "Nyan Wịyáù" ], "name": "Waja", - "names": [ "Nyan Wiyau", "Wiyaa", "Wiyau", "Wuya" ], + "names": [ "Nyan Wịyáù", "Wagga", "Wiyaa", "Wiyau", "Wuya" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -121192,6 +126901,7 @@ "full": "wji-Latn-NG", "iana": [ "Warji" ], "iso639_3": "wji", + "localnames": [ "Sirzakwai" ], "name": "Warji", "names": [ "Sar", "Sarawa", "Sirzakwai", "Warja", "Warjawa" ], "region": "NG", @@ -121349,6 +127059,20 @@ "unwritten": true, "windows": "wlc-Zyyy" }, + { + "full": "wle-Ethi-ET", + "iana": [ "Wolane" ], + "iso639_3": "wle", + "name": "Wolane", + "region": "ET", + "regionname": "Ethiopia", + "script": "Ethi", + "sldr": false, + "tag": "wle", + "tags": [ "wle-ET", "wle-Ethi" ], + "unwritten": true, + "windows": "wle-Ethi" + }, { "full": "wle-Arab-ET", "iana": [ "Wolane" ], @@ -121360,16 +127084,28 @@ "script": "Arab", "sldr": false, "tag": "wle-Arab", - "tags": [ "wle-ET" ], "unwritten": true, "windows": "wle-Arab" }, + { + "full": "wle-Latn-ET", + "iana": [ "Wolane" ], + "iso639_3": "wle", + "name": "Wolane", + "region": "ET", + "regionname": "Ethiopia", + "script": "Latn", + "sldr": false, + "tag": "wle-Latn", + "unwritten": true, + "windows": "wle-Latn" + }, { "full": "wlg-Latn-AU", "iana": [ "Kunbarlang" ], "iso639_3": "wlg", "name": "Kunbarlang", - "names": [ "Gunbalang", "Gunbarlang", "Gungalang", "Walang", "Warlang" ], + "names": [ "Barlang", "Gunbalang", "Gunbarlang", "Gungalang", "Walang", "Warlang" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -121381,11 +127117,13 @@ }, { "full": "wlh-Latn-TL", + "iana": [ "Welaun" ], "iso639_3": "wlh", "name": "Welaun", "names": [ "Bekais", "Wekais" ], "region": "TL", "regionname": "Timor-Leste", + "regions": [ "ID" ], "script": "Latn", "sldr": false, "tag": "wlh", @@ -121471,6 +127209,7 @@ "iana": [ "Wailapa" ], "iso639_3": "wlr", "name": "Wailapa", + "names": [ "Ale" ], "region": "VU", "regionname": "Vanuatu", "script": "Latn", @@ -121516,8 +127255,8 @@ "iana": [ "Wichí Lhamtés Vejoz" ], "iso639_3": "wlv", "localnames": [ "wichi" ], - "name": "Wichí Lhamtés Vejoz", - "names": [ "Vejos", "Vejoz", "wichi" ], + "name": "Bermejo Wichí", + "names": [ "Vejos", "Vejoz", "Wichí Lhamtés Vejoz", "wichi" ], "region": "AR", "regionname": "Argentina", "script": "Latn", @@ -121643,6 +127382,21 @@ "tags": [ "wme-Deva", "wme-NP" ], "windows": "wme-Deva" }, + { + "full": "wmg-Zyyy-CN", + "iana": [ "Western Minyag" ], + "iso639_3": "wmg", + "name": "Minyag, Western", + "names": [ "Darmdo Minyag", "Menya", "Munya", "Muya", "Western Minyag" ], + "region": "CN", + "regionname": "China", + "script": "Zyyy", + "sldr": false, + "tag": "wmg", + "tags": [ "mvm", "mvm-CN", "mvm-Zyyy", "mvm-Zyyy-CN", "wmg-CN", "wmg-Zyyy" ], + "unwritten": true, + "windows": "wmg-Zyyy" + }, { "full": "wmh-Latn-TL", "iana": [ "Waima'a" ], @@ -121792,8 +127546,8 @@ "full": "wnb-Latn-PG", "iana": [ "Wanambre" ], "iso639_3": "wnb", - "name": "Wanambre", - "names": [ "Vanambere" ], + "name": "Mokati", + "names": [ "Vanambere", "Wanambre" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -122032,7 +127786,9 @@ "full": "wo-Arab-SN", "iana": [ "Wolof" ], "iso639_3": "wol", + "latnnames": [ "Wolof" ], "localname": "Wolof", + "localnames": [ "ولوفل" ], "name": "Wolof", "names": [ "Ouolof", "Volof", "Walaf", "Waro-Waro", "Yallof" ], "nophonvars": true, @@ -122066,7 +127822,7 @@ "localname": "Wɛɛ", "localnames": [ "Wɛɛ" ], "name": "Wè Northern", - "names": [ "Ouobe", "Wobé", "Wèè", "Wɛɛ" ], + "names": [ "Ouobe", "Wobé", "Wèè", "Wê", "Wɛɛ" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -122080,7 +127836,7 @@ "iana": [ "Wogeo" ], "iso639_3": "woc", "name": "Wogeo", - "names": [ "Uageo" ], + "names": [ "Oageva", "Uageo" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -122109,6 +127865,7 @@ "iana": [ "Woleaian" ], "iso639_3": "woe", "name": "Woleaian", + "names": [ "Woleai" ], "region": "FM", "regionname": "Federated States of Micronesia", "script": "Latn", @@ -122122,6 +127879,7 @@ "iana": [ "Woleaian" ], "iso639_3": "woe", "name": "Woleaian", + "names": [ "Woleai" ], "nophonvars": true, "obsolete": true, "region": "FM", @@ -122322,20 +128080,6 @@ "tags": [ "wpc-Latn", "wpc-VE" ], "windows": "wpc-Latn" }, - { - "full": "wra-Latn-PG", - "iana": [ "Warapu" ], - "iso639_3": "wra", - "name": "Warapu", - "names": [ "Barupu" ], - "region": "PG", - "regionname": "Papua New Guinea", - "script": "Latn", - "sldr": false, - "tag": "wra", - "tags": [ "wra-Latn", "wra-PG" ], - "windows": "wra-Latn" - }, { "full": "wrb-Latn-AU", "iana": [ "Waluwarra", "Warluwara" ], @@ -122415,7 +128159,7 @@ "iana": [ "Garrwa" ], "iso639_3": "wrk", "localname": "Karrwa", - "localnames": [ "Karrwa" ], + "localnames": [ "Karawa" ], "name": "Garrwa", "names": [ "Gaarwa", "Garawa", "Garrawa", "Grawa", "Karawa", "Kariwa", "Karrawar", "Karrwa", "Korrawa", "Kurrawar", "Leearawa", "Leearrawa", "Wollongorang", "Wulungwara" ], "region": "AU", @@ -122596,6 +128340,7 @@ "full": "wry-Zyyy-IN", "iana": [ "Merwari" ], "iso639_3": "wry", + "macrolang": "mwr", "name": "Merwari", "names": [ "Ajmeri" ], "region": "IN", @@ -122638,26 +128383,25 @@ "windows": "wsa-Latn" }, { - "full": "wsg-Telu-IN", + "full": "wsg-Gong-IN", "iana": [ "Adilabad Gondi" ], "iso639_3": "wsg", - "latnnames": [ "Koyang", "Gondi" ], - "localname": "కోయంగ్", - "localnames": [ "కోయంగ్", "గోండి" ], + "macrolang": "gon", "name": "Gondi, Adilabad", "names": [ "Adilabad Gondi", "Gunjala Gondi", "Koyang", "Nirmal", "Raj Gond", "Telugu Gondi" ], "region": "IN", "regionname": "India", - "script": "Telu", - "sldr": true, + "script": "Gong", + "sldr": false, "tag": "wsg", - "tags": [ "ggo", "ggo-IN", "ggo-Telu", "ggo-Telu-IN", "wsg-IN", "wsg-Telu" ], - "windows": "wsg-Telu" + "tags": [ "wsg-Gong", "wsg-IN" ], + "windows": "wsg-Gong" }, { "full": "wsg-Deva-IN", "iana": [ "Adilabad Gondi" ], "iso639_3": "wsg", + "macrolang": "gon", "name": "Gondi, Adilabad", "names": [ "Adilabad Gondi", "Gunjala Gondi", "Koyang", "Nirmal", "Raj Gond", "Telugu Gondi" ], "nophonvars": true, @@ -122669,32 +128413,38 @@ "windows": "wsg-Deva" }, { - "full": "wsg-Gong-IN", + "full": "wsg-Gonm-IN", "iana": [ "Adilabad Gondi" ], "iso639_3": "wsg", + "macrolang": "gon", "name": "Gondi, Adilabad", "names": [ "Adilabad Gondi", "Gunjala Gondi", "Koyang", "Nirmal", "Raj Gond", "Telugu Gondi" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Gong", + "script": "Gonm", "sldr": false, - "tag": "wsg-Gong", - "windows": "wsg-Gong" + "tag": "wsg-Gonm", + "windows": "wsg-Gonm" }, { - "full": "wsg-Gonm-IN", + "full": "wsg-Telu-IN", "iana": [ "Adilabad Gondi" ], "iso639_3": "wsg", + "latnnames": [ "Koyang", "Gondi" ], + "localname": "కోయంగ్", + "localnames": [ "కోయంగ్", "గోండి" ], + "macrolang": "gon", "name": "Gondi, Adilabad", "names": [ "Adilabad Gondi", "Gunjala Gondi", "Koyang", "Nirmal", "Raj Gond", "Telugu Gondi" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Gonm", - "sldr": false, - "tag": "wsg-Gonm", - "windows": "wsg-Gonm" + "script": "Telu", + "sldr": true, + "tag": "wsg-Telu", + "tags": [ "ggo", "ggo-IN", "ggo-Telu", "ggo-Telu-IN" ], + "windows": "wsg-Telu" }, { "full": "wsi-Latn-VU", @@ -122786,6 +128536,19 @@ "unwritten": true, "windows": "wsv-Arab" }, + { + "full": "wtb-Latn-TZ", + "iso639_3": "wtb", + "name": "Matambwe", + "names": [ "Kimatambwe", "Matambe", "Tambwe" ], + "region": "TZ", + "regionname": "United Republic of Tanzania", + "script": "Latn", + "sldr": false, + "tag": "wtb", + "tags": [ "wtb-Latn", "wtb-TZ" ], + "windows": "wtb-Latn" + }, { "full": "wtf-Latn-PG", "iana": [ "Watiwa" ], @@ -122820,9 +128583,9 @@ "full": "wti-Latn-ET", "iana": [ "Berta" ], "iso639_3": "wti", - "localnames": [ "Beni-Shangul" ], + "localnames": [ "Ndú Benishangulú" ], "name": "Berta", - "names": [ "Gwami" ], + "names": [ "Benishangul", "Gwami", "Ndú Berthú" ], "region": "ET", "regionname": "Ethiopia", "regions": [ "SD" ], @@ -122984,15 +128747,15 @@ "full": "wun-Latn-TZ", "iana": [ "Bungu" ], "iso639_3": "wun", + "localnames": [ "Chɨwʉngʉ" ], "name": "Bungu", - "names": [ "Echiungu", "Iciwungu", "Kibungu", "Ungu", "Wungu" ], + "names": [ "Chɨwʉngʉ", "Echiungu", "Iciwungu", "Kibungu", "Ungu", "Wungu" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", "sldr": false, "tag": "wun", "tags": [ "wun-Latn", "wun-TZ" ], - "unwritten": true, "windows": "wun-Latn" }, { @@ -123028,8 +128791,9 @@ "full": "wuu-Hans-CN", "iana": [ "Wu Chinese" ], "iso639_3": "wuu", + "macrolang": "zh", "name": "Chinese, Wu", - "names": [ "Ghou Nyu", "Goetian", "Jiangnan", "Jiangsu-Zhujiang", "Jiangzhe", "Jiāngdōng Huà", "Wu", "Wu Chinese", "Wu Gniu", "Wu Nyu", "Wuyue", "Wúyuèyǔ" ], + "names": [ "Ghou Nyu", "Goetian", "Jiangnan", "Jiangsu-Zhejiang", "Jiangzhe", "Jiāngdōng Huà", "Wu", "Wu Chinese", "Wu Gniu", "Wu Nyu", "Wuyue", "Wúyuèyǔ" ], "region": "CN", "regionname": "China", "regions": [ "US" ], @@ -123043,8 +128807,9 @@ "full": "wuu-Hant-CN", "iana": [ "Wu Chinese" ], "iso639_3": "wuu", + "macrolang": "zh", "name": "Chinese, Wu", - "names": [ "Ghou Nyu", "Goetian", "Jiangnan", "Jiangsu-Zhujiang", "Jiangzhe", "Jiāngdōng Huà", "Wu", "Wu Chinese", "Wu Gniu", "Wu Nyu", "Wuyue", "Wúyuèyǔ" ], + "names": [ "Ghou Nyu", "Goetian", "Jiangnan", "Jiangsu-Zhejiang", "Jiangzhe", "Jiāngdōng Huà", "Wu", "Wu Chinese", "Wu Gniu", "Wu Nyu", "Wuyue", "Wúyuèyǔ" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -123059,8 +128824,9 @@ "iana": [ "Wu Chinese" ], "iso639_3": "wuu", "localnames": [ "Wu Nyu" ], + "macrolang": "zh", "name": "Chinese, Wu", - "names": [ "Ghou Nyu", "Goetian", "Jiangnan", "Jiangsu-Zhujiang", "Jiangzhe", "Jiāngdōng Huà", "Wu", "Wu Chinese", "Wu Gniu", "Wu Nyu", "Wuyue", "Wúyuèyǔ" ], + "names": [ "Ghou Nyu", "Goetian", "Jiangnan", "Jiangsu-Zhejiang", "Jiangzhe", "Jiāngdōng Huà", "Wu", "Wu Chinese", "Wu Gniu", "Wu Nyu", "Wuyue", "Wúyuèyǔ" ], "region": "CN", "regionname": "China", "regions": [ "US" ], @@ -123073,8 +128839,9 @@ "full": "wuv-Latn-PG", "iana": [ "Wuvulu-Aua" ], "iso639_3": "wuv", + "localnames": [ "Wuvulu" ], "name": "Wuvulu-Aua", - "names": [ "Aua-Viwulu", "Viwulu-Aua" ], + "names": [ "Aua-Viwulu", "Viwulu-Aua", "Wuvulu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -123215,22 +128982,6 @@ "unwritten": true, "windows": "wxw-Latn" }, - { - "full": "wya-Latn-US", - "iana": [ "Wyandot" ], - "iso639_3": "wya", - "name": "Wyandot", - "names": [ "Wendat", "Wyandotte", "Wyendat" ], - "region": "US", - "regionname": "United States", - "regions": [ "CA" ], - "script": "Latn", - "sldr": false, - "tag": "wya", - "tags": [ "wya-Latn", "wya-US" ], - "unwritten": true, - "windows": "wya-Latn" - }, { "full": "wyb-Latn-AU", "iana": [ "Wangaaybuwan-Ngiyambaa" ], @@ -123275,6 +129026,21 @@ "tags": [ "wym-Latn", "wym-PL" ], "windows": "wym-Latn" }, + { + "full": "wyn-Latn-US", + "iana": [ "Wyandot" ], + "iso639_3": "wyn", + "name": "Wyandot", + "names": [ "Wandat", "Wyandott", "Wyandotte" ], + "region": "US", + "regionname": "United States", + "regions": [ "CA" ], + "script": "Latn", + "sldr": false, + "tag": "wyn", + "tags": [ "wya", "wya-Latn", "wya-Latn-US", "wya-US", "wyn-Latn", "wyn-US" ], + "windows": "wyn-Latn" + }, { "full": "wyr-Latn-BR", "iana": [ "Wayoró" ], @@ -123304,17 +129070,6 @@ "tags": [ "wyy-FJ", "wyy-Latn" ], "windows": "wyy-Latn" }, - { - "full": "x-bad-mru-Cyrl-RU", - "name": "Mono (wrong)", - "region": "RU", - "regionname": "Russian Federation", - "script": "Cyrl", - "sldr": false, - "tag": "x-bad-mru", - "tags": [ "x-bad-mru-Cyrl", "x-bad-mru-RU" ], - "windows": "x-bad-mru-Cyrl" - }, { "full": "xaa-Latn-ES", "iana": [ "Andalusian Arabic" ], @@ -123383,16 +129138,16 @@ "windows": "xae-Zyyy" }, { - "full": "xag-Zyyy-XX", + "full": "xag-Aghb-XX", "iana": [ "Aghwan" ], "iso639_3": "xag", "name": "Aghwan", "region": "XX", - "script": "Zyyy", + "script": "Aghb", "sldr": false, "tag": "xag", - "tags": [ "xag-XX", "xag-Zyyy" ], - "windows": "xag-Zyyy" + "tags": [ "xag-Aghb", "xag-XX" ], + "windows": "xag-Aghb" }, { "full": "xai-Latn-BR", @@ -123560,7 +129315,7 @@ "iana": [ "Kamas" ], "iso639_3": "xas", "name": "Kamas", - "names": [ "Kamassian" ], + "names": [ "Kamassian", "Kaŋmažən šəkət" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", @@ -123659,16 +129414,28 @@ "windows": "xbb-Latn" }, { - "full": "xbc-Zyyy-XX", + "full": "xbc-Grek-XX", "iana": [ "Bactrian" ], "iso639_3": "xbc", "name": "Bactrian", "region": "XX", - "script": "Zyyy", + "script": "Grek", "sldr": false, "tag": "xbc", - "tags": [ "xbc-XX", "xbc-Zyyy" ], - "windows": "xbc-Zyyy" + "tags": [ "xbc-Grek", "xbc-XX" ], + "windows": "xbc-Grek" + }, + { + "full": "xbc-Mani-XX", + "iana": [ "Bactrian" ], + "iso639_3": "xbc", + "name": "Bactrian", + "nophonvars": true, + "region": "XX", + "script": "Mani", + "sldr": false, + "tag": "xbc-Mani", + "windows": "xbc-Mani" }, { "full": "xbd-Latn-AU", @@ -123764,6 +129531,7 @@ "iana": [ "Kenaboi" ], "iso639_3": "xbn", "name": "Kenaboi", + "names": [ "Kĕnaboi" ], "region": "MY", "regionname": "Malaysia", "script": "Latn", @@ -123894,29 +129662,30 @@ "windows": "xcg-Zyyy" }, { - "full": "xch-Zyyy-XX", + "full": "xch-Latn-US", "iana": [ "Chemakum", "Chimakum" ], "iso639_3": "xch", "name": "Chemakum", - "names": [ "Chimakum" ], - "region": "XX", - "script": "Zyyy", + "names": [ "Chimacum", "Chimakum" ], + "region": "US", + "regionname": "United States", + "script": "Latn", "sldr": false, "tag": "xch", - "tags": [ "cmk", "cmk-XX", "cmk-Zyyy", "cmk-Zyyy-XX", "xch-XX", "xch-Zyyy" ], - "windows": "xch-Zyyy" + "tags": [ "cmk", "cmk-Latn", "cmk-Latn-US", "cmk-US", "xch-Latn", "xch-US" ], + "windows": "xch-Latn" }, { - "full": "xcl-Zyyy-XX", + "full": "xcl-Armn-XX", "iana": [ "Classical Armenian" ], "iso639_3": "xcl", "name": "Classical Armenian", "region": "XX", - "script": "Zyyy", + "script": "Armn", "sldr": false, "tag": "xcl", - "tags": [ "xcl-XX", "xcl-Zyyy" ], - "windows": "xcl-Zyyy" + "tags": [ "xcl-Armn", "xcl-XX" ], + "windows": "xcl-Armn" }, { "full": "xcm-Zyyy-XX", @@ -123943,28 +129712,30 @@ "windows": "xcn-Zyyy" }, { - "full": "xco-Arab-XX", + "full": "xco-Chrs-UZ", "iana": [ "Chorasmian" ], "iso639_3": "xco", "name": "Chorasmian", - "region": "XX", - "script": "Arab", + "region": "UZ", + "regionname": "Uzbekistan", + "script": "Chrs", "sldr": false, "tag": "xco", - "tags": [ "xco-Arab", "xco-XX" ], - "windows": "xco-Arab" + "tags": [ "xco-Chrs", "xco-UZ" ], + "windows": "xco-Chrs-UZ" }, { - "full": "xco-Chrs-XX", + "full": "xco-Arab-XX", "iana": [ "Chorasmian" ], "iso639_3": "xco", "name": "Chorasmian", "nophonvars": true, "region": "XX", - "script": "Chrs", + "script": "Arab", "sldr": false, - "tag": "xco-Chrs", - "windows": "xco-Chrs" + "tag": "xco-Arab", + "tags": [ "xco-XX" ], + "windows": "xco-Arab-XX" }, { "full": "xcr-Cari-TR", @@ -124055,16 +129826,28 @@ "windows": "xda-Latn" }, { - "full": "xdc-Zyyy-XX", + "full": "xdc-Latn-XX", "iana": [ "Dacian" ], "iso639_3": "xdc", "name": "Dacian", "region": "XX", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xdc", - "tags": [ "xdc-XX", "xdc-Zyyy" ], - "windows": "xdc-Zyyy" + "tags": [ "xdc-Latn", "xdc-XX" ], + "windows": "xdc-Latn" + }, + { + "full": "xdc-Grek-XX", + "iana": [ "Dacian" ], + "iso639_3": "xdc", + "name": "Dacian", + "nophonvars": true, + "region": "XX", + "script": "Grek", + "sldr": false, + "tag": "xdc-Grek", + "windows": "xdc-Grek" }, { "full": "xdk-Latn-AU", @@ -124108,19 +129891,34 @@ "tags": [ "xdo-AO", "xdo-Latn" ], "windows": "xdo-Latn" }, + { + "full": "xdq-Cyrl-RU", + "iana": [ "Kaitag" ], + "iso639_3": "xdq", + "latnnames": [ "Xajdaqan" ], + "localnames": [ "Хайдакьан" ], + "name": "Kaitag", + "names": [ "Kaitagskiy", "Kaitagtsy", "Kaitak", "Kajtak", "Kaytak", "Khaydak", "Xajdak" ], + "region": "RU", + "regionname": "Russian Federation", + "script": "Cyrl", + "sldr": false, + "tag": "xdq", + "tags": [ "xdq-Cyrl", "xdq-RU" ], + "windows": "xdq-Cyrl" + }, { "full": "xdy-Latn-ID", "iana": [ "Malayic Dayak" ], "iso639_3": "xdy", "name": "Malayic Dayak", - "names": [ "Bamayo", "Bumayoh" ], + "names": [ "Bamayo", "Bumayoh", "Malayik Dayak", "Maleyik Dayak" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", "sldr": false, "tag": "xdy", "tags": [ "xdy-ID", "xdy-Latn" ], - "unwritten": true, "windows": "xdy-Latn" }, { @@ -124186,8 +129984,8 @@ "full": "xem-Latn-ID", "iana": [ "Kembayan" ], "iso639_3": "xem", - "name": "Kembayan", - "names": [ "Karambai" ], + "name": "Mateq", + "names": [ "Bu Mateq", "Kembayan" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -124228,8 +130026,9 @@ "full": "xes-Latn-PG", "iana": [ "Kesawai" ], "iso639_3": "xes", - "name": "Kesawai", - "names": [ "Kesawi", "Koromu", "Namuya" ], + "name": "Koromu", + "names": [ "Kesawai", "Kesawi", "Koromu sakine", "Namuya" ], + "obsolete": true, "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -124297,6 +130096,7 @@ "iana": [ "Gbin" ], "iso639_3": "xgb", "name": "Gbin", + "names": [ "Binfo", "Bïn", "Gbin-Gouro", "Gurombo", "Gurungo" ], "region": "CI", "regionname": "Côte d'Ivoire", "script": "Latn", @@ -124436,13 +130236,13 @@ "full": "xh-Latn-ZA", "iana": [ "Xhosa" ], "iso639_3": "xho", - "localname": "isiXhosa", + "localname": "IsiXhosa", "localnames": [ "isiXhosa" ], "name": "Xhosa", "names": [ "Isixhosa", "Koosa", "Xosa", "isiXhosa" ], "region": "ZA", "regionname": "South Africa", - "regions": [ "BW", "LS" ], + "regions": [ "BW", "LS", "ZW" ], "script": "Latn", "sldr": true, "suppress": true, @@ -124459,7 +130259,7 @@ "nophonvars": true, "region": "ZA", "regionname": "South Africa", - "regions": [ "BW", "LS" ], + "regions": [ "BW", "LS", "ZW" ], "script": "Brai", "sldr": false, "tag": "xh-Brai", @@ -124490,21 +130290,22 @@ "windows": "xhc-Zyyy" }, { - "full": "xhd-Zyyy-XX", + "full": "xhd-Sarb-XX", "iana": [ "Hadrami" ], "iso639_3": "xhd", "name": "Hadrami", "region": "XX", - "script": "Zyyy", + "script": "Sarb", "sldr": false, "tag": "xhd", - "tags": [ "xhd-XX", "xhd-Zyyy" ], - "windows": "xhd-Zyyy" + "tags": [ "xhd-Sarb", "xhd-XX" ], + "windows": "xhd-Sarb" }, { "full": "xhe-Arab-PK", "iana": [ "Khetrani" ], "iso639_3": "xhe", + "macrolang": "lah", "name": "Khetrani", "region": "PK", "regionname": "Pakistan", @@ -124516,16 +130317,29 @@ "windows": "xhe-Arab" }, { - "full": "xhr-Zyyy-XX", + "full": "xhm-Khmr-KH", + "iana": [ "Middle Khmer (1400 to 1850 CE)" ], + "iso639_3": "xhm", + "name": "Middle Khmer (1400 to 1850 CE)", + "region": "KH", + "regionname": "Cambodia", + "script": "Khmr", + "sldr": false, + "tag": "xhm", + "tags": [ "xhm-KH", "xhm-Khmr" ], + "windows": "xhm-Khmr" + }, + { + "full": "xhr-Ital-XX", "iana": [ "Hernican" ], "iso639_3": "xhr", "name": "Hernican", "region": "XX", - "script": "Zyyy", + "script": "Ital", "sldr": false, "tag": "xhr", - "tags": [ "xhr-XX", "xhr-Zyyy" ], - "windows": "xhr-Zyyy" + "tags": [ "xhr-Ital", "xhr-XX" ], + "windows": "xhr-Ital" }, { "full": "xht-Zyyy-TR", @@ -124628,13 +130442,13 @@ "iso639_3": "xir", "name": "Xiriâna", "names": [ "Bahuana", "Bahwana", "Barauána", "Chiriana", "Chiriána", "Shiriana", "Shriana", "Xiriana" ], + "obsolete": true, "region": "BR", "regionname": "Brazil", "script": "Latn", "sldr": false, "tag": "xir", "tags": [ "xir-BR", "xir-Latn" ], - "unwritten": true, "windows": "xir-Latn" }, { @@ -124642,7 +130456,7 @@ "iana": [ "Kisan" ], "iso639_3": "xis", "name": "Kisan", - "names": [ "Birhor", "Koda", "Kola", "Kora", "Kuda", "Kunha", "Kunhar", "Kunna", "Kunrukh", "Kunuk", "Mirdha", "Morva", "Nagesia", "Nageswar" ], + "names": [ "Birhor", "Koda", "Kola", "Kora", "Kuda", "Kunha", "Kunhar", "Kunhu", "Kunna", "Kunrukh", "Kunuk", "Mirdha", "Morva", "Nagesia", "Nageswar" ], "region": "IN", "regionname": "India", "script": "Orya", @@ -124683,7 +130497,7 @@ "iana": [ "Minjungbal" ], "iso639_3": "xjb", "name": "Minjungbal", - "names": [ "Cood-jingburra", "Cudgingberry", "Gan-dowal", "Gando Minjang", "Gendo", "Minjangbal", "Minyowa", "Minyung", "Ngandowul" ], + "names": [ "Cood-jingburra", "Cudgingberry", "Gan-dowal", "Gando Minjang", "Gendo", "Minjangbal", "Minyowa", "Minyung", "Ngandowul", "Tweed-Albert", "Yugambeh" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -124739,18 +130553,18 @@ "windows": "xkb-Latn" }, { - "full": "xkc-Zyyy-IR", + "full": "xkc-Arab-IR", "iana": [ "Kho'ini" ], "iso639_3": "xkc", "name": "Kho’ini", "names": [ "Kho'ini" ], "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "xkc", - "tags": [ "xkc-IR", "xkc-Zyyy" ], - "windows": "xkc-Zyyy" + "tags": [ "xkc-Arab", "xkc-IR" ], + "windows": "xkc-Arab" }, { "full": "xkd-Latn-ID", @@ -124782,18 +130596,17 @@ "windows": "xke-Latn" }, { - "full": "xkf-Tibt-BT-x-uchen", + "full": "xkf-Tibt-BT", "iana": [ "Khengkha" ], "iso639_3": "xkf", "name": "Khengkha", "names": [ "Ken", "Keng", "Kenkha", "Khen", "Khenkha", "Kyengkha" ], - "nophonvars": true, "region": "BT", "regionname": "Bhutan", "script": "Tibt", "sldr": false, - "tag": "xkf-Tibt", - "tags": [ "xkf-BT-x-uchen", "xkf-Tibt-BT", "xkf-Tibt-x-uchen" ], + "tag": "xkf", + "tags": [ "xkf-BT", "xkf-Tibt" ], "windows": "xkf-Tibt" }, { @@ -124840,10 +130653,10 @@ }, { "full": "xkk-Zyyy-KH", - "iana": [ "Kaco'" ], + "iana": [ "Kachok", "Kaco'" ], "iso639_3": "xkk", - "name": "Kaco’", - "names": [ "Kachah’", "Kachok", "Kaco'" ], + "name": "Kachok", + "names": [ "Kachah’", "Kaco'", "Kaco’" ], "region": "KH", "regionname": "Cambodia", "script": "Zyyy", @@ -124899,17 +130712,17 @@ "windows": "xko-Zyyy" }, { - "full": "xkp-Zyyy-IR", + "full": "xkp-Arab-IR", "iana": [ "Kabatei" ], "iso639_3": "xkp", "name": "Kabatei", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "xkp", - "tags": [ "xkp-IR", "xkp-Zyyy" ], - "windows": "xkp-Zyyy" + "tags": [ "xkp-Arab", "xkp-IR" ], + "windows": "xkp-Arab" }, { "full": "xkq-Latn-ID", @@ -125049,6 +130862,7 @@ "full": "xkz-Latn-BT", "iana": [ "Kurtokha" ], "iso639_3": "xkz", + "localnames": [ "Kurtöp" ], "name": "Kurtokha", "names": [ "Gurtü", "Kurteopkha", "Kurthopkha", "Kurtobikha", "Kurtopakha", "Kurtotpikha", "Kurtöp", "Kürthöpka" ], "region": "BT", @@ -125160,6 +130974,7 @@ "sldr": false, "tag": "xli", "tags": [ "xli-XX", "xli-Zyyy" ], + "unwritten": true, "windows": "xli-Zyyy" }, { @@ -125199,16 +131014,16 @@ "windows": "xlp-Zyyy" }, { - "full": "xls-Zyyy-XX", + "full": "xls-Latn-XX", "iana": [ "Lusitanian" ], "iso639_3": "xls", "name": "Lusitanian", "region": "XX", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xls", - "tags": [ "xls-XX", "xls-Zyyy" ], - "windows": "xls-Zyyy" + "tags": [ "xls-Latn", "xls-XX" ], + "windows": "xls-Latn" }, { "full": "xlu-Zyyy-XX", @@ -125236,18 +131051,18 @@ "windows": "xly-Elym" }, { - "full": "xma-Zyyy-SO", + "full": "xma-Latn-SO", "iana": [ "Mushungulu" ], "iso639_3": "xma", "name": "Mushungulu", - "names": [ "Kimushungulu", "Mushunguli" ], + "names": [ "Chizigula", "Kimushungulu", "Mushunguli" ], "region": "SO", "regionname": "Somalia", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xma", - "tags": [ "xma-SO", "xma-Zyyy" ], - "windows": "xma-Zyyy" + "tags": [ "xma-Latn", "xma-SO" ], + "windows": "xma-Latn" }, { "full": "xmb-Latn-CM", @@ -125311,7 +131126,7 @@ "iana": [ "Mingrelian" ], "iso639_3": "xmf", "name": "Mingrelian", - "names": [ "Margali", "Margaluri", "Megrel", "Megrelian", "Megruli" ], + "names": [ "Iverian", "Margalepi", "Margali", "Margaluri", "Margaluri nina", "Megrel", "Megrelian", "Megruli" ], "region": "GE", "regionname": "Georgia", "regions": [ "RU" ], @@ -125326,7 +131141,7 @@ "iana": [ "Mengaka" ], "iso639_3": "xmg", "name": "Mengaka", - "names": [ "Bamileke-Mengaka", "Benzing", "Eghap", "Ghap", "Megaka" ], + "names": [ "Bamileke-Mengaka", "Eghap", "Megaka", "Məgaka" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -125340,7 +131155,7 @@ "iana": [ "Mengaka" ], "iso639_3": "xmg", "name": "Mengaka", - "names": [ "Bamileke-Mengaka", "Benzing", "Eghap", "Ghap", "Megaka" ], + "names": [ "Bamileke-Mengaka", "Eghap", "Megaka", "Məgaka" ], "nophonvars": true, "region": "CM", "regionname": "Cameroon", @@ -125412,6 +131227,7 @@ "iana": [ "Manado Malay" ], "iso639_3": "xmm", "localnames": [ "Bahasa Manado" ], + "macrolang": "ms", "name": "Malay, Manado", "names": [ "Bahasa Manado", "Manado Malay", "Manadonese", "Manadonese Malay", "Minahasan Malay" ], "region": "ID", @@ -125541,6 +131357,7 @@ "iana": [ "Antankarana Malagasy", "Tankarana Malagasy" ], "iso639_3": "xmv", "localnames": [ "Antakarana" ], + "macrolang": "mg", "name": "Malagasy, Antankarana", "names": [ "Antakarana", "Antakarana Malagasy", "Antankarana Malagasy", "Antekarana", "Antekarana Malagasy", "Tankarana", "Tankarana Malagasy" ], "region": "MG", @@ -125556,6 +131373,7 @@ "iana": [ "Tsimihety Malagasy" ], "iso639_3": "xmw", "localnames": [ "Tsimihety Malagasy" ], + "macrolang": "mg", "name": "Malagasy, Tsimihety", "names": [ "Tsimihety", "Tsimihety Malagasy" ], "region": "MG", @@ -125568,10 +131386,10 @@ }, { "full": "xmx-Latn-ID", - "iana": [ "Maden" ], + "iana": [ "Salawati", "Maden" ], "iso639_3": "xmx", - "name": "Maden", - "names": [ "Palamul", "Saparan", "Sapran" ], + "name": "Salawati", + "names": [ "Maden", "Palamul", "Saparan", "Sapran" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -125624,18 +131442,18 @@ "windows": "xna-Narb" }, { - "full": "xnb-Zyyy-TW", + "full": "xnb-Latn-TW", "iana": [ "Kanakanabu" ], "iso639_3": "xnb", "name": "Kanakanabu", - "names": [ "Kanabu", "Kanakanavu" ], + "names": [ "Kanabu", "Kanakana Fu", "Kanakanavu" ], "region": "TW", "regionname": "China-Taiwan", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xnb", - "tags": [ "xnb-TW", "xnb-Zyyy" ], - "windows": "xnb-Zyyy" + "tags": [ "xnb-Latn", "xnb-TW" ], + "windows": "xnb-Latn" }, { "full": "xng-Zyyy-XX", @@ -125679,6 +131497,22 @@ "unwritten": true, "windows": "xni-Latn" }, + { + "full": "xnj-Latn-TZ", + "iana": [ "Ngoni (Tanzania)" ], + "iso639_3": "xnj", + "localnames": [ "Chingoni" ], + "name": "Chingoni", + "names": [ "Angoni", "Kingoni", "Kisutu", "Ngoni", "Ngoni (Tanzania)", "Sutu", "Tanzanian Ngoni" ], + "region": "TZ", + "regionname": "United Republic of Tanzania", + "regions": [ "MZ" ], + "script": "Latn", + "sldr": false, + "tag": "xnj", + "tags": [ "ngo", "ngo-Latn", "ngo-Latn-TZ", "ngo-TZ", "xnj-Latn", "xnj-TZ" ], + "windows": "xnj-Latn" + }, { "full": "xnk-Latn-AU", "iana": [ "Nganakarti" ], @@ -125696,6 +131530,7 @@ }, { "full": "xnm-Latn-AU", + "iana": [ "Ngumbarl" ], "iso639_3": "xnm", "name": "Ngumbarl", "names": [ "Ngombal" ], @@ -125713,7 +131548,7 @@ "iso639_3": "xnn", "localname": "Kankanay, Northern", "name": "Kankanay, Northern", - "names": [ "Kataugnan", "Northern Kankanay", "Sagada Igorot", "Western Bontoc" ], + "names": [ "Kataugnan", "Northern Kankanaey", "Northern Kankanay", "Sagada Igorot", "Western Bontoc" ], "region": "PH", "regionname": "Philippines", "script": "Latn", @@ -125734,10 +131569,26 @@ "tags": [ "xno-XX", "xno-Zyyy" ], "windows": "xno-Zyyy" }, + { + "full": "xnq-Latn-MZ", + "iana": [ "Ngoni (Mozambique)" ], + "iso639_3": "xnq", + "localnames": [ "Xingoni" ], + "name": "Xingoni", + "names": [ "Mozambican Ngoni", "Ngoni", "Ngoni (Mozambique)" ], + "region": "MZ", + "regionname": "Mozambique", + "script": "Latn", + "sldr": false, + "tag": "xnq", + "tags": [ "xnq-Latn", "xnq-MZ" ], + "windows": "xnq-Latn" + }, { "full": "xnr-Deva-IN", "iana": [ "Kangri" ], "iso639_3": "xnr", + "macrolang": "doi", "name": "Kangri", "names": [ "Kangra-Dogri", "Pahari", "Pahari Kangri" ], "region": "IN", @@ -125748,6 +131599,22 @@ "tags": [ "xnr-Deva", "xnr-IN" ], "windows": "xnr-Deva" }, + { + "full": "xnr-Takr-IN", + "iana": [ "Kangri" ], + "iso639_3": "xnr", + "macrolang": "doi", + "name": "Kangri", + "names": [ "Kangra-Dogri", "Pahari", "Pahari Kangri" ], + "nophonvars": true, + "obsolete": true, + "region": "IN", + "regionname": "India", + "script": "Takr", + "sldr": false, + "tag": "xnr-Takr", + "windows": "xnr-Takr" + }, { "full": "xns-Zyyy-IN", "iana": [ "Kanashi" ], @@ -125764,18 +131631,17 @@ "windows": "xns-Zyyy" }, { - "full": "xnt-Zyyy-US", + "full": "xnt-Latn-US", "iana": [ "Narragansett" ], "iso639_3": "xnt", "name": "Narragansett", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xnt", - "tags": [ "xnt-US", "xnt-Zyyy" ], - "unwritten": true, - "windows": "xnt-Zyyy" + "tags": [ "xnt-Latn", "xnt-US" ], + "windows": "xnt-Latn" }, { "full": "xnu-Latn-AU", @@ -125807,6 +131673,21 @@ "unwritten": true, "windows": "xny-Latn" }, + { + "full": "xnz-Latn-EG", + "iana": [ "Kenzi", "Mattoki" ], + "iso639_3": "xnz", + "localnames": [ "Mattokki" ], + "name": "Mattokki", + "names": [ "Kenuz", "Kenuzi", "Kenzi", "Kunuz", "Kunuz Nubian", "Kunuzi", "Mattoki" ], + "region": "EG", + "regionname": "Egypt", + "script": "Latn", + "sldr": false, + "tag": "xnz", + "tags": [ "xnz-EG", "xnz-Latn" ], + "windows": "xnz-Latn" + }, { "full": "xnz-Arab-EG", "iana": [ "Kenzi", "Mattoki" ], @@ -125831,31 +131712,17 @@ "region": "EG", "regionname": "Egypt", "script": "Copt", - "sldr": false, + "sldr": true, "tag": "xnz-Copt", "tags": [ "xnz-Copt-EG", "xnz-Copt-x-olnubian", "xnz-EG-x-olnubian" ], - "windows": "xnz-Copt" - }, - { - "full": "xnz-Latn-EG", - "iana": [ "Kenzi", "Mattoki" ], - "iso639_3": "xnz", - "localnames": [ "Mattokki" ], - "name": "Mattokki", - "names": [ "Kenuz", "Kenuzi", "Kenzi", "Kunuz", "Kunuz Nubian", "Kunuzi", "Mattoki" ], - "region": "EG", - "regionname": "Egypt", - "script": "Latn", - "sldr": false, - "tag": "xnz-Latn", - "windows": "xnz-Latn" + "windows": "xnz-Copt-x-olnubian" }, { "full": "xoc-Latn-NG", "iana": [ "O'chi'chi'" ], "iso639_3": "xoc", "name": "O’chi’chi’", - "names": [ "O'chi'chi'" ], + "names": [ "O'chi'chi'", "Ọchịchị" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -125901,14 +131768,13 @@ "iana": [ "Kominimung" ], "iso639_3": "xoi", "name": "Kominimung", - "names": [ "Konminimung", "Kuminimu", "Okmaŋɨheŋgi" ], + "names": [ "Iski", "Konminimung", "Kuminimu", "Okmaŋɨheŋgi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", "sldr": false, "tag": "xoi", "tags": [ "xoi-Latn", "xoi-PG" ], - "unwritten": true, "windows": "xoi-Latn" }, { @@ -125939,7 +131805,7 @@ "sldr": false, "tag": "xom", "tags": [ "xom-Latn", "xom-SD" ], - "windows": "xom-Latn" + "windows": "xom-Latn-SD" }, { "full": "xom-Ethi-ET", @@ -125954,7 +131820,7 @@ "sldr": false, "tag": "xom-Ethi", "tags": [ "xom-ET" ], - "windows": "xom-Ethi" + "windows": "xom-Ethi-ET" }, { "full": "xon-Latn-GH", @@ -125963,7 +131829,7 @@ "localname": "Likpakpaanl", "localnames": [ "Likpakpaanl" ], "name": "Konkomba", - "names": [ "Bikpakpam", "Kpankpamba", "Likpakpaanl" ], + "names": [ "Bikpakpam", "Kpankpamba", "Likpakpaaln", "Likpakpaanl", "Likpakpaln" ], "region": "GH", "regionname": "Ghana", "regions": [ "TG" ], @@ -125978,7 +131844,7 @@ "iana": [ "Konkomba" ], "iso639_3": "xon", "name": "Konkomba", - "names": [ "Bikpakpam", "Kpankpamba", "Likpakpaanl" ], + "names": [ "Bikpakpam", "Kpankpamba", "Likpakpaaln", "Likpakpaanl", "Likpakpaln" ], "nophonvars": true, "region": "GH", "regionname": "Ghana", @@ -126022,7 +131888,7 @@ "iana": [ "Korubo" ], "iso639_3": "xor", "name": "Korubo", - "names": [ "Caceteiros" ], + "names": [ "Caceteiro", "Caceteiros", "Corubo", "Kaniwa", "Maya" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -126063,8 +131929,10 @@ }, { "full": "xpb-Latn-AU", + "iana": [ "Northeastern Tasmanian", "Pyemmairrener" ], "iso639_3": "xpb", - "name": "Northeastern Tasmanian", + "name": "Tasmanian, Northeastern", + "names": [ "Northeastern Tasmanian", "Pyemmairrener" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126087,8 +131955,10 @@ }, { "full": "xpd-Latn-AU", + "iana": [ "Oyster Bay Tasmanian" ], "iso639_3": "xpd", - "name": "Oyster Bay Tasmanian", + "name": "Tasmanian, Oyster Bay", + "names": [ "Lairmairrener", "Oyster Bay Tasmanian", "Paredarerme" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126099,8 +131969,10 @@ }, { "full": "xpf-Latn-AU", + "iana": [ "Southeast Tasmanian", "Nuenonne" ], "iso639_3": "xpf", - "name": "Southeast Tasmanian", + "name": "Tasmanian, Southeast", + "names": [ "Nuenonne", "Southeast Tasmanian" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126124,8 +131996,10 @@ }, { "full": "xph-Latn-AU", + "iana": [ "North Midlands Tasmanian", "Tyerrenoterpanner" ], "iso639_3": "xph", - "name": "North Midlands Tasmanian", + "name": "Tasmanian, North Midlands", + "names": [ "North Midland Tasmanian", "North Midlands Tasmanian", "Tyerrenoterpanner", "Tyerrernotepanner" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126135,17 +132009,17 @@ "windows": "xph-Latn" }, { - "full": "xpi-Zyyy-GB", + "full": "xpi-Ogam-GB", "iana": [ "Pictish" ], "iso639_3": "xpi", "name": "Pictish", "region": "GB", "regionname": "United Kingdom", - "script": "Zyyy", + "script": "Ogam", "sldr": false, "tag": "xpi", - "tags": [ "xpi-GB", "xpi-Zyyy" ], - "windows": "xpi-Zyyy" + "tags": [ "xpi-GB", "xpi-Ogam" ], + "windows": "xpi-Ogam" }, { "full": "xpj-Latn-AU", @@ -126179,8 +132053,10 @@ }, { "full": "xpl-Latn-AU", + "iana": [ "Port Sorell Tasmanian" ], "iso639_3": "xpl", - "name": "Port Sorell Tasmanian", + "name": "Tasmanian, Port Sorell", + "names": [ "Port Sorell Tasmanian", "Tommeginne" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126247,18 +132123,17 @@ "windows": "xpp-Zyyy" }, { - "full": "xpq-Zyyy-US", + "full": "xpq-Latn-US", "iana": [ "Mohegan-Pequot" ], "iso639_3": "xpq", "name": "Mohegan-Pequot", "region": "US", "regionname": "United States", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xpq", - "tags": [ "xpq-US", "xpq-Zyyy" ], - "unwritten": true, - "windows": "xpq-Zyyy" + "tags": [ "xpq-Latn", "xpq-US" ], + "windows": "xpq-Latn" }, { "full": "xpr-Prti-IR", @@ -126302,23 +132177,25 @@ "windows": "xpt-Latn" }, { - "full": "xpu-Zyyy-XX", + "full": "xpu-Phnx-XX", "iana": [ "Punic" ], "iso639_3": "xpu", "name": "Punic", "region": "XX", "regions": [ "DZ", "ES", "IT", "LY", "MA", "MT", "TN" ], - "script": "Zyyy", + "script": "Phnx", "sldr": false, "tag": "xpu", - "tags": [ "xpu-XX", "xpu-Zyyy" ], + "tags": [ "xpu-Phnx", "xpu-XX" ], "unwritten": true, - "windows": "xpu-Zyyy" + "windows": "xpu-Phnx" }, { "full": "xpv-Latn-AU", + "iana": [ "Northern Tasmanian", "Tommeginne" ], "iso639_3": "xpv", - "name": "Northern Tasmanian", + "name": "Tasmanian, Northern", + "names": [ "Northern Tasmanian", "Tommeginne" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126329,8 +132206,10 @@ }, { "full": "xpw-Latn-AU", + "iana": [ "Northwestern Tasmanian", "Peerapper" ], "iso639_3": "xpw", - "name": "Northwestern Tasmanian", + "name": "Tasmanian, Northwestern", + "names": [ "Northwestern Tasmanian", "Peerapper" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126341,8 +132220,10 @@ }, { "full": "xpx-Latn-AU", + "iana": [ "Southwestern Tasmanian", "Toogee" ], "iso639_3": "xpx", - "name": "Southwestern Tasmanian", + "name": "Tasmanian, Southwestern", + "names": [ "Southwestern Tasmanian", "Toogee" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126366,8 +132247,10 @@ }, { "full": "xpz-Latn-AU", + "iana": [ "Bruny Island Tasmanian" ], "iso639_3": "xpz", - "name": "Bruny Island Tasmanian", + "name": "Tasmanian, Bruny Island", + "names": [ "Bruny Island Tasmanian", "Nuenonne" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -126490,7 +132373,6 @@ "sldr": false, "tag": "xri", "tags": [ "xri-BR", "xri-Latn" ], - "unwritten": true, "windows": "xri-Latn" }, { @@ -126523,24 +132405,25 @@ "windows": "xrn-Cyrl" }, { - "full": "xrr-Zyyy-IT", + "full": "xrr-Latn-IT", "iana": [ "Raetic" ], "iso639_3": "xrr", "name": "Raetic", "region": "IT", "regionname": "Italy", "regions": [ "AT", "CH" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "xrr", - "tags": [ "xrr-IT", "xrr-Zyyy" ], - "windows": "xrr-Zyyy" + "tags": [ "xrr-IT", "xrr-Latn" ], + "windows": "xrr-Latn" }, { "full": "xrt-Zyyy-US", "iana": [ "Aranama-Tamique" ], "iso639_3": "xrt", "name": "Aranama-Tamique", + "names": [ "Aranama" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -126615,7 +132498,6 @@ "iso639_3": "xsc", "name": "Scythian", "region": "XX", - "regions": [ "IR" ], "script": "Zyyy", "sldr": false, "tag": "xsc", @@ -126686,9 +132568,9 @@ "iana": [ "Kasem" ], "iso639_3": "xsm", "localname": "Kasem", - "localnames": [ "Kasem" ], + "localnames": [ "Kasɩm" ], "name": "Kasem", - "names": [ "Kasena", "Kasim", "Kassem", "Kassena", "Kassene" ], + "names": [ "Kasena", "Kasim", "Kassem", "Kassena", "Kassene", "Kasɩm" ], "region": "GH", "regionname": "Ghana", "regions": [ "BF" ], @@ -126703,14 +132585,13 @@ "iana": [ "Sanga (Nigeria)" ], "iso639_3": "xsn", "name": "Sanga", - "names": [ "Asanga", "Isanga", "Sanga (Nigeria)" ], + "names": [ "Asanga", "Aŋma Asanga", "Isanga", "Sanga (Nigeria)" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", "sldr": false, "tag": "xsn", "tags": [ "xsn-Latn", "xsn-NG" ], - "unwritten": true, "windows": "xsn-Latn" }, { @@ -126772,7 +132653,7 @@ "sldr": true, "tag": "xsr", "tags": [ "xsr-Deva", "xsr-NP" ], - "windows": "xsr-Deva" + "windows": "xsr-Deva-NP" }, { "full": "xsr-Tibt-CN", @@ -126788,21 +132669,7 @@ "sldr": false, "tag": "xsr-Tibt", "tags": [ "xsr-CN" ], - "windows": "xsr-Tibt" - }, - { - "full": "xss-Cyrl-RU", - "iana": [ "Assan" ], - "iso639_3": "xss", - "name": "Assan", - "region": "RU", - "regionname": "Russian Federation", - "script": "Cyrl", - "sldr": false, - "tag": "xss", - "tags": [ "xss-Cyrl", "xss-RU" ], - "unwritten": true, - "windows": "xss-Cyrl" + "windows": "xsr-Tibt-CN" }, { "full": "xsu-Latn-VE", @@ -126810,12 +132677,12 @@ "iso639_3": "xsu", "localnames": [ "Sanöma" ], "name": "Sanumá", - "names": [ "Caura", "Chirichano", "Guaika", "Samatali", "Samatari", "Sanema", "Sanima", "Sanma", "Sanïma", "Sanöma", "Sanɨma", "Sánuma", "Sánïma", "Tsanuma", "Tsanɨma", "Xamatari" ], + "names": [ "Caura", "Chirichano", "Guaika", "Samatali", "Samatari", "Sanema", "Sanima", "Sanma", "Sanïma", "Sanöma", "Sanɨma", "Sánïma", "Tsanuma", "Tsanɨma", "Xamatari" ], "region": "VE", "regionname": "Venezuela", "regions": [ "BR" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "xsu", "tags": [ "xsu-Latn", "xsu-VE" ], "windows": "xsu-Latn" @@ -126825,6 +132692,7 @@ "iana": [ "Sudovian" ], "iso639_3": "xsv", "name": "Sudovian", + "names": [ "Jatvingian", "Yatvingian", "Yotvingian" ], "region": "XX", "regions": [ "BY", "LT", "PL" ], "script": "Zyyy", @@ -126839,7 +132707,7 @@ "iso639_3": "xsy", "localnames": [ "SaySiyat" ], "name": "Saisiyat", - "names": [ "Amutoura", "Bouiok", "Saiset", "Saisett", "Saisiat", "Saisiett", "Saisirat", "Saisyet", "Saisyett", "SaySiyat", "Seisirat" ], + "names": [ "Amutoura", "Bouiok", "Saiset", "Saisett", "Saisiat", "Saisiett", "Saisirat", "Saisyet", "Saisyett", "Saixia", "SaySiyat", "Seisirat" ], "region": "TW", "regionname": "China-Taiwan", "script": "Latn", @@ -126852,8 +132720,9 @@ "full": "xta-Latn-MX", "iana": [ "Alcozauca Mixtec" ], "iso639_3": "xta", + "localnames": [ "Tu̱'un sâvi" ], "name": "Mixtec, Alcozauca", - "names": [ "Alcozauca Mixtec", "Mixteco de Alocozauca", "Mixteco de Xochapa" ], + "names": [ "Alcozauca Mixtec", "Mixteco de Alocozauca", "Mixteco de Xochapa", "Tu̱’un sâvi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -126885,7 +132754,7 @@ "region": "SD", "regionname": "Sudan", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "xtc", "tags": [ "xtc-Latn", "xtc-SD" ], "windows": "xtc-Latn" @@ -126894,10 +132763,10 @@ "full": "xtd-Latn-MX", "iana": [ "Diuxi-Tilantongo Mixtec" ], "iso639_3": "xtd", - "localname": "Tnu'un dau", - "localnames": [ "Tnu'un dau" ], + "localname": "Tnuꞌun dau", + "localnames": [ "Tnuꞌu ñudau" ], "name": "Mixtec, Diuxi-Tilantongo", - "names": [ "Central Nochistlán Mixtec", "Diuxi-Tilantongo Mixtec", "Mixteco de Diuxi-Tilantongo", "Mixteco del Este Central", "Tnu’un dau" ], + "names": [ "Central Nochistlán Mixtec", "Diuxi-Tilantongo Mixtec", "Mixteco de Diuxi-Tilantongo", "Mixteco de San Juan Diuxi y Santiago Tilantongo", "Mixteco del Este Central", "Tnu’un dau", "Tnuꞌu ñudau" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -126953,9 +132822,9 @@ "full": "xti-Latn-MX", "iana": [ "Sinicahua Mixtec" ], "iso639_3": "xti", - "localnames": [ "Tu'un savi" ], + "localnames": [ "Tnu'un savi" ], "name": "Mixtec, Sinicahua", - "names": [ "Mixteco de San Antonio Sinicahua", "Sinicahua Mixtec", "Tu’un savi" ], + "names": [ "Mixteco de San Antonio Sinicahua", "Sinicahua Mixtec", "Tnu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -126983,7 +132852,6 @@ "full": "xtl-Latn-MX", "iana": [ "Tijaltepec Mixtec" ], "iso639_3": "xtl", - "localnames": [ "Tu'un savi" ], "name": "Mixtec, Tijaltepec", "names": [ "Mixteco de San Pablo Tijaltepec", "Mixteco de Santa María Yosoyúa", "Tijaltepec Mixtec", "Tu’un savi" ], "region": "MX", @@ -127001,7 +132869,7 @@ "localname": "Sa'an Ñuu Savi", "localnames": [ "Sa'an Ñuu Savi" ], "name": "Mixtec, Magdalena Peñasco", - "names": [ "Magdalena Peñasco Mixtec", "Peñasco Mixtec", "Sa’an Ñuu Savi" ], + "names": [ "Magdalena Peñasco Mixtec", "Mixteco Central", "Peñasco Mixtec", "Sa’an Ñuu Savi", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -127014,10 +132882,10 @@ "full": "xtn-Latn-MX", "iana": [ "Northern Tlaxiaco Mixtec" ], "iso639_3": "xtn", - "localname": "Mixtec, Northern Tlaxiaco", + "localname": "Saꞌan savi", "localnames": [ "Sa'an savi" ], "name": "Mixtec, Northern Tlaxiaco", - "names": [ "Mixteco de San Juan Ñumí", "Mixteco del Norte de Tlaxiaco", "Northern Tlaxiaco Mixtec", "Sa’an nda’u", "Sa’an savi", "Ñumí Mixtec" ], + "names": [ "Mixteco de San Juan Ñumí", "Mixteco de Ñumi", "Mixteco del Norte de Tlaxiaco", "Northern Tlaxiaco Mixtec", "Sa’an nda’u", "Sa’an sau", "Sa’an savi", "Ñumí Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -127055,17 +132923,30 @@ "windows": "xtp-Latn" }, { - "full": "xtq-Zyyy-IR", + "full": "xtq-Brah-IR", "iana": [ "Tumshuqese" ], "iso639_3": "xtq", "name": "Tumshuqese", "region": "IR", "regionname": "Islamic Republic of Iran", - "script": "Zyyy", + "script": "Brah", "sldr": false, "tag": "xtq", - "tags": [ "xtq-IR", "xtq-Zyyy" ], - "windows": "xtq-Zyyy" + "tags": [ "xtq-Brah", "xtq-IR" ], + "windows": "xtq-Brah" + }, + { + "full": "xtq-Khar-IR", + "iana": [ "Tumshuqese" ], + "iso639_3": "xtq", + "name": "Tumshuqese", + "nophonvars": true, + "region": "IR", + "regionname": "Islamic Republic of Iran", + "script": "Khar", + "sldr": false, + "tag": "xtq-Khar", + "windows": "xtq-Khar" }, { "full": "xtr-Zyyy-IN", @@ -127100,7 +132981,7 @@ "iana": [ "Tacahua Mixtec" ], "iso639_3": "xtt", "name": "Mixtec, Tacahua", - "names": [ "Mixteco de Santa Cruz Tacahua", "Mixteco del Sur Medio", "Tacahua Mixtec" ], + "names": [ "Mixteco de Santa Cruz Tacahua", "Mixteco del Sur Medio", "Tacahua Mixtec", "Tu’un savi" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -127113,7 +132994,6 @@ "full": "xtu-Latn-MX", "iana": [ "Cuyamecalco Mixtec" ], "iso639_3": "xtu", - "localnames": [ "Tu'un savi" ], "name": "Mixtec, Cuyamecalco", "names": [ "Cuicatlán Mixtec", "Cuyamecalco Mixtec", "Mixteco de Cañada central", "Mixteco de Cuyamecalco", "Tu’un savi" ], "region": "MX", @@ -127158,9 +133038,8 @@ "full": "xty-Latn-MX", "iana": [ "Yoloxochitl Mixtec" ], "iso639_3": "xty", - "localnames": [ "Tu'un savi" ], "name": "Mixtec, Yoloxóchitl", - "names": [ "Mixteco de Yoloxóchitl", "Tu’un savi", "Yoloxochitl Mixtec" ], + "names": [ "Mixteco de San Luis Acatlán", "Mixteco de Yoloxóchitl", "Tu’un savi", "Yoloxochitl Mixtec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -127199,21 +133078,23 @@ "windows": "xua-Zyyy" }, { - "full": "xub-Knda-IN", + "full": "xub-Taml-IN", "iana": [ "Betta Kurumba" ], "iso639_3": "xub", + "latnnames": [ "Peṭṭak kuṟumpar" ], + "localnames": [ "பெட்டக் குறும்பர்" ], "name": "Kurumba, Betta", "names": [ "Betta Kurumba", "Kadu Bhasha", "Kadu Kurumba", "Kurubara Bhashe", "Nanga Man Pagi", "Urali Kurumba" ], - "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Knda", + "script": "Taml", "sldr": false, - "tag": "xub-Knda", - "windows": "xub-Knda" + "tag": "xub", + "tags": [ "xub-IN", "xub-Taml" ], + "windows": "xub-Taml" }, { - "full": "xub-Mlym-IN", + "full": "xub-Knda-IN", "iana": [ "Betta Kurumba" ], "iso639_3": "xub", "name": "Kurumba, Betta", @@ -127221,26 +133102,24 @@ "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Mlym", + "script": "Knda", "sldr": false, - "tag": "xub-Mlym", - "windows": "xub-Mlym" + "tag": "xub-Knda", + "windows": "xub-Knda" }, { - "full": "xub-Taml-IN", + "full": "xub-Mlym-IN", "iana": [ "Betta Kurumba" ], "iso639_3": "xub", - "latnnames": [ "Peṭṭak kuṟumpar" ], - "localnames": [ "பெட்டக் குறும்பர்" ], "name": "Kurumba, Betta", "names": [ "Betta Kurumba", "Kadu Bhasha", "Kadu Kurumba", "Kurubara Bhashe", "Nanga Man Pagi", "Urali Kurumba" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Taml", + "script": "Mlym", "sldr": false, - "tag": "xub-Taml", - "windows": "xub-Taml" + "tag": "xub-Mlym", + "windows": "xub-Mlym" }, { "full": "xud-Latn-AU", @@ -127271,26 +133150,26 @@ "windows": "xug-Zyyy" }, { - "full": "xuj-Zyyy-IN", + "full": "xuj-Taml-IN", "iana": [ "Jennu Kurumba" ], "iso639_3": "xuj", "name": "Kurumba, Jennu", "names": [ "Jen Kurumba", "Jennu Kurumba", "Jennu Nudi", "Kattu Nayaka", "Naik Kurumba", "Naikan", "Nonstandard Kannada", "Shola Nayakan", "Ten Kurumba" ], "region": "IN", "regionname": "India", - "script": "Zyyy", + "script": "Taml", "sldr": false, "tag": "xuj", - "tags": [ "xuj-IN", "xuj-Zyyy" ], + "tags": [ "xuj-IN", "xuj-Taml" ], "unwritten": true, - "windows": "xuj-Zyyy" + "windows": "xuj-Taml" }, { "full": "xul-Latn-AU", - "iana": [ "Ngunawal" ], + "iana": [ "Ngunawal", "Nunukul" ], "iso639_3": "xul", "name": "Ngunawal", - "names": [ "Gurungada", "Ngoonawal", "Ngunnawal", "Ngunuwal", "Nungawal", "Wonnawal", "Yarr" ], + "names": [ "Gurungada", "Ngoonawal", "Ngunnawal", "Ngunuwal", "Nungawal", "Nunukul", "Wonnawal", "Yarr" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -127363,6 +133242,7 @@ "iana": [ "Upper Umpqua" ], "iso639_3": "xup", "name": "Upper Umpqua", + "names": [ "Etnemitane", "Umpqua" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -127506,6 +133386,7 @@ "iana": [ "Woccon" ], "iso639_3": "xwc", "name": "Woccon", + "names": [ "Waccamaw", "Waccon", "Wacon" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -127535,7 +133416,7 @@ "iana": [ "Xwela Gbe" ], "iso639_3": "xwe", "name": "Gbe, Xwela", - "names": [ "Houeda", "Peda", "Phera", "Xwela", "Xwela Gbe", "Xwela-Gbe" ], + "names": [ "Houéda", "Peda", "Phera", "Xwela", "Xwela Gbe", "Xwela-Gbe", "Xweɖa" ], "region": "BJ", "regionname": "Benin", "script": "Latn", @@ -127667,7 +133548,8 @@ "full": "xxb-Latn-GH", "iana": [ "Boro (Ghana)" ], "iso639_3": "xxb", - "name": "Boro (Ghana)", + "name": "Boro", + "names": [ "Boro (Ghana)" ], "region": "GH", "regionname": "Ghana", "script": "Latn", @@ -127682,7 +133564,7 @@ "iana": [ "Ke'o" ], "iso639_3": "xxk", "name": "Ke’o", - "names": [ "Ke'o", "Nage-Keo" ], + "names": [ "Ke'o", "Nage-Keo", "sara kita", "sara ndia" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -127696,6 +133578,7 @@ "iana": [ "Minkin" ], "iso639_3": "xxm", "name": "Minkin", + "names": [ "Mingginda", "Mingin", "Minikin", "Minkina", "Myengeen" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -127799,6 +133682,7 @@ "iana": [ "Yalakalore" ], "iso639_3": "xyl", "name": "Yalakalore", + "names": [ "Yalakalodu" ], "region": "BR", "regionname": "Brazil", "script": "Latn", @@ -127839,23 +133723,24 @@ "windows": "xyy-Latn" }, { - "full": "xzh-Zyyy-CN", + "full": "xzh-Marc-CN", "iana": [ "Zhang-Zhung" ], "iso639_3": "xzh", "name": "Zhang-Zhung", "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Marc", "sldr": false, "tag": "xzh", - "tags": [ "xzh-CN", "xzh-Zyyy" ], - "windows": "xzh-Zyyy" + "tags": [ "xzh-CN", "xzh-Marc" ], + "windows": "xzh-Marc" }, { "full": "xzm-Zyyy-XX", "iana": [ "Zemgalian" ], "iso639_3": "xzm", "name": "Zemgalian", + "names": [ "Semigallian" ], "region": "XX", "regions": [ "LT", "LV" ], "script": "Zyyy", @@ -127882,8 +133767,10 @@ "full": "yaa-Latn-PE", "iana": [ "Yaminahua" ], "iso639_3": "yaa", + "localname": "Yaminawá", + "localnames": [ "Yaminawá" ], "name": "Yaminahua", - "names": [ "Iauminawa", "Jaminawa", "Jaminawá", "Yamanawa", "Yaminawa", "Yaminawá", "Yamináwa", "Yuminahua" ], + "names": [ "Iauminawa", "Jaminawa", "Yamanawa", "Yaminawá", "Yuminahua" ], "region": "PE", "regionname": "Peru", "regions": [ "BO", "BR" ], @@ -127899,7 +133786,7 @@ "iso639_3": "yab", "localnames": [ "Yuhup" ], "name": "Yuhup", - "names": [ "Makú-Yahup", "Yahup", "Yahup Makú", "Yuhuo Makú", "Yuhupdeh", "Yëhup" ], + "names": [ "Yahup", "Yuhupdeh", "Yëhup" ], "region": "BR", "regionname": "Brazil", "regions": [ "CO" ], @@ -127987,32 +133874,49 @@ "windows": "yag-Latn" }, { - "full": "yah-Cyrl-TJ", + "full": "yah-Latn-TJ", + "iana": [ "Yazgulyam" ], + "iso639_3": "yah", + "localnames": [ "Zǵamiǵai" ], + "name": "Yazghulami", + "names": [ "Iazgulem", "Yazghulomi", "Yazgulam", "Yazgulami", "Yazgulyam", "Yazgulyami", "Yuzdomi zavég", "Zgyamigyai", "Zǵamiǵai" ], + "region": "TJ", + "regionname": "Tajikistan", + "script": "Latn", + "sldr": false, + "tag": "yah", + "tags": [ "yah-Latn", "yah-TJ" ], + "windows": "yah-Latn" + }, + { + "full": "yah-Arab-TJ", "iana": [ "Yazgulyam" ], "iso639_3": "yah", - "name": "Yazgulyam", - "names": [ "Iazgulem", "Yazghulomi", "Yazgulam", "Yazgulami", "Yazgulyami", "Yuzdomi zavég" ], + "name": "Yazghulami", + "names": [ "Iazgulem", "Yazghulomi", "Yazgulam", "Yazgulami", "Yazgulyam", "Yazgulyami", "Yuzdomi zavég", "Zgyamigyai", "Zǵamiǵai" ], "nophonvars": true, "region": "TJ", "regionname": "Tajikistan", - "script": "Cyrl", + "script": "Arab", "sldr": false, - "tag": "yah-Cyrl", - "windows": "yah-Cyrl" + "tag": "yah-Arab", + "windows": "yah-Arab" }, { - "full": "yah-Latn-TJ", + "full": "yah-Cyrl-TJ", "iana": [ "Yazgulyam" ], "iso639_3": "yah", - "localnames": [ "Yuzdomi zavég" ], - "name": "Yazgulyam", - "names": [ "Iazgulem", "Yazghulomi", "Yazgulam", "Yazgulami", "Yazgulyami", "Yuzdomi zavég" ], + "latnnames": [ "Zǵamiǵai" ], + "localnames": [ "Зѓамиѓай" ], + "name": "Yazghulami", + "names": [ "Iazgulem", "Yazghulomi", "Yazgulam", "Yazgulami", "Yazgulyam", "Yazgulyami", "Yuzdomi zavég", "Zgyamigyai", "Zǵamiǵai" ], + "nophonvars": true, "region": "TJ", "regionname": "Tajikistan", - "script": "Latn", + "script": "Cyrl", "sldr": false, - "tag": "yah-Latn", - "windows": "yah-Latn" + "tag": "yah-Cyrl", + "windows": "yah-Cyrl" }, { "full": "yai-Cyrl-TJ", @@ -128035,7 +133939,7 @@ "iana": [ "Banda-Yangere" ], "iso639_3": "yaj", "name": "Banda-Yangere", - "names": [ "Yangere", "Yanguere" ], + "names": [ "Kra", "Yangere", "Yanguere" ], "region": "CF", "regionname": "Central African Republic", "script": "Latn", @@ -128061,35 +133965,36 @@ "windows": "yak-Latn" }, { - "full": "yal-Arab-GN", + "full": "yal-Latn-GN", "iana": [ "Yalunka" ], "iso639_3": "yal", + "localname": "Jalunga xuwiina'", + "localnames": [ "Jalunga xuwiina'" ], "name": "Yalunka", "names": [ "Dialonke", "Dialonké", "Djallonke", "Dyalonke", "Jalonke", "Jalonké", "Jalunga", "Jalunga xuwiina’", "Jalunganéé", "Jalungas", "Kjalonke", "Yalunke" ], - "nophonvars": true, "region": "GN", "regionname": "Guinea", "regions": [ "ML", "SL", "SN" ], - "script": "Arab", - "sldr": false, - "tag": "yal-Arab", - "windows": "yal-Arab" + "script": "Latn", + "sldr": true, + "tag": "yal", + "tags": [ "yal-GN", "yal-Latn" ], + "windows": "yal-Latn" }, { - "full": "yal-Latn-GN", + "full": "yal-Arab-GN", "iana": [ "Yalunka" ], "iso639_3": "yal", - "localname": "Yalunka", - "localnames": [ "Jalunga xuwiina'" ], "name": "Yalunka", "names": [ "Dialonke", "Dialonké", "Djallonke", "Dyalonke", "Jalonke", "Jalonké", "Jalunga", "Jalunga xuwiina’", "Jalunganéé", "Jalungas", "Kjalonke", "Yalunke" ], + "nophonvars": true, "region": "GN", "regionname": "Guinea", "regions": [ "ML", "SL", "SN" ], - "script": "Latn", - "sldr": true, - "tag": "yal-Latn", - "windows": "yal-Latn" + "script": "Arab", + "sldr": false, + "tag": "yal-Arab", + "windows": "yal-Arab" }, { "full": "yam-Latn-CM", @@ -128175,9 +134080,9 @@ "iana": [ "Yaqui" ], "iso639_3": "yaq", "localname": "Yoeme", - "localnames": [ "Yoeme" ], + "localnames": [ "Yo'em noki" ], "name": "Yaqui", - "names": [ "Cahita", "Hiak-nooki", "Hiaki", "Yoem Noki", "Yoeme" ], + "names": [ "Cahita", "Hiak-nooki", "Hiaki", "Yoem Noki", "Yoeme", "Yo’em noki" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -128222,8 +134127,9 @@ "full": "yat-Latn-CM", "iana": [ "Yambeta" ], "iso639_3": "yat", + "localnames": [ "Yambɛʼtá" ], "name": "Yambeta", - "names": [ "Njambeta", "Yambetta" ], + "names": [ "Njambeta", "Yambetta", "Yambɛʼtá", "Yaŋpɛdá" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -128236,8 +134142,9 @@ "full": "yau-Latn-VE", "iana": [ "Yuwana" ], "iso639_3": "yau", - "name": "Yuwana", - "names": [ "Chicano", "Chikano", "Ho", "Hodï", "Hoti", "Jodi", "Joti", "Waruwaru", "Yoana", "Yuana" ], + "localnames": [ "Jodï" ], + "name": "Hodï", + "names": [ "Chicano", "Chikano", "Ho", "Hoti", "Jodï", "Joti", "Waruwaru", "Yoana", "Yuana", "Yuwana" ], "region": "VE", "regionname": "Venezuela", "script": "Latn", @@ -128251,9 +134158,9 @@ "iana": [ "Yangben" ], "iso639_3": "yav", "localname": "nuasue", - "localnames": [ "Nuasue" ], + "localnames": [ "Nuasúɛ" ], "name": "Yangben", - "names": [ "Central Yambassa", "Kalong", "Nuasue", "Nukalonge", "Yambassa" ], + "names": [ "Central Yambassa", "Kalɔŋ", "Nuasue", "Nuasúɛ", "Nukalong", "Yambasa", "Yambassa" ], "region": "CM", "regionname": "Cameroon", "script": "Latn", @@ -128295,6 +134202,7 @@ "full": "yay-Latn-NG", "iana": [ "Agwagwune" ], "iso639_3": "yay", + "localname": "Gwune", "localnames": [ "Gwune" ], "name": "Agwagwune", "names": [ "Agwaguna", "Akurakura", "Gwune", "Okurikan" ], @@ -128313,7 +134221,7 @@ "localname": "Lokạạ", "localnames": [ "Lokạạ" ], "name": "Lokaa", - "names": [ "Loka", "Loke", "Loko", "Lokö", "Lokǝ", "Lokạạ", "Luko", "Yakurr", "Yakö" ], + "names": [ "Loka", "Loke", "Loko", "Lokö", "Lokǝ", "Lokạạ", "Luko", "Yaka", "Yakurr", "Yakö" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -128355,18 +134263,32 @@ "windows": "ybb-Latn" }, { - "full": "ybe-Zyyy-CN", + "full": "ybe-Latn-CN", "iana": [ "West Yugur" ], "iso639_3": "ybe", "name": "Yugur, West", "names": [ "Sari Yogur", "Sarig", "Sary-Uighur", "Sarygh Uygur", "West Yugur", "Ya Lu", "Yellow Uighur", "Yugu", "Yuku" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "ybe", - "tags": [ "ybe-CN", "ybe-Zyyy" ], - "windows": "ybe-Zyyy" + "tags": [ "ybe-CN", "ybe-Latn" ], + "windows": "ybe-Latn" + }, + { + "full": "ybe-Ougr-CN", + "iana": [ "West Yugur" ], + "iso639_3": "ybe", + "name": "Yugur, West", + "names": [ "Sari Yogur", "Sarig", "Sary-Uighur", "Sarygh Uygur", "West Yugur", "Ya Lu", "Yellow Uighur", "Yugu", "Yuku" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Ougr", + "sldr": false, + "tag": "ybe-Ougr", + "windows": "ybe-Ougr" }, { "full": "ybh-Deva-NP", @@ -128406,7 +134328,7 @@ "iana": [ "Hasha" ], "iso639_3": "ybj", "name": "Hasha", - "names": [ "Iyashi", "Yashi" ], + "names": [ "Ayashim", "Ayashin", "Iyashi", "Yashi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -128537,15 +134459,15 @@ "full": "ycl-Latn-CN", "iana": [ "Lolopo" ], "iso639_3": "ycl", + "localnames": [ "Loxrlavu" ], "name": "Lolopo", - "names": [ "Bai Yi", "Central Yi", "Gaoshanzu", "Hei Yi", "Lolopho", "Lulupu", "Luolu" ], + "names": [ "Bai Yi", "Central Yi", "Gaoshanzu", "Hei Yi", "Lolopho", "Loxrlavu", "Lulupu", "Luolu" ], "region": "CN", "regionname": "China", "script": "Latn", "sldr": true, "tag": "ycl", "tags": [ "ycl-CN", "ycl-Latn" ], - "unwritten": true, "windows": "ycl-Latn" }, { @@ -128577,6 +134499,20 @@ "tags": [ "ycp-LA", "ycp-Zyyy" ], "windows": "ycp-Zyyy" }, + { + "full": "ycr-Latn-TW", + "iso639_3": "ycr", + "name": "Yilan Creole", + "names": [ "YCJ", "Yilan Creole Japanese", "Yilan Japanese Creole" ], + "region": "TW", + "regionname": "China-Taiwan", + "script": "Latn", + "sldr": false, + "tag": "ycr", + "tags": [ "ycr-Latn", "ycr-TW" ], + "unwritten": true, + "windows": "ycr-Latn" + }, { "full": "yda-Latn-AU", "iana": [ "Yanda" ], @@ -128634,35 +134570,35 @@ "windows": "ydk-Latn" }, { - "full": "yea-Knda-IN", + "full": "yea-Mlym-IN", "iana": [ "Ravula" ], "iso639_3": "yea", "name": "Ravula", - "names": [ "Adiya", "Adiyan", "Iryavula", "Panjiri Yerava", "Yerava", "Yoruba" ], - "nophonvars": true, + "names": [ "Iryavula", "Yerava", "Yoruba" ], "region": "IN", "regionname": "India", - "script": "Knda", + "script": "Mlym", "sldr": false, - "tag": "yea-Knda", - "windows": "yea-Knda" + "tag": "yea", + "tags": [ "yea-IN", "yea-Mlym" ], + "windows": "yea-Mlym" }, { - "full": "yea-Mlym-IN", + "full": "yea-Knda-IN", "iana": [ "Ravula" ], "iso639_3": "yea", "name": "Ravula", - "names": [ "Adiya", "Adiyan", "Iryavula", "Panjiri Yerava", "Yerava", "Yoruba" ], + "names": [ "Iryavula", "Yerava", "Yoruba" ], "nophonvars": true, "region": "IN", "regionname": "India", - "script": "Mlym", + "script": "Knda", "sldr": false, - "tag": "yea-Mlym", - "windows": "yea-Mlym" + "tag": "yea-Knda", + "windows": "yea-Knda" }, { - "full": "yec-Zyyy-DE", + "full": "yec-Latn-DE", "iana": [ "Yeniche" ], "iso639_3": "yec", "name": "Yeniche", @@ -128670,11 +134606,11 @@ "region": "DE", "regionname": "Germany", "regions": [ "AT", "CH", "NL", "PG" ], - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "yec", - "tags": [ "yec-DE", "yec-Zyyy" ], - "windows": "yec-Zyyy" + "tags": [ "yec-DE", "yec-Latn" ], + "windows": "yec-Latn" }, { "full": "yee-Latn-PG", @@ -128754,8 +134690,9 @@ "full": "yes-Latn-NG", "iana": [ "Nyankpa" ], "iso639_3": "yes", + "localnames": [ "Nyenkpa" ], "name": "Nyankpa", - "names": [ "Nyenkpa", "Yasgua", "Yeskwa" ], + "names": [ "Anaŋkpa", "Nnaŋkpa", "Nyenkpa", "Yasgua", "Yeskwa" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -128783,6 +134720,8 @@ "full": "yeu-Telu-IN", "iana": [ "Yerukula" ], "iso639_3": "yeu", + "latnnames": [ "Erukala" ], + "localnames": [ "ఎఱుకల" ], "name": "Yerukula", "names": [ "Erukala", "Eruku Bhasha", "Korava", "Korchi", "Kurru Bhasha", "Kurutha", "Yarukula", "Yerkula", "Yerukala", "Yerukala-Korava", "Yerukla", "Yerukula-Bhasha" ], "region": "IN", @@ -128872,7 +134811,8 @@ "full": "ygm-Latn-PG", "iana": [ "Yagomi" ], "iso639_3": "ygm", - "name": "Yagomi", + "name": "Tandɨ", + "names": [ "Tanda", "Yagomi" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -128959,7 +134899,7 @@ "iana": [ "Baha Buyang" ], "iso639_3": "yha", "name": "Buyang, Baha", - "names": [ "Baha Buyang", "Buyang", "Buyang Zhuang", "Guangnan Buyang", "Western Buyang" ], + "names": [ "Baha", "Baha Buyang", "Buyang", "Buyang Zhuang", "Guangnan Buyang", "Paha", "Western Buyang" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -128973,6 +134913,7 @@ "full": "yhd-Hebr-IL", "iana": [ "Judeo-Iraqi Arabic" ], "iso639_3": "yhd", + "macrolang": "jrb", "name": "Arabic, Judeo-Iraqi", "names": [ "Iraqi Judeo-Arabic", "Jewish Iraqi-Baghdadi Arabic", "Judeo-Iraqi Arabic", "Yahudic" ], "region": "IL", @@ -129020,8 +134961,8 @@ "latnnames": [ "Yiddish" ], "localname": "ייִדיש", "localnames": [ "ייִדיש" ], - "name": "Yiddish, Eastern", - "names": [ "Judeo-German", "Yiddish" ], + "name": "Yiddish", + "names": [ "Judeo-German", "Yiddish, Eastern", "Yidish" ], "region": "001", "regionname": "World", "script": "Hebr", @@ -129080,6 +135021,7 @@ "iso639_3": "yih", "latnnames": [ "Yiddish" ], "localnames": [ "ייִדיש" ], + "macrolang": "yi", "name": "Yiddish, Western", "names": [ "Judeo-German", "Western Yiddish", "Yiddish", "Yidish" ], "region": "DE", @@ -129213,7 +135155,7 @@ "iana": [ "North Awyu" ], "iso639_3": "yir", "name": "Awyu, North", - "names": [ "Awyu", "Djair", "Dyair", "Jair", "North Awyu", "Yair" ], + "names": [ "Awyu", "Djair", "Dyair", "North Awyu", "Yair" ], "region": "ID", "regionname": "Indonesia", "script": "Latn", @@ -129266,19 +135208,19 @@ "windows": "yiu-Zyyy" }, { - "full": "yiv-Zyyy-CN", + "full": "yiv-Yiii-CN", "iana": [ "Northern Nisu" ], "iso639_3": "yiv", "name": "Nisu, Northern", "names": [ "E-Xin Yi", "Nasupho", "Nisupho", "Northern Nisu" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Yiii", "sldr": false, "tag": "yiv", - "tags": [ "yiv-CN", "yiv-Zyyy" ], + "tags": [ "yiv-CN", "yiv-Yiii" ], "unwritten": true, - "windows": "yiv-Zyyy" + "windows": "yiv-Yiii" }, { "full": "yix-Zyyy-CN", @@ -129329,13 +135271,14 @@ "full": "yka-Arab-PH", "iana": [ "Yakan" ], "iso639_3": "yka", + "localname": "Yakan", "name": "Yakan", "names": [ "Yacan" ], "nophonvars": true, "region": "PH", "regionname": "Philippines", "script": "Arab", - "sldr": false, + "sldr": true, "tag": "yka-Arab", "windows": "yka-Arab" }, @@ -129355,6 +135298,21 @@ "tags": [ "ykg-Cyrl", "ykg-RU" ], "windows": "ykg-Cyrl" }, + { + "full": "ykh-Cyrl-MN", + "iso639_3": "ykh", + "name": "Khamnigan Mongol", + "names": [ "Hamunikan", "Kamnigan", "Khamnigan", "Xamnigan" ], + "region": "MN", + "regionname": "Mongolia", + "regions": [ "CN", "RU" ], + "script": "Cyrl", + "sldr": false, + "tag": "ykh", + "tags": [ "ykh-Cyrl", "ykh-MN" ], + "unwritten": true, + "windows": "ykh-Cyrl" + }, { "full": "yki-Latn-ID", "iana": [ "Yoke" ], @@ -129435,8 +135393,8 @@ "iso639_3": "yko", "localname": "Iyasa", "localnames": [ "Iyasa" ], - "name": "Yasa", - "names": [ "Bongwe", "Iyasa", "Lyaasa", "Lyassa", "Maasa", "Yassa" ], + "name": "Iyasa", + "names": [ "Bongwe", "Lyaasa", "Lyassa", "Maasa", "Yasa", "Yassa" ], "region": "CM", "regionname": "Cameroon", "regions": [ "GA", "GQ" ], @@ -129506,7 +135464,8 @@ "full": "yla-Latn-PG", "iana": [ "Yaul" ], "iso639_3": "yla", - "name": "Yaul", + "name": "Ulwa", + "names": [ "Yaul" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -129537,8 +135496,8 @@ "iso639_3": "yle", "localname": "Yélî Dnye", "localnames": [ "Yélî Dnye" ], - "name": "Yele", - "names": [ "Rossel", "Yela", "Yelatnye", "Yelejong", "Yeletnye", "Yelidnye", "Yélî Dnye" ], + "name": "Yélî Dnye", + "names": [ "Rossel", "Yela", "Yelatnye", "Yele", "Yelejong", "Yeletnye", "Yelidnye" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -129551,8 +135510,8 @@ "full": "ylg-Latn-PG", "iana": [ "Yelogu" ], "iso639_3": "ylg", - "name": "Yelogu", - "names": [ "Buiamanambu", "Kaunga" ], + "name": "Yalaku", + "names": [ "Buiamanambu", "Kaunga", "Yelogu" ], "region": "PG", "regionname": "Papua New Guinea", "script": "Latn", @@ -129608,7 +135567,7 @@ "iana": [ "Langnian Buyang" ], "iso639_3": "yln", "name": "Buyang, Langnian", - "names": [ "Buozaang", "Buyang Zhuang", "Eastern Buyang", "E’cun Buyang", "Langnian Buyang" ], + "names": [ "Buyang Zhuang", "Eastern Buyang", "E’cun Buyang", "Langnian Buyang" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -130045,6 +136004,7 @@ "iana": [ "Yana" ], "iso639_3": "ynn", "name": "Yana", + "names": [ "Yanan" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -130123,12 +136083,12 @@ "names": [ "Yariba", "Yooba", "Yorouba", "Yorùbá", "Èdè Yorùbá" ], "region": "NG", "regionname": "Nigeria", - "regions": [ "CI", "SL", "TG", "US" ], + "regions": [ "CI", "GH", "SL", "TG", "US" ], "script": "Latn", "sldr": true, "tag": "yo", "tags": [ "yo-Latn", "yo-NG" ], - "windows": "yo-Latn" + "windows": "yo-Latn-NG" }, { "full": "yo-Arab-NG", @@ -130144,7 +136104,7 @@ "script": "Arab", "sldr": true, "tag": "yo-Arab", - "windows": "yo-Arab" + "windows": "yo-Arab-NG" }, { "full": "yo-Latn-BJ", @@ -130173,7 +136133,7 @@ "script": "Brai", "sldr": false, "tag": "yo-Brai", - "windows": "yo-Brai" + "windows": "yo-Brai-NG" }, { "full": "yob-Latn-PG", @@ -130204,17 +136164,17 @@ "windows": "yog-Latn" }, { - "full": "yoi-Zyyy-JP", + "full": "yoi-Jpan-JP", "iana": [ "Yonaguni" ], "iso639_3": "yoi", "name": "Yonaguni", "region": "JP", "regionname": "Japan", - "script": "Zyyy", + "script": "Jpan", "sldr": false, "tag": "yoi", - "tags": [ "yoi-JP", "yoi-Zyyy" ], - "windows": "yoi-Zyyy" + "tags": [ "yoi-JP", "yoi-Jpan" ], + "windows": "yoi-Jpan" }, { "full": "yok-Latn-US", @@ -130232,25 +136192,26 @@ "windows": "yok-Latn" }, { - "full": "yol-Zyyy-GB", + "full": "yol-Latn-GB", "iana": [ "Yola" ], "iso639_3": "yol", + "localnames": [ "Yola" ], "name": "Yola", "region": "GB", "regionname": "United Kingdom", - "script": "Zyyy", + "script": "Latn", "sldr": false, "tag": "yol", - "tags": [ "yol-GB", "yol-Zyyy" ], - "windows": "yol-Zyyy" + "tags": [ "yol-GB", "yol-Latn" ], + "windows": "yol-Latn" }, { "full": "yom-Latn-CD", "iana": [ "Yombe" ], "iso639_3": "yom", - "localnames": [ "Kiyoombi" ], - "name": "Yombe", - "names": [ "Bayombe", "Ciyoombe", "Ibinda", "Iombe", "Kiombi", "Kiyombe", "Kiyoombi", "Yoombe" ], + "localnames": [ "Kiyombe" ], + "name": "Kiyombe", + "names": [ "Bayombe", "Ciyoombe", "Ibinda", "Iombe", "Kiombi", "Kiyoombi", "Yombe", "Yoombe" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "regions": [ "AO", "CG" ], @@ -130305,7 +136266,7 @@ "windows": "yox-Zyyy" }, { - "full": "yoy-Zyyy-TH", + "full": "yoy-Thai-TH", "iana": [ "Yoy" ], "iso639_3": "yoy", "name": "Yoy", @@ -130313,12 +136274,11 @@ "region": "TH", "regionname": "Thailand", "regions": [ "LA" ], - "script": "Zyyy", + "script": "Thai", "sldr": false, "tag": "yoy", - "tags": [ "yoy-TH", "yoy-Zyyy" ], - "unwritten": true, - "windows": "yoy-Zyyy" + "tags": [ "yoy-TH", "yoy-Thai" ], + "windows": "yoy-Thai" }, { "full": "ypa-Zyyy-CN", @@ -130517,17 +136477,47 @@ "full": "yrl-Latn-BR", "iana": [ "Nhengatu" ], "iso639_3": "yrl", + "localname": "nheẽgatu", "localnames": [ "Ñe’engatú" ], "name": "Nhengatu", "names": [ "Coastal Tupian", "Geral", "Língua Geral", "Língua Geral Amazônica", "Modern Tupi", "Modern Tupí", "Nengatu", "Nheengatu", "Nheengatú", "Nyengato", "Nyengatú", "Waengatu", "Yeral", "Ñeegatú", "Ñengatú", "Ñe’engatú" ], "region": "BR", "regionname": "Brazil", - "regions": [ "CO", "VE" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "yrl", "tags": [ "yrl-BR", "yrl-Latn" ], - "windows": "yrl-Latn" + "windows": "yrl-Latn-BR" + }, + { + "full": "yrl-Latn-CO", + "iana": [ "Nhengatu" ], + "iso639_3": "yrl", + "localname": "ñengatú", + "localnames": [ "Ñe’engatú" ], + "name": "Nhengatu", + "names": [ "Geral", "Modern Tupi", "Nheengatú", "Nyengato", "Waengatu", "Yeral", "Ñe’engatú" ], + "region": "CO", + "regionname": "Colombia", + "script": "Latn", + "sldr": true, + "tag": "yrl-CO", + "windows": "yrl-Latn-CO" + }, + { + "full": "yrl-Latn-VE", + "iana": [ "Nhengatu" ], + "iso639_3": "yrl", + "localname": "ñengatú", + "localnames": [ "Ñe’engatú" ], + "name": "Nengatu", + "names": [ "Geral", "Modern Tupi", "Nheengatu", "Nhengatu", "Waengatu", "Yeral", "Ñengatú" ], + "region": "VE", + "regionname": "Venezuela", + "script": "Latn", + "sldr": true, + "tag": "yrl-VE", + "windows": "yrl-Latn-VE" }, { "full": "yrm-Latn-AU", @@ -130549,7 +136539,7 @@ "iana": [ "Yerong" ], "iso639_3": "yrn", "name": "Yerong", - "names": [ "Ban Yao", "Da Ia", "Daban Yao", "Eastern Buyang", "Guangxi Buyang", "Ia Hrong", "Iron Yao", "Khyung Buyang", "Liu Yao", "Napo Buyang", "Six Yao", "Tie Yao", "Tu Yao Indigenous Yao", "Yalang", "Yang Khyung", "Yerong Buyang" ], + "names": [ "Ban Yao", "Da Ia", "Daban Yao", "Eastern Buyang", "Guangxi Buyang", "Ia Hrong", "Iron Yao", "Khyung Buyang", "Liu Yao", "Napo Buyang", "Rongtun Buyang", "Six Yao", "Tie Yao", "Tu Yao", "Yalang", "Yang Khyung", "Yerong Buyang" ], "region": "CN", "regionname": "China", "script": "Zyyy", @@ -130621,6 +136611,7 @@ "iana": [ "Yassic" ], "iso639_3": "ysc", "name": "Yassic", + "names": [ "Jassic", "Jász" ], "region": "HU", "regionname": "Hungary", "script": "Zyyy", @@ -130664,16 +136655,30 @@ "iana": [ "Yugoslavian Sign Language" ], "iso639_3": "ysl", "name": "Serbian Sign Language", - "names": [ "SZJ", "Slovene Sign Language", "Slovenian Sign Language", "Slovenski Znakovni Jezik", "Srpski Znakovni Jezik", "YSL", "Yugoslav Sign Language", "Yugoslavian Sign Language" ], + "names": [ "BSL", "BZJ", "Bosanski znakovni jezik", "Bosnian Sign Language", "Macedonian Sign Language", "Montenegrin Sign Language", "Montenegro Sign Language", "SZJ", "Slovene Sign Language", "Slovenian Sign Language", "Slovenski Znakovni Jezik", "Srpski Znakovni Jezik", "YSL", "Yugoslav Sign Language", "Yugoslavian Sign Language" ], "region": "RS", "regionname": "Serbia", - "regions": [ "SI" ], + "regions": [ "BA", "ME", "MK", "SI" ], "script": "Zxxx", "sldr": false, "tag": "ysl", "tags": [ "sgn-ysl", "sgn-ysl-RS", "ysl-RS", "ysl-Zxxx" ], "windows": "ysl-Zxxx" }, + { + "full": "ysm-Zxxx-MM", + "iana": [ "Myanmar Sign Language" ], + "iso639_3": "ysm", + "name": "Myanmar Sign Language", + "names": [ "Burmese Sign Language" ], + "region": "MM", + "regionname": "Myanmar", + "script": "Zxxx", + "sldr": false, + "tag": "ysm", + "tags": [ "sgn-ysm", "sgn-ysm-MM", "ysm-MM", "ysm-Zxxx" ], + "windows": "ysm-Zxxx" + }, { "full": "ysn-Yiii-CN", "iana": [ "Sani" ], @@ -130703,19 +136708,19 @@ "windows": "yso-Zyyy" }, { - "full": "ysp-Zyyy-CN", + "full": "ysp-Yiii-CN", "iana": [ "Southern Lolopo" ], "iso639_3": "ysp", "name": "Lolopo, Southern", "names": [ "Southern Lolopo" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Yiii", "sldr": false, "tag": "ysp", - "tags": [ "ysp-CN", "ysp-Zyyy" ], + "tags": [ "ysp-CN", "ysp-Yiii" ], "unwritten": true, - "windows": "ysp-Zyyy" + "windows": "ysp-Yiii" }, { "full": "ysr-Cyrl-RU", @@ -130856,7 +136861,7 @@ "iana": [ "Yugambal" ], "iso639_3": "yub", "name": "Yugambal", - "names": [ "Jugumbir", "Jukamba", "Manaldjali", "Minjanbal", "Ngarrbal", "Ngarrubul", "Yugabeh", "Yugambeh", "Yugumbal" ], + "names": [ "Jugumbir", "Jukamba", "Manaldjali", "Minjanbal", "Ngarrbal", "Ngarrubul", "Yugabeh", "Yugumbal" ], "region": "AU", "regionname": "Australia", "script": "Latn", @@ -130885,6 +136890,7 @@ "full": "yud-Hebr-IL", "iana": [ "Judeo-Tripolitanian Arabic" ], "iso639_3": "yud", + "macrolang": "jrb", "name": "Arabic, Judeo-Tripolitanian", "names": [ "Jewish Tripolitanian-Libyan Arabic", "Judeo-Tripolitanian Arabic", "Tripolitanian Judeo-Arabic", "Tripolita’it", "Yudi" ], "region": "IL", @@ -130901,8 +136907,9 @@ "iana": [ "Yue Chinese", "Cantonese" ], "iso639_3": "yue", "localname": "粵語", + "macrolang": "zh", "name": "Chinese, Yue", - "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu" ], + "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu", "Yuht Yúh" ], "region": "HK", "regionname": "Hong Kong", "regions": [ "AU", "BN", "CA", "CN", "CR", "HN", "ID", "KH", "LA", "MO", "MY", "NR", "NZ", "PA", "PH", "SE", "SG", "SR", "TH", "US", "VN", "ZA" ], @@ -130910,14 +136917,15 @@ "sldr": true, "tag": "yue", "tags": [ "yue-HK", "yue-Hant", "zh-yue", "zh-yue-HK" ], - "windows": "yue-Hant" + "windows": "yue-Hant-HK" }, { "full": "yue-Brai-HK", "iana": [ "Yue Chinese", "Cantonese" ], "iso639_3": "yue", + "macrolang": "zh", "name": "Chinese, Yue", - "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu" ], + "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu", "Yuht Yúh" ], "nophonvars": true, "region": "HK", "regionname": "Hong Kong", @@ -130925,15 +136933,16 @@ "script": "Brai", "sldr": false, "tag": "yue-Brai", - "windows": "yue-Brai" + "windows": "yue-Brai-HK" }, { "full": "yue-Hans-CN", "iana": [ "Yue Chinese", "Cantonese" ], "iso639_3": "yue", "localname": "粤语", + "macrolang": "zh", "name": "Chinese, Yue", - "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu" ], + "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu", "Yuht Yúh" ], "region": "CN", "regionname": "China", "regions": [ "AU", "BN", "CA", "CR", "HK", "HN", "ID", "KH", "LA", "MO", "MY", "NR", "NZ", "PA", "PH", "SE", "SG", "SR", "TH", "US", "VN", "ZA" ], @@ -130947,8 +136956,9 @@ "full": "yue-Latn-CN", "iana": [ "Yue Chinese", "Cantonese" ], "iso639_3": "yue", + "macrolang": "zh", "name": "Chinese, Yue", - "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu" ], + "names": [ "Cantonese", "Chinese Nung", "Guangdong", "Guangfu", "Gwong Dung", "Ha Xa Phang", "Hai Nam", "Han", "Hoa", "Hong Kong Cantonese", "Kién", "Konghu", "Liem Chau", "Lowland Nung", "Macau Cantonese", "Minh Huong", "Nung", "Phúc", "Quang Dong", "Samg Phang", "Shatou", "Shiqi", "Suòng Phóng", "Trièu Chau", "Wancheng", "Yue", "Yue Chinese", "Yueh", "Yuet", "Yuet Yue", "Yueyu", "Yuht Yúh" ], "region": "CN", "regionname": "China", "regions": [ "AU", "BN", "CA", "CR", "HK", "HN", "ID", "KH", "LA", "MO", "MY", "NR", "NZ", "PA", "PH", "SE", "SG", "SR", "TH", "US", "VN", "ZA" ], @@ -130956,7 +136966,7 @@ "sldr": false, "tag": "yue-Latn", "variants": [ "jyutping" ], - "windows": "yue-Latn" + "windows": "yue-Latn-CN" }, { "full": "yuf-Latn-US", @@ -130998,7 +137008,7 @@ "regionname": "Colombia", "regions": [ "BR" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "yui", "tags": [ "yui-CO", "yui-Latn" ], "windows": "yui-Latn" @@ -131007,6 +137017,7 @@ "full": "yuj-Latn-PG", "iana": [ "Karkar-Yuri" ], "iso639_3": "yuj", + "localnames": [ "Karkar" ], "name": "Karkar-Yuri", "names": [ "Karkar", "Yuri" ], "region": "PG", @@ -131022,6 +137033,7 @@ "iana": [ "Yuki" ], "iso639_3": "yuk", "name": "Yuki", + "names": [ "Northern Yukian" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -131088,7 +137100,7 @@ "regionname": "Colombia", "regions": [ "VE" ], "script": "Latn", - "sldr": false, + "sldr": true, "tag": "yup", "tags": [ "yup-CO", "yup-Latn" ], "windows": "yup-Latn" @@ -131347,18 +137359,32 @@ "windows": "ywt-Zyyy" }, { - "full": "ywu-Zyyy-CN", + "full": "ywu-Plrd-CN", "iana": [ "Wumeng Nasu" ], "iso639_3": "ywu", "name": "Nasu, Wumeng", "names": [ "Neisu", "Nersu", "Nesu", "Wumeng Nasu", "Wumeng Yi", "Wusa Yi", "Yuan-Mo Yi" ], "region": "CN", "regionname": "China", - "script": "Zyyy", + "script": "Plrd", "sldr": false, "tag": "ywu", - "tags": [ "ywu-CN", "ywu-Zyyy" ], - "windows": "ywu-Zyyy" + "tags": [ "ywu-CN", "ywu-Plrd" ], + "windows": "ywu-Plrd" + }, + { + "full": "ywu-Yiii-CN", + "iana": [ "Wumeng Nasu" ], + "iso639_3": "ywu", + "name": "Nasu, Wumeng", + "names": [ "Neisu", "Nersu", "Nesu", "Wumeng Nasu", "Wumeng Yi", "Wusa Yi", "Yuan-Mo Yi" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Yiii", + "sldr": false, + "tag": "ywu-Yiii", + "windows": "ywu-Yiii" }, { "full": "yww-Latn-AU", @@ -131542,8 +137568,8 @@ "iana": [ "Zhuang", "Chuang" ], "iso639_3": "zha", "localname": "Vahmbanj", - "name": "Zhuang, Yongbei", - "names": [ "Chuang", "Zhuang" ], + "name": "Zhuang", + "names": [ "Chuang", "Zhuang, Yongbei" ], "region": "CN", "regionname": "China", "script": "Latn", @@ -131556,8 +137582,8 @@ "full": "za-Hani-CN", "iana": [ "Zhuang", "Chuang" ], "iso639_3": "zha", - "name": "Zhuang, Yongbei", - "names": [ "Chuang", "Zhuang" ], + "name": "Zhuang", + "names": [ "Chuang", "Zhuang, Yongbei" ], "nophonvars": true, "region": "CN", "regionname": "China", @@ -131585,8 +137611,10 @@ "full": "zaa-Latn-MX", "iana": [ "Sierra de Juárez Zapotec" ], "iso639_3": "zaa", + "localnames": [ "Titsaꞌ" ], + "macrolang": "zap", "name": "Zapotec, Sierra de Juárez", - "names": [ "Ixtlán Zapoteco", "Juárez Zapotec", "Sierra de Juárez Zapotec", "Zapoteco de Atepec" ], + "names": [ "Didzá", "Ixtlán Zapoteco", "Juárez Zapotec", "Sierra de Juárez Zapotec", "Titsaꞌ", "Zapoteco Serrano Bajo", "Zapoteco de Atepec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131600,9 +137628,10 @@ "iana": [ "Western Tlacolula Valley Zapotec", "San Juan Guelavía Zapotec" ], "iso639_3": "zab", "localname": "Ditsa", - "localnames": [ "Ditsa" ], + "localnames": [ "Ditsa", "Dixza" ], + "macrolang": "zap", "name": "Zapotec, Western Tlacolula Valley", - "names": [ "Ditsa", "Guelavía", "San Juan Guelavía Zapotec", "Western Tlacolula Valley Zapotec", "Western Tlacolula Zapotec", "Zapoteco de San Juan", "Zapoteco de Tlacolula occidental" ], + "names": [ "Ditsa", "Dixza", "Dixzhá", "Guelavía", "San Juan Guelavía Zapotec", "Western Tlacolula Valley Zapotec", "Western Tlacolula Zapotec", "Zapoteco de San Juan", "Zapoteco de Tlacolula occidental", "Zapoteco de Valles del Norte Central" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -131616,8 +137645,10 @@ "full": "zac-Latn-MX", "iana": [ "Ocotlán Zapotec" ], "iso639_3": "zac", + "localnames": [ "Dizë́" ], + "macrolang": "zap", "name": "Zapotec, Ocotlán", - "names": [ "Ocotlán Oeste Zapotec", "Ocotlán Zapotec", "Zapoteco del Poniente de Ocotlán" ], + "names": [ "Dizá", "Dizë́", "Ocotlán Oeste Zapotec", "Ocotlán Zapotec", "Zapoteco de Valles de Suroeste Alto", "Zapoteco de Valles del Suroeste Central", "Zapoteco de Valles del Suroeste Medio", "Zapoteco del Poniente de Ocotlán" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131631,9 +137662,10 @@ "iana": [ "Cajonos Zapotec" ], "iso639_3": "zad", "localname": "didza-xhon", - "localnames": [ "didza-xhon" ], + "localnames": [ "Xtillꞌchonaꞌ" ], + "macrolang": "zap", "name": "Zapotec, Cajonos", - "names": [ "Cajonos Zapotec", "Southern Villa Alta Zapotec", "Yaganiza Zapotec", "Zapoteco de San Pedro Cajonos", "didza-xhon" ], + "names": [ "Cajonos Zapotec", "Didza-xhon", "Ditse’", "Ditse’ hualats", "Southern Villa Alta Zapotec", "Xtillꞌchonaꞌ", "Yaganiza Zapotec", "Zapoteco Serrano del Sureste Bajo", "Zapoteco de San Pedro Cajonos" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -131648,8 +137680,10 @@ "iana": [ "Yareni Zapotec" ], "iso639_3": "zae", "localname": "Zapotec, Yareni", + "localnames": [ "Tisa" ], + "macrolang": "zap", "name": "Zapotec, Yareni", - "names": [ "Etla Zapotec", "Western Ixtlán Zapotec", "Yareni Zapotec", "Zapoteco de Santa Ana Yareni", "Zapoteco de Teococuilco de Marcos Pérez" ], + "names": [ "Etla Zapotec", "Tisa", "Tizha", "Western Ixtlán Zapotec", "Yareni Zapotec", "Zapoteco Serrano del Oeste Medio", "Zapoteco de Santa Ana Yareni", "Zapoteco de Teococuilco de Marcos Pérez" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131662,8 +137696,10 @@ "full": "zaf-Latn-MX", "iana": [ "Ayoquesco Zapotec" ], "iso639_3": "zaf", + "localnames": [ "Didzé" ], + "macrolang": "zap", "name": "Zapotec, Ayoquesco", - "names": [ "Ayoquesco Zapotec", "Western Ejutla Zapotec", "Zapoteco de Santa María Ayoquesco" ], + "names": [ "Ayoquesco Zapotec", "Didzé", "Dísè", "Western Ejutla Zapotec", "Zapoteco de Santa María Ayoquesco", "Zapoteco de Valles del Sur" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131782,8 +137818,10 @@ "full": "zam-Latn-MX", "iana": [ "Miahuatlán Zapotec" ], "iso639_3": "zam", + "localnames": [ "Diiste" ], + "macrolang": "zap", "name": "Zapotec, Miahuatlán", - "names": [ "Miahuatlán Zapotec", "Zapoteco de Miahuatlán" ], + "names": [ "Cuixtla Zapotec", "Diiste", "Miahuatlán Zapotec", "Zapoteco de Miahuatlán", "Zapoteco de la Sierra Sur Central" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131798,8 +137836,9 @@ "iso639_3": "zao", "localname": "Dizte chaan zha Ozolotepec", "localnames": [ "Dizte chaan zha Ozolotepec" ], + "macrolang": "zap", "name": "Zapotec, Ozolotepec", - "names": [ "Dizte chaan zha Ozolotepec", "Ozolotepec Zapotec", "Zapoteco de Ozolotepec", "diste" ], + "names": [ "Desté", "Diste", "Distèe", "Dizte chaan zha Ozolotepec", "Ozolotepec Zapotec", "Zapoteco de Ozolotepec", "Zapoteco de la Costa Noreste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131812,29 +137851,31 @@ "full": "zap-Latn-MX", "iana": [ "Zapotec" ], "iso639_3": "zap", - "localname": "diidxazá", - "localnames": [ "Diidxazá" ], - "name": "Zapotec, Isthmus", - "names": [ "Diidxazá", "Juchitán Zapotec", "Sapoteko", "Zapotec", "Zapoteco Istmeño", "Zapoteco del Istmo" ], + "localname": "Diidxazá", + "localnames": [ "Diidxazá", "Didza Xhidza" ], + "name": "Zapotec", + "names": [ "Didza Xhidza", "Diidxazá", "Juchitán Zapotec", "Sapoteko", "Zapotec, Isthmus", "Zapotec, Las Delicias", "Zapoteco Istmeño", "Zapoteco de Las Delicias", "Zapoteco de la Planicie Costera", "Zapoteco del Istmo" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], "script": "Latn", "sldr": true, "tag": "zap", - "tags": [ "zai", "zai-Latn", "zai-Latn-MX", "zai-MX", "zap-Latn", "zap-MX" ], + "tags": [ "zai", "zai-Latn", "zai-Latn-MX", "zai-MX", "zap-Latn", "zap-MX", "zcd", "zcd-Latn", "zcd-Latn-MX", "zcd-MX" ], "windows": "zap-Latn" }, { "full": "zaq-Latn-MX", "iana": [ "Aloápam Zapotec" ], "iso639_3": "zaq", + "localnames": [ "Tisa" ], + "macrolang": "zap", "name": "Zapotec, Aloápam", - "names": [ "Aloápam Zapotec", "Zapoteco de Aloápam" ], + "names": [ "Aloápam Zapotec", "Tisa", "Tizha’", "Zapoteco de Aloápam", "Zapoteco de San Miguel Aloápam" ], "region": "MX", "regionname": "Mexico", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "zaq", "tags": [ "zaq-Latn", "zaq-MX" ], "windows": "zaq-Latn" @@ -131843,10 +137884,11 @@ "full": "zar-Latn-MX", "iana": [ "Rincón Zapotec" ], "iso639_3": "zar", - "localname": "didza' xidza'", - "localnames": [ "didza' xidza'" ], + "localname": "didzaʼ xidzaʼ", + "localnames": [ "Didza Xidza" ], + "macrolang": "zap", "name": "Zapotec, Rincón", - "names": [ "Northern Villa Alta Zapotec", "Rincón Zapotec", "San Juan Yaeé Zapotec", "Zapoteco de Yagallo", "Zapoteco del Rincón", "didza’ xidza’" ], + "names": [ "Didsaˀ Shidsaˀ", "Didza Xidza", "Northern Villa Alta Zapotec", "Rincón Zapotec", "San Juan Yaeé Zapotec", "Tidza’", "Zapoteco Serrano del Noroeste Bajo", "Zapoteco de Yagallo", "Zapoteco del Rincón" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131860,9 +137902,10 @@ "iana": [ "Santo Domingo Albarradas Zapotec" ], "iso639_3": "zas", "localname": "Dihidx Bilyáhab", - "localnames": [ "Dihidx Bilyáhab" ], + "localnames": [ "Diidxzaj" ], + "macrolang": "zap", "name": "Zapotec, Santo Domingo Albarradas", - "names": [ "Albarradas Zapotec", "Dihidx Bilyáhab", "Santo Domingo Albarradas Zapotec", "Zapoteco de Santo Domingo Albarradas" ], + "names": [ "Albarradas Zapotec", "Didxazá", "Dihidx Bilyáhab", "Diidxzaj", "Santo Domingo Albarradas Zapotec", "Zapoteco de Santo Domingo Albarradas", "Zapoteco de Valles del Noreste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131875,8 +137918,9 @@ "full": "zat-Latn-MX", "iana": [ "Tabaa Zapotec" ], "iso639_3": "zat", + "macrolang": "zap", "name": "Zapotec, Tabaa", - "names": [ "Central Villa Alta Zapotec", "Tabaa Zapotec", "Zapoteco de Tabaa" ], + "names": [ "Central Villa Alta Zapotec", "Dilla xhon", "Diya dxon", "Tabaa Zapotec", "Zapoteco Serrano del Sureste Alto", "Zapoteco de Tabaa" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131899,14 +137943,29 @@ "tags": [ "zau-IN", "zau-Tibt" ], "windows": "zau-Tibt" }, + { + "full": "zau-Arab-IN", + "iana": [ "Zangskari" ], + "iso639_3": "zau", + "name": "Zangskari", + "names": [ "Zanskari", "Zaskari" ], + "nophonvars": true, + "region": "IN", + "regionname": "India", + "script": "Arab", + "sldr": false, + "tag": "zau-Arab", + "windows": "zau-Arab" + }, { "full": "zav-Latn-MX", "iana": [ "Yatzachi Zapotec" ], "iso639_3": "zav", "localname": "Dižə'əxon", "localnames": [ "Dižə'əxon" ], + "macrolang": "zap", "name": "Zapotec, Yatzachi", - "names": [ "Diosənꞌ", "Diozənꞌ", "Dižə’əxon", "Villa Alta Zapotec", "Yatzachi Zapotec", "Zapoteco de Yatzachi" ], + "names": [ "Didza shon", "Dilla xhon", "Dille xhon", "Diosənꞌ", "Diozənꞌ", "Dižə’əxon", "Villa Alta Zapotec", "Yatzachi Zapotec", "Zapoteco de Yatzachi" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -131922,8 +137981,9 @@ "iso639_3": "zaw", "localname": "Didxsaj", "localnames": [ "Didxsaj" ], + "macrolang": "zap", "name": "Zapotec, Mitla", - "names": [ "Didxsaj", "East Central Tlacolula Zapotec", "East Valley Zapotec", "Mitla Zapotec" ], + "names": [ "Dichsah", "Didxsaj", "Dixdà", "East Central Tlacolula Zapotec", "East Valley Zapotec", "Mitla Zapotec", "Zapoteco de Valles del Este Medio", "Zapoteco de Valles del Noreste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131936,8 +137996,9 @@ "full": "zax-Latn-MX", "iana": [ "Xadani Zapotec" ], "iso639_3": "zax", + "macrolang": "zap", "name": "Zapotec, Xadani", - "names": [ "Eastern Pochutla Zapotec", "Xadani Zapotec", "Zapoteco de Santa María Xadani" ], + "names": [ "Ditsa’", "Eastern Pochutla Zapotec", "Xadani Zapotec", "Zapoteco de Santa María Xadani", "Zapoteco de la Costa Este" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -131953,7 +138014,7 @@ "iso639_3": "zay", "localname": "Zaysite", "localnames": [ "Zaysite" ], - "name": "Zaysete", + "name": "Zayse", "region": "ET", "regionname": "Ethiopia", "script": "Latn", @@ -131966,9 +138027,8 @@ "full": "zay-Ethi-ET", "iana": [ "Zayse-Zergulla", "Zaysete" ], "iso639_3": "zay", - "name": "Zaysete", + "name": "Zayse", "nophonvars": true, - "obsolete": true, "region": "ET", "regionname": "Ethiopia", "script": "Ethi", @@ -131992,16 +138052,17 @@ "windows": "zaz-Latn" }, { - "full": "zba-Zyyy-001", + "full": "zba-Arab-001", + "iana": [ "Balaibalan" ], "iso639_3": "zba", "name": "Balaibalan", "region": "001", "regionname": "World", - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "zba", - "tags": [ "zba-001", "zba-Zyyy" ], - "windows": "zba-Zyyy" + "tags": [ "zba-001", "zba-Arab" ], + "windows": "zba-Arab" }, { "full": "zbc-Latn-MY", @@ -132064,6 +138125,21 @@ "unwritten": true, "windows": "zbt-Latn" }, + { + "full": "zbu-Latn-NG", + "iana": [ "Bu (Bauchi State)" ], + "iso639_3": "zbu", + "name": "Bu", + "names": [ "Bu (Bauchi State)", "Buu", "Zaranda", "Zarandawa" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "zbu", + "tags": [ "zbu-Latn", "zbu-NG" ], + "unwritten": true, + "windows": "zbu-Latn" + }, { "full": "zbw-Latn-MY", "iana": [ "West Berawan" ], @@ -132082,8 +138158,10 @@ "full": "zca-Latn-MX", "iana": [ "Coatecas Altas Zapotec" ], "iso639_3": "zca", + "localnames": [ "Diꞌzhzë" ], + "macrolang": "zap", "name": "Zapotec, Coatecas Altas", - "names": [ "Coatecas Altas Zapotec", "Zapoteco de San Juan Coatecas Altas" ], + "names": [ "Coatecas Altas Zapotec", "Didzé", "Dizhze", "Diꞌzhzë", "Diꞌzhzë cheꞌn men Latyezh", "Zapoteco de San Juan Coatecas Altas", "Zapoteco de Valles del Sur" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -132096,6 +138174,7 @@ "full": "zch-Hani-CN", "iana": [ "Central Hongshuihe Zhuang" ], "iso639_3": "zch", + "macrolang": "za", "name": "Zhuang, Central Hongshuihe", "names": [ "Central Hongshuihe Zhuang" ], "region": "CN", @@ -132170,6 +138249,7 @@ "full": "zeh-Hani-CN", "iana": [ "Eastern Hongshuihe Zhuang" ], "iso639_3": "zeh", + "macrolang": "za", "name": "Zhuang, Eastern Hongshuihe", "names": [ "Eastern Hongshuihe Zhuang" ], "region": "CN", @@ -132180,6 +138260,20 @@ "tags": [ "zeh-CN", "zeh-Hani" ], "windows": "zeh-Hani" }, + { + "full": "zem-Latn-NG", + "iso639_3": "zem", + "name": "Zeem", + "names": [ "Zaimanci", "Zaimawa" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "zem", + "tags": [ "zem-Latn", "zem-NG", "zua", "zua-Latn", "zua-Latn-NG", "zua-NG" ], + "unwritten": true, + "windows": "zem-Latn" + }, { "full": "zen-Tfng-MR", "iana": [ "Zenaga" ], @@ -132216,10 +138310,10 @@ "full": "zga-Latn-TZ", "iana": [ "Kinga" ], "iso639_3": "zga", - "localname": "Bakinga", - "localnames": [ "Bakinga" ], + "localname": "Kɨkɨnga", + "localnames": [ "Kɨkɨnga" ], "name": "Kinga", - "names": [ "Bakinga", "Ekikinga", "Kikinga" ], + "names": [ "Bakinga", "Ekikinga", "Kikinga", "Kɨkɨnga" ], "region": "TZ", "regionname": "United Republic of Tanzania", "script": "Latn", @@ -132232,6 +138326,7 @@ "full": "zgb-Hani-CN", "iana": [ "Guibei Zhuang" ], "iso639_3": "zgb", + "macrolang": "za", "name": "Zhuang, Guibei", "names": [ "Guibei Zhuang" ], "region": "CN", @@ -132263,6 +138358,7 @@ "full": "zgm-Hani-CN", "iana": [ "Minz Zhuang" ], "iso639_3": "zgm", + "macrolang": "za", "name": "Zhuang, Minz", "names": [ "Black Zhuang", "Bu Xiong", "Heiyi Zhuang", "Kon Min", "Minz Zhuang", "Sung", "Zong Zhuang" ], "region": "CN", @@ -132279,6 +138375,7 @@ "iso639_3": "zgn", "latnnames": [ "Zhuàngyǔ" ], "localnames": [ "壮语" ], + "macrolang": "za", "name": "Zhuang, Guibian", "names": [ "Buyei", "Buyoi", "Guibian Zhuang", "Kang Yei", "Northern Zhuang", "Vahcuengh" ], "region": "CN", @@ -132315,6 +138412,21 @@ "tag": "zh-AU", "windows": "zh-Hant-AU" }, + { + "full": "zh-Arab-CN", + "iana": [ "Chinese" ], + "iso639_3": "zho", + "name": "Chinese", + "names": [ "Beifang Fangyan", "Beijinghua", "Chinese, Mandarin", "Mandarin", "Northern Chinese", "Standard Chinese", "Zhongguohua" ], + "nophonvars": true, + "region": "CN", + "regionname": "China", + "script": "Arab", + "sldr": false, + "tag": "zh-Arab", + "tags": [ "cmn-Arab", "cmn-Arab-CN" ], + "windows": "zh-Arab-CN" + }, { "full": "zh-Hant-BN", "iana": [ "Chinese" ], @@ -132331,8 +138443,8 @@ "full": "zh-Bopo-TW", "iana": [ "Chinese" ], "iso639_3": "zho", - "name": "Chinese, Mandarin", - "names": [ "Beifang Fangyan", "Beijinghua", "Chinese", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], + "name": "Chinese", + "names": [ "Beifang Fangyan", "Beijinghua", "Chinese, Mandarin", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], "nophonvars": true, "region": "TW", "regionname": "China-Taiwan", @@ -132341,12 +138453,13 @@ "sldr": false, "tag": "zh-Bopo", "tags": [ "cmn-Bopo", "cmn-Bopo-TW" ], - "windows": "zh-Bopo" + "windows": "zh-Bopo-TW" }, { "full": "zh-Brai-CN", "iana": [ "Chinese" ], "iso639_3": "cmn", + "macrolang": "zh", "name": "Chinese, Mandarin", "names": [ "Beifang Fangyan", "Beijinghua", "Chinese", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], "nophonvars": true, @@ -132357,23 +138470,37 @@ "sldr": false, "tag": "zh-Brai", "tags": [ "cmn-Brai", "cmn-Brai-CN" ], - "windows": "zh-Brai" + "windows": "zh-Brai-CN" }, { "full": "zh-Hans-CN", "iana": [ "Chinese" ], "iso639_3": "zho", "localname": "中文", - "name": "Chinese, Mandarin", - "names": [ "Beifang Fangyan", "Beijinghua", "Chinese", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], + "name": "Chinese", + "names": [ "Beifang Fangyan", "Beijinghua", "Chinese, Mandarin", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], "region": "CN", "regionname": "China", - "regions": [ "AU", "BN", "BS", "CA", "GB", "GQ", "ID", "KH", "KR", "LA", "LY", "MM", "MN", "MY", "NL", "NZ", "PH", "RU", "TH", "TW", "US", "VN", "WS", "ZA", "ZM" ], + "regions": [ "AU", "BN", "BS", "CA", "GB", "GQ", "ID", "KH", "KR", "LA", "LY", "MM", "MN", "MU", "MY", "NL", "NZ", "PH", "RU", "TH", "TW", "US", "VN", "WS", "ZA", "ZM" ], "script": "Hans", "sldr": true, "tag": "zh-CN", "tags": [ "cmn", "cmn-CN", "cmn-Hans", "cmn-Hans-CN", "zh", "zh-Hans", "zh-cmn", "zh-cmn-CN" ], - "windows": "zh-Hans" + "windows": "zh-Hans-CN" + }, + { + "full": "zh-Hant-ES", + "iana": [ "Chinese" ], + "iso639_3": "cmn", + "macrolang": "zh", + "name": "Chinese, Mandarin", + "names": [ "Chinese" ], + "region": "ES", + "regionname": "Spain", + "script": "Hant", + "sldr": false, + "tag": "zh-ES", + "windows": "zh-Hant-ES" }, { "full": "zh-Hant-GB", @@ -132423,7 +138550,7 @@ "script": "Hanb", "sldr": false, "tag": "zh-Hanb", - "windows": "zh-Hanb" + "windows": "zh-Hanb-TW" }, { "full": "zh-Hans-HK", @@ -132467,17 +138594,17 @@ "full": "zh-Latn-CN", "iana": [ "Chinese" ], "iso639_3": "zho", - "name": "Chinese, Mandarin", - "names": [ "Beifang Fangyan", "Beijinghua", "Chinese", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], + "name": "Chinese", + "names": [ "Beifang Fangyan", "Beijinghua", "Chinese, Mandarin", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], "region": "CN", "regionname": "China", - "regions": [ "AU", "BN", "BS", "CA", "GB", "GQ", "HK", "ID", "KH", "KR", "LA", "LY", "MM", "MN", "MO", "MY", "NL", "NZ", "PH", "RU", "SG", "TH", "TW", "US", "VN", "WS", "ZA", "ZM" ], + "regions": [ "AU", "BN", "BS", "CA", "ES", "GB", "GQ", "HK", "ID", "KH", "KR", "LA", "LY", "MM", "MN", "MO", "MY", "NL", "NZ", "PH", "RU", "SG", "TH", "TW", "US", "VN", "WS", "ZA", "ZM" ], "script": "Latn", "sldr": false, "tag": "zh-Latn", "tags": [ "cmn-Latn", "cmn-Latn-CN" ], "variants": [ "pinyin", "wadegile" ], - "windows": "zh-Latn" + "windows": "zh-Latn-CN" }, { "full": "zh-Hant-MO", @@ -132549,11 +138676,11 @@ "obsolete": true, "region": "CN", "regionname": "China", - "regions": [ "ID", "MY", "TH", "US" ], + "regions": [ "ID", "MY", "TH", "US", "VN" ], "script": "Phag", "sldr": false, "tag": "zh-Phag", - "windows": "zh-Phag" + "windows": "zh-Phag-CN" }, { "full": "zh-Hans-SG", @@ -132597,8 +138724,8 @@ "iana": [ "Chinese" ], "iso639_3": "zho", "localname": "繁體中文", - "name": "Chinese, Mandarin", - "names": [ "Beifang Fangyan", "Beijinghua", "Chinese", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], + "name": "Chinese", + "names": [ "Beifang Fangyan", "Beijinghua", "Chinese, Mandarin", "Hoton", "Hui", "Hui-Zu", "Hytad", "Khoton", "Mandarin", "Northern Chinese", "Qotong", "Standard Chinese", "Tayok", "Xui", "Zhongguohua" ], "region": "TW", "regionname": "China-Taiwan", "regions": [ "BS", "CA", "CN", "GQ", "KH", "KR", "LA", "LY", "MM", "MN", "NL", "NZ", "RU", "SG", "WS", "ZA", "ZM" ], @@ -132651,22 +138778,23 @@ "full": "zhd-Hani-CN", "iana": [ "Dai Zhuang" ], "iso639_3": "zhd", + "macrolang": "za", "name": "Zhuang, Dai", "names": [ "Bu Dai", "Dai Zhuang", "Kau Ndae", "Khaau Daai", "Thu Lao", "Tu", "Tuliao", "Tuzu", "Wen-Ma Southern Zhuang", "Zhuangyu Nanbu Fangyan Wen-Ma Tuyu", "Zhuangyu Nanbu fangyan Dejing tuyu" ], - "nophonvars": true, "region": "CN", "regionname": "China", "regions": [ "VN" ], "script": "Hani", "sldr": false, - "tag": "zhd-Hani", - "tags": [ "zhd-CN" ], - "windows": "zhd-Hani" + "tag": "zhd", + "tags": [ "zhd-CN", "zhd-Hani" ], + "windows": "zhd-Hani-CN" }, { "full": "zhd-Latn-VN", "iana": [ "Dai Zhuang" ], "iso639_3": "zhd", + "macrolang": "za", "name": "Zhuang, Dai", "names": [ "Bu Dai", "Dai Zhuang", "Kau Ndae", "Khaau Daai", "Thu Lao", "Tu", "Tuliao", "Tuzu", "Wen-Ma Southern Zhuang", "Zhuangyu Nanbu Fangyan Wen-Ma Tuyu", "Zhuangyu Nanbu fangyan Dejing tuyu" ], "obsolete": true, @@ -132677,7 +138805,7 @@ "sldr": false, "tag": "zhd-Latn", "tags": [ "zhd-VN" ], - "windows": "zhd-Latn" + "windows": "zhd-Latn-VN" }, { "full": "zhi-Latn-NG", @@ -132695,32 +138823,35 @@ "windows": "zhi-Latn" }, { - "full": "zhn-Hani-CN", + "full": "zhn-Latn-CN", "iana": [ "Nong Zhuang" ], "iso639_3": "zhn", + "localnames": [ "kauq Daez", "kauq Noangz" ], + "macrolang": "za", "name": "Zhuang, Nong", "names": [ "Daez", "Kau Nong", "Khau Nong", "Noangx", "Nong Zhuang", "Nong hua", "Phu Nong", "Phu Tei", "Tei Nong", "Yan-Guang Southern Zhuang", "Zhuangyu Nanbu fanyan Yan-Guang tuyu", "kauq Daez", "kauq Noangz", "puj Daez", "puj Noangz", "puj Nuangz" ], - "nophonvars": true, "region": "CN", "regionname": "China", - "script": "Hani", + "script": "Latn", "sldr": false, - "tag": "zhn-Hani", - "windows": "zhn-Hani" + "tag": "zhn", + "tags": [ "zhn-CN", "zhn-Latn" ], + "windows": "zhn-Latn" }, { - "full": "zhn-Latn-CN", + "full": "zhn-Hani-CN", "iana": [ "Nong Zhuang" ], "iso639_3": "zhn", - "localnames": [ "kauq Daez", "kauq Noangz" ], + "macrolang": "za", "name": "Zhuang, Nong", "names": [ "Daez", "Kau Nong", "Khau Nong", "Noangx", "Nong Zhuang", "Nong hua", "Phu Nong", "Phu Tei", "Tei Nong", "Yan-Guang Southern Zhuang", "Zhuangyu Nanbu fanyan Yan-Guang tuyu", "kauq Daez", "kauq Noangz", "puj Daez", "puj Noangz", "puj Nuangz" ], + "nophonvars": true, "region": "CN", "regionname": "China", - "script": "Latn", + "script": "Hani", "sldr": false, - "tag": "zhn-Latn", - "windows": "zhn-Latn" + "tag": "zhn-Hani", + "windows": "zhn-Hani" }, { "full": "zhw-Latn-CM", @@ -132796,7 +138927,7 @@ "iana": [ "Zialo" ], "iso639_3": "zil", "name": "Zialo", - "names": [ "Shialu", "Ziolo", "Ziyolo" ], + "names": [ "Shialu", "Zialu", "Ziolo", "Ziyolo" ], "region": "GN", "regionname": "Guinea", "script": "Latn", @@ -132835,20 +138966,6 @@ "tags": [ "zin-Latn", "zin-TZ" ], "windows": "zin-Latn" }, - { - "full": "zir-Latn-NG", - "iana": [ "Ziriya" ], - "iso639_3": "zir", - "name": "Ziriya", - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "zir", - "tags": [ "zir-Latn", "zir-NG" ], - "unwritten": true, - "windows": "zir-Latn" - }, { "full": "ziw-Latn-TZ", "iana": [ "Zigula" ], @@ -132894,20 +139011,6 @@ "unwritten": true, "windows": "zka-Latn" }, - { - "full": "zkb-Cyrl-RU", - "iana": [ "Koibal" ], - "iso639_3": "zkb", - "name": "Koibal", - "region": "RU", - "regionname": "Russian Federation", - "script": "Cyrl", - "sldr": false, - "tag": "zkb", - "tags": [ "zkb-Cyrl", "zkb-RU" ], - "unwritten": true, - "windows": "zkb-Cyrl" - }, { "full": "zkd-Latn-MM", "iana": [ "Kadu" ], @@ -132955,6 +139058,7 @@ "iana": [ "Karankawa" ], "iso639_3": "zkk", "name": "Karankawa", + "names": [ "Clamcoches", "Clamcoet", "Clamcoëhs", "Clamcoëts", "Keles" ], "region": "US", "regionname": "United States", "script": "Zyyy", @@ -132984,12 +139088,13 @@ "iana": [ "Kott" ], "iso639_3": "zko", "name": "Kott", + "names": [ "Kott-Assan" ], "region": "RU", "regionname": "Russian Federation", "script": "Cyrl", "sldr": false, "tag": "zko", - "tags": [ "zko-Cyrl", "zko-RU" ], + "tags": [ "xss", "xss-Cyrl", "xss-Cyrl-RU", "xss-RU", "zko-Cyrl", "zko-RU" ], "unwritten": true, "windows": "zko-Cyrl" }, @@ -133013,7 +139118,7 @@ "iana": [ "Zakhring" ], "iso639_3": "zkr", "name": "Zakhring", - "names": [ "Charumba", "Eastern Mishmi", "Meyor", "Zaiwa", "Zha" ], + "names": [ "Charumba", "Eastern Mishmi", "Meye", "Meyor", "M’ye", "Songgu Zhahua", "Zaiwa", "Zha" ], "region": "CN", "regionname": "China", "regions": [ "IN" ], @@ -133024,21 +139129,21 @@ "windows": "zkr-Zyyy" }, { - "full": "zkt-Kitl-CN", + "full": "zkt-Kits-CN", "iana": [ "Kitan" ], "iso639_3": "zkt", "name": "Kitan", - "nophonvars": true, "region": "CN", "regionname": "China", "regions": [ "MN" ], - "script": "Kitl", + "script": "Kits", "sldr": false, - "tag": "zkt-Kitl", - "windows": "zkt-Kitl" + "tag": "zkt", + "tags": [ "zkt-CN", "zkt-Kits" ], + "windows": "zkt-Kits" }, { - "full": "zkt-Kits-CN", + "full": "zkt-Kitl-CN", "iana": [ "Kitan" ], "iso639_3": "zkt", "name": "Kitan", @@ -133046,10 +139151,10 @@ "region": "CN", "regionname": "China", "regions": [ "MN" ], - "script": "Kits", + "script": "Kitl", "sldr": false, - "tag": "zkt-Kits", - "windows": "zkt-Kits" + "tag": "zkt-Kitl", + "windows": "zkt-Kitl" }, { "full": "zku-Latn-AU", @@ -133071,6 +139176,7 @@ "iana": [ "Krevinian" ], "iso639_3": "zkv", "name": "Krevinian", + "names": [ "Krevin", "Krieviņu dialekts" ], "region": "LV", "regionname": "Latvia", "script": "Zyyy", @@ -133093,10 +139199,26 @@ "unwritten": true, "windows": "zkz-Cyrl" }, + { + "full": "zla-Latn-CD", + "iana": [ "Zula" ], + "iso639_3": "zla", + "name": "Zula", + "names": [ "Ba-Shuula", "Kishuula", "Shuula", "Wa-Zula", "Zura" ], + "region": "CD", + "regionname": "The Democratic Republic of the Congo", + "script": "Latn", + "sldr": false, + "tag": "zla", + "tags": [ "zla-CD", "zla-Latn" ], + "unwritten": true, + "windows": "zla-Latn" + }, { "full": "zlj-Hani-CN", "iana": [ "Liujiang Zhuang" ], "iso639_3": "zlj", + "macrolang": "za", "name": "Zhuang, Liujiang", "names": [ "Liujiang Zhuang" ], "region": "CN", @@ -133111,6 +139233,7 @@ "full": "zlj-Latn-CN", "iana": [ "Liujiang Zhuang" ], "iso639_3": "zlj", + "macrolang": "za", "name": "Zhuang, Liujiang", "names": [ "Liujiang Zhuang" ], "obsolete": true, @@ -133126,6 +139249,7 @@ "iana": [ "Malay (individual language)" ], "iso639_3": "zlm", "localnames": [ "Bahasa Melayu" ], + "macrolang": "ms", "name": "Malay", "names": [ "Bahasa Daerah", "Bahasa Melayu", "Colloquial Malay", "Informal Malay", "Local Malay", "Malay (individual language)", "Malayu" ], "region": "TG", @@ -133135,14 +139259,16 @@ "sldr": false, "tag": "zlm", "tags": [ "ms-zlm", "ms-zlm-TG", "zlm-Latn", "zlm-TG" ], - "windows": "zlm-Latn" + "windows": "zlm-Latn-TG" }, { "full": "zlm-Arab-MY", "iana": [ "Malay (individual language)" ], "iso639_3": "zlm", "latnnames": [ "Melayu" ], + "localname": "ملايو", "localnames": [ "ملايو" ], + "macrolang": "ms", "name": "Malay", "names": [ "Bahasa Daerah", "Bahasa Melayu", "Colloquial Malay", "Informal Malay", "Local Malay", "Malay (individual language)", "Malayu" ], "nophonvars": true, @@ -133153,12 +139279,13 @@ "sldr": true, "tag": "zlm-Arab", "tags": [ "ms-zlm-MY", "zlm-MY" ], - "windows": "zlm-Arab" + "windows": "zlm-Arab-MY" }, { "full": "zln-Hani-CN", "iana": [ "Lianshan Zhuang" ], "iso639_3": "zln", + "macrolang": "za", "name": "Zhuang, Lianshan", "names": [ "Lianshan", "Lianshan Zhuang" ], "region": "CN", @@ -133173,6 +139300,7 @@ "full": "zlq-Hani-CN", "iana": [ "Liuqian Zhuang" ], "iso639_3": "zlq", + "macrolang": "za", "name": "Zhuang, Liuqian", "names": [ "Liuqian Zhuang" ], "region": "CN", @@ -133183,6 +139311,21 @@ "tags": [ "zlq-CN", "zlq-Hani" ], "windows": "zlq-Hani" }, + { + "full": "zlu-Latn-NG", + "iso639_3": "zlu", + "localnames": [ "Bi Zule", "Zul" ], + "name": "Zul", + "names": [ "Bi Zule", "Man Zule", "Zulawa", "Zull", "Zulle" ], + "region": "NG", + "regionname": "Nigeria", + "script": "Latn", + "sldr": false, + "tag": "zlu", + "tags": [ "zlu-Latn", "zlu-NG" ], + "unwritten": true, + "windows": "zlu-Latn" + }, { "full": "zma-Latn-AU", "iana": [ "Manda (Australia)" ], @@ -133263,7 +139406,7 @@ "iana": [ "Mfinu" ], "iso639_3": "zmf", "name": "Mfinu", - "names": [ "Emfinu", "Funika", "Mfununga" ], + "names": [ "Emfinu", "Funika", "Infinu", "Mfununga" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -133306,6 +139449,7 @@ "full": "zmi-Latn-MY", "iana": [ "Negeri Sembilan Malay" ], "iso639_3": "zmi", + "macrolang": "ms", "name": "Negeri Sembilan Malay", "names": [ "Malaysian Minangkabau", "Orang Negeri", "Ulu Muar Malay" ], "region": "MY", @@ -133412,8 +139556,9 @@ "full": "zmp-Latn-CD", "iana": [ "Mpuono" ], "iso639_3": "zmp", - "name": "Mpuono", - "names": [ "Mbuun" ], + "localnames": [ "Mbuun" ], + "name": "Mbuun", + "names": [ "Ambuun", "Embuun", "Gimbunda", "Kibunda", "Kimbuun", "Mbunda", "Mpuono", "Mpuun" ], "region": "CD", "regionname": "The Democratic Republic of the Congo", "script": "Latn", @@ -133594,7 +139739,6 @@ "iana": [ "Zande (individual language)" ], "iso639_3": "zne", "localname": "Pa-Zande", - "localnames": [ "Pa-Zande" ], "name": "Zande", "names": [ "Asande", "Azande", "Badjande", "Bazande", "Bazenda", "Kizande", "Pa-Dio", "Pa-Zande", "Pazande", "Sande", "Zande (individual language)", "Zandi", "pa Zande", "paZande" ], "region": "CD", @@ -133604,7 +139748,7 @@ "sldr": true, "tag": "zne", "tags": [ "zne-CD", "zne-Latn" ], - "windows": "zne-Latn" + "windows": "zne-Latn-CD" }, { "full": "zne-Arab-SS", @@ -133620,14 +139764,14 @@ "sldr": false, "tag": "zne-Arab", "tags": [ "zne-SS" ], - "windows": "zne-Arab" + "windows": "zne-Arab-SS" }, { "full": "zng-Latn-VN", "iana": [ "Mang" ], "iso639_3": "zng", "name": "Mang", - "names": [ "Ba’e", "Chaman", "Lá Vàng", "Manbu", "Mang U", "Nieng Ó", "Phang", "Xamang", "Xá", "Xá Lá Vàng", "Xá Mang", "Xá Ó" ], + "names": [ "Ba’e", "Chaman", "Lá Vàng", "Manbu", "Mang U", "Nieng Ó", "Xamang", "Xá", "Xá Lá Vàng", "Xá Mang", "Xá Ó" ], "region": "VN", "regionname": "Viet Nam", "regions": [ "CN", "TH" ], @@ -133658,6 +139802,7 @@ "iana": [ "Mangas" ], "iso639_3": "zns", "name": "Mangas", + "names": [ "Mantsi", "Ma’as", "Pyik Mantsi" ], "region": "NG", "regionname": "Nigeria", "script": "Latn", @@ -133703,7 +139848,7 @@ "iso639_3": "zom", "localnames": [ "Zokam" ], "name": "Zou", - "names": [ "Jou", "Yo", "Yos", "Zo", "Zohâm", "Zokam", "Zome", "Zomi", "Zou Chin", "Zoukam" ], + "names": [ "Jou", "Yo", "Yos", "Zo", "Zohâm", "Zokam", "Zome", "Zou Chin", "Zoukam" ], "region": "IN", "regionname": "India", "regions": [ "MM" ], @@ -133717,8 +139862,9 @@ "full": "zoo-Latn-MX", "iana": [ "Asunción Mixtepec Zapotec" ], "iso639_3": "zoo", + "macrolang": "zap", "name": "Zapotec, Asunción Mixtepec", - "names": [ "Asunción Mixtepec Zapotec", "North Central Zimatlan Zapotec", "Zapoteco de Asunción Mixtepec" ], + "names": [ "Asunción Mixtepec Zapotec", "Dixasà", "Dizë", "North Central Zimatlan Zapotec", "Zapoteco de Asunción Mixtepec", "Zapoteco de Valles Oeste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133762,6 +139908,7 @@ "full": "zos-Latn-MX", "iana": [ "Francisco León Zoque" ], "iso639_3": "zos", + "localname": "Otetzame", "localnames": [ "Otetzame" ], "name": "Zoque, Francisco León", "names": [ "Francisco León Zoque", "Otetzame", "O’de pʌt", "Santa Magdalena Zoque", "Zoque de Francisco León" ], @@ -133777,8 +139924,10 @@ "full": "zpa-Latn-MX", "iana": [ "Lachiguiri Zapotec" ], "iso639_3": "zpa", + "localnames": [ "Didxsá" ], + "macrolang": "zap", "name": "Zapotec, Lachiguiri", - "names": [ "Lachiguiri Zapotec", "Northwestern Tehuantepec Zapotec", "Zapoteco de Santiago Lachiguiri" ], + "names": [ "Didxsá", "Ditsá", "Lachiguiri Zapotec", "Northwestern Tehuantepec Zapotec", "Zapoteco de Santiago Lachiguiri", "Zapoteco de la Montaña del Istmo Alto" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133791,8 +139940,9 @@ "full": "zpb-Latn-MX", "iana": [ "Yautepec Zapotec" ], "iso639_3": "zpb", + "macrolang": "zap", "name": "Zapotec, Yautepec", - "names": [ "Northwestern Yautepec Zapotec", "Yautepec Zapotec", "Zapoteco de San Bartolo Yautepec" ], + "names": [ "Di’izhdë", "Northwestern Yautepec Zapotec", "Yautepec Zapotec", "Zapoteco de San Bartolo Yautepec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133806,8 +139956,10 @@ "full": "zpc-Latn-MX", "iana": [ "Choapan Zapotec" ], "iso639_3": "zpc", + "localnames": [ "Diꞌidzaꞌ xidzaꞌ" ], + "macrolang": "zap", "name": "Zapotec, Choapan", - "names": [ "Choapan Zapotec", "Zapoteco de Choapan", "Zapoteco de San Juan Comaltepec" ], + "names": [ "Choapan Zapotec", "Choápam Zapotec", "Diza shisa’", "Diza shitsá", "Diꞌidzaꞌ xidzaꞌ", "Zapoteco de Choapan", "Zapoteco de San Juan Comaltepec", "Zapoteco del Oeste de Tuxtepec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133820,8 +139972,9 @@ "full": "zpd-Latn-MX", "iana": [ "Southeastern Ixtlán Zapotec" ], "iso639_3": "zpd", + "macrolang": "zap", "name": "Zapotec, Southeastern Ixtlán", - "names": [ "Ixtlan Zapotec", "Latuvi Zapotec", "Southeastern Ixtlán Zapotec", "Yavesía Zapotec", "Zapoteco del Sureste de Ixtlán" ], + "names": [ "Didzá", "Ixtlan Zapotec", "Latuvi Zapotec", "Southeastern Ixtlán Zapotec", "Yavesía Zapotec", "Zapoteco Serrano Bajo", "Zapoteco del Sureste de Ixtlán" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133835,8 +139988,9 @@ "full": "zpe-Latn-MX", "iana": [ "Petapa Zapotec" ], "iso639_3": "zpe", + "macrolang": "zap", "name": "Zapotec, Petapa", - "names": [ "Petapa Zapotec", "Zapoteco de Santa María Petapa" ], + "names": [ "Didxazé", "Dizazi", "Petapa Zapotec", "Zapoteco de Petapa", "Zapoteco de Santa María Petapa" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133850,9 +140004,10 @@ "full": "zpf-Latn-MX", "iana": [ "San Pedro Quiatoni Zapotec" ], "iso639_3": "zpf", - "localnames": [ "diidx zah" ], + "localnames": [ "Diidx Zah" ], + "macrolang": "zap", "name": "Zapotec, San Pedro Quiatoni", - "names": [ "Eastern Tlacolula Zapotec", "Quiatoni Zapotec", "San Pedro Quiatoni Zapotec", "Zapoteco de San Pedro Quiatoni", "diidx zah" ], + "names": [ "Dichsà", "Didzá", "Diidx Zah", "Ditsáh", "Eastern Tlacolula Zapotec", "Quiatoni Zapotec", "San Pedro Quiatoni Zapotec", "Zapoteco de San Pedro Quiatoni", "Zapoteco de Sierra Sur del Noreste Alto" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133866,9 +140021,10 @@ "iana": [ "Guevea De Humboldt Zapotec" ], "iso639_3": "zpg", "localname": "didz-rieꞌ", - "localnames": [ "didz-rieꞌ" ], + "localnames": [ "Didz-rieꞌ" ], + "macrolang": "zap", "name": "Zapotec, Guevea de Humboldt", - "names": [ "Guevea De Humboldt Zapotec", "Northern Isthmus Zapotec", "Zapoteco de Guevea de Humboldt", "didz-rieꞌ" ], + "names": [ "Didz-rieꞌ", "Ditsire", "Di’tsiri’e", "Guevea De Humboldt Zapotec", "Guevea Zapotec", "Northern Isthmus Zapotec", "Zapoteco de Guevea de Humboldt", "Zapoteco de la Montaña del Istmo Bajo" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133881,8 +140037,9 @@ "full": "zph-Latn-MX", "iana": [ "Totomachapan Zapotec" ], "iso639_3": "zph", + "macrolang": "zap", "name": "Zapotec, Totomachapan", - "names": [ "Totomachapan Zapotec", "Western Zimatlán Zapotec", "Zapoteco de San Pedro Totomachapan" ], + "names": [ "Dixasà", "Totomachapan Zapotec", "Western Zimatlán Zapotec", "Zapoteco de San Pedro Totomachapan", "Zapoteco de Zimatlán de Álvarez" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133898,6 +140055,7 @@ "iso639_3": "zpi", "localname": "disa", "localnames": [ "disa" ], + "macrolang": "zap", "name": "Zapotec, Santa María Quiegolani", "names": [ "Quiegolani Zapotec", "Santa María Quiegolani Zapotec", "Western Yautepec Zapotec", "Zapoteco de Santa María Quiegolani", "disa" ], "region": "MX", @@ -133912,8 +140070,9 @@ "full": "zpj-Latn-MX", "iana": [ "Quiavicuzas Zapotec" ], "iso639_3": "zpj", + "macrolang": "zap", "name": "Zapotec, Quiavicuzas", - "names": [ "Northeastern Yautepec Zapotec", "Quiavicuzas Zapotec", "Zapoteco de Quiavicuzas", "Zapoteco de San Juan Lachixila" ], + "names": [ "Ditsere’", "Dixhtiò", "Northeastern Yautepec Zapotec", "Quiavicuzas Zapotec", "Zapoteco de Quiavicuzas", "Zapoteco de San Juan Lachixila" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133927,8 +140086,9 @@ "full": "zpk-Latn-MX", "iana": [ "Tlacolulita Zapotec" ], "iso639_3": "zpk", + "macrolang": "zap", "name": "Zapotec, Tlacolulita", - "names": [ "Southeastern Yautepec Zapotec", "Tlacolulita Zapotec", "Zapoteco de Asunción Tlacolulita" ], + "names": [ "Southeastern Yautepec Zapotec", "Tixh nguizë", "Tlacolulita Zapotec", "Zapoteco de Asunción Tlacolulita" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133944,8 +140104,9 @@ "iso639_3": "zpl", "localname": "Dialu", "localnames": [ "Dialu" ], + "macrolang": "zap", "name": "Zapotec, Lachixío", - "names": [ "Dialu", "Eastern Sola de Vega Zapotec", "Lachixío Zapotec", "Zapoteco de Lachixío" ], + "names": [ "Beeh Dicchah Xe7yyoh", "Dialu", "Dialó", "Eastern Sola de Vega Zapotec", "Lachixío Zapotec", "Zapoteco de Lachixío", "Zapoteco de la Sierra Sur Noroeste Alto" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133959,9 +140120,10 @@ "iana": [ "Mixtepec Zapotec" ], "iso639_3": "zpm", "localname": "dìidz-zÈ", - "localnames": [ "dìidz-zÈ" ], + "localnames": [ "Diidz-zë" ], + "macrolang": "zap", "name": "Zapotec, Mixtepec", - "names": [ "Eastern Miahuatlán Zapotec", "Mixtepec Zapotec", "San Juan Mixtepec Zapoteco", "Zapoteco de San Juan Mixtepec", "diidz rii", "dìidz-zÈ" ], + "names": [ "Diidz-zë", "Diitsë", "Ditsë", "Dizde", "Dìidz-zÈ", "Eastern Miahuatlán Zapotec", "Mixtepec Zapotec", "San Juan Mixtepec Zapoteco", "Zapoteco de San Juan Mixtepec", "Zapoteco de la Sierra Sur Noreste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -133974,6 +140136,7 @@ "full": "zpn-Latn-MX", "iana": [ "Santa Inés Yatzechi Zapotec" ], "iso639_3": "zpn", + "macrolang": "zap", "name": "Zapotec, Santa Inés Yatzechi", "names": [ "Santa Inés Yatzechi Zapotec", "Southeastern Zimatlán Zapotec", "Zapoteco de Santa Inés Yatzechi", "Zapoteco de Zegache" ], "region": "MX", @@ -133989,9 +140152,10 @@ "iana": [ "Amatlán Zapotec" ], "iso639_3": "zpo", "localname": "Dizhze", - "localnames": [ "Dizhze" ], + "localnames": [ "Dizhze", "Dizhze Amatlan" ], + "macrolang": "zap", "name": "Zapotec, Amatlán", - "names": [ "Amatlán Zapotec", "Dizhze", "Zapoteco de San Cristóbal Amatlán", "Zapoteco del Noreste de Miahuatlán" ], + "names": [ "Amatlán Zapotec", "Dizhze", "Dizhze Amatlan", "Zapoteco de Mixtepec", "Zapoteco de San Cristóbal Amatlán", "Zapoteco del Noreste de Miahuatlán" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134004,8 +140168,9 @@ "full": "zpp-Latn-MX", "iana": [ "El Alto Zapotec" ], "iso639_3": "zpp", + "macrolang": "zap", "name": "Zapotec, El Alto", - "names": [ "El Alto Zapotec", "South Central Zimatlan Zapotec", "Zapoteco de San Pedro el Alto" ], + "names": [ "Dixasà", "Di’tse’e", "El Alto Zapotec", "South Central Zimatlan Zapotec", "Zapoteco de San Antonino el Alto", "Zapoteco de San Pedro el Alto", "Zapoteco de Zimatlán de Álvarez" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134021,8 +140186,9 @@ "iso639_3": "zpq", "localname": "Diža'xon", "localnames": [ "Diža'xon" ], + "macrolang": "zap", "name": "Zapotec, Zoogocho", - "names": [ "Dizha’ Xhon", "Diža’xon", "Zapoteco de San Bartolomé Zoogocho", "Zoogocho Zapotec" ], + "names": [ "Didza shon", "Dizha’ Xhon", "Diža’xon", "Zapoteco Serrano del Sureste Medio", "Zapoteco de San Bartolomé Zoogocho", "Zoogocho Zapotec" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -134036,8 +140202,10 @@ "full": "zpr-Latn-MX", "iana": [ "Santiago Xanica Zapotec" ], "iso639_3": "zpr", + "localnames": [ "Diste’" ], + "macrolang": "zap", "name": "Zapotec, Santiago Xanica", - "names": [ "Santiago Xanica Zapotec", "Xanica Zapotec" ], + "names": [ "Diste’", "Distse’e", "Santiago Xanica Zapotec", "Xanica Zapotec", "Zapoteco de la Sierra Sur del Sureste Bajo" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134050,8 +140218,9 @@ "full": "zps-Latn-MX", "iana": [ "Coatlán Zapotec" ], "iso639_3": "zps", + "macrolang": "zap", "name": "Zapotec, Coatlán", - "names": [ "Coatlán Zapotec", "San Miguel Zapotec", "Western Miahuatlán Zapotec", "Zapoteco de Santa María Coatlán" ], + "names": [ "Coatlán Zapotec", "Didxke’", "San Miguel Zapotec", "Santo Domingo Coatlán Zapotec", "Western Miahuatlán Zapotec", "Zapoteco de Santa María Coatlán", "Zapoteco de la Sierra Sur del Este Bajo" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134066,8 +140235,10 @@ "iana": [ "San Vicente Coatlán Zapotec" ], "iso639_3": "zpt", "localname": "Zapotec, San Vicente Coatlán", + "localnames": [ "Dizhnde" ], + "macrolang": "zap", "name": "Zapotec, San Vicente Coatlán", - "names": [ "Coatlán Zapotec", "San Vicente Coatlán Zapotec", "Southern Ejutla Zapotec", "Zapoteco de San Vicente Coatlán" ], + "names": [ "Coatlán Zapotec", "Didxnde", "Dizhnde", "Diꞌizhnde", "San Vicente Coatlán Zapotec", "Southern Ejutla Zapotec", "Zapoteco de San Vicente Coatlán" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134081,9 +140252,10 @@ "iana": [ "Yalálag Zapotec" ], "iso639_3": "zpu", "localname": "Dìʼll Wlhàll Yàlhálhg", - "localnames": [ "Dìʼll Wlhàll Yàlhálhg" ], + "localnames": [ "Dill Gurall ke Yarar'j" ], + "macrolang": "zap", "name": "Zapotec, Yalálag", - "names": [ "Dìʼll Wlhàll Yàlhálhg", "Yalálag Zapotec", "Zapoteco de Yalálag" ], + "names": [ "Dill Gurall ke Yarar’j", "Dilla xidza", "Diya xhon", "Dìʼll Wlhàll Yàlhálhg", "Yalálag Zapotec", "Zapoteco Serrano del Sureste", "Zapoteco de Yalálag" ], "region": "MX", "regionname": "Mexico", "regions": [ "US" ], @@ -134097,9 +140269,11 @@ "full": "zpv-Latn-MX", "iana": [ "Chichicapan Zapotec" ], "iso639_3": "zpv", + "localname": "Dxiꞌzah", "localnames": [ "Dxiꞌzah" ], + "macrolang": "zap", "name": "Zapotec, Chichicapan", - "names": [ "Chichicapan Zapotec", "Dxiꞌzah", "Eastern Ocotlán Zapotec", "Zapoteco", "Zapoteco de San Baltazar Chichicapan" ], + "names": [ "Chichicapan Zapotec", "Dxiꞌzah", "Dzhi’iza", "Eastern Ocotlán Zapotec", "Zapoteco", "Zapoteco de San Baltazar Chichicapan" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134112,8 +140286,9 @@ "full": "zpw-Latn-MX", "iana": [ "Zaniza Zapotec" ], "iso639_3": "zpw", + "macrolang": "zap", "name": "Zapotec, Zaniza", - "names": [ "Papabuco", "Western Sola de Vega Zapotec", "Zaniza Zapotec", "Zapoteco de Santa María Zaniza" ], + "names": [ "Papabuco", "Risna", "Rixhna", "Western Sola de Vega Zapotec", "Zaniza Zapotec", "Zapoteco de Santa María Zaniza", "Zapoteco de la Sierra Sur Noroeste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134126,8 +140301,10 @@ "full": "zpx-Latn-MX", "iana": [ "San Baltazar Loxicha Zapotec" ], "iso639_3": "zpx", + "localnames": [ "Diste’" ], + "macrolang": "zap", "name": "Zapotec, San Baltazar Loxicha", - "names": [ "Northwestern Pochutla Zapotec", "San Baltazar Loxicha Zapotec", "San Baltázar Loxicha Zapotec", "Zapoteco de San Baltázar Loxicha" ], + "names": [ "Diste’", "Dixhque’", "Northwestern Pochutla Zapotec", "Ristee’", "San Baltazar Loxicha Zapotec", "San Baltázar Loxicha Zapotec", "Zapoteco de San Baltázar Loxicha", "Zapoteco de la Costa Oeste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134140,8 +140317,9 @@ "full": "zpy-Latn-MX", "iana": [ "Mazaltepec Zapotec" ], "iso639_3": "zpy", + "macrolang": "zap", "name": "Zapotec, Mazaltepec", - "names": [ "Etla Zapotec", "Mazaltepec Zapotec", "Zapoteco de Santo Tomás Mazaltepec" ], + "names": [ "Etla Zapotec", "Mazaltepec Zapotec", "Tizá", "Zapoteco de Santo Tomás Mazaltepec", "Zapoteco de Valles Noroeste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134154,9 +140332,10 @@ "full": "zpz-Latn-MX", "iana": [ "Texmelucan Zapotec" ], "iso639_3": "zpz", - "localnames": [ "Rixhquei" ], + "localname": "Rixhquei", + "macrolang": "zap", "name": "Zapotec, Texmelucan", - "names": [ "Central Sola de Vega Zapotec", "Papabuco", "Rixhquei", "Texmelucan Zapotec", "Zapoteco de San Lorenzo Texmelucan" ], + "names": [ "Central Sola de Vega Zapotec", "Papabuco", "Rixhquei", "Texmelucan Zapotec", "Zapoteco de San Lorenzo Texmelucan", "Zapoteco de Texmelucan" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134169,6 +140348,7 @@ "full": "zqe-Hani-CN", "iana": [ "Qiubei Zhuang" ], "iso639_3": "zqe", + "macrolang": "za", "name": "Zhuang, Qiubei", "names": [ "Bau i", "Bui", "Buyi", "Northern Zhuang", "Qiubei Sha", "Qiubei Zhuang" ], "region": "CN", @@ -134183,6 +140363,7 @@ "full": "zqe-Latn-CN", "iana": [ "Qiubei Zhuang" ], "iso639_3": "zqe", + "macrolang": "za", "name": "Zhuang, Qiubei", "names": [ "Bau i", "Bui", "Buyi", "Northern Zhuang", "Qiubei Sha", "Qiubei Zhuang" ], "obsolete": true, @@ -134212,12 +140393,12 @@ "iso639_3": "zrg", "name": "Mirgan", "names": [ "Mirgami", "Mirkan", "Panika", "Panka" ], - "nophonvars": true, "region": "IN", "regionname": "India", "script": "Orya", "sldr": false, - "tag": "zrg-Orya", + "tag": "zrg", + "tags": [ "zrg-IN", "zrg-Orya" ], "windows": "zrg-Orya" }, { @@ -134264,17 +140445,17 @@ "windows": "zro-Latn" }, { - "full": "zrp-Zyyy-FR", + "full": "zrp-Hebr-FR", "iana": [ "Zarphatic" ], "iso639_3": "zrp", "name": "Zarphatic", "region": "FR", "regionname": "France", - "script": "Zyyy", + "script": "Hebr", "sldr": false, "tag": "zrp", - "tags": [ "zrp-FR", "zrp-Zyyy" ], - "windows": "zrp-Zyyy" + "tags": [ "zrp-FR", "zrp-Hebr" ], + "windows": "zrp-Hebr" }, { "full": "zrs-Latn-ID", @@ -134286,7 +140467,7 @@ "region": "ID", "regionname": "Indonesia", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "zrs", "tags": [ "zrs-ID", "zrs-Latn" ], "windows": "zrs-Latn" @@ -134337,8 +140518,10 @@ "full": "zsr-Latn-MX", "iana": [ "Southern Rincon Zapotec" ], "iso639_3": "zsr", + "localnames": [ "Didza'" ], + "macrolang": "zap", "name": "Zapotec, Southern Rincon", - "names": [ "Southern Rincon Zapotec", "Zapoteco de Rincón Sur" ], + "names": [ "Didza’", "Southern Rincon Zapotec", "Tidza’", "Zapoteco Serrano del Noroeste Bajo", "Zapoteco de Rincón Sur" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134366,8 +140549,9 @@ "full": "zte-Latn-MX", "iana": [ "Elotepec Zapotec" ], "iso639_3": "zte", + "macrolang": "zap", "name": "Zapotec, Elotepec", - "names": [ "Elotepec Zapotec", "Papabuco", "Zapoteco de San Juan Elotepec" ], + "names": [ "Elotepec Zapotec", "Papabuco", "Risna", "Rixhna", "Zapoteco de San Juan Elotepec", "Zapoteco de la Sierra Sur Noroeste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134381,9 +140565,10 @@ "full": "ztg-Latn-MX", "iana": [ "Xanaguía Zapotec" ], "iso639_3": "ztg", - "localnames": [ "Diidz Zë" ], + "localnames": [ "Di'tsë gu'n xne' minn", "Diꞌtsë" ], + "macrolang": "zap", "name": "Zapotec, Xanaguía", - "names": [ "Diidz Zë", "Xanaguía Zapotec", "Zapoteco de Santa Catarina Xanaguía" ], + "names": [ "Didsé", "Diidz Zë", "Ditsa", "Ditsè", "Di’tsë gu’n xne’ minn", "Diꞌtsë", "San Francisco Ozolotepec", "San Francisco Ozolotepec Zapotec", "Xanaguía Zapotec", "Zapotec of San Francisco and San José Ozolotepec", "Zapoteco de Santa Catarina Xanaguía", "Zapoteco de la Sierra Sur del Sureste Alto" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134396,8 +140581,9 @@ "full": "ztl-Latn-MX", "iana": [ "Lapaguía-Guivini Zapotec" ], "iso639_3": "ztl", + "macrolang": "zap", "name": "Zapotec, Lapaguía-Guivini", - "names": [ "Lapaguía-Guivini Zapotec", "Santiago Lapaguia Zapotec", "Zapoteco de Lapaguía-Guivini", "Zapoteco de Santiago Lapaguía" ], + "names": [ "Ditsa’", "Lapaguía Zapotec", "Lapaguía-Guivini Zapotec", "Santiago Lapaguia Zapotec", "Zapoteco de Lapaguía-Guivini", "Zapoteco de Santiago Lapaguía", "Zapoteco de la Costa Este" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134411,8 +140597,9 @@ "full": "ztm-Latn-MX", "iana": [ "San Agustín Mixtepec Zapotec" ], "iso639_3": "ztm", + "macrolang": "zap", "name": "Zapotec, San Agustín Mixtepec", - "names": [ "San Agustín Mixtepec Zapotec" ], + "names": [ "Dizde", "San Agustín Mixtepec Zapotec", "Zapoteco de Mixtepec" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134426,8 +140613,10 @@ "full": "ztn-Latn-MX", "iana": [ "Santa Catarina Albarradas Zapotec" ], "iso639_3": "ztn", + "localnames": [ "Dichsah" ], + "macrolang": "zap", "name": "Zapotec, Santa Catarina Albarradas", - "names": [ "Santa Catarina Albarradas Zapotec", "Zapoteco de Santa Catarina Albarradas" ], + "names": [ "Dichsah", "Santa Catarina Albarradas Zapotec", "Zapoteco de Santa Catarina Albarradas", "Zapoteco de Valles del Noreste" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134441,8 +140630,9 @@ "iana": [ "Loxicha Zapotec" ], "iso639_3": "ztp", "localnames": [ "Diste" ], + "macrolang": "zap", "name": "Zapotec, Loxicha", - "names": [ "Diste", "Loxicha Zapotec", "Western Pochutla Zapotec", "Zapoteco de Loxicha" ], + "names": [ "Diste", "Distèe", "Loxicha Zapotec", "Western Pochutla Zapotec", "Zapoteco de Loxicha", "Zapoteco de la Costa Central" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134456,9 +140646,10 @@ "iana": [ "Quioquitani-Quierí Zapotec" ], "iso639_3": "ztq", "localname": "tiits së", - "localnames": [ "tiits së" ], + "localnames": [ "Tiitssë" ], + "macrolang": "zap", "name": "Zapotec, Quioquitani-Quierí", - "names": [ "Quioquitani-Quierí Zapotec", "Zapoteco de Quioquitani y Quierí", "tiits së" ], + "names": [ "Distse’", "Ditsè", "Ditsëë", "Quioquitani-Quierí Zapotec", "Tiitssë", "Zapoteco de Quioquitani y Quierí", "Zapoteco de la Sierra Sur Noroeste Medio" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134471,8 +140662,10 @@ "full": "zts-Latn-MX", "iana": [ "Tilquiapan Zapotec" ], "iso639_3": "zts", + "localnames": [ "Diza" ], + "macrolang": "zap", "name": "Zapotec, Tilquiapan", - "names": [ "Tilquiapan Zapotec", "Zapoteco de San Miguel Tilquiapan" ], + "names": [ "Diza", "San Miguel Tilquiapam", "Tilquiapan Zapotec", "Zapoteco de San Miguel Tilquiapan", "Zapoteco de Valles del Centro Bajo" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134485,8 +140678,9 @@ "full": "ztt-Latn-MX", "iana": [ "Tejalapan Zapotec" ], "iso639_3": "ztt", + "macrolang": "zap", "name": "Zapotec, Tejalapan", - "names": [ "Tejalapan Zapotec", "Zapoteco de San Felipe Tejalapan", "Zapoteco de Tejalápam" ], + "names": [ "Tejalapan Zapotec", "Tizá", "Zapoteco de San Felipe Tejalapan", "Zapoteco de San Felipe Tejalápam", "Zapoteco de Tejalápam" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134500,8 +140694,10 @@ "full": "ztu-Latn-MX", "iana": [ "Güilá Zapotec" ], "iso639_3": "ztu", + "localnames": [ "Diza" ], + "macrolang": "zap", "name": "Zapotec, Güilá", - "names": [ "Güilá Zapotec", "San Dionisio Ocotepec Zapotec", "Zapoteco de San Dionisio Ocotepec", "Zapoteco de San Pablo Güilá" ], + "names": [ "Disàa", "Diza", "Güilá Zapotec", "San Dionisio Ocotepec Zapotec", "Zapoteco de San Dionisio Ocotepec", "Zapoteco de San Pablo Güilá", "Zapoteco de Valles del Este Central" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134514,8 +140710,9 @@ "full": "ztx-Latn-MX", "iana": [ "Zaachila Zapotec" ], "iso639_3": "ztx", + "macrolang": "zap", "name": "Zapotec, Zaachila", - "names": [ "San Raymundo Jalpan Zapotec", "Zaachila Zapotec" ], + "names": [ "Dixsà", "Dizá", "San Raymundo Jalpan Zapotec", "Zaachila Zapotec", "Zapoteco de Valles del Noroeste Bajo", "Zapoteco de Valles del Noroeste Medio" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134529,9 +140726,10 @@ "full": "zty-Latn-MX", "iana": [ "Yatee Zapotec" ], "iso639_3": "zty", - "localnames": [ "Didxa shiza'", "Didza shon" ], + "localname": "Didxa shiza'", + "macrolang": "zap", "name": "Zapotec, Yatee", - "names": [ "Didxa shiza’", "Didza shon", "Northern Sapoteko", "Yatee Zapotec", "Zapoteco de Yatee", "Zapoteco serrano del sureste" ], + "names": [ "Didxa shiza’", "Didza shon", "Northern Sapoteko", "Yatee Zapotec", "Zapoteco Serrano del Sureste", "Zapoteco de Yatee" ], "region": "MX", "regionname": "Mexico", "script": "Latn", @@ -134572,21 +140770,6 @@ "tag": "zu-Brai", "windows": "zu-Brai" }, - { - "full": "zua-Latn-NG", - "iana": [ "Zeem" ], - "iso639_3": "zua", - "name": "Zeem", - "names": [ "Chaari" ], - "region": "NG", - "regionname": "Nigeria", - "script": "Latn", - "sldr": false, - "tag": "zua", - "tags": [ "zua-Latn", "zua-NG" ], - "unwritten": true, - "windows": "zua-Latn" - }, { "full": "zuh-Latn-PG", "iana": [ "Tokano" ], @@ -134603,7 +140786,7 @@ "windows": "zuh-Latn" }, { - "full": "zum-Zyyy-OM", + "full": "zum-Arab-OM", "iana": [ "Kumzari" ], "iso639_3": "zum", "name": "Kumzari", @@ -134611,11 +140794,11 @@ "region": "OM", "regionname": "Oman", "regions": [ "IR" ], - "script": "Zyyy", + "script": "Arab", "sldr": false, "tag": "zum", - "tags": [ "zum-OM", "zum-Zyyy" ], - "windows": "zum-Zyyy" + "tags": [ "zum-Arab", "zum-OM" ], + "windows": "zum-Arab" }, { "full": "zun-Latn-US", @@ -134647,18 +140830,31 @@ "windows": "zuy-Latn" }, { - "full": "zwa-Zyyy-ET", + "full": "zwa-Ethi-ET", "iana": [ "Zay" ], "iso639_3": "zwa", "name": "Zay", "region": "ET", "regionname": "Ethiopia", - "script": "Zyyy", + "script": "Ethi", "sldr": false, "tag": "zwa", - "tags": [ "zwa-ET", "zwa-Zyyy" ], + "tags": [ "zwa-ET", "zwa-Ethi" ], + "unwritten": true, + "windows": "zwa-Ethi" + }, + { + "full": "zwa-Latn-ET", + "iana": [ "Zay" ], + "iso639_3": "zwa", + "name": "Zay", + "region": "ET", + "regionname": "Ethiopia", + "script": "Latn", + "sldr": false, + "tag": "zwa-Latn", "unwritten": true, - "windows": "zwa-Zyyy" + "windows": "zwa-Latn" }, { "full": "zxx-Zyyy-001", @@ -134678,10 +140874,11 @@ "full": "zyg-Hani-CN", "iana": [ "Yang Zhuang" ], "iso639_3": "zyg", - "latnnames": [ "Deijing Zhuang" ], - "localnames": [ "壮语徳靖" ], + "latnnames": [ "Yang zhuàng", "Deijing Zhuang" ], + "localnames": [ "佒壯", "壮语徳靖" ], + "macrolang": "za", "name": "Zhuang, Yang", - "names": [ "Caj coux", "Can Yang", "Dejing Zhuang", "Dianbao", "Fouh", "Gen Yang", "Jingxi Zhuang", "Lang", "Nong", "Nung Giang", "Tianpao", "Tuhua", "Yang Zhuang", "Yangx", "Yangyu", "Yangzhou", "Zhuangyu Nanbu fangyan Dejing tuyu" ], + "names": [ "Caj coux", "Can Yang", "Dejing Zhuang", "Dianbao", "Fouh", "Gen Yang", "Jingxi Zhuang", "Káang Thó", "Lang", "Nong", "Nung Giang", "Tianpao", "Tuhua", "Yang Zhuang", "Yangx", "Yangyu", "Yangzhou", "Zhuangyu Nanbu fangyan Dejing tuyu" ], "region": "CN", "regionname": "China", "regions": [ "VN" ], @@ -134691,25 +140888,11 @@ "tags": [ "zyg-CN", "zyg-Hani" ], "windows": "zyg-Hani" }, - { - "full": "zyg-Nshu-CN", - "iana": [ "Yang Zhuang" ], - "iso639_3": "zyg", - "name": "Zhuang, Yang", - "names": [ "Caj coux", "Can Yang", "Dejing Zhuang", "Dianbao", "Fouh", "Gen Yang", "Jingxi Zhuang", "Lang", "Nong", "Nung Giang", "Tianpao", "Tuhua", "Yang Zhuang", "Yangx", "Yangyu", "Yangzhou", "Zhuangyu Nanbu fangyan Dejing tuyu" ], - "nophonvars": true, - "region": "CN", - "regionname": "China", - "regions": [ "VN" ], - "script": "Nshu", - "sldr": false, - "tag": "zyg-Nshu", - "windows": "zyg-Nshu" - }, { "full": "zyj-Latn-CN", "iana": [ "Youjiang Zhuang" ], "iso639_3": "zyj", + "macrolang": "za", "name": "Zhuang, Youjiang", "names": [ "Youjiang Zhuang" ], "region": "CN", @@ -134724,6 +140907,7 @@ "full": "zyj-Hani-CN", "iana": [ "Youjiang Zhuang" ], "iso639_3": "zyj", + "macrolang": "za", "name": "Zhuang, Youjiang", "names": [ "Youjiang Zhuang" ], "nophonvars": true, @@ -134738,6 +140922,7 @@ "full": "zyn-Hani-CN", "iana": [ "Yongnan Zhuang" ], "iso639_3": "zyn", + "macrolang": "za", "name": "Zhuang, Yongnan", "names": [ "Bou Rau", "Boux Toj", "Long An", "Long’an", "Nongz Anx", "Nung An", "Southern Zhuang", "Yongnan Vernacular of the Southern Dialect of the Zhuang Language", "Yongnan Zhuang", "Zhuangyu nanbu fangyan Yongnan tuyu" ], "region": "CN", @@ -134770,12 +140955,13 @@ "full": "zza-Latn-TR", "iana": [ "Zaza", "Dimili", "Dimli (macrolanguage)", "Kirdki", "Kirmanjki (macrolanguage)", "Zazaki" ], "iso639_3": "zza", - "name": "Zazaki, Southern", - "names": [ "Dimili", "Dimli", "Dimli (macrolanguage)", "Kirdki", "Kirmanjki (macrolanguage)", "Southern Zaza", "Zaza", "Zazaca", "Zazaki" ], + "localnames": [ "Dimlî", "Zazakî" ], + "name": "Zaza", + "names": [ "Dimili", "Dimilî", "Dimli (macrolanguage)", "Dimlî", "Kirdki", "Kirmanjki (macrolanguage)", "Southern Zaza", "Zazaca", "Zazaki", "Zazaki, Southern", "Zazakî" ], "region": "TR", "regionname": "Turkey", "script": "Latn", - "sldr": false, + "sldr": true, "tag": "zza", "tags": [ "diq", "diq-Latn", "diq-Latn-TR", "diq-TR", "zza-Latn", "zza-TR" ], "windows": "zza-Latn" @@ -134784,8 +140970,8 @@ "full": "zza-Arab-TR", "iana": [ "Zaza", "Dimili", "Dimli (macrolanguage)", "Kirdki", "Kirmanjki (macrolanguage)", "Zazaki" ], "iso639_3": "zza", - "name": "Zazaki, Southern", - "names": [ "Dimili", "Dimli", "Dimli (macrolanguage)", "Kirdki", "Kirmanjki (macrolanguage)", "Southern Zaza", "Zaza", "Zazaca", "Zazaki" ], + "name": "Zaza", + "names": [ "Dimili", "Dimilî", "Dimli (macrolanguage)", "Dimlî", "Kirdki", "Kirmanjki (macrolanguage)", "Southern Zaza", "Zazaca", "Zazaki", "Zazaki, Southern", "Zazakî" ], "nophonvars": true, "region": "TR", "regionname": "Turkey", @@ -134799,6 +140985,7 @@ "full": "zzj-Hani-CN", "iana": [ "Zuojiang Zhuang" ], "iso639_3": "zzj", + "macrolang": "za", "name": "Zhuang, Zuojiang", "names": [ "Canto", "Ken Tho", "Longyin", "Longzhou", "Nongz Anx", "Nung Chao", "Pho Thai", "Potai", "Pu Tho", "Puto", "Southern Zhuang", "Zhuangyu nanbu fangyan Zuojiang tuyu", "Zuojiang Zhuang" ], "region": "CN", diff --git a/resources/standards-data/langtags/readme.md b/resources/standards-data/langtags/readme.md index de381ca7117adbfe81f6c54b92b49f1832c7c8be..f14a831ca4769d766b710f62bee5acdc7dcf848a 100644 --- a/resources/standards-data/langtags/readme.md +++ b/resources/standards-data/langtags/readme.md @@ -1,7 +1,5 @@ # langtags.json -This file is retrieved from https://ldml.api.sil.org/langtags.json - This file lists implicit script tags for language tags and equivalent BCP 47 tags per SLDR (and hence CLDR). We use this to build up good clean BCP 47 tags for adding languages to Windows. @@ -12,4 +10,3 @@ This file is intended to replace alltags, ethnologue, and iso639-3 data files in * Source repository: https://github.com/silnrsi/langtags/ * Documentation: https://github.com/silnrsi/langtags/blob/master/doc/langtags.md * Unofficial schema: https://gist.github.com/mcdurdin/eff65bb2d77034525120d48cbe280d30 - diff --git a/resources/standards-data/language-subtag-registry/language-subtag-registry b/resources/standards-data/language-subtag-registry/language-subtag-registry index b38d2c82319de58af0b48f1428340f2c2e06753c..48ba4d1d9aae193bd58a33c1d3240582c1d0e3ec 100644 --- a/resources/standards-data/language-subtag-registry/language-subtag-registry +++ b/resources/standards-data/language-subtag-registry/language-subtag-registry @@ -1,4 +1,4 @@ -File-Date: 2017-08-15 +File-Date: 2023-05-11 %% Type: language Subtag: aa @@ -378,6 +378,7 @@ Subtag: hy Description: Armenian Added: 2005-10-16 Suppress-Script: Armn +Comments: see also hyw %% Type: language Subtag: hz @@ -525,6 +526,7 @@ Suppress-Script: Latn %% Type: language Subtag: km +Description: Khmer Description: Central Khmer Added: 2005-10-16 Suppress-Script: Khmr @@ -591,6 +593,7 @@ Suppress-Script: Latn Type: language Subtag: lg Description: Ganda +Description: Luganda Added: 2005-10-16 %% Type: language @@ -957,6 +960,7 @@ Subtag: sr Description: Serbian Added: 2005-10-16 Macrolanguage: sh +Comments: see cnr for Montenegrin %% Type: language Subtag: ss @@ -1526,11 +1530,12 @@ Added: 2005-10-16 %% Type: language Subtag: adb -Description: Adabe +Description: Atauran Added: 2009-07-29 %% Type: language Subtag: add +Description: Lidzonka Description: Dzodinka Added: 2009-07-29 %% @@ -2091,6 +2096,8 @@ Type: language Subtag: ais Description: Nataoran Amis Added: 2009-07-29 +Deprecated: 2019-04-16 +Comments: see ami, szy %% Type: language Subtag: ait @@ -2114,7 +2121,7 @@ Added: 2009-07-29 %% Type: language Subtag: aja -Description: Aja (Sudan) +Description: Aja (South Sudan) Added: 2009-07-29 %% Type: language @@ -2136,12 +2143,21 @@ Type: language Subtag: ajp Description: South Levantine Arabic Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: apc Macrolanguage: ar %% Type: language +Subtag: ajs +Description: Algerian Jewish Sign Language +Added: 2022-02-25 +%% +Type: language Subtag: ajt Description: Judeo-Tunisian Arabic Added: 2009-07-29 +Deprecated: 2022-02-25 +Preferred-Value: aeb Macrolanguage: jrb %% Type: language @@ -2628,6 +2644,7 @@ Added: 2009-07-29 Type: language Subtag: ant Description: Antakarinya +Description: Antikarinya Added: 2009-07-29 %% Type: language @@ -2699,6 +2716,7 @@ Type: language Subtag: aoh Description: Arma Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: aoi @@ -2774,7 +2792,7 @@ Added: 2009-07-29 %% Type: language Subtag: apc -Description: North Levantine Arabic +Description: Levantine Arabic Added: 2009-07-29 Macrolanguage: ar %% @@ -2916,6 +2934,11 @@ Description: Arigidi Added: 2009-07-29 %% Type: language +Subtag: aqk +Description: Aninka +Added: 2021-02-20 +%% +Type: language Subtag: aql Description: Algic languages Added: 2009-07-29 @@ -3089,6 +3112,8 @@ Type: language Subtag: asd Description: Asas Added: 2009-07-29 +Deprecated: 2019-04-16 +Preferred-Value: snz %% Type: language Subtag: ase @@ -3097,6 +3122,7 @@ Added: 2009-07-29 %% Type: language Subtag: asf +Description: Auslan Description: Australian Sign Language Added: 2009-07-29 %% @@ -3346,7 +3372,7 @@ Added: 2009-07-29 %% Type: language Subtag: aue -Description: =/Kx'au//'ein +Description: ǂKxʼauǁʼein Added: 2009-07-29 Deprecated: 2015-02-12 Preferred-Value: ktz @@ -3759,6 +3785,7 @@ Type: language Subtag: ayy Description: Tayabas Ayta Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: ayz @@ -4074,6 +4101,7 @@ Type: language Subtag: bbz Description: Babalia Creole Arabic Added: 2009-07-29 +Deprecated: 2020-03-28 Macrolanguage: ar %% Type: language @@ -4129,7 +4157,7 @@ Added: 2009-07-29 %% Type: language Subtag: bck -Description: Bunaba +Description: Bunuba Added: 2009-07-29 %% Type: language @@ -4240,7 +4268,7 @@ Added: 2009-07-29 %% Type: language Subtag: bdh -Description: Baka (Sudan) +Description: Baka (South Sudan) Added: 2009-07-29 %% Type: language @@ -4250,6 +4278,7 @@ Added: 2009-07-29 %% Type: language Subtag: bdj +Description: Bai (South Sudan) Description: Bai Added: 2009-07-29 %% @@ -4863,6 +4892,8 @@ Type: language Subtag: bic Description: Bikaru Added: 2009-07-29 +Deprecated: 2021-02-20 +Preferred-Value: bir %% Type: language Subtag: bid @@ -4888,6 +4919,8 @@ Type: language Subtag: bij Description: Vaghat-Ya-Bijim-Legeri Added: 2009-07-29 +Deprecated: 2021-02-20 +Comments: see dkg, jbm, tyy %% Type: language Subtag: bik @@ -5264,6 +5297,8 @@ Type: language Subtag: blg Description: Balau Added: 2009-07-29 +Deprecated: 2021-02-20 +Preferred-Value: iba %% Type: language Subtag: blh @@ -5293,7 +5328,7 @@ Added: 2009-07-29 %% Type: language Subtag: blm -Description: Beli (Sudan) +Description: Beli (South Sudan) Added: 2009-07-29 %% Type: language @@ -5743,6 +5778,12 @@ Type: language Subtag: bpb Description: Barbacoas Added: 2009-07-29 +Deprecated: 2020-03-28 +%% +Type: language +Subtag: bpc +Description: Mbuk +Added: 2022-02-25 %% Type: language Subtag: bpd @@ -5750,6 +5791,11 @@ Description: Banda-Banda Added: 2009-07-29 %% Type: language +Subtag: bpe +Description: Bauni +Added: 2021-02-20 +%% +Type: language Subtag: bpg Description: Bonggo Added: 2009-07-29 @@ -5984,6 +6030,7 @@ Added: 2005-10-16 %% Type: language Subtag: brb +Description: Brao Description: Lave Added: 2009-07-29 %% @@ -5999,7 +6046,7 @@ Added: 2009-07-29 %% Type: language Subtag: brf -Description: Bera +Description: Bira Added: 2009-07-29 %% Type: language @@ -6923,7 +6970,7 @@ Added: 2009-07-29 %% Type: language Subtag: bym -Description: Bidyara +Description: Bidjara Added: 2009-07-29 %% Type: language @@ -7362,6 +7409,7 @@ Type: language Subtag: cca Description: Cauca Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: ccc @@ -7468,6 +7516,7 @@ Type: language Subtag: cdg Description: Chamari Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: cdh @@ -7557,6 +7606,11 @@ Description: Centúúm Added: 2009-07-29 %% Type: language +Subtag: cey +Description: Ekai Chin +Added: 2019-04-16 +%% +Type: language Subtag: cfa Description: Dijim-Bwilim Added: 2009-07-29 @@ -7858,6 +7912,11 @@ Description: Cibak Added: 2009-07-29 %% Type: language +Subtag: ckm +Description: Chakavian +Added: 2020-03-28 +%% +Type: language Subtag: ckn Description: Kaang Chin Added: 2013-09-10 @@ -8104,6 +8163,25 @@ Description: Con Added: 2009-07-29 %% Type: language +Subtag: cnp +Description: Northern Ping Chinese +Description: Northern Pinghua +Added: 2020-03-28 +Macrolanguage: zh +%% +Type: language +Subtag: cnq +Description: Chung +Added: 2022-02-25 +%% +Type: language +Subtag: cnr +Description: Montenegrin +Added: 2018-01-23 +Macrolanguage: sh +Comments: see sr for Serbian +%% +Type: language Subtag: cns Description: Central Asmat Added: 2009-07-29 @@ -8540,6 +8618,13 @@ Description: Sochiapan Chinantec Added: 2009-07-29 %% Type: language +Subtag: csp +Description: Southern Ping Chinese +Description: Southern Pinghua +Added: 2020-03-28 +Macrolanguage: zh +%% +Type: language Subtag: csq Description: Croatia Sign Language Added: 2009-07-29 @@ -8577,6 +8662,11 @@ Added: 2009-07-29 Macrolanguage: cr %% Type: language +Subtag: csx +Description: Cambodian Sign Language +Added: 2021-02-20 +%% +Type: language Subtag: csy Description: Siyin Chin Added: 2009-07-29 @@ -8658,6 +8748,11 @@ Description: Chol Added: 2009-07-29 %% Type: language +Subtag: cty +Description: Moundadan Chetty +Added: 2021-02-20 +%% +Type: language Subtag: ctz Description: Zacatepec Chatino Added: 2009-07-29 @@ -8682,6 +8777,8 @@ Subtag: cug Description: Chungmboko Description: Cung Added: 2009-07-29 +Deprecated: 2022-02-25 +Comments: see bpc, cnq %% Type: language Subtag: cuh @@ -8768,6 +8865,11 @@ Description: Tepeuxila Cuicatec Added: 2009-07-29 %% Type: language +Subtag: cuy +Description: Cuitlatec +Added: 2018-03-08 +%% +Type: language Subtag: cvg Description: Chug Added: 2009-07-29 @@ -8810,6 +8912,11 @@ Description: Kuwaataay Added: 2009-07-29 %% Type: language +Subtag: cxh +Description: Cha'ari +Added: 2023-03-17 +%% +Type: language Subtag: cya Description: Nopala Chatino Added: 2009-07-29 @@ -9289,6 +9396,7 @@ Macrolanguage: doi Type: language Subtag: dgr Description: Dogrib +Description: Tłı̨chǫ Added: 2005-10-16 %% Type: language @@ -9305,6 +9413,7 @@ Type: language Subtag: dgu Description: Degaru Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: dgw @@ -9420,6 +9529,7 @@ Added: 2009-07-29 Type: language Subtag: dif Description: Dieri +Description: Diyari Added: 2009-07-29 %% Type: language @@ -9496,6 +9606,8 @@ Type: language Subtag: dit Description: Dirari Added: 2009-07-29 +Deprecated: 2019-04-29 +Preferred-Value: dif %% Type: language Subtag: diu @@ -9541,6 +9653,7 @@ Added: 2009-07-29 Type: language Subtag: djd Description: Djamindjung +Description: Ngaliwurru Added: 2009-07-29 %% Type: language @@ -9584,6 +9697,7 @@ Added: 2009-07-29 %% Type: language Subtag: djn +Description: Jawoyn Description: Djauan Added: 2009-07-29 %% @@ -9613,6 +9727,11 @@ Description: Dakpakha Added: 2009-07-29 %% Type: language +Subtag: dkg +Description: Kadung +Added: 2021-02-20 +%% +Type: language Subtag: dkk Description: Dakka Added: 2009-07-29 @@ -9686,6 +9805,11 @@ Description: Dugwor Added: 2009-07-29 %% Type: language +Subtag: dmf +Description: Medefaidrin +Added: 2020-03-28 +%% +Type: language Subtag: dmg Description: Upper Kinabatangan Added: 2009-07-29 @@ -9794,6 +9918,12 @@ Description: Dzùùngoo Added: 2009-07-29 %% Type: language +Subtag: dno +Description: Ndrulo +Description: Northern Lendu +Added: 2018-10-28 +%% +Type: language Subtag: dnr Description: Danaru Added: 2009-07-29 @@ -10001,6 +10131,8 @@ Type: language Subtag: drr Description: Dororo Added: 2009-07-29 +Deprecated: 2020-03-28 +Preferred-Value: kzk %% Type: language Subtag: drs @@ -10051,6 +10183,11 @@ Description: Disa Added: 2009-07-29 %% Type: language +Subtag: dsk +Description: Dokshi +Added: 2023-03-17 +%% +Type: language Subtag: dsl Description: Danish Sign Language Added: 2009-07-29 @@ -10071,6 +10208,11 @@ Description: Tadaksahak Added: 2009-07-29 %% Type: language +Subtag: dsz +Description: Mardin Sign Language +Added: 2022-02-25 +%% +Type: language Subtag: dta Description: Daur Added: 2009-07-29 @@ -10166,6 +10308,8 @@ Type: language Subtag: dud Description: Hun-Saare Added: 2009-07-29 +Deprecated: 2019-04-16 +Comments: see uth, uss %% Type: language Subtag: due @@ -10288,6 +10432,11 @@ Description: Diri Added: 2009-07-29 %% Type: language +Subtag: dwk +Description: Dawik Kui +Added: 2020-03-28 +%% +Type: language Subtag: dwl Description: Walo Kumbe Dogon Added: 2009-07-29 @@ -10320,6 +10469,11 @@ Description: Dhuwaya Added: 2016-05-30 %% Type: language +Subtag: dwz +Description: Dewas Rai +Added: 2018-10-28 +%% +Type: language Subtag: dya Description: Dyan Added: 2009-07-29 @@ -10352,6 +10506,7 @@ Added: 2009-07-29 Type: language Subtag: dyn Description: Dyangadi +Description: Dhanggatti Added: 2009-07-29 %% Type: language @@ -10360,12 +10515,18 @@ Description: Jola-Fonyi Added: 2009-07-29 %% Type: language +Subtag: dyr +Description: Dyarim +Added: 2023-03-17 +%% +Type: language Subtag: dyu Description: Dyula Added: 2005-10-16 %% Type: language Subtag: dyy +Description: Djabugay Description: Dyaabugay Added: 2009-07-29 %% @@ -10378,7 +10539,6 @@ Type: language Subtag: dzd Description: Daza Added: 2009-07-29 -Deprecated: 2015-02-12 %% Type: language Subtag: dze @@ -10406,6 +10566,11 @@ Description: Karenggapa Added: 2013-09-10 %% Type: language +Subtag: ebc +Description: Beginci +Added: 2020-03-28 +%% +Type: language Subtag: ebg Description: Ebughu Added: 2009-07-29 @@ -10478,6 +10643,11 @@ Description: Emilian Added: 2009-07-29 %% Type: language +Subtag: egm +Description: Benamanga +Added: 2022-02-25 +%% +Type: language Subtag: ego Description: Eggon Added: 2009-07-29 @@ -10494,6 +10664,11 @@ Description: Egyptian (Ancient) Added: 2005-10-16 %% Type: language +Subtag: ehs +Description: Miyakubo Sign Language +Added: 2021-02-20 +%% +Type: language Subtag: ehu Description: Ehueun Added: 2009-07-29 @@ -10527,6 +10702,7 @@ Type: language Subtag: ekc Description: Eastern Karnic Added: 2013-09-10 +Deprecated: 2020-03-28 %% Type: language Subtag: eke @@ -10680,6 +10856,11 @@ Description: Northern Emberá Added: 2009-07-29 %% Type: language +Subtag: emq +Description: Eastern Minyag +Added: 2021-02-20 +%% +Type: language Subtag: ems Description: Pacific Gulf Yupik Added: 2009-07-29 @@ -10705,6 +10886,11 @@ Description: Epigraphic Mayan Added: 2009-07-29 %% Type: language +Subtag: emz +Description: Mbessa +Added: 2021-02-20 +%% +Type: language Subtag: ena Description: Apali Added: 2009-07-29 @@ -10773,7 +10959,7 @@ Added: 2009-07-29 %% Type: language Subtag: env -Description: Enwan (Edu State) +Description: Enwan (Edo State) Added: 2009-07-29 %% Type: language @@ -10976,6 +11162,11 @@ Description: Semimi Added: 2009-07-29 %% Type: language +Subtag: eud +Description: Eudeve +Added: 2023-03-17 +%% +Type: language Subtag: euq Description: Basque (family) Added: 2009-07-29 @@ -11089,7 +11280,7 @@ Added: 2005-10-16 %% Type: language Subtag: fap -Description: Palor +Description: Paloor Added: 2009-07-29 %% Type: language @@ -11166,6 +11357,11 @@ Description: Fyer Added: 2009-07-29 %% Type: language +Subtag: fif +Description: Faifi +Added: 2020-06-08 +%% +Type: language Subtag: fil Description: Filipino Description: Pilipino @@ -11184,6 +11380,7 @@ Added: 2009-07-29 Type: language Subtag: fit Description: Tornedalen Finnish +Description: Meänkieli Added: 2009-07-29 %% Type: language @@ -11642,7 +11839,7 @@ Comments: see wny, wrk %% Type: language Subtag: gbd -Description: Karadjeri +Description: Karajarri Added: 2009-07-29 %% Type: language @@ -11832,6 +12029,7 @@ Added: 2009-07-29 Type: language Subtag: gdh Description: Gadjerawang +Description: Gajirrabeng Added: 2009-07-29 %% Type: language @@ -11921,6 +12119,11 @@ Description: Gade Added: 2009-07-29 %% Type: language +Subtag: gef +Description: Gerai +Added: 2020-03-28 +%% +Type: language Subtag: geg Description: Gengle Added: 2009-07-29 @@ -12004,7 +12207,7 @@ Added: 2009-07-29 %% Type: language Subtag: gfx -Description: Mangetti Dune !Xung +Description: Mangetti Dune ǃXung Added: 2012-08-12 Deprecated: 2015-02-12 Preferred-Value: vaj @@ -12026,7 +12229,7 @@ Added: 2009-07-29 %% Type: language Subtag: gge -Description: Guragone +Description: Gurr-goni Added: 2009-07-29 %% Type: language @@ -12139,7 +12342,7 @@ Added: 2009-07-29 %% Type: language Subtag: gia -Description: Kitja +Description: Kija Added: 2009-07-29 %% Type: language @@ -12174,6 +12377,11 @@ Description: Githabul Added: 2013-09-10 %% Type: language +Subtag: gii +Description: Girirra +Added: 2021-02-20 +%% +Type: language Subtag: gil Description: Gilbertese Added: 2005-10-16 @@ -12249,6 +12457,8 @@ Type: language Subtag: gji Description: Geji Added: 2009-07-29 +Deprecated: 2021-02-20 +Comments: see gyz, zbu %% Type: language Subtag: gjk @@ -12282,6 +12492,11 @@ Description: Guya Added: 2009-07-29 %% Type: language +Subtag: gkd +Description: Magɨ (Madang Province) +Added: 2018-03-08 +%% +Type: language Subtag: gke Description: Ndai Added: 2009-07-29 @@ -12305,10 +12520,14 @@ Macrolanguage: kpe Type: language Subtag: gku Description: ǂUngkue -Description: =/Ungkue Added: 2015-02-12 %% Type: language +Subtag: glb +Description: Belning +Added: 2021-02-20 +%% +Type: language Subtag: glc Description: Bon Gula Added: 2009-07-29 @@ -12328,6 +12547,8 @@ Type: language Subtag: gli Description: Guliguli Added: 2009-07-29 +Deprecated: 2020-03-28 +Preferred-Value: kzk %% Type: language Subtag: glj @@ -12423,6 +12644,12 @@ Added: 2009-07-29 Scope: collection %% Type: language +Subtag: gmr +Description: Mirning +Description: Mirniny +Added: 2020-03-28 +%% +Type: language Subtag: gmu Description: Gumalu Added: 2009-07-29 @@ -12494,8 +12721,12 @@ Description: Gooniyandi Added: 2009-07-29 %% Type: language +Subtag: gnj +Description: Ngen +Added: 2018-03-08 +%% +Type: language Subtag: gnk -Description: //Gana Description: ǁGana Added: 2009-07-29 %% @@ -12659,6 +12890,11 @@ Description: Gavar Added: 2009-07-29 %% Type: language +Subtag: gov +Description: Goo +Added: 2022-02-25 +%% +Type: language Subtag: gow Description: Gorowa Added: 2009-07-29 @@ -12917,7 +13153,7 @@ Added: 2009-07-29 %% Type: language Subtag: gue -Description: Gurinji +Description: Gurindji Added: 2009-07-29 %% Type: language @@ -13098,6 +13334,7 @@ Added: 2009-07-29 %% Type: language Subtag: gwc +Description: Gawri Description: Kalami Added: 2009-07-29 %% @@ -13128,7 +13365,6 @@ Added: 2005-10-16 %% Type: language Subtag: gwj -Description: /Gwi Description: ǀGwi Added: 2009-07-29 %% @@ -13224,6 +13460,11 @@ Description: Guyanese Creole English Added: 2009-07-29 %% Type: language +Subtag: gyo +Description: Gyalsumdo +Added: 2018-03-08 +%% +Type: language Subtag: gyr Description: Guarayu Added: 2009-07-29 @@ -13234,6 +13475,12 @@ Description: Gunya Added: 2009-07-29 %% Type: language +Subtag: gyz +Description: Geji +Description: Gyaazi +Added: 2021-02-20 +%% +Type: language Subtag: gza Description: Ganza Added: 2009-07-29 @@ -13456,7 +13703,6 @@ Added: 2009-07-29 %% Type: language Subtag: hgm -Description: Hai//om Description: Haiǁom Added: 2009-07-29 %% @@ -13579,11 +13825,22 @@ Description: Hunde Added: 2009-07-29 %% Type: language +Subtag: hkh +Description: Khah +Description: Poguli +Added: 2021-02-20 +%% +Type: language Subtag: hkk Description: Hunjara-Kaina Ke Added: 2009-07-29 %% Type: language +Subtag: hkn +Description: Mel-Khaonh +Added: 2018-03-08 +%% +Type: language Subtag: hks Description: Hong Kong Sign Language Description: Heung Kong Sau Yue @@ -13794,8 +14051,12 @@ Description: Chhattisgarhi Added: 2009-07-29 %% Type: language +Subtag: hng +Description: Hungu +Added: 2020-03-28 +%% +Type: language Subtag: hnh -Description: //Ani Description: ǁAni Added: 2009-07-29 %% @@ -14075,8 +14336,8 @@ Added: 2009-07-29 %% Type: language Subtag: huc -Description: =/Hua Description: ǂHua +Description: ǂʼAmkhoe Added: 2009-07-29 %% Type: language @@ -14238,6 +14499,12 @@ Description: Hya Added: 2009-07-29 %% Type: language +Subtag: hyw +Description: Western Armenian +Added: 2018-03-08 +Comments: see also hy +%% +Type: language Subtag: hyx Description: Armenian (family) Added: 2009-07-29 @@ -14559,6 +14826,11 @@ Added: 2009-07-29 Macrolanguage: iu %% Type: language +Subtag: ikh +Description: Ikhin-Arokho +Added: 2023-03-17 +%% +Type: language Subtag: iki Description: Iko Added: 2009-07-29 @@ -14731,6 +15003,11 @@ Description: Marsian Added: 2009-07-29 %% Type: language +Subtag: imt +Description: Imotong +Added: 2022-02-25 +%% +Type: language Subtag: imy Description: Milyan Added: 2009-07-29 @@ -14860,6 +15137,7 @@ Added: 2009-07-29 %% Type: language Subtag: iri +Description: Rigwe Description: Irigwe Added: 2009-07-29 %% @@ -15128,6 +15406,11 @@ Deprecated: 2013-09-10 Comments: see eza, gmz, iqw, izz %% Type: language +Subtag: izm +Description: Kizamani +Added: 2023-03-17 +%% +Type: language Subtag: izr Description: Izere Added: 2009-07-29 @@ -15241,6 +15524,7 @@ Macrolanguage: ms Type: language Subtag: jay Description: Yan-nhangu +Description: Nhangu Added: 2009-07-29 %% Type: language @@ -15269,6 +15553,11 @@ Description: Barikewa Added: 2012-08-12 %% Type: language +Subtag: jbm +Description: Bijim +Added: 2021-02-20 +%% +Type: language Subtag: jbn Description: Nafusi Added: 2009-07-29 @@ -15427,7 +15716,7 @@ Added: 2009-07-29 %% Type: language Subtag: jid -Description: Bu +Description: Bu (Kaduna State) Added: 2009-07-29 %% Type: language @@ -15437,6 +15726,7 @@ Added: 2009-07-29 %% Type: language Subtag: jig +Description: Jingulu Description: Djingili Added: 2009-07-29 %% @@ -15528,6 +15818,11 @@ Description: Koro (India) Added: 2012-08-12 %% Type: language +Subtag: jks +Description: Amami Koniya Sign Language +Added: 2021-02-20 +%% +Type: language Subtag: jku Description: Labir Added: 2009-07-29 @@ -15701,7 +15996,7 @@ Added: 2009-07-29 %% Type: language Subtag: jrt -Description: Jorto +Description: Jakattoe Added: 2009-07-29 %% Type: language @@ -15838,6 +16133,7 @@ Added: 2009-07-29 Type: language Subtag: kaa Description: Kara-Kalpak +Description: Karakalpak Added: 2005-10-16 %% Type: language @@ -16657,6 +16953,8 @@ Type: language Subtag: kgm Description: Karipúna Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: plu %% Type: language Subtag: kgn @@ -16995,8 +17293,9 @@ Added: 2009-07-29 %% Type: language Subtag: kjf -Description: Khalaj +Description: Khalaj [Indo-Iranian] Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: kjg @@ -17171,11 +17470,12 @@ Added: 2009-07-29 Type: language Subtag: kkp Description: Gugubera +Description: Koko-Bera Added: 2009-07-29 %% Type: language Subtag: kkq -Description: Kaiku +Description: Kaeku Added: 2009-07-29 %% Type: language @@ -17215,6 +17515,7 @@ Added: 2009-07-29 %% Type: language Subtag: kky +Description: Guugu Yimidhirr Description: Guguyimidjir Added: 2009-07-29 %% @@ -17270,7 +17571,7 @@ Added: 2009-07-29 %% Type: language Subtag: klj -Description: Turkic Khalaj +Description: Khalaj Added: 2009-07-29 %% Type: language @@ -17632,7 +17933,7 @@ Added: 2009-07-29 %% Type: language Subtag: koe -Description: Kacipo-Balesi +Description: Kacipo-Bale Suri Added: 2009-07-29 %% Type: language @@ -18071,7 +18372,7 @@ Scope: collection %% Type: language Subtag: krp -Description: Korop +Description: Durop Added: 2009-07-29 %% Type: language @@ -18124,6 +18425,8 @@ Type: language Subtag: ksa Description: Shuwa-Zamani Added: 2009-07-29 +Deprecated: 2023-03-17 +Comments: see izm, rsw %% Type: language Subtag: ksb @@ -18269,6 +18572,7 @@ Added: 2009-07-29 Type: language Subtag: ktd Description: Kokata +Description: Kukatha Added: 2009-07-29 %% Type: language @@ -18380,7 +18684,6 @@ Added: 2009-07-29 %% Type: language Subtag: ktz -Description: Ju/'hoan Description: Juǀʼhoan Description: Juǀʼhoansi Added: 2009-07-29 @@ -18423,6 +18726,7 @@ Added: 2009-07-29 Type: language Subtag: kui Description: Kuikúro-Kalapálo +Description: Kalapalo Added: 2009-07-29 %% Type: language @@ -18834,6 +19138,8 @@ Type: language Subtag: kxl Description: Nepali Kurux Added: 2009-07-29 +Deprecated: 2020-03-28 +Preferred-Value: kru %% Type: language Subtag: kxm @@ -18879,6 +19185,8 @@ Type: language Subtag: kxu Description: Kui (India) Added: 2009-07-29 +Deprecated: 2020-03-28 +Comments: see dwk, uki %% Type: language Subtag: kxv @@ -19203,7 +19511,7 @@ Added: 2009-07-29 %% Type: language Subtag: lag -Description: Langi +Description: Rangi Added: 2009-07-29 %% Type: language @@ -19226,6 +19534,8 @@ Type: language Subtag: lak Description: Laka (Nigeria) Added: 2009-07-29 +Deprecated: 2022-02-25 +Preferred-Value: ksp %% Type: language Subtag: lal @@ -19291,6 +19601,7 @@ Type: language Subtag: lba Description: Lui Added: 2009-07-29 +Deprecated: 2019-04-16 %% Type: language Subtag: lbb @@ -19346,7 +19657,7 @@ Added: 2009-07-29 %% Type: language Subtag: lbn -Description: Lamet +Description: Rmeet Added: 2009-07-29 %% Type: language @@ -19396,6 +19707,7 @@ Added: 2009-07-29 %% Type: language Subtag: lby +Description: Lamalama Description: Lamu-Lamu Added: 2009-07-29 %% @@ -19719,6 +20031,11 @@ Description: Opuuo Added: 2009-07-29 %% Type: language +Subtag: lgo +Description: Lango (South Sudan) +Added: 2022-02-25 +%% +Type: language Subtag: lgq Description: Logba Added: 2009-07-29 @@ -19729,6 +20046,12 @@ Description: Lengo Added: 2009-07-29 %% Type: language +Subtag: lgs +Description: Guinea-Bissau Sign Language +Description: Língua Gestual Guineense +Added: 2023-03-17 +%% +Type: language Subtag: lgt Description: Pahi Added: 2009-07-29 @@ -20112,6 +20435,8 @@ Type: language Subtag: llo Description: Khlor Added: 2009-07-29 +Deprecated: 2019-04-16 +Preferred-Value: ngt %% Type: language Subtag: llp @@ -20259,6 +20584,7 @@ Type: language Subtag: lmz Description: Lumbee Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: lna @@ -20313,8 +20639,10 @@ Added: 2009-07-29 %% Type: language Subtag: lno -Description: Lango (Sudan) +Description: Lango (South Sudan) Added: 2009-07-29 +Deprecated: 2022-02-25 +Comments: see imt, lgo, lqr, oie %% Type: language Subtag: lns @@ -20368,6 +20696,7 @@ Added: 2009-07-29 %% Type: language Subtag: loh +Description: Laarim Description: Narim Added: 2009-07-29 %% @@ -20487,6 +20816,11 @@ Description: Lopit Added: 2009-07-29 %% Type: language +Subtag: lqr +Description: Logir +Added: 2022-02-25 +%% +Type: language Subtag: lra Description: Rara Bakati' Added: 2009-07-29 @@ -20566,6 +20900,18 @@ Description: Lasgerdi Added: 2009-07-29 %% Type: language +Subtag: lsb +Description: Burundian Sign Language +Description: Langue des Signes Burundaise +Added: 2021-02-20 +%% +Type: language +Subtag: lsc +Description: Albarradas Sign Language +Description: Lengua de señas Albarradas +Added: 2022-02-25 +%% +Type: language Subtag: lsd Description: Lishana Deni Added: 2009-07-29 @@ -20579,6 +20925,7 @@ Type: language Subtag: lsg Description: Lyons Sign Language Added: 2009-07-29 +Deprecated: 2018-03-08 %% Type: language Subtag: lsh @@ -20603,6 +20950,11 @@ Added: 2009-07-29 Macrolanguage: luy %% Type: language +Subtag: lsn +Description: Tibetan Sign Language +Added: 2019-04-16 +%% +Type: language Subtag: lso Description: Laos Sign Language Added: 2009-07-29 @@ -20629,6 +20981,18 @@ Description: Trinidad and Tobago Sign Language Added: 2009-07-29 %% Type: language +Subtag: lsv +Description: Sivia Sign Language +Added: 2019-04-16 +%% +Type: language +Subtag: lsw +Description: Seychelles Sign Language +Description: Lalang Siny Seselwa +Description: Langue des Signes Seychelloise +Added: 2022-02-25 +%% +Type: language Subtag: lsy Description: Mauritian Sign Language Added: 2010-03-11 @@ -20797,11 +21161,21 @@ Description: Maku'a Added: 2009-07-29 %% Type: language +Subtag: lvi +Description: Lavi +Added: 2019-04-16 +%% +Type: language Subtag: lvk Description: Lavukaleve Added: 2009-07-29 %% Type: language +Subtag: lvl +Description: Lwel +Added: 2023-03-17 +%% +Type: language Subtag: lvs Description: Standard Latvian Added: 2010-03-11 @@ -20850,6 +21224,11 @@ Description: Luwo Added: 2009-07-29 %% Type: language +Subtag: lws +Description: Malawian Sign Language +Added: 2018-03-08 +%% +Type: language Subtag: lwt Description: Lewotobi Added: 2009-07-29 @@ -20865,6 +21244,11 @@ Description: Lewo Added: 2009-07-29 %% Type: language +Subtag: lxm +Description: Lakurumau +Added: 2021-02-20 +%% +Type: language Subtag: lya Description: Layakha Added: 2009-07-29 @@ -20904,6 +21288,7 @@ Type: language Subtag: maa Description: San Jerónimo Tecóatl Mazatec Added: 2009-07-29 +Comments: see also pbm %% Type: language Subtag: mab @@ -21397,7 +21782,7 @@ Added: 2009-07-29 %% Type: language Subtag: mec -Description: Mara +Description: Marra Added: 2009-07-29 %% Type: language @@ -21466,7 +21851,7 @@ Macrolanguage: ms %% Type: language Subtag: mep -Description: Miriwung +Description: Miriwoong Added: 2009-07-29 %% Type: language @@ -21603,7 +21988,7 @@ Added: 2009-07-29 %% Type: language Subtag: mfr -Description: Marithiel +Description: Marrithiyel Added: 2009-07-29 %% Type: language @@ -22688,6 +23073,7 @@ Added: 2009-07-29 %% Type: language Subtag: moe +Description: Innu Description: Montagnais Added: 2009-07-29 %% @@ -22796,12 +23182,13 @@ Added: 2009-07-29 %% Type: language Subtag: mpb +Description: Malak Malak Description: Mullukmulluk Added: 2009-07-29 %% Type: language Subtag: mpc -Description: Mangarayi +Description: Mangarrayi Added: 2009-07-29 %% Type: language @@ -22832,6 +23219,7 @@ Added: 2009-07-29 Type: language Subtag: mpj Description: Martu Wangka +Description: Wangkajunga Added: 2009-07-29 %% Type: language @@ -23606,6 +23994,8 @@ Type: language Subtag: mvm Description: Muya Added: 2009-07-29 +Deprecated: 2021-02-20 +Comments: see emq, wmg %% Type: language Subtag: mvn @@ -23799,11 +24189,13 @@ Type: language Subtag: mwx Description: Mediak Added: 2009-07-29 +Deprecated: 2018-03-08 %% Type: language Subtag: mwy Description: Mosiro Added: 2009-07-29 +Deprecated: 2018-03-08 %% Type: language Subtag: mwz @@ -23956,6 +24348,8 @@ Type: language Subtag: myd Description: Maramba Added: 2009-07-29 +Deprecated: 2019-04-16 +Preferred-Value: aog %% Type: language Subtag: mye @@ -23981,6 +24375,7 @@ Type: language Subtag: myi Description: Mina (India) Added: 2009-07-29 +Deprecated: 2019-04-16 %% Type: language Subtag: myj @@ -24316,7 +24711,7 @@ Added: 2009-07-29 %% Type: language Subtag: nay -Description: Narrinyeri +Description: Ngarrindjeri Added: 2009-07-29 %% Type: language @@ -24373,7 +24768,7 @@ Added: 2009-07-29 %% Type: language Subtag: nbj -Description: Ngarinman +Description: Ngarinyman Added: 2009-07-29 %% Type: language @@ -24408,7 +24803,7 @@ Added: 2009-07-29 %% Type: language Subtag: nbr -Description: Numana-Nunku-Gbantu-Numbu +Description: Numana Added: 2009-07-29 %% Type: language @@ -24500,7 +24895,7 @@ Added: 2009-07-29 %% Type: language Subtag: nck -Description: Nakara +Description: Na-kara Added: 2009-07-29 %% Type: language @@ -24527,6 +24922,8 @@ Type: language Subtag: ncp Description: Ndaktup Added: 2009-07-29 +Deprecated: 2018-03-08 +Preferred-Value: kdz %% Type: language Subtag: ncq @@ -24870,8 +25267,7 @@ Added: 2009-07-29 %% Type: language Subtag: ngh -Description: N/u -Description: Nǀu +Description: Nǁng Added: 2009-07-29 %% Type: language @@ -24908,6 +25304,8 @@ Type: language Subtag: ngo Description: Ngoni Added: 2009-07-29 +Deprecated: 2021-02-20 +Comments: see xnj, xnq %% Type: language Subtag: ngp @@ -25116,7 +25514,7 @@ Added: 2009-07-29 %% Type: language Subtag: nig -Description: Ngalakan +Description: Ngalakgan Added: 2009-07-29 %% Type: language @@ -25458,6 +25856,11 @@ Description: Nihali Added: 2009-07-29 %% Type: language +Subtag: nlm +Description: Mankiyali +Added: 2018-03-08 +%% +Type: language Subtag: nln Description: Durango Nahuatl Added: 2009-07-29 @@ -25580,7 +25983,6 @@ Added: 2009-07-29 %% Type: language Subtag: nmn -Description: !Xóõ Description: ǃXóõ Added: 2009-07-29 %% @@ -25734,6 +26136,8 @@ Type: language Subtag: nns Description: Ningye Added: 2009-07-29 +Deprecated: 2019-04-16 +Preferred-Value: nbr %% Type: language Subtag: nnt @@ -25831,6 +26235,8 @@ Type: language Subtag: nom Description: Nocamán Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: cbr %% Type: language Subtag: non @@ -25988,6 +26394,11 @@ Description: Kyan-Karyaw Naga Added: 2013-09-10 %% Type: language +Subtag: nqt +Description: Nteng +Added: 2021-02-20 +%% +Type: language Subtag: nqy Description: Akyaung Ari Naga Added: 2012-08-12 @@ -26016,6 +26427,7 @@ Type: language Subtag: nrf Description: Jèrriais Description: Guernésiais +Description: Sercquiais Added: 2015-02-12 %% Type: language @@ -26085,6 +26497,11 @@ Description: Sangtam Naga Added: 2009-07-29 %% Type: language +Subtag: nsb +Description: Lower Nossob +Added: 2020-03-28 +%% +Type: language Subtag: nsc Description: Nshi Added: 2009-07-29 @@ -26480,6 +26897,11 @@ Description: Nawaru Added: 2009-07-29 %% Type: language +Subtag: nww +Description: Ndwewe +Added: 2022-02-25 +%% +Type: language Subtag: nwx Description: Middle Newar Added: 2009-07-29 @@ -26553,6 +26975,8 @@ Type: language Subtag: nxu Description: Narau Added: 2009-07-29 +Deprecated: 2020-03-28 +Preferred-Value: bpp %% Type: language Subtag: nxx @@ -26594,7 +27018,7 @@ Added: 2009-07-29 %% Type: language Subtag: nyh -Description: Nyigina +Description: Nyikina Added: 2009-07-29 %% Type: language @@ -26649,7 +27073,7 @@ Added: 2009-07-29 %% Type: language Subtag: nys -Description: Nyunga +Description: Nyungar Added: 2009-07-29 %% Type: language @@ -26693,6 +27117,11 @@ Description: Njebi Added: 2009-07-29 %% Type: language +Subtag: nzd +Description: Nzadi +Added: 2018-03-08 +%% +Type: language Subtag: nzi Description: Nzima Added: 2005-10-16 @@ -26708,6 +27137,11 @@ Description: Zeme Naga Added: 2009-07-29 %% Type: language +Subtag: nzr +Description: Dir-Nyamzak-Mbarimi +Added: 2023-03-17 +%% +Type: language Subtag: nzs Description: New Zealand Sign Language Added: 2009-07-29 @@ -26800,6 +27234,11 @@ Description: Old Chinese Added: 2009-07-29 %% Type: language +Subtag: ocm +Description: Old Cham +Added: 2021-02-20 +%% +Type: language Subtag: oco Description: Old Cornish Added: 2009-07-29 @@ -26890,6 +27329,11 @@ Description: Oirata Added: 2009-07-29 %% Type: language +Subtag: oie +Description: Okolie +Added: 2022-02-25 +%% +Type: language Subtag: oin Description: Inebu One Added: 2009-07-29 @@ -26945,6 +27389,11 @@ Description: Okobo Added: 2009-07-29 %% Type: language +Subtag: okc +Description: Kobo +Added: 2021-02-20 +%% +Type: language Subtag: okd Description: Okodia Added: 2009-07-29 @@ -27026,6 +27475,11 @@ Description: Okpe (Northwestern Edo) Added: 2009-07-29 %% Type: language +Subtag: okz +Description: Old Khmer +Added: 2021-02-20 +%% +Type: language Subtag: ola Description: Walungge Added: 2009-07-29 @@ -27164,6 +27618,11 @@ Description: Old Mon Added: 2009-07-29 %% Type: language +Subtag: omy +Description: Old Malay +Added: 2021-02-20 +%% +Type: language Subtag: ona Description: Ona Added: 2009-07-29 @@ -27399,6 +27858,11 @@ Description: Osing Added: 2009-07-29 %% Type: language +Subtag: osn +Description: Old Sundanese +Added: 2021-02-20 +%% +Type: language Subtag: oso Description: Ososo Added: 2009-07-29 @@ -27547,7 +28011,7 @@ Added: 2009-07-29 %% Type: language Subtag: oun -Description: !O!ung +Description: ǃOǃung Added: 2009-07-29 Deprecated: 2015-02-12 Preferred-Value: vaj @@ -27685,6 +28149,8 @@ Type: language Subtag: pat Description: Papitalai Added: 2009-07-29 +Deprecated: 2021-02-20 +Preferred-Value: kxr %% Type: language Subtag: pau @@ -27757,6 +28223,12 @@ Description: Mak (Nigeria) Added: 2009-07-29 %% Type: language +Subtag: pbm +Description: Puebla Mazatec +Added: 2018-03-08 +Comments: see also maa +%% +Type: language Subtag: pbn Description: Kpasam Added: 2009-07-29 @@ -28041,7 +28513,7 @@ Added: 2009-07-29 %% Type: language Subtag: pfe -Description: Peere +Description: Pere Added: 2009-07-29 %% Type: language @@ -28133,6 +28605,11 @@ Added: 2005-10-16 Scope: collection %% Type: language +Subtag: phj +Description: Pahari +Added: 2022-02-25 +%% +Type: language Subtag: phk Description: Phake Added: 2009-07-29 @@ -28233,6 +28710,7 @@ Type: language Subtag: pii Description: Pini Added: 2009-07-29 +Deprecated: 2022-02-25 %% Type: language Subtag: pij @@ -28422,6 +28900,8 @@ Type: language Subtag: plj Description: Polci Added: 2009-07-29 +Deprecated: 2023-03-17 +Comments: see nzr, pze, uly, zlu %% Type: language Subtag: plk @@ -28447,6 +28927,7 @@ Type: language Subtag: plp Description: Palpa Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: plq @@ -28546,6 +29027,8 @@ Type: language Subtag: pmk Description: Pamlico Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: crr %% Type: language Subtag: pml @@ -28632,12 +29115,18 @@ Description: Pannei Added: 2009-07-29 %% Type: language +Subtag: pnd +Description: Mpinda +Added: 2019-04-16 +%% +Type: language Subtag: pne Description: Western Penan Added: 2009-07-29 %% Type: language Subtag: png +Description: Pangu Description: Pongu Added: 2009-07-29 %% @@ -28719,6 +29208,7 @@ Added: 2009-07-29 %% Type: language Subtag: pnw +Description: Banyjima Description: Panytyima Added: 2009-07-29 %% @@ -29015,6 +29505,8 @@ Type: language Subtag: prp Description: Parsi Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: gu %% Type: language Subtag: prq @@ -29072,6 +29564,7 @@ Added: 2009-07-29 %% Type: language Subtag: psc +Description: Iranian Sign Language Description: Persian Sign Language Added: 2009-07-29 %% @@ -29176,7 +29669,8 @@ Added: 2009-07-29 %% Type: language Subtag: pti -Description: Pintiini +Description: Pindiini +Description: Wangkatha Added: 2009-07-29 %% Type: language @@ -29424,7 +29918,18 @@ Description: Pyen Added: 2009-07-29 %% Type: language +Subtag: pze +Description: Pesse +Added: 2023-03-17 +%% +Type: language +Subtag: pzh +Description: Pazeh +Added: 2022-02-25 +%% +Type: language Subtag: pzn +Description: Jejara Naga Description: Para Naga Added: 2009-07-29 %% @@ -30046,6 +30551,11 @@ Description: Riang (India) Added: 2009-07-29 %% Type: language +Subtag: rib +Description: Bribri Sign Language +Added: 2022-02-25 +%% +Type: language Subtag: rie Description: Rien Added: 2009-07-29 @@ -30058,6 +30568,7 @@ Added: 2009-07-29 %% Type: language Subtag: ril +Description: Riang Lang Description: Riang (Myanmar) Added: 2009-07-29 %% @@ -30078,7 +30589,7 @@ Added: 2009-07-29 %% Type: language Subtag: rit -Description: Ritarungo +Description: Ritharrngu Added: 2009-07-29 %% Type: language @@ -30144,7 +30655,7 @@ Added: 2009-07-29 %% Type: language Subtag: rmb -Description: Rembarunga +Description: Rembarrnga Added: 2009-07-29 %% Type: language @@ -30278,6 +30789,11 @@ Added: 2009-07-29 Deprecated: 2016-05-30 %% Type: language +Subtag: rnb +Description: Brunca Sign Language +Added: 2022-02-25 +%% +Type: language Subtag: rnd Description: Ruund Added: 2009-07-29 @@ -30421,6 +30937,12 @@ Added: 2009-07-29 Deprecated: 2017-02-23 %% Type: language +Subtag: rsk +Description: Ruthenian +Description: Rusnak +Added: 2022-02-25 +%% +Type: language Subtag: rsl Description: Russian Sign Language Added: 2009-07-29 @@ -30431,6 +30953,16 @@ Description: Miriwoong Sign Language Added: 2016-05-30 %% Type: language +Subtag: rsn +Description: Rwandan Sign Language +Added: 2022-02-25 +%% +Type: language +Subtag: rsw +Description: Rishiwa +Added: 2023-03-17 +%% +Type: language Subtag: rtc Description: Rungtu Chin Added: 2012-08-12 @@ -30543,6 +31075,11 @@ Description: Rwa Added: 2009-07-29 %% Type: language +Subtag: rwl +Description: Ruwila +Added: 2021-02-20 +%% +Type: language Subtag: rwm Description: Amba (Uganda) Added: 2009-07-29 @@ -30566,6 +31103,7 @@ Added: 2013-09-10 Type: language Subtag: rxw Description: Karuwali +Description: Garuwali Added: 2013-09-10 %% Type: language @@ -30902,6 +31440,7 @@ Added: 2005-10-16 %% Type: language Subtag: scp +Description: Hyolmo Description: Helambu Sherpa Added: 2009-07-29 %% @@ -30997,6 +31536,8 @@ Type: language Subtag: sdm Description: Semandang Added: 2009-07-29 +Deprecated: 2020-03-28 +Comments: see ebc, gef, sdq %% Type: language Subtag: sdn @@ -31015,6 +31556,11 @@ Description: Sherdukpen Added: 2009-07-29 %% Type: language +Subtag: sdq +Description: Semandang +Added: 2020-03-28 +%% +Type: language Subtag: sdr Description: Oraon Sadri Added: 2009-07-29 @@ -31853,6 +32399,7 @@ Type: language Subtag: slq Description: Salchuq Added: 2009-07-29 +Deprecated: 2023-03-17 %% Type: language Subtag: slr @@ -31913,6 +32460,8 @@ Type: language Subtag: smd Description: Sama Added: 2009-07-29 +Deprecated: 2022-02-25 +Preferred-Value: kmb %% Type: language Subtag: smf @@ -32019,6 +32568,8 @@ Type: language Subtag: snb Description: Sebuyau Added: 2009-07-29 +Deprecated: 2022-02-25 +Preferred-Value: iba %% Type: language Subtag: snc @@ -32130,7 +32681,7 @@ Added: 2009-07-29 %% Type: language Subtag: snz -Description: Sinsauru +Description: Kou Added: 2009-07-29 %% Type: language @@ -32414,6 +32965,11 @@ Description: Squamish Added: 2009-07-29 %% Type: language +Subtag: sqx +Description: Kufr Qassem Sign Language (KQSL) +Added: 2021-02-20 +%% +Type: language Subtag: sra Description: Saruga Added: 2009-07-29 @@ -32807,6 +33363,7 @@ Type: language Subtag: suj Description: Shubi Added: 2009-07-29 +Comments: see also xsj %% Type: language Subtag: suk @@ -32828,7 +33385,13 @@ Deprecated: 2010-03-11 Comments: see ulw, yan %% Type: language +Subtag: suo +Description: Bouni +Added: 2021-02-20 +%% +Type: language Subtag: suq +Description: Tirmaga-Chai Suri Description: Suri Added: 2009-07-29 %% @@ -33049,6 +33612,7 @@ Added: 2009-07-29 %% Type: language Subtag: sxg +Description: Shuhi Description: Shixing Added: 2009-07-29 %% @@ -33193,6 +33757,8 @@ Type: language Subtag: szd Description: Seru Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: umi %% Type: language Subtag: sze @@ -33235,6 +33801,11 @@ Description: Sawai Added: 2009-07-29 %% Type: language +Subtag: szy +Description: Sakizaya +Added: 2019-04-16 +%% +Type: language Subtag: taa Description: Lower Tanana Added: 2009-07-29 @@ -33360,6 +33931,7 @@ Type: language Subtag: tbb Description: Tapeba Added: 2009-07-29 +Deprecated: 2020-03-28 %% Type: language Subtag: tbc @@ -33388,6 +33960,7 @@ Added: 2009-07-29 %% Type: language Subtag: tbh +Description: Dharawal Description: Thurawal Added: 2009-07-29 %% @@ -33567,6 +34140,7 @@ Added: 2009-07-29 Type: language Subtag: tcs Description: Torres Strait Creole +Description: Yumplatok Added: 2009-07-29 %% Type: language @@ -33835,6 +34409,11 @@ Description: Tulishi Added: 2009-07-29 %% Type: language +Subtag: tez +Description: Tetserret +Added: 2018-03-08 +%% +Type: language Subtag: tfi Description: Tofin Gbe Added: 2009-07-29 @@ -33985,6 +34564,7 @@ Preferred-Value: tpo %% Type: language Subtag: thd +Description: Kuuk Thaayorre Description: Thayore Added: 2009-07-29 %% @@ -34069,6 +34649,8 @@ Type: language Subtag: thw Description: Thudam Added: 2009-07-29 +Deprecated: 2020-06-08 +Preferred-Value: ola %% Type: language Subtag: thx @@ -34228,6 +34810,11 @@ Description: Northern Tujia Added: 2009-07-29 %% Type: language +Subtag: tjj +Description: Tjungundji +Added: 2019-04-16 +%% +Type: language Subtag: tjl Description: Tai Laing Added: 2012-08-12 @@ -34248,6 +34835,11 @@ Description: Temacine Tamazight Added: 2009-07-29 %% Type: language +Subtag: tjp +Description: Tjupany +Added: 2019-04-16 +%% +Type: language Subtag: tjs Description: Southern Tujia Added: 2009-07-29 @@ -34399,7 +34991,7 @@ Added: 2009-07-29 Type: language Subtag: tlh Description: Klingon -Description: tlhIngan-Hol +Description: tlhIngan Hol Added: 2005-10-16 %% Type: language @@ -34547,6 +35139,8 @@ Type: language Subtag: tmk Description: Northwestern Tamang Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: tdg %% Type: language Subtag: tml @@ -34797,6 +35391,11 @@ Description: Tojolabal Added: 2009-07-29 %% Type: language +Subtag: tok +Description: Toki Pona +Added: 2022-02-25 +%% +Type: language Subtag: tol Description: Tolowa Added: 2009-07-29 @@ -34958,6 +35557,8 @@ Type: language Subtag: tpw Description: Tupí Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: tpn %% Type: language Subtag: tpx @@ -35139,6 +35740,8 @@ Added: 2009-07-29 %% Type: language Subtag: trv +Description: Sediq +Description: Seediq Description: Taroko Added: 2009-07-29 %% @@ -35551,6 +36154,11 @@ Description: Te'un Added: 2009-07-29 %% Type: language +Subtag: tvi +Description: Tulai +Added: 2023-03-17 +%% +Type: language Subtag: tvk Description: Southeast Ambrym Added: 2009-07-29 @@ -35597,6 +36205,11 @@ Description: Sedoa Added: 2009-07-29 %% Type: language +Subtag: tvx +Description: Taivoan +Added: 2019-04-16 +%% +Type: language Subtag: tvy Description: Timor Pidgin Added: 2009-07-29 @@ -35864,6 +36477,11 @@ Description: Teke-Tyee Added: 2009-07-29 %% Type: language +Subtag: tyy +Description: Tiyaa +Added: 2021-02-20 +%% +Type: language Subtag: tyz Description: Tày Added: 2009-07-29 @@ -36020,6 +36638,11 @@ Description: Ughele Added: 2009-07-29 %% Type: language +Subtag: ugh +Description: Kubachi +Added: 2022-02-25 +%% +Type: language Subtag: ugn Description: Ugandan Sign Language Added: 2009-07-29 @@ -36075,6 +36698,11 @@ Description: Ukhwejo Added: 2009-07-29 %% Type: language +Subtag: uki +Description: Kui (India) +Added: 2020-03-28 +%% +Type: language Subtag: ukk Description: Muak Sa-aak Added: 2017-02-23 @@ -36106,6 +36734,11 @@ Description: Ukue Added: 2009-07-29 %% Type: language +Subtag: ukv +Description: Kuku +Added: 2020-03-28 +%% +Type: language Subtag: ukw Description: Ukwuani-Aboh-Ndoni Added: 2009-07-29 @@ -36148,7 +36781,7 @@ Added: 2009-07-29 %% Type: language Subtag: ulk -Description: Meriam +Description: Meriam Mir Added: 2009-07-29 %% Type: language @@ -36177,6 +36810,11 @@ Description: Ulwa Added: 2010-03-11 %% Type: language +Subtag: uly +Description: Buli +Added: 2023-03-17 +%% +Type: language Subtag: uma Description: Umatilla Added: 2009-07-29 @@ -36198,6 +36836,7 @@ Added: 2009-07-29 %% Type: language Subtag: umg +Description: Morrobalama Description: Umbuygamu Added: 2009-07-29 %% @@ -36264,6 +36903,11 @@ Description: Ngarinyin Added: 2009-07-29 %% Type: language +Subtag: uni +Description: Uni +Added: 2021-02-20 +%% +Type: language Subtag: unk Description: Enawené-Nawé Added: 2009-07-29 @@ -36314,6 +36958,11 @@ Deprecated: 2015-02-12 Preferred-Value: ema %% Type: language +Subtag: uon +Description: Kulon +Added: 2022-02-25 +%% +Type: language Subtag: upi Description: Umeda Added: 2009-07-29 @@ -36468,6 +37117,11 @@ Description: Uspanteco Added: 2009-07-29 %% Type: language +Subtag: uss +Description: us-Saare +Added: 2019-04-16 +%% +Type: language Subtag: usu Description: Uya Added: 2009-07-29 @@ -36483,6 +37137,11 @@ Description: Ute-Southern Paiute Added: 2009-07-29 %% Type: language +Subtag: uth +Description: ut-Hun +Added: 2019-04-16 +%% +Type: language Subtag: utp Description: Amba (Solomon Islands) Added: 2009-07-29 @@ -36506,6 +37165,8 @@ Type: language Subtag: uun Description: Kulon-Pazeh Added: 2009-07-29 +Deprecated: 2022-02-25 +Comments: see pzh, uon %% Type: language Subtag: uur @@ -36588,7 +37249,6 @@ Added: 2005-10-16 Type: language Subtag: vaj Description: Sekele -Description: Northwestern !Kung Description: Northwestern ǃKung Description: Vasekele Added: 2009-07-29 @@ -36746,6 +37406,11 @@ Description: Iduna Added: 2009-07-29 %% Type: language +Subtag: vjk +Description: Bajjika +Added: 2023-03-17 +%% +Type: language Subtag: vka Description: Kariyarra Added: 2009-07-29 @@ -36754,6 +37419,8 @@ Type: language Subtag: vki Description: Ija-Zuba Added: 2009-07-29 +Deprecated: 2021-02-20 +Comments: see vkn, vkz %% Type: language Subtag: vkj @@ -36777,6 +37444,11 @@ Description: Kamakan Added: 2009-07-29 %% Type: language +Subtag: vkn +Description: Koro Nulu +Added: 2021-02-20 +%% +Type: language Subtag: vko Description: Kodeoha Added: 2009-07-29 @@ -36798,6 +37470,11 @@ Description: Kurrama Added: 2009-07-29 %% Type: language +Subtag: vkz +Description: Koro Zuba +Added: 2021-02-20 +%% +Type: language Subtag: vlp Description: Valpei Added: 2009-07-29 @@ -37030,7 +37707,8 @@ Added: 2009-07-29 %% Type: language Subtag: wad -Description: Wandamen +Description: Wamesa +Description: Wondama Added: 2009-07-29 %% Type: language @@ -37097,7 +37775,7 @@ Added: 2009-07-29 %% Type: language Subtag: waq -Description: Wageman +Description: Wagiman Added: 2009-07-29 %% Type: language @@ -37220,7 +37898,7 @@ Added: 2017-02-23 %% Type: language Subtag: wbt -Description: Wanman +Description: Warnman Added: 2009-07-29 %% Type: language @@ -37264,6 +37942,11 @@ Description: Wadikali Added: 2013-09-10 %% Type: language +Subtag: wdt +Description: Wendat +Added: 2022-02-25 +%% +Type: language Subtag: wdu Description: Wadjigu Added: 2009-07-29 @@ -37367,6 +38050,7 @@ Added: 2010-03-11 %% Type: language Subtag: wgg +Description: Wangkangurru Description: Wangganguru Added: 2009-07-29 %% @@ -37440,7 +38124,7 @@ Added: 2009-07-29 %% Type: language Subtag: wig -Description: Wik-Ngathana +Description: Wik Ngathan Added: 2009-07-29 %% Type: language @@ -37544,6 +38228,11 @@ Description: Kalanadi Added: 2009-07-29 %% Type: language +Subtag: wkr +Description: Keerray-Woorroong +Added: 2019-04-16 +%% +Type: language Subtag: wku Description: Kunduvadi Added: 2009-07-29 @@ -37579,6 +38268,11 @@ Description: Kunbarlang Added: 2009-07-29 %% Type: language +Subtag: wlh +Description: Welaun +Added: 2020-03-28 +%% +Type: language Subtag: wli Description: Waioli Added: 2009-07-29 @@ -37664,6 +38358,11 @@ Description: Wambule Added: 2009-07-29 %% Type: language +Subtag: wmg +Description: Western Minyag +Added: 2021-02-20 +%% +Type: language Subtag: wmh Description: Waima'a Added: 2009-07-29 @@ -37710,7 +38409,7 @@ Added: 2009-07-29 %% Type: language Subtag: wnb -Description: Wanambre +Description: Mokati Added: 2009-07-29 %% Type: language @@ -37776,10 +38475,12 @@ Added: 2013-09-10 Type: language Subtag: wny Description: Wanyi +Description: Waanyi Added: 2012-08-12 %% Type: language Subtag: woa +Description: Kuwema Description: Tyaraity Added: 2009-07-29 %% @@ -37867,9 +38568,12 @@ Type: language Subtag: wra Description: Warapu Added: 2009-07-29 +Deprecated: 2021-02-20 +Comments: see bpe, suo, uni %% Type: language Subtag: wrb +Description: Waluwarra Description: Warluwara Added: 2009-07-29 %% @@ -37877,15 +38581,17 @@ Type: language Subtag: wrd Description: Warduji Added: 2009-07-29 +Deprecated: 2022-02-25 %% Type: language Subtag: wrg Description: Warungu +Description: Gudjal Added: 2009-07-29 %% Type: language Subtag: wrh -Description: Wiradhuri +Description: Wiradjuri Added: 2009-07-29 %% Type: language @@ -38006,6 +38712,11 @@ Description: Wotapuri-Katarqalai Added: 2009-07-29 %% Type: language +Subtag: wtb +Description: Matambwe +Added: 2023-03-17 +%% +Type: language Subtag: wtf Description: Watiwa Added: 2009-07-29 @@ -38141,6 +38852,8 @@ Type: language Subtag: wya Description: Wyandot Added: 2009-07-29 +Deprecated: 2022-02-25 +Comments: see wdt, wyn %% Type: language Subtag: wyb @@ -38158,6 +38871,11 @@ Description: Wymysorys Added: 2009-07-29 %% Type: language +Subtag: wyn +Description: Wyandot +Added: 2022-02-25 +%% +Type: language Subtag: wyr Description: Wayoró Added: 2009-07-29 @@ -38220,7 +38938,6 @@ Added: 2005-10-16 %% Type: language Subtag: xam -Description: /Xam Description: ǀXam Added: 2009-07-29 %% @@ -38359,6 +39076,7 @@ Deprecated: 2016-05-30 %% Type: language Subtag: xby +Description: Batjala Description: Batyala Added: 2013-09-10 %% @@ -38464,6 +39182,11 @@ Description: Kwandu Added: 2017-02-23 %% Type: language +Subtag: xdq +Description: Kaitag +Added: 2022-02-25 +%% +Type: language Subtag: xdy Description: Malayic Dayak Added: 2009-07-29 @@ -38480,7 +39203,6 @@ Added: 2009-07-29 %% Type: language Subtag: xeg -Description: //Xegwi Description: ǁXegwi Added: 2009-07-29 %% @@ -38608,6 +39330,11 @@ Added: 2009-07-29 Macrolanguage: lah %% Type: language +Subtag: xhm +Description: Middle Khmer (1400 to 1850 CE) +Added: 2022-02-25 +%% +Type: language Subtag: xhr Description: Hernican Added: 2009-07-29 @@ -38744,6 +39471,7 @@ Added: 2009-07-29 %% Type: language Subtag: xkk +Description: Kachok Description: Kaco' Added: 2009-07-29 %% @@ -38919,7 +39647,7 @@ Added: 2009-07-29 %% Type: language Subtag: xmh -Description: Kuku-Muminh +Description: Kugu-Muminh Added: 2009-07-29 %% Type: language @@ -38998,6 +39726,7 @@ Macrolanguage: mg %% Type: language Subtag: xmx +Description: Salawati Description: Maden Added: 2009-07-29 %% @@ -39043,11 +39772,21 @@ Description: Ngarigu Added: 2013-09-10 %% Type: language +Subtag: xnj +Description: Ngoni (Tanzania) +Added: 2021-02-20 +%% +Type: language Subtag: xnk Description: Nganakarti Added: 2013-09-10 %% Type: language +Subtag: xnm +Description: Ngumbarl +Added: 2020-03-28 +%% +Type: language Subtag: xnn Description: Northern Kankanay Added: 2009-07-29 @@ -39058,6 +39797,11 @@ Description: Anglo-Norman Added: 2009-07-29 %% Type: language +Subtag: xnq +Description: Ngoni (Mozambique) +Added: 2021-02-20 +%% +Type: language Subtag: xnr Description: Kangri Added: 2009-07-29 @@ -39150,22 +39894,45 @@ Description: Pirriya Added: 2013-09-10 %% Type: language +Subtag: xpb +Description: Northeastern Tasmanian +Description: Pyemmairrener +Added: 2020-03-28 +%% +Type: language Subtag: xpc Description: Pecheneg Added: 2009-07-29 %% Type: language +Subtag: xpd +Description: Oyster Bay Tasmanian +Added: 2020-03-28 +%% +Type: language Subtag: xpe Description: Liberia Kpelle Added: 2009-07-29 Macrolanguage: kpe %% Type: language +Subtag: xpf +Description: Southeast Tasmanian +Description: Nuenonne +Added: 2020-03-28 +%% +Type: language Subtag: xpg Description: Phrygian Added: 2009-07-29 %% Type: language +Subtag: xph +Description: North Midlands Tasmanian +Description: Tyerrenoterpanner +Added: 2020-03-28 +%% +Type: language Subtag: xpi Description: Pictish Added: 2009-07-29 @@ -39181,6 +39948,11 @@ Description: Kulina Pano Added: 2009-07-29 %% Type: language +Subtag: xpl +Description: Port Sorell Tasmanian +Added: 2020-03-28 +%% +Type: language Subtag: xpm Description: Pumpokol Added: 2009-07-29 @@ -39226,11 +39998,34 @@ Description: Punic Added: 2009-07-29 %% Type: language +Subtag: xpv +Description: Northern Tasmanian +Description: Tommeginne +Added: 2020-03-28 +%% +Type: language +Subtag: xpw +Description: Northwestern Tasmanian +Description: Peerapper +Added: 2020-03-28 +%% +Type: language +Subtag: xpx +Description: Southwestern Tasmanian +Description: Toogee +Added: 2020-03-28 +%% +Type: language Subtag: xpy Description: Puyo Added: 2009-07-29 %% Type: language +Subtag: xpz +Description: Bruny Island Tasmanian +Added: 2020-03-28 +%% +Type: language Subtag: xqa Description: Karakhanid Added: 2009-07-29 @@ -39284,6 +40079,8 @@ Type: language Subtag: xrq Description: Karranga Added: 2013-09-10 +Deprecated: 2020-03-28 +Preferred-Value: dmw %% Type: language Subtag: xrr @@ -39344,8 +40141,7 @@ Type: language Subtag: xsj Description: Subi Added: 2009-07-29 -Deprecated: 2015-02-12 -Preferred-Value: suj +Comments: see also suj %% Type: language Subtag: xsl @@ -39387,6 +40183,8 @@ Type: language Subtag: xss Description: Assan Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: zko %% Type: language Subtag: xsu @@ -39517,6 +40315,8 @@ Type: language Subtag: xtz Description: Tasmanian Added: 2009-07-29 +Deprecated: 2020-03-28 +Comments: see xpb, xpd, xpf, xph, xpl, xpv, xpw, xpx, xpz %% Type: language Subtag: xua @@ -39546,6 +40346,7 @@ Added: 2009-07-29 Type: language Subtag: xul Description: Ngunawal +Description: Nunukul Added: 2013-09-10 %% Type: language @@ -39967,6 +40768,11 @@ Description: Chepya Added: 2009-07-29 %% Type: language +Subtag: ycr +Description: Yilan Creole +Added: 2023-03-17 +%% +Type: language Subtag: yda Description: Yanda Added: 2013-09-10 @@ -40179,6 +40985,7 @@ Added: 2009-07-29 %% Type: language Subtag: yin +Description: Riang Lai Description: Yinchia Added: 2009-07-29 %% @@ -40245,6 +41052,11 @@ Description: Northern Yukaghir Added: 2009-07-29 %% Type: language +Subtag: ykh +Description: Khamnigan Mongol +Added: 2023-03-17 +%% +Type: language Subtag: yki Description: Yoke Added: 2009-07-29 @@ -40711,6 +41523,11 @@ Description: Yugoslavian Sign Language Added: 2009-07-29 %% Type: language +Subtag: ysm +Description: Myanmar Sign Language +Added: 2021-02-20 +%% +Type: language Subtag: ysn Description: Sani Added: 2009-07-29 @@ -41137,6 +41954,11 @@ Description: Zari Added: 2009-07-29 %% Type: language +Subtag: zba +Description: Balaibalan +Added: 2020-03-28 +%% +Type: language Subtag: zbc Description: Central Berawan Added: 2009-07-29 @@ -41160,6 +41982,11 @@ Description: Batui Added: 2009-07-29 %% Type: language +Subtag: zbu +Description: Bu (Bauchi State) +Added: 2021-02-20 +%% +Type: language Subtag: zbw Description: West Berawan Added: 2009-07-29 @@ -41171,6 +41998,12 @@ Added: 2009-07-29 Macrolanguage: zap %% Type: language +Subtag: zcd +Description: Las Delicias Zapotec +Added: 2022-02-25 +Macrolanguage: zap +%% +Type: language Subtag: zch Description: Central Hongshuihe Zhuang Added: 2009-07-29 @@ -41198,6 +42031,11 @@ Added: 2009-07-29 Macrolanguage: za %% Type: language +Subtag: zem +Description: Zeem +Added: 2023-03-17 +%% +Type: language Subtag: zen Description: Zenaga Added: 2005-10-16 @@ -41302,6 +42140,8 @@ Type: language Subtag: zir Description: Ziriya Added: 2009-07-29 +Deprecated: 2020-03-28 +Preferred-Value: scv %% Type: language Subtag: ziw @@ -41322,6 +42162,8 @@ Type: language Subtag: zkb Description: Koibal Added: 2009-07-29 +Deprecated: 2023-03-17 +Preferred-Value: kjh %% Type: language Subtag: zkd @@ -41384,6 +42226,11 @@ Description: Khazar Added: 2009-07-29 %% Type: language +Subtag: zla +Description: Zula +Added: 2021-02-20 +%% +Type: language Subtag: zle Description: East Slavic languages Added: 2009-07-29 @@ -41420,6 +42267,11 @@ Added: 2009-07-29 Scope: collection %% Type: language +Subtag: zlu +Description: Zul +Added: 2023-03-17 +%% +Type: language Subtag: zlw Description: West Slavic languages Added: 2009-07-29 @@ -41483,12 +42335,13 @@ Added: 2009-07-29 %% Type: language Subtag: zml -Description: Madngele +Description: Matngala Added: 2009-07-29 %% Type: language Subtag: zmm Description: Marimanindji +Description: Marramaninyshi Added: 2009-07-29 %% Type: language @@ -41923,6 +42776,8 @@ Type: language Subtag: zua Description: Zeem Added: 2009-07-29 +Deprecated: 2023-03-17 +Comments: see cxh, dsk, dyr, tvi, zem %% Type: language Subtag: zuh @@ -42130,13 +42985,21 @@ Type: extlang Subtag: ajp Description: South Levantine Arabic Added: 2009-07-29 +Deprecated: 2023-03-17 Preferred-Value: ajp Prefix: ar Macrolanguage: ar %% Type: extlang +Subtag: ajs +Description: Algerian Jewish Sign Language +Added: 2022-02-25 +Preferred-Value: ajs +Prefix: sgn +%% +Type: extlang Subtag: apc -Description: North Levantine Arabic +Description: Levantine Arabic Added: 2009-07-29 Preferred-Value: apc Prefix: ar @@ -42199,6 +43062,7 @@ Prefix: sgn %% Type: extlang Subtag: asf +Description: Auslan Description: Australian Sign Language Added: 2009-07-29 Preferred-Value: asf @@ -42277,6 +43141,7 @@ Type: extlang Subtag: bbz Description: Babalia Creole Arabic Added: 2009-07-29 +Deprecated: 2020-03-28 Preferred-Value: bbz Prefix: ar Macrolanguage: ar @@ -42394,6 +43259,15 @@ Prefix: zh Macrolanguage: zh %% Type: extlang +Subtag: cnp +Description: Northern Ping Chinese +Description: Northern Pinghua +Added: 2020-03-28 +Preferred-Value: cnp +Prefix: zh +Macrolanguage: zh +%% +Type: extlang Subtag: coa Description: Cocos Islands Malay Added: 2009-07-29 @@ -42461,6 +43335,15 @@ Preferred-Value: csn Prefix: sgn %% Type: extlang +Subtag: csp +Description: Southern Ping Chinese +Description: Southern Pinghua +Added: 2020-03-28 +Preferred-Value: csp +Prefix: zh +Macrolanguage: zh +%% +Type: extlang Subtag: csq Description: Croatia Sign Language Added: 2009-07-29 @@ -42475,6 +43358,13 @@ Preferred-Value: csr Prefix: sgn %% Type: extlang +Subtag: csx +Description: Cambodian Sign Language +Added: 2021-02-20 +Preferred-Value: csx +Prefix: sgn +%% +Type: extlang Subtag: czh Description: Huizhou Chinese Added: 2009-07-29 @@ -42512,6 +43402,13 @@ Preferred-Value: dsl Prefix: sgn %% Type: extlang +Subtag: dsz +Description: Mardin Sign Language +Added: 2022-02-25 +Preferred-Value: dsz +Prefix: sgn +%% +Type: extlang Subtag: dup Description: Duano Added: 2009-07-29 @@ -42527,6 +43424,13 @@ Preferred-Value: ecs Prefix: sgn %% Type: extlang +Subtag: ehs +Description: Miyakubo Sign Language +Added: 2021-02-20 +Preferred-Value: ehs +Prefix: sgn +%% +Type: extlang Subtag: esl Description: Egypt Sign Language Added: 2009-07-29 @@ -42811,6 +43715,13 @@ Preferred-Value: jhs Prefix: sgn %% Type: extlang +Subtag: jks +Description: Amami Koniya Sign Language +Added: 2021-02-20 +Preferred-Value: jks +Prefix: sgn +%% +Type: extlang Subtag: jls Description: Jamaican Sign Language Added: 2010-03-11 @@ -42909,6 +43820,14 @@ Prefix: ms Macrolanguage: ms %% Type: extlang +Subtag: lgs +Description: Guinea-Bissau Sign Language +Description: Língua Gestual Guineense +Added: 2023-03-17 +Preferred-Value: lgs +Prefix: sgn +%% +Type: extlang Subtag: liw Description: Col Added: 2009-07-29 @@ -42924,9 +43843,26 @@ Preferred-Value: lls Prefix: sgn %% Type: extlang +Subtag: lsb +Description: Burundian Sign Language +Description: Langue des Signes Burundaise +Added: 2021-02-20 +Preferred-Value: lsb +Prefix: sgn +%% +Type: extlang +Subtag: lsc +Description: Albarradas Sign Language +Description: Lengua de señas Albarradas +Added: 2022-02-25 +Preferred-Value: lsc +Prefix: sgn +%% +Type: extlang Subtag: lsg Description: Lyons Sign Language Added: 2009-07-29 +Deprecated: 2018-03-08 Preferred-Value: lsg Prefix: sgn %% @@ -42938,6 +43874,13 @@ Preferred-Value: lsl Prefix: sgn %% Type: extlang +Subtag: lsn +Description: Tibetan Sign Language +Added: 2019-04-16 +Preferred-Value: lsn +Prefix: sgn +%% +Type: extlang Subtag: lso Description: Laos Sign Language Added: 2009-07-29 @@ -42960,6 +43903,22 @@ Preferred-Value: lst Prefix: sgn %% Type: extlang +Subtag: lsv +Description: Sivia Sign Language +Added: 2019-04-16 +Preferred-Value: lsv +Prefix: sgn +%% +Type: extlang +Subtag: lsw +Description: Seychelles Sign Language +Description: Lalang Siny Seselwa +Description: Langue des Signes Seychelloise +Added: 2022-02-25 +Preferred-Value: lsw +Prefix: sgn +%% +Type: extlang Subtag: lsy Description: Mauritian Sign Language Added: 2010-03-11 @@ -42983,6 +43942,13 @@ Prefix: lv Macrolanguage: lv %% Type: extlang +Subtag: lws +Description: Malawian Sign Language +Added: 2018-03-08 +Preferred-Value: lws +Prefix: sgn +%% +Type: extlang Subtag: lzh Description: Literary Chinese Added: 2009-07-29 @@ -43244,6 +44210,7 @@ Prefix: sgn %% Type: extlang Subtag: psc +Description: Iranian Sign Language Description: Persian Sign Language Added: 2009-07-29 Preferred-Value: psc @@ -43308,6 +44275,13 @@ Preferred-Value: pys Prefix: sgn %% Type: extlang +Subtag: rib +Description: Bribri Sign Language +Added: 2022-02-25 +Preferred-Value: rib +Prefix: sgn +%% +Type: extlang Subtag: rms Description: Romanian Sign Language Added: 2009-07-29 @@ -43315,10 +44289,18 @@ Preferred-Value: rms Prefix: sgn %% Type: extlang +Subtag: rnb +Description: Brunca Sign Language +Added: 2022-02-25 +Preferred-Value: rnb +Prefix: sgn +%% +Type: extlang Subtag: rsi Description: Rennellese Sign Language Added: 2009-07-29 Deprecated: 2017-02-23 +Preferred-Value: rsi Prefix: sgn %% Type: extlang @@ -43336,6 +44318,13 @@ Preferred-Value: rsm Prefix: sgn %% Type: extlang +Subtag: rsn +Description: Rwandan Sign Language +Added: 2022-02-25 +Preferred-Value: rsn +Prefix: sgn +%% +Type: extlang Subtag: sdl Description: Saudi Arabian Sign Language Added: 2009-07-29 @@ -43408,6 +44397,13 @@ Preferred-Value: sqs Prefix: sgn %% Type: extlang +Subtag: sqx +Description: Kufr Qassem Sign Language (KQSL) +Added: 2021-02-20 +Preferred-Value: sqx +Prefix: sgn +%% +Type: extlang Subtag: ssh Description: Shihhi Arabic Added: 2009-07-29 @@ -43673,6 +44669,7 @@ Subtag: yds Description: Yiddish Sign Language Added: 2009-07-29 Deprecated: 2015-02-12 +Preferred-Value: yds Prefix: sgn %% Type: extlang @@ -43697,6 +44694,13 @@ Preferred-Value: ysl Prefix: sgn %% Type: extlang +Subtag: ysm +Description: Myanmar Sign Language +Added: 2021-02-20 +Preferred-Value: ysm +Prefix: sgn +%% +Type: extlang Subtag: yue Description: Yue Chinese Description: Cantonese @@ -43876,6 +44880,11 @@ Description: Cherokee Added: 2005-10-16 %% Type: script +Subtag: Chrs +Description: Chorasmian +Added: 2019-09-11 +%% +Type: script Subtag: Cirt Description: Cirth Added: 2005-10-16 @@ -43912,6 +44921,11 @@ Description: Nagari Added: 2005-10-16 %% Type: script +Subtag: Diak +Description: Dives Akuru +Added: 2019-09-11 +%% +Type: script Subtag: Dogr Description: Dogra Added: 2017-01-13 @@ -43949,6 +44963,11 @@ Description: Elbasan Added: 2010-08-16 %% Type: script +Subtag: Elym +Description: Elymaic +Added: 2018-10-28 +%% +Type: script Subtag: Ethi Description: Ethiopic Description: Geʻez @@ -44126,6 +45145,11 @@ Description: Katakana Added: 2005-10-16 %% Type: script +Subtag: Kawi +Description: Kawi +Added: 2021-12-24 +%% +Type: script Subtag: Khar Description: Kharoshthi Added: 2005-10-16 @@ -44345,6 +45369,16 @@ Description: Burmese Added: 2005-10-16 %% Type: script +Subtag: Nagm +Description: Nag Mundari +Added: 2021-12-24 +%% +Type: script +Subtag: Nand +Description: Nandinagari +Added: 2018-10-28 +%% +Type: script Subtag: Narb Description: Old North Arabian Description: Ancient North Arabian @@ -44425,6 +45459,11 @@ Description: Osmanya Added: 2005-10-16 %% Type: script +Subtag: Ougr +Description: Old Uyghur +Added: 2021-02-12 +%% +Type: script Subtag: Palm Description: Palmyrene Added: 2010-04-10 @@ -44435,6 +45474,16 @@ Description: Pau Cin Hau Added: 2013-12-02 %% Type: script +Subtag: Pcun +Description: Proto-Cuneiform +Added: 2021-02-12 +%% +Type: script +Subtag: Pelm +Description: Proto-Elamite +Added: 2021-02-12 +%% +Type: script Subtag: Perm Description: Old Permic Added: 2005-10-16 @@ -44481,11 +45530,21 @@ Description: Inscriptional Parthian Added: 2007-12-05 %% Type: script +Subtag: Psin +Description: Proto-Sinaitic +Added: 2021-02-12 +%% +Type: script Subtag: Qaaa..Qabx Description: Private use Added: 2005-10-16 %% Type: script +Subtag: Ranj +Description: Ranjana +Added: 2021-02-12 +%% +Type: script Subtag: Rjng Description: Rejang Description: Redjang @@ -44493,6 +45552,11 @@ Description: Kaganga Added: 2006-10-17 %% Type: script +Subtag: Rohg +Description: Hanifi Rohingya +Added: 2017-12-13 +%% +Type: script Subtag: Roro Description: Rongorongo Added: 2005-10-16 @@ -44563,6 +45627,16 @@ Description: Sinhala Added: 2005-10-16 %% Type: script +Subtag: Sogd +Description: Sogdian +Added: 2017-12-13 +%% +Type: script +Subtag: Sogo +Description: Old Sogdian +Added: 2017-12-13 +%% +Type: script Subtag: Sora Description: Sora Sompeng Added: 2011-01-07 @@ -44578,6 +45652,11 @@ Description: Sundanese Added: 2006-07-21 %% Type: script +Subtag: Sunu +Description: Sunuwar +Added: 2021-12-24 +%% +Type: script Subtag: Sylo Description: Syloti Nagri Added: 2005-10-16 @@ -44683,6 +45762,16 @@ Description: Tirhuta Added: 2011-08-16 %% Type: script +Subtag: Tnsa +Description: Tangsa +Added: 2021-03-05 +%% +Type: script +Subtag: Toto +Description: Toto +Added: 2020-05-12 +%% +Type: script Subtag: Ugar Description: Ugaritic Added: 2005-10-16 @@ -44698,6 +45787,11 @@ Description: Visible Speech Added: 2005-10-16 %% Type: script +Subtag: Vith +Description: Vithkuqi +Added: 2021-03-05 +%% +Type: script Subtag: Wara Description: Warang Citi Description: Varang Kshiti @@ -44724,6 +45818,11 @@ Description: Sumero-Akkadian cuneiform Added: 2005-10-16 %% Type: script +Subtag: Yezi +Description: Yezidi +Added: 2019-09-11 +%% +Type: script Subtag: Yiii Description: Yi Added: 2005-10-16 @@ -45047,6 +46146,11 @@ Description: Clipperton Island Added: 2009-07-29 %% Type: region +Subtag: CQ +Description: Sark +Added: 2023-02-07 +%% +Type: region Subtag: CR Description: Costa Rica Added: 2005-10-16 @@ -45568,7 +46672,7 @@ Added: 2005-10-16 %% Type: region Subtag: MK -Description: The Former Yugoslav Republic of Macedonia +Description: North Macedonia Added: 2005-10-16 %% Type: region @@ -45930,6 +47034,8 @@ Added: 2005-10-16 %% Type: region Subtag: SZ +Description: Eswatini +Description: eSwatini Description: Swaziland Added: 2005-10-16 %% @@ -46002,6 +47108,7 @@ Preferred-Value: TL %% Type: region Subtag: TR +Description: Türkiye Description: Turkey Added: 2005-10-16 %% @@ -46378,6 +47485,12 @@ Comments: Denotes conventions established by the Academia Brasileira de Letras in 1943 and generally used in Brazil until 2009 %% Type: variant +Subtag: akuapem +Description: Akuapem Twi +Added: 2017-06-05 +Prefix: tw +%% +Type: variant Subtag: alalc97 Description: ALA-LC Romanization, 1997 edition Added: 2009-12-09 @@ -46396,12 +47509,6 @@ Comments: Aluku dialect of the "Busi Nenge Tongo" English-based Creole continuum in Eastern Suriname and Western French Guiana %% Type: variant -Subtag: akuapem -Description: Akuapem Twi -Added: 2017-06-05 -Prefix: tw -%% -Type: variant Subtag: ao1990 Description: Portuguese Language Orthographic Agreement of 1990 (Acordo Ortográfico da Língua Portuguesa de 1990) @@ -46412,16 +47519,35 @@ Comments: Portuguese orthography conventions established in 1990 but not brought into effect until 2009 %% Type: variant +Subtag: aranes +Description: Aranese +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in the Val d'Aran +%% +Type: variant Subtag: arevela Description: Eastern Armenian Added: 2006-09-18 +Deprecated: 2018-03-24 Prefix: hy +Comments: Preferred tag is hy %% Type: variant Subtag: arevmda Description: Western Armenian Added: 2006-09-18 +Deprecated: 2018-03-24 Prefix: hy +Comments: Preferred tag is hyw +%% +Type: variant +Subtag: arkaika +Description: Arcaicam Esperantom +Description: Arkaika Esperanto +Added: 2020-12-17 +Prefix: eo +Comments: Archaic Esperanto variant devised by Manuel Halvelik %% Type: variant Subtag: asante @@ -46431,6 +47557,13 @@ Added: 2017-06-05 Prefix: tw %% Type: variant +Subtag: auvern +Description: Auvergnat +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in Auvergne +%% +Type: variant Subtag: baku1926 Description: Unified Turkic Latin Alphabet (Historical) Added: 2007-04-18 @@ -46479,6 +47612,23 @@ Added: 2010-07-28 Prefix: sa %% Type: variant +Subtag: bciav +Description: BCI Blissymbolics AV +Added: 2023-05-11 +Prefix: zbl +Comments: Name given to a subset of the variety of Blissymbolics curated + by Blissymbolics Communication International, as represented by + entries in the BCI Authorized Vocabulary +%% +Type: variant +Subtag: bcizbl +Description: BCI Blissymbolics +Added: 2023-05-11 +Prefix: zbl +Comments: Name given to the variety of Blissymbolics curated by + Blissymbolics Communication International +%% +Type: variant Subtag: biscayan Description: Biscayan dialect of Basque Added: 2010-04-13 @@ -46510,6 +47660,19 @@ Prefix: en Comments: Jargon embedded in American English %% Type: variant +Subtag: bornholm +Description: Bornholmsk +Added: 2019-03-27 +Prefix: da +%% +Type: variant +Subtag: cisaup +Description: Cisalpine +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in northwestern Italy +%% +Type: variant Subtag: colb1945 Description: Portuguese-Brazilian Orthographic Convention of 1945 (Convenção Ortográfica Luso-Brasileira de 1945) @@ -46528,6 +47691,12 @@ Added: 2015-12-07 Prefix: en %% Type: variant +Subtag: creiss +Description: Occitan variants of the Croissant area +Added: 2018-04-22 +Prefix: oc +%% +Type: variant Subtag: dajnko Description: Slovene in Dajnko alphabet Added: 2012-06-27 @@ -46556,6 +47725,11 @@ Description: International Phonetic Alphabet Added: 2006-12-11 %% Type: variant +Subtag: fonkirsh +Description: Kirshenbaum Phonetic Alphabet +Added: 2018-04-22 +%% +Type: variant Subtag: fonnapa Description: North American Phonetic Alphabet Description: Americanist Phonetic Notation @@ -46573,6 +47747,65 @@ Added: 2010-10-23 Comments: Indicates that the content is transcribed according to X-SAMPA %% Type: variant +Subtag: gallo +Description: Gallo +Added: 2021-08-05 +Prefix: fr +%% +Type: variant +Subtag: gascon +Description: Gascon +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in Gascony +%% +Type: variant +Subtag: grclass +Description: Classical Occitan orthography +Added: 2018-04-22 +Prefix: oc +Prefix: oc-aranes +Prefix: oc-auvern +Prefix: oc-cisaup +Prefix: oc-creiss +Prefix: oc-gascon +Prefix: oc-lemosin +Prefix: oc-lengadoc +Prefix: oc-nicard +Prefix: oc-provenc +Prefix: oc-vivaraup +Comments: Classical written standard for Occitan developed in 1935 by + Alibèrt +%% +Type: variant +Subtag: grital +Description: Italian-inspired Occitan orthography +Added: 2018-04-22 +Prefix: oc +Prefix: oc-cisaup +Prefix: oc-nicard +Prefix: oc-provenc +%% +Type: variant +Subtag: grmistr +Description: Mistralian or Mistralian-inspired Occitan orthography +Added: 2018-04-22 +Prefix: oc +Prefix: oc-aranes +Prefix: oc-auvern +Prefix: oc-cisaup +Prefix: oc-creiss +Prefix: oc-gascon +Prefix: oc-lemosin +Prefix: oc-lengadoc +Prefix: oc-nicard +Prefix: oc-provenc +Prefix: oc-vivaraup +Comments: Written standard developed by Romanilha in 1853 and used by + Mistral and the Félibres, including derived standards such as Escolo + dóu Po, Escolo Gaston Febus, and others +%% +Type: variant Subtag: hepburn Description: Hepburn romanization Added: 2009-10-01 @@ -46617,6 +47850,13 @@ Added: 2010-07-28 Prefix: sa %% Type: variant +Subtag: ivanchov +Description: Bulgarian in 1899 orthography +Added: 2017-12-13 +Prefix: bg +Comments: Bulgarian orthography introduced by Todor Ivanchov in 1899 +%% +Type: variant Subtag: jauer Description: Jauer dialect of Romansh Added: 2010-06-29 @@ -46659,6 +47899,20 @@ Added: 2010-07-28 Prefix: sa %% Type: variant +Subtag: lemosin +Description: Limousin +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in Limousin +%% +Type: variant +Subtag: lengadoc +Description: Languedocien +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in Languedoc +%% +Type: variant Subtag: lipaw Description: The Lipovaz dialect of Resian Description: The Lipovec dialect of Resian @@ -46668,6 +47922,19 @@ Comments: The dialect of Lipovaz/Lipovec is one of the minor local dialects of Resian %% Type: variant +Subtag: ltg1929 +Description: The Latgalian language orthography codified in 1929 +Added: 2022-08-05 +Prefix: ltg +%% +Type: variant +Subtag: ltg2007 +Description: The Latgalian language orthography codified in the language + law in 2007 +Added: 2022-06-23 +Prefix: ltg +%% +Type: variant Subtag: luna1918 Description: Post-1917 Russian orthography Added: 2010-10-10 @@ -46712,6 +47979,13 @@ Added: 2015-11-25 Prefix: en-CA %% Type: variant +Subtag: nicard +Description: Niçard +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in Nice +%% +Type: variant Subtag: njiva Description: The Gniva dialect of Resian Description: The Njiva dialect of Resian @@ -46777,6 +48051,16 @@ Comments: Pamaka dialect of the "Busi Nenge Tongo" English-based Creole continuum in Eastern Suriname and Western French Guiana %% Type: variant +Subtag: peano +Description: Latino Sine Flexione +Description: Interlingua de API +Description: Interlingua de Peano +Prefix: la +Comments: Peano’s Interlingua, created in 1903 by Giuseppe Peano as an + international auxiliary language +Added: 2020-03-12 +%% +Type: variant Subtag: petr1708 Description: Petrine orthography Added: 2010-10-10 @@ -46798,6 +48082,13 @@ Added: 2006-12-11 Prefix: el %% Type: variant +Subtag: provenc +Description: Provençal +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in Provence +%% +Type: variant Subtag: puter Description: Puter idiom of Romansh Added: 2010-06-29 @@ -46897,6 +48188,13 @@ Comments: Sutsilvan is one of the five traditional written standards or "idioms" of the Romansh language. %% Type: variant +Subtag: synnejyl +Description: Synnejysk +Description: South Jutish +Added: 2021-07-17 +Prefix: da +%% +Type: variant Subtag: tarask Description: Belarusian in Taraskievica orthography Added: 2007-04-27 @@ -46907,6 +48205,23 @@ Comments: The subtag represents Branislau Taraskievic's Belarusian Miensk 2005). %% Type: variant +Subtag: tongyong +Description: Tongyong Pinyin romanization +Added: 2020-06-08 +Prefix: zh-Latn +Comments: Former official transcription standard for Mandarin Chinese in + Taiwan. +%% +Type: variant +Subtag: tunumiit +Description: Tunumiisiut +Description: East Greenlandic +Description: Østgrønlandsk +Added: 2020-07-16 +Prefix: kl +Comments: Also known as Tunumiit oraasiat +%% +Type: variant Subtag: uccor Description: Unified Cornish orthography of Revived Cornish Added: 2008-10-14 @@ -46959,6 +48274,21 @@ Comments: Vallader is one of the five traditional written standards or "idioms" of the Romansh language. %% Type: variant +Subtag: vecdruka +Description: Latvian orthography used before 1920s ("vecā druka") +Added: 2020-09-26 +Prefix: lv +Comments: The subtag represents the old orthography of the Latvian + language used during c. 1600s–1920s. +%% +Type: variant +Subtag: vivaraup +Description: Vivaro-Alpine +Added: 2018-04-22 +Prefix: oc +Comments: Occitan variant spoken in northeastern Occitania +%% +Type: variant Subtag: wadegile Description: Wade-Giles romanization Added: 2008-10-03 diff --git a/resources/standards-data/language-subtag-registry/readme.md b/resources/standards-data/language-subtag-registry/readme.md deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/resources/standards-data/ldml-keyboards/readme.md b/resources/standards-data/ldml-keyboards/readme.md index bcd04b1109b8c46c31f4e5500a467997d3ba0710..188c80ec765750d2e454ac8fc07f4b7fbe262492 100644 --- a/resources/standards-data/ldml-keyboards/readme.md +++ b/resources/standards-data/ldml-keyboards/readme.md @@ -1,7 +1,5 @@ # ldml-keyboards -This data is from https://github.com/unicode-org/cldr.git - Data will eventually be versioned, so there would be a `42`, `43` etc directory. Currently there is a `techpreview` directory referring to the technical preview—work in progress work in 2022. diff --git a/resources/standards-data/ldml-keyboards/techpreview/3.0/fr-t-k0-azerty.xml b/resources/standards-data/ldml-keyboards/techpreview/3.0/fr-t-k0-azerty.xml index 7a1916d1df9e76b969339d8a3e4a4c09e4750c31..156a195c058612717c220724423ca2113552cec8 100644 --- a/resources/standards-data/ldml-keyboards/techpreview/3.0/fr-t-k0-azerty.xml +++ b/resources/standards-data/ldml-keyboards/techpreview/3.0/fr-t-k0-azerty.xml @@ -63,12 +63,9 @@ - diff --git a/resources/standards-data/readme.md b/resources/standards-data/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..2458d7d629a28c7a5288c341182efe50b8042f2b --- /dev/null +++ b/resources/standards-data/readme.md @@ -0,0 +1,71 @@ +# standards-data + +The subfolders of this directory contain data from various standards +that are used in Keyman. If you update any files, be sure to update +the version data in this file also. + +## Current Versions + +| data | version | +|----------------------------|-------------| +| ethnologue | ? | +| iso639-3 | 2023 | +| langtags | 2023-05-04 | +| langtags-subtag-registry | 2023-05-11 | +| ldml-keyboards | techpreview | +| unicode-character-database | 15 | +| windows-lcid-to-bcp-47 | ? | + +## File Sources + +### Ethnologue + +This data comes from https://www.ethnologue.com/#resources, LanguageIndex.tab. It has been hand-converted from TSV to CSV, and the header line has been replaced. + + +### iso639-3 + +The file can be updated with: + +```bash +wget https://iso639-3.sil.org/sites/iso639-3/files/downloads/iso-639-3.tab -O iso639-3/iso639-3.tab +``` + +### langtags + +The file is retrieved from : + +```bash +wget https://ldml.api.sil.org/langtags.json -O langtags/langtags.json +``` + +See also [langtags/readme.md](langtags/readme.md). + +### language-subtag-registry + +The file can be updated with: + +```bash +wget https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry \ + -O language-subtag-registry/language-subtag-registry +``` + +### ldml-keyboards + +This data is from + +See also [ldml-keyboards/readme.md](ldml-keyboards/readme.md). + +### unicode-character-database + +Data from : + +```bash +UNICODE_VERSION=15.0.0 +wget https://www.unicode.org/Public/${UNICODE_VERSION}/ucd/Blocks.txt -O unicode-character-database/Blocks.txt +wget https://www.unicode.org/Public/${UNICODE_VERSION}/ucd/UnicodeData.txt -O unicode-character-database/UnicodeData.txt +``` + +### windows-lcid-to-bcp-47 + +See [readme.md](windows-lcid-to-bcp-47/readme.md). diff --git a/resources/standards-data/unicode-character-database/readme.md b/resources/standards-data/unicode-character-database/readme.md deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/tsconfig-base.json b/tsconfig-base.json index 8fb21424110afb5cf5691249d94a1921863764f9..8e3eafb6cf49d7b0638c8ac2c58ab25d49d743f5 100644 --- a/tsconfig-base.json +++ b/tsconfig-base.json @@ -12,10 +12,11 @@ "paths": { "@keymanapp/input-processor": ["./common/web/input-processor/src"], "@keymanapp/keyboard-processor": ["./common/web/keyboard-processor/src"], + "@keymanapp/keyman": ["./web" ], "@keymanapp/models-types": ["./common/models/types"], "@keymanapp/models-templates": ["./common/models/templates"], "@keymanapp/models-wordbreakers": ["./common/models/wordbreakers"], - "@keymanapp/utils": ["./common/web/utils"], + "@keymanapp/web-utils": ["./common/web/utils"], "@keymanapp/lm-message-types": ["./common/web/lm-message-types"], "@keymanapp/keyman-version": ["./common/web/keyman-version"], } diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 1a342edea0df39b0a33eff5b9b1d9468fa988b57..3dff61f8b647e467a09e88aaecc9fd931d6c5b3a 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -5,35 +5,14 @@ "files": [], "include": [], "references": [ - { "path": "./common/web/input-processor/src/tsconfig.json" }, - { "path": "./common/web/keyboard-processor/src/tsconfig.json" }, - { "path": "./common/web/recorder/src/nodeProctor.tsconfig.json" }, - { "path": "./common/web/recorder/src/tsconfig.json" }, - { "path": "./common/web/sentry-manager/src/tsconfig.json" }, - { "path": "./common/web/utils/tsconfig.json" }, - - { "path": "./common/models/templates/tsconfig.json" }, - { "path": "./common/models/types/tsconfig.json" }, - { "path": "./common/models/wordbreakers/tsconfig.json" }, - - { "path": "./common/predictive-text/browser.tsconfig.json" }, { "path": "./common/predictive-text/testing/one-stage-embedded-webworker/tsconfig.json" }, { "path": "./common/predictive-text/testing/two-stage-embedded-webworker/tsconfig.json" }, { "path": "./common/predictive-text/testing/two-stage-embedded-webworker/worker/tsconfig.json" }, - { "path": "./common/predictive-text/tsconfig.json" }, { "path": "./developer/src/server/tsconfig.json" }, { "path": "./resources/build/version/tsconfig.json" }, { "path": "./resources/build/version/tsconfig.production.json" }, - - { "path": "./web/bulk_rendering/tsconfig.json" }, - { "path": "./web/source/tsconfig.json" }, - { "path": "./web/tools/recorder/tsconfig.json" }, - { "path": "./web/tools/sourcemap-root/tsconfig.json" }, - - { "path": "./common/web/lm-message-types/" }, - { "path": "./common/web/lm-worker/" }, - { "path": "./common/web/keyman-version/" }, + // { "path": "./web/bulk_rendering/tsconfig.json" }, ] } \ No newline at end of file diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 44bf4011df97fe970e73e01794dc2fc562be0bb8..de47c84fdeafa7cfbf90c721ac86bd3a0abedd2c 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -21,7 +21,24 @@ { "path": "./developer/src/kmc-package/test/tsconfig.json" }, { "path": "./developer/src/kmc-package/tsconfig.json" }, - { "path": "./common/web/keyman-version/tsconfig.esm.json" }, + { "path": "./common/web/keyman-version" }, { "path": "./common/web/types/" }, + + { "path": "./common/web/input-processor/tsconfig.json" }, + { "path": "./common/web/keyboard-processor/tsconfig.json" }, + { "path": "./common/web/recorder/tsconfig.json" }, + { "path": "./common/web/sentry-manager/src/tsconfig.json" }, + { "path": "./common/web/utils/tsconfig.json" }, + + { "path": "./common/models/templates/tsconfig.json" }, + { "path": "./common/models/types/tsconfig.json" }, + { "path": "./common/models/wordbreakers/tsconfig.json" }, + { "path": "./common/predictive-text/tsconfig.json" }, + + { "path": "./web/src/tsconfig.all.json" }, + // { "path": "./web/tools/recorder/tsconfig.json" }, + // { "path": "./web/tools/sourcemap-root/tsconfig.json" }, + { "path": "./common/web/lm-message-types/" }, + { "path": "./common/web/lm-worker/" }, ] } \ No newline at end of file diff --git a/web/README.md b/web/README.md index 4f49dbdf085e2b2c3dec49bb70cbb5562314698c..7558c2a2c2067d858b3a9a519f522bbf130c2b98 100644 --- a/web/README.md +++ b/web/README.md @@ -9,19 +9,18 @@ configure your build environment. The following folders contain the distribution for Keyman Engine for Web: - source Source code - source/resources/osk OSK resources for inclusion in mobile app builds; - keymanweb-osk.ttf is maintained at https://github.com/silnrsi/font-keymanweb-osk - unit_tests Automated testing resources + src Source code + build/app/resources OSK + UI resources for inclusion in all build types; + keymanweb-osk.ttf is maintained at https://github.com/silnrsi/font-keymanweb-osk - build/web/release Fully-compiled KeymanWeb modules for release - build/embed/release Fully-compiled KMEA/KMEI modules for inclusion in mobile app builds - build/web/debug Fully-compiled but non-minified KeymanWeb modules - build/embed/debug Fully-compiled but non-minified KMEA/KMEI modules + build/app/browser/release Fully-compiled KeymanWeb modules for release + build/app/webview/release Fully-compiled KMEA/KMEI modules for inclusion in mobile app builds + build/app/browser/debug Fully-compiled but non-minified KeymanWeb modules + build/app/webview/debug Fully-compiled but non-minified KMEA/KMEI modules - samples Sample pages demonstrating ways to link with KeymanWeb - testing Test-case web-pages for various aspects of KeymanWeb functionality - unit_tests A Node-driven test suite for automated testing of KeymanWeb + src/samples Sample pages demonstrating ways to link with KeymanWeb + src/test/manual Test-case web-pages for various aspects of KeymanWeb functionality + src/test/auto A Node-driven test suite for automated testing of KeymanWeb ********************************************************************** @@ -32,7 +31,7 @@ compile Keyman Engine for Web before viewing the pages. Refer to the samples for usage details. To view pages using compiled Keyman Engine for Web, -1. cd to **keyman/web/source** +1. cd to **keyman/web/** 2. Run `./build.sh` - Use `./build.sh --help` for the script's documentation. @@ -42,18 +41,18 @@ Before running unit tests on Keyman Engine for Web, first run `./build.sh` according to the instructions above. Once the build is complete, running `npm test` will run the unit testing suite -on your local machine in-browser. Alternatively, see `unit_tests/test.sh`, which +on your local machine in-browser. Alternatively, see `test.sh`, which the former command executes. ### Debugging Unit Tests 1. During development, to run a specific unit test, change the `it` to `it.only`. You can also run all tests under a specific group with `describe.only`. -2. From this directory, run `./unit_tests/test.sh -debug`. Alternatively, from +2. From this directory, run `./test.sh --debug`. Alternatively, from `web/` or any `web/` subdirectory, ``` - npm run test -- -debug + npm run test -- --debug ``` The `--` part tells `npm` to funnel anything to the script as the script's @@ -65,3 +64,73 @@ the former command executes. tab. 4. In the Dev console, you can set a breakpoint in your test and refresh the page to debug + +### Approximate Overall Design + +```mermaid +graph TD; + OSK[web/src/engine/osk]; + KP["common/web/keyboard-processor"]; + IP["common/web/input-processor"]; + OSK-->KP; + IP-->KP; + Utils["common/web/utils"]; + KP---->Utils; + Wordbreakers["common/models/wordbreakers"]; + Models["common/models/templates"]; + Models-->Utils; + LMWorker["common/web/lm-worker"]; + LMWorker-->Models; + LMWorker-->Wordbreakers; + LMLayer["common/predictive-text"]; + LMLayer-->LMWorker; + IP-->LMLayer; + + subgraph PredText["WebWorker + its interface"] + LMLayer; + LMWorker; + Models; + Wordbreakers; + end + + subgraph Headless["Fully headless components"] + direction LR + KP; + IP; + Utils; + PredText; + end + + subgraph ClassicWeb["Previously unmodularized components"] + Device[web/src/engine/device-detect]; + Device----->Utils; + Elements[web/src/engine/element-wrappers]; + Elements-->KP; + KeyboardCache[web/src/engine/package-cache]; + KeyboardCache-->IP; + DomUtils[web/src/engine/dom-utils]; + DomUtils-->Utils; + OSK-->DomUtils; + OSK---->IP; + Configuration[web/src/engine/paths]; + Configuration-->OSK; + CommonEngine[web/src/engine/main]; + CommonEngine-->Configuration; + CommonEngine-->Device; + CommonEngine-->KeyboardCache; + CommonEngine-->OSK; + Attachment[web/src/engine/attachment]; + Attachment-->DomUtils; + Attachment-->Elements; + end + + subgraph WebEngine["Keyman Engine for Web (top-level libraries)"] + Browser[web/src/app/browser]; + WebView[web/src/app/webview]; + + WebView--->CommonEngine; + + Browser--->CommonEngine; + Browser-->Attachment; + end +``` \ No newline at end of file diff --git a/web/build.sh b/web/build.sh index e125e4712fdecd3bdf469337e5e7bb7bea297557..e01c95dc5eeb045b386c9e0a28b889591458045e 100755 --- a/web/build.sh +++ b/web/build.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash # -# Compiles the Keyman Engine for Web and its various end-products -# - -set -eu +# Compile keymanweb and copy compiled javascript and resources to output/embedded folder ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary @@ -18,496 +15,100 @@ cd "$THIS_SCRIPT_PATH" # ################################ Main script ################################ -# Definition of global compile constants - -# Ensures that we rely first upon the local npm-based install of Typescript. -# (Facilitates automated setup for build agents.) -# TODO: this should be removeable given set_keyman_standard_build_path does this in build-utils.sh (and relative paths are dodgy in $PATH!) -PATH="../node_modules/.bin:$PATH" - -PREDICTIVE_TEXT_SOURCE="../common/predictive-text/unit_tests/in_browser/resources/models/simple-trie.js" -PREDICTIVE_TEXT_OUTPUT="src/test/manual/web/prediction-ui/simple-en-trie.js" - -builder_describe "Builds Keyman Engine for Web (KMW)." \ - "@/common/web/keyman-version build" \ - "@/common/web/input-processor build" \ - "@/web/src/tools/building/sourcemap-root build" \ +builder_set_child_base src +builder_describe "Builds engine modules for Keyman Engine for Web (KMW)." \ "clean" \ "configure" \ "build" \ - "test Runs unit tests. Only $(builder_term test:web) is currently defined" \ - ":embed Builds the configuration of KMW used within the Keyman mobile apps" \ - ":engine Builds all common code used by other targets" \ - ":web Builds the website-oriented configuration of Keyman Engine for Web" \ - ":ui Builds the desktop UI modules used by the $(builder_term :web) build product" \ - ":samples Builds only sample & test pages found under src/samples and src/test" \ - ":tools Builds related development + unit-test resources" \ - "--no-minify Skips any minification steps in the build" \ - "--all Sets action to run on KMW's submodules as well if appropriate (actions - $(builder_term test))" + "test" \ + ":app/browser The form of Keyman Engine for Web for use on websites" \ + ":app/webview A puppetable version of KMW designed for use in a host app's WebView" \ + ":app/ui Builds KMW's desktop form-factor keyboard-selection UI modules" \ + ":engine/attachment Subset used for detecting valid page contexts for use in text editing " \ + ":engine/device-detect Subset used for device-detection " \ + ":engine/dom-utils A common subset of function used for DOM calculations, layout, etc" \ + ":engine/events Specialized classes utilized to support KMW API events" \ + ":engine/element-wrappers Subset used to integrate with website elements" \ + ":engine/main Builds all common code used by KMW's app/-level targets" \ + ":engine/osk Builds the Web OSK module" \ + ":engine/package-cache Subset used to collate keyboards and request them from the cloud" \ + ":engine/paths Subset used to configure KMW" \ + ":samples Builds all needed resources for the KMW sample-page set" \ + ":tools Builds engine-related development resources" \ + ":test-pages=src/test/manual Builds resources needed for the KMW manual testing pages" \ + "--ci+ Set to utilize CI-based test configurations & reporting." # Possible TODO? -# "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $(builder_term build:embed) and $(builder_term build:web)" \ +# "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ builder_describe_outputs \ - configure /node_modules \ - configure:embed /node_modules \ - configure:engine /node_modules \ - configure:web /node_modules \ - configure:ui /node_modules \ - configure:samples /node_modules \ - configure:tools /node_modules \ - build:embed /web/build/app/embed/release/keyman.js \ - build:engine /web/build/engine/main/obj/keymanweb.js \ - build:web /web/build/app/web/release/keymanweb.js \ - build:ui /web/build/app/ui/release/kmwuibutton.js \ - build:samples /web/$PREDICTIVE_TEXT_OUTPUT \ - build:tools /web/build/tools/building/sourcemap-root/index.js + configure /node_modules builder_parse "$@" -#### Build utility methods + definitions #### - -# Build products for each main target. -EMBED_TARGETS=( "keyman.js" ) -WEB_TARGETS=( "keymanweb.js" ) -UI_TARGETS=( "kmwuibutton.js" "kmwuifloat.js" "kmwuitoggle.js" "kmwuitoolbar.js" ) - -: ${CLOSURECOMPILERPATH:=../node_modules/google-closure-compiler-java} -: ${JAVA:=java} - -minifier="$CLOSURECOMPILERPATH/compiler.jar" - -# We'd love to add the argument --source_map_include_content for distribution in the future, -# but Closure doesn't include the TS sources properly at this time. -# -# `checkTypes` is blocked b/c TypeScript can perform our type checking... and it causes an error -# with TypeScript's `extend` implementation (it doesn't recognize a constructor without manual edits). -# We also get a global `this` warning from the same. -# -# `checkVars` is blocked b/c Closure will otherwise fail on TypeScript namespacing, as each original TS -# source file will redeclare the namespace variable, despite being merged into a single file post-compilation. -# -# `jsDocMissingType` prevents errors on type documentation Closure thinks is missing. TypeScript may not -# have the same requirements, and we trust TypeScript over Closure. -minifier_warnings="--jscomp_error=* --jscomp_off=lintChecks --jscomp_off=unusedLocalVariables --jscomp_off=globalThis --jscomp_off=checkTypes --jscomp_off=checkVars --jscomp_off=jsdocMissingType --jscomp_off=uselessCode --jscomp_off=missingRequire --jscomp_off=strictMissingRequire" - -# We use these to prevent Closure from auto-inserting its own polyfills. Turns out, they can break in the -# WebView used by Android API 19, which our app still supported when written. Unsure if it can still occur -# in Android API 21, our current minimum. -# -# Also, we currently apply all needed polyfills either manually or during TS compilation; we don't need the extra, -# excess code. -minifier_lang_specs="--language_in ECMASCRIPT5 --language_out ECMASCRIPT5" -minifycmd="$JAVA -jar $minifier --compilation_level WHITESPACE_ONLY $minifier_warnings --generate_exports $minifier_lang_specs" - -readonly minifier -readonly minifycmd - -minified_sourcemap_cleaner="build/tools/building/sourcemap-root/index.mjs" - -# Fails the build if a specified file does not exist. -assert_exists ( ) { - if [[ ! -f $1 ]]; then - builder_die "Build failed: expected file ${COLOR_GREY}$1${COLOR_RESET} is missing." - fi -} - -# $1 - base file name -# $2 - output path -# $3 - optimization level -# $4 - extra path info to add to minified sourcemap "sourceRoot" property. -# $5 - additional output wrapper -minify ( ) { - if [ $# -ge 4 ]; then - cleanerOptions="--clean --sourceRoot $4" - else - cleanerOptions="--clean" - fi - - if [ $# -ge 5 ]; then - wrapper=$5 - else - wrapper="%output%" - fi - - local INPUT="$1" - local INPUT_FILE="$(basename $1)" - local INPUT_DIR="$(dirname $1)" - local INPUT_SOURCEMAP="$INPUT_DIR/$INPUT_FILE.map" - local OUTPUT="$2" - local OUTPUT_FILE="$(basename $2)" - local OUTPUT_SOURCEMAP="$(dirname $2)/$OUTPUT_FILE.map" - - # --source_map_location_mapping - maps paths on INPUT source maps for consumption by Closure. - # ../../.. => keymanapp, ../.. => keymanapp/keyman. We have TS root sources on 'keyman'. - $minifycmd --source_map_input "$INPUT|$INPUT_SOURCEMAP" \ - --create_source_map "$OUTPUT_SOURCEMAP" --source_map_include_content \ - --source_map_location_mapping "$INPUT_DIR|../../.." \ - --js "$INPUT" --compilation_level $3 \ - --js_output_file "$OUTPUT" --warning_level VERBOSE --output_wrapper "$wrapper -//# sourceMappingURL=$INPUT_FILE.map" - - # Now to clean the source map. - assert_exists "$OUTPUT" - assert_exists "$OUTPUT_SOURCEMAP" - - # "Clean" the minified output sourcemaps. - node $minified_sourcemap_cleaner "$INPUT_SOURCEMAP" "$OUTPUT_SOURCEMAP" $cleanerOptions -} - -# Copies specified engine resources to the specified target's build output directories. -# -# ### Parameters -# -# * 1: `product` the product's source path under src/ -# * 2: `outputs` an array of resource types to copy over -# -# ### Example -# -# ```bash -# copy_resources app/web osk ui -# ``` -copy_resources ( ) { - local COMPILE_TARGET=$1 - shift - - local RESOURCES_TO_COPY=("$@") - - # We leave out obj here, as it's not a 'release' of any sort and - # thus doesn't need to publish sources or resources. - local CONFIGS=(debug) - - if ! builder_has_option --no-minify; then - CONFIGS+=(release) - fi - - builder_echo - - for CONFIG in "${CONFIGS[@]}"; - do - local CONFIG_OUT_PATH=build/$COMPILE_TARGET/$CONFIG - - builder_echo "Copying resources to $CONFIG_OUT_PATH/src" - - for RESOURCE in "${RESOURCES_TO_COPY[@]}"; - do - mkdir -p "$CONFIG_OUT_PATH/$RESOURCE" - mkdir -p "$CONFIG_OUT_PATH/src/resources/$RESOURCE" - - builder_echo "- src/resources/$RESOURCE/ => $CONFIG_OUT_PATH/$RESOURCE" - cp -Rf "src/resources/$RESOURCE" "$CONFIG_OUT_PATH/" >/dev/null - done - - builder_echo - done -} - -# Copies specified source folders corresponding to the specified target's build -# output directories. -# -# ### Parameters -# -# * 1: `product` the product's source path under src/ -# * 2: `outputs` an array of src/ subfolders to copy over -# -# ### Example -# -# ```bash -# copy_sources app/web engine resources/osk -# ``` -copy_sources ( ) { - local COMPILE_TARGET=$1 - shift - - local SOURCES_TO_COPY=("$@") - - # We leave out obj here, as it's not a 'release' of any sort and - # thus doesn't need to publish sources or resources. - CONFIGS=(debug) - - if ! builder_has_option --no-minify; then - CONFIGS+=(release) - fi - - for CONFIG in "${CONFIGS[@]}"; - do - local CONFIG_OUT_PATH=build/$COMPILE_TARGET/$CONFIG - builder_echo "Copying $COMPILE_TARGET sources to $CONFIG_OUT_PATH/src" - - rm -rf "$CONFIG_OUT_PATH/src" - mkdir -p "$CONFIG_OUT_PATH/src" - echo $VERSION_PATCH > "$CONFIG_OUT_PATH/src/version.txt" - - for SOURCE_FOLDER in "${SOURCES_TO_COPY[@]}"; - do - builder_echo "- src/$SOURCE_FOLDER/ => $CONFIG_OUT_PATH/src/$SOURCE_FOLDER/" - mkdir -p "$CONFIG_OUT_PATH/src/$SOURCE_FOLDER" - cp -Rf "src/$SOURCE_FOLDER/"* "$CONFIG_OUT_PATH/src/$SOURCE_FOLDER/" - done - - builder_echo - done -} - -# Compiles compiled scripts from the first folder specified to the second folder specified. -# -# ### Parameters -# -# * 1: `src` the folder containing scripts to be copied -# * 2: `dst` the destination folder for the copy operation -# * 3: `scripts` an array of filenames for expected compiled scripts -# -# ### Example -# -# ```bash -# compile_and_minify build/app/web/obj build/app/web/debug keymanweb.js -# ``` -copy_outputs ( ) { - local src="$1" - local dst="$2" - - shift - shift - - local BASE_SCRIPTS=("$@") - - mkdir -p "$dst" - - for SCRIPTJS in "${BASE_SCRIPTS[@]}"; - do - cp -Rf "$src/$SCRIPTJS" "$dst/" - cp -Rf "$src/$SCRIPTJS.map" "$dst/" - done -} - -# Compiles all build products corresponding to the specified target. -# -# ### Parameters -# -# * 1: `product` the product's source path under src/ -# -# ### Example -# -# ```bash -# compile app/embed -# ``` -compile ( ) { - local COMPILE_TARGET=$1 - tsc -b src/$COMPILE_TARGET -v || builder_die "Build command tsc -b src/$COMPILE_TARGET -v failed with exit code $?" - builder_echo "$COMPILE_TARGET TypeScript compiled under build/$COMPILE_TARGET/obj" -} - -# Finalizes all build products corresponding to the specified target. -# This should be called after `compile` for all `app/` targets. -# -# ### Parameters -# -# * 1: `product` the product's source path under src/ -# * 2: `outputs` an array of expected output script files for the build -# -# ### Example -# -# ```bash -# compile app/embed -# finalize app/embed keyman.js -# ``` -finalize ( ) { - if [ $# -lt 2 ]; then - builder_die "Scripting error: insufficient argument count!" - fi - - local COMPILE_TARGET=$1 - local COMPILED_INTERMEDIATE_PATH=build/$COMPILE_TARGET/obj - local DEBUG_OUT_PATH=build/$COMPILE_TARGET/debug - local RELEASE_OUT_PATH=build/$COMPILE_TARGET/release - - shift - - local OUTPUT_SCRIPTS=("$@") - - # START: debug output - - mkdir -p "$DEBUG_OUT_PATH" - copy_outputs "$COMPILED_INTERMEDIATE_PATH" "$DEBUG_OUT_PATH" "${OUTPUT_SCRIPTS[@]}" - - builder_echo "Compiled $COMPILE_TARGET debug version saved under $DEBUG_OUT_PATH: ${OUTPUT_SCRIPTS[*]}" - - # START: release output - if ! builder_has_option --no-minify; then - for SCRIPT in "${OUTPUT_SCRIPTS[@]}"; - do - minify "$COMPILED_INTERMEDIATE_PATH/$SCRIPT" "$RELEASE_OUT_PATH/$SCRIPT" SIMPLE_OPTIMIZATIONS - done - - builder_echo "Compiled $COMPILE_TARGET release version saved under $RELEASE_OUT_PATH: ${OUTPUT_SCRIPTS[*]}" - else - # The prior 'release' is now outdated: delete it. - rm -rf "$RELEASE_OUT_PATH" - fi -} - #### Build action definitions #### -if builder_start_action configure; then - verify_npm_setup +##################### TODO: call child action, verify things work as expected! - if ! builder_has_option --no-minify; then - # NPM install is required for the file to be present. - if ! [ -f $minifier ]; - then - builder_die "File $minifier does not exist: have you set the environment variable \$CLOSURECOMPILERPATH?" - fi - fi +# We can run all clean & configure actions at once without much issue. - builder_finish_action success configure -fi +builder_run_child_actions clean ## Clean actions -# Possible issue: there's no clear rule to `clean` the engine, which is auto-built -# by build:embed and build:web. -# -# Some sort of command to run ONLY for a general `clean` (no target specified) would -# be perfect for that, I think. - -if builder_start_action clean:engine; then - src/engine/build.sh clean - builder_finish_action success clean:engine -fi - -if builder_start_action clean:embed; then - rm -rf build/app/embed - builder_finish_action success clean:embed -fi - -if builder_start_action clean:web; then - rm -rf build/app/web - builder_finish_action success clean:web -fi - -if builder_start_action clean:ui; then - rm -rf build/app/ui - builder_finish_action success clean:ui -fi - -if builder_start_action clean:samples; then - rm -f $PREDICTIVE_TEXT_OUTPUT - - builder_finish_action success clean:samples -fi +###--- Future tie-in: if #8831 gets accepted, uncomment the next two lines. ---### +# # If a full-on general clean was requested, we can nuke the entire build folder. +# builder_run_action clean:project rm -rf ./build -if builder_start_action clean:tools; then - src/tools/build.sh clean - - builder_finish_action success clean:tools -fi +builder_run_child_actions configure ## Build actions -# Adds a simple version 'header' when there's a main engine build product. -if builder_has_action build:embed || \ - builder_has_action build:web || \ - builder_has_action build:ui; then +builder_run_child_actions build:engine/device-detect +builder_run_child_actions build:engine/dom-utils +builder_run_child_actions build:engine/element-wrappers +builder_run_child_actions build:engine/events - builder_echo "" - builder_echo purple "Compiling version ${VERSION}" -fi +# Uses engine/dom-utils +builder_run_child_actions build:engine/osk -builder_echo +# Uses engine/element-wrappers +builder_run_child_actions build:engine/attachment +# Uses engine/osk (due to resource-path config interface) +builder_run_child_actions build:engine/paths -if builder_start_action build:engine; then - src/engine/build.sh build +# Uses engine/config (also due to resource-path config interface, but for the +# more complete version of that interface) +builder_run_child_actions build:engine/package-cache - builder_finish_action success build:engine -fi +# Uses engine/paths, engine/device-detect, engine/package-cache, & engine/osk +builder_run_child_actions build:engine/main -if builder_start_action build:embed; then - compile app/embed - finalize app/embed ${EMBED_TARGETS[@]} +# Uses all but engine/element-wrappers and engine/attachment +builder_run_child_actions build:app/webview - # The embedded version doesn't use UI modules. - copy_resources app/embed osk - copy_sources app/embed app/embed engine resources/osk +# Uses literally everything `engine/` above +builder_run_child_actions build:app/browser - builder_finish_action success build:embed +# Uses app/browser above for engine type-defs +builder_run_child_actions build:app/ui - # TODO: handle this block somehow. +# Needs both app/browser and app/ui. +builder_run_child_actions build:samples - # if [ $UPLOAD_EMBED_SENTRY = true ]; then # upload-symbols:embed - # if [ $BUILD_DEBUG_EMBED = true ]; then - # ARTIFACT_FOLDER="release/unminified/embedded" - # pushd $EMBED_OUTPUT_NO_MINI - # else - # ARTIFACT_FOLDER="release/embedded" - # pushd $EMBED_OUTPUTs - # fi - # echo "Uploading to Sentry..." - # npm run sentry-cli -- releases files "$VERSION_GIT_TAG" upload-sourcemaps --strip-common-prefix $ARTIFACT_FOLDER --rewrite --ext js --ext map --ext ts || builder_die "Sentry upload failed." - # echo "Upload successful." - # popd - # fi -fi - -### -embed section complete. +builder_run_child_actions build:test-pages -if builder_start_action build:web; then - compile app/web - finalize app/web ${WEB_TARGETS[@]} +builder_run_child_actions build:tools - # The testing pages need both osk & ui resources in the same place. - copy_resources app/web osk ui - copy_sources app/web app/web engine resources/osk +builder_run_child_actions test - builder_finish_action success build:web -fi - -if builder_start_action build:ui; then - compile app/ui - finalize app/ui ${UI_TARGETS[@]} - - copy_resources app/ui ui - copy_sources app/ui app/ui resources/ui - - builder_finish_action success build:ui -fi - -if builder_start_action build:tools; then - src/tools/build.sh - builder_finish_action success build:tools -fi - -if builder_start_action build:samples; then - # Some test pages actually have build scripts. - ./src/test/manual/embed/android-harness/build.sh # is not yet builder-based. - - builder_echo "Copying samples & test page resources..." - # Should probably be changed into a build script for the `prediction-ui` test page. - cp "${PREDICTIVE_TEXT_SOURCE}" "${PREDICTIVE_TEXT_OUTPUT}" - - # Which could then have a parallel script for `prediction-mtnt` that downloads + extracts - # the current MTNT model. - - builder_finish_action success build:samples; -fi - -if builder_start_action test:web; then - if builder_has_option --all; then - ./test.sh - else - ./test.sh :engine +if builder_start_action test; then + TEST_OPTS= + if builder_has_option --ci; then + TEST_OPTS=--ci fi + ./test.sh $TEST_OPTS - builder_finish_action success test:web + builder_finish_action success test fi - -# TODO: handle the block below somehow. - -# # We can only upload 'web' / 'native' artifacts after ALL are done compiling. -# if [ $UPLOAD_WEB_SENTRY = true ]; then -# pushd $WEB_OUTPUT -# echo "Uploading to Sentry..." -# npm run sentry-cli -- releases files "$VERSION_GIT_TAG" upload-sourcemaps --strip-common-prefix release/web/ --rewrite --ext js --ext map --ext ts || builder_die "Sentry upload failed." -# echo "Upload successful." -# popd -# fi diff --git a/web/ci.sh b/web/ci.sh index 4d9670a9be2fb98d2c4feb500709470d60e68cd6..69bfd60eeb6029ac96ebd2971439f996a61bac01 100755 --- a/web/ci.sh +++ b/web/ci.sh @@ -25,6 +25,7 @@ cd "$THIS_SCRIPT_PATH" S_KEYMAN_COM= builder_describe "Defines and implements the CI build steps for Keyman Engine for Web (KMW)." \ + "@/web/src/tools/building/sourcemap-root prepare:s.keyman.com" \ "build" \ "test Runs all unit tests." \ "post-test Runs post-test cleanup. Should be run even if a prior step fails." \ @@ -38,6 +39,19 @@ builder_parse "$@" #### +TIER=`cat ../TIER.md` +BUILD_NUMBER=`cat ../VERSION.md` + +function web_sentry_upload () { + echo "Uploading $1 to Sentry..." + + # --strip-common-prefix does not take an argument, unlike --strip-prefix. It auto-detects + # the most common prefix instead. + sentry-cli releases files "$VERSION_GIT_TAG" upload-sourcemaps --strip-common-prefix "$1" \ + --rewrite --ext js --ext map --ext ts + echo "Upload successful." +} + if builder_start_action build; then # Build step: since CI builds start (and should start) from scratch, run the following # three actions: @@ -45,7 +59,20 @@ if builder_start_action build; then # - clean: make extra-sure that no prior build products exist. # - also useful when validating this script on a local dev machine! # - build: then do the ACTUAL build. - ./build.sh configure clean build + # one option: + # - --ci: For app/browser, outputs 'release' config filesize profiling logs + ./build.sh configure clean build --ci + + # Upload the sentry-configuration engine used by the mobile apps to sentry + # Also, clean 'em first. + for sourcemap in "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/"*.map; do + node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" null "$sourcemap" --clean + done + web_sentry_upload "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/" + + # And, of course, the main build-products too + web_sentry_upload "$KEYMAN_ROOT/web/build/app/webview/release/" + web_sentry_upload "$KEYMAN_ROOT/web/build/publish/release/" builder_finish_action success build fi @@ -53,15 +80,17 @@ fi if builder_start_action test; then # Testing step: run ALL unit tests, including those of the submodules. - # For only top-level build-product tests, specify the :engine target - # For all others, specify only the :libraries target - FLAGS= - + OPTIONS= if ! builder_is_debug_build; then - FLAGS=--ci + OPTIONS=--ci fi - ./test.sh $FLAGS + # No --reporter option exists yet for the headless modules. + + $KEYMAN_ROOT/common/web/keyboard-processor/build.sh test $OPTIONS + $KEYMAN_ROOT/common/web/input-processor/build.sh test $OPTIONS + + ./build.sh test $OPTIONS builder_finish_action success test fi @@ -109,15 +138,17 @@ if builder_start_action prepare:s.keyman.com; then # The main build products are expected to reside at the root of this folder. BASE_PUBLISH_FOLDER="$S_KEYMAN_COM/kmw/engine/$VERSION" + echo "FOLDER: $BASE_PUBLISH_FOLDER" mkdir -p "$BASE_PUBLISH_FOLDER" - cp -Rf build/app/web/release/* "$BASE_PUBLISH_FOLDER" - cp -Rf build/app/ui/release/* "$BASE_PUBLISH_FOLDER" + # s.keyman.com - release-config only. It's notably smaller, thus far more favorable + # for distribution via cloud service. + cp -Rf build/publish/release/* "$BASE_PUBLISH_FOLDER" # Third phase: tweak the sourcemaps # We can use an alt-mode of Web's sourcemap-root tool for this. for sourcemap in "$BASE_PUBLISH_FOLDER/"*.map; do - node build/tools/building/sourcemap-root/index.mjs null "$sourcemap" --sourceRoot "https://s.keyman.com/kmw/engine/$VERSION/src" + node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" null "$sourcemap" --sourceRoot "https://s.keyman.com/kmw/engine/$VERSION/src" done # Actual construction of the PR will be left to CI-config scripting for now. @@ -128,7 +159,7 @@ fi # Note: for now, this command is used to prepare the artifacts used by the download site, but # NOT to actually UPLOAD them via rsync or to produce related .download_info files. if builder_start_action prepare:downloads.keyman.com; then - UPLOAD_PATH="build/upload/$VERSION" + UPLOAD_PATH="$KEYMAN_ROOT/web/build/upload/$VERSION" # --- First action artifact - the KMW zip file --- ZIP="$UPLOAD_PATH/keymanweb-$VERSION.zip" @@ -156,16 +187,9 @@ if builder_start_action prepare:downloads.keyman.com; then fi fi - pushd build/app/web/release - "${COMPRESS_CMD}" $COMPRESS_ADD ../../../../$ZIP * - cd .. - "${COMPRESS_CMD}" $COMPRESS_ADD ../../../$ZIP debug - popd - - pushd build/app/ui/release - "${COMPRESS_CMD}" $COMPRESS_ADD ../../../../$ZIP * - cd .. - "${COMPRESS_CMD}" $COMPRESS_ADD ../../../$ZIP debug + pushd build/publish + # Zip both the 'debug' and 'release' configurations together. + "${COMPRESS_CMD}" $COMPRESS_ADD $ZIP * popd # --- Second action artifact - the 'static' folder (hosted user testing on downloads.keyman.com) --- diff --git a/web/common.inc.sh b/web/common.inc.sh new file mode 100644 index 0000000000000000000000000000000000000000..1dc057b1e9dd09552bd0e63f744d9b00cac8855b --- /dev/null +++ b/web/common.inc.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# + +# Compiles all build products corresponding to the specified target. +# This should be called from the working directory of a child project's +# build script. +# +# ### Parameters +# +# * 1: `product` the product's source path under src/ +# +# ### Example +# +# ```bash +# compile engine/main +# ``` +function compile() { + if [ $# -lt 1 ]; then + builder_die "Scripting error: insufficient argument count!" + fi + + local COMPILE_TARGET="$1" + local BUNDLE_FLAG="${2:-}" + + tsc -b "${KEYMAN_ROOT}/web/src/$COMPILE_TARGET" -v + + if [ -f "./build-bundler.js" ]; then + node "./build-bundler.js" "$BUNDLE_FLAG" + + # So... tsc does declaration-bundling on its own pretty well, at least for local development. + tsc --emitDeclarationOnly --outFile "${KEYMAN_ROOT}/web/build/$COMPILE_TARGET/lib/index.d.ts" -p "${KEYMAN_ROOT}/web/src/$COMPILE_TARGET" + fi +} + +function _copy_dir_if_exists() { + local SRC=$1 + local DST=$2 + + if [ -d "$SRC" ]; then + cp -rf "$SRC/." "$DST" + fi +} + +# Copies top-level build artifacts into common 'debug' and 'release' config folders +# for use in publishing and does a normalization pass on the sourcemaps. +function prepare() { + local CHILD_BUILD_ROOT="$KEYMAN_ROOT/web/build/app" + local PUBLISH_BUILD_ROOT="$KEYMAN_ROOT/web/build/publish" + + mkdir -p "$PUBLISH_BUILD_ROOT/debug" + mkdir -p "$PUBLISH_BUILD_ROOT/release" + + _copy_dir_if_exists "$CHILD_BUILD_ROOT/browser/debug" "$PUBLISH_BUILD_ROOT/debug" + _copy_dir_if_exists "$CHILD_BUILD_ROOT/browser/release" "$PUBLISH_BUILD_ROOT/release" + + _copy_dir_if_exists "$CHILD_BUILD_ROOT/resources" "$PUBLISH_BUILD_ROOT/debug" + _copy_dir_if_exists "$CHILD_BUILD_ROOT/resources" "$PUBLISH_BUILD_ROOT/release" + + _copy_dir_if_exists "$CHILD_BUILD_ROOT/ui/debug" "$PUBLISH_BUILD_ROOT/debug" + _copy_dir_if_exists "$CHILD_BUILD_ROOT/ui/release" "$PUBLISH_BUILD_ROOT/release" + + # Normalize paths in the sourcemaps + for sourcemap in "$PUBLISH_BUILD_ROOT/debug/"*.map; do + node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" null "$sourcemap" --clean + done + + for sourcemap in "$PUBLISH_BUILD_ROOT/release/"*.map; do + node "$KEYMAN_ROOT/web/build/tools/building/sourcemap-root/index.js" null "$sourcemap" --clean + done +} + +# Runs all headless tests corresponding to the specified target. +# This should be called from the working directory of a child project's +# build script. +# +# ### Parameters +# +# * 1: `product` the folder under src/test/auto/headless containing the +# child project's tests +# +# ### Example +# +# ```bash +# # from engine/osk +# test-headless osk +# ``` +function test-headless() { + TEST_FOLDER=$1 + + TEST_OPTS= + if builder_has_option --ci; then + TEST_OPTS="--reporter mocha-teamcity-reporter" + fi + + if [ -e .c8rc.json ]; then + c8 mocha --recursive "${KEYMAN_ROOT}/web/src/test/auto/headless/$TEST_FOLDER" $TEST_OPTS + else + mocha --recursive "${KEYMAN_ROOT}/web/src/test/auto/headless/$TEST_FOLDER" $TEST_OPTS + fi +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index e21450c22dac8db94f1fc84d23e4a59bd03d5fe5..06640cdca86980c9da335b1760ae287a1bf1b9f6 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,57 @@ { "name": "keyman", "description": "Facilitates text input in any language.", + "exports": { + "./app/browser": { + "types": "./build/app/browser/obj/test-index.d.js", + "import": "./build/app/browser/obj/test-index.js" + }, + "./engine/attachment": { + "types": "./build/engine/attachment/obj/index.d.ts", + "import": "./build/engine/attachment/obj/index.js" + }, + "./engine/paths": { + "types": "./build/engine/paths/obj/index.d.ts", + "import": "./build/engine/paths/obj/index.js" + }, + "./engine/device-detect": { + "types": "./build/engine/device-detect/obj/index.d.ts", + "import": "./build/engine/device-detect/obj/index.js" + }, + "./engine/dom-utils": { + "types": "./build/engine/dom-utils/obj/index.d.ts", + "import": "./build/engine/dom-utils/obj/index.js" + }, + "./engine/element-wrappers": { + "types": "./build/engine/element-wrappers/obj/index.d.ts", + "import": "./build/engine/element-wrappers/obj/index.js" + }, + "./engine/events": { + "types": "./build/engine/events/obj/index.d.ts", + "import": "./build/engine/events/obj/index.js" + }, + "./engine/package-cache": { + "types": "./build/engine/package-cache/obj/index.d.ts", + "import": "./build/engine/package-cache/obj/index.js", + "require": "./build/engine/package-cache/obj/index.js" + }, + "./engine/package-cache/dom-requester": { + "types": "./build/engine/package-cache/obj/domCloudRequester.d.ts", + "import": "./build/engine/package-cache/obj/domCloudRequester.js" + }, + "./engine/package-cache/node-requester": { + "types": "./build/engine/package-cache/obj/nodeCloudRequester.d.ts", + "import": "./build/engine/package-cache/obj/nodeCloudRequester.js" + }, + "./engine/main": { + "types": "./build/engine/main/obj/index.d.ts", + "import": "./build/engine/main/obj/index.js" + }, + "./engine/osk": { + "types": "./build/engine/osk/obj/index.d.ts", + "import": "./build/engine/osk/obj/index.js" + } + }, "repository": { "type": "git", "url": "git+https://github.com/keymanapp/keyman.git" @@ -20,8 +71,8 @@ "@keymanapp/resources-gosh": "*", "@keymanapp/web-sentry-manager": "*", "@sentry/cli": "2.2.0", + "c8": "^7.12.0", "chai": "^4.3.4", - "google-closure-compiler-java": "^20200224.0.0", "karma": "^6.4.1", "karma-browserstack-launcher": "^1.6.0", "karma-chai": "^0.1.0", @@ -42,12 +93,7 @@ "typescript": "^4.9.5" }, "scripts": { - "tsc": "tsc", - "test": "gosh ./test.sh", - "karma": "karma", - "mocha": "mocha", - "modernizr": "modernizr", - "sentry-cli": "sentry-cli" + "test": "gosh ./test.sh" }, "dependencies": { "@keymanapp/input-processor": "*", @@ -56,8 +102,11 @@ "@keymanapp/lexical-model-layer": "*", "@keymanapp/models-types": "*", "@keymanapp/recorder-core": "*", + "@keymanapp/tslib": "*", "@keymanapp/web-utils": "*", "@types/node": "^11.9.4", - "eventemitter3": "^4.0.0" - } + "eventemitter3": "^4.0.0", + "tslib": "^2.5.2" + }, + "type": "module" } diff --git a/web/src/app/browser/README.md b/web/src/app/browser/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a911dc6736c7f2c3ac4404a314b4a9dbfebeea60 --- /dev/null +++ b/web/src/app/browser/README.md @@ -0,0 +1 @@ +This subproject holds the new, modularized form of the old namespaced `app/web` subproject. \ No newline at end of file diff --git a/web/src/app/browser/build-bundler.js b/web/src/app/browser/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..056806260a4c64dff14f09a83a63e244eb844026 --- /dev/null +++ b/web/src/app/browser/build-bundler.js @@ -0,0 +1,76 @@ +/* + * Note: while this file is not meant to exist long-term, it provides a nice + * low-level proof-of-concept for esbuild bundling of the various Web submodules. + * + * Add some extra code at the end of src/index.ts and run it to verify successful bundling! + */ + +import esbuild from 'esbuild'; +import fs from 'fs'; +import { esmConfiguration, iifeConfiguration, prepareTslibTreeshaking } from '../../../../common/web/es-bundling/build/index.mjs'; + +let EMIT_FILESIZE_PROFILE = false; + +if(process.argv.length > 2) { + for(let i = 2; i < process.argv.length; i++) { + const arg = process.argv[i]; + + switch(arg) { + case '': + break; + case '--ci': + EMIT_FILESIZE_PROFILE=true + break; + // May add other options if desired in the future. + default: + console.error("Invalid command-line option set for script; only --ci is permitted."); + process.exit(1); + } + } +} + +const commonConfig = { + ...iifeConfiguration, + entryPoints: { + 'index': '../../../build/app/browser/obj/debug-main.js', + }, + outfile: '../../../build/app/browser/debug/keymanweb.js', + // `esbuild`'s sourcemap output puts relative paths to the original sources from the + // directory of the build output. The following keeps repo structure intact and + // puts our code under a common 'namespace' of sorts. + sourceRoot: '@keymanapp/keyman/web/build/app/browser/debug/' +}; + +await prepareTslibTreeshaking(commonConfig, /worker-main\.wrapped(?:\.min)?\.js/); + +// And now... do the actual builds. +await esbuild.build(commonConfig); + +let result = await esbuild.build({ + ...commonConfig, + entryPoints: { + 'index': '../../../build/app/browser/obj/release-main.js', + }, + // Enables source-file output size profiling! + metafile: true, + minify: true, + outfile: '../../../build/app/browser/release/keymanweb.js', +}); + +let filesizeProfile = await esbuild.analyzeMetafile(result.metafile, { verbose: true }); +fs.writeFileSync('../../../build/app/browser/filesize-profile.log', ` +// Minified Keyman Engine for Web ('app/browser' target), filesize profile +${filesizeProfile} +`); +if(EMIT_FILESIZE_PROFILE) { + // Profiles the sourcecode! + console.log(filesizeProfile); +} + +await esbuild.build({ + ...esmConfiguration, + entryPoints: { + 'index': '../../../build/app/browser/obj/test-index.js', + }, + outfile: '../../../build/app/browser/lib/index.mjs' +}); \ No newline at end of file diff --git a/web/src/app/browser/build.sh b/web/src/app/browser/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..9c9db68d8c799c17a0a3c2f9ddc8f369a4f2f7d1 --- /dev/null +++ b/web/src/app/browser/build.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +SUBPROJECT_NAME=app/browser +. "$KEYMAN_ROOT/web/common.inc.sh" +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# ################################ Main script ################################ + +builder_describe "Builds the Keyman Engine for Web's website-integrating version for use in non-puppeted browsers." \ + "@/web/src/engine/attachment build" \ + "@/web/src/engine/main build" \ + "@/web/src/tools/building/sourcemap-root" \ + "clean" \ + "configure" \ + "build" \ + "test" \ + "--ci+ Set to utilize CI-based test configurations & reporting." + +# Possible TODO?s +# "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ + +builder_parse "$@" + +config=release +if builder_is_debug_build; then + config=debug +fi + +builder_describe_outputs \ + configure /node_modules \ + build /web/build/$SUBPROJECT_NAME/$config/keymanweb.js + +#### Build action definitions #### + +compile_and_copy() { + local COMPILE_FLAGS= + if builder_has_option --ci; then + COMPILE_FLAGS=--ci + fi + compile $SUBPROJECT_NAME $COMPILE_FLAGS + + mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk" + cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/" + + # Update the build/publish copy of our build artifacts + prepare + + local PROFILE_DEST="$KEYMAN_ROOT/web/build/profiling/" + mkdir -p "$PROFILE_DEST" + cp "$KEYMAN_ROOT/web/build/app/browser/filesize-profile.log" "$PROFILE_DEST/web-engine-filesize.log" + cp "$KEYMAN_ROOT/common/web/lm-worker/build/filesize-profile.log" "$PROFILE_DEST/lm-worker-filesize.log" +} + +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf "$KEYMAN_ROOT/web/build/$SUBPROJECT_NAME" +builder_run_action build compile_and_copy + +# No headless tests for this child project. Currently, DOM-based unit & +# integrated tests are run solely by the top-level $KEYMAN_ROOT/web project. \ No newline at end of file diff --git a/web/src/app/browser/src/beepHandler.ts b/web/src/app/browser/src/beepHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fae87425777442d7faee91df9b092672d65222d --- /dev/null +++ b/web/src/app/browser/src/beepHandler.ts @@ -0,0 +1,87 @@ +import { type KeyboardInterface } from '@keymanapp/keyboard-processor'; +import { DesignIFrame, OutputTarget } from 'keyman/engine/element-wrappers'; + +// Utility object used to handle beep (keyboard error response) operations. +class BeepData { + e: HTMLElement; + c: string; + + constructor(e: HTMLElement) { + this.e = e; + this.c = e.style.backgroundColor; + } + + reset(): void { + this.e.style.backgroundColor = this.c; + } +} + +export class BeepHandler { + readonly keyboardInterface: KeyboardInterface; + + constructor(keyboardInterface: KeyboardInterface) { + this.keyboardInterface = keyboardInterface; + } + + _BeepObjects: BeepData[] = []; // BeepObjects - maintains a list of active 'beep' visual feedback elements + _BeepTimeout: number = 0; // BeepTimeout - a flag indicating if there is an active 'beep'. + // Set to 1 if there is an active 'beep', otherwise leave as '0'. + /** + * Function beep KB (DOM-side implementation) + * Scope Public + * @param {Object} Pelem element to flash + * Description Flash body as substitute for audible beep; notify embedded device to vibrate + */ + beep(outputTarget: OutputTarget) { + if(!(outputTarget instanceof OutputTarget)) { + return; + } + + // All code after this point is DOM-based, triggered by the beep. + var Pelem: HTMLElement = outputTarget.getElement(); + if(outputTarget instanceof DesignIFrame) { + Pelem = outputTarget.docRoot; // I1446 - beep sometimes fails to flash when using OSK and rich control + } + + if(!Pelem) { + return; // There's no way to signal a 'beep' to null, so just cut everything short. + } + + if(!Pelem.style || typeof(Pelem.style.backgroundColor)=='undefined') { + return; + } + + for(var Lbo=0; Lbo { + this.keyboardInterface.resetContextCache(); + + var Lbo; + this._BeepTimeout = 0; + for(Lbo=0;Lbo; + + initialize(options: Required) { + if(this._options) { + // Preserve old options, but replace with any newly-set ones if specified. + // If specified, even as 'undefined' or 'null', it will still override. + this._options = {...this._options, ...options}; + } else { + this._options = {...options}; + } + super.initialize(options); + this._options = options; + + this._ui = options.ui; + this._attachType = options.attachType; + + whenDocumentReady().then(() => { + if(options.useAlerts && !this.alertHost) { + this._alertHost = new AlertHost(); + } else if(!options.useAlerts && this.alertHost) { + this._alertHost?.shutdown(); + this._alertHost = null; + } + }); + } + + get options() { + return this._options; + } + + get attachType() { + return this._attachType; + } + + get alertHost(): AlertHost | undefined { + return this._alertHost; + } + + set signalUser(host: AlertHost) { + if(!host || host != this.alertHost) { + this.alertHost.shutdown(); + } + + this._alertHost = host; + } + + debugReport(): Record { + const baseReport = super.debugReport(); + baseReport.attachType = this.attachType; + baseReport.ui = this._ui; + baseReport.keymanEngine = 'app/browser'; + + return baseReport; + } + + onRuleFinalization(ruleBehavior: RuleBehavior, outputTarget: OutputTarget) { + // TODO: Patch up to modularized form. But that doesn't exist yet for some of these... + + // If the transform isn't empty, we've changed text - which should produce a 'changed' event in the DOM. + const ruleTransform = ruleBehavior.transcription.transform; + if(!isEmptyTransform(ruleTransform)) { + if(outputTarget instanceof DOMOutputTarget) { + outputTarget.changed = true; + } + } + } +} + +export interface BrowserInitOptionSpec extends InitOptionSpec { + ui?: string; + attachType?: 'auto' | 'manual' | ''; // If blank or undefined, attachType will be assigned to "auto" or "manual" + useAlerts?: boolean; +} + +export const BrowserInitOptionDefaults: Required = { + ui: '', + attachType: '', + useAlerts: true, + ...InitOptionDefaults +} \ No newline at end of file diff --git a/web/src/app/browser/src/context/focusAssistant.ts b/web/src/app/browser/src/context/focusAssistant.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0ae8a6bdb1b7caa8ead25b222cf655fbc5bd470 --- /dev/null +++ b/web/src/app/browser/src/context/focusAssistant.ts @@ -0,0 +1,162 @@ +import EventEmitter from "eventemitter3"; + +/** + * The return object documented for + * https://help.keyman.com/developer/engine/web/16.0/reference/core/getUIState. + * + * As it has long been documented in this format, property names should not be adjusted! + */ +export class FocusStateAPIObject { + /** + * Indicates that KMW is in a state of actively restoring focus to a previously-active element. + */ + activated: boolean; + + /** + * Indicates that KMW is actively maintaining focus on the currently active OutputTarget control + * while some UI element (the OSK, a keyboard-change UI) is the current focus of user-interaction. + */ + activationPending: boolean; + + constructor(pending: boolean, activated: boolean) { + this.activationPending = pending; + this.activated = activated; + } +} + +interface EventMap { + /** + * Called immediately after the `maintainingFocus` flag is cleared. + * @returns + */ + 'maintainingfocusend': () => void; +} + +// Formerly handled under "UIManager". +/** + * This class provides fields and methods useful for assisting context management. Control focus (and + * thus, activation of the corresponding OutputTarget) should not be lost to non-context components of + * KMW, such as the OSK or a keyboard selector. + */ +export class FocusAssistant extends EventEmitter { + private _maintainingFocus: boolean = false; // ActivatingKeymanWebUI - Does the OSK have active focus / an active interaction? + + /** + * Returns `true` only when the active target has an active `forceScroll` method/state, which deliberately + * blurs and then refocuses the same element in order to force a browser-default page scroll to keep the + * element and text-caret visible. + */ + readonly isTargetForcingScroll: () => boolean; + + constructor(isTargetForcingScroll: () => boolean) { + super(); + this.isTargetForcingScroll = isTargetForcingScroll; + } + + /* + * Long-term idea here: about all of the relevant OSK events that would interact with this have "enter" and + * "leave" variants - we could take a stack of `Promise`s. On a `Promise` fulfillment, remove it from the + * stack. When the last one is removed, the focus-maintenance state would end, allowing further events + * to deactivate the active OutputTarget. + */ + + /** + * Indicates that KMW is actively maintaining focus on the currently active OutputTarget control, rather + * than losing focus while some UI element (the OSK, a keyboard-change UI) is the most direct recipient + * of browser focus due to user-interaction - generally, with non-context engine components. + * + * While the flag is active, the context-management system should not deactivate an OutputTarget upon + * its element's loss of focus within the page unless setting a different OutputTarget as active. + * + * TODO: (potential) Future enhancement - this should not be possible to set if there is no currently-active + * context target to maintain. + */ + // Formerly `isActivating`. + public get maintainingFocus(): boolean { + return this._maintainingFocus; + } + + public set maintainingFocus(value: boolean) { + const priorValue = this._maintainingFocus; + this._maintainingFocus = value; + + // Needed to properly update .activeTarget upon loss of maintaining-focus state. + if(priorValue && !value) { + this.emit('maintainingfocusend'); + } + } + + /* + * Long-term idea here: as (aside from OSK title/resize bar interactions) it's always used to actively + * RESTORE focus, taking in a closure to perform during the 'focus restoration state' would make a nice + * design. Something like .performRefocus(closure: () => void): + * - would set this field before calling the closure + * - would unset this field after calling the closure + * - would try-catch to guarantee the 'unset'. + * + * OSK drag handlers should utilize the other field, anyway. + */ + + /** + * Indicates that KMW is in a state of actively restoring focus to a previously-active element. + * This is most commonly utilized whenever a keyboard is newly activated, generally due to + * user interaction to select the new keyboard. + */ + // Formerly `justActivated`. + restoringFocus: boolean = false; // JustActivatedKeymanWebUI - focussing back to control after KeymanWeb UI interaction + + /** + * JH (2023-04-24): given how it's used within the KMW engine, this seems extremely similar in purpose to + * `restoringFocus` - it's set before calling an element's focus method to prevent focus-handlers from causing + * unwanted side-effects. The ONE critical detail: KSF / `saveFocus` will block a single check, not waiting for + * control flow restoration before clearing, where the other matching cases will block 100 (maybe to prevent + * some sort of event softlock?). + * + * So, it's like the `saveFocus` variant immediately clears the flag once checked, while others are intended + * to only clear the flag once control returns to the method that triggered a focus op. + * + * A future refactor should be able to merge the two, though it's worth noting that there are early checks for + * this, but _not_ `restoringFocus`, in the context-management control-blur event handler. So, it's not 100% + * super-straightforward, but a refactor should be manageable all the same. + */ + _IgnoreNextSelChange = 0; + + /** + * Is used as a time-delayed async `restoringFocus` or `maintainingFocus` - could be modeled decently as a Promise. + * Probably more the latter, as it's a touch-OSK interaction like the other `maintainingFocus` cases. + */ + focusing: boolean; + /** + * Manages the time-delay aspect of `focusing` state. + */ + focusTimer: number; + + /** + * Function getUIState + * Scope Public + * @return {Object.} + * Description Return object with activation state of UI: + * activationPending (bool): KMW being activated + * activated (bool): KMW active + */ + getUIState(): FocusStateAPIObject { + return new FocusStateAPIObject(this.maintainingFocus, this.restoringFocus); + } + + /** + * Set or clear the IsActivatingKeymanWebUI flag (exposed function) + * + * @param {(boolean|number)} state Activate (true,false) + */ + setMaintainingFocus(state: boolean) { + this.maintainingFocus = state ? true : false; + } + + setFocusTimer(): void { + this.focusing=true; + + this.focusTimer = window.setTimeout(function() { + this.focusing=false; + }.bind(this), 50) + } +} \ No newline at end of file diff --git a/web/src/app/browser/src/context/pageIntegrationHandlers.ts b/web/src/app/browser/src/context/pageIntegrationHandlers.ts new file mode 100644 index 0000000000000000000000000000000000000000..30b91cb102e654dd46da183407f2176fb389efe8 --- /dev/null +++ b/web/src/app/browser/src/context/pageIntegrationHandlers.ts @@ -0,0 +1,236 @@ +import { DomEventTracker } from 'keyman/engine/events'; + +import KeymanEngine from "../keymanEngine.js"; +import { FocusAssistant } from './focusAssistant.js'; +import { RotationProcessor } from '../utils/rotationProcessor.js'; + +// Note: in the future, it'd probably be best to have an instance per iframe window as +// well as the top-level window. This was not done in or before KMW 16.0 though, so +// we'll leave that out for now within the initial modular form of app/browser KMW in 17.0. +export class PageIntegrationHandlers { + private readonly window: Window; + private readonly engine: KeymanEngine; + private readonly domEventTracker = new DomEventTracker(); + + /** + * Used together with `deactivateOnRelease` to determine the distance of vertical scrolls; + * if sufficiently far at any point, we avoid deactivating the current context when it ends. + */ + private touchY: number; + + /** + * Used together with `touchY` to determine the distance of vertical scrolls; + * if sufficiently far at any point, we avoid deactivating the current context when it ends. + */ + private deactivateOnRelease: boolean; + + /** + * Used on certain browser/OS combinations (e.g. Chrome on Android) to prevent odd behaviors + * that arise when URL bars scroll into view during an ongoing scroll, as this can impede + * proper / smooth positioning of the OSK. (Deactivating the active target also hides the OSK.) + */ + private deactivateOnScroll: boolean; + + /** + * This component should only ever be applied to a base page: we need the ability to add 'scroll + * space' on touch devices so that the OSK doesn't block the bottom. + */ + private mobilePageTrailer: HTMLDivElement; + + private rotationProcessor: RotationProcessor; + + constructor(window: Window, engine: KeymanEngine) { + this.window = window; + this.engine = engine; + + this.attachHandlers(); + + if(engine.config.hostDevice.touchable) { + this.buildPageTrailer(); + + this.rotationProcessor = new RotationProcessor(this.engine); + this.rotationProcessor.init(); + } + } + + private buildPageTrailer() { + // Add a blank DIV to the bottom of the page to allow the bottom of the page to be shown + const dTrailer = this.mobilePageTrailer = document.createElement('div'); + const ds=dTrailer.style; + ds.width='100%'; + ds.height=(screen.width/2)+'px'; // ... interesting choice, but okay. + document.body.appendChild(dTrailer); + } + + private get focusAssistant(): FocusAssistant { + return this.engine.contextManager.focusAssistant; + } + + private suppressFocusCheck: (e: FocusEvent) => boolean = (e) => { + if(this.focusAssistant.isTargetForcingScroll()) { + // Prevent triggering other blur-handling events (as possible) - this blur + // is programmatic in order to force a browser scroll-position update. + // All focus changes should be prevented at this time. + e.stopPropagation(); + e.cancelBubble = true; + } + // But DO perform default event behavior (actually blurring & focusing the affected element) + return true; + } + + /** + * Reset context when entering or exiting the active element. + * Will also trigger OSK shift state / layer reset. + **/ + private pageFocusHandler: (e: FocusEvent) => boolean = () => { + if(!this.focusAssistant.maintainingFocus && this.engine.osk?.vkbd) { + this.engine.contextManager.deactivateCurrentTarget(); + this.engine.contextManager.resetContext(); + } + return false; + } + + // Sets up page-default touch-based handling for activation-state management. + // These always trigger for the page, wherever a touch may occur. Does not + // prevent element-specific or OSK-key-specific handling from triggering. + + private touchStartActivationHandler: (e: TouchEvent) => boolean = (e) => { + const osk = this.engine.osk; + if(!osk) { + return false; + } + const device = this.engine.config.hostDevice; + + this.deactivateOnRelease=true; + this.touchY=e.touches[0].screenY; + + // On Chrome, scrolling up or down causes the URL bar to be shown or hidden + // according to whether or not the document is at the top of the screen. + // But when doing that, each OSK row top and height gets modified by Chrome + // looking very ugly. It would be best to hide the OSK then show it again + // when the user scroll finishes, but Chrome has no way to reliably report + // the touch end event after a move. c.f. http://code.google.com/p/chromium/issues/detail?id=152913 + // The best compromise behaviour is simply to hide the OSK whenever any + // non-input and non-OSK element is touched. + this.deactivateOnScroll=false; + if(device.OS == 'android' && device.browser == 'chrome') { + // this.deactivateOnScroll has the inverse of the 'true' default, + // but that fact actually facilitates the following conditional logic. + if(typeof(osk._Box) == 'undefined') return false; + if(typeof(osk._Box.style) == 'undefined') return false; + + // The following tests are needed to prevent the OSK from being hidden during normal input! + let p=(e.target as HTMLElement).parentElement; + if(typeof(p) != 'undefined' && p != null) { + if(p.className.indexOf('kmw-key-') >= 0) return false; + if(typeof(p.parentElement) != 'undefined' && p.parentElement != null) { + p=p.parentElement; + if(p.className.indexOf('kmw-key-') >= 0) return false; + } + } + + this.deactivateOnScroll = true; + } + return false; + }; + + private touchMoveActivationHandler: (e: TouchEvent) => boolean = (e) => { + if(this.deactivateOnScroll) { // Android / Chrone case. + this.focusAssistant.focusing = false; + this.engine.contextManager.deactivateCurrentTarget(); + } + + const y = e.touches[0].screenY; + const y0 = this.touchY; + if(y-y0 > 5 || y0-y < 5) { + this.deactivateOnRelease = false; + } + return false; + }; + + private touchEndActivationHandler: (e: TouchEvent) => boolean = (e) => { + // Should not hide OSK if simply closing the language menu (30/4/15) + // or if the focusing timer (focusAssistant.setFocusTimer) is still active. + if(this.deactivateOnRelease && !this.engine.touchLanguageMenu && !this.focusAssistant.focusing) { + this.engine.contextManager.deactivateCurrentTarget(); + } + this.deactivateOnRelease=false; + return false; + }; + + + private _WindowLoad: (e: Event) => void = () => { + // Always return to top of page after a page reload + document.body.scrollTop=0; + if(typeof document.documentElement != 'undefined') { + document.documentElement.scrollTop=0; + } + } + + /** + * Function _WindowUnload + * Scope Private + * Description Remove handlers before detaching KMW window + */ + private _WindowUnload: () => void = () => { + // Future note: should restrict this to anything for the corresponding document if on a + // child iframe, not the whole engine. + this.engine.shutdown(); + } + + private attachHandlers() { + const eventTracker = this.domEventTracker; + const device = this.engine.config.hostDevice; + const docBody = this.window.document.body; + + eventTracker.attachDOMEvent(this.window, 'focus', this.pageFocusHandler, false); + eventTracker.attachDOMEvent(this.window, 'blur', this.pageFocusHandler, false); + + /* + * To prevent propagation of focus & blur events from the input-scroll workaround, + * we attach top-level capturing listeners to the focus & blur events. They prevent propagation + * but NOT default behavior, allowing the scroll to complete while preventing nearly all + * possible event 'noise' that could result from the workaround. + */ + eventTracker.attachDOMEvent(docBody, 'focus', this.suppressFocusCheck, true); + eventTracker.attachDOMEvent(docBody, 'blur', this.suppressFocusCheck, true); + + if(device.touchable) { + eventTracker.attachDOMEvent(docBody, 'touchstart', this.touchStartActivationHandler,false); + eventTracker.attachDOMEvent(docBody, 'touchmove', this.touchMoveActivationHandler, false); + eventTracker.attachDOMEvent(docBody, 'touchend', this.touchEndActivationHandler, false); + } + + eventTracker.attachDOMEvent(window, 'load', this._WindowLoad, false); + eventTracker.attachDOMEvent(window, 'unload', this._WindowUnload,false); + + eventTracker.attachDOMEvent(document, 'keyup', this.engine.hotkeyManager._Process, false); + } + + public shutdown() { + const eventTracker = this.domEventTracker; + const device = this.engine.config.hostDevice; + const docBody = this.window.document.body; + + // See `attachHandlers` for the purpose behind all handlers listed here. + + eventTracker.detachDOMEvent(this.window, 'focus', this.pageFocusHandler, false); + eventTracker.detachDOMEvent(this.window, 'blur', this.pageFocusHandler, false); + + eventTracker.detachDOMEvent(docBody, 'focus', this.suppressFocusCheck, true); + eventTracker.detachDOMEvent(docBody, 'blur', this.suppressFocusCheck, true); + + if(device.touchable) { + eventTracker.detachDOMEvent(docBody, 'touchstart', this.touchStartActivationHandler,false); + eventTracker.detachDOMEvent(docBody, 'touchmove', this.touchMoveActivationHandler, false); + eventTracker.detachDOMEvent(docBody, 'touchend', this.touchEndActivationHandler, false); + + this.mobilePageTrailer?.parentElement.removeChild(this.mobilePageTrailer); + } + + eventTracker.detachDOMEvent(window, 'load', this._WindowLoad, false); + eventTracker.detachDOMEvent(window, 'unload', this._WindowUnload,false); + + eventTracker.detachDOMEvent(document, 'keyup', this.engine.hotkeyManager._Process, false); + } +} \ No newline at end of file diff --git a/web/src/app/browser/src/contextManager.ts b/web/src/app/browser/src/contextManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..492c627146ded6351f4795e7917e0ba505801b2f --- /dev/null +++ b/web/src/app/browser/src/contextManager.ts @@ -0,0 +1,801 @@ +import { type Keyboard, KeyboardScriptError } from '@keymanapp/keyboard-processor'; +import { type KeyboardStub } from 'keyman/engine/package-cache'; +import { CookieSerializer } from 'keyman/engine/dom-utils'; +import { eventOutputTarget, PageContextAttachment } from 'keyman/engine/attachment'; +import { DomEventTracker, LegacyEventEmitter } from 'keyman/engine/events'; +import { DesignIFrame, OutputTarget, nestedInstanceOf } from 'keyman/engine/element-wrappers'; +import { + ContextManagerBase, + type KeyboardInterface, + LegacyAPIEvents +} from 'keyman/engine/main'; +import { BrowserConfiguration } from './configuration.js'; +import { FocusAssistant } from './context/focusAssistant.js'; + +export interface KeyboardCookie { + current: string; +} + +/** + * Set target element text direction (LTR or RTL), but only if the element is empty + * + * If the element base directionality is changed after it contains content, unless all the text + * has the same directionality, text runs will be re-ordered which is confusing and causes + * incorrect caret positioning + * + * @param {Object} Ptarg Target element + */ +function _SetTargDir(Ptarg: HTMLElement, activeKeyboard: Keyboard) { + const elDir = activeKeyboard?.isRTL ? 'rtl' : 'ltr'; + + if(Ptarg) { + if(Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLInputElement + || Ptarg instanceof Ptarg.ownerDocument.defaultView.HTMLTextAreaElement) { + if((Ptarg as HTMLInputElement|HTMLTextAreaElement).value.length == 0) { + Ptarg.dir=elDir; + } + } else if(typeof Ptarg.textContent == "string" && Ptarg.textContent.length == 0) { // As with contenteditable DIVs, for example. + Ptarg.dir=elDir; + } + } +} + +export default class ContextManager extends ContextManagerBase { + private _activeKeyboard: {keyboard: Keyboard, metadata: KeyboardStub}; + private cookieManager = new CookieSerializer('KeymanWeb_Keyboard'); + readonly focusAssistant = new FocusAssistant(() => this.activeTarget?.isForcingScroll()); + readonly page: PageContextAttachment; + private mostRecentTarget: OutputTarget; + private currentTarget: OutputTarget; + + private globalKeyboard: {keyboard: Keyboard, metadata: KeyboardStub}; + + private _eventsObj: () => LegacyEventEmitter; + private domEventTracker = new DomEventTracker(); + + constructor(engineConfig: BrowserConfiguration, eventsClosure: () => LegacyEventEmitter) { + super(engineConfig); + + this._eventsObj = eventsClosure; + + this.page = new PageContextAttachment(window.document, { + hostDevice: this.engineConfig.hostDevice + }); + + this.focusAssistant.on('maintainingfocusend', () => { + // Basically, if the maintaining state were the reason we still had an `activeTarget`... + if(!this.activeTarget && this.mostRecentTarget) { + this.emit('targetchange', this.activeTarget); + } + }); + } + + get apiEvents(): LegacyEventEmitter { + return this._eventsObj(); + } + + initialize(): void { + this.on('keyboardasyncload', (stub, completion) => { + this.engineConfig.alertHost?.wait('Installing keyboard
' + stub.name); + + completion.then(() => { + this.engineConfig.alertHost?.wait(); // cancels the wait. + }); + }); + + this.engineConfig.deferForInitialization.then(() => { + const device = this.engineConfig.hostDevice; + + const noPropagation = (event: Event) => event.stopPropagation() + + // For any elements being attached, or being enabled after having been disabled... + this.page.on('enabled', (elem) => { + if(!(elem._kmwAttachment.interface instanceof DesignIFrame)) { + // For anything attached but (design-mode) iframes... + + // This block: has to do with maintaining focus. + if(device.touchable) { + // Remove any handlers for "NonKMWTouch" elements, since we're enabling it here. + this.domEventTracker.detachDOMEvent(elem, 'touchstart', this.nonKMWTouchHandler); + + // Prevent base-page touch handlers from causing a defocus when interacting + // with attached input elements. + this.domEventTracker.attachDOMEvent(elem, 'touchmove', noPropagation, false); + this.domEventTracker.attachDOMEvent(elem, 'touchend', noPropagation, false); + } + + // This block: has to do with maintaining focus. + this.domEventTracker.attachDOMEvent(elem,'focus', this._ControlFocus); + this.domEventTracker.attachDOMEvent(elem,'blur', this._ControlBlur); + this.domEventTracker.attachDOMEvent(elem,'click', this._Click); + } else { + // For design-mode iframes: + + // This block: has to do with maintaining focus. + var Lelem=(elem as HTMLIFrameElement).contentWindow.document; + // I2404 - Attach to IFRAMEs child objects, only editable IFRAMEs here + if(device.browser == 'firefox') { + this.domEventTracker.attachDOMEvent(Lelem,'focus', this._ControlFocus); + this.domEventTracker.attachDOMEvent(Lelem,'blur', this._ControlBlur); + } else { // Chrome, Safari + this.domEventTracker.attachDOMEvent(Lelem.body,'focus', this._ControlFocus); + this.domEventTracker.attachDOMEvent(Lelem.body,'blur', this._ControlBlur); + } + } + }); + + // For any elements being detached, disabled, or deliberately not being attached (b/c nonKMWTouchHandler)... + this.page.on('disabled', (elem) => { + // Note: we may not actually be attached at this point. + if(!(nestedInstanceOf(elem, "HTMLIFrameElement"))) { + // For anything attached but (design-mode) iframes... + + // This block: has to do with maintaining focus. + if(device.touchable) { + this.domEventTracker.attachDOMEvent(elem, 'touchstart', this.nonKMWTouchHandler, false); + + // does not detach the touch-handlers added in 'enabled'? + } + + // This block: has to do with maintaining focus. + this.domEventTracker.detachDOMEvent(elem,'focus', this._ControlFocus); + this.domEventTracker.detachDOMEvent(elem,'blur', this._ControlBlur); + this.domEventTracker.detachDOMEvent(elem,'click', this._Click); + } else { + // For design-mode iframes: + + // This block: has to do with maintaining focus. + let Lelem = (elem as HTMLIFrameElement).contentWindow.document; + // Mozilla // I2404 - Attach to IFRAMEs child objects, only editable IFRAMEs here + if(device.browser == 'firefox') { + // Firefox won't handle these events on Lelem.body - only directly on Lelem (the doc) instead. + this.domEventTracker.detachDOMEvent(Lelem,'focus', this._ControlFocus); + this.domEventTracker.detachDOMEvent(Lelem,'blur', this._ControlBlur); + } else { // Chrome, Safari + this.domEventTracker.detachDOMEvent(Lelem.body,'focus', this._ControlFocus); + this.domEventTracker.detachDOMEvent(Lelem.body,'blur', this._ControlBlur); + } + } + + // This block: has to do with maintaining focus (and consequences) + var lastElem = this.mostRecentTarget?.getElement(); + if(lastElem && lastElem == elem) { + this.forgetActiveTarget(); // should already auto-hide the OSK while at it via event. + } + }); + + // This fires the events we just registered for. + this.page.install(this.engineConfig.attachType == 'manual'); + }); + } + + get activeTarget(): OutputTarget { + /* + * Assumption: the maintainingFocus flag may only be set when there is a current target. + * This is not enforced proactively at present, but the assumption should hold. (2023-05-03) + */ + const maintainingFocus = this.focusAssistant.maintainingFocus; + return this.currentTarget || (maintainingFocus ? this.mostRecentTarget : null); + } + + get lastActiveTarget(): OutputTarget { + return this.mostRecentTarget; + } + + public deactivateCurrentTarget() { + const priorTarget = this.activeTarget || this.lastActiveTarget; + if(priorTarget) { + this._BlurKeyboardSettings(priorTarget.getElement()); + } + + // Because of focus-maintenance effects + if(!this.activeTarget) { + this.setActiveTarget(null, true); + } + } + + public forgetActiveTarget() { + this.focusAssistant.maintainingFocus = false; + this.focusAssistant.restoringFocus = false; + + const priorTarget = this.activeTarget || this.lastActiveTarget; + if(priorTarget) { + this._BlurKeyboardSettings(priorTarget.getElement()); + } + + this.setActiveTarget(null, true); + } + + public setActiveTarget(target: OutputTarget, sendEvents: boolean) { + const previousTarget = this.mostRecentTarget; + const originalTarget = this.activeTarget; // may differ, depending on focus state. + + if(target == originalTarget) { + // A focus state may have .currentTarget as null at this stage; if the func + // is being called with a non-null parameter, we want this SET. #9404 + if(originalTarget) { + this.currentTarget = originalTarget; + } + + /** + * If it's already active, we should cancel early. + * + * The #1 reason - we don't want .resetContext calls in this scenario. + * In particular, moving the caret or setting the selection range of an + * or + +

or in this input field:

+ + + +

Add a keyboard by keyboard name:

+ + + +

Add a keyboard by BCP-47 language code:

+ + + +

Add a keyboard by language name:

+ + + + +

Return to samples home page

+ + + + + + + diff --git a/web/src/samples/index.html b/web/src/samples/index.html index 8d0ac059c08f57a1c0f708a805d4c5f38593fc30..2a9259063f919bdeb29a8f2d489c905be9b83f3d 100644 --- a/web/src/samples/index.html +++ b/web/src/samples/index.html @@ -20,9 +20,9 @@

KeymanWeb Samples

-

Test unminified Keymanweb

-

Test fully compiled Keymanweb

-

Full multilingual (compiled) Keymanweb

-

Return to main index. +

Example 1 - Toggle UI, all resources in same folder as page

+

Example 2 - Toggle UI, all resources in a common subfolder

+

Example 3 - Toolbar UI, all resources in a common subfolder

+

Example 4 - a more complex file-organization setup

diff --git a/web/src/samples/samplehdr.js b/web/src/samples/samplehdr.js index 2ce215c666e9d49a04e468f79e6a19b7b3f7fc26..129d54fc011ebe663f889fb914524d9dbf04799b 100644 --- a/web/src/samples/samplehdr.js +++ b/web/src/samples/samplehdr.js @@ -1,111 +1,146 @@ // JavaScript Document samplehdr.js: Keyboard management for KeymanWeb demonstration pages -/* +/* The keyboard name and/or BCP-47 language code must be specified for each keyboard that is to be available. If the same keyboard is used for several languages, it must be listed for each - language, but the keyboard itself will only be loaded once. + language, but the keyboard itself will only be loaded once. If two (or more) keyboards are to be available for a given language, both must be listed. - Any number of keyboards may be specified in one or more calls. - Keyboard paths may be absolute (with respect to the server root) or relative to the keyboards option path. + Any number of keyboards may be specified in one or more calls. + Keyboard paths may be absolute (with respect to the server root) or relative to the keyboards option path. The actual keyboard object will be downloaded asynchronously when first selected for use. - + Each argument to addKeyboards() is a string, for example: european2 loads the current version of the Eurolatin 2 keyboard (for its default language) european2@fr loads the current version of the Eurolatin 2 keyboard for French european2@fr@1.2 loads version 1.2 of the Eurolatin 2 keyboard for French - + Argument syntax also supports the following extensions: @fr load the current version of the default keyboard for French @fr$ load all available keyboards (current version) for French - - Each call to addKeyboards() requires a single call to the remote server, + + Each call to addKeyboards() requires a single call to the remote server, (unless all keyboards listed are local and fully specified) so it is better - to use multiple arguments rather than separate function calls. - - Calling addKeyboards() with no arguments returns a list of *all* available keyboards. - The Toolbar (desktop browser) UI is best suited for allowing users to select + to use multiple arguments rather than separate function calls. + + Calling addKeyboards() with no arguments returns a list of *all* available keyboards. + The Toolbar (desktop browser) UI is best suited for allowing users to select the appropriate language and keyboard in this case. Keyboards may also be specified by language name using addKeyboardsForLanguage() for example: keymanweb.addKeyboardsForLanguage('Burmese'); - + Appending $ to the language name will again cause all available keyboards for that language to be loaded rather than the default keyboard. - - The first call to addKeyboardsForLanguage() makes an additional call to the + + The first call to addKeyboardsForLanguage() makes an additional call to the keyman API to load the current list of keyboard/language associations. In this example, the following function loads the indicated keyboards, - and is called when the page loads. + and is called when the page loads. */ - function loadKeyboards() - { + function errToString(err) { + // Painful? Kinda. But needed on un-updated Android API 21! + if(Array.isArray(err)) { + var result = ''; + for(var i = 0; i < err.length; i++) { + var e = err[i]; + if(e.error instanceof Error) { + result += e.error.message + '\n'; + } else { + result += JSON.stringify(e) + '\n'; + } + } + return result; + } + if(err instanceof Error) { + return err.message; + } + return JSON.stringify(err); + } + + function doAddKeyboards(data) { + return keyman.addKeyboards(data).catch(function(err) { + console.error('keyman.addKeyboards failed with '+errToString(err)+' for '+JSON.stringify(data)); + }); + } + + function doAddKeyboardsForLanguage(data) { + return keyman.addKeyboardsForLanguage(data).catch(function(err) { + console.error('keyman.addKeyboardsForLanguage failed with '+errToString(err)+' for '+JSON.stringify(data)); + }); + } + + function loadKeyboards(nestLevel) { var kmw=keyman; - + + var base_prefix = '../'; + var prefix = './'; // The default - when prefix == 0. + + if(nestLevel !== undefined && nestLevel > 0) { + prefix = ''; + for(var i=0; i < nestLevel; i++) { + prefix = prefix + base_prefix; + } + } + // The first keyboard added will be the default keyboard for touch devices. // For faster loading, it may be best for the default keyboard to be // locally sourced. - kmw.addKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'}, - filename:'./us-1.0.js'}); - - // Add more keyboards to the language menu, by keyboard name, - // keyboard name and language code, or just the BCP-47 language code. - kmw.addKeyboards('french', 'sil_euro_latin@no,sv', '@he'); // Loads all from uniquely-identifying strings. - + doAddKeyboards({id:'us',name:'English',languages:{id:'en',name:'English'}, + filename:(prefix + 'us-1.0.js')}); + + // Add more keyboards to the language menu by: + // 1. keyboard name ('french'), + // 2. keyboard name and language code ('sil_euro_latin@no,sv'), + // 3. or just the BCP-47 language code ('@he'). + kmw.addKeyboards('french', 'sil_euro_latin@no,sv', '@he'); + // Add a keyboard by language name. Note that the name must be spelled // correctly, or the keyboard will not be found. (Using BCP-47 codes is // usually easier.) - kmw.addKeyboardsForLanguage('Dzongkha'); - - // Add a fully-specified, locally-sourced, keyboard with custom font - kmw.addKeyboards({id:'lao_2008_basic',name:'Lao Basic', - languages:{ - id:'lo',name:'Lao',region:'Asia', - // A font can be specified here if its files are available. - // Example: - //font:{family:'LaoWeb',source:['../font/saysettha_web.ttf','../font/saysettha_web.woff','../font/saysettha_web.eot']} - }, - filename:'./lao_2008_basic-1.2.js' - }); - - // The following two optional calls should be delayed until language menus are fully loaded: - // (a) a specific mapped input element input is focused, to ensure that the OSK appears - // (b) a specific keyboard is loaded, rather than the keyboard last used. - //window.setTimeout(function(){kmw.setActiveElement('ta1',true);},2500); - //window.setTimeout(function(){kmw.setActiveKeyboard('Keyboard_french','fr');},3000); - - // Note that locally specified keyboards will be listed before keyboards + doAddKeyboardsForLanguage('Dzongkha'); + + // Add a fully-specified, locally-sourced, keyboard with custom font + doAddKeyboards({ + id:'lao_2008_basic', + name:'Lao Basic', + languages: { + id:'lo',name:'Lao',region:'Asia', + }, + filename:(prefix + 'lao_2008_basic-1.2.js') + }); + + // Note that locally specified keyboards will be listed before keyboards // requested from the remote server by user interfaces that do not order // keyboards alphabetically by language. } - - // Script to allow a user to add any keyboard to the keyboard menu - function addKeyboard(n) - { - var sKbd,kmw=keyman; - switch(n) - { + + // Script to allow a user to add any keyboard to the keyboard menu + function addKeyboard(n) { + var sKbd; + switch(n) { case 1: sKbd=document.getElementById('kbd_id1').value; - kmw.addKeyboards(sKbd); + doAddKeyboards(sKbd); break; case 2: sKbd=document.getElementById('kbd_id2').value.toLowerCase(); - kmw.addKeyboards('@'+sKbd); + doAddKeyboards('@'+sKbd); break; case 3: + // Add keyboard for comma-separated language name(s) sKbd=document.getElementById('kbd_id3').value; - kmw.addKeyboardsForLanguage(sKbd); + doAddKeyboardsForLanguage(sKbd); break; } } - + // Add keyboard on Enter (as well as pressing button) function clickOnEnter(e,id) { e = e || window.event; - if(e.keyCode == 13) addKeyboard(id); + if(e.keyCode == 13) addKeyboard(id); } diff --git a/web/src/samples/simplest/.gitignore b/web/src/samples/simplest/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..61b48a93bc558b4a380ea353106564be89a46d08 --- /dev/null +++ b/web/src/samples/simplest/.gitignore @@ -0,0 +1,3 @@ +**/* +!.gitignore +!index.html \ No newline at end of file diff --git a/web/src/samples/minified.html b/web/src/samples/simplest/index.html similarity index 89% rename from web/src/samples/minified.html rename to web/src/samples/simplest/index.html index 9b5d1674e89a537e6a33035bf31bcc2db8510b7a..c2ed9fd70bc53cdbfb61e754322cd9c72720522c 100644 --- a/web/src/samples/minified.html +++ b/web/src/samples/simplest/index.html @@ -23,7 +23,7 @@ - + - + - + -

KeymanWeb Sample Page - Fully Compiled/Minified Source

+

KeymanWeb Sample Page - Simple Subfolder + Toggle UI

- KeymanWeb Sample Page - Unminified Source + KeymanWeb Sample Page - Fully Compiled Source - - + + - + - + - + -

KeymanWeb Sample Page - Unminified Source

+

KeymanWeb Sample Page - Simple Subfolder + Toggle UI

+ diff --git a/web/src/samples/subfolder_toolbar/.gitignore b/web/src/samples/subfolder_toolbar/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..659fe973e2a11029bed8f9381d1dd9fa9b00e468 --- /dev/null +++ b/web/src/samples/subfolder_toolbar/.gitignore @@ -0,0 +1 @@ +keyman/**/* \ No newline at end of file diff --git a/web/src/samples/multilingual.html b/web/src/samples/subfolder_toolbar/index.html similarity index 82% rename from web/src/samples/multilingual.html rename to web/src/samples/subfolder_toolbar/index.html index de2174a49a8e4dd706ccc89480a4cfcc3347cbf4..7e9aed3f3f7074eca65c9603f410df2d24452607 100644 --- a/web/src/samples/multilingual.html +++ b/web/src/samples/subfolder_toolbar/index.html @@ -21,17 +21,16 @@ - + - + @@ -76,7 +72,7 @@ -

Return to samples home page

+

Return to samples home page

diff --git a/web/src/test/auto/CI.conf.js b/web/src/test/auto/CI.conf.js deleted file mode 100644 index d3d2b02e400591819170741716f02af73dce9277..0000000000000000000000000000000000000000 --- a/web/src/test/auto/CI.conf.js +++ /dev/null @@ -1,4 +0,0 @@ -var BASE_CONFIG = require("./base.conf.js"); -var ci_config_adapter = require("../../../../common/test/resources/karma-browserstack-config.js"); - -module.exports = ci_config_adapter(BASE_CONFIG, "full"); \ No newline at end of file diff --git a/web/src/test/auto/cases/attachmentAPI.js b/web/src/test/auto/cases/attachmentAPI.js deleted file mode 100644 index 0fe4d933b24130ca31f62924100b116569cd076b..0000000000000000000000000000000000000000 --- a/web/src/test/auto/cases/attachmentAPI.js +++ /dev/null @@ -1,257 +0,0 @@ -var assert = chai.assert; - -describe('Attachment API', function() { - this.timeout(testconfig.timeouts.standard); - - before(function() { - assert.isFalse(com.keyman.karma.DEVICE_DETECT_FAILURE, "Cannot run due to device detection failure."); - fixture.setBase('fixtures'); - - this.timeout(testconfig.timeouts.scriptLoad * 3); - return setupKMW({ attachType:'manual' }, testconfig.timeouts.scriptLoad).then(() => { - const kbd1 = loadKeyboardFromJSON("/keyboards/lao_2008_basic.json", testconfig.timeouts.scriptLoad, { passive: true }); - const kbd2 = loadKeyboardFromJSON("/keyboards/khmer_angkor.json", testconfig.timeouts.scriptLoad, { passive: true }); - return Promise.all([kbd1, kbd2]).then(() => { - return keyman.setActiveKeyboard("lao_2008_basic", "lo"); - }); - }); - }); - - after(function() { - keyman.removeKeyboards('lao_2008_basic'); - keyman.removeKeyboards('khmer_angkor'); - teardownKMW(); - }); - - beforeEach(function() { - fixture.load("robustAttachment.html"); - }); - - afterEach(function(done) { - fixture.cleanup(); - window.setTimeout(function(){ - done(); - }, testconfig.timeouts.eventDelay); - }); - - it("Attachment/Detachment", function(done) { - // Since we're in 'manual', we start detached. - var ele = document.getElementById(DynamicElements.addInput()); - - window.setTimeout(function() { - // Ensure we didn't auto-attach. - DynamicElements.assertDetached(ele); - let eventDriver = new KMWRecorder.BrowserDriver(ele); - eventDriver.simulateEvent(DynamicElements.keyCommand); - - var val = ele.value; - ele.value = ""; - assert.equal(val, DynamicElements.disabledOutput, "'Detached' element performed keystroke processing!"); - - keyman.attachToControl(ele); - DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment. - - eventDriver = new KMWRecorder.BrowserDriver(ele); - eventDriver.simulateEvent(DynamicElements.keyCommand); - - val = retrieveAndReset(ele); - - assert.equal(val, DynamicElements.enabledLaoOutput, "'Attached' element did not perform keystroke processing!"); - - done(); - }, testconfig.timeouts.eventDelay); - }); - - it("Enablement/Disablement", function(done) { - // Since we're in 'manual', we start detached. - var ele = document.getElementById(DynamicElements.addInput()); - window.setTimeout(function() { - keyman.attachToControl(ele); - keyman.disableControl(ele); - - // It appears that mobile devices do not instantly trigger the MutationObserver, so we need a small timeout - // for the change to take effect. - window.setTimeout(function() { - DynamicElements.assertAttached(ele); - let eventDriver = new KMWRecorder.BrowserDriver(ele); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(ele); - assert.equal(val, DynamicElements.disabledOutput, "'Disabled' element performed keystroke processing!"); - - keyman.enableControl(ele); - window.setTimeout(function() { - DynamicElements.assertAttached(ele); // Happens in-line, since we directly request the attachment. - let eventDriver = new KMWRecorder.BrowserDriver(ele); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(ele); - assert.equal(val, DynamicElements.enabledLaoOutput, "'Enabled' element did not perform keystroke processing!"); - done(); - }, testconfig.timeouts.eventDelay); - }, testconfig.timeouts.eventDelay); - }, testconfig.timeouts.eventDelay); - }); - - it("Keyboard Management (active control)", function() { - // It appears that event generation + inline event dispatching is a bit time-intensive on some browsers. - this.timeout(testconfig.timeouts.standard * 2); - - var input = document.getElementById(DynamicElements.addInput()); - var textarea = document.getElementById(DynamicElements.addText()); - - keyman.attachToControl(input); - keyman.attachToControl(textarea); - - keyman.setActiveElement(input); - // We assume from the other tests that running on the Lao keyboard will give proper output. - // It'd be a redundant check. - - // Set control with independent keyboard. - keyman.setKeyboardForControl(input, "khmer_angkor", "km"); - var eventDriver = new KMWRecorder.BrowserDriver(input); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(input); - assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not use control's keyboard settings!"); - - // Swap to a global-linked control... - keyman.setActiveElement(textarea); - eventDriver = new KMWRecorder.BrowserDriver(textarea); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(textarea); - assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not use manage keyboard settings correctly for global-linked control!"); - - // Swap back and check that the settings persist. - keyman.setActiveElement(input); - eventDriver = new KMWRecorder.BrowserDriver(input); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(input); - assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW forgot control's independent keyboard settings!"); - - // Finally, clear the independent setting. - keyman.setKeyboardForControl(input, null, null); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(input); - assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!"); - }); - - it("Keyboard Management (inactive control)", function() { - // It appears that event generation + inline event dispatching is a bit time-intensive on some browsers. - this.timeout(testconfig.timeouts.standard * 2); - - var input = document.getElementById(DynamicElements.addInput()); - var textarea = document.getElementById(DynamicElements.addText()); - - keyman.attachToControl(input); - keyman.attachToControl(textarea); - - // We assume from the other tests that running on the Lao keyboard will give proper output. - // It'd be a redundant check. - - // We are testing that setting a specific keyboard for an inactive control does not affect - // the currently active control. The textarea control will be manually set to Khmer, - // and the input control will get the document default of Lao. - - keyman.setActiveElement(input); - // Set textarea control with independent keyboard khmer_angkor. - keyman.setKeyboardForControl(textarea, "khmer_angkor", "km"); - var eventDriver = new KMWRecorder.BrowserDriver(input); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(input); - assert.equal(val, DynamicElements.enabledLaoOutput, "KMW set independent keyboard for the incorrect control!"); - - // Swap to the textarea control with its overridden khmer_angkor keyboard... - keyman.setActiveElement(textarea); - eventDriver = new KMWRecorder.BrowserDriver(textarea); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(textarea); - assert.equal(val, DynamicElements.enabledKhmerOutput, "KMW did not properly store keyboard for the previously-inactive control!"); - - // Swap back to the input control and check that the settings persist. - keyman.setActiveElement(input); - keyman.setKeyboardForControl(textarea, null, null); - - eventDriver = new KMWRecorder.BrowserDriver(input); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(input); - assert.equal(val, DynamicElements.enabledLaoOutput, "KMW made a strange error when clearing an inactive control's keyboard setting!"); - - keyman.setActiveElement(textarea); - // Finally, after clearing the independent setting, check that we are back to Lao output as expected for the textarea - eventDriver = new KMWRecorder.BrowserDriver(textarea); - eventDriver.simulateEvent(DynamicElements.keyCommand); - val = retrieveAndReset(textarea); - assert.equal(val, DynamicElements.enabledLaoOutput, "KMW did not properly clear control's independent keyboard settings!"); - }); -}); - - -describe('Attachment Checks (Desktop, \'auto\')', function() { - - this.timeout(testconfig.timeouts.standard); - - before(function() { - this.timeout(testconfig.timeouts.scriptLoad); - - fixture.setBase('fixtures'); - return setupKMW({ attachType:'auto' }, testconfig.timeouts.scriptLoad); - }); - - beforeEach(function() { - fixture.load("robustAttachment.html"); - }); - - after(function() { - teardownKMW(); - }); - - afterEach(function(done) { - fixture.cleanup(); - window.setTimeout(function(){ - done(); - }, testconfig.timeouts.eventDelay); - }) - - describe('Element Type', function() { - it('', function(done) { - var ID = DynamicElements.addInput(); - var ele = document.getElementById(ID); - - DynamicElements.assertAttached(ele, done); - }); - - it('
\ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/dynamic-element-container.html b/web/src/test/auto/resources/fixtures/dynamic-element-container.html new file mode 100644 index 0000000000000000000000000000000000000000..b6cbe497058caaa006f2a52de0ed923eb075fe05 --- /dev/null +++ b/web/src/test/auto/resources/fixtures/dynamic-element-container.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/input-and-disabled-text.html b/web/src/test/auto/resources/fixtures/input-and-disabled-text.html new file mode 100644 index 0000000000000000000000000000000000000000..9f6f7e204c4860a8fc5fb493e222397dee00325b --- /dev/null +++ b/web/src/test/auto/resources/fixtures/input-and-disabled-text.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/input-and-text.html b/web/src/test/auto/resources/fixtures/input-and-text.html new file mode 100644 index 0000000000000000000000000000000000000000..b3a77bac09ac22593dc80d7fd47a75e366e1d244 --- /dev/null +++ b/web/src/test/auto/resources/fixtures/input-and-text.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/inputs-numeric-none-disabled_email.html b/web/src/test/auto/resources/fixtures/inputs-numeric-none-disabled_email.html new file mode 100644 index 0000000000000000000000000000000000000000..c2e5756aeecbe0b8f7e3dea83af48b0420367c46 --- /dev/null +++ b/web/src/test/auto/resources/fixtures/inputs-numeric-none-disabled_email.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/nested-iframe.html b/web/src/test/auto/resources/fixtures/nested-iframe.html new file mode 100644 index 0000000000000000000000000000000000000000..3fcbf6f27425c4367614dab44ebbfa917e6cb7fa --- /dev/null +++ b/web/src/test/auto/resources/fixtures/nested-iframe.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/simple-design-iframe.html b/web/src/test/auto/resources/fixtures/simple-design-iframe.html new file mode 100644 index 0000000000000000000000000000000000000000..be97af45a73bc659434257a144642a614acf0dec --- /dev/null +++ b/web/src/test/auto/resources/fixtures/simple-design-iframe.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/simple-editable-div.html b/web/src/test/auto/resources/fixtures/simple-editable-div.html new file mode 100644 index 0000000000000000000000000000000000000000..7cf99857eb36f9913d6fb5c40c46ca7b4b9054de --- /dev/null +++ b/web/src/test/auto/resources/fixtures/simple-editable-div.html @@ -0,0 +1 @@ +
Edit me!
\ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/simple-iframe-with-input.html b/web/src/test/auto/resources/fixtures/simple-iframe-with-input.html new file mode 100644 index 0000000000000000000000000000000000000000..870a19625ad1502a15053caa41dd1309281ad56b --- /dev/null +++ b/web/src/test/auto/resources/fixtures/simple-iframe-with-input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/wild-absolute-positioning-extras.html b/web/src/test/auto/resources/fixtures/wild-absolute-positioning-extras.html new file mode 100644 index 0000000000000000000000000000000000000000..3ef8762c64ec683e3facd44dbf89fbaa9f20a5c4 --- /dev/null +++ b/web/src/test/auto/resources/fixtures/wild-absolute-positioning-extras.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/web/src/test/auto/resources/fixtures/wild-absolute-positioning.html b/web/src/test/auto/resources/fixtures/wild-absolute-positioning.html new file mode 100644 index 0000000000000000000000000000000000000000..95e0ce6458067b2cc31d455cd1b161ad90e6ac36 --- /dev/null +++ b/web/src/test/auto/resources/fixtures/wild-absolute-positioning.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/web/src/test/auto/resources/html/designIframe.html b/web/src/test/auto/resources/html/designIframe.html new file mode 100644 index 0000000000000000000000000000000000000000..604434258e2037f075bb32aa1e500a8ab459d495 --- /dev/null +++ b/web/src/test/auto/resources/html/designIframe.html @@ -0,0 +1,13 @@ + + + + + + + Edit me! + + \ No newline at end of file diff --git a/web/src/test/auto/resources/html/iframe.html b/web/src/test/auto/resources/html/iframe.html new file mode 100644 index 0000000000000000000000000000000000000000..366469f7bbc06d0fd75675546381611b1149c7eb --- /dev/null +++ b/web/src/test/auto/resources/html/iframe.html @@ -0,0 +1,7 @@ + + + +

Hello World!

+ + + \ No newline at end of file diff --git a/web/src/test/auto/resources/html/nesting-inner-iframe.html b/web/src/test/auto/resources/html/nesting-inner-iframe.html new file mode 100644 index 0000000000000000000000000000000000000000..fed5398c43da181dff06deff08060ac8fe4af7a2 --- /dev/null +++ b/web/src/test/auto/resources/html/nesting-inner-iframe.html @@ -0,0 +1,7 @@ + + + +

Hello World!

+ + + \ No newline at end of file diff --git a/web/src/test/auto/resources/html/nesting-outer-iframe.html b/web/src/test/auto/resources/html/nesting-outer-iframe.html new file mode 100644 index 0000000000000000000000000000000000000000..8428b4ab17f7e779dd9e0eca9e24a9f3d61ee74c --- /dev/null +++ b/web/src/test/auto/resources/html/nesting-outer-iframe.html @@ -0,0 +1,13 @@ + + + +

Hello World!

+

This is the 'outer' iframe, which itself hosts an INNER iframe used during the same test.

+ + + + +

Comments?

+ + + \ No newline at end of file diff --git a/web/src/test/auto/resources/query-mock-results/@dz.js.fixture b/web/src/test/auto/resources/query-mock-results/@dz.js.fixture new file mode 100644 index 0000000000000000000000000000000000000000..e63ab927651f573a24a702f1e4bc02b10aef959f --- /dev/null +++ b/web/src/test/auto/resources/query-mock-results/@dz.js.fixture @@ -0,0 +1,1609 @@ +keyman.register({ + "options": { + "context": "keyboard", + "dateFormat": "standard", + "device": "any", + "keyboardBaseUri": "https://s.keyman.com/keyboard/", + "fontBaseUri": "https://s.keyman.com/font/deploy/", + "keyboardid": "@dz", + "keyboardVersion": "current" + }, + "keyboard": [[{ + "id": "basic_kbddzo", + "name": "Dzongkha Basic", + "filename": "basic_kbddzo/1.0/basic_kbddzo-1.0.js", + "version": "1.0", + "source": "https://github.com/keymanapp/keyboards/tree/master/release/basic/basic_kbddzo", + "devices": { + "phone": 2, + "tablet": 2, + "desktop": 2 + }, + "languages": [{ + "id": "dz", + "name": "Dzongkha", + "region": 3 + }] + }, { + "id": "tibetan_direct_input", + "name": "Tibetan Direct Input", + "filename": "tibetan_direct_input/1.4/tibetan_direct_input-1.4.js", + "version": "1.4", + "source": "https://github.com/keymanapp/keyboards/tree/master/release/t/tibetan_direct_input", + "devices": { + "phone": 2, + "tablet": 2, + "desktop": 2 + }, + "languages": [{ + "id": "adi-tibt", + "name": "Adi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bft-tibt", + "name": "Balti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kjz-tibt", + "name": "Bumthangkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "cna-tibt", + "name": "Changthang", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dre-tibt", + "name": "Dolpo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dz", + "name": "Dzongkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bfu-tibt", + "name": "Gahri", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "clk-tibt", + "name": "Idu-Mishmi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "jya-tibt", + "name": "Jiarong", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kbg-tibt", + "name": "Khamba", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xkf-tibt", + "name": "Khengkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ght-tibt", + "name": "Kuke", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbj-tibt", + "name": "Ladakhi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lep-tibt", + "name": "Lepcha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "loy-tibt", + "name": "Lhowa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khk-tibt", + "name": "Mongolian, Halh", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "muk-tibt", + "name": "Mugom", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lae-tibt", + "name": "Pattani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "prx-tibt", + "name": "Purik", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xsr-tibt", + "name": "Sherpa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sip-tibt", + "name": "Sikkimese", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sbu-tibt", + "name": "Stod Bhoti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "taj-tibt", + "name": "Tamang, Eastern", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tdg-tibt", + "name": "Tamang, Western", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "adx-tibt", + "name": "Tibetan, Amdo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bo-tibt", + "name": "Tibetan, Central", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khg-tibt", + "name": "Tibetan, Khams", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbf-tibt", + "name": "Tinani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tsj", + "name": "Tshangla", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ola", + "name": "Walungge", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "zau-tibt", + "name": "Zangskari", + "region": 3 + }] + }, { + "id": "tibetan_ewts", + "name": "Tibetan EWTS", + "filename": "tibetan_ewts/2.4/tibetan_ewts-2.4.js", + "version": "2.4", + "source": "https://github.com/keymanapp/keyboards/tree/master/release/t/tibetan_ewts", + "devices": { + "phone": 2, + "tablet": 2, + "desktop": 2 + }, + "languages": [{ + "id": "adi-tibt", + "name": "Adi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bft-tibt", + "name": "Balti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kjz-tibt", + "name": "Bumthangkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "cna-tibt", + "name": "Changthang", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xct-tibt", + "name": "Classical Tibetan", + "region": 1, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dre-tibt", + "name": "Dolpo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dz", + "name": "Dzongkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bfu-tibt", + "name": "Gahri", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "clk-tibt", + "name": "Idu-Mishmi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "jya-tibt", + "name": "Jiarong", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kbg-tibt", + "name": "Khamba", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xkf-tibt", + "name": "Khengkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ght-tibt", + "name": "Kuke", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbj-tibt", + "name": "Ladakhi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lep-tibt", + "name": "Lepcha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "loy-tibt", + "name": "Lhowa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khk-tibt", + "name": "Mongolian, Halh", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "muk-tibt", + "name": "Mugom", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "otb-tibt", + "name": "Old Tibetan", + "region": 1, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lae-tibt", + "name": "Pattani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "prx-tibt", + "name": "Purik", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xsr-tibt", + "name": "Sherpa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sip-tibt", + "name": "Sikkimese", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sbu-tibt", + "name": "Stod Bhoti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "taj-tibt", + "name": "Tamang, Eastern", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tdg-tibt", + "name": "Tamang, Western", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "adx-tibt", + "name": "Tibetan, Amdo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bo-tibt", + "name": "Tibetan, Central", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khg-tibt", + "name": "Tibetan, Khams", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbf-tibt", + "name": "Tinani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tsj-tibt", + "name": "Tshangla", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ola-tibt", + "name": "Walungge", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "zau-tibt", + "name": "Zangskari", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }] + }, { + "id": "dzongkha", + "name": "Dzongkha", + "filename": "dzongkha/1.0/dzongkha-1.0.js", + "version": "1.0", + "desktopKeyboardID": 652, + "source": "https://github.com/keymanapp/keyboards/tree/master/legacy/d/dzongkha", + "devices": { + "phone": 0, + "tablet": 0, + "desktop": 2 + }, + "languages": [{ + "id": "dz", + "name": "Dzongkha", + "region": 3 + }] + }, { + "id": "tibetan_direct_unicode", + "name": "Tibetan Direct-to-Unicode v2", + "filename": "tibetan_direct_unicode/1.1/tibetan_direct_unicode-1.1.js", + "version": "1.1", + "source": "https://github.com/keymanapp/keyboards/tree/master/legacy/t/tibetan_direct_unicode", + "devices": { + "phone": 1, + "tablet": 1, + "desktop": 2 + }, + "languages": [{ + "id": "dz", + "name": "Dzongkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bo", + "name": "Tibetan, Central", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }] + }, { + "id": "tibetan_unicode_direct_input", + "name": "Tibetan Unicode Direct Input", + "filename": "tibetan_unicode_direct_input/1.1/tibetan_unicode_direct_input-1.1.js", + "version": "1.1", + "source": "https://github.com/keymanapp/keyboards/tree/master/legacy/t/tibetan_unicode_direct_input", + "devices": { + "phone": 1, + "tablet": 1, + "desktop": 2 + }, + "languages": [{ + "id": "adi", + "name": "Adi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bft", + "name": "Balti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kjz", + "name": "Bumthangkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "cna", + "name": "Changthang", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dre", + "name": "Dolpo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dz", + "name": "Dzongkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bfu", + "name": "Gahri", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "clk", + "name": "Idu-Mishmi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "jya", + "name": "Jiarong", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kbg", + "name": "Khamba", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xkf", + "name": "Khengkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ght", + "name": "Kuke", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbj", + "name": "Ladakhi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lep", + "name": "Lepcha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "loy", + "name": "Lhowa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khk", + "name": "Mongolian, Halh", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "muk", + "name": "Mugom", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lae", + "name": "Pattani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "prx", + "name": "Purik", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xsr", + "name": "Sherpa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sip", + "name": "Sikkimese", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sbu", + "name": "Stod Bhoti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "taj", + "name": "Tamang, Eastern", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tdg", + "name": "Tamang, Western", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "adx", + "name": "Tibetan, Amdo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bo", + "name": "Tibetan, Central", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khg", + "name": "Tibetan, Khams", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbf", + "name": "Tinani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tsj", + "name": "Tshangla", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ola", + "name": "Walungge", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }] + }, { + "id": "tibetan_unicode_ewts", + "name": "Tibetan Unicode EWTS", + "filename": "tibetan_unicode_ewts/1.1/tibetan_unicode_ewts-1.1.js", + "version": "1.1", + "source": "https://github.com/keymanapp/keyboards/tree/master/legacy/t/tibetan_unicode_ewts", + "devices": { + "phone": 1, + "tablet": 1, + "desktop": 2 + }, + "languages": [{ + "id": "adi", + "name": "Adi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bft", + "name": "Balti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kjz", + "name": "Bumthangkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "cna", + "name": "Changthang", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dre", + "name": "Dolpo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "dz", + "name": "Dzongkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bfu", + "name": "Gahri", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "clk", + "name": "Idu-Mishmi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "jya", + "name": "Jiarong", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "kbg", + "name": "Khamba", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xkf", + "name": "Khengkha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ght", + "name": "Kuke", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbj", + "name": "Ladakhi", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lep", + "name": "Lepcha", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "loy", + "name": "Lhowa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khk", + "name": "Mongolian, Halh", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "muk", + "name": "Mugom", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lae", + "name": "Pattani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "prx", + "name": "Purik", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "xsr", + "name": "Sherpa", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sip", + "name": "Sikkimese", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "sbu", + "name": "Stod Bhoti", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "taj", + "name": "Tamang, Eastern", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tdg", + "name": "Tamang, Western", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "adx", + "name": "Tibetan, Amdo", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "bo", + "name": "Tibetan, Central", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "khg", + "name": "Tibetan, Khams", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "lbf", + "name": "Tinani", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "tsj", + "name": "Tshangla", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }, { + "id": "ola", + "name": "Walungge", + "region": 3, + "font": { + "family": "TibetanWeb", + "source": ["DDC_Uchen.ttf"] + }, + "oskFont": { + "family": "TibetanWebOsk", + "source": ["DDC_Uchen.ttf", "ddc_uchen-webfont.svg#ddc_uchenregular"] + } + }] + }]] +}); diff --git a/web/src/test/auto/resources/query-mock-results/khmer_angkor.hand-edited.js.fixture b/web/src/test/auto/resources/query-mock-results/khmer_angkor.hand-edited.js.fixture new file mode 100644 index 0000000000000000000000000000000000000000..63456e826c56008a74773870007fc7aa396cf680 --- /dev/null +++ b/web/src/test/auto/resources/query-mock-results/khmer_angkor.hand-edited.js.fixture @@ -0,0 +1,49 @@ +keyman.register( + { + "options": { + "context":"keyboard", + "dateFormat":"standard", + "device":"any", + // Change 1: redirect to /common/test/resources' subfolder. + // "keyboardBaseUri":"https://s.keyman.com/keyboard/", + "keyboardBaseUri":"/resources/keyboards/", + "fontBaseUri":"https://s.keyman.com/font/deploy/", + "keyboardid":"khmer_angkor", + "keyboardVersion":"current" + }, + "keyboard": [ + { + "id":"khmer_angkor", + "name":"Khmer Angkor", + // Change 2: Is unversioned within /common/test/resources. + // "filename":"khmer_angkor/1.3/khmer_angkor-1.3.js", + "filename": "khmer_angkor.js", + "version":"1.3", + "source":"https://github.com/keymanapp/keyboards/tree/master/release/k/khmer_angkor", + "devices": { + "phone":2, + "tablet":2, + "desktop":2 + }, + "languages": [ + { + "id":"km", + "name":"Khmer", + "region":3, + "font": { + "family":"Khmer Mondulkiri", + "source": [ + "Mondulkiri-R.ttf" + ] + }, "oskFont": { + "family":"Khmer Busra Kbd", + "source": [ + "khmer_busra_kbd.ttf" + ] + } + } + ] + } + ] + } +); \ No newline at end of file diff --git a/web/src/test/auto/resources/query-mock-results/languages.js.fixture b/web/src/test/auto/resources/query-mock-results/languages.js.fixture new file mode 100644 index 0000000000000000000000000000000000000000..dc4644444d14acad97f76881ddfefb300ec73221 --- /dev/null +++ b/web/src/test/auto/resources/query-mock-results/languages.js.fixture @@ -0,0 +1,8760 @@ +keyman.register({ + "options": { + "context": "language", + "dateFormat": "standard", + "device": "any", + "keyboardBaseUri": "https://s.keyman.com/keyboard/", + "fontBaseUri": "https://s.keyman.com/font/deploy/", + "keyboardVersion": "current" + }, + "languages": [{ + "name": "\u2019Are\u2019are (Latin)", + "id": "alu-latn", + "region": 7 + }, { + "name": "Abenaki, Western (Latin)", + "id": "abe-latn", + "region": 6 + }, { + "name": "Aceh (Latin)", + "id": "ace-latn", + "region": 3 + }, { + "name": "Achterhoeks", + "id": "act", + "region": 4 + }, { + "name": "Achumawi", + "id": "acv", + "region": 6 + }, { + "name": "Adara (Latin)", + "id": "kad-latn", + "region": 2 + }, { + "name": "Adasen (Latin)", + "id": "tiu-latn", + "region": 3 + }, { + "name": "Adi (Tibetan)", + "id": "adi-tibt", + "region": 3 + }, { + "name": "Adyghe (Latin)", + "id": "ady-latn", + "region": 4 + }, { + "name": "Aer (Arabic)", + "id": "aeq-arab", + "region": 3 + }, { + "name": "Afade", + "id": "aal", + "region": 2 + }, { + "name": "Afrikaans", + "id": "af", + "region": 2 + }, { + "name": "Afrikaans (Braille)", + "id": "af-brai", + "region": 2 + }, { + "name": "Agatu (Latin)", + "id": "agc-latn", + "region": 2 + }, { + "name": "Aghem", + "id": "agq", + "region": 2 + }, { + "name": "Agta, Casiguran Dumagat (Latin)", + "id": "dgc-latn", + "region": 3 + }, { + "name": "Agta, Central Cagayan (Latin)", + "id": "agt-latn", + "region": 3 + }, { + "name": "Agta, Dupaninan (Latin)", + "id": "duo-latn", + "region": 3 + }, { + "name": "Agta, Pahanan (Latin)", + "id": "apf-latn", + "region": 3 + }, { + "name": "Agta, Umiray Dumaget (Latin)", + "id": "due-latn", + "region": 3 + }, { + "name": "Agutaynen (Latin)", + "id": "agn-latn", + "region": 3 + }, { + "name": "Ahom (Ahom)", + "id": "aho-ahom", + "region": 3 + }, { + "name": "ajp-arab (Arabic)", + "id": "ajp-arab", + "region": 1 + }, { + "name": "Ajumbu", + "id": "muc", + "region": 2 + }, { + "name": "Akan (Latin)", + "id": "ak-latn", + "region": 2 + }, { + "name": "Akha", + "id": "ahk", + "region": 3 + }, { + "name": "Aklanon (Latin)", + "id": "akl-latn", + "region": 3 + }, { + "name": "Akoose", + "id": "bss", + "region": 2 + }, { + "name": "Akum", + "id": "aku", + "region": 2 + }, { + "name": "Alangan (Latin)", + "id": "alj-latn", + "region": 3 + }, { + "name": "Albanian", + "id": "sq", + "region": 1 + }, { + "name": "Albanian (Braille)", + "id": "sq-brai", + "region": 1 + }, { + "name": "Albanian, Arb\u00ebresh\u00eb", + "id": "aae", + "region": 4 + }, { + "name": "Albanian, Gheg", + "id": "aln", + "region": 4 + }, { + "name": "Albanian, Gheg (Braille)", + "id": "aln-brai", + "region": 4 + }, { + "name": "Albanian, Tosk (Latin)", + "id": "als-latn", + "region": 4 + }, { + "name": "Ale (Latin)", + "id": "gwd-latn", + "region": 2 + }, { + "name": "Aleut", + "id": "ale", + "region": 6 + }, { + "name": "Algonquin (Latin)", + "id": "alq-latn", + "region": 6 + }, { + "name": "Alsea", + "id": "aes", + "region": 1 + }, { + "name": "Alta, Southern (Latin)", + "id": "agy-latn", + "region": 3 + }, { + "name": "Aluo (Miao)", + "id": "yna-plrd", + "region": 3 + }, { + "name": "Alur (Latin)", + "id": "alz-latn", + "region": 2 + }, { + "name": "Ama (Latin)", + "id": "nyi-latn", + "region": 2 + }, { + "name": "Amba (Latin)", + "id": "rwm-latn", + "region": 2 + }, { + "name": "Ambele", + "id": "ael", + "region": 2 + }, { + "name": "Amharic", + "id": "am", + "region": 2 + }, { + "name": "Amharic (Braille)", + "id": "am-brai", + "region": 2 + }, { + "name": "Andaandi (Coptic)", + "id": "dgl-copt", + "region": 2 + }, { + "name": "Anii (Latin)", + "id": "blo-latn", + "region": 2 + }, { + "name": "Antigua and Barbuda Creole English", + "id": "aig", + "region": 6 + }, { + "name": "Anuak (Latin)", + "id": "anu-latn", + "region": 2 + }, { + "name": "Arabic", + "id": "ar", + "region": 1 + }, { + "name": "Arabic (Braille)", + "id": "ar-brai", + "region": 1 + }, { + "name": "Arabic (Algeria)", + "id": "ar-dz", + "region": 1 + }, { + "name": "Arabic, Algerian Spoken (Arabic)", + "id": "arq-arab", + "region": 2 + }, { + "name": "Arabic, Baharna Spoken (Arabic)", + "id": "abv-arab", + "region": 3 + }, { + "name": "Arabic, Chadian Spoken (Arabic)", + "id": "shu-arab", + "region": 2 + }, { + "name": "Arabic, Chadian Spoken (Latin)", + "id": "shu-latn", + "region": 2 + }, { + "name": "Arabic, Cypriot Spoken (Arabic)", + "id": "acy-arab", + "region": 3 + }, { + "name": "Arabic, Eastern Egyptian Bedawi Spoken (Arabic)", + "id": "avl-arab", + "region": 2 + }, { + "name": "Arabic, Egyptian Spoken (Arabic)", + "id": "arz-arab", + "region": 2 + }, { + "name": "Arabic, Egyptian Spoken (Braille)", + "id": "arz-brai", + "region": 2 + }, { + "name": "Arabic, Gulf Spoken (Arabic)", + "id": "afb-arab", + "region": 3 + }, { + "name": "Arabic, Hijazi Spoken (Arabic)", + "id": "acw-arab", + "region": 3 + }, { + "name": "Arabic, Libyan Spoken (Arabic)", + "id": "ayl-arab", + "region": 2 + }, { + "name": "Arabic, Mesopotamian Spoken (Arabic)", + "id": "acm-arab", + "region": 3 + }, { + "name": "Arabic, Moroccan Spoken (Arabic)", + "id": "ary-arab", + "region": 2 + }, { + "name": "Arabic, Najdi Spoken (Arabic)", + "id": "ars-arab", + "region": 3 + }, { + "name": "Arabic, North Levantine Spoken (Arabic)", + "id": "apc-arab", + "region": 3 + }, { + "name": "Arabic, North Mesopotamian Spoken (Arabic)", + "id": "ayp-arab", + "region": 3 + }, { + "name": "Arabic, Omani Spoken (Arabic)", + "id": "acx-arab", + "region": 3 + }, { + "name": "Arabic, Sa\u2019idi Spoken (Arabic)", + "id": "aec-arab", + "region": 2 + }, { + "name": "Arabic, Sanaani Spoken (Arabic)", + "id": "ayn-arab", + "region": 3 + }, { + "name": "Arabic, Standard (Arabic)", + "id": "arb-arab", + "region": 3 + }, { + "name": "Arabic, Standard (Braille)", + "id": "arb-brai", + "region": 3 + }, { + "name": "Arabic, Sudanese Spoken (Arabic)", + "id": "apd-arab", + "region": 2 + }, { + "name": "Arabic, Ta\u2019izzi-Adeni Spoken (Arabic)", + "id": "acq-arab", + "region": 3 + }, { + "name": "Arabic, Tajiki Spoken (Arabic)", + "id": "abh-arab", + "region": 3 + }, { + "name": "Arabic, Tunisian Spoken (Arabic)", + "id": "aeb-arab", + "region": 2 + }, { + "name": "Aragonese", + "id": "an", + "region": 4 + }, { + "name": "Araona", + "id": "aro", + "region": 6 + }, { + "name": "Argobba", + "id": "agj", + "region": 2 + }, { + "name": "Armenian", + "id": "hy", + "region": 3 + }, { + "name": "Armenian (Braille)", + "id": "hy-brai", + "region": 3 + }, { + "name": "Aromanian", + "id": "rup", + "region": 4 + }, { + "name": "Arpitan", + "id": "frp", + "region": 4 + }, { + "name": "Ashe", + "id": "ahs", + "region": 2 + }, { + "name": "Asoa (Latin)", + "id": "asv-latn", + "region": 2 + }, { + "name": "Assamese", + "id": "as", + "region": 3 + }, { + "name": "Assamese (India)", + "id": "as-in", + "region": 3 + }, { + "name": "Assamese (Latin)", + "id": "as-latn", + "region": 3 + }, { + "name": "Assangori", + "id": "sjg", + "region": 2 + }, { + "name": "Assiniboine (Latin)", + "id": "asb-latn", + "region": 6 + }, { + "name": "Assyrian Neo-Aramaic (Syriac)", + "id": "aii-syrc", + "region": 3 + }, { + "name": "Asturian", + "id": "ast", + "region": 4 + }, { + "name": "Asu (Latin)", + "id": "asa-latn", + "region": 2 + }, { + "name": "Athpariya", + "id": "aph", + "region": 3 + }, { + "name": "Atikamekw (Latin)", + "id": "atj-latn", + "region": 6 + }, { + "name": "Atong", + "id": "ato", + "region": 2 + }, { + "name": "Atta, Pamplona (Latin)", + "id": "att-latn", + "region": 3 + }, { + "name": "Avestan (Gujarati)", + "id": "ae-gujr", + "region": 3 + }, { + "name": "Avokaya (Latin)", + "id": "avu-latn", + "region": 2 + }, { + "name": "Awadhi (Devanagari)", + "id": "awa-deva", + "region": 3 + }, { + "name": "Awakateko", + "id": "agu", + "region": 6 + }, { + "name": "Awing", + "id": "azo", + "region": 2 + }, { + "name": "Awngi", + "id": "awn", + "region": 2 + }, { + "name": "Aymara", + "id": "ay", + "region": 1 + }, { + "name": "Ayta, Abellen (Latin)", + "id": "abp-latn", + "region": 3 + }, { + "name": "Ayta, Ambala (Latin)", + "id": "abc-latn", + "region": 3 + }, { + "name": "Ayta, Mag-antsi (Latin)", + "id": "sgb-latn", + "region": 3 + }, { + "name": "Ayta, Mag-Indi (Latin)", + "id": "blx-latn", + "region": 3 + }, { + "name": "Azerbaijani (Braille)", + "id": "az-brai", + "region": 1 + }, { + "name": "Azerbaijani (Cyrillic)", + "id": "az-cyrl", + "region": 1 + }, { + "name": "Azerbaijani, North (Braille)", + "id": "azj-brai", + "region": 3 + }, { + "name": "Azerbaijani, North (Cyrillic)", + "id": "azj-cyrl", + "region": 3 + }, { + "name": "Azerbaijani, North (Latin)", + "id": "azj-latn", + "region": 3 + }, { + "name": "Azerbaijani, North (Latin, Azerbaijan)", + "id": "azj-latn-az", + "region": 3 + }, { + "name": "Azerbaijani, South (Cyrillic)", + "id": "azb-cyrl", + "region": 3 + }, { + "name": "Baatonum (Latin)", + "id": "bba-latn", + "region": 2 + }, { + "name": "Babango (Latin)", + "id": "bbm-latn", + "region": 2 + }, { + "name": "Babanki", + "id": "bbk", + "region": 2 + }, { + "name": "Babine (Latin)", + "id": "bcr-latn", + "region": 6 + }, { + "name": "Badyara", + "id": "pbp", + "region": 2 + }, { + "name": "Bafanji", + "id": "bfj", + "region": 2 + }, { + "name": "Bafaw-Balong", + "id": "bwt", + "region": 2 + }, { + "name": "Bafia", + "id": "ksf", + "region": 2 + }, { + "name": "Bafut", + "id": "bfd", + "region": 2 + }, { + "name": "Bagirmi", + "id": "bmi", + "region": 2 + }, { + "name": "Bahamas Creole English", + "id": "bah", + "region": 6 + }, { + "name": "Bahing", + "id": "bhj", + "region": 3 + }, { + "name": "Bahnar (Latin)", + "id": "bdq-latn", + "region": 3 + }, { + "name": "Baka (Latin)", + "id": "bdh-latn", + "region": 2 + }, { + "name": "Baka", + "id": "bkc", + "region": 2 + }, { + "name": "Bakaka", + "id": "bqz", + "region": 2 + }, { + "name": "Bakoko", + "id": "bkh", + "region": 2 + }, { + "name": "Bakole", + "id": "kme", + "region": 2 + }, { + "name": "Balangao (Latin)", + "id": "blw-latn", + "region": 3 + }, { + "name": "Balanta-Ganja", + "id": "bjt", + "region": 2 + }, { + "name": "Balanta-Ganja (Latin)", + "id": "bjt-latn", + "region": 2 + }, { + "name": "Balanta-Kentohe", + "id": "ble", + "region": 2 + }, { + "name": "Baldemu", + "id": "bdn", + "region": 2 + }, { + "name": "Bali (Balinese)", + "id": "ban-bali", + "region": 3 + }, { + "name": "Bali (Latin)", + "id": "ban-latn", + "region": 3 + }, { + "name": "Bali (Latin)", + "id": "bcp-latn", + "region": 2 + }, { + "name": "Balkan Gagauz Turkish (Latin)", + "id": "bgx-latn", + "region": 3 + }, { + "name": "Balo", + "id": "bqo", + "region": 2 + }, { + "name": "Balochi, Eastern (Arabic)", + "id": "bgp-arab", + "region": 3 + }, { + "name": "Balti", + "id": "bft", + "region": 3 + }, { + "name": "Balti (Arabic, Pakistan)", + "id": "bft-arab-pk", + "region": 3 + }, { + "name": "Balti (Tibetan)", + "id": "bft-tibt", + "region": 3 + }, { + "name": "Baluchi", + "id": "bal", + "region": 1 + }, { + "name": "Baluchi (Latin)", + "id": "bal-latn", + "region": 1 + }, { + "name": "Bamali", + "id": "bbq", + "region": 2 + }, { + "name": "Bamanankan (Latin)", + "id": "bm-latn", + "region": 2 + }, { + "name": "Bamanankan (N\u2019Ko)", + "id": "bm-nkoo", + "region": 2 + }, { + "name": "Bambalang", + "id": "bmo", + "region": 2 + }, { + "name": "Bambili-Bambui", + "id": "baw", + "region": 2 + }, { + "name": "Bamenyam", + "id": "bce", + "region": 2 + }, { + "name": "Bamukumbit", + "id": "bqt", + "region": 2 + }, { + "name": "Bamun", + "id": "bax", + "region": 2 + }, { + "name": "Bamunka", + "id": "bvm", + "region": 2 + }, { + "name": "Bana", + "id": "bcw", + "region": 2 + }, { + "name": "Bandi (Latin)", + "id": "bza-latn", + "region": 2 + }, { + "name": "Bangala (Latin)", + "id": "bxg-latn", + "region": 2 + }, { + "name": "Bangandu", + "id": "bgf", + "region": 2 + }, { + "name": "Bangba (Latin)", + "id": "bbe-latn", + "region": 2 + }, { + "name": "Bangime (Latin)", + "id": "dba-latn", + "region": 2 + }, { + "name": "Bangolan", + "id": "bgj", + "region": 2 + }, { + "name": "Bangon (Latin)", + "id": "bnj-latn", + "region": 3 + }, { + "name": "Bangubangu (Latin)", + "id": "bnx-latn", + "region": 2 + }, { + "name": "Banjar (Latin)", + "id": "bjn-latn", + "region": 3 + }, { + "name": "Bankon", + "id": "abb", + "region": 2 + }, { + "name": "Bantawa", + "id": "bap", + "region": 3 + }, { + "name": "Bantoanon (Latin)", + "id": "bno-latn", + "region": 3 + }, { + "name": "Barambu (Latin)", + "id": "brm-latn", + "region": 2 + }, { + "name": "Barein", + "id": "bva", + "region": 2 + }, { + "name": "Bari (Latin)", + "id": "bfa-latn", + "region": 2 + }, { + "name": "Bariai", + "id": "bch", + "region": 7 + }, { + "name": "Barombi", + "id": "bbi", + "region": 2 + }, { + "name": "Barzani-Sandu Jewish Neo-Aramaic (Syriac)", + "id": "bjf-syrc", + "region": 3 + }, { + "name": "Basa (Latin)", + "id": "bzw-latn", + "region": 2 + }, { + "name": "Basaa", + "id": "bas", + "region": 2 + }, { + "name": "Bashkort (Cyrillic)", + "id": "ba-cyrl", + "region": 4 + }, { + "name": "Basketo (Ethiopic)", + "id": "bst-ethi", + "region": 2 + }, { + "name": "Basketo (Latin)", + "id": "bst-latn", + "region": 2 + }, { + "name": "Basque", + "id": "eu", + "region": 4 + }, { + "name": "Bassa (Latin)", + "id": "bsq-latn", + "region": 2 + }, { + "name": "Bassossi", + "id": "bsi", + "region": 2 + }, { + "name": "Bata", + "id": "bta", + "region": 2 + }, { + "name": "Batak (Latin)", + "id": "bya-latn", + "region": 3 + }, { + "name": "Batak Dairi", + "id": "btd", + "region": 3 + }, { + "name": "Batak Dairi (Latin)", + "id": "btd-latn", + "region": 3 + }, { + "name": "Batak Karo (Batak)", + "id": "btx-batk", + "region": 3 + }, { + "name": "Batak Karo (Latin)", + "id": "btx-latn", + "region": 3 + }, { + "name": "Batak Mandailing", + "id": "btm", + "region": 3 + }, { + "name": "Batak Simalungun (Batak)", + "id": "bts-batk", + "region": 3 + }, { + "name": "Batak Toba (Batak)", + "id": "bbc-batk", + "region": 3 + }, { + "name": "Batak Toba (Latin)", + "id": "bbc-latn", + "region": 3 + }, { + "name": "Batanga", + "id": "bnm", + "region": 2 + }, { + "name": "Bati", + "id": "btc", + "region": 2 + }, { + "name": "Bavarian", + "id": "bar", + "region": 4 + }, { + "name": "Bayot", + "id": "bda", + "region": 2 + }, { + "name": "Beaver", + "id": "bea", + "region": 6 + }, { + "name": "Beba", + "id": "bfp", + "region": 2 + }, { + "name": "Bebele", + "id": "beb", + "region": 2 + }, { + "name": "Bebil", + "id": "bxp", + "region": 2 + }, { + "name": "Bedjond", + "id": "bjv", + "region": 2 + }, { + "name": "Beeke (Latin)", + "id": "bkf-latn", + "region": 2 + }, { + "name": "Beezen", + "id": "bnz", + "region": 2 + }, { + "name": "Befang", + "id": "bby", + "region": 2 + }, { + "name": "Bekwel", + "id": "bkw", + "region": 2 + }, { + "name": "Belarusian", + "id": "be", + "region": 4 + }, { + "name": "Belarusian (Braille)", + "id": "be-brai", + "region": 4 + }, { + "name": "Belize Kriol English", + "id": "bzj", + "region": 6 + }, { + "name": "Bella Coola (Latin)", + "id": "blc-latn", + "region": 6 + }, { + "name": "Bemba (Braille)", + "id": "bem-brai", + "region": 2 + }, { + "name": "Bembe (Latin)", + "id": "bmb-latn", + "region": 2 + }, { + "name": "Bena (Latin)", + "id": "bez-latn", + "region": 2 + }, { + "name": "Bench (Ethiopic)", + "id": "bcq-ethi", + "region": 2 + }, { + "name": "Bench (Latin)", + "id": "bcq-latn", + "region": 2 + }, { + "name": "Bende (Latin)", + "id": "bdp-latn", + "region": 2 + }, { + "name": "Bendi (Latin)", + "id": "bct-latn", + "region": 2 + }, { + "name": "Bengali", + "id": "bn", + "region": 3 + }, { + "name": "Bengali (Braille)", + "id": "bn-brai", + "region": 3 + }, { + "name": "Bengali (Latin)", + "id": "bn-latn", + "region": 3 + }, { + "name": "Berakou", + "id": "bxv", + "region": 2 + }, { + "name": "Berom (Latin)", + "id": "bom-latn", + "region": 2 + }, { + "name": "Berta (Latin)", + "id": "wti-latn", + "region": 2 + }, { + "name": "Besme", + "id": "bes", + "region": 2 + }, { + "name": "bh-deva (Devanagari)", + "id": "bh-deva", + "region": 1 + }, { + "name": "Bhele (Latin)", + "id": "bhy-latn", + "region": 2 + }, { + "name": "bhk-latn (Latin)", + "id": "bhk-latn", + "region": 1 + }, { + "name": "Bhojpuri (Devanagari)", + "id": "bho-deva", + "region": 3 + }, { + "name": "Biali (Latin)", + "id": "beh-latn", + "region": 2 + }, { + "name": "Bidiyo", + "id": "bid", + "region": 2 + }, { + "name": "Bikol (Braille)", + "id": "bik-brai", + "region": 1 + }, { + "name": "Bikol (Latin)", + "id": "bik-latn", + "region": 1 + }, { + "name": "Bikol, Buhi\u2019non (Latin)", + "id": "ubl-latn", + "region": 3 + }, { + "name": "Bikol, Central (Braille)", + "id": "bcl-brai", + "region": 3 + }, { + "name": "Bikol, Central (Latin)", + "id": "bcl-latn", + "region": 3 + }, { + "name": "Bikol, Rinconada (Latin)", + "id": "bto-latn", + "region": 3 + }, { + "name": "Bikol, West Albay (Latin)", + "id": "fbl-latn", + "region": 3 + }, { + "name": "Bikya", + "id": "byb", + "region": 2 + }, { + "name": "Bila (Latin)", + "id": "bip-latn", + "region": 2 + }, { + "name": "Bilen (Ethiopic)", + "id": "byn-ethi", + "region": 2 + }, { + "name": "Binukid (Latin)", + "id": "bkd-latn", + "region": 3 + }, { + "name": "Bira (Latin)", + "id": "brf-latn", + "region": 2 + }, { + "name": "Birgit", + "id": "btf", + "region": 2 + }, { + "name": "Birifor, Malba (Latin)", + "id": "bfo-latn", + "region": 2 + }, { + "name": "Bisa (Latin)", + "id": "bib-latn", + "region": 2 + }, { + "name": "Bis\u00e3 (Latin)", + "id": "bqp-latn", + "region": 2 + }, { + "name": "Bishuo", + "id": "bwh", + "region": 2 + }, { + "name": "Bislama", + "id": "bi", + "region": 7 + }, { + "name": "Bislama (Latin)", + "id": "bi-latn", + "region": 7 + }, { + "name": "Bitare", + "id": "brt", + "region": 2 + }, { + "name": "Blaan, Koronadal (Latin)", + "id": "bpr-latn", + "region": 3 + }, { + "name": "Blaan, Sarangani (Latin)", + "id": "bps-latn", + "region": 3 + }, { + "name": "Blackfoot (Latin)", + "id": "bla-latn", + "region": 6 + }, { + "name": "Blang (Tai Le)", + "id": "blr-tale", + "region": 3 + }, { + "name": "Bobo Madar\u00e9, Southern (Latin)", + "id": "bwq-latn", + "region": 2 + }, { + "name": "Bodo Parja (Oriya)", + "id": "bdv-orya", + "region": 3 + }, { + "name": "Bogkalot (Latin)", + "id": "ilk-latn", + "region": 3 + }, { + "name": "Boguru (Latin)", + "id": "bqu-latn", + "region": 2 + }, { + "name": "Bohtan Neo-Aramaic (Syriac)", + "id": "bhn-syrc", + "region": 3 + }, { + "name": "Boko (Latin)", + "id": "bqc-latn", + "region": 2 + }, { + "name": "Bokobaru (Latin)", + "id": "bus-latn", + "region": 2 + }, { + "name": "Bokyi", + "id": "bky", + "region": 2 + }, { + "name": "Bolgo", + "id": "bvo", + "region": 2 + }, { + "name": "Bolinao (Latin)", + "id": "smk-latn", + "region": 3 + }, { + "name": "Bolon (Latin)", + "id": "bof-latn", + "region": 2 + }, { + "name": "Bom-Kim (Latin)", + "id": "bmf-latn", + "region": 2 + }, { + "name": "Bomu (Latin)", + "id": "bmq-latn", + "region": 2 + }, { + "name": "Bomwali", + "id": "bmw", + "region": 2 + }, { + "name": "Bon Gula", + "id": "glc", + "region": 2 + }, { + "name": "Bondei (Latin)", + "id": "bou-latn", + "region": 2 + }, { + "name": "Bondo (Oriya)", + "id": "bfw-orya", + "region": 3 + }, { + "name": "Bonkeng", + "id": "bvg", + "region": 2 + }, { + "name": "Bontok (Latin)", + "id": "bnc-latn", + "region": 1 + }, { + "name": "Bontok, Central (Latin)", + "id": "lbk-latn", + "region": 3 + }, { + "name": "Bontok, Eastern (Latin)", + "id": "ebk-latn", + "region": 3 + }, { + "name": "Boor", + "id": "bvf", + "region": 2 + }, { + "name": "Borna (Latin)", + "id": "bwo-latn", + "region": 2 + }, { + "name": "Bosnian", + "id": "bs", + "region": 4 + }, { + "name": "Bosnian (Cyrillic)", + "id": "bs-cyrl", + "region": 4 + }, { + "name": "Bote", + "id": "bmj", + "region": 3 + }, { + "name": "Bozo, Jenaama (Latin)", + "id": "bze-latn", + "region": 2 + }, { + "name": "Bozo, Kelengaxo (Latin)", + "id": "bzx-latn", + "region": 2 + }, { + "name": "Bozo, Tiemac\u00e8w\u00e8 (Latin)", + "id": "boo-latn", + "region": 2 + }, { + "name": "Bozo, Tieyaxo (Latin)", + "id": "boz-latn", + "region": 2 + }, { + "name": "Brahui", + "id": "brh", + "region": 3 + }, { + "name": "Brahui (Arabic)", + "id": "brh-arab", + "region": 3 + }, { + "name": "Brahui (Latin)", + "id": "brh-latn", + "region": 3 + }, { + "name": "Braj Bhasha (Devanagari)", + "id": "bra-deva", + "region": 3 + }, { + "name": "Breton", + "id": "br", + "region": 4 + }, { + "name": "Bru, Eastern (Latin)", + "id": "bru-latn", + "region": 3 + }, { + "name": "Brunei (Latin)", + "id": "kxd-latn", + "region": 3 + }, { + "name": "btk-batk (Batak)", + "id": "btk-batk", + "region": 1 + }, { + "name": "Bua", + "id": "bub", + "region": 2 + }, { + "name": "Buamu (Latin)", + "id": "box-latn", + "region": 2 + }, { + "name": "Bubia", + "id": "bbx", + "region": 2 + }, { + "name": "Budu (Latin)", + "id": "buu-latn", + "region": 2 + }, { + "name": "Buduma", + "id": "bdm", + "region": 2 + }, { + "name": "Bugis (Buginese)", + "id": "bug-bugi", + "region": 3 + }, { + "name": "Bugis (Latin)", + "id": "bug-latn", + "region": 3 + }, { + "name": "Buhid (Hanunoo)", + "id": "bku-hano", + "region": 3 + }, { + "name": "Buhid (Latin)", + "id": "bku-latn", + "region": 3 + }, { + "name": "Bulgarian", + "id": "bg", + "region": 4 + }, { + "name": "Bulgarian (Braille)", + "id": "bg-brai", + "region": 4 + }, { + "name": "Bulgarian (Latin)", + "id": "bg-latn", + "region": 4 + }, { + "name": "Bullom So (Latin)", + "id": "buy-latn", + "region": 2 + }, { + "name": "Bulu", + "id": "bum", + "region": 2 + }, { + "name": "Bum", + "id": "bmv", + "region": 2 + }, { + "name": "Bumthangkha (Tibetan)", + "id": "kjz-tibt", + "region": 3 + }, { + "name": "Bung", + "id": "bqd", + "region": 2 + }, { + "name": "Bungu (Latin)", + "id": "wun-latn", + "region": 2 + }, { + "name": "Burak (Latin)", + "id": "bys-latn", + "region": 2 + }, { + "name": "Bura-Pabir (Latin)", + "id": "bwr-latn", + "region": 2 + }, { + "name": "Burmese", + "id": "my", + "region": 3 + }, { + "name": "Burmese (Braille)", + "id": "my-brai", + "region": 3 + }, { + "name": "Burushaski (Arabic)", + "id": "bsk-arab", + "region": 3 + }, { + "name": "Burushaski (Latin)", + "id": "bsk-latn", + "region": 3 + }, { + "name": "Busam", + "id": "bxs", + "region": 2 + }, { + "name": "Buso", + "id": "bso", + "region": 2 + }, { + "name": "Busuu", + "id": "bju", + "region": 2 + }, { + "name": "Buwal", + "id": "bhs", + "region": 2 + }, { + "name": "Buyu (Latin)", + "id": "byi-latn", + "region": 2 + }, { + "name": "Bwa (Latin)", + "id": "bww-latn", + "region": 2 + }, { + "name": "Bwamu, Cwi (Latin)", + "id": "bwy-latn", + "region": 2 + }, { + "name": "Bwamu, L\u00e1\u00e1 L\u00e1\u00e1 (Latin)", + "id": "bwj-latn", + "region": 2 + }, { + "name": "Bwela (Latin)", + "id": "bwl-latn", + "region": 2 + }, { + "name": "Byep", + "id": "mkk", + "region": 2 + }, { + "name": "Cab\u00e9car (Latin)", + "id": "cjp-latn", + "region": 6 + }, { + "name": "Caka", + "id": "ckx", + "region": 2 + }, { + "name": "Cakfem-Mushere (Latin)", + "id": "cky-latn", + "region": 2 + }, { + "name": "Caluyanun (Latin)", + "id": "clu-latn", + "region": 3 + }, { + "name": "Capiznon (Latin)", + "id": "cps-latn", + "region": 3 + }, { + "name": "Cappadocian Greek (Greek)", + "id": "cpg-grek", + "region": 4 + }, { + "name": "Carrier, Southern (Unified Canadian Aboriginal Syllabics)", + "id": "caf-cans", + "region": 6 + }, { + "name": "Carrier, Southern (Latin)", + "id": "caf-latn", + "region": 6 + }, { + "name": "Catalan", + "id": "ca", + "region": 4 + }, { + "name": "Cavine\u00f1a", + "id": "cav", + "region": 6 + }, { + "name": "Cayuga (Latin)", + "id": "cay-latn", + "region": 6 + }, { + "name": "Cebuano (Braille)", + "id": "ceb-brai", + "region": 3 + }, { + "name": "Cebuano (Latin)", + "id": "ceb-latn", + "region": 3 + }, { + "name": "Cerma (Latin)", + "id": "cme-latn", + "region": 2 + }, { + "name": "Ch\u00e1cobo", + "id": "cao", + "region": 6 + }, { + "name": "Chadian Sign Language", + "id": "cds", + "region": 2 + }, { + "name": "Chakma (Chakma, Bangladesh)", + "id": "ccp-cakm-bd", + "region": 3 + }, { + "name": "Chaldean Neo-Aramaic (Syriac)", + "id": "cld-syrc", + "region": 3 + }, { + "name": "Chamling", + "id": "rab", + "region": 3 + }, { + "name": "Changthang (Tibetan)", + "id": "cna-tibt", + "region": 3 + }, { + "name": "Chavacano (Braille)", + "id": "cbk-brai", + "region": 3 + }, { + "name": "Chavacano (Latin)", + "id": "cbk-latn", + "region": 3 + }, { + "name": "Chechen (Latin)", + "id": "ce-latn", + "region": 4 + }, { + "name": "Chepang", + "id": "cdm", + "region": 3 + }, { + "name": "Chepang (Devanagari)", + "id": "cdm-deva", + "region": 3 + }, { + "name": "Cherokee (Cherokee)", + "id": "chr-cher", + "region": 6 + }, { + "name": "Cheyenne", + "id": "chy", + "region": 6 + }, { + "name": "Cheyenne (Latin)", + "id": "chy-latn", + "region": 6 + }, { + "name": "Chhattisgarhi (Devanagari)", + "id": "hne-deva", + "region": 3 + }, { + "name": "Chichewa (Braille)", + "id": "ny-brai", + "region": 2 + }, { + "name": "Chiga (Latin)", + "id": "cgg-latn", + "region": 2 + }, { + "name": "Chilcotin (Latin)", + "id": "clc-latn", + "region": 6 + }, { + "name": "Chinese (Braille)", + "id": "zh-brai", + "region": 1 + }, { + "name": "Chinese (Phags-pa, China)", + "id": "zh-phag-cn", + "region": 1 + }, { + "name": "Chinese, Mandarin (Braille)", + "id": "cmn-brai", + "region": 3 + }, { + "name": "Chinese, Mandarin (Han (Simplified variant))", + "id": "cmn-hans", + "region": 3 + }, { + "name": "Chinese, Min Nan (Latin, Taiwan, Province of China)", + "id": "nan-latn-tw", + "region": 3 + }, { + "name": "Chinese, Yue (Braille)", + "id": "yue-brai", + "region": 3 + }, { + "name": "Chinook Wawa", + "id": "chn", + "region": 6 + }, { + "name": "Chipaya", + "id": "cap", + "region": 6 + }, { + "name": "Chiquitano", + "id": "cax", + "region": 6 + }, { + "name": "Choctaw", + "id": "cho", + "region": 6 + }, { + "name": "Cimbrian", + "id": "cim", + "region": 4 + }, { + "name": "Clallam (Latin)", + "id": "clm-latn", + "region": 6 + }, { + "name": "Classical Nahuatl", + "id": "nci", + "region": 1 + }, { + "name": "Classical Quechua", + "id": "qwc", + "region": 1 + }, { + "name": "Classical Tibetan (Tibetan)", + "id": "xct-tibt", + "region": 1 + }, { + "name": "Comox (Latin)", + "id": "coo-latn", + "region": 6 + }, { + "name": "Cook Islands Maori (Latin)", + "id": "rar-latn", + "region": 7 + }, { + "name": "Coos", + "id": "csz", + "region": 6 + }, { + "name": "Coptic (Coptic)", + "id": "cop-copt", + "region": 2 + }, { + "name": "Coptic (Greek)", + "id": "cop-grek", + "region": 2 + }, { + "name": "Cornish", + "id": "kw", + "region": 4 + }, { + "name": "Corsican", + "id": "co", + "region": 4 + }, { + "name": "Cree", + "id": "cr", + "region": 1 + }, { + "name": "Cree (Latin)", + "id": "cr-latn", + "region": 1 + }, { + "name": "Cree, Moose (Unified Canadian Aboriginal Syllabics)", + "id": "crm-cans", + "region": 6 + }, { + "name": "Cree, Northern East", + "id": "crl", + "region": 6 + }, { + "name": "Cree, Northern East (Unified Canadian Aboriginal Syllabics)", + "id": "crl-cans", + "region": 6 + }, { + "name": "Cree, Northern East (Latin)", + "id": "crl-latn", + "region": 6 + }, { + "name": "Cree, Plains (Unified Canadian Aboriginal Syllabics)", + "id": "crk-cans", + "region": 6 + }, { + "name": "Cree, Plains (Latin)", + "id": "crk-latn", + "region": 6 + }, { + "name": "Cree, Southern East", + "id": "crj", + "region": 6 + }, { + "name": "Cree, Swampy", + "id": "csw", + "region": 6 + }, { + "name": "Cree, Swampy (Unified Canadian Aboriginal Syllabics)", + "id": "csw-cans", + "region": 6 + }, { + "name": "Crimean Tatar (Latin)", + "id": "crh-latn", + "region": 4 + }, { + "name": "Croatian", + "id": "hr", + "region": 4 + }, { + "name": "Croatian (Braille)", + "id": "hr-brai", + "region": 4 + }, { + "name": "cug", + "id": "cug", + "region": 1 + }, { + "name": "Cuvok", + "id": "cuv", + "region": 2 + }, { + "name": "Cuyonon (Latin)", + "id": "cyo-latn", + "region": 3 + }, { + "name": "Czech", + "id": "cs", + "region": 4 + }, { + "name": "Czech (Braille)", + "id": "cs-brai", + "region": 4 + }, { + "name": "Daba", + "id": "dbq", + "region": 2 + }, { + "name": "Dagaare, Southern (Latin)", + "id": "dga-latn", + "region": 2 + }, { + "name": "Dagaari Dioula (Latin)", + "id": "dgd-latn", + "region": 2 + }, { + "name": "Dagara, Northern (Latin)", + "id": "dgi-latn", + "region": 2 + }, { + "name": "Dagba", + "id": "dgk", + "region": 2 + }, { + "name": "Dagbani", + "id": "dag", + "region": 2 + }, { + "name": "Daju, Dar Daju", + "id": "djc", + "region": 2 + }, { + "name": "Daju, Dar Sila", + "id": "dau", + "region": 2 + }, { + "name": "Dakota (Latin)", + "id": "dak-latn", + "region": 6 + }, { + "name": "Dama", + "id": "dmm", + "region": 2 + }, { + "name": "Dameli (Arabic)", + "id": "dml-arab", + "region": 3 + }, { + "name": "Dan (Latin, Liberia)", + "id": "dnj-latn-lr", + "region": 2 + }, { + "name": "Dangal\u00e9at", + "id": "daa", + "region": 2 + }, { + "name": "Dangme (Latin)", + "id": "ada-latn", + "region": 2 + }, { + "name": "Danish", + "id": "da", + "region": 4 + }, { + "name": "Danish (Braille)", + "id": "da-brai", + "region": 4 + }, { + "name": "Danish (Runic)", + "id": "da-runr", + "region": 4 + }, { + "name": "Darai", + "id": "dry", + "region": 3 + }, { + "name": "Dari", + "id": "prs", + "region": 3 + }, { + "name": "Day", + "id": "dai", + "region": 2 + }, { + "name": "Dazaga", + "id": "dzg", + "region": 2 + }, { + "name": "Dek", + "id": "dek", + "region": 2 + }, { + "name": "Dene", + "id": "chp", + "region": 6 + }, { + "name": "Dene (Unified Canadian Aboriginal Syllabics)", + "id": "chp-cans", + "region": 6 + }, { + "name": "Dene (Latin)", + "id": "chp-latn", + "region": 6 + }, { + "name": "Denya", + "id": "anv", + "region": 2 + }, { + "name": "Desiya (Oriya)", + "id": "dso-orya", + "region": 3 + }, { + "name": "Dewas Rai", + "id": "dwz", + "region": 3 + }, { + "name": "Dewoin (Latin)", + "id": "dee-latn", + "region": 2 + }, { + "name": "Dhaiso (Latin)", + "id": "dhs-latn", + "region": 2 + }, { + "name": "Dhangu-Djangu (Latin)", + "id": "dhg-latn", + "region": 7 + }, { + "name": "Dhatki (Arabic)", + "id": "mki-arab", + "region": 3 + }, { + "name": "Dhimal", + "id": "dhi", + "region": 3 + }, { + "name": "Dhuwal (Latin)", + "id": "dwu-latn", + "region": 7 + }, { + "name": "Dii", + "id": "dur", + "region": 2 + }, { + "name": "Dimbong", + "id": "dii", + "region": 2 + }, { + "name": "Dinka (Latin)", + "id": "din-latn", + "region": 1 + }, { + "name": "Dinka, Northeastern (Latin)", + "id": "dip-latn", + "region": 2 + }, { + "name": "Dinka, Northwestern (Latin)", + "id": "diw-latn", + "region": 2 + }, { + "name": "Dinka, South Central (Latin)", + "id": "dib-latn", + "region": 2 + }, { + "name": "Dinka, Southeastern (Latin)", + "id": "dks-latn", + "region": 2 + }, { + "name": "Dinka, Southwestern (Latin)", + "id": "dik-latn", + "region": 2 + }, { + "name": "Dirasha (Ethiopic)", + "id": "gdl-ethi", + "region": 2 + }, { + "name": "Disa", + "id": "dsi", + "region": 2 + }, { + "name": "Ditidaht (Latin)", + "id": "dtd-latn", + "region": 6 + }, { + "name": "Dizin", + "id": "mdx", + "region": 2 + }, { + "name": "Dizin (Ethiopic)", + "id": "mdx-ethi", + "region": 2 + }, { + "name": "Dizin (Latin)", + "id": "mdx-latn", + "region": 2 + }, { + "name": "Doe (Latin)", + "id": "doe-latn", + "region": 2 + }, { + "name": "Dogon, Bondum Dom (Latin)", + "id": "dbu-latn", + "region": 2 + }, { + "name": "Dogon, Donno So (Latin)", + "id": "dds-latn", + "region": 2 + }, { + "name": "Dogon, Jamsay (Latin)", + "id": "djm-latn", + "region": 2 + }, { + "name": "Dogon, Tene Kan (Latin)", + "id": "dtk-latn", + "region": 2 + }, { + "name": "Dogon, Tomo Kan (Latin)", + "id": "dtm-latn", + "region": 2 + }, { + "name": "Dogon, Toro So (Latin)", + "id": "dts-latn", + "region": 2 + }, { + "name": "Dogon, Toro Tegu (Latin)", + "id": "dtt-latn", + "region": 2 + }, { + "name": "Dogoso (Latin)", + "id": "dgs-latn", + "region": 2 + }, { + "name": "Dogri (Arabic, Pakistan)", + "id": "dgo-arab-pk", + "region": 3 + }, { + "name": "Dogri (macrolanguage) (Dogra)", + "id": "doi-dogr", + "region": 1 + }, { + "name": "Dolpo (Tibetan)", + "id": "dre-tibt", + "region": 3 + }, { + "name": "Dongo (Latin)", + "id": "doo-latn", + "region": 2 + }, { + "name": "Dotyali", + "id": "dty", + "region": 3 + }, { + "name": "Doyayo", + "id": "dow", + "region": 2 + }, { + "name": "Drents", + "id": "drt", + "region": 4 + }, { + "name": "Duala", + "id": "dua", + "region": 2 + }, { + "name": "Dugun", + "id": "ndu", + "region": 2 + }, { + "name": "Dugwor", + "id": "dme", + "region": 2 + }, { + "name": "Duli-Gey", + "id": "duz", + "region": 2 + }, { + "name": "Duruwa (Oriya)", + "id": "pci-orya", + "region": 3 + }, { + "name": "Dutch", + "id": "nl", + "region": 4 + }, { + "name": "Dutch (Braille)", + "id": "nl-brai", + "region": 4 + }, { + "name": "Duungooma (Latin)", + "id": "dux-latn", + "region": 2 + }, { + "name": "Duupa", + "id": "dae", + "region": 2 + }, { + "name": "Duya", + "id": "ldb", + "region": 2 + }, { + "name": "Duya (Latin)", + "id": "ldb-latn", + "region": 2 + }, { + "name": "Dyan (Latin)", + "id": "dya-latn", + "region": 2 + }, { + "name": "Dza (Latin)", + "id": "jen-latn", + "region": 2 + }, { + "name": "Dzongkha", + "id": "dz", + "region": 3 + }, { + "name": "Dzongkha (Bhutan)", + "id": "dz-bt", + "region": 3 + }, { + "name": "Dz\u00f9\u00f9ngoo (Latin)", + "id": "dnn-latn", + "region": 2 + }, { + "name": "Eastern Franconian", + "id": "vmf", + "region": 4 + }, { + "name": "Ebira (Latin)", + "id": "igb-latn", + "region": 2 + }, { + "name": "Edo", + "id": "bin", + "region": 2 + }, { + "name": "Edo (Latin)", + "id": "bin-latn", + "region": 2 + }, { + "name": "Efe (Latin)", + "id": "efe-latn", + "region": 2 + }, { + "name": "Efik", + "id": "efi", + "region": 2 + }, { + "name": "Efik (Latin)", + "id": "efi-latn", + "region": 2 + }, { + "name": "Egyptian (Ancient) (Egyptian hieroglyphs)", + "id": "egy-egyp", + "region": 1 + }, { + "name": "Ejagham", + "id": "etu", + "region": 2 + }, { + "name": "Elip", + "id": "ekm", + "region": 2 + }, { + "name": "Eman", + "id": "emn", + "region": 2 + }, { + "name": "Ember\u00e1, Northern (Latin)", + "id": "emp-latn", + "region": 6 + }, { + "name": "Emilian", + "id": "egl", + "region": 4 + }, { + "name": "Enga (Latin)", + "id": "enq-latn", + "region": 7 + }, { + "name": "Engenni (Latin)", + "id": "enn-latn", + "region": 2 + }, { + "name": "Enggano", + "id": "eno", + "region": 3 + }, { + "name": "English", + "id": "en", + "region": 4 + }, { + "name": "English (Australia)", + "id": "en-au", + "region": 4 + }, { + "name": "English (Braille)", + "id": "en-brai", + "region": 4 + }, { + "name": "English", + "id": "en-qp", + "region": 4 + }, { + "name": "English (Runic)", + "id": "en-runr", + "region": 4 + }, { + "name": "English (Shavian)", + "id": "en-shaw", + "region": 4 + }, { + "name": "English (Shavian)", + "id": "en-shaw-uk", + "region": 4 + }, { + "name": "English (Shavian, United States)", + "id": "en-shaw-us", + "region": 4 + }, { + "name": "Enya (Latin)", + "id": "gey-latn", + "region": 2 + }, { + "name": "Ese Ejja", + "id": "ese", + "region": 6 + }, { + "name": "Esimbi", + "id": "ags", + "region": 2 + }, { + "name": "Esperanto", + "id": "eo", + "region": 4 + }, { + "name": "Estonian", + "id": "et", + "region": 1 + }, { + "name": "Estonian (Braille)", + "id": "et-brai", + "region": 1 + }, { + "name": "Eton", + "id": "eto", + "region": 2 + }, { + "name": "Evant", + "id": "bzz", + "region": 2 + }, { + "name": "\u00c9w\u00e9 (Braille)", + "id": "ee-brai", + "region": 2 + }, { + "name": "\u00c9w\u00e9 (Latin)", + "id": "ee-latn", + "region": 2 + }, { + "name": "Ewondo", + "id": "ewo", + "region": 2 + }, { + "name": "Extremaduran", + "id": "ext", + "region": 4 + }, { + "name": "Fala", + "id": "fax", + "region": 4 + }, { + "name": "Fali, North", + "id": "fll", + "region": 2 + }, { + "name": "Fali, South", + "id": "fal", + "region": 2 + }, { + "name": "Fang", + "id": "fak", + "region": 2 + }, { + "name": "Fang", + "id": "fan", + "region": 2 + }, { + "name": "Fania", + "id": "fni", + "region": 2 + }, { + "name": "Faroese", + "id": "fo", + "region": 4 + }, { + "name": "Faroese (Runic)", + "id": "fo-runr", + "region": 4 + }, { + "name": "Fe\u2019fe\u2019", + "id": "fmp", + "region": 2 + }, { + "name": "Fijian", + "id": "fj", + "region": 7 + }, { + "name": "Filipino", + "id": "fil", + "region": 3 + }, { + "name": "Filipino (Latin)", + "id": "fil-latn", + "region": 3 + }, { + "name": "Finnish", + "id": "fi", + "region": 4 + }, { + "name": "Finnish (Braille)", + "id": "fi-brai", + "region": 4 + }, { + "name": "Finnish, Kven", + "id": "fkv", + "region": 4 + }, { + "name": "Finnish, Tornedalen", + "id": "fit", + "region": 4 + }, { + "name": "Finongan (Latin)", + "id": "fag-latn", + "region": 7 + }, { + "name": "Fipa (Latin)", + "id": "fip-latn", + "region": 2 + }, { + "name": "Flaaitaal", + "id": "fly", + "region": 2 + }, { + "name": "Foma (Latin)", + "id": "fom-latn", + "region": 2 + }, { + "name": "Fon (Latin)", + "id": "fon-latn", + "region": 2 + }, { + "name": "Fongoro", + "id": "fgr", + "region": 2 + }, { + "name": "Frankish", + "id": "frk", + "region": 1 + }, { + "name": "French", + "id": "fr", + "region": 4 + }, { + "name": "French (Braille)", + "id": "fr-brai", + "region": 4 + }, { + "name": "French (Canada)", + "id": "fr-ca", + "region": 4 + }, { + "name": "French (Switzerland)", + "id": "fr-ch", + "region": 4 + }, { + "name": "French, Cajun", + "id": "frc", + "region": 6 + }, { + "name": "Frisian", + "id": "fy", + "region": 4 + }, { + "name": "Frisian, Northern", + "id": "frr", + "region": 4 + }, { + "name": "Friulian", + "id": "fur", + "region": 4 + }, { + "name": "Fulah", + "id": "ff", + "region": 1 + }, { + "name": "Fulah (Adlam)", + "id": "ff-adlm", + "region": 1 + }, { + "name": "Fulah (Arabic)", + "id": "ff-arab", + "region": 1 + }, { + "name": "Fulah (Latin)", + "id": "ff-latn", + "region": 1 + }, { + "name": "Fulfulde, Adamawa", + "id": "fub", + "region": 2 + }, { + "name": "Fulfulde, Adamawa (Arabic)", + "id": "fub-arab", + "region": 2 + }, { + "name": "Fulfulde, Adamawa (Latin)", + "id": "fub-latn", + "region": 2 + }, { + "name": "Fulfulde, Bagirmi", + "id": "fui", + "region": 2 + }, { + "name": "Fulfulde, Borgu", + "id": "fue", + "region": 2 + }, { + "name": "Fulfulde, Borgu (Latin)", + "id": "fue-latn", + "region": 2 + }, { + "name": "Fulfulde, Central-Eastern Niger", + "id": "fuq", + "region": 2 + }, { + "name": "Fulfulde, Maasina", + "id": "ffm", + "region": 2 + }, { + "name": "Fulfulde, Maasina (Arabic)", + "id": "ffm-arab", + "region": 2 + }, { + "name": "Fulfulde, Maasina (Latin)", + "id": "ffm-latn", + "region": 2 + }, { + "name": "Fulfulde, Nigerian", + "id": "fuv", + "region": 2 + }, { + "name": "Fulfulde, Nigerian (Arabic)", + "id": "fuv-arab", + "region": 2 + }, { + "name": "Fulfulde, Nigerian (Latin)", + "id": "fuv-latn", + "region": 2 + }, { + "name": "Fulfulde, Western Niger (Arabic)", + "id": "fuh-arab", + "region": 2 + }, { + "name": "Fulfulde, Western Niger (Latin)", + "id": "fuh-latn", + "region": 2 + }, { + "name": "Fuliiru (Latin)", + "id": "flr-latn", + "region": 2 + }, { + "name": "Fur", + "id": "fvr", + "region": 2 + }, { + "name": "Ga\u2019dang (Latin)", + "id": "gdg-latn", + "region": 3 + }, { + "name": "Gabri", + "id": "gab", + "region": 2 + }, { + "name": "Gadaba, Bodo (Oriya)", + "id": "gbj-orya", + "region": 3 + }, { + "name": "Gadaba, Pottangi Ollar (Oriya)", + "id": "gdb-orya", + "region": 3 + }, { + "name": "Gadang", + "id": "gdk", + "region": 2 + }, { + "name": "Gagauz", + "id": "gag", + "region": 4 + }, { + "name": "Gahri (Tibetan)", + "id": "bfu-tibt", + "region": 3 + }, { + "name": "Galician", + "id": "gl", + "region": 4 + }, { + "name": "Gamo (Latin)", + "id": "gmv-latn", + "region": 2 + }, { + "name": "Ganda (Latin)", + "id": "lg-latn", + "region": 2 + }, { + "name": "G\u0101ndh\u0101r\u012b (Latin)", + "id": "pgd-latn", + "region": 1 + }, { + "name": "Garo (Braille)", + "id": "grt-brai", + "region": 3 + }, { + "name": "Gavar", + "id": "gou", + "region": 2 + }, { + "name": "Gawar-Bati (Arabic)", + "id": "gwt-arab", + "region": 3 + }, { + "name": "Gawri (Arabic)", + "id": "gwc-arab", + "region": 3 + }, { + "name": "Gbaya (Central African Republic)", + "id": "gba", + "region": 1 + }, { + "name": "Gbaya, Northwest", + "id": "gya", + "region": 2 + }, { + "name": "Gbaya-Mbodomo", + "id": "gmm", + "region": 2 + }, { + "name": "Gbii (Latin)", + "id": "ggb-latn", + "region": 2 + }, { + "name": "Gedeo", + "id": "drs", + "region": 2 + }, { + "name": "Geez", + "id": "gez", + "region": 2 + }, { + "name": "Geez (Ethiopic)", + "id": "gez-ethi", + "region": 2 + }, { + "name": "Georgian", + "id": "ka", + "region": 3 + }, { + "name": "Georgian (Braille)", + "id": "ka-brai", + "region": 3 + }, { + "name": "Gepo (Miao)", + "id": "ygp-plrd", + "region": 3 + }, { + "name": "German, Colonia Tovar", + "id": "gct", + "region": 6 + }, { + "name": "German, Pennsylvania", + "id": "pdc", + "region": 6 + }, { + "name": "German, Standard", + "id": "de", + "region": 4 + }, { + "name": "German, Standard (Braille)", + "id": "de-brai", + "region": 4 + }, { + "name": "German, Standard (Runic)", + "id": "de-runr", + "region": 4 + }, { + "name": "German, Swiss", + "id": "gsw", + "region": 4 + }, { + "name": "Ghale, Southern", + "id": "ghe", + "region": 3 + }, { + "name": "Ghom\u00e1l\u00e1\u2019", + "id": "bbj", + "region": 2 + }, { + "name": "Gidar", + "id": "gid", + "region": 2 + }, { + "name": "Gilaki", + "id": "glk", + "region": 3 + }, { + "name": "Gilaki (Latin)", + "id": "glk-latn", + "region": 3 + }, { + "name": "Gimme", + "id": "kmp", + "region": 2 + }, { + "name": "Gimnime", + "id": "gmn", + "region": 2 + }, { + "name": "Gitxsan (Latin)", + "id": "git-latn", + "region": 6 + }, { + "name": "Giziga", + "id": "giz", + "region": 2 + }, { + "name": "Giziga, North", + "id": "gis", + "region": 2 + }, { + "name": "Glaro-Twabo (Latin)", + "id": "glr-latn", + "region": 2 + }, { + "name": "Glavda", + "id": "glw", + "region": 2 + }, { + "name": "Glio-Oubi (Latin)", + "id": "oub-latn", + "region": 2 + }, { + "name": "Goemai (Latin)", + "id": "ank-latn", + "region": 2 + }, { + "name": "Gofa (Latin)", + "id": "gof-latn", + "region": 2 + }, { + "name": "Gogo (Latin)", + "id": "gog-latn", + "region": 2 + }, { + "name": "Gokana (Latin)", + "id": "gkn-latn", + "region": 2 + }, { + "name": "Gola (Latin)", + "id": "gol-latn", + "region": 2 + }, { + "name": "Gondi (Devanagari)", + "id": "gon-deva", + "region": 1 + }, { + "name": "Gondi (Gunjala Gondi)", + "id": "gon-gong", + "region": 1 + }, { + "name": "Gondi (Masaram Gondi)", + "id": "gon-gonm", + "region": 1 + }, { + "name": "Gondi, Adilabad (Devanagari)", + "id": "wsg-deva", + "region": 3 + }, { + "name": "Gondi, Adilabad (Gunjala Gondi)", + "id": "wsg-gong", + "region": 3 + }, { + "name": "Gondi, Adilabad (Telugu)", + "id": "wsg-telu", + "region": 3 + }, { + "name": "Gondi, Aheri (Devanagari)", + "id": "esg-deva", + "region": 3 + }, { + "name": "Gondi, Northern (Devanagari)", + "id": "gno-deva", + "region": 3 + }, { + "name": "Gor", + "id": "gqr", + "region": 2 + }, { + "name": "Gorontalo (Latin)", + "id": "gor-latn", + "region": 3 + }, { + "name": "Gothic (Gothic)", + "id": "got-goth", + "region": 1 + }, { + "name": "Goundo", + "id": "goy", + "region": 2 + }, { + "name": "Gourmanch\u00e9ma (Latin)", + "id": "gux-latn", + "region": 2 + }, { + "name": "Grebo (Latin)", + "id": "grb-latn", + "region": 1 + }, { + "name": "Grebo, Barclayville (Latin)", + "id": "gry-latn", + "region": 2 + }, { + "name": "Grebo, Central (Latin)", + "id": "grv-latn", + "region": 2 + }, { + "name": "Grebo, Gboloo (Latin)", + "id": "gec-latn", + "region": 2 + }, { + "name": "Grebo, Northern (Latin)", + "id": "gbo-latn", + "region": 2 + }, { + "name": "Grebo, Southern (Latin)", + "id": "grj-latn", + "region": 2 + }, { + "name": "Greek", + "id": "el", + "region": 4 + }, { + "name": "Greek (Braille)", + "id": "el-brai", + "region": 4 + }, { + "name": "Greek (Latin)", + "id": "el-latn", + "region": 4 + }, { + "name": "Greek, Ancient (Greek)", + "id": "grc-grek", + "region": 4 + }, { + "name": "Greek, Ancient (Latin)", + "id": "grc-latn", + "region": 4 + }, { + "name": "Greenlandic", + "id": "kl", + "region": 6 + }, { + "name": "grk", + "id": "grk", + "region": 1 + }, { + "name": "Gronings", + "id": "gos", + "region": 4 + }, { + "name": "Guadeloupean Creole French", + "id": "gcf", + "region": 6 + }, { + "name": "Guarani", + "id": "gn", + "region": 1 + }, { + "name": "Guaran\u00ed, Ava", + "id": "nhd", + "region": 6 + }, { + "name": "Guaran\u00ed, Eastern Bolivian", + "id": "gui", + "region": 6 + }, { + "name": "Guaran\u00ed, Mby\u00e1", + "id": "gun", + "region": 6 + }, { + "name": "Guaran\u00ed, Western Bolivian", + "id": "gnw", + "region": 6 + }, { + "name": "Guarayu", + "id": "gyr", + "region": 6 + }, { + "name": "Gude", + "id": "gde", + "region": 2 + }, { + "name": "Guianese Creole French", + "id": "gcr", + "region": 6 + }, { + "name": "Guinea-Bissau Creole", + "id": "pov", + "region": 2 + }, { + "name": "Gujarati", + "id": "gu", + "region": 3 + }, { + "name": "Gujarati (Braille)", + "id": "gu-brai", + "region": 3 + }, { + "name": "Gujari", + "id": "gju", + "region": 3 + }, { + "name": "Gula", + "id": "glu", + "region": 2 + }, { + "name": "Gula Iro", + "id": "glj", + "region": 2 + }, { + "name": "Gulay", + "id": "gvl", + "region": 2 + }, { + "name": "Gumuz (Ethiopic)", + "id": "guk-ethi", + "region": 2 + }, { + "name": "Gumuz (Latin)", + "id": "guk-latn", + "region": 2 + }, { + "name": "Gungu (Latin)", + "id": "rub-latn", + "region": 2 + }, { + "name": "Gurung", + "id": "gvr", + "region": 3 + }, { + "name": "Gusilay", + "id": "gsl", + "region": 2 + }, { + "name": "Guyanese Creole English", + "id": "gyn", + "region": 6 + }, { + "name": "Gvoko", + "id": "ngs", + "region": 2 + }, { + "name": "Gwama (Latin)", + "id": "kmq-latn", + "region": 2 + }, { + "name": "Gweno (Latin)", + "id": "gwe-latn", + "region": 2 + }, { + "name": "Gwere (Latin)", + "id": "gwr-latn", + "region": 2 + }, { + "name": "Gwich\u2019in", + "id": "gwi", + "region": 6 + }, { + "name": "Gwich\u2019in (Latin)", + "id": "gwi-latn", + "region": 6 + }, { + "name": "Gyele", + "id": "gyi", + "region": 2 + }, { + "name": "Ha (Latin)", + "id": "haq-latn", + "region": 2 + }, { + "name": "Hadiyya (Latin)", + "id": "hdy-latn", + "region": 2 + }, { + "name": "Haida, Southern (Latin)", + "id": "hax-latn", + "region": 6 + }, { + "name": "Haisla (Latin)", + "id": "has-latn", + "region": 6 + }, { + "name": "Haitian Creole", + "id": "ht", + "region": 6 + }, { + "name": "Halkomelem (Latin)", + "id": "hur-latn", + "region": 6 + }, { + "name": "Hamba (Latin)", + "id": "hba-latn", + "region": 2 + }, { + "name": "Hamer-Banna", + "id": "amf", + "region": 2 + }, { + "name": "Hamer-Banna (Latin)", + "id": "amf-latn", + "region": 2 + }, { + "name": "Han (Latin)", + "id": "haa-latn", + "region": 6 + }, { + "name": "Hanga (Latin)", + "id": "hag-latn", + "region": 2 + }, { + "name": "Hangaza (Latin)", + "id": "han-latn", + "region": 2 + }, { + "name": "Hanunoo (Hanunoo)", + "id": "hnn-hano", + "region": 3 + }, { + "name": "Hanunoo (Latin)", + "id": "hnn-latn", + "region": 3 + }, { + "name": "Harari", + "id": "har", + "region": 2 + }, { + "name": "Haroi (Latin)", + "id": "hro-latn", + "region": 3 + }, { + "name": "Haryanvi (Devanagari)", + "id": "bgc-deva", + "region": 3 + }, { + "name": "Hassaniyya", + "id": "mey", + "region": 2 + }, { + "name": "Hausa", + "id": "ha", + "region": 2 + }, { + "name": "Hausa (Arabic)", + "id": "ha-arab", + "region": 2 + }, { + "name": "Hausa (Braille)", + "id": "ha-brai", + "region": 2 + }, { + "name": "Hausa (Latin)", + "id": "ha-latn", + "region": 2 + }, { + "name": "Havu (Latin)", + "id": "hav-latn", + "region": 2 + }, { + "name": "Hawai\u2019i Pidgin", + "id": "hwc", + "region": 6 + }, { + "name": "Hawaiian (Latin)", + "id": "haw-latn", + "region": 6 + }, { + "name": "Hawaiian (Latin, United States)", + "id": "haw-latn-us", + "region": 6 + }, { + "name": "Haya (Latin)", + "id": "hay-latn", + "region": 2 + }, { + "name": "Hazaragi (Arabic)", + "id": "haz-arab", + "region": 3 + }, { + "name": "Hdi", + "id": "xed", + "region": 2 + }, { + "name": "Hebrew", + "id": "he", + "region": 3 + }, { + "name": "Hebrew (Braille)", + "id": "he-brai", + "region": 3 + }, { + "name": "Hebrew (Latin)", + "id": "he-latn", + "region": 3 + }, { + "name": "Hebrew, Ancient (Hebrew)", + "id": "hbo-hebr", + "region": 3 + }, { + "name": "Hebrew, Ancient (Latin)", + "id": "hbo-latn", + "region": 3 + }, { + "name": "Hehe (Latin)", + "id": "heh-latn", + "region": 2 + }, { + "name": "Heiltsuk", + "id": "hei", + "region": 6 + }, { + "name": "Hema (Latin)", + "id": "nix-latn", + "region": 2 + }, { + "name": "Hemba (Latin)", + "id": "hem-latn", + "region": 2 + }, { + "name": "Herd\u00e9", + "id": "hed", + "region": 2 + }, { + "name": "H\u00e9rtevin (Syriac)", + "id": "hrt-syrc", + "region": 3 + }, { + "name": "Hiberno-Scottish Gaelic", + "id": "ghc", + "region": 1 + }, { + "name": "Higaonon (Latin)", + "id": "mba-latn", + "region": 3 + }, { + "name": "Hijuk", + "id": "hij", + "region": 2 + }, { + "name": "Hiligaynon (Braille)", + "id": "hil-brai", + "region": 3 + }, { + "name": "Hiligaynon (Latin)", + "id": "hil-latn", + "region": 3 + }, { + "name": "Hindi", + "id": "hi", + "region": 3 + }, { + "name": "Hindi (Braille)", + "id": "hi-brai", + "region": 3 + }, { + "name": "Hindi (Latin)", + "id": "hi-latn", + "region": 3 + }, { + "name": "Hindko, Northern", + "id": "hno", + "region": 3 + }, { + "name": "Hindko, Northern (Arabic)", + "id": "hno-arab", + "region": 3 + }, { + "name": "Hindko, Southern (Arabic)", + "id": "hnd-arab", + "region": 3 + }, { + "name": "Ho (Oriya)", + "id": "hoc-orya", + "region": 3 + }, { + "name": "Ho (Warang Citi)", + "id": "hoc-wara", + "region": 3 + }, { + "name": "Holoholo (Latin)", + "id": "hoo-latn", + "region": 2 + }, { + "name": "Hopi", + "id": "hop", + "region": 6 + }, { + "name": "Horo", + "id": "hor", + "region": 2 + }, { + "name": "Humla", + "id": "hut", + "region": 3 + }, { + "name": "Hunde (Latin)", + "id": "hke-latn", + "region": 2 + }, { + "name": "Hungarian", + "id": "hu", + "region": 4 + }, { + "name": "Hungarian (Braille)", + "id": "hu-brai", + "region": 4 + }, { + "name": "Hutterisch", + "id": "geh", + "region": 6 + }, { + "name": "Hya", + "id": "hya", + "region": 2 + }, { + "name": "Hyolmo", + "id": "scp", + "region": 3 + }, { + "name": "Ibaloi (Latin)", + "id": "ibl-latn", + "region": 3 + }, { + "name": "Ibanag (Latin)", + "id": "ibg-latn", + "region": 3 + }, { + "name": "Ibatan (Latin)", + "id": "ivb-latn", + "region": 3 + }, { + "name": "Icelandic", + "id": "is", + "region": 4 + }, { + "name": "Icelandic (Runic)", + "id": "is-runr", + "region": 4 + }, { + "name": "Iceve-Maci", + "id": "bec", + "region": 2 + }, { + "name": "Ido (Latin)", + "id": "io-latn", + "region": 1 + }, { + "name": "Idoma", + "id": "idu", + "region": 2 + }, { + "name": "Idoma (Latin)", + "id": "idu-latn", + "region": 2 + }, { + "name": "Idu-Mishmi (Tibetan)", + "id": "clk-tibt", + "region": 3 + }, { + "name": "Ifugao, Amganad (Latin)", + "id": "ifa-latn", + "region": 3 + }, { + "name": "Ifugao, Batad (Latin)", + "id": "ifb-latn", + "region": 3 + }, { + "name": "Ifugao, Mayoyao (Latin)", + "id": "ifu-latn", + "region": 3 + }, { + "name": "Ifugao, Tuwali (Latin)", + "id": "ifk-latn", + "region": 3 + }, { + "name": "Igbo", + "id": "ig", + "region": 2 + }, { + "name": "Igbo (Latin)", + "id": "ig-latn", + "region": 2 + }, { + "name": "Igede (Latin)", + "id": "ige-latn", + "region": 2 + }, { + "name": "Ignaciano", + "id": "ign", + "region": 6 + }, { + "name": "Ijo, Southeast", + "id": "ijs", + "region": 2 + }, { + "name": "Ik (Latin)", + "id": "ikx-latn", + "region": 2 + }, { + "name": "Ikizu (Latin)", + "id": "ikz-latn", + "region": 2 + }, { + "name": "Ikoma-Nata-Isenye (Latin)", + "id": "ntk-latn", + "region": 2 + }, { + "name": "Ikwere (Latin)", + "id": "ikw-latn", + "region": 2 + }, { + "name": "Ilocano (Braille)", + "id": "ilo-brai", + "region": 3 + }, { + "name": "Ilocano (Latin)", + "id": "ilo-latn", + "region": 3 + }, { + "name": "Inabaknon (Latin)", + "id": "abx-latn", + "region": 3 + }, { + "name": "Indonesian", + "id": "id", + "region": 3 + }, { + "name": "Indonesian (Braille)", + "id": "id-brai", + "region": 3 + }, { + "name": "Indonesian (Javanese)", + "id": "id-java", + "region": 3 + }, { + "name": "Innu (Latin)", + "id": "moe-latn", + "region": 6 + }, { + "name": "Inonhan (Latin)", + "id": "loc-latn", + "region": 3 + }, { + "name": "Inor", + "id": "ior", + "region": 2 + }, { + "name": "Interlingua (International Auxiliary Language Association) (Latin)", + "id": "ia-latn", + "region": 1 + }, { + "name": "Inuinnaqtun", + "id": "ikt", + "region": 6 + }, { + "name": "Inuinnaqtun (Latin)", + "id": "ikt-latn", + "region": 6 + }, { + "name": "Inuktitut (Unified Canadian Aboriginal Syllabics)", + "id": "iu-cans", + "region": 1 + }, { + "name": "Inuktitut (Latin)", + "id": "iu-latn", + "region": 1 + }, { + "name": "Inuktitut, Eastern Canadian (Unified Canadian Aboriginal Syllabics)", + "id": "ike-cans", + "region": 6 + }, { + "name": "Inuktitut, Eastern Canadian (Latin)", + "id": "ike-latn", + "region": 6 + }, { + "name": "Inupiaq", + "id": "ik", + "region": 1 + }, { + "name": "Inupiatun, North Alaskan", + "id": "esi", + "region": 6 + }, { + "name": "Iowa-Oto", + "id": "iow", + "region": 6 + }, { + "name": "Ipulo", + "id": "ass", + "region": 2 + }, { + "name": "Iranun (Latin)", + "id": "ilp-latn", + "region": 3 + }, { + "name": "Iraya (Latin)", + "id": "iry-latn", + "region": 3 + }, { + "name": "Irish", + "id": "ga", + "region": 4 + }, { + "name": "Isanzu (Latin)", + "id": "isn-latn", + "region": 2 + }, { + "name": "Ishkashimi (Cyrillic, Tajikistan)", + "id": "isk-cyrl-tj", + "region": 3 + }, { + "name": "Islander Creole English", + "id": "icr", + "region": 6 + }, { + "name": "Isnag (Latin)", + "id": "isd-latn", + "region": 3 + }, { + "name": "Isu", + "id": "isu", + "region": 2 + }, { + "name": "Isu", + "id": "szv", + "region": 2 + }, { + "name": "Italian", + "id": "it", + "region": 4 + }, { + "name": "Italian (Braille)", + "id": "it-brai", + "region": 4 + }, { + "name": "Itawit (Latin)", + "id": "itv-latn", + "region": 3 + }, { + "name": "itc-ital (Old Italic (Etruscan, Oscan, etc.))", + "id": "itc-ital", + "region": 1 + }, { + "name": "Itneg, Binongan (Latin)", + "id": "itb-latn", + "region": 3 + }, { + "name": "Itneg, Masadiit (Latin)", + "id": "tis-latn", + "region": 3 + }, { + "name": "Ivatan (Latin)", + "id": "ivv-latn", + "region": 3 + }, { + "name": "Ivbie North-Okpela-Arhe (Latin)", + "id": "atg-latn", + "region": 2 + }, { + "name": "Iyive", + "id": "uiv", + "region": 2 + }, { + "name": "Jalkunan (Latin)", + "id": "bxl-latn", + "region": 2 + }, { + "name": "Jamaican Creole English", + "id": "jam", + "region": 6 + }, { + "name": "Japanese", + "id": "ja", + "region": 3 + }, { + "name": "Japanese (Braille)", + "id": "ja-brai", + "region": 3 + }, { + "name": "Jarai (Khmer)", + "id": "jra-khmr", + "region": 3 + }, { + "name": "Jarai (Latin)", + "id": "jra-latn", + "region": 3 + }, { + "name": "Javanese (Javanese)", + "id": "jv-java", + "region": 3 + }, { + "name": "Javanese (Latin)", + "id": "jv-latn", + "region": 3 + }, { + "name": "Jaya", + "id": "jyy", + "region": 2 + }, { + "name": "Jewish Babylonian Aramaic (Syriac)", + "id": "tmr-syrc", + "region": 3 + }, { + "name": "Jiarong (Tibetan)", + "id": "jya-tibt", + "region": 3 + }, { + "name": "Jimjimen", + "id": "jim", + "region": 2 + }, { + "name": "Jina", + "id": "jia", + "region": 2 + }, { + "name": "Jirel", + "id": "jul", + "region": 3 + }, { + "name": "Jita (Latin)", + "id": "jit-latn", + "region": 2 + }, { + "name": "Jju (Latin)", + "id": "kaj-latn", + "region": 2 + }, { + "name": "Joba (Latin)", + "id": "job-latn", + "region": 2 + }, { + "name": "Jola-Felupe", + "id": "eja", + "region": 2 + }, { + "name": "Jola-Fonyi", + "id": "dyo", + "region": 2 + }, { + "name": "Jola-Kasa", + "id": "csk", + "region": 2 + }, { + "name": "Jonkor Bourmataguil", + "id": "jeu", + "region": 2 + }, { + "name": "Jowulu (Latin)", + "id": "jow-latn", + "region": 2 + }, { + "name": "Juang (Oriya)", + "id": "jun-orya", + "region": 3 + }, { + "name": "Jukun Takum", + "id": "jbu", + "region": 2 + }, { + "name": "Jukun Takum (Latin)", + "id": "jbu-latn", + "region": 2 + }, { + "name": "Jula (Latin)", + "id": "dyu-latn", + "region": 2 + }, { + "name": "Jula (N\u2019Ko)", + "id": "dyu-nkoo", + "region": 2 + }, { + "name": "Jumli", + "id": "jml", + "region": 3 + }, { + "name": "Jutish", + "id": "jut", + "region": 1 + }, { + "name": "Kaansa (Latin)", + "id": "gna-latn", + "region": 2 + }, { + "name": "Kaba D\u00e9m\u00e9, Sara", + "id": "kwg", + "region": 2 + }, { + "name": "Kaba Naa, Sara", + "id": "kwv", + "region": 2 + }, { + "name": "Kabalai", + "id": "kvf", + "region": 2 + }, { + "name": "Kabba", + "id": "ksp", + "region": 2 + }, { + "name": "Kabiy\u00e8 (Braille)", + "id": "kbp-brai", + "region": 2 + }, { + "name": "Kabiy\u00e8 (Latin)", + "id": "kbp-latn", + "region": 2 + }, { + "name": "Kabuverdianu", + "id": "kea", + "region": 2 + }, { + "name": "Kabwa (Latin)", + "id": "cwa-latn", + "region": 2 + }, { + "name": "Kabwari (Latin)", + "id": "kcw-latn", + "region": 2 + }, { + "name": "Kabyle (Tifinagh)", + "id": "kab-tfng", + "region": 2 + }, { + "name": "Kafa (Latin)", + "id": "kbr-latn", + "region": 2 + }, { + "name": "Kagayanen (Latin)", + "id": "cgc-latn", + "region": 3 + }, { + "name": "Kagulu (Latin)", + "id": "kki-latn", + "region": 2 + }, { + "name": "Kahe (Latin)", + "id": "hka-latn", + "region": 2 + }, { + "name": "Kaiku (Latin)", + "id": "kkq-latn", + "region": 2 + }, { + "name": "Kajakse", + "id": "ckq", + "region": 2 + }, { + "name": "Kako", + "id": "kkj", + "region": 2 + }, { + "name": "Kakwa (Latin)", + "id": "keo-latn", + "region": 2 + }, { + "name": "Kalagan", + "id": "kqe", + "region": 3 + }, { + "name": "Kalagan (Latin)", + "id": "kqe-latn", + "region": 3 + }, { + "name": "Kalagan, Kagan (Latin)", + "id": "kll-latn", + "region": 3 + }, { + "name": "Kalanguya (Latin)", + "id": "kak-latn", + "region": 3 + }, { + "name": "Kalasha (Arabic)", + "id": "kls-arab", + "region": 3 + }, { + "name": "Kalinga, Butbut (Latin)", + "id": "kyb-latn", + "region": 3 + }, { + "name": "Kalinga, Limos (Latin)", + "id": "kmk-latn", + "region": 3 + }, { + "name": "Kalinga, Lubuagan (Latin)", + "id": "knb-latn", + "region": 3 + }, { + "name": "Kalinga, Majukayang (Latin)", + "id": "kmd-latn", + "region": 3 + }, { + "name": "Kalinga, Southern (Latin)", + "id": "ksc-latn", + "region": 3 + }, { + "name": "Kalinga, Tanudan (Latin)", + "id": "kml-latn", + "region": 3 + }, { + "name": "Kallahan, Keley-i (Latin)", + "id": "ify-latn", + "region": 3 + }, { + "name": "Kambaata", + "id": "ktb", + "region": 2 + }, { + "name": "Kambaata (Latin)", + "id": "ktb-latn", + "region": 2 + }, { + "name": "Kami (Latin)", + "id": "kcu-latn", + "region": 2 + }, { + "name": "Kamwe (Latin)", + "id": "hig-latn", + "region": 2 + }, { + "name": "Kanembu", + "id": "kbl", + "region": 2 + }, { + "name": "Kango (Latin)", + "id": "kty-latn", + "region": 2 + }, { + "name": "Kango (Latin)", + "id": "kzy-latn", + "region": 2 + }, { + "name": "Kankanaey (Latin)", + "id": "kne-latn", + "region": 3 + }, { + "name": "Kankanay, Northern (Latin)", + "id": "xnn-latn", + "region": 3 + }, { + "name": "Kannada", + "id": "kn", + "region": 3 + }, { + "name": "Kannada (Braille)", + "id": "kn-brai", + "region": 3 + }, { + "name": "Kanu (Latin)", + "id": "khx-latn", + "region": 2 + }, { + "name": "Kanuri", + "id": "kr", + "region": 1 + }, { + "name": "Kanuri (Latin)", + "id": "kr-latn", + "region": 1 + }, { + "name": "Kanuri, Bilma", + "id": "bms", + "region": 2 + }, { + "name": "Kanuri, Manga", + "id": "kby", + "region": 2 + }, { + "name": "Kanuri, Manga (Latin)", + "id": "kby-latn", + "region": 2 + }, { + "name": "Kanuri, Tumari", + "id": "krt", + "region": 2 + }, { + "name": "Kanuri, Yerwa (Latin)", + "id": "knc-latn", + "region": 2 + }, { + "name": "Kapampangan (Braille)", + "id": "pam-brai", + "region": 3 + }, { + "name": "Kapampangan (Latin)", + "id": "pam-latn", + "region": 3 + }, { + "name": "Kara (Latin)", + "id": "reg-latn", + "region": 2 + }, { + "name": "Karakalpak", + "id": "kaa", + "region": 3 + }, { + "name": "Karakalpak (Latin)", + "id": "kaa-latn", + "region": 3 + }, { + "name": "Karang", + "id": "kzr", + "region": 2 + }, { + "name": "Karanga", + "id": "kth", + "region": 2 + }, { + "name": "Karao (Latin)", + "id": "kyj-latn", + "region": 3 + }, { + "name": "Kare", + "id": "kbn", + "region": 2 + }, { + "name": "Karelian", + "id": "krl", + "region": 4 + }, { + "name": "Karen, Bwe (Latin)", + "id": "bwe-latn", + "region": 3 + }, { + "name": "Karen, S\u2019gaw (Myanmar)", + "id": "ksw-mymr", + "region": 3 + }, { + "name": "Kari (Latin)", + "id": "kbj-latn", + "region": 2 + }, { + "name": "Karipuna Creole French", + "id": "kmv", + "region": 6 + }, { + "name": "Karon", + "id": "krx", + "region": 2 + }, { + "name": "Kasabe", + "id": "luw", + "region": 2 + }, { + "name": "Kasem (Latin)", + "id": "xsm-latn", + "region": 2 + }, { + "name": "Kashmiri (Arabic)", + "id": "ks-arab", + "region": 3 + }, { + "name": "Kashubian", + "id": "csb", + "region": 4 + }, { + "name": "Kaska (Latin)", + "id": "kkz-latn", + "region": 6 + }, { + "name": "Kavet (Khmer)", + "id": "krv-khmr", + "region": 3 + }, { + "name": "Kawi (Balinese)", + "id": "kaw-bali", + "region": 1 + }, { + "name": "Kawi (Javanese)", + "id": "kaw-java", + "region": 1 + }, { + "name": "Kayah, Western (Kayah Li)", + "id": "kyu-kali", + "region": 3 + }, { + "name": "Kayah, Western (Latin)", + "id": "kyu-latn", + "region": 3 + }, { + "name": "Kayah, Western (Myanmar)", + "id": "kyu-mymr", + "region": 3 + }, { + "name": "Kayan (Latin)", + "id": "pdu-latn", + "region": 3 + }, { + "name": "Kayaw (Latin)", + "id": "kvl-latn", + "region": 3 + }, { + "name": "Kazakh", + "id": "kk", + "region": 3 + }, { + "name": "Kazakh (Braille)", + "id": "kk-brai", + "region": 3 + }, { + "name": "Kele (Latin)", + "id": "khy-latn", + "region": 2 + }, { + "name": "Keliko (Latin)", + "id": "kbo-latn", + "region": 2 + }, { + "name": "Kemedzung", + "id": "dmo", + "region": 2 + }, { + "name": "Kendeje", + "id": "klf", + "region": 2 + }, { + "name": "Kendem", + "id": "kvm", + "region": 2 + }, { + "name": "Kenga", + "id": "kyq", + "region": 2 + }, { + "name": "Kenswei Nsei", + "id": "ndb", + "region": 2 + }, { + "name": "Kenyang", + "id": "ken", + "region": 2 + }, { + "name": "Kenye (Latin)", + "id": "lke-latn", + "region": 2 + }, { + "name": "Kera", + "id": "ker", + "region": 2 + }, { + "name": "Kerewe (Latin)", + "id": "ked-latn", + "region": 2 + }, { + "name": "Kerinci (Latin)", + "id": "kvr-latn", + "region": 3 + }, { + "name": "Khaling", + "id": "klr", + "region": 3 + }, { + "name": "Kham, Gamale", + "id": "kgj", + "region": 3 + }, { + "name": "Kham, Western Parbate", + "id": "kjl", + "region": 3 + }, { + "name": "Khamba (Tibetan)", + "id": "kbg-tibt", + "region": 3 + }, { + "name": "Khengkha (Tibetan)", + "id": "xkf-tibt", + "region": 3 + }, { + "name": "Khinalugh", + "id": "kjj", + "region": 3 + }, { + "name": "Khmer", + "id": "km", + "region": 3 + }, { + "name": "Khmer, Northern (Khmer)", + "id": "kxm-khmr", + "region": 3 + }, { + "name": "Khmu (Lao)", + "id": "kjg-laoo", + "region": 3 + }, { + "name": "Khowar (Arabic)", + "id": "khw-arab", + "region": 3 + }, { + "name": "Kibet", + "id": "kie", + "region": 2 + }, { + "name": "Kigiryama (Latin)", + "id": "nyf-latn", + "region": 2 + }, { + "name": "Kim", + "id": "kia", + "region": 2 + }, { + "name": "Kimbu (Latin)", + "id": "kiv-latn", + "region": 2 + }, { + "name": "Kimr\u00e9", + "id": "kqp", + "region": 2 + }, { + "name": "Kinaray-a (Latin)", + "id": "krj-latn", + "region": 3 + }, { + "name": "Kinga (Latin)", + "id": "zga-latn", + "region": 2 + }, { + "name": "Kinnauri (Devanagari)", + "id": "kfk-deva", + "region": 3 + }, { + "name": "Kinyarwanda (Braille)", + "id": "rw-brai", + "region": 2 + }, { + "name": "Kipfokomo (Latin)", + "id": "pkb-latn", + "region": 2 + }, { + "name": "Kiribati (Latin)", + "id": "gil-latn", + "region": 7 + }, { + "name": "Kisan (Oriya)", + "id": "xis-orya", + "region": 3 + }, { + "name": "Kisi (Latin)", + "id": "kiz-latn", + "region": 2 + }, { + "name": "Kisi, Southern (Latin)", + "id": "kss-latn", + "region": 2 + }, { + "name": "Kistane", + "id": "gru", + "region": 2 + }, { + "name": "Kistane (Latin)", + "id": "gru-latn", + "region": 2 + }, { + "name": "Kitharaka (Latin)", + "id": "thk-latn", + "region": 2 + }, { + "name": "Kituba", + "id": "ktu", + "region": 2 + }, { + "name": "Kituba", + "id": "mkw", + "region": 2 + }, { + "name": "Klao (Latin)", + "id": "klu-latn", + "region": 2 + }, { + "name": "Koalib", + "id": "kib", + "region": 2 + }, { + "name": "Kohistani, Indus (Arabic)", + "id": "mvy-arab", + "region": 3 + }, { + "name": "Koho (Latin)", + "id": "kpm-latn", + "region": 3 + }, { + "name": "Koke", + "id": "kou", + "region": 2 + }, { + "name": "Kol", + "id": "biw", + "region": 2 + }, { + "name": "Kolbila", + "id": "klc", + "region": 2 + }, { + "name": "Koli, Parkari (Arabic)", + "id": "kvx-arab", + "region": 3 + }, { + "name": "Koli, Wadiyari (Arabic)", + "id": "kxp-arab", + "region": 3 + }, { + "name": "Kom", + "id": "bkm", + "region": 2 + }, { + "name": "Koma", + "id": "kmy", + "region": 2 + }, { + "name": "Komo (Latin)", + "id": "kmw-latn", + "region": 2 + }, { + "name": "Komo (Latin)", + "id": "xom-latn", + "region": 2 + }, { + "name": "Konab\u00e9r\u00e9 (Latin)", + "id": "bbo-latn", + "region": 2 + }, { + "name": "Konkani", + "id": "knn", + "region": 3 + }, { + "name": "Konkani (Devanagari)", + "id": "knn-deva", + "region": 3 + }, { + "name": "Konkani (macrolanguage) (Devanagari)", + "id": "kok-deva", + "region": 1 + }, { + "name": "Konkani, Goan (Devanagari)", + "id": "gom-deva", + "region": 3 + }, { + "name": "Konkomba (Braille)", + "id": "xon-brai", + "region": 2 + }, { + "name": "Kono (Latin)", + "id": "kno-latn", + "region": 2 + }, { + "name": "Konongo (Latin)", + "id": "kcz-latn", + "region": 2 + }, { + "name": "Konso", + "id": "kxc", + "region": 2 + }, { + "name": "Konso (Ethiopic)", + "id": "kxc-ethi", + "region": 2 + }, { + "name": "Konso (Latin)", + "id": "kxc-latn", + "region": 2 + }, { + "name": "Konzo (Latin)", + "id": "koo-latn", + "region": 2 + }, { + "name": "Koonzime", + "id": "ozm", + "region": 2 + }, { + "name": "Koorete (Latin)", + "id": "kqy-latn", + "region": 2 + }, { + "name": "Korean", + "id": "ko", + "region": 3 + }, { + "name": "Korean (Braille)", + "id": "ko-brai", + "region": 3 + }, { + "name": "Koreguaje", + "id": "coe", + "region": 6 + }, { + "name": "Korop", + "id": "krp", + "region": 2 + }, { + "name": "Koshin", + "id": "kid", + "region": 2 + }, { + "name": "Koy Sanjaq Surat (Syriac)", + "id": "kqd-syrc", + "region": 3 + }, { + "name": "Koya (Oriya)", + "id": "kff-orya", + "region": 3 + }, { + "name": "Kpelle (Latin)", + "id": "kpe-latn", + "region": 1 + }, { + "name": "Kpelle, Liberia (Latin)", + "id": "xpe-latn", + "region": 2 + }, { + "name": "Krahn, Eastern (Latin)", + "id": "kqo-latn", + "region": 2 + }, { + "name": "Krahn, Western (Latin)", + "id": "krw-latn", + "region": 2 + }, { + "name": "Krio (Latin)", + "id": "kri-latn", + "region": 2 + }, { + "name": "Kriol", + "id": "rop", + "region": 7 + }, { + "name": "Krung (Khmer)", + "id": "krr-khmr", + "region": 3 + }, { + "name": "Kuay (Khmer)", + "id": "kdt-khmr", + "region": 3 + }, { + "name": "Kudmali (Oriya)", + "id": "kyw-orya", + "region": 3 + }, { + "name": "Kujarge", + "id": "vkj", + "region": 2 + }, { + "name": "Kuk", + "id": "kfn", + "region": 2 + }, { + "name": "Kuke (Tibetan)", + "id": "ght-tibt", + "region": 3 + }, { + "name": "Kukele (Latin)", + "id": "kez-latn", + "region": 2 + }, { + "name": "Kulfa", + "id": "kxj", + "region": 2 + }, { + "name": "Kulung", + "id": "kle", + "region": 3 + }, { + "name": "Kung", + "id": "kfl", + "region": 2 + }, { + "name": "Kuo", + "id": "xuo", + "region": 2 + }, { + "name": "Kuranko (Latin)", + "id": "knk-latn", + "region": 2 + }, { + "name": "Kurdish, Central (Arabic)", + "id": "ckb-arab", + "region": 3 + }, { + "name": "Kuria (Latin)", + "id": "kuj-latn", + "region": 2 + }, { + "name": "Kurumba, Kannada (Kannada)", + "id": "kfi-knda", + "region": 3 + }, { + "name": "Kutenai (Latin)", + "id": "kut-latn", + "region": 6 + }, { + "name": "Kutep", + "id": "kub", + "region": 2 + }, { + "name": "Kutu (Latin)", + "id": "kdc-latn", + "region": 2 + }, { + "name": "Kuvi (Oriya)", + "id": "kxv-orya", + "region": 3 + }, { + "name": "Kuwaa (Latin)", + "id": "blh-latn", + "region": 2 + }, { + "name": "Kuwaataay", + "id": "cwt", + "region": 2 + }, { + "name": "Kwa\u2019", + "id": "bko", + "region": 2 + }, { + "name": "Kwaja", + "id": "kdz", + "region": 2 + }, { + "name": "Kwakum", + "id": "kwu", + "region": 2 + }, { + "name": "Kwakwala (Latin)", + "id": "kwk-latn", + "region": 6 + }, { + "name": "Kwami (Latin)", + "id": "ktf-latn", + "region": 2 + }, { + "name": "Kwang", + "id": "kvi", + "region": 2 + }, { + "name": "Kwanja", + "id": "knp", + "region": 2 + }, { + "name": "Kwasio", + "id": "nmg", + "region": 2 + }, { + "name": "Kwaya (Latin)", + "id": "kya-latn", + "region": 2 + }, { + "name": "Kwere (Latin)", + "id": "cwe-latn", + "region": 2 + }, { + "name": "kxu-orya (Oriya)", + "id": "kxu-orya", + "region": 1 + }, { + "name": "Kyerung", + "id": "kgy", + "region": 3 + }, { + "name": "Kyrgyz (Cyrillic)", + "id": "ky-cyrl", + "region": 3 + }, { + "name": "La\u2019bi", + "id": "lbi", + "region": 2 + }, { + "name": "Laal", + "id": "gdm", + "region": 2 + }, { + "name": "Laalaa", + "id": "cae", + "region": 2 + }, { + "name": "Ladakhi (Tibetan)", + "id": "lbj-tibt", + "region": 3 + }, { + "name": "Ladin", + "id": "lld", + "region": 4 + }, { + "name": "Ladin (Latin)", + "id": "lld-latn", + "region": 4 + }, { + "name": "Ladino", + "id": "lad", + "region": 3 + }, { + "name": "Lagwan", + "id": "kot", + "region": 2 + }, { + "name": "Lahnda", + "id": "lah", + "region": 1 + }, { + "name": "Lahu (Latin)", + "id": "lhu-latn", + "region": 3 + }, { + "name": "Laimbue", + "id": "lmx", + "region": 2 + }, { + "name": "Laka", + "id": "lap", + "region": 2 + }, { + "name": "Lakkia (Lisu)", + "id": "lbc-lisu", + "region": 3 + }, { + "name": "Lakota (Latin)", + "id": "lkt-latn", + "region": 6 + }, { + "name": "Lamang (Latin)", + "id": "hia-latn", + "region": 2 + }, { + "name": "Lamkang", + "id": "lmk", + "region": 3 + }, { + "name": "Lamnso\u2019", + "id": "lns", + "region": 2 + }, { + "name": "Landoma (Latin)", + "id": "ldm-latn", + "region": 2 + }, { + "name": "Langi (Latin)", + "id": "lag-latn", + "region": 2 + }, { + "name": "Lao", + "id": "lo", + "region": 3 + }, { + "name": "Latin", + "id": "la", + "region": 4 + }, { + "name": "Latvian", + "id": "lv", + "region": 1 + }, { + "name": "Latvian (Braille)", + "id": "lv-brai", + "region": 1 + }, { + "name": "Lave (Khmer)", + "id": "brb-khmr", + "region": 3 + }, { + "name": "Laz (Khutsuri (Asomtavruli and Nuskhuri), Georgia)", + "id": "lzz-geok-ge", + "region": 3 + }, { + "name": "Laz (Latin)", + "id": "lzz-latn", + "region": 3 + }, { + "name": "Laz (Latin, Georgia)", + "id": "lzz-latn-ge", + "region": 3 + }, { + "name": "Lefa", + "id": "lfa", + "region": 2 + }, { + "name": "Lega-Mwenga (Latin)", + "id": "lgm-latn", + "region": 2 + }, { + "name": "Lega-Shabunda (Latin)", + "id": "lea-latn", + "region": 2 + }, { + "name": "Lele", + "id": "lln", + "region": 2 + }, { + "name": "Lendu (Latin)", + "id": "led-latn", + "region": 2 + }, { + "name": "Lengola (Latin)", + "id": "lej-latn", + "region": 2 + }, { + "name": "Lepcha", + "id": "lep", + "region": 3 + }, { + "name": "Lepcha (Tibetan)", + "id": "lep-tibt", + "region": 3 + }, { + "name": "Lese (Latin)", + "id": "les-latn", + "region": 2 + }, { + "name": "Leti", + "id": "leo", + "region": 2 + }, { + "name": "Lhomi", + "id": "lhm", + "region": 3 + }, { + "name": "Lhowa", + "id": "loy", + "region": 3 + }, { + "name": "Lhowa (Tibetan)", + "id": "loy-tibt", + "region": 3 + }, { + "name": "Lidzonka", + "id": "add", + "region": 2 + }, { + "name": "Ligenza (Latin)", + "id": "lgz-latn", + "region": 2 + }, { + "name": "Ligurian", + "id": "lij", + "region": 4 + }, { + "name": "Lika (Latin)", + "id": "lik-latn", + "region": 2 + }, { + "name": "Lillooet (Latin)", + "id": "lil-latn", + "region": 6 + }, { + "name": "Limba, West-Central (Latin)", + "id": "lia-latn", + "region": 2 + }, { + "name": "Limbu", + "id": "lif", + "region": 3 + }, { + "name": "Limbu (Devanagari)", + "id": "lif-deva", + "region": 3 + }, { + "name": "Limbu (Limbu)", + "id": "lif-limb", + "region": 3 + }, { + "name": "Limbum", + "id": "lmp", + "region": 2 + }, { + "name": "Limburgish", + "id": "li", + "region": 4 + }, { + "name": "Lingala", + "id": "ln", + "region": 2 + }, { + "name": "Lipo (Lisu)", + "id": "lpo-lisu", + "region": 3 + }, { + "name": "Lipo (Miao)", + "id": "lpo-plrd", + "region": 3 + }, { + "name": "Lisu (Lisu)", + "id": "lis-lisu", + "region": 3 + }, { + "name": "Lithuanian", + "id": "lt", + "region": 4 + }, { + "name": "Liv", + "id": "liv", + "region": 4 + }, { + "name": "Livvi-Karelian", + "id": "olo", + "region": 4 + }, { + "name": "Logo (Latin)", + "id": "log-latn", + "region": 2 + }, { + "name": "Lohorung", + "id": "lbr", + "region": 3 + }, { + "name": "Lokaa (Latin)", + "id": "yaz-latn", + "region": 2 + }, { + "name": "Loko (Latin)", + "id": "lok-latn", + "region": 2 + }, { + "name": "Loma (Latin)", + "id": "lom-latn", + "region": 2 + }, { + "name": "Lombard", + "id": "lmo", + "region": 4 + }, { + "name": "Lombi (Latin)", + "id": "lmi-latn", + "region": 2 + }, { + "name": "Lombo (Latin)", + "id": "loo-latn", + "region": 2 + }, { + "name": "Longto", + "id": "wok", + "region": 2 + }, { + "name": "Longuda (Latin)", + "id": "lnu-latn", + "region": 2 + }, { + "name": "Louisiana Creole", + "id": "lou", + "region": 6 + }, { + "name": "L\u00fc (New Tai Lue)", + "id": "khb-talu", + "region": 3 + }, { + "name": "Ludian", + "id": "lud", + "region": 4 + }, { + "name": "Lugbara (Latin)", + "id": "lgg-latn", + "region": 2 + }, { + "name": "Luguru (Latin)", + "id": "ruf-latn", + "region": 2 + }, { + "name": "Lushootseed", + "id": "lut", + "region": 6 + }, { + "name": "Lushootseed (Latin)", + "id": "lut-latn", + "region": 6 + }, { + "name": "Lutos", + "id": "ndy", + "region": 2 + }, { + "name": "Luxembourgish", + "id": "lb", + "region": 4 + }, { + "name": "Ma (Latin)", + "id": "msj-latn", + "region": 2 + }, { + "name": "Ma\u2019di", + "id": "mhi", + "region": 2 + }, { + "name": "Maan (Latin)", + "id": "mev-latn", + "region": 2 + }, { + "name": "Maba (Latin)", + "id": "mde-latn", + "region": 2 + }, { + "name": "Mabire", + "id": "muj", + "region": 2 + }, { + "name": "Macedonian", + "id": "mk", + "region": 4 + }, { + "name": "Macedonian (Braille)", + "id": "mk-brai", + "region": 4 + }, { + "name": "Machame (Latin)", + "id": "jmc-latn", + "region": 2 + }, { + "name": "Machinga (Latin)", + "id": "mvw-latn", + "region": 2 + }, { + "name": "Mada", + "id": "mxu", + "region": 2 + }, { + "name": "Madura (Javanese)", + "id": "mad-java", + "region": 3 + }, { + "name": "Mafa", + "id": "maf", + "region": 2 + }, { + "name": "Magahi (Devanagari)", + "id": "mag-deva", + "region": 3 + }, { + "name": "Magar, Eastern", + "id": "mgp", + "region": 3 + }, { + "name": "Magar, Western", + "id": "mrd", + "region": 3 + }, { + "name": "Magoma (Latin)", + "id": "gmx-latn", + "region": 2 + }, { + "name": "Maguindanaon (Latin)", + "id": "mdh-latn", + "region": 3 + }, { + "name": "Maithili", + "id": "mai", + "region": 3 + }, { + "name": "Maithili (Devanagari)", + "id": "mai-deva", + "region": 3 + }, { + "name": "Maithili (Tirhuta)", + "id": "mai-tirh", + "region": 3 + }, { + "name": "Majang (Latin)", + "id": "mpe-latn", + "region": 2 + }, { + "name": "Majera", + "id": "xmj", + "region": 2 + }, { + "name": "Majhi", + "id": "mjz", + "region": 3 + }, { + "name": "Makaa", + "id": "mcp", + "region": 2 + }, { + "name": "Makah (Latin)", + "id": "myh-latn", + "region": 6 + }, { + "name": "Makasar (Buginese)", + "id": "mak-bugi", + "region": 3 + }, { + "name": "Makasar (Latin)", + "id": "mak-latn", + "region": 3 + }, { + "name": "Makasar (Makasar)", + "id": "mak-maka", + "region": 3 + }, { + "name": "Makonde (Latin)", + "id": "kde-latn", + "region": 2 + }, { + "name": "Malagasy (Braille)", + "id": "mg-brai", + "region": 1 + }, { + "name": "Malagasy, Plateau (Braille)", + "id": "plt-brai", + "region": 2 + }, { + "name": "Malay (Latin)", + "id": "zlm-latn", + "region": 3 + }, { + "name": "Malay (macrolanguage)", + "id": "ms", + "region": 1 + }, { + "name": "Malay (macrolanguage) (Braille)", + "id": "ms-brai", + "region": 1 + }, { + "name": "Malay, Jambi (Latin)", + "id": "jax-latn", + "region": 3 + }, { + "name": "Malay, Kedah (Latin)", + "id": "meo-latn", + "region": 3 + }, { + "name": "Malay, Manado (Latin)", + "id": "xmm-latn", + "region": 3 + }, { + "name": "Malay, North Moluccan (Latin)", + "id": "max-latn", + "region": 3 + }, { + "name": "Malay, Pattani (Latin)", + "id": "mfa-latn", + "region": 3 + }, { + "name": "Malay, Standard (Braille)", + "id": "zsm-brai", + "region": 3 + }, { + "name": "Malay, Standard (Latin)", + "id": "zsm-latn", + "region": 3 + }, { + "name": "Malayalam", + "id": "ml", + "region": 3 + }, { + "name": "Malayalam (Braille)", + "id": "ml-brai", + "region": 3 + }, { + "name": "Maldivian", + "id": "dv", + "region": 3 + }, { + "name": "Male (Latin)", + "id": "mdy-latn", + "region": 2 + }, { + "name": "Malecite-Passamaquoddy (Latin)", + "id": "pqm-latn", + "region": 6 + }, { + "name": "Malgbe", + "id": "mxf", + "region": 2 + }, { + "name": "Malila (Latin)", + "id": "mgq-latn", + "region": 2 + }, { + "name": "Malimba", + "id": "mzd", + "region": 2 + }, { + "name": "Maltese", + "id": "mt", + "region": 4 + }, { + "name": "Maltese (Braille)", + "id": "mt-brai", + "region": 4 + }, { + "name": "Mamanwa (Latin)", + "id": "mmn-latn", + "region": 3 + }, { + "name": "Mambai", + "id": "mcs", + "region": 2 + }, { + "name": "Mambila, Cameroon", + "id": "mcu", + "region": 2 + }, { + "name": "Mamvu (Latin)", + "id": "mdi-latn", + "region": 2 + }, { + "name": "Manda (Latin)", + "id": "mgs-latn", + "region": 2 + }, { + "name": "Mandaic (Mandaic)", + "id": "mid-mand", + "region": 3 + }, { + "name": "Mandar (Buginese)", + "id": "mdr-bugi", + "region": 3 + }, { + "name": "Mandingo (N\u2019Ko)", + "id": "man-nkoo", + "region": 1 + }, { + "name": "Mandinka (Latin)", + "id": "mnk-latn", + "region": 2 + }, { + "name": "Mandjak", + "id": "mfv", + "region": 2 + }, { + "name": "Mandjak (Latin)", + "id": "mfv-latn", + "region": 2 + }, { + "name": "Mangbetu (Latin)", + "id": "mdj-latn", + "region": 2 + }, { + "name": "Mangbutu (Latin)", + "id": "mdk-latn", + "region": 2 + }, { + "name": "Mango", + "id": "mge", + "region": 2 + }, { + "name": "Maninkakan, Eastern (Latin)", + "id": "emk-latn", + "region": 2 + }, { + "name": "Maninkakan, Eastern (N\u2019Ko)", + "id": "emk-nkoo", + "region": 2 + }, { + "name": "Maninkakan, Kita (Latin)", + "id": "mwk-latn", + "region": 2 + }, { + "name": "Maninkakan, Western (Latin)", + "id": "mlq-latn", + "region": 2 + }, { + "name": "Mankanya", + "id": "knf", + "region": 2 + }, { + "name": "Mankanya (Latin)", + "id": "knf-latn", + "region": 2 + }, { + "name": "Manobo, Agusan (Latin)", + "id": "msm-latn", + "region": 3 + }, { + "name": "Manobo, Ata (Latin)", + "id": "atd-latn", + "region": 3 + }, { + "name": "Manobo, Cotabato (Latin)", + "id": "mta-latn", + "region": 3 + }, { + "name": "Manobo, Dibabawon (Latin)", + "id": "mbd-latn", + "region": 3 + }, { + "name": "Manobo, Ilianen (Latin)", + "id": "mbi-latn", + "region": 3 + }, { + "name": "Manobo, Matigsalug (Latin)", + "id": "mbt-latn", + "region": 3 + }, { + "name": "Manobo, Obo (Latin)", + "id": "obo-latn", + "region": 3 + }, { + "name": "Manobo, Sarangani (Latin)", + "id": "mbs-latn", + "region": 3 + }, { + "name": "Manobo, Western Bukidnon (Latin)", + "id": "mbb-latn", + "region": 3 + }, { + "name": "Mansaka (Latin)", + "id": "msk-latn", + "region": 3 + }, { + "name": "Manta", + "id": "myg", + "region": 2 + }, { + "name": "Manx", + "id": "gv", + "region": 4 + }, { + "name": "Manya (Latin)", + "id": "mzj-latn", + "region": 2 + }, { + "name": "Maori (Latin)", + "id": "mi-latn", + "region": 7 + }, { + "name": "Mapun (Latin)", + "id": "sjm-latn", + "region": 3 + }, { + "name": "Maranao (Latin)", + "id": "mrw-latn", + "region": 3 + }, { + "name": "Mararit", + "id": "mgb", + "region": 2 + }, { + "name": "Marathi", + "id": "mr", + "region": 3 + }, { + "name": "Marathi (Braille)", + "id": "mr-brai", + "region": 3 + }, { + "name": "Marathi (Modi)", + "id": "mr-modi", + "region": 3 + }, { + "name": "Marba", + "id": "mpg", + "region": 2 + }, { + "name": "Marfa", + "id": "mvu", + "region": 2 + }, { + "name": "Maria (Devanagari)", + "id": "mrr-deva", + "region": 3 + }, { + "name": "Maria, Dandami (Devanagari)", + "id": "daq-deva", + "region": 3 + }, { + "name": "Markweeta (Latin)", + "id": "enb-latn", + "region": 2 + }, { + "name": "Marwari (Arabic)", + "id": "mve-arab", + "region": 3 + }, { + "name": "Marwari", + "id": "rwr", + "region": 3 + }, { + "name": "Masaaba (Latin)", + "id": "myx-latn", + "region": 2 + }, { + "name": "Masalit", + "id": "mls", + "region": 2 + }, { + "name": "Masana", + "id": "mcn", + "region": 2 + }, { + "name": "Masbatenyo (Latin)", + "id": "msb-latn", + "region": 3 + }, { + "name": "Maslam", + "id": "msv", + "region": 2 + }, { + "name": "Masmaje", + "id": "mes", + "region": 2 + }, { + "name": "Massalat", + "id": "mdg", + "region": 2 + }, { + "name": "Matal", + "id": "mfh", + "region": 2 + }, { + "name": "Matengo (Latin)", + "id": "mgv-latn", + "region": 2 + }, { + "name": "Matlatzinca, Atzingo", + "id": "ocu", + "region": 6 + }, { + "name": "Mattokki (Coptic)", + "id": "xnz-copt", + "region": 2 + }, { + "name": "Matumbi (Latin)", + "id": "mgw-latn", + "region": 2 + }, { + "name": "Mawa", + "id": "mcw", + "region": 2 + }, { + "name": "Ma\u0300we\u0301s Aas\u02bce\u0300 (Latin)", + "id": "myf-latn", + "region": 2 + }, { + "name": "Mayogo (Latin)", + "id": "mdm-latn", + "region": 2 + }, { + "name": "Mazagway-Hidi", + "id": "dkx", + "region": 2 + }, { + "name": "Mba (Latin)", + "id": "mfc-latn", + "region": 2 + }, { + "name": "Mbara", + "id": "mpk", + "region": 2 + }, { + "name": "Mbay", + "id": "myb", + "region": 2 + }, { + "name": "Mbembe, Tigon", + "id": "nza", + "region": 2 + }, { + "name": "Mbesa (Latin)", + "id": "zms-latn", + "region": 2 + }, { + "name": "Mbo", + "id": "mbo", + "region": 2 + }, { + "name": "Mbo (Latin)", + "id": "zmw-latn", + "region": 2 + }, { + "name": "Mbo\u2019", + "id": "mtk", + "region": 2 + }, { + "name": "Mbole (Latin)", + "id": "mdq-latn", + "region": 2 + }, { + "name": "Mbonga", + "id": "xmb", + "region": 2 + }, { + "name": "Mbongno", + "id": "bgu", + "region": 2 + }, { + "name": "Mbudum", + "id": "xmd", + "region": 2 + }, { + "name": "Mbugwe (Latin)", + "id": "mgz-latn", + "region": 2 + }, { + "name": "Mbuko", + "id": "mqb", + "region": 2 + }, { + "name": "Mbula-Bwazza (Latin)", + "id": "mbu-latn", + "region": 2 + }, { + "name": "Mbule", + "id": "mlb", + "region": 2 + }, { + "name": "Mbum", + "id": "mdd", + "region": 2 + }, { + "name": "Mbunga (Latin)", + "id": "mgy-latn", + "region": 2 + }, { + "name": "Me\u2019en (Latin)", + "id": "mym-latn", + "region": 2 + }, { + "name": "Medumba", + "id": "byv", + "region": 2 + }, { + "name": "Mefele", + "id": "mfj", + "region": 2 + }, { + "name": "Meitei (Braille)", + "id": "mni-brai", + "region": 3 + }, { + "name": "Meitei (Meitei Mayek)", + "id": "mni-mtei", + "region": 3 + }, { + "name": "Memoni (Arabic)", + "id": "mby-arab", + "region": 3 + }, { + "name": "Mendankwe-Nkwen", + "id": "mfd", + "region": 2 + }, { + "name": "Mende", + "id": "men", + "region": 2 + }, { + "name": "Mengaka", + "id": "xmg", + "region": 2 + }, { + "name": "Mengisa", + "id": "mct", + "region": 2 + }, { + "name": "Menka", + "id": "mea", + "region": 2 + }, { + "name": "Merey", + "id": "meq", + "region": 2 + }, { + "name": "Mesaka", + "id": "iyo", + "region": 2 + }, { + "name": "Mesme", + "id": "zim", + "region": 2 + }, { + "name": "Mesqan", + "id": "mvz", + "region": 2 + }, { + "name": "Meta\u2019", + "id": "mgo", + "region": 2 + }, { + "name": "Mewahang, Eastern", + "id": "emg", + "region": 3 + }, { + "name": "Mfumte", + "id": "nfu", + "region": 2 + }, { + "name": "Mi\u2019kmaq (Latin)", + "id": "mic-latn", + "region": 6 + }, { + "name": "Miao, Large Flowery (Miao)", + "id": "hmd-plrd", + "region": 3 + }, { + "name": "Middle English (1100-1500) (Runic)", + "id": "enm-runr", + "region": 1 + }, { + "name": "Migaama", + "id": "mmy", + "region": 2 + }, { + "name": "Miltu", + "id": "mlj", + "region": 2 + }, { + "name": "Miluk", + "id": "iml", + "region": 1 + }, { + "name": "Mina", + "id": "hna", + "region": 2 + }, { + "name": "Minangkabau (Latin)", + "id": "min-latn", + "region": 3 + }, { + "name": "Mingrelian (Khutsuri (Asomtavruli and Nuskhuri))", + "id": "xmf-geok", + "region": 3 + }, { + "name": "Mingrelian (Latin)", + "id": "xmf-latn", + "region": 3 + }, { + "name": "Mirandese", + "id": "mwl", + "region": 4 + }, { + "name": "Mirandese (Latin)", + "id": "mwl-latn", + "region": 4 + }, { + "name": "Mirgan (Oriya)", + "id": "zrg-orya", + "region": 3 + }, { + "name": "Mituku (Latin)", + "id": "zmq-latn", + "region": 2 + }, { + "name": "Mizo (Braille)", + "id": "lus-brai", + "region": 3 + }, { + "name": "Mlahs\u00f6 (Syriac)", + "id": "lhs-syrc", + "region": 3 + }, { + "name": "Mmaala", + "id": "mmu", + "region": 2 + }, { + "name": "Mmen", + "id": "bfm", + "region": 2 + }, { + "name": "Mnong, Central (Khmer)", + "id": "cmo-khmr", + "region": 3 + }, { + "name": "Mnong, Central (Latin)", + "id": "cmo-latn", + "region": 3 + }, { + "name": "Mochi (Latin)", + "id": "old-latn", + "region": 2 + }, { + "name": "Mofu, North", + "id": "mfk", + "region": 2 + }, { + "name": "Mofu-Gudur", + "id": "mif", + "region": 2 + }, { + "name": "Mogum", + "id": "mou", + "region": 2 + }, { + "name": "Mohawk (Latin)", + "id": "moh-latn", + "region": 6 + }, { + "name": "Moingi (Latin)", + "id": "mwz-latn", + "region": 2 + }, { + "name": "Mokpwe", + "id": "bri", + "region": 2 + }, { + "name": "Molbog (Latin)", + "id": "pwm-latn", + "region": 3 + }, { + "name": "Moloko", + "id": "mlw", + "region": 2 + }, { + "name": "Mon", + "id": "mnw", + "region": 3 + }, { + "name": "Mongolian (Braille)", + "id": "mn-brai", + "region": 1 + }, { + "name": "Mongolian (Mongolian)", + "id": "mn-mong", + "region": 1 + }, { + "name": "Mongolian, Halh (Braille)", + "id": "khk-brai", + "region": 3 + }, { + "name": "Mongolian, Halh (Cyrillic)", + "id": "khk-cyrl", + "region": 3 + }, { + "name": "Mongolian, Halh (Cyrillic, Mongolia)", + "id": "khk-cyrl-mn", + "region": 3 + }, { + "name": "Mongolian, Halh (Mongolian)", + "id": "khk-mong", + "region": 3 + }, { + "name": "Mongolian, Halh (Phags-pa, Mongolia)", + "id": "khk-phag-mn", + "region": 3 + }, { + "name": "Mongolian, Halh (Tibetan)", + "id": "khk-tibt", + "region": 3 + }, { + "name": "Mongolian, Peripheral (Phags-pa, China)", + "id": "mvf-phag-cn", + "region": 3 + }, { + "name": "Mono", + "id": "mru", + "region": 2 + }, { + "name": "M\u00f2or\u00e9 (Braille)", + "id": "mos-brai", + "region": 2 + }, { + "name": "M\u00f2or\u00e9 (Latin)", + "id": "mos-latn", + "region": 2 + }, { + "name": "Morisyen", + "id": "mfe", + "region": 2 + }, { + "name": "Morom", + "id": "bdo", + "region": 2 + }, { + "name": "Motu, Hiri (Braille)", + "id": "ho-brai", + "region": 7 + }, { + "name": "Mpade", + "id": "mpi", + "region": 2 + }, { + "name": "Mpiemo", + "id": "mcx", + "region": 2 + }, { + "name": "Mpoto (Latin)", + "id": "mpa-latn", + "region": 2 + }, { + "name": "Mpumpong", + "id": "mgg", + "region": 2 + }, { + "name": "Mru (Mro)", + "id": "mro-mroo", + "region": 3 + }, { + "name": "Mser", + "id": "kqx", + "region": 2 + }, { + "name": "Mubako", + "id": "muo", + "region": 2 + }, { + "name": "Mubi", + "id": "mub", + "region": 2 + }, { + "name": "Mugom (Tibetan)", + "id": "muk-tibt", + "region": 3 + }, { + "name": "Mukulu", + "id": "moz", + "region": 2 + }, { + "name": "Mulgi", + "id": "mvh", + "region": 2 + }, { + "name": "Mundabli", + "id": "boe", + "region": 2 + }, { + "name": "Mundang", + "id": "mua", + "region": 2 + }, { + "name": "Mundani", + "id": "mnf", + "region": 2 + }, { + "name": "Mundari (Oriya)", + "id": "unr-orya", + "region": 3 + }, { + "name": "M\u00fcnd\u00fc (Latin)", + "id": "muh-latn", + "region": 2 + }, { + "name": "Mungaka", + "id": "mhk", + "region": 2 + }, { + "name": "Mungbam", + "id": "mij", + "region": 2 + }, { + "name": "Munji (Arabic)", + "id": "mnj-arab", + "region": 3 + }, { + "name": "Munsee (Latin)", + "id": "umu-latn", + "region": 6 + }, { + "name": "Muria, Eastern (Devanagari)", + "id": "emu-deva", + "region": 3 + }, { + "name": "Muria, Far Western (Devanagari)", + "id": "fmu-deva", + "region": 3 + }, { + "name": "Muria, Western (Devanagari)", + "id": "mut-deva", + "region": 3 + }, { + "name": "Mursi (Latin)", + "id": "muz-latn", + "region": 2 + }, { + "name": "Musey", + "id": "mse", + "region": 2 + }, { + "name": "Musgu", + "id": "mug", + "region": 2 + }, { + "name": "Musi (Latin)", + "id": "mui-latn", + "region": 3 + }, { + "name": "Muskum", + "id": "mje", + "region": 2 + }, { + "name": "Muyang", + "id": "muy", + "region": 2 + }, { + "name": "Mvuba (Latin)", + "id": "mxh-latn", + "region": 2 + }, { + "name": "Mwaghavul (Latin)", + "id": "sur-latn", + "region": 2 + }, { + "name": "Mwera (Latin)", + "id": "mjh-latn", + "region": 2 + }, { + "name": "Mwera (Latin)", + "id": "mwe-latn", + "region": 2 + }, { + "name": "N\u2019ko", + "id": "nqo", + "region": 2 + }, { + "name": "Naaba", + "id": "nao", + "region": 3 + }, { + "name": "Naami", + "id": "bzv", + "region": 2 + }, { + "name": "Naba", + "id": "mne", + "region": 2 + }, { + "name": "Naga, Makuri (Latin)", + "id": "jmn-latn", + "region": 3 + }, { + "name": "Naga, Tangshang (Tangsa)", + "id": "nst-tnsa", + "region": 3 + }, { + "name": "Naga, Wancho (Wancho)", + "id": "nnp-wcho", + "region": 3 + }, { + "name": "Nagumi", + "id": "ngv", + "region": 2 + }, { + "name": "Nahuatl, Central", + "id": "nhn", + "region": 6 + }, { + "name": "Nahuatl, Central Huasteca", + "id": "nch", + "region": 6 + }, { + "name": "Nahuatl, Central Puebla", + "id": "ncx", + "region": 6 + }, { + "name": "Nahuatl, Coatepec", + "id": "naz", + "region": 6 + }, { + "name": "Nahuatl, Eastern Durango", + "id": "azd", + "region": 6 + }, { + "name": "Nahuatl, Eastern Huasteca", + "id": "nhe", + "region": 6 + }, { + "name": "Nahuatl, Guerrero", + "id": "ngu", + "region": 6 + }, { + "name": "Nahuatl, Highland Puebla", + "id": "azz", + "region": 6 + }, { + "name": "Nahuatl, Huaxcaleca", + "id": "nhq", + "region": 6 + }, { + "name": "Nahuatl, Isthmus-Cosoleacaque", + "id": "nhk", + "region": 6 + }, { + "name": "Nahuatl, Isthmus-Mecayapan", + "id": "nhx", + "region": 6 + }, { + "name": "Nahuatl, Isthmus-Pajapan", + "id": "nhp", + "region": 6 + }, { + "name": "Nahuatl, Michoac\u00e1n", + "id": "ncl", + "region": 6 + }, { + "name": "Nahuatl, Morelos", + "id": "nhm", + "region": 6 + }, { + "name": "Nahuatl, Northern Oaxaca", + "id": "nhy", + "region": 6 + }, { + "name": "Nahuatl, Northern Puebla", + "id": "ncj", + "region": 6 + }, { + "name": "Nahuatl, Ometepec", + "id": "nht", + "region": 6 + }, { + "name": "Nahuatl, Orizaba", + "id": "nlv", + "region": 6 + }, { + "name": "Nahuatl, Santa Mar\u00eda la Alta", + "id": "nhz", + "region": 6 + }, { + "name": "Nahuatl, Sierra Negra", + "id": "nsu", + "region": 6 + }, { + "name": "Nahuatl, Southeastern Puebla", + "id": "npl", + "region": 6 + }, { + "name": "Nahuatl, Temascaltepec", + "id": "nhv", + "region": 6 + }, { + "name": "Nahuatl, Tetelcingo", + "id": "nhg", + "region": 6 + }, { + "name": "Nahuatl, Tlamacazapa", + "id": "nuz", + "region": 6 + }, { + "name": "Nahuatl, Western Durango", + "id": "azn", + "region": 6 + }, { + "name": "Nahuatl, Western Huasteca", + "id": "nhw", + "region": 6 + }, { + "name": "Nahuatl, Zacatl\u00e1n-Ahuacatl\u00e1n-Tepetzintla", + "id": "nhi", + "region": 6 + }, { + "name": "Naki", + "id": "mff", + "region": 2 + }, { + "name": "Nancere", + "id": "nnc", + "region": 2 + }, { + "name": "Nande (Latin)", + "id": "nnb-latn", + "region": 2 + }, { + "name": "Napoletano-Calabrese", + "id": "nap", + "region": 4 + }, { + "name": "Naskapi (Unified Canadian Aboriginal Syllabics)", + "id": "nsk-cans", + "region": 6 + }, { + "name": "Navajo (Latin)", + "id": "nv-latn", + "region": 6 + }, { + "name": "Naxi (Lisu)", + "id": "nxq-lisu", + "region": 3 + }, { + "name": "Nchane", + "id": "ncr", + "region": 2 + }, { + "name": "Nda\u2019nda\u2019", + "id": "nnz", + "region": 2 + }, { + "name": "Ndai", + "id": "gke", + "region": 2 + }, { + "name": "Ndaka (Latin)", + "id": "ndk-latn", + "region": 2 + }, { + "name": "Ndali (Latin)", + "id": "ndh-latn", + "region": 2 + }, { + "name": "Ndam", + "id": "ndm", + "region": 2 + }, { + "name": "Ndamba (Latin)", + "id": "ndj-latn", + "region": 2 + }, { + "name": "Ndebele", + "id": "nd", + "region": 2 + }, { + "name": "Ndebele (Braille)", + "id": "nr-brai", + "region": 2 + }, { + "name": "Ndemli", + "id": "nml", + "region": 2 + }, { + "name": "Ndendeule (Latin)", + "id": "dne-latn", + "region": 2 + }, { + "name": "Ndengereko (Latin)", + "id": "ndg-latn", + "region": 2 + }, { + "name": "Ndo (Latin)", + "id": "ndp-latn", + "region": 2 + }, { + "name": "Ndonde Hamba (Latin)", + "id": "njd-latn", + "region": 2 + }, { + "name": "Ndoola", + "id": "ndr", + "region": 2 + }, { + "name": "Ndrulo (Latin)", + "id": "dno-latn", + "region": 2 + }, { + "name": "Ndut", + "id": "ndv", + "region": 2 + }, { + "name": "Negeri Sembilan Malay (Latin)", + "id": "zmi-latn", + "region": 3 + }, { + "name": "Nepali (Braille)", + "id": "npi-brai", + "region": 3 + }, { + "name": "Nepali (Devanagari)", + "id": "npi-deva", + "region": 3 + }, { + "name": "Nepali (macrolanguage)", + "id": "ne", + "region": 1 + }, { + "name": "Nepali (macrolanguage) (Braille)", + "id": "ne-brai", + "region": 1 + }, { + "name": "Newar", + "id": "new", + "region": 3 + }, { + "name": "Newar (Newa)", + "id": "new-newa", + "region": 3 + }, { + "name": "Ngam", + "id": "nmc", + "region": 2 + }, { + "name": "Ngamambo", + "id": "nbv", + "region": 2 + }, { + "name": "Ngambay", + "id": "sba", + "region": 2 + }, { + "name": "Ngando (Latin)", + "id": "nxd-latn", + "region": 2 + }, { + "name": "Ngas (Latin)", + "id": "anc-latn", + "region": 2 + }, { + "name": "Ngbandi, Northern (Latin)", + "id": "ngb-latn", + "region": 2 + }, { + "name": "Ngbee (Latin)", + "id": "jgb-latn", + "region": 2 + }, { + "name": "Ngelima (Latin)", + "id": "agh-latn", + "region": 2 + }, { + "name": "Ngemba", + "id": "nge", + "region": 2 + }, { + "name": "Ngete", + "id": "nnn", + "region": 2 + }, { + "name": "Ngie", + "id": "ngj", + "region": 2 + }, { + "name": "Ngiemboon", + "id": "nnh", + "region": 2 + }, { + "name": "Ngindo (Latin)", + "id": "nnq-latn", + "region": 2 + }, { + "name": "Ngiti (Latin)", + "id": "niy-latn", + "region": 2 + }, { + "name": "ngo-latn (Latin)", + "id": "ngo-latn", + "region": 1 + }, { + "name": "Ngomba", + "id": "jgo", + "region": 2 + }, { + "name": "Ngombale", + "id": "nla", + "region": 2 + }, { + "name": "Ngoreme (Latin)", + "id": "ngq-latn", + "region": 2 + }, { + "name": "Ngoshie", + "id": "nsh", + "region": 2 + }, { + "name": "Ngulu (Latin)", + "id": "ngp-latn", + "region": 2 + }, { + "name": "Ngwe", + "id": "nwe", + "region": 2 + }, { + "name": "Ngwo", + "id": "ngn", + "region": 2 + }, { + "name": "Nias (Latin)", + "id": "nia-latn", + "region": 3 + }, { + "name": "Nicaragua Creole English", + "id": "bzk", + "region": 6 + }, { + "name": "Niellim", + "id": "nie", + "region": 2 + }, { + "name": "Nilamba (Latin)", + "id": "nim-latn", + "region": 2 + }, { + "name": "Nimbari", + "id": "nmr", + "region": 2 + }, { + "name": "Nindi (Latin)", + "id": "nxi-latn", + "region": 2 + }, { + "name": "Nisenan", + "id": "nsz", + "region": 6 + }, { + "name": "Nisga\u2019a (Latin)", + "id": "ncg-latn", + "region": 6 + }, { + "name": "Niue", + "id": "niu", + "region": 7 + }, { + "name": "Nizaa", + "id": "sgi", + "region": 2 + }, { + "name": "Njen", + "id": "njj", + "region": 2 + }, { + "name": "Njyem", + "id": "njy", + "region": 2 + }, { + "name": "Nkongho", + "id": "nkc", + "region": 2 + }, { + "name": "Nkonya", + "id": "nko", + "region": 2 + }, { + "name": "Nobiin (Coptic)", + "id": "fia-copt", + "region": 2 + }, { + "name": "Nomaande", + "id": "lem", + "region": 2 + }, { + "name": "Noon", + "id": "snf", + "region": 2 + }, { + "name": "Noon (Latin)", + "id": "snf-latn", + "region": 2 + }, { + "name": "Noone", + "id": "nhu", + "region": 2 + }, { + "name": "Norwegian", + "id": "no", + "region": 4 + }, { + "name": "Norwegian (Runic)", + "id": "no-runr", + "region": 4 + }, { + "name": "Norwegian Bokm\u00e5l", + "id": "nb", + "region": 1 + }, { + "name": "Noy", + "id": "noy", + "region": 2 + }, { + "name": "Ntcham (Braille)", + "id": "bud-brai", + "region": 2 + }, { + "name": "Nubaca", + "id": "baf", + "region": 2 + }, { + "name": "Nuer (Latin)", + "id": "nus-latn", + "region": 2 + }, { + "name": "Nugunu", + "id": "yas", + "region": 2 + }, { + "name": "Numanggang (Latin)", + "id": "nop-latn", + "region": 7 + }, { + "name": "Nuosu", + "id": "ii", + "region": 3 + }, { + "name": "Nuu-chah-nulth (Latin)", + "id": "nuk-latn", + "region": 6 + }, { + "name": "Nya Huba (Latin)", + "id": "hbb-latn", + "region": 2 + }, { + "name": "Nyakyusa-Ngonde (Latin)", + "id": "nyy-latn", + "region": 2 + }, { + "name": "Nyali (Latin)", + "id": "nlj-latn", + "region": 2 + }, { + "name": "Nyambo (Latin)", + "id": "now-latn", + "region": 2 + }, { + "name": "Nyamwezi (Latin)", + "id": "nym-latn", + "region": 2 + }, { + "name": "Nyanga-li (Latin)", + "id": "nyc-latn", + "region": 2 + }, { + "name": "Nyangatom (Latin)", + "id": "nnj-latn", + "region": 2 + }, { + "name": "Nyankore (Latin)", + "id": "nyn-latn", + "region": 2 + }, { + "name": "Nyankpa", + "id": "yes", + "region": 2 + }, { + "name": "Nyaturu (Latin)", + "id": "rim-latn", + "region": 2 + }, { + "name": "Nyiha, Tanzania (Latin)", + "id": "nih-latn", + "region": 2 + }, { + "name": "Nyika, Tanzania (Latin)", + "id": "nkt-latn", + "region": 2 + }, { + "name": "Nyindu (Latin)", + "id": "nyg-latn", + "region": 2 + }, { + "name": "Nyokon", + "id": "nvo", + "region": 2 + }, { + "name": "Nyole (Latin)", + "id": "nuj-latn", + "region": 2 + }, { + "name": "Nyoro (Latin)", + "id": "nyo-latn", + "region": 2 + }, { + "name": "Nzakambay", + "id": "nzy", + "region": 2 + }, { + "name": "Nzakara (Latin)", + "id": "nzk-latn", + "region": 2 + }, { + "name": "Nzanyi", + "id": "nja", + "region": 2 + }, { + "name": "Nzema (Latin)", + "id": "nzi-latn", + "region": 2 + }, { + "name": "Oadki (Arabic)", + "id": "odk-arab", + "region": 3 + }, { + "name": "Oblo", + "id": "obl", + "region": 2 + }, { + "name": "Obolo (Latin)", + "id": "ann-latn", + "region": 2 + }, { + "name": "Occitan", + "id": "oc", + "region": 4 + }, { + "name": "Odia (Braille)", + "id": "ory-brai", + "region": 3 + }, { + "name": "Official Aramaic (700-300 BCE) (Hatran)", + "id": "arc-hatr", + "region": 1 + }, { + "name": "Official Aramaic (700-300 BCE) (Nabataean)", + "id": "arc-nbat", + "region": 1 + }, { + "name": "Official Aramaic (700-300 BCE) (Syriac)", + "id": "arc-syrc", + "region": 1 + }, { + "name": "Ojibwa (Unified Canadian Aboriginal Syllabics)", + "id": "oj-cans", + "region": 1 + }, { + "name": "Ojibwa, Northwestern (Unified Canadian Aboriginal Syllabics)", + "id": "ojb-cans", + "region": 6 + }, { + "name": "Oji-Cree", + "id": "ojs", + "region": 6 + }, { + "name": "Oji-Cree (Unified Canadian Aboriginal Syllabics)", + "id": "ojs-cans", + "region": 6 + }, { + "name": "Okanagan (Latin)", + "id": "oka-latn", + "region": 6 + }, { + "name": "Oku", + "id": "oku", + "region": 2 + }, { + "name": "Old Aramaic (up to 700 BCE) (Syriac)", + "id": "oar-syrc", + "region": 1 + }, { + "name": "Old English (ca. 450-1100)", + "id": "ang", + "region": 1 + }, { + "name": "Old English (ca. 450-1100) (Runic)", + "id": "ang-runr", + "region": 1 + }, { + "name": "Old Hungarian (Old Hungarian)", + "id": "ohu-hung", + "region": 1 + }, { + "name": "Old Norse", + "id": "non", + "region": 1 + }, { + "name": "Old Norse (Runic)", + "id": "non-runr", + "region": 1 + }, { + "name": "Old Tibetan (Tibetan)", + "id": "otb-tibt", + "region": 1 + }, { + "name": "Old Turkish (Old Turkic)", + "id": "otk-orkh", + "region": 1 + }, { + "name": "Ombo (Latin)", + "id": "oml-latn", + "region": 2 + }, { + "name": "Omi (Latin)", + "id": "omi-latn", + "region": 2 + }, { + "name": "Oneida (Latin)", + "id": "one-latn", + "region": 6 + }, { + "name": "Oniyan", + "id": "bsc", + "region": 2 + }, { + "name": "Onondaga (Latin)", + "id": "ono-latn", + "region": 6 + }, { + "name": "Oorlams", + "id": "oor", + "region": 2 + }, { + "name": "Oriya (macrolanguage)", + "id": "or", + "region": 1 + }, { + "name": "Oriya (macrolanguage) (Braille)", + "id": "or-brai", + "region": 1 + }, { + "name": "Orma", + "id": "orc", + "region": 2 + }, { + "name": "Orma (Latin)", + "id": "orc-latn", + "region": 2 + }, { + "name": "Ormuri (Arabic)", + "id": "oru-arab", + "region": 3 + }, { + "name": "Oroko", + "id": "bdu", + "region": 2 + }, { + "name": "Oromo", + "id": "om", + "region": 1 + }, { + "name": "Osage", + "id": "osa", + "region": 6 + }, { + "name": "Osage (Osage)", + "id": "osa-osge", + "region": 6 + }, { + "name": "Osatu", + "id": "ost", + "region": 2 + }, { + "name": "Oshiwambo (Braille)", + "id": "kj-brai", + "region": 2 + }, { + "name": "Osing (Javanese)", + "id": "osi-java", + "region": 3 + }, { + "name": "P\u00e1ez", + "id": "pbb", + "region": 6 + }, { + "name": "Pagibete (Latin)", + "id": "pae-latn", + "region": 2 + }, { + "name": "Pahari-Potwari", + "id": "phr", + "region": 3 + }, { + "name": "Pahari-Potwari (Arabic)", + "id": "phr-arab", + "region": 3 + }, { + "name": "Palatinate Franconian", + "id": "pfl", + "region": 4 + }, { + "name": "Palawano, Brooke\u2019s Point (Latin)", + "id": "plw-latn", + "region": 3 + }, { + "name": "Palawano, Central (Latin)", + "id": "plc-latn", + "region": 3 + }, { + "name": "Palawano, Southwest (Latin)", + "id": "plv-latn", + "region": 3 + }, { + "name": "Paloor", + "id": "fap", + "region": 2 + }, { + "name": "Palula", + "id": "phl", + "region": 3 + }, { + "name": "Palula (Arabic)", + "id": "phl-arab", + "region": 3 + }, { + "name": "Pam", + "id": "pmn", + "region": 2 + }, { + "name": "Pambia (Latin)", + "id": "pmb-latn", + "region": 2 + }, { + "name": "Pana", + "id": "pnz", + "region": 2 + }, { + "name": "Pangasinan (Braille)", + "id": "pag-brai", + "region": 3 + }, { + "name": "Pangasinan (Latin)", + "id": "pag-latn", + "region": 3 + }, { + "name": "Pangu", + "id": "png", + "region": 2 + }, { + "name": "Pangwa (Latin)", + "id": "pbr-latn", + "region": 2 + }, { + "name": "Papel", + "id": "pbo", + "region": 2 + }, { + "name": "Papiamentu", + "id": "pap", + "region": 6 + }, { + "name": "Paranan (Latin)", + "id": "prf-latn", + "region": 3 + }, { + "name": "Parkwa", + "id": "pbi", + "region": 2 + }, { + "name": "Pashai, Southeast (Arabic)", + "id": "psi-arab", + "region": 3 + }, { + "name": "Pashto, Central (Arabic)", + "id": "pst-arab", + "region": 3 + }, { + "name": "Pashto, Southern (Arabic)", + "id": "pbt-arab", + "region": 3 + }, { + "name": "Pattani (Tibetan)", + "id": "lae-tibt", + "region": 3 + }, { + "name": "Pengo (Oriya)", + "id": "peg-orya", + "region": 3 + }, { + "name": "Pere", + "id": "pfe", + "region": 2 + }, { + "name": "Persian", + "id": "fa", + "region": 1 + }, { + "name": "Persian, Iranian", + "id": "pes", + "region": 3 + }, { + "name": "Persian, Iranian (Arabic)", + "id": "pes-arab", + "region": 3 + }, { + "name": "P\u00e9v\u00e9", + "id": "lme", + "region": 2 + }, { + "name": "Phuie (Latin)", + "id": "pug-latn", + "region": 2 + }, { + "name": "Picard", + "id": "pcd", + "region": 4 + }, { + "name": "Pidgin, Cameroon", + "id": "wes", + "region": 2 + }, { + "name": "Pidgin, Cameroon (Latin)", + "id": "wes-latn", + "region": 2 + }, { + "name": "Piedmontese", + "id": "pms", + "region": 4 + }, { + "name": "Pijin", + "id": "pis", + "region": 7 + }, { + "name": "Pimbwe (Latin)", + "id": "piw-latn", + "region": 2 + }, { + "name": "Pingelapese (Latin)", + "id": "pif-latn", + "region": 7 + }, { + "name": "Pintupi-Luritja (Latin)", + "id": "piu-latn", + "region": 7 + }, { + "name": "Pinyin", + "id": "pny", + "region": 2 + }, { + "name": "Pitjantjatjara (Latin)", + "id": "pjt-latn", + "region": 7 + }, { + "name": "Plautdietsch", + "id": "pdt", + "region": 6 + }, { + "name": "Pogolo (Latin)", + "id": "poy-latn", + "region": 2 + }, { + "name": "Poke (Latin)", + "id": "pof-latn", + "region": 2 + }, { + "name": "P\u00f6koot (Latin)", + "id": "pko-latn", + "region": 2 + }, { + "name": "Pol", + "id": "pmm", + "region": 2 + }, { + "name": "Polish", + "id": "pl", + "region": 4 + }, { + "name": "Polish (Braille)", + "id": "pl-brai", + "region": 4 + }, { + "name": "Pontic (Greek)", + "id": "pnt-grek", + "region": 4 + }, { + "name": "Portuguese", + "id": "pt", + "region": 4 + }, { + "name": "Portuguese (Brazil)", + "id": "pt-br", + "region": 4 + }, { + "name": "Portuguese (Braille)", + "id": "pt-brai", + "region": 4 + }, { + "name": "Potawatomi (Latin)", + "id": "pot-latn", + "region": 6 + }, { + "name": "Primitive Irish (Ogham)", + "id": "pgl-ogam", + "region": 1 + }, { + "name": "Prussian", + "id": "prg", + "region": 4 + }, { + "name": "Psikye", + "id": "kvj", + "region": 2 + }, { + "name": "Pukapuka (Latin)", + "id": "pkp-latn", + "region": 7 + }, { + "name": "Pulaar (Latin)", + "id": "fuc-latn", + "region": 2 + }, { + "name": "Pular", + "id": "fuf", + "region": 2 + }, { + "name": "Pular (Adlam)", + "id": "fuf-adlm", + "region": 2 + }, { + "name": "Pular (Arabic)", + "id": "fuf-arab", + "region": 2 + }, { + "name": "Pular (Latin)", + "id": "fuf-latn", + "region": 2 + }, { + "name": "Punjabi, Eastern", + "id": "pa", + "region": 3 + }, { + "name": "Punjabi, Western", + "id": "pnb", + "region": 3 + }, { + "name": "Punjabi, Western (Arabic)", + "id": "pnb-arab", + "region": 3 + }, { + "name": "Purik (Tibetan)", + "id": "prx-tibt", + "region": 3 + }, { + "name": "Pushto", + "id": "ps", + "region": 1 + }, { + "name": "Qimant", + "id": "ahg", + "region": 2 + }, { + "name": "Quechua", + "id": "qu", + "region": 1 + }, { + "name": "Quechua (Bolivia)", + "id": "qu-bo", + "region": 1 + }, { + "name": "Quechua, Ambo-Pasco", + "id": "qva", + "region": 6 + }, { + "name": "Quechua, Arequipa-La Uni\u00f3n", + "id": "qxu", + "region": 6 + }, { + "name": "Quechua, Ayacucho", + "id": "quy", + "region": 6 + }, { + "name": "Quechua, Cajamarca", + "id": "qvc", + "region": 6 + }, { + "name": "Quechua, Cajatambo North Lima", + "id": "qvl", + "region": 6 + }, { + "name": "Quechua, Chachapoyas", + "id": "quk", + "region": 6 + }, { + "name": "Quechua, Chaupihuaranga", + "id": "qur", + "region": 6 + }, { + "name": "Quechua, Chincha", + "id": "qxc", + "region": 6 + }, { + "name": "Quechua, Chiqui\u00e1n", + "id": "qxa", + "region": 6 + }, { + "name": "Quechua, Corongo Ancash", + "id": "qwa", + "region": 6 + }, { + "name": "Quechua, Eastern Apur\u00edmac", + "id": "qve", + "region": 6 + }, { + "name": "Quechua, Huallaga", + "id": "qub", + "region": 6 + }, { + "name": "Quechua, Huamal\u00edes-Dos de Mayo Hu\u00e1nuco", + "id": "qvh", + "region": 6 + }, { + "name": "Quechua, Huaylas Ancash", + "id": "qwh", + "region": 6 + }, { + "name": "Quechua, Huaylla Wanca", + "id": "qvw", + "region": 6 + }, { + "name": "Quechua, Jauja Wanca", + "id": "qxw", + "region": 6 + }, { + "name": "Quechua, Lambayeque", + "id": "quf", + "region": 6 + }, { + "name": "Quechua, Margos-Yarowilca-Lauricocha", + "id": "qvm", + "region": 6 + }, { + "name": "Quechua, North Bolivian", + "id": "qul", + "region": 6 + }, { + "name": "Quechua, North Jun\u00edn", + "id": "qvn", + "region": 6 + }, { + "name": "Quechua, Northern Conchucos Ancash", + "id": "qxn", + "region": 6 + }, { + "name": "Quechua, Pacaraos", + "id": "qvp", + "region": 6 + }, { + "name": "Quechua, Panao", + "id": "qxh", + "region": 6 + }, { + "name": "Quechua, Puno", + "id": "qxp", + "region": 6 + }, { + "name": "Quechua, San Mart\u00edn", + "id": "qvs", + "region": 6 + }, { + "name": "Quechua, Santa Ana de Tusi Pasco", + "id": "qxt", + "region": 6 + }, { + "name": "Quechua, Sihuas Ancash", + "id": "qws", + "region": 6 + }, { + "name": "Quechua, Southern Conchucos", + "id": "qxo", + "region": 6 + }, { + "name": "Quechua, Southern Pastaza", + "id": "qup", + "region": 6 + }, { + "name": "Quechua, Yauyos", + "id": "qux", + "region": 6 + }, { + "name": "Quichua, Calder\u00f3n Highland", + "id": "qud", + "region": 6 + }, { + "name": "Quichua, Ca\u00f1ar Highland", + "id": "qxr", + "region": 6 + }, { + "name": "Quichua, Chimborazo Highland", + "id": "qug", + "region": 6 + }, { + "name": "Quichua, Imbabura Highland", + "id": "qvi", + "region": 6 + }, { + "name": "Quichua, Loja Highland", + "id": "qvj", + "region": 6 + }, { + "name": "Quichua, Napo", + "id": "qvo", + "region": 6 + }, { + "name": "Quichua, Northern Pastaza", + "id": "qvz", + "region": 6 + }, { + "name": "Quichua, Salasaca Highland", + "id": "qxl", + "region": 6 + }, { + "name": "Quichua, Santiago del Estero", + "id": "qus", + "region": 6 + }, { + "name": "Quichua, Tena Lowland", + "id": "quw", + "region": 6 + }, { + "name": "Quinault (Latin)", + "id": "qun-latn", + "region": 6 + }, { + "name": "Rade (Latin)", + "id": "rad-latn", + "region": 3 + }, { + "name": "Rajasthani (Devanagari)", + "id": "raj-deva", + "region": 1 + }, { + "name": "Rajbanshi", + "id": "rjs", + "region": 3 + }, { + "name": "Rawang (Latin)", + "id": "raw-latn", + "region": 3 + }, { + "name": "Rejang (Rejang)", + "id": "rej-rjng", + "region": 3 + }, { + "name": "Rendille (Latin)", + "id": "rel-latn", + "region": 2 + }, { + "name": "R\u00e9union Creole French", + "id": "rcf", + "region": 2 + }, { + "name": "Rigwe (Latin)", + "id": "iri-latn", + "region": 2 + }, { + "name": "Ripuarian", + "id": "ksh", + "region": 4 + }, { + "name": "Rohingya (Arabic)", + "id": "rhg-arab", + "region": 3 + }, { + "name": "Romagnol", + "id": "rgn", + "region": 4 + }, { + "name": "Romani, Balkan", + "id": "rmn", + "region": 4 + }, { + "name": "Romani, Carpathian", + "id": "rmc", + "region": 4 + }, { + "name": "Romani, Kalo Finnish", + "id": "rmf", + "region": 4 + }, { + "name": "Romani, Sinte", + "id": "rmo", + "region": 4 + }, { + "name": "Romani, Welsh", + "id": "rmw", + "region": 4 + }, { + "name": "Romanian", + "id": "ro", + "region": 4 + }, { + "name": "Romanian (Braille)", + "id": "ro-brai", + "region": 4 + }, { + "name": "Romanian, Megleno", + "id": "ruq", + "region": 4 + }, { + "name": "Romansh", + "id": "rm", + "region": 4 + }, { + "name": "Romany", + "id": "rom", + "region": 1 + }, { + "name": "Romblomanon (Latin)", + "id": "rol-latn", + "region": 3 + }, { + "name": "Rombo (Latin)", + "id": "rof-latn", + "region": 2 + }, { + "name": "Rotokas (Latin)", + "id": "roo-latn", + "region": 7 + }, { + "name": "Rufiji (Latin)", + "id": "rui-latn", + "region": 2 + }, { + "name": "Rundi (Braille)", + "id": "rn-brai", + "region": 2 + }, { + "name": "Runga", + "id": "rou", + "region": 2 + }, { + "name": "Rungwa (Latin)", + "id": "rnw-latn", + "region": 2 + }, { + "name": "Ruruuli-Runyala (Latin)", + "id": "ruc-latn", + "region": 2 + }, { + "name": "Russian", + "id": "ru", + "region": 4 + }, { + "name": "Russian (Braille)", + "id": "ru-brai", + "region": 4 + }, { + "name": "Rwa (Latin)", + "id": "rwk-latn", + "region": 2 + }, { + "name": "Saafi-Saafi", + "id": "sav", + "region": 2 + }, { + "name": "Saami, Inari", + "id": "smn", + "region": 4 + }, { + "name": "Saami, Lule", + "id": "smj", + "region": 4 + }, { + "name": "Saami, North", + "id": "se", + "region": 4 + }, { + "name": "Saami, North (Latin)", + "id": "se-latn", + "region": 4 + }, { + "name": "Saami, Skolt", + "id": "sms", + "region": 4 + }, { + "name": "Saami, South", + "id": "sma", + "region": 4 + }, { + "name": "Saami, South (Latin)", + "id": "sma-latn", + "region": 4 + }, { + "name": "Saami, Ume", + "id": "sju", + "region": 4 + }, { + "name": "Saamya-Gwe (Latin)", + "id": "lsm-latn", + "region": 2 + }, { + "name": "Saba", + "id": "saa", + "region": 2 + }, { + "name": "Sabaean (Old South Arabian)", + "id": "xsa-sarb", + "region": 1 + }, { + "name": "Sabaot (Latin)", + "id": "spy-latn", + "region": 2 + }, { + "name": "Sadri (Devanagari)", + "id": "sck-deva", + "region": 3 + }, { + "name": "Sadri, Oraon (Devanagari)", + "id": "sdr-deva", + "region": 3 + }, { + "name": "Safwa (Latin)", + "id": "sbk-latn", + "region": 2 + }, { + "name": "Sagala (Latin)", + "id": "sbm-latn", + "region": 2 + }, { + "name": "Saho (Latin)", + "id": "ssy-latn", + "region": 2 + }, { + "name": "Sahu (Latin)", + "id": "saj-latn", + "region": 3 + }, { + "name": "Saint Lucian Creole French", + "id": "acf", + "region": 6 + }, { + "name": "Salish, Straits", + "id": "str", + "region": 6 + }, { + "name": "Sama, Balangingih (Latin)", + "id": "sse-latn", + "region": 3 + }, { + "name": "Sama, Central (Latin)", + "id": "sml-latn", + "region": 3 + }, { + "name": "Sama, Pangutaran (Latin)", + "id": "slm-latn", + "region": 3 + }, { + "name": "Sama, Southern (Latin)", + "id": "ssb-latn", + "region": 3 + }, { + "name": "Samaritan Aramaic (Syriac)", + "id": "sam-syrc", + "region": 3 + }, { + "name": "Samba Leko", + "id": "ndi", + "region": 2 + }, { + "name": "Sambal (Latin)", + "id": "xsb-latn", + "region": 3 + }, { + "name": "Sambal, Botolan (Latin)", + "id": "sbl-latn", + "region": 3 + }, { + "name": "Sambalpuri (Oriya)", + "id": "spv-orya", + "region": 3 + }, { + "name": "Samoan", + "id": "sm", + "region": 7 + }, { + "name": "Sampang", + "id": "rav", + "region": 3 + }, { + "name": "Sanga (Braille)", + "id": "sng-brai", + "region": 2 + }, { + "name": "Sangil (Latin)", + "id": "snl-latn", + "region": 3 + }, { + "name": "Sango", + "id": "sg", + "region": 2 + }, { + "name": "Sangu (Latin)", + "id": "sbp-latn", + "region": 2 + }, { + "name": "Sanskrit", + "id": "sa", + "region": 3 + }, { + "name": "Sanskrit (Bhaiksuki)", + "id": "sa-bhks", + "region": 3 + }, { + "name": "Sanskrit (Brahmi)", + "id": "sa-brah", + "region": 3 + }, { + "name": "Sanskrit (Devanagari)", + "id": "sa-deva", + "region": 3 + }, { + "name": "Sanskrit (Kharoshthi)", + "id": "sa-khar", + "region": 3 + }, { + "name": "Sanskrit (Latin)", + "id": "sa-latn", + "region": 3 + }, { + "name": "Sanskrit (Myanmar)", + "id": "sa-mymr", + "region": 3 + }, { + "name": "Sanskrit (Siddham)", + "id": "sa-sidd", + "region": 3 + }, { + "name": "Sanskrit (Tirhuta)", + "id": "sa-tirh", + "region": 3 + }, { + "name": "Santhali (Latin)", + "id": "sat-latn", + "region": 3 + }, { + "name": "Santhali (Ol Chiki)", + "id": "sat-olck", + "region": 3 + }, { + "name": "Santhali (Oriya)", + "id": "sat-orya", + "region": 3 + }, { + "name": "Sapo (Latin)", + "id": "krn-latn", + "region": 2 + }, { + "name": "Sar", + "id": "mwm", + "region": 2 + }, { + "name": "Saraiki (Arabic)", + "id": "skr-arab", + "region": 3 + }, { + "name": "Saraiki (Gurmukhi)", + "id": "skr-guru", + "region": 3 + }, { + "name": "Saraiki (Multani)", + "id": "skr-mult", + "region": 3 + }, { + "name": "Saramaccan", + "id": "srm", + "region": 6 + }, { + "name": "Sardinian, Campidanese", + "id": "sro", + "region": 4 + }, { + "name": "Sardinian, Gallurese", + "id": "sdn", + "region": 4 + }, { + "name": "Sardinian, Sassarese", + "id": "sdc", + "region": 4 + }, { + "name": "Sari", + "id": "asj", + "region": 2 + }, { + "name": "Sarsi (Latin)", + "id": "srs-latn", + "region": 6 + }, { + "name": "Sarua", + "id": "swy", + "region": 2 + }, { + "name": "Sasak (Javanese)", + "id": "sas-java", + "region": 3 + }, { + "name": "Saterfriesisch", + "id": "stq", + "region": 4 + }, { + "name": "Sauria Paharia (Devanagari)", + "id": "mjt-deva", + "region": 3 + }, { + "name": "Saxon, East Frisian Low", + "id": "frs", + "region": 4 + }, { + "name": "Saxon, Low", + "id": "nds", + "region": 4 + }, { + "name": "Saxon, Low (Runic)", + "id": "nds-runr", + "region": 4 + }, { + "name": "Saxon, Upper", + "id": "sxu", + "region": 4 + }, { + "name": "Saxon, Upper (Runic)", + "id": "sxu-runr", + "region": 4 + }, { + "name": "Scots", + "id": "sco", + "region": 4 + }, { + "name": "Scots (Runic)", + "id": "sco-runr", + "region": 4 + }, { + "name": "Scottish Gaelic", + "id": "gd", + "region": 4 + }, { + "name": "Scottish Gaelic (Latin)", + "id": "gd-latn", + "region": 4 + }, { + "name": "Sea Island Creole English", + "id": "gul", + "region": 6 + }, { + "name": "Sebat Bet Gurage", + "id": "sgw", + "region": 2 + }, { + "name": "Sebat Bet Gurage (Ethiopic)", + "id": "sgw-ethi", + "region": 2 + }, { + "name": "Sechelt (Latin)", + "id": "sec-latn", + "region": 6 + }, { + "name": "Segeju (Latin)", + "id": "seg-latn", + "region": 2 + }, { + "name": "Sekani (Latin)", + "id": "sek-latn", + "region": 6 + }, { + "name": "Seke", + "id": "skj", + "region": 3 + }, { + "name": "Senaya (Syriac)", + "id": "syn-syrc", + "region": 3 + }, { + "name": "Seneca (Latin)", + "id": "see-latn", + "region": 6 + }, { + "name": "S\u00e9noufo, Mamara (Latin)", + "id": "myk-latn", + "region": 2 + }, { + "name": "S\u00e9noufo, Shempire (Latin)", + "id": "seb-latn", + "region": 2 + }, { + "name": "S\u00e9noufo, Supyire (Latin)", + "id": "spp-latn", + "region": 2 + }, { + "name": "S\u00e9noufo, Syenara (Latin)", + "id": "shz-latn", + "region": 2 + }, { + "name": "Serbian", + "id": "sr", + "region": 4 + }, { + "name": "Serbian (Braille)", + "id": "sr-brai", + "region": 4 + }, { + "name": "Serbian (Latin)", + "id": "sr-latn", + "region": 4 + }, { + "name": "Serbo-Croatian", + "id": "sh", + "region": 1 + }, { + "name": "Sere (Latin)", + "id": "swf-latn", + "region": 2 + }, { + "name": "Serer-Sine", + "id": "srr", + "region": 2 + }, { + "name": "Serer-Sine (Arabic)", + "id": "srr-arab", + "region": 2 + }, { + "name": "Serer-Sine (Latin)", + "id": "srr-latn", + "region": 2 + }, { + "name": "Setswana", + "id": "tn", + "region": 2 + }, { + "name": "Seychelles Creole French", + "id": "crs", + "region": 2 + }, { + "name": "Shambala (Latin)", + "id": "ksb-latn", + "region": 2 + }, { + "name": "Shan", + "id": "shn", + "region": 3 + }, { + "name": "Shan (Myanmar)", + "id": "shn-mymr", + "region": 3 + }, { + "name": "Shan (Tai Le)", + "id": "shn-tale", + "region": 3 + }, { + "name": "Sharwa", + "id": "swq", + "region": 2 + }, { + "name": "Shekkacho (Latin)", + "id": "moy-latn", + "region": 2 + }, { + "name": "Sheko (Latin)", + "id": "she-latn", + "region": 2 + }, { + "name": "Sherbro (Latin)", + "id": "bun-latn", + "region": 2 + }, { + "name": "Sherpa", + "id": "xsr", + "region": 3 + }, { + "name": "Sherpa (Tibetan)", + "id": "xsr-tibt", + "region": 3 + }, { + "name": "Shi (Latin)", + "id": "shr-latn", + "region": 2 + }, { + "name": "Shina", + "id": "scl", + "region": 3 + }, { + "name": "Shina (Arabic, India)", + "id": "scl-arab-in", + "region": 3 + }, { + "name": "Shina (Arabic, Pakistan)", + "id": "scl-arab-pk", + "region": 3 + }, { + "name": "Shona (Braille)", + "id": "sn-brai", + "region": 2 + }, { + "name": "Shubi (Latin)", + "id": "suj-latn", + "region": 2 + }, { + "name": "Shuswap (Latin)", + "id": "shs-latn", + "region": 6 + }, { + "name": "Sicilian", + "id": "scn", + "region": 4 + }, { + "name": "Sidamo", + "id": "sid", + "region": 2 + }, { + "name": "Sidamo (Latin)", + "id": "sid-latn", + "region": 2 + }, { + "name": "Sikkimese (Tibetan)", + "id": "sip-tibt", + "region": 3 + }, { + "name": "Silesian", + "id": "szl", + "region": 4 + }, { + "name": "Silesian, Lower", + "id": "sli", + "region": 4 + }, { + "name": "Silt\u2019e", + "id": "stv", + "region": 2 + }, { + "name": "Sindhi", + "id": "sd", + "region": 3 + }, { + "name": "Sindhi (Arabic)", + "id": "sd-arab", + "region": 3 + }, { + "name": "Sindhi (Gurmukhi)", + "id": "sd-guru", + "region": 3 + }, { + "name": "Sindhi (Khojki)", + "id": "sd-khoj", + "region": 3 + }, { + "name": "Sinhala", + "id": "si", + "region": 3 + }, { + "name": "Sinhala (Braille)", + "id": "si-brai", + "region": 3 + }, { + "name": "Sinyar", + "id": "sys", + "region": 2 + }, { + "name": "Sirion\u00f3", + "id": "srq", + "region": 6 + }, { + "name": "Siuslaw", + "id": "sis", + "region": 6 + }, { + "name": "Slave (Athapascan)", + "id": "den", + "region": 1 + }, { + "name": "Slavey, North", + "id": "scs", + "region": 6 + }, { + "name": "Slavey, North (Latin)", + "id": "scs-latn", + "region": 6 + }, { + "name": "Slavey, South", + "id": "xsl", + "region": 6 + }, { + "name": "Slavey, South (Latin)", + "id": "xsl-latn", + "region": 6 + }, { + "name": "Slovak", + "id": "sk", + "region": 4 + }, { + "name": "Slovak (Braille)", + "id": "sk-brai", + "region": 4 + }, { + "name": "Slovene", + "id": "sl", + "region": 4 + }, { + "name": "Slovene (Braille)", + "id": "sl-brai", + "region": 4 + }, { + "name": "So (Latin)", + "id": "soc-latn", + "region": 2 + }, { + "name": "Soga (Latin)", + "id": "xog-latn", + "region": 2 + }, { + "name": "Sokoro", + "id": "sok", + "region": 2 + }, { + "name": "Somali", + "id": "so", + "region": 2 + }, { + "name": "Somali (Osmanya)", + "id": "so-osma", + "region": 2 + }, { + "name": "Somrai", + "id": "sor", + "region": 2 + }, { + "name": "Songhay, Humburi Senni (Latin)", + "id": "hmb-latn", + "region": 2 + }, { + "name": "Songhay, Koyra Chiini (Latin)", + "id": "khq-latn", + "region": 2 + }, { + "name": "Songhay, Koyraboro Senni (Latin)", + "id": "ses-latn", + "region": 2 + }, { + "name": "Songoora (Latin)", + "id": "sod-latn", + "region": 2 + }, { + "name": "Soninke (Latin)", + "id": "snk-latn", + "region": 2 + }, { + "name": "Soqotri (Arabic)", + "id": "sqt-arab", + "region": 3 + }, { + "name": "Sora (Oriya)", + "id": "srb-orya", + "region": 3 + }, { + "name": "Sora (Sora Sompeng)", + "id": "srb-sora", + "region": 3 + }, { + "name": "Sorbian, Lower", + "id": "dsb", + "region": 4 + }, { + "name": "Sorbian, Upper", + "id": "hsb", + "region": 4 + }, { + "name": "Sotho, Northern (Braille)", + "id": "nso-brai", + "region": 2 + }, { + "name": "Sougb (Latin)", + "id": "mnx-latn", + "region": 3 + }, { + "name": "Spanish", + "id": "es", + "region": 4 + }, { + "name": "Spanish (Braille)", + "id": "es-brai", + "region": 4 + }, { + "name": "Spanish, Charapa", + "id": "spq", + "region": 6 + }, { + "name": "Squamish (Latin)", + "id": "squ-latn", + "region": 6 + }, { + "name": "Sranan Tongo", + "id": "srn", + "region": 6 + }, { + "name": "Stellingwerfs", + "id": "stl", + "region": 4 + }, { + "name": "Stod Bhoti (Tibetan)", + "id": "sbu-tibt", + "region": 3 + }, { + "name": "Stoney (Latin)", + "id": "sto-latn", + "region": 6 + }, { + "name": "Subanen, Central (Latin)", + "id": "syb-latn", + "region": 3 + }, { + "name": "Subanen, Northern (Latin)", + "id": "stb-latn", + "region": 3 + }, { + "name": "Subanen, Southern (Latin)", + "id": "laa-latn", + "region": 3 + }, { + "name": "Subanon, Kolibugan (Latin)", + "id": "skn-latn", + "region": 3 + }, { + "name": "Subanon, Western (Latin)", + "id": "suc-latn", + "region": 3 + }, { + "name": "Suba-Simbiti (Latin)", + "id": "ssc-latn", + "region": 2 + }, { + "name": "Sukuma (Latin)", + "id": "suk-latn", + "region": 2 + }, { + "name": "Sumbwa (Latin)", + "id": "suw-latn", + "region": 2 + }, { + "name": "Sunda (Javanese)", + "id": "su-java", + "region": 3 + }, { + "name": "Sunda (Latin)", + "id": "su-latn", + "region": 3 + }, { + "name": "Sunda (Sundanese)", + "id": "su-sund", + "region": 3 + }, { + "name": "Sunwar", + "id": "suz", + "region": 3 + }, { + "name": "Supapya", + "id": "bbw", + "region": 2 + }, { + "name": "Surbakhal", + "id": "sbj", + "region": 2 + }, { + "name": "Suri (Ethiopic)", + "id": "suq-ethi", + "region": 2 + }, { + "name": "Suri (Latin)", + "id": "suq-latn", + "region": 2 + }, { + "name": "Surigaonon (Latin)", + "id": "sgd-latn", + "region": 3 + }, { + "name": "Swabian", + "id": "swg", + "region": 4 + }, { + "name": "Swahili (Braille)", + "id": "swh-brai", + "region": 2 + }, { + "name": "Swahili (Latin)", + "id": "swh-latn", + "region": 2 + }, { + "name": "Swahili (macrolanguage)", + "id": "sw", + "region": 1 + }, { + "name": "Swahili, Congo (Latin)", + "id": "swc-latn", + "region": 2 + }, { + "name": "Swati", + "id": "ss", + "region": 2 + }, { + "name": "Swedish", + "id": "sv", + "region": 4 + }, { + "name": "Swedish (Braille)", + "id": "sv-brai", + "region": 4 + }, { + "name": "Swedish (Runic)", + "id": "sv-runr", + "region": 4 + }, { + "name": "Swo", + "id": "sox", + "region": 2 + }, { + "name": "Sylheti (Bengali)", + "id": "syl-beng", + "region": 3 + }, { + "name": "Sylheti (Syloti Nagri)", + "id": "syl-sylo", + "region": 3 + }, { + "name": "Syriac (Syriac)", + "id": "syc-syrc", + "region": 3 + }, { + "name": "Syriac (Syriac)", + "id": "syr-syrc", + "region": 1 + }, { + "name": "Syuba", + "id": "syw", + "region": 3 + }, { + "name": "T\u2019apo (Latin)", + "id": "lgn-latn", + "region": 2 + }, { + "name": "Taabwa (Latin)", + "id": "tap-latn", + "region": 2 + }, { + "name": "Tacana", + "id": "tna", + "region": 6 + }, { + "name": "Tachawit (Tifinagh)", + "id": "shy-tfng", + "region": 2 + }, { + "name": "Tachelhit (Latin)", + "id": "shi-latn", + "region": 2 + }, { + "name": "Tadaksahak (Latin)", + "id": "dsq-latn", + "region": 2 + }, { + "name": "Tagabawa (Latin)", + "id": "bgs-latn", + "region": 3 + }, { + "name": "Tagakaulo (Latin)", + "id": "klg-latn", + "region": 3 + }, { + "name": "Tagalog", + "id": "tl", + "region": 3 + }, { + "name": "Tagalog (Braille)", + "id": "tl-brai", + "region": 3 + }, { + "name": "Tagalog (Buhid)", + "id": "tl-buhd", + "region": 3 + }, { + "name": "Tagalog (Hanunoo)", + "id": "tl-hano", + "region": 3 + }, { + "name": "Tagalog (Tagalog)", + "id": "tl-tglg", + "region": 3 + }, { + "name": "Tagargrent (Latin)", + "id": "oua-latn", + "region": 2 + }, { + "name": "Tagbanwa (Hanunoo)", + "id": "tbw-hano", + "region": 3 + }, { + "name": "Tagbanwa (Latin)", + "id": "tbw-latn", + "region": 3 + }, { + "name": "Tagbanwa, Calamian (Hanunoo)", + "id": "tbk-hano", + "region": 3 + }, { + "name": "Tagbanwa, Calamian (Latin)", + "id": "tbk-latn", + "region": 3 + }, { + "name": "Tagbanwa, Central (Hanunoo)", + "id": "tgt-hano", + "region": 3 + }, { + "name": "Tagbanwa, Central (Latin)", + "id": "tgt-latn", + "region": 3 + }, { + "name": "Tagbanwa, Central (Tagbanwa)", + "id": "tgt-tagb", + "region": 3 + }, { + "name": "Tagdal (Tifinagh)", + "id": "tda-tfng", + "region": 2 + }, { + "name": "Tagish (Latin)", + "id": "tgx-latn", + "region": 6 + }, { + "name": "Tahitian (Latin)", + "id": "ty-latn", + "region": 7 + }, { + "name": "Tahltan (Latin)", + "id": "tht-latn", + "region": 6 + }, { + "name": "Tai Dam", + "id": "blt", + "region": 3 + }, { + "name": "Tai Dam (Lao)", + "id": "blt-laoo", + "region": 3 + }, { + "name": "Tai Dam (Latin)", + "id": "blt-latn", + "region": 3 + }, { + "name": "Tai Dam (Tai Viet)", + "id": "blt-tavt", + "region": 3 + }, { + "name": "Tai D\u00f3n (Latin)", + "id": "twh-latn", + "region": 3 + }, { + "name": "Tai D\u00f3n (Tai Viet)", + "id": "twh-tavt", + "region": 3 + }, { + "name": "Tai N\u00fca (Tai Le)", + "id": "tdd-tale", + "region": 3 + }, { + "name": "Tajik (Cyrillic)", + "id": "tg-cyrl", + "region": 3 + }, { + "name": "Tajuasohn (Latin)", + "id": "tja-latn", + "region": 2 + }, { + "name": "Talinga-Bwisi (Latin)", + "id": "tlj-latn", + "region": 2 + }, { + "name": "Tama", + "id": "tma", + "region": 2 + }, { + "name": "Tamahaq, Tahaggart (Tifinagh)", + "id": "thv-tfng", + "region": 2 + }, { + "name": "Tamajaq, Tawallammat (Latin)", + "id": "ttq-latn", + "region": 2 + }, { + "name": "Tamajaq, Tawallammat (Tifinagh)", + "id": "ttq-tfng", + "region": 2 + }, { + "name": "Tamajeq, Tayart (Tifinagh)", + "id": "thz-tfng", + "region": 2 + }, { + "name": "Tamang, Eastern", + "id": "taj", + "region": 3 + }, { + "name": "Tamang, Eastern (Tibetan)", + "id": "taj-tibt", + "region": 3 + }, { + "name": "Tamang, Western", + "id": "tdg", + "region": 3 + }, { + "name": "Tamang, Western (Tibetan)", + "id": "tdg-tibt", + "region": 3 + }, { + "name": "Tamashek", + "id": "tmh", + "region": 1 + }, { + "name": "Tamashek (Latin)", + "id": "tmh-latn", + "region": 1 + }, { + "name": "Tamashek (Tifinagh)", + "id": "tmh-tfng", + "region": 1 + }, { + "name": "Tamasheq (Latin)", + "id": "taq-latn", + "region": 2 + }, { + "name": "Tamasheq (Tifinagh)", + "id": "taq-tfng", + "region": 2 + }, { + "name": "Tamazight, Central Atlas", + "id": "tzm", + "region": 2 + }, { + "name": "Tamazight, Central Atlas (Tifinagh)", + "id": "tzm-tfng", + "region": 2 + }, { + "name": "Tamazight, Central Atlas (Tifinagh, Morocco)", + "id": "tzm-tfng-ma", + "region": 2 + }, { + "name": "Tamazight, Standard Moroccan (Tifinagh)", + "id": "zgh-tfng", + "region": 2 + }, { + "name": "Tamil", + "id": "ta", + "region": 3 + }, { + "name": "Tamil (Braille)", + "id": "ta-brai", + "region": 3 + }, { + "name": "Tamil (Latin)", + "id": "ta-latn", + "region": 3 + }, { + "name": "Tamki", + "id": "tax", + "region": 2 + }, { + "name": "Tampuan (Khmer)", + "id": "tpu-khmr", + "region": 3 + }, { + "name": "Tanacross (Latin)", + "id": "tcb-latn", + "region": 6 + }, { + "name": "Tanana, Upper (Latin)", + "id": "tau-latn", + "region": 6 + }, { + "name": "Tarifit (Tifinagh)", + "id": "rif-tfng", + "region": 2 + }, { + "name": "Tatar (Cyrillic)", + "id": "tt-cyrl", + "region": 4 + }, { + "name": "Tausug (Latin)", + "id": "tsg-latn", + "region": 3 + }, { + "name": "Tawbuid (Latin)", + "id": "twb-latn", + "region": 3 + }, { + "name": "Tayo", + "id": "cks", + "region": 7 + }, { + "name": "Tboli (Latin)", + "id": "tbl-latn", + "region": 3 + }, { + "name": "Tedaga", + "id": "tuq", + "region": 2 + }, { + "name": "Teduray (Latin)", + "id": "tiy-latn", + "region": 3 + }, { + "name": "Telugu", + "id": "te", + "region": 3 + }, { + "name": "Telugu (Braille)", + "id": "te-brai", + "region": 3 + }, { + "name": "Tem (Arabic)", + "id": "kdh-arab", + "region": 2 + }, { + "name": "Tembo (Latin)", + "id": "tbt-latn", + "region": 2 + }, { + "name": "Temi (Latin)", + "id": "soz-latn", + "region": 2 + }, { + "name": "Tengger (Javanese)", + "id": "tes-java", + "region": 3 + }, { + "name": "Tepehuan, Southeastern (Latin)", + "id": "stp-latn", + "region": 6 + }, { + "name": "Tepehuan, Southwestern (Latin)", + "id": "tla-latn", + "region": 6 + }, { + "name": "Termanu (Latin)", + "id": "twu-latn", + "region": 3 + }, { + "name": "Tetun (Latin)", + "id": "tet-latn", + "region": 3 + }, { + "name": "Thai", + "id": "th", + "region": 3 + }, { + "name": "Thai (Braille)", + "id": "th-brai", + "region": 3 + }, { + "name": "Thangmi", + "id": "thf", + "region": 3 + }, { + "name": "Tharu, Dangaura", + "id": "thl", + "region": 3 + }, { + "name": "Tharu, Madhya Ksetriya", + "id": "the", + "region": 3 + }, { + "name": "Tharu, Madhya-Purbiya", + "id": "thq", + "region": 3 + }, { + "name": "Themne", + "id": "tem", + "region": 2 + }, { + "name": "Thompson (Latin)", + "id": "thp-latn", + "region": 6 + }, { + "name": "Thulung", + "id": "tdh", + "region": 3 + }, { + "name": "Tibea", + "id": "ngy", + "region": 2 + }, { + "name": "Tibetan, Amdo (Tibetan)", + "id": "adx-tibt", + "region": 3 + }, { + "name": "Tibetan, Central (Phags-pa, China)", + "id": "bo-phag-cn", + "region": 3 + }, { + "name": "Tibetan, Central (Tibetan)", + "id": "bo-tibt", + "region": 3 + }, { + "name": "Tibetan, Central (Tibetan, China)", + "id": "bo-tibt-cn", + "region": 3 + }, { + "name": "Tibetan, Khams (Tibetan)", + "id": "khg-tibt", + "region": 3 + }, { + "name": "Tigr\u00e9", + "id": "tig", + "region": 2 + }, { + "name": "Tigr\u00e9 (Ethiopic)", + "id": "tig-ethi", + "region": 2 + }, { + "name": "Tigrigna", + "id": "ti", + "region": 2 + }, { + "name": "Tigrigna (Ethiopia)", + "id": "ti-et", + "region": 2 + }, { + "name": "Tikar", + "id": "tik", + "region": 2 + }, { + "name": "Tinani (Tibetan)", + "id": "lbf-tibt", + "region": 3 + }, { + "name": "Tiv", + "id": "tiv", + "region": 2 + }, { + "name": "Tiv (Latin)", + "id": "tiv-latn", + "region": 2 + }, { + "name": "Tlicho", + "id": "dgr", + "region": 6 + }, { + "name": "Tlicho (Latin)", + "id": "dgr-latn", + "region": 6 + }, { + "name": "Tlingit", + "id": "tli", + "region": 6 + }, { + "name": "To", + "id": "toz", + "region": 2 + }, { + "name": "Toba (Latin)", + "id": "tob-latn", + "region": 6 + }, { + "name": "Tobanga", + "id": "tng", + "region": 2 + }, { + "name": "Tohono O\u2019odham (Latin)", + "id": "ood-latn", + "region": 6 + }, { + "name": "Tok Pisin", + "id": "tpi", + "region": 7 + }, { + "name": "Tongan", + "id": "to", + "region": 7 + }, { + "name": "Tongwe (Latin)", + "id": "tny-latn", + "region": 2 + }, { + "name": "Tooro (Latin)", + "id": "ttj-latn", + "region": 2 + }, { + "name": "Toraja-Sa\u2019dan (Latin)", + "id": "sda-latn", + "region": 3 + }, { + "name": "Toram", + "id": "trj", + "region": 2 + }, { + "name": "Torres Strait Creole", + "id": "tcs", + "region": 7 + }, { + "name": "Torwali (Arabic)", + "id": "trw-arab", + "region": 3 + }, { + "name": "Toto", + "id": "txo", + "region": 3 + }, { + "name": "Tsakonian", + "id": "tsd", + "region": 4 + }, { + "name": "Tsakonian (Greek)", + "id": "tsd-grek", + "region": 4 + }, { + "name": "Tsamai (Latin)", + "id": "tsb-latn", + "region": 2 + }, { + "name": "Tshangla", + "id": "tsj", + "region": 3 + }, { + "name": "Tshangla (Tibetan)", + "id": "tsj-tibt", + "region": 3 + }, { + "name": "Tsikimba (Latin)", + "id": "kdl-latn", + "region": 2 + }, { + "name": "Tsimshian (Latin)", + "id": "tsi-latn", + "region": 6 + }, { + "name": "Tsonga (Braille)", + "id": "ts-brai", + "region": 2 + }, { + "name": "Tsuvan", + "id": "tsh", + "region": 2 + }, { + "name": "Tuki", + "id": "bag", + "region": 2 + }, { + "name": "Tumak", + "id": "tmc", + "region": 2 + }, { + "name": "Tumzabt (Tifinagh)", + "id": "mzb-tfng", + "region": 2 + }, { + "name": "Tunen", + "id": "tvu", + "region": 2 + }, { + "name": "Tunia", + "id": "tug", + "region": 2 + }, { + "name": "Tuotomb", + "id": "ttf", + "region": 2 + }, { + "name": "Tupuri", + "id": "tui", + "region": 2 + }, { + "name": "Turkish", + "id": "tr", + "region": 3 + }, { + "name": "Turkish (Braille)", + "id": "tr-brai", + "region": 3 + }, { + "name": "Turkmen", + "id": "tk", + "region": 3 + }, { + "name": "Turkmen (Cyrillic)", + "id": "tk-cyrl", + "region": 3 + }, { + "name": "Turkmen (Latin)", + "id": "tk-latn", + "region": 3 + }, { + "name": "Turoyo (Syriac)", + "id": "tru-syrc", + "region": 3 + }, { + "name": "Tuscarora (Latin)", + "id": "tus-latn", + "region": 6 + }, { + "name": "Tutchone, Northern (Latin)", + "id": "ttm-latn", + "region": 6 + }, { + "name": "Tutchone, Southern (Latin)", + "id": "tce-latn", + "region": 6 + }, { + "name": "Twendi", + "id": "twn", + "region": 2 + }, { + "name": "Twents", + "id": "twd", + "region": 4 + }, { + "name": "Tyap (Latin)", + "id": "kcg-latn", + "region": 2 + }, { + "name": "Ubi", + "id": "ubi", + "region": 2 + }, { + "name": "Udi (Latin)", + "id": "udi-latn", + "region": 3 + }, { + "name": "Ukrainian", + "id": "uk", + "region": 4 + }, { + "name": "Ukwuani-Aboh-Ndoni", + "id": "ukw", + "region": 2 + }, { + "name": "Ulithian (Latin)", + "id": "uli-latn", + "region": 7 + }, { + "name": "Umatilla", + "id": "uma", + "region": 6 + }, { + "name": "Undetermined", + "id": "und", + "region": 1 + }, { + "name": "Undetermined", + "id": "und-fonipa", + "region": 1 + }, { + "name": "Undetermined (Latin)", + "id": "und-latn", + "region": 1 + }, { + "name": "Urdu", + "id": "ur", + "region": 3 + }, { + "name": "Urdu (Braille)", + "id": "ur-brai", + "region": 3 + }, { + "name": "Urhobo", + "id": "urh", + "region": 2 + }, { + "name": "Usaghade", + "id": "usk", + "region": 2 + }, { + "name": "Ushojo (Arabic)", + "id": "ush-arab", + "region": 3 + }, { + "name": "Uyghur (Arabic)", + "id": "ug-arab", + "region": 3 + }, { + "name": "Uzbek (Braille)", + "id": "uz-brai", + "region": 1 + }, { + "name": "Uzbek (Latin)", + "id": "uz-latn", + "region": 1 + }, { + "name": "Uzbek, Northern (Braille)", + "id": "uzn-brai", + "region": 3 + }, { + "name": "Uzbek, Northern (Cyrillic)", + "id": "uzn-cyrl", + "region": 3 + }, { + "name": "Vai (Latin)", + "id": "vai-latn", + "region": 2 + }, { + "name": "Vai (Vai)", + "id": "vai-vaii", + "region": 2 + }, { + "name": "Vame", + "id": "mlr", + "region": 2 + }, { + "name": "Vanuma (Latin)", + "id": "vau-latn", + "region": 2 + }, { + "name": "Vemgo-Mabas", + "id": "vem", + "region": 2 + }, { + "name": "Venda (Braille)", + "id": "ve-brai", + "region": 2 + }, { + "name": "Venetian", + "id": "vec", + "region": 4 + }, { + "name": "Venetian (Latin)", + "id": "vec-latn", + "region": 4 + }, { + "name": "Vengo", + "id": "bav", + "region": 2 + }, { + "name": "Veps", + "id": "vep", + "region": 4 + }, { + "name": "Verre", + "id": "ver", + "region": 2 + }, { + "name": "Vidunda (Latin)", + "id": "vid-latn", + "region": 2 + }, { + "name": "Vietnamese", + "id": "vi", + "region": 3 + }, { + "name": "Vietnamese (Braille)", + "id": "vi-brai", + "region": 3 + }, { + "name": "Vincentian Creole English", + "id": "svc", + "region": 6 + }, { + "name": "Vinza (Latin)", + "id": "vin-latn", + "region": 2 + }, { + "name": "Virgin Islands Creole English", + "id": "vic", + "region": 6 + }, { + "name": "Vlaams", + "id": "vls", + "region": 4 + }, { + "name": "Vod", + "id": "vot", + "region": 4 + }, { + "name": "Vunjo (Latin)", + "id": "vun-latn", + "region": 2 + }, { + "name": "Vute", + "id": "vut", + "region": 2 + }, { + "name": "Vwanji (Latin)", + "id": "wbi-latn", + "region": 2 + }, { + "name": "Wakhi (Arabic)", + "id": "wbl-arab", + "region": 3 + }, { + "name": "Wakhi (Cyrillic, Tajikistan)", + "id": "wbl-cyrl-tj", + "region": 3 + }, { + "name": "Wakhi (Latin)", + "id": "wbl-latn", + "region": 3 + }, { + "name": "Wakhi (Latin, Pakistan)", + "id": "wbl-latn-pk", + "region": 3 + }, { + "name": "Walloon", + "id": "wa", + "region": 4 + }, { + "name": "Walser", + "id": "wae", + "region": 4 + }, { + "name": "Walungge", + "id": "ola", + "region": 3 + }, { + "name": "Walungge (Tibetan)", + "id": "ola-tibt", + "region": 3 + }, { + "name": "Wambule", + "id": "wme", + "region": 3 + }, { + "name": "Wamey", + "id": "cou", + "region": 2 + }, { + "name": "Wanda (Latin)", + "id": "wbh-latn", + "region": 2 + }, { + "name": "Wandala", + "id": "mfi", + "region": 2 + }, { + "name": "Waray-Waray (Braille)", + "id": "war-brai", + "region": 3 + }, { + "name": "Waray-Waray (Latin)", + "id": "war-latn", + "region": 3 + }, { + "name": "Wawa", + "id": "www", + "region": 2 + }, { + "name": "Wayuu", + "id": "guc", + "region": 6 + }, { + "name": "Weh", + "id": "weh", + "region": 2 + }, { + "name": "Welsh", + "id": "cy", + "region": 4 + }, { + "name": "Welsh (Braille)", + "id": "cy-brai", + "region": 4 + }, { + "name": "Western Neo-Aramaic (Syriac (Western variant))", + "id": "amw-syrj", + "region": 3 + }, { + "name": "Westphalien", + "id": "wep", + "region": 4 + }, { + "name": "Wolaytta (Latin)", + "id": "wal-latn", + "region": 2 + }, { + "name": "Wolof", + "id": "wo", + "region": 2 + }, { + "name": "Wolof (Arabic)", + "id": "wo-arab", + "region": 2 + }, { + "name": "Wolof (Latin)", + "id": "wo-latn", + "region": 2 + }, { + "name": "Wolof, Gambian", + "id": "wof", + "region": 2 + }, { + "name": "Wumboko", + "id": "bqm", + "region": 2 + }, { + "name": "Wushi", + "id": "bse", + "region": 2 + }, { + "name": "Wuzlam", + "id": "udl", + "region": 2 + }, { + "name": "wya-latn (Latin)", + "id": "wya-latn", + "region": 1 + }, { + "name": "Wymysorys", + "id": "wym", + "region": 4 + }, { + "name": "Xaasongaxango (Latin)", + "id": "kao-latn", + "region": 2 + }, { + "name": "Xamtanga", + "id": "xan", + "region": 2 + }, { + "name": "Xav\u00e1nte", + "id": "xav", + "region": 6 + }, { + "name": "Xhosa", + "id": "xh", + "region": 2 + }, { + "name": "Xhosa (Braille)", + "id": "xh-brai", + "region": 2 + }, { + "name": "Yadgha (Arabic)", + "id": "ydg-arab", + "region": 3 + }, { + "name": "Yakama (Latin)", + "id": "yak-latn", + "region": 6 + }, { + "name": "Yakan (Latin)", + "id": "yka-latn", + "region": 3 + }, { + "name": "Yakkha", + "id": "ybh", + "region": 3 + }, { + "name": "Yakut (Cyrillic, Russian Federation)", + "id": "sah-cyrl-ru", + "region": 4 + }, { + "name": "Yamba", + "id": "yam", + "region": 2 + }, { + "name": "Yambeta", + "id": "yat", + "region": 2 + }, { + "name": "Yaminahua", + "id": "yaa", + "region": 6 + }, { + "name": "Yamphu", + "id": "ybi", + "region": 3 + }, { + "name": "Yangben", + "id": "yav", + "region": 2 + }, { + "name": "Yapese (Latin)", + "id": "yap-latn", + "region": 7 + }, { + "name": "Yasa", + "id": "yko", + "region": 2 + }, { + "name": "Yele", + "id": "yle", + "region": 7 + }, { + "name": "Yemba", + "id": "ybb", + "region": 2 + }, { + "name": "Yemsa", + "id": "jnj", + "region": 2 + }, { + "name": "Yeni", + "id": "yei", + "region": 2 + }, { + "name": "Yevanic (Greek)", + "id": "yej-grek", + "region": 3 + }, { + "name": "Yi, Wuding-Luquan (Miao)", + "id": "ywq-plrd", + "region": 3 + }, { + "name": "Yiddish, Eastern (Hebrew)", + "id": "ydd-hebr", + "region": 3 + }, { + "name": "Yiddish, Western (Hebrew)", + "id": "yih-hebr", + "region": 4 + }, { + "name": "Yogad (Latin)", + "id": "yog-latn", + "region": 3 + }, { + "name": "Yoruba", + "id": "yo", + "region": 2 + }, { + "name": "Yoruba (Braille)", + "id": "yo-brai", + "region": 2 + }, { + "name": "Yoruba (Latin)", + "id": "yo-latn", + "region": 2 + }, { + "name": "Yukuben", + "id": "ybl", + "region": 2 + }, { + "name": "Yupik, Central", + "id": "esu", + "region": 6 + }, { + "name": "Yupik, Pacific Gulf", + "id": "ems", + "region": 6 + }, { + "name": "Yupik, Saint Lawrence Island", + "id": "ess", + "region": 6 + }, { + "name": "Yupik, Saint Lawrence Island (Cyrillic)", + "id": "ess-cyrl", + "region": 6 + }, { + "name": "Zaghawa", + "id": "zag", + "region": 2 + }, { + "name": "Zaiwa (Lisu)", + "id": "atb-lisu", + "region": 3 + }, { + "name": "Zan Gula", + "id": "zna", + "region": 2 + }, { + "name": "Zanaki (Latin)", + "id": "zak-latn", + "region": 2 + }, { + "name": "Zande (Latin)", + "id": "zne-latn", + "region": 2 + }, { + "name": "Zangskari (Tibetan)", + "id": "zau-tibt", + "region": 3 + }, { + "name": "Zapotec", + "id": "zap", + "region": 1 + }, { + "name": "Zapotec, Alo\u00e1pam", + "id": "zaq", + "region": 6 + }, { + "name": "Zapotec, Amatl\u00e1n", + "id": "zpo", + "region": 6 + }, { + "name": "Zapotec, Asunci\u00f3n Mixtepec", + "id": "zoo", + "region": 6 + }, { + "name": "Zapotec, Ayoquesco", + "id": "zaf", + "region": 6 + }, { + "name": "Zapotec, Cajonos", + "id": "zad", + "region": 6 + }, { + "name": "Zapotec, Chichicapan", + "id": "zpv", + "region": 6 + }, { + "name": "Zapotec, Choapan", + "id": "zpc", + "region": 6 + }, { + "name": "Zapotec, Coatecas Altas", + "id": "zca", + "region": 6 + }, { + "name": "Zapotec, Coatl\u00e1n", + "id": "zps", + "region": 6 + }, { + "name": "Zapotec, El Alto", + "id": "zpp", + "region": 6 + }, { + "name": "Zapotec, Elotepec", + "id": "zte", + "region": 6 + }, { + "name": "Zapotec, Guevea de Humboldt", + "id": "zpg", + "region": 6 + }, { + "name": "Zapotec, G\u00fcil\u00e1", + "id": "ztu", + "region": 6 + }, { + "name": "Zapotec, Lachiguiri", + "id": "zpa", + "region": 6 + }, { + "name": "Zapotec, Lachix\u00edo", + "id": "zpl", + "region": 6 + }, { + "name": "Zapotec, Lapagu\u00eda-Guivini", + "id": "ztl", + "region": 6 + }, { + "name": "Zapotec, Loxicha", + "id": "ztp", + "region": 6 + }, { + "name": "Zapotec, Mazaltepec", + "id": "zpy", + "region": 6 + }, { + "name": "Zapotec, Miahuatl\u00e1n", + "id": "zam", + "region": 6 + }, { + "name": "Zapotec, Mitla", + "id": "zaw", + "region": 6 + }, { + "name": "Zapotec, Mixtepec", + "id": "zpm", + "region": 6 + }, { + "name": "Zapotec, Ocotl\u00e1n", + "id": "zac", + "region": 6 + }, { + "name": "Zapotec, Ozolotepec", + "id": "zao", + "region": 6 + }, { + "name": "Zapotec, Petapa", + "id": "zpe", + "region": 6 + }, { + "name": "Zapotec, Quiavicuzas", + "id": "zpj", + "region": 6 + }, { + "name": "Zapotec, Quioquitani-Quier\u00ed", + "id": "ztq", + "region": 6 + }, { + "name": "Zapotec, Rinc\u00f3n", + "id": "zar", + "region": 6 + }, { + "name": "Zapotec, San Agust\u00edn Mixtepec", + "id": "ztm", + "region": 6 + }, { + "name": "Zapotec, San Baltazar Loxicha", + "id": "zpx", + "region": 6 + }, { + "name": "Zapotec, San Pedro Quiatoni", + "id": "zpf", + "region": 6 + }, { + "name": "Zapotec, San Vicente Coatl\u00e1n", + "id": "zpt", + "region": 6 + }, { + "name": "Zapotec, Santa Catarina Albarradas", + "id": "ztn", + "region": 6 + }, { + "name": "Zapotec, Santa In\u00e9s Yatzechi", + "id": "zpn", + "region": 6 + }, { + "name": "Zapotec, Santa Mar\u00eda Quiegolani", + "id": "zpi", + "region": 6 + }, { + "name": "Zapotec, Santiago Xanica", + "id": "zpr", + "region": 6 + }, { + "name": "Zapotec, Santo Domingo Albarradas", + "id": "zas", + "region": 6 + }, { + "name": "Zapotec, Sierra de Ju\u00e1rez", + "id": "zaa", + "region": 6 + }, { + "name": "Zapotec, Southeastern Ixtl\u00e1n", + "id": "zpd", + "region": 6 + }, { + "name": "Zapotec, Southern Rincon", + "id": "zsr", + "region": 6 + }, { + "name": "Zapotec, Tabaa", + "id": "zat", + "region": 6 + }, { + "name": "Zapotec, Tejalapan", + "id": "ztt", + "region": 6 + }, { + "name": "Zapotec, Texmelucan", + "id": "zpz", + "region": 6 + }, { + "name": "Zapotec, Tilquiapan", + "id": "zts", + "region": 6 + }, { + "name": "Zapotec, Tlacolulita", + "id": "zpk", + "region": 6 + }, { + "name": "Zapotec, Totomachapan", + "id": "zph", + "region": 6 + }, { + "name": "Zapotec, Western Tlacolula Valley", + "id": "zab", + "region": 6 + }, { + "name": "Zapotec, Xadani", + "id": "zax", + "region": 6 + }, { + "name": "Zapotec, Xanagu\u00eda", + "id": "ztg", + "region": 6 + }, { + "name": "Zapotec, Yal\u00e1lag", + "id": "zpu", + "region": 6 + }, { + "name": "Zapotec, Yareni", + "id": "zae", + "region": 6 + }, { + "name": "Zapotec, Yatee", + "id": "zty", + "region": 6 + }, { + "name": "Zapotec, Yatzachi", + "id": "zav", + "region": 6 + }, { + "name": "Zapotec, Yautepec", + "id": "zpb", + "region": 6 + }, { + "name": "Zapotec, Zaachila", + "id": "ztx", + "region": 6 + }, { + "name": "Zapotec, Zaniza", + "id": "zpw", + "region": 6 + }, { + "name": "Zapotec, Zoogocho", + "id": "zpq", + "region": 6 + }, { + "name": "Zaramo (Latin)", + "id": "zaj-latn", + "region": 2 + }, { + "name": "Zarma (Braille)", + "id": "dje-brai", + "region": 2 + }, { + "name": "Zarma (Latin)", + "id": "dje-latn", + "region": 2 + }, { + "name": "Zay", + "id": "zwa", + "region": 2 + }, { + "name": "Zaysete (Ethiopic)", + "id": "zay-ethi", + "region": 2 + }, { + "name": "Zaysete (Latin)", + "id": "zay-latn", + "region": 2 + }, { + "name": "Zeeuws", + "id": "zea", + "region": 4 + }, { + "name": "Zenaga (Tifinagh)", + "id": "zen-tfng", + "region": 2 + }, { + "name": "Zerenkel", + "id": "zrn", + "region": 2 + }, { + "name": "Zhoa", + "id": "zhw", + "region": 2 + }, { + "name": "Zhuang", + "id": "za", + "region": 1 + }, { + "name": "Zhuang, Dai", + "id": "zhd", + "region": 3 + }, { + "name": "Zhuang, Liujiang", + "id": "zlj", + "region": 3 + }, { + "name": "Zhuang, Nong", + "id": "zhn", + "region": 3 + }, { + "name": "Zhuang, Qiubei (Latin)", + "id": "zqe-latn", + "region": 3 + }, { + "name": "Zhuang, Youjiang", + "id": "zyj", + "region": 3 + }, { + "name": "Zigula (Latin)", + "id": "ziw-latn", + "region": 2 + }, { + "name": "Zimba (Latin)", + "id": "zmb-latn", + "region": 2 + }, { + "name": "Zinza (Latin)", + "id": "zin-latn", + "region": 2 + }, { + "name": "Zizilivakan", + "id": "ziz", + "region": 2 + }, { + "name": "Zulgo-Gemzek", + "id": "gnd", + "region": 2 + }, { + "name": "Zulu", + "id": "zu", + "region": 2 + }, { + "name": "Zulu (Braille)", + "id": "zu-brai", + "region": 2 + }, { + "name": "Zumaya", + "id": "zuy", + "region": 2 + }], + "timerid": "36" +}); diff --git a/web/src/test/auto/resources/query-mock-results/readme.md b/web/src/test/auto/resources/query-mock-results/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..77495eb2032710c3a0adf785919d1a8e27ff69a5 --- /dev/null +++ b/web/src/test/auto/resources/query-mock-results/readme.md @@ -0,0 +1,4 @@ +Note that the `timerid` field is omitted from these objects. + +It's difficult to mock within these files due to aspects of the current design - the requester-objects +autogenerate a value dynamically, and in production, said value is reflected in that field. \ No newline at end of file diff --git a/web/src/test/auto/resources/query-mock-results/sil_cameroon_azerty.js.fixture b/web/src/test/auto/resources/query-mock-results/sil_cameroon_azerty.js.fixture new file mode 100644 index 0000000000000000000000000000000000000000..39b61253275932bc333d83c6aaf4a7bd2850a78e --- /dev/null +++ b/web/src/test/auto/resources/query-mock-results/sil_cameroon_azerty.js.fixture @@ -0,0 +1,2248 @@ +keyman.register({ + "options": { + "context": "keyboard", + "dateFormat": "standard", + "device": "any", + "keyboardBaseUri": "https://s.keyman.com/keyboard/", + "fontBaseUri": "https://s.keyman.com/font/deploy/", + "keyboardid": "sil_cameroon_azerty", + "keyboardVersion": "current" + }, + "keyboard": [{ + "id": "sil_cameroon_azerty", + "name": "Cameroon AZERTY", + "filename": "sil_cameroon_azerty/6.0.8/sil_cameroon_azerty-6.0.8.js", + "version": "6.0.8", + "source": "https://github.com/keymanapp/keyboards/tree/master/release/sil/sil_cameroon_azerty", + "devices": { + "phone": 2, + "tablet": 2, + "desktop": 2 + }, + "languages": [{ + "id": "aal", + "name": "Afade", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "agq", + "name": "Aghem", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "muc", + "name": "Ajumbu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bss", + "name": "Akoose", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "aku", + "name": "Akum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ael", + "name": "Ambele", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ato", + "name": "Atong", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "azo", + "name": "Awing", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bbk", + "name": "Babanki", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bfj", + "name": "Bafanji", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bwt", + "name": "Bafaw-Balong", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ksf", + "name": "Bafia", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bfd", + "name": "Bafut", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bkc", + "name": "Baka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bqz", + "name": "Bakaka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bkh", + "name": "Bakoko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kme", + "name": "Bakole", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bdn", + "name": "Baldemu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bqo", + "name": "Balo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bbq", + "name": "Bamali", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bmo", + "name": "Bambalang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "baw", + "name": "Bambili-Bambui", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bce", + "name": "Bamenyam", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bqt", + "name": "Bamukumbit", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bax", + "name": "Bamun", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bvm", + "name": "Bamunka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bcw", + "name": "Bana", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bgf", + "name": "Bangandu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bgj", + "name": "Bangolan", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "abb", + "name": "Bankon", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bbi", + "name": "Barombi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bas", + "name": "Basaa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bsi", + "name": "Bassossi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bta", + "name": "Bata", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bnm", + "name": "Batanga", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "btc", + "name": "Bati", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bfp", + "name": "Beba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "beb", + "name": "Bebele", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bxp", + "name": "Bebil", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bnz", + "name": "Beezen", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bby", + "name": "Befang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bkw", + "name": "Bekwel", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "byb", + "name": "Bikya", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bwh", + "name": "Bishuo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "brt", + "name": "Bitare", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bky", + "name": "Bokyi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bmw", + "name": "Bomwali", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bvg", + "name": "Bonkeng", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bbx", + "name": "Bubia", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bdm", + "name": "Buduma", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bum", + "name": "Bulu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bmv", + "name": "Bum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bqd", + "name": "Bung", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bxs", + "name": "Busam", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bju", + "name": "Busuu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bhs", + "name": "Buwal", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mkk", + "name": "Byep", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ckx", + "name": "Caka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "cug", + "name": "cug", + "region": 1, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "cuv", + "name": "Cuvok", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dbq", + "name": "Daba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dmm", + "name": "Dama", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dek", + "name": "Dek", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "anv", + "name": "Denya", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dur", + "name": "Dii", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dii", + "name": "Dimbong", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dow", + "name": "Doyayo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dua", + "name": "Duala", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ndu", + "name": "Dugun", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dme", + "name": "Dugwor", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "duz", + "name": "Duli-Gey", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dae", + "name": "Duupa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "etu", + "name": "Ejagham", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ekm", + "name": "Elip", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "emn", + "name": "Eman", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ags", + "name": "Esimbi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "eto", + "name": "Eton", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bzz", + "name": "Evant", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ewo", + "name": "Ewondo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fll", + "name": "Fali, North", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fal", + "name": "Fali, South", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fak", + "name": "Fang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fan", + "name": "Fang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fmp", + "name": "Fe\u2019fe\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fub-latn", + "name": "Fulfulde, Adamawa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "fuv", + "name": "Fulfulde, Nigerian", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gou", + "name": "Gavar", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gba", + "name": "Gbaya (Central African Republic)", + "region": 1, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gmm", + "name": "Gbaya-Mbodomo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bbj", + "name": "Ghom\u00e1l\u00e1\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gid", + "name": "Gidar", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kmp", + "name": "Gimme", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gmn", + "name": "Gimnime", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "giz", + "name": "Giziga", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gis", + "name": "Giziga, North", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "glw", + "name": "Glavda", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gde", + "name": "Gude", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ngs", + "name": "Gvoko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gyi", + "name": "Gyele", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "xed", + "name": "Hdi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "hij", + "name": "Hijuk", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "hya", + "name": "Hya", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bec", + "name": "Iceve-Maci", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ass", + "name": "Ipulo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "isu", + "name": "Isu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "szv", + "name": "Isu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "uiv", + "name": "Iyive", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "jim", + "name": "Jimjimen", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "jia", + "name": "Jina", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "jbu", + "name": "Jukun Takum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kkj", + "name": "Kako", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kzr", + "name": "Karang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kbn", + "name": "Kare", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "luw", + "name": "Kasabe", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dmo", + "name": "Kemedzung", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kvm", + "name": "Kendem", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ndb", + "name": "Kenswei Nsei", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ken", + "name": "Kenyang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ker", + "name": "Kera", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "biw", + "name": "Kol", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "klc", + "name": "Kolbila", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bkm", + "name": "Kom", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kmy", + "name": "Koma", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ozm", + "name": "Koonzime", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "krp", + "name": "Korop", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kid", + "name": "Koshin", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kfn", + "name": "Kuk", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kfl", + "name": "Kung", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "xuo", + "name": "Kuo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kub", + "name": "Kutep", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bko", + "name": "Kwa\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kdz", + "name": "Kwaja", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kwu", + "name": "Kwakum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "knp", + "name": "Kwanja", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nmg", + "name": "Kwasio", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lbi", + "name": "La\u2019bi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kot", + "name": "Lagwan", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lmx", + "name": "Laimbue", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lns", + "name": "Lamnso\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lfa", + "name": "Lefa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "leo", + "name": "Leti", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "add", + "name": "Lidzonka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lmp", + "name": "Limbum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "wok", + "name": "Longto", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mxu", + "name": "Mada", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "maf", + "name": "Mafa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "xmj", + "name": "Majera", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mcp", + "name": "Makaa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mxf", + "name": "Malgbe", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mzd", + "name": "Malimba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mcs", + "name": "Mambai", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mcu", + "name": "Mambila, Cameroon", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "myg", + "name": "Manta", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mcn", + "name": "Masana", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "msv", + "name": "Maslam", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mfh", + "name": "Matal", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "dkx", + "name": "Mazagway-Hidi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nza", + "name": "Mbembe, Tigon", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mbo", + "name": "Mbo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mtk", + "name": "Mbo\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "xmb", + "name": "Mbonga", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bgu", + "name": "Mbongno", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "xmd", + "name": "Mbudum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mqb", + "name": "Mbuko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mlb", + "name": "Mbule", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mdd", + "name": "Mbum", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "byv", + "name": "Medumba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mfj", + "name": "Mefele", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mfd", + "name": "Mendankwe-Nkwen", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "xmg", + "name": "Mengaka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mct", + "name": "Mengisa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mea", + "name": "Menka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "meq", + "name": "Merey", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "iyo", + "name": "Mesaka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mgo", + "name": "Meta\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nfu", + "name": "Mfumte", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "hna", + "name": "Mina", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mmu", + "name": "Mmaala", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bfm", + "name": "Mmen", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mfk", + "name": "Mofu, North", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mif", + "name": "Mofu-Gudur", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bri", + "name": "Mokpwe", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mlw", + "name": "Moloko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mru", + "name": "Mono", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mpi", + "name": "Mpade", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mcx", + "name": "Mpiemo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mgg", + "name": "Mpumpong", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kqx", + "name": "Mser", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "muo", + "name": "Mubako", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "boe", + "name": "Mundabli", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mua", + "name": "Mundang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mnf", + "name": "Mundani", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mhk", + "name": "Mungaka", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mij", + "name": "Mungbam", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mse", + "name": "Musey", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mug", + "name": "Musgu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "muy", + "name": "Muyang", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bzv", + "name": "Naami", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ngv", + "name": "Nagumi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mff", + "name": "Naki", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ncr", + "name": "Nchane", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nnz", + "name": "Nda\u2019nda\u2019", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gke", + "name": "Ndai", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nml", + "name": "Ndemli", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ndr", + "name": "Ndoola", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nbv", + "name": "Ngamambo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "sba", + "name": "Ngambay", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nge", + "name": "Ngemba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ngj", + "name": "Ngie", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nnh", + "name": "Ngiemboon", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "jgo", + "name": "Ngomba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nla", + "name": "Ngombale", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nsh", + "name": "Ngoshie", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nwe", + "name": "Ngwe", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ngn", + "name": "Ngwo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nmr", + "name": "Nimbari", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "sgi", + "name": "Nizaa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "njj", + "name": "Njen", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "njy", + "name": "Njyem", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nkc", + "name": "Nkongho", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lem", + "name": "Nomaande", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nhu", + "name": "Noone", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "baf", + "name": "Nubaca", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "yas", + "name": "Nugunu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nvo", + "name": "Nyokon", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nzy", + "name": "Nzakambay", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "nja", + "name": "Nzanyi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "obl", + "name": "Oblo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "oku", + "name": "Oku", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bdu", + "name": "Oroko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ost", + "name": "Osatu", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "pmn", + "name": "Pam", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "pnz", + "name": "Pana", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "pbi", + "name": "Parkwa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "pfe", + "name": "Pere", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "lme", + "name": "P\u00e9v\u00e9", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "wes", + "name": "Pidgin, Cameroon", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "pny", + "name": "Pinyin", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "pmm", + "name": "Pol", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "kvj", + "name": "Psikye", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ndi", + "name": "Samba Leko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "asj", + "name": "Sari", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "swq", + "name": "Sharwa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bbw", + "name": "Supapya", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "sox", + "name": "Swo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ngy", + "name": "Tibea", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "tik", + "name": "Tikar", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "tiv", + "name": "Tiv", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "toz", + "name": "To", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "tsh", + "name": "Tsuvan", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bag", + "name": "Tuki", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "tvu", + "name": "Tunen", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ttf", + "name": "Tuotomb", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "tui", + "name": "Tupuri", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "twn", + "name": "Twendi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "usk", + "name": "Usaghade", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mlr", + "name": "Vame", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "vem", + "name": "Vemgo-Mabas", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bav", + "name": "Vengo", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ver", + "name": "Verre", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "vut", + "name": "Vute", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "mfi", + "name": "Wandala", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "www", + "name": "Wawa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "weh", + "name": "Weh", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bqm", + "name": "Wumboko", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "bse", + "name": "Wushi", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "udl", + "name": "Wuzlam", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "yam", + "name": "Yamba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "yat", + "name": "Yambeta", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "yav", + "name": "Yangben", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "yko", + "name": "Yasa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ybb", + "name": "Yemba", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "yei", + "name": "Yeni", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ybl", + "name": "Yukuben", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "zhw", + "name": "Zhoa", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "ziz", + "name": "Zizilivakan", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "gnd", + "name": "Zulgo-Gemzek", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }, { + "id": "zuy", + "name": "Zumaya", + "region": 2, + "font": { + "family": "AndikaAfr", + "source": ["AndikaAfr-R.ttf"] + } + }] + }] +}); diff --git a/web/src/test/auto/resources/query-mock-results/sil_euro_latin@no_sv.js.fixture b/web/src/test/auto/resources/query-mock-results/sil_euro_latin@no_sv.js.fixture new file mode 100644 index 0000000000000000000000000000000000000000..307196a6518812041e55213992668abc981dcc60 --- /dev/null +++ b/web/src/test/auto/resources/query-mock-results/sil_euro_latin@no_sv.js.fixture @@ -0,0 +1,52 @@ +keyman.register({ + "options": { + "context": "keyboard", + "dateFormat": "standard", + "device": "any", + "keyboardBaseUri": "https://s.keyman.com/keyboard/", + "fontBaseUri": "https://s.keyman.com/font/deploy/", + "keyboardid": "sil_euro_latin@no,sil_euro_latin@sv", + "keyboardVersion": "current" + }, + "keyboard": [{ + "id": "sil_euro_latin", + "name": "EuroLatin (SIL)", + "filename": "sil_euro_latin/2.0.3/sil_euro_latin-2.0.3.js", + "version": "2.0.3", + "source": "https://github.com/keymanapp/keyboards/tree/master/release/sil/sil_euro_latin", + "devices": { + "phone": 2, + "tablet": 2, + "desktop": 2 + }, + "languages": [{ + "id": "no", + "name": "Norwegian", + "region": 4, + "font": { + "family": "LatinWeb", + "source": ["DejaVuSans.ttf", "DejaVuSans.mobileconfig"] + } + }] + }, { + "id": "sil_euro_latin", + "name": "EuroLatin (SIL)", + "filename": "sil_euro_latin/2.0.3/sil_euro_latin-2.0.3.js", + "version": "2.0.3", + "source": "https://github.com/keymanapp/keyboards/tree/master/release/sil/sil_euro_latin", + "devices": { + "phone": 2, + "tablet": 2, + "desktop": 2 + }, + "languages": [{ + "id": "sv", + "name": "Swedish", + "region": 4, + "font": { + "family": "LatinWeb", + "source": ["DejaVuSans.ttf", "DejaVuSans.mobileconfig"] + } + }] + }] +}); \ No newline at end of file diff --git a/web/src/test/manual/build.sh b/web/src/test/manual/build.sh new file mode 100755 index 0000000000000000000000000000000000000000..5a65edca0da6ce8bf1ac258afd893915018c1e23 --- /dev/null +++ b/web/src/test/manual/build.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +## START STANDARD BUILD SCRIPT INCLUDE +# adjust relative paths as necessary +THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" +. "${THIS_SCRIPT%/*}/../../../../resources/build/build-utils.sh" +## END STANDARD BUILD SCRIPT INCLUDE + +. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" + +# This script runs from its own folder +cd "$THIS_SCRIPT_PATH" + +# ################################ Main script ################################ + +builder_describe "Builds the Keyman Engine for Web's sample page setups." \ + "@/common/web/sentry-manager build" \ + "@/web/src/app/browser build" \ + "@/web/src/app/ui build" \ + "@/web/src/tools build" \ + "configure Does nothing for this project" \ + "clean" \ + "build" \ + "test Does nothing for this project" \ + "--ci Does nothing for this project" + +# Possible TODO?s +# "upload-symbols Uploads build product to Sentry for error report symbolification. Only defined for $DOC_BUILD_EMBED_WEB" \ + +builder_parse "$@" + +DEST="web/build/test-resources" + +builder_describe_outputs \ + build "/$DEST/sentry-manager.js" + +#### Resource paths #### + +SENTRY_MANAGER_SRC="$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" +SENTRY_MANAGER_MAP="$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js.map" +SENTRY_SRC="$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" +SENTRY_MAP="$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js.map" + +#### Build action definitions #### + +function do_copy() { + mkdir -p "$KEYMAN_ROOT/$DEST" + + # The next four lines are needed for the sentry-integration manual test page. + cp "$SENTRY_MANAGER_SRC" "$KEYMAN_ROOT/$DEST/sentry-manager.js" + cp "$SENTRY_MANAGER_MAP" "$KEYMAN_ROOT/$DEST/sentry-manager.js.map" + cp "$SENTRY_SRC" "$KEYMAN_ROOT/$DEST/sentry-bundle.min.js" + cp "$SENTRY_MAP" "$KEYMAN_ROOT/$DEST/sentry-bundle.min.js.map" +} + +builder_run_action clean rm -rf "$KEYMAN_ROOT/$DEST" +builder_run_action build do_copy \ No newline at end of file diff --git a/web/src/test/manual/embed/android-harness/build.sh b/web/src/test/manual/embed/android-harness/build.sh index 0445022e5c9103edad0d21fc633a53346559485b..f8e7975d592ff5ee7b4e683be34624d30dc489fb 100755 --- a/web/src/test/manual/embed/android-harness/build.sh +++ b/web/src/test/manual/embed/android-harness/build.sh @@ -8,21 +8,52 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../../../../resources/build/build-utils.sh" ## END STANDARD BUILD SCRIPT INCLUDE -THIS_DIR="$(dirname "$THIS_SCRIPT")" +cd "$THIS_SCRIPT_PATH" -mkdir -p "$THIS_DIR/host" -cp -R "$KEYMAN_ROOT/android/KMEA/app/src/main/assets/"* "$THIS_DIR/host/" -cp "$KEYMAN_ROOT/web/build/app/embed/debug/keyman.js" "$THIS_DIR/host/keymanandroid.js" -cp "$KEYMAN_ROOT/web/build/app/embed/debug/keyman.js.map" "$THIS_DIR/host/keyman.js.map" -cp "$KEYMAN_ROOT/web/build/app/embed/debug/osk/kmwosk.css" "$THIS_DIR/host/kmwosk.css" -cp "$KEYMAN_ROOT/web/build/app/embed/debug/osk/globe-hint.css" "$THIS_DIR/host/globe-hint.css" -cp "$KEYMAN_ROOT/web/build/app/embed/debug/osk/keymanweb-osk.ttf" "$THIS_DIR/host/keymanweb-osk.ttf" +builder_describe "Builds a debug-host page simulating Keyman Android's WebView setup for KMW use" \ + "@/common/web/keyman-version" \ + "@/web/src/app/webview" \ + "@/common/web/sentry-manager" \ + clean configure build -# We shouldn't rely on a prior Android build for these files! We can retrieve them separately. -cp "$KEYMAN_ROOT/common/web/sentry-manager/build/index.js" "$THIS_DIR/host/keyman-sentry.js" -cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "$THIS_DIR/host/es6-shim.min.js" +builder_describe_outputs \ + configure /node_modules \ + build /web/src/test/manual/embed/android-harness/host/keyboard.html -# Test keyboards +builder_parse "$@" -cp "$KEYMAN_ROOT/web/src/test/manual/web/web_context_tests.js" "$THIS_DIR/host/" -# android\KMEA\app\src\main\assets\keyboard.html \ No newline at end of file +### CLEAN ACTIONS + +if builder_start_action clean; then + rm -rf build/ + builder_finish_action success clean +fi + +### CONFIGURE ACTIONS + +if builder_start_action configure; then + verify_npm_setup + builder_finish_action success configure +fi + +if builder_start_action build; then + mkdir -p "host/osk" + cp -R "$KEYMAN_ROOT/android/KMEA/app/src/main/assets/"* "host/" + cp "$KEYMAN_ROOT/web/build/app/webview/debug/keymanweb-webview.js" "host/keymanweb-webview.js" + cp "$KEYMAN_ROOT/web/build/app/webview/debug/keymanweb-webview.js.map" "host/keymanweb-webview.js.map" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/ajax-loader.gif" "host/ajax-loader.gif" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/kmwosk.css" "host/kmwosk.css" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/globe-hint.css" "host/globe-hint.css" + cp "$KEYMAN_ROOT/web/build/app/resources/osk/keymanweb-osk.ttf" "host/keymanweb-osk.ttf" + + # We shouldn't rely on a prior Android build for these files! We can retrieve them separately. + cp "$KEYMAN_ROOT/common/web/sentry-manager/build/lib/index.js" "host/keyman-sentry.js" + cp "$KEYMAN_ROOT/node_modules/es6-shim/es6-shim.min.js" "host/es6-shim.min.js" + cp "$KEYMAN_ROOT/node_modules/@sentry/browser/build/bundle.min.js" "host/sentry.min.js" + + # Test keyboards + + cp "$KEYMAN_ROOT/web/src/test/manual/web/web_context_tests.js" "host/" + # android\KMEA\app\src\main\assets\keyboard.html + builder_finish_action success build +fi \ No newline at end of file diff --git a/web/src/test/manual/web/attachment-api/index.html b/web/src/test/manual/web/attachment-api/index.html index 0ee6896af425d0d964f1d1f7ec2c866695dc89a9..f904d25f08e4b631f55473c5f38f979bd3a06a26 100644 --- a/web/src/test/manual/web/attachment-api/index.html +++ b/web/src/test/manual/web/attachment-api/index.html @@ -23,7 +23,7 @@ - + - + + - + diff --git a/web/src/test/manual/web/build-visual-keyboard/index.html b/web/src/test/manual/web/build-visual-keyboard/index.html index a572a638a7373e3a84814ff8d5248e783b6b4d54..2c3e1d6c0aa02477f6bac5e6f27a947e1bc3b0be 100644 --- a/web/src/test/manual/web/build-visual-keyboard/index.html +++ b/web/src/test/manual/web/build-visual-keyboard/index.html @@ -24,7 +24,7 @@ - + - + diff --git a/web/src/test/manual/web/caps-lock-layer-3620/index.html b/web/src/test/manual/web/caps-lock-layer-3620/index.html index 0c8981a72581b5f097056bb1b5e02b59e715aabe..c5b328189f62aef858a86ec3c6122bd8b585d881 100644 --- a/web/src/test/manual/web/caps-lock-layer-3620/index.html +++ b/web/src/test/manual/web/caps-lock-layer-3620/index.html @@ -17,7 +17,7 @@ - + - + + - + + - + + - + + - + diff --git a/web/src/test/manual/web/desktop-ui/float.html b/web/src/test/manual/web/desktop-ui/float.html index 049fa9d316b0d377e3fa2bee87e7c840028aa707..338a66a160c8d07db1d31eed2bbd4a3e961703fe 100644 --- a/web/src/test/manual/web/desktop-ui/float.html +++ b/web/src/test/manual/web/desktop-ui/float.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/desktop-ui/toggle.html b/web/src/test/manual/web/desktop-ui/toggle.html index 6aa99b1c5abb6f9934bc8f4223c7b8dc75a4de4f..857e98466c337f0248cbb8e2367ab547e718e303 100644 --- a/web/src/test/manual/web/desktop-ui/toggle.html +++ b/web/src/test/manual/web/desktop-ui/toggle.html @@ -23,7 +23,7 @@ - + - + @@ -41,7 +41,7 @@ + - + diff --git a/web/src/test/manual/web/empty-row/index.html b/web/src/test/manual/web/empty-row/index.html index 460d544b64528bbc0668c58ae36296e89718da21..02953493e91a9231c2e299c1c0c802c16008090f 100644 --- a/web/src/test/manual/web/empty-row/index.html +++ b/web/src/test/manual/web/empty-row/index.html @@ -23,7 +23,7 @@ - + - + + - + + #KeymanWebControl {width:50%;min-width:600px;} + + + + - - - + + - + keyman.init({ + attachType:'auto' + }); + + - - + + - - + +

KeymanWeb: 'Test case' for proper implementation of the removeKeyboards API function

- -
+

This version is used to test the removeKeyboards API function for proper argument handling.

Automatically-loaded keyboard IDs:

    @@ -71,34 +70,34 @@


    -
+

Add a keyboard by keyboard ID:

- - + +

Remove a keyboard by keyboard ID:

- +

Return to testing home page

- - + + - - + * + *** + --> diff --git a/web/src/test/manual/web/issue103/index.html b/web/src/test/manual/web/issue103/index.html index c02d9312ed49c0d9f672fc5b25cea6f01ccc9487..35b006e34721cc3e4c0ac5d1273ed0b954df909e 100644 --- a/web/src/test/manual/web/issue103/index.html +++ b/web/src/test/manual/web/issue103/index.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/issue115/index.html b/web/src/test/manual/web/issue115/index.html index 5c9ffeb3afb0b7630e3565f2bcd2393634a8adcb..cd9afcb8fa00258eb353669fc173e32abaddb090 100644 --- a/web/src/test/manual/web/issue115/index.html +++ b/web/src/test/manual/web/issue115/index.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/issue116/index.html b/web/src/test/manual/web/issue116/index.html index b8b49e1375e17758af2fe77edf29639338f62a87..d81fb947c04ba2c81fa34efbfba19fcf6018b1e7 100644 --- a/web/src/test/manual/web/issue116/index.html +++ b/web/src/test/manual/web/issue116/index.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/issue1332/index.html b/web/src/test/manual/web/issue1332/index.html index 4b8715a7f170494626f20a98925cdd1b8a5f726e..8bd8c403aa4af93ff275f2d4f850e0add033fef6 100644 --- a/web/src/test/manual/web/issue1332/index.html +++ b/web/src/test/manual/web/issue1332/index.html @@ -23,7 +23,7 @@ - + - + + - + diff --git a/web/src/test/manual/web/issue266/index.html b/web/src/test/manual/web/issue266/index.html index aca186b178488f480c0c6277c506e6f967449ce9..56e52016d765ca9c752ca85638692ffa2e6e7918 100644 --- a/web/src/test/manual/web/issue266/index.html +++ b/web/src/test/manual/web/issue266/index.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/issue271/index.html b/web/src/test/manual/web/issue271/index.html index 8c055c4015f06d62862f21a7040e93ed8754d835..fa969195a138f80723073ff1ec7ea7297fc1bae1 100644 --- a/web/src/test/manual/web/issue271/index.html +++ b/web/src/test/manual/web/issue271/index.html @@ -23,7 +23,7 @@ - + - + @@ -57,10 +57,10 @@
  • us, en
  • french, fr
  • -
  • european2, no
  • -
  • european2, sv
  • +
  • sil_euro_latin, no
  • +
  • sil_euro_latin, sv
  • hebrew, he
  • -
  • dzongkha, dzo
  • +
  • dzongkha, dz
  • lao_2008_basic, lo

diff --git a/web/src/test/manual/web/issue29/index.html b/web/src/test/manual/web/issue29/index.html index 92fed6a8c1ca8a658e2e5cdb2cc5b4d8c5dbf555..24a226326ca54147df38c301f00aa732754c62a4 100644 --- a/web/src/test/manual/web/issue29/index.html +++ b/web/src/test/manual/web/issue29/index.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/issue2924/header.js b/web/src/test/manual/web/issue2924/header.js index 8e3ecb745ab7c4a9ba8a6c85d200c03b6a6f89b1..dcd8083c7dc52e8a10883c0bf56702ef3a8e2094 100644 --- a/web/src/test/manual/web/issue2924/header.js +++ b/web/src/test/manual/web/issue2924/header.js @@ -35,7 +35,7 @@ window.addEventListener('load', function() { ? window.location.href.substr(0, window.location.href.lastIndexOf('/')+1) : window.location.href; - keyman.modelManager.register({ + keyman.addModel({ id: 'nrc.en.mtnt', languages: ['en'], path: pageRef + 'dmg.dv.test.model.js' diff --git a/web/src/test/manual/web/issue2924/index.html b/web/src/test/manual/web/issue2924/index.html index 61a1be97cde25f9efe1b20a3c5043ca536f0b96f..12396420c24bf2a13f6feecc31d409e7e8fb245b 100644 --- a/web/src/test/manual/web/issue2924/index.html +++ b/web/src/test/manual/web/issue2924/index.html @@ -6,8 +6,8 @@ KeymanWeb Issue 2924 - Variable Stores and Predictive Text - - + + diff --git a/web/src/test/manual/web/issue3701/index.html b/web/src/test/manual/web/issue3701/index.html index ca2729133abb2df3d439af5c8dd54eedeccbc6b4..4b621678cc699a8ee002a4364c308434f4aca961 100644 --- a/web/src/test/manual/web/issue3701/index.html +++ b/web/src/test/manual/web/issue3701/index.html @@ -23,7 +23,7 @@ - + - + + - + + - + diff --git a/web/src/test/manual/web/issue5312-touch-alias-optimization/index.html b/web/src/test/manual/web/issue5312-touch-alias-optimization/index.html deleted file mode 100644 index 682e279ab6b37293354289e1e6f62c55cf9667a5..0000000000000000000000000000000000000000 --- a/web/src/test/manual/web/issue5312-touch-alias-optimization/index.html +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - KeymanWeb Sample Page - Unminified Source - - - - - - - - - - - - - - - - - - - - -

KeymanWeb Sample Page - Touch-Alias Element Optimization

- -
- -
- -

Text performance with this element:

- -

Text was generated by the Lorem Ipsum Generator.

- -

Return to testing home page

-
- - -
-

--End of Document--

- - - - - diff --git a/web/src/test/manual/web/issue5455/index.html b/web/src/test/manual/web/issue5455/index.html deleted file mode 100644 index 0ba1f0f84a97b84fe623ac9b0de88ebb6e945c24..0000000000000000000000000000000000000000 --- a/web/src/test/manual/web/issue5455/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - KeymanWeb Testing Page - util.wait checks options.useAlerts - - - - - - - - - - - - - - - - - -

KeymanWeb Sample Page - Issue 5455

-

This page is for testing that util.wait() checks for options.useAlerts. - See Issue #5455. -

-

- About 2 seconds after this test page loads, you should see a util.wait modal "This is a waiting message" - if the option for useAlerts is enabled. -

-

- After another 5 seconds, the modal should disappear. -

- -

You are currently testing under the internal alerts mode. - Two alert modes are available: - Disabled or - Enabled (Default)

-

This will only affect whether or not KeymanWeb's alert and wait modals are displayed. -

- - - -
- -
- -

Type in your language in this text area:

- - -

or in this input field:

- - -

Return to testing home page

-
- - - - - diff --git a/web/src/test/manual/web/issue6005/index.html b/web/src/test/manual/web/issue6005/index.html index 8cc31067a0514af0bf7d507bf9f3b13d9c4a0748..7326f99c7dbdbe9a18ed8ef996ccc8033a7b8f6e 100644 --- a/web/src/test/manual/web/issue6005/index.html +++ b/web/src/test/manual/web/issue6005/index.html @@ -23,7 +23,7 @@ - + - + + - + diff --git a/web/src/test/manual/web/issue63/index.html b/web/src/test/manual/web/issue63/index.html index 43842dc3e9cad571b93f2850f700e70b9a9a877a..55cee63c97995cd2e9f6a064b7e54f6fe7e3abfc 100644 --- a/web/src/test/manual/web/issue63/index.html +++ b/web/src/test/manual/web/issue63/index.html @@ -23,7 +23,7 @@ - + - + diff --git a/web/src/test/manual/web/issue917-context-and-notany/index.html b/web/src/test/manual/web/issue917-context-and-notany/index.html index 1db3b10c4dbf0b3204094b195b64a018c5fea3e0..251edb9142a6c04df491e22c081b0aca7867ed73 100644 --- a/web/src/test/manual/web/issue917-context-and-notany/index.html +++ b/web/src/test/manual/web/issue917-context-and-notany/index.html @@ -23,7 +23,7 @@ - + - + + - + + - + diff --git a/web/src/test/manual/web/keyboard-errors/timeout.js b/web/src/test/manual/web/keyboard-errors/timeout.js index 2af6bf907a3a953dcc3ccce76a13c64e23001c29..d4e21179dfcd6905d02b10d06bd4aeb9a7f708a5 100644 --- a/web/src/test/manual/web/keyboard-errors/timeout.js +++ b/web/src/test/manual/web/keyboard-errors/timeout.js @@ -1,10 +1,21 @@ (function() { var me = document.currentScript; console.log(me); - document.body.addEventListener('load', function(e) { + + var onload = me.onload; + if(onload) { + me.onload = null; + } + + document.body.addEventListener('load', function(e) { // Prevent the element's onload event from firing if(e.srcElement == me || e.target == me) { e.cancelBubble = true; } }, {capture: true}); + + window.setTimeout(function () { + // Restores the function after a slight delay. + me.onload = onload; + }, 1); })(); \ No newline at end of file diff --git a/web/src/test/manual/web/mnemonic/index.html b/web/src/test/manual/web/mnemonic/index.html index 65331615aa5d7eeea99e761b4d5b7f24ac070c51..e0b0f0de79be9402cfe76a55b3a2bf330dacb82e 100644 --- a/web/src/test/manual/web/mnemonic/index.html +++ b/web/src/test/manual/web/mnemonic/index.html @@ -20,7 +20,7 @@ - + - + diff --git a/web/src/test/manual/web/options-with-save/index.html b/web/src/test/manual/web/options-with-save/index.html index 0f91ea354a2f080a232168d55b1205ba29a88aa5..9f607fd11f47c3b7ccd90cc52186de36f85ce960 100644 --- a/web/src/test/manual/web/options-with-save/index.html +++ b/web/src/test/manual/web/options-with-save/index.html @@ -20,7 +20,7 @@ - + - + diff --git a/web/src/test/manual/web/osk-event-buttons/index.html b/web/src/test/manual/web/osk-event-buttons/index.html index 3c88e222017a7797530e2fd385aaa0aa66836519..92dafcdb4a9ec84152176d7e25f168a3e0f57f87 100644 --- a/web/src/test/manual/web/osk-event-buttons/index.html +++ b/web/src/test/manual/web/osk-event-buttons/index.html @@ -23,7 +23,7 @@ - + - + + - + + + + + + + + +

KeymanWeb OSK Module Testing

+

+ Note: if hosted via an IIS setup, you may need to add the 'mjs' extension under + "MIME Types" for it to properly serve related module-based JS files. +

+ +
+ +
+
+
+

Keyboard

+
+ + + +
+
+ + +
+

Layer

+
+ + +
+
+
+ +
+
+

OSK Mode

+
+ + + +
+
+
+

Target Device

+
+ + + + + +
+
+
+ +
+
+ +

Active Element

+
+ + + +
+
+
+ +

Visibility

+
+ + + +
+
+ Disabled (touch-based) +
+
+
+ +
+
+ +

Active Element

+
+ + + +
+
+
+ +

Visibility

+
+ + + +
+
+ Disabled (touch-based) +
+
+
+ +
+
+

OSK Sizing

+
Width:
+
Height:
+ +
+
+
+ +
+ +
+ +

Textbox target:

+ + +

Input target:

+ + +
+ +

Event log:

+ + + + +
+ +

Return to testing home page

+ + +
+

--End of Document--

+ + + + + \ No newline at end of file diff --git a/web/src/test/manual/web/osk/scratchspace/setup.mjs b/web/src/test/manual/web/osk/scratchspace/setup.mjs new file mode 100644 index 0000000000000000000000000000000000000000..ec3ae0dcac58e90b1aeae2bd8edeb22f7502bbbf --- /dev/null +++ b/web/src/test/manual/web/osk/scratchspace/setup.mjs @@ -0,0 +1,48 @@ +import * as KeymanOSK from '../../../../../../build/engine/osk/lib/index.mjs'; +import Device from '../../../../../../build/engine/device-detect/lib/index.mjs'; +import { loadKeyboardFromPath, loadKeyboardsFromStubs } from '../kbdLoader.mjs'; + +// NOTE: we must actually set these in this manner to have accessibility outside of +// this script tag! +window.modules = { + osk: KeymanOSK, + device: Device +}; + +let prefix = '../../'; +const stubs = [ + {id:'us',name:'English',languages:{id:'en',name:'English'}, filename:(prefix + 'us-1.0.js')}, + {id:'obolo_chwerty_6351',name:'obolo_chwerty_6351',languages:{id:'en',name:'English'}, + filename:(prefix + 'obolo_chwerty_6351.js')}, + {id:'lao_2008_basic',name:'Lao Basic', languages: {id:'lo',name:'Lao',region:'Asia'}, + filename:(prefix + 'lao_2008_basic-1.2.js')} +] + +window.keyboards = loadKeyboardsFromStubs(stubs).then((keyboards) => { + window.keyboards = keyboards; +}); + +let device = new Device(); +device.detect(); + +if(device.formFactor == 'phone') { + document.body.classList.add('phone'); +} + +if(device.touchable) { + document.body.classList.add('touch'); +} + +window.HOST_DEVICE = device.coreSpec; +window.targetDevice = new modules.osk.DeviceSpec('safari', 'phone', 'ios', true); + +window.BaseConfiguration = { + isEmbedded: false, + // Relative path to the resources' original versions. + pathConfig: { + resources: '../../../../../resources', + fonts: '' + } +}; + +window.keyboardLoader = loadKeyboardFromPath; \ No newline at end of file diff --git a/web/src/test/manual/web/platform/index.html b/web/src/test/manual/web/platform/index.html index c91e667e52c9c9f12188f00a59415ee7df15f4ad..c528862ead5d09aacbedcaa8709f956ebf58af7b 100644 --- a/web/src/test/manual/web/platform/index.html +++ b/web/src/test/manual/web/platform/index.html @@ -23,7 +23,7 @@ - + - + + - + + - + + - + @@ -51,6 +51,7 @@ addKeyboards('french','@he'); kmw.init({ + attachType: 'auto', useAlerts: alertType }).then(function() { loadKeyboards(); diff --git a/web/src/test/manual/web/rotation-events/index.html b/web/src/test/manual/web/rotation-events/index.html index 6e884f505a248541b8a546e15f759a1b71c0618e..c07b3300c9426d230712a366794ab4c4231752b1 100644 --- a/web/src/test/manual/web/rotation-events/index.html +++ b/web/src/test/manual/web/rotation-events/index.html @@ -14,7 +14,7 @@ - + - + diff --git a/web/src/test/manual/web/scrolling/index.html b/web/src/test/manual/web/scrolling/index.html deleted file mode 100644 index 138e5a43c115bbcb8a26913d0edc06d26d02f2ce..0000000000000000000000000000000000000000 --- a/web/src/test/manual/web/scrolling/index.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - KeymanWeb Testing Page - Predictive Text: robust testing - - - - - - - - - - - - - - - - -

KeymanWeb Sample Page - Scrolling Touch Elements

-
- -
- -

Type in your language in this text area:

- - -

Return to testing home page

-
- - - - - - - diff --git a/web/src/test/manual/web/sentry-integration/index.html b/web/src/test/manual/web/sentry-integration/index.html index 9dbf167b88bb85606aa9960f9498da6c37d77c40..ca7f7a66eb74e23e9b7a918734e76864d7db09eb 100644 --- a/web/src/test/manual/web/sentry-integration/index.html +++ b/web/src/test/manual/web/sentry-integration/index.html @@ -23,7 +23,7 @@ - + - + - + + + diff --git a/web/src/test/manual/web/spacebar-text/index.html b/web/src/test/manual/web/spacebar-text/index.html index ee1053be0caf7affc0f0dea3e1559a94f537a7a3..2db281323bd6567c3411f6c5d5bbbd22eca7705d 100644 --- a/web/src/test/manual/web/spacebar-text/index.html +++ b/web/src/test/manual/web/spacebar-text/index.html @@ -17,7 +17,7 @@ - + - + + - + + - + + - + + - + + @@ -34,9 +34,9 @@ var kmw=window.keyman; kmw.init({ attachType:'auto' - }).then(function() { - kmw.addKeyboards(); - }); + }).then(function() { + kmw.addKeyboards(); + }); function runRenderer() { diff --git a/web/src/tools/testing/bulk_rendering/renderer_core.ts b/web/src/tools/testing/bulk_rendering/renderer_core.ts index adcbc45069f8eb81f4595edd97296eb068899e51..3348b72f16715d89c8dd481d61cbfc8fb656a938 100644 --- a/web/src/tools/testing/bulk_rendering/renderer_core.ts +++ b/web/src/tools/testing/bulk_rendering/renderer_core.ts @@ -1,296 +1,297 @@ // Includes KeymanWeb's Device class, as it's quite a useful resource for KMW-related projects. - -type KeyboardMap = {[id: string]: any}; - -namespace com.keyman.renderer { - export class BatchRenderer { - static divMaster: HTMLDivElement; - static dummy: HTMLInputElement; - static captureStream: any; - static video: any; - static boundingRect: DOMRect; - static allLayers = false; - static keyboardMatch: RegExp; - - async startCapture() { - let captureStream = null; - - try { - captureStream = await (navigator.mediaDevices as any).getDisplayMedia({ - audio: false, - video: { - width: screen.width, - height: screen.height, - frameRate: 60, - } - }); - } catch(err) { - console.error("Error: " + err); - } - - const video = document.createElement("video"); - video.srcObject = captureStream; - video.play(); - - const result = await new Promise((resolve, reject) => { - video.onloadedmetadata = function () { - resolve(); +import { Device } from 'keyman/engine/device-detect'; + +import type { KeymanEngine } from 'keyman/app/browser'; +import type { FloatingOSKView } from 'keyman/engine/osk'; + +declare var keyman: KeymanEngine; + +type KeyboardData = ReturnType; +type KeyboardMap = {[id: string]: KeyboardData}; + +export class BatchRenderer { + static divMaster: HTMLDivElement; + static dummy: HTMLInputElement; + static captureStream: any; + static video: any; + static boundingRect: DOMRect; + static allLayers = false; + static keyboardMatch: RegExp; + + async startCapture() { + let captureStream = null; + + try { + captureStream = await (navigator.mediaDevices as any).getDisplayMedia({ + audio: false, + video: { + width: screen.width, + height: screen.height, + frameRate: 60, } }); - - return {captureStream: captureStream, video: video}; + } catch(err) { + console.error("Error: " + err); } - // Filters the keyboard array to ensure only a single entry remains, rather than an entry per language. - private filterKeyboards(): KeyboardMap { - let keyman = window['keyman']; + const video = document.createElement("video"); + video.srcObject = captureStream; + video.play(); - let kbds = keyman['getKeyboards'](); + const result = await new Promise((resolve, reject) => { + video.onloadedmetadata = function () { + resolve(); + } + }); - let keyboardMap = []; + return {captureStream: captureStream, video: video}; + } - for(var i = 0; i < kbds.length; i++) { - let id: string = kbds[i]['InternalName']; - if(!id.match(BatchRenderer.keyboardMatch)) continue; - if(id.match(/^Keyboard_bod/)) continue; // bod keyboards currently don't load on Chrome and break the test run; they need rebuild. - if(keyboardMap[id]) { - continue; - } else { - keyboardMap[id] = kbds[i]; - } - } + // Filters the keyboard array to ensure only a single entry remains, rather than an entry per language. + private filterKeyboards(): KeyboardMap { + let kbds = keyman.getKeyboards(); - return keyboardMap; + let keyboardMap = {}; + + for(var i = 0; i < kbds.length; i++) { + let id: string = kbds[i].InternalName; + if(!id.match(BatchRenderer.keyboardMatch)) continue; + if(id.match(/^Keyboard_bod/)) continue; // bod keyboards currently don't load on Chrome and break the test run; they need rebuild. + if(keyboardMap[id]) { + continue; + } else { + keyboardMap[id] = kbds[i]; + } } - private render(ele: HTMLElement, isMobile?: boolean): Promise { - const capture = async () => { - const result = await new Promise((resolve, reject) => { - // from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js - const canvas = document.createElement('canvas'); - canvas.width = BatchRenderer.boundingRect.width; - canvas.height = BatchRenderer.boundingRect.height; - const context = canvas.getContext('2d'); - // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement - context.drawImage(BatchRenderer.video, - BatchRenderer.boundingRect.left, - BatchRenderer.boundingRect.top, - canvas.width, canvas.height, - 0, 0, canvas.width, canvas.height); - - const frame = canvas.toDataURL("image/png"); - let imgOut = document.createElement('img'); - imgOut.src = frame; - - resolve(imgOut); - }); + return keyboardMap; + } - return result; - } + private render(ele: HTMLElement, isMobile?: boolean): Promise { + const capture = async () => { + const result = await new Promise((resolve, reject) => { + // from: https://github.com/kasprownik/electron-screencapture/blob/master/index.js + const canvas = document.createElement('canvas'); + canvas.width = BatchRenderer.boundingRect.width; + canvas.height = BatchRenderer.boundingRect.height; + const context = canvas.getContext('2d'); + // see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement + context.drawImage(BatchRenderer.video, + BatchRenderer.boundingRect.left, + BatchRenderer.boundingRect.top, + canvas.width, canvas.height, + 0, 0, canvas.width, canvas.height); + + const frame = canvas.toDataURL("image/png"); + let imgOut = document.createElement('img'); + imgOut.src = frame; + + resolve(imgOut); + }); - return capture(); + return result; } - createKeyboardHeader(kbd, loaded: boolean): HTMLDivElement { - let divHeader = document.createElement('div'); - let eleName = document.createElement('h2'); + return capture(); + } - eleName.textContent = 'ID: ' + kbd['InternalName']; - divHeader.appendChild(eleName); + createKeyboardHeader(kbd: KeyboardData, loaded: boolean): HTMLDivElement { + let divHeader = document.createElement('div'); + let eleName = document.createElement('h2'); - let eleDescription = document.createElement('p'); + eleName.textContent = 'ID: ' + kbd.InternalName; + divHeader.appendChild(eleName); - if(loaded) { + let eleDescription = document.createElement('p'); - eleDescription.appendChild(document.createTextNode('Name: ' + kbd['Name'])); - eleDescription.appendChild(document.createElement('br')); - eleDescription.appendChild(document.createTextNode('Font: ' + window['keyman'].core.activeKeyboard._legacyLayoutSpec.F)); + if(loaded) { - } else { - eleDescription.appendChild(document.createTextNode('Unable to load this keyboard!')); - } + eleDescription.appendChild(document.createTextNode('Name: ' + kbd.Name)); + eleDescription.appendChild(document.createElement('br')); + eleDescription.appendChild(document.createTextNode('Font: ' + keyman.core.activeKeyboard['_legacyLayoutSpec'].F)); - divHeader.appendChild(eleDescription); - - return divHeader; + } else { + eleDescription.appendChild(document.createTextNode('Unable to load this keyboard!')); } - private processKeyboard(kbd) { - let keyman = window['keyman']; - let p: Promise = keyman.setActiveKeyboard(kbd['InternalName']); - let isMobile = keyman.util.device.formFactor != 'desktop'; + divHeader.appendChild(eleDescription); - // Establish common keyboard header info. - let divSummary = document.createElement('div'); - // Establishes a linkable target for this keyboard's data. - divSummary.id = "summary-" + kbd['InternalName']; + return divHeader; + } - BatchRenderer.divMaster.insertAdjacentElement('afterbegin', divSummary); + private processKeyboard(kbd: KeyboardData) { + let p: Promise = keyman.setActiveKeyboard(kbd.InternalName); + let isMobile = keyman.config.hostDevice.formFactor != 'desktop'; - // A nice, closure-friendly reference for use in our callbacks. - let renderer = this; + // Establish common keyboard header info. + let divSummary = document.createElement('div'); + // Establishes a linkable target for this keyboard's data. + divSummary.id = "summary-" + kbd.InternalName; + BatchRenderer.divMaster.insertAdjacentElement('afterbegin', divSummary); - // Once the keyboard's loaded, we can really get started. - return p.then(function() { - let box: HTMLDivElement = keyman.osk._Box; + // A nice, closure-friendly reference for use in our callbacks. + let renderer = this; - BatchRenderer.boundingRect = box.getBoundingClientRect(); + // Once the keyboard's loaded, we can really get started. + return p.then(function() { + let box: HTMLDivElement = keyman.osk._Box; - // Appromixate handling for non-fullscreen mode runs. - // Adjusts for the browser window's title bars, address bars, etc. - const screenOffsetY = window.outerHeight - window.innerHeight; - BatchRenderer.boundingRect.y += screenOffsetY; + BatchRenderer.boundingRect = box.getBoundingClientRect(); - divSummary.appendChild(renderer.createKeyboardHeader(kbd, true)); + // Appromixate handling for non-fullscreen mode runs. + // Adjusts for the browser window's title bars, address bars, etc. + const screenOffsetY = window.outerHeight - window.innerHeight; + BatchRenderer.boundingRect.y += screenOffsetY; - let divRenders = document.createElement('div'); - divSummary.appendChild(divRenders); + divSummary.appendChild(renderer.createKeyboardHeader(kbd, true)); - // Uses 'private' APIs that may be subject to change in the future. Keep it updated! - var layers; - if(isMobile) { - layers = keyman.osk.vkbd.layerGroup.layers; - } else { - // The desktop OSK will be overpopulated, with a number of blank layers to display in most cases. - // We instead rely upon the KLS definition to ensure we keep the renders sparse. - layers = keyman.core.activeKeyboard._legacyLayoutSpec.KLS; - } + let divRenders = document.createElement('div'); + divSummary.appendChild(divRenders); - let renderLayer = function(i: number) { - return new Promise(function(resolve) { - // (Private API) Directly sets the keyboard layer within KMW, then uses .show to force-display it. - if(keyman.osk.vkbd) { - keyman.core.keyboardProcessor.layerId = Object.keys(layers)[i]; - } else { - console.error("Error - keyman.osk.vkbd is undefined!"); - } - // Make sure the active element's still set! - renderer.setActiveDummy(); - keyman.osk.show(true); - - (document as any).fonts.ready.then(function() { - window.setTimeout(function() { - renderer.render(box, isMobile).then(function(imgEle: HTMLImageElement) { - let eleLayer = document.createElement('div'); - let eleLayerId = document.createElement('p'); - eleLayerId.textContent = 'Layer ID: ' + Object.keys(layers)[i]; - - eleLayer.appendChild(eleLayerId); - eleLayer.appendChild(imgEle); - eleLayer.appendChild(document.createElement('br')); - - divRenders.appendChild(eleLayer); - resolve(i); - }); - }, 100); - }); + // Uses 'private' APIs that may be subject to change in the future. Keep it updated! + var layers; + if(isMobile) { + layers = keyman.osk.vkbd.layerGroup.layers; + } else { + // The desktop OSK will be overpopulated, with a number of blank layers to display in most cases. + // We instead rely upon the KLS definition to ensure we keep the renders sparse. + // + // _legacyLayoutSpec is technically private, but it's what we've been using, so... yeah. + layers = keyman.core.activeKeyboard['_legacyLayoutSpec'].KLS; + } + + let renderLayer = function(i: number) { + return new Promise(function(resolve) { + // (Private API) Directly sets the keyboard layer within KMW, then uses .show to force-display it. + if(keyman.osk.vkbd) { + keyman.core.keyboardProcessor.layerId = Object.keys(layers)[i]; + } else { + console.error("Error - keyman.osk.vkbd is undefined!"); + } + // Make sure the active element's still set! + renderer.setActiveDummy(); + keyman.osk.show(true); + + (document as any).fonts.ready.then(function() { + window.setTimeout(function() { + renderer.render(box, isMobile).then(function(imgEle: HTMLImageElement) { + let eleLayer = document.createElement('div'); + let eleLayerId = document.createElement('p'); + eleLayerId.textContent = 'Layer ID: ' + Object.keys(layers)[i]; + + eleLayer.appendChild(eleLayerId); + eleLayer.appendChild(imgEle); + eleLayer.appendChild(document.createElement('br')); + + divRenders.appendChild(eleLayer); + resolve(i); + }); + }, 100); }); - }; + }); + }; + + // The resulting Promise will only call it's `.then()` once all of this keyboard's renders have been completed. + return renderer.arrayPromiseIteration(renderLayer, Object.keys(layers).length); + }).catch(function() { + console.log("Failed to load the \"" + kbd.InternalName + "\" keyboard for rendering!"); + divSummary.appendChild(renderer.createKeyboardHeader(kbd, false)); + return Promise.resolve(); + }); + } - // The resulting Promise will only call it's `.then()` once all of this keyboard's renders have been completed. - return renderer.arrayPromiseIteration(renderLayer, Object.keys(layers).length); - }).catch(function() { - console.log("Failed to load the \"" + kbd['InternalName'] + "\" keyboard for rendering!"); - divSummary.appendChild(renderer.createKeyboardHeader(kbd, false)); + // Synchronously performs asynchronous operations across a loop, one at a time. + // Necessary due to the nature of KMW OSK rendering. + private arrayPromiseIteration(promiseGenerator: (i: number) => Promise, length: number): Promise { + let iteration = function(index: number): Promise { + if(index < length) { + var promise = promiseGenerator(index); + return promise.then(function(index: number) { + return iteration(++index); + }) + } else { return Promise.resolve(); - }); - } - - // Synchronously performs asynchronous operations across a loop, one at a time. - // Necessary due to the nature of KMW OSK rendering. - private arrayPromiseIteration(promiseGenerator: (i: number) => Promise, length: number): Promise { - let iteration = function(index: number): Promise { - if(index < length) { - var promise = promiseGenerator(index); - return promise.then(function(index: number) { - return iteration(++index); - }) - } else { - return Promise.resolve(); - } } - - return iteration(0); } - fillDeviceNotes() { - let description = document.createElement('p'); - let device = new com.keyman.Device(); - device.detect(); + return iteration(0); + } - description.appendChild(document.createTextNode('Browser: ' + device.browser)); - description.appendChild(document.createElement('br')); - description.appendChild(document.createTextNode('OS: ' + device.OS)); - description.appendChild(document.createElement('br')); - description.appendChild(document.createTextNode('Form factor: ' + device.formFactor)); - description.appendChild(document.createElement('br')); - description.appendChild(document.createTextNode('Touchable: ' + device.touchable)); + fillDeviceNotes() { + let description = document.createElement('p'); + let device = new Device(); + device.detect(); - document.getElementById('deviceNotes').appendChild(description); - } + description.appendChild(document.createTextNode('Browser: ' + device.browser)); + description.appendChild(document.createElement('br')); + description.appendChild(document.createTextNode('OS: ' + device.OS)); + description.appendChild(document.createElement('br')); + description.appendChild(document.createTextNode('Form factor: ' + device.formFactor)); + description.appendChild(document.createElement('br')); + description.appendChild(document.createTextNode('Touchable: ' + device.touchable)); - setActiveDummy() { - window['keyman'].domManager.activeElement = BatchRenderer.dummy; - } + document.getElementById('deviceNotes').appendChild(description); + } - async run(allLayers, filter) { - BatchRenderer.allLayers = allLayers; - BatchRenderer.keyboardMatch = new RegExp('^Keyboard_('+filter+')', 'i'); - if(window['keyman']) { - let keyman = window['keyman']; - - let cc = await this.startCapture(); - BatchRenderer.captureStream = cc.captureStream; - BatchRenderer.video = cc.video; - - // Establish a 'dummy' element to bypass the 'nothing's active' check KMW usually uses. - BatchRenderer.dummy = document.createElement('input'); - window['keyman'].attachToControl(BatchRenderer.dummy); - this.setActiveDummy(); - - BatchRenderer.divMaster = document.getElementById('renderList'); - if(BatchRenderer.divMaster.childElementCount > 0) { - console.log("Prior bulk-renderer run detected. Terminating execution."); - return; - } + setActiveDummy() { + keyman.setActiveElement(BatchRenderer.dummy, true); + } - // We want the renderer to control where the keyboard is displayed. - // Also bypasses another 'fun' OSK complication. - if(keyman.util.device.formFactor == 'desktop') { - keyman.osk.userPositioned = true; - } + async run(allLayers, filter) { + BatchRenderer.allLayers = allLayers; + BatchRenderer.keyboardMatch = new RegExp('^Keyboard_('+filter+')', 'i'); + if(window['keyman']) { + let cc = await this.startCapture(); + BatchRenderer.captureStream = cc.captureStream; + BatchRenderer.video = cc.video; + + // Establish a 'dummy' element to bypass the 'nothing's active' check KMW usually uses. + BatchRenderer.dummy = document.createElement('input'); + keyman.attachToControl(BatchRenderer.dummy); + this.setActiveDummy(); + + BatchRenderer.divMaster = document.getElementById('renderList'); + if(BatchRenderer.divMaster.childElementCount > 0) { + console.log("Prior bulk-renderer run detected. Terminating execution."); + return; + } - // Assumes that the keyboards have been preloaded for us. - let kbds = this.filterKeyboards(); + // We want the renderer to control where the keyboard is displayed. + // Also bypasses another 'fun' OSK complication. + if(keyman.config.hostDevice.formFactor == 'desktop') { + (keyman.osk as FloatingOSKView).userPositioned = true; + } - console.log("Unique keyboard ids detected: " + Object.keys(kbds).length); + // Assumes that the keyboards have been preloaded for us. + let kbds = this.filterKeyboards(); - let renderer = this; + console.log("Unique keyboard ids detected: " + Object.keys(kbds).length); - let keyboardIterator = function(i) { - return new Promise(function(resolve) { - renderer.processKeyboard(kbds[Object.keys(kbds)[i]]).then(function () { - //console.log("Keyboard " + i + " processed!"); - resolve(i); - }); + let renderer = this; + + let keyboardIterator = function(i) { + return new Promise(function(resolve) { + renderer.processKeyboard(kbds[Object.keys(kbds)[i]]).then(function () { + //console.log("Keyboard " + i + " processed!"); + resolve(i); }); - }; + }); + }; - this.arrayPromiseIteration(keyboardIterator, Object.keys(kbds).length).then(function() { - // Once all renders are done, we can now tidy the page up and prep it for final display + potential file-saving. + this.arrayPromiseIteration(keyboardIterator, Object.keys(kbds).length).then(function() { + // Once all renders are done, we can now tidy the page up and prep it for final display + potential file-saving. - // This will go at the top of the page when finished, but not when actively rendering. - // We want to leave as much space visible as possible when actively rendering keyboards - // so that auto-scrolling isn't an issue. - renderer.fillDeviceNotes(); - }); - } else { - console.error("KeymanWeb not detected!"); - } + // This will go at the top of the page when finished, but not when actively rendering. + // We want to leave as much space visible as possible when actively rendering keyboards + // so that auto-scrolling isn't an issue. + renderer.fillDeviceNotes(); + }); + } else { + console.error("KeymanWeb not detected!"); } } +} - (function(){ - window['kmw_renderer'] = new com.keyman.renderer.BatchRenderer(); - })(); -} \ No newline at end of file +(function(){ + window['kmw_renderer'] = new BatchRenderer(); +})(); \ No newline at end of file diff --git a/web/src/tools/testing/bulk_rendering/tsconfig.json b/web/src/tools/testing/bulk_rendering/tsconfig.json index b200d751f079d1a8e6c194fdd240e7db9578e142..3ada9b18532afe4f8c05e2a891d2b46c793edadf 100644 --- a/web/src/tools/testing/bulk_rendering/tsconfig.json +++ b/web/src/tools/testing/bulk_rendering/tsconfig.json @@ -1,13 +1,11 @@ { - "extends": "../../../../../tsconfig-base.json", + "extends": "../../../tsconfig.dom.json", "compilerOptions": { - "allowJs": true, - "inlineSources": true, - "module": "none", - "outFile": "../../../../build/tools/testing/bulk_rendering/bulk_render.js", - "sourceMap": true, - "target": "es5" + "baseUrl": "./", + "outDir": "../../../../build/tools/testing/bulk_rendering/obj/", + "tsBuildInfoFile": "../../../../build/tools/testing/bulk_rendering/obj/tsconfig.tsbuildinfo", + "rootDir": "." }, "files": [ @@ -15,9 +13,7 @@ ], "references": [ - { "path": "../../../../../common/web/utils", "prepend": true }, - { "path": "../../../../../common/web/keyman-version", "prepend": true }, - { "path": "../../../../../common/web/lm-message-types" }, - { "path": "../../../engine/device-detect", "prepend": true} + { "path": "../../../app/browser" }, + { "path": "../../../engine/device-detect" } ] } diff --git a/web/src/tools/testing/recorder/browserDriver.ts b/web/src/tools/testing/recorder/browserDriver.ts index 55402f5afb6a302d3994406cdb147745018b687e..53b16ab53d98068372120b507de2aa718343afd5 100644 --- a/web/src/tools/testing/recorder/browserDriver.ts +++ b/web/src/tools/testing/recorder/browserDriver.ts @@ -1,93 +1,102 @@ -namespace KMWRecorder { - export class BrowserDriver { - static readonly physicalEventClass: string = "KeyboardEvent"; - static readonly physicalEventType: string = "keydown"; - - static readonly oskEventClass: string = "MouseEvent"; - static readonly oskDownMouseType: string = "mousedown"; - static readonly oskUpMouseType: string = "mouseup"; - static readonly oskDownTouchType: string = "touchstart"; - static readonly oskUpTouchType: string = "touchend"; - - /** The destination element for use in simulation. */ - private target: HTMLElement; - - constructor(target: HTMLElement) { - this.target = target; - } +import { + InputEventSpec, + InputEventSpecSequence, + OSKInputEventSpec, + PhysicalInputEventSpec +} from "@keymanapp/recorder-core"; + +import { type KeymanEngine } from 'keyman/app/browser'; + +declare var keyman: KeymanEngine; + +export class BrowserDriver { + static readonly physicalEventClass: string = "KeyboardEvent"; + static readonly physicalEventType: string = "keydown"; + + static readonly oskEventClass: string = "MouseEvent"; + static readonly oskDownMouseType: string = "mousedown"; + static readonly oskUpMouseType: string = "mouseup"; + static readonly oskDownTouchType: string = "touchstart"; + static readonly oskUpTouchType: string = "touchend"; + + /** The destination element for use in simulation. */ + private target: HTMLElement; + + constructor(target: HTMLElement) { + this.target = target; + } - simulateEvent(eventSpec: InputEventSpec) { - switch(eventSpec.type) { - case "key": - this.simulateHardwareEvent(eventSpec as PhysicalInputEventSpec); - break; - case "osk": - this.simulateOSKEvent(eventSpec as OSKInputEventSpec); - break; - } + simulateEvent(eventSpec: InputEventSpec) { + switch(eventSpec.type) { + case "key": + this.simulateHardwareEvent(eventSpec as PhysicalInputEventSpec); + break; + case "osk": + this.simulateOSKEvent(eventSpec as OSKInputEventSpec); + break; } + } + + simulateHardwareEvent(eventSpec: PhysicalInputEventSpec) { + // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. + let event = new Event(BrowserDriver.physicalEventType); + event['key'] = eventSpec.key; + event['code'] = eventSpec.code; + event['keyCode'] = eventSpec.keyCode; + event['location'] = eventSpec.location; + event['getModifierState'] = eventSpec.getModifierState.bind(eventSpec); + + this.target.dispatchEvent(event); + } - simulateHardwareEvent(eventSpec: PhysicalInputEventSpec) { - // Yep, not KeyboardEvent. "keyCode" is nasty-bugged in Chrome and unusable if initializing through KeyboardEvent. - let event = new Event(BrowserDriver.physicalEventType); - event['key'] = eventSpec.key; - event['code'] = eventSpec.code; - event['keyCode'] = eventSpec.keyCode; - event['location'] = eventSpec.location; - event['getModifierState'] = eventSpec.getModifierState.bind(eventSpec); + simulateOSKEvent(eventSpec: OSKInputEventSpec) { + let target = this.target; + let oskKeyElement = document.getElementById(eventSpec.keyID); - this.target.dispatchEvent(event); + if(!oskKeyElement) { + console.error('Could not find OSK key "' + eventSpec.keyID + '"!'); + // The following lines will throw an appropriate-enough error. + return; } - simulateOSKEvent(eventSpec: OSKInputEventSpec) { - let target = this.target; - let oskKeyElement = document.getElementById(eventSpec.keyID); - - if(!oskKeyElement) { - console.error('Could not find OSK key "' + eventSpec.keyID + '"!'); - // The following lines will throw an appropriate-enough error. - return; - } - - // To be safe, we replicate the MouseEvent similarly to the keystroke event. - var downEvent; - var upEvent; - if((window['keyman'] as any).util.device.touchable) { - downEvent = new Event(BrowserDriver.oskDownTouchType); - upEvent = new Event(BrowserDriver.oskUpTouchType); - downEvent['touches'] = [{"target": oskKeyElement}]; - upEvent['touches'] = [{"target": oskKeyElement}]; - downEvent['changedTouches'] = [{"target": oskKeyElement}]; - upEvent['changedTouches'] = [{"target": oskKeyElement}]; - } else { - downEvent = new Event(BrowserDriver.oskDownMouseType); - upEvent = new Event(BrowserDriver.oskUpMouseType); - downEvent['relatedTarget'] = target; - upEvent['relatedTarget'] = target; - // Mouse-click driven OSK use involves use of at least one mouse button. - downEvent['button'] = upEvent['button'] = 0; - downEvent['buttons'] = 1; - upEvent['buttons'] = 0; - } - - oskKeyElement.dispatchEvent(downEvent); - oskKeyElement.dispatchEvent(upEvent); + // To be safe, we replicate the MouseEvent similarly to the keystroke event. + var downEvent; + var upEvent; + if(keyman.config.hostDevice.touchable) { + downEvent = new Event(BrowserDriver.oskDownTouchType); + upEvent = new Event(BrowserDriver.oskUpTouchType); + downEvent['touches'] = [{"target": oskKeyElement}]; + upEvent['touches'] = [{"target": oskKeyElement}]; + downEvent['changedTouches'] = [{"target": oskKeyElement}]; + upEvent['changedTouches'] = [{"target": oskKeyElement}]; + } else { + downEvent = new Event(BrowserDriver.oskDownMouseType); + upEvent = new Event(BrowserDriver.oskUpMouseType); + downEvent['relatedTarget'] = target; + upEvent['relatedTarget'] = target; + // Mouse-click driven OSK use involves use of at least one mouse button. + downEvent['button'] = upEvent['button'] = 0; + downEvent['buttons'] = 1; + upEvent['buttons'] = 0; } - // Execution of a test sequence depends on the testing environment; integrated - // testing requires browser-specific code. - simulateSequence(sequence: InputEventSpecSequence): string { - let ele = this.target; + oskKeyElement.dispatchEvent(downEvent); + oskKeyElement.dispatchEvent(upEvent); + } + + // Execution of a test sequence depends on the testing environment; integrated + // testing requires browser-specific code. + simulateSequence(sequence: InputEventSpecSequence): string { + let ele = this.target; - for(var i=0; i < sequence.inputs.length; i++) { - this.simulateEvent(sequence.inputs[i]); - } + for(var i=0; i < sequence.inputs.length; i++) { + this.simulateEvent(sequence.inputs[i]); + } - if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { - return ele.value; - } else { - return ele.textContent; - } + if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { + return ele.value; + } else { + return ele.textContent; } } } \ No newline at end of file diff --git a/web/src/tools/testing/recorder/browserProctor.ts b/web/src/tools/testing/recorder/browserProctor.ts index 74b02449b298cae67ffcd4673884b8389906455e..8a4d4f6b860db11ebbaf77f1fe9c6fbeeab41eea 100644 --- a/web/src/tools/testing/recorder/browserProctor.ts +++ b/web/src/tools/testing/recorder/browserProctor.ts @@ -1,80 +1,97 @@ -/// // In theory, we could totally split Scribe off into its own, separate micro-module. // Only BrowserDriver.ts and its prerequisites are needed during actual integrated unit testing. /// -namespace KMWRecorder { - type AssertCallback = (s1: any, s2: any, msg?: string) => void; +import { type DeviceSpec } from "@keymanapp/web-utils"; - function resetElement(ele: HTMLElement): void { - if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { - (window['keyman'] as any).resetContext(); - ele.value = ""; - } else { - (window['keyman'] as any).resetContext(); - ele.textContent = ""; - } - } +import { type OutputTarget } from "@keymanapp/keyboard-processor"; - /** - * Runs Recorder-generated tests in a browser. - * - * Future notes: further abstraction needed; much of this code will be in common with Node. - */ - export class BrowserProctor extends Proctor { - target: HTMLElement; - usingOSK: boolean; +import { type KeymanEngine } from 'keyman/app/browser'; - constructor(target: HTMLElement, device: com.keyman.utils.DeviceSpec, usingOSK: boolean, assert: AssertCallback) { - super(device, assert); +declare var keyman: KeymanEngine; - this.target = target; - this.usingOSK = usingOSK; - } +import { + InputEventSpec, + InputEventSpecSequence, + KeyboardTest, + Proctor, + RecordedKeystrokeSequence, + TestSequence, + TestSet +} from "@keymanapp/recorder-core"; +import { BrowserDriver } from "./browserDriver.js"; - // Performs browser-specific global test prep. - beforeAll() { - let ele = this.target; - (window['keyman'] as any).setActiveElement(ele['base'] ? ele['base'] : ele); - } - before() { - resetElement(this.target); - } +type AssertCallback = (s1: any, s2: any, msg?: string) => void; - compatibleWithSuite(testSuite: KeyboardTest): boolean { - // So far, the only thing using Recorder code always specifies the necessary data to construct - // DOM events for use in integrated testing. - return true; - } +function resetElement(ele: HTMLElement): void { + if(ele instanceof HTMLInputElement || ele instanceof HTMLTextAreaElement) { + keyman.resetContext(); + ele.value = ""; + } else { + keyman.resetContext(); + ele.textContent = ""; + } +} - // We only need to filter test cases when performing tests in an integrated - // environment. - matchesTestSet(testSet: TestSet) { - return testSet.isValidForDevice(this.device, this.usingOSK); - } +/** + * Runs Recorder-generated tests in a browser. + * + * Future notes: further abstraction needed; much of this code will be in common with Node. + */ +export class BrowserProctor extends Proctor { + target: HTMLElement; + usingOSK: boolean; - // Execution of a test sequence depends on the testing environment; this handles - // the browser-specific aspects. - simulateSequence(sequence: TestSequence, outputTarget?: com.keyman.text.OutputTarget): string { - let driver = new BrowserDriver(this.target); + constructor(target: HTMLElement, device: DeviceSpec, usingOSK: boolean, assert: AssertCallback) { + super(device, assert); - // For the version 10.0 spec - if(sequence instanceof InputEventSpecSequence) { - return driver.simulateSequence(sequence); + this.target = target; + this.usingOSK = usingOSK; + } - // For the version 14.0+ spec - } else if(sequence instanceof RecordedKeystrokeSequence) { - let inputSpecs: InputEventSpec[] = []; + // Performs browser-specific global test prep. + beforeAll() { + let ele = this.target; + (window['keyman'] as any).setActiveElement(ele['base'] ? ele['base'] : ele); + } - for(let i=0; i < sequence.inputs.length; i++) { - inputSpecs[i] = sequence.inputs[i].inputEventSpec; - } + before() { + resetElement(this.target); + } + + compatibleWithSuite(testSuite: KeyboardTest): boolean { + // So far, the only thing using Recorder code always specifies the necessary data to construct + // DOM events for use in integrated testing. + return true; + } + + // We only need to filter test cases when performing tests in an integrated + // environment. + matchesTestSet(testSet: TestSet) { + return testSet.isValidForDevice(this.device, this.usingOSK); + } - // Converts the sequence back to version 10.0, since it's very well made for browser simulation. - let eventSpecSequence = new InputEventSpecSequence(inputSpecs, sequence.output, sequence.msg); - return driver.simulateSequence(eventSpecSequence); + // Execution of a test sequence depends on the testing environment; this handles + // the browser-specific aspects. + simulateSequence(sequence: TestSequence, outputTarget?: OutputTarget): string { + let driver = new BrowserDriver(this.target); + + // For the version 10.0 spec + if(sequence instanceof InputEventSpecSequence) { + return driver.simulateSequence(sequence); + + // For the version 14.0+ spec + } else if(sequence instanceof RecordedKeystrokeSequence) { + let inputSpecs: InputEventSpec[] = []; + + for(let i=0; i < sequence.inputs.length; i++) { + inputSpecs[i] = sequence.inputs[i].inputEventSpec; } + + // Converts the sequence back to version 10.0, since it's very well made for browser simulation. + let eventSpecSequence = new InputEventSpecSequence(inputSpecs, sequence.output, sequence.msg); + return driver.simulateSequence(eventSpecSequence); } } } \ No newline at end of file diff --git a/web/src/tools/testing/recorder/build-bundler.js b/web/src/tools/testing/recorder/build-bundler.js new file mode 100644 index 0000000000000000000000000000000000000000..d74fbecd2bd4b6831dec41aa02af7d42bf5b6e39 --- /dev/null +++ b/web/src/tools/testing/recorder/build-bundler.js @@ -0,0 +1,15 @@ +import esbuild from 'esbuild'; + +await esbuild.build({ + bundle: true, + sourcemap: true, + format: "esm", + nodePaths: ['../../../../../node_modules'], + entryPoints: { + 'index': '../../../../build/tools/testing/recorder/obj/index.js', + }, + outdir: '../../../../build/tools/testing/recorder/lib/', + outExtension: { '.js': '.mjs' }, + tsconfig: './tsconfig.json', + target: "es5" +}); \ No newline at end of file diff --git a/web/src/tools/testing/recorder/build.sh b/web/src/tools/testing/recorder/build.sh index dfe0d600d7e560aad0b5cf81bb8a6c10f8e03dd4..6b5cb18342668eee08e4938148103a1c810ecc61 100755 --- a/web/src/tools/testing/recorder/build.sh +++ b/web/src/tools/testing/recorder/build.sh @@ -1,8 +1,6 @@ #!/usr/bin/env bash # # Compile KeymanWeb's dev & test tool modules -# -set -eu ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary @@ -10,6 +8,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../../../../resources/build/build-utils.sh" ## END STANDARD BUILD SCRIPT INCLUDE +SUBPROJECT_NAME=tools/testing/recorder +. "$KEYMAN_ROOT/web/common.inc.sh" . "$KEYMAN_ROOT/resources/shellHelperFunctions.sh" # This script runs from its own folder @@ -27,28 +27,10 @@ builder_describe "Builds the Keyman Engine for Web's test-sequence recording too builder_describe_outputs \ configure /node_modules \ - build /web/build/tools/testing/recorder/index.js + build /web/build/$SUBPROJECT_NAME/lib/index.mjs builder_parse "$@" -### CONFIGURE ACTIONS - -if builder_start_action configure; then - verify_npm_setup - builder_finish_action success configure -fi - -### CLEAN ACTIONS - -if builder_start_action clean; then - rm -rf ../../../../build/tools/testing/recorder/ - builder_finish_action success clean -fi - -### BUILD ACTIONS - -if builder_start_action build; then - tsc -b $THIS_SCRIPT_PATH/tsconfig.json - - builder_finish_action success build -fi \ No newline at end of file +builder_run_action configure verify_npm_setup +builder_run_action clean rm -rf ../../../../build/$SUBPROJECT_NAME/ +builder_run_action build compile $SUBPROJECT_NAME \ No newline at end of file diff --git a/web/src/tools/testing/recorder/index.html b/web/src/tools/testing/recorder/index.html index 5f7dfe19efb50da42d7403362c18debc3d63917a..f4a68ba76902102e7d46e1fbd34f4af4c245fd8f 100644 --- a/web/src/tools/testing/recorder/index.html +++ b/web/src/tools/testing/recorder/index.html @@ -46,23 +46,17 @@ } - - - - - + + + +