Move i18n utils to its own module

This commit is contained in:
Germain Souquet 2021-04-26 16:55:12 +01:00
parent a43ad8d881
commit 4123406785
5 changed files with 97 additions and 388 deletions

View file

@ -23,9 +23,7 @@
"package.json"
],
"bin": {
"reskindex": "scripts/reskindex.js",
"matrix-gen-i18n": "scripts/gen-i18n.js",
"matrix-prune-i18n": "scripts/prune-i18n.js"
"reskindex": "scripts/reskindex.js"
},
"main": "./src/index.js",
"matrix_src_main": "./src/index.js",
@ -33,9 +31,9 @@
"matrix_lib_typings": "./lib/index.d.ts",
"scripts": {
"prepublishOnly": "yarn build",
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"i18n": "gen-i18n",
"prunei18n": "prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && gen-i18n && compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"reskindex": "node scripts/reskindex.js -h header",
"reskindex:watch": "node scripts/reskindex.js -h header -w",
"rethemendex": "res/css/rethemendex.sh",
@ -160,6 +158,7 @@
"jest-fetch-mock": "^3.0.3",
"matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2",
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"react-test-renderer": "^16.14.0",
"rimraf": "^3.0.2",

View file

@ -1,10 +0,0 @@
const fs = require("fs");
if (process.argv.length < 4) throw new Error("Missing source and target file arguments");
const sourceFile = fs.readFileSync(process.argv[2], 'utf8');
const targetFile = fs.readFileSync(process.argv[3], 'utf8');
if (sourceFile !== targetFile) {
throw new Error("Files do not match");
}

View file

@ -1,304 +0,0 @@
#!/usr/bin/env node
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Regenerates the translations en_EN file by walking the source tree and
* parsing each file with the appropriate parser. Emits a JSON file with the
* translatable strings mapped to themselves in the order they appeared
* in the files and grouped by the file they appeared in.
*
* Usage: node scripts/gen-i18n.js
*/
const fs = require('fs');
const path = require('path');
const walk = require('walk');
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const TRANSLATIONS_FUNCS = ['_t', '_td'];
const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
// NB. The sync version of walk is broken for single files so we walk
// all of res rather than just res/home.html.
// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it,
// or if we get bored waiting for it to be merged, we could switch
// to a project that's actively maintained.
const SEARCH_PATHS = ['src', 'res'];
function getObjectValue(obj, key) {
for (const prop of obj.properties) {
if (prop.key.type === 'Identifier' && prop.key.name === key) {
return prop.value;
}
}
return null;
}
function getTKey(arg) {
if (arg.type === 'Literal' || arg.type === "StringLiteral") {
return arg.value;
} else if (arg.type === 'BinaryExpression' && arg.operator === '+') {
return getTKey(arg.left) + getTKey(arg.right);
} else if (arg.type === 'TemplateLiteral') {
return arg.quasis.map((q) => {
return q.value.raw;
}).join('');
}
return null;
}
function getFormatStrings(str) {
// Match anything that starts with %
// We could make a regex that matched the full placeholder, but this
// would just not match invalid placeholders and so wouldn't help us
// detect the invalid ones.
// Also note that for simplicity, this just matches a % character and then
// anything up to the next % character (or a single %, or end of string).
const formatStringRe = /%([^%]+|%|$)/g;
const formatStrings = new Set();
let match;
while ( (match = formatStringRe.exec(str)) !== null ) {
const placeholder = match[1]; // Minus the leading '%'
if (placeholder === '%') continue; // Literal % is %%
const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/);
if (placeholderMatch === null) {
throw new Error("Invalid format specifier: '"+match[0]+"'");
}
if (placeholderMatch.length < 3) {
throw new Error("Malformed format specifier");
}
const placeholderName = placeholderMatch[1];
const placeholderFormat = placeholderMatch[2];
if (placeholderFormat !== 's') {
throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`);
}
formatStrings.add(placeholderName);
}
return formatStrings;
}
function getTranslationsJs(file) {
const contents = fs.readFileSync(file, { encoding: 'utf8' });
const trs = new Set();
try {
const plugins = [
// https://babeljs.io/docs/en/babel-parser#plugins
"classProperties",
"objectRestSpread",
"throwExpressions",
"exportDefaultFrom",
"decorators-legacy",
];
if (file.endsWith(".js") || file.endsWith(".jsx")) {
// all JS is assumed to be flow or react
plugins.push("flow", "jsx");
} else if (file.endsWith(".ts")) {
// TS can't use JSX unless it's a TSX file (otherwise angle casts fail)
plugins.push("typescript");
} else if (file.endsWith(".tsx")) {
// When the file is a TSX file though, enable JSX parsing
plugins.push("typescript", "jsx");
}
const babelParsed = parser.parse(contents, {
allowImportExportEverywhere: true,
errorRecovery: true,
sourceFilename: file,
tokens: true,
plugins,
});
traverse.default(babelParsed, {
enter: (p) => {
const node = p.node;
if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) {
const tKey = getTKey(node.arguments[0]);
// This happens whenever we call _t with non-literals (ie. whenever we've
// had to use a _td to compensate) so is expected.
if (tKey === null) return;
// check the format string against the args
// We only check _t: _td has no args
if (node.callee.name === '_t') {
try {
const placeholders = getFormatStrings(tKey);
for (const placeholder of placeholders) {
if (node.arguments.length < 2) {
throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`);
}
const value = getObjectValue(node.arguments[1], placeholder);
if (value === null) {
throw new Error(`No value found for placeholder '${placeholder}'`);
}
}
// Validate tag replacements
if (node.arguments.length > 2) {
const tagMap = node.arguments[2];
for (const prop of tagMap.properties || []) {
if (prop.key.type === 'Literal') {
const tag = prop.key.value;
// RegExp same as in src/languageHandler.js
const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`);
if (!tKey.match(regexp)) {
throw new Error(`No match for ${regexp} in ${tKey}`);
}
}
}
}
} catch (e) {
console.log();
console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
console.error(e);
process.exit(1);
}
}
let isPlural = false;
if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') {
const countVal = getObjectValue(node.arguments[1], 'count');
if (countVal) {
isPlural = true;
}
}
if (isPlural) {
trs.add(tKey + "|other");
const plurals = enPlurals[tKey];
if (plurals) {
for (const pluralType of Object.keys(plurals)) {
trs.add(tKey + "|" + pluralType);
}
}
} else {
trs.add(tKey);
}
}
},
});
} catch (e) {
console.error(e);
process.exit(1);
}
return trs;
}
function getTranslationsOther(file) {
const contents = fs.readFileSync(file, { encoding: 'utf8' });
const trs = new Set();
// Taken from element-web src/components/structures/HomePage.js
const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg;
let matches;
while (matches = translationsRegex.exec(contents)) {
trs.add(matches[1]);
}
return trs;
}
// gather en_EN plural strings from the input translations file:
// the en_EN strings are all in the source with the exception of
// pluralised strings, which we need to pull in from elsewhere.
const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' }));
const enPlurals = {};
for (const key of Object.keys(inputTranslationsRaw)) {
const parts = key.split("|");
if (parts.length > 1) {
const plurals = enPlurals[parts[0]] || {};
plurals[parts[1]] = inputTranslationsRaw[key];
enPlurals[parts[0]] = plurals;
}
}
const translatables = new Set();
const walkOpts = {
listeners: {
names: function(root, nodeNamesArray) {
// Sort the names case insensitively and alphabetically to
// maintain some sense of order between the different strings.
nodeNamesArray.sort((a, b) => {
a = a.toLowerCase();
b = b.toLowerCase();
if (a > b) return 1;
if (a < b) return -1;
return 0;
});
},
file: function(root, fileStats, next) {
const fullPath = path.join(root, fileStats.name);
let trs;
if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) {
trs = getTranslationsJs(fullPath);
} else if (fileStats.name.endsWith('.html')) {
trs = getTranslationsOther(fullPath);
} else {
return;
}
console.log(`${fullPath} (${trs.size} strings)`);
for (const tr of trs.values()) {
// Convert DOS line endings to unix
translatables.add(tr.replace(/\r\n/g, "\n"));
}
},
}
};
for (const path of SEARCH_PATHS) {
if (fs.existsSync(path)) {
walk.walkSync(path, walkOpts);
}
}
const trObj = {};
for (const tr of translatables) {
if (tr.includes("|")) {
if (inputTranslationsRaw[tr]) {
trObj[tr] = inputTranslationsRaw[tr];
} else {
trObj[tr] = tr.split("|")[0];
}
} else {
trObj[tr] = tr;
}
}
fs.writeFileSync(
OUTPUT_FILE,
JSON.stringify(trObj, translatables.values(), 4) + "\n"
);
console.log();
console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`);

View file

@ -1,68 +0,0 @@
#!/usr/bin/env node
/*
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* Looks through all the translation files and removes any strings
* which don't appear in en_EN.json.
* Use this if you remove a translation, but merge any outstanding changes
* from weblate first or you'll need to resolve the conflict in weblate.
*/
const fs = require('fs');
const path = require('path');
const I18NDIR = 'src/i18n/strings';
const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json')));
const enStrings = new Set();
for (const str of Object.keys(enStringsRaw)) {
const parts = str.split('|');
if (parts.length > 1) {
enStrings.add(parts[0]);
} else {
enStrings.add(str);
}
}
for (const filename of fs.readdirSync(I18NDIR)) {
if (filename === 'en_EN.json') continue;
if (filename === 'basefile.json') continue;
if (!filename.endsWith('.json')) continue;
const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename)));
const oldLen = Object.keys(trs).length;
for (const tr of Object.keys(trs)) {
const parts = tr.split('|');
const trKey = parts.length > 1 ? parts[0] : tr;
if (!enStrings.has(trKey)) {
delete trs[tr];
}
}
const removed = oldLen - Object.keys(trs).length;
if (removed > 0) {
console.log(`${filename}: removed ${removed} translations`);
// XXX: This is totally relying on the impl serialising the JSON object in the
// same order as they were parsed from the file. JSON.stringify() has a specific argument
// that can be used to control the order, but JSON.parse() lacks any kind of equivalent.
// Empirically this does maintain the order on my system, so I'm going to leave it like
// this for now.
fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n");
}
}

View file

@ -26,6 +26,13 @@
dependencies:
"@babel/highlight" "^7.10.4"
"@babel/code-frame@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==
dependencies:
"@babel/highlight" "^7.12.13"
"@babel/compat-data@^7.12.5", "@babel/compat-data@^7.12.7":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.12.7.tgz#9329b4782a7d6bbd7eef57e11addf91ee3ef1e41"
@ -61,6 +68,15 @@
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/generator@^7.13.16":
version "7.13.16"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.16.tgz#0befc287031a201d84cdfc173b46b320ae472d14"
integrity sha512-grBBR75UnKOcUWMp8WoDxNsWCFl//XCK6HWTrBQKTr5SV9f5g0pNOjdyzi/DTBv12S9GnYPInIXQBTky7OXEMg==
dependencies:
"@babel/types" "^7.13.16"
jsesc "^2.5.1"
source-map "^0.5.0"
"@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d"
@ -130,6 +146,15 @@
"@babel/template" "^7.12.7"
"@babel/types" "^7.12.11"
"@babel/helper-function-name@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a"
integrity sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==
dependencies:
"@babel/helper-get-function-arity" "^7.12.13"
"@babel/template" "^7.12.13"
"@babel/types" "^7.12.13"
"@babel/helper-get-function-arity@^7.12.10":
version "7.12.10"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
@ -137,6 +162,13 @@
dependencies:
"@babel/types" "^7.12.10"
"@babel/helper-get-function-arity@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583"
integrity sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==
dependencies:
"@babel/types" "^7.12.13"
"@babel/helper-hoist-variables@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz#d49b001d1d5a68ca5e6604dda01a6297f7c9381e"
@ -225,6 +257,13 @@
dependencies:
"@babel/types" "^7.12.11"
"@babel/helper-split-export-declaration@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05"
integrity sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==
dependencies:
"@babel/types" "^7.12.13"
"@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed"
@ -263,11 +302,25 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/highlight@^7.12.13":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
dependencies:
"@babel/helper-validator-identifier" "^7.12.11"
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.12.10", "@babel/parser@^7.12.11", "@babel/parser@^7.12.7", "@babel/parser@^7.7.0":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
"@babel/parser@^7.12.13", "@babel/parser@^7.13.16":
version "7.13.16"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.16.tgz#0f18179b0448e6939b1f3f5c4c355a3a9bcdfd37"
integrity sha512-6bAg36mCwuqLO0hbR+z7PHuqWiCeP7Dzg73OpQwsAB1Eb8HnGEz5xYBzCfbu+YjoaJsJs+qheDxVAuqbt3ILEw==
"@babel/plugin-proposal-async-generator-functions@^7.12.1":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.12.tgz#04b8f24fd4532008ab4e79f788468fd5a8476566"
@ -980,6 +1033,15 @@
"@babel/parser" "^7.12.7"
"@babel/types" "^7.12.7"
"@babel/template@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
integrity sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==
dependencies:
"@babel/code-frame" "^7.12.13"
"@babel/parser" "^7.12.13"
"@babel/types" "^7.12.13"
"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.4", "@babel/traverse@^7.12.1", "@babel/traverse@^7.12.10", "@babel/traverse@^7.12.12", "@babel/traverse@^7.12.5", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.4":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
@ -995,6 +1057,20 @@
globals "^11.1.0"
lodash "^4.17.19"
"@babel/traverse@^7.13.17":
version "7.13.17"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.17.tgz#c85415e0c7d50ac053d758baec98b28b2ecfeea3"
integrity sha512-BMnZn0R+X6ayqm3C3To7o1j7Q020gWdqdyP50KEoVqaCO2c/Im7sYZSmVgvefp8TTMQ+9CtwuBp0Z1CZ8V3Pvg==
dependencies:
"@babel/code-frame" "^7.12.13"
"@babel/generator" "^7.13.16"
"@babel/helper-function-name" "^7.12.13"
"@babel/helper-split-export-declaration" "^7.12.13"
"@babel/parser" "^7.13.16"
"@babel/types" "^7.13.17"
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.10.5", "@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
@ -1004,6 +1080,14 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
"@babel/types@^7.12.13", "@babel/types@^7.13.16", "@babel/types@^7.13.17":
version "7.13.17"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.17.tgz#48010a115c9fba7588b4437dd68c9469012b38b4"
integrity sha512-RawydLgxbOPDlTLJNtoIypwdmAy//uQIzlKt2+iBiJaRlVuI6QLUxVAyWGNfOzp8Yu4L4lLIacoCyTNtpb4wiA==
dependencies:
"@babel/helper-validator-identifier" "^7.12.11"
to-fast-properties "^2.0.0"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@ -5614,6 +5698,14 @@ matrix-react-test-utils@^0.2.2:
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
"matrix-web-i18n@github:matrix-org/matrix-web-i18n":
version "1.1.1"
resolved "https://codeload.github.com/matrix-org/matrix-web-i18n/tar.gz/68ea0c57b6c74c40df6419eb5ac0fa8945ff8a75"
dependencies:
"@babel/parser" "^7.13.16"
"@babel/traverse" "^7.13.17"
walk "^2.3.14"
matrix-widget-api@^0.1.0-beta.13:
version "0.1.0-beta.13"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.13.tgz#ebddc83eaef39bbb87b621a02a35902e1a29b9ef"