Fix i18n lint rule with emoji->emojify component
This commit is contained in:
parent
35e07832a6
commit
5e8c22bf28
7 changed files with 48 additions and 21 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue