fix(compiler): fix i18n IDs generation, add a migration tool#15621
fix(compiler): fix i18n IDs generation, add a migration tool#15621ocombe wants to merge 3 commits intoangular:masterfrom
Conversation
adce335 to
bc15bbf
Compare
There was a problem hiding this comment.
Not sure if we should expect(sourceId) here
There was a problem hiding this comment.
not really, I can remove it
There was a problem hiding this comment.
Copy the former serializer to tests and use it. The expectations should not be modified here.
There was a problem hiding this comment.
ahah damn, that's what I did the first time and then I thought that we should remove it because it was only used in the tests
|
@ocombe thanks for the PR - I have added a few comments. We need to come with a migration tool before merging this PR, can you work on it ? |
bc15bbf to
d2acf58
Compare
|
I changed the code that you requested @vicb, let me know if it's ok now |
d2acf58 to
5e04000
Compare
9a15596 to
7382687
Compare
There was a problem hiding this comment.
good question... removing this
There was a problem hiding this comment.
revert (everywhere it has been added in this commit)
BREAKING CHANGE: updates the xliff serializer to use the same digest function. This is a breaking change because the IDs generated won't match with your existing translations. We provide a migration tool to update your existing files to the new method.
76deb4a to
cd6d937
Compare
BREAKING CHANGE: This fix updates the digest function to ignore the content of placeholders.
Example previously `<p i18n>{{expr}}</p>`, `<p i18n>{{ expr }}</p>` and `<p i18n>{{expr2}}</p>` had different ids but the exact same content once extracted. With this fix those 3 messages will only be extracted once with the same id.
This is a breaking change because the auto-generated ids for extracted messages will change if the message contains placeholders... To mitigate the problem, a migration tool will be provided.
Fixes angular#15573
Because of the bug angular#15573 we had to make a breaking change in the way i18n ids are generated. To mitigate the problem, a new flag can be used to set the version of the digest method used: `--i18nVersion` with values 0 (old, default for v4) or 1 (new, default for v5) can be used by the cli tools for extraction (ng-xi18n) and AOT (ngc). The token `I18N_VERSION` can be used in your code for JIT. We also added a new migration cli tool in @angular/compiler-cli named ng-migrate-i18n. Usage is the following: `ng-migrate-i18n --files src/i18n/*.xlf --i18nFormat xlf` --files: (required) the translation files that you want to migrate, you can use glob expressions to migrate multiple files at the same time (as long as they use the same format) --i18nFormat: (required) the format of the translation files that you want to migrate --i18nVersion: (optional) the version from which you are migrating. The only value for now is "0" (the new version is "1") --mapping: (optional) if you add this option it will generate a json file of mappings {newId --> [oldIds]} instead of migrating the translation files --resolve: (optional) "manual" by default, you can use "auto" so that the cli tool doesn't ask when it finds conflicts (in which case it will take the first value)
| return computeMsgId(parts.join(''), message.meaning); | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
We still depend on the placeholder name "ignoring placeholders" is not correct in that sense.
Could you please add more details ?
Please remove the comment about versions (version 1 here). Reason is digest and version are separate concern and this comment will become obsolete when the next version is out.
| /** | ||
| * Serialize the i18n ast to something xml-like in order to generate an UID. | ||
| * Used by Id generation version 1 | ||
| * |
There was a problem hiding this comment.
same comment as above for PH and version
|
|
||
| function createSerializer(format?: string): Serializer { | ||
| export function createSerializer( | ||
| format?: string, version: I18nVersion = I18nVersion.Version0): Serializer { |
There was a problem hiding this comment.
Why do we have a default parameter here ?
What about export function createSerializer(format: string, version: I18nVersion)
-> make both params mandatory
| return promiseBundle.then(bundle => { | ||
| const content = this.serialize(bundle, formatName); | ||
| return this.extractBundle().then((bundle: compiler.MessageBundle) => { | ||
| const serializer = compiler.createSerializer(formatName); |
There was a problem hiding this comment.
pass an explicit version here.
(Probably we need to think about making this parameter - let's discuss this)
| locale?: string|null, compilerHostContext?: CompilerHostContext, | ||
| ngCompilerHost?: CompilerHost): Extractor { | ||
| locale: string|null = null, compilerHostContext?: CompilerHostContext, | ||
| ngCompilerHost?: CompilerHost, version: I18nVersion = I18nVersion.Version0): Extractor { |
There was a problem hiding this comment.
changing the version here as no effect on extraction, right ?
(the version is only passed to the i18nHtmlParser that does not use it for extraction)
There was a problem hiding this comment.
Yes, we always extract with the latest version, but this might be a mistake, shouldn't we let people use the old version for the extraction? in which case we should pass it here: https://github.com/ocombe/angular/blob/d8eaf6f215074ab4f9641dfd724da1b1909eaa99/packages/compiler-cli/src/extractor.ts#L37
| private _htmlParser: HtmlParser, translations?: string, translationsFormat?: string, | ||
| missingTranslation: MissingTranslationStrategy = MissingTranslationStrategy.Warning, | ||
| console?: Console) { | ||
| console?: Console, version: I18nVersion = I18nVersion.Version0) { |
There was a problem hiding this comment.
remove default value here = I18nVersion.Version0 ?
| const msgIdToElement = getElementsMapping(translations); | ||
| msgIdToElement.reverse().forEach(([id, element]: [string, ml.Element]) => { | ||
| const newId = mapping[id]; | ||
| if (typeof newId !== 'undefined' && newId !== id) { |
There was a problem hiding this comment.
if (mapping.hasOwnProperty(id)) {
const newId = mapping[id];
// ...
}
not sure if the test newId !== id is really helpful here (There is no reason why we should produce this anyway, right ?)
|
|
||
| /** | ||
| * Updates translations based on the provided mapping | ||
| * @param translations |
There was a problem hiding this comment.
rename translations or add a comment that it might also be message.
Do we have support to migrate an xmb file ?
There was a problem hiding this comment.
yes we can migrate all formats (including xmb)
| /** | ||
| * @internal | ||
| */ | ||
| export function getTranslations(i18nFile: string | null): string { |
There was a problem hiding this comment.
rename to readFileOrEmptyString
| return {newToOld, oldToNew}; | ||
| } | ||
|
|
||
| export function getElementsMapping( |
There was a problem hiding this comment.
what does this return ?
return an object ?
document ?
|
|
||
| const res: [string, ml.Element, string][] = []; | ||
| msgIdToElement.forEach(([id, element]: [string, ml.Element]) => { | ||
| res.push([id, element, msgMap[id] ? msgMap[id] : '']); |
| } | ||
| }); | ||
|
|
||
| return {newToOld, oldToNew}; |
There was a problem hiding this comment.
where do we need oldToNew
I think the names should be newIdToOldIds and oldIdToNewId (id vs ids is what is important here)
| private map( | ||
| mapping: {newToOld: {[newId: string]: string[]}, oldToNew: {[oldId: string]: string}}, | ||
| i18nFile: string): string { | ||
| let dstPath = `${i18nFile.split('.').slice(0, -1).join('.')}_mapping.json`; |
There was a problem hiding this comment.
we still need the name of the file to have a different mapping file for each file
There was a problem hiding this comment.
path the package to manipulate paths
| mapping: {newToOld: {[newId: string]: string[]}, oldToNew: {[oldId: string]: string}}, | ||
| i18nFile: string, formatName: string, version: I18nVersion, | ||
| resolve: string): Promise<string> { | ||
| const translations = getTranslations(i18nFile); |
There was a problem hiding this comment.
- I would separate I/O (reading files, prompt) from processing.
- It would still be nice to know conflicts even in the
resolvemode (To displayno conflict detected / N conflicts have been solved automatically) - more comments would be nice
There was a problem hiding this comment.
Also probably the code related conflicts resolution should be in @angular/compiler
compiler-cli should only be orchestrating the calls & dealing with I/O
There was a problem hiding this comment.
I would separate I/O (reading files, prompt) from processing.
can be done
It would still be nice to know conflicts even in the resolve mode (To display no conflict detected / N conflicts have been solved automatically)
I agree
more comments would be nice
everytime I add comments you make me remove them :D I'll add some
Also probably the code related conflicts resolution should be in @angular/compiler
compiler-cli should only be orchestrating the calls & dealing with I/O
it would complicate the code I think, the conflict resolution seems to be related to the prompts (for me) which are in the compiler-cli package
|
|
||
| constructor( | ||
| {defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation, | ||
| {defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation = null, |
There was a problem hiding this comment.
seems like this does not match the type ?
There was a problem hiding this comment.
true, weird that the compiler didn't throw an error here
|
Closed in favor of #17638 |
|
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. |
Please check if the PR fulfills these requirements
What kind of change does this PR introduce?
What is the current behavior?
See #15573.
What is the new behavior?
When we extract messages we digest them to generate ids (unless the id is provided).
With this fix we ignore the content of the placeholder when we digest to only use the fact that it's a placeholder and its position.
We already did the same thing with ICU expressions for XMB and this previous fix is now also applied to ICU expressions for XLIFF (now that XLIFF supports ICU expressions).
Example previously
<p i18n>{{expr}}</p>,<p i18n>{{ expr }}</p>and<p i18n>{{expr2}}</p>had different ids but the exact same content once extracted. With this fix those 3 messages will only be extracted once with the same id.We also aligned the digest functions for all serializers, they will all use the decimal digest (instead of sha1 previously for xliff).
Because of the bug #15573 we had to make a breaking change in the way i18n ids are generated.
To mitigate the problem, a new flag can be used to set the version of the digest method used:
--i18nVersionwith values 0 (old, default for v4) or 1 (new, default for v5) can be used by the cli tools for extraction (ng-xi18n) and AOT (ngc). The tokenI18N_VERSIONcan be used in your code for JIT.We also added a new migration cli tool in @angular/compiler-cli named ng-migrate-i18n.
Usage is the following:
ng-migrate-i18n --files src/i18n/*.xlf --i18nFormat xlf--files: (required) the translation files that you want to migrate, you can use glob expressions to migrate multiple files at the same time (as long as they use the same format)
--i18nFormat: (required) the format of the translation files that you want to migrate
--i18nVersion: (optional) the version from which you are migrating. The only value for now is "0" (the new version is "1")
--mapping: (optional) if you add this option it will generate a json file of mappings {newId --> [oldIds]} instead of migrating the translation files
--resolve: (optional) "manual" by default, you can use "auto" so that the cli tool doesn't ask when it finds conflicts (in which case it will take the first value)
Does this PR introduce a breaking change?
Unfortunately this is a breaking change because the auto-generated ids for extracted messages will change if the message contains placeholders (or ICU expressions for XLIFF)...
To mitigate the problem, a new flag can be used to set the version of the digest method used. It iss
--idVersionwith values 0 (old, default for v4) or 1 (new, default for v5) for extraction (ng-xi18n) and AOT (ngc), and the tokenID_VERSIONfor JIT.Fow now we still use the old method to generate the ids, but once v5 is released we will reverse the flag to only use the new version.