Fix i18n lint rule with emoji->emojify component

This commit is contained in:
Jamie Kyle 2023-06-14 17:57:04 -07:00 committed by GitHub
parent 35e07832a6
commit 5e8c22bf28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 48 additions and 21 deletions

View file

@ -8,6 +8,7 @@ const globalMessages = require('../../_locales/en/messages.json');
const messageKeys = Object.keys(globalMessages).sort((a, b) => { const messageKeys = Object.keys(globalMessages).sort((a, b) => {
return a.localeCompare(b); return a.localeCompare(b);
}); });
const allIcuParams = messageKeys const allIcuParams = messageKeys
.filter(key => { .filter(key => {
return isIcuMessageKey(globalMessages, key); return isIcuMessageKey(globalMessages, key);
@ -18,9 +19,12 @@ const allIcuParams = messageKeys
).join('\n'); ).join('\n');
}); });
const DEFAULT_RICH_TEXT_ELEMENT_NAMES = ['emojify'];
const hashSum = crypto.createHash('sha256'); const hashSum = crypto.createHash('sha256');
hashSum.update(messageKeys.join('\n')); hashSum.update(messageKeys.join('\n'));
hashSum.update(allIcuParams.join('\n')); hashSum.update(allIcuParams.join('\n'));
hashSum.update(DEFAULT_RICH_TEXT_ELEMENT_NAMES.join('\n'));
const messagesCacheKey = hashSum.digest('hex'); const messagesCacheKey = hashSum.digest('hex');
function isI18nCall(node) { function isI18nCall(node) {
@ -129,7 +133,7 @@ function isDeletedMessageKey(messages, key) {
return description?.toLowerCase().startsWith('(deleted '); return description?.toLowerCase().startsWith('(deleted ');
} }
function getIcuMessageParams(message) { function getIcuMessageParams(message, defaultRichTextElementNames = []) {
const params = new Set(); const params = new Set();
function visitOptions(options) { function visitOptions(options) {
@ -177,6 +181,10 @@ function getIcuMessageParams(message) {
visit(icuParser.parse(message)); visit(icuParser.parse(message));
for (const defaultRichTextElementName of defaultRichTextElementNames) {
params.delete(defaultRichTextElementName);
}
return params; return params;
} }
@ -286,7 +294,10 @@ module.exports = {
return; return;
} }
const params = getIcuMessageParams(messages[key].messageformat); const params = getIcuMessageParams(
messages[key].messageformat,
DEFAULT_RICH_TEXT_ELEMENT_NAMES
);
const components = getIntlElementComponents(node); const components = getIntlElementComponents(node);
if (params.size === 0) { if (params.size === 0) {
@ -389,7 +400,10 @@ module.exports = {
return; return;
} }
const params = getIcuMessageParams(messages[key].messageformat); const params = getIcuMessageParams(
messages[key].messageformat,
DEFAULT_RICH_TEXT_ELEMENT_NAMES
);
const values = getI18nCallValues(node); const values = getI18nCallValues(node);
if (params.size === 0) { if (params.size === 0) {

View file

@ -23,6 +23,9 @@ const __mockMessages__ = {
'icu:nested': { 'icu:nested': {
messageformat: '{one, select, other {{two, plural, other {{three}}}}}}', messageformat: '{one, select, other {{two, plural, other {{three}}}}}}',
}, },
'icu:emojify': {
messageformat: '<emojify>👩</emojify>',
},
}; };
// Need to load so mocha doesn't complain about polluting the global namespace // 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" })`, code: `i18n("icu:nested", { one: "1", two: "2", three: "3" })`,
options: [{ messagesCacheKey, __mockMessages__ }], options: [{ messagesCacheKey, __mockMessages__ }],
}, },
{
code: `i18n("icu:emojify")`,
options: [{ messagesCacheKey, __mockMessages__ }],
},
{
code: `let jsx = <Intl id="icu:emojify"/>`,
options: [{ messagesCacheKey, __mockMessages__ }],
},
], ],
invalid: [ invalid: [
{ {

View file

@ -6491,7 +6491,7 @@
"description": "Release notes for v6.22" "description": "Release notes for v6.22"
}, },
"icu:WhatsNew__v6.22--1": { "icu:WhatsNew__v6.22--1": {
"messageformat": "We added support for the latest emoji characters, so now you can express your excitement with \"Shaking Face\" (<emoji>🫨</emoji>) or react with a \"Pea Pod\" (<emoji>🫛</emoji>) 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\" (<emojify>🫨</emojify>) or react with a \"Pea Pod\" (<emojify>🫛</emojify>) when someone asks you how close you are to your friends.",
"description": "Release notes for v6.22" "description": "Release notes for v6.22"
} }
} }

View file

@ -81,19 +81,19 @@ const tests: Record<string, Test> = {
expectErrors: ['wrapEmoji'], expectErrors: ['wrapEmoji'],
}, },
'icu:wrapEmoji:2': { 'icu:wrapEmoji:2': {
messageformat: '<emoji>👩 extra</emoji>', messageformat: '<emojify>👩 extra</emojify>',
expectErrors: ['wrapEmoji'], expectErrors: ['wrapEmoji'],
}, },
'icu:wrapEmoji:3': { 'icu:wrapEmoji:3': {
messageformat: '<emoji>👩👩</emoji>', messageformat: '<emojify>👩👩</emojify>',
expectErrors: ['wrapEmoji'], expectErrors: ['wrapEmoji'],
}, },
'icu:wrapEmoji:4': { 'icu:wrapEmoji:4': {
messageformat: '<emoji>{emoji}</emoji>', messageformat: '<emojify>{emoji}</emojify>',
expectErrors: ['wrapEmoji'], expectErrors: ['wrapEmoji'],
}, },
'icu:wrapEmoji:5': { 'icu:wrapEmoji:5': {
messageformat: '<emoji>👩</emoji>', messageformat: '<emojify>👩</emojify>',
expectErrors: [], expectErrors: [],
}, },
}; };

View file

@ -12,24 +12,26 @@ import {
} from '@formatjs/icu-messageformat-parser'; } from '@formatjs/icu-messageformat-parser';
import { rule } from '../utils/rule'; import { rule } from '../utils/rule';
function isEmojiTag( function isEmojifyTag(
element: MessageFormatElement | null element: MessageFormatElement | null
): element is TagElement { ): element is TagElement {
return element != null && isTagElement(element) && element.value === 'emoji'; return (
element != null && isTagElement(element) && element.value === 'emojify'
);
} }
export default rule('wrapEmoji', context => { export default rule('wrapEmoji', context => {
const emojiRegex = getEmojiRegex(); const emojiRegex = getEmojiRegex();
return { return {
enterTag(element) { enterTag(element) {
if (!isEmojiTag(element)) { if (!isEmojifyTag(element)) {
return; return;
} }
if (element.children.length !== 1) { if (element.children.length !== 1) {
// multiple children // multiple children
context.report( context.report(
'Only use a single literal emoji in <emoji> tags with no additional text.', 'Only use a single literal emoji in <emojify> tags with no additional text.',
element.location element.location
); );
return; return;
@ -39,7 +41,7 @@ export default rule('wrapEmoji', context => {
if (!isLiteralElement(child)) { if (!isLiteralElement(child)) {
// non-literal // non-literal
context.report( context.report(
'Only use a single literal emoji in <emoji> tags with no additional text.', 'Only use a single literal emoji in <emojify> tags with no additional text.',
child.location child.location
); );
} }
@ -51,10 +53,10 @@ export default rule('wrapEmoji', context => {
return; return;
} }
if (!isEmojiTag(parent)) { if (!isEmojifyTag(parent)) {
// unwrapped // unwrapped
context.report( context.report(
'Use <emoji> to wrap emoji in translation strings.', 'Use <emojify> to wrap emoji in translation strings.',
element.location element.location
); );
return; return;
@ -64,7 +66,7 @@ export default rule('wrapEmoji', context => {
if (emoji !== element.value) { if (emoji !== element.value) {
// extra text other than emoji // extra text other than emoji
context.report( context.report(
'Only use a single literal emoji in <emoji> tags with no additional text.', 'Only use a single literal emoji in <emojify> tags with no additional text.',
element.location element.location
); );
} }

View file

@ -70,7 +70,7 @@ MultipleTagReplacement.args = createProps({
export function Emoji(): JSX.Element { export function Emoji(): JSX.Element {
const customI18n = setupI18n('en', { const customI18n = setupI18n('en', {
'icu:emoji': { 'icu:emoji': {
messageformat: '<emoji>👋</emoji> Hello, world!', messageformat: '<emojify>👋</emojify> Hello, world!',
}, },
}); });

View file

@ -33,10 +33,10 @@ function filterLegacyMessages(
return icuMessages; return icuMessages;
} }
export function renderEmoji(parts: ReadonlyArray<unknown>): JSX.Element { export function renderEmojify(parts: ReadonlyArray<unknown>): JSX.Element {
strictAssert(parts.length === 1, '<emoji> must contain only one child'); strictAssert(parts.length === 1, '<emojify> must contain only one child');
const text = parts[0]; const text = parts[0];
strictAssert(typeof text === 'string', '<emoji> must contain only text'); strictAssert(typeof text === 'string', '<emojify> must contain only text');
return <Emojify text={text} />; return <Emojify text={text} />;
} }
@ -50,7 +50,7 @@ export function createCachedIntl(
locale: locale.replace('_', '-'), // normalize supported locales to browser format locale: locale.replace('_', '-'), // normalize supported locales to browser format
messages: icuMessages, messages: icuMessages,
defaultRichTextElements: { defaultRichTextElements: {
emoji: renderEmoji, emojify: renderEmojify,
}, },
}, },
intlCache intlCache