Skip to content
This repository has been archived by the owner on Mar 1, 2021. It is now read-only.

Commit

Permalink
intl+babel makes translation easier by collecting translatable pieces…
Browse files Browse the repository at this point in the history
… in the code
  • Loading branch information
clemsos committed Aug 1, 2016
1 parent 4710474 commit 28f0463
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 43 deletions.
6 changes: 5 additions & 1 deletion .babelrc
@@ -1,3 +1,7 @@
{
"presets": [ "es2015" ]
"presets": [
"es2015",
"stage-0",
"react"
]
}
156 changes: 156 additions & 0 deletions .scripts/extract-intl.js
@@ -0,0 +1,156 @@
/* eslint-disable */
/**
* This script will extract the internationalization messages from all components
and package them in the transalation json files in the translations file.
*/
const fs = require('fs');
const nodeGlob = require('glob');
const transform = require('babel-core').transform;

const animateProgress = require('./helpers/progress');
const addCheckmark = require('./helpers/checkmark');

const pkg = require('../package.json');
const i18n = require('../imports/i18n.js');

require('shelljs/global');

// Glob to match all js files except test files
const FILES_TO_PARSE = 'imports/**/*.jsx'
// 'imports/client/**/*.jsx!(*.test).js!(*.spec).js!imports/client/helpers';
const DEST_FOLDER = 'i18n'
const locales = i18n.appLocales;

const newLine = () => process.stdout.write('\n');

// Progress Logger
let progress;
const task = (message) => {
progress = animateProgress(message);
process.stdout.write(message);

return (error) => {
if (error) {
process.stderr.write(error);
}
clearTimeout(progress);
return addCheckmark(() => newLine());
}
}

// Wrap async functions below into a promise
const glob = (pattern) => new Promise((resolve, reject) => {
nodeGlob(pattern, (error, value) => (error ? reject(error) : resolve(value)));
});

const readFile = (fileName) => new Promise((resolve, reject) => {
fs.readFile(fileName, (error, value) => (error ? reject(error) : resolve(value)));
});

const writeFile = (fileName, data) => new Promise((resolve, reject) => {
fs.writeFile(fileName, data, (error, value) => (error ? reject(error) : resolve(value)));
});

// Store existing translations into memory
const oldLocaleMappings = [];
const localeMappings = [];
// Loop to run once per locale
for (const locale of locales) {
oldLocaleMappings[locale] = {};
localeMappings[locale] = {};
// File to store translation messages into
const translationFileName = `${DEST_FOLDER}/${locale}.json`;
try {
// Parse the old translation message JSON files
const messages = JSON.parse(fs.readFileSync(translationFileName));
for (const message of messages) {
oldLocaleMappings[locale][message.id] = message;
}
} catch (error) {
if (error.code !== 'ENOENT') {
process.stderr.write(
`There was an error loading this translation file: ${translationFileName}
\n${error}`
);
}
}
}

const extractFromFile = async (fileName) => {
try {
const code = await readFile(fileName);
// Use babel plugin to extract instances where react-intl is used
const { metadata: result } = await transform(code, {
presets: pkg.babel.presets,
plugins: [
['react-intl'],
],
});


for (const message of result['react-intl'].messages) {
for (const locale of locales) {
const oldLocaleMapping = oldLocaleMappings[locale][message.id];
// Merge old translations into the babel extracted instances where react-intl is used
localeMappings[locale][message.id] = {
id: message.id,
description: message.description,
defaultMessage: message.defaultMessage,
message: (oldLocaleMapping && oldLocaleMapping.message)
? oldLocaleMapping.message
: '',
};
}
}
} catch (error) {
process.stderr.write(`Error transforming file: ${fileName}\n${error}`);
}
};

(async function main() {
const memoryTaskDone = task('Storing language files in memory');
const files = await glob(FILES_TO_PARSE);
memoryTaskDone()

const extractTaskDone = task('Run extraction on all files')
// Run extraction on all files that match the glob on line 16
await Promise.all(files.map((fileName) => extractFromFile(fileName)));
extractTaskDone()

// Make the directory if it doesn't exist, especially for first run
mkdir('-p', DEST_FOLDER);
for (const locale of locales) {
const translationFileName = `${DEST_FOLDER}/${locale}.json`;
try {
const localeTaskDone = task(
`Writing translation messages for ${locale} to: ${translationFileName}`
);

// Sort the translation JSON file so that git diffing is easier
// Otherwise the translation messages will jump around every time we extract
let messages = Object.values(localeMappings[locale]).sort((a, b) => {
a = a.id.toUpperCase();
b = b.id.toUpperCase();
return do {
if (a < b) -1;
else if (a > b) 1;
else 0;
};
});

// Write to file the JSON representation of the translation messages
const prettified = `${JSON.stringify(messages, null, 2)}\n`;

await writeFile(translationFileName, prettified);

localeTaskDone();
} catch (error) {
localeTaskDone(
`There was an error saving this translation file: ${translationFileName}
\n${error}`
);
}
}

process.exit()
}());
11 changes: 11 additions & 0 deletions .scripts/helpers/checkmark.js
@@ -0,0 +1,11 @@
var chalk = require('chalk');

/**
* Adds mark check symbol
*/
function addCheckMark(callback) {
process.stdout.write(chalk.green(' ✓'));
callback();
}

module.exports = addCheckMark;
23 changes: 23 additions & 0 deletions .scripts/helpers/progress.js
@@ -0,0 +1,23 @@
var readline = require('readline');

/**
* Adds an animated progress indicator
*
* @param {string} message The message to write next to the indicator
* @param {number} amountOfDots The amount of dots you want to animate
*/
function animateProgress(message, amountOfDots) {
if (typeof amountOfDots !== 'number') {
amountOfDots = 3;
}

var i = 0;
return setInterval(function () {
readline.cursorTo(process.stdout, 0);
i = (i + 1) % (amountOfDots + 1);
var dots = new Array(i + 1).join('.');
process.stdout.write(message + dots);
}, 500);
}

module.exports = animateProgress;
38 changes: 19 additions & 19 deletions i18n/en-US.json → i18n/en.json
@@ -1,47 +1,47 @@
[
{
"id": "home.tagline",
"defaultMessage": "Social Network Analysis for humans.",
"message": "Social Network Analysis for humans."
"id": "home.browseTopograms",
"defaultMessage": "Browse publics topograms",
"message": ""
},
{
"id": "home.subtitle",
"defaultMessage": "An open-source toolkit to process, visualize and analyze networks.",
"message": "An open-source toolkit to process, visualize and analyze networks."
"message": ""
},
{
"id": "home.browseTopograms",
"defaultMessage": "Browse publics topograms",
"message": "Browse publics topograms"
"id": "home.tagline",
"defaultMessage": "Social Network Analysis for humans.",
"message": ""
},
{
"id": "home.topogram.addForm.label",
"defaultMessage": "Create a new Topogram",
"id": "topogram.addForm.hint",
"defaultMessage": "Input a name",
"message": ""
},
{
"id": "home.topogram.addForm.hint",
"defaultMessage": "Input a name",
"id": "topogram.addForm.label",
"defaultMessage": "Create a new Topogram",
"message": ""
},
{
"id": "topogram.index.card.button.browse",
"defaultMessage": "Browse",
"id": "topogram.deleteDialog.button.cancel",
"defaultMessage": "Cancel",
"message": ""
},
{
"id": "topogram.index.card.button.data",
"defaultMessage": "Data",
"id": "topogram.deleteDialog.button.delete",
"defaultMessage": "Delete",
"message": ""
},
{
"id": "topogram.index.card.button.cancel",
"defaultMessage": "Cancel",
"id": "topogram.index.card.button.browse",
"defaultMessage": "Browse",
"message": ""
},
{
"id": "topogram.index.card.button.delete",
"defaultMessage": "Delete",
"id": "topogram.index.card.button.data",
"defaultMessage": "Data",
"message": ""
},
{
Expand Down
22 changes: 0 additions & 22 deletions i18n/fr-FR.json

This file was deleted.

52 changes: 52 additions & 0 deletions i18n/fr.json
@@ -0,0 +1,52 @@
[
{
"id": "home.browseTopograms",
"defaultMessage": "Browse publics topograms",
"message": "Consulter les topograms publics"
},
{
"id": "home.subtitle",
"defaultMessage": "An open-source toolkit to process, visualize and analyze networks.",
"message": "Un outil open-source pour créer,visualiser et analyser des réseaux."
},
{
"id": "home.tagline",
"defaultMessage": "Social Network Analysis for humans.",
"message": "L'analyse de réseaux sociaux enfin à la portée des humains."
},
{
"id": "topogram.addForm.hint",
"defaultMessage": "Input a name",
"message": "Entrer un nom"
},
{
"id": "topogram.addForm.label",
"defaultMessage": "Create a new Topogram",
"message": "Créer un nouveau topogram"
},
{
"id": "topogram.deleteDialog.button.cancel",
"defaultMessage": "Cancel",
"message": "Annuler"
},
{
"id": "topogram.deleteDialog.button.delete",
"defaultMessage": "Delete",
"message": "Supprimer"
},
{
"id": "topogram.index.card.button.browse",
"defaultMessage": "Browse",
"message": "Voir"
},
{
"id": "topogram.index.card.button.data",
"defaultMessage": "Data",
"message": "Données"
},
{
"id": "topogram.index.card.deleteDialog.confirmQuestion",
"defaultMessage": "Are you sure you want to delete this topogram ?",
"message": "Êtes-vous sûr de vouloir supprimer ce topogram ?"
}
]
13 changes: 12 additions & 1 deletion package.json
Expand Up @@ -34,8 +34,11 @@
"winston": "^2.2.0"
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-core": "^6.10.4",
"babel-plugin-react-intl": "^2.1.3",
"babel-preset-es2015": "^6.9.0",
"babel-preset-react": "^6.11.1",
"chai": "^3.5.0",
"daemon": "^1.1.0",
"enzyme": "^2.4.1",
Expand Down Expand Up @@ -64,11 +67,19 @@
"run-sequence": "^1.0.0",
"sinon": "^1.17.5"
},
"babel": {
"presets": [
"es2015",
"stage-0",
"react"
]
},
"scripts": {
"start": "meteor",
"lint": "eslint --ext .jsx --ext .js --quiet .",
"test": "gulp test",
"test:ui": "meteor test --driver-package=practicalmeteor:mocha --port=3010"
"test:ui": "meteor test --driver-package=practicalmeteor:mocha --port=3010",
"extract-intl": "babel-node --presets es2015,stage-0 -- ./.scripts/extract-intl.js"
},
"repository": {
"type": "git",
Expand Down

0 comments on commit 28f0463

Please sign in to comment.