Skip to content

Commit 3c83530

Browse files
committed
fix(@angular-devkit/build-angular): insert locale data when localizing
1 parent da6d74a commit 3c83530

File tree

3 files changed

+84
-8
lines changed

3 files changed

+84
-8
lines changed

packages/angular_devkit/build_angular/src/utils/i18n-options.ts

+48-8
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import { createTranslationLoader } from './load-translations';
1919
export interface I18nOptions {
2020
inlineLocales: Set<string>;
2121
sourceLocale: string;
22-
locales: Record<string, { file: string; format?: string; translation?: unknown }>;
22+
locales: Record<
23+
string,
24+
{ file: string; format?: string; translation?: unknown; dataPath?: string }
25+
>;
2326
flatOutput?: boolean;
2427
readonly shouldInline: boolean;
2528
}
@@ -127,9 +130,16 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
127130
}
128131

129132
if (i18n.inlineLocales.size > 0) {
133+
const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
134+
const localeDataBasePath = findLocaleDataBasePath(projectRoot);
135+
if (!localeDataBasePath) {
136+
throw new Error(
137+
`Unable to find locale data within '@angular/common'. Please ensure '@angular/common' is installed.`,
138+
);
139+
}
140+
130141
// Load locales
131142
const loader = await createTranslationLoader();
132-
const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
133143
const usedFormats = new Set<string>();
134144
for (const [locale, desc] of Object.entries(i18n.locales)) {
135145
if (i18n.inlineLocales.has(locale) && desc.file) {
@@ -145,19 +155,22 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
145155

146156
desc.format = result.format;
147157
desc.translation = result.translation;
158+
159+
const localeDataPath = findLocaleDataPath(locale, localeDataBasePath);
160+
if (!localeDataPath) {
161+
context.logger.warn(
162+
`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
163+
);
164+
} else {
165+
desc.dataPath = localeDataPath;
166+
}
148167
}
149168
}
150169

151170
// Legacy message id's require the format of the translations
152171
if (usedFormats.size > 0) {
153172
buildOptions.i18nFormat = [...usedFormats][0];
154173
}
155-
156-
// If only one locale is specified set the deprecated option to enable the webpack plugin
157-
// transform to register the locale directly in the output bundle.
158-
if (i18n.inlineLocales.size === 1) {
159-
buildOptions.i18nLocale = [...i18n.inlineLocales][0];
160-
}
161174
}
162175

163176
// If inlining store the output in a temporary location to facilitate post-processing
@@ -202,3 +215,30 @@ function mergeDeprecatedI18nOptions(
202215

203216
return i18n;
204217
}
218+
219+
function findLocaleDataBasePath(projectRoot: string): string | null {
220+
try {
221+
const commonPath = path.dirname(
222+
require.resolve('@angular/common/package.json', { paths: [projectRoot] }),
223+
);
224+
const localesPath = path.join(commonPath, 'locales/global');
225+
226+
if (!fs.existsSync(localesPath)) {
227+
return null;
228+
}
229+
230+
return localesPath;
231+
} catch {
232+
return null;
233+
}
234+
}
235+
236+
function findLocaleDataPath(locale: string, basePath: string): string | null {
237+
const localeDataPath = path.join(basePath, locale + '.js');
238+
239+
if (!fs.existsSync(localeDataPath)) {
240+
return null;
241+
}
242+
243+
return localeDataPath;
244+
}

packages/angular_devkit/build_angular/src/utils/process-bundle.ts

+25
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,14 @@ export async function inlineLocales(options: InlineOptions) {
541541
const setLocaleText = `var $localize=Object.assign(void 0===$localize?{}:$localize,{locale:"${locale}"});`;
542542
contentClone = content.clone();
543543
content.prepend(setLocaleText);
544+
545+
// If locale data is provided, load it and prepend to file
546+
const localeDataPath = i18n.locales[locale] && i18n.locales[locale].dataPath;
547+
if (localeDataPath) {
548+
const localDataContent = loadLocaleData(localeDataPath, true);
549+
// The semicolon ensures that there is no syntax error between statements
550+
content.prepend(localDataContent + ';');
551+
}
544552
}
545553

546554
const output = content.toString();
@@ -669,3 +677,20 @@ function findLocalizePositions(
669677

670678
return positions;
671679
}
680+
681+
function loadLocaleData(path: string, optimize: boolean): string {
682+
// The path is validated during option processing before the build starts
683+
const content = fs.readFileSync(path, 'utf8');
684+
685+
// NOTE: This can be removed once the locale data files are preprocessed in the framework
686+
if (optimize) {
687+
const result = terserMangle(content, {
688+
compress: true,
689+
ecma: 5,
690+
});
691+
692+
return result.code;
693+
}
694+
695+
return content;
696+
}

tests/legacy-cli/e2e/tests/i18n/ivy-localize-dl.ts

+11
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,15 @@ export default async function () {
2828
await expectFileToMatch(`${outputPath}/main-es2015.js`, translation.helloPartial);
2929
await expectToFail(() => expectFileToMatch(`${outputPath}/main-es5.js`, '$localize`'));
3030
await expectToFail(() => expectFileToMatch(`${outputPath}/main-es2015.js`, '$localize`'));
31+
32+
// Verify the locale ID is present
3133
await expectFileToMatch(`${outputPath}/main-es5.js`, lang);
3234
await expectFileToMatch(`${outputPath}/main-es2015.js`, lang);
3335

36+
// Verify the locale data is registered using the global files
37+
await expectFileToMatch(`${outputPath}/main-es5.js`, '.ng.common.locales');
38+
await expectFileToMatch(`${outputPath}/main-es2015.js`, '.ng.common.locales');
39+
3440
const server = externalServer(outputPath);
3541
try {
3642
// Execute without a devserver.
@@ -40,6 +46,11 @@ export default async function () {
4046
}
4147
}
4248

49+
// Verify deprecated locale data registration is not present
50+
await ng('build', '--configuration=fr', '--optimization=false');
51+
await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es5.js`, 'registerLocaleData('));
52+
await expectToFail(() => expectFileToMatch(`${baseDir}/fr/main-es2015.js`, 'registerLocaleData('));
53+
4354
// Verify missing translation behaviour.
4455
await appendToFile('src/app/app.component.html', '<p i18n>Other content</p>');
4556
await ng('build', '--i18n-missing-translation', 'ignore');

0 commit comments

Comments
 (0)