/** * @license * Copyright Google LLC All Rights Reserved. * * 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 */ import {runfiles} from '@bazel/runfiles'; import {CldrStatic} from 'cldrjs'; import {sync as globSync} from 'glob'; // TypeScript doesn't allow us to import the default export without the `esModuleInterop`. We use // the NodeJS require function instead as specifying a custom tsconfig complicates the setup // unnecessarily. // TODO: See if we can improve this by having better types for `cldrjs`. const cldrjs: typeof import('cldrjs') = require('cldrjs'); /** * Globs that match CLDR JSON data files that should be fetched. We limit these intentionally * as loading unused data results in significant slow-down of the generation * (noticeable in local development if locale data is re-generated). */ const CLDR_DATA_GLOBS = [ 'cldr-core-37.0.0/scriptMetadata.json', 'cldr-core-37.0.0/supplemental/**/*.json', 'cldr-dates-full-37.0.0/main/**/*.json', 'cldr-numbers-full-37.0.0/main/**/*.json', ]; /** Path to the CLDR available locales file. */ 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 type with the missing `bundle` attribute property. */ export type CldrLocaleData = CldrStatic&{ attributes: { /** * Resolved bundle name for the locale. * More details: http://www.unicode.org/reports/tr35/#Bundle_vs_Item_Lookup */ bundle: string; } }; /** * 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 * the `@cldr_data` Bazel repository. */ export class CldrData { /** Path to the CLDR data Bazel repository. i.e. `@cldr_data//`. */ readonly cldrDataDir = runfiles.resolve('cldr_data'); /** List of all available locales CLDR provides data for. */ readonly availableLocales: readonly CldrLocaleData[]; constructor() { this._loadAndPopulateCldrData(); this.availableLocales = this._getAvailableLocales(); } /** Gets the CLDR data for the specified locale. */ getLocaleData(localeName: string): CldrLocaleData|null { // Cast to `CldrLocaleData` because the default `cldrjs` types from `DefinitelyTyped` // are outdated and do not capture the `bundle` attribute. See: // https://github.com/rxaviers/cldrjs#instantiate-a-locale-and-get-it-normalized. const localeData = new cldrjs(localeName) as CldrLocaleData; // In case a locale has been requested for which no data is available, we return // `null` immediately instead of returning an empty `CldrStatic` instance. if (localeData.attributes.bundle === null) { return null; } 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. */ private _getAvailableLocales(): CldrLocaleData[] { const allLocales = require(`${this.cldrDataDir}/${CLDR_AVAILABLE_LOCALES_PATH}`).availableLocales.full; const localesWithData: CldrLocaleData[] = []; for (const localeName of allLocales) { const localeData = this.getLocaleData(localeName); // If `cldrjs` is unable to resolve a `bundle` for the current locale, then there is no data // for this locale, and it should not be generated. This can happen as with older versions of // CLDR where `availableLocales.json` specifies locales for which no data is available // (even within the `full` tier packages). See: // http://cldr.unicode.org/development/development-process/design-proposals/json-packaging. // TODO(devversion): Remove if we update to CLDR v39 where this seems fixed. Note that this // worked before in the Gulp tooling without such a check because the `cldr-data-downloader` // overwrote the `availableLocales` to only capture locales with data. if (localeData !== null) { localesWithData.push(localeData); } } return localesWithData; } /** Loads the CLDR data and populates the `cldrjs` library with it. */ private _loadAndPopulateCldrData() { const localeData = this._readCldrDataFromRepository(); if (localeData.length === 0) { throw Error('No CLDR data could be found.'); } // Populate the `cldrjs` library with the locale data. Note that we need this type cast // to satisfy the first `cldrjs.load` parameter which cannot be undefined. cldrjs.load(...localeData as [object, ...object[]]); } /** * Reads the CLDR JSON data from the Bazel repository. * @returns a list of read JSON objects representing the CLDR data. */ private _readCldrDataFromRepository(): object[] { const jsonFiles = CLDR_DATA_GLOBS.map(pattern => globSync(pattern, {cwd: this.cldrDataDir, absolute: true})) .reduce((acc, dataFiles) => [...acc, ...dataFiles], []); // Read the JSON for all determined CLDR json files. return jsonFiles.map(filePath => { const parsed = require(filePath); // Guards against cases where non-CLDR data files are accidentally picked up // by the glob above and would throw-off the bundle lookup in `cldrjs`. if (parsed.main !== undefined && typeof parsed.main !== 'object') { throw Error('Unexpected CLDR json file with "main" field which is not an object.'); } return parsed; }); } }