Skip to content
Permalink
Browse files

fix(ivy): i18n - update the compiler to output `MessageId`s (#32594)

Now that the `$localize` translations are `MessageId` based the
compiler must render `MessageId`s in its generated `$localize` code.
This is because the `MessageId` used by the compiler is computed
from information that does not get passed through to the `$localize`
tagged string.

For example, the generated code for the following template

```html
<div id="static" i18n-title="m|d" title="introduction"></div>
```

will contain these localization statements

```ts
if (ngI18nClosureMode) {
  /**
    * @desc d
    * @meaning m
    */
  const MSG_EXTERNAL_8809028065680254561$$APP_SPEC_TS_1 = goog.getMsg("introduction");
  I18N_1 = MSG_EXTERNAL_8809028065680254561$$APP_SPEC_TS_1;
}
else {
  I18N_1 = $localize \`:m|d@@8809028065680254561:introduction\`;
}
```

Since `$localize` is not able to accurately regenerate the source-message
(and so the `MessageId`) from the generated code, it must rely upon the
`MessageId` being provided explicitly in the generated code.

The compiler now prepends all localized messages with a "metadata block"
containing the id (and the meaning and description if defined).

Note that this metadata block will also allow translation file extraction
from the compiled code - rather than relying on the legacy ViewEngine
extraction code. (This will be implemented post-v9).

Although these metadata blocks add to the initial code size, compile-time
inlining will completely remove these strings and so will not impact on
production bundle size.

PR Close #32594
  • Loading branch information...
petebacondarwin authored and AndrewKushnir committed Sep 13, 2019
1 parent 357aa4a commit b741a1c3e72a407abf8959c87a8eb65b870ab689

Large diffs are not rendered by default.

@@ -9,22 +9,21 @@ import * as i18n from '../../../i18n/i18n_ast';
import * as o from '../../../output/output_ast';

import {serializeIcuNode} from './icu_serializer';
import {metaFromI18nMessage, serializeI18nMeta} from './meta';
import {formatI18nPlaceholderName} from './util';

export function createLocalizeStatements(
variable: o.ReadVarExpr, message: i18n.Message,
params: {[name: string]: o.Expression}): o.Statement[] {
const statements = [];

// TODO: re-enable these comments when we have a plan on how to make them work so that Closure
// compiler doesn't complain about the JSDOC comments.

// const jsdocComment = i18nMetaToDocStmt(metaFromI18nMessage(message));
// if (jsdocComment !== null) {
// statements.push(jsdocComment);
// }
const metaBlock = serializeI18nMeta(metaFromI18nMessage(message));

const {messageParts, placeHolders} = serializeI18nMessageForLocalize(message);

// Update first message part with metadata
messageParts[0] = `:${metaBlock}:${messageParts[0]}`;

statements.push(new o.ExpressionStatement(variable.set(
o.localizedString(messageParts, placeHolders, placeHolders.map(ph => params[ph])))));

@@ -132,4 +131,4 @@ function processMessagePieces(pieces: MessagePiece[]):
messageParts.push('');
}
return {messageParts, placeHolders};
}
}
@@ -178,6 +178,23 @@ export function parseI18nMeta(meta?: string): I18nMeta {
return {id, meaning, description};
}

/**
* Serialize the given `meta` into a string that can be used in a `$localize` tagged string metadata
* block. The format is the same as that parsed by `parseI18nMeta()`.
*
* @param meta The metadata to serialize
*/
export function serializeI18nMeta(meta: I18nMeta): string {
let metaBlock = meta.description || '';
if (meta.meaning) {
metaBlock = `${meta.meaning}|${metaBlock}`;
}
if (meta.id) {
metaBlock = `${metaBlock}@@${meta.id}`;
}
return metaBlock;
}

// Converts i18n meta information for a message (id, description, meaning)
// to a JsDoc statement formatted as expected by the Closure compiler.
export function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
@@ -5,8 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {I18nMeta, parseI18nMeta} from '@angular/compiler/src/render3/view/i18n/meta';

import {AST} from '../../../src/expression_parser/ast';
import {Lexer} from '../../../src/expression_parser/lexer';
import {Parser} from '../../../src/expression_parser/parser';
@@ -17,6 +15,7 @@ import {I18nContext} from '../../../src/render3/view/i18n/context';
import {serializeI18nMessageForGetMsg} from '../../../src/render3/view/i18n/get_msg_utils';
import {serializeIcuNode} from '../../../src/render3/view/i18n/icu_serializer';
import {serializeI18nMessageForLocalize} from '../../../src/render3/view/i18n/localize_utils';
import {I18nMeta, parseI18nMeta, serializeI18nMeta} from '../../../src/render3/view/i18n/meta';
import {formatI18nPlaceholderName} from '../../../src/render3/view/i18n/util';

import {parseR3 as parse} from './util';
@@ -200,20 +199,29 @@ describe('Utils', () => {
([input, output]) => { expect(formatI18nPlaceholderName(input)).toEqual(output); });
});

it('parseI18nMeta', () => {
const meta = (id?: string, meaning?: string, description?: string) =>
({id, meaning, description});
const cases = [
describe('metadata serialization', () => {
const metadataCases: [string, I18nMeta][] = [
['', meta()],
['desc', meta('', '', 'desc')],
['desc@@id', meta('id', '', 'desc')],
['meaning|desc', meta('', 'meaning', 'desc')],
['meaning|desc@@id', meta('id', 'meaning', 'desc')],
['@@id', meta('id', '', '')],
];
cases.forEach(([input, output]) => {
expect(parseI18nMeta(input as string)).toEqual(output as I18nMeta, input);

it('parseI18nMeta()', () => {
metadataCases.forEach(
([input, output]) => { expect(parseI18nMeta(input)).toEqual(output, input); });
});

it('serializeI18nMeta()', () => {
metadataCases.forEach(
([output, input]) => { expect(serializeI18nMeta(input)).toEqual(output, input); });
});

function meta(id?: string, meaning?: string, description?: string): I18nMeta {
return {id, meaning, description};
}
});
});

0 comments on commit b741a1c

Please sign in to comment.
You can’t perform that action at this time.