UNPKG

@open-condo/miniapp-utils

Version:

A set of helper functions / components / hooks used to build new condo apps fast

411 lines (407 loc) 16.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/helpers/i18n.tsx var i18n_exports = {}; __export(i18n_exports, { TranslationsHelper: () => TranslationsHelper }); module.exports = __toCommonJS(i18n_exports); var import_cookies_next = require("cookies-next"); var import_react2 = __toESM(require("react")); // src/helpers/environment.ts function isSSR() { return typeof window === "undefined"; } // src/hooks/useEffectOnce.ts var import_react = require("react"); function useEffectOnce(cb) { (0, import_react.useEffect)(cb, []); } // src/helpers/i18n.tsx var LOCALE_PARSING_OPTIONS = [ { part: "primary", matcher: /^[a-z]+$/ }, { part: "extended", matcher: /^[a-z]{3}$/ }, { part: "script", matcher: /^[A-Z][a-z]{3,}$/ }, { part: "region", matcher: /^([A-Z]{2,3}|[0-9]{3})$/ } ]; var LOCALE_RESOLVE_ORDER = [ // Full resolution first ["primary", "extended", "script", "region"], // Extended usually does not affect message a lot, so it can be omitted first ["primary", "script", "region"], // Primary + script is more important, since script change alphabet ["primary", "script"], ["primary", "region"], ["primary", "extended"], ["primary"] ]; var I18N_SELECTED_LOCALE_PROP_NAME = "__I18N_SELECTED_LOCALE__"; var I18N_FULL_LOCALE_PROP_NAME = "__I18N_FULL_LOCALE__"; var I18N_MESSAGES_PROP_NAME = "__I18N_MESSAGES__"; var TranslationsHelper = class _TranslationsHelper { constructor(options) { this._translations = {}; this.localeCookieName = "NEXT_LOCALE"; this.localeQueryParam = void 0; this._locales = new Set(options.locales); this._defaultLocale = options.defaultLocale; this._loadMessages = options.loadMessages; this._loadDefaultMessages = options.loadDefaultMessages; if (options.localeCookieName) { this.localeCookieName = options.localeCookieName; } if (options.localeQueryParam) { this.localeQueryParam = options.localeQueryParam; } this.getTranslationsProvider = this.getTranslationsProvider.bind(this); this.getUseTranslationsHook = this.getUseTranslationsHook.bind(this); this.getUseTranslationsExtractorHook = this.getUseTranslationsExtractorHook.bind(this); this.getPreferredLocale = this.getPreferredLocale.bind(this); this.selectSupportedLocale = this.selectSupportedLocale.bind(this); this.getTranslations = this.getTranslations.bind(this); this.prefetchTranslations = this.prefetchTranslations.bind(this); this.getHOC = this.getHOC.bind(this); } /** * This util parses language-defining string according to RFC5646: https://datatracker.ietf.org/doc/html/rfc5646 * It also automatically detect and accept-language header format by enhancing result with quality info */ static parseLocaleString(localeString) { const stringParts = localeString.trim().split(";"); const localeParts = stringParts[0].split("-"); const quality = stringParts.length > 1 ? parseFloat(stringParts[1].split("=")[1]) : 1; const locale = { primary: localeParts[0], extended: void 0, script: void 0, region: void 0, quality }; let currentParser = 0; localePartsLoop: for (const localePart of localeParts) { for (; currentParser < LOCALE_PARSING_OPTIONS.length; currentParser++) { const { part, matcher } = LOCALE_PARSING_OPTIONS[currentParser]; if (matcher.test(localePart)) { locale[part] = localePart; currentParser++; continue localePartsLoop; } } break; } return locale; } /** * Parses "Accept-Language" header value using "parseLocaleString" util and returns array of AcceptLanguageInfo * sorted by descending quality. * * NOTE: Empty header or non-defined header is treated as "*" */ static parseAcceptLanguageHeader(headerValue) { return (headerValue || "*").split(",").map(_TranslationsHelper.parseLocaleString).sort((a, b) => b.quality - a.quality); } /** * Generates locale-string from LocaleInfo or AcceptLanguageInfo */ static toLocaleString(locale) { return [locale.primary, locale.extended, locale.script, locale.region].filter(Boolean).join("-"); } /** * Enrich selected locale by scanning through requested locales * and finding the first one, which is sub-locale of selected one. * For example: selectedLocale = "en", requestedLocales: ["en-GB", "fr", "en"] -> fullLocale = "en-GB" * If none of requested locales is valid sub-locales, returns selectedLocale as fallback */ _getFullLocale(selectedLocale, requestedLocales) { let fullLocale = selectedLocale; const selectedLocaleInfo = _TranslationsHelper.parseLocaleString(selectedLocale); for (const locale of requestedLocales) { let isSubLocale = true; for (const [fieldName, fieldValue] of Object.entries(selectedLocaleInfo)) { if (typeof fieldValue !== "string") { continue; } if (locale[fieldName] !== fieldValue) { isSubLocale = false; break; } } if (isSubLocale) { fullLocale = _TranslationsHelper.toLocaleString(locale); break; } } return fullLocale; } /** * Takes list of locales and build traverse order according to LOCALE_RESOLVE_ORDER * Then select first available locale from that list defaulting to defaultLocale * After that enhancing it with first locale, matching selected one * * @example * const availableLocales = ["zh", "en"] // that's what we have * const locales = ["zh-Hans-CN", "en-GB", "zh"] // that's what user want * // During function execution we build order * const helper = new TranslationsHelper({ locales, defaultLocale: "zh" }) * const { selectedLocale, fullLocale } = helper.selectSupportedLocale(locales.map(TranslationsHelper.parseLocaleString)) * // ["zh-Hans-CN", "zh-Hans", "en-GB", "en", "zh"] - resolved order * // selectedLocale = "en" - first match, on which we can load messages * // fullLocale = "en-GB" - sub-locale, providing additional info */ selectSupportedLocale(locales) { const reversedResolveOrder = []; for (let i = locales.length - 1; i >= 0; i--) { const localeToProcess = locales[i]; for (let j = LOCALE_RESOLVE_ORDER.length - 1; j >= 0; j--) { const fields = LOCALE_RESOLVE_ORDER[j]; const localeCandidate = { primary: localeToProcess.primary, extended: void 0, script: void 0, region: void 0 }; let isValidCandidate = true; for (const fieldName of fields) { if (typeof localeToProcess[fieldName] === "undefined") { isValidCandidate = false; break; } localeCandidate[fieldName] = localeToProcess[fieldName]; } if (isValidCandidate) { const stringCandidate = _TranslationsHelper.toLocaleString(localeCandidate); if (!reversedResolveOrder.includes(stringCandidate)) { reversedResolveOrder.push(stringCandidate); } } } } reversedResolveOrder.reverse(); let selectedLocale = this._defaultLocale; for (const localeString of reversedResolveOrder) { if (this._locales.has(localeString)) { selectedLocale = localeString; break; } } return { selectedLocale, fullLocale: this._getFullLocale(selectedLocale, locales) }; } /** * Obtains locale preference from query parameter (if specified), cookie, request.headers['accept-language'] or window.navigator.languages * and then selects supported locale using selectSupportedLocale method */ getPreferredLocale(req, res) { if (this.localeQueryParam) { let paramValue = null; if (req && req.url) { paramValue = new URL(req.url, "https://_").searchParams.get(this.localeQueryParam); } else if (!isSSR()) { paramValue = new URLSearchParams(window.location.search).get(this.localeQueryParam); } if (paramValue) { const localeSelection = this.selectSupportedLocale([_TranslationsHelper.parseLocaleString(paramValue)]); if (localeSelection.fullLocale === paramValue) { return localeSelection; } } } const cookieValue = (0, import_cookies_next.getCookie)(this.localeCookieName, { req, res }); if (cookieValue) { const localeSelection = this.selectSupportedLocale([_TranslationsHelper.parseLocaleString(cookieValue)]); if (localeSelection.fullLocale === cookieValue) { return localeSelection; } } if (req) { return this.selectSupportedLocale(_TranslationsHelper.parseAcceptLanguageHeader(req.headers["accept-language"])); } else if (!isSSR()) { return this.selectSupportedLocale(window.navigator.languages.map(_TranslationsHelper.parseLocaleString)); } return { selectedLocale: this._defaultLocale, fullLocale: this._defaultLocale }; } /** * Extracts prefetched translations to pageProps, so it can be available during SSR */ extractI18NInfo(translationsData, pageParams) { return { ...pageParams, props: { ...pageParams.props, [I18N_SELECTED_LOCALE_PROP_NAME]: translationsData.selectedLocale, [I18N_FULL_LOCALE_PROP_NAME]: translationsData.fullLocale, [I18N_MESSAGES_PROP_NAME]: translationsData.messages } }; } async getTranslations(locale) { if (!this._defaultMessages) { const existingMessages2 = this._translations[this._defaultLocale]; if (existingMessages2) { this._defaultMessages = existingMessages2; } else { this._defaultMessages = await this._loadDefaultMessages(); } this._translations[this._defaultLocale] = this._defaultMessages; } const existingMessages = this._translations[locale]; if (existingMessages) { return existingMessages; } const partialTranslatedMessages = await this._loadMessages(locale); const messages = { ...this._defaultMessages, ...partialTranslatedMessages }; this._translations[locale] = messages; return messages; } async prefetchTranslations(req, res) { const localeSelection = this.getPreferredLocale(req, res); const messages = await this.getTranslations(localeSelection.selectedLocale); return { selectedLocale: localeSelection.selectedLocale, fullLocale: localeSelection.fullLocale, messages }; } getTranslationsProvider() { if (!this._context) { this._context = (0, import_react2.createContext)({ selectedLocale: this._defaultLocale, fullLocale: this._defaultLocale, messages: void 0, switchLocale: () => ({}) }); } const Context = this._context; const getFullLocale = this._getFullLocale; const localeCookieName = this.localeCookieName; const getTranslations = this.getTranslations; const translationsObj = this._translations; const getPreferredLocale = this.getPreferredLocale; return function TranslationsProvider({ initialSelectedLocale, initialFullLocale, initialMessages, children }) { const [selectedLocale, setSelectedLocale] = (0, import_react2.useState)(initialSelectedLocale); const [fullLocale, setFullLocale] = (0, import_react2.useState)(initialFullLocale); const [messages, setMessages] = (0, import_react2.useState)(initialMessages); useEffectOnce(() => { if (!isSSR() && initialSelectedLocale && initialMessages) { translationsObj[initialSelectedLocale] = initialMessages; (0, import_cookies_next.setCookie)(localeCookieName, initialFullLocale, { sameSite: "none", secure: true }); } else if (!isSSR() && (!initialSelectedLocale || !initialFullLocale || !initialMessages)) { const localeSelection = getPreferredLocale(); setSelectedLocale(localeSelection.selectedLocale); setFullLocale(localeSelection.fullLocale); getTranslations(localeSelection.selectedLocale).then(setMessages); (0, import_cookies_next.setCookie)(localeCookieName, localeSelection.fullLocale, { sameSite: "none", secure: true }); } }); const switchLocale = (0, import_react2.useCallback)(async (newLocale) => { const fullLocale2 = getFullLocale(newLocale, window.navigator.languages.map(_TranslationsHelper.parseLocaleString)); const messages2 = await getTranslations(newLocale); setSelectedLocale(newLocale); setFullLocale(fullLocale2); setMessages(messages2); (0, import_cookies_next.setCookie)(localeCookieName, fullLocale2, { sameSite: "none", secure: true }); }, []); return /* @__PURE__ */ import_react2.default.createElement(Context.Provider, { value: { selectedLocale, fullLocale, messages, switchLocale } }, children); }; } getUseTranslationsExtractorHook() { const defaultLocale = this._defaultLocale; return function useTranslationsExtractor(pageProps) { return { initialSelectedLocale: pageProps[I18N_SELECTED_LOCALE_PROP_NAME] || defaultLocale, initialFullLocale: pageProps[I18N_FULL_LOCALE_PROP_NAME] || defaultLocale, initialMessages: pageProps[I18N_MESSAGES_PROP_NAME] || void 0 }; }; } getUseTranslationsHook() { if (!this._context) { this._context = (0, import_react2.createContext)({ selectedLocale: this._defaultLocale, fullLocale: this._defaultLocale, messages: void 0, switchLocale: () => ({}) }); } const context = this._context; return function useTranslations() { return (0, import_react2.useContext)(context); }; } getHOC() { const useTranslationsExtractor = this.getUseTranslationsExtractorHook(); const TranslationsProvider = this.getTranslationsProvider(); const prefetchTranslations = this.prefetchTranslations; const extractI18NInfo = this.extractI18NInfo; return function withTranslations(App) { const WithTranslations = (props) => { const { pageProps } = props; const { initialSelectedLocale, initialFullLocale, initialMessages } = useTranslationsExtractor(pageProps); return /* @__PURE__ */ import_react2.default.createElement( TranslationsProvider, { initialSelectedLocale, initialFullLocale, initialMessages }, /* @__PURE__ */ import_react2.default.createElement(App, { ...props }) ); }; const appGetInitialProps = App.getInitialProps; if (appGetInitialProps) { WithTranslations.getInitialProps = async function(context) { const appProps = await appGetInitialProps(context); const { ctx } = context; const translationsData = await prefetchTranslations(ctx.req, ctx.res); const { props } = extractI18NInfo(translationsData, { props: appProps.pageProps }); return { ...appProps, pageProps: props }; }; } return WithTranslations; }; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { TranslationsHelper }); //# sourceMappingURL=i18n.js.map