Merge pull request #4271 from owncloud/plural_translations

Plural translations
This commit is contained in:
Owen Winkler 2013-08-08 10:34:28 -07:00
commit a2ac5e0163
9 changed files with 409 additions and 53 deletions

View File

@ -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()));

View File

@ -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];
}
else{
if(typeof vars === 'object') {
return _build(text, vars);
} else {
return text;
}
if(typeof vars === 'object' || count !== undefined ) {
return _build(translation, vars, count);
} else {
return translation;
}
}
t.cache={};
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{
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('&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]]={};

View File

@ -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 );

View File

@ -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
*/
@ -54,6 +56,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
*/
@ -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')) {
@ -153,6 +181,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
@ -166,6 +253,37 @@ 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.
*
* 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
@ -200,6 +318,42 @@ class OC_L10N{
return $this->translations;
}
/**
* @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
@ -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]);

View File

@ -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);
}
}

5
tests/data/l10n/cs.php Normal file
View File

@ -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;";

5
tests/data/l10n/de.php Normal file
View File

@ -0,0 +1,5 @@
<?php
$TRANSLATIONS = array(
"_%n file__%n files_" => array("%n Datei", "%n Dateien")
);
$PLURAL_FORMS = "nplurals=2; plural=(n != 1);";

5
tests/data/l10n/ru.php Normal file
View File

@ -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);";

55
tests/lib/l10n.php Normal file
View File

@ -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));
}
}