Skip to content

Commit b847204

Browse files
committed
feat(@angular-devkit/build-angular): watch i18n translation files with dev server
When using i18n with the dev server, the translation files will now be linked as a dependency to any file containing translated text. This allows translation files to be watched and the application to be rebuilt using the changed translation files. This change should also provide some performance improvements as well since now only files containing `$localize` will be parsed and processed by the babel-based localization inliner. Closes #16341
1 parent e02d737 commit b847204

File tree

2 files changed

+81
-56
lines changed

2 files changed

+81
-56
lines changed

packages/angular_devkit/build_angular/src/babel/webpack-loader.ts

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
*/
88
import { custom } from 'babel-loader';
99
import { ScriptTarget } from 'typescript';
10+
import { ApplicationPresetOptions } from './presets/application';
1011

1112
interface AngularCustomOptions {
1213
forceAsyncTransformation: boolean;
1314
forceES5: boolean;
1415
shouldLink: boolean;
16+
i18n: ApplicationPresetOptions['i18n'];
1517
}
1618

1719
/**
@@ -65,48 +67,76 @@ export default custom<AngularCustomOptions>(() => {
6567
});
6668

6769
return {
68-
async customOptions({ scriptTarget, ...loaderOptions }, { source }) {
70+
async customOptions({ i18n, scriptTarget, ...rawOptions }, { source }) {
6971
// Must process file if plugins are added
70-
let shouldProcess = Array.isArray(loaderOptions.plugins) && loaderOptions.plugins.length > 0;
72+
let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0;
73+
74+
const customOptions: AngularCustomOptions = {
75+
forceAsyncTransformation: false,
76+
forceES5: false,
77+
shouldLink: false,
78+
i18n: undefined,
79+
};
7180

7281
// Analyze file for linking
73-
let shouldLink = false;
7482
const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source);
7583
if (requiresLinking && !hasLinkerSupport) {
7684
// Cannot link if there is no linker support
7785
this.emitError(
7886
'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.',
7987
);
8088
} else {
81-
shouldLink = requiresLinking;
89+
customOptions.shouldLink = requiresLinking;
8290
}
83-
shouldProcess ||= shouldLink;
91+
shouldProcess ||= customOptions.shouldLink;
8492

8593
// Analyze for ES target processing
86-
let forceES5 = false;
87-
let forceAsyncTransformation = false;
88-
const esTarget = scriptTarget as ScriptTarget;
89-
if (esTarget < ScriptTarget.ES2015) {
90-
// TypeScript files will have already been downlevelled
91-
forceES5 = !/\.tsx?$/.test(this.resourcePath);
92-
} else if (esTarget >= ScriptTarget.ES2017) {
93-
forceAsyncTransformation = source.includes('async');
94+
const esTarget = scriptTarget as ScriptTarget | undefined;
95+
if (esTarget !== undefined) {
96+
if (esTarget < ScriptTarget.ES2015) {
97+
// TypeScript files will have already been downlevelled
98+
customOptions.forceES5 = !/\.tsx?$/.test(this.resourcePath);
99+
} else if (esTarget >= ScriptTarget.ES2017) {
100+
customOptions.forceAsyncTransformation = source.includes('async');
101+
}
102+
shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5;
103+
}
104+
105+
// Analyze for i18n inlining
106+
if (
107+
i18n &&
108+
!/[\\\/]@angular[\\\/](?:compiler|localize)/.test(this.resourcePath) &&
109+
source.includes('$localize')
110+
) {
111+
const { translationFiles, ...i18nOptions } = i18n as ApplicationPresetOptions['i18n'] & {
112+
translationFiles?: string[];
113+
};
114+
customOptions.i18n = i18nOptions;
115+
116+
// Add translation files as dependencies of the file to support rebuilds
117+
// Except for `@angular/core` which needs locale injection but has no translations
118+
if (translationFiles && !/[\\\/]@angular[\\\/]core/.test(this.resourcePath)) {
119+
for (const file of translationFiles) {
120+
this.addDependency(file);
121+
}
122+
}
123+
124+
shouldProcess = true;
94125
}
95-
shouldProcess ||= forceAsyncTransformation || forceES5;
96126

97127
// Add provided loader options to default base options
98-
const options: Record<string, unknown> = {
128+
const loaderOptions: Record<string, unknown> = {
99129
...baseOptions,
100-
...loaderOptions,
130+
...rawOptions,
101131
};
102132

103133
// Skip babel processing if no actions are needed
104134
if (!shouldProcess) {
105135
// Force the current file to be ignored
106-
options.ignore = [() => true];
136+
loaderOptions.ignore = [() => true];
107137
}
108138

109-
return { custom: { forceAsyncTransformation, forceES5, shouldLink }, loader: options };
139+
return { custom: customOptions, loader: loaderOptions };
110140
},
111141
config(configuration, { customOptions }) {
112142
return {
@@ -119,6 +149,7 @@ export default custom<AngularCustomOptions>(() => {
119149
angularLinker: customOptions.shouldLink,
120150
forceES5: customOptions.forceES5,
121151
forceAsyncTransformation: customOptions.forceAsyncTransformation,
152+
i18n: customOptions.i18n,
122153
diagnosticReporter: (type, message) => {
123154
switch (type) {
124155
case 'error':
@@ -131,7 +162,7 @@ export default custom<AngularCustomOptions>(() => {
131162
break;
132163
}
133164
},
134-
} as import('./presets/application').ApplicationPresetOptions,
165+
} as ApplicationPresetOptions,
135166
],
136167
],
137168
};

packages/angular_devkit/build_angular/src/dev-server/index.ts

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export function serveWebpackBrowser(
236236
);
237237
}
238238

239-
await setupLocalize(locale, i18n, browserOptions, webpackConfig);
239+
await setupLocalize(locale, i18n, browserOptions, webpackConfig, context);
240240
}
241241
}
242242

@@ -346,9 +346,9 @@ async function setupLocalize(
346346
i18n: I18nOptions,
347347
browserOptions: BrowserBuilderSchema,
348348
webpackConfig: webpack.Configuration,
349+
context: BuilderContext,
349350
) {
350351
const localeDescription = i18n.locales[locale];
351-
const i18nDiagnostics: { type: string, message: string }[] = [];
352352

353353
// Modify main entrypoint to include locale data
354354
if (
@@ -366,43 +366,37 @@ async function setupLocalize(
366366

367367
let missingTranslationBehavior = browserOptions.i18nMissingTranslation || 'ignore';
368368
let translation = localeDescription?.translation || {};
369+
const sourceLocale = i18n.sourceLocale;
370+
const shouldInline = i18n.shouldInline;
369371

370-
if (locale === i18n.sourceLocale) {
372+
if (locale === sourceLocale) {
371373
missingTranslationBehavior = 'ignore';
372374
translation = {};
373375
}
374376

377+
const i18nLoaderOptions = {
378+
locale,
379+
missingTranslationBehavior,
380+
translation: shouldInline ? translation : undefined,
381+
translationFiles: localeDescription?.files.map((file) =>
382+
path.resolve(context.workspaceRoot, file.path),
383+
),
384+
};
385+
375386
const i18nRule: webpack.RuleSetRule = {
376387
test: /\.(?:m?js|ts)$/,
377388
enforce: 'post',
378389
use: [
379390
{
380-
loader: require.resolve('babel-loader'),
391+
loader: require.resolve('../babel/webpack-loader'),
381392
options: {
382-
babelrc: false,
383-
configFile: false,
384-
compact: false,
385-
cacheCompression: false,
386-
cacheDirectory: findCachePath('babel-loader'),
387-
cacheIdentifier: JSON.stringify({
388-
buildAngular: require('../../package.json').version,
389-
locale,
390-
translationIntegrity: localeDescription?.files.map((file) => file.integrity),
391-
}),
392-
sourceType: 'unambiguous',
393-
presets: [
394-
[
395-
require.resolve('../babel/presets/application'),
396-
{
397-
i18n: {
398-
locale,
399-
translation: i18n.shouldInline ? translation : undefined,
400-
missingTranslationBehavior,
401-
},
402-
diagnosticReporter: (type, message) => i18nDiagnostics.push({ type, message }),
403-
} as import('../babel/presets/application').ApplicationPresetOptions,
404-
],
405-
],
393+
// cacheDirectory: findCachePath('babel-dev-server-i18n'),
394+
// cacheIdentifier: JSON.stringify({
395+
// buildAngular: require('../../package.json').version,
396+
// locale,
397+
// translationIntegrity: localeDescription?.files.map((file) => file.integrity),
398+
// }),
399+
i18n: i18nLoaderOptions,
406400
},
407401
},
408402
],
@@ -418,20 +412,20 @@ async function setupLocalize(
418412

419413
rules.push(i18nRule);
420414

421-
// Add a plugin to inject the i18n diagnostics
415+
// Add a plugin to reload translation files on rebuilds
422416
// tslint:disable-next-line: no-non-null-assertion
423417
webpackConfig.plugins!.push({
424418
apply: (compiler: webpack.Compiler) => {
425419
compiler.hooks.thisCompilation.tap('build-angular', compilation => {
420+
if (shouldInline && i18nLoaderOptions.translation === undefined) {
421+
// Reload translation
422+
// NOTE: This could be further optimized by checking if any translation file actually changed
423+
424+
}
425+
426426
compilation.hooks.finishModules.tap('build-angular', () => {
427-
for (const diagnostic of i18nDiagnostics) {
428-
if (diagnostic.type === 'error') {
429-
addError(compilation, diagnostic.message);
430-
} else {
431-
addWarning(compilation, diagnostic.message);
432-
}
433-
}
434-
i18nDiagnostics.length = 0;
427+
// After loaders are finished, clear out the now unneeded translations
428+
i18nLoaderOptions.translation = undefined;
435429
});
436430
});
437431
},

0 commit comments

Comments
 (0)