diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json
index 924b196222..1da0721b05 100644
--- a/goldens/circular-deps/packages.json
+++ b/goldens/circular-deps/packages.json
@@ -1758,9 +1758,10 @@
"packages/core/src/render3/index.ts"
],
[
- "packages/core/src/render3/i18n.ts",
+ "packages/core/src/render3/i18n/i18n_apply.ts",
"packages/core/src/render3/interfaces/type_checks.ts",
- "packages/core/src/render3/index.ts"
+ "packages/core/src/render3/index.ts",
+ "packages/core/src/render3/instructions/i18n.ts"
],
[
"packages/core/src/render3/interfaces/container.ts",
diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json
index 19530230df..5e0e6dd3ee 100644
--- a/goldens/size-tracking/integration-payloads.json
+++ b/goldens/size-tracking/integration-payloads.json
@@ -62,7 +62,7 @@
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
- "bundle": 1213769
+ "bundle": 1214317
}
}
}
diff --git a/packages/core/src/application_module.ts b/packages/core/src/application_module.ts
index 7db809abed..b5eedcede5 100644
--- a/packages/core/src/application_module.ts
+++ b/packages/core/src/application_module.ts
@@ -21,7 +21,7 @@ import {ComponentFactoryResolver} from './linker';
import {Compiler} from './linker/compiler';
import {NgModule} from './metadata';
import {SCHEDULER} from './render3/component_ref';
-import {setLocaleId} from './render3/i18n';
+import {setLocaleId} from './render3/i18n/i18n_locale_id';
import {NgZone} from './zone';
declare const $localize: {locale?: string};
diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts
index a701f7518e..172ad182e4 100644
--- a/packages/core/src/application_ref.ts
+++ b/packages/core/src/application_ref.ts
@@ -30,7 +30,7 @@ import {InternalViewRef, ViewRef} from './linker/view_ref';
import {isComponentResourceResolutionQueueEmpty, resolveComponentResources} from './metadata/resource_loading';
import {assertNgModuleType} from './render3/assert';
import {ComponentFactory as R3ComponentFactory} from './render3/component_ref';
-import {setLocaleId} from './render3/i18n';
+import {setLocaleId} from './render3/i18n/i18n_locale_id';
import {setJitOptions} from './render3/jit/jit_options';
import {NgModuleFactory as R3NgModuleFactory} from './render3/ng_module_ref';
import {publishDefaultGlobalUtils as _publishDefaultGlobalUtils} from './render3/util/global_utils';
diff --git a/packages/core/src/render3/i18n.ts b/packages/core/src/render3/i18n.ts
deleted file mode 100644
index e7da73a264..0000000000
--- a/packages/core/src/render3/i18n.ts
+++ /dev/null
@@ -1,1516 +0,0 @@
-/**
- * @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 '../util/ng_i18n_closure_mode';
-import '../util/ng_dev_mode';
-
-import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
-import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
-import {getInertBodyHelper} from '../sanitization/inert_body';
-import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
-import {addAllToArray} from '../util/array_utils';
-import {assertDefined, assertEqual, assertGreaterThan, assertIndexInRange} from '../util/assert';
-
-import {bindingUpdated} from './bindings';
-import {attachPatchData} from './context_discovery';
-import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug';
-import {setDelayProjection} from './instructions/all';
-import {allocExpando, elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, setInputsForProperty, setNgReflectProperties, textBindingInternal as applyTextBinding} from './instructions/shared';
-import {LContainer, NATIVE} from './interfaces/container';
-import {getDocument} from './interfaces/document';
-import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
-import {TElementNode, TIcuContainerNode, TNode, TNodeFlags, TNodeType, TProjectionNode} from './interfaces/node';
-import {RComment, RElement, RText} from './interfaces/renderer';
-import {SanitizerFn} from './interfaces/sanitization';
-import {isLContainer} from './interfaces/type_checks';
-import {HEADER_OFFSET, LView, RENDERER, T_HOST, TVIEW, TView} from './interfaces/view';
-import {appendChild, applyProjection, createTextNode, nativeRemoveNode} from './node_manipulation';
-import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, getTView, nextBindingIndex, setIsNotParent, setPreviousOrParentTNode} from './state';
-import {attachDebugGetter} from './util/debug_utils';
-import {renderStringify} from './util/misc_utils';
-import {getNativeByIndex, getNativeByTNode, getTNode, load} from './util/view_utils';
-
-
-const MARKER = `�`;
-const ICU_BLOCK_REGEXP = /^\s*(�\d+:?\d*�)\s*,\s*(select|plural)\s*,/;
-const SUBTEMPLATE_REGEXP = /�\/?\*(\d+:\d+)�/gi;
-const PH_REGEXP = /�(\/?[#*!]\d+):?\d*�/gi;
-const BINDING_REGEXP = /�(\d+):?\d*�/gi;
-const ICU_REGEXP = /({\s*�\d+:?\d*�\s*,\s*\S{6}\s*,[\s\S]*})/gi;
-const enum TagType {
- ELEMENT = '#',
- TEMPLATE = '*',
- PROJECTION = '!',
-}
-
-// i18nPostprocess consts
-const ROOT_TEMPLATE_ID = 0;
-const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]/;
-const PP_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]|(�\/?\*\d+:\d+�)/g;
-const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
-const PP_ICU_PLACEHOLDERS_REGEXP = /{([A-Z0-9_]+)}/g;
-const PP_ICUS_REGEXP = /�I18N_EXP_(ICU(_\d+)?)�/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 {
- 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);
- 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 (cases.length > values.length) {
- values.push(blocks);
- }
- }
-
- // 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:
- * ```
- *
Translate me!
- * ```
- *
- * @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 (isRootTemplateMessage(subTemplateIndex)) {
- // 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
- if (ngDevMode) {
- attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
- }
- 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:
- * - `�{index}(:{block})�`: *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.
- * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *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.
- * - `�!{index}(:{block})�`/`�/!{index}(:{block})�`: *Projection Placeholder*: Marks the
- * beginning and end of that was 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.
- * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *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 = getTView();
- ngDevMode && assertDefined(tView, `tView should be defined`);
- i18nIndexStack[++i18nIndexStackPointer] = index;
- // We need to delay projections until `i18nEnd`
- setDelayProjection(true);
- if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
- i18nStartFirstPass(getLView(), 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;
-
-function allocNodeIndex(startIndex: number): number {
- return startIndex + i18nVarsCount++;
-}
-
-/**
- * See `i18nStart` above.
- */
-function i18nStartFirstPass(
- lView: LView, tView: TView, index: number, message: string, subTemplateIndex?: number) {
- const startIndex = tView.blueprint.length - HEADER_OFFSET;
- i18nVarsCount = 0;
- const previousOrParentTNode = getPreviousOrParentTNode();
- const parentTNode =
- getIsParent() ? previousOrParentTNode : previousOrParentTNode && previousOrParentTNode.parent;
- let parentIndex =
- parentTNode && parentTNode !== lView[T_HOST] ? parentTNode.index - HEADER_OFFSET : index;
- let parentIndexPointer = 0;
- parentIndexStack[parentIndexPointer] = parentIndex;
- const createOpCodes: I18nMutateOpCodes = [];
- if (ngDevMode) {
- attachDebugGetter(createOpCodes, i18nMutateOpCodesToString);
- }
- // 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. We should also
- // keep track whether an element was a parent node or not, so that the logic that consumes
- // the generated `I18nMutateOpCode`s can leverage this information to properly set TNode state
- // (whether it's a parent or sibling).
- if (index > 0 && previousOrParentTNode !== parentTNode) {
- let previousTNodeIndex = previousOrParentTNode.index - HEADER_OFFSET;
- // If current TNode is a sibling node, encode it using a negative index. This information is
- // required when the `Select` action is processed (see the `readCreateOpCodes` function).
- if (!getIsParent()) {
- previousTNodeIndex = ~previousTNodeIndex;
- }
- // Create an OpCode to select the previous TNode
- createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select);
- }
- const updateOpCodes: I18nUpdateOpCodes = [];
- if (ngDevMode) {
- attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
- }
- const icuExpressions: TIcu[] = [];
-
- if (message === '' && isRootTemplateMessage(subTemplateIndex)) {
- // If top level translation is an empty string, do not invoke additional processing
- // and just create op codes for empty text node instead.
- createOpCodes.push(
- message, allocNodeIndex(startIndex),
- parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
- } else {
- const templateTranslation = getTranslationForTemplate(message, subTemplateIndex);
- const msgParts = replaceNgsp(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) === TagType.ELEMENT) {
- 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);
- const isElement = value.charAt(0) === TagType.ELEMENT;
- // The value represents a placeholder that we move to the designated index.
- // Note: positive indicies indicate that a TNode with a given index should also be marked
- // as parent while executing `Select` instruction.
- createOpCodes.push(
- (isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF |
- I18nMutateOpCode.Select,
- parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
-
- if (isElement) {
- 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
- const icuExpression = parts[j] as IcuExpression;
-
- // Verify that ICU expression has the right shape. Translations might contain invalid
- // constructions (while original messages were correct), so ICU parsing at runtime may
- // not succeed (thus `icuExpression` remains a string).
- if (typeof icuExpression !== 'object') {
- throw new Error(
- `Unable to parse ICU expression in "${templateTranslation}" message.`);
- }
-
- // Create the comment node that will anchor the ICU expression
- const icuNodeIndex = allocNodeIndex(startIndex);
- createOpCodes.push(
- COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
- parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
-
- // Update codes for the ICU expression
- 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 = allocNodeIndex(startIndex);
- 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);
- }
- }
- }
- }
- }
- }
-
- if (i18nVarsCount > 0) {
- allocExpando(tView, lView, i18nVarsCount);
- }
-
- // 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(
- tView: TView, tNode: TNode, parentTNode: TNode, previousTNode: TNode|null,
- lView: LView): TNode {
- ngDevMode && ngDevMode.rendererMoveNode++;
- const nextNode = tNode.next;
- 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 !== lView[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;
- }
-
- // If the placeholder to append is a projection, we need to move the projected nodes instead
- if (tNode.type === TNodeType.Projection) {
- applyProjection(tView, lView, tNode as TProjectionNode);
- return tNode;
- }
-
- appendChild(tView, lView, getNativeByTNode(tNode, lView), tNode);
-
- const slotValue = lView[tNode.index];
- if (tNode.type !== TNodeType.Container && isLContainer(slotValue)) {
- // Nodes that inject ViewContainerRef also have a comment node that should be moved
- appendChild(tView, lView, slotValue[NATIVE], tNode);
- }
- return tNode;
-}
-
-function isRootTemplateMessage(subTemplateIndex: number|undefined): subTemplateIndex is undefined {
- return subTemplateIndex === undefined;
-}
-
-/**
- * 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 [�*1:1��#2:1�|�#4:1�|�5�])
- * 2. Replace all ICU vars (like "VAR_PLURAL")
- * 3. Replace all placeholders used inside ICUs in a form of {PLACEHOLDER}
- * 4. Replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�)
- * 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 [�#5�|�*1:1��#2:1�|�#4:1�]
- *
- * Note: due to the way we process nested templates (BFS), multi-value placeholders are typically
- * grouped by templates, for example: [�#5�|�#6�|�#1:1�|�#3:2�] where �#5� and �#6� belong to root
- * template, �#1:1� belong to nested template with index 1 and �#1:2� - nested template with index
- * 3. However in real templates the order might be different: i.e. �#1:1� and/or �#3:2� may go in
- * front of �#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;
- const placeholders: PostprocessPlaceholder[] = matches[content] || [];
- if (!placeholders.length) {
- 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 (!placeholders.length) {
- throw new Error(`i18n postprocess: unmatched placeholder - ${content}`);
- }
-
- const currentTemplateId = templateIdsStack[templateIdsStack.length - 1];
- 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;
- });
- }
-
- // 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 placeholders used inside ICUs in a form of {PLACEHOLDER}
- */
- result = result.replace(PP_ICU_PLACEHOLDERS_REGEXP, (match, key): string => {
- return replacements.hasOwnProperty(key) ? replacements[key] as string : match;
- });
-
- /**
- * Step 4: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) 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 lView = getLView();
- const tView = getTView();
- ngDevMode && assertDefined(tView, `tView should be defined`);
- i18nEndFirstPass(tView, lView);
- // Stop delaying projections
- setDelayProjection(false);
-}
-
-/**
- * See `i18nEnd` above.
- */
-function i18nEndFirstPass(tView: TView, lView: LView) {
- ngDevMode &&
- assertEqual(
- getBindingIndex(), 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`
- const lastCreatedNode = getPreviousOrParentTNode();
-
- // Read the instructions to insert/move/remove DOM elements
- const visitedNodes = applyCreateOpCodes(tView, rootIndex, tI18n.create, lView);
-
- // Remove deleted nodes
- let index = rootIndex + 1;
- while (index <= lastCreatedNode.index - HEADER_OFFSET) {
- if (visitedNodes.indexOf(index) === -1) {
- removeNode(tView, lView, index, /* markAsDetached */ true);
- }
- // Check if an element has any local refs and skip them
- const tNode = getTNode(tView, index);
- if (tNode &&
- (tNode.type === TNodeType.Container || tNode.type === TNodeType.Element ||
- tNode.type === TNodeType.ElementContainer) &&
- tNode.localNames !== null) {
- // Divide by 2 to get the number of local refs,
- // since they are stored as an array that also includes directive indexes,
- // i.e. ["localRef", directiveIndex, ...]
- index += tNode.localNames.length >> 1;
- }
- index++;
- }
-}
-
-/**
- * Creates and stores the dynamic TNode, and unhooks it from the tree for now.
- */
-function createDynamicNodeAtIndex(
- tView: TView, lView: LView, index: number, type: TNodeType, native: RElement|RText|null,
- name: string|null): TElementNode|TIcuContainerNode {
- const previousOrParentTNode = getPreviousOrParentTNode();
- ngDevMode && assertIndexInRange(lView, index + HEADER_OFFSET);
- lView[index + HEADER_OFFSET] = native;
- // FIXME(misko): Why does this create A TNode??? I would not expect this to be here.
- const tNode = getOrCreateTNode(tView, lView[T_HOST], index, type as any, 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 && previousOrParentTNode.next === tNode) {
- previousOrParentTNode.next = null;
- }
-
- return tNode;
-}
-
-/**
- * Apply `I18nMutateOpCodes` OpCodes.
- *
- * @param tView Current `TView`
- * @param rootIndex Pointer to the root (parent) tNode for the i18n.
- * @param createOpCodes OpCodes to process
- * @param lView Current `LView`
- */
-function applyCreateOpCodes(
- tView: TView, rootindex: number, createOpCodes: I18nMutateOpCodes, lView: LView): number[] {
- const renderer = lView[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(tView, lView, textNodeIndex, TNodeType.Element, textRNode, null);
- visitedNodes.push(textNodeIndex);
- setIsNotParent();
- } else if (typeof opCode == 'number') {
- switch (opCode & I18nMutateOpCode.MASK_INSTRUCTION) {
- case I18nMutateOpCode.AppendChild:
- const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
- let destinationTNode: TNode;
- if (destinationNodeIndex === rootindex) {
- // If the destination node is `i18nStart`, we don't have a
- // top-level node and we should use the host node instead
- destinationTNode = lView[T_HOST]!;
- } else {
- destinationTNode = getTNode(tView, destinationNodeIndex);
- }
- ngDevMode &&
- assertDefined(
- currentTNode!,
- `You need to create or select a node before you can insert it into the DOM`);
- previousTNode =
- appendI18nNode(tView, currentTNode!, destinationTNode, previousTNode, lView);
- break;
- case I18nMutateOpCode.Select:
- // Negative indices indicate that a given TNode is a sibling node, not a parent node
- // (see `i18nStartFirstPass` for additional information).
- const isParent = opCode >= 0;
- // FIXME(misko): This SHIFT_REF looks suspect as it does not have mask.
- const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF;
- visitedNodes.push(nodeIndex);
- previousTNode = currentTNode;
- currentTNode = getTNode(tView, nodeIndex);
- if (currentTNode) {
- setPreviousOrParentTNode(currentTNode, isParent);
- }
- break;
- case I18nMutateOpCode.ElementEnd:
- const elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
- previousTNode = currentTNode = getTNode(tView, elementIndex);
- setPreviousOrParentTNode(currentTNode, false);
- break;
- case I18nMutateOpCode.Attr:
- const elementNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
- const attrName = createOpCodes[++i] as string;
- const attrValue = createOpCodes[++i] as string;
- // This code is used for ICU expressions only, since we don't support
- // directives/components in ICUs, we don't need to worry about inputs here
- elementAttributeInternal(
- getTNode(tView, elementNodeIndex), lView, attrName, attrValue, null, null);
- 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(
- tView, lView, commentNodeIndex, TNodeType.IcuContainer, commentRNode, null);
- visitedNodes.push(commentNodeIndex);
- attachPatchData(commentRNode, lView);
- // We will add the case nodes later, during the update phase
- setIsNotParent();
- 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(
- tView, lView, elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue);
- visitedNodes.push(elementNodeIndex);
- break;
- default:
- throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
- }
- }
- }
-
- setIsNotParent();
-
- return visitedNodes;
-}
-
-/**
- * Apply `I18nUpdateOpCodes` OpCodes
- *
- * @param tView Current `TView`
- * @param tIcus If ICUs present than this contains them.
- * @param lView Current `LView`
- * @param updateOpCodes OpCodes to process
- * @param bindingsStartIndex Location of the first `ɵɵi18nApply`
- * @param changeMask Each bit corresponds to a `ɵɵi18nExp` (Counting backwards from
- * `bindingsStartIndex`)
- */
-function applyUpdateOpCodes(
- tView: TView, tIcus: TIcu[]|null, lView: LView, updateOpCodes: I18nUpdateOpCodes,
- bindingsStartIndex: number, changeMask: number) {
- 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 (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) {
- // Negative opCode represent `i18nExp` values offset.
- value += renderStringify(lView[bindingsStartIndex - opCode]);
- } else {
- const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
- switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
- case I18nUpdateOpCode.Attr:
- const propName = updateOpCodes[++j] as string;
- const sanitizeFn = updateOpCodes[++j] as SanitizerFn | null;
- elementPropertyInternal(
- tView, getTNode(tView, nodeIndex), lView, propName, value, lView[RENDERER],
- sanitizeFn, false);
- break;
- case I18nUpdateOpCode.Text:
- applyTextBinding(lView, nodeIndex, value);
- break;
- case I18nUpdateOpCode.IcuSwitch:
- caseCreated =
- applyIcuSwitchCase(tView, tIcus!, updateOpCodes[++j] as number, lView, value);
- break;
- case I18nUpdateOpCode.IcuUpdate:
- applyIcuUpdateCase(
- tView, tIcus!, updateOpCodes[++j] as number, bindingsStartIndex, lView,
- caseCreated);
- break;
- }
- }
- }
- }
- }
- i += skipCodes;
- }
-}
-
-/**
- * Apply OpCodes associated with updating an existing ICU.
- *
- * @param tView Current `TView`
- * @param tIcus tIcus ICUs active at this location them.
- * @param tIcuIndex Index into `tIcus` to process.
- * @param bindingsStartIndex Location of the first `ɵɵi18nApply`
- * @param lView Current `LView`
- * @param changeMask Each bit corresponds to a `ɵɵi18nExp` (Counting backwards from
- * `bindingsStartIndex`)
- */
-function applyIcuUpdateCase(
- tView: TView, tIcus: TIcu[], tIcuIndex: number, bindingsStartIndex: number, lView: LView,
- caseCreated: boolean) {
- ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
- const tIcu = tIcus[tIcuIndex];
- ngDevMode && assertIndexInRange(lView, tIcu.currentCaseLViewIndex);
- const activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
- if (activeCaseIndex !== null) {
- const mask = caseCreated ?
- -1 : // -1 is same as all bits on, which simulates creation since it marks all bits dirty
- changeMask;
- applyUpdateOpCodes(tView, tIcus, lView, tIcu.update[activeCaseIndex], bindingsStartIndex, mask);
- }
-}
-
-/**
- * Apply OpCodes associated with switching a case on ICU.
- *
- * This involves tearing down existing case and than building up a new case.
- *
- * @param tView Current `TView`
- * @param tIcus ICUs active at this location.
- * @param tIcuIndex Index into `tIcus` to process.
- * @param lView Current `LView`
- * @param value Value of the case to update to.
- * @returns true if a new case was created (needed so that the update executes regardless of the
- * bitmask)
- */
-function applyIcuSwitchCase(
- tView: TView, tIcus: TIcu[], tIcuIndex: number, lView: LView, value: string): boolean {
- applyIcuSwitchCaseRemove(tView, tIcus, tIcuIndex, lView);
-
- // Rebuild a new case for this ICU
- let caseCreated = false;
- ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
- const tIcu = tIcus[tIcuIndex];
- const caseIndex = getCaseIndex(tIcu, value);
- lView[tIcu.currentCaseLViewIndex] = caseIndex !== -1 ? caseIndex : null;
- if (caseIndex > -1) {
- // Add the nodes for the new case
- applyCreateOpCodes(
- tView, -1, // -1 means we don't have parent node
- tIcu.create[caseIndex], lView);
- caseCreated = true;
- }
- return caseCreated;
-}
-
-/**
- * Apply OpCodes associated with tearing down of DOM.
- *
- * This involves tearing down existing case and than building up a new case.
- *
- * @param tView Current `TView`
- * @param tIcus ICUs active at this location.
- * @param tIcuIndex Index into `tIcus` to process.
- * @param lView Current `LView`
- * @returns true if a new case was created (needed so that the update executes regardless of the
- * bitmask)
- */
-function applyIcuSwitchCaseRemove(tView: TView, tIcus: TIcu[], tIcuIndex: number, lView: LView) {
- ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
- const tIcu = tIcus[tIcuIndex];
- const activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
- if (activeCaseIndex !== null) {
- const removeCodes = tIcu.remove[activeCaseIndex];
- for (let k = 0; k < removeCodes.length; k++) {
- const removeOpCode = removeCodes[k] as number;
- const nodeOrIcuIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
- switch (removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION) {
- case I18nMutateOpCode.Remove:
- // FIXME(misko): this comment is wrong!
- // Remove DOM element, but do *not* mark TNode as detached, since we are
- // just switching ICU cases (while keeping the same TNode), so a DOM element
- // representing a new ICU case will be re-created.
- removeNode(tView, lView, nodeOrIcuIndex, /* markAsDetached */ false);
- break;
- case I18nMutateOpCode.RemoveNestedIcu:
- applyIcuSwitchCaseRemove(tView, tIcus, nodeOrIcuIndex, lView);
- break;
- }
- }
- }
-}
-
-function removeNode(tView: TView, lView: LView, index: number, markAsDetached: boolean) {
- const removedPhTNode = getTNode(tView, index);
- const removedPhRNode = getNativeByIndex(index, lView);
- if (removedPhRNode) {
- nativeRemoveNode(lView[RENDERER], removedPhRNode);
- }
-
- const slotValue = load(lView, index) as RElement | RComment | LContainer;
- if (isLContainer(slotValue)) {
- const lContainer = slotValue as LContainer;
- if (removedPhTNode.type !== TNodeType.Container) {
- nativeRemoveNode(lView[RENDERER], lContainer[NATIVE]);
- }
- }
-
- if (markAsDetached) {
- // Define this node as detached to avoid projecting it later
- removedPhTNode.flags |= TNodeFlags.isDetached;
- }
- 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:
- * - `�{index}(:{block})�`: *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.
- * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *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.
- * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *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 lView = getLView();
- const tView = getTView();
- ngDevMode && assertDefined(tView, `tView should be defined`);
- i18nAttributesFirstPass(lView, tView, index, values);
-}
-
-/**
- * See `i18nAttributes` above.
- */
-function i18nAttributesFirstPass(lView: LView, tView: TView, index: number, values: string[]) {
- const previousElement = getPreviousOrParentTNode();
- const previousElementIndex = previousElement.index - HEADER_OFFSET;
- const updateOpCodes: I18nUpdateOpCodes = [];
- if (ngDevMode) {
- attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
- }
- 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
- throw new Error('ICU expressions are not yet supported in attributes');
- } else if (value !== '') {
- // Even indexes are text (including bindings)
- const hasBinding = !!value.match(BINDING_REGEXP);
- if (hasBinding) {
- if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
- addAllToArray(
- generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes);
- }
- } else {
- const tNode = getTNode(tView, previousElementIndex);
- // Set attributes for Elements only, for other types (like ElementContainer),
- // only set inputs below
- if (tNode.type === TNodeType.Element) {
- elementAttributeInternal(tNode, lView, attrName, value, null, null);
- }
- // Check if that attribute is a directive input
- const dataValue = tNode.inputs !== null && tNode.inputs[attrName];
- if (dataValue) {
- setInputsForProperty(tView, lView, dataValue, attrName, value);
- if (ngDevMode) {
- const element = getNativeByIndex(previousElementIndex, lView) as RElement | RComment;
- setNgReflectProperties(lView, element, tNode.type, dataValue, value);
- }
- }
- }
- }
- }
- }
-
- if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
- 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 value The binding's value
- * @returns This function returns itself so that it may be chained
- * (e.g. `i18nExp(ctx.name)(ctx.title)`)
- *
- * @codeGenApi
- */
-export function ɵɵi18nExp(value: T): typeof ɵɵi18nExp {
- const lView = getLView();
- if (bindingUpdated(lView, nextBindingIndex(), value)) {
- changeMask = changeMask | (1 << shiftsCounter);
- }
- shiftsCounter++;
- return ɵɵi18nExp;
-}
-
-/**
- * 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 tView = getTView();
- ngDevMode && assertDefined(tView, `tView should be defined`);
- const tI18n = tView.data[index + HEADER_OFFSET] as TI18n | I18nUpdateOpCodes;
- let updateOpCodes: I18nUpdateOpCodes;
- let tIcus: TIcu[]|null = null;
- if (Array.isArray(tI18n)) {
- updateOpCodes = tI18n as I18nUpdateOpCodes;
- } else {
- updateOpCodes = (tI18n as TI18n).update;
- tIcus = (tI18n as TI18n).icus;
- }
- const bindingsStartIndex = getBindingIndex() - shiftsCounter - 1;
- const lView = getLView();
- applyUpdateOpCodes(tView, tIcus, lView, updateOpCodes, bindingsStartIndex, changeMask);
-
- // Reset changeMask & maskBit to default for the next update cycle
- changeMask = 0b0;
- shiftsCounter = 0;
- }
-}
-
-/**
- * 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: {
- const resolvedCase = getPluralCase(bindingValue, getLocaleId());
- 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: I18nMutateOpCodes[] = [];
- const removeCodes: I18nMutateOpCodes[] = [];
- const updateCodes: I18nUpdateOpCodes[] = [];
- const vars = [];
- const childIcus: number[][] = [];
- const values = icuExpression.values;
- for (let i = 0; i < values.length; i++) {
- // Each value is an array of strings & other ICU expressions
- const valueArr = 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] = ``;
- }
- }
- 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,
- currentCaseLViewIndex: HEADER_OFFSET +
- expandoStartIndex // expandoStartIndex does not include the header so add it.
- + 1, // The first item stored is the `` anchor so skip it.
- 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 = getInertBodyHelper(getDocument());
- 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: 1, // allocate space for `TIcu.currentCaseLViewIndex`
- childIcus: [],
- create: [],
- remove: [],
- update: []
- };
- if (ngDevMode) {
- attachDebugGetter(opCodes.create, i18nMutateOpCodesToString);
- attachDebugGetter(opCodes.remove, i18nMutateOpCodesToString);
- attachDebugGetter(opCodes.update, i18nUpdateOpCodesToString);
- }
- parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex);
- return opCodes;
-}
-
-const NESTED_ICU = /�(\d+)�/;
-
-/**
- * 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,
- // FIXME(misko): Index should be part of the opcode
- 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,
- // FIXME(misko): Index should be part of the opcode
- nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
- }
- }
-}
-
-/**
- * Angular Dart introduced &ngsp; as a placeholder for non-removable space, see:
- * https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32
- * In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
- * and later on replaced by a space. We are re-implementing the same idea here, since translations
- * might contain this special character.
- */
-const NGSP_UNICODE_REGEXP = /\uE500/g;
-function replaceNgsp(value: string): string {
- return value.replace(NGSP_UNICODE_REGEXP, ' ');
-}
-
-/**
- * The locale id that the application is currently using (for translations and ICU expressions).
- * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine
- * but is now defined as a global value.
- */
-let LOCALE_ID = DEFAULT_LOCALE_ID;
-
-/**
- * Sets the locale id that will be used for translations and ICU expressions.
- * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine
- * but is now defined as a global value.
- *
- * @param localeId
- */
-export function setLocaleId(localeId: string) {
- assertDefined(localeId, `Expected localeId to be defined`);
- if (typeof localeId === 'string') {
- LOCALE_ID = localeId.toLowerCase().replace(/_/g, '-');
- }
-}
-
-/**
- * Gets the locale id that will be used for translations and ICU expressions.
- * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine
- * but is now defined as a global value.
- */
-export function getLocaleId(): string {
- return LOCALE_ID;
-}
diff --git a/packages/core/src/render3/i18n.md b/packages/core/src/render3/i18n/i18n.md
similarity index 100%
rename from packages/core/src/render3/i18n.md
rename to packages/core/src/render3/i18n/i18n.md
diff --git a/packages/core/src/render3/i18n/i18n_apply.ts b/packages/core/src/render3/i18n/i18n_apply.ts
new file mode 100644
index 0000000000..af7728decd
--- /dev/null
+++ b/packages/core/src/render3/i18n/i18n_apply.ts
@@ -0,0 +1,503 @@
+/**
+ * @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 {getPluralCase} from '../../i18n/localization';
+import {assertDefined, assertEqual, assertIndexInRange} from '../../util/assert';
+import {attachPatchData} from '../context_discovery';
+import {elementAttributeInternal, elementPropertyInternal, getOrCreateTNode, textBindingInternal} 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, TNodeFlags, TNodeType, TProjectionNode} from '../interfaces/node';
+import {RComment, RElement, RText} from '../interfaces/renderer';
+import {SanitizerFn} from '../interfaces/sanitization';
+import {isLContainer} from '../interfaces/type_checks';
+import {HEADER_OFFSET, LView, RENDERER, T_HOST, TView} from '../interfaces/view';
+import {appendChild, applyProjection, createTextNode, nativeRemoveNode} from '../node_manipulation';
+import {getBindingIndex, getLView, getPreviousOrParentTNode, getTView, setIsNotParent, setPreviousOrParentTNode} from '../state';
+import {renderStringify} from '../util/misc_utils';
+import {getNativeByIndex, getNativeByTNode, getTNode, load} from '../util/view_utils';
+import {getLocaleId} from './i18n_locale_id';
+
+
+const i18nIndexStack: number[] = [];
+let i18nIndexStackPointer = -1;
+
+function popI18nIndex() {
+ return i18nIndexStack[i18nIndexStackPointer--];
+}
+
+export function pushI18nIndex(index: number) {
+ i18nIndexStack[++i18nIndexStackPointer] = index;
+}
+
+let changeMask = 0b0;
+let shiftsCounter = 0;
+
+export function setMaskBit(bit: boolean) {
+ if (bit) {
+ changeMask = changeMask | (1 << shiftsCounter);
+ }
+ shiftsCounter++;
+}
+
+export function applyI18n(tView: TView, lView: LView, index: number) {
+ if (shiftsCounter > 0) {
+ ngDevMode && assertDefined(tView, `tView should be defined`);
+ const tI18n = tView.data[index + HEADER_OFFSET] as TI18n | I18nUpdateOpCodes;
+ let updateOpCodes: I18nUpdateOpCodes;
+ let tIcus: TIcu[]|null = null;
+ if (Array.isArray(tI18n)) {
+ updateOpCodes = tI18n as I18nUpdateOpCodes;
+ } else {
+ updateOpCodes = (tI18n as TI18n).update;
+ tIcus = (tI18n as TI18n).icus;
+ }
+ const bindingsStartIndex = getBindingIndex() - shiftsCounter - 1;
+ applyUpdateOpCodes(tView, tIcus, lView, updateOpCodes, bindingsStartIndex, changeMask);
+
+ // Reset changeMask & maskBit to default for the next update cycle
+ changeMask = 0b0;
+ shiftsCounter = 0;
+ }
+}
+
+/**
+ * Apply `I18nMutateOpCodes` OpCodes.
+ *
+ * @param tView Current `TView`
+ * @param rootIndex Pointer to the root (parent) tNode for the i18n.
+ * @param createOpCodes OpCodes to process
+ * @param lView Current `LView`
+ */
+export function applyCreateOpCodes(
+ tView: TView, rootindex: number, createOpCodes: I18nMutateOpCodes, lView: LView): number[] {
+ const renderer = lView[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(tView, lView, textNodeIndex, TNodeType.Element, textRNode, null);
+ visitedNodes.push(textNodeIndex);
+ setIsNotParent();
+ } else if (typeof opCode == 'number') {
+ switch (opCode & I18nMutateOpCode.MASK_INSTRUCTION) {
+ case I18nMutateOpCode.AppendChild:
+ const destinationNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_PARENT;
+ let destinationTNode: TNode;
+ if (destinationNodeIndex === rootindex) {
+ // If the destination node is `i18nStart`, we don't have a
+ // top-level node and we should use the host node instead
+ destinationTNode = lView[T_HOST]!;
+ } else {
+ destinationTNode = getTNode(tView, destinationNodeIndex);
+ }
+ ngDevMode &&
+ assertDefined(
+ currentTNode!,
+ `You need to create or select a node before you can insert it into the DOM`);
+ previousTNode =
+ appendI18nNode(tView, currentTNode!, destinationTNode, previousTNode, lView);
+ break;
+ case I18nMutateOpCode.Select:
+ // Negative indices indicate that a given TNode is a sibling node, not a parent node
+ // (see `i18nStartFirstPass` for additional information).
+ const isParent = opCode >= 0;
+ // FIXME(misko): This SHIFT_REF looks suspect as it does not have mask.
+ const nodeIndex = (isParent ? opCode : ~opCode) >>> I18nMutateOpCode.SHIFT_REF;
+ visitedNodes.push(nodeIndex);
+ previousTNode = currentTNode;
+ currentTNode = getTNode(tView, nodeIndex);
+ if (currentTNode) {
+ setPreviousOrParentTNode(currentTNode, isParent);
+ }
+ break;
+ case I18nMutateOpCode.ElementEnd:
+ const elementIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
+ previousTNode = currentTNode = getTNode(tView, elementIndex);
+ setPreviousOrParentTNode(currentTNode, false);
+ break;
+ case I18nMutateOpCode.Attr:
+ const elementNodeIndex = opCode >>> I18nMutateOpCode.SHIFT_REF;
+ const attrName = createOpCodes[++i] as string;
+ const attrValue = createOpCodes[++i] as string;
+ // This code is used for ICU expressions only, since we don't support
+ // directives/components in ICUs, we don't need to worry about inputs here
+ elementAttributeInternal(
+ getTNode(tView, elementNodeIndex), lView, attrName, attrValue, null, null);
+ 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(
+ tView, lView, commentNodeIndex, TNodeType.IcuContainer, commentRNode, null);
+ visitedNodes.push(commentNodeIndex);
+ attachPatchData(commentRNode, lView);
+ // We will add the case nodes later, during the update phase
+ setIsNotParent();
+ 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(
+ tView, lView, elementNodeIndex, TNodeType.Element, elementRNode, tagNameValue);
+ visitedNodes.push(elementNodeIndex);
+ break;
+ default:
+ throw new Error(`Unable to determine the type of mutate operation for "${opCode}"`);
+ }
+ }
+ }
+
+ setIsNotParent();
+
+ return visitedNodes;
+}
+
+
+/**
+ * Apply `I18nUpdateOpCodes` OpCodes
+ *
+ * @param tView Current `TView`
+ * @param tIcus If ICUs present than this contains them.
+ * @param lView Current `LView`
+ * @param updateOpCodes OpCodes to process
+ * @param bindingsStartIndex Location of the first `ɵɵi18nApply`
+ * @param changeMask Each bit corresponds to a `ɵɵi18nExp` (Counting backwards from
+ * `bindingsStartIndex`)
+ */
+export function applyUpdateOpCodes(
+ tView: TView, tIcus: TIcu[]|null, lView: LView, updateOpCodes: I18nUpdateOpCodes,
+ bindingsStartIndex: number, changeMask: number) {
+ 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 (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) {
+ // Negative opCode represent `i18nExp` values offset.
+ value += renderStringify(lView[bindingsStartIndex - opCode]);
+ } else {
+ const nodeIndex = opCode >>> I18nUpdateOpCode.SHIFT_REF;
+ switch (opCode & I18nUpdateOpCode.MASK_OPCODE) {
+ case I18nUpdateOpCode.Attr:
+ const propName = updateOpCodes[++j] as string;
+ const sanitizeFn = updateOpCodes[++j] as SanitizerFn | null;
+ elementPropertyInternal(
+ tView, getTNode(tView, nodeIndex), lView, propName, value, lView[RENDERER],
+ sanitizeFn, false);
+ break;
+ case I18nUpdateOpCode.Text:
+ textBindingInternal(lView, nodeIndex, value);
+ break;
+ case I18nUpdateOpCode.IcuSwitch:
+ caseCreated =
+ applyIcuSwitchCase(tView, tIcus!, updateOpCodes[++j] as number, lView, value);
+ break;
+ case I18nUpdateOpCode.IcuUpdate:
+ applyIcuUpdateCase(
+ tView, tIcus!, updateOpCodes[++j] as number, bindingsStartIndex, lView,
+ caseCreated);
+ break;
+ }
+ }
+ }
+ }
+ }
+ i += skipCodes;
+ }
+}
+
+/**
+ * Apply OpCodes associated with updating an existing ICU.
+ *
+ * @param tView Current `TView`
+ * @param tIcus ICUs active at this location.
+ * @param tIcuIndex Index into `tIcus` to process.
+ * @param bindingsStartIndex Location of the first `ɵɵi18nApply`
+ * @param lView Current `LView`
+ * @param changeMask Each bit corresponds to a `ɵɵi18nExp` (Counting backwards from
+ * `bindingsStartIndex`)
+ */
+function applyIcuUpdateCase(
+ tView: TView, tIcus: TIcu[], tIcuIndex: number, bindingsStartIndex: number, lView: LView,
+ caseCreated: boolean) {
+ ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
+ const tIcu = tIcus[tIcuIndex];
+ ngDevMode && assertIndexInRange(lView, tIcu.currentCaseLViewIndex);
+ const activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
+ if (activeCaseIndex !== null) {
+ const mask = caseCreated ?
+ -1 : // -1 is same as all bits on, which simulates creation since it marks all bits dirty
+ changeMask;
+ applyUpdateOpCodes(tView, tIcus, lView, tIcu.update[activeCaseIndex], bindingsStartIndex, mask);
+ }
+}
+
+/**
+ * Apply OpCodes associated with switching a case on ICU.
+ *
+ * This involves tearing down existing case and than building up a new case.
+ *
+ * @param tView Current `TView`
+ * @param tIcus ICUs active at this location.
+ * @param tICuIndex Index into `tIcus` to process.
+ * @param lView Current `LView`
+ * @param value Value of the case to update to.
+ * @returns true if a new case was created (needed so that the update executes regardless of the
+ * bitmask)
+ */
+function applyIcuSwitchCase(
+ tView: TView, tIcus: TIcu[], tICuIndex: number, lView: LView, value: string): boolean {
+ applyIcuSwitchCaseRemove(tView, tIcus, tICuIndex, lView);
+
+ // Rebuild a new case for this ICU
+ let caseCreated = false;
+ const tIcu = tIcus[tICuIndex];
+ const caseIndex = getCaseIndex(tIcu, value);
+ lView[tIcu.currentCaseLViewIndex] = caseIndex !== -1 ? caseIndex : null;
+ if (caseIndex > -1) {
+ // Add the nodes for the new case
+ applyCreateOpCodes(
+ tView, -1, // -1 means we don't have parent node
+ tIcu.create[caseIndex], lView);
+ caseCreated = true;
+ }
+ return caseCreated;
+}
+
+/**
+ * Apply OpCodes associated with tearing down of DOM.
+ *
+ * This involves tearing down existing case and than building up a new case.
+ *
+ * @param tView Current `TView`
+ * @param tIcus ICUs active at this location.
+ * @param tIcuIndex Index into `tIcus` to process.
+ * @param lView Current `LView`
+ * @returns true if a new case was created (needed so that the update executes regardless of the
+ * bitmask)
+ */
+function applyIcuSwitchCaseRemove(tView: TView, tIcus: TIcu[], tIcuIndex: number, lView: LView) {
+ ngDevMode && assertIndexInRange(tIcus, tIcuIndex);
+ const tIcu = tIcus[tIcuIndex];
+ const activeCaseIndex = lView[tIcu.currentCaseLViewIndex];
+ if (activeCaseIndex !== null) {
+ const removeCodes = tIcu.remove[activeCaseIndex];
+ for (let k = 0; k < removeCodes.length; k++) {
+ const removeOpCode = removeCodes[k] as number;
+ const nodeOrIcuIndex = removeOpCode >>> I18nMutateOpCode.SHIFT_REF;
+ switch (removeOpCode & I18nMutateOpCode.MASK_INSTRUCTION) {
+ case I18nMutateOpCode.Remove:
+ // FIXME(misko): this comment is wrong!
+ // Remove DOM element, but do *not* mark TNode as detached, since we are
+ // just switching ICU cases (while keeping the same TNode), so a DOM element
+ // representing a new ICU case will be re-created.
+ removeNode(tView, lView, nodeOrIcuIndex, /* markAsDetached */ false);
+ break;
+ case I18nMutateOpCode.RemoveNestedIcu:
+ applyIcuSwitchCaseRemove(tView, tIcus, nodeOrIcuIndex, lView);
+ break;
+ }
+ }
+ }
+}
+
+function appendI18nNode(
+ tView: TView, tNode: TNode, parentTNode: TNode, previousTNode: TNode|null,
+ lView: LView): TNode {
+ ngDevMode && ngDevMode.rendererMoveNode++;
+ const nextNode = tNode.next;
+ 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 !== lView[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;
+ }
+
+ // If the placeholder to append is a projection, we need to move the projected nodes instead
+ if (tNode.type === TNodeType.Projection) {
+ applyProjection(tView, lView, tNode as TProjectionNode);
+ return tNode;
+ }
+
+ appendChild(tView, lView, getNativeByTNode(tNode, lView), tNode);
+
+ const slotValue = lView[tNode.index];
+ if (tNode.type !== TNodeType.Container && isLContainer(slotValue)) {
+ // Nodes that inject ViewContainerRef also have a comment node that should be moved
+ appendChild(tView, lView, slotValue[NATIVE], tNode);
+ }
+ return tNode;
+}
+
+/**
+ * See `i18nEnd` above.
+ */
+export function i18nEndFirstPass(tView: TView, lView: LView) {
+ ngDevMode &&
+ assertEqual(
+ getBindingIndex(), tView.bindingStartIndex,
+ 'i18nEnd should be called before any binding');
+
+ const rootIndex = popI18nIndex();
+ 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`
+ const lastCreatedNode = getPreviousOrParentTNode();
+
+ // Read the instructions to insert/move/remove DOM elements
+ const visitedNodes = applyCreateOpCodes(tView, rootIndex, tI18n.create, lView);
+
+ // Remove deleted nodes
+ let index = rootIndex + 1;
+ while (index <= lastCreatedNode.index - HEADER_OFFSET) {
+ if (visitedNodes.indexOf(index) === -1) {
+ removeNode(tView, lView, index, /* markAsDetached */ true);
+ }
+ // Check if an element has any local refs and skip them
+ const tNode = getTNode(tView, index);
+ if (tNode &&
+ (tNode.type === TNodeType.Container || tNode.type === TNodeType.Element ||
+ tNode.type === TNodeType.ElementContainer) &&
+ tNode.localNames !== null) {
+ // Divide by 2 to get the number of local refs,
+ // since they are stored as an array that also includes directive indexes,
+ // i.e. ["localRef", directiveIndex, ...]
+ index += tNode.localNames.length >> 1;
+ }
+ index++;
+ }
+}
+
+function removeNode(tView: TView, lView: LView, index: number, markAsDetached: boolean) {
+ const removedPhTNode = getTNode(tView, index);
+ const removedPhRNode = getNativeByIndex(index, lView);
+ if (removedPhRNode) {
+ nativeRemoveNode(lView[RENDERER], removedPhRNode);
+ }
+
+ const slotValue = load(lView, index) as RElement | RComment | LContainer;
+ if (isLContainer(slotValue)) {
+ const lContainer = slotValue as LContainer;
+ if (removedPhTNode.type !== TNodeType.Container) {
+ nativeRemoveNode(lView[RENDERER], lContainer[NATIVE]);
+ }
+ }
+
+ if (markAsDetached) {
+ // Define this node as detached to avoid projecting it later
+ removedPhTNode.flags |= TNodeFlags.isDetached;
+ }
+ ngDevMode && ngDevMode.rendererRemoveNode++;
+}
+
+/**
+ * Creates and stores the dynamic TNode, and unhooks it from the tree for now.
+ */
+function createDynamicNodeAtIndex(
+ tView: TView, lView: LView, index: number, type: TNodeType, native: RElement|RText|null,
+ name: string|null): TElementNode|TIcuContainerNode {
+ const previousOrParentTNode = getPreviousOrParentTNode();
+ ngDevMode && assertIndexInRange(lView, index + HEADER_OFFSET);
+ lView[index + HEADER_OFFSET] = native;
+ // FIXME(misko): Why does this create A TNode??? I would not expect this to be here.
+ const tNode = getOrCreateTNode(tView, lView[T_HOST], index, type as any, 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 && previousOrParentTNode.next === tNode) {
+ previousOrParentTNode.next = null;
+ }
+
+ return tNode;
+}
+
+
+/**
+ * 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: {
+ const resolvedCase = getPluralCase(bindingValue, getLocaleId());
+ 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;
+}
diff --git a/packages/core/src/render3/i18n_debug.ts b/packages/core/src/render3/i18n/i18n_debug.ts
similarity index 98%
rename from packages/core/src/render3/i18n_debug.ts
rename to packages/core/src/render3/i18n/i18n_debug.ts
index 930f810428..9c89b86e66 100644
--- a/packages/core/src/render3/i18n_debug.ts
+++ b/packages/core/src/render3/i18n/i18n_debug.ts
@@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {assertNumber, assertString} from '../util/assert';
+import {assertNumber, assertString} from '../../util/assert';
-import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from './interfaces/i18n';
+import {COMMENT_MARKER, ELEMENT_MARKER, getInstructionFromI18nMutateOpCode, getParentFromI18nMutateOpCode, getRefFromI18nMutateOpCode, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes} from '../interfaces/i18n';
/**
* Converts `I18nUpdateOpCodes` array into a human readable format.
diff --git a/packages/core/src/render3/i18n/i18n_locale_id.ts b/packages/core/src/render3/i18n/i18n_locale_id.ts
new file mode 100644
index 0000000000..66d9074854
--- /dev/null
+++ b/packages/core/src/render3/i18n/i18n_locale_id.ts
@@ -0,0 +1,41 @@
+/**
+ * @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 {DEFAULT_LOCALE_ID} from '../../i18n/localization';
+import {assertDefined} from '../../util/assert';
+
+
+/**
+ * The locale id that the application is currently using (for translations and ICU expressions).
+ * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine
+ * but is now defined as a global value.
+ */
+let LOCALE_ID = DEFAULT_LOCALE_ID;
+
+/**
+ * Sets the locale id that will be used for translations and ICU expressions.
+ * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine
+ * but is now defined as a global value.
+ *
+ * @param localeId
+ */
+export function setLocaleId(localeId: string) {
+ assertDefined(localeId, `Expected localeId to be defined`);
+ if (typeof localeId === 'string') {
+ LOCALE_ID = localeId.toLowerCase().replace(/_/g, '-');
+ }
+}
+
+/**
+ * Gets the locale id that will be used for translations and ICU expressions.
+ * This is the ivy version of `LOCALE_ID` that was defined as an injection token for the view engine
+ * but is now defined as a global value.
+ */
+export function getLocaleId(): string {
+ return LOCALE_ID;
+}
diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts
new file mode 100644
index 0000000000..3794965243
--- /dev/null
+++ b/packages/core/src/render3/i18n/i18n_parse.ts
@@ -0,0 +1,733 @@
+/**
+ * @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 '../../util/ng_dev_mode';
+import '../../util/ng_i18n_closure_mode';
+
+import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../../sanitization/html_sanitizer';
+import {getInertBodyHelper} from '../../sanitization/inert_body';
+import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer';
+import {addAllToArray} from '../../util/array_utils';
+import {assertEqual} from '../../util/assert';
+import {allocExpando, elementAttributeInternal, setInputsForProperty, setNgReflectProperties} from '../instructions/shared';
+import {getDocument} from '../interfaces/document';
+import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuCase, IcuExpression, IcuType, TI18n, TIcu} from '../interfaces/i18n';
+import {TNodeType} from '../interfaces/node';
+import {RComment, RElement} from '../interfaces/renderer';
+import {SanitizerFn} from '../interfaces/sanitization';
+import {HEADER_OFFSET, LView, T_HOST, TView} from '../interfaces/view';
+import {getIsParent, getPreviousOrParentTNode} from '../state';
+import {attachDebugGetter} from '../util/debug_utils';
+import {getNativeByIndex, getTNode} from '../util/view_utils';
+
+import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from './i18n_debug';
+
+
+
+const BINDING_REGEXP = /�(\d+):?\d*�/gi;
+const ICU_REGEXP = /({\s*�\d+:?\d*�\s*,\s*\S{6}\s*,[\s\S]*})/gi;
+const NESTED_ICU = /�(\d+)�/;
+const ICU_BLOCK_REGEXP = /^\s*(�\d+:?\d*�)\s*,\s*(select|plural)\s*,/;
+
+
+// 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;
+
+const parentIndexStack: number[] = [];
+
+const MARKER = `�`;
+const SUBTEMPLATE_REGEXP = /�\/?\*(\d+:\d+)�/gi;
+const PH_REGEXP = /�(\/?[#*!]\d+):?\d*�/gi;
+const enum TagType {
+ ELEMENT = '#',
+ TEMPLATE = '*',
+ PROJECTION = '!',
+}
+
+/**
+ * Angular Dart introduced &ngsp; as a placeholder for non-removable space, see:
+ * https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32
+ * In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
+ * and later on replaced by a space. We are re-implementing the same idea here, since translations
+ * might contain this special character.
+ */
+const NGSP_UNICODE_REGEXP = /\uE500/g;
+function replaceNgsp(value: string): string {
+ return value.replace(NGSP_UNICODE_REGEXP, ' ');
+}
+
+
+/**
+ * See `i18nStart` above.
+ */
+export function i18nStartFirstPass(
+ lView: LView, tView: TView, index: number, message: string, subTemplateIndex?: number) {
+ const startIndex = tView.blueprint.length - HEADER_OFFSET;
+ i18nVarsCount = 0;
+ const previousOrParentTNode = getPreviousOrParentTNode();
+ const parentTNode =
+ getIsParent() ? previousOrParentTNode : previousOrParentTNode && previousOrParentTNode.parent;
+ let parentIndex =
+ parentTNode && parentTNode !== lView[T_HOST] ? parentTNode.index - HEADER_OFFSET : index;
+ let parentIndexPointer = 0;
+ parentIndexStack[parentIndexPointer] = parentIndex;
+ const createOpCodes: I18nMutateOpCodes = [];
+ if (ngDevMode) {
+ attachDebugGetter(createOpCodes, i18nMutateOpCodesToString);
+ }
+ // 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. We should also
+ // keep track whether an element was a parent node or not, so that the logic that consumes
+ // the generated `I18nMutateOpCode`s can leverage this information to properly set TNode state
+ // (whether it's a parent or sibling).
+ if (index > 0 && previousOrParentTNode !== parentTNode) {
+ let previousTNodeIndex = previousOrParentTNode.index - HEADER_OFFSET;
+ // If current TNode is a sibling node, encode it using a negative index. This information is
+ // required when the `Select` action is processed (see the `readCreateOpCodes` function).
+ if (!getIsParent()) {
+ previousTNodeIndex = ~previousTNodeIndex;
+ }
+ // Create an OpCode to select the previous TNode
+ createOpCodes.push(previousTNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select);
+ }
+ const updateOpCodes: I18nUpdateOpCodes = [];
+ if (ngDevMode) {
+ attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
+ }
+ const icuExpressions: TIcu[] = [];
+
+ if (message === '' && isRootTemplateMessage(subTemplateIndex)) {
+ // If top level translation is an empty string, do not invoke additional processing
+ // and just create op codes for empty text node instead.
+ createOpCodes.push(
+ message, allocNodeIndex(startIndex),
+ parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
+ } else {
+ const templateTranslation = getTranslationForTemplate(message, subTemplateIndex);
+ const msgParts = replaceNgsp(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) === TagType.ELEMENT) {
+ 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);
+ const isElement = value.charAt(0) === TagType.ELEMENT;
+ // The value represents a placeholder that we move to the designated index.
+ // Note: positive indicies indicate that a TNode with a given index should also be marked
+ // as parent while executing `Select` instruction.
+ createOpCodes.push(
+ (isElement ? phIndex : ~phIndex) << I18nMutateOpCode.SHIFT_REF |
+ I18nMutateOpCode.Select,
+ parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
+
+ if (isElement) {
+ 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
+ const icuExpression = parts[j] as IcuExpression;
+
+ // Verify that ICU expression has the right shape. Translations might contain invalid
+ // constructions (while original messages were correct), so ICU parsing at runtime may
+ // not succeed (thus `icuExpression` remains a string).
+ if (typeof icuExpression !== 'object') {
+ throw new Error(
+ `Unable to parse ICU expression in "${templateTranslation}" message.`);
+ }
+
+ // Create the comment node that will anchor the ICU expression
+ const icuNodeIndex = allocNodeIndex(startIndex);
+ createOpCodes.push(
+ COMMENT_MARKER, ngDevMode ? `ICU ${icuNodeIndex}` : '', icuNodeIndex,
+ parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
+
+ // Update codes for the ICU expression
+ 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 = allocNodeIndex(startIndex);
+ 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);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (i18nVarsCount > 0) {
+ allocExpando(tView, lView, i18nVarsCount);
+ }
+
+ // 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;
+}
+
+/**
+ * See `i18nAttributes` above.
+ */
+export function i18nAttributesFirstPass(
+ lView: LView, tView: TView, index: number, values: string[]) {
+ const previousElement = getPreviousOrParentTNode();
+ const previousElementIndex = previousElement.index - HEADER_OFFSET;
+ const updateOpCodes: I18nUpdateOpCodes = [];
+ if (ngDevMode) {
+ attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
+ }
+ 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
+ throw new Error('ICU expressions are not yet supported in attributes');
+ } else if (value !== '') {
+ // Even indexes are text (including bindings)
+ const hasBinding = !!value.match(BINDING_REGEXP);
+ if (hasBinding) {
+ if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
+ addAllToArray(
+ generateBindingUpdateOpCodes(value, previousElementIndex, attrName), updateOpCodes);
+ }
+ } else {
+ const tNode = getTNode(tView, previousElementIndex);
+ // Set attributes for Elements only, for other types (like ElementContainer),
+ // only set inputs below
+ if (tNode.type === TNodeType.Element) {
+ elementAttributeInternal(tNode, lView, attrName, value, null, null);
+ }
+ // Check if that attribute is a directive input
+ const dataValue = tNode.inputs !== null && tNode.inputs[attrName];
+ if (dataValue) {
+ setInputsForProperty(tView, lView, dataValue, attrName, value);
+ if (ngDevMode) {
+ const element = getNativeByIndex(previousElementIndex, lView) as RElement | RComment;
+ setNgReflectProperties(lView, element, tNode.type, dataValue, value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
+ tView.data[index + HEADER_OFFSET] = updateOpCodes;
+ }
+}
+
+
+/**
+ * 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.
+ */
+export function generateBindingUpdateOpCodes(
+ str: string, destinationNode: number, attrName?: string,
+ sanitizeFn: SanitizerFn|null = null): I18nUpdateOpCodes {
+ const updateOpCodes: I18nUpdateOpCodes = [null, null]; // Alloc space for mask and size
+ if (ngDevMode) {
+ attachDebugGetter(updateOpCodes, i18nUpdateOpCodesToString);
+ }
+ 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;
+}
+
+function allocNodeIndex(startIndex: number): number {
+ return startIndex + i18nVarsCount++;
+}
+
+
+/**
+ * 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);
+}
+
+export function isRootTemplateMessage(subTemplateIndex: number|
+ undefined): subTemplateIndex is undefined {
+ return subTemplateIndex === undefined;
+}
+
+
+/**
+ * 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:
+ * ```
+ * Translate me!
+ * ```
+ *
+ * @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 (isRootTemplateMessage(subTemplateIndex)) {
+ // 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 for ICU expressions.
+ *
+ * @param tIcus
+ * @param icuExpression
+ * @param startIndex
+ * @param expandoStartIndex
+ */
+export function icuStart(
+ tIcus: TIcu[], icuExpression: IcuExpression, startIndex: number,
+ expandoStartIndex: number): void {
+ const createCodes: I18nMutateOpCodes[] = [];
+ const removeCodes: I18nMutateOpCodes[] = [];
+ const updateCodes: I18nUpdateOpCodes[] = [];
+ const vars = [];
+ const childIcus: number[][] = [];
+ const values = icuExpression.values;
+ for (let i = 0; i < values.length; i++) {
+ // Each value is an array of strings & other ICU expressions
+ const valueArr = 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] = ``;
+ }
+ }
+ 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,
+ currentCaseLViewIndex: HEADER_OFFSET +
+ expandoStartIndex // expandoStartIndex does not include the header so add it.
+ + 1, // The first item stored is the `` anchor so skip it.
+ 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);
+}
+
+/**
+ * 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.
+ *
+ */
+export 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 (cases.length > values.length) {
+ values.push(blocks);
+ }
+ }
+
+ // TODO(ocombe): support ICU expressions in attributes, see #21615
+ return {type: icuType, mainBinding: mainBinding, cases, values};
+}
+
+
+/**
+ * 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 = getInertBodyHelper(getDocument());
+ 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: 1, // allocate space for `TIcu.currentCaseLViewIndex`
+ childIcus: [],
+ create: [],
+ remove: [],
+ update: []
+ };
+ if (ngDevMode) {
+ attachDebugGetter(opCodes.create, i18nMutateOpCodesToString);
+ attachDebugGetter(opCodes.remove, i18nMutateOpCodesToString);
+ attachDebugGetter(opCodes.update, i18nUpdateOpCodesToString);
+ }
+ parseNodes(wrapper.firstChild, opCodes, parentIndex, nestedIcus, tIcus, expandoStartIndex);
+ return opCodes;
+}
+
+/**
+ * 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 {
+ 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);
+ results.push(substring);
+ return results;
+}
+
+
+/**
+ * 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
+ */
+export 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,
+ // FIXME(misko): Index should be part of the opcode
+ 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,
+ // FIXME(misko): Index should be part of the opcode
+ nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
+ }
+ }
+}
diff --git a/packages/core/src/render3/i18n/i18n_postprocess.ts b/packages/core/src/render3/i18n/i18n_postprocess.ts
new file mode 100644
index 0000000000..10ecf87f09
--- /dev/null
+++ b/packages/core/src/render3/i18n/i18n_postprocess.ts
@@ -0,0 +1,134 @@
+/**
+ * @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
+ */
+
+// i18nPostprocess consts
+const ROOT_TEMPLATE_ID = 0;
+const PP_MULTI_VALUE_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]/;
+const PP_PLACEHOLDERS_REGEXP = /\[(�.+?�?)\]|(�\/?\*\d+:\d+�)/g;
+const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
+const PP_ICU_PLACEHOLDERS_REGEXP = /{([A-Z0-9_]+)}/g;
+const PP_ICUS_REGEXP = /�I18N_EXP_(ICU(_\d+)?)�/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];
+
+
+/**
+ * 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 [�*1:1��#2:1�|�#4:1�|�5�])
+ * 2. Replace all ICU vars (like "VAR_PLURAL")
+ * 3. Replace all placeholders used inside ICUs in a form of {PLACEHOLDER}
+ * 4. Replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�)
+ * 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 [�#5�|�*1:1��#2:1�|�#4:1�]
+ *
+ * Note: due to the way we process nested templates (BFS), multi-value placeholders are typically
+ * grouped by templates, for example: [�#5�|�#6�|�#1:1�|�#3:2�] where �#5� and �#6� belong to root
+ * template, �#1:1� belong to nested template with index 1 and �#1:2� - nested template with index
+ * 3. However in real templates the order might be different: i.e. �#1:1� and/or �#3:2� may go in
+ * front of �#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;
+ const placeholders: PostprocessPlaceholder[] = matches[content] || [];
+ if (!placeholders.length) {
+ 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 (!placeholders.length) {
+ throw new Error(`i18n postprocess: unmatched placeholder - ${content}`);
+ }
+
+ const currentTemplateId = templateIdsStack[templateIdsStack.length - 1];
+ 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;
+ });
+ }
+
+ // 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 placeholders used inside ICUs in a form of {PLACEHOLDER}
+ */
+ result = result.replace(PP_ICU_PLACEHOLDERS_REGEXP, (match, key): string => {
+ return replacements.hasOwnProperty(key) ? replacements[key] as string : match;
+ });
+
+ /**
+ * Step 4: replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�) 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;
+}
diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts
index 85764cece7..4374446383 100644
--- a/packages/core/src/render3/index.ts
+++ b/packages/core/src/render3/index.ts
@@ -16,7 +16,7 @@ import {getComponent, getDirectives, getHostElement, getRenderedText} from './ut
export {ComponentFactory, ComponentFactoryResolver, ComponentRef, injectComponentFactoryResolver} from './component_ref';
export {ɵɵgetFactoryOf, ɵɵgetInheritedFactory} from './di';
-export {getLocaleId, setLocaleId, ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp, ɵɵi18nPostprocess, ɵɵi18nStart,} from './i18n';
+export {getLocaleId, setLocaleId} from './i18n/i18n_locale_id';
// clang-format off
export {
detectChanges,
@@ -129,6 +129,7 @@ export {
ɵɵtextInterpolate8,
ɵɵtextInterpolateV,
} from './instructions/all';
+export {ɵɵi18n, ɵɵi18nApply, ɵɵi18nAttributes, ɵɵi18nEnd, ɵɵi18nExp,ɵɵi18nPostprocess, ɵɵi18nStart} from './instructions/i18n';
export {RenderFlags} from './interfaces/definition';
export {
AttributeMarker
diff --git a/packages/core/src/render3/instructions/i18n.ts b/packages/core/src/render3/instructions/i18n.ts
new file mode 100644
index 0000000000..bcc865e8d2
--- /dev/null
+++ b/packages/core/src/render3/instructions/i18n.ts
@@ -0,0 +1,176 @@
+/**
+ * @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 '../../util/ng_dev_mode';
+import '../../util/ng_i18n_closure_mode';
+
+import {assertDefined} from '../../util/assert';
+import {bindingUpdated} from '../bindings';
+import {applyI18n, i18nEndFirstPass, pushI18nIndex, setMaskBit} from '../i18n/i18n_apply';
+import {i18nAttributesFirstPass, i18nStartFirstPass} from '../i18n/i18n_parse';
+import {i18nPostprocess} from '../i18n/i18n_postprocess';
+import {HEADER_OFFSET} from '../interfaces/view';
+import {getLView, getTView, nextBindingIndex} from '../state';
+
+import {setDelayProjection} from './all';
+
+/**
+ * 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:
+ * - `�{index}(:{block})�`: *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.
+ * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *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.
+ * - `�!{index}(:{block})�`/`�/!{index}(:{block})�`: *Projection Placeholder*: Marks the
+ * beginning and end of that was 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.
+ * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *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 = getTView();
+ ngDevMode && assertDefined(tView, `tView should be defined`);
+ pushI18nIndex(index);
+ // We need to delay projections until `i18nEnd`
+ setDelayProjection(true);
+ if (tView.firstCreatePass && tView.data[index + HEADER_OFFSET] === null) {
+ i18nStartFirstPass(getLView(), tView, index, message, subTemplateIndex);
+ }
+}
+
+
+
+/**
+ * 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 lView = getLView();
+ const tView = getTView();
+ ngDevMode && assertDefined(tView, `tView should be defined`);
+ i18nEndFirstPass(tView, lView);
+ // Stop delaying projections
+ setDelayProjection(false);
+}
+
+/**
+ *
+ * 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:
+ * - `�{index}(:{block})�`: *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.
+ * - `�#{index}(:{block})�`/`�/#{index}(:{block})�`: *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.
+ * - `�*{index}:{block}�`/`�/*{index}:{block}�`: *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 lView = getLView();
+ const tView = getTView();
+ ngDevMode && assertDefined(tView, `tView should be defined`);
+ i18nAttributesFirstPass(lView, tView, index, values);
+}
+
+
+/**
+ * Stores the values of the bindings during each update cycle in order to determine if we need to
+ * update the translated nodes.
+ *
+ * @param value The binding's value
+ * @returns This function returns itself so that it may be chained
+ * (e.g. `i18nExp(ctx.name)(ctx.title)`)
+ *
+ * @codeGenApi
+ */
+export function ɵɵi18nExp(value: T): typeof ɵɵi18nExp {
+ const lView = getLView();
+ setMaskBit(bindingUpdated(lView, nextBindingIndex(), value));
+ return ɵɵi18nExp;
+}
+
+/**
+ * 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) {
+ applyI18n(getTView(), getLView(), index);
+}
+
+/**
+ * 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 [�*1:1��#2:1�|�#4:1�|�5�])
+ * 2. Replace all ICU vars (like "VAR_PLURAL")
+ * 3. Replace all placeholders used inside ICUs in a form of {PLACEHOLDER}
+ * 4. Replace all ICU references with corresponding values (like �ICU_EXP_ICU_1�)
+ * 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 {
+ return i18nPostprocess(message, replacements);
+}
\ No newline at end of file
diff --git a/packages/core/src/render3/interfaces/i18n.ts b/packages/core/src/render3/interfaces/i18n.ts
index 7b85b495f5..7270e0ece9 100644
--- a/packages/core/src/render3/interfaces/i18n.ts
+++ b/packages/core/src/render3/interfaces/i18n.ts
@@ -411,3 +411,41 @@ export interface TIcu {
// Note: This hack is necessary so we don't erroneously get a circular dependency
// failure based on types.
export const unusedValueExportToPlacateAjd = 1;
+
+export interface IcuExpression {
+ type: IcuType;
+ mainBinding: number;
+ cases: string[];
+ values: (string|IcuExpression)[][];
+}
+
+export 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;
+}
diff --git a/packages/core/src/render3/ng_module_ref.ts b/packages/core/src/render3/ng_module_ref.ts
index 29d19e2b62..4cb7227c6f 100644
--- a/packages/core/src/render3/ng_module_ref.ts
+++ b/packages/core/src/render3/ng_module_ref.ts
@@ -20,7 +20,7 @@ import {stringify} from '../util/stringify';
import {ComponentFactoryResolver} from './component_ref';
import {getNgLocaleIdDef, getNgModuleDef} from './definition';
-import {setLocaleId} from './i18n';
+import {setLocaleId} from './i18n/i18n_locale_id';
import {maybeUnwrapFn} from './util/misc_utils';
export interface NgModuleType extends Type {
diff --git a/packages/core/test/render3/i18n_debug_spec.ts b/packages/core/test/render3/i18n_debug_spec.ts
index 50c1abd43d..68fceaf27b 100644
--- a/packages/core/test/render3/i18n_debug_spec.ts
+++ b/packages/core/test/render3/i18n_debug_spec.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from '@angular/core/src/render3/i18n_debug';
+import {i18nMutateOpCodesToString, i18nUpdateOpCodesToString} from '@angular/core/src/render3/i18n/i18n_debug';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nUpdateOpCode} from '@angular/core/src/render3/interfaces/i18n';
describe('i18n debug', () => {
diff --git a/packages/core/test/render3/i18n_spec.ts b/packages/core/test/render3/i18n_spec.ts
index 20f2df3272..249a18ffc7 100644
--- a/packages/core/test/render3/i18n_spec.ts
+++ b/packages/core/test/render3/i18n_spec.ts
@@ -6,12 +6,15 @@
* found in the LICENSE file at https://angular.io/license
*/
+import {ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '@angular/core';
+import {getTranslationForTemplate} from '@angular/core/src/render3/i18n/i18n_parse';
+
import {noop} from '../../../compiler/src/render3/view/util';
-import {getTranslationForTemplate, ɵɵi18nAttributes, ɵɵi18nPostprocess, ɵɵi18nStart} from '../../src/render3/i18n';
import {setDelayProjection, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/instructions/all';
import {I18nUpdateOpCodes, TI18n, TIcu} from '../../src/render3/interfaces/i18n';
import {HEADER_OFFSET, LView, TVIEW} from '../../src/render3/interfaces/view';
import {getNativeByIndex} from '../../src/render3/util/view_utils';
+
import {TemplateFixture} from './render_util';
import {debugMatch} from './utils';