diff --git a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts b/packages/angular_devkit/build_angular/src/utils/i18n-options.ts index ae3b85bbfb06..3091cd22c7fa 100644 --- a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/i18n-options.ts @@ -19,7 +19,10 @@ import { createTranslationLoader } from './load-translations'; export interface I18nOptions { inlineLocales: Set; sourceLocale: string; - locales: Record; + locales: Record< + string, + { file: string; format?: string; translation?: unknown; dataPath?: string } + >; flatOutput?: boolean; readonly shouldInline: boolean; } @@ -127,9 +130,16 @@ export async function configureI18nBuild 0) { + const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || ''); + const localeDataBasePath = findLocaleDataBasePath(projectRoot); + if (!localeDataBasePath) { + throw new Error( + `Unable to find locale data within '@angular/common'. Please ensure '@angular/common' is installed.`, + ); + } + // LoadĀ locales const loader = await createTranslationLoader(); - const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || ''); const usedFormats = new Set(); for (const [locale, desc] of Object.entries(i18n.locales)) { if (i18n.inlineLocales.has(locale) && desc.file) { @@ -145,6 +155,15 @@ export async function configureI18nBuild 0) { buildOptions.i18nFormat = [...usedFormats][0]; } - - // If only one locale is specified set the deprecated option to enable the webpack plugin - // transform to register the locale directly in the output bundle. - if (i18n.inlineLocales.size === 1) { - buildOptions.i18nLocale = [...i18n.inlineLocales][0]; - } } // If inlining store the output in a temporary location to facilitate post-processing @@ -202,3 +215,30 @@ function mergeDeprecatedI18nOptions( return i18n; } + +function findLocaleDataBasePath(projectRoot: string): string | null { + try { + const commonPath = path.dirname( + require.resolve('@angular/common/package.json', { paths: [projectRoot] }), + ); + const localesPath = path.join(commonPath, 'locales/global'); + + if (!fs.existsSync(localesPath)) { + return null; + } + + return localesPath; + } catch { + return null; + } +} + +function findLocaleDataPath(locale: string, basePath: string): string | null { + const localeDataPath = path.join(basePath, locale + '.js'); + + if (!fs.existsSync(localeDataPath)) { + return null; + } + + return localeDataPath; +} diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index 9848343021a2..d8a04314c0ac 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -541,6 +541,14 @@ export async function inlineLocales(options: InlineOptions) { const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});`; contentClone = content.clone(); content.prepend(setLocaleText); + + // If locale data is provided, load it and prepend to file + const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath; + if (localeDataPath) { + const localDataContent = loadLocaleData(localeDataPath, true); + // The semicolon ensures that there is no syntax error between statements + content.prepend(localDataContent + ';'); + } } const output = content.toString(); @@ -669,3 +677,20 @@ function findLocalizePositions( return positions; } + +function loadLocaleData(path: string, optimize: boolean): string { + // The path is validated during option processing before the build starts + const content = fs.readFileSync(path, 'utf8'); + + // NOTE: This can be removed once the locale data files are preprocessed in the framework + if (optimize) { + const result = terserMangle(content, { + compress: true, + ecma: 5, + }); + + return result.code; + } + + return content; +} diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl.ts index 024f203bc3be..32689a493d6d 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl.ts @@ -28,9 +28,15 @@ export default async function () { await expectFileToMatch(`${outputPath}/main-es2015.js`, translation.helloPartial); await expectToFail(() => expectFileToMatch(`${outputPath}/main-es5.js`, '$localize`')); await expectToFail(() => expectFileToMatch(`${outputPath}/main-es2015.js`, '$localize`')); + + // Verify the locale ID is present await expectFileToMatch(`${outputPath}/main-es5.js`, lang); await expectFileToMatch(`${outputPath}/main-es2015.js`, lang); + // Verify the locale data is registered using the global files + await expectFileToMatch(`${outputPath}/main-es5.js`, '.ng.common.locales'); + await expectFileToMatch(`${outputPath}/main-es2015.js`, '.ng.common.locales'); + const server = externalServer(outputPath); try { // Execute without a devserver. @@ -40,6 +46,11 @@ export default async function () { } } + // Verify deprecated locale data registration is not present + await ng('build', '--configuration=fr', '--optimization=false'); + await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es5.js`, 'registerLocaleData(')); + await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es2015.js`, 'registerLocaleData(')); + // Verify missing translation behaviour. await appendToFile('src/app/app.component.html', '

Other content

'); await ng('build', '--i18n-missing-translation', 'ignore');