@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
text/typescript
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 };