ez-localize
Version:
Super-simple localization of strings in a Node/Browserify application
195 lines (162 loc) • 5.67 kB
text/typescript
import { Locale, LocalizedString, LocalizerData, LocalizeString } from "."
/**
* Localizer is a function that sets up global variable "T" which is
* used to translate strings. Also sets up Handlebars helper with same name
* Function "T" maps to Localizer "localizeString" function
* Helper "T" maps to Localizer "localizeString" function
*/
export default class Localizer {
data: LocalizerData
/** Current locale */
locale: string
englishMap: { [english: string]: LocalizedString }
T: LocalizeString
/** Locale defaults to "en" */
constructor(data: LocalizerData, locale?: string) {
this.data = data
this.locale = locale || "en"
// Index strings by English if data present
this.englishMap = {}
if (data != null) {
for (let str of this.data.strings) {
this.englishMap[str.en] = str
}
}
const localizer = this
Object.defineProperty(this.localizeString, "locale", { get() { return localizer.locale }})
Object.defineProperty(this.localizeString, "localizer", { get() { return localizer }})
this.T = this.localizeString as LocalizeString
}
/** Set the current locale */
setLocale(locale: string): void {
this.locale = locale
}
getLocales(): Locale[] {
return this.data.locales
}
/**
* Localize a single string of the form "some text {0} more text {1} etc", replacing the
* parts with the arguments.
*
* Can also replace where the first parameter is an array for ES6 tagged templates.
*
* Can also localize to a specified locale with LocalizationRequest.
*/
localizeString = (str: TemplateStringsArray | LocalizedString | LocalizationRequest | string | null | undefined, ...args: any[]): string | null => {
// Null is just pass-through
if (str == null) {
return str ?? null
}
// Handle localization request
if (typeof str === "object" && "text" in str) {
return this.localizeStringRequest(str as LocalizationRequest)
}
// Handle localized string
if (typeof str === "object" && !Array.isArray(str) && (str as LocalizedString)._base) {
// Get localized string
const locStr = (str as LocalizedString)[this.locale] || (str as LocalizedString).en
return this.localizeString(locStr, ...args)
}
// True if object passed in as arg (react style)
let hasObject = args.some(arg => arg && typeof arg === "object")
// Handle ES6-style
if (Array.isArray(str)) {
// Change to format of standard localizer string
let newStr = str[0]
for (let i = 1 ; i < str.length ; i++) {
newStr += "{" + (i - 1) + "}"
newStr += str[i]
}
str = newStr
}
if (!hasObject) {
return this.localizeStringRequest({ locale: this.locale, text: str as string, args })
} else {
// Find string, falling back to English
let locstr
const item = this.englishMap[str as string]
if (item && item[this.locale]) {
locstr = item[this.locale]
} else {
locstr = str as string
}
// Split and do react-style replacement where string is made into array
const parts = locstr.split(/(\{\d+\})/)
const output = []
for (let part of parts) {
if (part.match(/^\{\d+\}$/)) {
output.push(args[parseInt(part.substr(1, part.length - 2))])
} else {
output.push(part)
}
}
return (output as unknown) as string
}
}
/**
* Localizes a plain string without React-style interpretation. Needed for handlebars as it passes extra arguments
*/
localizePlainString = (str: any, ...args: any[]) => {
return this.localizeStringRequest({ locale: this.locale, text: str, args })
}
/**
* Localizes a string based on a localization request.
*/
localizeStringRequest(request: LocalizationRequest): string | null {
let { locale, text, args } = request
if (text == null) {
return null
}
let textStr: string
if (typeof text === "object" && (text as LocalizedString)._base) {
// Get localized string
const localizedStr = (text as LocalizedString)[locale ?? this.locale] ?? (text as LocalizedString).en
if (localizedStr == null) {
return null
}
textStr = localizedStr
} else {
textStr = text as string
}
// Find string, falling back to English
let locstr: string
const item = this.englishMap[textStr]
if (item && item[locale ?? this.locale]) {
locstr = item[locale ?? this.locale]
} else {
locstr = textStr
}
// Strip context if present
locstr = locstr.split("|")[0]
// Fill in arguments
if (args) {
for (let i = 0; i < args.length ; i++) {
locstr = locstr.replace("{" + i + "}", args[i])
}
}
return locstr
}
/** Determines if a string is localized */
isLocalized(str: string): boolean {
return str != null && this.englishMap[str] != null && this.englishMap[str][this.locale] != null
}
// Makes this localizer global. handlebars is instance to register
// helper on, null for none
makeGlobal(handlebars: any) {
(global as any).T = this.T;
if (handlebars != null) {
handlebars.registerHelper("T", this.localizePlainString)
}
}
}
/**
* Localization request
*/
export interface LocalizationRequest {
/** Locale to localize to. e.g. "en" or "fr". Default is current locale. */
locale?: string
/** Text to localize in format of "some text {0} more text {1} etc" */
text: string | LocalizedString | null | undefined
/** Arguments to substitute into the localized string */
args?: any[]
}