Skip to content

Commit 3a57657

Browse files
committed
refactor(@angular-devkit/build-angular): restructure i18n extraction builder to allow for application builder support
The `extract-i18n` builder code has been restructured in a similar fashion to the `dev-server` builder. This refactor provides the groundwork to add support for the `application` and `browser-esbuild` builders during message extraction. The webpack related logic has been split into a separate file and is dynamically imported when needed. Additionally the options processing has been moved to a separate file and the `index.ts` now only exports instead of previously containing most of the builder logic.
1 parent 83cfe29 commit 3a57657

File tree

5 files changed

+402
-301
lines changed

5 files changed

+402
-301
lines changed

goldens/public-api/angular_devkit/build_angular/index.md

+9-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { BuilderContext } from '@angular-devkit/architect';
88
import { BuilderOutput } from '@angular-devkit/architect';
9-
import { BuildResult } from '@angular-devkit/build-webpack';
109
import type { ConfigOptions } from 'karma';
1110
import { Configuration } from 'webpack';
1211
import { DevServerBuildOutput } from '@angular-devkit/build-webpack';
@@ -153,7 +152,7 @@ export function executeDevServerBuilder(options: DevServerBuilderOptions, contex
153152
// @public
154153
export function executeExtractI18nBuilder(options: ExtractI18nBuilderOptions, context: BuilderContext, transforms?: {
155154
webpackConfiguration?: ExecutionTransformer<webpack.Configuration>;
156-
}): Promise<BuildResult>;
155+
}): Promise<BuilderOutput>;
157156

158157
// @public
159158
export function executeKarmaBuilder(options: KarmaBuilderOptions, context: BuilderContext, transforms?: {
@@ -175,8 +174,14 @@ export function executeServerBuilder(options: ServerBuilderOptions, context: Bui
175174
// @public
176175
export type ExecutionTransformer<T> = (input: T) => T | Promise<T>;
177176

178-
// @public (undocumented)
179-
export type ExtractI18nBuilderOptions = Schema;
177+
// @public
178+
export interface ExtractI18nBuilderOptions {
179+
browserTarget: string;
180+
format?: Format;
181+
outFile?: string;
182+
outputPath?: string;
183+
progress?: boolean;
184+
}
180185

181186
// @public (undocumented)
182187
export interface FileReplacement {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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 type { Diagnostics } from '@angular/localize/tools';
10+
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
11+
import fs from 'node:fs';
12+
import path from 'node:path';
13+
import type webpack from 'webpack';
14+
import type { ExecutionTransformer } from '../../transforms';
15+
import { loadEsmModule } from '../../utils/load-esm';
16+
import { purgeStaleBuildCache } from '../../utils/purge-cache';
17+
import { assertCompatibleAngularVersion } from '../../utils/version';
18+
import { normalizeOptions } from './options';
19+
import { Schema as ExtractI18nBuilderOptions, Format } from './schema';
20+
21+
/**
22+
* @experimental Direct usage of this function is considered experimental.
23+
*/
24+
export async function execute(
25+
options: ExtractI18nBuilderOptions,
26+
context: BuilderContext,
27+
transforms?: {
28+
webpackConfiguration?: ExecutionTransformer<webpack.Configuration>;
29+
},
30+
): Promise<BuilderOutput> {
31+
// Determine project name from builder context target
32+
const projectName = context.target?.project;
33+
if (!projectName) {
34+
context.logger.error(`The 'extract-i18n' builder requires a target to be specified.`);
35+
36+
return { success: false };
37+
}
38+
39+
// Check Angular version.
40+
assertCompatibleAngularVersion(context.workspaceRoot);
41+
42+
// Load the Angular localize package.
43+
// The package is a peer dependency and might not be present
44+
let localizeToolsModule;
45+
try {
46+
localizeToolsModule = await loadEsmModule<typeof import('@angular/localize/tools')>(
47+
'@angular/localize/tools',
48+
);
49+
} catch {
50+
return {
51+
success: false,
52+
error: `i18n extraction requires the '@angular/localize' package.`,
53+
};
54+
}
55+
56+
// Purge old build disk cache.
57+
await purgeStaleBuildCache(context);
58+
59+
// Normalize options
60+
const normalizedOptions = await normalizeOptions(context, projectName, options);
61+
const builderName = await context.getBuilderNameForTarget(normalizedOptions.browserTarget);
62+
63+
// Extract messages based on configured builder
64+
// TODO: Implement application/browser-esbuild support
65+
let extractionResult;
66+
if (
67+
builderName === '@angular-devkit/build-angular:application' ||
68+
builderName === '@angular-devkit/build-angular:browser-esbuild'
69+
) {
70+
return {
71+
error: 'i18n extraction is currently only supported with the "browser" builder.',
72+
success: false,
73+
};
74+
} else {
75+
const { extractMessages } = await import('./webpack-extraction');
76+
extractionResult = await extractMessages(normalizedOptions, builderName, context, transforms);
77+
}
78+
79+
// Return the builder result if it failed
80+
if (!extractionResult.builderResult.success) {
81+
return extractionResult.builderResult;
82+
}
83+
84+
// Perform duplicate message checks
85+
const { checkDuplicateMessages } = localizeToolsModule;
86+
87+
// The filesystem is used to create a relative path for each file
88+
// from the basePath. This relative path is then used in the error message.
89+
const checkFileSystem = {
90+
relative(from: string, to: string): string {
91+
return path.relative(from, to);
92+
},
93+
};
94+
const diagnostics = checkDuplicateMessages(
95+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
96+
checkFileSystem as any,
97+
extractionResult.messages,
98+
'warning',
99+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
100+
extractionResult.basePath as any,
101+
);
102+
if (diagnostics.messages.length > 0) {
103+
context.logger.warn(diagnostics.formatDiagnostics(''));
104+
}
105+
106+
// Serialize all extracted messages
107+
const serializer = await createSerializer(
108+
localizeToolsModule,
109+
normalizedOptions.format,
110+
normalizedOptions.i18nOptions.sourceLocale,
111+
extractionResult.basePath,
112+
extractionResult.useLegacyIds,
113+
diagnostics,
114+
);
115+
const content = serializer.serialize(extractionResult.messages);
116+
117+
// Ensure directory exists
118+
const outputPath = path.dirname(normalizedOptions.outFile);
119+
if (!fs.existsSync(outputPath)) {
120+
fs.mkdirSync(outputPath, { recursive: true });
121+
}
122+
123+
// Write translation file
124+
fs.writeFileSync(normalizedOptions.outFile, content);
125+
126+
return extractionResult.builderResult;
127+
}
128+
129+
async function createSerializer(
130+
localizeToolsModule: typeof import('@angular/localize/tools'),
131+
format: Format,
132+
sourceLocale: string,
133+
basePath: string,
134+
useLegacyIds: boolean,
135+
diagnostics: Diagnostics,
136+
) {
137+
const {
138+
XmbTranslationSerializer,
139+
LegacyMessageIdMigrationSerializer,
140+
ArbTranslationSerializer,
141+
Xliff1TranslationSerializer,
142+
Xliff2TranslationSerializer,
143+
SimpleJsonTranslationSerializer,
144+
} = localizeToolsModule;
145+
146+
switch (format) {
147+
case Format.Xmb:
148+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
149+
return new XmbTranslationSerializer(basePath as any, useLegacyIds);
150+
case Format.Xlf:
151+
case Format.Xlif:
152+
case Format.Xliff:
153+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
154+
return new Xliff1TranslationSerializer(sourceLocale, basePath as any, useLegacyIds, {});
155+
case Format.Xlf2:
156+
case Format.Xliff2:
157+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
158+
return new Xliff2TranslationSerializer(sourceLocale, basePath as any, useLegacyIds, {});
159+
case Format.Json:
160+
return new SimpleJsonTranslationSerializer(sourceLocale);
161+
case Format.LegacyMigrate:
162+
return new LegacyMessageIdMigrationSerializer(diagnostics);
163+
case Format.Arb:
164+
const fileSystem = {
165+
relative(from: string, to: string): string {
166+
return path.relative(from, to);
167+
},
168+
};
169+
170+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
171+
return new ArbTranslationSerializer(sourceLocale, basePath as any, fileSystem as any);
172+
}
173+
}

0 commit comments

Comments
 (0)