Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

add translatePlural support with compiled plural functions #6

Open
wants to merge 6 commits into from

2 participants

@kkaefer

Syntax: "This post has #{comments:1 comment|@ comments}"

@kkaefer

This allows for varying amounts of plural versions for different languages and uses a similar format as GNU gettext. I.e. there's a plural formula that determines the index of the plural form chosen, based on the number.

@tj
Owner
tj commented

ah :) good call. perhaps we should try and work it in to the regular translate() call, and just check for the new syntax with numeric values

@kkaefer

Yeah, I'm not set on the syntax, I've tried various ways and eventually just submitted this. I also plan to integrate lingo with jade so that you can compile language-specific templates and have templates automatically translated. This would be facilitated if jade and lingo would use the same syntax for string interpolation (jade uses #{var}, while lingo uses {var}).

@tj
Owner
tj commented

yeah that would be sweet, I will try and toy around with this later. I think we should do it just through translate() but the syntax is fine IMO. maybe flip it so plural is first

@kkaefer

I used the order singular-plural because that seems to be the standard (e.g. gettext, Rails' i18n use that order). I'll write and rewrite some of the translation related code later today.

Konstantin Käfer merge .translatePlural into .translate(). This also adds compiled int…
…erpolation functions for regular (non-plural) strings, while short-circuiting strings without parameters for interpolation.
8cea05a
@kkaefer

Now uses the absence of the params object to determine whether compilation into a interpolation function is necessary. If so, it automatically finds plurals and creates a special pluralization function for them.

@WickedSik WickedSik referenced this pull request from a commit
Jurriën Dokter add translatePlural support with compiled plural functions
[Importing Pull Request #6]

Syntax: `"This post has #{comments:1 comment|@ comments}"`
e9ad459
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 31, 2010
Commits on Nov 1, 2010
  1. merge .translatePlural into .translate(). This also adds compiled int…

    Konstantin Käfer authored
    …erpolation functions for regular (non-plural) strings, while short-circuiting strings without parameters for interpolation.
  2. Allow escaping characters

    Konstantin Käfer authored
  3. allow escaping the @ character in plural strings

    Konstantin Käfer authored
Commits on Nov 2, 2010
  1. rewrite string interpolation and fix escaping problems

    Konstantin Käfer authored
This page is out of date. Refresh to see the latest.
Showing with 191 additions and 6 deletions.
  1. +120 −6 lib/language.js
  2. +71 −0 test/translation.test.js
View
126 lib/language.js
@@ -20,6 +20,8 @@ var lingo = require('./lingo');
var Language = module.exports = function Language(code) {
this.code = code;
+ this.pluralCount = 2;
+ this.pluralFormula = 'n == 1 ? 0 : 1';
this.translations = {};
this.rules = {
plural: []
@@ -39,12 +41,124 @@ var Language = module.exports = function Language(code) {
* @api public
*/
-Language.prototype.translate = function(str, params){
- str = this.translations[str] || str;
+Language.prototype.translate = function(str, params) {
if (params) {
- str = str.replace(/\{([^}]+)\}/g, function(_, key){
- return params[key];
- });
+ var fn = this.translations[str] || str;
+ if (typeof fn !== 'function') {
+ fn = this.translations[str] = this.compileTranslation(str);
+ }
+ return fn(params);
+ }
+
+ str = this.translations[str] || str;
+ return str.replace(/\\(.)/g, '$1');
+};
+
+
+
+
+function stringify(str) {
+ // Remove escapes and then (re-)escape special chars.
+ return "'" + str.replace(/\\(.)/g, '$1').replace(/([\\'])/g, '\\$1') + "'";
+}
+
+function tokenize(str, stringFn, tokenFn, regex) {
+ var match, index = 0;
+ var regex = /(\\*)\{([^:}]+)(?::((?:[^\\}]|\\.)+))?\}/g;
+
+ // Thanks to lack of backreferences, we have to use this structure instead
+ // of a simple replace.
+ do {
+ match = regex.exec(str);
+ // Skip escaped tokens.
+ if (match && (match[1].length % 2) > 0) {
+ regex.lastIndex = match.index + match[1].length + 1;
+ continue;
+ }
+
+ // Add string up to this match.
+ var endIndex = match ? match.index + match[1].length : undefined;
+ var before = str.substring(index, endIndex);
+ index = regex.lastIndex;
+
+ if (before) stringFn(before);
+ if (match) tokenFn(match[0], match[2], match[3]);
+ } while (match);
+}
+
+function interpolate(str) {
+ var parts = [];
+
+ tokenize(
+ str,
+ function(token) { parts.push(stringify(token)); },
+ function(all, expr, plurals) { parts.push("(" + expr + ")"); }
+ );
+
+ return parts.join(' + ');
+}
+
+function demultiplex(str, i) {
+ var parts = [];
+ var regex = new RegExp('^(?:(?:[^\\\\|]|\\\\.)*\\|){' + (i || 0) +'}((?:[^\\\\|]|\\\\.)*)');
+
+ tokenize(
+ str,
+ function(token) {
+ parts.push(token);
+ },
+ function(all, expr, plurals) {
+ if (!plurals) return parts.push(all);
+ var match = plurals.match(regex);
+ if (match) {
+ return parts.push(match[1].replace(/(\\*)@/g, function(all, escaped) {
+ if (escaped && escaped.length % 2) return all;
+ else return escaped + '{' + expr + '}';
+ }));
+ }
+ }
+ );
+
+ return parts.join('');
+}
+
+function pluralExpression(str) {
+ var pluralExpr;
+
+ tokenize(str, function() {}, function(all, variable, plurals) {
+ if (plurals) pluralExpr = variable;
+ });
+
+ return pluralExpr;
+}
+
+Language.prototype.compileTranslation = function(str) {
+ var pluralExpr = pluralExpression(str);
+ var translation = this.translations[str] || str;
+
+ if (!pluralExpr) {
+ // There is no plural in this.
+ var fn = 'with (params) return (' + interpolate(translation) + ');';
+ }
+ else {
+ if (typeof translation === 'string') {
+ // Parse string and convert them to array.
+ var plurals = [];
+ for (var i = 0; i < this.pluralCount; i++) {
+ plurals.push(demultiplex(translation, i));
+ }
+ translation = plurals;
+ }
+
+ var fn =
+ 'with (params) {' +
+ 'var n = (' + pluralExpr + '), _pluralIndex = (' + this.pluralFormula + ');' +
+ translation.map(function(str, i) {
+ if (i === translation.length - 1) return 'return (' + interpolate(str) + ');';
+ else return 'if (_pluralIndex === ' + i + ') return (' + interpolate(str) + ');';
+ }).join('') +
+ '}';
}
- return str;
+
+ return new Function('params', fn);
};
View
71 test/translation.test.js
@@ -8,10 +8,16 @@ var lingo = require('lingo')
var fr = new lingo.Language('fr');
+fr.pluralCount = 2;
+fr.pluralFormula = 'n > 1 ? 1 : 0';
fr.translations = {
'Hello World': 'Bonjour tout le monde'
, 'Hello {name}': 'Bonjour {name}'
, 'Hello {first} {last}': 'Bonjour {first} {last}'
+ , 'There {number:is 1 apple|are @ apples} on the table':
+ 'Il y a {number:@ pomme|@ pommes} sur la table'
+ , '{comments:1 comment|@ comments} since {ago}':
+ '{comments:@ commentaire|@ commentaires} depuis {ago}'
};
module.exports = {
@@ -25,5 +31,70 @@ module.exports = {
assert.equal('Bonjour tout le monde', fr.translate('Hello World'));
assert.equal('Bonjour TJ', fr.translate('Hello {name}', { name: 'TJ' }));
assert.equal('Bonjour foo bar', fr.translate('Hello {first} {last}', { first: 'foo', last: 'bar' }));
+ },
+
+ 'test .translate() with plurals': function(assert) {
+ assert.equal(
+ 'There are 4 apples on the table',
+ en.translate('There {number:is 1 apple|are @ apples} on the table', { number: 4 })
+ );
+ assert.equal(
+ 'There is 1 apple on the table',
+ en.translate('There {number:is 1 apple|are @ apples} on the table', { number: 1 })
+ );
+ assert.equal(
+ 'Il y a 4 pommes sur la table',
+ fr.translate('There {number:is 1 apple|are @ apples} on the table', { number: 4 })
+ );
+ assert.equal(
+ 'Il y a 1 pomme sur la table',
+ fr.translate('There {number:is 1 apple|are @ apples} on the table', { number: 1 })
+ );
+ },
+
+ 'test .translate() with plurals and interpolation': function(assert) {
+ assert.equal(
+ '27 comments since yesterday',
+ en.translate('{comments:1 comment|@ comments} since {ago}', { comments: 27, ago: 'yesterday' })
+ );
+ assert.equal(
+ '1 comment since yesterday',
+ en.translate('{comments:1 comment|@ comments} since {ago}', { comments: 1, ago: 'yesterday' })
+ );
+ assert.equal(
+ '27 commentaires depuis hier',
+ fr.translate('{comments:1 comment|@ comments} since {ago}', { comments: 27, ago: 'hier' })
+ );
+ assert.equal(
+ '1 commentaire depuis hier',
+ fr.translate('{comments:1 comment|@ comments} since {ago}', { comments: 1, ago: 'hier' })
+ );
+ },
+
+ 'test .translate() with escaped interpolation tokens': function(assert) {
+ assert.equal('Lingo uses {token} style interpolation"', en.translate('Lingo uses \\{token\\} style interpolation\\"'));
+ assert.equal('Lingo uses {token} style interpolation"', en.translate('Lingo uses \\{token\\} style interpolation\\"', { }));
+ assert.equal('Lingo uses {token} style interpolation"', en.translate('Lingo uses \\{token\\} style interpolation\\"', { token: 'brace' }));
+ assert.equal('Lingo uses \\brace style interpolation"', en.translate('Lingo uses \\\\{token} style interpolation\\"', { token: 'brace' }));
+ assert.equal('Lingo uses \\{token} style interpolation"', en.translate('Lingo uses \\\\\\{token} style interpolation\\"'));
+ assert.equal('Lingo uses \\{token} style interpolation"', en.translate('Lingo uses \\\\\\{token} style interpolation\\"', { }));
+ assert.equal('Lingo uses \\{token} style interpolation"', en.translate('Lingo uses \\\\\\{token} style interpolation\\"', { token: 'brace' }));
+
+ assert.equal('Plurals: {variable:singular|plural}', en.translate('Plurals: \\{variable:singular|plural}'));
+ assert.equal('Plurals: {variable:singular|plural}', en.translate('Plurals: \\{variable:singular|plural}', {}));
+ assert.equal('Plurals: {variable:singular|plural}', en.translate('Plurals: \\{variable:singular|plural}', { variable: 4 }));
+
+ assert.equal('Plurals: \\2 plural', en.translate('Plurals: {variable:\\\\@ singular|\\\\@ plural}', { variable: 2 }));
+ assert.equal('Plurals: \\2 plural', en.translate('Plurals: \\\\{variable:@ singular|@ plural}', { variable: 2 }));
+
+ assert.equal('Plurals: @ 2 plural', en.translate('Plurals: {variable:\\@ @ singular|\\@ @ plural}', { variable: 2 }));
+ assert.equal('Plurals: \\2 2 plural', en.translate('Plurals: {variable:\\\\@ @ singular|\\\\@ @ plural}', { variable: 2 }));
+ assert.equal('Plurals: 2|plural', en.translate('Plurals: {variable:@\\|singular|@\\|plural}', { variable: 2 }));
+ assert.equal('Plurals: 2 plural', en.translate('Plurals: {variable:@ singular\\\\|@ plural}', { variable: 2 }));
+
+ assert.equal('Plurals: 2 plural}xy', en.translate('Plurals: {variable:\\@ singular|@ plural\\}x}y', { variable: 2 }));
+ assert.equal('Plurals: 2 plural\\x}y', en.translate('Plurals: {variable:\\@ singular|@ plural\\\\}x}y', { variable: 2 }));
+
+ assert.equal("{This Lin'go us'es \\5b}ar style {foo} interpolation\\", en.translate("\\{{bar} Lin'go us'es \\\\{token:@f\\|oo|@b\\}ar} style \\{foo} interpolation\\", { bar: 'This', token: 5 }));
}
}
Something went wrong with that request. Please try again.