diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/element_attributes/invalid_i18n_meta.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/element_attributes/invalid_i18n_meta.js index 692e4464b54..8a9a5db2c27 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/element_attributes/invalid_i18n_meta.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/element_attributes/invalid_i18n_meta.js @@ -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 { diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/escape_quotes.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/escape_quotes.js index 3f23331e0fa..2ee89dde2b9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/escape_quotes.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/escape_quotes.js @@ -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$; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/shared_placeholder.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/shared_placeholder.js index 0fa9fc336f8..f60c13e5649 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/shared_placeholder.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/icu_logic/shared_placeholder.js @@ -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]", diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/foreign_object.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/foreign_object.js index b1c6dc28564..b644eb0408d 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/foreign_object.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/foreign_object.js @@ -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", diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/namespaced_div.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/namespaced_div.js index b3305e68289..cf7dc2c098b 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/namespaced_div.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/namespaces/namespaced_div.js @@ -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" diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/backtick_quotes.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/backtick_quotes.js index baa4f5a02cd..e66ddc50597 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/backtick_quotes.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/backtick_quotes.js @@ -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$; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/escape_quotes.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/escape_quotes.js index 8c4e9a799d0..a9144938a1e 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/escape_quotes.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/escape_quotes.js @@ -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$; } diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/named_interpolations.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/named_interpolations.js index 4080bfa6f3b..b7bcda32ab9 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/named_interpolations.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/nested_nodes/named_interpolations.js @@ -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" diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/whitespace_preserving_mode/preserve_inner_content.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/whitespace_preserving_mode/preserve_inner_content.js index 32f8225e829..e089ff0eb75 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/whitespace_preserving_mode/preserve_inner_content.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_i18n/whitespace_preserving_mode/preserve_inner_content.js @@ -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" diff --git a/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts b/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts index 03c15cdfa2a..09958f631fb 100644 --- a/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts +++ b/packages/compiler-cli/test/compliance/test_helpers/i18n_helpers.ts @@ -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 : ''} */ `; diff --git a/packages/compiler/src/output/output_ast.ts b/packages/compiler/src/output/output_ast.ts index 40bc7b83483..3f5ad7467a6 100644 --- a/packages/compiler/src/output/output_ast.ts +++ b/packages/compiler/src/output/output_ast.ts @@ -1340,6 +1340,7 @@ export const enum JSDocTagName { Desc = 'desc', Id = 'id', Meaning = 'meaning', + Suppress = 'suppress', } /* diff --git a/packages/compiler/src/render3/view/i18n/get_msg_utils.ts b/packages/compiler/src/render3/view/i18n/get_msg_utils.ts index 1ead29cb4e6..61fc84b77b9 100644 --- a/packages/compiler/src/render3/view/i18n/get_msg_utils.ts +++ b/packages/compiler/src/render3/view/i18n/get_msg_utils.ts @@ -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]; } diff --git a/packages/compiler/src/render3/view/i18n/meta.ts b/packages/compiler/src/render3/view/i18n/meta.ts index d4e229eb35c..3958f29079f 100644 --- a/packages/compiler/src/render3/view/i18n/meta.ts +++ b/packages/compiler/src/render3/view/i18n/meta.ts @@ -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); } diff --git a/packages/compiler/test/render3/view/i18n_spec.ts b/packages/compiler/test/render3/view/i18n_spec.ts index 71cc38a32aa..ec664981f62 100644 --- a/packages/compiler/test/render3/view/i18n_spec.ts +++ b/packages/compiler/test/render3/view/i18n_spec.ts @@ -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', () => {