From e13d1d0cf2548c172e3c670ef7fe1b35befbb755 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Sun, 7 Jul 2013 19:51:24 +0200 Subject: [PATCH 01/20] Add possibility to read/write plural translations --- l10n/l10n.pl | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/l10n/l10n.pl b/l10n/l10n.pl index b07d6d686b..8d13ec29a3 100644 --- a/l10n/l10n.pl +++ b/l10n/l10n.pl @@ -39,7 +39,7 @@ sub crawlFiles{ foreach my $i ( @files ){ next if substr( $i, 0, 1 ) eq '.'; next if $i eq 'l10n'; - + if( -d $dir.'/'.$i ){ push( @found, crawlFiles( $dir.'/'.$i )); } @@ -100,11 +100,17 @@ if( $task eq 'read' ){ foreach my $file ( @totranslate ){ next if $ignore{$file}; - my $keyword = ( $file =~ /\.js$/ ? 't:2' : 't'); + my $keywords = ''; + if( $file =~ /\.js$/ ){ + $keywords = '--keyword=t:2 --keyword=tp:2,3'; + } + else{ + $keywords = '--keyword=t --keyword=tp:1,2'; + } my $language = ( $file =~ /\.js$/ ? 'Python' : 'PHP'); my $joinexisting = ( -e $output ? '--join-existing' : ''); print " Reading $file\n"; - `xgettext --output="$output" $joinexisting --keyword=$keyword --language=$language "$file" --from-code=UTF-8 --package-version="5.0.0" --package-name="ownCloud Core" --msgid-bugs-address="translations\@owncloud.org"`; + `xgettext --output="$output" $joinexisting $keywords --language=$language "$file" --from-code=UTF-8 --package-version="5.0.0" --package-name="ownCloud Core" --msgid-bugs-address="translations\@owncloud.org"`; } chdir( $whereami ); } @@ -118,7 +124,7 @@ elsif( $task eq 'write' ){ print " Processing $app\n"; foreach my $language ( @languages ){ next if $language eq 'templates'; - + my $input = "${whereami}/$language/$app.po"; next unless -e $input; @@ -128,8 +134,18 @@ elsif( $task eq 'write' ){ my @strings = (); foreach my $string ( @{$array} ){ next if $string->msgid() eq '""'; - next if $string->msgstr() eq '""'; - push( @strings, $string->msgid()." => ".$string->msgstr()); + + # Do we use singular or plural? + if( defined( $string->msgstr_n() )){ + next if $string->msgstr_n()->{"0"} eq '""' || + $string->msgstr_n()->{"1"} eq '""'; + push( @strings, $string->msgid()." => ".$string->msgstr_n()->{"0"} ); + push( @strings, $string->msgid_plural()." => ".$string->msgstr_n()->{"1"} ); + } + else{ + next if $string->msgstr() eq '""'; + push( @strings, $string->msgid()." => ".$string->msgstr()); + } } next if $#strings == -1; # Skip empty files From 560839195e97abdaca4f8c144a6a760a0ab90c19 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Sun, 7 Jul 2013 20:06:14 +0200 Subject: [PATCH 02/20] make l10n libs capable of handling plural translations --- lib/l10n.php | 20 ++++++++++++++++++++ lib/l10n/string.php | 11 ++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/l10n.php b/lib/l10n.php index d35ce5fed1..03528c2274 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -166,6 +166,26 @@ class OC_L10N{ return new OC_L10N_String($this, $text, $parameters); } + /** + * @brief Translating + * @param $text_singular String the string to translate for exactly one object + * @param $text_plural String the string to translate for n objects + * @param $count Integer Number of objects + * @param array $parameters default:array() Parameters for sprintf + * @return \OC_L10N_String Translation or the same text + * + * Returns the translation. If no translation is found, $text will be + * returned. %n will be replaced with the number of objects. + */ + public function tp($text_singular, $text_plural, $count, $parameters = array()) { + if($count == 1){ + return new OC_L10N_String($this, $text_singular, $parameters, $count); + } + else{ + return new OC_L10N_String($this, $text_plural, $parameters, $count); + } + } + /** * @brief Translating * @param $textArray The text array we need a translation for diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 8eef10071e..1bef733094 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -8,18 +8,23 @@ class OC_L10N_String{ protected $l10n; - public function __construct($l10n, $text, $parameters) { + public function __construct($l10n, $text, $parameters, $count = 1) { $this->l10n = $l10n; $this->text = $text; $this->parameters = $parameters; + $this->count = $count; } public function __toString() { $translations = $this->l10n->getTranslations(); + + $text = $this->text; if(array_key_exists($this->text, $translations)) { - return vsprintf($translations[$this->text], $this->parameters); + $text = $translations[$this->text]; } - return vsprintf($this->text, $this->parameters); + // Replace %n first (won't interfere with vsprintf) + $text = str_replace('%n', $this->count, $text); + return vsprintf($text, $this->parameters); } } From 4e214883d64acf4dc9cc3191780086d0a9dac60f Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Sun, 7 Jul 2013 20:26:09 +0200 Subject: [PATCH 03/20] Partly integrate plural translations into js --- core/js/js.js | 55 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/core/js/js.js b/core/js/js.js index 3904787c4e..15cf1d286f 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1,6 +1,6 @@ /** * Disable console output unless DEBUG mode is enabled. - * Add + * Add * define('DEBUG', true); * To the end of config/config.php to enable debug mode. * The undefined checks fix the broken ie8 console @@ -28,9 +28,11 @@ if (oc_debug !== true || typeof console === "undefined" || typeof console.log == * translate a string * @param app the id of the app for which to translate the string * @param text the string to translate + * @param vars (optional) FIXME + * @param count (optional) number to replace %n with * @return string */ -function t(app,text, vars){ +function t(app, text, vars, count){ if( !( t.cache[app] )){ $.ajax(OC.filePath('core','ajax','translations.php'),{ async:false,//todo a proper sollution for this without sync ajax calls @@ -46,7 +48,8 @@ function t(app,text, vars){ t.cache[app] = []; } } - var _build = function (text, vars) { + var _build = function (text, vars, count) { + // FIXME: replace %n with content of count return text.replace(/{([^{}]*)}/g, function (a, b) { var r = vars[b]; @@ -54,30 +57,44 @@ function t(app,text, vars){ } ); }; + var translation = text; if( typeof( t.cache[app][text] ) !== 'undefined' ){ - if(typeof vars === 'object') { - return _build(t.cache[app][text], vars); - } else { - return t.cache[app][text]; - } + translation = t.cache[app][text]; } - else{ - if(typeof vars === 'object') { - return _build(text, vars); - } else { - return text; - } + + if(typeof vars === 'object' || typeof count !== 'undefined' ) { + return _build(translation, vars, count); + } else { + return translation; } } t.cache={}; -/* +/** + * translate a string + * @param app the id of the app for which to translate the string + * @param text_singular the string to translate for exactly one object + * @param text_plural the string to translate for n objects + * @param count number to determine whether to use singular or plural + * @param vars (optional) FIXME + * @return string + */ +function tp(app, text_singular, text_plural, count, vars){ + if(count==1){ + return t(app, text_singular, vars, count); + } + else{ + return t(app, text_plural, vars, count); + } +} + +/** * Sanitizes a HTML string -* @param string +* @param s string * @return Sanitized string */ function escapeHTML(s) { - return s.toString().split('&').join('&').split('<').join('<').split('"').join('"'); + return s.toString().split('&').join('&').split('<').join('<').split('"').join('"'); } /** @@ -741,7 +758,7 @@ OC.get=function(name) { var namespaces = name.split("."); var tail = namespaces.pop(); var context=window; - + for(var i = 0; i < namespaces.length; i++) { context = context[namespaces[i]]; if(!context){ @@ -760,7 +777,7 @@ OC.set=function(name, value) { var namespaces = name.split("."); var tail = namespaces.pop(); var context=window; - + for(var i = 0; i < namespaces.length; i++) { if(!context[namespaces[i]]){ context[namespaces[i]]={}; From 18fc22b52bdf1d8372a6e9c602872c423ce50eff Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Tue, 16 Jul 2013 21:38:31 +0200 Subject: [PATCH 04/20] Allow several plural forms in l10n php files --- l10n/l10n.pl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/l10n/l10n.pl b/l10n/l10n.pl index 8d13ec29a3..7694749dad 100644 --- a/l10n/l10n.pl +++ b/l10n/l10n.pl @@ -102,10 +102,10 @@ if( $task eq 'read' ){ next if $ignore{$file}; my $keywords = ''; if( $file =~ /\.js$/ ){ - $keywords = '--keyword=t:2 --keyword=tp:2,3'; + $keywords = '--keyword=t:2 --keyword=n:2,3'; } else{ - $keywords = '--keyword=t --keyword=tp:1,2'; + $keywords = '--keyword=t --keyword=n:1,2'; } my $language = ( $file =~ /\.js$/ ? 'Python' : 'PHP'); my $joinexisting = ( -e $output ? '--join-existing' : ''); @@ -137,10 +137,15 @@ elsif( $task eq 'write' ){ # Do we use singular or plural? if( defined( $string->msgstr_n() )){ - next if $string->msgstr_n()->{"0"} eq '""' || - $string->msgstr_n()->{"1"} eq '""'; - push( @strings, $string->msgid()." => ".$string->msgstr_n()->{"0"} ); - push( @strings, $string->msgid_plural()." => ".$string->msgstr_n()->{"1"} ); + my @variants = (); + my $identifier = $string->msgid()."::".$string->msgid_plural() + $identifier =~ s/"/_/g; + + foreach my $variant ( sort { $a <=> $b} keys( %{$string->msgstr_n()} )){ + push( @variants, $string->msgstr_n()->{$variant} ); + } + + push( @strings, "\"$identifier\" => array(".join(@variants, ",").")"; } else{ next if $string->msgstr() eq '""'; From bb0c5bff5fc493dd24c2c9cbbf27986fd5098de1 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Tue, 16 Jul 2013 22:16:53 +0200 Subject: [PATCH 05/20] Add multiple plural forms to the php part --- core/js/js.js | 11 +++++------ lib/l10n.php | 34 ++++++++++++++++++++++++++++++---- lib/l10n/string.php | 9 ++++++++- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/core/js/js.js b/core/js/js.js index 15cf1d286f..37540bd15e 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -33,7 +33,7 @@ if (oc_debug !== true || typeof console === "undefined" || typeof console.log == * @return string */ function t(app, text, vars, count){ - if( !( t.cache[app] )){ + if( !( t.cache[app] )) { $.ajax(OC.filePath('core','ajax','translations.php'),{ async:false,//todo a proper sollution for this without sync ajax calls data:{'app': app}, @@ -49,8 +49,7 @@ function t(app, text, vars, count){ } } var _build = function (text, vars, count) { - // FIXME: replace %n with content of count - return text.replace(/{([^{}]*)}/g, + return text.replace(/%n/g, count).replace(/{([^{}]*)}/g, function (a, b) { var r = vars[b]; return typeof r === 'string' || typeof r === 'number' ? r : a; @@ -62,7 +61,7 @@ function t(app, text, vars, count){ translation = t.cache[app][text]; } - if(typeof vars === 'object' || typeof count !== 'undefined' ) { + if(typeof vars === 'object' || count !== undefined ) { return _build(translation, vars, count); } else { return translation; @@ -79,8 +78,8 @@ t.cache={}; * @param vars (optional) FIXME * @return string */ -function tp(app, text_singular, text_plural, count, vars){ - if(count==1){ +function n(app, text_singular, text_plural, count, vars){ + if(count === 1) { return t(app, text_singular, vars, count); } else{ diff --git a/lib/l10n.php b/lib/l10n.php index 03528c2274..8348962cc1 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -176,13 +176,28 @@ class OC_L10N{ * * Returns the translation. If no translation is found, $text will be * returned. %n will be replaced with the number of objects. + * + * In case there is more than one plural form you can add a function + * "selectplural" in core/l10n/l10n-*.php + * + * Example: + * + * [...] + * 'selectplural' => function($i){return $i == 1 ? 0 : $i == 2 ? 1 : 2}, + * [...] */ - public function tp($text_singular, $text_plural, $count, $parameters = array()) { - if($count == 1){ - return new OC_L10N_String($this, $text_singular, $parameters, $count); + public function n($text_singular, $text_plural, $count, $parameters = array()) { + $identifier = "_${text_singular}__${text_plural}_"; + if(array_key_exists( $this->localizations, "selectplural") && array_key_exists($this->translations, $identifier)) { + return new OC_L10N_String( $this, $identifier, $parameters, $count ); } else{ - return new OC_L10N_String($this, $text_plural, $parameters, $count); + if($count === 1) { + return new OC_L10N_String($this, $text_singular, $parameters, $count); + } + else{ + return new OC_L10N_String($this, $text_plural, $parameters, $count); + } } } @@ -220,6 +235,17 @@ class OC_L10N{ return $this->translations; } + /** + * @brief get localizations + * @returns Fetch all localizations + * + * Returns an associative array with all localizations + */ + public function getLocalizations() { + $this->init(); + return $this->localizations; + } + /** * @brief Localization * @param $type Type of localization diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 1bef733094..3cda5eab50 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -18,10 +18,17 @@ class OC_L10N_String{ public function __toString() { $translations = $this->l10n->getTranslations(); + $localizations = $this->l10n->getLocalizations(); $text = $this->text; if(array_key_exists($this->text, $translations)) { - $text = $translations[$this->text]; + if(is_array($translations[$this->text])) { + $id = $localizations["selectplural"]( $count ); + $text = $translations[$this->text][$id] + } + else{ + $text = $translations[$this->text]; + } } // Replace %n first (won't interfere with vsprintf) $text = str_replace('%n', $this->count, $text); From 69b175dbf9979af05a51a3b1d7e349c53a2a6d9d Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Tue, 16 Jul 2013 22:25:19 +0200 Subject: [PATCH 06/20] Fix two bugs in l10n.pl --- l10n/l10n.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/l10n/l10n.pl b/l10n/l10n.pl index 7694749dad..9e5caa6bad 100644 --- a/l10n/l10n.pl +++ b/l10n/l10n.pl @@ -138,14 +138,14 @@ elsif( $task eq 'write' ){ # Do we use singular or plural? if( defined( $string->msgstr_n() )){ my @variants = (); - my $identifier = $string->msgid()."::".$string->msgid_plural() + my $identifier = $string->msgid()."::".$string->msgid_plural(); $identifier =~ s/"/_/g; foreach my $variant ( sort { $a <=> $b} keys( %{$string->msgstr_n()} )){ push( @variants, $string->msgstr_n()->{$variant} ); } - push( @strings, "\"$identifier\" => array(".join(@variants, ",").")"; + push( @strings, "\"$identifier\" => array(".join(@variants, ",").")"); } else{ next if $string->msgstr() eq '""'; From fb976000914e291dce5d2ab61980149e8cec1a98 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Tue, 16 Jul 2013 22:27:55 +0200 Subject: [PATCH 07/20] Fix an error jenkins found --- lib/l10n/string.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 3cda5eab50..5b9dbaee8a 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -24,7 +24,7 @@ class OC_L10N_String{ if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { $id = $localizations["selectplural"]( $count ); - $text = $translations[$this->text][$id] + $text = $translations[$this->text][$id]; } else{ $text = $translations[$this->text]; From e064e53aa318bdfa43e62105e56f390f19a309f4 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 19:13:52 +0200 Subject: [PATCH 08/20] Save the plural information given in po files --- l10n/l10n.pl | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/l10n/l10n.pl b/l10n/l10n.pl index 9e5caa6bad..2a02e19c71 100644 --- a/l10n/l10n.pl +++ b/l10n/l10n.pl @@ -64,6 +64,16 @@ sub readIgnorelist{ return %ignore; } +sub getPluralInfo { + my( $info ) = @_; + + # get string + $info =~ s/.*Plural-Forms: (.+)\\n.*/$1/; + $info =~ s/^(.*)\\n.*/$1/g; + + return $info; +} + my $task = shift( @ARGV ); my $place = '..'; @@ -132,11 +142,15 @@ elsif( $task eq 'write' ){ my $array = Locale::PO->load_file_asarray( $input ); # Create array my @strings = (); - foreach my $string ( @{$array} ){ - next if $string->msgid() eq '""'; + my $plurals; - # Do we use singular or plural? - if( defined( $string->msgstr_n() )){ + foreach my $string ( @{$array} ){ + if( $string->msgid() eq '""' ){ + # Translator information + $plurals = getPluralInfo( $string->msgstr()); + } + elsif( defined( $string->msgstr_n() )){ + # plural translations my @variants = (); my $identifier = $string->msgid()."::".$string->msgid_plural(); $identifier =~ s/"/_/g; @@ -148,6 +162,7 @@ elsif( $task eq 'write' ){ push( @strings, "\"$identifier\" => array(".join(@variants, ",").")"); } else{ + # singular translations next if $string->msgstr() eq '""'; push( @strings, $string->msgid()." => ".$string->msgstr()); } @@ -158,7 +173,7 @@ elsif( $task eq 'write' ){ open( OUT, ">$language.php" ); print OUT " Date: Thu, 1 Aug 2013 19:54:51 +0200 Subject: [PATCH 09/20] Update l10n.php to read plural_forms string --- lib/l10n.php | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/l10n.php b/lib/l10n.php index 8348962cc1..d24717a23a 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -54,6 +54,11 @@ class OC_L10N{ */ private $translations = array(); + /** + * Plural forms + */ + private $plural_forms = ""; + /** * Localization */ @@ -138,6 +143,9 @@ class OC_L10N{ } } } + if(isset($PLURAL_FORMS)) { + $this->plural_forms = $PLURAL_FORMS; + } } if(file_exists(OC::$SERVERROOT.'/core/l10n/l10n-'.$lang.'.php')) { @@ -177,18 +185,13 @@ class OC_L10N{ * Returns the translation. If no translation is found, $text will be * returned. %n will be replaced with the number of objects. * - * In case there is more than one plural form you can add a function - * "selectplural" in core/l10n/l10n-*.php + * The correct plural is determined by the plural_forms-function + * provided by the po file. * - * Example: - * - * [...] - * 'selectplural' => function($i){return $i == 1 ? 0 : $i == 2 ? 1 : 2}, - * [...] */ public function n($text_singular, $text_plural, $count, $parameters = array()) { $identifier = "_${text_singular}__${text_plural}_"; - if(array_key_exists( $this->localizations, "selectplural") && array_key_exists($this->translations, $identifier)) { + if( array_key_exists($this->translations, $identifier)) { return new OC_L10N_String( $this, $identifier, $parameters, $count ); } else{ @@ -235,6 +238,17 @@ class OC_L10N{ return $this->translations; } + /** + * @brief getPluralForms + * @returns Fetches the gettext "Plural-Forms"-string + * + * Returns a string like "nplurals=2; plural=(n != 1);" + */ + public function getPluralForms() { + $this->init(); + return $this->plural_forms; + } + /** * @brief get localizations * @returns Fetch all localizations From 9673a563b567ee6f580c157a901f038f38f0b5a2 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 21:18:18 +0200 Subject: [PATCH 10/20] Create plural functions on the fly --- lib/l10n.php | 91 ++++++++++++++++++++++++++++++++++++++++++--- lib/l10n/string.php | 2 +- 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/lib/l10n.php b/lib/l10n.php index d24717a23a..208fa930c9 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -55,9 +55,14 @@ class OC_L10N{ private $translations = array(); /** - * Plural forms + * Plural forms (string) */ - private $plural_forms = ""; + private $plural_form_string; + + /** + * Plural forms (function) + */ + private $plural_form_function = null; /** * Localization @@ -144,7 +149,7 @@ class OC_L10N{ } } if(isset($PLURAL_FORMS)) { - $this->plural_forms = $PLURAL_FORMS; + $this->plural_form_string = $PLURAL_FORMS; } } @@ -161,6 +166,66 @@ class OC_L10N{ } } + /** + * @brief Creates a function that The constructor + * @param $app the app requesting l10n + * @param $lang default: null Language + * @returns OC_L10N-Object + * + * If language is not set, the constructor tries to find the right + * language. + * + * Parts of the code is copied from Habari: + * https://github.com/habari/system/blob/master/classes/locale.php + */ + protected function createPluralFormFunction($string){ + if(preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) { + // sanitize + $nplurals = preg_replace( '/[^0-9]/', '', $matches[1] ); + $plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] ); + + $body = str_replace( + array( 'plural', 'n', '$n$plurals', ), + array( '$plural', '$n', '$nplurals', ), + 'nplurals='. $nplurals . '; plural=' . $plural + ); + + // add parens + // important since PHP's ternary evaluates from left to right + $body .= ';'; + $res = ''; + $p = 0; + for($i = 0; $i < strlen($body); $i++) { + $ch = $body[$i]; + switch ( $ch ) { + case '?': + $res .= ' ? ('; + $p++; + break; + case ':': + $res .= ') : ('; + break; + case ';': + $res .= str_repeat( ')', $p ) . ';'; + $p = 0; + break; + default: + $res .= $ch; + } + } + + $body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);'; + return create_function('$n', $body); + } + else { + // default: one plural form for all cases but n==1 (english) + return create_function( + '$n', + '$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);' + ); + } + } + /** * @brief Translating * @param $text String The text we need a translation for @@ -239,14 +304,28 @@ class OC_L10N{ } /** - * @brief getPluralForms + * @brief getPluralFormString * @returns Fetches the gettext "Plural-Forms"-string * * Returns a string like "nplurals=2; plural=(n != 1);" */ - public function getPluralForms() { + public function getPluralFormString() { $this->init(); - return $this->plural_forms; + return $this->plural_form_string; + } + + /** + * @brief getPluralFormFunction + * @returns returns the plural form function + * + * returned function accepts the argument $n + */ + public function getPluralFormString() { + $this->init(); + if(is_null($this->plural_form_function)) { + $this->plural_form_function = createPluralFormFunction($this->plural_form_string); + } + return $this->plural_form_function; } /** diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 5b9dbaee8a..8d83f1f61f 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -23,7 +23,7 @@ class OC_L10N_String{ $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $id = $localizations["selectplural"]( $count ); + $id = $l10n->getPluralFormFunction()( $count ); $text = $translations[$this->text][$id]; } else{ From eee2c369d9b743634a062ad0c0b9f85496cecc87 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 21:36:33 +0200 Subject: [PATCH 11/20] Fix copy and paste errors in l10n.php --- lib/l10n.php | 2 +- lib/l10n/string.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n.php b/lib/l10n.php index 208fa930c9..ab2de5d9f1 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -320,7 +320,7 @@ class OC_L10N{ * * returned function accepts the argument $n */ - public function getPluralFormString() { + public function getPluralFormFunction() { $this->init(); if(is_null($this->plural_form_function)) { $this->plural_form_function = createPluralFormFunction($this->plural_form_string); diff --git a/lib/l10n/string.php b/lib/l10n/string.php index 8d83f1f61f..c72b744a7e 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -23,7 +23,7 @@ class OC_L10N_String{ $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $id = $l10n->getPluralFormFunction()( $count ); + $id = $l10n->getPluralFormFunction()($count); $text = $translations[$this->text][$id]; } else{ From 2f17dcdba81193b48a1c843e58e8b116c6309714 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Thu, 1 Aug 2013 21:46:52 +0200 Subject: [PATCH 12/20] Fix syntax error. --- lib/l10n/string.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/l10n/string.php b/lib/l10n/string.php index c72b744a7e..c78b06428d 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -23,7 +23,8 @@ class OC_L10N_String{ $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $id = $l10n->getPluralFormFunction()($count); + $fn = $l10n->getPluralFormFunction(); + $id = $fn($count); $text = $translations[$this->text][$id]; } else{ From 389d0824e8c5b87e4a49dffa06a7985d891a53bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 2 Aug 2013 21:45:36 +0200 Subject: [PATCH 13/20] add new line to generated php code --- l10n/l10n.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l10n/l10n.pl b/l10n/l10n.pl index 2a02e19c71..e3a42aefe2 100644 --- a/l10n/l10n.pl +++ b/l10n/l10n.pl @@ -173,7 +173,7 @@ elsif( $task eq 'write' ){ open( OUT, ">$language.php" ); print OUT " Date: Fri, 2 Aug 2013 21:46:25 +0200 Subject: [PATCH 14/20] PHPDoc comments adjusted *partially* - fixing call to createPluralFormFunction --- lib/l10n.php | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/l10n.php b/lib/l10n.php index ab2de5d9f1..0e47215369 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -2,8 +2,10 @@ /** * ownCloud * + * @author Frank Karlitschek * @author Jakob Sack * @copyright 2012 Frank Karlitschek frank@owncloud.org + * @copyright 2013 Jakob Sack * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE @@ -76,6 +78,8 @@ class OC_L10N{ /** * get an L10N instance + * @param $app string + * @param $lang string|null * @return OC_L10N */ public static function get($app, $lang=null) { @@ -91,8 +95,8 @@ class OC_L10N{ /** * @brief The constructor - * @param $app the app requesting l10n - * @param $lang default: null Language + * @param $app string app requesting l10n + * @param $lang string default: null Language * @returns OC_L10N-Object * * If language is not set, the constructor tries to find the right @@ -168,15 +172,14 @@ class OC_L10N{ /** * @brief Creates a function that The constructor - * @param $app the app requesting l10n - * @param $lang default: null Language - * @returns OC_L10N-Object * * If language is not set, the constructor tries to find the right * language. * * Parts of the code is copied from Habari: * https://github.com/habari/system/blob/master/classes/locale.php + * @param $string string + * @return string */ protected function createPluralFormFunction($string){ if(preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) { @@ -190,7 +193,7 @@ class OC_L10N{ 'nplurals='. $nplurals . '; plural=' . $plural ); - // add parens + // add parents // important since PHP's ternary evaluates from left to right $body .= ';'; $res = ''; @@ -305,7 +308,7 @@ class OC_L10N{ /** * @brief getPluralFormString - * @returns Fetches the gettext "Plural-Forms"-string + * @returns string containing the gettext "Plural-Forms"-string * * Returns a string like "nplurals=2; plural=(n != 1);" */ @@ -316,14 +319,14 @@ class OC_L10N{ /** * @brief getPluralFormFunction - * @returns returns the plural form function + * @returns string the plural form function * * returned function accepts the argument $n */ public function getPluralFormFunction() { $this->init(); if(is_null($this->plural_form_function)) { - $this->plural_form_function = createPluralFormFunction($this->plural_form_string); + $this->plural_form_function = $this->createPluralFormFunction($this->plural_form_string); } return $this->plural_form_function; } @@ -369,8 +372,12 @@ class OC_L10N{ case 'date': case 'datetime': case 'time': - if($data instanceof DateTime) return $data->format($this->localizations[$type]); - elseif(is_string($data)) $data = strtotime($data); + if($data instanceof DateTime) { + return $data->format($this->localizations[$type]); + } + elseif(is_string($data)) { + $data = strtotime($data); + } $locales = array(self::findLanguage()); if (strlen($locales[0]) == 2) { $locales[] = $locales[0].'_'.strtoupper($locales[0]); From 550b647d8a777c11b74377002d58b4806f9e97e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Fri, 2 Aug 2013 23:07:53 +0200 Subject: [PATCH 15/20] another new line --- l10n/l10n.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l10n/l10n.pl b/l10n/l10n.pl index e3a42aefe2..2790ca9201 100644 --- a/l10n/l10n.pl +++ b/l10n/l10n.pl @@ -171,7 +171,7 @@ elsif( $task eq 'write' ){ # Write PHP file open( OUT, ">$language.php" ); - print OUT " Date: Fri, 2 Aug 2013 23:08:41 +0200 Subject: [PATCH 16/20] unit tests for plural translations added --- lib/l10n.php | 16 ++++++++++-- lib/l10n/string.php | 26 +++++++++++++++++--- tests/data/l10n/cs.php | 5 ++++ tests/data/l10n/de.php | 5 ++++ tests/data/l10n/ru.php | 5 ++++ tests/lib/l10n.php | 55 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 tests/data/l10n/cs.php create mode 100644 tests/data/l10n/de.php create mode 100644 tests/data/l10n/ru.php create mode 100644 tests/lib/l10n.php diff --git a/lib/l10n.php b/lib/l10n.php index 0e47215369..a11ed785c5 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -25,7 +25,7 @@ /** * This class is for i18n and l10n */ -class OC_L10N{ +class OC_L10N { /** * cached instances */ @@ -107,6 +107,17 @@ class OC_L10N{ $this->lang = $lang; } + public function load($transFile) { + $this->app = true; + include $transFile; + if(isset($TRANSLATIONS) && is_array($TRANSLATIONS)) { + $this->translations = $TRANSLATIONS; + } + if(isset($PLURAL_FORMS)) { + $this->plural_form_string = $PLURAL_FORMS; + } + } + protected function init() { if ($this->app === true) { return; @@ -258,8 +269,9 @@ class OC_L10N{ * */ public function n($text_singular, $text_plural, $count, $parameters = array()) { + $this->init(); $identifier = "_${text_singular}__${text_plural}_"; - if( array_key_exists($this->translations, $identifier)) { + if( array_key_exists($identifier, $this->translations)) { return new OC_L10N_String( $this, $identifier, $parameters, $count ); } else{ diff --git a/lib/l10n/string.php b/lib/l10n/string.php index c78b06428d..88c85b32e7 100644 --- a/lib/l10n/string.php +++ b/lib/l10n/string.php @@ -7,30 +7,48 @@ */ class OC_L10N_String{ + /** + * @var OC_L10N + */ protected $l10n; + + /** + * @var string + */ + protected $text; + + /** + * @var array + */ + protected $parameters; + + /** + * @var integer + */ + protected $count; + public function __construct($l10n, $text, $parameters, $count = 1) { $this->l10n = $l10n; $this->text = $text; $this->parameters = $parameters; $this->count = $count; - } public function __toString() { $translations = $this->l10n->getTranslations(); - $localizations = $this->l10n->getLocalizations(); $text = $this->text; if(array_key_exists($this->text, $translations)) { if(is_array($translations[$this->text])) { - $fn = $l10n->getPluralFormFunction(); - $id = $fn($count); + $fn = $this->l10n->getPluralFormFunction(); + $id = $fn($this->count); $text = $translations[$this->text][$id]; } else{ $text = $translations[$this->text]; } } + // Replace %n first (won't interfere with vsprintf) $text = str_replace('%n', $this->count, $text); return vsprintf($text, $this->parameters); diff --git a/tests/data/l10n/cs.php b/tests/data/l10n/cs.php new file mode 100644 index 0000000000..1c5907bc14 --- /dev/null +++ b/tests/data/l10n/cs.php @@ -0,0 +1,5 @@ + array("%n okno", "%n okna", "%n oken") +); +$PLURAL_FORMS = "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"; diff --git a/tests/data/l10n/de.php b/tests/data/l10n/de.php new file mode 100644 index 0000000000..858ec8af49 --- /dev/null +++ b/tests/data/l10n/de.php @@ -0,0 +1,5 @@ + array("%n Datei", "%n Dateien") +); +$PLURAL_FORMS = "nplurals=2; plural=(n != 1);"; diff --git a/tests/data/l10n/ru.php b/tests/data/l10n/ru.php new file mode 100644 index 0000000000..dd46293db6 --- /dev/null +++ b/tests/data/l10n/ru.php @@ -0,0 +1,5 @@ + array("%n файл", "%n файла", "%n файлов") +); +$PLURAL_FORMS = "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"; diff --git a/tests/lib/l10n.php b/tests/lib/l10n.php new file mode 100644 index 0000000000..846a2f22c8 --- /dev/null +++ b/tests/lib/l10n.php @@ -0,0 +1,55 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +class Test_L10n extends PHPUnit_Framework_TestCase { + + public function testGermanPluralTranslations() { + $l = new OC_L10N('test'); + $transFile = OC::$SERVERROOT.'/tests/data/l10n/de.php'; + + $l->load($transFile); + $this->assertEquals('1 Datei', (string)$l->n('%n file', '%n files', 1, array(1))); + $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 2, array(2))); + } + + public function testRussianPluralTranslations() { + $l = new OC_L10N('test'); + $transFile = OC::$SERVERROOT.'/tests/data/l10n/ru.php'; + + $l->load($transFile); + $this->assertEquals('1 файл', (string)$l->n('%n file', '%n files', 1)); + $this->assertEquals('2 файла', (string)$l->n('%n file', '%n files', 2)); + $this->assertEquals('6 файлов', (string)$l->n('%n file', '%n files', 6)); + $this->assertEquals('21 файл', (string)$l->n('%n file', '%n files', 21)); + $this->assertEquals('22 файла', (string)$l->n('%n file', '%n files', 22)); + $this->assertEquals('26 файлов', (string)$l->n('%n file', '%n files', 26)); + + /* + 1 file 1 файл 1 папка + 2-4 files 2-4 файла 2-4 папки + 5-20 files 5-20 файлов 5-20 папок + 21 files 21 файл 21 папка + 22-24 files 22-24 файла 22-24 папки + 25-30 files 25-30 файлов 25-30 папок + etc + 100 files 100 файлов, 100 папок + 1000 files 1000 файлов 1000 папок + */ + } + + public function testCzechPluralTranslations() { + $l = new OC_L10N('test'); + $transFile = OC::$SERVERROOT.'/tests/data/l10n/cs.php'; + + $l->load($transFile); + $this->assertEquals('1 okno', (string)$l->n('%n window', '%n windows', 1)); + $this->assertEquals('2 okna', (string)$l->n('%n window', '%n windows', 2)); + $this->assertEquals('5 oken', (string)$l->n('%n window', '%n windows', 5)); + } + +} From 2e5f1142be7101ccca0b45539d585df95dc61116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 3 Aug 2013 23:27:18 +0200 Subject: [PATCH 17/20] return plural form in ajax translations --- core/ajax/translations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ajax/translations.php b/core/ajax/translations.php index c9c6420779..e829453dbc 100644 --- a/core/ajax/translations.php +++ b/core/ajax/translations.php @@ -27,4 +27,4 @@ $app = OC_App::cleanAppId($app); $l = OC_L10N::get( $app ); -OC_JSON::success(array('data' => $l->getTranslations())); +OC_JSON::success(array('data' => $l->getTranslations(), 'plural_form' => $l->getPluralFormString())); From b229381dda7203402e00cb6f58c3b8a147556b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 3 Aug 2013 23:29:31 +0200 Subject: [PATCH 18/20] initialize $plural_form_string with English default --- lib/l10n.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n.php b/lib/l10n.php index a11ed785c5..b1f2fceeb1 100644 --- a/lib/l10n.php +++ b/lib/l10n.php @@ -59,7 +59,7 @@ class OC_L10N { /** * Plural forms (string) */ - private $plural_form_string; + private $plural_form_string = 'nplurals=2; plural=(n != 1);'; /** * Plural forms (function) From d97e3c3571acb2ea88623384f4b17ced41dc0be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 3 Aug 2013 23:31:49 +0200 Subject: [PATCH 19/20] adding proper evaluation of plural form - uses LGPL licensed code of jsgettext --- core/js/js.js | 79 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/core/js/js.js b/core/js/js.js index 37540bd15e..b687c408bc 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -24,6 +24,55 @@ if (oc_debug !== true || typeof console === "undefined" || typeof console.log == } } +function initL10N(app) { + if (!( t.cache[app] )) { + $.ajax(OC.filePath('core', 'ajax', 'translations.php'), { + async: false,//todo a proper solution for this without sync ajax calls + data: {'app': app}, + type: 'POST', + success: function (jsondata) { + t.cache[app] = jsondata.data; + t.plural_form = jsondata.plural_form; + } + }); + + // Bad answer ... + if (!( t.cache[app] )) { + t.cache[app] = []; + } + } + if (typeof t.plural_function == 'undefined') { + t.plural_function = function (n) { + var p = (n != 1) ? 1 : 0; + return { 'nplural' : 2, 'plural' : p }; + }; + + /** + * code below has been taken from jsgettext - which is LGPL licensed + * https://developer.berlios.de/projects/jsgettext/ + * http://cvs.berlios.de/cgi-bin/viewcvs.cgi/jsgettext/jsgettext/lib/Gettext.js + */ + var pf_re = new RegExp('^(\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;a-zA-Z0-9_\(\)])+)', 'm'); + if (pf_re.test(t.plural_form)) { + //ex english: "Plural-Forms: nplurals=2; plural=(n != 1);\n" + //pf = "nplurals=2; plural=(n != 1);"; + //ex russian: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 or n%100>=20) ? 1 : 2) + //pf = "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)"; + var pf = t.plural_form; + if (! /;\s*$/.test(pf)) pf = pf.concat(';'); + /* We used to use eval, but it seems IE has issues with it. + * We now use "new Function", though it carries a slightly + * bigger performance hit. + var code = 'function (n) { var plural; var nplurals; '+pf+' return { "nplural" : nplurals, "plural" : (plural === true ? 1 : plural ? plural : 0) }; };'; + Gettext._locale_data[domain].head.plural_func = eval("("+code+")"); + */ + var code = 'var plural; var nplurals; '+pf+' return { "nplural" : nplurals, "plural" : (plural === true ? 1 : plural ? plural : 0) };'; + t.plural_function = new Function("n", code); + } else { + console.log("Syntax error in language file. Plural-Forms header is invalid ["+plural_forms+"]"); + } + } +} /** * translate a string * @param app the id of the app for which to translate the string @@ -33,21 +82,7 @@ if (oc_debug !== true || typeof console === "undefined" || typeof console.log == * @return string */ function t(app, text, vars, count){ - if( !( t.cache[app] )) { - $.ajax(OC.filePath('core','ajax','translations.php'),{ - async:false,//todo a proper sollution for this without sync ajax calls - data:{'app': app}, - type:'POST', - success:function(jsondata){ - t.cache[app] = jsondata.data; - } - }); - - // Bad answer ... - if( !( t.cache[app] )){ - t.cache[app] = []; - } - } + initL10N(app); var _build = function (text, vars, count) { return text.replace(/%n/g, count).replace(/{([^{}]*)}/g, function (a, b) { @@ -67,7 +102,7 @@ function t(app, text, vars, count){ return translation; } } -t.cache={}; +t.cache = {}; /** * translate a string @@ -78,7 +113,17 @@ t.cache={}; * @param vars (optional) FIXME * @return string */ -function n(app, text_singular, text_plural, count, vars){ +function n(app, text_singular, text_plural, count, vars) { + initL10N(app); + var identifier = '_' + text_singular + '__' + text_plural + '_'; + if( typeof( t.cache[app][identifier] ) !== 'undefined' ){ + var translation = t.cache[app][identifier]; + if ($.isArray(translation)) { + var plural = t.plural_function(count); + return t(app, translation[plural.plural], vars, count); + } + } + if(count === 1) { return t(app, text_singular, vars, count); } From 19e3780ef24113bcb7950b5671e4c0149271c5c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Mon, 5 Aug 2013 10:21:51 +0200 Subject: [PATCH 20/20] remove unneeded vars from unit test --- tests/lib/l10n.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/lib/l10n.php b/tests/lib/l10n.php index 846a2f22c8..dfc5790c2e 100644 --- a/tests/lib/l10n.php +++ b/tests/lib/l10n.php @@ -13,8 +13,8 @@ class Test_L10n extends PHPUnit_Framework_TestCase { $transFile = OC::$SERVERROOT.'/tests/data/l10n/de.php'; $l->load($transFile); - $this->assertEquals('1 Datei', (string)$l->n('%n file', '%n files', 1, array(1))); - $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 2, array(2))); + $this->assertEquals('1 Datei', (string)$l->n('%n file', '%n files', 1)); + $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 2)); } public function testRussianPluralTranslations() {