Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion packages/compiler/src/i18n/extractor_merger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import * as html from '../ml_parser/ast';
import {DEFAULT_CONTAINER_BLOCKS, InterpolationConfig} from '../ml_parser/defaults';
import {ParseTreeResult} from '../ml_parser/parser';
import {TokenType} from '../ml_parser/tokens';

import * as i18n from './i18n_ast';
import {createI18nMessageFactory, I18nMessageFactory} from './i18n_parser';
Expand Down Expand Up @@ -380,7 +381,9 @@ class _Visitor implements html.Visitor {
private _addMessage(ast: html.Node[], msgMeta?: string): i18n.Message | null {
if (
ast.length == 0 ||
(ast.length == 1 && ast[0] instanceof html.Attribute && !(<html.Attribute>ast[0]).value)
this._isEmptyAttributeValue(ast) ||
this._isPlaceholderOnlyAttributeValue(ast) ||
this._isPlaceholderOnlyMessage(ast)
) {
// Do not create empty messages
return null;
Expand All @@ -392,6 +395,48 @@ class _Visitor implements html.Visitor {
return message;
}

// Check for cases like `<div i18n-title title="">`.
private _isEmptyAttributeValue(ast: html.Node[]): boolean {
if (!isAttrNode(ast)) return false;
const node = ast[0];

return node.value.trim() === '';
}

// Check for cases like `<div i18n-title title="{{ name }}">`.
private _isPlaceholderOnlyAttributeValue(ast: html.Node[]): boolean {
if (!isAttrNode(ast)) return false;
const tokens = ast[0].valueTokens ?? [];

const interpolations = tokens.filter(
(token) => token.type === TokenType.ATTR_VALUE_INTERPOLATION,
);
const plainText = tokens
.filter((token) => token.type === TokenType.ATTR_VALUE_TEXT)
// `AttributeValueTextToken` always has exactly one part per its type.
.map((token) => token.parts[0].trim())
.join('');

// Check if there is a single interpolation and all text around it is empty.
return interpolations.length === 1 && plainText === '';
}

// Check for cases like `<div i18n>{{ name }}</div>`.
private _isPlaceholderOnlyMessage(ast: html.Node[]): boolean {
if (!isTextNode(ast)) return false;
const tokens = ast[0].tokens;

const interpolations = tokens.filter((token) => token.type === TokenType.INTERPOLATION);
const plainText = tokens
.filter((token) => token.type === TokenType.TEXT)
// `TextToken` always has exactly one part per its type.
.map((token) => token.parts[0].trim())
.join('');

// Check if there is a single interpolation and all text around it is empty.
return interpolations.length === 1 && plainText === '';
}

// Translates the given message given the `TranslationBundle`
// This is used for translating elements / blocks - see `_translateAttributes` for attributes
// no-op when called in extraction mode (returns [])
Expand Down Expand Up @@ -593,3 +638,11 @@ function _parseMessageMeta(i18n?: string): {meaning: string; description: string

return {meaning, description, id: id.trim()};
}

function isTextNode(ast: html.Node[]): ast is [html.Text] {
return ast.length === 1 && ast[0] instanceof html.Text;
}

function isAttrNode(ast: html.Node[]): ast is [html.Attribute] {
return ast.length === 1 && ast[0] instanceof html.Attribute;
}
8 changes: 8 additions & 0 deletions packages/compiler/test/i18n/extractor_merger_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ describe('Extractor', () => {
expect(extract('<div i18n="m|d"></div>')).toEqual([]);
});

it('should not create a message for placeholder-only elements', () => {
expect(extract('<div i18n="m|d">{{ foo }}</div>')).toEqual([]);
});

it('should ignore implicit elements in translatable elements', () => {
expect(extract('<div i18n="m|d"><p></p></div>', ['p'])).toEqual([
[['<ph tag name="START_PARAGRAPH"></ph name="CLOSE_PARAGRAPH">'], 'm', 'd', ''],
Expand Down Expand Up @@ -406,6 +410,10 @@ describe('Extractor', () => {
it('should not create a message for empty attributes', () => {
expect(extract('<div i18n-title="m|d" title></div>')).toEqual([]);
});

it('should not create a message for placeholder-only attributes', () => {
expect(extract('<div i18n-title="m|d" title="{{ foo }}"></div>')).toEqual([]);
});
});

describe('implicit elements', () => {
Expand Down
20 changes: 10 additions & 10 deletions packages/compiler/test/i18n/whitespace_sensitivity_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('i18nPreserveWhitespaceForLegacyExtraction', () => {
const initial = extractMessages(
`
<div i18n>Hello, World!</div>
<div i18n>{{ abc }}</div>
<div i18n>Hello {{ abc }}</div>
<div i18n>Start {{ abc }} End</div>
<div i18n>{{ first }} middle {{ end }}</div>
<div i18n><a href="/foo">First Second</a></div>
Expand All @@ -41,7 +41,7 @@ Test case is disabled by omitting the i18n attribute.
Hello, World!
</div>
<div i18n>
{{ abc }}
Hello {{ abc }}
</div>
<div i18n>
Start {{ abc }} End
Expand Down Expand Up @@ -111,7 +111,7 @@ Test case is disabled by omitting the i18n attribute.
Hello, World!
</div>
<div i18n>
{{ abc }}
Hello {{ abc }}
</div>
<div i18n>
Start {{ abc }} End
Expand Down Expand Up @@ -171,7 +171,7 @@ Test case is disabled by omitting the i18n attribute.
Hello, World!
</div>
<div i18n>
{{ abc }}
Hello {{ abc }}
</div>
<div i18n>
Start {{ abc }} End
Expand Down Expand Up @@ -237,7 +237,7 @@ Test case is disabled by omitting the i18n attribute.
exceeds line length.
</div>
<div i18n>
{{ veryLongExpressionWhichMaybeExceedsLineLength | async }}
Hello {{ veryLongExpressionWhichMaybeExceedsLineLength | async }}
</div>
<div i18n>
This is a long {{ abc }} which maybe
Expand Down Expand Up @@ -280,7 +280,7 @@ Test case is disabled by omitting the i18n attribute.
maybe exceeds line length.
</div>
<div i18n>
{{
Hello {{
veryLongExpressionWhichMaybeExceedsLineLength
| async
}}
Expand Down Expand Up @@ -346,7 +346,7 @@ Test case is disabled by omitting the i18n attribute.
const initial = extractMessages(
`
<div i18n> Hello, World! </div>
<div i18n> {{ abc }} </div>
<div i18n> Hello {{ abc }} </div>
<div i18n> Start {{ abc }} End </div>
<div i18n> {{ first }} middle {{ end }} </div>
<div i18n> <a href="/foo">Foo</a> </div>
Expand All @@ -357,7 +357,7 @@ Test case is disabled by omitting the i18n attribute.

i18nPreserveWhitespaceForLegacyExtraction does not support trimming ICU case text.
Test case is disabled by omitting the i18n attribute.
<div>{
<div>Hello {
apples, plural,
=1 { One apple. }
=other { Many apples. }
Expand All @@ -369,7 +369,7 @@ Test case is disabled by omitting the i18n attribute.
const trimmed = extractMessages(
`
<div i18n>Hello, World!</div>
<div i18n>{{ abc }}</div>
<div i18n>Hello {{ abc }}</div>
<div i18n>Start {{ abc }} End</div>
<div i18n>{{ first }} middle {{ end }}</div>
<div i18n><a href="/foo">Foo</a></div>
Expand All @@ -380,7 +380,7 @@ Test case is disabled by omitting the i18n attribute.

i18nPreserveWhitespaceForLegacyExtraction does not support trimming ICU case text.
Test case is disabled by omitting the i18n attribute.
<div>{
<div>Hello {
apples, plural,
=1 {One apple.}
=other {Many apples.}
Expand Down