
There is an encoding issue with using delta `Δ`, where the browser will attempt to detect the file encoding if the character set is not explicitly declared on a `<script/>` tag, and Chrome will find the `Δ` character and decide it is window-1252 encoding, which misinterprets the `Δ` character to be some other character that is not a valid JS identifier character So back to the frog eyes we go. ``` __ /ɵɵ\ ( -- ) - I am ineffable. I am forever. _/ \_ / \ / \ == == == ``` PR Close #30546
1644 lines
62 KiB
TypeScript
1644 lines
62 KiB
TypeScript
/**
|
||
* @license
|
||
* Copyright Google Inc. 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 '../util/ng_i18n_closure_mode';
|
||
|
||
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
|
||
import {InertBodyHelper} from '../sanitization/inert_body';
|
||
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
||
import {addAllToArray} from '../util/array_utils';
|
||
import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
|
||
|
||
import {attachPatchData} from './context_discovery';
|
||
import {attachI18nOpCodesDebug} from './debug';
|
||
import {ɵɵelementAttribute, ɵɵload, ɵɵtextBinding} from './instructions/all';
|
||
import {allocExpando, createNodeAtIndex} from './instructions/shared';
|
||
import {LContainer, NATIVE} from './interfaces/container';
|
||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
|
||
import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
|
||
import {RComment, RElement, RText} from './interfaces/renderer';
|
||
import {SanitizerFn} from './interfaces/sanitization';
|
||
import {StylingContext} from './interfaces/styling';
|
||
import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from './interfaces/view';
|
||
import {appendChild, createTextNode, nativeRemoveNode} from './node_manipulation';
|
||
import {getIsParent, getLView, getPreviousOrParentTNode, setIsParent, setPreviousOrParentTNode} from './state';
|
||
import {NO_CHANGE} from './tokens';
|
||
import {renderStringify} from './util/misc_utils';
|
||
import {getNativeByIndex, getNativeByTNode, getTNode, isLContainer} from './util/view_utils';
|
||
|
||
const MARKER = `<EFBFBD>`;
|
||
const ICU_BLOCK_REGEXP = /^\s*(<28>\d+:?\d*<2A>)\s*,\s*(select|plural)\s*,/;
|
||
const SUBTEMPLATE_REGEXP = /<2F>\/?\*(\d+:\d+)<29>/gi;
|
||
const PH_REGEXP = /<2F>(\/?[#*]\d+):?\d*<2A>/gi;
|
||
const BINDING_REGEXP = /<2F>(\d+):?\d*<2A>/gi;
|
||
const ICU_REGEXP = /({\s*<2A>\d+:?\d*<2A>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
|
||
|
||
// i18nPostprocess consts
|
||
const ROOT_TEMPLATE_ID = 0;
|
||
const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]/;
|
||
const PP_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]|(<28>\/?\*\d+:\d+<2B>)/g;
|
||
const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
|
||
const PP_ICUS_REGEXP = /<2F>I18N_EXP_(ICU(_\d+)?)<29>/g;
|
||
const PP_CLOSE_TEMPLATE_REGEXP = /\/\*/;
|
||
const PP_TEMPLATE_ID_REGEXP = /\d+\:(\d+)/;
|
||
|
||
// Parsed placeholder structure used in postprocessing (within `i18nPostprocess` function)
|
||
// Contains the following fields: [templateId, isCloseTemplateTag, placeholder]
|
||
type PostprocessPlaceholder = [number, boolean, string];
|
||
|
||
interface IcuExpression {
|
||
type: IcuType;
|
||
mainBinding: number;
|
||
cases: string[];
|
||
values: (string|IcuExpression)[][];
|
||
}
|
||
|
||
interface IcuCase {
|
||
/**
|
||
* Number of slots to allocate in expando for this case.
|
||
*
|
||
* This is the max number of DOM elements which will be created by this i18n + ICU blocks. When
|
||
* the DOM elements are being created they are stored in the EXPANDO, so that update OpCodes can
|
||
* write into them.
|
||
*/
|
||
vars: number;
|
||
|
||
/**
|
||
* An optional array of child/sub ICUs.
|
||
*/
|
||
childIcus: number[];
|
||
|
||
/**
|
||
* A set of OpCodes to apply in order to build up the DOM render tree for the ICU
|
||
*/
|
||
create: I18nMutateOpCodes;
|
||
|
||
/**
|
||
* A set of OpCodes to apply in order to destroy the DOM render tree for the ICU.
|
||
*/
|
||
remove: I18nMutateOpCodes;
|
||
|
||
/**
|
||
* A set of OpCodes to apply in order to update the DOM render tree for the ICU bindings.
|
||
*/
|
||
update: I18nUpdateOpCodes;
|
||
}
|
||
|
||
/**
|
||
* Breaks pattern into strings and top level {...} blocks.
|
||
* Can be used to break a message into text and ICU expressions, or to break an ICU expression into
|
||
* keys and cases.
|
||
* Original code from closure library, modified for Angular.
|
||
*
|
||
* @param pattern (sub)Pattern to be broken.
|
||
*
|
||
*/
|
||
function extractParts(pattern: string): (string | IcuExpression)[] {
|
||
if (!pattern) {
|
||
return [];
|
||
}
|
||
|
||
let prevPos = 0;
|
||
const braceStack = [];
|
||
const results: (string | IcuExpression)[] = [];
|
||
const braces = /[{}]/g;
|
||
// lastIndex doesn't get set to 0 so we have to.
|
||
braces.lastIndex = 0;
|
||
|
||
let match;
|
||
while (match = braces.exec(pattern)) {
|
||
const pos = match.index;
|
||
if (match[0] == '}') {
|
||
braceStack.pop();
|
||
|
||
if (braceStack.length == 0) {
|
||
// End of the block.
|
||
const block = pattern.substring(prevPos, pos);
|
||
if (ICU_BLOCK_REGEXP.test(block)) {
|
||
results.push(parseICUBlock(block));
|
||
} else if (block) { // Don't push empty strings
|
||
results.push(block);
|
||
}
|
||
|
||
prevPos = pos + 1;
|
||
}
|
||
} else {
|
||
if (braceStack.length == 0) {
|
||
const substring = pattern.substring(prevPos, pos);
|
||
results.push(substring);
|
||
prevPos = pos + 1;
|
||
}
|
||
braceStack.push('{');
|
||
}
|
||
}
|
||
|
||
const substring = pattern.substring(prevPos);
|
||
if (substring != '') {
|
||
results.push(substring);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Parses text containing an ICU expression and produces a JSON object for it.
|
||
* Original code from closure library, modified for Angular.
|
||
*
|
||
* @param pattern Text containing an ICU expression that needs to be parsed.
|
||
*
|
||
*/
|
||
function parseICUBlock(pattern: string): IcuExpression {
|
||
const cases = [];
|
||
const values: (string | IcuExpression)[][] = [];
|
||
let icuType = IcuType.plural;
|
||
let mainBinding = 0;
|
||
pattern = pattern.replace(ICU_BLOCK_REGEXP, function(str: string, binding: string, type: string) {
|
||
if (type === 'select') {
|
||
icuType = IcuType.select;
|
||
} else {
|
||
icuType = IcuType.plural;
|
||
}
|
||
mainBinding = parseInt(binding.substr(1), 10);
|
||
return '';
|
||
});
|
||
|
||
const parts = extractParts(pattern) as string[];
|
||
// Looking for (key block)+ sequence. One of the keys has to be "other".
|
||
for (let pos = 0; pos < parts.length;) {
|
||
let key = parts[pos++].trim();
|
||
if (icuType === IcuType.plural) {
|
||
// Key can be "=x", we just want "x"
|
||
key = key.replace(/\s*(?:=)?(\w+)\s*/, '$1');
|
||
}
|
||
if (key.length) {
|
||
cases.push(key);
|
||
}
|
||
|
||
const blocks = extractParts(parts[pos++]) as string[];
|
||
if (blocks.length) {
|
||
values.push(blocks);
|
||
}
|
||
}
|
||
|
||
assertGreaterThan(cases.indexOf('other'), -1, 'Missing key "other" in ICU statement.');
|
||
// TODO(ocombe): support ICU expressions in attributes, see #21615
|
||
return {type: icuType, mainBinding: mainBinding, cases, values};
|
||
}
|
||
|
||
/**
|
||
* Removes everything inside the sub-templates of a message.
|
||
*/
|
||
function removeInnerTemplateTranslation(message: string): string {
|
||
let match;
|
||
let res = '';
|
||
let index = 0;
|
||
let inTemplate = false;
|
||
let tagMatched;
|
||
|
||
while ((match = SUBTEMPLATE_REGEXP.exec(message)) !== null) {
|
||
if (!inTemplate) {
|
||
res += message.substring(index, match.index + match[0].length);
|
||
tagMatched = match[1];
|
||
inTemplate = true;
|
||
} else {
|
||
if (match[0] === `${MARKER}/*${tagMatched}${MARKER}`) {
|
||
index = match.index;
|
||
inTemplate = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
ngDevMode &&
|
||
assertEqual(
|
||
inTemplate, false,
|
||
`Tag mismatch: unable to find the end of the sub-template in the translation "${message}"`);
|
||
|
||
res += message.substr(index);
|
||
return res;
|
||
}
|
||
|
||
/**
|
||
* Extracts a part of a message and removes the rest.
|
||
*
|
||
* This method is used for extracting a part of the message associated with a template. A translated
|
||
* message can span multiple templates.
|
||
*
|
||
* Example:
|
||
* ```
|
||
* <div i18n>Translate <span *ngIf>me</span>!</div>
|
||
* ```
|
||
*
|
||
* @param message The message to crop
|
||
* @param subTemplateIndex Index of the sub-template to extract. If undefined it returns the
|
||
* external template and removes all sub-templates.
|
||
*/
|
||
export function getTranslationForTemplate(message: string, subTemplateIndex?: number) {
|
||
if (typeof subTemplateIndex !== 'number') {
|
||
// We want the root template message, ignore all sub-templates
|
||
return removeInnerTemplateTranslation(message);
|
||
} else {
|
||
// We want a specific sub-template
|
||
const start =
|
||
message.indexOf(`:${subTemplateIndex}${MARKER}`) + 2 + subTemplateIndex.toString().length;
|
||
const end = message.search(new RegExp(`${MARKER}\\/\\*\\d+:${subTemplateIndex}${MARKER}`));
|
||
return removeInnerTemplateTranslation(message.substring(start, end));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generate the OpCodes to update the bindings of a string.
|
||
*
|
||
* @param str The string containing the bindings.
|
||
* @param destinationNode Index of the destination node which will receive the binding.
|
||
* @param attrName Name of the attribute, if the string belongs to an attribute.
|
||
* @param sanitizeFn Sanitization function used to sanitize the string after update, if necessary.
|
||
*/
|
||
function generateBindingUpdateOpCodes(
|
||
str: string, destinationNode: number, attrName?: string,
|
||
sanitizeFn: SanitizerFn | null = null): I18nUpdateOpCodes {
|
||
const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size
|
||
const textParts = str.split(BINDING_REGEXP);
|
||
let mask = 0;
|
||
|
||
for (let j = 0; j < textParts.length; j++) {
|
||
const textValue = textParts[j];
|
||
|
||
if (j & 1) {
|
||
// Odd indexes are bindings
|
||
const bindingIndex = parseInt(textValue, 10);
|
||
updateOpCodes.push(-1 - bindingIndex);
|
||
mask = mask | toMaskBit(bindingIndex);
|
||
} else if (textValue !== '') {
|
||
// Even indexes are text
|
||
updateOpCodes.push(textValue);
|
||
}
|
||
}
|
||
|
||
updateOpCodes.push(
|
||
destinationNode << I18nUpdateOpCode.SHIFT_REF |
|
||
(attrName ? I18nUpdateOpCode.Attr : I18nUpdateOpCode.Text));
|
||
if (attrName) {
|
||
updateOpCodes.push(attrName, sanitizeFn);
|
||
}
|
||
updateOpCodes[0] = mask;
|
||
updateOpCodes[1] = updateOpCodes.length - 2;
|
||
return updateOpCodes;
|
||
}
|
||
|
||
function getBindingMask(icuExpression: IcuExpression, mask = 0): number {
|
||
mask = mask | toMaskBit(icuExpression.mainBinding);
|
||
let match;
|
||
for (let i = 0; i < icuExpression.values.length; i++) {
|
||
const valueArr = icuExpression.values[i];
|
||
for (let j = 0; j < valueArr.length; j++) {
|
||
const value = valueArr[j];
|
||
if (typeof value === 'string') {
|
||
while (match = BINDING_REGEXP.exec(value)) {
|
||
mask = mask | toMaskBit(parseInt(match[1], 10));
|
||
}
|
||
} else {
|
||
mask = getBindingMask(value as IcuExpression, mask);
|
||
}
|
||
}
|
||
}
|
||
return mask;
|
||
}
|
||
|
||
const i18nIndexStack: number[] = [];
|
||
let i18nIndexStackPointer = -1;
|
||
|
||
/**
|
||
* Convert binding index to mask bit.
|
||
*
|
||
* Each index represents a single bit on the bit-mask. Because bit-mask only has 32 bits, we make
|
||
* the 32nd bit share all masks for all bindings higher than 32. Since it is extremely rare to have
|
||
* more than 32 bindings this will be hit very rarely. The downside of hitting this corner case is
|
||
* that we will execute binding code more often than necessary. (penalty of performance)
|
||
*/
|
||
function toMaskBit(bindingIndex: number): number {
|
||
return 1 << Math.min(bindingIndex, 31);
|
||
}
|
||
|
||
const parentIndexStack: number[] = [];
|
||
|
||
/**
|
||
* Marks a block of text as translatable.
|
||
*
|
||
* The instructions `i18nStart` and `i18nEnd` mark the translation block in the template.
|
||
* The translation `message` is the value which is locale specific. The translation string may
|
||
* contain placeholders which associate inner elements and sub-templates within the translation.
|
||
*
|
||
* The translation `message` placeholders are:
|
||
* - `<60>{index}(:{block})<29>`: *Binding Placeholder*: Marks a location where an expression will be
|
||
* interpolated into. The placeholder `index` points to the expression binding index. An optional
|
||
* `block` that matches the sub-template in which it was declared.
|
||
* - `<60>#{index}(:{block})<29>`/`<60>/#{index}(:{block})<29>`: *Element Placeholder*: Marks the beginning
|
||
* and end of DOM element that were embedded in the original translation block. The placeholder
|
||
* `index` points to the element index in the template instructions set. An optional `block` that
|
||
* matches the sub-template in which it was declared.
|
||
* - `<60>*{index}:{block}<7D>`/`<60>/*{index}:{block}<7D>`: *Sub-template Placeholder*: Sub-templates must be
|
||
* split up and translated separately in each angular template function. The `index` points to the
|
||
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
|
||
*
|
||
* @param index A unique index of the translation in the static block.
|
||
* @param message The translation message.
|
||
* @param subTemplateIndex Optional sub-template index in the `message`.
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?: number): void {
|
||
const tView = getLView()[TVIEW];
|
||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||
i18nIndexStack[++i18nIndexStackPointer] = index;
|
||
if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) {
|
||
i18nStartFirstPass(tView, index, message, subTemplateIndex);
|
||
}
|
||
}
|
||
|
||
// Count for the number of vars that will be allocated for each i18n block.
|
||
// It is global because this is used in multiple functions that include loops and recursive calls.
|
||
// This is reset to 0 when `i18nStartFirstPass` is called.
|
||
let i18nVarsCount: number;
|
||
|
||
/**
|
||
* See `i18nStart` above.
|
||
*/
|
||
function i18nStartFirstPass(
|
||
tView: TView, index: number, message: string, subTemplateIndex?: number) {
|
||
const viewData = getLView();
|
||
const startIndex = tView.blueprint.length - HEADER_OFFSET;
|
||
i18nVarsCount = 0;
|
||
const previousOrParentTNode = getPreviousOrParentTNode();
|
||
const parentTNode = getIsParent() ? getPreviousOrParentTNode() :
|
||
previousOrParentTNode && previousOrParentTNode.parent;
|
||
let parentIndex =
|
||
parentTNode && parentTNode !== viewData[T_HOST] ? parentTNode.index - HEADER_OFFSET : index;
|
||
let parentIndexPointer = 0;
|
||
parentIndexStack[parentIndexPointer] = parentIndex;
|
||
const createOpCodes: I18nMutateOpCodes = [];
|
||
// If the previous node wasn't the direct parent then we have a translation without top level
|
||
// element and we need to keep a reference of the previous element if there is one
|
||
if (index > 0 && previousOrParentTNode !== parentTNode) {
|
||
// Create an OpCode to select the previous TNode
|
||
createOpCodes.push(
|
||
previousOrParentTNode.index << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select);
|
||
}
|
||
const updateOpCodes: I18nUpdateOpCodes = [];
|
||
const icuExpressions: TIcu[] = [];
|
||
|
||
const templateTranslation = getTranslationForTemplate(message, subTemplateIndex);
|
||
const msgParts = templateTranslation.split(PH_REGEXP);
|
||
for (let i = 0; i < msgParts.length; i++) {
|
||
let value = msgParts[i];
|
||
if (i & 1) {
|
||
// Odd indexes are placeholders (elements and sub-templates)
|
||
if (value.charAt(0) === '/') {
|
||
// It is a closing tag
|
||
if (value.charAt(1) === '#') {
|
||
const phIndex = parseInt(value.substr(2), 10);
|
||
parentIndex = parentIndexStack[--parentIndexPointer];
|
||
createOpCodes.push(phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd);
|
||
}
|
||
} else {
|
||
const phIndex = parseInt(value.substr(1), 10);
|
||
// The value represents a placeholder that we move to the designated index
|
||
createOpCodes.push(
|
||
phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
|
||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||
|
||
if (value.charAt(0) === '#') {
|
||
parentIndexStack[++parentIndexPointer] = parentIndex = phIndex;
|
||
}
|
||
}
|
||
} else {
|
||
// Even indexes are text (including bindings & ICU expressions)
|
||
const parts = extractParts(value);
|
||
for (let j = 0; j < parts.length; j++) {
|
||
if (j & 1) {
|
||
// Odd indexes are ICU expressions
|
||
// Create the comment node that will anchor the ICU expression
|
||
const icuNodeIndex = startIndex + i18nVarsCount++;
|
||
createOpCodes.push(
|
||
COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
|
||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||
|
||
// Update codes for the ICU expression
|
||
const icuExpression = parts[j] as IcuExpression;
|
||
const mask = getBindingMask(icuExpression);
|
||
icuStart(icuExpressions, icuExpression, icuNodeIndex, icuNodeIndex);
|
||
// Since this is recursive, the last TIcu that was pushed is the one we want
|
||
const tIcuIndex = icuExpressions.length - 1;
|
||
updateOpCodes.push(
|
||
toMaskBit(icuExpression.mainBinding), // mask of the main binding
|
||
3, // skip 3 opCodes if not changed
|
||
-1 - icuExpression.mainBinding,
|
||
icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch, tIcuIndex,
|
||
mask, // mask of all the bindings of this ICU expression
|
||
2, // skip 2 opCodes if not changed
|
||
icuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate, tIcuIndex);
|
||
} else if (parts[j] !== '') {
|
||
const text = parts[j] as string;
|
||
// Even indexes are text (including bindings)
|
||
const hasBinding = text.match(BINDING_REGEXP);
|
||
// Create text nodes
|
||
const textNodeIndex = startIndex + i18nVarsCount++;
|
||
createOpCodes.push(
|
||
// If there is a binding, the value will be set during update
|
||
hasBinding ? '' : text, textNodeIndex,
|
||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||
|
||
if (hasBinding) {
|
||
addAllToArray(generateBindingUpdateOpCodes(text, textNodeIndex), updateOpCodes);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
allocExpando(viewData, i18nVarsCount);
|
||
|
||
ngDevMode &&
|
||
attachI18nOpCodesDebug(
|
||
createOpCodes, updateOpCodes, icuExpressions.length ? icuExpressions : null, viewData);
|
||
|
||
// NOTE: local var needed to properly assert the type of `TI18n`.
|
||
const tI18n: TI18n = {
|
||
vars: i18nVarsCount,
|
||
create: createOpCodes,
|
||
update: updateOpCodes,
|
||
icus: icuExpressions.length ? icuExpressions : null,
|
||
};
|
||
|
||
tView.data[index + HEADER_OFFSET] = tI18n;
|
||
}
|
||
|
||
function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode | null): TNode {
|
||
ngDevMode && ngDevMode.rendererMoveNode++;
|
||
const nextNode = tNode.next;
|
||
const viewData = getLView();
|
||
if (!previousTNode) {
|
||
previousTNode = parentTNode;
|
||
}
|
||
|
||
// Re-organize node tree to put this node in the correct position.
|
||
if (previousTNode === parentTNode && tNode !== parentTNode.child) {
|
||
tNode.next = parentTNode.child;
|
||
parentTNode.child = tNode;
|
||
} else if (previousTNode !== parentTNode && tNode !== previousTNode.next) {
|
||
tNode.next = previousTNode.next;
|
||
previousTNode.next = tNode;
|
||
} else {
|
||
tNode.next = null;
|
||
}
|
||
|
||
if (parentTNode !== viewData[T_HOST]) {
|
||
tNode.parent = parentTNode as TElementNode;
|
||
}
|
||
|
||
// If tNode was moved around, we might need to fix a broken link.
|
||
let cursor: TNode|null = tNode.next;
|
||
while (cursor) {
|
||
if (cursor.next === tNode) {
|
||
cursor.next = nextNode;
|
||
}
|
||
cursor = cursor.next;
|
||
}
|
||
|
||
appendChild(getNativeByTNode(tNode, viewData), tNode, viewData);
|
||
|
||
const slotValue = viewData[tNode.index];
|
||
if (tNode.type !== TNodeType.Container && isLContainer(slotValue)) {
|
||
// Nodes that inject ViewContainerRef also have a comment node that should be moved
|
||
appendChild(slotValue[NATIVE], tNode, viewData);
|
||
}
|
||
return tNode;
|
||
}
|
||
|
||
/**
|
||
* Handles message string post-processing for internationalization.
|
||
*
|
||
* Handles message string post-processing by transforming it from intermediate
|
||
* format (that might contain some markers that we need to replace) to the final
|
||
* form, consumable by i18nStart instruction. Post processing steps include:
|
||
*
|
||
* 1. Resolve all multi-value cases (like [<5B>*1:1<><31>#2:1<>|<7C>#4:1<>|<7C>5<EFBFBD>])
|
||
* 2. Replace all ICU vars (like "VAR_PLURAL")
|
||
* 3. Replace all ICU references with corresponding values (like <20>ICU_EXP_ICU_1<5F>)
|
||
* in case multiple ICUs have the same placeholder name
|
||
*
|
||
* @param message Raw translation string for post processing
|
||
* @param replacements Set of replacements that should be applied
|
||
*
|
||
* @returns Transformed string that can be consumed by i18nStart instruction
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18nPostprocess(
|
||
message: string, replacements: {[key: string]: (string | string[])} = {}): string {
|
||
/**
|
||
* Step 1: resolve all multi-value placeholders like [<5B>#5<>|<7C>*1:1<><31>#2:1<>|<7C>#4:1<>]
|
||
*
|
||
* Note: due to the way we process nested templates (BFS), multi-value placeholders are typically
|
||
* grouped by templates, for example: [<5B>#5<>|<7C>#6<>|<7C>#1:1<>|<7C>#3:2<>] where <20>#5<> and <20>#6<> belong to root
|
||
* template, <20>#1:1<> belong to nested template with index 1 and <20>#1:2<> - nested template with index
|
||
* 3. However in real templates the order might be different: i.e. <20>#1:1<> and/or <20>#3:2<> may go in
|
||
* front of <20>#6<>. The post processing step restores the right order by keeping track of the
|
||
* template id stack and looks for placeholders that belong to the currently active template.
|
||
*/
|
||
let result: string = message;
|
||
if (PP_MULTI_VALUE_PLACEHOLDERS_REGEXP.test(message)) {
|
||
const matches: {[key: string]: PostprocessPlaceholder[]} = {};
|
||
const templateIdsStack: number[] = [ROOT_TEMPLATE_ID];
|
||
result = result.replace(PP_PLACEHOLDERS_REGEXP, (m: any, phs: string, tmpl: string): string => {
|
||
const content = phs || tmpl;
|
||
if (!matches[content]) {
|
||
const placeholders: PostprocessPlaceholder[] = [];
|
||
content.split('|').forEach((placeholder: string) => {
|
||
const match = placeholder.match(PP_TEMPLATE_ID_REGEXP);
|
||
const templateId = match ? parseInt(match[1], 10) : ROOT_TEMPLATE_ID;
|
||
const isCloseTemplateTag = PP_CLOSE_TEMPLATE_REGEXP.test(placeholder);
|
||
placeholders.push([templateId, isCloseTemplateTag, placeholder]);
|
||
});
|
||
matches[content] = placeholders;
|
||
}
|
||
if (!matches[content].length) {
|
||
throw new Error(`i18n postprocess: unmatched placeholder - ${content}`);
|
||
}
|
||
const currentTemplateId = templateIdsStack[templateIdsStack.length - 1];
|
||
const placeholders = matches[content];
|
||
let idx = 0;
|
||
// find placeholder index that matches current template id
|
||
for (let i = 0; i < placeholders.length; i++) {
|
||
if (placeholders[i][0] === currentTemplateId) {
|
||
idx = i;
|
||
break;
|
||
}
|
||
}
|
||
// update template id stack based on the current tag extracted
|
||
const [templateId, isCloseTemplateTag, placeholder] = placeholders[idx];
|
||
if (isCloseTemplateTag) {
|
||
templateIdsStack.pop();
|
||
} else if (currentTemplateId !== templateId) {
|
||
templateIdsStack.push(templateId);
|
||
}
|
||
// remove processed tag from the list
|
||
placeholders.splice(idx, 1);
|
||
return placeholder;
|
||
});
|
||
|
||
// verify that we injected all values
|
||
const hasUnmatchedValues = Object.keys(matches).some(key => !!matches[key].length);
|
||
if (hasUnmatchedValues) {
|
||
throw new Error(`i18n postprocess: unmatched values - ${JSON.stringify(matches)}`);
|
||
}
|
||
}
|
||
|
||
// return current result if no replacements specified
|
||
if (!Object.keys(replacements).length) {
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Step 2: replace all ICU vars (like "VAR_PLURAL")
|
||
*/
|
||
result = result.replace(PP_ICU_VARS_REGEXP, (match, start, key, _type, _idx, end): string => {
|
||
return replacements.hasOwnProperty(key) ? `${start}${replacements[key]}${end}` : match;
|
||
});
|
||
|
||
/**
|
||
* Step 3: replace all ICU references with corresponding values (like <20>ICU_EXP_ICU_1<5F>) in case
|
||
* multiple ICUs have the same placeholder name
|
||
*/
|
||
result = result.replace(PP_ICUS_REGEXP, (match, key): string => {
|
||
if (replacements.hasOwnProperty(key)) {
|
||
const list = replacements[key] as string[];
|
||
if (!list.length) {
|
||
throw new Error(`i18n postprocess: unmatched ICU - ${match} with key: ${key}`);
|
||
}
|
||
return list.shift() !;
|
||
}
|
||
return match;
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* Translates a translation block marked by `i18nStart` and `i18nEnd`. It inserts the text/ICU nodes
|
||
* into the render tree, moves the placeholder nodes and removes the deleted nodes.
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18nEnd(): void {
|
||
const tView = getLView()[TVIEW];
|
||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||
i18nEndFirstPass(tView);
|
||
}
|
||
|
||
/**
|
||
* See `i18nEnd` above.
|
||
*/
|
||
function i18nEndFirstPass(tView: TView) {
|
||
const viewData = getLView();
|
||
ngDevMode && assertEqual(
|
||
viewData[BINDING_INDEX], viewData[TVIEW].bindingStartIndex,
|
||
'i18nEnd should be called before any binding');
|
||
|
||
const rootIndex = i18nIndexStack[i18nIndexStackPointer--];
|
||
const tI18n = tView.data[rootIndex + HEADER_OFFSET] as TI18n;
|
||
ngDevMode && assertDefined(tI18n, `You should call i18nStart before i18nEnd`);
|
||
|
||
// Find the last node that was added before `i18nEnd`
|
||
let lastCreatedNode = getPreviousOrParentTNode();
|
||
|
||
// Read the instructions to insert/move/remove DOM elements
|
||
const visitedNodes = readCreateOpCodes(rootIndex, tI18n.create, tI18n.icus, viewData);
|
||
|
||
// Remove deleted nodes
|
||
for (let i = rootIndex + 1; i <= lastCreatedNode.index - HEADER_OFFSET; i++) {
|
||
if (visitedNodes.indexOf(i) === -1) {
|
||
removeNode(i, viewData);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates and stores the dynamic TNode, and unhooks it from the tree for now.
|
||
*/
|
||
function createDynamicNodeAtIndex(
|
||
index: number, type: TNodeType, native: RElement | RText | null,
|
||
name: string | null): TElementNode|TIcuContainerNode {
|
||
const previousOrParentTNode = getPreviousOrParentTNode();
|
||
const tNode = createNodeAtIndex(index, type as any, native, name, null);
|
||
|
||
// We are creating a dynamic node, the previous tNode might not be pointing at this node.
|
||
// We will link ourselves into the tree later with `appendI18nNode`.
|
||
if (previousOrParentTNode.next === tNode) {
|
||
previousOrParentTNode.next = null;
|
||
}
|
||
|
||
return tNode;
|
||
}
|
||
|
||
function readCreateOpCodes(
|
||
index: number, createOpCodes: I18nMutateOpCodes, icus: TIcu[] | null,
|
||
viewData: LView): number[] {
|
||
const renderer = getLView()[RENDERER];
|
||
let currentTNode: TNode|null = null;
|
||
let previousTNode: TNode|null = null;
|
||
const visitedNodes: number[] = [];
|
||
for (let i = 0; i < createOpCodes.length; i++) {
|
||
const opCode = createOpCodes[i];
|
||
if (typeof opCode == 'string') {
|
||
const textRNode = createTextNode(opCode, renderer);
|
||
const textNodeIndex = createOpCodes[++i] as number;
|
||
ngDevMode && ngDevMode.rendererCreateTextNode++;
|
||
previousTNode = currentTNode;
|
||
currentTNode = createDynamicNodeAtIndex(textNodeIndex, TNodeType.Element, textRNode, null);
|
||
visitedNodes.push(textNodeIndex);
|
||
setIsParent(false);
|
||
} else if (typeof opCode == 'number') {
|
||
switch (opCode & I18nMutateOpCode.MASK_OPCODE) {
|
||
case I18nMutateOpCode.AppendChild:
|
||
const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
|
||
let destinationTNode: TNode;
|
||
if (destinationNodeIndex === index) {
|
||
// If the destination node is `i18nStart`, we don't have a
|
||
// top-level node and we should use the host node instead
|
||
destinationTNode = viewData[T_HOST] !;
|
||
} else {
|
||
destinationTNode = getTNode(destinationNodeIndex, viewData);
|
||
}
|
||
ngDevMode &&
|
||
assertDefined(
|
||
currentTNode !,
|
||
`You need to create or select a node before you can insert it into the DOM`);
|
||
previousTNode = appendI18nNode(currentTNode !, destinationTNode, previousTNode);
|
||
break;
|
||
case I18nMutateOpCode.Select:
|
||
const nodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
|
||
visitedNodes.push(nodeIndex);
|
||
previousTNode = currentTNode;
|
||
currentTNode = getTNode(nodeIndex, viewData);
|
||
if (currentTNode) {
|
||
setPreviousOrParentTNode(currentTNode);
|
||
if (currentTNode.type === TNodeType.Element) {
|
||
setIsParent(true);
|
||
}
|
||
}
|
||
break;
|
||
case I18nMutateOpCode.ElementEnd:
|
||
const elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
|
||
previousTNode = currentTNode = getTNode(elementIndex, viewData);
|
||
setPreviousOrParentTNode(currentTNode);
|
||
setIsParent(false);
|
||
break;
|
||
case I18nMutateOpCode.Attr:
|
||
const elementNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
|
||
const attrName = createOpCodes[++i] as string;
|
||
const attrValue = createOpCodes[++i] as string;
|
||
ɵɵelementAttribute(elementNodeIndex, attrName, attrValue);
|
||
break;
|
||
default:
|
||
throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
|
||
}
|
||
} else {
|
||
switch (opCode) {
|
||
case COMMENT_MARKER:
|
||
const commentValue = createOpCodes[++i] as string;
|
||
const commentNodeIndex = createOpCodes[++i] as number;
|
||
ngDevMode && assertEqual(
|
||
typeof commentValue, 'string',
|
||
`Expected "${commentValue}" to be a comment node value`);
|
||
const commentRNode = renderer.createComment(commentValue);
|
||
ngDevMode && ngDevMode.rendererCreateComment++;
|
||
previousTNode = currentTNode;
|
||
currentTNode = createDynamicNodeAtIndex(
|
||
commentNodeIndex, TNodeType.IcuContainer, commentRNode, null);
|
||
visitedNodes.push(commentNodeIndex);
|
||
attachPatchData(commentRNode, viewData);
|
||
(currentTNode as TIcuContainerNode).activeCaseIndex = null;
|
||
// We will add the case nodes later, during the update phase
|
||
setIsParent(false);
|
||
break;
|
||
case ELEMENT_MARKER:
|
||
const tagNameValue = createOpCodes[++i] as string;
|
||
const elementNodeIndex = createOpCodes[++i] as number;
|
||
ngDevMode && assertEqual(
|
||
typeof tagNameValue, 'string',
|
||
`Expected "${tagNameValue}" to be an element node tag name`);
|
||
const elementRNode = renderer.createElement(tagNameValue);
|
||
ngDevMode && ngDevMode.rendererCreateElement++;
|
||
previousTNode = currentTNode;
|
||
currentTNode = createDynamicNodeAtIndex(
|
||
elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue);
|
||
visitedNodes.push(elementNodeIndex);
|
||
break;
|
||
default:
|
||
throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
|
||
}
|
||
}
|
||
}
|
||
|
||
setIsParent(false);
|
||
|
||
return visitedNodes;
|
||
}
|
||
|
||
function readUpdateOpCodes(
|
||
updateOpCodes: I18nUpdateOpCodes, icus: TIcu[] | null, bindingsStartIndex: number,
|
||
changeMask: number, viewData: LView, bypassCheckBit = false) {
|
||
let caseCreated = false;
|
||
for (let i = 0; i < updateOpCodes.length; i++) {
|
||
// bit code to check if we should apply the next update
|
||
const checkBit = updateOpCodes[i] as number;
|
||
// Number of opCodes to skip until next set of update codes
|
||
const skipCodes = updateOpCodes[++i] as number;
|
||
if (bypassCheckBit || (checkBit & changeMask)) {
|
||
// The value has been updated since last checked
|
||
let value = '';
|
||
for (let j = i + 1; j <= (i + skipCodes); j++) {
|
||
const opCode = updateOpCodes[j];
|
||
if (typeof opCode == 'string') {
|
||
value += opCode;
|
||
} else if (typeof opCode == 'number') {
|
||
if (opCode < 0) {
|
||
// It's a binding index whose value is negative
|
||
value += renderStringify(viewData[bindingsStartIndex - opCode]);
|
||
} else {
|
||
const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
|
||
let tIcuIndex: number;
|
||
let tIcu: TIcu;
|
||
let icuTNode: TIcuContainerNode;
|
||
switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
|
||
case I18nUpdateOpCode.Attr:
|
||
const attrName = updateOpCodes[++j] as string;
|
||
const sanitizeFn = updateOpCodes[++j] as SanitizerFn | null;
|
||
ɵɵelementAttribute(nodeIndex, attrName, value, sanitizeFn);
|
||
break;
|
||
case I18nUpdateOpCode.Text:
|
||
ɵɵtextBinding(nodeIndex, value);
|
||
break;
|
||
case I18nUpdateOpCode.IcuSwitch:
|
||
tIcuIndex = updateOpCodes[++j] as number;
|
||
tIcu = icus ![tIcuIndex];
|
||
icuTNode = getTNode(nodeIndex, viewData) as TIcuContainerNode;
|
||
// If there is an active case, delete the old nodes
|
||
if (icuTNode.activeCaseIndex !== null) {
|
||
const removeCodes = tIcu.remove[icuTNode.activeCaseIndex];
|
||
for (let k = 0; k < removeCodes.length; k++) {
|
||
const removeOpCode = removeCodes[k] as number;
|
||
switch (removeOpCode & I18nMutateOpCode.MASK_OPCODE) {
|
||
case I18nMutateOpCode.Remove:
|
||
const nodeIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
|
||
removeNode(nodeIndex, viewData);
|
||
break;
|
||
case I18nMutateOpCode.RemoveNestedIcu:
|
||
const nestedIcuNodeIndex =
|
||
removeCodes[k + 1] as number >>> I18nMutateOpCode.SHIFT_REF;
|
||
const nestedIcuTNode =
|
||
getTNode(nestedIcuNodeIndex, viewData) as TIcuContainerNode;
|
||
const activeIndex = nestedIcuTNode.activeCaseIndex;
|
||
if (activeIndex !== null) {
|
||
const nestedIcuTIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
|
||
const nestedTIcu = icus ![nestedIcuTIndex];
|
||
addAllToArray(nestedTIcu.remove[activeIndex], removeCodes);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update the active caseIndex
|
||
const caseIndex = getCaseIndex(tIcu, value);
|
||
icuTNode.activeCaseIndex = caseIndex !== -1 ? caseIndex : null;
|
||
|
||
// Add the nodes for the new case
|
||
readCreateOpCodes(-1, tIcu.create[caseIndex], icus, viewData);
|
||
caseCreated = true;
|
||
break;
|
||
case I18nUpdateOpCode.IcuUpdate:
|
||
tIcuIndex = updateOpCodes[++j] as number;
|
||
tIcu = icus ![tIcuIndex];
|
||
icuTNode = getTNode(nodeIndex, viewData) as TIcuContainerNode;
|
||
readUpdateOpCodes(
|
||
tIcu.update[icuTNode.activeCaseIndex !], icus, bindingsStartIndex, changeMask,
|
||
viewData, caseCreated);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
i += skipCodes;
|
||
}
|
||
}
|
||
|
||
function removeNode(index: number, viewData: LView) {
|
||
const removedPhTNode = getTNode(index, viewData);
|
||
const removedPhRNode = getNativeByIndex(index, viewData);
|
||
if (removedPhRNode) {
|
||
nativeRemoveNode(viewData[RENDERER], removedPhRNode);
|
||
}
|
||
|
||
const slotValue = ɵɵload(index) as RElement | RComment | LContainer | StylingContext;
|
||
if (isLContainer(slotValue)) {
|
||
const lContainer = slotValue as LContainer;
|
||
if (removedPhTNode.type !== TNodeType.Container) {
|
||
nativeRemoveNode(viewData[RENDERER], lContainer[NATIVE]);
|
||
}
|
||
}
|
||
|
||
ngDevMode && ngDevMode.rendererRemoveNode++;
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Use this instruction to create a translation block that doesn't contain any placeholder.
|
||
* It calls both {@link i18nStart} and {@link i18nEnd} in one instruction.
|
||
*
|
||
* The translation `message` is the value which is locale specific. The translation string may
|
||
* contain placeholders which associate inner elements and sub-templates within the translation.
|
||
*
|
||
* The translation `message` placeholders are:
|
||
* - `<60>{index}(:{block})<29>`: *Binding Placeholder*: Marks a location where an expression will be
|
||
* interpolated into. The placeholder `index` points to the expression binding index. An optional
|
||
* `block` that matches the sub-template in which it was declared.
|
||
* - `<60>#{index}(:{block})<29>`/`<60>/#{index}(:{block})<29>`: *Element Placeholder*: Marks the beginning
|
||
* and end of DOM element that were embedded in the original translation block. The placeholder
|
||
* `index` points to the element index in the template instructions set. An optional `block` that
|
||
* matches the sub-template in which it was declared.
|
||
* - `<60>*{index}:{block}<7D>`/`<60>/*{index}:{block}<7D>`: *Sub-template Placeholder*: Sub-templates must be
|
||
* split up and translated separately in each angular template function. The `index` points to the
|
||
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
|
||
*
|
||
* @param index A unique index of the translation in the static block.
|
||
* @param message The translation message.
|
||
* @param subTemplateIndex Optional sub-template index in the `message`.
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18n(index: number, message: string, subTemplateIndex?: number): void {
|
||
ɵɵi18nStart(index, message, subTemplateIndex);
|
||
ɵɵi18nEnd();
|
||
}
|
||
|
||
/**
|
||
* Marks a list of attributes as translatable.
|
||
*
|
||
* @param index A unique index in the static block
|
||
* @param values
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18nAttributes(index: number, values: string[]): void {
|
||
const tView = getLView()[TVIEW];
|
||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||
if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) {
|
||
i18nAttributesFirstPass(tView, index, values);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* See `i18nAttributes` above.
|
||
*/
|
||
function i18nAttributesFirstPass(tView: TView, index: number, values: string[]) {
|
||
const previousElement = getPreviousOrParentTNode();
|
||
const previousElementIndex = previousElement.index - HEADER_OFFSET;
|
||
const updateOpCodes: I18nUpdateOpCodes = [];
|
||
for (let i = 0; i < values.length; i += 2) {
|
||
const attrName = values[i];
|
||
const message = values[i + 1];
|
||
const parts = message.split(ICU_REGEXP);
|
||
for (let j = 0; j < parts.length; j++) {
|
||
const value = parts[j];
|
||
|
||
if (j & 1) {
|
||
// Odd indexes are ICU expressions
|
||
// TODO(ocombe): support ICU expressions in attributes
|
||
} else if (value !== '') {
|
||
// Even indexes are text (including bindings)
|
||
const hasBinding = !!value.match(BINDING_REGEXP);
|
||
if (hasBinding) {
|
||
addAllToArray(
|
||
generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes);
|
||
} else {
|
||
ɵɵelementAttribute(previousElementIndex, attrName, value);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
tView.data[index + HEADER_OFFSET] = updateOpCodes;
|
||
}
|
||
|
||
let changeMask = 0b0;
|
||
let shiftsCounter = 0;
|
||
|
||
/**
|
||
* Stores the values of the bindings during each update cycle in order to determine if we need to
|
||
* update the translated nodes.
|
||
*
|
||
* @param expression The binding's new value or NO_CHANGE
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18nExp<T>(expression: T | NO_CHANGE): void {
|
||
if (expression !== NO_CHANGE) {
|
||
changeMask = changeMask | (1 << shiftsCounter);
|
||
}
|
||
shiftsCounter++;
|
||
}
|
||
|
||
/**
|
||
* Updates a translation block or an i18n attribute when the bindings have changed.
|
||
*
|
||
* @param index Index of either {@link i18nStart} (translation block) or {@link i18nAttributes}
|
||
* (i18n attribute) on which it should update the content.
|
||
*
|
||
* @codeGenApi
|
||
*/
|
||
export function ɵɵi18nApply(index: number) {
|
||
if (shiftsCounter) {
|
||
const lView = getLView();
|
||
const tView = lView[TVIEW];
|
||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||
const tI18n = tView.data[index + HEADER_OFFSET];
|
||
let updateOpCodes: I18nUpdateOpCodes;
|
||
let icus: TIcu[]|null = null;
|
||
if (Array.isArray(tI18n)) {
|
||
updateOpCodes = tI18n as I18nUpdateOpCodes;
|
||
} else {
|
||
updateOpCodes = (tI18n as TI18n).update;
|
||
icus = (tI18n as TI18n).icus;
|
||
}
|
||
const bindingsStartIndex = lView[BINDING_INDEX] - shiftsCounter - 1;
|
||
readUpdateOpCodes(updateOpCodes, icus, bindingsStartIndex, changeMask, lView);
|
||
|
||
// Reset changeMask & maskBit to default for the next update cycle
|
||
changeMask = 0b0;
|
||
shiftsCounter = 0;
|
||
}
|
||
}
|
||
|
||
enum Plural {
|
||
Zero = 0,
|
||
One = 1,
|
||
Two = 2,
|
||
Few = 3,
|
||
Many = 4,
|
||
Other = 5,
|
||
}
|
||
|
||
/**
|
||
* Returns the plural case based on the locale.
|
||
* This is a copy of the deprecated function that we used in Angular v4.
|
||
* // TODO(ocombe): remove this once we can the real getPluralCase function
|
||
*
|
||
* @deprecated from v5 the plural case function is in locale data files common/locales/*.ts
|
||
*/
|
||
function getPluralCase(locale: string, nLike: number | string): Plural {
|
||
if (typeof nLike === 'string') {
|
||
nLike = parseInt(<string>nLike, 10);
|
||
}
|
||
const n: number = nLike as number;
|
||
const nDecimal = n.toString().replace(/^[^.]*\.?/, '');
|
||
const i = Math.floor(Math.abs(n));
|
||
const v = nDecimal.length;
|
||
const f = parseInt(nDecimal, 10);
|
||
const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0;
|
||
|
||
const lang = locale.split('-')[0].toLowerCase();
|
||
|
||
switch (lang) {
|
||
case 'af':
|
||
case 'asa':
|
||
case 'az':
|
||
case 'bem':
|
||
case 'bez':
|
||
case 'bg':
|
||
case 'brx':
|
||
case 'ce':
|
||
case 'cgg':
|
||
case 'chr':
|
||
case 'ckb':
|
||
case 'ee':
|
||
case 'el':
|
||
case 'eo':
|
||
case 'es':
|
||
case 'eu':
|
||
case 'fo':
|
||
case 'fur':
|
||
case 'gsw':
|
||
case 'ha':
|
||
case 'haw':
|
||
case 'hu':
|
||
case 'jgo':
|
||
case 'jmc':
|
||
case 'ka':
|
||
case 'kk':
|
||
case 'kkj':
|
||
case 'kl':
|
||
case 'ks':
|
||
case 'ksb':
|
||
case 'ky':
|
||
case 'lb':
|
||
case 'lg':
|
||
case 'mas':
|
||
case 'mgo':
|
||
case 'ml':
|
||
case 'mn':
|
||
case 'nb':
|
||
case 'nd':
|
||
case 'ne':
|
||
case 'nn':
|
||
case 'nnh':
|
||
case 'nyn':
|
||
case 'om':
|
||
case 'or':
|
||
case 'os':
|
||
case 'ps':
|
||
case 'rm':
|
||
case 'rof':
|
||
case 'rwk':
|
||
case 'saq':
|
||
case 'seh':
|
||
case 'sn':
|
||
case 'so':
|
||
case 'sq':
|
||
case 'ta':
|
||
case 'te':
|
||
case 'teo':
|
||
case 'tk':
|
||
case 'tr':
|
||
case 'ug':
|
||
case 'uz':
|
||
case 'vo':
|
||
case 'vun':
|
||
case 'wae':
|
||
case 'xog':
|
||
if (n === 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'ak':
|
||
case 'ln':
|
||
case 'mg':
|
||
case 'pa':
|
||
case 'ti':
|
||
if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'am':
|
||
case 'as':
|
||
case 'bn':
|
||
case 'fa':
|
||
case 'gu':
|
||
case 'hi':
|
||
case 'kn':
|
||
case 'mr':
|
||
case 'zu':
|
||
if (i === 0 || n === 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'ar':
|
||
if (n === 0) return Plural.Zero;
|
||
if (n === 1) return Plural.One;
|
||
if (n === 2) return Plural.Two;
|
||
if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few;
|
||
if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'ast':
|
||
case 'ca':
|
||
case 'de':
|
||
case 'en':
|
||
case 'et':
|
||
case 'fi':
|
||
case 'fy':
|
||
case 'gl':
|
||
case 'it':
|
||
case 'nl':
|
||
case 'sv':
|
||
case 'sw':
|
||
case 'ur':
|
||
case 'yi':
|
||
if (i === 1 && v === 0) return Plural.One;
|
||
return Plural.Other;
|
||
case 'be':
|
||
if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One;
|
||
if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 &&
|
||
!(n % 100 >= 12 && n % 100 <= 14))
|
||
return Plural.Few;
|
||
if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 ||
|
||
n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14)
|
||
return Plural.Many;
|
||
return Plural.Other;
|
||
case 'br':
|
||
if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One;
|
||
if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two;
|
||
if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) &&
|
||
!(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 ||
|
||
n % 100 >= 90 && n % 100 <= 99))
|
||
return Plural.Few;
|
||
if (!(n === 0) && n % 1e6 === 0) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'bs':
|
||
case 'hr':
|
||
case 'sr':
|
||
if (v === 0 && i % 10 === 1 && !(i % 100 === 11) || f % 10 === 1 && !(f % 100 === 11))
|
||
return Plural.One;
|
||
if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 &&
|
||
!(i % 100 >= 12 && i % 100 <= 14) ||
|
||
f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 &&
|
||
!(f % 100 >= 12 && f % 100 <= 14))
|
||
return Plural.Few;
|
||
return Plural.Other;
|
||
case 'cs':
|
||
case 'sk':
|
||
if (i === 1 && v === 0) return Plural.One;
|
||
if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few;
|
||
if (!(v === 0)) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'cy':
|
||
if (n === 0) return Plural.Zero;
|
||
if (n === 1) return Plural.One;
|
||
if (n === 2) return Plural.Two;
|
||
if (n === 3) return Plural.Few;
|
||
if (n === 6) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'da':
|
||
if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One;
|
||
return Plural.Other;
|
||
case 'dsb':
|
||
case 'hsb':
|
||
if (v === 0 && i % 100 === 1 || f % 100 === 1) return Plural.One;
|
||
if (v === 0 && i % 100 === 2 || f % 100 === 2) return Plural.Two;
|
||
if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 ||
|
||
f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4)
|
||
return Plural.Few;
|
||
return Plural.Other;
|
||
case 'ff':
|
||
case 'fr':
|
||
case 'hy':
|
||
case 'kab':
|
||
if (i === 0 || i === 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'fil':
|
||
if (v === 0 && (i === 1 || i === 2 || i === 3) ||
|
||
v === 0 && !(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) ||
|
||
!(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9))
|
||
return Plural.One;
|
||
return Plural.Other;
|
||
case 'ga':
|
||
if (n === 1) return Plural.One;
|
||
if (n === 2) return Plural.Two;
|
||
if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few;
|
||
if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'gd':
|
||
if (n === 1 || n === 11) return Plural.One;
|
||
if (n === 2 || n === 12) return Plural.Two;
|
||
if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few;
|
||
return Plural.Other;
|
||
case 'gv':
|
||
if (v === 0 && i % 10 === 1) return Plural.One;
|
||
if (v === 0 && i % 10 === 2) return Plural.Two;
|
||
if (v === 0 &&
|
||
(i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80))
|
||
return Plural.Few;
|
||
if (!(v === 0)) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'he':
|
||
if (i === 1 && v === 0) return Plural.One;
|
||
if (i === 2 && v === 0) return Plural.Two;
|
||
if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'is':
|
||
if (t === 0 && i % 10 === 1 && !(i % 100 === 11) || !(t === 0)) return Plural.One;
|
||
return Plural.Other;
|
||
case 'ksh':
|
||
if (n === 0) return Plural.Zero;
|
||
if (n === 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'kw':
|
||
case 'naq':
|
||
case 'se':
|
||
case 'smn':
|
||
if (n === 1) return Plural.One;
|
||
if (n === 2) return Plural.Two;
|
||
return Plural.Other;
|
||
case 'lag':
|
||
if (n === 0) return Plural.Zero;
|
||
if ((i === 0 || i === 1) && !(n === 0)) return Plural.One;
|
||
return Plural.Other;
|
||
case 'lt':
|
||
if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One;
|
||
if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 &&
|
||
!(n % 100 >= 11 && n % 100 <= 19))
|
||
return Plural.Few;
|
||
if (!(f === 0)) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'lv':
|
||
case 'prg':
|
||
if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 ||
|
||
v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19)
|
||
return Plural.Zero;
|
||
if (n % 10 === 1 && !(n % 100 === 11) || v === 2 && f % 10 === 1 && !(f % 100 === 11) ||
|
||
!(v === 2) && f % 10 === 1)
|
||
return Plural.One;
|
||
return Plural.Other;
|
||
case 'mk':
|
||
if (v === 0 && i % 10 === 1 || f % 10 === 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'mt':
|
||
if (n === 1) return Plural.One;
|
||
if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10)
|
||
return Plural.Few;
|
||
if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many;
|
||
return Plural.Other;
|
||
case 'pl':
|
||
if (i === 1 && v === 0) return Plural.One;
|
||
if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 &&
|
||
!(i % 100 >= 12 && i % 100 <= 14))
|
||
return Plural.Few;
|
||
if (v === 0 && !(i === 1) && i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 ||
|
||
v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 ||
|
||
v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14)
|
||
return Plural.Many;
|
||
return Plural.Other;
|
||
case 'pt':
|
||
if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One;
|
||
return Plural.Other;
|
||
case 'ro':
|
||
if (i === 1 && v === 0) return Plural.One;
|
||
if (!(v === 0) || n === 0 ||
|
||
!(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19)
|
||
return Plural.Few;
|
||
return Plural.Other;
|
||
case 'ru':
|
||
case 'uk':
|
||
if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One;
|
||
if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 &&
|
||
!(i % 100 >= 12 && i % 100 <= 14))
|
||
return Plural.Few;
|
||
if (v === 0 && i % 10 === 0 ||
|
||
v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 ||
|
||
v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && i % 100 <= 14)
|
||
return Plural.Many;
|
||
return Plural.Other;
|
||
case 'shi':
|
||
if (i === 0 || n === 1) return Plural.One;
|
||
if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few;
|
||
return Plural.Other;
|
||
case 'si':
|
||
if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One;
|
||
return Plural.Other;
|
||
case 'sl':
|
||
if (v === 0 && i % 100 === 1) return Plural.One;
|
||
if (v === 0 && i % 100 === 2) return Plural.Two;
|
||
if (v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0))
|
||
return Plural.Few;
|
||
return Plural.Other;
|
||
case 'tzm':
|
||
if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99)
|
||
return Plural.One;
|
||
return Plural.Other;
|
||
// When there is no specification, the default is always "other"
|
||
// Spec: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||
// > other (required—general plural form — also used if the language only has a single form)
|
||
default:
|
||
return Plural.Other;
|
||
}
|
||
}
|
||
|
||
function getPluralCategory(value: any, locale: string): string {
|
||
const plural = getPluralCase(locale, value);
|
||
|
||
switch (plural) {
|
||
case Plural.Zero:
|
||
return 'zero';
|
||
case Plural.One:
|
||
return 'one';
|
||
case Plural.Two:
|
||
return 'two';
|
||
case Plural.Few:
|
||
return 'few';
|
||
case Plural.Many:
|
||
return 'many';
|
||
default:
|
||
return 'other';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns the index of the current case of an ICU expression depending on the main binding value
|
||
*
|
||
* @param icuExpression
|
||
* @param bindingValue The value of the main binding used by this ICU expression
|
||
*/
|
||
function getCaseIndex(icuExpression: TIcu, bindingValue: string): number {
|
||
let index = icuExpression.cases.indexOf(bindingValue);
|
||
if (index === -1) {
|
||
switch (icuExpression.type) {
|
||
case IcuType.plural: {
|
||
// TODO(ocombe): replace this hard-coded value by the real LOCALE_ID value
|
||
const locale = 'en-US';
|
||
const resolvedCase = getPluralCategory(bindingValue, locale);
|
||
index = icuExpression.cases.indexOf(resolvedCase);
|
||
if (index === -1 && resolvedCase !== 'other') {
|
||
index = icuExpression.cases.indexOf('other');
|
||
}
|
||
break;
|
||
}
|
||
case IcuType.select: {
|
||
index = icuExpression.cases.indexOf('other');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return index;
|
||
}
|
||
|
||
/**
|
||
* Generate the OpCodes for ICU expressions.
|
||
*
|
||
* @param tIcus
|
||
* @param icuExpression
|
||
* @param startIndex
|
||
* @param expandoStartIndex
|
||
*/
|
||
function icuStart(
|
||
tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number,
|
||
expandoStartIndex: number): void {
|
||
const createCodes = [];
|
||
const removeCodes = [];
|
||
const updateCodes = [];
|
||
const vars = [];
|
||
const childIcus: number[][] = [];
|
||
for (let i = 0; i < icuExpression.values.length; i++) {
|
||
// Each value is an array of strings & other ICU expressions
|
||
const valueArr = icuExpression.values[i];
|
||
const nestedIcus: IcuExpression[] = [];
|
||
for (let j = 0; j < valueArr.length; j++) {
|
||
const value = valueArr[j];
|
||
if (typeof value !== 'string') {
|
||
// It is an nested ICU expression
|
||
const icuIndex = nestedIcus.push(value as IcuExpression) - 1;
|
||
// Replace nested ICU expression by a comment node
|
||
valueArr[j] = `<!--<2D>${icuIndex}<EFBFBD>-->`;
|
||
}
|
||
}
|
||
const icuCase: IcuCase =
|
||
parseIcuCase(valueArr.join(''), startIndex, nestedIcus, tIcus, expandoStartIndex);
|
||
createCodes.push(icuCase.create);
|
||
removeCodes.push(icuCase.remove);
|
||
updateCodes.push(icuCase.update);
|
||
vars.push(icuCase.vars);
|
||
childIcus.push(icuCase.childIcus);
|
||
}
|
||
const tIcu: TIcu = {
|
||
type: icuExpression.type,
|
||
vars,
|
||
childIcus,
|
||
cases: icuExpression.cases,
|
||
create: createCodes,
|
||
remove: removeCodes,
|
||
update: updateCodes
|
||
};
|
||
tIcus.push(tIcu);
|
||
// Adding the maximum possible of vars needed (based on the cases with the most vars)
|
||
i18nVarsCount += Math.max(...vars);
|
||
}
|
||
|
||
/**
|
||
* Transforms a string template into an HTML template and a list of instructions used to update
|
||
* attributes or nodes that contain bindings.
|
||
*
|
||
* @param unsafeHtml The string to parse
|
||
* @param parentIndex
|
||
* @param nestedIcus
|
||
* @param tIcus
|
||
* @param expandoStartIndex
|
||
*/
|
||
function parseIcuCase(
|
||
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
|
||
expandoStartIndex: number): IcuCase {
|
||
const inertBodyHelper = new InertBodyHelper(document);
|
||
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||
if (!inertBodyElement) {
|
||
throw new Error('Unable to generate inert body element');
|
||
}
|
||
const wrapper = getTemplateContent(inertBodyElement !) as Element || inertBodyElement;
|
||
const opCodes: IcuCase = {vars: 0, childIcus: [], create: [], remove: [], update: []};
|
||
parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex);
|
||
return opCodes;
|
||
}
|
||
|
||
const NESTED_ICU = /<2F>(\d+)<29>/;
|
||
|
||
/**
|
||
* Parses a node, its children and its siblings, and generates the mutate & update OpCodes.
|
||
*
|
||
* @param currentNode The first node to parse
|
||
* @param icuCase The data for the ICU expression case that contains those nodes
|
||
* @param parentIndex Index of the current node's parent
|
||
* @param nestedIcus Data for the nested ICU expressions that this case contains
|
||
* @param tIcus Data for all ICU expressions of the current message
|
||
* @param expandoStartIndex Expando start index for the current ICU expression
|
||
*/
|
||
function parseNodes(
|
||
currentNode: Node | null, icuCase: IcuCase, parentIndex: number, nestedIcus: IcuExpression[],
|
||
tIcus: TIcu[], expandoStartIndex: number) {
|
||
if (currentNode) {
|
||
const nestedIcusToCreate: [IcuExpression, number][] = [];
|
||
while (currentNode) {
|
||
const nextNode: Node|null = currentNode.nextSibling;
|
||
const newIndex = expandoStartIndex + ++icuCase.vars;
|
||
switch (currentNode.nodeType) {
|
||
case Node.ELEMENT_NODE:
|
||
const element = currentNode as Element;
|
||
const tagName = element.tagName.toLowerCase();
|
||
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
|
||
// This isn't a valid element, we won't create an element for it
|
||
icuCase.vars--;
|
||
} else {
|
||
icuCase.create.push(
|
||
ELEMENT_MARKER, tagName, newIndex,
|
||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||
const elAttrs = element.attributes;
|
||
for (let i = 0; i < elAttrs.length; i++) {
|
||
const attr = elAttrs.item(i) !;
|
||
const lowerAttrName = attr.name.toLowerCase();
|
||
const hasBinding = !!attr.value.match(BINDING_REGEXP);
|
||
// we assume the input string is safe, unless it's using a binding
|
||
if (hasBinding) {
|
||
if (VALID_ATTRS.hasOwnProperty(lowerAttrName)) {
|
||
if (URI_ATTRS[lowerAttrName]) {
|
||
addAllToArray(
|
||
generateBindingUpdateOpCodes(attr.value, newIndex, attr.name, _sanitizeUrl),
|
||
icuCase.update);
|
||
} else if (SRCSET_ATTRS[lowerAttrName]) {
|
||
addAllToArray(
|
||
generateBindingUpdateOpCodes(
|
||
attr.value, newIndex, attr.name, sanitizeSrcset),
|
||
icuCase.update);
|
||
} else {
|
||
addAllToArray(
|
||
generateBindingUpdateOpCodes(attr.value, newIndex, attr.name),
|
||
icuCase.update);
|
||
}
|
||
} else {
|
||
ngDevMode &&
|
||
console.warn(
|
||
`WARNING: ignoring unsafe attribute value ${lowerAttrName} on element ${tagName} (see http://g.co/ng/security#xss)`);
|
||
}
|
||
} else {
|
||
icuCase.create.push(
|
||
newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Attr, attr.name,
|
||
attr.value);
|
||
}
|
||
}
|
||
// Parse the children of this node (if any)
|
||
parseNodes(
|
||
currentNode.firstChild, icuCase, newIndex, nestedIcus, tIcus, expandoStartIndex);
|
||
// Remove the parent node after the children
|
||
icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
|
||
}
|
||
break;
|
||
case Node.TEXT_NODE:
|
||
const value = currentNode.textContent || '';
|
||
const hasBinding = value.match(BINDING_REGEXP);
|
||
icuCase.create.push(
|
||
hasBinding ? '' : value, newIndex,
|
||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||
icuCase.remove.push(newIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
|
||
if (hasBinding) {
|
||
addAllToArray(generateBindingUpdateOpCodes(value, newIndex), icuCase.update);
|
||
}
|
||
break;
|
||
case Node.COMMENT_NODE:
|
||
// Check if the comment node is a placeholder for a nested ICU
|
||
const match = NESTED_ICU.exec(currentNode.textContent || '');
|
||
if (match) {
|
||
const nestedIcuIndex = parseInt(match[1], 10);
|
||
const newLocal = ngDevMode ? `nested ICU ${nestedIcuIndex}` : '';
|
||
// Create the comment node that will anchor the ICU expression
|
||
icuCase.create.push(
|
||
COMMENT_MARKER, newLocal, newIndex,
|
||
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
|
||
const nestedIcu = nestedIcus[nestedIcuIndex];
|
||
nestedIcusToCreate.push([nestedIcu, newIndex]);
|
||
} else {
|
||
// We do not handle any other type of comment
|
||
icuCase.vars--;
|
||
}
|
||
break;
|
||
default:
|
||
// We do not handle any other type of element
|
||
icuCase.vars--;
|
||
}
|
||
currentNode = nextNode !;
|
||
}
|
||
|
||
for (let i = 0; i < nestedIcusToCreate.length; i++) {
|
||
const nestedIcu = nestedIcusToCreate[i][0];
|
||
const nestedIcuNodeIndex = nestedIcusToCreate[i][1];
|
||
icuStart(tIcus, nestedIcu, nestedIcuNodeIndex, expandoStartIndex + icuCase.vars);
|
||
// Since this is recursive, the last TIcu that was pushed is the one we want
|
||
const nestTIcuIndex = tIcus.length - 1;
|
||
icuCase.vars += Math.max(...tIcus[nestTIcuIndex].vars);
|
||
icuCase.childIcus.push(nestTIcuIndex);
|
||
const mask = getBindingMask(nestedIcu);
|
||
icuCase.update.push(
|
||
toMaskBit(nestedIcu.mainBinding), // mask of the main binding
|
||
3, // skip 3 opCodes if not changed
|
||
-1 - nestedIcu.mainBinding,
|
||
nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuSwitch,
|
||
nestTIcuIndex,
|
||
mask, // mask of all the bindings of this ICU expression
|
||
2, // skip 2 opCodes if not changed
|
||
nestedIcuNodeIndex << I18nUpdateOpCode.SHIFT_REF | I18nUpdateOpCode.IcuUpdate,
|
||
nestTIcuIndex);
|
||
icuCase.remove.push(
|
||
nestTIcuIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.RemoveNestedIcu,
|
||
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
|
||
}
|
||
}
|
||
}
|
||
|
||
let TRANSLATIONS: {[key: string]: string} = {};
|
||
export interface I18nLocalizeOptions { translations: {[key: string]: string}; }
|
||
|
||
/**
|
||
* Set the configuration for `i18nLocalize`.
|
||
*
|
||
* @deprecated this method is temporary & should not be used as it will be removed soon
|
||
*/
|
||
export function i18nConfigureLocalize(options: I18nLocalizeOptions = {
|
||
translations: {}
|
||
}) {
|
||
TRANSLATIONS = options.translations;
|
||
}
|
||
|
||
const LOCALIZE_PH_REGEXP = /\{\$(.*?)\}/g;
|
||
|
||
/**
|
||
* A goog.getMsg-like function for users that do not use Closure.
|
||
*
|
||
* This method is required as a *temporary* measure to prevent i18n tests from being blocked while
|
||
* running outside of Closure Compiler. This method will not be needed once runtime translation
|
||
* service support is introduced.
|
||
*
|
||
* @publicApi
|
||
* @deprecated this method is temporary & should not be used as it will be removed soon
|
||
*/
|
||
export function ɵɵi18nLocalize(input: string, placeholders: {[key: string]: string} = {}) {
|
||
if (typeof TRANSLATIONS[input] !== 'undefined') { // to account for empty string
|
||
input = TRANSLATIONS[input];
|
||
}
|
||
return Object.keys(placeholders).length ?
|
||
input.replace(LOCALIZE_PH_REGEXP, (match, key) => placeholders[key] || '') :
|
||
input;
|
||
}
|