feat(ivy): i18n - implement compile-time inlining (#32881)
This commit implements a tool that will inline translations and generate a translated copy of a set of application files from a set of translation files. PR Close #32881
This commit is contained in:

committed by
Alex Rickabaugh

parent
d5b87d32b0
commit
2cdb3a079d
@ -10,6 +10,7 @@ ts_library(
|
||||
"//packages:types",
|
||||
"//packages/localize",
|
||||
"//packages/localize/init",
|
||||
"//packages/localize/src/utils",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -8,8 +8,8 @@
|
||||
// Ensure that `$localize` is loaded to the global scope.
|
||||
import '@angular/localize/init';
|
||||
|
||||
import {clearTranslations, loadTranslations} from '../src/translate';
|
||||
import {MessageId, TargetMessage, computeMsgId} from '../src/utils/messages';
|
||||
import {clearTranslations, loadTranslations} from '../localize';
|
||||
import {MessageId, TargetMessage, computeMsgId} from '../src/utils';
|
||||
|
||||
describe('$localize tag with translations', () => {
|
||||
describe('identities', () => {
|
||||
|
@ -1,173 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {findEndOfBlock, parseMessage, parseMetadata, splitBlock} from '../../src/utils/messages';
|
||||
import {makeTemplateObject} from '../../src/utils/translations';
|
||||
|
||||
describe('messages utils', () => {
|
||||
describe('parseMessage', () => {
|
||||
it('should use the message-id parsed from the metadata if available', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(
|
||||
[':@@custom-message-id:a', ':one:b', ':two:c'],
|
||||
[':@@custom-message-id:a', ':one:b', ':two:c']),
|
||||
[1, 2]);
|
||||
expect(message.messageId).toEqual('custom-message-id');
|
||||
});
|
||||
|
||||
it('should compute the translation key if no metadata', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(['a', ':one:b', ':two:c'], ['a', ':one:b', ':two:c']), [1, 2]);
|
||||
expect(message.messageId).toEqual('8865273085679272414');
|
||||
});
|
||||
|
||||
it('should compute the translation key if no id in the metadata', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(
|
||||
[':description:a', ':one:b', ':two:c'], [':description:a', ':one:b', ':two:c']),
|
||||
[1, 2]);
|
||||
expect(message.messageId).toEqual('8865273085679272414');
|
||||
});
|
||||
|
||||
it('should compute a different id if the meaning changes', () => {
|
||||
const message1 = parseMessage(makeTemplateObject(['abc'], ['abc']), []);
|
||||
const message2 = parseMessage(makeTemplateObject([':meaning1|:abc'], [':meaning1|:abc']), []);
|
||||
const message3 = parseMessage(makeTemplateObject([':meaning2|:abc'], [':meaning2|:abc']), []);
|
||||
expect(message1.messageId).not.toEqual(message2.messageId);
|
||||
expect(message2.messageId).not.toEqual(message3.messageId);
|
||||
expect(message3.messageId).not.toEqual(message1.messageId);
|
||||
});
|
||||
|
||||
it('should compute the translation key, inferring placeholder names if not given', () => {
|
||||
const message = parseMessage(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), [1, 2]);
|
||||
expect(message.messageId).toEqual('8107531564991075946');
|
||||
});
|
||||
|
||||
it('should compute the translation key, ignoring escaped placeholder names', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(['a', ':one:b', ':two:c'], ['a', '\\:one:b', '\\:two:c']), [1, 2]);
|
||||
expect(message.messageId).toEqual('2623373088949454037');
|
||||
});
|
||||
|
||||
it('should compute the translation key, handling empty raw values', () => {
|
||||
const message =
|
||||
parseMessage(makeTemplateObject(['a', ':one:b', ':two:c'], ['', '', '']), [1, 2]);
|
||||
expect(message.messageId).toEqual('8865273085679272414');
|
||||
});
|
||||
|
||||
it('should build a map of named placeholders to expressions', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(['a', ':one:b', ':two:c'], ['a', ':one:b', ':two:c']), [1, 2]);
|
||||
expect(message.substitutions).toEqual({one: 1, two: 2});
|
||||
});
|
||||
|
||||
it('should build a map of implied placeholders to expressions', () => {
|
||||
const message = parseMessage(makeTemplateObject(['a', 'b', 'c'], ['a', 'b', 'c']), [1, 2]);
|
||||
expect(message.substitutions).toEqual({PH: 1, PH_1: 2});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('splitBlock()', () => {
|
||||
it('should return just the text if there is no block',
|
||||
() => { expect(splitBlock('abc def', 'abc def')).toEqual({text: 'abc def'}); });
|
||||
|
||||
it('should return just the text and block if there is one', () => {
|
||||
expect(splitBlock(':block info:abc def', ':block info:abc def'))
|
||||
.toEqual({text: 'abc def', block: 'block info'});
|
||||
});
|
||||
|
||||
it('should handle an empty block if there is one', () => {
|
||||
expect(splitBlock('::abc def', '::abc def')).toEqual({text: 'abc def', block: ''});
|
||||
});
|
||||
|
||||
it('should error on an unterminated block', () => {
|
||||
expect(() => splitBlock(':abc def', ':abc def'))
|
||||
.toThrowError('Unterminated $localize metadata block in ":abc def".');
|
||||
});
|
||||
|
||||
it('should handle escaped block markers', () => {
|
||||
expect(splitBlock(':part of the message:abc def', '\\:part of the message:abc def')).toEqual({
|
||||
text: ':part of the message:abc def'
|
||||
});
|
||||
expect(splitBlock(
|
||||
':block with escaped : in it:abc def', ':block with escaped \\: in it:abc def'))
|
||||
.toEqual({text: 'abc def', block: 'block with escaped : in it'});
|
||||
});
|
||||
|
||||
it('should handle the empty raw part', () => {
|
||||
expect(splitBlock(':block info:abc def', '')).toEqual({text: 'abc def', block: 'block info'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findEndOfBlock()', () => {
|
||||
it('should throw error if there is no end of block marker', () => {
|
||||
expect(() => findEndOfBlock(':some text', ':some text'))
|
||||
.toThrowError('Unterminated $localize metadata block in ":some text".');
|
||||
expect(() => findEndOfBlock(':escaped colon:', ':escaped colon\\:'))
|
||||
.toThrowError('Unterminated $localize metadata block in ":escaped colon\\:".');
|
||||
});
|
||||
|
||||
it('should return index of the end of block marker', () => {
|
||||
expect(findEndOfBlock(':block:', ':block:')).toEqual(6);
|
||||
expect(findEndOfBlock(':block::', ':block::')).toEqual(6);
|
||||
expect(findEndOfBlock(':block:some text', ':block:some text')).toEqual(6);
|
||||
expect(findEndOfBlock(':block:some text:more text', ':block:some text:more text')).toEqual(6);
|
||||
expect(findEndOfBlock('::::', ':\\:\\::')).toEqual(3);
|
||||
expect(findEndOfBlock(':block::', ':block\\::')).toEqual(7);
|
||||
expect(findEndOfBlock(':block:more:some text', ':block\\:more:some text')).toEqual(11);
|
||||
expect(findEndOfBlock(':block:more:and-more:some text', ':block\\:more\\:and-more:some text'))
|
||||
.toEqual(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMetadata()', () => {
|
||||
it('should return just the text if there is no block', () => {
|
||||
expect(parseMetadata('abc def', 'abc def'))
|
||||
.toEqual({text: 'abc def', meaning: undefined, description: undefined, id: undefined});
|
||||
});
|
||||
|
||||
it('should extract the metadata if provided', () => {
|
||||
expect(parseMetadata(':description:abc def', ':description:abc def'))
|
||||
.toEqual(
|
||||
{text: 'abc def', description: 'description', meaning: undefined, id: undefined});
|
||||
expect(parseMetadata(':meaning|:abc def', ':meaning|:abc def'))
|
||||
.toEqual({text: 'abc def', description: undefined, meaning: 'meaning', id: undefined});
|
||||
expect(parseMetadata(':@@message-id:abc def', ':@@message-id:abc def'))
|
||||
.toEqual({text: 'abc def', description: undefined, meaning: undefined, id: 'message-id'});
|
||||
expect(parseMetadata(':meaning|description:abc def', ':meaning|description:abc def'))
|
||||
.toEqual(
|
||||
{text: 'abc def', description: 'description', meaning: 'meaning', id: undefined});
|
||||
expect(parseMetadata(':description@@message-id:abc def', ':description@@message-id:abc def'))
|
||||
.toEqual(
|
||||
{text: 'abc def', description: 'description', meaning: undefined, id: 'message-id'});
|
||||
expect(parseMetadata(':meaning|@@message-id:abc def', ':meaning|@@message-id:abc def'))
|
||||
.toEqual({text: 'abc def', description: undefined, meaning: 'meaning', id: 'message-id'});
|
||||
});
|
||||
|
||||
it('should handle an empty block if there is one', () => {
|
||||
expect(parseMetadata('::abc def', '::abc def'))
|
||||
.toEqual({text: 'abc def', meaning: undefined, description: undefined, id: undefined});
|
||||
});
|
||||
|
||||
it('should handle escaped block markers', () => {
|
||||
expect(parseMetadata(':part of the message:abc def', '\\:part of the message:abc def'))
|
||||
.toEqual({
|
||||
text: ':part of the message:abc def',
|
||||
meaning: undefined,
|
||||
description: undefined,
|
||||
id: undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the empty raw part', () => {
|
||||
expect(parseMetadata(':description:abc def', ''))
|
||||
.toEqual(
|
||||
{text: 'abc def', meaning: undefined, description: 'description', id: undefined});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,180 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. 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 {TargetMessage, computeMsgId} from '../../src/utils/messages';
|
||||
import {ParsedTranslation, makeTemplateObject, parseTranslation, translate} from '../../src/utils/translations';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('makeTemplateObject', () => {
|
||||
it('should return an array containing the cooked items', () => {
|
||||
const template =
|
||||
makeTemplateObject(['cooked-a', 'cooked-b', 'cooked-c'], ['raw-a', 'raw-b', 'raw-c']);
|
||||
expect(template).toEqual(['cooked-a', 'cooked-b', 'cooked-c']);
|
||||
});
|
||||
|
||||
it('should return an array that has a raw property containing the raw items', () => {
|
||||
const template =
|
||||
makeTemplateObject(['cooked-a', 'cooked-b', 'cooked-c'], ['raw-a', 'raw-b', 'raw-c']);
|
||||
expect(template.raw).toEqual(['raw-a', 'raw-b', 'raw-c']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTranslation', () => {
|
||||
it('should extract the messageParts as a TemplateStringsArray', () => {
|
||||
const translation = parseTranslation('a{$one}b{$two}c');
|
||||
expect(translation.messageParts).toEqual(['a', 'b', 'c']);
|
||||
expect(translation.messageParts.raw).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should extract the messageParts with leading expression as a TemplateStringsArray', () => {
|
||||
const translation = parseTranslation('{$one}a{$two}b');
|
||||
expect(translation.messageParts).toEqual(['', 'a', 'b']);
|
||||
expect(translation.messageParts.raw).toEqual(['', 'a', 'b']);
|
||||
});
|
||||
|
||||
it('should extract the messageParts with trailing expression as a TemplateStringsArray', () => {
|
||||
const translation = parseTranslation('a{$one}b{$two}');
|
||||
expect(translation.messageParts).toEqual(['a', 'b', '']);
|
||||
expect(translation.messageParts.raw).toEqual(['a', 'b', '']);
|
||||
});
|
||||
|
||||
it('should extract the messageParts with escaped characters as a TemplateStringsArray', () => {
|
||||
const translation = parseTranslation('a{$one}\nb\n{$two}c');
|
||||
expect(translation.messageParts).toEqual(['a', '\nb\n', 'c']);
|
||||
// `messageParts.raw` are not actually escaped as they are not generally used by `$localize`.
|
||||
// See the "escaped placeholders" test below...
|
||||
expect(translation.messageParts.raw).toEqual(['a', '\nb\n', 'c']);
|
||||
});
|
||||
|
||||
it('should extract the messageParts with escaped placeholders as a TemplateStringsArray',
|
||||
() => {
|
||||
const translation = parseTranslation('a{$one}:marker:b{$two}c');
|
||||
expect(translation.messageParts).toEqual(['a', ':marker:b', 'c']);
|
||||
// A `messagePart` that starts with a placeholder marker does get escaped in
|
||||
// `messageParts.raw` as this is used by `$localize`.
|
||||
expect(translation.messageParts.raw).toEqual(['a', '\\:marker:b', 'c']);
|
||||
});
|
||||
|
||||
it('should extract the placeholder names, in order', () => {
|
||||
const translation = parseTranslation('a{$one}b{$two}c');
|
||||
expect(translation.placeholderNames).toEqual(['one', 'two']);
|
||||
});
|
||||
|
||||
it('should handle a translation with no substitutions', () => {
|
||||
const translation = parseTranslation('abc');
|
||||
expect(translation.messageParts).toEqual(['abc']);
|
||||
expect(translation.messageParts.raw).toEqual(['abc']);
|
||||
expect(translation.placeholderNames).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle a translation with only substitutions', () => {
|
||||
const translation = parseTranslation('{$one}{$two}');
|
||||
expect(translation.messageParts).toEqual(['', '', '']);
|
||||
expect(translation.messageParts.raw).toEqual(['', '', '']);
|
||||
expect(translation.placeholderNames).toEqual(['one', 'two']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('translate', () => {
|
||||
it('should throw an error if there is no matching translation', () => {
|
||||
expect(() => doTranslate({}, parts `abc`))
|
||||
.toThrowError('No translation found for "2674653928643152084" ("abc").');
|
||||
expect(() => doTranslate({}, parts `:meaning|:abc`))
|
||||
.toThrowError('No translation found for "1071947593002928768" ("abc" - "meaning").');
|
||||
});
|
||||
|
||||
it('should throw an error if the translation contains placeholders that are not in the message',
|
||||
() => {
|
||||
expect(() => doTranslate({'abc': 'a{$PH}bc'}, parts `abc`))
|
||||
.toThrowError(
|
||||
'No placeholder found with name PH in message "2674653928643152084" ("abc").');
|
||||
});
|
||||
|
||||
it('(with identity translations) should render template literals as-is', () => {
|
||||
const translations = {
|
||||
'abc': 'abc',
|
||||
'abc{$PH}': 'abc{$PH}',
|
||||
'abc{$PH}def': 'abc{$PH}def',
|
||||
'abc{$PH}def{$PH_1}': 'abc{$PH}def{$PH_1}',
|
||||
'Hello, {$PH}!': 'Hello, {$PH}!',
|
||||
};
|
||||
expect(doTranslate(translations, parts `abc`)).toEqual(parts `abc`);
|
||||
expect(doTranslate(translations, parts `abc${1 + 2 + 3}`)).toEqual(parts `abc${1 + 2 + 3}`);
|
||||
expect(doTranslate(translations, parts `abc${1 + 2 + 3}def`))
|
||||
.toEqual(parts `abc${1 + 2 + 3}def`);
|
||||
expect(doTranslate(translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6}`))
|
||||
.toEqual(parts `abc${1 + 2 + 3}def${4 + 5 + 6}`);
|
||||
const getName = () => 'World';
|
||||
expect(doTranslate(translations, parts `Hello, ${getName()}!`))
|
||||
.toEqual(parts `Hello, ${'World'}!`);
|
||||
});
|
||||
|
||||
it('(with upper-casing translations) should render template literals with messages upper-cased',
|
||||
() => {
|
||||
const translations = {
|
||||
'abc': 'ABC',
|
||||
'abc{$PH}': 'ABC{$PH}',
|
||||
'abc{$PH}def': 'ABC{$PH}DEF',
|
||||
'abc{$PH}def{$PH_1}': 'ABC{$PH}DEF{$PH_1}',
|
||||
'Hello, {$PH}!': 'HELLO, {$PH}!',
|
||||
};
|
||||
expect(doTranslate(translations, parts `abc`)).toEqual(parts `ABC`);
|
||||
expect(doTranslate(translations, parts `abc${1 + 2 + 3}`))
|
||||
.toEqual(parts `ABC${1 + 2 + 3}`);
|
||||
expect(doTranslate(translations, parts `abc${1 + 2 + 3}def`))
|
||||
.toEqual(parts `ABC${1 + 2 + 3}DEF`);
|
||||
expect(doTranslate(translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6}`))
|
||||
.toEqual(parts `ABC${1 + 2 + 3}DEF${4 + 5 + 6}`);
|
||||
const getName = () => 'World';
|
||||
expect(doTranslate(translations, parts `Hello, ${getName()}!`))
|
||||
.toEqual(parts `HELLO, ${'World'}!`);
|
||||
});
|
||||
|
||||
it('(with translations to reverse expressions) should render template literals with expressions reversed',
|
||||
() => {
|
||||
const translations = {
|
||||
'abc{$PH}def{$PH_1} - Hello, {$PH_2}!': 'abc{$PH_2}def{$PH_1} - Hello, {$PH}!',
|
||||
};
|
||||
const getName = () => 'World';
|
||||
expect(doTranslate(
|
||||
translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6} - Hello, ${getName()}!`))
|
||||
.toEqual(parts `abc${'World'}def${4 + 5 + 6} - Hello, ${1 + 2 + 3}!`);
|
||||
});
|
||||
|
||||
it('(with translations to remove expressions) should render template literals with expressions removed',
|
||||
() => {
|
||||
const translations = {
|
||||
'abc{$PH}def{$PH_1} - Hello, {$PH_2}!': 'abc{$PH} - Hello, {$PH_2}!',
|
||||
};
|
||||
const getName = () => 'World';
|
||||
expect(doTranslate(
|
||||
translations, parts `abc${1 + 2 + 3}def${4 + 5 + 6} - Hello, ${getName()}!`))
|
||||
.toEqual(parts `abc${1 + 2 + 3} - Hello, ${'World'}!`);
|
||||
});
|
||||
|
||||
function parts(messageParts: TemplateStringsArray, ...substitutions: any[]):
|
||||
[TemplateStringsArray, any[]] {
|
||||
return [messageParts, substitutions];
|
||||
}
|
||||
|
||||
function parseTranslations(translations: Record<string, TargetMessage>):
|
||||
Record<string, ParsedTranslation> {
|
||||
const parsedTranslations: Record<string, ParsedTranslation> = {};
|
||||
Object.keys(translations).forEach(key => {
|
||||
|
||||
parsedTranslations[computeMsgId(key, '')] = parseTranslation(translations[key]);
|
||||
});
|
||||
return parsedTranslations;
|
||||
}
|
||||
|
||||
function doTranslate(
|
||||
translations: Record<string, TargetMessage>,
|
||||
message: [TemplateStringsArray, any[]]): [TemplateStringsArray, readonly any[]] {
|
||||
return translate(parseTranslations(translations), message[0], message[1]);
|
||||
}
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user