mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
ICU types
This commit is contained in:
@@ -1,471 +0,0 @@
|
||||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const crypto = require('crypto');
|
||||
const icuParser = require('@formatjs/icu-messageformat-parser');
|
||||
|
||||
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);
|
||||
})
|
||||
.map(key => {
|
||||
return Array.from(
|
||||
getIcuMessageParams(globalMessages[key].messageformat)
|
||||
).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) {
|
||||
return (
|
||||
(node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.name === 'i18n') ||
|
||||
(node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.name === 'i18n')
|
||||
);
|
||||
}
|
||||
|
||||
function isIntlElement(node) {
|
||||
return (
|
||||
node.type === 'JSXOpeningElement' &&
|
||||
node.name.type === 'JSXIdentifier' &&
|
||||
node.name.name === 'Intl'
|
||||
);
|
||||
}
|
||||
|
||||
function isStringLiteral(node) {
|
||||
return node.type === 'Literal' && typeof node.value === 'string';
|
||||
}
|
||||
|
||||
function valueToMessageKey(node) {
|
||||
if (isStringLiteral(node)) {
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getI18nCallMessageKey(node) {
|
||||
if (node.arguments.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let arg1 = node.arguments[0];
|
||||
if (arg1 == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return valueToMessageKey(arg1);
|
||||
}
|
||||
|
||||
function getI18nCallValues(node) {
|
||||
// babel-eslint messes with elements arrays in some cases because of TS
|
||||
if (node.arguments.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return node.arguments[1];
|
||||
}
|
||||
|
||||
function getIntlElementMessageKey(node) {
|
||||
let idAttribute = node.attributes.find(attribute => {
|
||||
return (
|
||||
attribute.type === 'JSXAttribute' &&
|
||||
attribute.name.type === 'JSXIdentifier' &&
|
||||
attribute.name.name === 'id'
|
||||
);
|
||||
});
|
||||
|
||||
if (idAttribute == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let value = idAttribute.value;
|
||||
|
||||
return valueToMessageKey(value);
|
||||
}
|
||||
|
||||
function getIntlElementComponents(node) {
|
||||
let componentsAttribute = node.attributes.find(attribute => {
|
||||
return (
|
||||
attribute.type === 'JSXAttribute' &&
|
||||
attribute.name.type === 'JSXIdentifier' &&
|
||||
attribute.name.name === 'components'
|
||||
);
|
||||
});
|
||||
|
||||
if (componentsAttribute == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let value = componentsAttribute.value;
|
||||
if (value?.type !== 'JSXExpressionContainer') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value.expression;
|
||||
}
|
||||
|
||||
function isValidMessageKey(messages, key) {
|
||||
return Object.hasOwn(messages, key);
|
||||
}
|
||||
|
||||
function isIcuMessageKey(messages, key) {
|
||||
if (!key.startsWith('icu:')) {
|
||||
return false;
|
||||
}
|
||||
const message = messages[key];
|
||||
return message?.messageformat != null;
|
||||
}
|
||||
|
||||
function isDeletedMessageKey(messages, key) {
|
||||
const description = messages[key]?.description;
|
||||
return description?.toLowerCase().startsWith('(deleted ');
|
||||
}
|
||||
|
||||
function getIcuMessageParams(message, defaultRichTextElementNames = []) {
|
||||
const params = new Set();
|
||||
|
||||
function visitOptions(options) {
|
||||
for (const option of Object.values(options)) {
|
||||
visit(option.value);
|
||||
}
|
||||
}
|
||||
|
||||
function visit(elements) {
|
||||
for (const element of elements) {
|
||||
switch (element.type) {
|
||||
case icuParser.TYPE.argument:
|
||||
params.add(element.value);
|
||||
break;
|
||||
case icuParser.TYPE.date:
|
||||
params.add(element.value);
|
||||
break;
|
||||
case icuParser.TYPE.literal:
|
||||
break;
|
||||
case icuParser.TYPE.number:
|
||||
params.add(element.value);
|
||||
break;
|
||||
case icuParser.TYPE.plural:
|
||||
params.add(element.value);
|
||||
visitOptions(element.options);
|
||||
break;
|
||||
case icuParser.TYPE.pound:
|
||||
break;
|
||||
case icuParser.TYPE.select:
|
||||
params.add(element.value);
|
||||
visitOptions(element.options);
|
||||
break;
|
||||
case icuParser.TYPE.tag:
|
||||
params.add(element.value);
|
||||
visit(element.children);
|
||||
break;
|
||||
case icuParser.TYPE.time:
|
||||
params.add(element.value);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown element type: ${element.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit(icuParser.parse(message));
|
||||
|
||||
for (const defaultRichTextElementName of defaultRichTextElementNames) {
|
||||
params.delete(defaultRichTextElementName);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function getMissingFromSet(expected, actual) {
|
||||
const result = new Set();
|
||||
for (const item of expected) {
|
||||
if (!actual.has(item)) {
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
messagesCacheKey,
|
||||
meta: {
|
||||
type: 'problem',
|
||||
hasSuggestions: false,
|
||||
fixable: false,
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
messagesCacheKey: {
|
||||
type: 'string',
|
||||
},
|
||||
__mockMessages__: {
|
||||
type: 'object',
|
||||
patternProperties: {
|
||||
'.*': {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
},
|
||||
required: ['message'],
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
messageformat: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
},
|
||||
required: ['messageformat'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['messagesCacheKey'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
create(context) {
|
||||
const messagesCacheKeyOption = context.options[0].messagesCacheKey;
|
||||
if (messagesCacheKeyOption !== messagesCacheKey) {
|
||||
throw new Error(
|
||||
`The cache key for the i18n rule does not match the current messages.json file (expected: ${messagesCacheKey}, received: ${messagesCacheKeyOption})`
|
||||
);
|
||||
}
|
||||
|
||||
const mockMessages = context.options[0].__mockMessages__;
|
||||
const messages = mockMessages ?? globalMessages;
|
||||
|
||||
return {
|
||||
JSXOpeningElement(node) {
|
||||
if (!isIntlElement(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getIntlElementMessageKey(node);
|
||||
|
||||
if (key == null) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"<Intl> must always be provided an 'id' attribute with a literal string",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidMessageKey(messages, key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `<Intl> id "${key}" not found in _locales/en/messages.json`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isIcuMessageKey(messages, key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `<Intl> id "${key}" is not an ICU message in _locales/en/messages.json`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDeletedMessageKey(messages, key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `<Intl> id "${key}" is marked as deleted in _locales/en/messages.json`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const params = getIcuMessageParams(
|
||||
messages[key].messageformat,
|
||||
DEFAULT_RICH_TEXT_ELEMENT_NAMES
|
||||
);
|
||||
const components = getIntlElementComponents(node);
|
||||
|
||||
if (params.size === 0) {
|
||||
if (components != null) {
|
||||
context.report({
|
||||
node,
|
||||
message: `<Intl> message "${key}" does not have any params, but has a "components" attribute`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (components == null) {
|
||||
context.report({
|
||||
node,
|
||||
message: `<Intl> message "${key}" has params, but is missing a "components" attribute`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (components.type !== 'ObjectExpression') {
|
||||
context.report({
|
||||
node: components,
|
||||
message: `<Intl> "components" attribute must be an object literal`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const props = new Set();
|
||||
for (const property of components.properties) {
|
||||
if (property.type !== 'Property' || property.computed) {
|
||||
context.report({
|
||||
node: property,
|
||||
message: `<Intl> "components" attribute must only contain literal keys`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
props.add(property.key.name);
|
||||
}
|
||||
|
||||
const missingParams = getMissingFromSet(params, props);
|
||||
if (missingParams.size > 0) {
|
||||
for (const param of missingParams) {
|
||||
context.report({
|
||||
node: components,
|
||||
message: `<Intl> message "${key}" has a param "${param}", but no corresponding component`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const extraComponents = getMissingFromSet(props, params);
|
||||
if (extraComponents.size > 0) {
|
||||
for (const prop of extraComponents) {
|
||||
context.report({
|
||||
node: components,
|
||||
message: `<Intl> message "${key}" has a component "${prop}", but no corresponding param`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
CallExpression(node) {
|
||||
if (!isI18nCall(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getI18nCallMessageKey(node);
|
||||
|
||||
if (key == null) {
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
"i18n()'s first argument should always be a literal string",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidMessageKey(messages, key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `i18n() key "${key}" not found in _locales/en/messages.json`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isIcuMessageKey(messages, key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `i18n() key "${key}" is not an ICU message in _locales/en/messages.json`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDeletedMessageKey(messages, key)) {
|
||||
context.report({
|
||||
node,
|
||||
message: `i18n() key "${key}" is marked as deleted in _locales/en/messages.json`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const params = getIcuMessageParams(
|
||||
messages[key].messageformat,
|
||||
DEFAULT_RICH_TEXT_ELEMENT_NAMES
|
||||
);
|
||||
const values = getI18nCallValues(node);
|
||||
|
||||
if (params.size === 0) {
|
||||
if (values != null) {
|
||||
context.report({
|
||||
node,
|
||||
message: `i18n() message "${key}" does not have any params, but has a "values" argument`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (values == null) {
|
||||
context.report({
|
||||
node,
|
||||
message: `i18n() message "${key}" has params, but is missing a "values" argument`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.type !== 'ObjectExpression') {
|
||||
context.report({
|
||||
node: values,
|
||||
message: `i18n() "values" argument must be an object literal`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const props = new Set();
|
||||
for (const property of values.properties) {
|
||||
if (property.type !== 'Property' || property.computed) {
|
||||
context.report({
|
||||
node: property,
|
||||
message: `i18n() "values" argument must only contain literal keys`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
props.add(property.key.name);
|
||||
}
|
||||
|
||||
const missingParams = getMissingFromSet(params, props);
|
||||
if (missingParams.size > 0) {
|
||||
for (const param of missingParams) {
|
||||
context.report({
|
||||
node: values,
|
||||
message: `i18n() message "${key}" has a param "${param}", but no corresponding value`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const extraProps = getMissingFromSet(props, params);
|
||||
if (extraProps.size > 0) {
|
||||
for (const prop of extraProps) {
|
||||
context.report({
|
||||
node: values,
|
||||
message: `i18n() message "${key}" has a value "${prop}", but no corresponding param`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,415 +0,0 @@
|
||||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
const rule = require('./valid-i18n-keys');
|
||||
const RuleTester = require('eslint').RuleTester;
|
||||
|
||||
const messagesCacheKey = rule.messagesCacheKey;
|
||||
|
||||
const __mockMessages__ = {
|
||||
legacy_real_message: {
|
||||
message: 'Legacy $message$',
|
||||
},
|
||||
'icu:real_message': {
|
||||
messageformat: 'ICU {message}',
|
||||
},
|
||||
'icu:deleted_message': {
|
||||
messageformat: 'shouldnt use me anymore',
|
||||
description: '(deleted 01/01/1970)',
|
||||
},
|
||||
'icu:no_params': {
|
||||
messageformat: 'ICU message',
|
||||
},
|
||||
'icu:nested': {
|
||||
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
|
||||
require('@typescript-eslint/parser');
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
parser: require.resolve('@typescript-eslint/parser'),
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ruleTester.run('valid-i18n-keys', rule, {
|
||||
valid: [
|
||||
{
|
||||
code: `i18n("icu:real_message", { message: "foo" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `window.i18n("icu:real_message", { message: "foo" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message" components={{ message: "foo" }}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:no_params")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:no_params"/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:nested", { one: "1", two: "2", three: "3" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:emojify")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:emojify"/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: `i18n("legacy_real_message")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() key "legacy_real_message" is not an ICU message in _locales/en/messages.json',
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `window.i18n("legacy_real_message")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() key "legacy_real_message" is not an ICU message in _locales/en/messages.json',
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="legacy_real_message"/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> id "legacy_real_message" is not an ICU message in _locales/en/messages.json',
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'i18n(`icu:real_${message}`)',
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: "i18n()'s first argument should always be a literal string",
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'window.i18n(`icu:real_${message}`)',
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: "i18n()'s first argument should always be a literal string",
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id={"icu:real_message"}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
"<Intl> must always be provided an 'id' attribute with a literal string",
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'let jsx = <Intl id={`icu:real_message`}/>',
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
"<Intl> must always be provided an 'id' attribute with a literal string",
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'let jsx = <Intl id={`icu:real_${message}`}/>',
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
"<Intl> must always be provided an 'id' attribute with a literal string",
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("THIS_KEY_SHOULD_NEVER_EXIST")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() key "THIS_KEY_SHOULD_NEVER_EXIST" not found in _locales/en/messages.json',
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n(cond ? "icu:real_message" : "icu:real_message")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: "i18n()'s first argument should always be a literal string",
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n(42)`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: "i18n()'s first argument should always be a literal string",
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="THIS_KEY_SHOULD_NEVER_EXIST"/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> id "THIS_KEY_SHOULD_NEVER_EXIST" not found in _locales/en/messages.json',
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id={cond ? "icu:real_message" : "icu:real_message"}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
"<Intl> must always be provided an 'id' attribute with a literal string",
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id={42}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
"<Intl> must always be provided an 'id' attribute with a literal string",
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:deleted_message")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() key "icu:deleted_message" is marked as deleted in _locales/en/messages.json',
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:deleted_message"/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> id "icu:deleted_message" is marked as deleted in _locales/en/messages.json',
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:no_params", { message: "foo" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() message "icu:no_params" does not have any params, but has a "values" argument',
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:real_message")`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() message "icu:real_message" has params, but is missing a "values" argument',
|
||||
type: 'CallExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:real_message", null)`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: 'i18n() "values" argument must be an object literal',
|
||||
type: 'Literal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:real_message", { [foo]: "foo" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: 'i18n() "values" argument must only contain literal keys',
|
||||
type: 'Property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:real_message", { ...props })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: 'i18n() "values" argument must only contain literal keys',
|
||||
type: 'SpreadElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:real_message", {})`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() message "icu:real_message" has a param "message", but no corresponding value',
|
||||
type: 'ObjectExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:real_message", { message: "foo", foo: "bar" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() message "icu:real_message" has a value "foo", but no corresponding param',
|
||||
type: 'ObjectExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:no_params" components={{ message: "foo" }}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> message "icu:no_params" does not have any params, but has a "components" attribute',
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message"/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> message "icu:real_message" has params, but is missing a "components" attribute',
|
||||
type: 'JSXOpeningElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message" components={null}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message: '<Intl> "components" attribute must be an object literal',
|
||||
type: 'Literal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message" components={{ [foo]: "foo" }}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> "components" attribute must only contain literal keys',
|
||||
type: 'Property',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message" components={{ ...props }}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> "components" attribute must only contain literal keys',
|
||||
type: 'SpreadElement',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message" components={{}}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> message "icu:real_message" has a param "message", but no corresponding component',
|
||||
type: 'ObjectExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `let jsx = <Intl id="icu:real_message" components={{ message: "foo", foo: "bar" }}/>`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'<Intl> message "icu:real_message" has a component "foo", but no corresponding param',
|
||||
type: 'ObjectExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: `i18n("icu:nested", { one: "1", two: "2" })`,
|
||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||
errors: [
|
||||
{
|
||||
message:
|
||||
'i18n() message "icu:nested" has a param "three", but no corresponding value',
|
||||
type: 'ObjectExpression',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user