Skip to content

Commit

Permalink
refactor(compiler): Initial support for i18n attributes
Browse files Browse the repository at this point in the history
Add support for i18n attributes:
- Generate i18n contexts from i18n attributes, and extract the eventual messages into the constant pool.
- Emit I18nAttributes config instructions when needed.
- Use the generated i18n variable in the appropriate places, including extracted attribute instructions, as well as I18nAttributes config arrays.
  • Loading branch information
dylhunn committed Dec 5, 2023
1 parent 12dfa9b commit 71fa376
Show file tree
Hide file tree
Showing 26 changed files with 624 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,16 @@
"extraChecks": [
"verifyPlaceholdersIntegrity",
"verifyUniqueConsts"
],
"files": [
{
"expected": "interpolation_basic_template.js",
"templatePipelineExpected": "interpolation_basic_template.pipeline.js",
"generated": "interpolation_basic.js"
}
]
}
],
"skipForTemplatePipeline": true
]
},
{
"description": "should support interpolation with custom interpolation config",
Expand Down Expand Up @@ -190,25 +196,16 @@
"extraChecks": [
"verifyPlaceholdersIntegrity",
"verifyUniqueConsts"
],
"files": [
{
"expected": "interpolation_complex_expressions_template.js",
"templatePipelineExpected": "interpolation_complex_expressions_template.pipeline.js",
"generated": "interpolation_complex_expressions.js"
}
]
}
],
"skipForTemplatePipeline": true
},
{
"description": "should support complex expressions in interpolation",
"inputFiles": [
"interpolation_complex_expressions.ts"
],
"expectations": [
{
"extraChecks": [
"verifyPlaceholdersIntegrity",
"verifyUniqueConsts"
]
}
],
"skipForTemplatePipeline": true
]
},
{
"description": "should work correctly when placed on i18n root node",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
decls: 5,
vars: 8,
consts: () => {
__i18nMsg__('static text', [], {}, {})
__i18nMsg__('intro {$interpolation}', [['interpolation', String.raw`\uFFFD0\uFFFD`]], {original_code: {'interpolation': '{{ valueA | uppercase }}'}}, {meaning: 'm', desc: 'd'})
__i18nMsg__('{$interpolation}', [['interpolation', String.raw`\uFFFD0\uFFFD`]], {original_code: {'interpolation': '{{ valueB }}'}}, {meaning: 'm1', desc: 'd1'})
__i18nMsg__('{$interpolation} and {$interpolation_1} and again {$interpolation_2}', [['interpolation', String.raw`\uFFFD0\uFFFD`],['interpolation_1', String.raw`\uFFFD1\uFFFD`],['interpolation_2', String.raw`\uFFFD2\uFFFD`]], {original_code: {'interpolation': '{{ valueA }}', 'interpolation_1': '{{ valueB }}', 'interpolation_2': '{{ valueA + valueB }}'}}, {meaning: 'm2', desc: 'd2'})
__i18nMsg__('{$interpolation}', [['interpolation', String.raw`\uFFFD0\uFFFD`]], {original_code: {'interpolation': '{{ valueC }}'}}, {})
return [
["title", $i18n_1$, "aria-label", $i18n_2$],
["title", $i18n_3$, "aria-roledescription", $i18n_4$],
["id", "dynamic-1", "aria-roledescription", $i18n_0$, __AttributeMarker.I18n__,
"title", "aria-label"],
["id", "dynamic-2", __AttributeMarker.I18n__, "title", "aria-roledescription"]
];
},
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", 2);
$r3$.ɵɵpipe(1, "uppercase");
$r3$.ɵɵi18nAttributes(2, 0);
$r3$.ɵɵelementEnd();
$r3$.ɵɵelementStart(3, "div", 3);
$r3$.ɵɵi18nAttributes(4, 1);
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
$r3$.ɵɵi18nExp($r3$.ɵɵpipeBind1(1, 6, ctx.valueA))(ctx.valueB);
$r3$.ɵɵi18nApply(2);
$r3$.ɵɵadvance(3);
$r3$.ɵɵi18nExp(ctx.valueA)(ctx.valueB)(ctx.valueA + ctx.valueB)(ctx.valueC);
$r3$.ɵɵi18nApply(4);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
decls: 2,
vars: 1,
consts: () => {
__i18nMsg__('{$interpolation} title', [['interpolation', String.raw`\uFFFD0\uFFFD`]], {original_code: {'interpolation': '{{valueA.getRawValue()?.getTitle()}}'}}, {})
return [
["title", $i18n_0$],
[__AttributeMarker.I18n__, "title"]
];
},
template: function MyComponent_Template(rf, ctx) {
if (rf & 1) {
$r3$.ɵɵelementStart(0, "div", 1);
$r3$.ɵɵi18nAttributes(1, 0);
$r3$.ɵɵelementEnd();
}
if (rf & 2) {
let $tmp_0_0$;
$r3$.ɵɵi18nExp(($tmp_0_0$ = ctx.valueA.getRawValue()) == null ? null : $tmp_0_0$.getTitle());
$r3$.ɵɵi18nApply(1);
}
}
23 changes: 22 additions & 1 deletion packages/compiler/src/template/pipeline/ir/src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ export enum OpKind {
* An i18n context containing information needed to generate an i18n message.
*/
I18nContext,

/**
* A creation op that corresponds to i18n attributes on an element.
*/
I18nAttributes,
}

/**
Expand Down Expand Up @@ -490,6 +495,21 @@ export enum I18nParamResolutionTime {
Postproccessing
}

/**
* The contexts in which an i18n expression can be used.
*/
export enum I18nExpressionContext {
/**
* This expression is used as a value (i.e. inside an i18n block).
*/
Normal,

/**
* This expression is used in a binding.
*/
Binding,
}

/**
* Flags that describe what an i18n param value. These determine how the value is serialized into
* the final map.
Expand Down Expand Up @@ -559,7 +579,8 @@ export enum DerivedRepeaterVarIdentity {
*/
export enum I18nContextKind {
RootI18n,
Icu
Icu,
Attr
}

export enum TemplateKind {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,7 @@ export function transformExpressionsInOp(
case OpKind.ProjectionDef:
case OpKind.Template:
case OpKind.Text:
case OpKind.I18nAttributes:
// These operations contain no expressions.
break;
default:
Expand Down
74 changes: 63 additions & 11 deletions packages/compiler/src/template/pipeline/ir/src/ops/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type CreateOp = ListEndOp<CreateOp>|StatementOp<CreateOp>|ElementOp|Eleme
ElementEndOp|ContainerOp|ContainerStartOp|ContainerEndOp|TemplateOp|EnableBindingsOp|
DisableBindingsOp|TextOp|ListenerOp|PipeOp|VariableOp<CreateOp>|NamespaceOp|ProjectionDefOp|
ProjectionOp|ExtractedAttributeOp|DeferOp|DeferOnOp|RepeaterCreateOp|I18nMessageOp|I18nOp|
I18nStartOp|I18nEndOp|IcuStartOp|IcuEndOp|I18nContextOp;
I18nStartOp|I18nEndOp|IcuStartOp|IcuEndOp|I18nContextOp|I18nAttributesOp;

/**
* An operation representing the creation of an element or container.
Expand Down Expand Up @@ -636,20 +636,27 @@ export interface ExtractedAttributeOp extends Op<CreateOp> {
* The value expression of the extracted attribute.
*/
expression: o.Expression|null;

/**
* If this attribute has a corresponding i18n attribute (e.g. `i18n-foo="m:d"`), then this is the
* i18n context for it.
*/
i18nContext: XrefId|null;
}

/**
* Create an `ExtractedAttributeOp`.
*/
export function createExtractedAttributeOp(
target: XrefId, bindingKind: BindingKind, name: string,
expression: o.Expression|null): ExtractedAttributeOp {
target: XrefId, bindingKind: BindingKind, name: string, expression: o.Expression|null,
i18nContext: XrefId|null): ExtractedAttributeOp {
return {
kind: OpKind.ExtractedAttribute,
target,
bindingKind,
name,
expression,
i18nContext,
...NEW_OP,
};
}
Expand Down Expand Up @@ -861,10 +868,19 @@ export interface I18nMessageOp extends Op<CreateOp> {
*/
xref: XrefId;

/**
* The context from which this message was extracted
* TODO: remove this, and add another property here instead to match ExtractedAttributes
*/
i18nContext: XrefId;

/**
* A reference to the i18n op this message was extracted from.
*
* This might be null, which means this message is not associated with a block. This probably
* means it is an i18n attribute's message.
*/
i18nBlock: XrefId;
i18nBlock: XrefId|null;

/**
* The i18n message represented by this op.
Expand Down Expand Up @@ -902,12 +918,13 @@ export interface I18nMessageOp extends Op<CreateOp> {
* Create an `ExtractedMessageOp`.
*/
export function createI18nMessageOp(
xref: XrefId, i18nBlock: XrefId, message: i18n.Message, messagePlaceholder: string|null,
params: Map<string, o.Expression>, postprocessingParams: Map<string, o.Expression>,
needsPostprocessing: boolean): I18nMessageOp {
xref: XrefId, i18nContext: XrefId, i18nBlock: XrefId|null, message: i18n.Message,
messagePlaceholder: string|null, params: Map<string, o.Expression>,
postprocessingParams: Map<string, o.Expression>, needsPostprocessing: boolean): I18nMessageOp {
return {
kind: OpKind.I18nMessage,
xref,
i18nContext,
i18nBlock,
message,
messagePlaceholder,
Expand Down Expand Up @@ -1080,13 +1097,14 @@ export function createIcuEndOp(xref: XrefId): IcuEndOp {

/**
* An i18n context that is used to generate a translated i18n message. A separate context is created
* for two different scenarios:
* for three different scenarios:
*
* 1. For each top-level i18n block.
* 2. For each ICU referenced as a sub-message. ICUs that are referenced as a sub-message will be
* used to generate a separate i18n message, but will not be extracted directly into the consts
* array. Instead they will be pulled in as part of the initialization statements for the message
* that references them.
* 3. For each i18n attribute.
*
* Child i18n blocks, resulting from the use of an ng-template inside of a parent i18n block, do not
* generate a separate context. Instead their content is included in the translated message for
Expand All @@ -1098,7 +1116,7 @@ export interface I18nContextOp extends Op<CreateOp> {
contextKind: I18nContextKind;

/**
* The id of this context.
* The id of this context.
*/
xref: XrefId;

Expand All @@ -1107,8 +1125,11 @@ export interface I18nContextOp extends Op<CreateOp> {
*
* It is possible for multiple contexts to belong to the same block, since both the block and any
* ICUs inside the block will each get their own context.
*
* This might be `null`, in which case the context is not associated with an i18n block. This
* probably means that it belongs to an i18n attribute.
*/
i18nBlock: XrefId;
i18nBlock: XrefId|null;

/**
* The i18n message associated with this context.
Expand All @@ -1129,8 +1150,12 @@ export interface I18nContextOp extends Op<CreateOp> {
}

export function createI18nContextOp(
contextKind: I18nContextKind, xref: XrefId, i18nBlock: XrefId, message: i18n.Message,
contextKind: I18nContextKind, xref: XrefId, i18nBlock: XrefId|null, message: i18n.Message,
sourceSpan: ParseSourceSpan): I18nContextOp {
if (i18nBlock === null && contextKind !== I18nContextKind.Attr) {
throw new Error('AssertionError: i18nBlock must be provided for non-attribute contexts.');
}

return {
kind: OpKind.I18nContext,
contextKind,
Expand All @@ -1144,6 +1169,33 @@ export function createI18nContextOp(
};
}

export interface I18nAttributesOp extends Op<CreateOp>, ConsumesSlotOpTrait {
kind: OpKind.I18nAttributes;

/**
* The element targeted by these attributes.
*/
target: XrefId;

/**
* I18nAttributes instructions correspond to a const array with configuration information.
*/
i18nAttributesConfig: ConstIndex|null;
}

export function createI18nAttributesOp(
xref: XrefId, handle: SlotHandle, target: XrefId): I18nAttributesOp {
return {
kind: OpKind.I18nAttributes,
xref,
handle,
target,
i18nAttributesConfig: null,
...NEW_OP,
...TRAIT_CONSUMES_SLOT,
};
}

/**
* An index into the `consts` array which is shared across the compilation of all views in a
* component.
Expand Down
8 changes: 6 additions & 2 deletions packages/compiler/src/template/pipeline/ir/src/ops/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import * as o from '../../../../../../src/output/output_ast';
import {ParseSourceSpan} from '../../../../../../src/parse_util';
import {OpKind} from '../enums';
import {Op} from '../operations';
import {Op, XrefId} from '../operations';
import {ConsumesVarsTrait, TRAIT_CONSUMES_VARS} from '../traits';

import {NEW_OP} from './shared';
Expand All @@ -26,17 +26,21 @@ export interface HostPropertyOp extends Op<UpdateOp>, ConsumesVarsTrait {
expression: o.Expression|Interpolation;
isAnimationTrigger: boolean;

i18nContext: XrefId|null;


sourceSpan: ParseSourceSpan|null;
}

export function createHostPropertyOp(
name: string, expression: o.Expression|Interpolation, isAnimationTrigger: boolean,
sourceSpan: ParseSourceSpan|null): HostPropertyOp {
i18nContext: XrefId|null, sourceSpan: ParseSourceSpan|null): HostPropertyOp {
return {
kind: OpKind.HostProperty,
name,
expression,
isAnimationTrigger,
i18nContext,
sourceSpan,
...TRAIT_CONSUMES_VARS,
...NEW_OP,
Expand Down
Loading

0 comments on commit 71fa376

Please sign in to comment.