138 lines
5.4 KiB
TypeScript
Raw Normal View History

/**
* @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
*/
import * as i18n from '../../../i18n/i18n_ast';
import * as o from '../../../output/output_ast';
import {ParseLocation, ParseSourceSpan} from '../../../parse_util';
import {serializeIcuNode} from './icu_serializer';
import {formatI18nPlaceholderName} from './util';
export function createLocalizeStatements(
variable: o.ReadVarExpr, message: i18n.Message,
params: {[name: string]: o.Expression}): o.Statement[] {
const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message);
const sourceSpan = getSourceSpan(message);
const expressions = placeHolders.map(ph => params[ph.text]);
const localizedString =
o.localizedString(message, messageParts, placeHolders, expressions, sourceSpan);
const variableInitialization = variable.set(localizedString);
return [new o.ExpressionStatement(variableInitialization)];
}
/**
* This visitor walks over an i18n tree, capturing literal strings and placeholders.
*
* The result can be used for generating the `$localize` tagged template literals.
*/
class LocalizeSerializerVisitor implements i18n.Visitor {
visitText(text: i18n.Text, context: o.MessagePiece[]): any {
if (context[context.length - 1] instanceof o.LiteralPiece) {
// Two literal pieces in a row means that there was some comment node in-between.
context[context.length - 1].text += text.value;
} else {
context.push(new o.LiteralPiece(text.value, text.sourceSpan));
}
}
visitContainer(container: i18n.Container, context: o.MessagePiece[]): any {
container.children.forEach(child => child.visit(this, context));
}
visitIcu(icu: i18n.Icu, context: o.MessagePiece[]): any {
context.push(new o.LiteralPiece(serializeIcuNode(icu), icu.sourceSpan));
}
visitTagPlaceholder(ph: i18n.TagPlaceholder, context: o.MessagePiece[]): any {
context.push(this.createPlaceholderPiece(ph.startName, ph.startSourceSpan ?? ph.sourceSpan));
if (!ph.isVoid) {
ph.children.forEach(child => child.visit(this, context));
context.push(this.createPlaceholderPiece(ph.closeName, ph.endSourceSpan ?? ph.sourceSpan));
}
}
visitPlaceholder(ph: i18n.Placeholder, context: o.MessagePiece[]): any {
context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
visitIcuPlaceholder(ph: i18n.IcuPlaceholder, context?: any): any {
context.push(this.createPlaceholderPiece(ph.name, ph.sourceSpan));
}
private createPlaceholderPiece(name: string, sourceSpan: ParseSourceSpan): o.PlaceholderPiece {
return new o.PlaceholderPiece(
formatI18nPlaceholderName(name, /* useCamelCase */ false), sourceSpan);
}
}
const serializerVisitor = new LocalizeSerializerVisitor();
/**
* Serialize an i18n message into two arrays: messageParts and placeholders.
*
* These arrays will be used to generate `$localize` tagged template literals.
*
* @param message The message to be serialized.
* @returns an object containing the messageParts and placeholders.
*/
export function serializeI18nMessageForLocalize(message: i18n.Message):
{messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
const pieces: o.MessagePiece[] = [];
message.nodes.forEach(node => node.visit(serializerVisitor, pieces));
return processMessagePieces(pieces);
}
function getSourceSpan(message: i18n.Message): ParseSourceSpan {
const startNode = message.nodes[0];
const endNode = message.nodes[message.nodes.length - 1];
return new ParseSourceSpan(
startNode.sourceSpan.start, endNode.sourceSpan.end, startNode.sourceSpan.fullStart,
startNode.sourceSpan.details);
}
/**
* Convert the list of serialized MessagePieces into two arrays.
*
* One contains the literal string pieces and the other the placeholders that will be replaced by
* expressions when rendering `$localize` tagged template literals.
*
* @param pieces The pieces to process.
* @returns an object containing the messageParts and placeholders.
*/
function processMessagePieces(pieces: o.MessagePiece[]):
{messageParts: o.LiteralPiece[], placeHolders: o.PlaceholderPiece[]} {
const messageParts: o.LiteralPiece[] = [];
const placeHolders: o.PlaceholderPiece[] = [];
if (pieces[0] instanceof o.PlaceholderPiece) {
// The first piece was a placeholder so we need to add an initial empty message part.
messageParts.push(createEmptyMessagePart(pieces[0].sourceSpan.start));
}
for (let i = 0; i < pieces.length; i++) {
const part = pieces[i];
if (part instanceof o.LiteralPiece) {
messageParts.push(part);
} else {
placeHolders.push(part);
if (pieces[i - 1] instanceof o.PlaceholderPiece) {
// There were two placeholders in a row, so we need to add an empty message part.
messageParts.push(createEmptyMessagePart(pieces[i - 1].sourceSpan.end));
}
}
}
if (pieces[pieces.length - 1] instanceof o.PlaceholderPiece) {
// The last piece was a placeholder so we need to add a final empty message part.
messageParts.push(createEmptyMessagePart(pieces[pieces.length - 1].sourceSpan.end));
}
return {messageParts, placeHolders};
fix(ivy): i18n - update the compiler to output `MessageId`s (#32594) Now that the `$localize` translations are `MessageId` based the compiler must render `MessageId`s in its generated `$localize` code. This is because the `MessageId` used by the compiler is computed from information that does not get passed through to the `$localize` tagged string. For example, the generated code for the following template ```html <div id="static" i18n-title="m|d" title="introduction"></div> ``` will contain these localization statements ```ts if (ngI18nClosureMode) { /** * @desc d * @meaning m */ const MSG_EXTERNAL_8809028065680254561$$APP_SPEC_TS_1 = goog.getMsg("introduction"); I18N_1 = MSG_EXTERNAL_8809028065680254561$$APP_SPEC_TS_1; } else { I18N_1 = $localize \`:m|d@@8809028065680254561:introduction\`; } ``` Since `$localize` is not able to accurately regenerate the source-message (and so the `MessageId`) from the generated code, it must rely upon the `MessageId` being provided explicitly in the generated code. The compiler now prepends all localized messages with a "metadata block" containing the id (and the meaning and description if defined). Note that this metadata block will also allow translation file extraction from the compiled code - rather than relying on the legacy ViewEngine extraction code. (This will be implemented post-v9). Although these metadata blocks add to the initial code size, compile-time inlining will completely remove these strings and so will not impact on production bundle size. PR Close #32594
2019-09-13 12:46:05 +01:00
}
function createEmptyMessagePart(location: ParseLocation): o.LiteralPiece {
return new o.LiteralPiece('', new ParseSourceSpan(location, location));
}