feat(ivy): support i18n without closure (#28689)
So far using runtime i18n with ivy meant that you needed to use Closure and `goog.getMsg` (or a polyfill). This PR changes the compiler to output both closure & non-closure code, while the unused option will be tree-shaken by minifiers. This means that if you use the Angular CLI with ivy and load a translations file, you can use i18n and the application will not throw at runtime. For now it will not translate your application, but at least you can try ivy without having to remove all of your i18n code and configuration. PR Close #28689
This commit is contained in:

committed by
Igor Minar

parent
387fbb8106
commit
91c7b451d5
@ -130,6 +130,7 @@ export class Identifiers {
|
||||
static i18nEnd: o.ExternalReference = {name: 'Δi18nEnd', moduleName: CORE};
|
||||
static i18nApply: o.ExternalReference = {name: 'Δi18nApply', moduleName: CORE};
|
||||
static i18nPostprocess: o.ExternalReference = {name: 'Δi18nPostprocess', moduleName: CORE};
|
||||
static i18nLocalize: o.ExternalReference = {name: 'Δi18nLocalize', moduleName: CORE};
|
||||
|
||||
static load: o.ExternalReference = {name: 'Δload', moduleName: CORE};
|
||||
|
||||
|
@ -11,19 +11,21 @@ import {toPublicName} from '../../../i18n/serializers/xmb';
|
||||
import * as html from '../../../ml_parser/ast';
|
||||
import {mapLiteral} from '../../../output/map_util';
|
||||
import * as o from '../../../output/output_ast';
|
||||
import {Identifiers as R3} from '../../r3_identifiers';
|
||||
|
||||
|
||||
/* Closure variables holding messages must be named `MSG_[A-Z0-9]+` */
|
||||
const CLOSURE_TRANSLATION_PREFIX = 'MSG_';
|
||||
const CLOSURE_TRANSLATION_MATCHER_REGEXP = new RegExp(`^${CLOSURE_TRANSLATION_PREFIX}`);
|
||||
|
||||
/* Prefix for non-`goog.getMsg` i18n-related vars */
|
||||
const TRANSLATION_PREFIX = 'I18N_';
|
||||
|
||||
export const TRANSLATION_PREFIX = 'I18N_';
|
||||
|
||||
/** Closure uses `goog.getMsg(message)` to lookup translations */
|
||||
const GOOG_GET_MSG = 'goog.getMsg';
|
||||
|
||||
/** Name of the global variable that is used to determine if we use Closure translations or not */
|
||||
const NG_I18N_CLOSURE_MODE = 'ngI18nClosureMode';
|
||||
|
||||
/** I18n separators for metadata **/
|
||||
const I18N_MEANING_SEPARATOR = '|';
|
||||
const I18N_ID_SEPARATOR = '@@';
|
||||
@ -48,17 +50,36 @@ export type I18nMeta = {
|
||||
};
|
||||
|
||||
function i18nTranslationToDeclStmt(
|
||||
variable: o.ReadVarExpr, message: string,
|
||||
params?: {[name: string]: o.Expression}): o.DeclareVarStmt {
|
||||
variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
|
||||
params?: {[name: string]: o.Expression}): o.Statement[] {
|
||||
const statements: o.Statement[] = [];
|
||||
// var I18N_X;
|
||||
statements.push(
|
||||
new o.DeclareVarStmt(variable.name !, undefined, o.INFERRED_TYPE, null, variable.sourceSpan));
|
||||
|
||||
const args = [o.literal(message) as o.Expression];
|
||||
if (params && Object.keys(params).length) {
|
||||
args.push(mapLiteral(params, true));
|
||||
}
|
||||
const fnCall = o.variable(GOOG_GET_MSG).callFn(args);
|
||||
return variable.set(fnCall).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]);
|
||||
|
||||
// Closure JSDoc comments
|
||||
const docStatements = i18nMetaToDocStmt(meta);
|
||||
const thenStatements: o.Statement[] = docStatements ? [docStatements] : [];
|
||||
const googFnCall = o.variable(GOOG_GET_MSG).callFn(args);
|
||||
// const MSG_... = goog.getMsg(..);
|
||||
thenStatements.push(closureVar.set(googFnCall).toConstDecl());
|
||||
// I18N_X = MSG_...;
|
||||
thenStatements.push(new o.ExpressionStatement(variable.set(closureVar)));
|
||||
const localizeFnCall = o.importExpr(R3.i18nLocalize).callFn(args);
|
||||
// I18N_X = i18nLocalize(...);
|
||||
const elseStatements = [new o.ExpressionStatement(variable.set(localizeFnCall))];
|
||||
// if(ngI18nClosureMode) { ... } else { ... }
|
||||
statements.push(o.ifStmt(o.variable(NG_I18N_CLOSURE_MODE), thenStatements, elseStatements));
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
// Converts i18n meta informations for a message (id, description, meaning)
|
||||
// Converts i18n meta information for a message (id, description, meaning)
|
||||
// to a JsDoc statement formatted as expected by the Closure compiler.
|
||||
function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
|
||||
const tags: o.JSDocTag[] = [];
|
||||
@ -231,6 +252,7 @@ export function getTranslationConstPrefix(extra: string): string {
|
||||
* Generates translation declaration statements.
|
||||
*
|
||||
* @param variable Translation value reference
|
||||
* @param closureVar Variable for Closure `goog.getMsg` calls
|
||||
* @param message Text message to be translated
|
||||
* @param meta Object that contains meta information (id, meaning and description)
|
||||
* @param params Object with placeholders key-value pairs
|
||||
@ -238,27 +260,16 @@ export function getTranslationConstPrefix(extra: string): string {
|
||||
* @returns Array of Statements that represent a given translation
|
||||
*/
|
||||
export function getTranslationDeclStmts(
|
||||
variable: o.ReadVarExpr, message: string, meta: I18nMeta,
|
||||
variable: o.ReadVarExpr, closureVar: o.ReadVarExpr, message: string, meta: I18nMeta,
|
||||
params: {[name: string]: o.Expression} = {},
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.Statement[] {
|
||||
const statements: o.Statement[] = [];
|
||||
const docStatements = i18nMetaToDocStmt(meta);
|
||||
if (docStatements) {
|
||||
statements.push(docStatements);
|
||||
}
|
||||
|
||||
statements.push(...i18nTranslationToDeclStmt(variable, closureVar, message, meta, params));
|
||||
|
||||
if (transformFn) {
|
||||
statements.push(i18nTranslationToDeclStmt(variable, message, params));
|
||||
|
||||
// Closure Compiler doesn't allow non-goo.getMsg const names to start with `MSG_`,
|
||||
// so we update variable name prefix in case post processing is required, so we can
|
||||
// assign the result of post-processing function to the var that starts with `I18N_`
|
||||
const raw = o.variable(variable.name !);
|
||||
variable.name = variable.name !.replace(CLOSURE_TRANSLATION_MATCHER_REGEXP, TRANSLATION_PREFIX);
|
||||
|
||||
statements.push(
|
||||
variable.set(transformFn(raw)).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
||||
} else {
|
||||
statements.push(i18nTranslationToDeclStmt(variable, message, params));
|
||||
statements.push(new o.ExpressionStatement(variable.set(transformFn(variable))));
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prep
|
||||
import {I18nContext} from './i18n/context';
|
||||
import {I18nMetaVisitor} from './i18n/meta';
|
||||
import {getSerializedI18nContent} from './i18n/serializer';
|
||||
import {I18N_ICU_MAPPING_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {I18N_ICU_MAPPING_PREFIX, TRANSLATION_PREFIX, assembleBoundTextPlaceholders, assembleI18nBoundString, formatI18nPlaceholderName, getTranslationConstPrefix, getTranslationDeclStmts, icuFromI18nMessage, isI18nRootNode, isSingleI18nIcu, metaFromI18nMessage, placeholdersToParams, wrapI18nPlaceholder} from './i18n/util';
|
||||
import {Instruction, StylingBuilder} from './styling_builder';
|
||||
import {CONTEXT_NAME, IMPLICIT_REFERENCE, NON_BINDABLE_ATTR, REFERENCE_PREFIX, RENDER_FLAGS, asLiteral, getAttrsForDirectiveMatching, invalid, trimTrailingNulls, unsupported} from './util';
|
||||
|
||||
@ -321,14 +321,18 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
i18nTranslate(
|
||||
message: i18n.Message, params: {[name: string]: o.Expression} = {}, ref?: o.ReadVarExpr,
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
|
||||
const _ref = ref || this.i18nAllocateRef(message.id);
|
||||
const _ref = ref || o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
|
||||
// Closure Compiler requires const names to start with `MSG_` but disallows any other const to
|
||||
// start with `MSG_`. We define a variable starting with `MSG_` just for the `goog.getMsg` call
|
||||
const closureVar = this.i18nGenerateClosureVar(message.id);
|
||||
const _params: {[key: string]: any} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
|
||||
}
|
||||
const meta = metaFromI18nMessage(message);
|
||||
const content = getSerializedI18nContent(message);
|
||||
const statements = getTranslationDeclStmts(_ref, content, meta, _params, transformFn);
|
||||
const statements =
|
||||
getTranslationDeclStmts(_ref, closureVar, content, meta, _params, transformFn);
|
||||
this.constantPool.statements.push(...statements);
|
||||
return _ref;
|
||||
}
|
||||
@ -360,7 +364,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
return bound;
|
||||
}
|
||||
|
||||
i18nAllocateRef(messageId: string): o.ReadVarExpr {
|
||||
i18nGenerateClosureVar(messageId: string): o.ReadVarExpr {
|
||||
let name: string;
|
||||
const suffix = this.fileBasedI18nSuffix.toUpperCase();
|
||||
if (this.i18nUseExternalIds) {
|
||||
@ -424,7 +428,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
if (this.i18nContext) {
|
||||
this.i18n = this.i18nContext.forkChildContext(index, this.templateIndex !, meta);
|
||||
} else {
|
||||
const ref = this.i18nAllocateRef((meta as i18n.Message).id);
|
||||
const ref = o.variable(this.constantPool.uniqueName(TRANSLATION_PREFIX));
|
||||
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user