diff --git a/goldens/public-api/angular_devkit/build_angular/index.md b/goldens/public-api/angular_devkit/build_angular/index.md index 4a43ab838420..1450faeb3199 100644 --- a/goldens/public-api/angular_devkit/build_angular/index.md +++ b/goldens/public-api/angular_devkit/build_angular/index.md @@ -76,6 +76,11 @@ export type BrowserBuilderOutput = BuilderOutput & { baseOutputPath: string; outputPaths: string[]; outputPath: string; + outputs: { + locale?: string; + path: string; + baseHref: string; + }[]; }; // @public (undocumented) @@ -272,6 +277,10 @@ export type ServerBuilderOutput = BuilderOutput & { baseOutputPath: string; outputPaths: string[]; outputPath: string; + outputs: { + locale?: string; + path: string; + }[]; }; // @public (undocumented) diff --git a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts index 131e71281eee..ec88c92cbcc4 100644 --- a/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/app-shell/index.ts @@ -63,7 +63,7 @@ async function _renderUniversal( }) : undefined; - for (const outputPath of browserResult.outputPaths) { + for (const { path: outputPath, baseHref } of browserResult.outputs) { const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath); const browserIndexOutputPath = path.join(outputPath, 'index.html'); const indexHtml = await fs.promises.readFile(browserIndexOutputPath, 'utf8'); @@ -118,7 +118,7 @@ async function _renderUniversal( projectRoot, root, outputPath, - browserOptions.baseHref || '/', + baseHref, browserOptions.ngswConfigPath, ); } diff --git a/packages/angular_devkit/build_angular/src/builders/browser/index.ts b/packages/angular_devkit/build_angular/src/builders/browser/index.ts index ba8b1ac4a507..0651ae889f8b 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/browser/index.ts @@ -67,11 +67,20 @@ import { Schema as BrowserBuilderSchema } from './schema'; */ export type BrowserBuilderOutput = BuilderOutput & { baseOutputPath: string; + /** + * @deprecated in version 14. Use 'outputs' instead. + */ outputPaths: string[]; /** - * @deprecated in version 9. Use 'outputPaths' instead. + * @deprecated in version 9. Use 'outputs' instead. */ outputPath: string; + + outputs: { + locale?: string; + path: string; + baseHref: string; + }[]; }; /** @@ -174,6 +183,8 @@ export function buildWebpackBrowser( ({ config, projectRoot, projectSourceRoot, i18n, target, cacheOptions }) => { const normalizedOptimization = normalizeOptimization(options.optimization); + const defaultBaseHref = options.baseHref ?? '/'; + return runWebpack(config, context, { webpackFactory: require('webpack') as typeof webpack, logging: @@ -308,7 +319,7 @@ export function buildWebpackBrowser( for (const [locale, outputPath] of outputPaths.entries()) { try { const { content, warnings, errors } = await indexHtmlGenerator.process({ - baseHref: getLocaleBaseHref(i18n, locale) || options.baseHref, + baseHref: getLocaleBaseHref(i18n, locale) || defaultBaseHref, // i18nLocale is used when Ivy is disabled lang: locale || undefined, outputPath, @@ -352,7 +363,7 @@ export function buildWebpackBrowser( projectRoot, context.workspaceRoot, outputPath, - getLocaleBaseHref(i18n, locale) || options.baseHref || '/', + getLocaleBaseHref(i18n, locale) ?? defaultBaseHref, options.ngswConfigPath, ); } catch (error) { @@ -378,6 +389,15 @@ export function buildWebpackBrowser( baseOutputPath, outputPath: baseOutputPath, outputPaths: (outputPaths && Array.from(outputPaths.values())) || [baseOutputPath], + outputs: (outputPaths && + [...outputPaths.entries()].map(([locale, path]) => ({ + locale, + path, + baseHref: getLocaleBaseHref(i18n, locale) ?? defaultBaseHref, + }))) || { + path: baseOutputPath, + baseHref: defaultBaseHref, + }, } as BrowserBuilderOutput), ), ); diff --git a/packages/angular_devkit/build_angular/src/builders/server/index.ts b/packages/angular_devkit/build_angular/src/builders/server/index.ts index 8bb95e58d425..10a920f64fbf 100644 --- a/packages/angular_devkit/build_angular/src/builders/server/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/server/index.ts @@ -31,11 +31,19 @@ import { Schema as ServerBuilderOptions } from './schema'; */ export type ServerBuilderOutput = BuilderOutput & { baseOutputPath: string; + /** + * @deprecated in version 14. Use 'outputs' instead. + */ outputPaths: string[]; /** - * @deprecated in version 9. Use 'outputPaths' instead. + * @deprecated in version 9. Use 'outputs' instead. */ outputPath: string; + + outputs: { + locale?: string; + path: string; + }[]; }; export { ServerBuilderOptions }; @@ -130,6 +138,13 @@ export function execute( baseOutputPath, outputPath: baseOutputPath, outputPaths: outputPaths || [baseOutputPath], + outputs: (outputPaths && + [...outputPaths.entries()].map(([locale, path]) => ({ + locale, + path, + }))) || { + path: baseOutputPath, + }, } as ServerBuilderOutput; }), ); diff --git a/packages/angular_devkit/build_angular/src/testing/test-utils.ts b/packages/angular_devkit/build_angular/src/testing/test-utils.ts index 14de4ebeba32..3387b8cee8f9 100644 --- a/packages/angular_devkit/build_angular/src/testing/test-utils.ts +++ b/packages/angular_devkit/build_angular/src/testing/test-utils.ts @@ -83,8 +83,10 @@ export async function browserBuild( }; } - expect(output.outputPaths[0]).not.toBeUndefined(); - const outputPath = normalize(output.outputPaths[0]); + const [{ path, baseHref }] = output.outputs; + expect(baseHref).toBeTruthy(); + expect(path).toBeTruthy(); + const outputPath = normalize(path); const fileNames = await host.list(outputPath).toPromise(); const files = fileNames.reduce((acc: { [name: string]: Promise }, path) => { diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts new file mode 100644 index 000000000000..3493148b6678 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell-service-worker.ts @@ -0,0 +1,90 @@ +import { getGlobalVariable } from '../../utils/env'; +import { appendToFile, createDir, expectFileToMatch, writeFile } from '../../utils/fs'; +import { installWorkspacePackages } from '../../utils/packages'; +import { silentNg } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; +import { readNgVersion } from '../../utils/version'; + +const snapshots = require('../../ng-snapshot/package.json'); + +export default async function () { + const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; + + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/localize'] = isSnapshotBuild + ? snapshots.dependencies['@angular/localize'] + : readNgVersion(); + }); + + await appendToFile('src/app/app.component.html', ''); + + // Add app-shell and service-worker + await silentNg('generate', 'app-shell'); + await silentNg('generate', 'service-worker'); + + if (isSnapshotBuild) { + await updateJsonFile('package.json', (packageJson) => { + const dependencies = packageJson['dependencies']; + dependencies['@angular/platform-server'] = snapshots.dependencies['@angular/platform-server']; + dependencies['@angular/service-worker'] = snapshots.dependencies['@angular/service-worker']; + dependencies['@angular/router'] = snapshots.dependencies['@angular/router']; + }); + } + + await installWorkspacePackages(); + + const browserBaseDir = 'dist/test-project/browser'; + + // Set configurations for each locale. + const langTranslations = [ + { lang: 'en-US', translation: 'Hello i18n!' }, + { lang: 'fr', translation: 'Bonjour i18n!' }, + ]; + + await updateJsonFile('angular.json', (workspaceJson) => { + const appProject = workspaceJson.projects['test-project']; + const appArchitect = appProject.architect; + const buildOptions = appArchitect['build'].options; + const serverOptions = appArchitect['server'].options; + + // Enable localization for all locales + buildOptions.localize = true; + buildOptions.outputHashing = 'none'; + serverOptions.localize = true; + serverOptions.outputHashing = 'none'; + + // Add locale definitions to the project + const i18n: Record = (appProject.i18n = { locales: {} }); + for (const { lang } of langTranslations) { + if (lang == 'en-US') { + i18n.sourceLocale = lang; + } else { + i18n.locales[lang] = `src/locale/messages.${lang}.xlf`; + } + } + }); + + await createDir('src/locale'); + + for (const { lang } of langTranslations) { + // dummy translation file. + await writeFile( + `src/locale/messages.${lang}.xlf`, + ` + + + + `, + ); + } + + // Build each locale and verify the SW output. + await silentNg('run', 'test-project:app-shell:development'); + for (const { lang } of langTranslations) { + await Promise.all([ + expectFileToMatch(`${browserBaseDir}/${lang}/ngsw.json`, `/${lang}/main.js`), + expectFileToMatch(`${browserBaseDir}/${lang}/ngsw.json`, `/${lang}/index.html`), + ]); + } +}