Fixes for locale matching
This commit is contained in:
parent
f42192fb5a
commit
0032d49e23
8 changed files with 179 additions and 5890 deletions
File diff suppressed because it is too large
Load diff
|
@ -85,6 +85,15 @@ function finalize(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function _getAvailableLocales(): Array<string> {
|
||||||
|
return JSON.parse(
|
||||||
|
readFileSync(
|
||||||
|
join(__dirname, '..', 'build', 'available-locales.json'),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
|
) as Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
export function load({
|
export function load({
|
||||||
preferredSystemLocales,
|
preferredSystemLocales,
|
||||||
logger,
|
logger,
|
||||||
|
@ -100,12 +109,7 @@ export function load({
|
||||||
logger.warn('locale: `preferredSystemLocales` was empty');
|
logger.warn('locale: `preferredSystemLocales` was empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableLocales = JSON.parse(
|
const availableLocales = _getAvailableLocales();
|
||||||
readFileSync(
|
|
||||||
join(__dirname, '..', 'build', 'available-locales.json'),
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
) as Array<string>;
|
|
||||||
|
|
||||||
logger.info('locale: Supported locales:', availableLocales.join(', '));
|
logger.info('locale: Supported locales:', availableLocales.join(', '));
|
||||||
logger.info('locale: Preferred locales: ', preferredSystemLocales.join(', '));
|
logger.info('locale: Preferred locales: ', preferredSystemLocales.join(', '));
|
||||||
|
|
|
@ -10,6 +10,14 @@ import { maybeParseUrl } from '../ts/util/url';
|
||||||
|
|
||||||
import type { MenuListType } from '../ts/types/menu';
|
import type { MenuListType } from '../ts/types/menu';
|
||||||
import type { LocalizerType } from '../ts/types/Util';
|
import type { LocalizerType } from '../ts/types/Util';
|
||||||
|
import { strictAssert } from '../ts/util/assert';
|
||||||
|
|
||||||
|
export const FAKE_DEFAULT_LOCALE = 'en-x-ignore'; // -x- is an extension space for attaching other metadata to the locale
|
||||||
|
|
||||||
|
strictAssert(
|
||||||
|
new Intl.Locale(FAKE_DEFAULT_LOCALE).toString() === FAKE_DEFAULT_LOCALE,
|
||||||
|
"Ensure Intl doesn't change our fake locale ever"
|
||||||
|
);
|
||||||
|
|
||||||
export function getLanguages(
|
export function getLanguages(
|
||||||
preferredSystemLocales: ReadonlyArray<string>,
|
preferredSystemLocales: ReadonlyArray<string>,
|
||||||
|
@ -19,17 +27,20 @@ export function getLanguages(
|
||||||
const matchedLocales = [];
|
const matchedLocales = [];
|
||||||
|
|
||||||
preferredSystemLocales.forEach(preferredSystemLocale => {
|
preferredSystemLocales.forEach(preferredSystemLocale => {
|
||||||
if (preferredSystemLocale === defaultLocale) {
|
|
||||||
matchedLocales.push(defaultLocale);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const matchedLocale = LocaleMatcher.match(
|
const matchedLocale = LocaleMatcher.match(
|
||||||
[preferredSystemLocale],
|
[preferredSystemLocale],
|
||||||
availableLocales as Array<string>, // bad types
|
availableLocales as Array<string>, // bad types
|
||||||
defaultLocale,
|
// We don't want to fallback to the default locale right away in case we might
|
||||||
|
// match some other locales first.
|
||||||
|
//
|
||||||
|
// However, we do want to match the default locale in case the user's locales
|
||||||
|
// actually matches it.
|
||||||
|
//
|
||||||
|
// This fake locale allows us to reliably filter it out within the loop.
|
||||||
|
FAKE_DEFAULT_LOCALE,
|
||||||
{ algorithm: 'best fit' }
|
{ algorithm: 'best fit' }
|
||||||
);
|
);
|
||||||
if (matchedLocale !== defaultLocale) {
|
if (matchedLocale !== FAKE_DEFAULT_LOCALE) {
|
||||||
matchedLocales.push(matchedLocale);
|
matchedLocales.push(matchedLocale);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,8 +51,7 @@
|
||||||
"sk-SK",
|
"sk-SK",
|
||||||
"sl-SI",
|
"sl-SI",
|
||||||
"sq-AL",
|
"sq-AL",
|
||||||
"sr-RS",
|
"sr",
|
||||||
"sr-YR",
|
|
||||||
"sv",
|
"sv",
|
||||||
"sw",
|
"sw",
|
||||||
"ta-IN",
|
"ta-IN",
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import fsExtra from 'fs-extra';
|
import fsExtra from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fastGlob from 'fast-glob';
|
||||||
|
|
||||||
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
|
const { SMARTLING_USER, SMARTLING_SECRET } = process.env;
|
||||||
|
|
||||||
|
@ -16,6 +17,16 @@ if (!SMARTLING_SECRET) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Cleaning _locales directory...');
|
||||||
|
const dirEntries = fastGlob.sync(['_locales/*', '!_locales/en'], {
|
||||||
|
onlyDirectories: true,
|
||||||
|
absolute: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const dirEntry of dirEntries) {
|
||||||
|
fsExtra.rmdirSync(dirEntry, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Fetching latest strings!');
|
console.log('Fetching latest strings!');
|
||||||
console.log();
|
console.log();
|
||||||
execSync(
|
execSync(
|
||||||
|
@ -51,6 +62,9 @@ rename('zh-YU', 'yue');
|
||||||
// we need to rename it to "zh-Hant" explicitly to make it work.
|
// we need to rename it to "zh-Hant" explicitly to make it work.
|
||||||
rename('zh-TW', 'zh-Hant');
|
rename('zh-TW', 'zh-Hant');
|
||||||
|
|
||||||
|
// "YR" is not a valid region subtag. Smartling made it up.
|
||||||
|
rename('sr-YR', 'sr');
|
||||||
|
|
||||||
console.log('Formatting newly-downloaded strings!');
|
console.log('Formatting newly-downloaded strings!');
|
||||||
console.log();
|
console.log();
|
||||||
execSync('yarn format', {
|
execSync('yarn format', {
|
||||||
|
|
|
@ -3,26 +3,27 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { stub } from 'sinon';
|
import { stub } from 'sinon';
|
||||||
import { load } from '../../../app/locale';
|
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||||
|
import { load, _getAvailableLocales } from '../../../app/locale';
|
||||||
|
import { FAKE_DEFAULT_LOCALE } from '../../../app/spell_check';
|
||||||
|
|
||||||
describe('locale', async () => {
|
describe('locale', async () => {
|
||||||
describe('load', () => {
|
describe('load', () => {
|
||||||
it('resolves expected locales correctly', async () => {
|
it('resolves expected locales correctly', async () => {
|
||||||
|
const logger = {
|
||||||
|
fatal: stub().throwsArg(0),
|
||||||
|
error: stub().throwsArg(0),
|
||||||
|
warn: stub().throwsArg(0),
|
||||||
|
info: stub(),
|
||||||
|
debug: stub(),
|
||||||
|
trace: stub(),
|
||||||
|
};
|
||||||
|
|
||||||
async function testCase(
|
async function testCase(
|
||||||
preferredSystemLocales: Array<string>,
|
preferredSystemLocales: Array<string>,
|
||||||
expectedLocale: string
|
expectedLocale: string
|
||||||
) {
|
) {
|
||||||
const actualLocale = await load({
|
const actualLocale = await load({ preferredSystemLocales, logger });
|
||||||
preferredSystemLocales,
|
|
||||||
logger: {
|
|
||||||
fatal: stub().throwsArg(0),
|
|
||||||
error: stub().throwsArg(0),
|
|
||||||
warn: stub().throwsArg(0),
|
|
||||||
info: stub(),
|
|
||||||
debug: stub(),
|
|
||||||
trace: stub(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
assert.strictEqual(actualLocale.name, expectedLocale);
|
assert.strictEqual(actualLocale.name, expectedLocale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +45,123 @@ describe('locale', async () => {
|
||||||
await testCase(['ug'], 'ug');
|
await testCase(['ug'], 'ug');
|
||||||
await testCase(['nn', 'nb'], 'nb');
|
await testCase(['nn', 'nb'], 'nb');
|
||||||
await testCase(['es-419'], 'es');
|
await testCase(['es-419'], 'es');
|
||||||
|
await testCase(['sr-RO', 'sr'], 'sr');
|
||||||
|
await testCase(['sr-RS', 'sr'], 'sr');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Intl.LocaleMatcher', () => {
|
||||||
|
it('should work for single locales outside of their region', () => {
|
||||||
|
// Our supported locales where we only have a single region for a language
|
||||||
|
const SINGLE_REGION_LOCALES = [
|
||||||
|
'af-ZA',
|
||||||
|
'az-AZ',
|
||||||
|
'bg-BG',
|
||||||
|
'bn-BD',
|
||||||
|
'bs-BA',
|
||||||
|
'et-EE',
|
||||||
|
'fa-IR',
|
||||||
|
'ga-IE',
|
||||||
|
'gl-ES',
|
||||||
|
'gu-IN',
|
||||||
|
'hi-IN',
|
||||||
|
'hr-HR',
|
||||||
|
'ka-GE',
|
||||||
|
'kk-KZ',
|
||||||
|
'km-KH',
|
||||||
|
'kn-IN',
|
||||||
|
'ky-KG',
|
||||||
|
'lt-LT',
|
||||||
|
'lv-LV',
|
||||||
|
'mk-MK',
|
||||||
|
'ml-IN',
|
||||||
|
'mr-IN',
|
||||||
|
'pa-IN',
|
||||||
|
'ro-RO',
|
||||||
|
'sk-SK',
|
||||||
|
'sl-SI',
|
||||||
|
'sq-AL',
|
||||||
|
'ta-IN',
|
||||||
|
'te-IN',
|
||||||
|
'tl-PH',
|
||||||
|
'uk-UA',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Just a whole bunch of common regions
|
||||||
|
const TEST_REGIONS = [
|
||||||
|
'AE',
|
||||||
|
'AR',
|
||||||
|
'AT',
|
||||||
|
'AU',
|
||||||
|
'BE',
|
||||||
|
'CA',
|
||||||
|
'CH',
|
||||||
|
'CL',
|
||||||
|
'CN',
|
||||||
|
'CO',
|
||||||
|
'CR',
|
||||||
|
'CZ',
|
||||||
|
'DE',
|
||||||
|
'DK',
|
||||||
|
'DO',
|
||||||
|
'EC',
|
||||||
|
'EG',
|
||||||
|
'ES',
|
||||||
|
'FI',
|
||||||
|
'FR',
|
||||||
|
'GB',
|
||||||
|
'GR',
|
||||||
|
'GT',
|
||||||
|
'HK',
|
||||||
|
'IE',
|
||||||
|
'IL',
|
||||||
|
'IN',
|
||||||
|
'IT',
|
||||||
|
'JP',
|
||||||
|
'KR',
|
||||||
|
'MY',
|
||||||
|
'NL',
|
||||||
|
'NO',
|
||||||
|
'PA',
|
||||||
|
'PE',
|
||||||
|
'PH',
|
||||||
|
'PL',
|
||||||
|
'PT',
|
||||||
|
'RO',
|
||||||
|
'RS',
|
||||||
|
'RU',
|
||||||
|
'SA',
|
||||||
|
'SE',
|
||||||
|
'SG',
|
||||||
|
'SK',
|
||||||
|
'TH',
|
||||||
|
'TR',
|
||||||
|
'UA',
|
||||||
|
'US',
|
||||||
|
'VE',
|
||||||
|
];
|
||||||
|
|
||||||
|
const availableLocales = _getAvailableLocales();
|
||||||
|
|
||||||
|
for (const locale of SINGLE_REGION_LOCALES) {
|
||||||
|
const { language } = new Intl.Locale(locale);
|
||||||
|
for (const region of TEST_REGIONS) {
|
||||||
|
const newLocale = new Intl.Locale(language, { region });
|
||||||
|
|
||||||
|
const matched = LocaleMatcher.match(
|
||||||
|
[newLocale.baseName],
|
||||||
|
availableLocales,
|
||||||
|
FAKE_DEFAULT_LOCALE,
|
||||||
|
{ algorithm: 'best fit' }
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
matched,
|
||||||
|
locale,
|
||||||
|
`${locale} -> ${language} -> ${region} -> ${newLocale.baseName} -> ${matched}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,5 +52,12 @@ describe('SpellCheck', () => {
|
||||||
'en',
|
'en',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('matches en along with other languages', () => {
|
||||||
|
assert.deepEqual(getLanguages(['en', 'fr'], ['fr', 'en'], 'en'), [
|
||||||
|
'en',
|
||||||
|
'fr',
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue