@ -1,71 +1,367 @@
; ( function ( translator ) {
/* global define, jQuery, config, RELATIVE_PATH, utils, window, Promise, winston */
"use strict" ;
/* globals RELATIVE_PATH, config, define */
var S = null ;
( function ( factory ) {
var stringDefer = null ;
'use strict' ;
function loadClient ( language , filename ) {
// export the class if we are in a Node-like system.
return Promise . resolve ( jQuery . getJSON ( config . relative _path + '/language/' + language + '/' + ( filename + '.json?v=' + config [ 'cache-buster' ] ) ) ) ;
if ( typeof module === 'object' && module . exports === translator ) {
}
exports = module . exports = translator ;
if ( typeof define === 'function' && define . amd ) {
S = require ( 'string' ) ;
// AMD. Register as a named module
define ( 'translator' , [ 'string' ] , function ( string ) {
return factory ( string , loadClient ) ;
} ) ;
} else if ( typeof module === 'object' && module . exports ) {
// Node
( function ( ) {
require ( 'promise-polyfill' ) ;
var languages = require ( '../../../src/languages' ) ;
function loadServer ( language , filename ) {
return new Promise ( function ( resolve , reject ) {
languages . get ( language , filename + '.json' , function ( err , data ) {
if ( err ) {
reject ( err ) ;
} else {
} else {
stringDefer = $ . Deferred ( ) ;
resolve ( data ) ;
require ( [ 'string' ] , function ( stringLib ) {
}
S = stringLib ;
stringDefer . resolve ( S ) ;
} ) ;
} ) ;
} ) ;
}
module . exports = factory ( require ( 'string' ) , loadServer ) ;
} ) ( ) ;
} else {
window . translator = factory ( window . string , loadClient ) ;
}
} ) ( function ( string , load ) {
'use strict' ;
var assign = Object . assign || jQuery . extend ;
function classCallCheck ( instance , Constructor ) { if ( ! ( instance instanceof Constructor ) ) { throw new TypeError ( "Cannot call a class as a function" ) ; } }
var Translator = function ( ) {
/ * *
* Construct a new Translator object
* @ param { string } language - Language code for this translator instance
* /
function Translator ( language ) {
classCallCheck ( this , Translator ) ;
if ( ! language ) {
throw new TypeError ( 'Parameter `language` must be a language string. Received ' + language + ( language === '' ? '(empty string)' : '' ) ) ;
}
this . lang = language ;
this . translations = { } ;
}
Translator . prototype . load = load ;
/ * *
* Parse the translation instructions into the language of the Translator instance
* @ param { string } str - Source string
* @ returns { Promise < string > }
* /
Translator . prototype . translate = function translate ( str ) {
// current cursor position
var cursor = 0 ;
// last break of the input string
var lastBreak = 0 ;
// length of the input string
var len = str . length ;
// array to hold the promises for the translations
// and the strings of untranslated text in between
var toTranslate = [ ] ;
// split a translator string into an array of tokens
// but don't split by commas inside other translator strings
function split ( text ) {
var len = text . length ;
var arr = [ ] ;
var i = 0 ;
var brk = 0 ;
var level = 0 ;
while ( i + 2 <= len ) {
if ( text . slice ( i , i + 2 ) === '[[' ) {
level += 1 ;
i += 1 ;
} else if ( text . slice ( i , i + 2 ) === ']]' ) {
level -= 1 ;
i += 1 ;
} else if ( level === 0 && text [ i ] === ',' ) {
arr . push ( text . slice ( brk , i ) . trim ( ) ) ;
i += 1 ;
brk = i ;
}
i += 1 ;
}
arr . push ( text . slice ( brk , i + 1 ) . trim ( ) ) ;
return arr ;
}
// the loooop, we'll go to where the cursor
// is equal to the length of the string since
// slice doesn't include the ending index
while ( cursor + 2 <= len ) {
// if the current position in the string looks
// like the beginning of a translation string
if ( str . slice ( cursor , cursor + 2 ) === '[[' ) {
// split the string from the last break
// to the character before the cursor
// add that to the result array
toTranslate . push ( str . slice ( lastBreak , cursor ) ) ;
// set the cursor position past the beginning
// brackets of the translation string
cursor += 2 ;
// set the last break to our current
// spot since we just broke the string
lastBreak = cursor ;
// the current level of nesting of the translation strings
var level = 0 ;
var sliced ;
while ( cursor + 2 <= len ) {
sliced = str . slice ( cursor , cursor + 2 ) ;
// if we're at the beginning of another translation string,
// we're nested, so add to our level
if ( sliced === '[[' ) {
level += 1 ;
cursor += 2 ;
// if we're at the end of a translation string
} else if ( sliced === ']]' ) {
// if we're at the base level, then this is the end
if ( level === 0 ) {
// so grab the name and args
var result = split ( str . slice ( lastBreak , cursor ) ) ;
var name = result [ 0 ] ;
var args = result . slice ( 1 ) ;
// add the translation promise to the array
toTranslate . push ( this . translateKey ( name , args ) ) ;
// skip past the ending brackets
cursor += 2 ;
// set this as our last break
lastBreak = cursor ;
// and we're no longer in a translation string,
// so continue with the main loop
break ;
}
// otherwise we lower the level
level -= 1 ;
// and skip past the ending brackets
cursor += 2 ;
} else {
// otherwise just move to the next character
cursor += 1 ;
}
}
}
// move to the next character
cursor += 1 ;
}
}
var languages = { } ,
// add the remaining text after the last translation string
regexes = {
toTranslate . push ( str . slice ( lastBreak , cursor + 2 ) ) ;
match : /\[\[\w+:[\w\.]+((?!\[\[).)*?\]\]/g , // see tests/translator.js for an explanation re: this monster
split : /[,][\s]*/ ,
// and return a promise for the concatenated translated string
replace : /\]+$/
return Promise . all ( toTranslate ) . then ( function ( translated ) {
return translated . join ( '' ) ;
} ) ;
} ;
} ;
translator . addTranslation = function ( language , filename , translations ) {
/ * *
languages [ language ] = languages [ language ] || { } ;
* Translates a specific key and array of arguments
languages [ language ] . loaded = languages [ language ] . loaded || { } ;
* @ param { string } name - Translation key ( ex . 'global:home' )
languages [ language ] . loading = languages [ language ] . loading || { } ;
* @ param { string [ ] } args - Arguments for ` %1 ` , ` %2 ` , etc
* @ returns { Promise < string > }
* /
Translator . prototype . translateKey = function translateKey ( name , args ) {
var self = this ;
var result = name . split ( ':' , 2 ) ;
var namespace = result [ 0 ] ;
var key = result [ 1 ] ;
var translation = this . getTranslation ( namespace , key ) ;
var argsToTranslate = args . map ( function ( arg ) {
return string ( arg ) . collapseWhitespace ( ) . decodeHTMLEntities ( ) . escapeHTML ( ) . s ;
} ) . map ( function ( arg ) {
return self . translate ( arg ) ;
} ) ;
if ( languages [ language ] . loaded [ filename ] ) {
// so we can await all promises at once
for ( var t in translations ) {
argsToTranslate . unshift ( translation ) ;
if ( translations . hasOwnProperty ( t ) ) {
languages [ language ] . loaded [ filename ] [ t ] = translations [ t ] ;
return Promise . all ( argsToTranslate ) . then ( function ( result ) {
}
var translated = result [ 0 ] ;
}
var translatedArgs = result . slice ( 1 ) ;
} else {
languages [ language ] . loaded [ filename ] = translations ;
if ( ! translated ) {
return key ;
}
}
var out = translated ;
translatedArgs . forEach ( function ( arg , i ) {
out = out . replace ( new RegExp ( '%' + ( i + 1 ) , 'g' ) , arg ) ;
} ) ;
return out ;
} ) ;
} ;
} ;
translator . getTranslations = function ( language , filename , callback ) {
/ * *
if ( languages [ language ] && languages [ language ] . loaded [ filename ] ) {
* Load translation file ( or use a cached version ) , and optionally return the translation of a certain key
callback ( languages [ language ] . loaded [ filename ] ) ;
* @ param { string } namespace - The file name of the translation namespace
* @ param { string } [ key ] - The key of the specific translation to getJSON
* @ returns { Promise < Object | string > }
* /
Translator . prototype . getTranslation = function getTranslation ( namespace , key ) {
var translation ;
if ( ! namespace ) {
winston . warn ( '[translator] Parameter `namespace` is ' + namespace + ( namespace === '' ? '(empty string)' : '' ) ) ;
translation = Promise . resolve ( { } ) ;
} else if ( this . translations [ namespace ] ) {
translation = this . translations [ namespace ] ;
} else {
} else {
translator . load ( language , filename , function ( ) {
translation = this . load ( this . lang , namespace ) ;
callback ( languages [ language ] . loaded [ filename ] ) ;
this . translations [ namespace ] = translation ;
}
if ( key ) {
return translation . then ( function ( x ) {
return x [ key ] ;
} ) ;
} ) ;
}
}
return translation ;
} ;
} ;
translator . escape = function ( text ) {
/ * *
return typeof text === 'string' ? text . replace ( /\[\[([\S]*?)\]\]/g , '\\[\\[$1\\]\\]' ) : text ;
* Get the language of the current environment , falling back to defaults
} ;
* @ returns { string }
* /
Translator . getLanguage = function getLanguage ( ) {
var lang ;
translator . unescape = function ( text ) {
if ( typeof window === 'object' && window . config && window . utils ) {
return typeof text === 'string' ? text . replace ( /\\\[\\\[([\S]*?)\\\]\\\]/g , '[[$1]]' ) : text ;
lang = utils . params ( ) . lang || config . userLang || config . defaultLang || 'en_GB' ;
} else {
var meta = require ( '../../../src/meta' ) ;
lang = meta . config . defaultLang || 'en_GB' ;
}
return lang ;
} ;
} ;
translator . getLanguage = function ( ) {
/ * *
return config . defaultLang ;
* Create and cache a new Translator instance , or return a cached one
* @ param { string } [ language ] - ( 'en_GB' ) Language string
* @ returns { Translator }
* /
Translator . create = function create ( language ) {
if ( ! language ) {
language = Translator . getLanguage ( ) ;
}
Translator . cache [ language ] = Translator . cache [ language ] || new Translator ( language ) ;
return Translator . cache [ language ] ;
} ;
} ;
translator . prepareDOM = function ( ) {
Translator . cache = { } ;
// Load the appropriate timeago locale file, and correct NodeBB language codes to timeago codes, if necessary
var languageCode ;
return Translator ;
switch ( config . userLang ) {
} ( ) ;
var adaptor = {
/ * *
* The Translator class
* /
Translator : Translator ,
/ * *
* Legacy translator function for backwards compatibility
* /
translate : function translate ( text , language , callback ) {
// console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' +
// 'Use the `translator.Translator` class instead.');
var cb = callback ;
var lang = language ;
if ( typeof language === 'function' ) {
cb = language ;
lang = null ;
}
Translator . create ( lang ) . translate ( text ) . then ( function ( output ) {
return cb ( output ) ;
} ) . catch ( function ( err ) {
console . error ( 'Translation failed: ' + err . message ) ;
} ) ;
} ,
/ * *
* Construct a translator pattern
* @ param { string } name - Translation name
* @ param { string [ ] } args - Optional arguments for the pattern
* /
compile : function compile ( ) {
var args = Array . prototype . slice . call ( arguments , 0 ) ;
return '[[' + args . join ( ', ' ) + ']]' ;
} ,
/ * *
* Escape translation patterns from text
* /
escape : function escape ( text ) {
return typeof text === 'string' ? text . replace ( /\[\[([\S]*?)\]\]/g , '\\[\\[$1\\]\\]' ) : text ;
} ,
/ * *
* Unescape translation patterns from text
* /
unescape : function unescape ( text ) {
return typeof text === 'string' ? text . replace ( /\\\[\\\[([\S]*?)\\\]\\\]/g , '[[$1]]' ) : text ;
} ,
/ * *
* Add translations to the cache
* /
addTranslation : function addTranslation ( language , filename , translation ) {
Translator . create ( language ) . getTranslation ( filename ) . then ( function ( translations ) {
assign ( translations , translation ) ;
} ) ;
} ,
/ * *
* Get the translations object
* /
getTranslations : function getTranslations ( language , filename , callback ) {
callback = callback || function ( ) { } ;
Translator . create ( language ) . getTranslation ( filename ) . then ( callback ) ;
} ,
/ * *
* Alias of getTranslations
* /
load : function load ( language , filename , callback ) {
adaptor . getTranslations ( language , filename , callback ) ;
} ,
/ * *
* Get the language of the current environment , falling back to defaults
* /
getLanguage : Translator . getLanguage ,
toggleTimeagoShorthand : function toggleTimeagoShorthand ( ) {
var tmp = assign ( { } , jQuery . timeago . settings . strings ) ;
jQuery . timeago . settings . strings = assign ( { } , adaptor . timeagoShort ) ;
adaptor . timeagoShort = assign ( { } , tmp ) ;
} ,
prepareDOM : function prepareDOM ( ) {
// Load the appropriate timeago locale file,
// and correct NodeBB language codes to timeago codes, if necessary
var languageCode = void 0 ;
switch ( config . userLang ) {
case 'en_GB' :
case 'en_GB' :
case 'en_US' :
case 'en_US' :
languageCode = 'en' ;
languageCode = 'en' ;
@ -96,252 +392,25 @@
break ;
break ;
}
}
$ . getScript ( RELATIVE _PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js' ) . done ( function ( ) {
jQuery . getScript ( RELATIVE _PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '.js' ) . done ( function ( ) {
$ ( '.timeago' ) . timeago ( ) ;
jQuery ( '.timeago' ) . timeago ( ) ;
translator . timeagoShort = $ . extend ( { } , jQuery . timeago . settings . strings ) ;
adaptor . timeagoShort = assign ( { } , jQuery . timeago . settings . strings ) ;
// Retrieve the shorthand timeago values as well
// Retrieve the shorthand timeago values as well
$ . getScript ( RELATIVE _PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js' ) . done ( function ( ) {
jQuery . getScript ( RELATIVE _PATH + '/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js' ) . done ( function ( ) {
// Switch back to long-form
// Switch back to long-form
transla tor. toggleTimeagoShorthand ( ) ;
adap tor. toggleTimeagoShorthand ( ) ;
} ) ;
} ) ;
} ) ;
} ) ;
// Add directional code if necessary
// Add directional code if necessary
transla tor. translate ( '[[language:dir]]' , function ( value ) {
adap tor. translate ( '[[language:dir]]' , function ( value ) {
if ( value ) {
if ( value ) {
$ ( 'html' ) . css ( 'direction' , value ) . attr ( 'data-dir' , value ) ;
jQuery ( 'html' ) . css ( 'direction' , value ) . attr ( 'data-dir' , value ) ;
}
}
} ) ;
} ) ;
} ;
translator . toggleTimeagoShorthand = function ( ) {
var tmp = $ . extend ( { } , jQuery . timeago . settings . strings ) ;
jQuery . timeago . settings . strings = $ . extend ( { } , translator . timeagoShort ) ;
translator . timeagoShort = $ . extend ( { } , tmp ) ;
} ;
translator . translate = function ( text , language , callback ) {
if ( typeof language === 'function' ) {
callback = language ;
if ( 'undefined' !== typeof window && config ) {
language = utils . params ( ) . lang || config . userLang || 'en_GB' ;
} else {
var meta = require ( '../../../src/meta' ) ;
language = meta . config . defaultLang || 'en_GB' ;
}
}
if ( ! text ) {
return callback ( text ) ;
}
}
var keys = text . match ( regexes . match ) ;
if ( ! keys ) {
return callback ( text ) ;
}
translateKeys ( keys , text , language , function ( translated ) {
keys = translated . match ( regexes . match ) ;
if ( ! keys ) {
callback ( translated ) ;
} else {
translateKeys ( keys , translated , language , callback ) ;
}
} ) ;
} ;
} ;
function translateKeys ( keys , text , language , callback ) {
return adaptor ;
} ) ;
var count = keys . length ;
if ( ! count ) {
return callback ( text ) ;
}
if ( S === null ) { // browser environment and S not yet initialized
// we need to wait for async require call
stringDefer . then ( function ( ) { translateKeys ( keys , text , language , callback ) ; } ) ;
return ;
}
var data = { text : text } ;
keys . forEach ( function ( key ) {
translateKey ( key , data , language , function ( translated ) {
-- count ;
if ( count <= 0 ) {
callback ( translated . text ) ;
}
} ) ;
} ) ;
}
function translateKey ( key , data , language , callback ) {
key = '' + key ;
var variables = key . split ( regexes . split ) ;
var parsedKey = key . replace ( '[[' , '' ) . replace ( ']]' , '' ) . split ( ':' ) ;
parsedKey = [ parsedKey [ 0 ] ] . concat ( parsedKey . slice ( 1 ) . join ( ':' ) ) ;
if ( ! ( parsedKey [ 0 ] && parsedKey [ 1 ] ) ) {
return callback ( data ) ;
}
var languageFile = parsedKey [ 0 ] ;
parsedKey = ( '' + parsedKey [ 1 ] ) . split ( ',' ) [ 0 ] ;
translator . load ( language , languageFile , function ( languageData ) {
data . text = insertLanguage ( data . text , key , languageData [ parsedKey ] , variables ) ;
callback ( data ) ;
} ) ;
}
function insertLanguage ( text , key , value , variables ) {
if ( value ) {
variables . forEach ( function ( variable , index ) {
if ( index > 0 ) {
variable = S ( variable ) . chompRight ( ']]' ) . collapseWhitespace ( ) . decodeHTMLEntities ( ) . escapeHTML ( ) . s ;
value = value . replace ( '%' + index , function ( ) { return variable ; } ) ;
}
} ) ;
text = text . replace ( key , function ( ) { return value ; } ) ;
} else {
var string = key . split ( ':' ) ;
text = text . replace ( key , string [ string . length - 1 ] . replace ( regexes . replace , '' ) ) ;
}
return text ;
}
translator . compile = function ( ) {
var args = Array . prototype . slice . call ( arguments , 0 ) ;
return '[[' + args . join ( ', ' ) + ']]' ;
} ;
translator . load = function ( language , filename , callback ) {
if ( isLanguageFileLoaded ( language , filename ) ) {
if ( callback ) {
callback ( languages [ language ] . loaded [ filename ] ) ;
}
} else if ( isLanguageFileLoading ( language , filename ) ) {
if ( callback ) {
addLanguageFileCallback ( language , filename , callback ) ;
}
} else {
languages [ language ] = languages [ language ] || { loading : { } , loaded : { } , callbacks : [ ] } ;
languages [ language ] . loading [ filename ] = true ;
load ( language , filename , function ( translations ) {
languages [ language ] . loaded [ filename ] = translations ;
if ( callback ) {
callback ( translations ) ;
}
while ( languages [ language ] . callbacks && languages [ language ] . callbacks [ filename ] && languages [ language ] . callbacks [ filename ] . length ) {
languages [ language ] . callbacks [ filename ] . pop ( ) ( translations ) ;
}
languages [ language ] . loading [ filename ] = false ;
} ) ;
}
} ;
function isLanguageFileLoaded ( language , filename ) {
var languageObj = languages [ language ] ;
return languageObj && languageObj . loaded && languageObj . loaded [ filename ] && ! languageObj . loading [ filename ] ;
}
function isLanguageFileLoading ( language , filename ) {
return languages [ language ] && languages [ language ] . loading && languages [ language ] . loading [ filename ] ;
}
function addLanguageFileCallback ( language , filename , callback ) {
languages [ language ] . callbacks = languages [ language ] . callbacks || { } ;
languages [ language ] . callbacks [ filename ] = languages [ language ] . callbacks [ filename ] || [ ] ;
languages [ language ] . callbacks [ filename ] . push ( callback ) ;
}
function load ( language , filename , callback ) {
if ( 'undefined' !== typeof window ) {
loadClient ( language , filename , callback ) ;
} else {
loadServer ( language , filename , callback ) ;
}
}
function loadClient ( language , filename , callback ) {
$ . getJSON ( config . relative _path + '/language/' + language + '/' + filename + '.json?v=' + config [ 'cache-buster' ] , callback ) ;
}
function loadServer ( language , filename , callback ) {
var fs = require ( 'fs' ) ,
path = require ( 'path' ) ,
winston = require ( 'winston' ) ,
_ = require ( 'underscore' ) ,
file = require ( '../../../src/file' ) ,
plugins = require ( '../../../src/plugins' ) ,
meta = require ( '../../../src/meta' ) ;
var hash = language + '/' + filename + '.json' ;
language = language || meta . config . defaultLang || 'en_GB' ;
if ( ! file . existsSync ( path . join ( _ _dirname , '../../language' , language ) ) ) {
winston . warn ( '[translator] Language \'' + meta . config . defaultLang + '\' not found. Defaulting to \'en_GB\'' ) ;
language = 'en_GB' ;
}
fs . readFile ( path . join ( _ _dirname , '../../language' , hash ) , function ( err , data ) {
var onData = function ( data ) {
try {
data = JSON . parse ( data . toString ( ) ) ;
if ( plugins . customLanguages . hasOwnProperty ( hash ) ) {
_ . extendOwn ( data , plugins . customLanguages [ hash ] ) ;
}
} catch ( e ) {
winston . error ( 'Could not parse `' + filename + '.json`, syntax error? Skipping...' ) ;
data = { } ;
}
callback ( data ) ;
} ;
if ( err && err . code === 'ENOENT' ) {
data = '{}' ;
} else if ( err ) {
winston . error ( '[translator] Error while loading language file: ' + err . message ) ;
return callback ( { } ) ;
}
onData ( data ) ;
} ) ;
}
// Use the define() function if we're in AMD land
if ( typeof define === 'function' && define . amd ) {
define ( 'translator' , translator ) ;
var _translator = translator ;
// Expose a global `translator` object for backwards compatibility
window . translator = {
translate : function ( ) {
if ( typeof console !== 'undefined' && console . warn ) {
console . warn ( '[translator] Global invocation of the translator is now deprecated, please `require` the module instead.' ) ;
}
_translator . translate . apply ( _translator , arguments ) ;
}
} ;
}
} ) (
typeof exports === 'object' ? exports :
typeof define === 'function' && define . amd ? { } :
translator = { }
) ;