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