Skip to content

Commit 2763577

Browse files
vicbjasonaden
authored andcommitted
fix(compiler): remove i18n markup even if no translations (#17999)
Fixes #11042
1 parent 2108c23 commit 2763577

File tree

11 files changed

+148
-30
lines changed

11 files changed

+148
-30
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 {BasicComp} from '../../src/basic';
10+
import {MainModuleNgFactory} from './module.ngfactory';
11+
12+
MainModuleNgFactory.create(null).instance.appRef.bootstrap(BasicComp);

packages/compiler-cli/integrationtest/test/basic_spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as path from 'path';
1212
import {MultipleComponentsMyComp} from '../src/a/multiple_components';
1313
import {BasicComp} from '../src/basic';
1414
import {createComponent} from './util';
15+
import {createComponentAlt} from './util_alt';
1516

1617
describe('template codegen output', () => {
1718
const outDir = 'src';
@@ -88,5 +89,17 @@ describe('template codegen output', () => {
8889
const pText = pElement.children.map((c: any) => c.data).join('').trim();
8990
expect(pText).toBe('tervetuloa');
9091
});
92+
93+
it('should have removed i18n markup', () => {
94+
const containerElement = createComponent(BasicComp).debugElement.children[0];
95+
expect(containerElement.attributes['title']).toBe('käännä teksti');
96+
expect(containerElement.attributes['i18n-title']).toBeUndefined();
97+
});
98+
99+
it('should have removed i18n markup event without translations', () => {
100+
const containerElement = createComponentAlt(BasicComp).debugElement.children[0];
101+
expect(containerElement.attributes['title']).toBe('translate me');
102+
expect(containerElement.attributes['i18n-title']).toBeUndefined();
103+
});
91104
});
92105
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 {NgModuleRef} from '@angular/core';
10+
import {ComponentFixture} from '@angular/core/testing';
11+
import {platformServerTesting} from '@angular/platform-server/testing';
12+
13+
import {MainModuleNgFactory} from '../alt/src/module.ngfactory';
14+
import {MainModule} from '../src/module';
15+
16+
let mainModuleRef: NgModuleRef<MainModule> = null !;
17+
beforeEach((done) => {
18+
platformServerTesting().bootstrapModuleFactory(MainModuleNgFactory).then((moduleRef: any) => {
19+
mainModuleRef = moduleRef;
20+
done();
21+
});
22+
});
23+
24+
export function createModule(): NgModuleRef<MainModule> {
25+
return mainModuleRef;
26+
}
27+
28+
export function createComponentAlt<C>(comp: {new (...args: any[]): C}): ComponentFixture<C> {
29+
const moduleRef = createModule();
30+
const compRef =
31+
moduleRef.componentFactoryResolver.resolveComponentFactory(comp).create(moduleRef.injector);
32+
return new ComponentFixture(compRef, null, false);
33+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"angularCompilerOptions": {
3+
// For TypeScript 1.8, we have to lay out generated files
4+
// in the same source directory with your code.
5+
"genDir": "./alt",
6+
"debug": true,
7+
"enableSummariesForJit": true,
8+
"alwaysCompileGeneratedCode": true
9+
},
10+
11+
"compilerOptions": {
12+
"target": "es5",
13+
"experimentalDecorators": true,
14+
"noImplicitAny": true,
15+
"strictNullChecks": true,
16+
"skipLibCheck": true,
17+
"moduleResolution": "node",
18+
"rootDir": "",
19+
"declaration": true,
20+
"lib": ["es6", "dom"],
21+
"baseUrl": ".",
22+
// Prevent scanning up the directory tree for types
23+
"typeRoots": ["node_modules/@types"],
24+
"noUnusedLocals": true,
25+
"sourceMap": true
26+
},
27+
28+
"files": [
29+
"src/module",
30+
"alt/src/bootstrap"
31+
]
32+
}

packages/compiler-cli/src/codegen.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ export class CodeGenerator {
9595
`Unknown option for missingTranslation (${cliOptions.missingTranslation}). Use either error, warning or ignore.`);
9696
}
9797
}
98+
if (!transContent) {
99+
missingTranslation = MissingTranslationStrategy.Ignore
100+
}
98101
const {compiler: aotCompiler} = compiler.createAotCompiler(ngCompilerHost, {
99102
translations: transContent,
100103
i18nFormat: cliOptions.i18nFormat,

packages/compiler/src/i18n/extractor.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ import {ParseError} from '../parse_util';
2929
import {PipeResolver} from '../pipe_resolver';
3030
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
3131
import {createOfflineCompileUrlResolver} from '../url_resolver';
32-
33-
import {I18NHtmlParser} from './i18n_html_parser';
3432
import {MessageBundle} from './message_bundle';
3533

3634
/**
@@ -87,7 +85,7 @@ export class Extractor {
8785

8886
static create(host: ExtractorHost, locale: string|null):
8987
{extractor: Extractor, staticReflector: StaticReflector} {
90-
const htmlParser = new I18NHtmlParser(new HtmlParser());
88+
const htmlParser = new HtmlParser();
9189

9290
const urlResolver = createOfflineCompileUrlResolver();
9391
const symbolCache = new StaticSymbolCache();

packages/compiler/src/i18n/i18n_html_parser.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
*/
88

99
import {MissingTranslationStrategy, ɵConsole as Console} from '@angular/core';
10+
1011
import {HtmlParser} from '../ml_parser/html_parser';
1112
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from '../ml_parser/interpolation_config';
1213
import {ParseTreeResult} from '../ml_parser/parser';
14+
15+
import {digest} from './digest';
1316
import {mergeTranslations} from './extractor_merger';
1417
import {Serializer} from './serializers/serializer';
1518
import {Xliff} from './serializers/xliff';
@@ -32,6 +35,9 @@ export class I18NHtmlParser implements HtmlParser {
3235
const serializer = createSerializer(translationsFormat);
3336
this._translationBundle =
3437
TranslationBundle.load(translations, 'i18n', serializer, missingTranslation, console);
38+
} else {
39+
this._translationBundle =
40+
new TranslationBundle({}, null, digest, undefined, missingTranslation, console);
3541
}
3642
}
3743

@@ -41,11 +47,6 @@ export class I18NHtmlParser implements HtmlParser {
4147
const parseResult =
4248
this._htmlParser.parse(source, url, parseExpansionForms, interpolationConfig);
4349

44-
if (!this._translationBundle) {
45-
// Do not enable i18n when no translation bundle is provided
46-
return parseResult;
47-
}
48-
4950
if (parseResult.errors.length) {
5051
return new ParseTreeResult(parseResult.rootNodes, parseResult.errors);
5152
}

packages/compiler/src/jit/compiler_factory.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,13 @@ export const COMPILER_PROVIDERS: Array<any|Type<any>|{[k: string]: any}|any[]> =
5959
},
6060
{
6161
provide: i18n.I18NHtmlParser,
62-
useFactory: (parser: HtmlParser, translations: string, format: string, config: CompilerConfig,
63-
console: Console) =>
64-
new i18n.I18NHtmlParser(
65-
parser, translations, format, config.missingTranslation !, console),
62+
useFactory: (parser: HtmlParser, translations: string | null, format: string,
63+
config: CompilerConfig, console: Console) => {
64+
translations = translations || '';
65+
const missingTranslation =
66+
translations ? config.missingTranslation ! : MissingTranslationStrategy.Ignore;
67+
return new i18n.I18NHtmlParser(parser, translations, format, missingTranslation, console);
68+
},
6669
deps: [
6770
baseHtmlParser,
6871
[new Optional(), new Inject(TRANSLATIONS)],

packages/compiler/test/i18n/extractor_merger_spec.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import {DEFAULT_INTERPOLATION_CONFIG, HtmlParser} from '@angular/compiler';
10+
import {MissingTranslationStrategy} from '@angular/core';
1011

1112
import {digest, serializeNodes as serializeI18nNodes} from '../../src/i18n/digest';
1213
import {extractMessages, mergeTranslations} from '../../src/i18n/extractor_merger';
@@ -465,6 +466,31 @@ export function main() {
465466
.toEqual(`<div title="">some element</div>`);
466467
});
467468
});
469+
470+
describe('no translations', () => {
471+
it('should remove i18n attributes', () => {
472+
const HTML = `<p i18n="m|d">foo</p>`;
473+
expect(fakeNoTranslate(HTML)).toEqual('<p>foo</p>');
474+
});
475+
476+
it('should remove i18n- attributes', () => {
477+
const HTML = `<p i18n-title="m|d" title="foo"></p>`;
478+
expect(fakeNoTranslate(HTML)).toEqual('<p title="foo"></p>');
479+
});
480+
481+
it('should remove i18n comment blocks', () => {
482+
const HTML = `before<!-- i18n --><p>foo</p><span><i>bar</i></span><!-- /i18n -->after`;
483+
expect(fakeNoTranslate(HTML)).toEqual('before<p>foo</p><span><i>bar</i></span>after');
484+
});
485+
486+
it('should remove nested i18n markup', () => {
487+
const HTML =
488+
`<!-- i18n --><span someAttr="ok">foo</span><div>{count, plural, =0 {<p i18n-title title="foo"></p>}}</div><!-- /i18n -->`;
489+
expect(fakeNoTranslate(HTML))
490+
.toEqual(
491+
'<span someAttr="ok">foo</span><div>{count, plural, =0 {<p title="foo"></p>}}</div>');
492+
});
493+
})
468494
});
469495
}
470496

@@ -493,10 +519,22 @@ function fakeTranslate(
493519
i18nMsgMap[id] = [new i18n.Text(`**${text}**`, null !)];
494520
});
495521

496-
const translations = new TranslationBundle(i18nMsgMap, null, digest);
522+
const translationBundle = new TranslationBundle(i18nMsgMap, null, digest);
523+
const output = mergeTranslations(
524+
htmlNodes, translationBundle, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
525+
expect(output.errors).toEqual([]);
526+
527+
return serializeHtmlNodes(output.rootNodes).join('');
528+
}
497529

530+
function fakeNoTranslate(
531+
content: string, implicitTags: string[] = [],
532+
implicitAttrs: {[k: string]: string[]} = {}): string {
533+
const htmlNodes: html.Node[] = parseHtml(content);
534+
const translationBundle = new TranslationBundle(
535+
{}, null, digest, undefined, MissingTranslationStrategy.Ignore, console);
498536
const output = mergeTranslations(
499-
htmlNodes, translations, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
537+
htmlNodes, translationBundle, DEFAULT_INTERPOLATION_CONFIG, implicitTags, implicitAttrs);
500538
expect(output.errors).toEqual([]);
501539

502540
return serializeHtmlNodes(output.rootNodes).join('');

packages/compiler/test/i18n/i18n_html_parser_spec.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,6 @@ import {ParseTreeResult} from '@angular/compiler/src/ml_parser/parser';
1313

1414
export function main() {
1515
describe('I18N html parser', () => {
16-
17-
it('should return the html nodes when no translations are given', () => {
18-
const htmlParser = new HtmlParser();
19-
const i18nHtmlParser = new I18NHtmlParser(htmlParser);
20-
const ptResult = new ParseTreeResult([], []);
21-
22-
spyOn(htmlParser, 'parse').and.returnValue(ptResult);
23-
spyOn(i18nHtmlParser, 'parse').and.callThrough();
24-
25-
expect(i18nHtmlParser.parse('source', 'url')).toBe(ptResult);
26-
27-
expect(htmlParser.parse).toHaveBeenCalledTimes(1);
28-
expect(htmlParser.parse)
29-
.toHaveBeenCalledWith('source', 'url', jasmine.anything(), jasmine.anything());
30-
});
31-
3216
// https://github.com/angular/angular/issues/14322
3317
it('should parse the translations only once', () => {
3418
const transBundle = new TranslationBundle({}, null, () => 'id');

0 commit comments

Comments
 (0)