import User from './models/User'; import username from './helpers/username'; import extract from './utils/extract'; /** * Translator with the same API as Symfony's. * * Derived from https://github.com/willdurand/BazingaJsTranslationBundle * which is available under the MIT License. * Copyright (c) William Durand */ export default class Translator { constructor() { /** * A map of translation keys to their translated values. * * @type {Object} * @public */ this.translations = {}; this.locale = null; } addTranslations(translations) { Object.assign(this.translations, translations); } trans(id, parameters) { const translation = this.translations[id]; if (translation) { return this.apply(translation, parameters || {}); } return id; } transChoice(id, number, parameters) { let translation = this.translations[id]; if (translation) { number = parseInt(number, 10); translation = this.pluralize(translation, number); return this.apply(translation, parameters || {}); } return id; } apply(translation, input) { // If we've been given a user model as one of the input parameters, then // we'll extract the username and use that for the translation. In the // future there should be a hook here to inspect the user and change the // translation key. This will allow a gender property to determine which // translation key is used. if ('user' in input) { const user = extract(input, 'user'); if (!input.username) input.username = username(user); } translation = translation.split(new RegExp('({[a-z0-9_]+}|)', 'gi')); const hydrated = []; const open = [hydrated]; translation.forEach((part) => { const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i')); if (match) { if (match[1]) { open[0].push(input[match[1]]); } else if (match[3]) { if (match[2]) { open.shift(); } else { let tag = input[match[3]] || { tag: match[3], children: [] }; open[0].push(tag); open.unshift(tag.children || tag); } } } else { open[0].push(part); } }); return hydrated.filter((part) => part); } pluralize(translation, number) { const sPluralRegex = new RegExp(/^\w+\: +(.+)$/), cPluralRegex = new RegExp(/^\s*((\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]]))\s?(.+?)$/), iPluralRegex = new RegExp(/^\s*(\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]])/), standardRules = [], explicitRules = []; translation.split('|').forEach((part) => { if (cPluralRegex.test(part)) { const matches = part.match(cPluralRegex); explicitRules[matches[0]] = matches[matches.length - 1]; } else if (sPluralRegex.test(part)) { const matches = part.match(sPluralRegex); standardRules.push(matches[1]); } else { standardRules.push(part); } }); explicitRules.forEach((rule, e) => { if (iPluralRegex.test(e)) { const matches = e.match(iPluralRegex); if (matches[1]) { const ns = matches[2].split(','); for (let n in ns) { if (number == ns[n]) { return explicitRules[e]; } } } else { var leftNumber = this.convertNumber(matches[4]); var rightNumber = this.convertNumber(matches[5]); if ( ('[' === matches[3] ? number >= leftNumber : number > leftNumber) && (']' === matches[6] ? number <= rightNumber : number < rightNumber) ) { return explicitRules[e]; } } } }); return standardRules[this.pluralPosition(number, this.locale)] || standardRules[0] || undefined; } convertNumber(number) { if ('-Inf' === number) { return Number.NEGATIVE_INFINITY; } else if ('+Inf' === number || 'Inf' === number) { return Number.POSITIVE_INFINITY; } return parseInt(number, 10); } pluralPosition(number, locale) { if ('pt_BR' === locale) { locale = 'xbr'; } if (locale.length > 3) { locale = locale.split('_')[0]; } switch (locale) { case 'bo': case 'dz': case 'id': case 'ja': case 'jv': case 'ka': case 'km': case 'kn': case 'ko': case 'ms': case 'th': case 'vi': case 'zh': return 0; case 'af': case 'az': 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 '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 'tr': case 'ur': case 'zu': return number == 1 ? 0 : 1; case 'am': case 'bh': case 'fil': case 'fr': case 'gun': case 'hi': 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 '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 >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5; default: return 0; } } }