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 path = require('path');
|
||||
const { promisify } = require('util');
|
||||
const log = require('loglevel');
|
||||
const glob = require('fast-glob');
|
||||
const matchAll = require('string.prototype.matchall').getPolyfill();
|
||||
const localeIndex = require('../app/_locales/index.json');
|
||||
const {
|
||||
@ -34,7 +34,6 @@ const {
|
||||
getLocalePath,
|
||||
} = require('./lib/locales');
|
||||
|
||||
const readdir = promisify(fs.readdir);
|
||||
const readFile = promisify(fs.readFile);
|
||||
const writeFile = promisify(fs.writeFile);
|
||||
|
||||
@ -168,15 +167,18 @@ async function verifyLocale(code) {
|
||||
|
||||
async function verifyEnglishLocale() {
|
||||
const englishLocale = await getLocale('en');
|
||||
const uiJSFiles = await findJavascriptFiles(
|
||||
path.resolve(__dirname, '..', 'ui'),
|
||||
);
|
||||
const sharedJSFiles = await findJavascriptFiles(
|
||||
path.resolve(__dirname, '..', 'shared'),
|
||||
);
|
||||
|
||||
const javascriptFiles = sharedJSFiles.concat(uiJSFiles);
|
||||
// As time allows we'll switch to only performing the strict search.
|
||||
// In the meantime we'll use glob to specify which paths can be strict searched
|
||||
// and gradually phase out the key based search
|
||||
const globsToStrictSearch = [
|
||||
'ui/app/components/app/metamask-translation/*.js',
|
||||
];
|
||||
const javascriptFiles = await glob(['ui/**/*.js', 'shared/**/*.js'], {
|
||||
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
|
||||
// prevents this script from finding the messages, and then inappropriately
|
||||
// deletes them
|
||||
@ -190,10 +192,21 @@ async function verifyEnglishLocale() {
|
||||
for (const match of matchAll.call(fileContents, keyRegex)) {
|
||||
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);
|
||||
if (templateMatches) {
|
||||
// concat doesn't work here for some reason
|
||||
templateMatches.forEach((match) => templateUsage.push(match));
|
||||
}
|
||||
}
|
||||
@ -237,21 +250,6 @@ async function verifyEnglishLocale() {
|
||||
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) {
|
||||
for (const filename of filenames) {
|
||||
yield readFile(filename, 'utf8');
|
||||
|
@ -77,7 +77,7 @@ const MetaMaskTemplateRenderer = ({ sections }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const SectionShape = {
|
||||
export const SectionShape = {
|
||||
props: PropTypes.object,
|
||||
element: PropTypes.oneOf(Object.keys(safeComponentList)).isRequired,
|
||||
key: PropTypes.string,
|
||||
|
@ -5,8 +5,10 @@ import TruncatedDefinitionList from '../../ui/truncated-definition-list';
|
||||
import Popover from '../../ui/popover';
|
||||
import Typography from '../../ui/typography';
|
||||
import Box from '../../ui/box';
|
||||
import MetaMaskTranslation from '../metamask-translation';
|
||||
|
||||
export const safeComponentList = {
|
||||
MetaMaskTranslation,
|
||||
b: 'b',
|
||||
p: 'p',
|
||||
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…
Reference in New Issue
Block a user