mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(compiler): add @suppress {msgDescriptions} if no description is present on an i18n message (#44787)
Refs http://b/214103351. This happens if a user writes `<span i18n>Message</span>`. This is accepted as an internationalized message, but without a description. JSCompiler will throw an error in this situation because descriptions are generally required. Now, the Angular compiler will generate a suppression annotation so JSCompiler allows the syntax. This will ease an internal migration to JSCompiler-based i18n. PR Close #44787
This commit is contained in:
+6
@@ -1,6 +1,9 @@
|
||||
// NOTE: Keeping raw content (avoiding `__i18nMsg__` macro) to illustrate message id sanitization.
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_ID_WITH_INVALID_CHARS$$APP_SPEC_TS_1$ = goog.getMsg("Element title");
|
||||
$I18N_0$ = $MSG_EXTERNAL_ID_WITH_INVALID_CHARS$$APP_SPEC_TS_1$;
|
||||
} else {
|
||||
@@ -11,6 +14,9 @@ if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
|
||||
let $I18N_2$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_ID_WITH_INVALID_CHARS_2$$APP_SPEC_TS_4$ = goog.getMsg(" Some content ");
|
||||
$I18N_2$ = $MSG_EXTERNAL_ID_WITH_INVALID_CHARS_2$$APP_SPEC_TS_4$;
|
||||
} else {
|
||||
|
||||
+3
@@ -1,5 +1,8 @@
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_4166854826696768832$$APP_SPEC_TS_0$ = goog.getMsg("{VAR_SELECT, select, single {'single quotes'} double {\"double quotes\"} other {other}}");
|
||||
$I18N_0$ = $MSG_EXTERNAL_4166854826696768832$$APP_SPEC_TS_0$;
|
||||
}
|
||||
|
||||
+12
@@ -18,6 +18,9 @@ consts: function() {
|
||||
// NOTE: Keeping raw content here to illustrate the difference in placeholders generated for goog.getMsg and $localize calls (see last i18n block).
|
||||
let $I18N_1$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("{VAR_SELECT, select, male {male} female {female} other {other}}");
|
||||
$I18N_1$ = $MSG_APP_SPEC_TS_1$;
|
||||
}
|
||||
@@ -30,6 +33,9 @@ consts: function() {
|
||||
|
||||
let $I18N_2$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_APP_SPEC_TS_2$ = goog.getMsg("{VAR_SELECT, select, male {male} female {female} other {other}}");
|
||||
$I18N_2$ = $MSG_APP_SPEC_TS_2$;
|
||||
}
|
||||
@@ -42,6 +48,9 @@ consts: function() {
|
||||
|
||||
let $I18N_4$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_APP_SPEC_TS__4$ = goog.getMsg("{VAR_SELECT, select, male {male} female {female} other {other}}");
|
||||
$I18N_4$ = $MSG_APP_SPEC_TS__4$;
|
||||
}
|
||||
@@ -54,6 +63,9 @@ consts: function() {
|
||||
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_APP_SPEC_TS_0$ = goog.getMsg(" {$icu} {$startTagDiv} {$icu} {$closeTagDiv}{$startTagDiv_1} {$icu} {$closeTagDiv}", {
|
||||
"startTagDiv": "\uFFFD#2\uFFFD",
|
||||
"closeTagDiv": "[\uFFFD/#2\uFFFD|\uFFFD/#1:1\uFFFD\uFFFD/*3:1\uFFFD]",
|
||||
|
||||
+3
@@ -2,6 +2,9 @@
|
||||
consts: function() {
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_7128002169381370313$$APP_SPEC_TS_1$ = goog.getMsg("{$startTagXhtmlDiv} Count: {$startTagXhtmlSpan}5{$closeTagXhtmlSpan}{$closeTagXhtmlDiv}", {
|
||||
"startTagXhtmlDiv": "\uFFFD#3\uFFFD",
|
||||
"startTagXhtmlSpan": "\uFFFD#4\uFFFD",
|
||||
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
consts: function() {
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_7428861019045796010$$APP_SPEC_TS_1$ = goog.getMsg(" Count: {$startTagXhtmlSpan}5{$closeTagXhtmlSpan}", {
|
||||
"startTagXhtmlSpan": "\uFFFD#4\uFFFD",
|
||||
"closeTagXhtmlSpan": "\uFFFD/#4\uFFFD"
|
||||
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
// NOTE: Keeping raw content (avoiding `__i18nMsg__` macro) to illustrate backticks escaping.
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_APP_SPEC_TS_1$ = goog.getMsg("`{$interpolation}`", { "interpolation": "\uFFFD0\uFFFD" });
|
||||
$I18N_0$ = $MSG_APP_SPEC_TS_1$;
|
||||
}
|
||||
|
||||
+3
@@ -1,6 +1,9 @@
|
||||
// NOTE: Keeping raw content (avoiding `__i18nMsg__` macro) to illustrate quotes escaping.
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_4924931801512133405$$APP_SPEC_TS_0$ = goog.getMsg("Some text 'with single quotes', \"with double quotes\", `with backticks` and without quotes.");
|
||||
$I18N_0$ = $MSG_EXTERNAL_4924931801512133405$$APP_SPEC_TS_0$;
|
||||
}
|
||||
|
||||
+3
@@ -4,6 +4,9 @@ vars: 2,
|
||||
consts: function() {
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_7597881511811528589$$APP_SPEC_TS_0$ = goog.getMsg(" Named interpolation: {$phA} Named interpolation with spaces: {$phB} ", {
|
||||
"phA": "\uFFFD0\uFFFD",
|
||||
"phB": "\uFFFD1\uFFFD"
|
||||
|
||||
+3
@@ -2,6 +2,9 @@ consts: function() {
|
||||
// NOTE: Keeping raw content (avoiding `i18nMsg`) to illustrate message layout in case of whitespace preserving mode.
|
||||
let $I18N_0$;
|
||||
if (typeof ngI18nClosureMode !== "undefined" && ngI18nClosureMode) {
|
||||
/**
|
||||
* @suppress {msgDescriptions}
|
||||
*/
|
||||
const $MSG_EXTERNAL_963542717423364282$$APP_SPEC_TS_0$ = goog.getMsg("\n Some text\n {$startTagSpan}Text inside span{$closeTagSpan}\n ", {
|
||||
"startTagSpan": "\uFFFD#3\uFFFD",
|
||||
"closeTagSpan": "\uFFFD/#3\uFFFD"
|
||||
|
||||
@@ -107,10 +107,10 @@ function i18nMsgInsertLocalizePlaceholders(message: string, placeholders: Placeh
|
||||
* Generate a string that represents expected Closure metadata output comment.
|
||||
*/
|
||||
function i18nMsgClosureMeta(meta?: Meta): string {
|
||||
if (!meta || !(meta.desc || meta.meaning)) return '';
|
||||
if (!meta) return '';
|
||||
return `
|
||||
/**
|
||||
${meta.desc ? '* @desc ' + meta.desc : ''}
|
||||
* ${meta.desc ? '@desc ' + meta.desc : '@suppress {msgDescriptions}'}
|
||||
${meta.meaning ? '* @meaning ' + meta.meaning : ''}
|
||||
*/
|
||||
`;
|
||||
|
||||
@@ -1340,6 +1340,7 @@ export const enum JSDocTagName {
|
||||
Desc = 'desc',
|
||||
Id = 'id',
|
||||
Meaning = 'meaning',
|
||||
Suppress = 'suppress',
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -32,10 +32,7 @@ export function createGoogleGetMsgStatements(
|
||||
// const MSG_... = goog.getMsg(..);
|
||||
// I18N_X = MSG_...;
|
||||
const googGetMsgStmt = closureVar.set(o.variable(GOOG_GET_MSG).callFn(args)).toConstDecl();
|
||||
const metaComment = i18nMetaToJSDoc(message);
|
||||
if (metaComment !== null) {
|
||||
googGetMsgStmt.addLeadingComment(metaComment);
|
||||
}
|
||||
googGetMsgStmt.addLeadingComment(i18nMetaToJSDoc(message));
|
||||
const i18nAssignmentStmt = new o.ExpressionStatement(variable.set(closureVar));
|
||||
return [googGetMsgStmt, i18nAssignmentStmt];
|
||||
}
|
||||
|
||||
@@ -180,8 +180,9 @@ export class I18nMetaVisitor implements html.Visitor {
|
||||
* @returns the parsed metadata.
|
||||
*/
|
||||
private _parseMetadata(meta: string|i18n.I18nMeta): I18nMeta {
|
||||
return typeof meta === 'string' ? parseI18nMeta(meta) :
|
||||
meta instanceof i18n.Message ? meta : {};
|
||||
return typeof meta === 'string' ? parseI18nMeta(meta) :
|
||||
meta instanceof i18n.Message ? meta :
|
||||
{};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,9 +208,9 @@ export class I18nMetaVisitor implements html.Visitor {
|
||||
// `packages/compiler/src/render3/view/template.ts`).
|
||||
// In that case we want to reuse the legacy message generated in the 1st pass (see
|
||||
// `setI18nRefs()`).
|
||||
const previousMessage = meta instanceof i18n.Message ?
|
||||
meta :
|
||||
meta instanceof i18n.IcuPlaceholder ? meta.previousMessage : undefined;
|
||||
const previousMessage = meta instanceof i18n.Message ? meta :
|
||||
meta instanceof i18n.IcuPlaceholder ? meta.previousMessage :
|
||||
undefined;
|
||||
message.legacyIds = previousMessage ? previousMessage.legacyIds : [];
|
||||
}
|
||||
}
|
||||
@@ -255,13 +256,16 @@ export function parseI18nMeta(meta: string = ''): I18nMeta {
|
||||
|
||||
// Converts i18n meta information for a message (id, description, meaning)
|
||||
// to a JsDoc statement formatted as expected by the Closure compiler.
|
||||
export function i18nMetaToJSDoc(meta: I18nMeta): o.JSDocComment|null {
|
||||
export function i18nMetaToJSDoc(meta: I18nMeta): o.JSDocComment {
|
||||
const tags: o.JSDocTag[] = [];
|
||||
if (meta.description) {
|
||||
tags.push({tagName: o.JSDocTagName.Desc, text: meta.description});
|
||||
} else {
|
||||
// Suppress the JSCompiler warning that a `@desc` was not given for this message.
|
||||
tags.push({tagName: o.JSDocTagName.Suppress, text: '{msgDescriptions}'});
|
||||
}
|
||||
if (meta.meaning) {
|
||||
tags.push({tagName: o.JSDocTagName.Meaning, text: meta.meaning});
|
||||
}
|
||||
return tags.length == 0 ? null : o.jsDocComment(tags);
|
||||
return o.jsDocComment(tags);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,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} from '../../../src/render3/view/i18n/meta';
|
||||
import {I18nMeta, i18nMetaToJSDoc, parseI18nMeta} from '../../../src/render3/view/i18n/meta';
|
||||
import {formatI18nPlaceholderName} from '../../../src/render3/view/i18n/util';
|
||||
import {LEADING_TRIVIA_CHARS} from '../../../src/render3/view/template';
|
||||
|
||||
@@ -326,11 +326,36 @@ describe('Utils', () => {
|
||||
.toEqual(
|
||||
{cooked: ':abc::message', raw: ':abc::message', range: jasmine.any(ParseSourceSpan)});
|
||||
});
|
||||
|
||||
function meta(customId?: string, meaning?: string, description?: string): I18nMeta {
|
||||
return {customId, meaning, description};
|
||||
}
|
||||
});
|
||||
|
||||
describe('jsdoc generation', () => {
|
||||
it('generates with description', () => {
|
||||
const docComment = i18nMetaToJSDoc(meta('', '', 'desc'));
|
||||
|
||||
expect(docComment.tags.length).toBe(1);
|
||||
expect(docComment.tags[0]).toEqual({tagName: o.JSDocTagName.Desc, text: 'desc'});
|
||||
});
|
||||
|
||||
it('generates with no description suppressed', () => {
|
||||
const docComment = i18nMetaToJSDoc(meta('', '', ''));
|
||||
|
||||
expect(docComment.tags.length).toBe(1);
|
||||
expect(docComment.tags[0]).toEqual({
|
||||
tagName: o.JSDocTagName.Suppress,
|
||||
text: '{msgDescriptions}',
|
||||
});
|
||||
});
|
||||
|
||||
it('generates with description and meaning', () => {
|
||||
const docComment = i18nMetaToJSDoc(meta('', 'meaning', ''));
|
||||
|
||||
expect(docComment.tags).toContain({tagName: o.JSDocTagName.Meaning, text: 'meaning'});
|
||||
});
|
||||
});
|
||||
|
||||
function meta(customId?: string, meaning?: string, description?: string): I18nMeta {
|
||||
return {customId, meaning, description};
|
||||
}
|
||||
});
|
||||
|
||||
describe('serializeI18nMessageForGetMsg', () => {
|
||||
|
||||
Reference in New Issue
Block a user