UNPKG

@open-condo/miniapp-utils

Version:

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

1 lines 36.1 kB
{"version":3,"sources":["../../src/helpers/i18n.tsx","../../src/helpers/environment.ts","../../src/hooks/useEffectOnce.ts"],"sourcesContent":["import { getCookie, setCookie } from 'cookies-next'\nimport React, { createContext, useState, useCallback, useContext } from 'react'\n\nimport { isSSR } from './environment'\n\nimport { useEffectOnce } from '../hooks/useEffectOnce'\n\nimport type { Optional, AppType } from './common/types'\nimport type { IncomingMessage, ServerResponse } from 'http'\nimport type { Context, PropsWithChildren, FC } from 'react'\n\n/**\n * Based on RFC5646: https://datatracker.ietf.org/doc/html/rfc5646\n */\nexport type LocaleInfo = {\n /** primary language tag, like \"en\", \"zh\", \"ru\", affects everything in messages */\n primary: string\n /** extended language tag, which specify language dialect like \"gan\" in Gan Chinese (\"zh-gan\"), affects almost nothing */\n extended: Optional<string>\n /** region tag, like \"US\", \"CN\", which usually does not affect messages itself, but number / currency formatting */\n region: Optional<string>\n /** script tag, like \"Latn\", \"Cyrl\", which refers to used script / alphabet. Affects all messages */\n script: Optional<string>\n}\n\nexport type AcceptLanguageInfo = LocaleInfo & {\n /** number in [0.0, 1.0] range to order languages by. The larger the number, the more locale is preferred */\n quality: number\n}\n\nexport type LocaleSelection<AvailableLocale extends string> = {\n /** Locale from list of apps available locales, used for language selection and messages loading. Example: \"en\" */\n selectedLocale: AvailableLocale\n /** 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\" */\n fullLocale: string\n}\n\nexport type PrefetchResult<\n AvailableLocale extends string,\n MessagesShape extends Record<string, string>,\n> = LocaleSelection<AvailableLocale> & { messages: MessagesShape }\n\ntype SSRResult<PropsType extends Record<string, unknown>> = {\n props: PropsType\n}\ntype SSRResultWithI18N<\n AvailableLocale extends string,\n MessagesShape extends Record<string, string>,\n PropsType extends Record<string, unknown>,\n> = {\n props: PropsType & {\n [I18N_SELECTED_LOCALE_PROP_NAME]?: AvailableLocale\n [I18N_FULL_LOCALE_PROP_NAME]?: string\n [I18N_MESSAGES_PROP_NAME]?: MessagesShape\n }\n}\n\n\n\ntype TranslationsContextType<\n AvailableLocale extends string,\n MessagesShape extends Record<string, string>,\n> = {\n /**\n * Selected locale, which is used to determine, what messages set to show to user.\n * For example: \"en\"\n * */\n selectedLocale: AvailableLocale\n /**\n * Original locale, from which `selectedLocale` was chosen. Sub-locale of `selectedLocale`.\n * Should be passed to tools such as `intl`, since it can contain additional info helping with number-formating and so on\n * For example: \"en-GB\"\n */\n fullLocale: string\n /** Extracted messages to pass into `intl` or any similar tools */\n messages: MessagesShape | undefined\n /** Callback to change current language */\n switchLocale(newLocale: AvailableLocale): void\n}\n\n/**\n * Translations helper which is used to parse / stringify locales,\n * select most suitable locale based on user preferences, load partial translations with caching and many others\n *\n * @example Init helper inside your app and re-export utils\n * import fetch from 'cross-fetch'\n * import getConfig from 'next/config'\n * import { IntlProvider as DefaultIntlProvider } from 'react-intl'\n *\n * import { TranslationsHelper } from '@open-condo/miniapp-utils/helpers/i18n'\n * import type { TranslationsProviderProps } from '@open-condo/miniapp-utils/helpers/i18n'\n *\n * import { LOCALES, DEFAULT_LOCALE } from '@/domains/common/constants/locales'\n *\n * import type { MessagesKeysType } from '@/global'\n * import type { FC, PropsWithChildren } from 'react'\n *\n * const { publicRuntimeConfig: { serviceUrl } } = getConfig()\n *\n * export type AvailableLocale = typeof LOCALES[number]\n * export type MessagesShape = Record<MessagesKeysType, string>\n *\n * const translationsAPIEndpoint = `${serviceUrl}/api/translations`\n *\n * async function loadDefaultMessages (): Promise<MessagesShape> {\n * return (await import(`@/lang/${DEFAULT_LOCALE}.json`)).default\n * }\n *\n * async function loadMessages (locale: AvailableLocale): Promise<MessagesShape> {\n * const response = await fetch(`${translationsAPIEndpoint}/${locale}`)\n * if (!response.ok) throw new Error(`Could not load translations for ${locale} locale`)\n * return response.json()\n * }\n *\n * const translationsHelper = new TranslationsHelper({\n * locales: LOCALES,\n * defaultLocale: DEFAULT_LOCALE,\n * loadMessages,\n * loadDefaultMessages,\n * })\n *\n * export type { PrefetchResult } from '@open-condo/miniapp-utils/helpers/i18n'\n *\n * export const prefetchTranslations = translationsHelper.prefetchTranslations\n * export const extractI18NInfo = translationsHelper.extractI18NInfo\n * export const useTranslationsExtractor = translationsHelper.getUseTranslationsExtractorHook()\n * export const TranslationsProvider: FC<TranslationsProviderProps<AvailableLocale, MessagesShape>> = translationsHelper.getTranslationsProvider()\n * export const useTranslations = translationsHelper.getUseTranslationsHook()\n *\n * export const IntlProvider: FC<PropsWithChildren> = ({ children }) => {\n * const { messages, fullLocale } = useTranslations()\n *\n * return (\n * <DefaultIntlProvider locale={fullLocale} messages={messages}>\n * {children}\n * </DefaultIntlProvider>\n * )\n * }\n *\n * @example use in _app.tsx SSR to prefetch translations\n * const translationsData = await prefetchTranslations(req, res)\n *\n * return extractI18NInfo(translationsData, {\n * props: {},\n * })\n *\n * @example use in _app.tsx global layout to provide translations\n * const { initialSelectedLocale, initialFullLocale, initialMessages } = useTranslationsExtractor(pageProps)\n *\n * return (\n * <TranslationsProvider\n * initialSelectedLocale={initialSelectedLocale}\n * initialFullLocale={initialFullLocale}\n * initialMessages={initialMessages}\n * >\n * <IntlProvider>\n * {children}\n * </IntlProvider>\n * </TranslationsProvider>\n * )\n * */\nexport type TranslationsProviderProps<\n AvailableLocale extends string,\n MessagesShape extends Record<string, string>,\n> = PropsWithChildren<{\n initialSelectedLocale: AvailableLocale\n initialFullLocale: string\n initialMessages: MessagesShape | undefined\n}>\n\ntype TranslationsHelperOptions<\n AvailableLocale extends string,\n MessagesShape extends Record<string, string>,\n> = {\n locales: ReadonlyArray<AvailableLocale>\n defaultLocale: AvailableLocale\n loadDefaultMessages: () => Promise<MessagesShape>\n loadMessages: (locale: AvailableLocale) => Promise<Partial<MessagesShape>>\n localeCookieName?: string\n localeQueryParam?: string\n}\n\ntype LocalePartMatcher = {\n part: 'primary' | 'extended' | 'script' | 'region'\n matcher: RegExp\n}\n\nconst LOCALE_PARSING_OPTIONS: Array<LocalePartMatcher> = [\n { part: 'primary', matcher: /^[a-z]+$/ },\n { part: 'extended', matcher: /^[a-z]{3}$/ },\n { part: 'script', matcher: /^[A-Z][a-z]{3,}$/ },\n { part: 'region', matcher: /^([A-Z]{2,3}|[0-9]{3})$/ },\n]\n\nconst LOCALE_RESOLVE_ORDER: Array<Array<keyof LocaleInfo>> = [\n // Full resolution first\n ['primary', 'extended', 'script', 'region'],\n // Extended usually does not affect message a lot, so it can be omitted first\n ['primary', 'script', 'region'],\n // Primary + script is more important, since script change alphabet\n ['primary', 'script'],\n ['primary', 'region'],\n ['primary', 'extended'],\n ['primary'],\n]\n\nconst I18N_SELECTED_LOCALE_PROP_NAME = '__I18N_SELECTED_LOCALE__'\nconst I18N_FULL_LOCALE_PROP_NAME = '__I18N_FULL_LOCALE__'\nconst I18N_MESSAGES_PROP_NAME = '__I18N_MESSAGES__'\n\nexport class TranslationsHelper<\n AvailableLocale extends string,\n MessagesShape extends Record<string, string>,\n> {\n private readonly _locales: Set<string>\n private readonly _defaultLocale: AvailableLocale\n private _context: Context<TranslationsContextType<AvailableLocale, MessagesShape>> | undefined\n private readonly _loadMessages: (locale: AvailableLocale) => Promise<Partial<MessagesShape>>\n private readonly _loadDefaultMessages: () => Promise<MessagesShape>\n private readonly _translations: Partial<Record<AvailableLocale, MessagesShape>> = {}\n private _defaultMessages: MessagesShape | undefined\n readonly localeCookieName: string = 'NEXT_LOCALE'\n readonly localeQueryParam: string | undefined = undefined\n\n constructor (options: TranslationsHelperOptions<AvailableLocale, MessagesShape>) {\n this._locales = new Set(options.locales)\n this._defaultLocale = options.defaultLocale\n this._loadMessages = options.loadMessages\n this._loadDefaultMessages = options.loadDefaultMessages\n\n if (options.localeCookieName) {\n this.localeCookieName = options.localeCookieName\n }\n if (options.localeQueryParam) {\n this.localeQueryParam = options.localeQueryParam\n }\n\n this.getTranslationsProvider = this.getTranslationsProvider.bind(this)\n this.getUseTranslationsHook = this.getUseTranslationsHook.bind(this)\n this.getUseTranslationsExtractorHook = this.getUseTranslationsExtractorHook.bind(this)\n this.getPreferredLocale = this.getPreferredLocale.bind(this)\n this.selectSupportedLocale = this.selectSupportedLocale.bind(this)\n this.getTranslations = this.getTranslations.bind(this)\n this.prefetchTranslations = this.prefetchTranslations.bind(this)\n this.getHOC = this.getHOC.bind(this)\n }\n\n /**\n * This util parses language-defining string according to RFC5646: https://datatracker.ietf.org/doc/html/rfc5646\n * It also automatically detect and accept-language header format by enhancing result with quality info\n */\n static parseLocaleString (localeString: string): AcceptLanguageInfo {\n const stringParts = localeString.trim().split(';')\n const localeParts = stringParts[0].split('-')\n\n const quality = stringParts.length > 1\n ? parseFloat(stringParts[1].split('=')[1])\n : 1.0\n\n const locale: AcceptLanguageInfo = {\n primary: localeParts[0],\n extended: undefined,\n script: undefined,\n region: undefined,\n quality,\n }\n\n let currentParser = 0\n localePartsLoop: for (const localePart of localeParts) {\n for (; currentParser < LOCALE_PARSING_OPTIONS.length; currentParser++) {\n const { part, matcher } = LOCALE_PARSING_OPTIONS[currentParser]\n if (matcher.test(localePart)) {\n locale[part] = localePart\n currentParser++\n continue localePartsLoop\n }\n }\n break\n }\n\n return locale\n }\n\n /**\n * Parses \"Accept-Language\" header value using \"parseLocaleString\" util and returns array of AcceptLanguageInfo\n * sorted by descending quality.\n *\n * NOTE: Empty header or non-defined header is treated as \"*\"\n */\n static parseAcceptLanguageHeader (headerValue: Optional<string>): Array<AcceptLanguageInfo> {\n return (headerValue || '*')\n .split(',')\n .map(TranslationsHelper.parseLocaleString)\n .sort((a, b) => b.quality - a.quality)\n }\n\n /**\n * Generates locale-string from LocaleInfo or AcceptLanguageInfo\n */\n static toLocaleString (locale: LocaleInfo | AcceptLanguageInfo): string {\n return [locale.primary, locale.extended, locale.script, locale.region].filter(Boolean).join('-')\n }\n\n /**\n * Enrich selected locale by scanning through requested locales\n * and finding the first one, which is sub-locale of selected one.\n * For example: selectedLocale = \"en\", requestedLocales: [\"en-GB\", \"fr\", \"en\"] -> fullLocale = \"en-GB\"\n * If none of requested locales is valid sub-locales, returns selectedLocale as fallback\n */\n private _getFullLocale (selectedLocale: AvailableLocale, requestedLocales: Array<AcceptLanguageInfo | LocaleInfo>): string {\n let fullLocale: string = selectedLocale\n const selectedLocaleInfo = TranslationsHelper.parseLocaleString(selectedLocale)\n for (const locale of requestedLocales) {\n let isSubLocale = true\n for (const [fieldName, fieldValue] of Object.entries(selectedLocaleInfo)) {\n if (typeof fieldValue !== 'string') {\n continue\n }\n if (locale[fieldName as keyof LocaleInfo] !== fieldValue) {\n isSubLocale = false\n break\n }\n }\n if (isSubLocale) {\n fullLocale = TranslationsHelper.toLocaleString(locale)\n break\n }\n }\n\n return fullLocale\n }\n\n /**\n * Takes list of locales and build traverse order according to LOCALE_RESOLVE_ORDER\n * Then select first available locale from that list defaulting to defaultLocale\n * After that enhancing it with first locale, matching selected one\n *\n * @example\n * const availableLocales = [\"zh\", \"en\"] // that's what we have\n * const locales = [\"zh-Hans-CN\", \"en-GB\", \"zh\"] // that's what user want\n * // During function execution we build order\n * const helper = new TranslationsHelper({ locales, defaultLocale: \"zh\" })\n * const { selectedLocale, fullLocale } = helper.selectSupportedLocale(locales.map(TranslationsHelper.parseLocaleString))\n * // [\"zh-Hans-CN\", \"zh-Hans\", \"en-GB\", \"en\", \"zh\"] - resolved order\n * // selectedLocale = \"en\" - first match, on which we can load messages\n * // fullLocale = \"en-GB\" - sub-locale, providing additional info\n */\n selectSupportedLocale (locales: Array<AcceptLanguageInfo | LocaleInfo>): LocaleSelection<AvailableLocale> {\n const reversedResolveOrder: Array<string> = []\n\n // NOTE: For each locale passed build resolve order starting from end\n // Order is important, since direct pass on \"en-GB,fr,en-US\" will produce [\"en-GB\", \"en\", \"fr\", \"en-US\"]\n // While reverse logic will produce [\"en-GB\", \"fr\", \"en-US\", \"en\"]\n for (let i = locales.length - 1; i >= 0; i--) {\n const localeToProcess = locales[i]\n for (let j = LOCALE_RESOLVE_ORDER.length - 1; j >= 0; j--) {\n const fields = LOCALE_RESOLVE_ORDER[j]\n const localeCandidate: LocaleInfo = {\n primary: localeToProcess.primary,\n extended: undefined,\n script: undefined,\n region: undefined,\n }\n let isValidCandidate = true\n for (const fieldName of fields) {\n if (typeof localeToProcess[fieldName] === 'undefined') {\n isValidCandidate = false\n break\n }\n localeCandidate[fieldName] = localeToProcess[fieldName]\n }\n if (isValidCandidate) {\n const stringCandidate = TranslationsHelper.toLocaleString(localeCandidate)\n if (!reversedResolveOrder.includes(stringCandidate)) {\n reversedResolveOrder.push(stringCandidate)\n }\n }\n }\n }\n\n // NOTE: now convert it back to direct order\n reversedResolveOrder.reverse()\n\n let selectedLocale: AvailableLocale = this._defaultLocale\n\n for (const localeString of reversedResolveOrder) {\n if (this._locales.has(localeString)) {\n selectedLocale = localeString as AvailableLocale\n break\n }\n }\n\n // NOTE: We select the language from available ones,\n // but we need to enrich return value with first matching locale, which might affect currency display\n // and others non-related to message staff\n return {\n selectedLocale,\n fullLocale: this._getFullLocale(selectedLocale, locales),\n }\n }\n\n /**\n * Obtains locale preference from query parameter (if specified), cookie, request.headers['accept-language'] or window.navigator.languages\n * and then selects supported locale using selectSupportedLocale method\n */\n getPreferredLocale (req?: Optional<IncomingMessage>, res?: Optional<ServerResponse>): LocaleSelection<AvailableLocale> {\n // Step 1: Query must be resolved before any cookies, since it's more explicit\n if (this.localeQueryParam) {\n let paramValue: string | null = null\n if (req && req.url) {\n paramValue = new URL(req.url, 'https://_').searchParams.get(this.localeQueryParam)\n } else if (!isSSR()) {\n paramValue = new URLSearchParams(window.location.search).get(this.localeQueryParam)\n }\n if (paramValue) {\n const localeSelection = this.selectSupportedLocale([TranslationsHelper.parseLocaleString(paramValue)])\n if (localeSelection.fullLocale === paramValue) {\n return localeSelection\n }\n }\n }\n\n // Step 2: Cookie must be parsed after query and before other preferences\n const cookieValue = getCookie(this.localeCookieName, { req, res })\n if (cookieValue) {\n const localeSelection = this.selectSupportedLocale([TranslationsHelper.parseLocaleString(cookieValue)])\n if (localeSelection.fullLocale === cookieValue) {\n return localeSelection\n }\n }\n\n // NOTE: on server extracts locale from accept-language\n if (req) {\n return this.selectSupportedLocale(TranslationsHelper.parseAcceptLanguageHeader(req.headers['accept-language']))\n } else if (!isSSR()) {\n return this.selectSupportedLocale(window.navigator.languages.map(TranslationsHelper.parseLocaleString))\n }\n\n return {\n selectedLocale: this._defaultLocale,\n fullLocale: this._defaultLocale,\n }\n }\n\n /**\n * Extracts prefetched translations to pageProps, so it can be available during SSR\n */\n extractI18NInfo<PropsType extends Record<string, unknown>> (\n translationsData: PrefetchResult<AvailableLocale, MessagesShape>,\n pageParams: SSRResult<PropsType>\n ): SSRResultWithI18N<AvailableLocale, MessagesShape, PropsType> {\n return {\n ...pageParams,\n props: {\n ...pageParams.props,\n [I18N_SELECTED_LOCALE_PROP_NAME]: translationsData.selectedLocale,\n [I18N_FULL_LOCALE_PROP_NAME]: translationsData.fullLocale,\n [I18N_MESSAGES_PROP_NAME]: translationsData.messages,\n },\n }\n }\n\n async getTranslations (locale: AvailableLocale): Promise<MessagesShape> {\n // Step 1. Load default messages once to have full set of messages\n if (!this._defaultMessages) {\n const existingMessages = this._translations[this._defaultLocale]\n // NOTE: translations can be prepopulated during SSR or other prefetches\n if (existingMessages) {\n this._defaultMessages = existingMessages\n } else {\n this._defaultMessages = await this._loadDefaultMessages()\n }\n\n this._translations[this._defaultLocale] = this._defaultMessages\n }\n\n // Step 2. If messages were already fetched - return existing one\n const existingMessages = this._translations[locale]\n if (existingMessages) {\n return existingMessages\n }\n\n // Step 3. Fetch language messages, which might be partially translated\n // and combined with default messages to build full message set\n const partialTranslatedMessages = await this._loadMessages(locale)\n const messages: MessagesShape = {\n ...this._defaultMessages,\n ...partialTranslatedMessages,\n }\n this._translations[locale] = messages\n\n return messages\n }\n\n async prefetchTranslations (req: Optional<IncomingMessage>, res: Optional<ServerResponse>): Promise<PrefetchResult<AvailableLocale, MessagesShape>> {\n const localeSelection = this.getPreferredLocale(req, res)\n const messages = await this.getTranslations(localeSelection.selectedLocale)\n\n return {\n selectedLocale: localeSelection.selectedLocale,\n fullLocale: localeSelection.fullLocale,\n messages,\n }\n }\n\n getTranslationsProvider (): FC<TranslationsProviderProps<AvailableLocale, MessagesShape>> {\n if (!this._context) {\n this._context = createContext<TranslationsContextType<AvailableLocale, MessagesShape>>({\n selectedLocale: this._defaultLocale,\n fullLocale: this._defaultLocale,\n messages: undefined,\n switchLocale: () => ({}),\n })\n }\n\n const Context = this._context\n const getFullLocale = this._getFullLocale\n const localeCookieName = this.localeCookieName\n const getTranslations = this.getTranslations\n const translationsObj = this._translations\n const getPreferredLocale = this.getPreferredLocale\n\n return function TranslationsProvider ({\n initialSelectedLocale,\n initialFullLocale,\n initialMessages,\n children,\n }) {\n const [selectedLocale, setSelectedLocale] = useState(initialSelectedLocale)\n const [fullLocale, setFullLocale] = useState(initialFullLocale)\n const [messages, setMessages] = useState(initialMessages)\n\n useEffectOnce(() => {\n if (!isSSR() && initialSelectedLocale && initialMessages) {\n translationsObj[initialSelectedLocale] = initialMessages\n setCookie(localeCookieName, initialFullLocale, { sameSite: 'none', secure: true })\n } else if (!isSSR() && (!initialSelectedLocale || !initialFullLocale || !initialMessages)) {\n const localeSelection = getPreferredLocale()\n setSelectedLocale(localeSelection.selectedLocale)\n setFullLocale(localeSelection.fullLocale)\n getTranslations(localeSelection.selectedLocale).then(setMessages)\n setCookie(localeCookieName, localeSelection.fullLocale, { sameSite: 'none', secure: true })\n }\n })\n\n const switchLocale = useCallback(async (newLocale: AvailableLocale) => {\n const fullLocale = getFullLocale(newLocale, window.navigator.languages.map(TranslationsHelper.parseLocaleString))\n const messages = await getTranslations(newLocale)\n setSelectedLocale(newLocale)\n setFullLocale(fullLocale)\n setMessages(messages)\n setCookie(localeCookieName, fullLocale, { sameSite: 'none', secure: true })\n }, [])\n\n return (\n <Context.Provider value={{ selectedLocale, fullLocale, messages, switchLocale }}>\n {children}\n </Context.Provider>\n )\n }\n }\n\n getUseTranslationsExtractorHook () {\n const defaultLocale = this._defaultLocale\n\n return function useTranslationsExtractor<PropsType extends Record<string, unknown>> (\n pageProps: SSRResultWithI18N<AvailableLocale, MessagesShape, PropsType>['props']\n ) {\n return {\n initialSelectedLocale: pageProps[I18N_SELECTED_LOCALE_PROP_NAME] || defaultLocale,\n initialFullLocale: pageProps[I18N_FULL_LOCALE_PROP_NAME] || defaultLocale,\n initialMessages: pageProps[I18N_MESSAGES_PROP_NAME] || undefined,\n }\n }\n }\n\n getUseTranslationsHook () {\n if (!this._context) {\n this._context = createContext<TranslationsContextType<AvailableLocale, MessagesShape>>({\n selectedLocale: this._defaultLocale,\n fullLocale: this._defaultLocale,\n messages: undefined,\n switchLocale: () => ({}),\n })\n }\n const context = this._context\n\n return function useTranslations () {\n return useContext(context)\n }\n }\n\n getHOC () {\n const useTranslationsExtractor = this.getUseTranslationsExtractorHook()\n const TranslationsProvider = this.getTranslationsProvider()\n const prefetchTranslations = this.prefetchTranslations\n const extractI18NInfo = this.extractI18NInfo\n\n return function withTranslations<\n PropsType extends Record<string, unknown>,\n ComponentType,\n RouterType,\n > (App: AppType<PropsType, ComponentType, RouterType>): AppType<PropsType, ComponentType, RouterType> {\n const WithTranslations: AppType<PropsType, ComponentType, RouterType> = (props) => {\n const { pageProps } = props\n const { initialSelectedLocale, initialFullLocale, initialMessages } = useTranslationsExtractor(pageProps)\n\n return (\n <TranslationsProvider\n initialSelectedLocale={initialSelectedLocale}\n initialFullLocale={initialFullLocale}\n initialMessages={initialMessages}\n >\n <App {...props} />\n </TranslationsProvider>\n )\n }\n\n const appGetInitialProps = App.getInitialProps\n if (appGetInitialProps) {\n WithTranslations.getInitialProps = async function (context) {\n const appProps = await appGetInitialProps(context)\n const { ctx } = context\n const translationsData = await prefetchTranslations(ctx.req, ctx.res)\n const { props } = extractI18NInfo(translationsData, { props: appProps.pageProps })\n\n return { ...appProps, pageProps: props }\n }\n }\n\n return WithTranslations\n }\n }\n}\n","/**\n * Check whether it's a server or client environment\n * @example\n * if (!isSSR()) {\n * console.log(window.location.href)\n * }\n */\nexport function isSSR (): boolean {\n return typeof window === 'undefined'\n}\n\n/**\n * Check whether it's development environment or not\n * @example\n * const IS_DEBUG_LOGS_ENABLED = isDebug()\n */\nexport function isDebug (): boolean {\n return process.env.NODE_ENV === 'development'\n}\n","// SRC: https://github.com/streamich/react-use/blob/master/src/useEffectOnce.ts\n\nimport { EffectCallback, useEffect } from 'react'\n\n/**\n * useEffect wrapper, that runs side effect only once on initial component render\n * @example\n * useEffectOnce(() => {\n * initAnalytics()\n * })\n */\nexport function useEffectOnce (cb: EffectCallback): void {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n useEffect(cb, [])\n}\n"],"mappings":"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","names":["existingMessages","fullLocale","messages"]}