diff --git a/packages/localize/private.ts b/packages/localize/private.ts index 824a2e69d1..b686e7b5ba 100644 --- a/packages/localize/private.ts +++ b/packages/localize/private.ts @@ -8,4 +8,4 @@ // This file exports all the `utils` as private exports so that other parts of `@angular/localize` // can make use of them. -export {computeMsgId as ɵcomputeMsgId, findEndOfBlock as ɵfindEndOfBlock, isMissingTranslationError as ɵisMissingTranslationError, makeParsedTranslation as ɵmakeParsedTranslation, makeTemplateObject as ɵmakeTemplateObject, MessageId as ɵMessageId, MissingTranslationError as ɵMissingTranslationError, ParsedMessage as ɵParsedMessage, ParsedTranslation as ɵParsedTranslation, ParsedTranslations as ɵParsedTranslations, parseMessage as ɵparseMessage, parseMetadata as ɵparseMetadata, parseTranslation as ɵparseTranslation, SourceMessage as ɵSourceMessage, splitBlock as ɵsplitBlock, TargetMessage as ɵTargetMessage, translate as ɵtranslate} from './src/utils'; +export {computeMsgId as ɵcomputeMsgId, findEndOfBlock as ɵfindEndOfBlock, isMissingTranslationError as ɵisMissingTranslationError, makeParsedTranslation as ɵmakeParsedTranslation, makeTemplateObject as ɵmakeTemplateObject, MessageId as ɵMessageId, MissingTranslationError as ɵMissingTranslationError, ParsedMessage as ɵParsedMessage, ParsedTranslation as ɵParsedTranslation, ParsedTranslations as ɵParsedTranslations, parseMessage as ɵparseMessage, parseMetadata as ɵparseMetadata, parseTranslation as ɵparseTranslation, SourceLocation as ɵSourceLocation, SourceMessage as ɵSourceMessage, splitBlock as ɵsplitBlock, TargetMessage as ɵTargetMessage, translate as ɵtranslate} from './src/utils'; diff --git a/packages/localize/src/tools/src/source_file_utils.ts b/packages/localize/src/tools/src/source_file_utils.ts index cbe0c2ddb8..d906a03468 100644 --- a/packages/localize/src/tools/src/source_file_utils.ts +++ b/packages/localize/src/tools/src/source_file_utils.ts @@ -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}; +} diff --git a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts index 9a9452b41d..1dd97984aa 100644 --- a/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts +++ b/packages/localize/src/tools/test/translate/translation_files/translation_parsers/simple_json_spec.ts @@ -53,6 +53,7 @@ describe('SimpleJsonTranslationParser', () => { }`); expect(result.translations).toEqual({ 'Hello, {$ph_1}!': { + text: 'Bonjour, {$ph_1}!', messageParts: ɵmakeTemplateObject(['Bonjour, ', '!'], ['Bonjour, ', '!']), placeholderNames: ['ph_1'] }, diff --git a/packages/localize/src/utils/src/messages.ts b/packages/localize/src/utils/src/messages.ts index 827c5c242e..21dc2d1902 100644 --- a/packages/localize/src/utils/src/messages.ts +++ b/packages/localize/src/utils/src/messages.ts @@ -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; - /** - * 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}; } } diff --git a/packages/localize/src/utils/src/translations.ts b/packages/localize/src/utils/src/translations.ts index 3376ff609b..67d2fae02b 100644 --- a/packages/localize/src/utils/src/translations.ts +++ b/packages/localize/src/utils/src/translations.ts @@ -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})`; } \ No newline at end of file diff --git a/packages/localize/src/utils/test/messages_spec.ts b/packages/localize/src/utils/test/messages_spec.ts index 40a55ecc88..af29e5178c 100644 --- a/packages/localize/src/utils/test/messages_spec.ts +++ b/packages/localize/src/utils/test/messages_spec.ts @@ -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'}); }); }); });