@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
JavaScript
"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