1
0
mirror of https://github.com/flarum/core.git synced 2025-07-23 09:41:26 +02:00

Refactor translation and validation

We now use Symfony's Translation component. Yay! We get more powerful pluralisation and better a fallback mechanism. Will want to implement the caching mechanism at some point too. The API is replicated in JavaScript, which could definitely use some testing.

Validators have been refactored so that they are decoupled from models completely (i.e. they simply validate arrays of user input). Language packs should include Laravel's validation messages.

ref #267
This commit is contained in:
Toby Zerner
2015-10-15 22:30:45 +10:30
parent 154465069d
commit bc3fa5d451
32 changed files with 578 additions and 4096 deletions

View File

@@ -125,6 +125,8 @@ export default class App {
* @public
*/
boot() {
this.translator.locale = this.locale;
this.initializers.toArray().forEach(initializer => initializer(this));
}
@@ -149,8 +151,6 @@ export default class App {
* Set the <title> of the page.
*
* @param {String} title
* @param {Boolean} [separator] Whether or not to separate the given title and
* the forum's title.
* @public
*/
setTitle(title) {

View File

@@ -4,7 +4,11 @@ import extractText from 'flarum/utils/extractText';
import extract from 'flarum/utils/extract';
/**
* The `Translator` class translates strings using the loaded localization.
* 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 <william.durand1@gmail.com>
*/
export default class Translator {
constructor() {
@@ -15,46 +19,35 @@ export default class Translator {
* @public
*/
this.translations = {};
this.locale = null;
}
/**
* Determine the key of a translation that should be used for the given count.
* The default implementation is for English plurals. It should be overridden
* by a locale's JavaScript file if necessary.
*
* @param {Integer} count
* @return {String}
* @public
*/
plural(count) {
return count === 1 ? 'one' : 'other';
}
trans(id, parameters) {
const translation = this.translations[id];
/**
* Translate a string.
*
* @param {String} key
* @param {Object} input
* @param {VirtualElement} fallback
* @return {VirtualElement}
*/
trans(key, input = {}, fallback = null) {
const parts = key.split('.');
let translation = this.translations;
// Drill down into the translation tree to find the translation for this
// key.
parts.forEach(part => {
translation = translation && translation[part];
});
// If this translation has multiple options and a 'count' has been provided
// in the input, we'll work out which option to choose using the `plural`
// method.
if (translation && typeof translation === 'object' && typeof input.count !== 'undefined') {
translation = translation[this.plural(extractText(input.count))];
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
@@ -66,37 +59,226 @@ export default class Translator {
if (!input.username) input.username = username(user);
}
// If we've found the appropriate translation string, then we'll sub in the
// input.
if (typeof translation === 'string') {
translation = translation.split(new RegExp('({[a-z0-9_]+}|</?[a-z0-9_]+>)', 'gi'));
translation = translation.split(new RegExp('({[a-z0-9_]+}|</?[a-z0-9_]+>)', 'gi'));
const hydrated = [];
const open = [hydrated];
const hydrated = [];
const open = [hydrated];
translation.forEach(part => {
const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));
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]] || [];
open[0].push(tag);
open.unshift(tag.children || tag);
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]] || [];
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 {
open[0].push(part);
}
});
var leftNumber = this.convertNumber(matches[4]);
var rightNumber = this.convertNumber(matches[5]);
return hydrated.filter(part => part);
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 fallback || [key];
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 'tr':
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 '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;
}
}
}