From c318ac1b15e067d11d8322ec5cfd76195e3d42ee Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 6 May 2025 08:47:41 +0000 Subject: [PATCH] fix(@angular-devkit/build-angular): correctly set i18n subPath in webpack browser builder The `subPath` option was not being properly applied when using the webpack-based browser builder. Closes #30247 --- .../build_angular/src/utils/process-bundle.ts | 10 ++- .../e2e/tests/i18n/ivy-localize-sub-path.ts | 73 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/legacy-cli/e2e/tests/i18n/ivy-localize-sub-path.ts 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 f1f76a966e9e..ebd6f5e511ca 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -192,10 +192,10 @@ export async function inlineLocales(options: InlineOptions) { if (!transformResult || !transformResult.code) { throw new Error(`Unknown error occurred processing bundle for "${options.filename}".`); } - + const subPath = i18n.locales[locale].subPath; const outputPath = path.join( options.outputPath, - i18n.flatOutput ? '' : locale, + i18n.flatOutput ? '' : subPath, options.filename, ); await fs.writeFile(outputPath, transformResult.code); @@ -284,9 +284,10 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) { source: string; map: { file: string; sourceRoot?: string }; }; + const subPath = i18n.locales[locale].subPath; const outputPath = path.join( options.outputPath, - i18n.flatOutput ? '' : locale, + i18n.flatOutput ? '' : subPath, options.filename, ); await fs.writeFile(outputPath, outputCode); @@ -309,9 +310,10 @@ async function inlineCopyOnly(options: InlineOptions) { } for (const locale of i18n.inlineLocales) { + const subPath = i18n.locales[locale].subPath; const outputPath = path.join( options.outputPath, - i18n.flatOutput ? '' : locale, + i18n.flatOutput ? '' : subPath, options.filename, ); await fs.writeFile(outputPath, options.code); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-sub-path.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-sub-path.ts new file mode 100644 index 000000000000..d6640534c45f --- /dev/null +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-sub-path.ts @@ -0,0 +1,73 @@ +/** + * @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.dev/license + */ + +import { join } from 'node:path'; +import { expectFileToMatch } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { baseDir, baseHrefs, externalServer, langTranslations, setupI18nConfig } from './setup'; + +export default async function () { + // Setup i18n tests and config. + await setupI18nConfig(); + + const URL_SUB_PATH: Record = { + 'en-US': '', + 'fr': 'fr', + 'de': 'deutsche', + }; + + // Update angular.json + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const i18n: Record = appProject.i18n; + + i18n.sourceLocale = { + subPath: URL_SUB_PATH['en-US'], + }; + + i18n.locales['fr'] = { + translation: i18n.locales['fr'], + subPath: URL_SUB_PATH['fr'], + }; + + i18n.locales['de'] = { + translation: i18n.locales['de'], + subPath: URL_SUB_PATH['de'], + }; + }); + + // Build each locale and verify the output. + await ng('build'); + for (const { lang } of langTranslations) { + const subPath = URL_SUB_PATH[lang]; + const baseHref = subPath ? `/${subPath}/` : '/'; + const outputPath = join(baseDir, subPath); + + // Verify the HTML lang attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `lang="${lang}"`); + + // Verify the HTML base HREF attribute is present + await expectFileToMatch(`${outputPath}/index.html`, `href="${baseHref}"`); + + // Execute Application E2E tests for a production build without dev server + const { server, port, url } = await externalServer(outputPath, baseHref); + + try { + await ng( + 'e2e', + `--port=${port}`, + `--configuration=${lang}`, + '--dev-server-target=', + `--base-url=${url}`, + ); + } finally { + server.close(); + } + } +}