|
|
|
import {Injectable} from '../../di'
|
|
|
|
import {ErrorWithContext} from '../../util'
|
|
|
|
import * as pluralize from 'pluralize'
|
|
|
|
import {AppClass} from '../../lifecycle/AppClass'
|
|
|
|
import {Config} from '../../service/Config'
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Type name for the standalone localization helper function that can be passed around
|
|
|
|
* in lieu of the Locale service.
|
|
|
|
*/
|
|
|
|
export type LocaleHelper = (phrase: string, { plural, fallback, interp }: {plural?: number, fallback?: string, interp?: {[key: string]: any}}) => string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Request-level service that provides localization of phrases based on config files.
|
|
|
|
*/
|
|
|
|
@Injectable()
|
|
|
|
export class Locale extends AppClass {
|
|
|
|
protected get config(): Config {
|
|
|
|
// For some reason, was having issues with this not injecting properly.
|
|
|
|
// TODO convert this back to @Inject() and solve that bug
|
|
|
|
return this.app().make<Config>(Config)
|
|
|
|
}
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
/**
|
|
|
|
* The preferred locale. This corresponds to the config prefix.
|
|
|
|
* @example en_US means "lang:en_US" config scope
|
|
|
|
* @example es_MX means "lang:es_MX" config scope
|
|
|
|
*/
|
|
|
|
protected locale?: string,
|
|
|
|
) {
|
|
|
|
super()
|
|
|
|
if ( !this.locale ) {
|
|
|
|
this.locale = this.getDefaultLocale()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the default locale that should be assigned if none is specified in the session.
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
getDefaultLocale(): string {
|
|
|
|
return this.config.get('locale.default', 'en_US')
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the preferred locale for lookups by this service.
|
|
|
|
* @param locale
|
|
|
|
*/
|
|
|
|
setLocale(locale: string): void {
|
|
|
|
this.locale = locale
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a wrapped helper function that can be passed around instead of this service.
|
|
|
|
*/
|
|
|
|
helper(): LocaleHelper {
|
|
|
|
return (phrase: string, { plural, fallback, interp }: {plural?: number, fallback?: string, interp?: {[key: string]: any}} = {}) => {
|
|
|
|
return this.get(phrase, {plural,
|
|
|
|
fallback,
|
|
|
|
interp})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resolve the given phrase to the locale of this service. This is where all the magic happens.
|
|
|
|
*
|
|
|
|
* This method tries to load the given phrase from the config using `load()` then localizes it
|
|
|
|
* using the specified parameters.
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* The pluralization can be specified using the `plural` parameter. If an explicit pluralization
|
|
|
|
* is specified in the phrase config, that will be used. Otherwise, the `pluralize` library is
|
|
|
|
* used to generate one automatically.
|
|
|
|
*
|
|
|
|
* ```typescript
|
|
|
|
* // Example phrase config:
|
|
|
|
* {
|
|
|
|
* apple: 'Apple',
|
|
|
|
* bunch_of_bananas: {
|
|
|
|
* one: 'Bunch of Bananas',
|
|
|
|
* many: 'Bunches of Bananas',
|
|
|
|
* }
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* // Example usage:
|
|
|
|
* locale.get('apple', { plural: 3 }) // => 'Apples'
|
|
|
|
* locale.get('apple') // => 'Apple'
|
|
|
|
* locale.get('bunch_of_bananas', { plural: 2 }) // => 'Bunches of Bananas'
|
|
|
|
* locale.get('bunch_of_bananas', { plural: 1}) // => 'Bunch of Bananas'
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* If a translation cannot be found, and a fallback is specified, the fallback will be returned.
|
|
|
|
* Otherwise, the value of `phrase` is returned instead.
|
|
|
|
*
|
|
|
|
* ```typescript
|
|
|
|
* locale.get('nonexistent_phrase', { fallback: 'Hello, world!' }) // => 'Hello, world!'
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* Values can be interpolated into phrases using the `interp` parameter. For example, if there is
|
|
|
|
* a phrase `my_phrase: 'Hello, :name:!`, the value of `:name:` can be replaced like so:
|
|
|
|
*
|
|
|
|
* ```typescript
|
|
|
|
* locale.get('my_phrase', {interp: {name: 'John Doe'}}) // => 'Hello, John Doe!'
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @example
|
|
|
|
* If a phrase cannot be found in the specific locale config, the service will try to load it from
|
|
|
|
* the equivalent `common` locale config. For example, if `this.locale` is `es_MX`:
|
|
|
|
*
|
|
|
|
* ```typescript
|
|
|
|
* // Example "lang:common:dashboard" config:
|
|
|
|
* {
|
|
|
|
* title: "MyDash2.0",
|
|
|
|
* header: "Welcome to the dashboard!",
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* // Example "lang:es_MX:dashboard" config:
|
|
|
|
* {
|
|
|
|
* header: "¡Bienvenido al panel de control!",
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* // Example usage:
|
|
|
|
* locale.get('dashboard.title') // => 'MyDash2.0'
|
|
|
|
* locale.get('dashboard.header') // => '¡Bienvenido al panel de control!'
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param phrase
|
|
|
|
* @param plural
|
|
|
|
* @param fallback
|
|
|
|
* @param interp
|
|
|
|
*/
|
|
|
|
get(phrase: string, { plural, fallback, interp }: {plural?: number, fallback?: string, interp?: {[key: string]: any}} = {}): string {
|
|
|
|
const scope = phrase.split(':').reverse()
|
|
|
|
.slice(1)
|
|
|
|
.reverse()
|
|
|
|
.join(':')
|
|
|
|
const specific = phrase.split(':').reverse()[0]
|
|
|
|
const load = `${this.locale}${scope ? ':' + scope : ''}.${specific}`
|
|
|
|
const translated = this.load(load)
|
|
|
|
const isPlural = plural && plural !== 1
|
|
|
|
|
|
|
|
if ( !translated ) {
|
|
|
|
return fallback ?? specific
|
|
|
|
}
|
|
|
|
|
|
|
|
let ret = ''
|
|
|
|
|
|
|
|
if ( typeof translated === 'object' ) {
|
|
|
|
if ( isPlural && translated.many ) {
|
|
|
|
ret = translated.many
|
|
|
|
} else if ( isPlural && translated.one ) {
|
|
|
|
ret = pluralize(translated.one, plural)
|
|
|
|
} else if ( !isPlural && translated.one ) {
|
|
|
|
ret = translated.one
|
|
|
|
} else if ( !isPlural && translated.many ) {
|
|
|
|
ret = pluralize(translated.many, 1)
|
|
|
|
} else {
|
|
|
|
throw new ErrorWithContext(`Invalid translation config for ${phrase}. Must provide 'one' or 'many' keys.`, {
|
|
|
|
locale: this.locale,
|
|
|
|
phrase,
|
|
|
|
plural,
|
|
|
|
fallback,
|
|
|
|
translated,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
} else if ( typeof translated === 'string' ) {
|
|
|
|
ret = pluralize(translated, isPlural ? 5 : 1)
|
|
|
|
} else {
|
|
|
|
throw new ErrorWithContext(`Invalid translation object for ${phrase}.`, {
|
|
|
|
locale: this.locale,
|
|
|
|
phrase,
|
|
|
|
plural,
|
|
|
|
fallback,
|
|
|
|
translated,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( interp ) {
|
|
|
|
for ( const key in interp ) {
|
|
|
|
if ( !Object.prototype.hasOwnProperty.call(interp, key) ) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
const rex = new RegExp(`:${key}:`, 'g')
|
|
|
|
ret = ret.replace(rex, interp[key])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempt to load the given locale string, merging in the appropriate common locale values.
|
|
|
|
* @param locale
|
|
|
|
* @protected
|
|
|
|
*/
|
|
|
|
protected load(locale: string): {[key: string]: string} | string {
|
|
|
|
const subloc = locale.split(':').slice(1)
|
|
|
|
.join(':')
|
|
|
|
const specificLocale = locale.split(':').reverse()[0].split('.').slice(1)
|
|
|
|
.join('.')
|
|
|
|
|
|
|
|
let common: any = this.config.get(`lang:common${subloc ? ':' + subloc : ''}${specificLocale ? '.' + specificLocale : ''}`, undefined)
|
|
|
|
let specific: any = this.config.get(`lang:${locale}`, undefined)
|
|
|
|
|
|
|
|
if ( typeof specific === 'string' ) {
|
|
|
|
return specific
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( typeof common === 'string' && typeof specific === 'undefined' ) {
|
|
|
|
return common
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !common ) {
|
|
|
|
common = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !specific ) {
|
|
|
|
specific = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {...common,
|
|
|
|
...specific}
|
|
|
|
}
|
|
|
|
}
|