Update i18n eslint rule to validate missing/extra icu params
This commit is contained in:
parent
4e6c3ba9df
commit
8ca192a48d
16 changed files with 449 additions and 66 deletions
|
@ -2,14 +2,25 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const icuParser = require('@formatjs/icu-messageformat-parser');
|
||||||
|
|
||||||
const globalMessages = require('../../_locales/en/messages.json');
|
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
|
||||||
|
.filter(key => {
|
||||||
|
return isIcuMessageKey(globalMessages, key);
|
||||||
|
})
|
||||||
|
.map(key => {
|
||||||
|
return Array.from(
|
||||||
|
getIcuMessageParams(globalMessages[key].messageformat)
|
||||||
|
).join('\n');
|
||||||
|
});
|
||||||
|
|
||||||
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'));
|
||||||
const messagesCacheKey = hashSum.digest('hex');
|
const messagesCacheKey = hashSum.digest('hex');
|
||||||
|
|
||||||
function isI18nCall(node) {
|
function isI18nCall(node) {
|
||||||
|
@ -54,6 +65,14 @@ function getI18nCallMessageKey(node) {
|
||||||
return valueToMessageKey(arg1);
|
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) {
|
function getIntlElementMessageKey(node) {
|
||||||
let idAttribute = node.attributes.find(attribute => {
|
let idAttribute = node.attributes.find(attribute => {
|
||||||
return (
|
return (
|
||||||
|
@ -72,6 +91,27 @@ function getIntlElementMessageKey(node) {
|
||||||
return valueToMessageKey(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) {
|
function isValidMessageKey(messages, key) {
|
||||||
return Object.hasOwn(messages, key);
|
return Object.hasOwn(messages, key);
|
||||||
}
|
}
|
||||||
|
@ -89,6 +129,67 @@ function isDeletedMessageKey(messages, key) {
|
||||||
return description?.toLowerCase().startsWith('(deleted ');
|
return description?.toLowerCase().startsWith('(deleted ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIcuMessageParams(message) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
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 = {
|
module.exports = {
|
||||||
messagesCacheKey,
|
messagesCacheKey,
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -150,7 +251,7 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = getIntlElementMessageKey(node);
|
const key = getIntlElementMessageKey(node);
|
||||||
|
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
context.report({
|
context.report({
|
||||||
|
@ -184,13 +285,76 @@ module.exports = {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const params = getIcuMessageParams(messages[key].messageformat);
|
||||||
|
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) {
|
CallExpression(node) {
|
||||||
if (!isI18nCall(node)) {
|
if (!isI18nCall(node)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = getI18nCallMessageKey(node);
|
const key = getI18nCallMessageKey(node);
|
||||||
|
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
context.report({
|
context.report({
|
||||||
|
@ -222,6 +386,70 @@ module.exports = {
|
||||||
node,
|
node,
|
||||||
message: `i18n() key "${key}" is marked as deleted in _locales/en/messages.json`,
|
message: `i18n() key "${key}" is marked as deleted in _locales/en/messages.json`,
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = getIcuMessageParams(messages[key].messageformat);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,6 +17,12 @@ const __mockMessages__ = {
|
||||||
messageformat: 'shouldnt use me anymore',
|
messageformat: 'shouldnt use me anymore',
|
||||||
description: '(deleted 01/01/1970)',
|
description: '(deleted 01/01/1970)',
|
||||||
},
|
},
|
||||||
|
'icu:no_params': {
|
||||||
|
messageformat: 'ICU message',
|
||||||
|
},
|
||||||
|
'icu:nested': {
|
||||||
|
messageformat: '{one, select, other {{two, plural, other {{three}}}}}}',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -36,15 +42,27 @@ const ruleTester = new RuleTester({
|
||||||
ruleTester.run('valid-i18n-keys', rule, {
|
ruleTester.run('valid-i18n-keys', rule, {
|
||||||
valid: [
|
valid: [
|
||||||
{
|
{
|
||||||
code: `i18n("icu:real_message")`,
|
code: `i18n("icu:real_message", { message: "foo" })`,
|
||||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: `window.i18n("icu:real_message")`,
|
code: `window.i18n("icu:real_message", { message: "foo" })`,
|
||||||
options: [{ messagesCacheKey, __mockMessages__ }],
|
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: `let jsx = <Intl id="icu:real_message"/>`,
|
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__ }],
|
options: [{ messagesCacheKey, __mockMessages__ }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -221,5 +239,166 @@ ruleTester.run('valid-i18n-keys', rule, {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -10084,7 +10084,7 @@
|
||||||
"description": "(deleted 03/29/2023) Information shown at the bottom of the profile editor section"
|
"description": "(deleted 03/29/2023) Information shown at the bottom of the profile editor section"
|
||||||
},
|
},
|
||||||
"icu:ProfileEditor--info--link": {
|
"icu:ProfileEditor--info--link": {
|
||||||
"messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. <learnMoreLink>{learnMore}</learnMoreLink>",
|
"messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. <learnMoreLink>Learn More</learnMoreLink>",
|
||||||
"description": "Information shown at the bottom of the profile editor section"
|
"description": "Information shown at the bottom of the profile editor section"
|
||||||
},
|
},
|
||||||
"ProfileEditor--learnMore": {
|
"ProfileEditor--learnMore": {
|
||||||
|
@ -10093,7 +10093,7 @@
|
||||||
},
|
},
|
||||||
"icu:ProfileEditor--learnMore": {
|
"icu:ProfileEditor--learnMore": {
|
||||||
"messageformat": "Learn More",
|
"messageformat": "Learn More",
|
||||||
"description": "Text that links to a support article"
|
"description": "(deleted 04/03/2023) Text that links to a support article"
|
||||||
},
|
},
|
||||||
"Bio--speak-freely": {
|
"Bio--speak-freely": {
|
||||||
"message": "Speak Freely",
|
"message": "Speak Freely",
|
||||||
|
|
|
@ -70,7 +70,10 @@ export function StoriesAddStoryButton({
|
||||||
|
|
||||||
if (result.reason === ReasonVideoNotGood.TooBig) {
|
if (result.reason === ReasonVideoNotGood.TooBig) {
|
||||||
setError(
|
setError(
|
||||||
i18n('icu:StoryCreator__error--video-too-big', result.renderDetails)
|
i18n('icu:StoryCreator__error--video-too-big', {
|
||||||
|
limit: result.renderDetails.limit,
|
||||||
|
units: result.renderDetails.units,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -861,7 +861,10 @@ export function StoryViewer({
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MyStories__views--strong"
|
id="icu:MyStories__views--strong"
|
||||||
components={{ viewCount, strong: renderStrong }}
|
components={{
|
||||||
|
views: viewCount,
|
||||||
|
strong: renderStrong,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(isSent || viewCount > 0) && replyCount > 0 && ' '}
|
{(isSent || viewCount > 0) && replyCount > 0 && ' '}
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { get } from 'lodash';
|
|
||||||
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
|
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
|
||||||
import { SECOND } from '../util/durations';
|
import { SECOND } from '../util/durations';
|
||||||
import { Toast } from './Toast';
|
import { Toast } from './Toast';
|
||||||
|
@ -41,7 +40,7 @@ export function ToastManager({
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
|
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
|
||||||
{i18n('icu:AddUserToAnotherGroupModal__toast--adding-user-to-group', {
|
{i18n('icu:AddUserToAnotherGroupModal__toast--adding-user-to-group', {
|
||||||
...toast.parameters,
|
contact: toast.parameters?.contact,
|
||||||
})}
|
})}
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
|
@ -106,9 +105,7 @@ export function ToastManager({
|
||||||
if (toastType === ToastType.CannotStartGroupCall) {
|
if (toastType === ToastType.CannotStartGroupCall) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
{i18n('icu:GroupV2--cannot-start-group-call', {
|
{i18n('icu:GroupV2--cannot-start-group-call')}
|
||||||
...toast.parameters,
|
|
||||||
})}
|
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -219,7 +216,10 @@ export function ToastManager({
|
||||||
if (toastType === ToastType.FileSize) {
|
if (toastType === ToastType.FileSize) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
{i18n('icu:fileSizeWarning', toast?.parameters)}
|
{i18n('icu:fileSizeWarning', {
|
||||||
|
limit: toast.parameters?.limit,
|
||||||
|
units: toast.parameters?.units,
|
||||||
|
})}
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -323,9 +323,7 @@ export function ToastManager({
|
||||||
if (toastType === ToastType.TooManyMessagesToForward) {
|
if (toastType === ToastType.TooManyMessagesToForward) {
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
{i18n('icu:SelectModeActions__toast--TooManyMessagesToForward', {
|
{i18n('icu:SelectModeActions__toast--TooManyMessagesToForward')}
|
||||||
count: get(toast.parameters, 'count'),
|
|
||||||
})}
|
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -356,7 +354,8 @@ export function ToastManager({
|
||||||
return (
|
return (
|
||||||
<Toast onClose={hideToast}>
|
<Toast onClose={hideToast}>
|
||||||
{i18n('icu:AddUserToAnotherGroupModal__toast--user-added-to-group', {
|
{i18n('icu:AddUserToAnotherGroupModal__toast--user-added-to-group', {
|
||||||
...toast.parameters,
|
contact: toast.parameters?.contact,
|
||||||
|
group: toast.parameters?.group,
|
||||||
})}
|
})}
|
||||||
</Toast>
|
</Toast>
|
||||||
);
|
);
|
||||||
|
|
|
@ -35,12 +35,7 @@ export function WhatsNewModal({
|
||||||
date: new Date(window.getBuildCreation?.() || Date.now()),
|
date: new Date(window.getBuildCreation?.() || Date.now()),
|
||||||
version: window.getVersion?.(),
|
version: window.getVersion?.(),
|
||||||
features: [
|
features: [
|
||||||
<Intl
|
<Intl i18n={i18n} id="icu:WhatsNew__v6.13--0" renderText={renderText} />,
|
||||||
i18n={i18n}
|
|
||||||
id="icu:WhatsNew__v6.13--0"
|
|
||||||
renderText={renderText}
|
|
||||||
components={{}}
|
|
||||||
/>,
|
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:WhatsNew__v6.13--1"
|
id="icu:WhatsNew__v6.13--1"
|
||||||
|
|
|
@ -52,9 +52,7 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const intlComponents = {
|
const senderTitle = <Emojify text={sender.title} />;
|
||||||
sender: <Emojify text={sender.title} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -80,13 +78,13 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
|
||||||
{inGroup ? (
|
{inGroup ? (
|
||||||
<Intl
|
<Intl
|
||||||
id="icu:DeliveryIssue--summary--group"
|
id="icu:DeliveryIssue--summary--group"
|
||||||
components={intlComponents}
|
components={{ sender: senderTitle }}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Intl
|
<Intl
|
||||||
id="icu:DeliveryIssue--summary"
|
id="icu:DeliveryIssue--summary"
|
||||||
components={intlComponents}
|
components={{ sender: senderTitle }}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -92,7 +92,7 @@ export function MandatoryProfileSharingActions({
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MessageRequests--profile-sharing--group--link"
|
id="icu:MessageRequests--profile-sharing--group--link"
|
||||||
components={{ firstName: firstNameContact, learnMoreLink }}
|
components={{ learnMoreLink }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -2555,7 +2555,7 @@ export class Message extends React.PureComponent<Props, State> {
|
||||||
<span className="module-message__alt-accessibility-tree">
|
<span className="module-message__alt-accessibility-tree">
|
||||||
<span id={`message-accessibility-label:${id}`}>
|
<span id={`message-accessibility-label:${id}`}>
|
||||||
{author.isMe
|
{author.isMe
|
||||||
? i18n('icu:messageAccessibilityLabel--outgoing', {})
|
? i18n('icu:messageAccessibilityLabel--outgoing')
|
||||||
: i18n('icu:messageAccessibilityLabel--incoming', {
|
: i18n('icu:messageAccessibilityLabel--incoming', {
|
||||||
author: author.title,
|
author: author.title,
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -77,18 +77,10 @@ export function MessageRequestActions({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{conversationType === 'group' && isBlocked && (
|
{conversationType === 'group' && isBlocked && (
|
||||||
<Intl
|
<Intl i18n={i18n} id="icu:MessageRequests--message-group-blocked" />
|
||||||
i18n={i18n}
|
|
||||||
id="icu:MessageRequests--message-group-blocked"
|
|
||||||
components={{ name }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{conversationType === 'group' && !isBlocked && (
|
{conversationType === 'group' && !isBlocked && (
|
||||||
<Intl
|
<Intl i18n={i18n} id="icu:MessageRequests--message-group" />
|
||||||
i18n={i18n}
|
|
||||||
id="icu:MessageRequests--message-group"
|
|
||||||
components={{ name }}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="module-message-request-actions__buttons">
|
<div className="module-message-request-actions__buttons">
|
||||||
|
|
|
@ -136,9 +136,6 @@ export function MessageRequestActionsConfirmation({
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:MessageRequests--delete-direct-confirm-title"
|
id="icu:MessageRequests--delete-direct-confirm-title"
|
||||||
components={{
|
|
||||||
title: <ContactName key="name" title={title} />,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Intl
|
<Intl
|
||||||
|
|
|
@ -30,9 +30,7 @@ export function RemoveGroupMemberConfirmationDialog({
|
||||||
group.accessControlAddFromInviteLink
|
group.accessControlAddFromInviteLink
|
||||||
);
|
);
|
||||||
|
|
||||||
const intlComponents = {
|
const contactName = <ContactName title={conversation.title} />;
|
||||||
name: <ContactName title={conversation.title} />,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmationDialog
|
<ConfirmationDialog
|
||||||
|
@ -51,13 +49,13 @@ export function RemoveGroupMemberConfirmationDialog({
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:RemoveGroupMemberConfirmation__description__with-link"
|
id="icu:RemoveGroupMemberConfirmation__description__with-link"
|
||||||
components={intlComponents}
|
components={{ name: contactName }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Intl
|
<Intl
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:RemoveGroupMemberConfirmation__description"
|
id="icu:RemoveGroupMemberConfirmation__description"
|
||||||
components={intlComponents}
|
components={{ name: contactName }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,11 +57,7 @@ export function SafetyNumberNotification({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Intl
|
<Intl id="icu:safetyNumberChanged" i18n={i18n} />
|
||||||
id="icu:safetyNumberChanged"
|
|
||||||
components={{ name }}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
button={
|
button={
|
||||||
|
|
|
@ -50,28 +50,23 @@ function UnsupportedMessageContents({ canProcessNow, contact, i18n }: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return <Intl id="icu:Message--from-me-unsupported-message" i18n={i18n} />;
|
||||||
<Intl
|
|
||||||
id="icu:Message--from-me-unsupported-message"
|
|
||||||
components={{ contact: contactName }}
|
|
||||||
i18n={i18n}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (canProcessNow) {
|
if (canProcessNow) {
|
||||||
return (
|
return (
|
||||||
<Intl
|
<Intl
|
||||||
id="icu:Message--from-me-unsupported-message-ask-to-resend"
|
id="icu:Message--from-me-unsupported-message-ask-to-resend"
|
||||||
components={{ contact: contactName }}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Intl
|
<Intl
|
||||||
id="icu:Message--from-me-unsupported-message"
|
id="icu:Message--unsupported-message"
|
||||||
components={{ contact: contactName }}
|
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
components={{
|
||||||
|
contact: contactName,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -519,7 +519,7 @@ async function getGroupPreview(
|
||||||
window.Signal.Groups.decryptGroupTitle(result.title, secretParams) ||
|
window.Signal.Groups.decryptGroupTitle(result.title, secretParams) ||
|
||||||
window.i18n('icu:unknownGroup');
|
window.i18n('icu:unknownGroup');
|
||||||
const description = window.i18n('icu:GroupV2--join--group-metadata--full', {
|
const description = window.i18n('icu:GroupV2--join--group-metadata--full', {
|
||||||
count: result?.memberCount ?? 0,
|
memberCount: result?.memberCount ?? 0,
|
||||||
});
|
});
|
||||||
let image: undefined | LinkPreviewImage;
|
let image: undefined | LinkPreviewImage;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue