Reimplement UI translation JS code

Add a new Translator class that handles the loading of
translation data and provides a simpler translate() API.

Also refactor l10n.js to export a single function for translating
static text, splitting it up into separate function calls for
each different area of the UI with strings to translate.
This commit is contained in:
Oliver Hamlet
2016-01-06 18:26:54 +00:00
parent e31c97b3e9
commit ce1b53d5d5
10 changed files with 490 additions and 318 deletions
+2 -1
View File
@@ -315,9 +315,10 @@
<script src="../../../bower_components/marked/lib/marked.js"></script>
<script src="../../../bower_components/Jed/jed.js"></script>
<script src="../../../bower_components/jed-gettext-parser/jedGettextParser.js"></script>
<script src="js/l10n.js"></script>
<script src="js/helpers.js"></script>
<script src="js/loot.js"></script>
<script src="js/translator.js"></script>
<script src="js/l10n.js"></script>
<script src="js/plugin.js"></script>
<script src="js/filters.js"></script>
<script src="js/events.js"></script>
+16 -16
View File
@@ -53,7 +53,7 @@ function onChangeGame(evt) {
}
/* Send off a CEF query with the folder name of the new game. */
showProgress(l10n.jed.translate('Loading game data...').fetch());
showProgress(loot.l10n.translate('Loading game data...'));
var request = JSON.stringify({
name: 'changeGame',
args: [
@@ -123,14 +123,14 @@ function updateMasterlistNoProgress() {
/* Hack to stop cards overlapping. */
document.getElementById('main').lastElementChild.updateSize();
toast(l10n.jed.translate('Masterlist updated to revision %s.').fetch(loot.game.masterlist.revision));
toast(loot.l10n.translate('Masterlist updated to revision %s.', loot.game.masterlist.revision));
} else {
toast(l10n.jed.translate('No masterlist update was necessary.').fetch());
toast(loot.l10n.translate('No masterlist update was necessary.'));
}
}).catch(processCefError);
}
function onUpdateMasterlist(evt) {
showProgress(l10n.jed.translate('Updating masterlist...').fetch());
showProgress(loot.l10n.translate('Updating masterlist...'));
updateMasterlistNoProgress().then(function(result){
closeProgressDialog();
}).catch(processCefError);
@@ -154,7 +154,7 @@ function onSortPlugins(evt) {
promise = promise.then(updateMasterlistNoProgress());
}
promise.then(function(){
showProgress(l10n.jed.translate('Sorting plugins...').fetch());
showProgress(loot.l10n.translate('Sorting plugins...'));
loot.query('sortPlugins').then(JSON.parse).then(function(result){
if (result) {
loot.game.oldLoadOrder = loot.game.plugins;
@@ -256,7 +256,7 @@ function onRedatePlugins(evt) {
return;
}
showMessageDialog(l10n.jed.translate('Redate Plugins?').fetch(), l10n.jed.translate('This feature is provided so that modders using the Creation Kit may set the load order it uses. A side-effect is that any subscribed Steam Workshop mods will be re-downloaded by Steam. Do you wish to continue?').fetch(), l10n.jed.translate('Redate').fetch(), function(result){
showMessageDialog(loot.l10n.translate('Redate Plugins?'), loot.l10n.translate('This feature is provided so that modders using the Creation Kit may set the load order it uses. A side-effect is that any subscribed Steam Workshop mods will be re-downloaded by Steam. Do you wish to continue?'), loot.l10n.translate('Redate'), function(result){
if (result) {
loot.query('redatePlugins').then(function(response){
toast('Plugins were successfully redated.');
@@ -265,7 +265,7 @@ function onRedatePlugins(evt) {
});
}
function onClearAllMetadata(evt) {
showMessageDialog('', l10n.jed.translate('Are you sure you want to clear all existing user-added metadata from all plugins?').fetch(), l10n.jed.translate('Clear').fetch(), function(result){
showMessageDialog('', loot.l10n.translate('Are you sure you want to clear all existing user-added metadata from all plugins?'), loot.l10n.translate('Clear'), function(result){
if (result) {
loot.query('clearAllMetadata').then(JSON.parse).then(function(result){
if (result) {
@@ -287,7 +287,7 @@ function onClearAllMetadata(evt) {
}
});
toast(l10n.jed.translate('All user-added metadata has been cleared.').fetch());
toast(loot.l10n.translate('All user-added metadata has been cleared.'));
}
}).catch(processCefError);
}
@@ -344,7 +344,7 @@ function onCopyContent(evt) {
});
loot.query(request).then(function(){
toast(l10n.jed.translate("LOOT's content has been copied to the clipboard.").fetch());
toast(loot.l10n.translate("LOOT's content has been copied to the clipboard."));
}).catch(processCefError);
}
function onCopyLoadOrder(evt) {
@@ -366,7 +366,7 @@ function onCopyLoadOrder(evt) {
});
loot.query(request).then(function(){
toast(l10n.jed.translate("The load order has been copied to the clipboard.").fetch());
toast(loot.l10n.translate("The load order has been copied to the clipboard."));
}).catch(processCefError);
}
function onSwitchSidebarTab(evt) {
@@ -577,11 +577,11 @@ function onCopyMetadata(evt) {
});
loot.query(request).then(function(){
toast(l10n.jed.translate('The metadata for "%s" has been copied to the clipboard.').fetch(evt.target.getName()));
toast(loot.l10n.translate('The metadata for "%s" has been copied to the clipboard.', evt.target.getName()));
}).catch(processCefError);
}
function onClearMetadata(evt) {
showMessageDialog('', l10n.jed.translate('Are you sure you want to clear all existing user-added metadata from "%s"?').fetch(evt.target.getName()), l10n.jed.translate('Clear').fetch(), function(result){
showMessageDialog('', loot.l10n.translate('Are you sure you want to clear all existing user-added metadata from "%s"?', evt.target.getName()), loot.l10n.translate('Clear'), function(result){
if (result) {
var request = JSON.stringify({
name: 'clearPluginMetadata',
@@ -607,7 +607,7 @@ function onClearMetadata(evt) {
break;
}
}
toast(l10n.jed.translate('The user-added metadata for "%s" has been cleared.').fetch(evt.target.getName()));
toast(loot.l10n.translate('The user-added metadata for "%s" has been cleared.', evt.target.getName()));
/* Now perform search again. If there is no current search, this won't
do anything. */
document.getElementById('searchBar').search();
@@ -630,9 +630,9 @@ function onSidebarClick(evt) {
}
function onQuit(evt) {
if (!document.getElementById('applySortButton').classList.contains('hidden')) {
handleUnappliedChangesClose(l10n.jed.translate('sorted load order').fetch());
handleUnappliedChangesClose(loot.l10n.translate('sorted load order'));
} else if (document.body.hasAttribute('data-editors')) {
handleUnappliedChangesClose(l10n.jed.translate('metadata edits').fetch());
handleUnappliedChangesClose(loot.l10n.translate('metadata edits'));
} else {
window.close();
}
@@ -643,7 +643,7 @@ function onJumpToGeneralInfo(evt) {
}
function onContentRefresh(evt) {
/* Send a query for updated load order and plugin header info. */
showProgress(l10n.jed.translate('Refreshing data...').fetch());
showProgress(loot.l10n.translate('Refreshing data...'));
loot.query('getGameData').then(function(result){
/* Parse the data sent from C++. */
try {
+2 -2
View File
@@ -118,7 +118,7 @@ var filters = {
doNotCleanFilter: function(message) {
if (document.getElementById('hideDoNotCleanMessages').checked) {
return message.content[0].str.indexOf(l10n.jed.translate("Do not clean").fetch()) == -1;
return message.content[0].str.indexOf(loot.l10n.translate("Do not clean")) == -1;
} else {
return true;
}
@@ -157,7 +157,7 @@ function getConflictingPluginsFromFilter() {
]
});
showProgress(l10n.jed.translate('Checking if plugins have been loaded...').fetch());
showProgress(loot.l10n.translate('Checking if plugins have been loaded...'));
return loot.query(request).then(JSON.parse).then(function(result){
if (result) {
+4 -5
View File
@@ -4,7 +4,7 @@ function processCefError(err) {
promise errors, not just CEF errors. */
console.log(err.stack);
closeProgressDialog();
showMessageBox(l10n.jed.translate('Error').fetch(), err.message);
showMessageBox(loot.l10n.translate('Error'), err.message);
}
function showElement(element) {
@@ -24,13 +24,13 @@ function toast(text) {
}
function showMessageDialog(title, text, positiveText, closeCallback) {
var dialog = document.createElement('loot-message-dialog');
dialog.setButtonText(positiveText, l10n.jed.translate('Cancel').fetch());
dialog.setButtonText(positiveText, loot.l10n.translate('Cancel'));
dialog.showModal(title, text, closeCallback);
document.body.appendChild(dialog);
}
function showMessageBox(title, text) {
var dialog = document.createElement('loot-message-dialog');
dialog.setButtonText(l10n.jed.translate('OK').fetch());
dialog.setButtonText(loot.l10n.translate('OK'));
dialog.showModal(title, text);
document.body.appendChild(dialog);
}
@@ -51,7 +51,7 @@ function closeProgressDialog() {
}
}
function handleUnappliedChangesClose(change) {
showMessageDialog('', l10n.jed.translate('You have not yet applied or cancelled your %s. Are you sure you want to quit?').fetch(change), l10n.jed.translate('Quit').fetch(), function(result){
showMessageDialog('', loot.l10n.translate('You have not yet applied or cancelled your %s. Are you sure you want to quit?', change), loot.l10n.translate('Quit'), function(result){
if (result) {
/* Cancel any sorting and close any editors. Cheat by sending a
cancelSort query for as many times as necessary. */
@@ -70,4 +70,3 @@ function handleUnappliedChangesClose(change) {
}
});
}
+7 -7
View File
@@ -35,7 +35,7 @@ function initVars() {
if (loot.version.length > pos + 1) {
document.getElementById('LOOTBuild').textContent = loot.version.substring(pos + 1);
} else {
document.getElementById('LOOTBuild').textContent = l10n.jed.translate('unknown').fetch();
document.getElementById('LOOTBuild').textContent = loot.l10n.translate('unknown');
}
loot.version = loot.version.substring(0, pos);
@@ -139,12 +139,12 @@ function initVars() {
console.log('getSettings response: ' + results[2]);
}
}).then(function(){
return l10n.getJedInstance(loot.settings.language).then(function(jed){
l10n.translateStaticText(jed);
l10n.jed = jed;
/* Also need to update the settings UI. */
loot.updateSettingsUI();
/* Translate static text. */
loot.l10n = new loot.Translator(loot.settings.language);
loot.l10n.load().then(() => {
loot.translateStaticText(loot.l10n);
/* Also need to update the settings UI. */
loot.updateSettingsUI();
}).catch(processCefError);
}).then(function(){
if (result) {
+282 -285
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -180,12 +180,12 @@ var loot = {
if (change.object[change.name] && change.object[change.name].revision) {
document.getElementById('masterlistRevision').textContent = change.object[change.name].revision;
} else {
document.getElementById('masterlistRevision').textContent = l10n.jed.translate("N/A").fetch();
document.getElementById('masterlistRevision').textContent = loot.l10n.translate("N/A");
}
if (change.object[change.name] && change.object[change.name].date) {
document.getElementById('masterlistDate').textContent = change.object[change.name].date;
} else {
document.getElementById('masterlistDate').textContent = l10n.jed.translate("N/A").fetch();
document.getElementById('masterlistDate').textContent = loot.l10n.translate("N/A");
}
} else if (change.name == 'globalMessages') {
/* For the messages, they don't have a JS 'class' so need to everything
+74
View File
@@ -0,0 +1,74 @@
'use strict';
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['bower_components/Jed/jed', 'bower_components/jed-gettext-parser/jedGettextParser'], factory);
} else {
// Browser globals
root.loot = root.loot || {};
root.loot.Translator = factory(root.Jed, root.jedGettextParser);
}
}(this, (Jed, jedGettextParser) => {
return class Translator {
/* Returns a Promise */
constructor(locale) {
this.locale = locale || 'en';
this.jed = undefined;
}
load() {
const defaultTranslationData = {
'messages': {
'': {
'domain': 'messages',
'lang': 'en',
'plural_forms': 'nplurals=2; plural=(n != 1);',
},
},
};
let translationDataPromise;
if (this.locale === 'en') {
/* Just resolve to an empty data set. */
translationDataPromise = Promise.resolve(defaultTranslationData);
} else {
translationDataPromise = new Promise((resolve, reject) => {
const url = 'loot://l10n/' + this.locale + '/LC_MESSAGES/loot.mo';
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('readystatechange', (evt) => {
if (evt.target.readyState === 4) {
/* Status is 0 for local file URL loading. */
if (evt.target.status >= 200 && evt.target.status < 400) {
resolve(jedGettextParser.mo.parse(evt.target.response));
} else {
reject(new Error(evt.target.statusText));
}
}
}, false);
xhr.send();
});
}
return translationDataPromise.catch((error) => {
console.log('Error loading translation data: ' + error.message);
return defaultTranslationData;
}).then((result) => {
this.jed = new Jed({
'locale_data': result,
'domain': 'messages',
});
});
}
translate(text, ...substitutions) {
if (text === undefined) {
return '';
}
const func = this.jed.translate(text);
return func.fetch.apply(func, substitutions);
}
};
}));
+5
View File
@@ -7,9 +7,14 @@
<script src="../../../../../node_modules/should/should.js"></script>
<script src="../../../../../node_modules/mocha/mocha.js"></script>
<script src="../../../../../bower_components/Jed/jed.js"></script>
<script src="../../../../../bower_components/jed-gettext-parser/jedGettextParser.js"></script>
<script>mocha.setup('bdd')</script>
<script src="../../../../gui/html/js/plugin.js"></script>
<script src="../../../../gui/html/js/translator.js"></script>
<script src="test_plugin.js"></script>
<script src="test_translator.js"></script>
<script>
onload = function(){
mocha.checkLeaks();
+96
View File
@@ -0,0 +1,96 @@
'use strict';
describe('Translator', () => {
describe('#Tranlsator()', () => {
it('should set locale to "en" if no locale is given', () => {
const l10n = new loot.Translator();
l10n.locale.should.equal('en');
});
it('should set the locale to the given value', () => {
const l10n = new loot.Translator('de');
l10n.locale.should.equal('de');
});
});
describe('#load()', () => {
it('should return a Promise', () => {
const l10n = new loot.Translator();
l10n.load().should.be.a.Promise();
});
it('should be fulfilled for a locale of "en"', () => {
const l10n = new loot.Translator('en');
return l10n.load().should.be.fulfilled();
});
/* Cannot test rejection or other locales as the URL uses is invalid in the
browser, causing a CORS error that cannot be handled in JavaScript. */
});
describe('#translate()', () => {
let l10n;
beforeEach(() => {
l10n = new loot.Translator();
});
it('should throw if the translator has not been loaded', () => {
(() => { l10n.translate('foo'); }).should.throw();
});
it('should return an empty string if nothing is passed', () => {
return l10n.load().then(() => {
l10n.translate().should.equal('');
});
});
it('should return the input string if the current locale is "en"', () => {
return l10n.load().then(() => {
l10n.translate('foo').should.equal('foo');
});
});
it('should return the translated string if locale data has been loaded', () => {
/* Since loading data doesn't work in the browser, hack it by setting some
data manually. */
l10n.jed = new window.Jed({
'locale_data': {
'messages': {
'': {
'domain': 'messages',
'lang': 'en',
'plural_forms': 'nplurals=2; plural=(n != 1);',
},
'foo': ['bar'],
},
},
'domain': 'messages',
});
l10n.translate('foo').should.equal('bar');
});
it('should subsitute additional arguments into string', () => {
/* Since loading data doesn't work in the browser, hack it by setting some
data manually. */
l10n.jed = new window.Jed({
'locale_data': {
'messages': {
'': {
'domain': 'messages',
'lang': 'en',
'plural_forms': 'nplurals=2; plural=(n != 1);',
},
'foo %1$s %2$s': ['%2$s is bar'],
},
},
'domain': 'messages',
});
l10n.translate('foo %1$s %2$s', 'is not', 'bar').should.equal('bar is bar');
});
});
});