-
Notifications
You must be signed in to change notification settings - Fork 24.8k
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
feat(ivy): runtime i18n #24037
Conversation
This comment has been minimized.
This comment has been minimized.
6cd2e22
to
391ac36
Compare
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, |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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])); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)]; }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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 LNode
s themselves. So return would be number[]
. Than you could do the dereference when processing instructions.
You can preview 767144e at https://pr24037-767144e.ngbuilds.io/. |
767144e
to
b4a00f4
Compare
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]; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good idea, thanks!
b4a00f4
to
927a3cb
Compare
childrenIndex = previousOrParentNode.tNode !.childrenIndex !; | ||
} | ||
|
||
let children = data.slice(startIndex, endIndex + 1); |
There was a problem hiding this comment.
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.
1171f4c
to
b528710
Compare
You can preview 1171f4c at https://pr24037-1171f4c.ngbuilds.io/. |
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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 readText
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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[] { |
There was a problem hiding this comment.
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 = []; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if (i & 1) {...}
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
b697333
to
05d843d
Compare
new fixup commit:
|
05d843d
to
ffc36db
Compare
You can preview ffc36db at https://pr24037-ffc36db.ngbuilds.io/. |
packages/core/src/render3/i18n.ts
Outdated
* 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. */ |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see the design spec.
packages/core/src/render3/i18n.ts
Outdated
/** | ||
* 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. |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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 ?)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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}]); |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"nested" ?
new fixup commit:
|
You can preview fe62b7b at https://pr24037-fe62b7b.ngbuilds.io/. |
fe62b7b
to
fad434f
Compare
You can preview fad434f at https://pr24037-fad434f.ngbuilds.io/. |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
PR Type
What kind of change does this PR introduce?
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:
container
instructions (with or without embedded template, including moving/removing them)ng-container
To do:
Does this PR introduce a breaking change?