refactor(ivy): i18n - run-time translation API to use message-id (#32594)
Previously the translation key used for translations was the `SourceMessage` but it turns out that this is insufficient because "meaning" and "custom-id" metadata affect the translation key. Now run-time translation is keyed off the `MessageId`. PR Close #32594
This commit is contained in:
parent
c7abb7d196
commit
870d189433
|
@ -6,7 +6,8 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {LocalizeFn} from './localize';
|
||||
import {ParsedTranslation, TargetMessage, TranslationKey, parseTranslation, translate as _translate} from './utils/translations';
|
||||
import {MessageId, TargetMessage} from './utils/messages';
|
||||
import {ParsedTranslation, parseTranslation, translate as _translate} from './utils/translations';
|
||||
|
||||
/**
|
||||
* We augment the `$localize` object to also store the translations.
|
||||
|
@ -24,7 +25,7 @@ declare const $localize: LocalizeFn&{TRANSLATIONS: Record<string, ParsedTranslat
|
|||
*
|
||||
* @publicApi
|
||||
*/
|
||||
export function loadTranslations(translations: Record<TranslationKey, TargetMessage>) {
|
||||
export function loadTranslations(translations: Record<MessageId, TargetMessage>) {
|
||||
// Ensure the translate function exists
|
||||
if (!$localize.translate) {
|
||||
$localize.translate = translate;
|
||||
|
|
|
@ -7,13 +7,15 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* The character used to mark the start and end of a placeholder name in a `$localize` tagged
|
||||
* string.
|
||||
* The character used to mark the start and end of a "block" in a `$localize` tagged string.
|
||||
* A block can indicate metadata about the message or specify a name of a placeholder for a
|
||||
* substitution expressions.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```
|
||||
* ```ts
|
||||
* $localize`Hello, ${title}:title:!`;
|
||||
* $localize`:meaning|description@@id:source message text`;
|
||||
* ```
|
||||
*/
|
||||
export const PLACEHOLDER_NAME_MARKER = ':';
|
||||
export const BLOCK_MARKER = ':';
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 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 {PLACEHOLDER_NAME_MARKER} from './constants';
|
||||
import {TranslationKey} from './translations';
|
||||
import {BLOCK_MARKER} from './constants';
|
||||
|
||||
/**
|
||||
* A string containing a translation source message.
|
||||
|
@ -17,6 +16,20 @@ import {TranslationKey} from './translations';
|
|||
*/
|
||||
export type SourceMessage = string;
|
||||
|
||||
/**
|
||||
* A string containing a translation target message.
|
||||
*
|
||||
* I.E. the message that indicates what will be translated to.
|
||||
*
|
||||
* Uses `{$placeholder-name}` to indicate a placeholder.
|
||||
*/
|
||||
export type TargetMessage = string;
|
||||
|
||||
/**
|
||||
* A string that uniquely identifies a message, to be used for matching translations.
|
||||
*/
|
||||
export type MessageId = string;
|
||||
|
||||
/**
|
||||
* Information parsed from a `$localize` tagged string that is used to translate it.
|
||||
*
|
||||
|
@ -31,7 +44,7 @@ export type SourceMessage = string;
|
|||
*
|
||||
* ```
|
||||
* {
|
||||
* translationKey: 'Hello {$title}!',
|
||||
* messageId: '6998194507597730591',
|
||||
* substitutions: { title: 'Jo Bloggs' },
|
||||
* }
|
||||
* ```
|
||||
|
@ -40,7 +53,7 @@ export interface ParsedMessage {
|
|||
/**
|
||||
* The key used to look up the appropriate translation target.
|
||||
*/
|
||||
translationKey: TranslationKey;
|
||||
messageId: MessageId;
|
||||
/**
|
||||
* A mapping of placeholder names to substitution values.
|
||||
*/
|
||||
|
@ -55,7 +68,7 @@ export interface ParsedMessage {
|
|||
export function parseMessage(
|
||||
messageParts: TemplateStringsArray, expressions: readonly any[]): ParsedMessage {
|
||||
const replacements: {[placeholderName: string]: any} = {};
|
||||
let translationKey = messageParts[0];
|
||||
let messageId = messageParts[0];
|
||||
for (let i = 1; i < messageParts.length; i++) {
|
||||
const messagePart = messageParts[i];
|
||||
const expression = expressions[i - 1];
|
||||
|
@ -66,16 +79,16 @@ export function parseMessage(
|
|||
// This should be OK because synthesized nodes only come from the template compiler and they
|
||||
// will always contain placeholder name information.
|
||||
// So there will be no escaped placeholder marker character (`:`) directly after a substitution.
|
||||
if ((messageParts.raw[i] || messagePart).charAt(0) === PLACEHOLDER_NAME_MARKER) {
|
||||
const endOfPlaceholderName = messagePart.indexOf(PLACEHOLDER_NAME_MARKER, 1);
|
||||
if ((messageParts.raw[i] || messagePart).charAt(0) === BLOCK_MARKER) {
|
||||
const endOfPlaceholderName = messagePart.indexOf(BLOCK_MARKER, 1);
|
||||
const placeholderName = messagePart.substring(1, endOfPlaceholderName);
|
||||
translationKey += `{$${placeholderName}}${messagePart.substring(endOfPlaceholderName + 1)}`;
|
||||
messageId += `{$${placeholderName}}${messagePart.substring(endOfPlaceholderName + 1)}`;
|
||||
replacements[placeholderName] = expression;
|
||||
} else {
|
||||
const placeholderName = `ph_${i}`;
|
||||
translationKey += `{$${placeholderName}}${messagePart}`;
|
||||
messageId += `{$${placeholderName}}${messagePart}`;
|
||||
replacements[placeholderName] = expression;
|
||||
}
|
||||
}
|
||||
return {translationKey, substitutions: replacements};
|
||||
return {messageId: messageId, substitutions: replacements};
|
||||
}
|
||||
|
|
|
@ -5,22 +5,8 @@
|
|||
* 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 {PLACEHOLDER_NAME_MARKER} from './constants';
|
||||
import {SourceMessage, parseMessage} from './messages';
|
||||
|
||||
/**
|
||||
* A key used to lookup a `TargetMessage` in a hash map.
|
||||
*/
|
||||
export type TranslationKey = SourceMessage;
|
||||
|
||||
/**
|
||||
* A string containing a translation target message.
|
||||
*
|
||||
* I.E. the message that indicates what will be translated to.
|
||||
*
|
||||
* Uses `{$placeholder-name}` to indicate a placeholder.
|
||||
*/
|
||||
export type TargetMessage = string;
|
||||
import {BLOCK_MARKER} from './constants';
|
||||
import {MessageId, TargetMessage, parseMessage} from './messages';
|
||||
|
||||
/**
|
||||
* A translation message that has been processed to extract the message parts and placeholders.
|
||||
|
@ -33,14 +19,14 @@ export interface ParsedTranslation {
|
|||
/**
|
||||
* The internal structure used by the runtime localization to translate messages.
|
||||
*/
|
||||
export type ParsedTranslations = Record<TranslationKey, ParsedTranslation>;
|
||||
export type ParsedTranslations = Record<MessageId, ParsedTranslation>;
|
||||
|
||||
|
||||
/**
|
||||
* Translate the text of the `$localize` tagged-string (i.e. `messageParts` and
|
||||
* `substitutions`) using the given `translations`.
|
||||
*
|
||||
* The tagged-string is parsed to extract its `translationKey` which is used to find an appropriate
|
||||
* The tagged-string is parsed to extract its `messageId` which is used to find an appropriate
|
||||
* `ParsedTranslation`.
|
||||
*
|
||||
* If one is found then it is used to translate the message into a new set of `messageParts` and
|
||||
|
@ -53,7 +39,7 @@ export function translate(
|
|||
translations: Record<string, ParsedTranslation>, messageParts: TemplateStringsArray,
|
||||
substitutions: readonly any[]): [TemplateStringsArray, readonly any[]] {
|
||||
const message = parseMessage(messageParts, substitutions);
|
||||
const translation = translations[message.translationKey];
|
||||
const translation = translations[message.messageId];
|
||||
if (translation !== undefined) {
|
||||
return [
|
||||
translation.messageParts,
|
||||
|
@ -81,7 +67,7 @@ export function parseTranslation(message: TargetMessage): ParsedTranslation {
|
|||
messageParts.push(`${parts[i + 1]}`);
|
||||
}
|
||||
const rawMessageParts =
|
||||
messageParts.map(part => part.charAt(0) === PLACEHOLDER_NAME_MARKER ? '\\' + part : part);
|
||||
messageParts.map(part => part.charAt(0) === BLOCK_MARKER ? '\\' + part : part);
|
||||
return {messageParts: makeTemplateObject(messageParts, rawMessageParts), placeholderNames};
|
||||
}
|
||||
|
||||
|
|
|
@ -13,24 +13,24 @@ describe('messages utils', () => {
|
|||
it('should compute the translation key', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(['a', ':one:b', ':two:c'], ['a', ':one:b', ':two:c']), [1, 2]);
|
||||
expect(message.translationKey).toEqual('a{$one}b{$two}c');
|
||||
expect(message.messageId).toEqual('a{$one}b{$two}c');
|
||||
});
|
||||
|
||||
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.translationKey).toEqual('a{$ph_1}b{$ph_2}c');
|
||||
expect(message.messageId).toEqual('a{$ph_1}b{$ph_2}c');
|
||||
});
|
||||
|
||||
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.translationKey).toEqual('a{$ph_1}:one:b{$ph_2}:two:c');
|
||||
expect(message.messageId).toEqual('a{$ph_1}:one:b{$ph_2}:two:c');
|
||||
});
|
||||
|
||||
it('should compute the translation key, handling empty raw values', () => {
|
||||
const message =
|
||||
parseMessage(makeTemplateObject(['a', ':one:b', ':two:c'], ['', '', '']), [1, 2]);
|
||||
expect(message.translationKey).toEqual('a{$one}b{$two}c');
|
||||
expect(message.messageId).toEqual('a{$one}b{$two}c');
|
||||
});
|
||||
|
||||
it('should build a map of named placeholders to expressions', () => {
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 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 {ParsedTranslation, TargetMessage, TranslationKey, makeTemplateObject, parseTranslation, translate} from '../../src/utils/translations';
|
||||
import {TargetMessage} from '@angular/localize/src/utils/messages';
|
||||
import {ParsedTranslation, makeTemplateObject, parseTranslation, translate} from '../../src/utils/translations';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('makeTemplateObject', () => {
|
||||
|
@ -146,7 +147,7 @@ describe('utils', () => {
|
|||
return [messageParts, substitutions];
|
||||
}
|
||||
|
||||
function parseTranslations(translations: Record<TranslationKey, TargetMessage>):
|
||||
function parseTranslations(translations: Record<string, TargetMessage>):
|
||||
Record<string, ParsedTranslation> {
|
||||
const parsedTranslations: Record<string, ParsedTranslation> = {};
|
||||
Object.keys(translations).forEach(key => {
|
||||
|
@ -156,7 +157,7 @@ describe('utils', () => {
|
|||
}
|
||||
|
||||
function doTranslate(
|
||||
translations: Record<string, string>,
|
||||
translations: Record<string, TargetMessage>,
|
||||
message: [TemplateStringsArray, any[]]): [TemplateStringsArray, readonly any[]] {
|
||||
return translate(parseTranslations(translations), message[0], message[1]);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue