Skip to content

Commit

Permalink
first release
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean-Philippe Joyal committed Jun 29, 2010
1 parent 422b76d commit 3ab5240
Show file tree
Hide file tree
Showing 9 changed files with 1,604 additions and 0 deletions.
20 changes: 20 additions & 0 deletions MIT-LICENSE.txt
@@ -0,0 +1,20 @@
Copyright (c) 2010 Jean-Philippe Joyal, http://leastusedfeature.com

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
150 changes: 150 additions & 0 deletions README
@@ -0,0 +1,150 @@
jsperanto
=========

Simple translation for your javascripts, yummy with your favorite templates engine like EJS.

* Pluralization, interpolation & "nested lookup" support for your translations
* Uses XHR to get a JSON dictionary (or load it your own way & format)
* JSLint-ed, QUnit-ed
* similar to Rails's i18n but sans backend needed
* No global pollution (hides under jQuery.jsperanto)
* Works with : IE6+, Firefox 3+, Safari 3+, Chrome, Opera 9+

Depends on jQuery 1.3.2+ (uses $.ajax, $.each, $.extend)

Usage example
=============

$.jsperanto.init(function(t){
t('project.name'); //-> "jsperanto"
$.t('project.store'); //-> "JSON"

$.t('can_speak',{count:1}); //-> "I can only speak one language"
$.t('can_speak',{count:3}); //-> "I can speak 3 languages"
$.t('can_speak_plural',{count:'any'}); //-> "I can speak any languages"

$.t('project.size.source',{value:4,unit:"kb"}); //-> "jsperanto is 4 kb"
$.t('project.size.min',{value:1727,unit:"bytes"}) //-> "jsperanto is 1727 bytes when minified"
$.t('project.size.gzip',{value:833,unit:"bytes"}) //-> "jsperanto is 833 bytes when minified and gzipped"
});

//given this dictionary
{
"project" : {
"name" : "jsperanto",
"store" : "JSON",
"size" : {
"source" : "$t(project.name) is __value__ __unit__",
"min" : "$t(project.size.source) when minified",
"gzip" : "$t(project.size.min) and gzipped"
}
},
"can_speak" : "I can only speak one language",
"can_speak_plural" : "I can speak __count__ languages"
}

API
===

**$.jsperanto.init(function(t),options)**

initialize jsperanto by loading the dictionary, calling back when ready

**function(t)** : is called once jsperanto is ready, passing the translate method ($.jsperanto.translate)

**options** extends these defaults

o.interpolationPrefix = '__';
o.interpolationSuffix = '__';
o.pluralSuffix = "_plural";
o.maxRecursion = 50; //used while applying reuse of strings to avoid infinite loop
o.reusePrefix = "$t("; //nested lookup prefix
o.reuseSuffix = ")"; //nested lookup suffix
o.fallbackLang = 'en-US'; // see Language fallback section
o.dicoPath = 'locales'; // see Dictionary section
o.keyseparator = "."; // keys passed to $.jsperanto.translate use this separator
o.setDollarT = true; // $.t aliases $.jsperanto.translate, nice shortcut
o.dictionary = false; // to supply the dictionary instead of loading it using $.ajax. A (big) javascript object containing your namespaced translations
o.lang = false; //specify a language to use i.e en-US

Use init to switch language too :

$.jsperanto.init(someMethod,{lang:"fr"})

**$.jsperanto.translate(key,options)**

looks up the key in the dictionary applying plural, interpolation & nested lookup.

**key** to lookup in the dictionary, for example "register.error.email"

**options** each prop name are are used for interpolation

**options.count** special prop that indicates to retrieve the plural version (**key**_plural) if its greater than 1. Also used for interpolation

**options.defaultValue** specify default value if the key can't be resolved (the key itself will be sent back if no defaultValue is provided)

**aliases** : $.jsperanto.t , $.t if init option _setDollarT_ is left to true.

Dictionary loading
==================

Using defaults, jsperanto uses a basic browser language detection

(navigator.language) ? navigator.language : navigator.userLanguage

to determine what dictionary file to load. You can also instruct jsperanto to load a specific language (via init option _lang_).

Once the language is determined, jsperanto will use $.ajax to load the dictionary using _locales/**somelang**.json_ as url. For example _locales/fr-CA.json_ is used for a french canadian browser. If the json file can't be retrieved, it will try to get the fallback language, which is 'en-US' by default. (you can change this using init option _fallbacklang_). If no dictionary file can be retrieved at all, jsperanto translate method will simply return the provided key.

You can bypass this loading mechanism completely by providing a dictionary object to init (_options.dictionary_)

Switching language
==================

Simply use init again and specify a language (or dictionary) to use.

$.jsperanto.init(someMethod,{lang:"fr"})

Licence
=======

MIT License

Copyright (c) 2010 Jean-Philippe Joyal, <http://leastusedfeature.wordpress.com>

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Inspiration & similar projects
==============================

[Rails i18n](http://guides.rubyonrails.org/i18n.html)

[Babilu](http://tore.darell.no/posts/introducing_babilu_rails_i18n_for_your_javascript)

[l10n.js](http://github.com/eligrey/l10n.js)

[javascript_i18n](http://github.com/qoobaa/javascript_i18n)


Thanks
======

Many thanks to Radialpoint for letting me open source this work

6 changes: 6 additions & 0 deletions TODO.txt
@@ -0,0 +1,6 @@
license
* file header
*

Tests
* init options
143 changes: 143 additions & 0 deletions jquery.jsperanto.js
@@ -0,0 +1,143 @@
//jquery 1.3.2 dependencies : $.each, $.extend, $.ajax

(function($) {
//defaults
var o = {};
o.interpolationPrefix = '__';
o.interpolationSuffix = '__';
o.pluralSuffix = "_plural";
o.maxRecursion = 50; //used while applying reuse of strings to avoid infinite loop
o.reusePrefix = "$t(";
o.reuseSuffix = ")";
o.fallbackLang = 'en-US'; // see Language fallback section
o.dicoPath = 'locales'; // see Dictionary section
o.keyseparator = "."; // keys passed to $.jsperanto.translate use this separator
o.setDollarT = true; // $.t aliases $.jsperanto.translate, nice shortcut
o.dictionary = false; // to supply the dictionary instead of loading it using $.ajax. A (big) javascript object containing your namespaced translations
o.lang = false; //specify a language to use
o.pluralNotFound = ["plural_not_found_", Math.random()].join(''); // used internally by translate

var dictionary = false; //not yet loaded
var currentLang = false;
var count_of_replacement = 0;

function init(callback,options){
$.extend(o,options);
if(!o.lang){o.lang = detectLanguage();}
loadDictionary(o.lang,function(loadedLang){
currentLang = loadedLang;
if(o.setDollarT){$.t = $.t || translate;} //shortcut
callback(translate);
});
}

function applyReplacement(string,replacementHash){
$.each(replacementHash,function(key,value){
string = string.replace([o.interpolationPrefix,key,o.interpolationSuffix].join(''),value);
});
return string;
}

function applyReuse(translated,options){
while (translated.indexOf(o.reusePrefix) != -1){
count_of_replacement++;
if(count_of_replacement > o.maxRecursion){break;} // safety net for too much recursion
var index_of_opening = translated.indexOf(o.reusePrefix);
var index_of_end_of_closing = translated.indexOf(o.reuseSuffix,index_of_opening) + o.reuseSuffix.length;
var token = translated.substring(index_of_opening,index_of_end_of_closing);
var token_sans_symbols = token.replace(o.reusePrefix,"").replace(o.reuseSuffix,"");
var translated_token = _translate(token_sans_symbols,options);
translated = translated.replace(token,translated_token);
}
return translated;
}

function detectLanguage(){
if(navigator){
return (navigator.language) ? navigator.language : navigator.userLanguage;
}else{
return o.fallbackLang;
}
}

function needsPlural(options){
return (options.count && typeof options.count != 'string' && options.count > 1);
}


function translate(dottedkey,options){
count_of_replacement = 0;
return _translate(dottedkey,options);
}

/*
options.defaultValue
options.count
*/
function _translate(dottedkey,options){
options = options || {};
var notfound = options.defaultValue || dottedkey;
if(!dictionary){return notfound;} // No dictionary to translate from

if(needsPlural(options)){
var optionsSansCount = $.extend({},options);
delete optionsSansCount.count;
optionsSansCount.defaultValue = o.pluralNotFound;
var pluralKey = dottedkey + o.pluralSuffix;
var translated = translate(pluralKey,optionsSansCount);
if(translated != o.pluralNotFound){
return applyReplacement(translated,{count:options.count});//apply replacement for count only
}// else continue translation with original/singular key
}

var keys = dottedkey.split(o.keyseparator);
var i = 0;
var value = dictionary;
while(keys[i]) {
value = value && value[keys[i]];
i++;
}
if(value){
value = applyReplacement(value,options);
value = applyReuse(value,options);
return value;
}else{
return notfound;
}
}

function loadDictionary(lang,doneCallback){
if(o.dictionary){
dictionary = o.dictionary;
doneCallback(lang);
return;
}
$.ajax({
url: [o.dicoPath,"/", lang, '.json'].join(''),
success: function(data,status,xhr){
dictionary = data;
doneCallback(lang);
},
error : function(xhr,status,error){
if(lang != o.fallbackLang){
loadDictionary(o.fallbackLang,doneCallback);
}else{
doneCallback(false);
}
},
dataType: "json"
});
}

function lang(){
return currentLang;
}

$.jsperanto = $.jsperanto || {
init:init,
t:translate,
translate:translate,
detectLanguage : detectLanguage,
lang : lang
};
})(jQuery);
21 changes: 21 additions & 0 deletions test/index.html
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>QUnit Test Suite</title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.4.2.min.js"></script>

<script type="text/javascript" src="../jquery.jsperanto.js"></script>

<link rel="stylesheet" href="qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="qunit.js"></script>

<script type="text/javascript" src="test.js"></script>
</head>
<body>
<h1 id="qunit-header">QUnit Test Suite</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
</body>
</html>
38 changes: 38 additions & 0 deletions test/locales/testlang.json
@@ -0,0 +1,38 @@
{
"sections" : {
"home" : "Home",
"files" : "My Files"
},
"product" : {
"name" : "jsperanto"
},
"withHTML" : "<b>this would be bold</b>",
"withreuse" : "$t(product.name) and $t(sections.home)",
"withreplacement" : "since __year__",
"4" : {
"level" : {
"of" : {
"nesting": "4 level of nesting"
}
}
},
"pluralversionexist" : "singular version of pluralversionexist",
"pluralversionexist_plural" : "plural version of pluralversionexist",
"pluralversiondoesnotexist" : "plural version does not exist",
"count and replacement" : "you have __count__ friend",
"count and replacement_plural" : "you have __count__ friends",

"infinite" : "infinite $t(recursion)",
"recursion" : "recursion $t(infinite)",
"project" : {
"name" : "jsperanto",
"store" : "JSON",
"size" : {
"source" : "$t(project.name) is __value__ __unit__",
"min" : "$t(project.size.source) when minified",
"gzip" : "$t(project.size.min) and gzipped"
}
},
"can_speak" : "I can only speak one language",
"can_speak_plural" : "I can speak __count__ languages"
}

0 comments on commit 3ab5240

Please sign in to comment.