├── .gitignore ├── examples ├── langs │ ├── lang-en.json │ └── lang-et.json ├── jstorage-example.html └── example.html ├── LICENSE ├── README.md └── j18s.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /examples/langs/lang-en.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "Current language: %s": "Current language: %s", 4 | "English": "English", 5 | "Estonian": "Estonian", 6 | "en": "English", 7 | "et": "Estonian" 8 | } 9 | } -------------------------------------------------------------------------------- /examples/langs/lang-et.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "Current language: %s": "Hetke keel: %s", 4 | "Open this page in multiple tabs or windows and if language is changed in one tab, the others should change too.":"Ava see leht mitmes brauseri aknas/tabis ning kui ühes keel muutub, muutub see ka teistes", 5 | "English": "Inglise keel", 6 | "Estonian": "Eesti keel", 7 | "en": "Inglise keel", 8 | "et": "Eesti keel" 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /examples/jstorage-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | j18s translations 6 | 7 | 8 | 9 | 10 | 11 | 42 | 43 | 44 |

45 | English 46 | | 47 | Estonian 48 |

49 |

50 | Current language: English 51 |

52 |

Open this page in multiple tabs or windows and if language is changed in one tab, the others should change too.

53 | 54 | -------------------------------------------------------------------------------- /examples/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | j18s translations 6 | 7 | 8 | 57 | 58 | 59 |

60 | English 61 | | 62 | Estonian 63 |

64 |

65 | Current language: English

66 |

67 | This post has 5 comments 74 |

75 |

76 | Change comment count 77 |

78 |

79 | Generate translation object 80 |

81 | 82 | 88 | 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | j18s 2 | ==== 3 | 4 | Yet another JavaScript i18n library, cross browser and library agnostic - has no dependencies and can be used standalone. 5 | 6 | This module mainly deals with DOM elements that are marked to be translated. If you change the active 7 | language, all DOM elements that are marked for translation are translated automatically, keeping 8 | correct plurals etc. 9 | 10 | See the [demo here](http://tahvel.info/j18s/examples/example.html). 11 | 12 | ## Usage 13 | 14 | Include j18s.js in your page 15 | 16 | 17 | 18 | In your HTML, add `data-j18s` attribute to an element to translate it. 19 | 20 | Translate this 21 | 22 | Create a script to add language data 23 | 24 | 31 | 32 | And finally, change active language (should be done after DOMContentLoaded or 33 | include the script block in the end of the document body) 34 | 35 | 38 | 39 | And *voilà*, "Translate this" has been changed to "Tõlgi seda"! 40 | 41 | The translations are "live" - that means that if you change the active language, 42 | all the elements that are marked as translatable will be retranslated. 43 | 44 | ### Context 45 | 46 | You can use different contexts for the translations. By default, the main context is "default" 47 | but you can use any other if you need to. 48 | 49 | j18s.addLang("et",{ 50 | "default": {}, // default context translations 51 | "other": {} // some other context 52 | }) 53 | 54 | Later, when translating, you can define the context you want to use for given string. 55 | 56 | ### Plurals 57 | 58 | j18s uses gettext compatible plural forms definitions. By default English plural rules are used 59 | (`plural = n !=1`). You can define your own when adding the language data as the optional third parameter. 60 | 61 | j18s.addLang("et", {default:{}}, "nplurals=2; plural=n == 1 ? 0 : 1;"); 62 | 63 | To use plurals, you need to define translations as arrays instead of a string. If plural expression 64 | returns 0, the first element of the array will be used, if it returns 1, the second is used etc. 65 | 66 | j18s.addLang("et", {default:{ 67 | "Menu": ["Menu", "Menus"] 68 | }}); 69 | 70 | ### Text replacment 71 | 72 | j18s supports a simple sprintf like functionality to automatically replace `%s` 73 | and `%1$s` type blocks when translated. 74 | 75 | j18s.addLang("et", {default:{ 76 | "%s comment": ["%s comment", "%s comments"], 77 | "First %s, second %s": "Second %2$s, first %1$s" 78 | }}); 79 | 80 | ## API 81 | 82 | ### Detect language change 83 | 84 | You can register language change handlers with `on("change")` 85 | 86 | j18s.on("change", function(lang){ 87 | console.log("Language changed to: "+lang); 88 | }); 89 | 90 | j18s.setLang("et"); // outputs 'Language changed to: et' 91 | 92 | For example, you could use this feature to lazy load the language data from the server. 93 | 94 | j18s.on("change", function(lang){ 95 | $.getJSON("/languages/"+lang+".json", function(langData){ 96 | j18s.addLang(lang, langData); 97 | }); 98 | }); 99 | 100 | You could also mix this library with [jStorage](http://www.jstorage.info/) to automatically 101 | detect if the language has been changed in another tab/window/frame and change it in the current 102 | window as well. 103 | 104 | 105 | 106 | 107 | 108 | 132 | 133 | See [examples/jstorage-example.html](http://tahvel.info/j18s/examples/jstorage-example.html) for a demo. 134 | 135 | ### Translate a String 136 | 137 | Translate a string with `translate()` 138 | 139 | j18s.translate(text, options) 140 | 141 | Where 142 | 143 | * **text** is the string to translate 144 | * **options** is optional translation options 145 | 146 | Options can use the following properties 147 | 148 | * **plural** is the default plural form to use, if the translation is not found and `pluralCount` != 1 149 | * **pluralCount** is the input for plural expression function to find the correct translation from the plurals array (defaults to 1) 150 | * **context** is the translation context, defaults to "default" 151 | * **useLang** can be used to explicitly use a language for translating, even if the currently active language is something else 152 | * **textArgs** is an array for %s replacements in the translation 153 | 154 | Example 155 | 156 | var translation = j18s.translate("%s comment", { 157 | plural: "%s comments", 158 | pluralCount: 6, 159 | textArgs: [6] 160 | }); 161 | console.log(translation); // "6 comments" 162 | 163 | ### Convert existing DOM element to translatable 164 | 165 | If you want an element to be automatically translated when changing the active language, 166 | then you can set it to be transatable and you can add some defaults for the translation. 167 | 168 | j18s.createTranslationElement(element, options) 169 | 170 | Where 171 | 172 | * **element** is the DOM element that is going to be automatically translated 173 | * **options** is the optional translation options 174 | 175 | Options can use the following properties 176 | 177 | * **text** is the default singular form to use for the translation, if not set, innerHTML value is used 178 | * **plural** is the default plural form to use, if the translation is not found and `pluralCount` != 1 179 | * **pluralCount** is the input for plural expression function to find the correct translation from the plurals array (defaults to 1) 180 | * **context** is the translation context, defaults to "default" 181 | * **useLang** can be used to explicitly use a language for translating, even if the currently active language is something else 182 | * **textArgs** is an array for %s replacements in the translation 183 | 184 | Example 185 | 186 | var elm = document.getElementById("text"); 187 | j18s.createTranslationElement(elm, { 188 | text: "%s comment", 189 | plural: "%s comments", 190 | pluralCount: 6, 191 | textArgs: [6] 192 | }); 193 | console.log(elm.innerHTML); // "6 comments" 194 | 195 | ### Update translation for existing element 196 | 197 | If you want to modify a translation of an existing translatable object, you can do it with `update()` 198 | 199 | j18s.update(element, options) 200 | 201 | Where 202 | 203 | * **element** is the DOM element that you want to update 204 | * **options** is the optional translation options 205 | 206 | Options can use the following properties 207 | 208 | * **text** is the default singular form to use for the translation, if not set, innerHTML value is used 209 | * **plural** is the default plural form to use, if the translation is not found and `pluralCount` != 1 210 | * **pluralCount** is the input for plural expression function to find the correct translation from the plurals array (defaults to 1) 211 | * **context** is the translation context, defaults to "default" 212 | * **useLang** can be used to explicitly use a language for translating, even if the currently active language is something else 213 | * **textArgs** is an array for %s replacements in the translation 214 | 215 | Example 216 | 217 | var elm = document.getElementById("text"); 218 | j18s.update(elm, { 219 | text: "%s comment", 220 | plural: "%s comments", 221 | pluralCount: 6, 222 | textArgs: [6] 223 | }); 224 | console.log(elm.innerHTML); // "6 comments" 225 | 226 | ## Setting the defaults 227 | 228 | You can set default option values (plural information etc.) directly to the HTML 229 | with `data-j18s-*` parameters. 230 | 231 | Any element that is being automatically translated need to have 232 | `data-j18s` attribute set 233 | 234 | ### Singular text 235 | 236 | Singular text can be defined with `data-j18s-text` and if it not defined, `innerHTML` of the element 237 | value will be used instead. 238 | 239 | Menu // 'text' value is 'Menu' 240 | Menu // 'text' value is 'Menüü' 241 | 242 | ### Plural text 243 | 244 | Default plural text can be defined with `data-j18s-plural` and if it not defined, `data-j18s-text` value will be used instead. 245 | 246 | Menu // 'plural' value is 'Menus' 247 | 248 | ### Plural count 249 | 250 | Current plural count for selecting the correct plural form can be set with `data-j18s-plural-count` 251 | 252 | 253 | 254 | ### Fix language 255 | 256 | If you want to fix the language that will be used to translate this element, use `data-j18s-use-lang` 257 | 258 | // always use 'en' 259 | 260 | ### Set context 261 | 262 | If you want to set the context for the used language, use `data-j18s-context` 263 | 264 | // user "other" context 265 | 266 | ### Replacement strings 267 | 268 | If you want to use %s replacements, you can define the replacement strings with `data-j18s-text-args`. Split 269 | multiple strings with semicolons. 270 | 271 | 272 | 273 | Example 274 | 275 | // outputs '4 comments' 276 | 277 | 278 | ## License 279 | 280 | **MIT** -------------------------------------------------------------------------------- /j18s.js: -------------------------------------------------------------------------------- 1 | 2 | var j18s = { 3 | 4 | /** 5 | * Current language identifier 6 | * @private 7 | */ 8 | _curLang: "en", 9 | 10 | /** 11 | * Translation table for different languages 12 | * @private 13 | */ 14 | _translations: {}, 15 | 16 | /** 17 | * Event listeners 18 | * @private 19 | */ 20 | _eventListeners: {}, 21 | 22 | // PUBLIC API 23 | 24 | /** 25 | * Add a new language (or overwrite existing) to the translations list. 26 | * 27 | * Translations object is grouped by context identifiers, use "default" 28 | * for the default context 29 | * 30 | * { 31 | * "default":{ 32 | * "original": "translation" 33 | * }, 34 | * "mycontext":{ 35 | * "original": "another translation" 36 | * } 37 | * } 38 | * 39 | * Use arrays for plural forms - if the plural expression retrieves 0, the first 40 | * element from the array is used, 1 uses the second one etc. 41 | * 42 | * "original": ["orignaalne", "originaalsed"] 43 | * 44 | * @param {String} lang Language identifier 45 | * @param {Object} translations Translation strings for the language 46 | * @param {String|Function} pluralForms Rules for the plurals 47 | */ 48 | addLang: function(lang, translations, pluralForms){ 49 | lang = (lang || "").toString(); 50 | translations = translations || {}; 51 | this._translations[lang] = translations; 52 | this._translations[lang]._pluralForms = typeof pluralForms == "function" ? pluralForms : this._compilePluralForms(pluralForms); 53 | 54 | if(this._curLang == lang){ 55 | this.setLang(lang, true); 56 | } 57 | }, 58 | 59 | /** 60 | * Set currently active language. Also updates all strings on the active document 61 | * 62 | * @param {String} lang Language identifier 63 | * @param {Boolean} forceRefresh If set to true, force refreshing all the strings 64 | */ 65 | setLang: function(lang, forceRefresh){ 66 | if(this._curLang == lang && !forceRefresh){ 67 | return; 68 | } 69 | if(this._curLang != lang){ 70 | this._curLang = lang; 71 | this._emit("change", this._curLang); 72 | }else{ 73 | this._curLang = lang; 74 | } 75 | 76 | this._updateTranslations(); 77 | }, 78 | 79 | /** 80 | * Checks if a language has been already loaded 81 | * 82 | * @param {String} lang Language identifier 83 | * @return {Boolean} Return true if yes, or false if not 84 | */ 85 | hasLang: function(lang){ 86 | lang = (lang || "").toString(); 87 | return !!this._translations[lang]; 88 | }, 89 | 90 | /** 91 | * Converts ordinary DOM element into translated element 92 | * 93 | * @param {Element} elm DOM element 94 | * @param {Object} options Translation options for the element 95 | * @param {String} [options.text] Untranslated text for the element (defaults to innerHTML) 96 | * @param {String} [options.plural] Untranslated plural text for the element (defaults to innerHTML) 97 | * @param {Number} [options.pluralCount] Plural count for determining the correct translation string, defaults to 1 98 | * @param {String} [options.context] Context for the translation, defaults to "default" 99 | * @param {String} [options.useLang] Explicilty set the language that should be used when translating this element 100 | * @param {String|Array} [options.textArgs] Replacement string or an array of strings for %s and %1$s blocks 101 | */ 102 | createTranslationElement: function(elm, options){ 103 | var translationData; 104 | 105 | options = options || {}; 106 | 107 | this._setDataAttribute(elm, "text", options.text || this._trim(elm.innerHTML)); 108 | this._setDataAttribute(elm, "plural", options.plural || options.text || this._trim(elm.innerHTML)); 109 | this._setDataAttribute(elm, "context", options.context || "default"); 110 | 111 | if("pluralCount" in options){ 112 | this._setDataAttribute(elm, "pluralCount", options.pluralCount); 113 | } 114 | 115 | if(options.textArgs){ 116 | elm._cachedTextArgs = options.textArgs; 117 | this._setDataAttribute(elm, "textArgs", options.textArgs.join("; ")); 118 | } 119 | 120 | if(options.useLang){ 121 | this._setDataAttribute(elm, "useLang", options.useLang); 122 | } 123 | 124 | this._setDataAttribute(elm, "translate", true); 125 | 126 | translationData = this._getTranslationData(elm); 127 | this.update.apply(this, [elm, translationData.context, translationData.pluralCount].concat(translationData.textArgs)); 128 | }, 129 | 130 | /** 131 | * Translate the text of a DOM element 132 | * 133 | * @param {Element} elm DOM element 134 | * @param {String} context Context string, if falsy value defaults to "default" 135 | * @param {Number} pluralCount Plural count for determining the correct translation string 136 | * @param {String} [arg1] Replacment string for first %s 137 | * @param {String} [arg2] Replacment string for second %s 138 | * @param {String} [argN] Replacment string for nth %s 139 | * 140 | * Or alternatively 141 | * 142 | * @param {Element} elm DOM element 143 | * @param {Object} options Translation options for the element 144 | * @param {String} [options.text] Untranslated text for the element (defaults to innerHTML) 145 | * @param {String} [options.plural] Untranslated plural text for the element (defaults to innerHTML) 146 | * @param {Number} [options.pluralCount] Plural count for determining the correct translation string, defaults to 1 147 | * @param {String} [options.context] Context for the translation, defaults to "default" 148 | * @param {String} [options.useLang] Explicilty set the language that should be used when translating this element 149 | * @param {String|Array} [options.textArgs] Replacement string or an array of strings for %s and %1$s blocks 150 | */ 151 | update: function(/* elm, context, pluralCount, arg1, arg2, ... */){ 152 | var args = Array.prototype.slice.call(arguments), 153 | elm = args.shift(), 154 | context, pluralCount, translationString, 155 | translationData = this._getTranslationData(elm); 156 | 157 | if(typeof args[0] == "object"){ 158 | context = args[0].context; 159 | pluralCount = args[0].pluralCount; 160 | args = args[0].textArgs && [].concat(args[0].textArgs); 161 | }else{ 162 | context = args.shift(); 163 | pluralCount = args.shift(); 164 | } 165 | 166 | if(typeof context == "undefined"){ 167 | context = translationData.context; 168 | } 169 | 170 | if(typeof pluralCount == "undefined"){ 171 | pluralCount = translationData.pluralCount; 172 | } 173 | 174 | if(typeof args == "undefined"){ 175 | args = translationData.textArgs; 176 | } 177 | 178 | translationString = this._getTranslationString(elm, context, pluralCount, args); 179 | elm.innerHTML = this._formatString.apply(this, [translationString].concat(args)); 180 | }, 181 | 182 | /* 183 | * Translate a string 184 | * 185 | * @param {String} text String to be translated 186 | * @param {Object} options Translation options 187 | * @param {String} [options.plural] Untranslated plural text for the element 188 | * @param {Number} [options.pluralCount] Plural count for determining the correct translation string, defaults to 1 189 | * @param {String} [options.context] Context for the translation, defaults to "default" 190 | * @param {String} [options.useLang] Explicilty set the language that should be used when translating this element 191 | * @param {String|Array} [options.textArgs] Replacement string or an array of strings for %s and %1$s blocks 192 | * @return {String} Translated string 193 | */ 194 | translate: function(text, options){ 195 | options = options || {}; 196 | 197 | var plural = options.plural || text, 198 | pluralForm, 199 | lang = options.lang || this._curLang, 200 | context = options.context || "default", 201 | pluralCount = options.pluralCount, 202 | translation; 203 | 204 | if(typeof pluralCount == "undefined"){ 205 | pluralCount = 1; 206 | } 207 | 208 | if(this._translations[lang]){ 209 | pluralForm = this._translations[lang]._pluralForms(pluralCount); 210 | }else{ 211 | pluralForm = Number(pluralCount != 1); 212 | } 213 | 214 | if(this._translations[lang] && this._translations[lang][context] && text in this._translations[lang][context]){ 215 | translation = [].concat(this._translations[lang][context][text])[pluralForm] || 216 | [].concat(this._translations[lang][context][text])[0] || 217 | [text, plural || text][pluralForm] || 218 | text; 219 | }else{ 220 | translation = [text, plural][pluralForm] || text; 221 | } 222 | 223 | return this._formatString.apply(this, [translation].concat(options.textArgs || [])); 224 | }, 225 | 226 | /** 227 | * Register an event listener 228 | * 229 | * @param {String} eventName The name of the event 230 | * @param {Function} listener Event callback 231 | */ 232 | on: function(eventName, listener){ 233 | eventName = (eventName || "").toString(); 234 | if(typeof listener != "function"){ 235 | return; 236 | } 237 | if(!this._eventListeners[eventName]){ 238 | this._eventListeners[eventName] = [listener]; 239 | }else{ 240 | this._eventListeners[eventName].push(listener); 241 | } 242 | }, 243 | 244 | /** 245 | * Gathers all active translation strings (originals, not translations) 246 | * 247 | * @return {Object} Currently used translation strings 248 | */ 249 | gatherTranslationStrings: function(){ 250 | var translationTable = {}, 251 | elements = this._selectorFunc(), 252 | element, context, singular, plural, translation; 253 | 254 | for(var i=0, len = elements.length; i user-data 532 | * 533 | * @param {String} str Camel cased string 534 | * @return {String} Scored string 535 | */ 536 | _fromCamelCase: function(str){ 537 | return str.replace(/[A-Z]/g, function(o){ 538 | return "-" + o.toLowerCase(); 539 | }); 540 | }, 541 | 542 | /** 543 | * Emits an event 544 | */ 545 | _emit: function(/* eventName, data */){ 546 | var args = Array.prototype.slice.call(arguments), 547 | eventName = (args.shift() || "").toString(), 548 | that = this; 549 | 550 | if(this._eventListeners[eventName]){ 551 | for(var i=0, len = this._eventListeners[eventName].length; i