From 250e299dc32de91b4b94f7f43be84bfd6968b020 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 6 Aug 2020 14:01:59 -0700 Subject: [PATCH] refactor(core): break `i18n.ts` into smaller files (#38368) This commit contains no changes to code. It only breaks `i18n.ts` file into `i18n.ts` + `i18n_apply.ts` + `i18n_parse.ts` + `i18n_postprocess.ts` for easier maintenance. PR Close #38368 --- goldens/circular-deps/packages.json | 5 +- .../size-tracking/integration-payloads.json | 2 +- packages/core/src/application_module.ts | 2 +- packages/core/src/application_ref.ts | 2 +- packages/core/src/render3/i18n.ts | 1516 ----------------- packages/core/src/render3/{ => i18n}/i18n.md | 0 packages/core/src/render3/i18n/i18n_apply.ts | 503 ++++++ .../core/src/render3/{ => i18n}/i18n_debug.ts | 4 +- .../core/src/render3/i18n/i18n_locale_id.ts | 41 + packages/core/src/render3/i18n/i18n_parse.ts | 733 ++++++++ .../core/src/render3/i18n/i18n_postprocess.ts | 134 ++ packages/core/src/render3/index.ts | 3 +- .../core/src/render3/instructions/i18n.ts | 176 ++ packages/core/src/render3/interfaces/i18n.ts | 38 + packages/core/src/render3/ng_module_ref.ts | 2 +- packages/core/test/render3/i18n_debug_spec.ts | 2 +- packages/core/test/render3/i18n_spec.ts | 5 +- 17 files changed, 1641 insertions(+), 1527 deletions(-) delete mode 100644 packages/core/src/render3/i18n.ts rename packages/core/src/render3/{ => i18n}/i18n.md (100%) create mode 100644 packages/core/src/render3/i18n/i18n_apply.ts rename packages/core/src/render3/{ => i18n}/i18n_debug.ts (98%) create mode 100644 packages/core/src/render3/i18n/i18n_locale_id.ts create mode 100644 packages/core/src/render3/i18n/i18n_parse.ts create mode 100644 packages/core/src/render3/i18n/i18n_postprocess.ts create mode 100644 packages/core/src/render3/instructions/i18n.ts 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';