summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/ajax/translations.php2
-rw-r--r--core/js/js.js125
-rw-r--r--l10n/l10n.pl54
-rw-r--r--lib/l10n.php168
-rw-r--r--lib/l10n/string.php39
-rw-r--r--tests/data/l10n/cs.php5
-rw-r--r--tests/data/l10n/de.php5
-rw-r--r--tests/data/l10n/ru.php5
-rw-r--r--tests/lib/l10n.php55
9 files changed, 407 insertions, 51 deletions
diff --git a/core/ajax/translations.php b/core/ajax/translations.php
index c9c64207798..e829453dbcc 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()));
diff --git a/core/js/js.js b/core/js/js.js
index 03f660be62c..1d1711383f7 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
@@ -24,60 +24,121 @@ 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
- * @return string
- */
-function t(app,text, vars){
- 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){
+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] )){
+ if (!( t.cache[app] )) {
t.cache[app] = [];
}
}
- var _build = function (text, vars) {
- return text.replace(/{([^{}]*)}/g,
+ 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
+ * @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, count){
+ initL10N(app);
+ var _build = function (text, vars, count) {
+ return text.replace(/%n/g, count).replace(/{([^{}]*)}/g,
function (a, b) {
var r = vars[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
}
);
};
+ 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];
+ }
+
+ if(typeof vars === 'object' || 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 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);
+ }
else{
- if(typeof vars === 'object') {
- return _build(text, vars);
- } else {
- return text;
- }
+ return t(app, text_plural, vars, count);
}
}
-t.cache={};
-/*
+/**
* Sanitizes a HTML string
-* @param string
+* @param s string
* @return Sanitized string
*/
function escapeHTML(s) {
- return s.toString().split('&').join('&amp;').split('<').join('&lt;').split('"').join('&quot;');
+ return s.toString().split('&').join('&amp;').split('<').join('&lt;').split('"').join('&quot;');
}
/**
@@ -773,7 +834,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){
@@ -792,7 +853,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]]={};
diff --git a/l10n/l10n.pl b/l10n/l10n.pl
index b07d6d686bc..2790ca92015 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 ));
}
@@ -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 = '..';
@@ -100,11 +110,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=n:2,3';
+ }
+ else{
+ $keywords = '--keyword=t --keyword=n: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 +134,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;
@@ -126,18 +142,38 @@ elsif( $task eq 'write' ){
my $array = Locale::PO->load_file_asarray( $input );
# Create array
my @strings = ();
+ my $plurals;
+
foreach my $string ( @{$array} ){
- next if $string->msgid() eq '""';
- next if $string->msgstr() eq '""';
- push( @strings, $string->msgid()." => ".$string->msgstr());
+ 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;
+
+ foreach my $variant ( sort { $a <=> $b} keys( %{$string->msgstr_n()} )){
+ push( @variants, $string->msgstr_n()->{$variant} );
+ }
+
+ push( @strings, "\"$identifier\" => array(".join(@variants, ",").")");
+ }
+ else{
+ # singular translations
+ next if $string->msgstr() eq '""';
+ push( @strings, $string->msgid()." => ".$string->msgstr());
+ }
}
next if $#strings == -1; # Skip empty files
# Write PHP file
open( OUT, ">$language.php" );
- print OUT "<?php \$TRANSLATIONS = array(\n";
+ print OUT "<?php\n\$TRANSLATIONS = array(\n";
print OUT join( ",\n", @strings );
- print OUT "\n);\n";
+ print OUT "\n);\n\$PLURAL_FORMS = \"$plurals\";\n";
close( OUT );
}
chdir( $whereami );
diff --git a/lib/l10n.php b/lib/l10n.php
index a28aa89c5f1..d2da302b644 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
@@ -23,7 +25,7 @@
/**
* This class is for i18n and l10n
*/
-class OC_L10N{
+class OC_L10N {
/**
* cached instances
*/
@@ -55,6 +57,16 @@ class OC_L10N{
private $translations = array();
/**
+ * Plural forms (string)
+ */
+ private $plural_form_string = 'nplurals=2; plural=(n != 1);';
+
+ /**
+ * Plural forms (function)
+ */
+ private $plural_form_function = null;
+
+ /**
* Localization
*/
private $localizations = array(
@@ -66,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) {
@@ -81,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
@@ -93,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;
@@ -138,6 +163,9 @@ class OC_L10N{
}
}
}
+ if(isset($PLURAL_FORMS)) {
+ $this->plural_form_string = $PLURAL_FORMS;
+ }
}
if(file_exists(OC::$SERVERROOT.'/core/l10n/l10n-'.$lang.'.php')) {
@@ -154,6 +182,65 @@ class OC_L10N{
}
/**
+ * @brief Creates a function that The constructor
+ *
+ * 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)) {
+ // 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 parents
+ // 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
* @param array $parameters default:array() Parameters for sprintf
@@ -168,6 +255,37 @@ class OC_L10N{
/**
* @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.
+ *
+ * The correct plural is determined by the plural_forms-function
+ * provided by the po file.
+ *
+ */
+ public function n($text_singular, $text_plural, $count, $parameters = array()) {
+ $this->init();
+ $identifier = "_${text_singular}__${text_plural}_";
+ if( array_key_exists($identifier, $this->translations)) {
+ return new OC_L10N_String( $this, $identifier, $parameters, $count );
+ }
+ else{
+ 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
* @returns Translation or the same text
*
@@ -201,6 +319,42 @@ class OC_L10N{
}
/**
+ * @brief getPluralFormString
+ * @returns string containing the gettext "Plural-Forms"-string
+ *
+ * Returns a string like "nplurals=2; plural=(n != 1);"
+ */
+ public function getPluralFormString() {
+ $this->init();
+ return $this->plural_form_string;
+ }
+
+ /**
+ * @brief getPluralFormFunction
+ * @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 = $this->createPluralFormFunction($this->plural_form_string);
+ }
+ return $this->plural_form_function;
+ }
+
+ /**
+ * @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
* @param $params parameters for this localization
@@ -230,8 +384,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]);
diff --git a/lib/l10n/string.php b/lib/l10n/string.php
index 8eef10071e6..88c85b32e70 100644
--- a/lib/l10n/string.php
+++ b/lib/l10n/string.php
@@ -7,19 +7,50 @@
*/
class OC_L10N_String{
+ /**
+ * @var OC_L10N
+ */
protected $l10n;
- public function __construct($l10n, $text, $parameters) {
+
+ /**
+ * @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();
+
+ $text = $this->text;
if(array_key_exists($this->text, $translations)) {
- return vsprintf($translations[$this->text], $this->parameters);
+ if(is_array($translations[$this->text])) {
+ $fn = $this->l10n->getPluralFormFunction();
+ $id = $fn($this->count);
+ $text = $translations[$this->text][$id];
+ }
+ else{
+ $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);
}
}
diff --git a/tests/data/l10n/cs.php b/tests/data/l10n/cs.php
new file mode 100644
index 00000000000..1c5907bc148
--- /dev/null
+++ b/tests/data/l10n/cs.php
@@ -0,0 +1,5 @@
+<?php
+$TRANSLATIONS = array(
+ "_%n window__%n windows_" => 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 00000000000..858ec8af49c
--- /dev/null
+++ b/tests/data/l10n/de.php
@@ -0,0 +1,5 @@
+<?php
+$TRANSLATIONS = array(
+ "_%n file__%n files_" => 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 00000000000..dd46293db6c
--- /dev/null
+++ b/tests/data/l10n/ru.php
@@ -0,0 +1,5 @@
+<?php
+$TRANSLATIONS = array(
+ "_%n file__%n files_" => 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 00000000000..dfc5790c2e7
--- /dev/null
+++ b/tests/lib/l10n.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Copyright (c) 2013 Thomas Müller <thomas.mueller@tmit.eu>
+ * 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));
+ $this->assertEquals('2 Dateien', (string)$l->n('%n file', '%n files', 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));
+ }
+
+}