scratch-www/bin/build-locales
Chris Garrity 45d672ab30 Add translation resource mapping
When the name of the view in `routes.json` does not match the name of the resource in transifex there needs to be a mapping added to the script that generates translations. _Note: the resource name in transifex is usually based on the name of the source file in the `views` directory. Usually the name of the view in the routes file matches._

Since `routes.json` is a JSON file that does not support comments there’s no way to add a warning to the file when someone is adding a route.

The sympton was the message that there were no translations for ‘vernier’ or ‘embed’ when running translate. The `vernier` route is actually the `gdxfor` view, and `embed` is just another version of `preview`
2019-10-29 17:48:01 -04:00

263 lines
8.8 KiB
JavaScript
Executable File

#!/usr/bin/env node
/*
Generates javascript translation files for each view from the individual language
json files downloaded from Transifex for each view. Each view also includes the
general translations.
Expects two inputs, the directory for the input json files, and an output
to put the generated js files into.
The input directory is to have subfolders for each view, with json files named by
langage code. E.g., localizations/about/fr.json
The output files are javascript files that declare objects by locale. Each locale
has a sub-object with FormattedMessage ids as keys, and translated strings as
values. If no translation was found for a string, the default english will be the
value.
Output Example:
'''
var message = {
en: {
'general.inAWorld': 'In a world, where bears are invisible...',
'general.question': 'Are there bears here?',
'general.answer': 'I dunno, but there could be...'
},
es: {
'general.inAWorld': 'En un mundo, donde hay osos invisibles',
'general.question': 'Are there bears here?',
'general.answer': 'No sé, pero es posible...'
}
}
'''
*/
var async = require('async');
var fs = require('fs');
const languages = require('scratch-l10n').default;
var localizedUrls = require('./lib/localized-urls');
var merge = require('lodash.merge');
var defaults = require('lodash.defaults');
var path = require('path');
var routes = require('../src/routes.json');
// -----------------------------------------------------------------------------
// Utility function
// -----------------------------------------------------------------------------
var writeIntlFile = function (outputDir, view, translations, callback) {
var fileString = 'window._messages = ' + JSON.stringify(translations) + ';';
var fileName = outputDir + '/' + view + '.intl.js';
fs.writeFile(fileName, fileString, 'utf8', function (err) {
if (err) {
callback(err);
} else {
callback();
}
});
};
// -----------------------------------------------------------------------------
// Main script
// -----------------------------------------------------------------------------
var args = process.argv.slice(2);
if (!args.length) {
process.stdout.write('A localizations directory must be specified.\n');
process.exit(1);
}
var localesDir = path.resolve(__dirname, '../', args.shift());
try {
fs.accessSync(localesDir, fs.F_OK);
} catch (err) {
// Doesn't exist - create it.
process.stdout.write('Fatal error: No localizations directory.\n');
process.exit(1);
}
if (!args.length) {
process.stdout.write('A destination directory must be specified.\n');
process.exit(1);
}
var outputDir = path.resolve(__dirname, '../', args[0]);
try {
fs.accessSync(outputDir, fs.F_OK);
} catch (err) {
// Doesn't exist - create it.
fs.mkdirSync(outputDir);
}
// get global locale strings first.
var globalTemplateFile = path.resolve(__dirname, '../src/l10n.json');
// generate object with all translations of general strings;
// every view includes these strings
var generalLocales = {'en': JSON.parse(fs.readFileSync(globalTemplateFile, 'utf8'))};
for ( var l in languages ) {
try {
var langTranslations = path.resolve(
__dirname,
'../',
localesDir,
'scratch-website.general-l10njson',
l + '.json'
);
fs.accessSync(langTranslations);
if (l !== 'en') {
generalLocales[l] = JSON.parse(fs.readFileSync(langTranslations, 'utf8'));
}
} catch (err) {
// just use english
generalLocales[l] = generalLocales['en'];
}
}
// default strings (en), for all views
var defaultLocales = {
//e.g.
// 'explore: {'explore.recent': 'recent'}
// 'about': {'about.title': 'about Scratch'}
// ... etc
};
var views = [];
var localizedAssetUrls = {};
// Most views will have the same name for:
// - the view folder
// - the route name
// - the transifex resource
// Add exceptions:
// - txMapping: if the name of the transifex resource is different from the route name
var txMapping = {
'projects': 'preview',
'embed': 'preview',
'vernier': 'gdxfor',
'scratch_1.4': 'scratch_14' // transifex doesn't allow dots in resource names
};
// start with english default for all views
for (var v in routes) {
if (typeof routes[v].redirect !== 'undefined') {
continue;
}
views.push(routes[v].name);
try {
var subdir = routes[v].view.split('/');
subdir.pop();
var l10n = path.resolve(__dirname, '../src/views/' + subdir.join('/') + '/l10n.json');
var viewIds = JSON.parse(fs.readFileSync(l10n, 'utf8'));
defaultLocales[routes[v].name] = viewIds;
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
try {
fs.accessSync(path.resolve(__dirname, '../src/views/' + routes[v].view + '.jsx'));
} catch (err) {
// the config for the view is not set up correctly, so throw the error.
throw err;
}
}
// get asset url translations
try {
subdir = routes[v].view.split('/');
subdir.pop();
var l10nStatic = path.resolve(__dirname, '../src/views/' + subdir.join('/') + '/l10n-static.json');
localizedAssetUrls[routes[v].name] = {};
var viewUrls = require(l10nStatic);
localizedAssetUrls[routes[v].name]['en'] = viewUrls;
var defaultUrls = localizedUrls['en'];
for (var lang in languages) {
localizedAssetUrls[routes[v].name][lang] = {};
var langUrls = localizedUrls[lang] || {};
for (var key in viewUrls) {
if (langUrls.hasOwnProperty(key)) {
localizedAssetUrls[routes[v].name][lang][key] = langUrls[key];
} else {
localizedAssetUrls[routes[v].name][lang][key] = defaultUrls[key];
}
}
}
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
try {
fs.accessSync(path.resolve(__dirname, '../src/views/' + routes[v].view + '.jsx'));
} catch (err) {
// the config for the view is not set up correctly, so throw the error.
throw err;
}
}
}
var allLangs = Object.keys(languages);
async.forEachLimit(views, 5, function (view, cb) {
//start with localizedAssets, merge the view translations
var viewLocales = {};
viewLocales['en'] = merge({}, generalLocales['en'], defaultLocales[view]);
// merge view specific english strings, first then other languages
async.forEach(allLangs, function (isoCode, cb) {
var txResource = txMapping[view] || view;
var translationsFile = path.resolve(
__dirname,
'../',
localesDir,
'scratch-website.' + txResource + '-l10njson',
isoCode + '.json'
);
fs.readFile(translationsFile, 'utf8', function (err, data) {
if (err) {
if (err.code === 'ENOENT') {
// ignore missing files for english, Meow, and Edible Scratch
if (isoCode !== 'en') {
if (isoCode !== 'cat' && isoCode !== 'yum' && defaultLocales.hasOwnProperty(view)) {
process.stdout.write('No translations for ' + view + ' ' + isoCode + ', using english\n');
}
viewLocales[isoCode] = merge({}, generalLocales[isoCode], defaultLocales[view]);
defaults(viewLocales[isoCode], generalLocales['en']);
}
return cb();
} else {
return cb(err);
}
}
try {
// don't overwrite english view strings
if (isoCode !== 'en') {
viewLocales[isoCode] = merge({}, generalLocales[isoCode], JSON.parse(data));
defaults(viewLocales[isoCode], viewLocales['en']);
}
} catch (e) {
return cb(e);
}
cb();
});
}, function (err) { // eslint-disable-line no-unused-vars
// ignore translation file errors, just keep going.
var viewTranslations = merge({}, viewLocales, localizedAssetUrls[view]);
writeIntlFile(outputDir, view, viewTranslations, function (err) {
if (err) {
process.stdout.write('Failed to save: ' + view + '\n');
return cb(err);
}
});
});
cb();
}, function (err) {
if (err) {
process.stdout.write('Writing intl files no haz successes\n');
process.exit(1);
}
return;
});