From 5e8c22bf28f4c1a3f92976dd9eae12a914c2d183 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Wed, 14 Jun 2023 17:57:04 -0700 Subject: [PATCH] Fix i18n lint rule with emoji->emojify component --- .eslint/rules/valid-i18n-keys.js | 20 +++++++++++++++++--- .eslint/rules/valid-i18n-keys.test.js | 11 +++++++++++ _locales/en/messages.json | 2 +- build/intl-linter/linter.ts | 8 ++++---- build/intl-linter/rules/wrapEmoji.ts | 18 ++++++++++-------- ts/components/Intl.stories.tsx | 2 +- ts/util/setupI18n.tsx | 8 ++++---- 7 files changed, 48 insertions(+), 21 deletions(-) diff --git a/.eslint/rules/valid-i18n-keys.js b/.eslint/rules/valid-i18n-keys.js index 6013d0650c0a..545f5d9933c9 100644 --- a/.eslint/rules/valid-i18n-keys.js +++ b/.eslint/rules/valid-i18n-keys.js @@ -8,6 +8,7 @@ const globalMessages = require('../../_locales/en/messages.json'); const messageKeys = Object.keys(globalMessages).sort((a, b) => { return a.localeCompare(b); }); + const allIcuParams = messageKeys .filter(key => { return isIcuMessageKey(globalMessages, key); @@ -18,9 +19,12 @@ const allIcuParams = messageKeys ).join('\n'); }); +const DEFAULT_RICH_TEXT_ELEMENT_NAMES = ['emojify']; + const hashSum = crypto.createHash('sha256'); hashSum.update(messageKeys.join('\n')); hashSum.update(allIcuParams.join('\n')); +hashSum.update(DEFAULT_RICH_TEXT_ELEMENT_NAMES.join('\n')); const messagesCacheKey = hashSum.digest('hex'); function isI18nCall(node) { @@ -129,7 +133,7 @@ function isDeletedMessageKey(messages, key) { return description?.toLowerCase().startsWith('(deleted '); } -function getIcuMessageParams(message) { +function getIcuMessageParams(message, defaultRichTextElementNames = []) { const params = new Set(); function visitOptions(options) { @@ -177,6 +181,10 @@ function getIcuMessageParams(message) { visit(icuParser.parse(message)); + for (const defaultRichTextElementName of defaultRichTextElementNames) { + params.delete(defaultRichTextElementName); + } + return params; } @@ -286,7 +294,10 @@ module.exports = { return; } - const params = getIcuMessageParams(messages[key].messageformat); + const params = getIcuMessageParams( + messages[key].messageformat, + DEFAULT_RICH_TEXT_ELEMENT_NAMES + ); const components = getIntlElementComponents(node); if (params.size === 0) { @@ -389,7 +400,10 @@ module.exports = { return; } - const params = getIcuMessageParams(messages[key].messageformat); + const params = getIcuMessageParams( + messages[key].messageformat, + DEFAULT_RICH_TEXT_ELEMENT_NAMES + ); const values = getI18nCallValues(node); if (params.size === 0) { diff --git a/.eslint/rules/valid-i18n-keys.test.js b/.eslint/rules/valid-i18n-keys.test.js index ae60794705e0..c64176779f28 100644 --- a/.eslint/rules/valid-i18n-keys.test.js +++ b/.eslint/rules/valid-i18n-keys.test.js @@ -23,6 +23,9 @@ const __mockMessages__ = { 'icu:nested': { messageformat: '{one, select, other {{two, plural, other {{three}}}}}}', }, + 'icu:emojify': { + messageformat: '👩', + }, }; // Need to load so mocha doesn't complain about polluting the global namespace @@ -65,6 +68,14 @@ ruleTester.run('valid-i18n-keys', rule, { code: `i18n("icu:nested", { one: "1", two: "2", three: "3" })`, options: [{ messagesCacheKey, __mockMessages__ }], }, + { + code: `i18n("icu:emojify")`, + options: [{ messagesCacheKey, __mockMessages__ }], + }, + { + code: `let jsx = `, + options: [{ messagesCacheKey, __mockMessages__ }], + }, ], invalid: [ { diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6784f66ae3cf..adafcd090596 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -6491,7 +6491,7 @@ "description": "Release notes for v6.22" }, "icu:WhatsNew__v6.22--1": { - "messageformat": "We added support for the latest emoji characters, so now you can express your excitement with \"Shaking Face\" (🫨) or react with a \"Pea Pod\" (🫛) when someone asks you how close you are to your friends.", + "messageformat": "We added support for the latest emoji characters, so now you can express your excitement with \"Shaking Face\" (🫨) or react with a \"Pea Pod\" (🫛) when someone asks you how close you are to your friends.", "description": "Release notes for v6.22" } } diff --git a/build/intl-linter/linter.ts b/build/intl-linter/linter.ts index 0f8a10c4e29a..6e1b52387959 100644 --- a/build/intl-linter/linter.ts +++ b/build/intl-linter/linter.ts @@ -81,19 +81,19 @@ const tests: Record = { expectErrors: ['wrapEmoji'], }, 'icu:wrapEmoji:2': { - messageformat: '👩 extra', + messageformat: '👩 extra', expectErrors: ['wrapEmoji'], }, 'icu:wrapEmoji:3': { - messageformat: '👩👩', + messageformat: '👩👩', expectErrors: ['wrapEmoji'], }, 'icu:wrapEmoji:4': { - messageformat: '{emoji}', + messageformat: '{emoji}', expectErrors: ['wrapEmoji'], }, 'icu:wrapEmoji:5': { - messageformat: '👩', + messageformat: '👩', expectErrors: [], }, }; diff --git a/build/intl-linter/rules/wrapEmoji.ts b/build/intl-linter/rules/wrapEmoji.ts index dac933ae1c10..627d1fc30925 100644 --- a/build/intl-linter/rules/wrapEmoji.ts +++ b/build/intl-linter/rules/wrapEmoji.ts @@ -12,24 +12,26 @@ import { } from '@formatjs/icu-messageformat-parser'; import { rule } from '../utils/rule'; -function isEmojiTag( +function isEmojifyTag( element: MessageFormatElement | null ): element is TagElement { - return element != null && isTagElement(element) && element.value === 'emoji'; + return ( + element != null && isTagElement(element) && element.value === 'emojify' + ); } export default rule('wrapEmoji', context => { const emojiRegex = getEmojiRegex(); return { enterTag(element) { - if (!isEmojiTag(element)) { + if (!isEmojifyTag(element)) { return; } if (element.children.length !== 1) { // multiple children context.report( - 'Only use a single literal emoji in tags with no additional text.', + 'Only use a single literal emoji in tags with no additional text.', element.location ); return; @@ -39,7 +41,7 @@ export default rule('wrapEmoji', context => { if (!isLiteralElement(child)) { // non-literal context.report( - 'Only use a single literal emoji in tags with no additional text.', + 'Only use a single literal emoji in tags with no additional text.', child.location ); } @@ -51,10 +53,10 @@ export default rule('wrapEmoji', context => { return; } - if (!isEmojiTag(parent)) { + if (!isEmojifyTag(parent)) { // unwrapped context.report( - 'Use to wrap emoji in translation strings.', + 'Use to wrap emoji in translation strings.', element.location ); return; @@ -64,7 +66,7 @@ export default rule('wrapEmoji', context => { if (emoji !== element.value) { // extra text other than emoji context.report( - 'Only use a single literal emoji in tags with no additional text.', + 'Only use a single literal emoji in tags with no additional text.', element.location ); } diff --git a/ts/components/Intl.stories.tsx b/ts/components/Intl.stories.tsx index 3d3e2c5b3ba2..d2dfb2359b90 100644 --- a/ts/components/Intl.stories.tsx +++ b/ts/components/Intl.stories.tsx @@ -70,7 +70,7 @@ MultipleTagReplacement.args = createProps({ export function Emoji(): JSX.Element { const customI18n = setupI18n('en', { 'icu:emoji': { - messageformat: '👋 Hello, world!', + messageformat: '👋 Hello, world!', }, }); diff --git a/ts/util/setupI18n.tsx b/ts/util/setupI18n.tsx index 3b861246abb5..a62cb384c40f 100644 --- a/ts/util/setupI18n.tsx +++ b/ts/util/setupI18n.tsx @@ -33,10 +33,10 @@ function filterLegacyMessages( return icuMessages; } -export function renderEmoji(parts: ReadonlyArray): JSX.Element { - strictAssert(parts.length === 1, ' must contain only one child'); +export function renderEmojify(parts: ReadonlyArray): JSX.Element { + strictAssert(parts.length === 1, ' must contain only one child'); const text = parts[0]; - strictAssert(typeof text === 'string', ' must contain only text'); + strictAssert(typeof text === 'string', ' must contain only text'); return ; } @@ -50,7 +50,7 @@ export function createCachedIntl( locale: locale.replace('_', '-'), // normalize supported locales to browser format messages: icuMessages, defaultRichTextElements: { - emoji: renderEmoji, + emojify: renderEmojify, }, }, intlCache