mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-24 17:36:14 -04:00
1003 lines
26 KiB
TypeScript
1003 lines
26 KiB
TypeScript
import {Func} from "mocha";
|
|
|
|
|
|
type PluralFunc = (n: number) => string
|
|
|
|
export class Html10n {
|
|
public language?: string
|
|
private rtl: string[]
|
|
private _pluralRules?: PluralFunc
|
|
public mt: MicroEvent
|
|
private loader: Loader | undefined
|
|
public translations: Map<string, any>
|
|
private macros: Map<string, Function>
|
|
|
|
constructor() {
|
|
this.language = undefined
|
|
this.rtl = ["ar","dv","fa","ha","he","ks","ku","ps","ur","yi"]
|
|
this.mt = new MicroEvent()
|
|
this.translations = new Map()
|
|
this.macros = new Map()
|
|
|
|
this.macros.set('plural', (_key: string, param:string, opts: any)=>{
|
|
let str
|
|
, n = parseFloat(param);
|
|
if (isNaN(n))
|
|
return;
|
|
|
|
// initialize _pluralRules
|
|
if (this._pluralRules === undefined) {
|
|
this._pluralRules = this.getPluralRules(this.language!);
|
|
}
|
|
let index = this._pluralRules!(n);
|
|
|
|
// try to find a [zero|one|two] key if it's defined
|
|
if (n === 0 && ('zero') in opts) {
|
|
str = opts['zero'];
|
|
} else if (n == 1 && ('one') in opts) {
|
|
str = opts['one'];
|
|
} else if (n == 2 && ('two') in opts) {
|
|
str = opts['two'];
|
|
} else if (index in opts) {
|
|
str = opts[index];
|
|
}
|
|
|
|
return str;
|
|
})
|
|
|
|
document.addEventListener('DOMContentLoaded', ()=> {
|
|
this.index()
|
|
}, false)
|
|
}
|
|
|
|
bind(event: string, fct: Func) {
|
|
this.mt.bind(event, fct)
|
|
}
|
|
|
|
/**
|
|
* Get rules for plural forms (shared with JetPack), see:
|
|
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
|
|
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
|
|
*
|
|
* @param {string} lang
|
|
* locale (language) used.
|
|
*
|
|
* @return {PluralFunc}
|
|
* returns a function that gives the plural form name for a given integer:
|
|
* var fun = getPluralRules('en');
|
|
* fun(1) -> 'one'
|
|
* fun(0) -> 'other'
|
|
* fun(1000) -> 'other'.
|
|
*/
|
|
getPluralRules(lang: string): PluralFunc {
|
|
const locales2rules = new Map([
|
|
['af', 3],
|
|
['ak', 4],
|
|
['am', 4],
|
|
['ar', 1],
|
|
['asa', 3],
|
|
['az', 0],
|
|
['be', 11],
|
|
['bem', 3],
|
|
['bez', 3],
|
|
['bg', 3],
|
|
['bh', 4],
|
|
['bm', 0],
|
|
['bn', 3],
|
|
['bo', 0],
|
|
['br', 20],
|
|
['brx', 3],
|
|
['bs', 11],
|
|
['ca', 3],
|
|
['cgg', 3],
|
|
['chr', 3],
|
|
['cs', 12],
|
|
['cy', 17],
|
|
['da', 3],
|
|
['de', 3],
|
|
['dv', 3],
|
|
['dz', 0],
|
|
['ee', 3],
|
|
['el', 3],
|
|
['en', 3],
|
|
['eo', 3],
|
|
['es', 3],
|
|
['et', 3],
|
|
['eu', 3],
|
|
['fa', 0],
|
|
['ff', 5],
|
|
['fi', 3],
|
|
['fil', 4],
|
|
['fo', 3],
|
|
['fr', 5],
|
|
['fur', 3],
|
|
['fy', 3],
|
|
['ga', 8],
|
|
['gd', 24],
|
|
['gl', 3],
|
|
['gsw', 3],
|
|
['gu', 3],
|
|
['guw', 4],
|
|
['gv', 23],
|
|
['ha', 3],
|
|
['haw', 3],
|
|
['he', 2],
|
|
['hi', 4],
|
|
['hr', 11],
|
|
['hu', 0],
|
|
['id', 0],
|
|
['ig', 0],
|
|
['ii', 0],
|
|
['is', 3],
|
|
['it', 3],
|
|
['iu', 7],
|
|
['ja', 0],
|
|
['jmc', 3],
|
|
['jv', 0],
|
|
['ka', 0],
|
|
['kab', 5],
|
|
['kaj', 3],
|
|
['kcg', 3],
|
|
['kde', 0],
|
|
['kea', 0],
|
|
['kk', 3],
|
|
['kl', 3],
|
|
['km', 0],
|
|
['kn', 0],
|
|
['ko', 0],
|
|
['ksb', 3],
|
|
['ksh', 21],
|
|
['ku', 3],
|
|
['kw', 7],
|
|
['lag', 18],
|
|
['lb', 3],
|
|
['lg', 3],
|
|
['ln', 4],
|
|
['lo', 0],
|
|
['lt', 10],
|
|
['lv', 6],
|
|
['mas', 3],
|
|
['mg', 4],
|
|
['mk', 16],
|
|
['ml', 3],
|
|
['mn', 3],
|
|
['mo', 9],
|
|
['mr', 3],
|
|
['ms', 0],
|
|
['mt', 15],
|
|
['my', 0],
|
|
['nah', 3],
|
|
['naq', 7],
|
|
['nb', 3],
|
|
['nd', 3],
|
|
['ne', 3],
|
|
['nl', 3],
|
|
['nn', 3],
|
|
['no', 3],
|
|
['nr', 3],
|
|
['nso', 4],
|
|
['ny', 3],
|
|
['nyn', 3],
|
|
['om', 3],
|
|
['or', 3],
|
|
['pa', 3],
|
|
['pap', 3],
|
|
['pl', 13],
|
|
['ps', 3],
|
|
['pt', 3],
|
|
['rm', 3],
|
|
['ro', 9],
|
|
['rof', 3],
|
|
['ru', 11],
|
|
['rwk', 3],
|
|
['sah', 0],
|
|
['saq', 3],
|
|
['se', 7],
|
|
['seh', 3],
|
|
['ses', 0],
|
|
['sg', 0],
|
|
['sh', 11],
|
|
['shi', 19],
|
|
['sk', 12],
|
|
['sl', 14],
|
|
['sma', 7],
|
|
['smi', 7],
|
|
['smj', 7],
|
|
['smn', 7],
|
|
['sms', 7],
|
|
['sn', 3],
|
|
['so', 3],
|
|
['sq', 3],
|
|
['sr', 11],
|
|
['ss', 3],
|
|
['ssy', 3],
|
|
['st', 3],
|
|
['sv', 3],
|
|
['sw', 3],
|
|
['syr', 3],
|
|
['ta', 3],
|
|
['te', 3],
|
|
['teo', 3],
|
|
['th', 0],
|
|
['ti', 4],
|
|
['tig', 3],
|
|
['tk', 3],
|
|
['tl', 4],
|
|
['tn', 3],
|
|
['to', 0],
|
|
['tr', 0],
|
|
['ts', 3],
|
|
['tzm', 22],
|
|
['uk', 11],
|
|
['ur', 3],
|
|
['ve', 3],
|
|
['vi', 0],
|
|
['vun', 3],
|
|
['wa', 4],
|
|
['wae', 3],
|
|
['wo', 0],
|
|
['xh', 3],
|
|
['xog', 3],
|
|
['yo', 0],
|
|
['zh', 0],
|
|
['zu', 3]
|
|
])
|
|
|
|
function isIn(n: number, list: number[]) {
|
|
return list.indexOf(n) !== -1;
|
|
}
|
|
function isBetween(n: number, start: number, end: number) {
|
|
return start <= n && n <= end;
|
|
}
|
|
|
|
type PluralFunc = (n: number) => string
|
|
|
|
|
|
const pluralRules: {
|
|
[key: string]: PluralFunc
|
|
} = {
|
|
'0': function() {
|
|
return 'other';
|
|
},
|
|
'1': function(n: number) {
|
|
if ((isBetween((n % 100), 3, 10)))
|
|
return 'few';
|
|
if (n === 0)
|
|
return 'zero';
|
|
if ((isBetween((n % 100), 11, 99)))
|
|
return 'many';
|
|
if (n == 2)
|
|
return 'two';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'2': function(n: number) {
|
|
if (n !== 0 && (n % 10) === 0)
|
|
return 'many';
|
|
if (n == 2)
|
|
return 'two';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'3': function(n: number) {
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'4': function(n: number) {
|
|
if ((isBetween(n, 0, 1)))
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'5': function(n: number) {
|
|
if ((isBetween(n, 0, 2)) && n != 2)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'6': function(n: number) {
|
|
if (n === 0)
|
|
return 'zero';
|
|
if ((n % 10) == 1 && (n % 100) != 11)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'7': function(n: number) {
|
|
if (n == 2)
|
|
return 'two';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'8': function(n: number) {
|
|
if ((isBetween(n, 3, 6)))
|
|
return 'few';
|
|
if ((isBetween(n, 7, 10)))
|
|
return 'many';
|
|
if (n == 2)
|
|
return 'two';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'9': function(n: number) {
|
|
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
|
|
return 'few';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'10': function(n: number) {
|
|
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
|
|
return 'few';
|
|
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'11': function(n: number) {
|
|
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
|
|
return 'few';
|
|
if ((n % 10) === 0 ||
|
|
(isBetween((n % 10), 5, 9)) ||
|
|
(isBetween((n % 100), 11, 14)))
|
|
return 'many';
|
|
if ((n % 10) == 1 && (n % 100) != 11)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'12': function(n: number) {
|
|
if ((isBetween(n, 2, 4)))
|
|
return 'few';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'13': function(n: number) {
|
|
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
|
|
return 'few';
|
|
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
|
|
(isBetween((n % 10), 5, 9)) ||
|
|
(isBetween((n % 100), 12, 14)))
|
|
return 'many';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'14': function(n: number) {
|
|
if ((isBetween((n % 100), 3, 4)))
|
|
return 'few';
|
|
if ((n % 100) == 2)
|
|
return 'two';
|
|
if ((n % 100) == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'15': function(n: number) {
|
|
if (n === 0 || (isBetween((n % 100), 2, 10)))
|
|
return 'few';
|
|
if ((isBetween((n % 100), 11, 19)))
|
|
return 'many';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'16': function(n: number) {
|
|
if ((n % 10) == 1 && n != 11)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'17': function(n: number) {
|
|
if (n == 3)
|
|
return 'few';
|
|
if (n === 0)
|
|
return 'zero';
|
|
if (n == 6)
|
|
return 'many';
|
|
if (n == 2)
|
|
return 'two';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'18': function(n: number) {
|
|
if (n === 0)
|
|
return 'zero';
|
|
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'19': function(n: number) {
|
|
if ((isBetween(n, 2, 10)))
|
|
return 'few';
|
|
if ((isBetween(n, 0, 1)))
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'20': function(n: number) {
|
|
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
|
|
isBetween((n % 100), 10, 19) ||
|
|
isBetween((n % 100), 70, 79) ||
|
|
isBetween((n % 100), 90, 99)
|
|
))
|
|
return 'few';
|
|
if ((n % 1000000) === 0 && n !== 0)
|
|
return 'many';
|
|
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
|
|
return 'two';
|
|
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'21': function(n: number) {
|
|
if (n === 0)
|
|
return 'zero';
|
|
if (n == 1)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'22': function(n: number) {
|
|
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'23': function(n: number) {
|
|
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
|
|
return 'one';
|
|
return 'other';
|
|
},
|
|
'24': function(n: number) {
|
|
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
|
|
return 'few';
|
|
if (isIn(n, [2, 12]))
|
|
return 'two';
|
|
if (isIn(n, [1, 11]))
|
|
return 'one';
|
|
return 'other';
|
|
}
|
|
};
|
|
|
|
const index = locales2rules.get(lang.replace(/-.*$/, ''));
|
|
// @ts-ignore
|
|
if (!(index in pluralRules)) {
|
|
console.warn('plural form unknown for [' + lang + ']');
|
|
return function() { return 'other'; };
|
|
}
|
|
// @ts-ignore
|
|
return pluralRules[index];
|
|
}
|
|
|
|
getTranslatableChildren(element: HTMLElement) {
|
|
return element.querySelectorAll('*[data-l10n-id]')
|
|
}
|
|
|
|
localize(langs: (string|undefined)[]|string) {
|
|
console.log('Available langs ', langs)
|
|
if ('string' === typeof langs) {
|
|
langs = [langs];
|
|
}
|
|
let i = 0
|
|
langs.forEach((lang) => {
|
|
if(!lang) return;
|
|
langs[i++] = lang;
|
|
if(~lang.indexOf('-')) langs[i++] = lang.substring(0, lang.indexOf('-'));
|
|
})
|
|
|
|
this.build(langs, (er: null, translations: Map<string, any>) =>{
|
|
this.translations = translations
|
|
this.translateElement(translations)
|
|
this.mt.trigger('localized')
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Triggers the translation process
|
|
* for an element
|
|
* @param translations A hash of all translation strings
|
|
* @param element A DOM element, if omitted, the document element will be used
|
|
*/
|
|
translateElement(translations: Map<string, any>, element?: HTMLElement) {
|
|
element = element || document.documentElement
|
|
const children = element ? this.getTranslatableChildren(element): document.childNodes
|
|
|
|
for (let child of children) {
|
|
this.translateNode(translations, child as HTMLElement)
|
|
}
|
|
|
|
// translate element itself if necessary
|
|
this.translateNode(translations, element)
|
|
}
|
|
|
|
asyncForEach(list: (string|undefined)[], iterator: any, cb: Function) {
|
|
let i = 0
|
|
, n = list.length
|
|
iterator(list[i], i, function each(err?: string) {
|
|
if(err) console.error(err)
|
|
i++
|
|
if (i < n) return iterator(list[i],i, each);
|
|
cb()
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Builds a translation object from a list of langs (loads the necessary translations)
|
|
* @param langs Array - a list of langs sorted by priority (default langs should go last)
|
|
* @param cb Function - a callback that will be called once all langs have been loaded
|
|
*/
|
|
build(langs: (string|undefined)[], cb: Function) {
|
|
const build = new Map<string, any>()
|
|
|
|
this.asyncForEach(langs, (lang: string, _i: number, next:LoaderFunc)=> {
|
|
if(!lang) return next();
|
|
this.loader!.load(lang, next)
|
|
}, () =>{
|
|
let lang;
|
|
langs.reverse()
|
|
|
|
// loop through the priority array...
|
|
for (let i=0, n=langs.length; i < n; i++) {
|
|
lang = langs[i]
|
|
if(!lang) continue;
|
|
if(!(lang in langs)) {// uh, we don't have this lang availbable..
|
|
// then check for related langs
|
|
if(~lang.indexOf('-') != -1) {
|
|
lang = lang.split('-')[0];
|
|
}
|
|
let l: string|undefined = ''
|
|
for(l of langs) {
|
|
if(l && lang != l && l.indexOf(lang) === 0) {
|
|
lang = l
|
|
break;
|
|
}
|
|
}
|
|
|
|
// @ts-ignore
|
|
if(lang != l) continue;
|
|
}
|
|
|
|
|
|
// ... and apply all strings of the current lang in the list
|
|
// to our build object
|
|
//lang = "de"
|
|
if (this.loader!.langs.has(lang)) {
|
|
for (let string in this.loader!.langs.get(lang)) {
|
|
build.set(string,this.loader!.langs.get(lang)[string])
|
|
}
|
|
this.language = lang
|
|
} else {
|
|
const loaderLang = lang.split('-')[0]
|
|
for (let string in this.loader!.langs.get(loaderLang)) {
|
|
build.set(string,this.loader!.langs.get(loaderLang)[string])
|
|
}
|
|
this.language = loaderLang
|
|
}
|
|
|
|
// the last applied lang will be exposed as the
|
|
// lang the page was translated to
|
|
}
|
|
cb(null, build)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Returns the language that was last applied to the translations hash
|
|
* thus overriding most of the formerly applied langs
|
|
*/
|
|
getLanguage() {
|
|
return this.language
|
|
}
|
|
|
|
/**
|
|
* Returns the direction of the language returned be html10n#getLanguage
|
|
*/
|
|
getDirection() {
|
|
if(!this.language) return
|
|
const langCode = this.language.indexOf('-') == -1? this.language : this.language.substring(0, this.language.indexOf('-'))
|
|
return this.rtl.indexOf(langCode) == -1? 'ltr' : 'rtl'
|
|
}
|
|
|
|
|
|
/**
|
|
* Index all <link>s
|
|
*/
|
|
index() {
|
|
// Find all <link>s
|
|
const links = document.getElementsByTagName('link')
|
|
, resources = []
|
|
for (let i=0, n=links.length; i < n; i++) {
|
|
if (links[i].type != 'application/l10n+json')
|
|
continue;
|
|
resources.push(links[i].href)
|
|
}
|
|
this.loader = new Loader(resources)
|
|
this.mt.trigger('indexed')
|
|
}
|
|
|
|
translateNode(translations: Map<string, any>, node: HTMLElement) {
|
|
const str: {
|
|
id?: string,
|
|
args?: any,
|
|
str?: string
|
|
|
|
} = {}
|
|
|
|
// get id
|
|
str.id = node.getAttribute('data-l10n-id') as string
|
|
if (!str.id) return
|
|
|
|
if(!translations.get(str.id)) return console.warn('Couldn\'t find translation key '+str.id)
|
|
|
|
// get args
|
|
if(window.JSON) {
|
|
str.args = JSON.parse(node.getAttribute('data-l10n-args') as string)
|
|
}else{
|
|
try{
|
|
//str.args = eval(node.getAttribute('data-l10n-args') as string)
|
|
console.error("Old eval method invoked!!")
|
|
}catch(e) {
|
|
console.warn('Couldn\'t parse args for '+str.id)
|
|
}
|
|
}
|
|
|
|
str.str = this.get(str.id, str.args)
|
|
|
|
// get attribute name to apply str to
|
|
let prop
|
|
, index = str.id.lastIndexOf('.')
|
|
, attrList = // allowed attributes
|
|
{ "title": 1
|
|
, "innerHTML": 1
|
|
, "alt": 1
|
|
, "textContent": 1
|
|
, "value": 1
|
|
, "placeholder": 1
|
|
}
|
|
if (index > 0 && str.id.substring(index + 1) in attrList) {
|
|
// an attribute has been specified (example: "my_translation_key.placeholder")
|
|
prop = str.id.substring(index + 1)
|
|
} else { // no attribute: assuming text content by default
|
|
prop = document.body.textContent ? 'textContent' : 'innerText'
|
|
}
|
|
|
|
// Apply translation
|
|
if (node.children.length === 0 || prop != 'textContent') {
|
|
// @ts-ignore
|
|
node[prop] = str.str!
|
|
node.setAttribute("aria-label", str.str!); // Sets the aria-label
|
|
// The idea of the above is that we always have an aria value
|
|
// This might be a bit of an abrupt solution but let's see how it goes
|
|
} else {
|
|
let children = node.childNodes,
|
|
found = false
|
|
let i = 0, n = children.length;
|
|
for (; i < n; i++) {
|
|
if (children[i].nodeType === 3 && /\S/.test(children[i].textContent!)) {
|
|
if (!found) {
|
|
children[i].nodeValue = str.str!
|
|
found = true
|
|
} else {
|
|
children[i].nodeValue = ''
|
|
}
|
|
}
|
|
}
|
|
if (!found) {
|
|
console.warn('Unexpected error: could not translate element content for key '+str.id, node)
|
|
}
|
|
}
|
|
}
|
|
|
|
get(id: string, args?:any) {
|
|
let translations = this.translations
|
|
if(!translations) return console.warn('No translations available (yet)')
|
|
if(!translations.get(id)) return console.warn('Could not find string '+id)
|
|
|
|
// apply macros
|
|
let str = translations.get(id)
|
|
|
|
str = this.substMacros(id, str, args)
|
|
|
|
// apply args
|
|
str = this.substArguments(str, args)
|
|
|
|
return str
|
|
}
|
|
|
|
substMacros(key: string, str:string, args:any) {
|
|
let regex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)((\s*([a-zA-Z]+)\: ?([ a-zA-Z{}]+),?)+)*\s*\]\}/ //.exec('{[ plural(n) other: are {{n}}, one: is ]}')
|
|
, match
|
|
|
|
while(match = regex.exec(str)) {
|
|
// a macro has been found
|
|
// Note: at the moment, only one parameter is supported
|
|
let macroName = match[1]
|
|
, paramName = match[2]
|
|
, optv = match[3]
|
|
, opts: {[key:string]:any} = {}
|
|
|
|
if (!(this.macros.has(macroName))) continue
|
|
|
|
if(optv) {
|
|
optv.match(/(?=\s*)([a-zA-Z]+)\: ?([ a-zA-Z{}]+)(?=,?)/g)!.forEach(function(arg) {
|
|
const parts = arg.split(':')
|
|
, name = parts[0];
|
|
opts[name] = parts[1].trim()
|
|
})
|
|
}
|
|
|
|
let param
|
|
if (args && paramName in args) {
|
|
param = args[paramName]
|
|
} else if (paramName in this.translations) {
|
|
param = this.translations.get(paramName)
|
|
}
|
|
|
|
// there's no macro parser: it has to be defined in html10n.macros
|
|
let macro = this.macros.get(macroName)!
|
|
str = str.substring(0, match.index) + macro(key, param, opts) + str.substring(match.index+match[0].length)
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
substArguments(str: string, args:any) {
|
|
let reArgs = /\{\{\s*([a-zA-Z\.]+)\s*\}\}/
|
|
, match
|
|
let translations = this.translations;
|
|
while (match = reArgs.exec(str)) {
|
|
if (!match || match.length < 2)
|
|
return str // argument key not found
|
|
|
|
let arg = match[1]
|
|
, sub = ''
|
|
if (args && arg in args) {
|
|
sub = args[arg]
|
|
} else if (translations && arg in translations) {
|
|
sub = translations.get(arg)
|
|
} else {
|
|
console.warn('Could not find argument {{' + arg + '}}')
|
|
return str
|
|
}
|
|
|
|
str = str.substring(0, match.index) + sub + str.substring(match.index + match[0].length)
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
}
|
|
|
|
|
|
class MicroEvent {
|
|
private events: Map<string, Function[]>
|
|
|
|
constructor() {
|
|
this.events = new Map();
|
|
}
|
|
|
|
bind(event: string, fct: Func) {
|
|
if (this.events.get(event) === undefined) {
|
|
this.events.set(event, []);
|
|
}
|
|
|
|
this.events.get(event)!.push(fct);
|
|
}
|
|
|
|
unbind(event: string, fct: Func) {
|
|
if (this.events.get(event) === undefined) {
|
|
return;
|
|
}
|
|
|
|
const index = this.events.get(event)!.indexOf(fct);
|
|
if (index !== -1) {
|
|
this.events.get(event)!.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
trigger(event: string, ...args: any[]) {
|
|
if (this.events.get(event) === undefined) {
|
|
return;
|
|
}
|
|
|
|
for (const fct of this.events.get(event)!) {
|
|
fct(...args);
|
|
}
|
|
}
|
|
|
|
mixin(destObject: any) {
|
|
const props = ['bind', 'unbind', 'trigger'];
|
|
if (destObject !== undefined) {
|
|
for (const prop of props) {
|
|
// @ts-ignore
|
|
destObject[prop] = this[prop];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type LoaderFunc = () => void
|
|
|
|
type ErrorFunc = (data?:any)=>void
|
|
|
|
class Loader {
|
|
private resources: any
|
|
private cache: Map<string, any>
|
|
langs: Map<string, any>
|
|
|
|
constructor(resources: any) {
|
|
this.resources = resources;
|
|
this.cache = new Map();
|
|
this.langs = new Map();
|
|
}
|
|
|
|
load(lang: string, callback: LoaderFunc) {
|
|
if (this.langs.get(lang) !== undefined) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
if (this.resources.length > 0) {
|
|
let reqs = 0
|
|
for (const resource of this.resources) {
|
|
this.fetch(resource, lang, (e)=> {
|
|
reqs++;
|
|
if (e) console.warn(e)
|
|
|
|
if (reqs < this.resources.length) return;// Call back once all reqs are completed
|
|
callback && callback()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fetch(href: string, lang: string, callback: ErrorFunc) {
|
|
|
|
if (this.cache.get(href)) {
|
|
this.parse(lang, href, this.cache.get(href), callback)
|
|
return;
|
|
}
|
|
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('GET', href, /*async: */true)
|
|
if (xhr.overrideMimeType) {
|
|
xhr.overrideMimeType('application/json; charset=utf-8');
|
|
}
|
|
xhr.onreadystatechange = ()=> {
|
|
if (xhr.readyState == 4) {
|
|
if (xhr.status == 200 || xhr.status === 0) {
|
|
const data = JSON.parse(xhr.responseText);
|
|
this.cache.set(href, data)
|
|
// Pass on the contents for parsing
|
|
this.parse(lang, href, data, callback)
|
|
} else {
|
|
callback(new Error('Failed to load '+href))
|
|
}
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
}
|
|
|
|
|
|
parse(lang: string, href: string, data: {
|
|
[key: string]: string
|
|
}, callback: ErrorFunc) {
|
|
if ('object' !== typeof data) {
|
|
callback(new Error('A file couldn\'t be parsed as json.'))
|
|
return
|
|
}
|
|
|
|
function getBcp47LangCode(browserLang: string) {
|
|
const bcp47Lang = browserLang.toLowerCase();
|
|
|
|
// Browser => BCP 47
|
|
const langCodeMap = new Map([
|
|
['zh-cn', 'zh-hans-cn'],
|
|
['zh-hk', 'zh-hant-hk'],
|
|
['zh-mo', 'zh-hant-mo'],
|
|
['zh-my', 'zh-hans-my'],
|
|
['zh-sg', 'zh-hans-sg'],
|
|
['zh-tw', 'zh-hant-tw'],
|
|
])
|
|
|
|
return langCodeMap.get(bcp47Lang) ?? bcp47Lang;
|
|
}
|
|
|
|
// Issue #6129: Fix exceptions
|
|
// NOTE: translatewiki.net use all lowercase form by default ('en-gb' insted of 'en-GB')
|
|
function getJsonLangCode(bcp47Lang: string) {
|
|
const jsonLang = bcp47Lang.toLowerCase();
|
|
// BCP 47 => JSON
|
|
const langCodeMap = new Map([
|
|
['sr-ec', 'sr-cyrl'],
|
|
['sr-el', 'sr-latn'],
|
|
['zh-hk', 'zh-hant-hk'],
|
|
])
|
|
|
|
return langCodeMap.get(jsonLang) ?? jsonLang;
|
|
}
|
|
|
|
let bcp47LangCode = getBcp47LangCode(lang);
|
|
let jsonLangCode = getJsonLangCode(bcp47LangCode);
|
|
|
|
if (!data[jsonLangCode]) {
|
|
// lang not found
|
|
// This may be due to formatting (expected 'ru' but browser sent 'ru-RU')
|
|
// Set err msg before mutating lang (we may need this later)
|
|
const msg = 'Couldn\'t find translations for ' + lang +
|
|
'(lowercase BCP 47 lang tag ' + bcp47LangCode +
|
|
', JSON lang code ' + jsonLangCode + ')';
|
|
// Check for '-' (BCP 47 'ROOT-SCRIPT-REGION-VARIANT') and fallback until found data or ROOT
|
|
// - 'ROOT-SCRIPT-REGION': 'zh-Hans-CN'
|
|
// - 'ROOT-SCRIPT': 'zh-Hans'
|
|
// - 'ROOT-REGION': 'en-GB'
|
|
// - 'ROOT-VARIANT': 'be-tarask'
|
|
while (!data[jsonLangCode] && bcp47LangCode.lastIndexOf('-') > -1) {
|
|
// ROOT-SCRIPT-REGION-VARIANT formatting detected
|
|
bcp47LangCode = bcp47LangCode.substring(0, bcp47LangCode.lastIndexOf('-')); // set lang to ROOT lang
|
|
jsonLangCode = getJsonLangCode(bcp47LangCode);
|
|
}
|
|
|
|
if (!data[jsonLangCode]) {
|
|
// ROOT lang not found. (e.g 'zh')
|
|
// Loop through langs data. Maybe we have a variant? e.g (zh-hans)
|
|
let l; // langs item. Declare outside of loop
|
|
|
|
for (l in data) {
|
|
// Is not ROOT?
|
|
// And is variant of ROOT?
|
|
// (NOTE: index of ROOT equals 0 would cause unexpected ISO 639-1 vs. 639-3 issues,
|
|
// so append dash into query string)
|
|
// And is known lang?
|
|
if (bcp47LangCode != l && l.indexOf(lang + '-') === 0 && data[l]) {
|
|
bcp47LangCode = l; // set lang to ROOT-SCRIPT (e.g 'zh-hans')
|
|
jsonLangCode = getJsonLangCode(bcp47LangCode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Did we find a variant? If not, return err.
|
|
if (bcp47LangCode != l) {
|
|
return callback(new Error(msg));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
lang = jsonLangCode
|
|
|
|
if('string' === typeof data[lang]) {
|
|
// Import rule
|
|
|
|
// absolute path
|
|
let importUrl = data[lang];
|
|
|
|
// relative path
|
|
if(data[lang].indexOf("http") != 0 && data[lang].indexOf("/") != 0) {
|
|
importUrl = href+"/../"+data[lang]
|
|
}
|
|
|
|
this.fetch(importUrl, lang, callback)
|
|
return
|
|
}
|
|
|
|
if ('object' != typeof data[lang]) {
|
|
callback(new Error('Translations should be specified as JSON objects!'))
|
|
return
|
|
}
|
|
|
|
this.langs.set(lang,data[lang])
|
|
// TODO: Also store accompanying langs
|
|
callback()
|
|
}
|
|
}
|
|
|
|
const html10n = new Html10n()
|
|
export default html10n
|
|
|
|
// @ts-ignore
|
|
window.html10n = html10n
|
|
|
|
// gettext-like shortcut
|
|
if (window._ === undefined){
|
|
// @ts-ignore
|
|
window._ = html10n.get;
|
|
}
|