mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
add MetaMaskTranslation component (#10405)
* add MetaMaskTranslation component * add stories * Update ui/app/components/app/metamask-translation/metamask-translation.js Co-authored-by: Mark Stacey <markjstacey@gmail.com> * Update ui/app/components/app/metamask-translation/metamask-translation.js Co-authored-by: Mark Stacey <markjstacey@gmail.com> * fix regex Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
e48053a6d5
commit
8e2d4e6fdd
@ -22,9 +22,9 @@
|
|||||||
// //////////////////////////////////////////////////////////////////////////////
|
// //////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
|
||||||
const { promisify } = require('util');
|
const { promisify } = require('util');
|
||||||
const log = require('loglevel');
|
const log = require('loglevel');
|
||||||
|
const glob = require('fast-glob');
|
||||||
const matchAll = require('string.prototype.matchall').getPolyfill();
|
const matchAll = require('string.prototype.matchall').getPolyfill();
|
||||||
const localeIndex = require('../app/_locales/index.json');
|
const localeIndex = require('../app/_locales/index.json');
|
||||||
const {
|
const {
|
||||||
@ -34,7 +34,6 @@ const {
|
|||||||
getLocalePath,
|
getLocalePath,
|
||||||
} = require('./lib/locales');
|
} = require('./lib/locales');
|
||||||
|
|
||||||
const readdir = promisify(fs.readdir);
|
|
||||||
const readFile = promisify(fs.readFile);
|
const readFile = promisify(fs.readFile);
|
||||||
const writeFile = promisify(fs.writeFile);
|
const writeFile = promisify(fs.writeFile);
|
||||||
|
|
||||||
@ -168,15 +167,18 @@ async function verifyLocale(code) {
|
|||||||
|
|
||||||
async function verifyEnglishLocale() {
|
async function verifyEnglishLocale() {
|
||||||
const englishLocale = await getLocale('en');
|
const englishLocale = await getLocale('en');
|
||||||
const uiJSFiles = await findJavascriptFiles(
|
// As time allows we'll switch to only performing the strict search.
|
||||||
path.resolve(__dirname, '..', 'ui'),
|
// In the meantime we'll use glob to specify which paths can be strict searched
|
||||||
);
|
// and gradually phase out the key based search
|
||||||
const sharedJSFiles = await findJavascriptFiles(
|
const globsToStrictSearch = [
|
||||||
path.resolve(__dirname, '..', 'shared'),
|
'ui/app/components/app/metamask-translation/*.js',
|
||||||
);
|
];
|
||||||
|
const javascriptFiles = await glob(['ui/**/*.js', 'shared/**/*.js'], {
|
||||||
const javascriptFiles = sharedJSFiles.concat(uiJSFiles);
|
ignore: globsToStrictSearch,
|
||||||
|
});
|
||||||
|
const javascriptFilesToStrictSearch = await glob(globsToStrictSearch);
|
||||||
|
|
||||||
|
const strictSearchRegex = /\bt\(\s*'(\w+)'\s*\)|\btranslationKey:\s*'(\w+)'/gu;
|
||||||
// match "t(`...`)" because constructing message keys from template strings
|
// match "t(`...`)" because constructing message keys from template strings
|
||||||
// prevents this script from finding the messages, and then inappropriately
|
// prevents this script from finding the messages, and then inappropriately
|
||||||
// deletes them
|
// deletes them
|
||||||
@ -190,10 +192,21 @@ async function verifyEnglishLocale() {
|
|||||||
for (const match of matchAll.call(fileContents, keyRegex)) {
|
for (const match of matchAll.call(fileContents, keyRegex)) {
|
||||||
usedMessages.add(match[1] || match[2]);
|
usedMessages.add(match[1] || match[2]);
|
||||||
}
|
}
|
||||||
|
const templateMatches = fileContents.match(templateStringRegex);
|
||||||
|
if (templateMatches) {
|
||||||
|
templateMatches.forEach((match) => templateUsage.push(match));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const fileContents of getFileContents(
|
||||||
|
javascriptFilesToStrictSearch,
|
||||||
|
)) {
|
||||||
|
for (const match of matchAll.call(fileContents, strictSearchRegex)) {
|
||||||
|
usedMessages.add(match[1] || match[2] || match[3] || match[4]);
|
||||||
|
}
|
||||||
|
|
||||||
const templateMatches = fileContents.match(templateStringRegex);
|
const templateMatches = fileContents.match(templateStringRegex);
|
||||||
if (templateMatches) {
|
if (templateMatches) {
|
||||||
// concat doesn't work here for some reason
|
|
||||||
templateMatches.forEach((match) => templateUsage.push(match));
|
templateMatches.forEach((match) => templateUsage.push(match));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,21 +250,6 @@ async function verifyEnglishLocale() {
|
|||||||
return true; // failed === true
|
return true; // failed === true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findJavascriptFiles(rootDir) {
|
|
||||||
const javascriptFiles = [];
|
|
||||||
const contents = await readdir(rootDir, { withFileTypes: true });
|
|
||||||
for (const file of contents) {
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
javascriptFiles.push(
|
|
||||||
...(await findJavascriptFiles(path.join(rootDir, file.name))),
|
|
||||||
);
|
|
||||||
} else if (file.isFile() && file.name.endsWith('.js')) {
|
|
||||||
javascriptFiles.push(path.join(rootDir, file.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return javascriptFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function* getFileContents(filenames) {
|
async function* getFileContents(filenames) {
|
||||||
for (const filename of filenames) {
|
for (const filename of filenames) {
|
||||||
yield readFile(filename, 'utf8');
|
yield readFile(filename, 'utf8');
|
||||||
|
@ -77,7 +77,7 @@ const MetaMaskTemplateRenderer = ({ sections }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SectionShape = {
|
export const SectionShape = {
|
||||||
props: PropTypes.object,
|
props: PropTypes.object,
|
||||||
element: PropTypes.oneOf(Object.keys(safeComponentList)).isRequired,
|
element: PropTypes.oneOf(Object.keys(safeComponentList)).isRequired,
|
||||||
key: PropTypes.string,
|
key: PropTypes.string,
|
||||||
|
@ -5,8 +5,10 @@ import TruncatedDefinitionList from '../../ui/truncated-definition-list';
|
|||||||
import Popover from '../../ui/popover';
|
import Popover from '../../ui/popover';
|
||||||
import Typography from '../../ui/typography';
|
import Typography from '../../ui/typography';
|
||||||
import Box from '../../ui/box';
|
import Box from '../../ui/box';
|
||||||
|
import MetaMaskTranslation from '../metamask-translation';
|
||||||
|
|
||||||
export const safeComponentList = {
|
export const safeComponentList = {
|
||||||
|
MetaMaskTranslation,
|
||||||
b: 'b',
|
b: 'b',
|
||||||
p: 'p',
|
p: 'p',
|
||||||
div: 'div',
|
div: 'div',
|
||||||
|
1
ui/app/components/app/metamask-translation/index.js
Normal file
1
ui/app/components/app/metamask-translation/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './metamask-translation';
|
@ -0,0 +1,80 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import MetaMaskTemplateRenderer, {
|
||||||
|
SectionShape,
|
||||||
|
} from '../metamask-template-renderer/metamask-template-renderer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetaMaskTranslation is a simple helper Component for adding full translation
|
||||||
|
* support to the template system. We do pass the translation function to the
|
||||||
|
* template getValues function, but passing it React components as variables
|
||||||
|
* would require React to be in scope, and breaks the object pattern paradigm.
|
||||||
|
*
|
||||||
|
* This component gets around that by converting variables that are templates
|
||||||
|
* themselves into tiny React trees. This component does additional validation
|
||||||
|
* to make sure that the tree has a single root node, with maximum two leaves.
|
||||||
|
* Each subnode can have a maximum of one child that must be a string.
|
||||||
|
*
|
||||||
|
* This enforces a maximum recursion depth of 2, preventing translation strings
|
||||||
|
* from being performance hogs. We could further limit this, and also attenuate
|
||||||
|
* the safeComponentList for what kind of components we allow these special
|
||||||
|
* trees to contain.
|
||||||
|
*/
|
||||||
|
export default function MetaMaskTranslation({ translationKey, variables }) {
|
||||||
|
const t = useI18nContext();
|
||||||
|
|
||||||
|
return t(
|
||||||
|
translationKey,
|
||||||
|
variables?.map((variable) => {
|
||||||
|
if (
|
||||||
|
typeof variable === 'object' &&
|
||||||
|
!Array.isArray(variable) &&
|
||||||
|
variable.element
|
||||||
|
) {
|
||||||
|
if (!variable.key) {
|
||||||
|
throw new Error(
|
||||||
|
`When using MetaMask Template Language in a MetaMaskTranslation variable, you must provide a key for the section regardless of syntax.
|
||||||
|
Section with element '${variable.element}' for translationKey: '${translationKey}' has no key property`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
variable.children &&
|
||||||
|
Array.isArray(variable.children) &&
|
||||||
|
variable.children.length > 2
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'MetaMaskTranslation only renders templates with a single section and maximum two children',
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
(variable.children?.[0]?.children !== undefined &&
|
||||||
|
typeof variable.children[0].children !== 'string') ||
|
||||||
|
(variable.children?.[1]?.children !== undefined &&
|
||||||
|
typeof variable.children[1].children !== 'string')
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
'MetaMaskTranslation does not allow for component trees of non trivial depth',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<MetaMaskTemplateRenderer
|
||||||
|
key={`${translationKey}-${variable.key}`}
|
||||||
|
sections={variable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return variable;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MetaMaskTranslation.propTypes = {
|
||||||
|
translationKey: PropTypes.string.isRequired,
|
||||||
|
variables: PropTypes.arrayOf(
|
||||||
|
PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.shape(SectionShape),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
};
|
@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { select, object } from '@storybook/addon-knobs';
|
||||||
|
import { groupBy } from 'lodash';
|
||||||
|
import en from '../../../../../app/_locales/en/messages.json';
|
||||||
|
import MetaMaskTranslation from './metamask-translation';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'MetaMaskTranslation',
|
||||||
|
};
|
||||||
|
|
||||||
|
const { keysWithSubstitution, keysWithoutSubstitution } = groupBy(
|
||||||
|
Object.keys(en),
|
||||||
|
(key) => {
|
||||||
|
if (en[key].message.includes('$1')) {
|
||||||
|
return 'keysWithSubstitution';
|
||||||
|
}
|
||||||
|
return 'keysWithoutSubstitution';
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const withoutSubstitutions = () => (
|
||||||
|
<MetaMaskTranslation
|
||||||
|
translationKey={select(
|
||||||
|
'translationKey',
|
||||||
|
keysWithoutSubstitution,
|
||||||
|
keysWithoutSubstitution[0],
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const withSubstitutions = () => (
|
||||||
|
<MetaMaskTranslation
|
||||||
|
translationKey={select(
|
||||||
|
'translationKey',
|
||||||
|
keysWithSubstitution,
|
||||||
|
keysWithSubstitution[0],
|
||||||
|
)}
|
||||||
|
variables={object('variables', [])}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const withTemplate = () => (
|
||||||
|
<MetaMaskTranslation
|
||||||
|
translationKey={select(
|
||||||
|
'translationKey',
|
||||||
|
keysWithSubstitution,
|
||||||
|
keysWithSubstitution[0],
|
||||||
|
)}
|
||||||
|
variables={[
|
||||||
|
{
|
||||||
|
element: 'span',
|
||||||
|
key: 'link',
|
||||||
|
children: {
|
||||||
|
element: 'MetaMaskTranslation',
|
||||||
|
props: {
|
||||||
|
translationKey: select(
|
||||||
|
'innerTranslationKey',
|
||||||
|
keysWithoutSubstitution,
|
||||||
|
keysWithoutSubstitution[0],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
Loading…
x
Reference in New Issue
Block a user