Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): ensure i18n locale data is includ…
Browse files Browse the repository at this point in the history
…ed in SSR application builds

When using the application builder to create server builds for SSR and/or prerendering with localization
enabled, the locale data for each enabled locale is now properly included in the server output files.
While browser builds contain this data in the polyfills output file, the server builds do not contain a
polyfill output file, as a result, the locale data is added to the main server code instead. This is also
the case for other polyfill-like code including zone.js.

(cherry picked from commit a136f2e)
  • Loading branch information
clydin authored and alan-agius4 committed Oct 16, 2023
1 parent 20a0c2a commit 52f5956
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 66 deletions.
Expand Up @@ -308,6 +308,23 @@ export function createServerCodeBundleOptions(

polyfills.push(`import '@angular/platform-server/init';`);

// Add Angular's global locale data if i18n options are present.
let needLocaleDataPlugin = false;
if (options.i18nOptions.shouldInline) {
// Add locale data for all active locales
for (const locale of options.i18nOptions.inlineLocales) {
polyfills.unshift(`import 'angular:locale/data:${locale}';`);
}
needLocaleDataPlugin = true;
} else if (options.i18nOptions.hasDefinedSourceLocale) {
// When not inlining and a source local is present, use the source locale data directly
polyfills.unshift(`import 'angular:locale/data:${options.i18nOptions.sourceLocale}';`);
needLocaleDataPlugin = true;
}
if (needLocaleDataPlugin) {
buildOptions.plugins.push(createAngularLocaleDataPlugin());
}

buildOptions.plugins.push(
createVirtualModulePlugin({
namespace: mainServerNamespace,
Expand Down
102 changes: 36 additions & 66 deletions tests/legacy-cli/e2e/tests/i18n/ivy-localize-ssr.ts
@@ -1,12 +1,10 @@
import { readFileSync, readdirSync } from 'node:fs';
import { getGlobalVariable } from '../../utils/env';
import { copyFile, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs';
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
import { ng } from '../../utils/process';
import { updateJsonFile, useSha } from '../../utils/project';
import { readNgVersion } from '../../utils/version';

const snapshots = require('../../ng-snapshot/package.json');
import { langTranslations, setupI18nConfig } from './setup';
import { expectFileToMatch } from '../../utils/fs';

export default async function () {
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
Expand All @@ -15,12 +13,28 @@ export default async function () {
return;
}

const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots'];
await updateJsonFile('package.json', (packageJson) => {
const dependencies = packageJson['dependencies'];
dependencies['@angular/localize'] = isSnapshotBuild
? snapshots.dependencies['@angular/localize']
: readNgVersion();
// Setup i18n tests and config.
await setupI18nConfig();

// Update angular.json
await updateJsonFile('angular.json', (workspaceJson) => {
const appProject = workspaceJson.projects['test-project'];
// tslint:disable-next-line: no-any
const i18n: Record<string, any> = appProject.i18n;

i18n.sourceLocale = {
baseHref: '',
};

i18n.locales['fr'] = {
translation: i18n.locales['fr'],
baseHref: '',
};

i18n.locales['de'] = {
translation: i18n.locales['de'],
baseHref: '',
};
});

// forcibly remove in case another test doesn't clean itself up
Expand All @@ -31,65 +45,12 @@ export default async function () {
await useSha();
await installWorkspacePackages();

// 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 || appProject.targets;
const buildOptions = appArchitect['build'].options;

// Enable localization for all locales
buildOptions.localize = true;

// Add locale definitions to the project
const i18n: Record<string, any> = (appProject.i18n = { locales: {} });
for (const { lang } of langTranslations) {
if (lang == 'en-US') {
i18n.sourceLocale = lang;
} else {
i18n.locales[lang] = `src/locale/messages.${lang}.xlf`;
}
}
});

// Add a translatable element
// Extraction of i18n only works on browser targets.
// Let's add the same translation that there is in the app-shell
await writeFile(
'src/app/app.component.html',
'<h1 i18n="An introduction header for this sample">Hello i18n!</h1>',
);

// Extract the translation messages and copy them for each language.
await ng('extract-i18n', '--output-path=src/locale');
await expectFileToMatch('src/locale/messages.xlf', `source-language="en-US"`);
await expectFileToMatch('src/locale/messages.xlf', `An introduction header for this sample`);

for (const { lang, translation } of langTranslations) {
if (lang != 'en-US') {
await copyFile('src/locale/messages.xlf', `src/locale/messages.${lang}.xlf`);
await replaceInFile(
`src/locale/messages.${lang}.xlf`,
'source-language="en-US"',
`source-language="en-US" target-language="${lang}"`,
);
await replaceInFile(
`src/locale/messages.${lang}.xlf`,
'<source>Hello i18n!</source>',
`<source>Hello i18n!</source>\n<target>${translation}</target>`,
);
}
}

// Build each locale and verify the output.
await ng('build', '--output-hashing=none');

for (const { lang, translation } of langTranslations) {
let foundTranslation = false;
let foundLocaleData = false;

// The translation may be in any of the lazy-loaded generated chunks
for (const entry of readdirSync(`dist/test-project/server/${lang}/`)) {
Expand All @@ -98,14 +59,23 @@ export default async function () {
}

const contents = readFileSync(`dist/test-project/server/${lang}/${entry}`, 'utf-8');
foundTranslation ||= contents.includes(translation);
if (foundTranslation) {

// Check for translated content
foundTranslation ||= contents.includes(translation.helloPartial);
// Check for the locale data month name to be present
foundLocaleData ||= contents.includes(translation.date);

if (foundTranslation && foundLocaleData) {
break;
}
}

if (!foundTranslation) {
throw new Error(`Translation not found in 'dist/test-project/server/${lang}/'`);
}

if (!foundLocaleData) {
throw new Error(`Locale data not found in 'dist/test-project/server/${lang}/'`);
}
}
}

0 comments on commit 52f5956

Please sign in to comment.