Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ivy): runtime i18n #24037

Closed
wants to merge 5 commits into from
Closed

Conversation

ocombe
Copy link
Contributor

@ocombe ocombe commented May 21, 2018

PR Type

What kind of change does this PR introduce?

[x] Feature

What is the current behavior?

There is no runtime i18n for ivy

What is the new behavior?

Runtime i18n works for ivy
This is only the first step: html and expressions.
We still need to do ICUs, projected content, and the service for code translations.

Status

Done:

  • translate HTML elements (including moving/removing elements)
  • translate expressions (including moving/removing expressions)
  • translate attributes (including with expressions)
  • support multiple i18n blocks
  • support container instructions (with or without embedded template, including moving/removing them)
  • support ng-container
  • support projections (including with selectors, and multiple projections)
  • canonical tests for html/expressions/expressions in attributes for the compiler

To do:

  • updating design spec with recent changes

Does this PR introduce a breaking change?

[x] No

@ocombe ocombe added state: WIP area: i18n target: major This PR is targeted for the next major release comp: ivy labels May 21, 2018
@ocombe ocombe requested a review from mhevery May 21, 2018 18:57
@ngbot

This comment has been minimized.

@mary-poppins
Copy link

You can preview 391ac36 at https://pr24037-391ac36.ngbuilds.io/.

EXPRESSION = 3 << 29,
CLOSE_NODE = 4 << 29,
REMOVE_NODE = 5 << 29,
UNMASK = (1 << 29) - 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename to INDEX_MASK

CLOSE_NODE = 4 << 29,
REMOVE_NODE = 5 << 29,
UNMASK = (1 << 29) - 1,
MASK = ~((1 << 29) - 1),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to INSTRUCTION_MASK

MASK = ~((1 << 29) - 1),
}

export function i18nMapping(msg: string, placeholders: string[]): any[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be public method needs detail public API comments.

const elementRegex = /\{\$(p_\d+)\}/g;
const expRegex = /\{\$(exp_\d+)\}/g;
const closeRegex = /\{\$(c_\w+)\}/g;
const tagRegex = /(\{\$\w+_\w+\})/g;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every time this method runs it will allocate the above rgexps, which will create memory pressure. Move to top level.

MASK = ~((1 << 29) - 1),
}

export function i18nMapping(msg: string, placeholders: string[]): any[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you type the return value better than any[] maybe I18nInstruction[] where type Instruction = number|string

// TODO(ocombe): find a way to cache this
const children = getChildren(previousOrParentNode);
for (let i = 0; i < children.length; i++) {
children.splice(i + 1, 0, ...getChildren(children[i]));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you not use ... operators since when down-leveled to ES5 it is slow.
Notice that it causes apply to be called
http://www.typescriptlang.org/play/#src=children.splice(i%20%2B%201%2C%200%2C%20...getChildren(children%5Bi%5D))%3B

Copy link
Contributor Author

@ocombe ocombe May 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can use slice & concat instead: https://jsperf.com/slice-concat-vs-splice-spread
It's 27% faster in chrome (5% slower in firefox).
Unless you have a better idea?

if (args.some(value => bind(value) !== NO_CHANGE)) {
const expRegex = /\{\$(exp_\d+)\}/g;
return msg.replace(
expRegex, (match: string, p1: string) => { return args[placeholders.indexOf(p1)]; });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are allocating a regexp and than doing string replace. This will create GC pressure every time we run change detection and it is not the fastest. Can we refactor this so that it does not allocate anything when there is no change to inputs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually this is the same regex that it used above, I'll use the same constant

}
}

export function i18nInterpolation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of this method with 7 args? Why 7?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I searched the answer that Victor did to me when I asked that and it was:

this is something we do in the runtime when we know the number of args, see interpolate1, interpolate2, ... interpolate8 and interpolateV. Creating an array consumes memory and has slower access so this is an optimization when we have only a few args (common case)

So that's probably not the implementation that he was suggesting, I'll switch to 8 functions instead

/** A variable length version of i18nInterpolation */
export function i18nInterpolationV(msg: string, placeholders: string[], args: any[]): string|
NO_CHANGE {
if (args.some(value => bind(value) !== NO_CHANGE)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use some same argument as forEach

@@ -456,6 +456,28 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: LVi
return false;
}

export function getChildren(parent: LNode): LNode[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way you can make this method cacheable, is to collect method induces rather than LNodes themselves. So return would be number[]. Than you could do the dereference when processing instructions.

@mary-poppins
Copy link

You can preview 767144e at https://pr24037-767144e.ngbuilds.io/.

@mary-poppins
Copy link

You can preview b4a00f4 at https://pr24037-b4a00f4.ngbuilds.io/.

const instruction = instructions[i] as number;
switch (instruction & I18nFlags.INSTRUCTION_MASK) {
case I18nFlags.ELEMENT:
const element = children[instruction & I18nFlags.INDEX_MASK];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data[children[instruction & I18nFlags.INDEX_MASK]]

data[instruction & I18nFlags.INDEX_MASK]

const children = [];
let currentNode = parent.child;
while (currentNode) {
if (currentNode.native) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentNode.tNode.index

*/
export function i18nInterpolation2(msg: string, ph0: string, ph1: string, a0: any, a1: any): string|
NO_CHANGE {
if (bind(a0) !== NO_CHANGE || bind(a1) !== NO_CHANGE) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use bindingUpdatedN here, with N in {,2,4} to combine the bind checks. That would also get rid of the need for NO_CHANGE as then only booleans are handled. Refer to the interpolation# instructions where the same approach is used.

Applies to all i18nInterpolation# functions.

Copy link
Member

@JoostK JoostK May 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, watch out with short circuiting. We do not want that to happen, because it would cause bindings not to be updated. Recall that bind either updates or consumes a binding which causes bindingIndex to be incremented past the binding slot, we need to make sure that always occurs for the bindings to stay in sync.

Copy link
Contributor Author

@ocombe ocombe May 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points, I didn't think about that! thanks
and I should use stringify too

NO_CHANGE {
for (let i = 0; i < args.length; i++) {
if (bind(args[i]) !== NO_CHANGE) {
return msg.replace(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return here can't be right. We'd need to traverse all bindings for them to be updated and become string replaced.

for (let i = 0; i < msgList.length; i++) {
const value = msgList[i];
let match;
if (match = elementRegex.exec(value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably get away with a lot less regex matching here, if we were to change the regex into /\{\$((?:exp|p|c)_\w+)\}/g. We'd then know that all odd indices actually match, and we can easily figure out the instruction per the odd matches' prefix and the identifier from a substring. One regex match, then just string manipulation with known offsets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good idea, thanks!

childrenIndex = previousOrParentNode.tNode !.childrenIndex !;
}

let children = data.slice(startIndex, endIndex + 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we offset each lookup into data with startIndex, we'd void having to create a copy. We may then even get rid of endIndex, or use it only in dev mode for bounds checking.

@ocombe ocombe force-pushed the feat/ivy/runtime-i18n branch 2 times, most recently from 1171f4c to b528710 Compare May 24, 2018 11:56
@mary-poppins
Copy link

You can preview 1171f4c at https://pr24037-1171f4c.ngbuilds.io/.

@mary-poppins
Copy link

You can preview b528710 at https://pr24037-b528710.ngbuilds.io/.


/**
* A list of flags to encode the i18n instructions used to translate the template.
* We shift the flags by 29 so that 30 & 31 & 32 bits contains the instructions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use constant rather than number in the comment and in the enum to avoid then getting out of sync later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that at first but for some reason typescript doesn't want a variable in a an enum value

/** Used to decode the number encoded with the instruction. */
INDEX_MASK = (1 << 29) - 1,
/** Used to test the type of instruction. */
INSTRUCTION_MASK = ~((1 << 29) - 1),
Copy link
Contributor

@vicb vicb May 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits:

  • 0b111 << INSTRUCTION_SHIFT might be clearer / faster,
  • TEXT should read Text according to coding standards,
  • I would use Text = 1, Element = 2, ... InstructionShift = 29, probably clearer.


/** Represents the instructions used to translate the template. */
export type I18nInstruction = number | string;
const tagRegex = /\{\$((?:exp|p|c)_\w+)\}/g;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably super slow and the reason why I had element and expression placeholder maps generated as distinct maps by the compiler.

Note: exp_..., p_..., c_... were only exemple names given in the i18n compiler spec, this will probably not be the final scheme

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how to split the translation efficiently without a regex?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should probably be a regexp, but a simpler/faster one ie /\{\$[^}]+\}/g
The other part of the perf gain is that placeholders would be specified as maps (one for element and an other one for expression).

* Takes a translation string and the initial list of placeholders (elements and expressions)
* and returns a list of instructions that will be used to translate the template.
*/
export function i18nMapping(msg: string, placeholders: string[]): I18nInstruction[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: msg -> translation ?

* and returns a list of instructions that will be used to translate the template.
*/
export function i18nMapping(msg: string, placeholders: string[]): I18nInstruction[] {
const instructions = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add type ?


for (let i = 0; i < msgList.length; i++) {
const value = msgList[i];
// odd index is either element, expression or close tag
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// odd index are placeholders

for (let i = 0; i < msgList.length; i++) {
const value = msgList[i];
// odd index is either element, expression or close tag
if ((i % 2) === 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if (i & 1) {...}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh nice trick

const value = msgList[i];
// odd index is either element, expression or close tag
if ((i % 2) === 1) {
switch (value.substr(0, 1)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value.charAt(0)

Note: p/e/c will change later

if ((i % 2) === 1) {
switch (value.substr(0, 1)) {
case 'p':
instructions.push(placeholders.indexOf(value) | I18nFlags.ELEMENT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why use .indexOf(value) instead of the map as proposed in the compiler side spec ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, I've changed the code a few times, and now it seems like a map would be better, I'll update my code

@ocombe
Copy link
Contributor Author

ocombe commented Jun 21, 2018

new fixup commit:

  • moved everything into i18n.ts
  • fixed reordering of exports for core_render3_private_export.ts
  • improved performance for i18nInterpolationV and added a test for it
  • moved detached property to tNode
  • other small stuff

@mary-poppins
Copy link

You can preview ffc36db at https://pr24037-ffc36db.ngbuilds.io/.

* value will be concatenated into the final translation.
*/
export type I18nExpInstruction = number | string;
/** Mapping of placeholder names to their absolute index in the current view. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this true for expression / embedded templates ?

Copy link
Contributor Author

@ocombe ocombe Jun 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, "in the current view" could probably be "in their templates" or something like that


/**
* Takes a translation string, the initial list of placeholders (elements and expressions) and the
* indexes of their corresponding expression nodes to return a list of instructions for each
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indexes of their corresponding expression nodes unclear, as already commented

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have something better in mind?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see the design spec.

/**
* Takes a translation string, the initial list of placeholders (elements and expressions) and the
* indexes of their corresponding expression nodes to return a list of instructions for each
* template.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

template ->template function ?

}
});

it('should support expressions', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add html to assert html vs expression indexes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is already a test for that: "should support both html elements, expressions and expressions in attributes"

}
});

it('should support expressions in attributes', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a test for attributes without expression (unless there is one already ?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a test for embedded templates

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attributes without expressions are static attributes, it's already tested in the compiler (nothing required at runtime).
There is a test for embedded templates line ~441

// Html tags are replaced by placeholders.
// Open tag placeholders are never re-used (closing tag placeholders can be).
const MSG_DIV_SECTION_1 = `{$p_3}trad 1{$c_p}{$p_0}trad 2{$p_1}trad 3{$c_p}{$c_p}`;
const i18n_1 = i18nMapping(MSG_DIV_SECTION_1, [{p_0: 1, p_1: 2, p_2: 3, p_3: 4}]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change the placeholder name: if <tag> / </tag> -> START_TAG and END_TAG

'<div><a>trad expr 1</a>hello<b title="start expr 2 middle expr 1 end"><c>trad</c></b></div>');
});

describe('containers', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view containers / embedded templates


it('should support sibling embedded templates', () => {
const MSG_DIV_SECTION_1 = `{$p_0}valeur: {$exp_1}!{$c_p}{$p_1}valeur bis: {$exp_2}!{$c_p}`;
// The indexes are based on each template function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a canonical to assert that the compiler generates the correct code

// Initial template:
// <ul i18n>
// <li *ngFor="let item of items">value: {{item}}</li>
// </ul>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for each test, please give:

  • the initial structure (the lines above),
  • the translation.

'<ul><li>valeur: one!</li><li>valeur: two!</li><li>valeur bis: one!</li><li>valeur bis: two!</li></ul>');
});

it('should support multiple levels of embedded templates', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"nested" ?

@mhevery mhevery added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Jun 21, 2018
@mhevery
Copy link
Contributor

mhevery commented Jun 21, 2018

@ocombe
Copy link
Contributor Author

ocombe commented Jun 21, 2018

new fixup commit:

  • renamed all tags in the tests to START_... / END_...
  • added a canonical test for embedded templates

@mary-poppins
Copy link

You can preview fe62b7b at https://pr24037-fe62b7b.ngbuilds.io/.

@mary-poppins
Copy link

You can preview fad434f at https://pr24037-fad434f.ngbuilds.io/.

@mhevery mhevery closed this in 84272e2 Jun 21, 2018
@ocombe ocombe deleted the feat/ivy/runtime-i18n branch June 21, 2018 20:20
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: i18n cla: yes target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants