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…
	
	Add table
		Add a link
		
	
		Reference in a new issue