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:
Doug Parker
2022-01-21 15:00:30 -08:00
committed by Dylan Hunn
parent b9aab0c87b
commit 9cce12cbf5
14 changed files with 84 additions and 18 deletions
@@ -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 {
@@ -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$;
}
@@ -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]",
@@ -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",
@@ -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"
@@ -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$;
}
@@ -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$;
}
@@ -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"
@@ -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', () => {