build: generate alias locale data for closure locale (#42230)

Within Google, closure compiler is used for dealing with translations.
We generate a closure-compatible locale file that allows for
registration within Angular, so that Closure i18n works well together
with Angular applications. Closure compiler does not limit its
locales to BCP47-canonical locale identifiers. This commit updates
the generation logic so that we also support deprecated (but aliased)
locale identifiers, or other aliases which are likely used within
Closure. We use CLDR's alias supplemental data for this. It instructs
us to alias `iw` to `he` for example. `iw` is still supported in Closure.

Note that we do not manually extract all locales supported in Closure;
instead we only support the CLDR canonical locales (as done before) +
common aliases that CLDR provides data for. We are not aware of other
locale aliases within Closure that wouldn't be part of the CLDR aliases.
If there would be, then Angular/Closure would fail accordingly.

PR Close #42230
This commit is contained in:
Paul Gschwendtner 2021-06-10 20:56:25 +02:00 committed by Alex Rickabaugh
parent 8f24d71142
commit 044e0229bd
6 changed files with 3871 additions and 23 deletions

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,9 @@ const CLDR_DATA_GLOBS = [
/** Path to the CLDR available locales file. */ /** Path to the CLDR available locales file. */
const CLDR_AVAILABLE_LOCALES_PATH = 'cldr-core-37.0.0/availableLocales.json'; const CLDR_AVAILABLE_LOCALES_PATH = 'cldr-core-37.0.0/availableLocales.json';
/** Path to the CLDR locale aliases file. */
const CLDR_LOCALE_ALIASES_PATH = 'cldr-core-37.0.0/supplemental/aliases.json';
/** /**
* Instance providing access to a locale's CLDR data. This type extends the `cldrjs` * Instance providing access to a locale's CLDR data. This type extends the `cldrjs`
* instance type with the missing `bundle` attribute property. * instance type with the missing `bundle` attribute property.
@ -45,6 +48,13 @@ export type CldrLocaleData = CldrStatic&{
} }
}; };
/**
* Possible reasons for an alias in the CLDR supplemental data. See:
* https://unicode.org/reports/tr35/tr35-info.html#Appendix_Supplemental_Metadata.
*/
export type CldrLocaleAliasReason =
'deprecated'|'overlong'|'macrolanguage'|'legacy'|'bibliographic';
/** /**
* Class that provides access to the CLDR data downloaded as part of * Class that provides access to the CLDR data downloaded as part of
* the `@cldr_data` Bazel repository. * the `@cldr_data` Bazel repository.
@ -77,6 +87,16 @@ export class CldrData {
return localeData; return localeData;
} }
/**
* Gets the CLDR language aliases.
* http://cldr.unicode.org/index/cldr-spec/language-tag-equivalences.
*/
getLanguageAliases():
{[localeName: string]: {_reason: CldrLocaleAliasReason, _replacement: string}} {
return require(`${this.cldrDataDir}/${CLDR_LOCALE_ALIASES_PATH}`)
.supplemental.metadata.alias.languageAlias;
}
/** Gets a list of all locales CLDR provides data for. */ /** Gets a list of all locales CLDR provides data for. */
private _getAvailableLocales(): CldrLocaleData[] { private _getAvailableLocales(): CldrLocaleData[] {
const allLocales = const allLocales =

View File

@ -6,35 +6,45 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CldrStatic} from 'cldrjs'; import {CldrData, CldrLocaleData} from './cldr-data';
import {CldrData} from './cldr-data';
import {fileHeader} from './file-header'; import {fileHeader} from './file-header';
import {BaseCurrencies} from './locale-base-currencies'; import {BaseCurrencies} from './locale-base-currencies';
import {generateLocale} from './locale-file'; import {generateLocale} from './locale-file';
interface ClosureLocale {
/** Locale name to match with a Closure-supported locale. */
closureLocaleName: string;
/** Locale data. Can have a different locale name if this captures an aliased locale. */
data: CldrLocaleData;
}
/** /**
* Generate a file that contains all locale to import for closure. * Generate a file that contains all locale to import for closure.
* Tree shaking will only keep the data for the `goog.LOCALE` locale. * Tree shaking will only keep the data for the `goog.LOCALE` locale.
*/ */
export function generateClosureLocaleFile(cldrData: CldrData, baseCurrencies: BaseCurrencies) { export function generateClosureLocaleFile(cldrData: CldrData, baseCurrencies: BaseCurrencies) {
const locales = cldrData.availableLocales; const locales: ClosureLocale[] =
[...cldrData.availableLocales.map(data => ({closureLocaleName: data.locale, data}))];
const aliases = cldrData.getLanguageAliases();
function generateLocaleConstant(localeData: CldrStatic): string { // We also generate locale data for aliases known within CLDR. Closure compiler does not
const locale = localeData.locale; // limit its locale identifiers to CLDR-canonical identifiers/or BCP47 identifiers.
const localeNameFormattedForJs = formatLocale(locale); // To ensure deprecated/historical locale identifiers which are supported by Closure
return generateLocale(locale, localeData, baseCurrencies) // can work with closure-compiled Angular applications, we respect CLDR locale aliases.
.replace(`${fileHeader}\n`, '') for (const [aliasName, data] of Object.entries(aliases)) {
.replace('export default ', `export const locale_${localeNameFormattedForJs} = `) // We skip bibliographic aliases as those have never been supported by Closure compiler.
.replace('function plural', `function plural_${localeNameFormattedForJs}`) if (data._reason === 'bibliographic') {
.replace(/,\s+plural/, `, plural_${localeNameFormattedForJs}`) continue;
.replace(/\s*const u = undefined;\s*/, ''); }
}
function generateCase(localeName: string) { const localeData = cldrData.getLocaleData(data._replacement);
return `case '${localeName}':\n` +
`l = locale_${formatLocale(localeName)};\n` + // If CLDR does not provide data for the replacement locale, we skip this alias.
`break;\n`; if (localeData === null) {
continue;
}
locales.push({closureLocaleName: aliasName, data: localeData});
} }
return `${fileHeader} return `${fileHeader}
@ -48,12 +58,28 @@ ${locales.map(locale => `${generateLocaleConstant(locale)}`).join('\n')}
let l: any; let l: any;
switch (goog.LOCALE) { switch (goog.LOCALE) {
${locales.map(localeData => generateCase(localeData.locale)).join('')}} ${locales.map(locale => generateCase(locale)).join('')}}
if (l) { if (l) {
registerLocaleData(l); registerLocaleData(l);
} }
`; `;
function generateLocaleConstant(locale: ClosureLocale): string {
const localeNameFormattedForJs = formatLocale(locale.closureLocaleName);
return generateLocale(locale.closureLocaleName, locale.data, baseCurrencies)
.replace(`${fileHeader}\n`, '')
.replace('export default ', `export const locale_${localeNameFormattedForJs} = `)
.replace('function plural', `function plural_${localeNameFormattedForJs}`)
.replace(/,\s+plural/, `, plural_${localeNameFormattedForJs}`)
.replace(/\s*const u = undefined;\s*/, '');
}
function generateCase(locale: ClosureLocale) {
return `case '${locale.closureLocaleName}':\n` +
`l = locale_${formatLocale(locale.closureLocaleName)};\n` +
`break;\n`;
}
} }
function formatLocale(locale: string): string { function formatLocale(locale: string): string {

View File

@ -23,7 +23,7 @@ export function generateLocale(
return `${fileHeader} return `${fileHeader}
const u = undefined; const u = undefined;
${getPluralFunction(locale)} ${getPluralFunction(localeData)}
export default ${generateBasicLocaleString(locale, localeData, baseCurrencies)}; export default ${generateBasicLocaleString(locale, localeData, baseCurrencies)};
`; `;

View File

@ -27,7 +27,7 @@ export function generateLocaleGlobalFile(
global.ng.common = global.ng.common || {}; global.ng.common = global.ng.common || {};
global.ng.common.locales = global.ng.common.locales || {}; global.ng.common.locales = global.ng.common.locales || {};
const u = undefined; const u = undefined;
${getPluralFunction(locale, false)} ${getPluralFunction(localeData, false)}
global.ng.common.locales['${normalizeLocale(locale)}'] = ${data}; global.ng.common.locales['${normalizeLocale(locale)}'] = ${data};
})(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global || typeof window !== 'undefined' && window); })(typeof globalThis !== 'undefined' && globalThis || typeof global !== 'undefined' && global || typeof window !== 'undefined' && window);
`; `;

View File

@ -5,6 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CldrLocaleData} from './cldr-data';
// There are no types available for `cldr`. // There are no types available for `cldr`.
const cldr = require('cldr'); const cldr = require('cldr');
@ -14,8 +15,13 @@ const cldr = require('cldr');
* TODO(ocombe): replace "cldr" extractPluralRuleFunction with our own extraction using "CldrJS" * TODO(ocombe): replace "cldr" extractPluralRuleFunction with our own extraction using "CldrJS"
* because the 2 libs can become out of sync if they use different versions of the cldr database * because the 2 libs can become out of sync if they use different versions of the cldr database
*/ */
export function getPluralFunction(locale: string, withTypes = true) { export function getPluralFunction(localeData: CldrLocaleData, withTypes = true) {
let fn = cldr.extractPluralRuleFunction(locale).toString(); // We use the resolved bundle for extracting the plural function. This matches with the
// lookup logic used by other extractions in the tool (using `cldrjs`), and also ensures
// we follow the CLDR-specified bundle lookup algorithm. A language does not necessarily
// resolve directly to a bundle CLDR provides data for.
const bundleName = localeData.attributes.bundle;
let fn = cldr.extractPluralRuleFunction(bundleName).toString();
const numberType = withTypes ? ': number' : ''; const numberType = withTypes ? ': number' : '';
fn = fn =