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
This commit is contained in:
parent
8f708b561c
commit
250e299dc3
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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';
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
|
@ -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 = /<2F>(\d+):?\d*<2A>/gi;
|
||||
const ICU_REGEXP = /({\s*<2A>\d+:?\d*<2A>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
|
||||
const NESTED_ICU = /<2F>(\d+)<29>/;
|
||||
const ICU_BLOCK_REGEXP = /^\s*(<28>\d+:?\d*<2A>)\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 = `<EFBFBD>`;
|
||||
const SUBTEMPLATE_REGEXP = /<2F>\/?\*(\d+:\d+)<29>/gi;
|
||||
const PH_REGEXP = /<2F>(\/?[#*!]\d+):?\d*<2A>/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:
|
||||
* ```
|
||||
* <div i18n>Translate <span *ngIf>me</span>!</div>
|
||||
* ```
|
||||
*
|
||||
* @param message The message to crop
|
||||
* @param subTemplateIndex Index of the sub-template to extract. If undefined it returns the
|
||||
* external template and removes all sub-templates.
|
||||
*/
|
||||
export function getTranslationForTemplate(message: string, subTemplateIndex?: number) {
|
||||
if (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] = `<!--<2D>${icuIndex}<EFBFBD>-->`;
|
||||
}
|
||||
}
|
||||
const icuCase: IcuCase =
|
||||
parseIcuCase(valueArr.join(''), startIndex, nestedIcus, tIcus, expandoStartIndex);
|
||||
createCodes.push(icuCase.create);
|
||||
removeCodes.push(icuCase.remove);
|
||||
updateCodes.push(icuCase.update);
|
||||
vars.push(icuCase.vars);
|
||||
childIcus.push(icuCase.childIcus);
|
||||
}
|
||||
const tIcu: TIcu = {
|
||||
type: icuExpression.type,
|
||||
vars,
|
||||
currentCaseLViewIndex: HEADER_OFFSET +
|
||||
expandoStartIndex // expandoStartIndex does not include the header so add it.
|
||||
+ 1, // The first item stored is the `<!--ICU #-->` 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = /\[(<28>.+?<3F>?)\]/;
|
||||
const PP_PLACEHOLDERS_REGEXP = /\[(<28>.+?<3F>?)\]|(<28>\/?\*\d+:\d+<2B>)/g;
|
||||
const PP_ICU_VARS_REGEXP = /({\s*)(VAR_(PLURAL|SELECT)(_\d+)?)(\s*,)/g;
|
||||
const PP_ICU_PLACEHOLDERS_REGEXP = /{([A-Z0-9_]+)}/g;
|
||||
const PP_ICUS_REGEXP = /<2F>I18N_EXP_(ICU(_\d+)?)<29>/g;
|
||||
const PP_CLOSE_TEMPLATE_REGEXP = /\/\*/;
|
||||
const PP_TEMPLATE_ID_REGEXP = /\d+\:(\d+)/;
|
||||
|
||||
// Parsed placeholder structure used in postprocessing (within `i18nPostprocess` function)
|
||||
// Contains the following fields: [templateId, isCloseTemplateTag, placeholder]
|
||||
type PostprocessPlaceholder = [number, boolean, string];
|
||||
|
||||
|
||||
/**
|
||||
* 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 [<EFBFBD>*1:1<EFBFBD><EFBFBD>#2:1<EFBFBD>|<EFBFBD>#4:1<EFBFBD>|<EFBFBD>5<EFBFBD>])
|
||||
* 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 <EFBFBD>ICU_EXP_ICU_1<EFBFBD>)
|
||||
* 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 [<EFBFBD>#5<EFBFBD>|<EFBFBD>*1:1<EFBFBD><EFBFBD>#2:1<EFBFBD>|<EFBFBD>#4:1<EFBFBD>]
|
||||
*
|
||||
* Note: due to the way we process nested templates (BFS), multi-value placeholders are typically
|
||||
* grouped by templates, for example: [<EFBFBD>#5<EFBFBD>|<EFBFBD>#6<EFBFBD>|<EFBFBD>#1:1<EFBFBD>|<EFBFBD>#3:2<EFBFBD>] where <EFBFBD>#5<EFBFBD> and <EFBFBD>#6<EFBFBD> belong to root
|
||||
* template, <EFBFBD>#1:1<EFBFBD> belong to nested template with index 1 and <EFBFBD>#1:2<EFBFBD> - nested template with index
|
||||
* 3. However in real templates the order might be different: i.e. <EFBFBD>#1:1<EFBFBD> and/or <EFBFBD>#3:2<EFBFBD> may go in
|
||||
* front of <EFBFBD>#6<EFBFBD>. 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 <EFBFBD>ICU_EXP_ICU_1<EFBFBD>) 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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
* - `<EFBFBD>{index}(:{block})<29>`: *Binding Placeholder*: Marks a location where an expression will be
|
||||
* interpolated into. The placeholder `index` points to the expression binding index. An optional
|
||||
* `block` that matches the sub-template in which it was declared.
|
||||
* - `<EFBFBD>#{index}(:{block})<29>`/`<EFBFBD>/#{index}(:{block})<29>`: *Element Placeholder*: Marks the beginning
|
||||
* and end of DOM element that were embedded in the original translation block. The placeholder
|
||||
* `index` points to the element index in the template instructions set. An optional `block` that
|
||||
* matches the sub-template in which it was declared.
|
||||
* - `<EFBFBD>!{index}(:{block})<29>`/`<EFBFBD>/!{index}(:{block})<29>`: *Projection Placeholder*: Marks the
|
||||
* beginning and end of <ng-content> 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.
|
||||
* - `<EFBFBD>*{index}:{block}<7D>`/`<EFBFBD>/*{index}:{block}<7D>`: *Sub-template Placeholder*: Sub-templates must be
|
||||
* split up and translated separately in each angular template function. The `index` points to the
|
||||
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
|
||||
*
|
||||
* @param index A unique index of the translation in the static block.
|
||||
* @param message The translation message.
|
||||
* @param subTemplateIndex Optional sub-template index in the `message`.
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?: number): void {
|
||||
const tView = 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:
|
||||
* - `<EFBFBD>{index}(:{block})<29>`: *Binding Placeholder*: Marks a location where an expression will be
|
||||
* interpolated into. The placeholder `index` points to the expression binding index. An optional
|
||||
* `block` that matches the sub-template in which it was declared.
|
||||
* - `<EFBFBD>#{index}(:{block})<29>`/`<EFBFBD>/#{index}(:{block})<29>`: *Element Placeholder*: Marks the beginning
|
||||
* and end of DOM element that were embedded in the original translation block. The placeholder
|
||||
* `index` points to the element index in the template instructions set. An optional `block` that
|
||||
* matches the sub-template in which it was declared.
|
||||
* - `<EFBFBD>*{index}:{block}<7D>`/`<EFBFBD>/*{index}:{block}<7D>`: *Sub-template Placeholder*: Sub-templates must be
|
||||
* split up and translated separately in each angular template function. The `index` points to the
|
||||
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
|
||||
*
|
||||
* @param index A unique index of the translation in the static block.
|
||||
* @param message The translation message.
|
||||
* @param subTemplateIndex Optional sub-template index in the `message`.
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵi18n(index: number, message: string, subTemplateIndex?: number): void {
|
||||
ɵɵi18nStart(index, message, subTemplateIndex);
|
||||
ɵɵi18nEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a list of attributes as translatable.
|
||||
*
|
||||
* @param index A unique index in the static block
|
||||
* @param values
|
||||
*
|
||||
* @codeGenApi
|
||||
*/
|
||||
export function ɵɵi18nAttributes(index: number, values: string[]): void {
|
||||
const 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<T>(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 [<EFBFBD>*1:1<EFBFBD><EFBFBD>#2:1<EFBFBD>|<EFBFBD>#4:1<EFBFBD>|<EFBFBD>5<EFBFBD>])
|
||||
* 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 <EFBFBD>ICU_EXP_ICU_1<EFBFBD>)
|
||||
* 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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<T = any> extends Type<T> {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Reference in New Issue