Skip to content

Commit dcee94b

Browse files
committed
refactor(@angular-devkit/build-angular): split Webpack-specific functionality from i18n option creation
The i18n option creation for a project, which is a combination of the `i18n` project field and the `localize` build option, is now in a separate file from the Webpack specific i18n configuration setup. This allows the i18n option creation to be used without loading anything Webpack specific.
1 parent 03d0fca commit dcee94b

File tree

10 files changed

+154
-140
lines changed

10 files changed

+154
-140
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { colors } from '../../utils/color';
4141
import { copyAssets } from '../../utils/copy-assets';
4242
import { assertIsError } from '../../utils/error';
4343
import { i18nInlineEmittedFiles } from '../../utils/i18n-inlining';
44-
import { I18nOptions } from '../../utils/i18n-options';
44+
import { I18nOptions } from '../../utils/i18n-webpack';
4545
import { FileInfo } from '../../utils/index-file/augment-index-html';
4646
import {
4747
IndexHtmlGenerator,

packages/angular_devkit/build_angular/src/builders/dev-server/webpack-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
import { ExecutionTransformer } from '../../transforms';
3030
import { normalizeOptimization } from '../../utils';
3131
import { colors } from '../../utils/color';
32-
import { I18nOptions, loadTranslations } from '../../utils/i18n-options';
32+
import { I18nOptions, loadTranslations } from '../../utils/i18n-webpack';
3333
import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
3434
import { createTranslationLoader } from '../../utils/load-translations';
3535
import { NormalizedCachedOptions } from '../../utils/normalize-cache';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { colors } from '../../utils/color';
3131
import { copyAssets } from '../../utils/copy-assets';
3232
import { assertIsError } from '../../utils/error';
3333
import { i18nInlineEmittedFiles } from '../../utils/i18n-inlining';
34-
import { I18nOptions } from '../../utils/i18n-options';
34+
import { I18nOptions } from '../../utils/i18n-webpack';
3535
import { ensureOutputPaths } from '../../utils/output-paths';
3636
import { purgeStaleBuildCache } from '../../utils/purge-cache';
3737
import { Spinner } from '../../utils/spinner';

packages/angular_devkit/build_angular/src/utils/action-executor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Piscina from 'piscina';
1010
import { InlineOptions } from './bundle-inline-options';
1111
import { maxWorkers } from './environment-options';
12-
import { I18nOptions } from './i18n-options';
12+
import { I18nOptions } from './i18n-webpack';
1313

1414
const workerFile = require.resolve('./process-bundle');
1515

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { BundleActionExecutor } from './action-executor';
1414
import { InlineOptions } from './bundle-inline-options';
1515
import { copyAssets } from './copy-assets';
1616
import { assertIsError } from './error';
17-
import { I18nOptions } from './i18n-options';
17+
import { I18nOptions } from './i18n-webpack';
1818
import { Spinner } from './spinner';
1919

2020
function emittedFilesToInlineOptions(

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

Lines changed: 5 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { BuilderContext } from '@angular-devkit/architect';
10-
import fs from 'node:fs';
11-
import { createRequire } from 'node:module';
12-
import os from 'node:os';
139
import path from 'node:path';
14-
import { Schema as BrowserBuilderSchema, I18NTranslation } from '../builders/browser/schema';
15-
import { Schema as ServerBuilderSchema } from '../builders/server/schema';
16-
import { readTsconfig } from '../utils/read-tsconfig';
17-
import { TranslationLoader, createTranslationLoader } from './load-translations';
18-
19-
/**
20-
* The base module location used to search for locale specific data.
21-
*/
22-
const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global';
10+
import type { TranslationLoader } from './load-translations';
2311

2412
export interface LocaleDescription {
2513
files: {
@@ -168,129 +156,14 @@ export function createI18nOptions(
168156
return i18n;
169157
}
170158

171-
export async function configureI18nBuild<T extends BrowserBuilderSchema | ServerBuilderSchema>(
172-
context: BuilderContext,
173-
options: T,
174-
): Promise<{
175-
buildOptions: T;
176-
i18n: I18nOptions;
177-
}> {
178-
if (!context.target) {
179-
throw new Error('The builder requires a target.');
180-
}
181-
182-
const buildOptions = { ...options };
183-
const tsConfig = await readTsconfig(buildOptions.tsConfig, context.workspaceRoot);
184-
const metadata = await context.getProjectMetadata(context.target);
185-
const i18n = createI18nOptions(metadata, buildOptions.localize);
186-
187-
// No additional processing needed if no inlining requested and no source locale defined.
188-
if (!i18n.shouldInline && !i18n.hasDefinedSourceLocale) {
189-
return { buildOptions, i18n };
190-
}
191-
192-
const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
193-
// The trailing slash is required to signal that the path is a directory and not a file.
194-
const projectRequire = createRequire(projectRoot + '/');
195-
const localeResolver = (locale: string) =>
196-
projectRequire.resolve(path.join(LOCALE_DATA_BASE_MODULE, locale));
197-
198-
// Load locale data and translations (if present)
199-
let loader;
200-
const usedFormats = new Set<string>();
201-
for (const [locale, desc] of Object.entries(i18n.locales)) {
202-
if (!i18n.inlineLocales.has(locale) && locale !== i18n.sourceLocale) {
203-
continue;
204-
}
205-
206-
let localeDataPath = findLocaleDataPath(locale, localeResolver);
207-
if (!localeDataPath) {
208-
const [first] = locale.split('-');
209-
if (first) {
210-
localeDataPath = findLocaleDataPath(first.toLowerCase(), localeResolver);
211-
if (localeDataPath) {
212-
context.logger.warn(
213-
`Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`,
214-
);
215-
}
216-
}
217-
}
218-
if (!localeDataPath) {
219-
context.logger.warn(
220-
`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
221-
);
222-
} else {
223-
desc.dataPath = localeDataPath;
224-
}
225-
226-
if (!desc.files.length) {
227-
continue;
228-
}
229-
230-
loader ??= await createTranslationLoader();
231-
232-
loadTranslations(
233-
locale,
234-
desc,
235-
context.workspaceRoot,
236-
loader,
237-
{
238-
warn(message) {
239-
context.logger.warn(message);
240-
},
241-
error(message) {
242-
throw new Error(message);
243-
},
244-
},
245-
usedFormats,
246-
buildOptions.i18nDuplicateTranslation,
247-
);
248-
249-
if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
250-
// This limitation is only for legacy message id support (defaults to true as of 9.0)
251-
throw new Error(
252-
'Localization currently only supports using one type of translation file format for the entire application.',
253-
);
254-
}
255-
}
256-
257-
// If inlining store the output in a temporary location to facilitate post-processing
258-
if (i18n.shouldInline) {
259-
// TODO: we should likely save these in the .angular directory in the next major version.
260-
// We'd need to do a migration to add the temp directory to gitignore.
261-
const tempPath = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'angular-cli-i18n-'));
262-
buildOptions.outputPath = tempPath;
263-
264-
process.on('exit', () => {
265-
try {
266-
fs.rmSync(tempPath, { force: true, recursive: true, maxRetries: 3 });
267-
} catch {}
268-
});
269-
}
270-
271-
return { buildOptions, i18n };
272-
}
273-
274-
function findLocaleDataPath(locale: string, resolver: (locale: string) => string): string | null {
275-
// Remove private use subtags
276-
const scrubbedLocale = locale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, '');
277-
278-
try {
279-
return resolver(scrubbedLocale);
280-
} catch {
281-
// fallback to known existing en-US locale data as of 14.0
282-
return scrubbedLocale === 'en-US' ? findLocaleDataPath('en', resolver) : null;
283-
}
284-
}
285-
286159
export function loadTranslations(
287160
locale: string,
288161
desc: LocaleDescription,
289162
workspaceRoot: string,
290163
loader: TranslationLoader,
291164
logger: { warn: (message: string) => void; error: (message: string) => void },
292165
usedFormats?: Set<string>,
293-
duplicateTranslation?: I18NTranslation,
166+
duplicateTranslation?: 'ignore' | 'error' | 'warning',
294167
) {
295168
let translations: Record<string, unknown> | undefined = undefined;
296169
for (const file of desc.files) {
@@ -320,12 +193,12 @@ export function loadTranslations(
320193
if (translations[id] !== undefined) {
321194
const duplicateTranslationMessage = `[${file.path}]: Duplicate translations for message '${id}' when merging.`;
322195
switch (duplicateTranslation) {
323-
case I18NTranslation.Ignore:
196+
case 'ignore':
324197
break;
325-
case I18NTranslation.Error:
198+
case 'error':
326199
logger.error(`ERROR ${duplicateTranslationMessage}`);
327200
break;
328-
case I18NTranslation.Warning:
201+
case 'warning':
329202
default:
330203
logger.warn(`WARNING ${duplicateTranslationMessage}`);
331204
break;
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { BuilderContext } from '@angular-devkit/architect';
10+
import fs from 'node:fs';
11+
import { createRequire } from 'node:module';
12+
import os from 'node:os';
13+
import path from 'node:path';
14+
import { Schema as BrowserBuilderSchema } from '../builders/browser/schema';
15+
import { Schema as ServerBuilderSchema } from '../builders/server/schema';
16+
import { readTsconfig } from '../utils/read-tsconfig';
17+
import { I18nOptions, createI18nOptions, loadTranslations } from './i18n-options';
18+
import { createTranslationLoader } from './load-translations';
19+
20+
/**
21+
* The base module location used to search for locale specific data.
22+
*/
23+
const LOCALE_DATA_BASE_MODULE = '@angular/common/locales/global';
24+
25+
// Re-export for use within Webpack related builders
26+
export { I18nOptions, loadTranslations };
27+
28+
export async function configureI18nBuild<T extends BrowserBuilderSchema | ServerBuilderSchema>(
29+
context: BuilderContext,
30+
options: T,
31+
): Promise<{
32+
buildOptions: T;
33+
i18n: I18nOptions;
34+
}> {
35+
if (!context.target) {
36+
throw new Error('The builder requires a target.');
37+
}
38+
39+
const buildOptions = { ...options };
40+
const tsConfig = await readTsconfig(buildOptions.tsConfig, context.workspaceRoot);
41+
const metadata = await context.getProjectMetadata(context.target);
42+
const i18n = createI18nOptions(metadata, buildOptions.localize);
43+
44+
// No additional processing needed if no inlining requested and no source locale defined.
45+
if (!i18n.shouldInline && !i18n.hasDefinedSourceLocale) {
46+
return { buildOptions, i18n };
47+
}
48+
49+
const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
50+
// The trailing slash is required to signal that the path is a directory and not a file.
51+
const projectRequire = createRequire(projectRoot + '/');
52+
const localeResolver = (locale: string) =>
53+
projectRequire.resolve(path.join(LOCALE_DATA_BASE_MODULE, locale));
54+
55+
// Load locale data and translations (if present)
56+
let loader;
57+
const usedFormats = new Set<string>();
58+
for (const [locale, desc] of Object.entries(i18n.locales)) {
59+
if (!i18n.inlineLocales.has(locale) && locale !== i18n.sourceLocale) {
60+
continue;
61+
}
62+
63+
let localeDataPath = findLocaleDataPath(locale, localeResolver);
64+
if (!localeDataPath) {
65+
const [first] = locale.split('-');
66+
if (first) {
67+
localeDataPath = findLocaleDataPath(first.toLowerCase(), localeResolver);
68+
if (localeDataPath) {
69+
context.logger.warn(
70+
`Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`,
71+
);
72+
}
73+
}
74+
}
75+
if (!localeDataPath) {
76+
context.logger.warn(
77+
`Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
78+
);
79+
} else {
80+
desc.dataPath = localeDataPath;
81+
}
82+
83+
if (!desc.files.length) {
84+
continue;
85+
}
86+
87+
loader ??= await createTranslationLoader();
88+
89+
loadTranslations(
90+
locale,
91+
desc,
92+
context.workspaceRoot,
93+
loader,
94+
{
95+
warn(message) {
96+
context.logger.warn(message);
97+
},
98+
error(message) {
99+
throw new Error(message);
100+
},
101+
},
102+
usedFormats,
103+
buildOptions.i18nDuplicateTranslation,
104+
);
105+
106+
if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
107+
// This limitation is only for legacy message id support (defaults to true as of 9.0)
108+
throw new Error(
109+
'Localization currently only supports using one type of translation file format for the entire application.',
110+
);
111+
}
112+
}
113+
114+
// If inlining store the output in a temporary location to facilitate post-processing
115+
if (i18n.shouldInline) {
116+
// TODO: we should likely save these in the .angular directory in the next major version.
117+
// We'd need to do a migration to add the temp directory to gitignore.
118+
const tempPath = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'angular-cli-i18n-'));
119+
buildOptions.outputPath = tempPath;
120+
121+
process.on('exit', () => {
122+
try {
123+
fs.rmSync(tempPath, { force: true, recursive: true, maxRetries: 3 });
124+
} catch {}
125+
});
126+
}
127+
128+
return { buildOptions, i18n };
129+
}
130+
131+
function findLocaleDataPath(locale: string, resolver: (locale: string) => string): string | null {
132+
// Remove private use subtags
133+
const scrubbedLocale = locale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, '');
134+
135+
try {
136+
return resolver(scrubbedLocale);
137+
} catch {
138+
// fallback to known existing en-US locale data as of 14.0
139+
return scrubbedLocale === 'en-US' ? findLocaleDataPath('en', resolver) : null;
140+
}
141+
}

packages/angular_devkit/build_angular/src/utils/output-paths.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { existsSync, mkdirSync } from 'fs';
1010
import { join } from 'path';
11-
import { I18nOptions } from './i18n-options';
11+
import { I18nOptions } from './i18n-webpack';
1212

1313
export function ensureOutputPaths(baseOutputPath: string, i18n: I18nOptions): Map<string, string> {
1414
const outputPaths: [string, string][] = i18n.shouldInline

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { workerData } from 'worker_threads';
2323
import { InlineOptions } from './bundle-inline-options';
2424
import { allowMinify, shouldBeautify } from './environment-options';
2525
import { assertIsError } from './error';
26-
import { I18nOptions } from './i18n-options';
26+
import { I18nOptions } from './i18n-webpack';
2727
import { loadEsmModule } from './load-esm';
2828

2929
// Extract Sourcemap input type from the remapping function since it is not currently exported

packages/angular_devkit/build_angular/src/utils/webpack-browser-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { NormalizedBrowserBuilderSchema, defaultProgress, normalizeBrowserSchema } from '../utils';
1919
import { WebpackConfigOptions } from '../utils/build-options';
2020
import { readTsconfig } from '../utils/read-tsconfig';
21-
import { I18nOptions, configureI18nBuild } from './i18n-options';
21+
import { I18nOptions, configureI18nBuild } from './i18n-webpack';
2222

2323
export type BrowserWebpackConfigOptions = WebpackConfigOptions<NormalizedBrowserBuilderSchema>;
2424

0 commit comments

Comments
 (0)