UNPKG

@open-condo/miniapp-utils

Version:

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

227 lines (224 loc) 11.3 kB
import { O as Optional, A as AppType } from '../types-DHs6TUPa.mjs'; import { IncomingMessage, ServerResponse } from 'http'; import { FC, PropsWithChildren } from 'react'; /** * Based on RFC5646: https://datatracker.ietf.org/doc/html/rfc5646 */ type LocaleInfo = { /** primary language tag, like "en", "zh", "ru", affects everything in messages */ primary: string; /** extended language tag, which specify language dialect like "gan" in Gan Chinese ("zh-gan"), affects almost nothing */ extended: Optional<string>; /** region tag, like "US", "CN", which usually does not affect messages itself, but number / currency formatting */ region: Optional<string>; /** script tag, like "Latn", "Cyrl", which refers to used script / alphabet. Affects all messages */ script: Optional<string>; }; type AcceptLanguageInfo = LocaleInfo & { /** number in [0.0, 1.0] range to order languages by. The larger the number, the more locale is preferred */ quality: number; }; type LocaleSelection<AvailableLocale extends string> = { /** Locale from list of apps available locales, used for language selection and messages loading. Example: "en" */ selectedLocale: AvailableLocale; /** Full locale, which must be passed to tools such react-intl. Can be equal sub-locale of selectedLocale or equal to it. Example: "en-GB" */ fullLocale: string; }; type PrefetchResult<AvailableLocale extends string, MessagesShape extends Record<string, string>> = LocaleSelection<AvailableLocale> & { messages: MessagesShape; }; type SSRResult<PropsType extends Record<string, unknown>> = { props: PropsType; }; type SSRResultWithI18N<AvailableLocale extends string, MessagesShape extends Record<string, string>, PropsType extends Record<string, unknown>> = { props: PropsType & { [I18N_SELECTED_LOCALE_PROP_NAME]?: AvailableLocale; [I18N_FULL_LOCALE_PROP_NAME]?: string; [I18N_MESSAGES_PROP_NAME]?: MessagesShape; }; }; type TranslationsContextType<AvailableLocale extends string, MessagesShape extends Record<string, string>> = { /** * Selected locale, which is used to determine, what messages set to show to user. * For example: "en" * */ selectedLocale: AvailableLocale; /** * Original locale, from which `selectedLocale` was chosen. Sub-locale of `selectedLocale`. * Should be passed to tools such as `intl`, since it can contain additional info helping with number-formating and so on * For example: "en-GB" */ fullLocale: string; /** Extracted messages to pass into `intl` or any similar tools */ messages: MessagesShape | undefined; /** Callback to change current language */ switchLocale(newLocale: AvailableLocale): void; }; /** * Translations helper which is used to parse / stringify locales, * select most suitable locale based on user preferences, load partial translations with caching and many others * * @example Init helper inside your app and re-export utils * import fetch from 'cross-fetch' * import getConfig from 'next/config' * import { IntlProvider as DefaultIntlProvider } from 'react-intl' * * import { TranslationsHelper } from '@open-condo/miniapp-utils/helpers/i18n' * import type { TranslationsProviderProps } from '@open-condo/miniapp-utils/helpers/i18n' * * import { LOCALES, DEFAULT_LOCALE } from '@/domains/common/constants/locales' * * import type { MessagesKeysType } from '@/global' * import type { FC, PropsWithChildren } from 'react' * * const { publicRuntimeConfig: { serviceUrl } } = getConfig() * * export type AvailableLocale = typeof LOCALES[number] * export type MessagesShape = Record<MessagesKeysType, string> * * const translationsAPIEndpoint = `${serviceUrl}/api/translations` * * async function loadDefaultMessages (): Promise<MessagesShape> { * return (await import(`@/lang/${DEFAULT_LOCALE}.json`)).default * } * * async function loadMessages (locale: AvailableLocale): Promise<MessagesShape> { * const response = await fetch(`${translationsAPIEndpoint}/${locale}`) * if (!response.ok) throw new Error(`Could not load translations for ${locale} locale`) * return response.json() * } * * const translationsHelper = new TranslationsHelper({ * locales: LOCALES, * defaultLocale: DEFAULT_LOCALE, * loadMessages, * loadDefaultMessages, * }) * * export type { PrefetchResult } from '@open-condo/miniapp-utils/helpers/i18n' * * export const prefetchTranslations = translationsHelper.prefetchTranslations * export const extractI18NInfo = translationsHelper.extractI18NInfo * export const useTranslationsExtractor = translationsHelper.getUseTranslationsExtractorHook() * export const TranslationsProvider: FC<TranslationsProviderProps<AvailableLocale, MessagesShape>> = translationsHelper.getTranslationsProvider() * export const useTranslations = translationsHelper.getUseTranslationsHook() * * export const IntlProvider: FC<PropsWithChildren> = ({ children }) => { * const { messages, fullLocale } = useTranslations() * * return ( * <DefaultIntlProvider locale={fullLocale} messages={messages}> * {children} * </DefaultIntlProvider> * ) * } * * @example use in _app.tsx SSR to prefetch translations * const translationsData = await prefetchTranslations(req, res) * * return extractI18NInfo(translationsData, { * props: {}, * }) * * @example use in _app.tsx global layout to provide translations * const { initialSelectedLocale, initialFullLocale, initialMessages } = useTranslationsExtractor(pageProps) * * return ( * <TranslationsProvider * initialSelectedLocale={initialSelectedLocale} * initialFullLocale={initialFullLocale} * initialMessages={initialMessages} * > * <IntlProvider> * {children} * </IntlProvider> * </TranslationsProvider> * ) * */ type TranslationsProviderProps<AvailableLocale extends string, MessagesShape extends Record<string, string>> = PropsWithChildren<{ initialSelectedLocale: AvailableLocale; initialFullLocale: string; initialMessages: MessagesShape | undefined; }>; type TranslationsHelperOptions<AvailableLocale extends string, MessagesShape extends Record<string, string>> = { locales: ReadonlyArray<AvailableLocale>; defaultLocale: AvailableLocale; loadDefaultMessages: () => Promise<MessagesShape>; loadMessages: (locale: AvailableLocale) => Promise<Partial<MessagesShape>>; localeCookieName?: string; localeQueryParam?: string; }; declare const I18N_SELECTED_LOCALE_PROP_NAME = "__I18N_SELECTED_LOCALE__"; declare const I18N_FULL_LOCALE_PROP_NAME = "__I18N_FULL_LOCALE__"; declare const I18N_MESSAGES_PROP_NAME = "__I18N_MESSAGES__"; declare class TranslationsHelper<AvailableLocale extends string, MessagesShape extends Record<string, string>> { private readonly _locales; private readonly _defaultLocale; private _context; private readonly _loadMessages; private readonly _loadDefaultMessages; private readonly _translations; private _defaultMessages; readonly localeCookieName: string; readonly localeQueryParam: string | undefined; constructor(options: TranslationsHelperOptions<AvailableLocale, MessagesShape>); /** * 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: string): AcceptLanguageInfo; /** * 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: Optional<string>): Array<AcceptLanguageInfo>; /** * Generates locale-string from LocaleInfo or AcceptLanguageInfo */ static toLocaleString(locale: LocaleInfo | AcceptLanguageInfo): string; /** * 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 */ private _getFullLocale; /** * 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: Array<AcceptLanguageInfo | LocaleInfo>): LocaleSelection<AvailableLocale>; /** * 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?: Optional<IncomingMessage>, res?: Optional<ServerResponse>): LocaleSelection<AvailableLocale>; /** * Extracts prefetched translations to pageProps, so it can be available during SSR */ extractI18NInfo<PropsType extends Record<string, unknown>>(translationsData: PrefetchResult<AvailableLocale, MessagesShape>, pageParams: SSRResult<PropsType>): SSRResultWithI18N<AvailableLocale, MessagesShape, PropsType>; getTranslations(locale: AvailableLocale): Promise<MessagesShape>; prefetchTranslations(req: Optional<IncomingMessage>, res: Optional<ServerResponse>): Promise<PrefetchResult<AvailableLocale, MessagesShape>>; getTranslationsProvider(): FC<TranslationsProviderProps<AvailableLocale, MessagesShape>>; getUseTranslationsExtractorHook(): <PropsType extends Record<string, unknown>>(pageProps: SSRResultWithI18N<AvailableLocale, MessagesShape, PropsType>["props"]) => { initialSelectedLocale: AvailableLocale; initialFullLocale: string; initialMessages: MessagesShape | undefined; }; getUseTranslationsHook(): () => TranslationsContextType<AvailableLocale, MessagesShape>; getHOC(): <PropsType extends Record<string, unknown>, ComponentType, RouterType>(App: AppType<PropsType, ComponentType, RouterType>) => AppType<PropsType, ComponentType, RouterType>; } export { type AcceptLanguageInfo, type LocaleInfo, type LocaleSelection, type PrefetchResult, TranslationsHelper, type TranslationsProviderProps };