refactor(localize): consolidate message/translation metadata (#36745)
PR Close #36745
This commit is contained in:

committed by
Matias Niemelä

parent
519f2baff0
commit
b7acf07a70
@ -5,7 +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 {ɵisMissingTranslationError, ɵmakeTemplateObject, ɵParsedTranslation, ɵtranslate} from '@angular/localize';
|
||||
import {ɵisMissingTranslationError, ɵmakeTemplateObject, ɵParsedTranslation, ɵSourceLocation, ɵtranslate} from '@angular/localize';
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {Diagnostics} from './diagnostics';
|
||||
@ -354,3 +354,16 @@ export function buildCodeFrameError(path: NodePath, e: BabelParseError): string
|
||||
const message = path.hub.file.buildCodeFrameError(e.node, e.message).message;
|
||||
return `${filename}: ${message}`;
|
||||
}
|
||||
|
||||
export function getLocation(path: NodePath): ɵSourceLocation|undefined {
|
||||
const location = path.node.loc;
|
||||
const file = path.hub.file.ops.fileName;
|
||||
|
||||
if (!location || !file) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Note we clone the `start` and `end` objects so that their prototype chains,
|
||||
// from Babel, do not leak into our code.
|
||||
return {start: {...location.start}, end: {...location.end}, file};
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ describe('SimpleJsonTranslationParser', () => {
|
||||
}`);
|
||||
expect(result.translations).toEqual({
|
||||
'Hello, {$ph_1}!': {
|
||||
text: 'Bonjour, {$ph_1}!',
|
||||
messageParts: ɵmakeTemplateObject(['Bonjour, ', '!'], ['Bonjour, ', '!']),
|
||||
placeholderNames: ['ph_1']
|
||||
},
|
||||
|
@ -38,6 +38,52 @@ export type TargetMessage = string;
|
||||
*/
|
||||
export type MessageId = string;
|
||||
|
||||
/**
|
||||
* The location of the message
|
||||
*/
|
||||
export interface SourceLocation {
|
||||
start: {line: number, column: number};
|
||||
end: {line: number, column: number};
|
||||
file: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional information that can be associated with a message.
|
||||
*/
|
||||
export interface MessageMetadata {
|
||||
/**
|
||||
* A human readable rendering of the message
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* A unique identifier for this message.
|
||||
*/
|
||||
id?: MessageId;
|
||||
/**
|
||||
* Legacy message ids, if provided.
|
||||
*
|
||||
* In legacy message formats the message id can only be computed directly from the original
|
||||
* template source.
|
||||
*
|
||||
* Since this information is not available in `$localize` calls, the legacy message ids may be
|
||||
* attached by the compiler to the `$localize` metablock so it can be used if needed at the point
|
||||
* of translation if the translations are encoded using the legacy message id.
|
||||
*/
|
||||
legacyIds?: string[];
|
||||
/**
|
||||
* The meaning of the `message`, used to distinguish identical `messageString`s.
|
||||
*/
|
||||
meaning?: string;
|
||||
/**
|
||||
* The description of the `message`, used to aid translation.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* The location of the message in the source.
|
||||
*/
|
||||
location?: SourceLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information parsed from a `$localize` tagged string that is used to translate it.
|
||||
*
|
||||
@ -52,44 +98,23 @@ export type MessageId = string;
|
||||
*
|
||||
* ```
|
||||
* {
|
||||
* messageId: '6998194507597730591',
|
||||
* id: '6998194507597730591',
|
||||
* substitutions: { title: 'Jo Bloggs' },
|
||||
* messageString: 'Hello {$title}!',
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface ParsedMessage {
|
||||
export interface ParsedMessage extends MessageMetadata {
|
||||
/**
|
||||
* The key used to look up the appropriate translation target.
|
||||
*/
|
||||
messageId: MessageId;
|
||||
/**
|
||||
* Legacy message ids, if provided.
|
||||
*
|
||||
* In legacy message formats the message id can only be computed directly from the original
|
||||
* template source.
|
||||
*
|
||||
* Since this information is not available in `$localize` calls, the legacy message ids may be
|
||||
* attached by the compiler to the `$localize` metablock so it can be used if needed at the point
|
||||
* of translation if the translations are encoded using the legacy message id.
|
||||
* In `ParsedMessage` this is a required field, whereas it is optional in `MessageMetadata`.
|
||||
*/
|
||||
legacyIds: MessageId[];
|
||||
id: MessageId;
|
||||
/**
|
||||
* A mapping of placeholder names to substitution values.
|
||||
*/
|
||||
substitutions: Record<string, any>;
|
||||
/**
|
||||
* A human readable rendering of the message
|
||||
*/
|
||||
messageString: string;
|
||||
/**
|
||||
* The meaning of the `message`, used to distinguish identical `messageString`s.
|
||||
*/
|
||||
meaning: string;
|
||||
/**
|
||||
* The description of the `message`, used to aid translation.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* The static parts of the message.
|
||||
*/
|
||||
@ -106,7 +131,8 @@ export interface ParsedMessage {
|
||||
* See `ParsedMessage` for an example.
|
||||
*/
|
||||
export function parseMessage(
|
||||
messageParts: TemplateStringsArray, expressions?: readonly any[]): ParsedMessage {
|
||||
messageParts: TemplateStringsArray, expressions?: readonly any[],
|
||||
location?: SourceLocation): ParsedMessage {
|
||||
const substitutions: {[placeholderName: string]: any} = {};
|
||||
const metadata = parseMetadata(messageParts[0], messageParts.raw[0]);
|
||||
const cleanedMessageParts: string[] = [metadata.text];
|
||||
@ -123,27 +149,20 @@ export function parseMessage(
|
||||
cleanedMessageParts.push(messagePart);
|
||||
}
|
||||
const messageId = metadata.id || computeMsgId(messageString, metadata.meaning || '');
|
||||
const legacyIds = metadata.legacyIds.filter(id => id !== messageId);
|
||||
const legacyIds = metadata.legacyIds && metadata.legacyIds.filter(id => id !== messageId);
|
||||
return {
|
||||
messageId,
|
||||
id: messageId,
|
||||
legacyIds,
|
||||
substitutions,
|
||||
messageString,
|
||||
text: messageString,
|
||||
meaning: metadata.meaning || '',
|
||||
description: metadata.description || '',
|
||||
messageParts: cleanedMessageParts,
|
||||
placeholderNames,
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
export interface MessageMetadata {
|
||||
text: string;
|
||||
meaning: string|undefined;
|
||||
description: string|undefined;
|
||||
id: string|undefined;
|
||||
legacyIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given message part (`cooked` + `raw`) to extract the message metadata from the text.
|
||||
*
|
||||
@ -171,9 +190,9 @@ export interface MessageMetadata {
|
||||
* @returns A object containing any metadata that was parsed from the message part.
|
||||
*/
|
||||
export function parseMetadata(cooked: string, raw: string): MessageMetadata {
|
||||
const {text, block} = splitBlock(cooked, raw);
|
||||
const {text: messageString, block} = splitBlock(cooked, raw);
|
||||
if (block === undefined) {
|
||||
return {text, meaning: undefined, description: undefined, id: undefined, legacyIds: []};
|
||||
return {text: messageString};
|
||||
} else {
|
||||
const [meaningDescAndId, ...legacyIds] = block.split(LEGACY_ID_INDICATOR);
|
||||
const [meaningAndDesc, id] = meaningDescAndId.split(ID_SEPARATOR, 2);
|
||||
@ -185,7 +204,7 @@ export function parseMetadata(cooked: string, raw: string): MessageMetadata {
|
||||
if (description === '') {
|
||||
description = undefined;
|
||||
}
|
||||
return {text, meaning, description, id, legacyIds};
|
||||
return {text: messageString, meaning, description, id, legacyIds};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,13 +6,13 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {BLOCK_MARKER} from './constants';
|
||||
import {MessageId, ParsedMessage, parseMessage, TargetMessage} from './messages';
|
||||
import {MessageId, MessageMetadata, ParsedMessage, parseMessage, TargetMessage} from './messages';
|
||||
|
||||
|
||||
/**
|
||||
* A translation message that has been processed to extract the message parts and placeholders.
|
||||
*/
|
||||
export interface ParsedTranslation {
|
||||
export interface ParsedTranslation extends MessageMetadata {
|
||||
messageParts: TemplateStringsArray;
|
||||
placeholderNames: string[];
|
||||
}
|
||||
@ -54,10 +54,12 @@ export function translate(
|
||||
substitutions: readonly any[]): [TemplateStringsArray, readonly any[]] {
|
||||
const message = parseMessage(messageParts, substitutions);
|
||||
// Look up the translation using the messageId, and then the legacyId if available.
|
||||
let translation = translations[message.messageId];
|
||||
let translation = translations[message.id];
|
||||
// If the messageId did not match a translation, try matching the legacy ids instead
|
||||
for (let i = 0; i < message.legacyIds.length && translation === undefined; i++) {
|
||||
translation = translations[message.legacyIds[i]];
|
||||
if (message.legacyIds !== undefined) {
|
||||
for (let i = 0; i < message.legacyIds.length && translation === undefined; i++) {
|
||||
translation = translations[message.legacyIds[i]];
|
||||
}
|
||||
}
|
||||
if (translation === undefined) {
|
||||
throw new MissingTranslationError(message);
|
||||
@ -85,8 +87,8 @@ export function translate(
|
||||
*
|
||||
* @param message the message to be parsed.
|
||||
*/
|
||||
export function parseTranslation(message: TargetMessage): ParsedTranslation {
|
||||
const parts = message.split(/{\$([^}]*)}/);
|
||||
export function parseTranslation(messageString: TargetMessage): ParsedTranslation {
|
||||
const parts = messageString.split(/{\$([^}]*)}/);
|
||||
const messageParts = [parts[0]];
|
||||
const placeholderNames: string[] = [];
|
||||
for (let i = 1; i < parts.length - 1; i += 2) {
|
||||
@ -95,7 +97,11 @@ export function parseTranslation(message: TargetMessage): ParsedTranslation {
|
||||
}
|
||||
const rawMessageParts =
|
||||
messageParts.map(part => part.charAt(0) === BLOCK_MARKER ? '\\' + part : part);
|
||||
return {messageParts: makeTemplateObject(messageParts, rawMessageParts), placeholderNames};
|
||||
return {
|
||||
text: messageString,
|
||||
messageParts: makeTemplateObject(messageParts, rawMessageParts),
|
||||
placeholderNames,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +112,15 @@ export function parseTranslation(message: TargetMessage): ParsedTranslation {
|
||||
*/
|
||||
export function makeParsedTranslation(
|
||||
messageParts: string[], placeholderNames: string[] = []): ParsedTranslation {
|
||||
return {messageParts: makeTemplateObject(messageParts, messageParts), placeholderNames};
|
||||
let messageString = messageParts[0];
|
||||
for (let i = 0; i < placeholderNames.length - 1; i++) {
|
||||
messageString += `{$${placeholderNames[i]}}${messageParts[i + 1]}`;
|
||||
}
|
||||
return {
|
||||
text: messageString,
|
||||
messageParts: makeTemplateObject(messageParts, messageParts),
|
||||
placeholderNames
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,7 +137,8 @@ export function makeTemplateObject(cooked: string[], raw: string[]): TemplateStr
|
||||
|
||||
function describeMessage(message: ParsedMessage): string {
|
||||
const meaningString = message.meaning && ` - "${message.meaning}"`;
|
||||
const legacy =
|
||||
message.legacyIds.length > 0 ? ` [${message.legacyIds.map(l => `"${l}"`).join(', ')}]` : '';
|
||||
return `"${message.messageId}"${legacy} ("${message.messageString}"${meaningString})`;
|
||||
const legacy = message.legacyIds && message.legacyIds.length > 0 ?
|
||||
` [${message.legacyIds.map(l => `"${l}"`).join(', ')}]` :
|
||||
'';
|
||||
return `"${message.id}"${legacy} ("${message.text}"${meaningString})`;
|
||||
}
|
@ -15,13 +15,13 @@ describe('messages utils', () => {
|
||||
[':@@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');
|
||||
expect(message.id).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');
|
||||
expect(message.id).toEqual('8865273085679272414');
|
||||
});
|
||||
|
||||
it('should compute the translation key if no id in the metadata', () => {
|
||||
@ -29,16 +29,16 @@ describe('messages utils', () => {
|
||||
makeTemplateObject(
|
||||
[':description:a', ':one:b', ':two:c'], [':description:a', ':one:b', ':two:c']),
|
||||
[1, 2]);
|
||||
expect(message.messageId).toEqual('8865273085679272414');
|
||||
expect(message.id).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);
|
||||
expect(message1.id).not.toEqual(message2.id);
|
||||
expect(message2.id).not.toEqual(message3.id);
|
||||
expect(message3.id).not.toEqual(message1.id);
|
||||
});
|
||||
|
||||
it('should capture legacy ids if available', () => {
|
||||
@ -47,7 +47,7 @@ describe('messages utils', () => {
|
||||
[':␟legacy-1␟legacy-2␟legacy-3:a', ':one:b', ':two:c'],
|
||||
[':␟legacy-1␟legacy-2␟legacy-3:a', ':one:b', ':two:c']),
|
||||
[1, 2]);
|
||||
expect(message1.messageId).toEqual('8865273085679272414');
|
||||
expect(message1.id).toEqual('8865273085679272414');
|
||||
expect(message1.legacyIds).toEqual(['legacy-1', 'legacy-2', 'legacy-3']);
|
||||
|
||||
const message2 = parseMessage(
|
||||
@ -55,7 +55,7 @@ describe('messages utils', () => {
|
||||
[':@@custom-message-id␟legacy-message-id:a', ':one:b', ':two:c'],
|
||||
[':@@custom-message-id␟legacy-message-id:a', ':one:b', ':two:c']),
|
||||
[1, 2]);
|
||||
expect(message2.messageId).toEqual('custom-message-id');
|
||||
expect(message2.id).toEqual('custom-message-id');
|
||||
expect(message2.legacyIds).toEqual(['legacy-message-id']);
|
||||
|
||||
const message3 = parseMessage(
|
||||
@ -63,28 +63,28 @@ describe('messages utils', () => {
|
||||
[':@@custom-message-id:a', ':one:b', ':two:c'],
|
||||
[':@@custom-message-id:a', ':one:b', ':two:c']),
|
||||
[1, 2]);
|
||||
expect(message3.messageId).toEqual('custom-message-id');
|
||||
expect(message3.id).toEqual('custom-message-id');
|
||||
expect(message3.legacyIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('should infer placeholder names if not given', () => {
|
||||
const parts1 = ['a', 'b', 'c'];
|
||||
const message1 = parseMessage(makeTemplateObject(parts1, parts1), [1, 2]);
|
||||
expect(message1.messageId).toEqual('8107531564991075946');
|
||||
expect(message1.id).toEqual('8107531564991075946');
|
||||
|
||||
const parts2 = ['a', ':custom1:b', ':custom2:c'];
|
||||
const message2 = parseMessage(makeTemplateObject(parts2, parts2), [1, 2]);
|
||||
expect(message2.messageId).toEqual('1822117095464505589');
|
||||
expect(message2.id).toEqual('1822117095464505589');
|
||||
|
||||
// Note that the placeholder names are part of the message so affect the message id.
|
||||
expect(message1.messageId).not.toEqual(message2.messageId);
|
||||
expect(message1.messageString).not.toEqual(message2.messageString);
|
||||
expect(message1.id).not.toEqual(message2.id);
|
||||
expect(message1.text).not.toEqual(message2.text);
|
||||
});
|
||||
|
||||
it('should ignore placeholder blocks whose markers have been escaped', () => {
|
||||
const message = parseMessage(
|
||||
makeTemplateObject(['a', ':one:b', ':two:c'], ['a', '\\:one:b', '\\:two:c']), [1, 2]);
|
||||
expect(message.messageId).toEqual('2623373088949454037');
|
||||
expect(message.id).toEqual('2623373088949454037');
|
||||
});
|
||||
|
||||
it('should extract the meaning, description and placeholder names', () => {
|
||||
@ -173,13 +173,7 @@ describe('messages utils', () => {
|
||||
|
||||
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,
|
||||
legacyIds: []
|
||||
});
|
||||
expect(parseMetadata('abc def', 'abc def')).toEqual({text: 'abc def'});
|
||||
});
|
||||
|
||||
it('should extract the metadata if provided', () => {
|
||||
@ -279,13 +273,7 @@ describe('messages utils', () => {
|
||||
|
||||
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,
|
||||
legacyIds: []
|
||||
});
|
||||
.toEqual({text: ':part of the message:abc def'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Reference in New Issue
Block a user