From 7d072e2ec053f388adce00be54c75d8c76373699 Mon Sep 17 00:00:00 2001 From: Adrian Baran <93612066+adbaran1@users.noreply.github.com> Date: Thu, 18 Nov 2021 07:12:44 -0600 Subject: [PATCH] feat(i18n): option to require description for i18n metadata (#804) --- .../eslint-plugin-template/docs/rules/i18n.md | 60 +++++++++++++++++++ .../eslint-plugin-template/src/rules/i18n.ts | 43 +++++++++---- .../tests/rules/i18n/cases.ts | 32 ++++++++++ 3 files changed, 123 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin-template/docs/rules/i18n.md b/packages/eslint-plugin-template/docs/rules/i18n.md index 2a1552c51..8c04e01d5 100644 --- a/packages/eslint-plugin-template/docs/rules/i18n.md +++ b/packages/eslint-plugin-template/docs/rules/i18n.md @@ -49,6 +49,7 @@ interface Options { */ ignoreAttributes?: string[]; ignoreTags?: string[]; + requireDescription?: boolean; } ``` @@ -450,6 +451,36 @@ interface Options { ``` +
+ +--- + +
+ +#### Custom Config + +```json +{ + "rules": { + "@angular-eslint/template/i18n": [ + "error", + { + "requireDescription": true + } + ] + } +} +``` + +
+ +#### ❌ Invalid Code + +```html +

Hello

+~~~~~~~~~~~~~~~~~~~ +``` +
@@ -1130,6 +1161,35 @@ interface Options { ``` +
+ +--- + +
+ +#### Custom Config + +```json +{ + "rules": { + "@angular-eslint/template/i18n": [ + "error", + { + "requireDescription": true + } + ] + } +} +``` + +
+ +#### ✅ Valid Code + +```html +

Hello i18n!

+``` +
diff --git a/packages/eslint-plugin-template/src/rules/i18n.ts b/packages/eslint-plugin-template/src/rules/i18n.ts index 91295cc3c..6ad47fcf3 100644 --- a/packages/eslint-plugin-template/src/rules/i18n.ts +++ b/packages/eslint-plugin-template/src/rules/i18n.ts @@ -58,6 +58,7 @@ type Options = [ readonly checkText?: boolean; readonly ignoreAttributes?: readonly string[]; readonly ignoreTags?: readonly string[]; + readonly requireDescription?: boolean; }, ]; export type MessageIds = @@ -66,7 +67,8 @@ export type MessageIds = | 'i18nCustomIdOnAttribute' | 'i18nCustomIdOnElement' | 'i18nDuplicateCustomId' - | 'suggestAddI18nAttribute'; + | 'suggestAddI18nAttribute' + | 'i18nMissingDescription'; type StronglyTypedElement = Omit & { i18n: Message; parent?: AST; @@ -98,6 +100,8 @@ const STYLE_GUIDE_LINK_ICU = `${STYLE_GUIDE_LINK}#mark-plurals-and-alternates-fo const STYLE_GUIDE_LINK_TEXTS = `${STYLE_GUIDE_LINK}#mark-text-for-translations`; const STYLE_GUIDE_LINK_CUSTOM_IDS = `${STYLE_GUIDE_LINK}#manage-marked-text-with-custom-ids`; const STYLE_GUIDE_LINK_UNIQUE_CUSTOM_IDS = `${STYLE_GUIDE_LINK}#define-unique-custom-ids`; +const STYLE_GUIDE_LINK_COMMON_PREPARE = `${STYLE_GUIDE_LINK}-common-prepare`; +const STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION = `${STYLE_GUIDE_LINK_COMMON_PREPARE}#i18n-metadata-for-translation`; export default createESLintRule({ name: RULE_NAME, @@ -147,6 +151,10 @@ export default createESLintRule({ type: 'string', }, }, + requireDescription: { + type: 'boolean', + default: DEFAULT_OPTIONS.requireDescription, + }, }, additionalProperties: false, }, @@ -158,6 +166,7 @@ export default createESLintRule({ i18nCustomIdOnElement: `Missing custom ID on element. See more at ${STYLE_GUIDE_LINK_CUSTOM_IDS}`, i18nDuplicateCustomId: `Duplicate custom ID "@@{{customId}}". See more at ${STYLE_GUIDE_LINK_UNIQUE_CUSTOM_IDS}`, suggestAddI18nAttribute: 'Add the `i18n` attribute', + i18nMissingDescription: `Missing i18n description on element. See more at ${STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION}`, }, }, defaultOptions: [DEFAULT_OPTIONS], @@ -171,6 +180,7 @@ export default createESLintRule({ checkText, ignoreAttributes, ignoreTags, + requireDescription, }, ], ) { @@ -187,7 +197,7 @@ export default createESLintRule({ const collectedCustomIds = new Map(); function handleElement({ - i18n: { customId }, + i18n: { description, customId }, name, parent, sourceSpan, @@ -196,17 +206,26 @@ export default createESLintRule({ return; } - if (!isEmpty(customId)) { - const sourceSpans = collectedCustomIds.get(customId) ?? []; - collectedCustomIds.set(customId, [...sourceSpans, sourceSpan]); - return; + const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan); + + if (checkId) { + if (isEmpty(customId)) { + context.report({ + messageId: 'i18nCustomIdOnElement', + loc, + }); + } else { + const sourceSpans = collectedCustomIds.get(customId) ?? []; + collectedCustomIds.set(customId, [...sourceSpans, sourceSpan]); + } } - const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan); - context.report({ - messageId: 'i18nCustomIdOnElement', - loc, - }); + if (requireDescription && isEmpty(description)) { + context.report({ + messageId: 'i18nMissingDescription', + loc, + }); + } } function handleTextAttribute({ @@ -308,7 +327,7 @@ export default createESLintRule({ } return { - ...(checkId && { + ...((checkId || requireDescription) && { 'Element[i18n]'(node: StronglyTypedElement) { handleElement(node); }, diff --git a/packages/eslint-plugin-template/tests/rules/i18n/cases.ts b/packages/eslint-plugin-template/tests/rules/i18n/cases.ts index 4e17a6bf7..453881e7f 100644 --- a/packages/eslint-plugin-template/tests/rules/i18n/cases.ts +++ b/packages/eslint-plugin-template/tests/rules/i18n/cases.ts @@ -7,6 +7,7 @@ const i18nCustomIdOnAttribute: MessageIds = 'i18nCustomIdOnAttribute'; const i18nCustomIdOnElement: MessageIds = 'i18nCustomIdOnElement'; const i18nDuplicateCustomId: MessageIds = 'i18nDuplicateCustomId'; const suggestAddI18nAttribute: MessageIds = 'suggestAddI18nAttribute'; +const i18nMissingDescription: MessageIds = 'i18nMissingDescription'; export const valid = [ ` @@ -157,6 +158,18 @@ export const valid = [
  • ItemC
  • `, + { + code: ` +

    Hello i18n!

    + `, + options: [{ checkId: false, requireDescription: true }], + }, + { + code: ` +

    Hello i18n!

    + `, + options: [{ requireDescription: true }], + }, ]; export const invalid = [ @@ -451,4 +464,23 @@ export const invalid = [ `, }), + convertAnnotatedSourceToFailureCase({ + description: 'should fail if i18n description is missing', + annotatedSource: ` +

    Hello

    + ~~~~~~~~~~~~~~~~~~~ + `, + messageId: i18nMissingDescription, + options: [{ checkId: false, requireDescription: true }], + }), + convertAnnotatedSourceToFailureCase({ + description: + 'should fail if i18n description is missing, despite an ID being provided', + annotatedSource: ` +

    Hello

    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + `, + messageId: i18nMissingDescription, + options: [{ requireDescription: true }], + }), ];