Use LocaleMatcher to resolve system preferred locales
This commit is contained in:
parent
68ae25f5cd
commit
cdc68d1c34
9 changed files with 224 additions and 48 deletions
62
ts/scripts/gen-locales-config.ts
Normal file
62
ts/scripts/gen-locales-config.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import fastGlob from 'fast-glob';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, '..', '..');
|
||||
|
||||
function matches(input: string, expected: string) {
|
||||
const match = LocaleMatcher.match([input], [expected], 'en', {
|
||||
algorithm: 'best fit',
|
||||
});
|
||||
return match === expected;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const dirEntries = await fastGlob('_locales/*', {
|
||||
cwd: ROOT_DIR,
|
||||
onlyDirectories: true,
|
||||
});
|
||||
|
||||
const localeDirNames = [];
|
||||
|
||||
for (const dirEntry of dirEntries) {
|
||||
const dirName = path.basename(dirEntry);
|
||||
const locale = new Intl.Locale(dirName);
|
||||
|
||||
// Smartling doesn't always use the correct language tag, so this check and
|
||||
// reverse check are to make sure we don't accidentally add a locale that
|
||||
// doesn't match its directory name (using LocaleMatcher).
|
||||
//
|
||||
// If this check ever fails, we may need to update our get-strings script to
|
||||
// manually rename language tags before writing them to disk.
|
||||
//
|
||||
// Such is the case for Smartling's "zh-YU" locale, which we renamed to
|
||||
// "yue" to match the language tag used by... everyone else.
|
||||
|
||||
if (!matches(dirName, locale.baseName)) {
|
||||
throw new Error(
|
||||
`Matched locale "${dirName}" does not match its resolved name "${locale.baseName}"`
|
||||
);
|
||||
}
|
||||
if (!matches(locale.baseName, dirName)) {
|
||||
throw new Error(
|
||||
`Matched locale "${dirName}" does not match its dir name "${dirName}"`
|
||||
);
|
||||
}
|
||||
|
||||
localeDirNames.push(dirName);
|
||||
}
|
||||
|
||||
const jsonPath = path.join(ROOT_DIR, 'build', 'available-locales.json');
|
||||
console.log(`Writing to "${jsonPath}"...`);
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(localeDirNames, null, 2)}\n`);
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
|
@ -2,6 +2,8 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import fsExtra from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
|
||||
|
||||
|
@ -29,6 +31,19 @@ execSync(
|
|||
}
|
||||
);
|
||||
|
||||
function rename(from: string, to: string) {
|
||||
console.log(`Renaming "${from}" to "${to}"`);
|
||||
fsExtra.moveSync(path.join('_locales', from), path.join('_locales', to), {
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Smartling uses "zh-YU" for Cantonese (or Yue Chinese).
|
||||
// This is wrong.
|
||||
// The language tag for Yue Chinese is "yue"
|
||||
// "zh-YU" actually implies "Chinese as spoken in Yugoslavia (canonicalized to Serbia)"
|
||||
rename('zh-YU', 'yue');
|
||||
|
||||
console.log('Formatting newly-downloaded strings!');
|
||||
console.log();
|
||||
execSync('yarn format', {
|
||||
|
|
46
ts/test-node/app/locale_test.ts
Normal file
46
ts/test-node/app/locale_test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { load } from '../../../app/locale';
|
||||
import type { LoggerType } from '../../types/Logging';
|
||||
|
||||
const logger: Pick<LoggerType, 'info' | 'warn'> = {
|
||||
info(..._args: Array<unknown>) {
|
||||
// noop
|
||||
},
|
||||
warn(..._args: Array<unknown>) {
|
||||
throw new Error(String(_args));
|
||||
},
|
||||
};
|
||||
|
||||
describe('locale', async () => {
|
||||
describe('load', () => {
|
||||
it('resolves expected locales correctly', async () => {
|
||||
async function testCase(
|
||||
preferredSystemLocales: Array<string>,
|
||||
expectedLocale: string
|
||||
) {
|
||||
const actualLocale = await load({ preferredSystemLocales, logger });
|
||||
assert.strictEqual(actualLocale.name, expectedLocale);
|
||||
}
|
||||
|
||||
// Basic tests
|
||||
await testCase(['en'], 'en');
|
||||
await testCase(['es'], 'es');
|
||||
await testCase(['fr', 'hk'], 'fr');
|
||||
await testCase(['fr-FR', 'hk'], 'fr');
|
||||
await testCase(['fa-UK'], 'fa-IR');
|
||||
await testCase(['an', 'fr-FR'], 'fr'); // If we ever add support for Aragonese, this test will fail.
|
||||
|
||||
// Specific cases we want to ensure work as expected
|
||||
await testCase(['zh-Hant-TW'], 'zh-TW');
|
||||
await testCase(['zh-Hant-HK'], 'zh-HK');
|
||||
await testCase(['zh'], 'zh-CN');
|
||||
await testCase(['yue'], 'yue');
|
||||
await testCase(['ug'], 'ug');
|
||||
await testCase(['nn', 'nb'], 'nb');
|
||||
await testCase(['es-419'], 'es');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -202,9 +202,6 @@ describe('createTemplate', () => {
|
|||
info(_arg: unknown) {
|
||||
// noop
|
||||
},
|
||||
error(arg: unknown) {
|
||||
throw new Error(String(arg));
|
||||
},
|
||||
warn(arg: unknown) {
|
||||
throw new Error(String(arg));
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue