UNPKG

@socketsupply/socket

Version:

A Cross-Platform, Native Runtime for Desktop and Mobile Apps — Create apps using HTML, CSS, and JavaScript. Written from the ground up to be small and maintainable.

228 lines (196 loc) 6.23 kB
/** * @module i18n * Functions to internationalize your application. * These module APIs can be used to get localized strings from locale files * packaged with your application, find out the browser's current language, and find out the value of its Accept-Language header. */ /* global XMLHttpRequest */ import Enumeration from './enumeration.js' import application from './application.js' import language from './language.js' import location from './location.js' import hooks from './hooks.js' // preload environment languages when environment is ready and listen // for language change events hooks.onReady(preloadUILanguage) hooks.onLanguageChange(preloadUILanguage) /** * Preloads current UI language into cache. * @ignore */ function preloadUILanguage () { for (const language of getAcceptLanguages()) { queueMicrotask(() => getMessagesForLocale(language)) } } /** * A cache of loaded locale messages. * @type {Map} */ export const cache = new Map() /** * Default location of i18n locale messages * @type {string} */ export const DEFAULT_LOCALES_LOCATION = ( application.config.i18n_locales_default || 'i18n/locales' ) /** * Get messages for `locale` pattern. This function could return many results * for various locales given a `locale` pattern. such as `fr`, which could * return results for `fr`, `fr-FR`, `fr-BE`, etc. * @ignore * @param {string} locale * @return {object[]} */ export function getMessagesForLocale (locale) { const results = [] const tags = language.lookup(locale) .map((l) => l.tags) .reduce((a, t) => a.concat(t), []) while (tags.length) { const tag = tags.shift() const source = ( application.config[`i18n_locales_${tag}_source`] || application.config[`i18n_locales_${tag.toLowerCase()}_source`] || application.config[`i18n_locales_${tag}`] || application.config[`i18n_locales_${tag.toLowerCase()}`] || `${DEFAULT_LOCALES_LOCATION}/${tag}` ) const path = source.endsWith('/') ? source : source + '/' const url = String(new URL('messages.json', new URL(path, location.origin))) if (cache.has(url)) { results.push(cache.get(url)) continue } const request = new XMLHttpRequest() try { request.open('GET', url, false) request.send(null) } catch (err) { console.warn(err.message) continue } let result = null // `request.responseText` can throw `InvalidStateError` error try { result = { locale: tag, // @ts-ignore messages: JSON.parse(request.responseText) } } catch { try { result = { locale: tag, messages: JSON.parse(String(request.response)) } } catch { continue } } if (result) { results.push(result) cache.set(url, result) } } return results } /** * An enumeration of supported ISO 639 language codes or RFC 5646 language tags. * @type {Enumeration} * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/LanguageCode} * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} */ export const LanguageCode = Enumeration.from(language.tags) /** * Returns user preferred ISO 639 language codes or RFC 5646 language tags. * @return {string[]} */ export function getAcceptLanguages () { return globalThis.navigator?.languages ?? [] } /** * Returns the current user ISO 639 language code or RFC 5646 language tag. * @return {?string} */ export function getUILanguage () { return globalThis.navigator?.language ?? null } /** * Gets a localized message string for the specified message name. * @param {string} messageName * @param {object|string[]=} [substitutions = []] * @param {object=} [options] * @param {string=} [options.locale = null] * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} * @see {@link https://www.ibm.com/docs/en/rbd/9.5.1?topic=syslib-getmessage} * @return {?string} */ export function getMessage ( messageName, substitutions = [], options = { locale: null } ) { if (typeof substitutions === 'object' && !Array.isArray(substitutions)) { options = substitutions } const locale = options?.locale ?? getUILanguage() const results = getMessagesForLocale(locale) for (const result of results) { if (messageName in result.messages) { const { message, placeholders } = result.messages[messageName] let interpolated = message for (let i = 0; i < substitutions.length; ++i) { interpolated = interpolated .replace(new RegExp(`\\{${i}\\}`, 'g'), substitutions[i]) .replace(new RegExp(`\\$${i}\\$?`, 'g'), substitutions[i]) } if (placeholders && typeof placeholders === 'object') { for (const key in placeholders) { const placeholder = placeholders[key] let content = placeholder.content || placeholder // interpolate placeholder content for (let i = 0; i < substitutions.length; ++i) { content = content .replace(new RegExp(`\\{${i}\\}`, 'g'), substitutions[i]) .replace(new RegExp(`\\$${i}\\$?`, 'g'), substitutions[i]) } interpolated = interpolated .replace(new RegExp(`\\{${key}\\}`, 'gi'), content) .replace(new RegExp(`\\$${key}\\$?`, 'gi'), content) } } return interpolated } } return null } /** * Gets a localized message description string for the specified message name. * @param {string} messageName * @param {object=} [options] * @param {string=} [options.locale = null] * @return {?string} */ export function getMessageDescription ( messageName, options = { locale: null } ) { const locale = options?.locale ?? getUILanguage() const results = getMessagesForLocale(locale) for (const result of results) { if (messageName in result.messages) { const { description } = result.messages[messageName] return description || null } } return null } export default { LanguageCode, getAcceptLanguages, getMessage, getUILanguage }