@angular/localize
Version:
Angular - library for localizing messages
417 lines (406 loc) • 15.5 kB
JavaScript
/**
* @license Angular v10.1.0-next.7
* (c) 2010-2020 Google LLC. https://angular.io/
* License: MIT
*/
import { computeMsgId } from '@angular/compiler';
export { computeMsgId as ɵcomputeMsgId } from '@angular/compiler';
/**
* @license
* Copyright Google LLC 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
*/
/**
* 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`;
* ```
*/
const BLOCK_MARKER = ':';
/**
* The marker used to separate a message's "meaning" from its "description" in a metadata block.
*
* For example:
*
* ```ts
* $localize `:correct|Indicates that the user got the answer correct: Right!`;
* $localize `:movement|Button label for moving to the right: Right!`;
* ```
*/
const MEANING_SEPARATOR = '|';
/**
* The marker used to separate a message's custom "id" from its "description" in a metadata block.
*
* For example:
*
* ```ts
* $localize `:A welcome message on the home page@@myApp-homepage-welcome: Welcome!`;
* ```
*/
const ID_SEPARATOR = '@@';
/**
* The marker used to separate legacy message ids from the rest of a metadata block.
*
* For example:
*
* ```ts
* $localize `:@@custom-id␟2df64767cd895a8fabe3e18b94b5b6b6f9e2e3f0: Welcome!`;
* ```
*
* Note that this character is the "symbol for the unit separator" (␟) not the "unit separator
* character" itself, since that has no visual representation. See https://graphemica.com/%E2%90%9F.
*
* Here is some background for the original "unit separator character":
* https://stackoverflow.com/questions/8695118/whats-the-file-group-record-unit-separator-control-characters-and-its-usage
*/
const LEGACY_ID_INDICATOR = '\u241F';
/**
* @license
* Copyright Google LLC 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
*/
/**
* Parse a `$localize` tagged string into a structure that can be used for translation.
*
* See `ParsedMessage` for an example.
*/
function parseMessage(messageParts, expressions, location) {
const substitutions = {};
const metadata = parseMetadata(messageParts[0], messageParts.raw[0]);
const cleanedMessageParts = [metadata.text];
const placeholderNames = [];
let messageString = metadata.text;
for (let i = 1; i < messageParts.length; i++) {
const { text: messagePart, block: placeholderName = computePlaceholderName(i) } = splitBlock(messageParts[i], messageParts.raw[i]);
messageString += `{$${placeholderName}}${messagePart}`;
if (expressions !== undefined) {
substitutions[placeholderName] = expressions[i - 1];
}
placeholderNames.push(placeholderName);
cleanedMessageParts.push(messagePart);
}
const messageId = metadata.id || computeMsgId(messageString, metadata.meaning || '');
const legacyIds = metadata.legacyIds ? metadata.legacyIds.filter(id => id !== messageId) : [];
return {
id: messageId,
legacyIds,
substitutions,
text: messageString,
meaning: metadata.meaning || '',
description: metadata.description || '',
messageParts: cleanedMessageParts,
placeholderNames,
location,
};
}
/**
* Parse the given message part (`cooked` + `raw`) to extract the message metadata from the text.
*
* If the message part has a metadata block this function will extract the `meaning`,
* `description`, `customId` and `legacyId` (if provided) from the block. These metadata properties
* are serialized in the string delimited by `|`, `@@` and `␟` respectively.
*
* (Note that `␟` is the `LEGACY_ID_INDICATOR` - see `constants.ts`.)
*
* For example:
*
* ```ts
* `:meaning|description@@custom-id`
* `:meaning|@@custom-id`
* `:meaning|description`
* `description@@custom-id`
* `meaning|`
* `description`
* `@@custom-id`
* `:meaning|description@@custom-id␟legacy-id-1␟legacy-id-2`
* ```
*
* @param cooked The cooked version of the message part to parse.
* @param raw The raw version of the message part to parse.
* @returns A object containing any metadata that was parsed from the message part.
*/
function parseMetadata(cooked, raw) {
const { text: messageString, block } = splitBlock(cooked, raw);
if (block === undefined) {
return { text: messageString };
}
else {
const [meaningDescAndId, ...legacyIds] = block.split(LEGACY_ID_INDICATOR);
const [meaningAndDesc, id] = meaningDescAndId.split(ID_SEPARATOR, 2);
let [meaning, description] = meaningAndDesc.split(MEANING_SEPARATOR, 2);
if (description === undefined) {
description = meaning;
meaning = undefined;
}
if (description === '') {
description = undefined;
}
return { text: messageString, meaning, description, id, legacyIds };
}
}
/**
* Split a message part (`cooked` + `raw`) into an optional delimited "block" off the front and the
* rest of the text of the message part.
*
* Blocks appear at the start of message parts. They are delimited by a colon `:` character at the
* start and end of the block.
*
* If the block is in the first message part then it will be metadata about the whole message:
* meaning, description, id. Otherwise it will be metadata about the immediately preceding
* substitution: placeholder name.
*
* Since blocks are optional, it is possible that the content of a message block actually starts
* with a block marker. In this case the marker must be escaped `\:`.
*
* @param cooked The cooked version of the message part to parse.
* @param raw The raw version of the message part to parse.
* @returns An object containing the `text` of the message part and the text of the `block`, if it
* exists.
* @throws an error if the `block` is unterminated
*/
function splitBlock(cooked, raw) {
if (raw.charAt(0) !== BLOCK_MARKER) {
return { text: cooked };
}
else {
const endOfBlock = findEndOfBlock(cooked, raw);
return {
block: cooked.substring(1, endOfBlock),
text: cooked.substring(endOfBlock + 1),
};
}
}
function computePlaceholderName(index) {
return index === 1 ? 'PH' : `PH_${index - 1}`;
}
/**
* Find the end of a "marked block" indicated by the first non-escaped colon.
*
* @param cooked The cooked string (where escaped chars have been processed)
* @param raw The raw string (where escape sequences are still in place)
*
* @returns the index of the end of block marker
* @throws an error if the block is unterminated
*/
function findEndOfBlock(cooked, raw) {
/************************************************************************************************
* This function is repeated in `src/localize/src/localize.ts` and the two should be kept in sync.
* (See that file for more explanation of why.)
************************************************************************************************/
for (let cookedIndex = 1, rawIndex = 1; cookedIndex < cooked.length; cookedIndex++, rawIndex++) {
if (raw[rawIndex] === '\\') {
rawIndex++;
}
else if (cooked[cookedIndex] === BLOCK_MARKER) {
return cookedIndex;
}
}
throw new Error(`Unterminated $localize metadata block in "${raw}".`);
}
/**
* @license
* Copyright Google LLC 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
*/
class MissingTranslationError extends Error {
constructor(parsedMessage) {
super(`No translation found for ${describeMessage(parsedMessage)}.`);
this.parsedMessage = parsedMessage;
this.type = 'MissingTranslationError';
}
}
function isMissingTranslationError(e) {
return e.type === 'MissingTranslationError';
}
/**
* 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 `messageId` which is used to find an appropriate
* `ParsedTranslation`. If this doesn't match and there are legacy ids then try matching a
* translation using those.
*
* If one is found then it is used to translate the message into a new set of `messageParts` and
* `substitutions`.
* The translation may reorder (or remove) substitutions as appropriate.
*
* If there is no translation with a matching message id then an error is thrown.
* If a translation contains a placeholder that is not found in the message being translated then an
* error is thrown.
*/
function translate(translations, messageParts, substitutions) {
const message = parseMessage(messageParts, substitutions);
// Look up the translation using the messageId, and then the legacyId if available.
let translation = translations[message.id];
// If the messageId did not match a translation, try matching the legacy ids instead
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);
}
return [
translation.messageParts, translation.placeholderNames.map(placeholder => {
if (message.substitutions.hasOwnProperty(placeholder)) {
return message.substitutions[placeholder];
}
else {
throw new Error(`There is a placeholder name mismatch with the translation provided for the message ${describeMessage(message)}.\n` +
`The translation contains a placeholder with name ${placeholder}, which does not exist in the message.`);
}
})
];
}
/**
* Parse the `messageParts` and `placeholderNames` out of a target `message`.
*
* Used by `loadTranslations()` to convert target message strings into a structure that is more
* appropriate for doing translation.
*
* @param message the message to be parsed.
*/
function parseTranslation(messageString) {
const parts = messageString.split(/{\$([^}]*)}/);
const messageParts = [parts[0]];
const placeholderNames = [];
for (let i = 1; i < parts.length - 1; i += 2) {
placeholderNames.push(parts[i]);
messageParts.push(`${parts[i + 1]}`);
}
const rawMessageParts = messageParts.map(part => part.charAt(0) === BLOCK_MARKER ? '\\' + part : part);
return {
text: messageString,
messageParts: makeTemplateObject(messageParts, rawMessageParts),
placeholderNames,
};
}
/**
* Create a `ParsedTranslation` from a set of `messageParts` and `placeholderNames`.
*
* @param messageParts The message parts to appear in the ParsedTranslation.
* @param placeholderNames The names of the placeholders to intersperse between the `messageParts`.
*/
function makeParsedTranslation(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
};
}
/**
* Create the specialized array that is passed to tagged-string tag functions.
*
* @param cooked The message parts with their escape codes processed.
* @param raw The message parts with their escaped codes as-is.
*/
function makeTemplateObject(cooked, raw) {
Object.defineProperty(cooked, 'raw', { value: raw });
return cooked;
}
function describeMessage(message) {
const meaningString = message.meaning && ` - "${message.meaning}"`;
const legacy = message.legacyIds && message.legacyIds.length > 0 ?
` [${message.legacyIds.map(l => `"${l}"`).join(', ')}]` :
'';
return `"${message.id}"${legacy} ("${message.text}"${meaningString})`;
}
/**
* @license
* Copyright Google LLC 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
*/
/**
* Load translations for `$localize`.
*
* The given `translations` are processed and added to a lookup based on their `MessageId`.
* A new translation will overwrite a previous translation if it has the same `MessageId`.
*
* * If a message is generated by the Angular compiler from an `i18n` marker in a template, the
* `MessageId` is passed through to the `$localize` call as a custom `MessageId`. The `MessageId`
* will match what is extracted into translation files.
*
* * If the translation is from a call to `$localize` in application code, and no custom `MessageId`
* is provided, then the `MessageId` can be generated by passing the tagged string message-parts
* to the `parseMessage()` function (not currently public API).
*
* @publicApi
*
*/
function loadTranslations(translations) {
// Ensure the translate function exists
if (!$localize.translate) {
$localize.translate = translate$1;
}
if (!$localize.TRANSLATIONS) {
$localize.TRANSLATIONS = {};
}
Object.keys(translations).forEach(key => {
$localize.TRANSLATIONS[key] = parseTranslation(translations[key]);
});
}
/**
* Remove all translations for `$localize`.
*
* @publicApi
*/
function clearTranslations() {
$localize.translate = undefined;
$localize.TRANSLATIONS = {};
}
/**
* Translate the text of the given message, using the loaded translations.
*
* This function may reorder (or remove) substitutions as indicated in the matching translation.
*/
function translate$1(messageParts, substitutions) {
try {
return translate($localize.TRANSLATIONS, messageParts, substitutions);
}
catch (e) {
console.warn(e.message);
return [messageParts, substitutions];
}
}
/**
* @license
* Copyright Google LLC 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
*/
/**
* @license
* Copyright Google LLC 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
*/
/**
* @license
* Copyright Google LLC 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
*/
export { clearTranslations, loadTranslations, MissingTranslationError as ɵMissingTranslationError, findEndOfBlock as ɵfindEndOfBlock, isMissingTranslationError as ɵisMissingTranslationError, makeParsedTranslation as ɵmakeParsedTranslation, makeTemplateObject as ɵmakeTemplateObject, parseMessage as ɵparseMessage, parseMetadata as ɵparseMetadata, parseTranslation as ɵparseTranslation, splitBlock as ɵsplitBlock, translate as ɵtranslate };
//# sourceMappingURL=localize.js.map