From ec376151fa90d7cd3647b39b1428132f2c2cacb1 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Mon, 17 Mar 2025 22:49:51 +0100 Subject: [PATCH] Import CLDR data directly from XML, switch to CLDR 47 --- .github/workflows/tests.yml | 79 +++- .php-cs-fixer.dist.php | 3 +- README.md | 58 +-- bin/import-cldr-data | 656 +++++++++++++++++++++++++++++ bin/import-cldr-data.bat | 1 + composer.json | 3 +- src/CldrData.php | 2 + tests/test/ExecutableFilesTest.php | 1 + 8 files changed, 731 insertions(+), 72 deletions(-) create mode 100755 bin/import-cldr-data create mode 100644 bin/import-cldr-data.bat diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7eefddc..42ab917 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,9 +1,14 @@ name: Tests +env: + BUILD_CLDR_VERSION: "47" + on: push: branches: - main + tags-ignore: + - "**" pull_request: branches: - main @@ -13,18 +18,21 @@ jobs: name: Check PHP coding style runs-on: ubuntu-latest steps: - - name: Setup PHP + - + name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "8.1" extensions: mbstring, zip tools: composer:2, php-cs-fixer:3 coverage: none - - name: Checkout - uses: actions/checkout@v2 - - name: Check PHP coding style - run: | - php-cs-fixer fix --path-mode=intersection --config=./.php-cs-fixer.dist.php --dry-run --using-cache=no --diff --show-progress=dots --verbose --no-interaction --ansi . + - + name: Checkout + uses: actions/checkout@v4 + - + name: Check PHP coding style + run: php-cs-fixer fix --config=./.php-cs-fixer.dist.php --dry-run --using-cache=no --diff --show-progress=dots --verbose --no-interaction --ansi + phpunit: name: Run PHPUnit tests needs: php-coding-style @@ -45,24 +53,71 @@ jobs: - "8.0" - "8.1" - "8.2" + - "8.3" + - "8.4" include: - os: windows-latest php-version: "5.6" - os: windows-latest php-version: "7.4" - os: windows-latest - php-version: "8.1" + php-version: "8.4" runs-on: ${{ matrix.os }} steps: - - name: Setup PHP + - + name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-version }} tools: composer:v2 coverage: none - - name: Checkout - uses: actions/checkout@v2 - - name: Install Composer dependencies + - + name: Checkout + uses: actions/checkout@v4 + - + name: Install Composer dependencies run: composer update --no-progress --no-suggest --optimize-autoloader --ansi --no-interaction - - name: Run PHPUnit + - + name: Build CLDR data + run: php ./bin/import-cldr-data ${{ env.BUILD_CLDR_VERSION }} + - + name: Run PHPUnit run: composer --no-interaction run-script test + + commit: + name: Commit CLDR data + runs-on: ubuntu-latest + needs: phpunit + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + steps: + - + name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + tools: none + coverage: none + - + name: Checkout + uses: actions/checkout@v4 + - + name: Build CLDR data + run: php ./bin/import-cldr-data ${{ env.BUILD_CLDR_VERSION }} + - + name: Check changes + id: check-changes + run: | + git add --all src/cldr-data + if git diff-index --name-status --exit-code HEAD src/cldr-data; then + echo 'No changes detected.' + else + echo 'Changes detected.' + echo 'commit=yes' >> $GITHUB_OUTPUT + fi + - name: Commit changes + if: steps.check-changes.outputs.commit == 'yes' + run: | + git config --local user.name 'GitHub Actions' + git config --local user.email noreply@github.com + git commit -m 'Update CLDR data' + git push diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index ee45cc4..45c4dfa 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -26,6 +26,7 @@ ]) ->append([ __DIR__ . '/bin/export-plural-rules', + __DIR__ . '/bin/import-cldr-data', ]) ->in(__DIR__) ) @@ -245,8 +246,6 @@ 'phpdoc_no_access' => true, // No alias PHPDoc tags should be used. 'phpdoc_no_alias_tag' => true, - // `@return void` and `@return null` annotations should be omitted from PHPDoc. - 'phpdoc_no_empty_return' => true, // `@package` and `@subpackage` annotations should be omitted from PHPDoc. 'phpdoc_no_package' => true, // Classy that does not inherit must not have `@inheritdoc` tags. diff --git a/README.md b/README.md index 63dcb8e..59f5a17 100644 --- a/README.md +++ b/README.md @@ -172,63 +172,7 @@ the resulting formulas will be in this format: ### Generating the CLDR data This repository uses the CLDR data, including American English (`en_US`) json files. -In order to generate this data, you can use Docker. -Start a new Docker container by running - -```sh -docker run --rm -it -v path/to/src/cldr-data:/output alpine:3.13 sh -``` - -Then run the following script, setting the values of the variables accordingly to your needs: - -```sh -# The value of the CLDR version (eg 39, 38.1, ...) -CLDR_VERSION=39 -# Your GitHub username (required since CLDR 38) - see http://cldr.unicode.org/development/maven#TOC-Introduction -GITHUB_USERNAME= -# Your GitHub personal access token (required since CLDR 38) - see http://cldr.unicode.org/development/maven#TOC-Introduction -GITHUB_TOKEN= - -if ! test -d /output; then - echo 'Missing output directory' >&2 - return 1 -fi -apk -U upgrade -apk add --no-cache git git-lfs openjdk8 apache-ant maven -CLDR_MAJORVERSION="$(printf '%s' "$CLDR_VERSION" | sed -E 's/^([0-9]+).*/\1/')" -SOURCE_DIR="$(mktemp -d)" -DESTINATION_DIR="$(mktemp -d)" -git clone --single-branch --depth=1 "--branch=release-$(printf '%s' "$CLDR_VERSION" | tr '.' '-')" https://github.com/unicode-org/cldr.git "$SOURCE_DIR" -if test $CLDR_MAJORVERSION -lt 38; then - git -C "$SOURCE_DIR" lfs pull --include tools/java || true - ant -f "$SOURCE_DIR/tools/java/build.xml" jar - JARFILE="$SOURCE_DIR/tools/java/cldr.jar" - DESTINATION_DIR_LOCALE="$DESTINATION_DIR/en_US" - DESTINATION_FILE_PLURALS="$DESTINATION_DIR/supplemental/plurals.json" -else - if test -z "${GITHUB_USERNAME:-}"; then - echo 'GITHUB_USERNAME is missing' >&2 - return 1 - fi - if test -z "${GITHUB_TOKEN:-}"; then - echo 'GITHUB_TOKEN is missing' >&2 - return 1 - fi - printf 'githubicu%s%s' "$GITHUB_USERNAME" "$GITHUB_TOKEN" > "$SOURCE_DIR/mvn-settings.xml" - mvn --settings "$SOURCE_DIR/mvn-settings.xml" package -DskipTests=true --file "$SOURCE_DIR/tools/cldr-code/pom.xml" - JARFILE="$SOURCE_DIR//tools/cldr-code/target/cldr-code.jar" - DESTINATION_DIR_LOCALE="$DESTINATION_DIR" - DESTINATION_FILE_PLURALS="$DESTINATION_DIR/supplemental/plurals/plurals.json" -fi -java -Duser.language=en -Duser.country=US "-DCLDR_DIR=$SOURCE_DIR" "-DCLDR_GEN_DIR=$DESTINATION_DIR_LOCALE" -jar "$JARFILE" ldml2json -t main -r true -s contributed -m en_US -java -Duser.language=en -Duser.country=US "-DCLDR_DIR=$SOURCE_DIR" "-DCLDR_GEN_DIR=$DESTINATION_DIR/supplemental" -jar "$JARFILE" ldml2json -s contributed -o true -t supplemental -mkdir -p /output/main/en-US -cp $DESTINATION_DIR/en_US/languages.json /output/main/en-US/ -cp $DESTINATION_DIR/en_US/scripts.json /output/main/en-US/ -cp $DESTINATION_DIR/en_US/territories.json /output/main/en-US/ -mkdir -p /output/supplemental -cp "$DESTINATION_FILE_PLURALS" /output/supplemental/ -``` +In order to generate this data, you can use the `bin/import-cldr-data` CLI command. ## Support this project diff --git a/bin/import-cldr-data b/bin/import-cldr-data new file mode 100755 index 0000000..db6d2fa --- /dev/null +++ b/bin/import-cldr-data @@ -0,0 +1,656 @@ +#!/usr/bin/env php +outputDir) && !mkdir($options->outputDir, 0777, true)) { + throw new RuntimeException("Cannot create output directory: {$options->outputDir}\n"); + } + $options->outputDir = str_replace(DIRECTORY_SEPARATOR, '/', realpath($options->outputDir)); + $documentStorage = new DocumentStorage($options); + echo 'Processing languages... '; + $languages = new Languages($options, $documentStorage); + echo "done.\n"; + echo 'Processing scripts... '; + $scripts = new Scripts($options, $documentStorage); + echo "done.\n"; + echo 'Processing territories... '; + $territories = new Territories($options, $documentStorage); + echo "done.\n"; + echo 'Processing plural rules... '; + $plurals = new Plurals($options, $documentStorage, $languages); + echo "done.\n"; + echo 'Saving... '; + + $languages->save(); + $scripts->save(); + $territories->save(); + $plurals->save(); + + echo "done.\n"; +} + +class Options +{ + /** + * @var string + */ + public $cldrVersion; + + /** + * @var string + */ + public $outputDir; + + public function __construct(array $argv) + { + if (array_intersect($argv, array('-h', '--help'))) { + $this->showSyntax($argv[0], 0); + } + $this->outputDir = $this->getDefaultOutputDir(); + switch (count($argv)) { + case 3: + $this->outputDir = str_replace(DIRECTORY_SEPARATOR, '/', $argv[2]); + // no break + case 2: + $this->cldrVersion = $argv[1]; + if (!preg_match('/^\d+(\.\d+)?(-(alpha|beta)\d+)?$/', $this->cldrVersion)) { + throw new RuntimeException("{$this->cldrVersion} is not a valid CLDR version identifier"); + } + break; + default: + $this->showSyntax($argv[0], 1); + } + } + + /** + * @param string $programName + * @param int $exitCode + * + * @return never + */ + private function showSyntax($programName, $exitCode) + { + $programName = str_replace('/', DIRECTORY_SEPARATOR, $programName); + $defaultOutputDir = str_replace('/', DIRECTORY_SEPARATOR, $this->getDefaultOutputDir()); + + echo << [output-dir] + +Arguments: + cldr-version: the version of the CLDR data. + Examples: + 47 + 47-beta2 + 47-alpha1 + 46.1 + 46.1-beta1 + output-dir: the directory where the data will be written to + Default: {$defaultOutputDir} + +EOT; + exit($exitCode); + } + + /** + * @return string + */ + private function getDefaultOutputDir() + { + return str_replace(DIRECTORY_SEPARATOR, '/', dirname(__DIR__)) . '/src/cldr-data'; + } +} + +class DocumentStorage +{ + /** + * @var string + */ + private $baseUrl; + + private $context; + + private $cache; + + public function __construct(Options $options) + { + $this->baseUrl = 'https://raw.githubusercontent.com/unicode-org/cldr/refs/tags/release-' . str_replace('.', '-', $options->cldrVersion); + $this->context = stream_context_create(array( + 'http' => array( + 'follow_location' => 1, + 'ignore_errors' => false, + ), + )); + $this->cache = array(); + } + + /** + * @param string $path + * + * @throws RuntimeException + * + * @return DOMDocument + */ + public function get($path) + { + if (!isset($this->cache[$path])) { + $xml = $this->fetch($path); + $doc = $this->loadXml($xml); + $this->cache[$path] = $doc; + } + + return $this->cache[$path]; + } + + /** + * @param string $path + * + * @throws RuntimeException + * + * @return string + */ + private function fetch($path) + { + $url = $this->baseUrl . '/' . ltrim($path, '/'); + set_error_handler(function () {}, -1); + $content = file_get_contents($url, false, $this->context); + restore_error_handler(); + if ($content === false) { + $details = ''; + /** @var array $http_response_header */ + if (!empty($http_response_header)) { + $details = " - {$http_response_header[0]}"; + } + throw new RuntimeException("Failed to download from {$url}{$details}"); + } + + return $content; + } + + /** + * @param string $xml + * + * @throws RuntimeException + * + * @return DOMDocument + */ + private function loadXml($xml) + { + $doc = new DOMDocument(); + libxml_clear_errors(); + $restore = libxml_use_internal_errors(true); + $loaded = $doc->loadXML($xml); + $errors = libxml_get_errors(); + libxml_use_internal_errors($restore); + $lines = array(); + foreach ($errors as $error) { + $lines[] = "{$error->message} at line {$error->line}"; + } + if (!$loaded || $errors !== array()) { + throw new RuntimeException("Failed to parse XML:\n" . implode("\n", $lines)); + } + + return $doc; + } +} + +abstract class Processor +{ + /** + * @var Options + */ + protected $options; + + /** + * @var array + */ + protected $data; + + /** + * @var DocumentStorage + */ + private $documentStorage; + + /** + * @var string + */ + private $path; + + protected function __construct(Options $options, DocumentStorage $documentStorage, $path) + { + $this->options = $options; + $this->documentStorage = $documentStorage; + $this->path = ltrim($path, '/'); + $doc = $this->documentStorage->get($this->path); + $this->data = $this->parse($doc); + } + + /** + * @return void + */ + public function save() + { + $file = $this->getOutputFile(); + $dir = dirname($file); + if (!is_dir($dir) && !mkdir($dir, 0777, true)) { + throw new RuntimeException("Cannot create directory: {$dir}"); + } + $flags = 0; + if (defined('JSON_UNESCAPED_SLASHES')) { + $flags |= JSON_UNESCAPED_SLASHES; + } + if (defined('JSON_UNESCAPED_UNICODE')) { + $flags |= JSON_UNESCAPED_UNICODE; + } + if (defined('JSON_PRETTY_PRINT')) { + $flags |= JSON_PRETTY_PRINT; + } + if (defined('JSON_THROW_ON_ERROR')) { + $flags |= JSON_THROW_ON_ERROR; + } + $json = json_encode($this->data, $flags); + if (!file_put_contents($file, $json)) { + throw new RuntimeException("Failed to write to file: {$file}"); + } + } + + /** + * @return array + */ + abstract protected function parse(DOMDocument $doc); + + /** + * @return void + */ + protected function sortByKeyWithPossiblyAlt(array &$data) + { + uksort($data, function ($a, $b) { + $aAlt = strpos($a, '-alt-') !== false; + $bAlt = strpos($b, '-alt-') !== false; + if ($aAlt !== $bAlt) { + if (strpos("{$a}-alt-", $b) === 0) { + return 0; + } + if (strpos($a, "{$b}-alt-") === 0) { + return -1; + } + } + + return strcasecmp($a, $b); + }); + } + + /** + * @return string + */ + abstract protected function getOutputRelativeFileName(); + + /** + * @param string $xml + * + * @return DOMDocument + */ + private function loadXml($xml) + { + $doc = new DOMDocument(); + libxml_clear_errors(); + $restore = libxml_use_internal_errors(true); + $loaded = $doc->loadXML($xml); + $errors = libxml_get_errors(); + libxml_use_internal_errors($restore); + $lines = array(); + foreach ($errors as $error) { + $lines[] = "{$error->message} at line {$error->line}"; + } + if (!$loaded || $errors !== array()) { + throw new RuntimeException("Failed to parse XML:\n" . implode("\n", $lines)); + } + return $doc; + } + + /** + * @return string + */ + private function getOutputFile() + { + return $this->options->outputDir . '/' . ltrim($this->getOutputRelativeFileName(), '/'); + } +} + +class Plurals extends Processor +{ + /** + * @var Languages + */ + private $languages; + + public function __construct(Options $options, DocumentStorage $documentStorage, Languages $languages) + { + $this->languages = $languages; + parent::__construct($options, $documentStorage, 'common/supplemental/plurals.xml'); + } + + /** + * {@inheritdoc} + * + * @see Processor::parse() + */ + protected function parse(DOMDocument $doc) + { + $data = array(); + $xpath = new DOMXPath($doc); + $xPluralRulesList = $xpath->query('/supplementalData/plurals[@type="cardinal"]/pluralRules'); + $definedLanguageIDs = $this->languages->getDefinedLanguageIDs(); + $knownMissingLanguages = array( + 'guw', // Gun + 'lld', // Dolomitic Ladin + 'hnj', // Hmong Njua + 'nah', // Nahuatl + 'smi', // Sami + ); + $replacements = array( + 'in' => 'id', // Former Indonesian + 'iw' => 'he', // Former Hebrew + 'jw' => 'jv', // Former Javanese + 'ji' => 'yi', // Former Yiddish + 'mo' => 'ro-MD', // former Moldavian + 'bh' => '', // Former Bihari: dismissed because it can be 'bho', 'mai' or 'mag' + // Just a CLDR placeholder + 'root' => '', + ); + $unrecognizedLocaleCodes = array(); + foreach ($xPluralRulesList as $xPluralRules) { + $locales = preg_split('/\s+/', (string) $xPluralRules->getAttribute('locales'), -1, PREG_SPLIT_NO_EMPTY); + if ($locales === array()) { + throw new RuntimeException('No locales found in pluralRules element'); + } + $elements = array( + 'pluralRule-count-zero' => null, + 'pluralRule-count-one' => null, + 'pluralRule-count-two' => null, + 'pluralRule-count-few' => null, + 'pluralRule-count-many' => null, + 'pluralRule-count-other' => null, + ); + foreach ($xPluralRules->childNodes as $xPluralRule) { + if (!$xPluralRule instanceof DOMElement) { + continue; + } + if ($xPluralRule->tagName !== 'pluralRule') { + throw new RuntimeException("Unexpected element: {$xPluralRule->tagName}"); + } + $count = (string) $xPluralRule->getAttribute('count'); + if ($count === '') { + throw new RuntimeException('Missing count attribute'); + } + $key = "pluralRule-count-{$count}"; + if (!array_key_exists($key, $elements)) { + throw new RuntimeException("Unknown count: {$count}"); + } + if ($elements[$key] !== null) { + throw new RuntimeException("Duplicate count: {$count}"); + } + $elements[$key] = $xPluralRule->textContent; + } + $elements = array_filter($elements, function ($value) { + return $value !== null; + }); + if ($elements === array()) { + throw new RuntimeException('No plural rules found'); + } + foreach ($locales as $locale) { + $locale = str_replace('_', '-', $locale); + $overwrite = true; + if (isset($data[$locale]) && array_search($locale, $replacements, true) === false) { + throw new RuntimeException("Duplicate locale: {$locale}"); + } + if (!in_array($locale, $definedLanguageIDs, true) && !in_array($locale, $knownMissingLanguages, true)) { + if (!isset($replacements[$locale])) { + $unrecognizedLocaleCodes[] = $locale; + continue; + } + $locale = $replacements[$locale]; + if ($locale === '') { + continue; + } + $overwrite = false; + } + if ($overwrite || !isset($data[$locale])) { + $data[$locale] = $elements; + } + } + } + if ($unrecognizedLocaleCodes !== array()) { + throw new RuntimeException("The following locales are not defined:\n- " . implode("\n- ", $unrecognizedLocaleCodes)); + } + if ($data === array()) { + throw new RuntimeException('No plural rules found'); + } + $this->sortByKeyWithPossiblyAlt($data); + + return array( + 'supplemental' => array( + 'version' => array( + '_cldrVersion' => $this->options->cldrVersion, + ), + 'plurals-type-cardinal' => $data, + ), + ); + } + + /** + * {@inheritdoc} + * + * @see Processor::getOutputRelativeFileName() + */ + protected function getOutputRelativeFileName() + { + return 'supplemental/plurals.json'; + } +} + +abstract class LocaleDisplayName extends Processor +{ + public function __construct(Options $options, DocumentStorage $documentStorage) + { + parent::__construct($options, $documentStorage, 'common/main/en.xml'); + } + + /** + * {@inheritdoc} + * + * @see Processor::parse() + */ + protected function parse(DOMDocument $doc) + { + $data = array(); + $xpath = new DOMXPath($doc); + $xElementList = $xpath->query($this->getXPathSelector()); + foreach ($xElementList as $xElement) { + $type = (string) $xElement->getAttribute('type'); + if ($type === '') { + throw new RuntimeException('Missing type attribute'); + } + $key = str_replace('_', '-', $type); + $alt = (string) $xElement->getAttribute('alt'); + if ($alt !== '') { + $key = "{$key}-alt-{$alt}"; + } + if (isset($data[$key])) { + throw new RuntimeException("Duplicate key: {$key}"); + } + $data[$key] = (string) $xElement->textContent; + } + if ($data === array()) { + throw new RuntimeException('No elements found'); + } + $this->sortByKeyWithPossiblyAlt($data); + + return array( + 'main' => array( + 'en-US' => array( + 'identity' => array( + 'version' => array( + '_cldrVersion' => $this->options->cldrVersion, + ), + 'language' => 'en', + 'territory' => 'US', + ), + 'localeDisplayNames' => array( + $this->getExportedNodeName() => $data, + ), + ), + ), + ); + } + + /** + * @return string + */ + abstract protected function getXPathSelector(); + + /** + * @return string + */ + abstract protected function getExportedNodeName(); +} + +class Languages extends LocaleDisplayName +{ + /** + * @return string[] + */ + public function getDefinedLanguageIDs() + { + return array_values(array_filter( + array_keys($this->data['main']['en-US']['localeDisplayNames'][$this->getExportedNodeName()]), + function ($key) { + return strpos((string) $key, '-alt-') === false; + } + )); + } + + /** + * {@inheritdoc} + * + * @see LocaleDisplayName::getXPathSelector() + */ + protected function getXPathSelector() + { + return '/ldml/localeDisplayNames/languages/language'; + } + + /** + * {@inheritdoc} + * + * @see LocaleDisplayName::getExportedNodeName() + */ + protected function getExportedNodeName() + { + return 'languages'; + } + + /** + * {@inheritdoc} + * + * @see Processor::getOutputRelativeFileName() + */ + protected function getOutputRelativeFileName() + { + return 'main/en-US/languages.json'; + } +} + +class Scripts extends LocaleDisplayName +{ + /** + * {@inheritdoc} + * + * @see LocaleDisplayName::getXPathSelector() + */ + protected function getXPathSelector() + { + return '/ldml/localeDisplayNames/scripts/script'; + } + + /** + * {@inheritdoc} + * + * @see LocaleDisplayName::getExportedNodeName() + */ + protected function getExportedNodeName() + { + return 'scripts'; + } + + /** + * {@inheritdoc} + * + * @see Processor::getOutputRelativeFileName() + */ + protected function getOutputRelativeFileName() + { + return 'main/en-US/scripts.json'; + } +} + +class Territories extends LocaleDisplayName +{ + /** + * {@inheritdoc} + * + * @see LocaleDisplayName::getXPathSelector() + */ + protected function getXPathSelector() + { + return '/ldml/localeDisplayNames/territories/territory'; + } + + /** + * {@inheritdoc} + * + * @see LocaleDisplayName::getExportedNodeName() + */ + protected function getExportedNodeName() + { + return 'territories'; + } + + /** + * {@inheritdoc} + * + * @see Processor::getOutputRelativeFileName() + */ + protected function getOutputRelativeFileName() + { + return 'main/en-US/territories.json'; + } +} + +try { + main($argv); +} catch (RuntimeException $e) { + fwrite(STDERR, $e->getMessage() . "\n"); + exit(1); +} diff --git a/bin/import-cldr-data.bat b/bin/import-cldr-data.bat new file mode 100644 index 0000000..b99ef8d --- /dev/null +++ b/bin/import-cldr-data.bat @@ -0,0 +1 @@ +@php "%~dpn0" %* \ No newline at end of file diff --git a/composer.json b/composer.json index 2f914a4..6969ccc 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "test": "phpunit" }, "bin": [ - "bin/export-plural-rules" + "bin/export-plural-rules", + "bin/import-cldr-data" ] } \ No newline at end of file diff --git a/src/CldrData.php b/src/CldrData.php index 59503a9..cdaebc2 100644 --- a/src/CldrData.php +++ b/src/CldrData.php @@ -306,6 +306,8 @@ private static function getData($key) ); $knownMissingLanguages = array( 'guw' => 'Gun', + 'hnj' => 'Hmong Njua', + 'lld' => 'Dolomitic Ladin', 'nah' => 'Nahuatl', 'smi' => 'Sami', ); diff --git a/tests/test/ExecutableFilesTest.php b/tests/test/ExecutableFilesTest.php index edf9200..09d51a2 100644 --- a/tests/test/ExecutableFilesTest.php +++ b/tests/test/ExecutableFilesTest.php @@ -11,6 +11,7 @@ public function testExecutableFiles() } $expected = array( 'bin/export-plural-rules', + 'bin/import-cldr-data', ); $actual = $this->listExecutableFiles(); $this->assertSame($expected, $actual);