mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-26 06:47:17 +10:00
Merge pull request #955 from sharetribe/translations-cli
Translations management CLI
This commit is contained in:
commit
e2aca7ac5a
6 changed files with 379 additions and 3 deletions
|
|
@ -19,13 +19,13 @@ way to update this template, but currently, we follow a pattern:
|
|||
[#960](https://github.com/sharetribe/flex-template-web/pull/960)
|
||||
* [fix] Remove unused translation keys and update PasswordChangePage title
|
||||
[#959](https://github.com/sharetribe/flex-template-web/pull/959)
|
||||
* [fix] Add translations CLI tool [#955](https://github.com/sharetribe/flex-template-web/pull/955)
|
||||
|
||||
## [v2.3.2] 2018-11-20
|
||||
|
||||
* [fix] Take 2: don't set currentUserHasListings if fetched listing is in draft state.
|
||||
[#956](https://github.com/sharetribe/flex-template-web/pull/956)
|
||||
* [fix] PriceFilter styles
|
||||
[#954](https://github.com/sharetribe/flex-template-web/pull/954)
|
||||
* [fix] PriceFilter styles [#954](https://github.com/sharetribe/flex-template-web/pull/954)
|
||||
|
||||
[v2.3.2]: https://github.com/sharetribe/flex-template-web/compare/v2.3.1...v2.3.2
|
||||
|
||||
|
|
@ -322,4 +322,3 @@ way to update this template, but currently, we follow a pattern:
|
|||
## v0.2.0
|
||||
|
||||
* Starting a change log for Flex Template for Web.
|
||||
|
||||
|
|
|
|||
BIN
docs/assets/translations/translations_cli.gif
Normal file
BIN
docs/assets/translations/translations_cli.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
|
|
@ -161,3 +161,26 @@ in tests. To change the translation file used in tests change the `messages` var
|
|||
```js
|
||||
import messages from '../translations/es.json';
|
||||
```
|
||||
|
||||
## Managing translations
|
||||
|
||||
In case you have added a new language translation file and are pulling translation updates to
|
||||
`en.json` from the upstream repo there is a command line tool to help keeping the translation files
|
||||
in sync. Running the following command in the project root
|
||||
|
||||
```
|
||||
node translations.js
|
||||
```
|
||||
|
||||
will start a command line application:
|
||||
|
||||

|
||||
|
||||
The command line application can be used to match a translation file against the English
|
||||
translations. If your new translations file follows the `<LANG CODE>.json` naming, the CLI will pick
|
||||
it up automatically. In order to improve readability, you can add the language name to the
|
||||
`TARGET_LANG_NAMES` map in `translations.js` if it is not yet in there and the CLI will use the
|
||||
correct name for your language instead of the language code when prompting about translations.
|
||||
|
||||
In case you wish to use something else than English as the source language, modify the `SOURCE_LANG`
|
||||
object in `translations.js` to match your needs.
|
||||
|
|
|
|||
|
|
@ -56,9 +56,11 @@
|
|||
"url": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chalk": "^2.4.1",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-16": "^1.1.1",
|
||||
"enzyme-to-json": "^3.3.3",
|
||||
"inquirer": "^6.2.0",
|
||||
"nodemon": "^1.17.2",
|
||||
"nsp": "^3.2.1",
|
||||
"nsp-preprocessor-yarn": "^1.0.1",
|
||||
|
|
|
|||
300
translations.js
Normal file
300
translations.js
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
/**
|
||||
* A translations too that can be used to keep a translation file(s)
|
||||
* up to date with one source language file.
|
||||
*
|
||||
* Possible target languages are resolved from the translation files
|
||||
* available. When adding a new tranlation file, the language name can
|
||||
* be added to the TARGET_LANG_NAMES map for clearer prompt messages.
|
||||
* By default the translations are matched agains English translations
|
||||
* but that can be changed by modifying the SOURCE_LANG.
|
||||
*/
|
||||
|
||||
const inquirer = require('inquirer');
|
||||
const difference = require('lodash/difference');
|
||||
const fs = require('fs');
|
||||
const chalk = require('chalk');
|
||||
|
||||
const PATH = './src/translations/';
|
||||
|
||||
const SOURCE_LANG = { name: 'English', code: 'en' };
|
||||
const TARGET_LANG_NAMES = {
|
||||
es: 'Spanish',
|
||||
de: 'German',
|
||||
fr: 'French',
|
||||
};
|
||||
|
||||
class BreakToRun {}
|
||||
class BreakToRunWithTarget {}
|
||||
|
||||
/**
|
||||
* Resolves translation file path for a lang code: en, es, de, etc.
|
||||
*/
|
||||
const filePath = lang => `${PATH}${lang}.json`;
|
||||
|
||||
/**
|
||||
* Resolves name of a target language based on
|
||||
* a language code: en, es, de, etc.
|
||||
*
|
||||
* If a lang name is not found, language code is returned.
|
||||
*/
|
||||
const targetLangName = code => {
|
||||
const name = TARGET_LANG_NAMES[code];
|
||||
|
||||
return name || code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a list of target language choices by finding all possible
|
||||
* translation files and filtering out the SOURCE_LANG. Provides a list
|
||||
* of objects that can be passed to inquirer.
|
||||
*
|
||||
* Relies on translation file naming: <lang code>.json
|
||||
*/
|
||||
const targetLangChoices = () => {
|
||||
const folder = './src/translations/';
|
||||
const filenames = fs.readdirSync(folder);
|
||||
|
||||
const choices = filenames
|
||||
.filter(name => name.endsWith('.json'))
|
||||
.map(name => name.split('.')[0])
|
||||
.filter(code => code !== SOURCE_LANG.code)
|
||||
.map(code => {
|
||||
return {
|
||||
name: targetLangName(code),
|
||||
value: code,
|
||||
};
|
||||
});
|
||||
|
||||
return choices;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts an object by key.
|
||||
*
|
||||
* Relies on an object keeping the order in which entries are added for string keys.
|
||||
* See: http://exploringjs.com/es6/ch_oop-besides-classes.html#_traversal-order-of-properties
|
||||
*/
|
||||
const sort = obj => {
|
||||
const sorted = {};
|
||||
Object.keys(obj)
|
||||
.sort()
|
||||
.forEach(key => (sorted[key] = obj[key]));
|
||||
|
||||
return sorted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a file with JSON content and returns
|
||||
* a JS object.
|
||||
*
|
||||
* @param filepath
|
||||
*/
|
||||
const readFileToJSON = filepath => {
|
||||
const rawdata = fs.readFileSync(filepath);
|
||||
return JSON.parse(rawdata);
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the application.
|
||||
*
|
||||
*/
|
||||
const run = () => {
|
||||
const choices = targetLangChoices();
|
||||
|
||||
selectLanguage(choices)
|
||||
.then(answers => {
|
||||
runWithTarget(answers.lang);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(chalk.red(`An error occurred due to: ${err.message}`));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* When a language is selected this will prompt
|
||||
* the user about adding translations to that language.
|
||||
*
|
||||
* @param {String} targetLang Target language code
|
||||
*/
|
||||
const runWithTarget = targetLang => {
|
||||
let key;
|
||||
let translation;
|
||||
|
||||
const sourceLang = SOURCE_LANG.code;
|
||||
const source = readFileToJSON(filePath(sourceLang));
|
||||
const target = readFileToJSON(filePath(targetLang));
|
||||
const sourceKeys = Object.keys(source);
|
||||
const targetKeys = Object.keys(target);
|
||||
const diff = difference(sourceKeys, targetKeys);
|
||||
|
||||
if (diff.length === 0) {
|
||||
console.log(`No translations missing from the ${targetLangName(targetLang)} translations`);
|
||||
run();
|
||||
} else {
|
||||
translateLanguage(targetLang, source, target, diff);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Does the actual prompting for translations.
|
||||
*
|
||||
* @param {String} targetLang Target language code
|
||||
* @param {Object} source Source translations
|
||||
* @param {Object} target Target translations
|
||||
* @param {Array} diff Missing keys in target
|
||||
*/
|
||||
const translateLanguage = (targetLang, source, target, diff) => {
|
||||
selectKey(targetLang, diff, source, target)
|
||||
.then(answers => {
|
||||
key = answers.key;
|
||||
if (key === null) {
|
||||
throw new BreakToRun();
|
||||
}
|
||||
if (key != null) {
|
||||
return addTranslation(targetLang, key, source);
|
||||
}
|
||||
})
|
||||
.then(answers => {
|
||||
translation = answers.value;
|
||||
return confirmTranslation(targetLang, key, translation);
|
||||
})
|
||||
.then(answers => {
|
||||
// y|Y|n|N
|
||||
const confirmation = answers.value;
|
||||
|
||||
if (/n/i.test(confirmation)) {
|
||||
console.log('Discarding new translation');
|
||||
throw new BreakToRunWithTarget();
|
||||
}
|
||||
|
||||
target[key] = translation;
|
||||
|
||||
const sorted = sort(target);
|
||||
const data = JSON.stringify(sorted, null, 2);
|
||||
|
||||
return updateTranslationsFile(data, targetLang);
|
||||
})
|
||||
.then(() => {
|
||||
runWithTarget(targetLang);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err instanceof BreakToRun) {
|
||||
// break out of Promise chain, start over
|
||||
run();
|
||||
} else if (err instanceof BreakToRunWithTarget) {
|
||||
// break out of Promise chain, continue with this language
|
||||
runWithTarget(targetLang);
|
||||
} else {
|
||||
console.log(chalk.red(`An error occurred due to: ${err.message}`));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt for selecting the language of which
|
||||
* translations are inspected.
|
||||
*
|
||||
* @return a Promise
|
||||
*/
|
||||
const selectLanguage = choices => {
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'lang',
|
||||
message: 'Which language would you like to inspect? (Or hit Ctrl+C to quit)',
|
||||
choices: choices,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt for selecting a key that is missing from the target language translations.
|
||||
*
|
||||
* @param {String} targetLang The language that is being translated
|
||||
* @param {Array} diff Keys missing frrom target language translations
|
||||
*
|
||||
* @return a Promise
|
||||
*/
|
||||
const selectKey = (targetLang, diff, source, target) => {
|
||||
const choices = [{ name: 'Back to languages', value: null }, ...diff];
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'key',
|
||||
message: `The following translation keys (${
|
||||
diff.length
|
||||
}) are missing from the ${targetLangName(
|
||||
targetLang
|
||||
)} translations. Select a key to add a translation.`,
|
||||
choices: choices,
|
||||
pageSize: 30,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt for a new translation in target language.
|
||||
*
|
||||
* @param {String} targetLang The language that is being translated
|
||||
* @param {String} key Key for the new translation
|
||||
* @param {Object} source Source language translations
|
||||
*
|
||||
* @return a Promise
|
||||
*/
|
||||
const addTranslation = (targetLang, key, source) => {
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'value',
|
||||
message: `Please provide a translation in ${targetLangName(
|
||||
targetLang
|
||||
)} for the key ${chalk.blueBright(key)}. The current ${
|
||||
SOURCE_LANG.name
|
||||
} translation is ${chalk.green(source[key])} \n`,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prompt for a confirming a new translation with 'y' or 'N'.
|
||||
*
|
||||
* @param {String} targetLang The language that is being translated
|
||||
* @param {String} key Key for the new translation
|
||||
* @param {Object} source Source language translations
|
||||
*
|
||||
* @return a Promise with with answers.value being 'y' or 'N'
|
||||
*/
|
||||
const confirmTranslation = (targetLang, key, translation) => {
|
||||
return inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'value',
|
||||
message: `Do you want to add the ${targetLangName(targetLang)} translation ${chalk.green(
|
||||
translation
|
||||
)} for the key ${chalk.blueBright(key)}? (y/n)`,
|
||||
validate: value => /y/i.test(value) || /n/i.test(value),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update translations file.
|
||||
*
|
||||
* @param {String} data String data to be written to file
|
||||
* @param {String} targetLang Language code of the translation file
|
||||
*
|
||||
* @return a Promise that resolves when file is written
|
||||
*/
|
||||
const updateTranslationsFile = (data, targetLang) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(filePath(targetLang), data, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
console.log(chalk.bold('The translation file is updated'));
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
run();
|
||||
52
yarn.lock
52
yarn.lock
|
|
@ -1512,6 +1512,11 @@ change-emitter@^0.1.2:
|
|||
version "0.1.6"
|
||||
resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
|
||||
|
||||
chardet@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
|
|
@ -3005,6 +3010,15 @@ external-editor@^2.0.4:
|
|||
jschardet "^1.4.2"
|
||||
tmp "^0.0.33"
|
||||
|
||||
external-editor@^3.0.0:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27"
|
||||
integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==
|
||||
dependencies:
|
||||
chardet "^0.7.0"
|
||||
iconv-lite "^0.4.24"
|
||||
tmp "^0.0.33"
|
||||
|
||||
extglob@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
|
||||
|
|
@ -3819,6 +3833,13 @@ iconv-lite@0.4.23, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
|||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@^0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
icss-replace-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
||||
|
|
@ -3925,6 +3946,25 @@ inquirer@3.3.0, inquirer@^3.0.6, inquirer@^3.3.0:
|
|||
strip-ansi "^4.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8"
|
||||
integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg==
|
||||
dependencies:
|
||||
ansi-escapes "^3.0.0"
|
||||
chalk "^2.0.0"
|
||||
cli-cursor "^2.1.0"
|
||||
cli-width "^2.0.0"
|
||||
external-editor "^3.0.0"
|
||||
figures "^2.0.0"
|
||||
lodash "^4.17.10"
|
||||
mute-stream "0.0.7"
|
||||
run-async "^2.2.0"
|
||||
rxjs "^6.1.0"
|
||||
string-width "^2.1.0"
|
||||
strip-ansi "^4.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
internal-ip@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
|
||||
|
|
@ -7312,6 +7352,13 @@ rx-lite@*, rx-lite@^4.0.8:
|
|||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
|
||||
|
||||
rxjs@^6.1.0:
|
||||
version "6.3.3"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55"
|
||||
integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==
|
||||
dependencies:
|
||||
tslib "^1.9.0"
|
||||
|
||||
safe-buffer@5.1.1, safe-buffer@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
|
@ -8106,6 +8153,11 @@ trim-right@^1.0.1:
|
|||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
|
||||
|
||||
tslib@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
|
||||
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
|
||||
|
||||
tty-browserify@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue