/** * Copyright (c) 2014 Vincent Petry * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) * * @author Christoph Wurst * @author Daniel Kesselberg * @author Joas Schilling * @author John Molakvoæ * @author Morris Jobke * @author Roeland Jago Douma * @author Vincent Petry * * @license AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * */ import _ from 'underscore' import $ from 'jquery' import DOMPurify from 'dompurify' import Handlebars from 'handlebars' import identity from 'lodash/fp/identity' import escapeHTML from 'escape-html' import { generateFilePath } from '@nextcloud/router' import OC from './index' import { getAppTranslations, hasAppTranslations, registerAppTranslations, unregisterAppTranslations, } from './l10n-registry' /** * L10N namespace with localization functions. * * @namespace OC.L10n */ const L10n = { /** * Load an app's translation bundle if not loaded already. * * @param {string} appName name of the app * @param {Function} callback callback to be called when * the translations are loaded * @return {Promise} promise */ load(appName, callback) { // already available ? if (hasAppTranslations(appName) || OC.getLocale() === 'en') { const deferred = $.Deferred() const promise = deferred.promise() promise.then(callback) deferred.resolve() return promise } const self = this const url = generateFilePath(appName, 'l10n', OC.getLocale() + '.json') // load JSON translation bundle per AJAX return $.get(url) .then( function(result) { if (result.translations) { self.register(appName, result.translations, result.pluralForm) } }) .then(callback) }, /** * Register an app's translation bundle. * * @param {string} appName name of the app * @param {object} bundle bundle */ register(appName, bundle) { registerAppTranslations(appName, bundle, this._getPlural) }, /** * @private */ _unregister: unregisterAppTranslations, /** * 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 {object} [vars] map of placeholder key to value * @param {number} [count] number to replace %n with * @param {Array} [options] options array * @param {boolean} [options.escape=true] enable/disable auto escape of placeholders (by default enabled) * @param {boolean} [options.sanitize=true] enable/disable sanitization (by default enabled) * @return {string} */ translate(app, text, vars, count, options) { const defaultOptions = { escape: true, sanitize: true, } const allOptions = options || {} _.defaults(allOptions, defaultOptions) const optSanitize = allOptions.sanitize ? DOMPurify.sanitize : identity const optEscape = allOptions.escape ? escapeHTML : identity // TODO: cache this function to avoid inline recreation // of the same function over and over again in case // translate() is used in a loop const _build = function(text, vars, count) { return text.replace(/%n/g, count).replace(/{([^{}]*)}/g, function(a, b) { const r = vars[b] if (typeof r === 'string' || typeof r === 'number') { return optSanitize(optEscape(r)) } else { return optSanitize(a) } } ) } let translation = text const bundle = getAppTranslations(app) const value = bundle.translations[text] if (typeof (value) !== 'undefined') { translation = value } if (typeof vars === 'object' || count !== undefined) { return optSanitize(_build(translation, vars, count)) } else { return optSanitize(translation) } }, /** * Translate a plural string * * @param {string} app the id of the app for which to translate the string * @param {string} textSingular the string to translate for exactly one object * @param {string} textPlural the string to translate for n objects * @param {number} count number to determine whether to use singular or plural * @param {object} [vars] map of placeholder key to value * @param {Array} [options] options array * @param {boolean} [options.escape=true] enable/disable auto escape of placeholders (by default enabled) * @return {string} Translated string */ translatePlural(app, textSingular, textPlural, count, vars, options) { const identifier = '_' + textSingular + '_::_' + textPlural + '_' const bundle = getAppTranslations(app) const value = bundle.translations[identifier] if (typeof (value) !== 'undefined') { const translation = value if ($.isArray(translation)) { const plural = bundle.pluralFunction(count) return this.translate(app, translation[plural], vars, count, options) } } if (count === 1) { return this.translate(app, textSingular, vars, count, options) } else { return this.translate(app, textPlural, vars, count, options) } }, /** * The plural function taken from symfony * * @param {number} number the number of elements * @return {number} * @private */ _getPlural(number) { let language = OC.getLanguage() if (language === 'pt-BR') { // temporary set a locale for brazilian language = 'xbr' } if (typeof language === 'undefined' || language === '') { return (number === 1) ? 0 : 1 } if (language.length > 3) { language = language.substring(0, language.lastIndexOf('-')) } /* * The plural rules are derived from code of the Zend Framework (2010-09-25), * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) */ switch (language) { case 'az': case 'bo': case 'dz': case 'id': case 'ja': case 'jv': case 'ka': case 'km': case 'kn': case 'ko': case 'ms': case 'th': case 'tr': case 'vi': case 'zh': return 0 case 'af': case 'bn': case 'bg': case 'ca': case 'da': case 'de': case 'el': case 'en': case 'eo': case 'es': case 'et': case 'eu': case 'fa': case 'fi': case 'fo': case 'fur': case 'fy': case 'gl': case 'gu': case 'ha': case 'he': case 'hu': case 'is': case 'it': case 'ku': case 'lb': case 'ml': case 'mn': case 'mr': case 'nah': case 'nb': case 'ne': case 'nl': case 'nn': case 'no': case 'oc': case 'om': case 'or': case 'pa': case 'pap': case 'ps': case 'pt': case 'so': case 'sq': case 'sv': case 'sw': case 'ta': case 'te': case 'tk': case 'ur': case 'zu': return (number === 1) ? 0 : 1 case 'am': case 'bh': case 'fil': case 'fr': case 'gun': case 'hi': case 'hy': case 'ln': case 'mg': case 'nso': case 'xbr': case 'ti': case 'wa': return ((number === 0) || (number === 1)) ? 0 : 1 case 'be': case 'bs': case 'hr': case 'ru': case 'sh': case 'sr': case 'uk': return ((number % 10 === 1) && (number % 100 !== 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2) case 'cs': case 'sk': return (number === 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2) case 'ga': return (number === 1) ? 0 : ((number === 2) ? 1 : 2) case 'lt': return ((number % 10 === 1) && (number % 100 !== 11)) ? 0 : (((number % 10 >= 2) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2) case 'sl': return (number % 100 === 1) ? 0 : ((number % 100 === 2) ? 1 : (((number % 100 === 3) || (number % 100 === 4)) ? 2 : 3)) case 'mk': return (number % 10 === 1) ? 0 : 1 case 'mt': return (number === 1) ? 0 : (((number === 0) || ((number % 100 > 1) && (number % 100 < 11))) ? 1 : (((number % 100 > 10) && (number % 100 < 20)) ? 2 : 3)) case 'lv': return (number === 0) ? 0 : (((number % 10 === 1) && (number % 100 !== 11)) ? 1 : 2) case 'pl': return (number === 1) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 12) || (number % 100 > 14))) ? 1 : 2) case 'cy': return (number === 1) ? 0 : ((number === 2) ? 1 : (((number === 8) || (number === 11)) ? 2 : 3)) case 'ro': return (number === 1) ? 0 : (((number === 0) || ((number % 100 > 0) && (number % 100 < 20))) ? 1 : 2) case 'ar': return (number === 0) ? 0 : ((number === 1) ? 1 : ((number === 2) ? 2 : (((number % 100 >= 3) && (number % 100 <= 10)) ? 3 : (((number % 100 >= 11) && (number % 100 <= 99)) ? 4 : 5)))) default: return 0 } }, } export default L10n /** * Returns the user's locale * * @return {string} locale string */ export const getLocale = () => $('html').data('locale') ?? 'en' /** * Returns the user's language * * @return {string} language string */ export const getLanguage = () => $('html').prop('lang') Handlebars.registerHelper('t', function(app, text) { return L10n.translate(app, text) })