-
Notifications
You must be signed in to change notification settings - Fork 12k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(@angular-devkit/build-angular): move i18n extraction for applica…
…tion builder to new build system package With the `application` builder already within the new `@angular/build` package, the `extract-i18n` builder with application builder support is now also contained within this package. Only the application builder aspects of `extract-i18n` have been moved. The compatibility builder `browser-esbuild` is not supported with `@angular/build:extract-i18n`. The existing `extract-i18n` builder found within `@angular-devkit/build-angular` should continue to be used for both the Webpack-based `browser` builder and the esbuild-based compatibility `browser-esbuild` builder. To maintain backwards compatibility, the existing `@angular-devkit/build-angular:extract-i18n` builder continues to support builders it has previously. No change to existing applications is required.
- Loading branch information
Showing
11 changed files
with
550 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
packages/angular/build/src/builders/extract-i18n/application-extraction.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import type { ɵParsedMessage as LocalizeMessage } from '@angular/localize'; | ||
import type { MessageExtractor } from '@angular/localize/tools'; | ||
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; | ||
import assert from 'node:assert'; | ||
import nodePath from 'node:path'; | ||
import { buildApplicationInternal } from '../application'; | ||
import type { | ||
ApplicationBuilderExtensions, | ||
ApplicationBuilderInternalOptions, | ||
} from '../application/options'; | ||
import type { NormalizedExtractI18nOptions } from './options'; | ||
|
||
export async function extractMessages( | ||
options: NormalizedExtractI18nOptions, | ||
builderName: string, | ||
context: BuilderContext, | ||
extractorConstructor: typeof MessageExtractor, | ||
extensions?: ApplicationBuilderExtensions, | ||
): Promise<{ | ||
builderResult: BuilderOutput; | ||
basePath: string; | ||
messages: LocalizeMessage[]; | ||
useLegacyIds: boolean; | ||
}> { | ||
const messages: LocalizeMessage[] = []; | ||
|
||
// Setup the build options for the application based on the buildTarget option | ||
const buildOptions = (await context.validateOptions( | ||
await context.getTargetOptions(options.buildTarget), | ||
builderName, | ||
)) as unknown as ApplicationBuilderInternalOptions; | ||
buildOptions.optimization = false; | ||
buildOptions.sourceMap = { scripts: true, vendor: true, styles: false }; | ||
buildOptions.localize = false; | ||
buildOptions.budgets = undefined; | ||
buildOptions.index = false; | ||
buildOptions.serviceWorker = false; | ||
|
||
buildOptions.ssr = false; | ||
buildOptions.appShell = false; | ||
buildOptions.prerender = false; | ||
|
||
// Build the application with the build options | ||
let builderResult; | ||
try { | ||
for await (const result of buildApplicationInternal( | ||
buildOptions, | ||
context, | ||
{ write: false }, | ||
extensions, | ||
)) { | ||
builderResult = result; | ||
break; | ||
} | ||
|
||
assert(builderResult !== undefined, 'Application builder did not provide a result.'); | ||
} catch (err) { | ||
builderResult = { | ||
success: false, | ||
error: (err as Error).message, | ||
}; | ||
} | ||
|
||
// Extract messages from each output JavaScript file. | ||
// Output files are only present on a successful build. | ||
if (builderResult.outputFiles) { | ||
// Store the JS and JS map files for lookup during extraction | ||
const files = new Map<string, string>(); | ||
for (const outputFile of builderResult.outputFiles) { | ||
if (outputFile.path.endsWith('.js')) { | ||
files.set(outputFile.path, outputFile.text); | ||
} else if (outputFile.path.endsWith('.js.map')) { | ||
files.set(outputFile.path, outputFile.text); | ||
} | ||
} | ||
|
||
// Setup the localize message extractor based on the in-memory files | ||
const extractor = setupLocalizeExtractor(extractorConstructor, files, context); | ||
|
||
// Attempt extraction of all output JS files | ||
for (const filePath of files.keys()) { | ||
if (!filePath.endsWith('.js')) { | ||
continue; | ||
} | ||
|
||
const fileMessages = extractor.extractMessages(filePath); | ||
messages.push(...fileMessages); | ||
} | ||
} | ||
|
||
return { | ||
builderResult, | ||
basePath: context.workspaceRoot, | ||
messages, | ||
// Legacy i18n identifiers are not supported with the new application builder | ||
useLegacyIds: false, | ||
}; | ||
} | ||
|
||
function setupLocalizeExtractor( | ||
extractorConstructor: typeof MessageExtractor, | ||
files: Map<string, string>, | ||
context: BuilderContext, | ||
): MessageExtractor { | ||
// Setup a virtual file system instance for the extractor | ||
// * MessageExtractor itself uses readFile, relative and resolve | ||
// * Internal SourceFileLoader (sourcemap support) uses dirname, exists, readFile, and resolve | ||
const filesystem = { | ||
readFile(path: string): string { | ||
// Output files are stored as relative to the workspace root | ||
const requestedPath = nodePath.relative(context.workspaceRoot, path); | ||
|
||
const content = files.get(requestedPath); | ||
if (content === undefined) { | ||
throw new Error('Unknown file requested: ' + requestedPath); | ||
} | ||
|
||
return content; | ||
}, | ||
relative(from: string, to: string): string { | ||
return nodePath.relative(from, to); | ||
}, | ||
resolve(...paths: string[]): string { | ||
return nodePath.resolve(...paths); | ||
}, | ||
exists(path: string): boolean { | ||
// Output files are stored as relative to the workspace root | ||
const requestedPath = nodePath.relative(context.workspaceRoot, path); | ||
|
||
return files.has(requestedPath); | ||
}, | ||
dirname(path: string): string { | ||
return nodePath.dirname(path); | ||
}, | ||
}; | ||
|
||
const logger = { | ||
// level 2 is warnings | ||
level: 2, | ||
debug(...args: string[]): void { | ||
// eslint-disable-next-line no-console | ||
console.debug(...args); | ||
}, | ||
info(...args: string[]): void { | ||
context.logger.info(args.join('')); | ||
}, | ||
warn(...args: string[]): void { | ||
context.logger.warn(args.join('')); | ||
}, | ||
error(...args: string[]): void { | ||
context.logger.error(args.join('')); | ||
}, | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const extractor = new extractorConstructor(filesystem as any, logger, { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
basePath: context.workspaceRoot as any, | ||
useSourceMaps: true, | ||
}); | ||
|
||
return extractor; | ||
} |
165 changes: 165 additions & 0 deletions
165
packages/angular/build/src/builders/extract-i18n/builder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import type { Diagnostics } from '@angular/localize/tools'; | ||
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; | ||
import fs from 'node:fs'; | ||
import path from 'node:path'; | ||
import { loadEsmModule } from '../../utils/load-esm'; | ||
import { assertCompatibleAngularVersion } from '../../utils/version'; | ||
import type { ApplicationBuilderExtensions } from '../application/options'; | ||
import { normalizeOptions } from './options'; | ||
import { Schema as ExtractI18nBuilderOptions, Format } from './schema'; | ||
|
||
/** | ||
* @experimental Direct usage of this function is considered experimental. | ||
*/ | ||
export async function execute( | ||
options: ExtractI18nBuilderOptions, | ||
context: BuilderContext, | ||
extensions?: ApplicationBuilderExtensions, | ||
): Promise<BuilderOutput> { | ||
// Determine project name from builder context target | ||
const projectName = context.target?.project; | ||
if (!projectName) { | ||
context.logger.error(`The 'extract-i18n' builder requires a target to be specified.`); | ||
|
||
return { success: false }; | ||
} | ||
|
||
// Check Angular version. | ||
assertCompatibleAngularVersion(context.workspaceRoot); | ||
|
||
// Load the Angular localize package. | ||
// The package is a peer dependency and might not be present | ||
let localizeToolsModule; | ||
try { | ||
localizeToolsModule = | ||
await loadEsmModule<typeof import('@angular/localize/tools')>('@angular/localize/tools'); | ||
} catch { | ||
return { | ||
success: false, | ||
error: | ||
`i18n extraction requires the '@angular/localize' package.` + | ||
` You can add it by using 'ng add @angular/localize'.`, | ||
}; | ||
} | ||
|
||
// Normalize options | ||
const normalizedOptions = await normalizeOptions(context, projectName, options); | ||
const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); | ||
|
||
// Extract messages based on configured builder | ||
const { extractMessages } = await import('./application-extraction'); | ||
const extractionResult = await extractMessages( | ||
normalizedOptions, | ||
builderName, | ||
context, | ||
localizeToolsModule.MessageExtractor, | ||
extensions, | ||
); | ||
|
||
// Return the builder result if it failed | ||
if (!extractionResult.builderResult.success) { | ||
return extractionResult.builderResult; | ||
} | ||
|
||
// Perform duplicate message checks | ||
const { checkDuplicateMessages } = localizeToolsModule; | ||
|
||
// The filesystem is used to create a relative path for each file | ||
// from the basePath. This relative path is then used in the error message. | ||
const checkFileSystem = { | ||
relative(from: string, to: string): string { | ||
return path.relative(from, to); | ||
}, | ||
}; | ||
const diagnostics = checkDuplicateMessages( | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
checkFileSystem as any, | ||
extractionResult.messages, | ||
'warning', | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
extractionResult.basePath as any, | ||
); | ||
if (diagnostics.messages.length > 0) { | ||
context.logger.warn(diagnostics.formatDiagnostics('')); | ||
} | ||
|
||
// Serialize all extracted messages | ||
const serializer = await createSerializer( | ||
localizeToolsModule, | ||
normalizedOptions.format, | ||
normalizedOptions.i18nOptions.sourceLocale, | ||
extractionResult.basePath, | ||
extractionResult.useLegacyIds, | ||
diagnostics, | ||
); | ||
const content = serializer.serialize(extractionResult.messages); | ||
|
||
// Ensure directory exists | ||
const outputPath = path.dirname(normalizedOptions.outFile); | ||
if (!fs.existsSync(outputPath)) { | ||
fs.mkdirSync(outputPath, { recursive: true }); | ||
} | ||
|
||
// Write translation file | ||
fs.writeFileSync(normalizedOptions.outFile, content); | ||
|
||
if (normalizedOptions.progress) { | ||
context.logger.info(`Extraction Complete. (Messages: ${extractionResult.messages.length})`); | ||
} | ||
|
||
return { success: true, outputPath: normalizedOptions.outFile }; | ||
} | ||
|
||
async function createSerializer( | ||
localizeToolsModule: typeof import('@angular/localize/tools'), | ||
format: Format, | ||
sourceLocale: string, | ||
basePath: string, | ||
useLegacyIds: boolean, | ||
diagnostics: Diagnostics, | ||
) { | ||
const { | ||
XmbTranslationSerializer, | ||
LegacyMessageIdMigrationSerializer, | ||
ArbTranslationSerializer, | ||
Xliff1TranslationSerializer, | ||
Xliff2TranslationSerializer, | ||
SimpleJsonTranslationSerializer, | ||
} = localizeToolsModule; | ||
|
||
switch (format) { | ||
case Format.Xmb: | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return new XmbTranslationSerializer(basePath as any, useLegacyIds); | ||
case Format.Xlf: | ||
case Format.Xlif: | ||
case Format.Xliff: | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return new Xliff1TranslationSerializer(sourceLocale, basePath as any, useLegacyIds, {}); | ||
case Format.Xlf2: | ||
case Format.Xliff2: | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return new Xliff2TranslationSerializer(sourceLocale, basePath as any, useLegacyIds, {}); | ||
case Format.Json: | ||
return new SimpleJsonTranslationSerializer(sourceLocale); | ||
case Format.LegacyMigrate: | ||
return new LegacyMessageIdMigrationSerializer(diagnostics); | ||
case Format.Arb: | ||
const fileSystem = { | ||
relative(from: string, to: string): string { | ||
return path.relative(from, to); | ||
}, | ||
}; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
return new ArbTranslationSerializer(sourceLocale, basePath as any, fileSystem as any); | ||
} | ||
} |
Oops, something went wrong.