feat(ivy): updated translation const names (that include message ids) (#27185)
PR Close #27185
This commit is contained in:

committed by
Igor Minar

parent
9129f9ac9b
commit
aedc343003
@ -11,6 +11,7 @@ import {MissingTranslationStrategy} from '../core';
|
||||
export interface AotCompilerOptions {
|
||||
locale?: string;
|
||||
i18nFormat?: string;
|
||||
i18nUseExternalIds?: boolean;
|
||||
translations?: string;
|
||||
missingTranslation?: MissingTranslationStrategy;
|
||||
enableSummariesForJit?: boolean;
|
||||
|
@ -104,11 +104,9 @@ export class CompilerFacadeImpl implements CompilerFacade {
|
||||
const constantPool = new ConstantPool();
|
||||
|
||||
// Parse the template and check for errors.
|
||||
const template = parseTemplate(
|
||||
facade.template, sourceMapUrl, {
|
||||
preserveWhitespaces: facade.preserveWhitespaces || false,
|
||||
},
|
||||
'');
|
||||
const template = parseTemplate(facade.template, sourceMapUrl, {
|
||||
preserveWhitespaces: facade.preserveWhitespaces || false,
|
||||
});
|
||||
if (template.errors !== undefined) {
|
||||
const errors = template.errors.map(err => err.toString()).join(', ');
|
||||
throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`);
|
||||
@ -129,6 +127,8 @@ export class CompilerFacadeImpl implements CompilerFacade {
|
||||
animations: facade.animations != null ? new WrappedNodeExpr(facade.animations) : null,
|
||||
viewProviders: facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) :
|
||||
null,
|
||||
relativeContextFilePath: '',
|
||||
i18nUseExternalIds: true,
|
||||
},
|
||||
constantPool, makeBindingParser());
|
||||
const preStatements = [...constantPool.statements, ...res.statements];
|
||||
|
@ -132,13 +132,6 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||
* Selectors found in the <ng-content> tags in the template.
|
||||
*/
|
||||
ngContentSelectors: string[];
|
||||
|
||||
/**
|
||||
* Path to the .ts file in which this template's generated code will be included, relative to
|
||||
* the compilation root. This will be used to generate identifiers that need to be globally
|
||||
* unique in certain contexts (such as g3).
|
||||
*/
|
||||
relativeContextFilePath: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -190,6 +183,20 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
|
||||
* The list of view providers defined in the component.
|
||||
*/
|
||||
viewProviders: o.Expression|null;
|
||||
|
||||
|
||||
/**
|
||||
* Path to the .ts file in which this template's generated code will be included, relative to
|
||||
* the compilation root. This will be used to generate identifiers that need to be globally
|
||||
* unique in certain contexts (such as g3).
|
||||
*/
|
||||
relativeContextFilePath: string;
|
||||
|
||||
/**
|
||||
* Whether translation variable name should contain external message id
|
||||
* (used by Closure Compiler's output of `goog.getMsg` for transition period)
|
||||
*/
|
||||
i18nUseExternalIds: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -254,7 +254,7 @@ export function compileComponentFromMetadata(
|
||||
const templateBuilder = new TemplateDefinitionBuilder(
|
||||
constantPool, BindingScope.ROOT_SCOPE, 0, templateTypeName, null, null, templateName,
|
||||
meta.viewQueries, directiveMatcher, directivesUsed, meta.pipes, pipesUsed, R3.namespaceHTML,
|
||||
meta.template.relativeContextFilePath);
|
||||
meta.relativeContextFilePath, meta.i18nUseExternalIds);
|
||||
|
||||
const templateFunctionExpression = templateBuilder.buildTemplateFunction(
|
||||
template.nodes, [], template.hasNgContent, template.ngContentSelectors);
|
||||
@ -373,7 +373,6 @@ export function compileComponentFromRender2(
|
||||
nodes: render3Ast.nodes,
|
||||
hasNgContent: render3Ast.hasNgContent,
|
||||
ngContentSelectors: render3Ast.ngContentSelectors,
|
||||
relativeContextFilePath: '',
|
||||
},
|
||||
directives: [],
|
||||
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
|
||||
@ -384,7 +383,9 @@ export function compileComponentFromRender2(
|
||||
(summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated,
|
||||
animations: null,
|
||||
viewProviders:
|
||||
component.viewProviders.length > 0 ? new o.WrappedNodeExpr(component.viewProviders) : null
|
||||
component.viewProviders.length > 0 ? new o.WrappedNodeExpr(component.viewProviders) : null,
|
||||
relativeContextFilePath: '',
|
||||
i18nUseExternalIds: true,
|
||||
};
|
||||
const res = compileComponentFromMetadata(meta, outputCtx.constantPool, bindingParser);
|
||||
|
||||
|
@ -19,12 +19,6 @@ const TRANSLATION_PREFIX = 'MSG_';
|
||||
/** Closure uses `goog.getMsg(message)` to lookup translations */
|
||||
const GOOG_GET_MSG = 'goog.getMsg';
|
||||
|
||||
/** String key that is used to provide backup id of translatable message in Closure */
|
||||
const BACKUP_MESSAGE_ID = 'BACKUP_MESSAGE_ID';
|
||||
|
||||
/** Regexp to identify whether backup id already provided in description */
|
||||
const BACKUP_MESSAGE_ID_REGEXP = new RegExp(BACKUP_MESSAGE_ID);
|
||||
|
||||
/** I18n separators for metadata **/
|
||||
const I18N_MEANING_SEPARATOR = '|';
|
||||
const I18N_ID_SEPARATOR = '@@';
|
||||
@ -63,15 +57,11 @@ function i18nTranslationToDeclStmt(
|
||||
// to a JsDoc statement formatted as expected by the Closure compiler.
|
||||
function i18nMetaToDocStmt(meta: I18nMeta): o.JSDocCommentStmt|null {
|
||||
const tags: o.JSDocTag[] = [];
|
||||
const {id, description, meaning} = meta;
|
||||
if (id || description) {
|
||||
const hasBackupId = !!description && BACKUP_MESSAGE_ID_REGEXP.test(description);
|
||||
const text =
|
||||
id && !hasBackupId ? `[${BACKUP_MESSAGE_ID}:${id}] ${description || ''}` : description;
|
||||
tags.push({tagName: o.JSDocTagName.Desc, text: text !.trim()});
|
||||
if (meta.description) {
|
||||
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
|
||||
}
|
||||
if (meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meaning});
|
||||
if (meta.meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||
}
|
||||
return tags.length == 0 ? null : new o.JSDocCommentStmt(tags);
|
||||
}
|
||||
@ -92,9 +82,9 @@ export function hasI18nAttrs(element: html.Element): boolean {
|
||||
return element.attrs.some((attr: html.Attribute) => isI18nAttribute(attr.name));
|
||||
}
|
||||
|
||||
export function metaFromI18nMessage(message: i18n.Message): I18nMeta {
|
||||
export function metaFromI18nMessage(message: i18n.Message, id: string | null = null): I18nMeta {
|
||||
return {
|
||||
id: message.id || '',
|
||||
id: typeof id === 'string' ? id : message.id || '',
|
||||
meaning: message.meaning || '',
|
||||
description: message.description || ''
|
||||
};
|
||||
@ -222,8 +212,14 @@ export function formatI18nPlaceholderName(name: string): string {
|
||||
return postfix ? `${raw}_${postfix}` : raw;
|
||||
}
|
||||
|
||||
export function getTranslationConstPrefix(fileBasedSuffix: string): string {
|
||||
return `${TRANSLATION_PREFIX}${fileBasedSuffix}`.toUpperCase();
|
||||
/**
|
||||
* Generates a prefix for translation const name.
|
||||
*
|
||||
* @param extra Additional local prefix that should be injected into translation var name
|
||||
* @returns Complete translation const prefix
|
||||
*/
|
||||
export function getTranslationConstPrefix(extra: string): string {
|
||||
return `${TRANSLATION_PREFIX}${extra}`.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -246,7 +242,7 @@ export function getTranslationDeclStmts(
|
||||
statements.push(docStatements);
|
||||
}
|
||||
if (transformFn) {
|
||||
const raw = o.variable(`${variable.name}_RAW`);
|
||||
const raw = o.variable(`${variable.name}$$RAW`);
|
||||
statements.push(i18nTranslationToDeclStmt(raw, message, params));
|
||||
statements.push(
|
||||
variable.set(transformFn(raw)).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
|
||||
|
@ -109,7 +109,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
private viewQueries: R3QueryMetadata[], private directiveMatcher: SelectorMatcher|null,
|
||||
private directives: Set<o.Expression>, private pipeTypeByName: Map<string, o.Expression>,
|
||||
private pipes: Set<o.Expression>, private _namespace: o.ExternalReference,
|
||||
private relativeContextFilePath: string) {
|
||||
private relativeContextFilePath: string, private i18nUseExternalIds: boolean) {
|
||||
// view queries can take up space in data and allocation happens earlier (in the "viewQuery"
|
||||
// function)
|
||||
this._dataIndex = viewQueries.length;
|
||||
@ -184,8 +184,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
// - this template has parent i18n context
|
||||
// - or the template has i18n meta associated with it,
|
||||
// but it's not initiated by the Element (e.g. <ng-template i18n>)
|
||||
const initI18nContext = this.i18nContext ||
|
||||
(isI18nRootNode(i18n) && !(isSingleElementTemplate(nodes) && nodes[0].i18n === i18n));
|
||||
const initI18nContext =
|
||||
this.i18nContext || (isI18nRootNode(i18n) && !isSingleI18nIcu(i18n) &&
|
||||
!(isSingleElementTemplate(nodes) && nodes[0].i18n === i18n));
|
||||
const selfClosingI18nInstruction = hasTextChildrenOnly(nodes);
|
||||
if (initI18nContext) {
|
||||
this.i18nStart(null, i18n !, selfClosingI18nInstruction);
|
||||
@ -254,8 +255,8 @@ 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.Expression {
|
||||
const _ref = ref || this.i18nAllocateRef();
|
||||
transformFn?: (raw: o.ReadVarExpr) => o.Expression): o.ReadVarExpr {
|
||||
const _ref = ref || this.i18nAllocateRef(message.id);
|
||||
const _params: {[key: string]: any} = {};
|
||||
if (params && Object.keys(params).length) {
|
||||
Object.keys(params).forEach(key => _params[formatI18nPlaceholderName(key)] = params[key]);
|
||||
@ -296,9 +297,16 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
return bound;
|
||||
}
|
||||
|
||||
i18nAllocateRef() {
|
||||
const prefix = getTranslationConstPrefix(this.fileBasedI18nSuffix);
|
||||
return o.variable(this.constantPool.uniqueName(prefix));
|
||||
i18nAllocateRef(messageId: string): o.ReadVarExpr {
|
||||
let name: string;
|
||||
if (this.i18nUseExternalIds) {
|
||||
const prefix = getTranslationConstPrefix(`EXTERNAL_`);
|
||||
name = `${prefix}${messageId}`;
|
||||
} else {
|
||||
const prefix = getTranslationConstPrefix(this.fileBasedI18nSuffix);
|
||||
name = this.constantPool.uniqueName(prefix);
|
||||
}
|
||||
return o.variable(name);
|
||||
}
|
||||
|
||||
i18nUpdateRef(context: I18nContext): void {
|
||||
@ -350,7 +358,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();
|
||||
const ref = this.i18nAllocateRef((meta as i18n.Message).id);
|
||||
this.i18n = new I18nContext(index, ref, 0, this.templateIndex, meta);
|
||||
}
|
||||
|
||||
@ -434,7 +442,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
const stylingBuilder = new StylingBuilder(o.literal(elementIndex), null);
|
||||
|
||||
let isNonBindableMode: boolean = false;
|
||||
const isI18nRootElement: boolean = isI18nRootNode(element.i18n);
|
||||
const isI18nRootElement: boolean =
|
||||
isI18nRootNode(element.i18n) && !isSingleI18nIcu(element.i18n);
|
||||
|
||||
if (isI18nRootElement && this.i18n) {
|
||||
throw new Error(`Could not mark an element as translatable inside of a translatable section`);
|
||||
@ -704,7 +713,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
||||
const templateVisitor = new TemplateDefinitionBuilder(
|
||||
this.constantPool, this._bindingScope, this.level + 1, contextName, this.i18n,
|
||||
templateIndex, templateName, [], this.directiveMatcher, this.directives,
|
||||
this.pipeTypeByName, this.pipes, this._namespace, this.fileBasedI18nSuffix);
|
||||
this.pipeTypeByName, this.pipes, this._namespace, this.fileBasedI18nSuffix,
|
||||
this.i18nUseExternalIds);
|
||||
|
||||
// Nested templates must not be visited until after their parent templates have completed
|
||||
// processing, so they are queued here until after the initial pass. Otherwise, we wouldn't
|
||||
@ -1383,25 +1393,14 @@ function interpolate(args: o.Expression[]): o.Expression {
|
||||
* @param templateUrl URL to use for source mapping of the parsed template
|
||||
*/
|
||||
export function parseTemplate(
|
||||
template: string, templateUrl: string, options: {preserveWhitespaces?: boolean} = {},
|
||||
relativeContextFilePath: string): {
|
||||
errors?: ParseError[],
|
||||
nodes: t.Node[],
|
||||
hasNgContent: boolean,
|
||||
ngContentSelectors: string[],
|
||||
relativeContextFilePath: string
|
||||
} {
|
||||
template: string, templateUrl: string, options: {preserveWhitespaces?: boolean}):
|
||||
{errors?: ParseError[], nodes: t.Node[], hasNgContent: boolean, ngContentSelectors: string[]} {
|
||||
const bindingParser = makeBindingParser();
|
||||
const htmlParser = new HtmlParser();
|
||||
const parseResult = htmlParser.parse(template, templateUrl, true);
|
||||
|
||||
if (parseResult.errors && parseResult.errors.length > 0) {
|
||||
return {
|
||||
errors: parseResult.errors,
|
||||
nodes: [],
|
||||
hasNgContent: false,
|
||||
ngContentSelectors: [], relativeContextFilePath
|
||||
};
|
||||
return {errors: parseResult.errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
|
||||
}
|
||||
|
||||
let rootNodes: html.Node[] = parseResult.rootNodes;
|
||||
@ -1426,15 +1425,10 @@ export function parseTemplate(
|
||||
const {nodes, hasNgContent, ngContentSelectors, errors} =
|
||||
htmlAstToRender3Ast(rootNodes, bindingParser);
|
||||
if (errors && errors.length > 0) {
|
||||
return {
|
||||
errors,
|
||||
nodes: [],
|
||||
hasNgContent: false,
|
||||
ngContentSelectors: [], relativeContextFilePath
|
||||
};
|
||||
return {errors, nodes: [], hasNgContent: false, ngContentSelectors: []};
|
||||
}
|
||||
|
||||
return {nodes, hasNgContent, ngContentSelectors, relativeContextFilePath};
|
||||
return {nodes, hasNgContent, ngContentSelectors};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1478,4 +1472,4 @@ function hasTextChildrenOnly(children: t.Node[]): boolean {
|
||||
return !children.find(
|
||||
child =>
|
||||
!(child instanceof t.Text || child instanceof t.BoundText || child instanceof t.Icu));
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,7 @@ function makeSelectorMatcher(): SelectorMatcher<DirectiveMeta> {
|
||||
|
||||
describe('t2 binding', () => {
|
||||
it('should bind a simple template', () => {
|
||||
const template =
|
||||
parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {}, '');
|
||||
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});
|
||||
const binder = new R3TargetBinder(new SelectorMatcher<DirectiveMeta>());
|
||||
const res = binder.bind({template: template.nodes});
|
||||
|
||||
@ -48,8 +47,7 @@ describe('t2 binding', () => {
|
||||
});
|
||||
|
||||
it('should match directives when binding a simple template', () => {
|
||||
const template =
|
||||
parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {}, '');
|
||||
const template = parseTemplate('<div *ngFor="let item of items">{{item.name}}</div>', '', {});
|
||||
const binder = new R3TargetBinder(makeSelectorMatcher());
|
||||
const res = binder.bind({template: template.nodes});
|
||||
const tmpl = template.nodes[0] as a.Template;
|
||||
|
Reference in New Issue
Block a user