Browse Source

Added OC.L10N namespace with translation functions

Added addTranslations and fixed de.js file

Fixed de.js to use OC.L10N.register() and use to correct expected
format.

Added JS unit tests for OC.L10N class

Include translations JS script for all apps
tags/v8.0.0alpha1
Vincent Petry 9 years ago
parent
commit
ec1a73fab9

+ 1
- 0
apps/files/index.php View File

@@ -28,6 +28,7 @@ OCP\User::checkLoggedIn();
OCP\Util::addStyle('files', 'files');
OCP\Util::addStyle('files', 'upload');
OCP\Util::addStyle('files', 'mobile');
OCP\Util::addTranslations('files');
OCP\Util::addscript('files', 'app');
OCP\Util::addscript('files', 'file-upload');
OCP\Util::addscript('files', 'jquery.iframe-transport');

+ 1
- 0
apps/files_encryption/appinfo/app.php View File

@@ -14,6 +14,7 @@ OC::$CLASSPATH['OCA\Encryption\Helper'] = 'files_encryption/lib/helper.php';
OC::$CLASSPATH['OCA\Encryption\Exceptions\MultiKeyEncryptException'] = 'files_encryption/lib/exceptions.php';
OC::$CLASSPATH['OCA\Encryption\Exceptions\MultiKeyDecryptException'] = 'files_encryption/lib/exceptions.php';

\OCP\Util::addTranslations('files_encryption');
\OCP\Util::addscript('files_encryption', 'encryption');
\OCP\Util::addscript('files_encryption', 'detect-migration');


+ 2
- 0
apps/files_external/appinfo/app.php View File

@@ -21,6 +21,8 @@ OC::$CLASSPATH['OC\Files\Storage\SFTP'] = 'files_external/lib/sftp.php';
OC::$CLASSPATH['OC_Mount_Config'] = 'files_external/lib/config.php';
OC::$CLASSPATH['OCA\Files\External\Api'] = 'files_external/lib/api.php';

OCP\Util::addTranslations('files_external');

OCP\App::registerAdmin('files_external', 'settings');
if (OCP\Config::getAppValue('files_external', 'allow_user_mounting', 'yes') == 'yes') {
OCP\App::registerPersonal('files_external', 'personal');

+ 1
- 0
apps/files_sharing/appinfo/app.php View File

@@ -22,6 +22,7 @@ OC::$CLASSPATH['OCA\Files_Sharing\Exceptions\BrokenPath'] = 'files_sharing/lib/e
OCP\Share::registerBackend('file', 'OC_Share_Backend_File');
OCP\Share::registerBackend('folder', 'OC_Share_Backend_Folder', 'file');

OCP\Util::addTranslations('files_sharing');
OCP\Util::addScript('files_sharing', 'share');
OCP\Util::addScript('files_sharing', 'external');


+ 2
- 0
apps/files_trashbin/appinfo/app.php View File

@@ -1,6 +1,8 @@
<?php
$l = \OC::$server->getL10N('files_trashbin');

OCP\Util::addTranslations('files_trashbin');

OC::$CLASSPATH['OCA\Files_Trashbin\Exceptions\CopyRecursiveException'] = 'files_trashbin/lib/exceptions.php';

// register hooks

+ 1
- 0
apps/files_versions/appinfo/app.php View File

@@ -5,6 +5,7 @@ OC::$CLASSPATH['OCA\Files_Versions\Storage'] = 'files_versions/lib/versions.php'
OC::$CLASSPATH['OCA\Files_Versions\Hooks'] = 'files_versions/lib/hooks.php';
OC::$CLASSPATH['OCA\Files_Versions\Capabilities'] = 'files_versions/lib/capabilities.php';

OCP\Util::addTranslations('files_versions');
OCP\Util::addscript('files_versions', 'versions');
OCP\Util::addStyle('files_versions', 'versions');


+ 1
- 0
apps/user_ldap/appinfo/app.php View File

@@ -54,6 +54,7 @@ $entry = array(
'href' => OCP\Util::linkTo( 'user_ldap', 'settings.php' ),
'name' => 'LDAP'
);
OCP\Util::addTranslations('user_ldap');

OCP\Backgroundjob::registerJob('OCA\user_ldap\lib\Jobs');
if(OCP\App::isEnabled('user_webdavauth')) {

+ 2
- 0
apps/user_webdavauth/appinfo/app.php View File

@@ -28,6 +28,8 @@ OC_APP::registerAdmin('user_webdavauth', 'settings');
OC_User::registerBackend("WEBDAVAUTH");
OC_User::useBackend( "WEBDAVAUTH" );

OCP\Util::addTranslations('user_webdavauth');

// add settings page to navigation
$entry = array(
'id' => "user_webdavauth_settings",

+ 1
- 0
core/js/core.json View File

@@ -13,6 +13,7 @@
"jquery.ocdialog.js",
"oc-dialogs.js",
"js.js",
"l10n.js",
"share.js",
"octemplate.js",
"eventsource.js",

+ 6
- 119
core/js/js.js View File

@@ -37,121 +37,6 @@ if (
}
}

function initL10N(app) {
if (!( t.cache[app] )) {
$.ajax(OC.filePath('core', 'ajax', 'translations.php'), {
// TODO a proper solution for this without sync ajax calls
async: false,
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[app] === 'undefined') {
t.plural_function[app] = 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[app] = new Function("n", code);
} else {
console.log("Syntax error in language file. Plural-Forms header is invalid ["+t.plural_forms+"]");
}
}
}
/**
* translate a string
* @param {string} app the id of the app for which to translate the string
* @param {string} text the string to translate
* @param [vars] FIXME
* @param {number} [count] 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' ){
translation = t.cache[app][text];
}

if(typeof vars === 'object' || count !== undefined ) {
return _build(translation, vars, count);
} else {
return translation;
}
}
t.cache = {};
// different apps might or might not redefine the nplurals function correctly
// this is to make sure that a "broken" app doesn't mess up with the
// other app's plural function
t.plural_function = {};

/**
* translate a string
* @param {string} app the id of the app for which to translate the string
* @param {string} text_singular the string to translate for exactly one object
* @param {string} text_plural the string to translate for n objects
* @param {number} count number to determine whether to use singular or plural
* @param [vars] FIXME
* @return {string} Translated 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[app](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 by replacing all potential dangerous characters with HTML entities
* @param {string} s String to sanitize
@@ -584,11 +469,13 @@ OC.search.currentResult=-1;
OC.search.lastQuery='';
OC.search.lastResults={};
//translations for result type ids, can be extended by apps
// FIXME: move to later in the init process, after translations were loaded

OC.search.resultTypes={
file: t('core','File'),
folder: t('core','Folder'),
image: t('core','Image'),
audio: t('core','Audio')
file: 'File', //t('core','File'),
folder: 'Folder', //t('core','Folder'),
image: 'Image', //t('core','Image'),
audio: 'Audio' //t('core','Audio')
};
OC.addStyle.loaded=[];
OC.addScript.loaded=[];

+ 178
- 0
core/js/l10n.js View File

@@ -0,0 +1,178 @@
/**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/

/**
* L10N namespace with localization functions.
*
* @namespace
*/
OC.L10N = {
/**
* String bundles with app name as key.
* @type {Object.<String,String>}
*/
_bundles: {},

/**
* Plural functions, key is app name and value is function.
* @type {Object.<String,Function>}
*/
_pluralFunctions: {},

/**
* Register an app's translation bundle.
*
* @param {String} appName name of the app
* @param {Object<String,String>} strings bundle
* @param {{Function|String}} [pluralForm] optional plural function or plural string
*/
register: function(appName, bundle, pluralForm) {
this._bundles[appName] = bundle || {};

if (_.isFunction(pluralForm)) {
this._pluralFunctions[appName] = pluralForm;
} else {
// generate plural function based on form
this._pluralFunctions[appName] = this._generatePluralFunction(pluralForm);
}
},

/**
* Generates a plural function based on the given plural form.
* If an invalid form has been given, returns a default function.
*
* @param {String} pluralForm plural form
*/
_generatePluralFunction: function(pluralForm) {
// default func
var func = function (n) {
var p = (n !== 1) ? 1 : 0;
return { 'nplural' : 2, 'plural' : p };
};

if (!pluralForm) {
console.warn('Missing plural form in language file');
return func;
}

/**
* 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(pluralForm)) {
//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 = pluralForm;
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) };';
func = new Function("n", code);
} else {
console.warn('Invalid plural form in language file: "' + pluralForm + '"');
}
return func;
},

/**
* Translate a string
* @param {string} app the id of the app for which to translate the string
* @param {string} text the string to translate
* @param [vars] map of placeholder key to value
* @param {number} [count] number to replace %n with
* @return {string}
*/
translate: function(app, text, vars, count) {
// TODO: cache this function to avoid inline recreation
// of the same function over and over again in case
// translate() is used in a loop
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;
var bundle = this._bundles[app] || {};
var value = bundle[text];
if( typeof(value) !== 'undefined' ){
translation = value;
}

if(typeof vars === 'object' || count !== undefined ) {
return _build(translation, vars, count);
} else {
return translation;
}
},

/**
* Translate a plural string
* @param {string} app the id of the app for which to translate the string
* @param {string} text_singular the string to translate for exactly one object
* @param {string} text_plural the string to translate for n objects
* @param {number} count number to determine whether to use singular or plural
* @param [vars] map of placeholder key to value
* @return {string} Translated string
*/
translatePlural: function(app, textSingular, textPlural, count, vars) {
var identifier = '_' + textSingular + '_::_' + textPlural + '_';
var bundle = this._bundles[app] || {};
var value = bundle[identifier];
if( typeof(value) !== 'undefined' ){
var translation = value;
if ($.isArray(translation)) {
var plural = this._pluralFunctions[app](count);
return this.translate(app, translation[plural.plural], vars, count);
}
}

if(count === 1) {
return this.translate(app, textSingular, vars, count);
}
else{
return this.translate(app, textPlural, vars, count);
}
}
};

/**
* translate a string
* @param {string} app the id of the app for which to translate the string
* @param {string} text the string to translate
* @param [vars] map of placeholder key to value
* @param {number} [count] number to replace %n with
* @return {string}
*/
window.t = _.bind(OC.L10N.translate, OC.L10N);

/**
* translate a string
* @param {string} app the id of the app for which to translate the string
* @param {string} text_singular the string to translate for exactly one object
* @param {string} text_plural the string to translate for n objects
* @param {number} count number to determine whether to use singular or plural
* @param [vars] map of placeholder key to value
* @return {string} Translated string
*/
window.n = _.bind(OC.L10N.translatePlural, OC.L10N);


+ 0
- 9
core/js/tests/specHelper.js View File

@@ -113,15 +113,6 @@ window.isPhantom = /phantom/i.test(navigator.userAgent);
// must use fake responses for expected calls
fakeServer = sinon.fakeServer.create();

// return fake translations as they might be requested for many test runs
fakeServer.respondWith(/\/index.php\/core\/ajax\/translations.php$/, [
200, {
"Content-Type": "application/json"
},
'{"data": [], "plural_form": "nplurals=2; plural=(n != 1);"}'
]
);

// make it globally available, so that other tests can define
// custom responses
window.fakeServer = fakeServer;

+ 101
- 0
core/js/tests/specs/l10nSpec.js View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/

describe('OC.L10N tests', function() {
var TEST_APP = 'jsunittestapp';

afterEach(function() {
delete OC.L10N._bundles[TEST_APP];
});

describe('text translation', function() {
beforeEach(function() {
OC.L10N.register(TEST_APP, {
'Hello world!': 'Hallo Welt!',
'Hello {name}, the weather is {weather}': 'Hallo {name}, das Wetter ist {weather}',
'sunny': 'sonnig'
});
});
it('returns untranslated text when no bundle exists', function() {
delete OC.L10N._bundles[TEST_APP];
expect(t(TEST_APP, 'unknown text')).toEqual('unknown text');
});
it('returns untranslated text when no key exists', function() {
expect(t(TEST_APP, 'unknown text')).toEqual('unknown text');
});
it('returns translated text when key exists', function() {
expect(t(TEST_APP, 'Hello world!')).toEqual('Hallo Welt!');
});
it('returns translated text with placeholder', function() {
expect(
t(TEST_APP, 'Hello {name}, the weather is {weather}', {name: 'Steve', weather: t(TEST_APP, 'sunny')})
).toEqual('Hallo Steve, das Wetter ist sonnig');
});
});
describe('plurals', function() {
function checkPlurals() {
expect(
n(TEST_APP, 'download %n file', 'download %n files', 0)
).toEqual('0 Dateien herunterladen');
expect(
n(TEST_APP, 'download %n file', 'download %n files', 1)
).toEqual('1 Datei herunterladen');
expect(
n(TEST_APP, 'download %n file', 'download %n files', 2)
).toEqual('2 Dateien herunterladen');
expect(
n(TEST_APP, 'download %n file', 'download %n files', 1024)
).toEqual('1024 Dateien herunterladen');
}

it('generates plural for default text when translation does not exist', function() {
OC.L10N.register(TEST_APP, {
});
expect(
n(TEST_APP, 'download %n file', 'download %n files', 0)
).toEqual('download 0 files');
expect(
n(TEST_APP, 'download %n file', 'download %n files', 1)
).toEqual('download 1 file');
expect(
n(TEST_APP, 'download %n file', 'download %n files', 2)
).toEqual('download 2 files');
expect(
n(TEST_APP, 'download %n file', 'download %n files', 1024)
).toEqual('download 1024 files');
});
it('generates plural with default function when no forms specified', function() {
OC.L10N.register(TEST_APP, {
'_download %n file_::_download %n files_':
['%n Datei herunterladen', '%n Dateien herunterladen']
});
checkPlurals();
});
it('generates plural with generated function when forms is specified', function() {
OC.L10N.register(TEST_APP, {
'_download %n file_::_download %n files_':
['%n Datei herunterladen', '%n Dateien herunterladen']
}, 'nplurals=2; plural=(n != 1);');
checkPlurals();
});
it('generates plural with function when forms is specified as function', function() {
OC.L10N.register(TEST_APP, {
'_download %n file_::_download %n files_':
['%n Datei herunterladen', '%n Dateien herunterladen']
}, function(n) {
return {
nplurals: 2,
plural: (n !== 1) ? 1 : 0
};
});
checkPlurals();
});
});
});

+ 1
- 0
lib/base.php View File

@@ -338,6 +338,7 @@ class OC {
OC_Util::addScript("jquery.ocdialog");
OC_Util::addScript("oc-dialogs");
OC_Util::addScript("js");
OC_Util::addScript("l10n");
OC_Util::addScript("octemplate");
OC_Util::addScript("eventsource");
OC_Util::addScript("config");

+ 16
- 0
lib/private/template/functions.php View File

@@ -55,6 +55,22 @@ function style($app, $file) {
}
}

/**
* Shortcut for adding translations to a page
* @param string $app the appname
* @param string|string[] $file the filename,
* if an array is given it will add all styles
*/
function translation($app, $file) {
if(is_array($file)) {
foreach($file as $f) {
OC_Util::addStyle($app, $f);
}
} else {
OC_Util::addStyle($app, $file);
}
}

/**
* Shortcut for HTML imports
* @param string $app the appname

+ 20
- 2
lib/private/util.php View File

@@ -333,7 +333,7 @@ class OC_Util {
/**
* add a javascript file
*
* @param string $application
* @param string $application application id
* @param string|null $file filename
* @return void
*/
@@ -349,10 +349,28 @@ class OC_Util {
}
}

/**
* add a translation JS file
*
* @param string $application application id
* @param string $languageCode language code, defaults to the current language
*/
public static function addTranslations($application, $languageCode = null) {
if (is_null($languageCode)) {
$l = new \OC_L10N($application);
$languageCode = $l->getLanguageCode($application);
}
if (!empty($application)) {
self::$scripts[] = "$application/l10n/$languageCode";
} else {
self::$scripts[] = "js/$languageCode";
}
}

/**
* add a css file
*
* @param string $application
* @param string $application application id
* @param string|null $file filename
* @return void
*/

+ 9
- 0
lib/public/util.php View File

@@ -136,6 +136,15 @@ class Util {
\OC_Util::addScript( $application, $file );
}

/**
* Add a translation JS file
* @param string $application application id
* @param string $languageCode language code, defaults to the current locale
*/
public static function addTranslations($application, $languageCode = null) {
\OC_Util::addTranslations($application, $languageCode);
}

/**
* Add a custom element to the header
* @param string $tag tag name of the element

Loading…
Cancel
Save