Commit cc457aed authored by Matthew Taylor's avatar Matthew Taylor

Merge pull request #392 from mewtaylor/cleanup/localization-stuff

Cleanup: catch more localization errors that might arise
parents faf35495 c448b15d
...@@ -59,6 +59,8 @@ test: ...@@ -59,6 +59,8 @@ test:
@echo "" @echo ""
@make functional @make functional
@echo "" @echo ""
@make localization
@echo ""
lint: lint:
$(ESLINT) ./*.js $(ESLINT) ./*.js
...@@ -80,6 +82,9 @@ functional: ...@@ -80,6 +82,9 @@ functional:
integration: integration:
$(TAP) ./test/integration/*.js $(TAP) ./test/integration/*.js
localization:
$(TAP) ./test/localization/*.js
# ------------------------------------ # ------------------------------------
.PHONY: build clean deploy static tag translations webpack watch stop start test lint .PHONY: build clean deploy static tag translations webpack watch stop start test lint
...@@ -39,8 +39,8 @@ var fs = require('fs'); ...@@ -39,8 +39,8 @@ var fs = require('fs');
var glob = require('glob'); var glob = require('glob');
var merge = require('lodash.merge'); var merge = require('lodash.merge');
var path = require('path'); var path = require('path');
var po2icu = require('po2icu');
var languages = require('../languages.json');
var localeCompare = require('./lib/locale-compare'); var localeCompare = require('./lib/locale-compare');
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
...@@ -54,12 +54,7 @@ if (!args.length) { ...@@ -54,12 +54,7 @@ if (!args.length) {
process.stdout.write('A destination directory must be specified.'); process.stdout.write('A destination directory must be specified.');
process.exit(1); process.exit(1);
} }
var verbose = false;
if (args.length > 1) {
verbose = (args[1] === '-v') ? true : false;
}
var poUiDir = path.resolve(__dirname, '../node_modules/scratchr2_translations/ui');
var outputDir = path.resolve(__dirname, '../', args[0]); var outputDir = path.resolve(__dirname, '../', args[0]);
try { try {
fs.accessSync(outputDir, fs.F_OK); fs.accessSync(outputDir, fs.F_OK);
...@@ -68,27 +63,19 @@ try { ...@@ -68,27 +63,19 @@ try {
fs.mkdirSync(outputDir); fs.mkdirSync(outputDir);
} }
// get global locale strings first.
var globalTemplateFile = path.resolve(__dirname, '../src/l10n.json');
// message key with english string values (i.e. default values) // message key with english string values (i.e. default values)
var generalIds = JSON.parse(fs.readFileSync(globalTemplateFile, 'utf8'));
var viewLocales = {}; var viewLocales = {};
var generalLocales = {
en: generalIds
};
// FormattedMessage id with english string as value. Use for default values in translations // FormattedMessage id with english string as value. Use for default values in translations
// Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' } // Sample structure: { 'general-general.blah': 'blah', 'about-about.blah': 'blahblah' }
var idsWithICU = {}; var idsWithICU = {};
// reverse (i.e. english string with message key as the value) object for searching po files. // reverse (i.e. english string with message key as the value) object for searching po files.
// Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' } // Sample structure: { 'blah': 'general-general.blah', 'blahblah': 'about-about.blah' }
var icuWithIds = {}; var icuWithIds = {};
for (var id in generalIds) { // get global locale strings first.
idsWithICU['general-' + id] = generalIds[id]; var globalTemplateFile = path.resolve(__dirname, '../src/l10n.json');
icuWithIds[generalIds[id]] = 'general-' + id; localeCompare.getIdsForView('general', globalTemplateFile, viewLocales, idsWithICU, icuWithIds);
}
// start with all views, and remove localized ones as they are iterated over // start with all views, and remove localized ones as they are iterated over
var views = glob.sync(path.resolve(__dirname, '../src/views/*')); var views = glob.sync(path.resolve(__dirname, '../src/views/*'));
...@@ -102,15 +89,7 @@ files.forEach(function (file) { ...@@ -102,15 +89,7 @@ files.forEach(function (file) {
var dirPath = file.split('/'); var dirPath = file.split('/');
dirPath.pop(); dirPath.pop();
var view = dirPath.pop(); var view = dirPath.pop();
localeCompare.getIdsForView(view, file, viewLocales, idsWithICU, icuWithIds);
var viewIds = JSON.parse(fs.readFileSync(file, 'utf8'));
viewLocales[view] = {
en: viewIds
};
for (var id in viewIds) {
idsWithICU[view + '-' + id] = viewIds[id];
icuWithIds[viewIds[id]] = view + '-' + id; // add viewName to identifier for later
}
}); });
// md5 of english strings with message key as the value for searching po files. // md5 of english strings with message key as the value for searching po files.
...@@ -118,53 +97,18 @@ files.forEach(function (file) { ...@@ -118,53 +97,18 @@ files.forEach(function (file) {
var md5WithIds = localeCompare.getMD5Map(icuWithIds); var md5WithIds = localeCompare.getMD5Map(icuWithIds);
// Get ui localization strings first // Get ui localization strings first
glob(poUiDir + '/*', function (err, files) { var isoCodes = Object.keys(languages);
if (err) throw new Error(err); for (i in isoCodes) {
var translations = localeCompare.getTranslationsForLanguage(isoCodes[i], idsWithICU, md5WithIds);
files.forEach(function (file) { for (var key in translations) {
var lang = file.split('/').pop(); viewLocales[key] = merge(viewLocales[key], translations[key]);
var jsFile = path.resolve(file, 'LC_MESSAGES/djangojs.po');
var pyFile = path.resolve(file, 'LC_MESSAGES/django.po');
var translations = {};
try {
var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
translations = localeCompare.mergeNewTranslations(translations, jsTranslations, idsWithICU, md5WithIds);
} catch (err) {
if (verbose) process.stdout.write(lang + ': ' + err + '\n');
}
try {
var pyTranslations = po2icu.poFileToICUSync(lang, pyFile);
translations = localeCompare.mergeNewTranslations(translations, pyTranslations, idsWithICU, md5WithIds);
} catch (err) {
if (verbose) process.stdout.write(lang + ': ' + err + '\n');
}
// add new translations to locale object
for (var id in translations) {
var ids = id.split('-'); // [viewName, stringId]
var viewName = ids[0];
var stringId = ids[1];
if (viewLocales.hasOwnProperty(viewName)) {
if (!viewLocales[viewName].hasOwnProperty(lang)) viewLocales[viewName][lang] = {};
viewLocales[viewName][lang][stringId] = translations[id];
} else {
// default to general
if (!generalLocales.hasOwnProperty(lang)) generalLocales[lang] = {};
generalLocales[lang][stringId] = translations[id];
} }
} }
});
for (var i in views) { for (i in views) {
var viewTranslations = generalLocales; var viewTranslations = viewLocales['general'];
if (views[i] in viewLocales) { if (views[i] in viewLocales) {
viewTranslations = merge(viewLocales[views[i]], viewTranslations); viewTranslations = merge(viewLocales[views[i]], viewTranslations);
} }
var objectString = JSON.stringify(viewTranslations); localeCompare.writeTranslationsToJS(outputDir, views[i], viewTranslations);
var fileString = 'window._messages = ' + objectString + ';'; }
fs.writeFileSync(outputDir + '/' + views[i] + '.intl.js', fileString);
}
});
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
var crypto = require('crypto'); var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var po2icu = require('po2icu');
var Helpers = {}; var Helpers = {};
...@@ -62,4 +65,75 @@ Helpers.getMD5Map = function (ICUIdMap) { ...@@ -62,4 +65,75 @@ Helpers.getMD5Map = function (ICUIdMap) {
return md5Map; return md5Map;
}; };
/**
* Grabs the translated strings from the po files for the given language and strings
* @param {str} lang iso code of the language to use
* @param {object} idsWithICU key: '<viewName>-<react-intl string id>'.
* value: english strings for translation
* @param {object} md5WithIds key: md5 hash of the english strings for translation.
* value: '<viewName>-<react-intl string id>'
* @return {object} translations – sub-objects by view containing:
* key: '<react-intl string id>'
* value: translated version of string
*/
Helpers.getTranslationsForLanguage = function (lang, idsWithICU, md5WithIds) {
var poUiDir = path.resolve(__dirname, '../../node_modules/scratchr2_translations/ui');
var jsFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/djangojs.po');
var pyFile = path.resolve(poUiDir, lang + '/LC_MESSAGES/django.po');
var translations = {};
try {
fs.accessSync(jsFile, fs.R_OK);
var jsTranslations = po2icu.poFileToICUSync(lang, jsFile);
translations = Helpers.mergeNewTranslations(translations, jsTranslations, idsWithICU, md5WithIds);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
try {
fs.accessSync(pyFile, fs.R_OK);
var pyTranslations = po2icu.poFileToICUSync(lang, pyFile);
translations = Helpers.mergeNewTranslations(translations, pyTranslations, idsWithICU, md5WithIds);
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
}
var translationsByView = {};
for (var id in translations) {
var ids = id.split('-'); // [viewName, stringId]
var viewName = ids[0];
var stringId = ids[1];
if (!translationsByView.hasOwnProperty(viewName)) {
translationsByView[viewName] = {};
}
if (!translationsByView[viewName].hasOwnProperty(lang)) {
translationsByView[viewName][lang] = {};
}
translationsByView[viewName][lang][stringId] = translations[id];
}
return translationsByView;
};
Helpers.writeTranslationsToJS = function (outputDir, viewName, translationObject) {
var objectString = JSON.stringify(translationObject);
var fileString = 'window._messages = ' + objectString + ';';
fs.writeFileSync(outputDir + '/' + viewName + '.intl.js', fileString);
};
Helpers.getIdsForView = function (viewName, viewFile, localeObject, idsWithICU, icuWithIds) {
var ids = JSON.parse(fs.readFileSync(viewFile, 'utf8'));
localeObject[viewName] = {
en: ids
};
for (var id in ids) {
idsWithICU[viewName + '-' + id] = ids[id];
icuWithIds[ids[id]] = viewName + '-' + id; // add viewName to identifier for later
}
};
module.exports = Helpers; module.exports = Helpers;
/*
* spot check that each language has values for the string id keys on About page
* that are contained in English (i.e. make sure strings will show up, not ids")
*/
var merge = require('lodash.merge');
var path = require('path');
var tap = require('tap');
var languages = require('../../languages.json');
var localeCompare = require('../../bin/lib/locale-compare');
tap.test('spotCheckAboutStrings', function (t) {
var isoCodes = Object.keys(languages);
isoCodes.splice(isoCodes.indexOf('en'), 1);
var viewLocales = {};
var idsWithICU = {};
var icuWithIds = {};
localeCompare.getIdsForView(
'about',
path.resolve(__dirname, '../../src/views/about/l10n.json'),
viewLocales,
idsWithICU,
icuWithIds
);
var md5WithIds = localeCompare.getMD5Map(icuWithIds);
var keysToCheck = Object.keys(merge(viewLocales['about']['en'])).sort();
for (var i in isoCodes) {
var translations = localeCompare.getTranslationsForLanguage(isoCodes[i], idsWithICU, md5WithIds);
t.same(
Object.keys(translations['about'][isoCodes[i]]).sort(),
keysToCheck,
'check About keys for language ' + isoCodes[i]
);
}
t.end();
});
/*
* spot check that each language has values for the string id keys used generally in the site
* that are contained in English (i.e. make sure strings will show up, not ids")
*/
var merge = require('lodash.merge');
var path = require('path');
var tap = require('tap');
var languages = require('../../languages.json');
var localeCompare = require('../../bin/lib/locale-compare');
tap.test('spotCheckAboutStrings', function (t) {
var isoCodes = Object.keys(languages);
isoCodes.splice(isoCodes.indexOf('en'), 1);
var viewLocales = {};
var idsWithICU = {};
var icuWithIds = {};
localeCompare.getIdsForView(
'general',
path.resolve(__dirname, '../../src/l10n.json'),
viewLocales,
idsWithICU,
icuWithIds
);
var md5WithIds = localeCompare.getMD5Map(icuWithIds);
var keysToCheck = Object.keys(merge(viewLocales['general']['en'])).sort();
for (var i in isoCodes) {
var translations = localeCompare.getTranslationsForLanguage(isoCodes[i], idsWithICU, md5WithIds);
t.same(
Object.keys(translations['general'][isoCodes[i]]).sort(),
keysToCheck,
'check About keys for language ' + isoCodes[i]
);
}
t.end();
});
/*
* spot checks the translation of the nav bar for a select set
* of languages that cover a number of types of translations.
*
* Languages checked:
* - Hebrew
* - Edible Scratch (fake language)
* - Mandarin
* - Japanese
* - Brasilian Portuguese
* - Polish
* - Norwegian
* - German
*/
var path = require('path');
var tap = require('tap');
var localeCompare = require('../../bin/lib/locale-compare');
var viewLocales = {};
var idsWithICU = {};
var icuWithIds = {};
var languagesToCheck = [
'he', 'zh-cn', 'ja', 'pt-br', 'pl', 'nb'
];
var idsToCheck = [
'general.about', 'general.create', 'general.help', 'general.joinScratch',
'general.signIn', 'general.discuss'
];
// Test nav for real languages.
localeCompare.getIdsForView(
'general',
path.resolve(__dirname, '../../src/l10n.json'),
viewLocales,
idsWithICU,
icuWithIds
);
var md5WithIds = localeCompare.getMD5Map(icuWithIds);
tap.test('spotCheckNavBar', function (t) {
for (var i in languagesToCheck) {
var translations = localeCompare.getTranslationsForLanguage(languagesToCheck[i], idsWithICU, md5WithIds);
for (var j in idsToCheck) {
t.notEqual(
translations['general'][languagesToCheck[i]][idsToCheck[j]],
viewLocales['general']['en'][idsToCheck[j]],
'check localization of ' + idsToCheck[j] + ' for ' + languagesToCheck[i]
);
}
}
t.end();
});
// Test splash items for fake language.
var fakeLanguageIdsToCheck = ['news.scratchNews', 'splash.featuredProjects', 'splash.featuredStudios'];
localeCompare.getIdsForView(
'splash',
path.resolve(__dirname, '../../src/views/splash/l10n.json'),
viewLocales,
idsWithICU,
icuWithIds
);
md5WithIds = localeCompare.getMD5Map(icuWithIds);
tap.test('spotCheckNavBarFakeLanguage', function (t) {
var translations = localeCompare.getTranslationsForLanguage('yum', idsWithICU, md5WithIds);
for (var i in fakeLanguageIdsToCheck) {
t.notEqual(
translations['general']['yum'][fakeLanguageIdsToCheck[i]],
viewLocales['splash']['en'][fakeLanguageIdsToCheck[i]],
'check localization of ' + fakeLanguageIdsToCheck[i] + ' for yum'
);
}
t.end();
});
/*
* spot check that each language has values for the string id keys on Splash page
* that are contained in English (i.e. make sure strings will show up, not ids")
*/
var merge = require('lodash.merge');
var path = require('path');
var tap = require('tap');
var languages = require('../../languages.json');
var localeCompare = require('../../bin/lib/locale-compare');
tap.test('spotCheckAboutStrings', function (t) {
var isoCodes = Object.keys(languages);
isoCodes.splice(isoCodes.indexOf('en'), 1);
var viewLocales = {};
var idsWithICU = {};
var icuWithIds = {};
localeCompare.getIdsForView(
'splash',
path.resolve(__dirname, '../../src/views/splash/l10n.json'),
viewLocales,
idsWithICU,
icuWithIds
);
var md5WithIds = localeCompare.getMD5Map(icuWithIds);
var keysToCheck = Object.keys(merge(viewLocales['splash']['en'])).sort();
for (var i in isoCodes) {
var translations = localeCompare.getTranslationsForLanguage(isoCodes[i], idsWithICU, md5WithIds);
t.same(
Object.keys(translations['splash'][isoCodes[i]]).sort(),
keysToCheck,
'check Splash keys for language ' + isoCodes[i]
);
}
t.end();
});
/*
* spot check that each language has values for the string id keys on Wedo2 page
* that are contained in English (i.e. make sure strings will show up, not ids")
*/
var merge = require('lodash.merge');
var path = require('path');
var tap = require('tap');
var languages = require('../../languages.json');
var localeCompare = require('../../bin/lib/locale-compare');
tap.test('spotCheckAboutStrings', function (t) {
var isoCodes = Object.keys(languages);
isoCodes.splice(isoCodes.indexOf('en'), 1);
var viewLocales = {};
var idsWithICU = {};
var icuWithIds = {};
localeCompare.getIdsForView(
'wedo2',
path.resolve(__dirname, '../../src/views/wedo2/l10n.json'),
viewLocales,
idsWithICU,
icuWithIds
);
var md5WithIds = localeCompare.getMD5Map(icuWithIds);
var keysToCheck = Object.keys(merge(viewLocales['wedo2']['en'])).sort();
for (var i in isoCodes) {
var translations = localeCompare.getTranslationsForLanguage(isoCodes[i], idsWithICU, md5WithIds);
t.same(
Object.keys(translations['wedo2'][isoCodes[i]]).sort(),
keysToCheck,
'check About keys for language ' + isoCodes[i]
);
}
t.end();
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment