Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Jean-Philippe Joyal
committed
Jun 29, 2010
1 parent
422b76d
commit 3ab5240
Showing
9 changed files
with
1,604 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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 | |||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -0,0 +1,6 @@ | |||
license | |||
* file header | |||
* | |||
|
|||
Tests | |||
* init options |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Original file line | Diff line number | Diff line change |
---|---|---|---|
@@ -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" | |||
} |
Oops, something went wrong.