Finish translation system. #191

This commit is contained in:
Michael Teeuw 2016-05-11 12:38:41 +02:00
parent 32c9f197b1
commit 7a067a0f6e
20 changed files with 393 additions and 100 deletions

View File

@ -33,6 +33,7 @@
<script type="text/javascript" src="vendor/vendor.js"></script>
<script type="text/javascript" src="modules/default/defaultmodules.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
<script type="text/javascript" src="js/class.js"></script>
<script type="text/javascript" src="js/module.js"></script>

View File

@ -35,7 +35,7 @@ var Loader = (function() {
// This is done after all the moduels so we can
// overwrite all the defined styls.
loadFile('css/custom.css', function() {
loadFile("css/custom.css", function() {
// custom.css loaded. Start all modules.
startModules();
});

View File

@ -331,6 +331,7 @@ var MM = (function() {
init: function() {
Log.info("Initializing MagicMirror.");
loadConfig();
Translator.loadCoreTranslations(config.language);
Loader.loadModules();
},

View File

@ -58,7 +58,7 @@ var Module = Class.extend({
* return Map<String, String> - A map with langKeys and filenames.
*/
getTranslations: function() {
return {};
return false;
},
/* getDom()
@ -221,9 +221,25 @@ var Module = Class.extend({
loadTranslations: function(callback) {
var self = this;
var translations = this.getTranslations();
var translationFile = translations && (translations[config.language.toLowerCase()] || translations.en) || undefined;
if(translationFile) {
Translator.load(this, translationFile, callback);
var lang = config.language.toLowerCase();
// The variable `first` will contain the first
// defined translation after the following line.
for (var first in translations) {break;}
if (translations) {
var translationFile = translations[lang] || undefined;
var translationsFallbackFile = translations[first];
// If a translation file is set, load it and then also load the fallback translation file.
// Otherwise only load the fallback translation file.
if (translationFile !== undefined) {
Translator.load(self, translationFile, false, function() {
Translator.load(self, translationsFallbackFile, true, callback);
});
} else {
Translator.load(self, translationsFallbackFile, true, callback);
}
} else {
callback();
}

View File

@ -21,6 +21,7 @@ var Server = function(config, callback) {
app.use("/fonts", express.static(path.resolve(__dirname + "/../fonts")));
app.use("/modules", express.static(path.resolve(__dirname + "/../modules")));
app.use("/vendor", express.static(path.resolve(__dirname + "/../vendor")));
app.use("/translations", express.static(path.resolve(__dirname + "/../translations")));
app.get("/", function(req, res) {
res.sendFile(path.resolve(__dirname + "/../index.html"));

View File

@ -6,8 +6,111 @@
* MIT Licensed.
*/
var Translator = (function() {
/* loadJSON(file, callback)
* Load a JSON file via XHR.
*
* argument file string - Path of the file we want to load.
* argument callback function - Function called when done.
*/
function loadJSON(file, callback) {
var xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/json");
xhr.open("GET", file, true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == "200") {
callback(JSON.parse(stripComments(xhr.responseText)));
}
};
xhr.send(null);
}
/* loadJSON(str, options)
* Remove any commenting from a json file so it can be parsed.
*
* argument str string - The string that contains json with comments.
* argument opts function - Strip options.
*
* return the stripped string.
*/
function stripComments(str, opts) {
// strip comments copied from: https://github.com/sindresorhus/strip-json-comments
var singleComment = 1;
var multiComment = 2;
function stripWithoutWhitespace() {
return "";
}
function stripWithWhitespace(str, start, end) {
return str.slice(start, end).replace(/\S/g, " ");
}
opts = opts || {};
var currentChar;
var nextChar;
var insideString = false;
var insideComment = false;
var offset = 0;
var ret = "";
var strip = opts.whitespace === false ? stripWithoutWhitespace : stripWithWhitespace;
for (var i = 0; i < str.length; i++) {
currentChar = str[i];
nextChar = str[i + 1];
if (!insideComment && currentChar === "\"") {
var escaped = str[i - 1] === "\\" && str[i - 2] !== "\\";
if (!escaped) {
insideString = !insideString;
}
}
if (insideString) {
continue;
}
if (!insideComment && currentChar + nextChar === "//") {
ret += str.slice(offset, i);
offset = i;
insideComment = singleComment;
i++;
} else if (insideComment === singleComment && currentChar + nextChar === "\r\n") {
i++;
insideComment = false;
ret += strip(str, offset, i);
offset = i;
continue;
} else if (insideComment === singleComment && currentChar === "\n") {
insideComment = false;
ret += strip(str, offset, i);
offset = i;
} else if (!insideComment && currentChar + nextChar === "/*") {
ret += str.slice(offset, i);
offset = i;
insideComment = multiComment;
i++;
continue;
} else if (insideComment === multiComment && currentChar + nextChar === "*/") {
i++;
insideComment = false;
ret += strip(str, offset, i + 1);
offset = i + 1;
continue;
}
}
return ret + (insideComment ? strip(str.substr(offset)) : str.substr(offset));
}
return {
coreTranslations: {},
coreTranslationsFallback: {},
translations: {},
translationsFallback: {},
/* translate(module, key)
* Load a translation for a given key for a given module.
*
@ -15,10 +118,28 @@ var Translator = (function() {
* argument key string - The key of the text to translate.
*/
translate: function(module, key) {
if(this.translations[module.name]) {
if(this.translations[module.name] && key in this.translations[module.name]) {
// Log.log("Got translation for " + key + " from module translation: ");
return this.translations[module.name][key];
}
return undefined;
if (key in this.coreTranslations) {
// Log.log("Got translation for " + key + " from core translation.");
return this.coreTranslations[key];
}
if (this.translationsFallback[module.name] && key in this.translationsFallback[module.name]) {
// Log.log("Got translation for " + key + " from module translation fallback.");
return this.translationsFallback[module.name][key];
}
if (key in this.coreTranslationsFallback) {
// Log.log("Got translation for " + key + " from core translation fallback.");
return this.coreTranslationsFallback[key];
}
return key;
},
/* load(module, file, callback)
* Load a translation file (json) and remember the data.
@ -27,33 +148,63 @@ var Translator = (function() {
* argument file string - Path of the file we want to load.
* argument callback function - Function called when done.
*/
load: function(module, file, callback) {
load: function(module, file, isFallback, callback) {
if (!isFallback) {
Log.log(module.name + " - Load translation: " + file);
} else {
Log.log(module.name + " - Load translation fallback: " + file);
}
var self = this;
if(!this.translations[module.name]) {
this._loadJSON(module.file(file), function(json) {
self.translations[module.name] = json;
if(!this.translationsFallback[module.name]) {
loadJSON(module.file(file), function(json) {
if (!isFallback) {
self.translations[module.name] = json;
} else {
self.translationsFallback[module.name] = json;
}
callback();
});
} else {
callback();
}
},
/* _loadJSON(file, callback)
* Load a JSON file via XHR.
/* loadCoreTranslations(lang)
* Load the core translations.
*
* argument file string - Path of the file we want to load.
* argument callback function - Function called when done.
* argument lang String - The language identifier of the core language.
*/
_loadJSON: function(file, callback) {
var xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/json");
xhr.open("GET", file, true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == "200") {
callback(JSON.parse(xhr.responseText));
}
};
xhr.send(null);
}
loadCoreTranslations: function(lang) {
var self = this;
if (lang in translations) {
Log.log("Loading core translation file: " + translations[lang]);
loadJSON(translations[lang], function(translations) {
self.coreTranslations = translations;
});
} else {
Log.log("Configured language not found in core translations.");
}
self.loadCoreTranslationsFallback();
},
/* loadCoreTranslationsFallback()
* Load the core translations fallback.
* The first language defined in translations.js will be used.
*/
loadCoreTranslationsFallback: function() {
var self = this;
// The variable `first` will contain the first
// defined translation after the following line.
for (var first in translations) {break;}
Log.log("Loading core translation fallback file: " + translations[first]);
loadJSON(translations[first], function(translations) {
self.coreTranslationsFallback = translations;
});
},
};
})();

View File

@ -138,6 +138,8 @@ getStyles: function() {
The getTranslations method is called to request translation files that need to be loaded. This method should therefore return a dictionary with the files to load, identified by the country's short name.
If the module does not have any module specific translations, the function can just be omitted or return `false`.
**Example:**
````javascript
getTranslations: function() {
@ -282,6 +284,13 @@ To show a module, you can call the `show(speed, callback)` method. You can call
The Magic Mirror contains a convenience wrapper for `l18n`. You can use this to automatically serve different translations for your modules based on the user's `language` configuration.
If no translation is found, a fallback will be used. The fallback sequence is as follows:
1. Translation as defined in module translation file of the user's preferred language.
2. Translation as defined in core translation file of the user's preferred language.
3. Translation as defined in module translation file of the fallback language (the first defined module translation file).
4. Translation as defined in core translation file of the fallback language (the first defined core translation file).
5. The key (identifier) of the translation.
**Example:**
````javascript
this.translate("INFO") //Will return a translated string for the identifier INFO
@ -294,7 +303,7 @@ this.translate("INFO") //Will return a translated string for the identifier INFO
}
````
**Note:** Currently there is no fallback if a translation identifier does not exist in one language. Right now you always have to add all identifier to all your translations even if they are not translated yet (see [#191](https://github.com/MichMich/MagicMirror/issues/191)).
**Note:** although comments are officially not supported in JSON files, MagicMirror allows it by stripping the comments before parsing the JSON file. Comments in translation files could help other translators.
## The Node Helper: node_helper.js

View File

@ -47,12 +47,10 @@ Module.register("calendar",{
// Define required translations.
getTranslations: function() {
return {
en: "translations/en.json",
de: "translations/de.json",
nl: "translations/nl.json",
fr: "translations/fr.json"
};
// The translations for the defaut modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionairy.
// If you're trying to build yiur own module including translations, check out the documentation.
return false;
},
// Override start method.
@ -356,4 +354,3 @@ Module.register("calendar",{
return title;
}
});

View File

@ -1,7 +0,0 @@
{
"TODAY": "Heute"
, "TOMORROW": "Morgen"
, "RUNNING": "noch"
, "LOADING": "Lade Termine &hellip;"
, "EMPTY": "Keine Termine."
}

View File

@ -1,7 +0,0 @@
{
"TODAY": "Today"
, "TOMORROW": "Tomorrow"
, "RUNNING": "Ends in"
, "LOADING": "Loading events &hellip;"
, "EMPTY": "No upcoming events."
}

View File

@ -1,7 +0,0 @@
{
"TODAY": "Aujourd'hui"
, "TOMORROW": "Demain"
, "RUNNING": "Se termine dans"
, "LOADING": "Chargement des RDV &hellip;"
, "EMPTY": "Aucun RDV."
}

View File

@ -1,7 +0,0 @@
{
"TODAY": "Vandaag"
, "TOMORROW": "Morgen"
, "RUNNING": "Eindigd over"
, "LOADING": "Bezig met laden &hellip;"
, "EMPTY": "Geen geplande afspraken."
}

View File

@ -62,6 +62,14 @@ Module.register("currentweather",{
return ["weather-icons.css", "currentweather.css"];
},
// Define required translations.
getTranslations: function() {
// The translations for the defaut modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionairy.
// If you're trying to build yiur own module including translations, check out the documentation.
return false;
},
// Define start sequence.
start: function() {
Log.info("Starting module: " + this.name);
@ -100,7 +108,7 @@ Module.register("currentweather",{
}
if (!this.loaded) {
wrapper.innerHTML = "Loading weather ...";
wrapper.innerHTML = this.translate('LOADING');
wrapper.className = "dimmed light small";
return wrapper;
}
@ -118,7 +126,7 @@ Module.register("currentweather",{
if (this.config.showWindDirection) {
var windDirection = document.createElement("span");
windDirection.innerHTML = " " + this.windDirection;
windDirection.innerHTML = " " + this.translate(this.windDirection);
small.appendChild(windDirection);
}
var spacer = document.createElement("span");

View File

@ -31,6 +31,14 @@ Module.register("newsfeed",{
return ["moment.js"];
},
// Define required translations.
getTranslations: function() {
// The translations for the defaut modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionairy.
// If you're trying to build yiur own module including translations, check out the documentation.
return false;
},
// Define start sequence.
start: function() {
Log.info("Starting module: " + this.name);
@ -100,7 +108,7 @@ Module.register("newsfeed",{
}
} else {
wrapper.innerHTML = "Loading news ...";
wrapper.innerHTML = this.translate("LOADING");
wrapper.className = "small dimmed";
}

View File

@ -61,6 +61,14 @@ Module.register("weatherforecast",{
return ["weather-icons.css", "weatherforecast.css"];
},
// Define required translations.
getTranslations: function() {
// The translations for the defaut modules are defined in the core translation files.
// Therefor we can just return false. Otherwise we should have returned a dictionairy.
// If you're trying to build yiur own module including translations, check out the documentation.
return false;
},
// Define start sequence.
start: function() {
Log.info("Starting module: " + this.name);
@ -93,7 +101,7 @@ Module.register("weatherforecast",{
}
if (!this.loaded) {
wrapper.innerHTML = "Loading weather ...";
wrapper.innerHTML = this.translate('LOADING');
wrapper.className = "dimmed light small";
return wrapper;
}

27
translations/de.json Normal file
View File

@ -0,0 +1,27 @@
{
/* GENERAL */
"LOADING": "Lade &hellip;",
/* CALENDAR */
"TODAY": "Heute",
"TOMORROW": "Morgen",
"RUNNING": "noch",
"EMPTY": "Keine Termine.",
/* WEATHER */
"N": "N",
"NNE": "NNO",
"ENE": "ONO",
"E": "O",
"ESE": "OSO",
"SE": "SO",
"SSE": "SSO",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
}

27
translations/en.json Normal file
View File

@ -0,0 +1,27 @@
{
/* GENERAL */
"LOADING": "Loading &hellip;",
/* CALENDAR */
"TODAY": "Today",
"TOMORROW": "Tomorrow",
"RUNNING": "Ends in",
"EMPTY": "No upcoming events.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSW",
"SW": "SW",
"WSW": "WSW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
}

27
translations/fr.json Normal file
View File

@ -0,0 +1,27 @@
{
/* GENERAL */
"LOADING": "Chargement &hellip;",
/* CALENDAR */
"TODAY": "Aujourd'hui",
"TOMORROW": "Demain",
"RUNNING": "Se termine dans",
"EMPTY": "Aucun RDV.",
/* WEATHER */
"N": "N",
"NNE": "NNE",
"ENE": "ENE",
"E": "E",
"ESE": "ESE",
"SE": "SE",
"SSE": "SSE",
"S": "S",
"SSW": "SSO",
"SW": "SO",
"WSW": "OSO",
"W": "O",
"WNW": "ONO",
"NW": "NO",
"NNW": "NNO"
}

27
translations/nl.json Normal file
View File

@ -0,0 +1,27 @@
{
/* GENERAL */
"LOADING": "Bezig met laden &hellip;",
/* CALENDAR */
"TODAY": "Vandaag",
"TOMORROW": "Morgen",
"RUNNING": "Eindigd over",
"EMPTY": "Geen geplande afspraken.",
/* WEATHER */
"N": "N",
"NNE": "NNO",
"ENE": "ONO",
"E": "O",
"ESE": "OZO",
"SE": "ZO",
"SSE": "ZZO",
"S": "Z",
"SSW": "ZZW",
"SW": "ZW",
"WSW": "WZW",
"W": "W",
"WNW": "WNW",
"NW": "NW",
"NNW": "NNW"
}

View File

@ -0,0 +1,13 @@
/* Magic Mirror
* Translation Definition
*
* By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*/
var translations = {
"en" : "translations/en.json",
"nl" : "translations/nl.json",
"de" : "translations/de.json",
"fr" : "translations/fr.json",
};