feat(ivy): support ng-content in runtime i18n translations (#30782)

Added a new syntax for projections (`¤` will represent `ng-content` nodes) so that we can treat them specifically.
When we enter an i18n block with the instruction `i18nStart`, a new `delayProjection` variable is set to true to prevent the instruction `projection` from projecting the nodes. Once we reach the `i18nEnd` instruction and encounter a projection in the translation we will project its nodes.
If a projection was removed from a translation, then its nodes won't be projected at all.
The variable `delayProjection` is restored to `false` at the end of `i18nEnd` so that it doesn't stop projections outside of i18n blocks.

FW-1261 #resolve
PR Close #30782
This commit is contained in:
Olivier Combe
2019-05-31 17:11:57 +02:00
committed by Miško Hevery
parent 337b6fe003
commit 00cc905b98
6 changed files with 222 additions and 13 deletions

View File

@ -7,38 +7,42 @@
*/
import '../util/ng_i18n_closure_mode';
import {getPluralCase} from '../i18n/localization';
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
import {InertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {addAllToArray} from '../util/array_utils';
import {assertDataInRange, assertDefined, assertEqual, assertGreaterThan} from '../util/assert';
import {attachPatchData} from './context_discovery';
import {elementAttributeInternal, ɵɵload, ɵɵtextBinding} from './instructions/all';
import {elementAttributeInternal, setDelayProjection, ɵɵload, ɵɵtextBinding} from './instructions/all';
import {attachI18nOpCodesDebug} from './instructions/lview_debug';
import {allocExpando, elementPropertyInternal, getOrCreateTNode, setInputsForProperty} from './instructions/shared';
import {LContainer, NATIVE} from './interfaces/container';
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
import {TElementNode, TIcuContainerNode, TNode, TNodeType} from './interfaces/node';
import {TElementNode, TIcuContainerNode, TNode, TNodeType, TProjectionNode} from './interfaces/node';
import {RComment, RElement, RText} from './interfaces/renderer';
import {SanitizerFn} from './interfaces/sanitization';
import {StylingContext} from './interfaces/styling';
import {BINDING_INDEX, HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from './interfaces/view';
import {appendChild, createTextNode, nativeRemoveNode} from './node_manipulation';
import {appendChild, appendProjectedNodes, createTextNode, nativeRemoveNode} from './node_manipulation';
import {getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from './state';
import {NO_CHANGE} from './tokens';
import {renderStringify} from './util/misc_utils';
import {findComponentView} from './util/view_traversal_utils';
import {getNativeByIndex, getNativeByTNode, getTNode, isLContainer} from './util/view_utils';
const MARKER = `<EFBFBD>`;
const ICU_BLOCK_REGEXP = /^\s*(<28>\d+:?\d*<2A>)\s*,\s*(select|plural)\s*,/;
const SUBTEMPLATE_REGEXP = /<2F>\/?\*(\d+:\d+)<29>/gi;
const PH_REGEXP = /<2F>(\/?[#*]\d+):?\d*<2A>/gi;
const PH_REGEXP = /<2F>(\/?[#*!]\d+):?\d*<2A>/gi;
const BINDING_REGEXP = /<2F>(\d+):?\d*<2A>/gi;
const ICU_REGEXP = /({\s*<2A>\d+:?\d*<2A>\s*,\s*\S{6}\s*,[\s\S]*})/gi;
const enum TagType {
ELEMENT = '#',
TEMPLATE = '*',
PROJECTION = '!',
}
// i18nPostprocess consts
const ROOT_TEMPLATE_ID = 0;
@ -340,6 +344,10 @@ const parentIndexStack: number[] = [];
* and end of DOM element that were embedded in the original translation block. The placeholder
* `index` points to the element index in the template instructions set. An optional `block` that
* matches the sub-template in which it was declared.
* - `<60>!{index}(:{block})<29>`/`<60>/!{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.
* - `<60>*{index}:{block}<7D>`/`<60>/*{index}:{block}<7D>`: *Sub-template Placeholder*: Sub-templates must be
* split up and translated separately in each angular template function. The `index` points to the
* `template` instruction index. A `block` that matches the sub-template in which it was declared.
@ -354,6 +362,8 @@ export function ɵɵi18nStart(index: number, message: string, subTemplateIndex?:
const tView = getLView()[TVIEW];
ngDevMode && assertDefined(tView, `tView should be defined`);
i18nIndexStack[++i18nIndexStackPointer] = index;
// We need to delay projections until `i18nEnd`
setDelayProjection(true);
if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) {
i18nStartFirstPass(tView, index, message, subTemplateIndex);
}
@ -398,7 +408,7 @@ function i18nStartFirstPass(
// Odd indexes are placeholders (elements and sub-templates)
if (value.charAt(0) === '/') {
// It is a closing tag
if (value.charAt(1) === '#') {
if (value.charAt(1) === TagType.ELEMENT) {
const phIndex = parseInt(value.substr(2), 10);
parentIndex = parentIndexStack[--parentIndexPointer];
createOpCodes.push(phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.ElementEnd);
@ -410,7 +420,7 @@ function i18nStartFirstPass(
phIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Select,
parentIndex << I18nMutateOpCode.SHIFT_PARENT | I18nMutateOpCode.AppendChild);
if (value.charAt(0) === '#') {
if (value.charAt(0) === TagType.ELEMENT) {
parentIndexStack[++parentIndexPointer] = parentIndex = phIndex;
}
}
@ -508,6 +518,14 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
cursor = cursor.next;
}
// If the placeholder to append is a projection, we need to move the projected nodes instead
if (tNode.type === TNodeType.Projection) {
const tProjectionNode = tNode as TProjectionNode;
appendProjectedNodes(
viewData, tProjectionNode, tProjectionNode.projection, findComponentView(viewData));
return tNode;
}
appendChild(getNativeByTNode(tNode, viewData), tNode, viewData);
const slotValue = viewData[tNode.index];
@ -632,6 +650,8 @@ export function ɵɵi18nEnd(): void {
const tView = getLView()[TVIEW];
ngDevMode && assertDefined(tView, `tView should be defined`);
i18nEndFirstPass(tView);
// Stop delaying projections
setDelayProjection(false);
}
/**

View File

@ -12,7 +12,6 @@ import {appendProjectedNodes} from '../node_manipulation';
import {getProjectAsAttrValue, isNodeMatchingSelectorList, isSelectorInSelectorList} from '../node_selector_matcher';
import {getLView, setIsNotParent} from '../state';
import {findComponentView} from '../util/view_traversal_utils';
import {getOrCreateTNode} from './shared';
@ -103,6 +102,11 @@ export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
}
}
let delayProjection = false;
export function setDelayProjection(value: boolean) {
delayProjection = value;
}
/**
* Inserts previously re-distributed projected nodes. This instruction must be preceded by a call
@ -127,6 +131,9 @@ export function ɵɵprojection(
// `<ng-content>` has no content
setIsNotParent();
// re-distribution of projectable nodes is stored on a component's view level
appendProjectedNodes(lView, tProjectionNode, selectorIndex, findComponentView(lView));
// We might need to delay the projection of nodes if they are in the middle of an i18n block
if (!delayProjection) {
// re-distribution of projectable nodes is stored on a component's view level
appendProjectedNodes(lView, tProjectionNode, selectorIndex, findComponentView(lView));
}
}