mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 19:53:36 +00:00
Finish translation system. #191
This commit is contained in:
parent
32c9f197b1
commit
7a067a0f6e
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -331,6 +331,7 @@ var MM = (function() {
|
||||
init: function() {
|
||||
Log.info("Initializing MagicMirror.");
|
||||
loadConfig();
|
||||
Translator.loadCoreTranslations(config.language);
|
||||
Loader.loadModules();
|
||||
},
|
||||
|
||||
|
24
js/module.js
24
js/module.js
@ -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();
|
||||
}
|
||||
|
@ -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"));
|
||||
|
193
js/translator.js
193
js/translator.js
@ -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;
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"TODAY": "Heute"
|
||||
, "TOMORROW": "Morgen"
|
||||
, "RUNNING": "noch"
|
||||
, "LOADING": "Lade Termine …"
|
||||
, "EMPTY": "Keine Termine."
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"TODAY": "Today"
|
||||
, "TOMORROW": "Tomorrow"
|
||||
, "RUNNING": "Ends in"
|
||||
, "LOADING": "Loading events …"
|
||||
, "EMPTY": "No upcoming events."
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"TODAY": "Aujourd'hui"
|
||||
, "TOMORROW": "Demain"
|
||||
, "RUNNING": "Se termine dans"
|
||||
, "LOADING": "Chargement des RDV …"
|
||||
, "EMPTY": "Aucun RDV."
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"TODAY": "Vandaag"
|
||||
, "TOMORROW": "Morgen"
|
||||
, "RUNNING": "Eindigd over"
|
||||
, "LOADING": "Bezig met laden …"
|
||||
, "EMPTY": "Geen geplande afspraken."
|
||||
}
|
@ -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");
|
||||
|
@ -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";
|
||||
}
|
||||
|
||||
|
@ -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
27
translations/de.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
/* GENERAL */
|
||||
"LOADING": "Lade …",
|
||||
|
||||
/* 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
27
translations/en.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
/* GENERAL */
|
||||
"LOADING": "Loading …",
|
||||
|
||||
/* 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
27
translations/fr.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
/* GENERAL */
|
||||
"LOADING": "Chargement …",
|
||||
|
||||
/* 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
27
translations/nl.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
/* GENERAL */
|
||||
"LOADING": "Bezig met laden …",
|
||||
|
||||
/* 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"
|
||||
}
|
13
translations/translations.js
Normal file
13
translations/translations.js
Normal 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",
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user