Import other modules into monorepo
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
3
src/i18n/index.ts
Normal file
3
src/i18n/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './service/Locale'
|
||||
export * from './modules/InjectRequestLocale'
|
||||
export * from './service/Internationalization'
|
||||
40
src/i18n/modules/InjectRequestLocale.ts
Normal file
40
src/i18n/modules/InjectRequestLocale.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import {Request} from "../../http/lifecycle/Request";
|
||||
import {Injectable} from "../../di"
|
||||
import {Locale} from "../service/Locale";
|
||||
import {HTTPKernelModule} from "../../http/kernel/HTTPKernelModule";
|
||||
import {HTTPKernel} from "../../http/kernel/HTTPKernel";
|
||||
import {InjectSessionHTTPModule} from "../../http/kernel/module/InjectSessionHTTPModule";
|
||||
import {Session} from "../../http/session/Session";
|
||||
|
||||
/**
|
||||
* An HTTP kernel module that adds the Locale service to the request container.
|
||||
*/
|
||||
@Injectable()
|
||||
export class InjectRequestLocale extends HTTPKernelModule {
|
||||
public executeWithBlockingWriteback = true
|
||||
|
||||
/** Register this kernel module to the given kernel. */
|
||||
public static register(kernel: HTTPKernel) {
|
||||
kernel.register(this).after(InjectSessionHTTPModule)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the default locale in the session and instantiates a Locale
|
||||
* service into the Request container based on said locale.
|
||||
* @param request
|
||||
*/
|
||||
public async apply(request: Request) {
|
||||
const session = <Session> request.make(Session)
|
||||
const locale = <Locale> request.make(Locale)
|
||||
|
||||
// Set the default locale in the session
|
||||
if ( !session.get('i18n.locale') ) {
|
||||
session.set('i18n.locale', locale.getDefaultLocale())
|
||||
}
|
||||
|
||||
locale.setLocale(session.get('i18n.locale'))
|
||||
request.registerSingletonInstance(Locale, locale)
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
||||
43
src/i18n/service/Internationalization.ts
Normal file
43
src/i18n/service/Internationalization.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {Singleton, Inject} from "../../di"
|
||||
import {CommandLine} from "../../cli"
|
||||
import {InjectRequestLocale} from "../modules/InjectRequestLocale"
|
||||
import {locale_template} from "../template/locale"
|
||||
import {Unit} from "../../lifecycle/Unit";
|
||||
import {HTTPKernel} from "../../http/kernel/HTTPKernel";
|
||||
import {Config} from "../../service/Config";
|
||||
import {Logging} from "../../service/Logging";
|
||||
|
||||
/**
|
||||
* Application unit to register @extollo/i18n resources.
|
||||
*/
|
||||
@Singleton()
|
||||
export class Internationalization extends Unit {
|
||||
@Inject()
|
||||
protected readonly kernel!: HTTPKernel
|
||||
|
||||
@Inject()
|
||||
protected readonly config!: Config
|
||||
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
@Inject()
|
||||
protected readonly cli!: CommandLine
|
||||
|
||||
/**
|
||||
* Registers the locale template and middleware, if enabled by config.
|
||||
*
|
||||
* You can set the "locale.enable" config property to `false` to disable
|
||||
* the InjectRequestLocale HTTP kernel module.
|
||||
*/
|
||||
up() {
|
||||
this.logging.debug(`Registering locale template with CLI...`)
|
||||
this.cli.registerTemplate(locale_template)
|
||||
|
||||
if ( this.config.get('locale.enable', true) ) {
|
||||
this.kernel.register(InjectRequestLocale).before()
|
||||
} else {
|
||||
this.logging.warn(`@extollo/i18n is registered, but disabled by config. Localization will not be done per-request.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/i18n/service/Locale.ts
Normal file
206
src/i18n/service/Locale.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
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() {
|
||||
// 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() {
|
||||
return this.config.get('locale.default', 'en_US')
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the preferred locale for lookups by this service.
|
||||
* @param locale
|
||||
*/
|
||||
setLocale(locale: string) {
|
||||
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 is_plural = plural && plural !== 1
|
||||
|
||||
if ( !translated ) {
|
||||
return fallback ?? specific
|
||||
}
|
||||
|
||||
let ret = ''
|
||||
|
||||
if ( typeof translated === 'object' ) {
|
||||
if ( is_plural && translated.many ) {
|
||||
ret = translated.many
|
||||
} else if ( is_plural && translated.one ) {
|
||||
ret = pluralize(translated.one, plural)
|
||||
} else if ( !is_plural && translated.one ) {
|
||||
ret = translated.one
|
||||
} else if ( !is_plural && 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, is_plural ? 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 ) {
|
||||
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) {
|
||||
const subloc = locale.split(':').slice(1).join(':')
|
||||
const specific_loc = locale.split(':').reverse()[0].split('.').slice(1).join('.')
|
||||
|
||||
let common: any = this.config.get(`lang:common${subloc ? ':' + subloc : ''}${specific_loc ? '.' + specific_loc : ''}`, 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}
|
||||
}
|
||||
}
|
||||
30
src/i18n/template/locale.ts
Normal file
30
src/i18n/template/locale.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import {Template} from "../../cli"
|
||||
import {UniversalPath} from "../../util"
|
||||
import {Container} from "../../di"
|
||||
import {Config} from "../../service/Config";
|
||||
|
||||
/**
|
||||
* CLI template that generates a new locale file.
|
||||
* Automatically adds placeholder entries for phrases that exist in the
|
||||
* associated common locale file.
|
||||
*/
|
||||
const locale_template: Template = {
|
||||
name: 'locale',
|
||||
fileSuffix: '.config.ts',
|
||||
description: 'Create a new config file that specifies translations for a locale.',
|
||||
baseAppPath: ['configs', 'lang'],
|
||||
render: (name: string, fullCanonicalName: string, targetFilePath: UniversalPath) => {
|
||||
const config = <Config> Container.getContainer().make(Config)
|
||||
const subloc = fullCanonicalName.split(':').slice(1).join(':')
|
||||
const common: any = config.get(`lang:common${subloc ? ':' + subloc : ''}`, {})
|
||||
|
||||
return `import {env} from '@extollo/lib'
|
||||
|
||||
export default {
|
||||
${Object.keys(common).map(key => ' ' + key + ': \'\',\n')}
|
||||
}
|
||||
`
|
||||
},
|
||||
}
|
||||
|
||||
export { locale_template }
|
||||
Reference in New Issue
Block a user