Skip to content

Commit d16e358

Browse files
igordalan-agius4
igord
authored andcommitted
fix(@angular-devkit/build-angular): respect i18nDuplicateTranslation option when duplicates exist
Running the extract-i18n command will always log duplicate i18n message IDs as warnings, regardless of the value of the i18nDuplicateTranslation option. With this fix, command will log duplication errors and exit with code 1 Fixes angular#23635
1 parent 776366f commit d16e358

File tree

10 files changed

+76
-12
lines changed

10 files changed

+76
-12
lines changed

goldens/public-api/angular/build/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export function executeNgPackagrBuilder(options: NgPackagrBuilderOptions, contex
158158
export type ExtractI18nBuilderOptions = {
159159
buildTarget?: string;
160160
format?: Format;
161+
i18nDuplicateTranslation?: I18NDuplicateTranslation;
161162
outFile?: string;
162163
outputPath?: string;
163164
progress?: boolean;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export type ExecutionTransformer<T> = (input: T) => T | Promise<T>;
191191
export type ExtractI18nBuilderOptions = {
192192
buildTarget?: string;
193193
format?: Format;
194+
i18nDuplicateTranslation?: I18NDuplicateTranslation;
194195
outFile?: string;
195196
outputPath?: string;
196197
progress?: boolean;

packages/angular/build/src/builders/extract-i18n/builder.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,23 @@ export async function execute(
9090
return path.relative(from, to);
9191
},
9292
};
93+
const duplicateTranslationBehavior = normalizedOptions.i18nOptions.duplicateTranslationBehavior;
9394
const diagnostics = checkDuplicateMessages(
9495
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9596
checkFileSystem as any,
9697
extractionResult.messages,
97-
normalizedOptions.i18nOptions.i18nDuplicateTranslation || 'warning',
98+
duplicateTranslationBehavior,
9899
// eslint-disable-next-line @typescript-eslint/no-explicit-any
99100
extractionResult.basePath as any,
100101
);
101-
if (diagnostics.messages.length > 0) {
102-
context.logger.warn(diagnostics.formatDiagnostics(''));
102+
if (diagnostics.messages.length > 0 && duplicateTranslationBehavior !== 'ignore') {
103+
if (duplicateTranslationBehavior === 'error') {
104+
context.logger.error(`Extraction Failed: ${diagnostics.formatDiagnostics('')}`);
105+
106+
return { success: false };
107+
} else {
108+
context.logger.warn(diagnostics.formatDiagnostics(''));
109+
}
103110
}
104111

105112
// Serialize all extracted messages

packages/angular/build/src/builders/extract-i18n/options.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { type DiagnosticHandlingStrategy } from '@angular/localize/tools';
910
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
1011
import { fail } from 'node:assert';
1112
import path from 'node:path';
12-
import { createI18nOptions } from '../../utils/i18n-options';
13+
import { type I18nOptions, createI18nOptions } from '../../utils/i18n-options';
1314
import { Schema as ExtractI18nOptions, Format } from './schema';
1415

1516
export type NormalizedExtractI18nOptions = Awaited<ReturnType<typeof normalizeOptions>>;
@@ -36,7 +37,12 @@ export async function normalizeOptions(
3637
// Target specifier defaults to the current project's build target with no specified configuration
3738
const buildTargetSpecifier = options.buildTarget ?? ':';
3839
const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build');
39-
const i18nOptions = createI18nOptions(projectMetadata, /** inline */ false, context.logger);
40+
const i18nOptions: I18nOptions & {
41+
duplicateTranslationBehavior: DiagnosticHandlingStrategy;
42+
} = {
43+
...createI18nOptions(projectMetadata, /** inline */ false, context.logger),
44+
duplicateTranslationBehavior: options.i18nDuplicateTranslation || 'warning',
45+
};
4046

4147
// Normalize xliff format extensions
4248
let format = options.format;

packages/angular/build/src/builders/extract-i18n/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
"outFile": {
2828
"type": "string",
2929
"description": "Name of the file to output."
30+
},
31+
"i18nDuplicateTranslation": {
32+
"type": "string",
33+
"description": "How to handle duplicate translations.",
34+
"enum": ["error", "warning", "ignore"]
3035
}
3136
},
3237
"additionalProperties": false

packages/angular/build/src/utils/i18n-options.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { DiagnosticHandlingStrategy } from '@angular/localize/tools';
109
import path from 'node:path';
1110
import type { TranslationLoader } from './load-translations';
1211

@@ -29,7 +28,6 @@ export interface I18nOptions {
2928
flatOutput?: boolean;
3029
readonly shouldInline: boolean;
3130
hasDefinedSourceLocale?: boolean;
32-
i18nDuplicateTranslation?: DiagnosticHandlingStrategy;
3331
}
3432

3533
function normalizeTranslationFileOption(

packages/angular_devkit/build_angular/src/builders/extract-i18n/builder.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,23 @@ export async function execute(
110110
return path.relative(from, to);
111111
},
112112
};
113+
const duplicateTranslationBehavior = normalizedOptions.i18nOptions.duplicateTranslationBehavior;
113114
const diagnostics = checkDuplicateMessages(
114115
// eslint-disable-next-line @typescript-eslint/no-explicit-any
115116
checkFileSystem as any,
116117
extractionResult.messages,
117-
'warning',
118+
duplicateTranslationBehavior,
118119
// eslint-disable-next-line @typescript-eslint/no-explicit-any
119120
extractionResult.basePath as any,
120121
);
121-
if (diagnostics.messages.length > 0) {
122-
context.logger.warn(diagnostics.formatDiagnostics(''));
122+
if (diagnostics.messages.length > 0 && duplicateTranslationBehavior !== 'ignore') {
123+
if (duplicateTranslationBehavior === 'error') {
124+
context.logger.error(`Extraction Failed: ${diagnostics.formatDiagnostics('')}`);
125+
126+
return { success: false };
127+
} else {
128+
context.logger.warn(diagnostics.formatDiagnostics(''));
129+
}
123130
}
124131

125132
// Serialize all extracted messages

packages/angular_devkit/build_angular/src/builders/extract-i18n/options.ts

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

9-
import { createI18nOptions } from '@angular/build/private';
9+
import { type I18nOptions, createI18nOptions } from '@angular/build/private';
10+
import { type DiagnosticHandlingStrategy } from '@angular/localize/tools';
1011
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
1112
import { fail } from 'node:assert';
1213
import path from 'node:path';
@@ -36,7 +37,12 @@ export async function normalizeOptions(
3637
// Target specifier defaults to the current project's build target with no specified configuration
3738
const buildTargetSpecifier = options.buildTarget ?? ':';
3839
const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build');
39-
const i18nOptions = createI18nOptions(projectMetadata, /** inline */ false, context.logger);
40+
const i18nOptions: I18nOptions & {
41+
duplicateTranslationBehavior: DiagnosticHandlingStrategy;
42+
} = {
43+
...createI18nOptions(projectMetadata, /** inline */ false, context.logger),
44+
duplicateTranslationBehavior: options.i18nDuplicateTranslation || 'warning',
45+
};
4046

4147
// Normalize xliff format extensions
4248
let format = options.format;

packages/angular_devkit/build_angular/src/builders/extract-i18n/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
"outFile": {
2828
"type": "string",
2929
"description": "Name of the file to output."
30+
},
31+
"i18nDuplicateTranslation": {
32+
"type": "string",
33+
"description": "How to handle duplicate translations.",
34+
"enum": ["error", "warning", "ignore"]
3035
}
3136
},
3237
"additionalProperties": false

packages/angular_devkit/build_angular/src/builders/extract-i18n/works_spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ describe('Extract i18n Target', () => {
147147
expect(fullLog).toContain('Duplicate messages with id');
148148
});
149149

150+
it('issues errors for duplicate message identifiers', async () => {
151+
host.appendToFile(
152+
'src/app/app.component.ts',
153+
'const c = $localize`:@@message-2:message contents`; const d = $localize`:@@message-2:different message contents`;',
154+
);
155+
156+
const logger = new logging.Logger('');
157+
const logs: string[] = [];
158+
logger.subscribe((e) => logs.push(e.message));
159+
160+
const run = await architect.scheduleTarget(
161+
extractI18nTargetSpec,
162+
{
163+
i18nDuplicateTranslation: 'error',
164+
},
165+
{ logger },
166+
);
167+
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: false }));
168+
169+
await run.stop();
170+
171+
expect(host.scopedSync().exists(extractionFile)).toBe(false);
172+
173+
const fullLog = logs.join();
174+
expect(fullLog).toContain('Duplicate messages with id');
175+
expect(fullLog).toContain('Extraction Failed');
176+
});
177+
150178
it('ignores inline styles', async () => {
151179
host.appendToFile('src/app/app.component.html', '<p i18n>i18n test</p>');
152180
host.replaceInFile('src/app/app.component.ts', 'styleUrls', 'styles');

0 commit comments

Comments
 (0)