Skip to content

Commit 91019bd

Browse files
clydinalan-agius4
authored andcommitted
feat(@angular-devkit/build-angular): enable localize support for SSR with application builder
With the output path directory structure updates in place, the localize support for SSR has now been enabled. This allows for the `application` builder to produce both browser and server output that is localized based on the i18n configuration for the project.
1 parent 223a82f commit 91019bd

File tree

2 files changed

+111
-8
lines changed

2 files changed

+111
-8
lines changed

packages/angular_devkit/build_angular/src/builders/application/index.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,6 @@ export async function* buildApplicationInternal(
4444

4545
const normalizedOptions = await normalizeOptions(context, projectName, options);
4646

47-
// Warn about SSR not yet supporting localize
48-
if (normalizedOptions.i18nOptions.shouldInline && normalizedOptions.ssrOptions) {
49-
context.logger.warn(
50-
`SSR is not yet supported with the 'localize' option and will be disabled for this build.`,
51-
);
52-
normalizedOptions.ssrOptions = undefined;
53-
}
54-
5547
yield* runEsBuildBuildAction(
5648
(rebuildState) => executeBuild(normalizedOptions, context, rebuildState),
5749
{
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { readFileSync, readdirSync } from 'node:fs';
2+
import { getGlobalVariable } from '../../utils/env';
3+
import { copyFile, expectFileToMatch, replaceInFile, writeFile } from '../../utils/fs';
4+
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
5+
import { ng } from '../../utils/process';
6+
import { updateJsonFile, useSha } from '../../utils/project';
7+
import { readNgVersion } from '../../utils/version';
8+
9+
const snapshots = require('../../ng-snapshot/package.json');
10+
11+
export default async function () {
12+
const useWebpackBuilder = !getGlobalVariable('argv')['esbuild'];
13+
if (useWebpackBuilder) {
14+
// This test is for the `application` builder only
15+
return;
16+
}
17+
18+
const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots'];
19+
await updateJsonFile('package.json', (packageJson) => {
20+
const dependencies = packageJson['dependencies'];
21+
dependencies['@angular/localize'] = isSnapshotBuild
22+
? snapshots.dependencies['@angular/localize']
23+
: readNgVersion();
24+
});
25+
26+
// forcibly remove in case another test doesn't clean itself up
27+
await uninstallPackage('@angular/ssr');
28+
29+
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
30+
31+
await useSha();
32+
await installWorkspacePackages();
33+
34+
// Set configurations for each locale.
35+
const langTranslations = [
36+
{ lang: 'en-US', translation: 'Hello i18n!' },
37+
{ lang: 'fr', translation: 'Bonjour i18n!' },
38+
];
39+
40+
await updateJsonFile('angular.json', (workspaceJson) => {
41+
const appProject = workspaceJson.projects['test-project'];
42+
const appArchitect = appProject.architect || appProject.targets;
43+
const buildOptions = appArchitect['build'].options;
44+
45+
// Enable localization for all locales
46+
buildOptions.localize = true;
47+
48+
// Add locale definitions to the project
49+
const i18n: Record<string, any> = (appProject.i18n = { locales: {} });
50+
for (const { lang } of langTranslations) {
51+
if (lang == 'en-US') {
52+
i18n.sourceLocale = lang;
53+
} else {
54+
i18n.locales[lang] = `src/locale/messages.${lang}.xlf`;
55+
}
56+
}
57+
});
58+
59+
// Add a translatable element
60+
// Extraction of i18n only works on browser targets.
61+
// Let's add the same translation that there is in the app-shell
62+
await writeFile(
63+
'src/app/app.component.html',
64+
'<h1 i18n="An introduction header for this sample">Hello i18n!</h1>',
65+
);
66+
67+
// Extract the translation messages and copy them for each language.
68+
await ng('extract-i18n', '--output-path=src/locale');
69+
await expectFileToMatch('src/locale/messages.xlf', `source-language="en-US"`);
70+
await expectFileToMatch('src/locale/messages.xlf', `An introduction header for this sample`);
71+
72+
for (const { lang, translation } of langTranslations) {
73+
if (lang != 'en-US') {
74+
await copyFile('src/locale/messages.xlf', `src/locale/messages.${lang}.xlf`);
75+
await replaceInFile(
76+
`src/locale/messages.${lang}.xlf`,
77+
'source-language="en-US"',
78+
`source-language="en-US" target-language="${lang}"`,
79+
);
80+
await replaceInFile(
81+
`src/locale/messages.${lang}.xlf`,
82+
'<source>Hello i18n!</source>',
83+
`<source>Hello i18n!</source>\n<target>${translation}</target>`,
84+
);
85+
}
86+
}
87+
88+
// Build each locale and verify the output.
89+
await ng('build', '--output-hashing=none');
90+
91+
for (const { lang, translation } of langTranslations) {
92+
let foundTranslation = false;
93+
94+
// The translation may be in any of the lazy-loaded generated chunks
95+
for (const entry of readdirSync(`dist/test-project/server/${lang}/`)) {
96+
if (!entry.endsWith('.mjs')) {
97+
continue;
98+
}
99+
100+
const contents = readFileSync(`dist/test-project/server/${lang}/${entry}`, 'utf-8');
101+
foundTranslation ||= contents.includes(translation);
102+
if (foundTranslation) {
103+
break;
104+
}
105+
}
106+
107+
if (!foundTranslation) {
108+
throw new Error(`Translation not found in 'dist/test-project/server/${lang}/'`);
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)