Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
["react-intl", {
"messagesDir": "./tmp/messages/",
"enforceDescriptions": false
["formatjs", {
"ast": true
}],
"transform-object-assign",
"transform-flow-strip-types",
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ prod/
package-lock.json

/run_server.bat
/run_export_strings.bat
/run_manage_translations.bat
/etc/env.json
/bin/server.js.LICENSE.txt
/yarn-error.log
213 changes: 203 additions & 10 deletions bin/manageTranslations.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,208 @@
import manageTranslations from 'react-intl-translations-manager';
/* eslint no-console: "off" */
import fs from 'fs';
import { extract } from '@formatjs/cli';
import glob from 'glob';
import 'colors';

// global config parameters
const searchPattern = './src/**/*.js';
const refLanguage = 'en'; // the language stored in default messages
const otherLanguages = ['cs']; // translations
const translationsDirectory = './src/locales/';

manageTranslations({
messagesDirectory: './tmp/messages',
translationsDirectory,
languages: ['en', 'cs'],
detectDuplicateIds: false,
});
/**
* Use formatjs to extract messages from all source files.
* @returns {object} where keys are IDs and values are messages
*/
async function extractMessages() {
// extract messages from all src files
const files = glob.sync(searchPattern);
console.log(`Extracting messages from ${files.length} files...`);
const extractedAsString = await extract(files, {});
const extractedJson = JSON.parse(extractedAsString);
console.log(`Total ${Object.keys(extractedJson).length} messages extracted.`);

const enData = JSON.parse(fs.readFileSync(translationsDirectory + 'en.json', 'utf8'));
const whiteList = Object.keys(enData);
fs.writeFileSync(translationsDirectory + 'whitelist_en.json', JSON.stringify(whiteList, null, 2));
const extracted = {};
Object.keys(extractedJson).forEach(key => {
extracted[key] = extractedJson[key].defaultMessage;
});
return extracted;
}

/**
* Load messages from existing json file
* @param {string} language (en, cs ...)
* @returns {object} where keys are IDs and values are messages
*/
function loadMessages(language) {
const fileName = `${translationsDirectory}/${language}.json`;
if (!fs.existsSync(fileName)) {
console.warn(`File ${fileName} does not exist!`);
return {};
}

const rawData = fs.readFileSync(fileName, 'utf8');
return JSON.parse(rawData);
}

/**
* Save messages into json language file.
* @param {string} language
* @param {object} messages where keys are IDs and values are messages
*/
function saveMessages(language, messages) {
const fileName = `${translationsDirectory}/${language}.json`;
const sortedMessages = {};
Object.keys(messages)
.sort()
.forEach(key => {
sortedMessages[key] = messages[key];
});

fs.writeFileSync(fileName, JSON.stringify(sortedMessages, null, 2));
}

/**
* Load whitelist (list of not-translated keys).
* @param {string} language
* @returns {Set} of whitelisted message IDs
*/
function loadWhitelist(language) {
const fileName = `${translationsDirectory}/whitelist_${language}.json`;
const whitelist = new Set();

if (fs.existsSync(fileName)) {
const rawData = fs.readFileSync(fileName, 'utf8');
JSON.parse(rawData).forEach(key => {
whitelist.add(key);
});
} else {
console.warn(`File ${fileName} does not exist!`);
}

return whitelist;
}

/**
* Save whitelist into json file.
* @param {string} language
* @param {Set} whitelist
*/
function saveWhitelist(language, whitelist) {
const fileName = `${translationsDirectory}/whitelist_${language}.json`;
const whitelistArray = Array.from(whitelist).sort();
fs.writeFileSync(fileName, JSON.stringify(whitelistArray, null, 2));
}

/**
* Compare extracted messages with existing messages and create lists of newly created keys, modified keys
* deleted keys, and possibly keys that were not translated yet.
* If whitelist is present, noTranslated list is constructed and modified list is not.
* I.e., ref language is diff-ed without whitelist, other languages with whitelist.
* @param {object} extracted messages from source code
* @param {object} existing messages loaded from json file
* @param {Set|null} whitelist keys that are ignored when not translated
* @param {string[]} modifiedRef initial modified list (othre languages are given modified list of ref. language).
* @returns
*/
function diff(extracted, existing, whitelist = null, modifiedRef = []) {
const created = [];
let modified = [...modifiedRef];
const notTranslated = [];
const deleted = [];

Object.keys(extracted).forEach(key => {
if (existing[key] === undefined) {
created.push(key);
} else if (existing[key] !== extracted[key] && !whitelist) {
modified.push(key);
} else if (existing[key] === extracted[key] && whitelist && !whitelist.has(key)) {
notTranslated.push(key);
}
});

Object.keys(existing).forEach(key => {
if (extracted[key] === undefined) {
deleted.push(key);
}
});

created.sort();
modified = modified.filter(key => existing[key] !== undefined).sort();
deleted.sort();
return { created, modified, notTranslated, deleted };
}

/**
* Use diff results to modify the messages and the whitelist.
* @param {object} messages to be updated inplace
* @param {object} extracted messages from the sources (read only)
* @param {Set} whitelist to be updated inplace
* @param {object} result of the previous diff operation
*/
function applyDiff(messages, extracted, whitelist, { created, deleted }) {
created.forEach(key => {
messages[key] = extracted[key];
});
deleted.forEach(key => {
delete messages[key];
whitelist.delete(key);
});
}

/**
* Helper function that prints out one colored list with caption
* @param {string[]} list
* @param {string} caption
* @param {string} color
*/
function _printInfo(list, caption, color) {
if (list.length > 0) {
console.log(`\t${caption}:`);
list.forEach(key => console.log(`\t\t${key}`[color]));
console.log();
}
}

/**
* Print results of a diff in human-readable format.
* @param {string} language
* @param {object} result of previous diff operation
*/
function printDiffInfo(language, { created = [], modified = [], notTranslated = [], deleted = [] }) {
console.log(`[${language}] translations:`.brightWhite.bold);

_printInfo(created, 'Newly created messages', 'brightGreen');
_printInfo(modified, 'Modified', 'brightYellow');
_printInfo(notTranslated, 'Not translated', 'yellow');
_printInfo(deleted, 'Deleted', 'red');

if (created.length + modified.length + notTranslated.length + deleted.length === 0) {
console.log();
console.log('\tNo modifications.'.brightGreen);
console.log();
}
}

/**
* Main function that do all the stuff.
*/
async function processTranslations() {
const extracted = await extractMessages();
const refMessages = loadMessages(refLanguage);
const refDiffRes = diff(extracted, refMessages);
printDiffInfo(refLanguage, refDiffRes);
saveMessages(refLanguage, extracted);

otherLanguages.forEach(lang => {
const messages = loadMessages(lang);
const whitelist = loadWhitelist(lang);
const diffRes = diff(extracted, messages, whitelist, refDiffRes.modified);
printDiffInfo(lang, diffRes);
applyDiff(messages, extracted, whitelist, diffRes);
saveMessages(lang, messages);
saveWhitelist(lang, whitelist);
});
}

processTranslations().catch(e => console.error(e));
86 changes: 43 additions & 43 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,88 +22,88 @@
"dev": "babel-node bin/dev.js --max-old-space-size=4096",
"start": "node bin/server.js",
"deploy": "mkdir -p ./prod && mkdir -p ./prod/etc && cp -rf ./views ./prod && cp -rf ./bin ./prod && cp -n ./etc/env.json.example ./prod/etc/env.json && rm -f ./prod/public/bundle* && rm -f ./prod/public/style* && cp -rf ./public ./prod",
"exportStrings": "rm -rf node_modules/.cache/babel-loader/* && rm -rf tmp/messages/* && npm run build && babel-node bin/manageTranslations.js",
"manageTranslations": "babel-node bin/manageTranslations.js",
"format": "prettier --config .prettierrc --write \"src/**/*.js\""
},
"dependencies": {
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
"@babel/plugin-transform-react-inline-elements": "^7.14.5",
"@formatjs/intl-pluralrules": "^4.0.27",
"@formatjs/intl-relativetimeformat": "^9.1.6",
"@fortawesome/fontawesome-free": "^5.15.3",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-brands-svg-icons": "^5.15.3",
"@fortawesome/free-regular-svg-icons": "^5.15.3",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@iktakahiro/markdown-it-katex": "^3.0.3",
"@iktakahiro/markdown-it-katex": "^4.0.1",
"admin-lte": "3.1.0",
"ajv": "5.5.1",
"ajv-keywords": "2.1.1",
"babel-plugin-react-intl": "^4.1.2",
"babel-plugin-formatjs": "^10.3.0",
"bluebird": "^3.3.5",
"browser-cookies": "^1.0.8",
"buffer": "^5.0.7",
"chai-immutable": "^1.6.0",
"classnames": "^2.2.5",
"codemirror": "^5.58.2",
"buffer": "^6.0.3",
"chai-immutable": "^2.1.0",
"classnames": "^2.3.1",
"codemirror": "^5.62.0",
"cookie-parser": "^1.4.1",
"cross-fetch": "^3.1.4",
"css-loader": "^5.2.6",
"deep-equal": "^2.0.1",
"ejs": "^2.6.1",
"deep-equal": "^2.0.5",
"ejs": "^3.1.6",
"exenv": "^1.2.1",
"express": "^4.13.4",
"file-saver": "^1.3.3",
"flat": "^4.0.0",
"flow-bin": "^0.46.0",
"font-awesome-animation": "^0.2.1",
"glob": "^7.1.2",
"file-saver": "^2.0.5",
"flat": "^5.0.2",
"font-awesome-animation": "^1.1.1",
"glob": "^7.1.7",
"global": "^4.3.1",
"highlight.js": "^10.4.1",
"highlight.js": "^11.0.1",
"immutable": "^3.8.2",
"jwt-decode": "^2.2.0",
"markdown-it": "^8.4.1",
"moment": "^2.24.0",
"pretty-ms": "^6.0.1",
"jwt-decode": "^3.1.2",
"markdown-it": "^12.0.6",
"moment": "^2.29.1",
"pretty-ms": "^7.0.1",
"prop-types": "^15.5.8",
"react": "^16.13.0",
"react-ace": "5.9.0",
"react-ace": "^9.4.1",
"react-bootstrap": "1.6.1",
"react-collapse": "^4.0.2",
"react-copy-to-clipboard": "^5.0.1",
"react-collapse": "^5.1.0",
"react-copy-to-clipboard": "^5.0.3",
"react-datetime": "^3.0.4",
"react-dom": "^16.8.6",
"react-dropzone": "^3.5.3",
"react-dropzone": "^11.3.2",
"react-height": "^3.0.0",
"react-helmet": "^5.0.3",
"react-helmet": "^6.1.0",
"react-immutable-proptypes": "^2.1.0",
"react-intl": "2.4.0",
"react-intl": "5.20.3",
"react-motion": "^0.5.2",
"react-redux": "^7.2.0",
"react-responsive": "^8.0.1",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1",
"react-toggle": "4.1.1",
"redux": "^4.0.4",
"react-responsive": "^8.2.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-toggle": "4.1.2",
"redux": "^4.1.0",
"redux-actions": "^2.6.5",
"redux-form": "^8.2.4",
"redux-form": "^8.3.7",
"redux-promise-middleware": "^6.1.1",
"redux-storage": "^4.1.2",
"redux-storage-decorator-filter": "^1.1.8",
"redux-storage-engine-localstorage": "^1.1.4",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"serialize-javascript": "^3.1.0",
"serialize-javascript": "^6.0.0",
"statuscode": "0.0.0",
"validator": "^7.0.0",
"viz.js": "^1.8.0"
"validator": "^13.6.0",
"viz.js": "^2.1.2"
},
"devDependencies": {
"@babel/cli": "^7.14.5",
"@babel/core": "^7.14.6",
"@babel/node": "^7.14.5",
"@babel/node": "^7.14.7",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/preset-env": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@babel/register": "^7.14.5",
"@formatjs/cli": "^4.2.21",
"async": "^3.1.0",
"babel-eslint": "^10.0.2",
"babel-loader": "^8.2.2",
Expand All @@ -114,11 +114,11 @@
"babel-preset-stage-1": "^6.24.0",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
"colors": "^1.1.2",
"colors": "^1.4.0",
"css-loader": "^5.2.6",
"css-modules-require-hook": "^4.2.3",
"dotenv": "^10.0.0",
"eslint": "^7.28.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "16.0.3",
"eslint-config-standard-react": "11.0.1",
Expand All @@ -137,7 +137,7 @@
"json-loader": "^0.5.4",
"less": "^4.1.1",
"less-loader": "^10.0.0",
"marked": "^2.1.1",
"marked": "^2.1.2",
"mini-css-extract-plugin": "^1.6.0",
"mocha": "^9.0.1",
"mocha-lcov-reporter": "^1.3.0",
Expand All @@ -150,7 +150,7 @@
"strip-loader": "^0.1.2",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.3",
"webpack": "^5.39.1",
"webpack": "^5.40.0",
"webpack-cli": "^4.7.2",
"webpack-dev-middleware": "^5.0.0",
"webpack-dev-server": "^3.11.2",
Expand Down
Loading