diff --git a/apps/files/index.php b/apps/files/index.php
index bc74e17aee131b9ea429c6d502646c154591477d..4142a02b97e04f17627d2dd0d851ce53216f42c7 100644
--- a/apps/files/index.php
+++ b/apps/files/index.php
@@ -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');
diff --git a/apps/files_encryption/appinfo/app.php b/apps/files_encryption/appinfo/app.php
index 922d9885164e14120b913ccd3993c827529aac3d..aa709fbac652e4255df65bb0640be765a4073aad 100644
--- a/apps/files_encryption/appinfo/app.php
+++ b/apps/files_encryption/appinfo/app.php
@@ -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');
 
diff --git a/apps/files_external/appinfo/app.php b/apps/files_external/appinfo/app.php
index 3486b8db51be1233ddf91ea04f07c22906a42ea3..ea14f7adbcf69c46d59c3f4786077dca3ab5de0e 100644
--- a/apps/files_external/appinfo/app.php
+++ b/apps/files_external/appinfo/app.php
@@ -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');
diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php
index f2c454a9ae27bfd3dfd33879fa2cc017ed20a99d..a01f8d98c7d1f49bb92eb0d8dc713a49e6257b42 100644
--- a/apps/files_sharing/appinfo/app.php
+++ b/apps/files_sharing/appinfo/app.php
@@ -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');
 
diff --git a/apps/files_trashbin/appinfo/app.php b/apps/files_trashbin/appinfo/app.php
index fe428121a25e19c4029803e2a9365332e43457a0..7df52da6314f33e78ee167bd3dc63cd7c1d218e1 100644
--- a/apps/files_trashbin/appinfo/app.php
+++ b/apps/files_trashbin/appinfo/app.php
@@ -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
diff --git a/apps/files_versions/appinfo/app.php b/apps/files_versions/appinfo/app.php
index 8c517d4d0ff02dd83ed8a5b7f279563c50c72e28..78de1528f322f5360d7c8e146a173db19c6038f6 100644
--- a/apps/files_versions/appinfo/app.php
+++ b/apps/files_versions/appinfo/app.php
@@ -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');
 
diff --git a/apps/user_ldap/appinfo/app.php b/apps/user_ldap/appinfo/app.php
index a26c7709d41096b34cc16c8ae26dd5e018ee9a7c..8f9fbc5129b05b5419bcb9bdc4e9398180c0eda1 100644
--- a/apps/user_ldap/appinfo/app.php
+++ b/apps/user_ldap/appinfo/app.php
@@ -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')) {
diff --git a/apps/user_webdavauth/appinfo/app.php b/apps/user_webdavauth/appinfo/app.php
index 3cd227bddbefa4bbe7b1005f992d0ed54c64fd3e..125f5f406547df42fa636890c376f9589985d414 100644
--- a/apps/user_webdavauth/appinfo/app.php
+++ b/apps/user_webdavauth/appinfo/app.php
@@ -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",
diff --git a/core/js/core.json b/core/js/core.json
index caff2b05252f5c18b0ccd12484b075744c5ffeea..e2da14028888d0712f7fe14d466f960477cfe8d1 100644
--- a/core/js/core.json
+++ b/core/js/core.json
@@ -13,6 +13,7 @@
 		"jquery.ocdialog.js",
 		"oc-dialogs.js",
 		"js.js",
+		"l10n.js",
 		"share.js",
 		"octemplate.js",
 		"eventsource.js",
diff --git a/core/js/js.js b/core/js/js.js
index 94b78a2e9a9c89d536b285d12f61091dc2cf6ad7..7f657f0e945c66fc0ac12fa862e46559a874f052 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -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=[];
diff --git a/core/js/l10n.js b/core/js/l10n.js
new file mode 100644
index 0000000000000000000000000000000000000000..e375b7eca807dc8338842f07f588b2b93a74ecf3
--- /dev/null
+++ b/core/js/l10n.js
@@ -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);
+
diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js
index b62a0efe40daab54fc99874e916f91f288e43e1a..4111b6763d985095824148fe26d04a0af8dcfff2 100644
--- a/core/js/tests/specHelper.js
+++ b/core/js/tests/specHelper.js
@@ -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;
diff --git a/core/js/tests/specs/l10nSpec.js b/core/js/tests/specs/l10nSpec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5b0363ea383dcb1f2ef472ea7af101ccfb75596
--- /dev/null
+++ b/core/js/tests/specs/l10nSpec.js
@@ -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();
+		});
+	});
+});
diff --git a/lib/base.php b/lib/base.php
index 3d374f12f7d7d356c8934fadd3bb228f2659c2fd..3554911abb9752a66e61d5f7987f758b457a9bf6 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -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");
diff --git a/lib/private/template/functions.php b/lib/private/template/functions.php
index cbe751e59b507a0c72ad730f13b2ac8a5b37a59d..8c94b7cf345bba08273206e77b95ad4eea6dc14d 100644
--- a/lib/private/template/functions.php
+++ b/lib/private/template/functions.php
@@ -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
diff --git a/lib/private/util.php b/lib/private/util.php
index 6cd982c222ee27b4f99bace11844ca9e10ddd3d7..5105bb2293117c825644b3da2b75a00a2cb509de 100644
--- a/lib/private/util.php
+++ b/lib/private/util.php
@@ -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
 	 */
diff --git a/lib/public/util.php b/lib/public/util.php
index 4c4a84af240968bcca87cdf224f74ba99d421b6a..22ded1d0fc5f9f592834fe212cd09240b7f935a2 100644
--- a/lib/public/util.php
+++ b/lib/public/util.php
@@ -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