Last active
February 6, 2024 17:07
-
-
Save lightningspirit/7508f635d083851b4611bd6884987ecf to your computer and use it in GitHub Desktop.
React I18n only server side
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'server-only' | |
import serverOnlyContext from './server-only-context' | |
export type Locale = string | |
export const I18nContext = serverOnlyContext<Locale>('en') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
I18nLanguage, | |
I18nParams, | |
I18nPluralValue, | |
I18nSingleValue, | |
useI18n, | |
useI18nContext, | |
} from '@/core/hooks/use-i18n' | |
import { PropsWithChildren } from 'react' | |
type I18nProps = | |
| Record<I18nLanguage, I18nSingleValue | I18nPluralValue> | |
| { | |
count?: number | |
params?: I18nParams | |
} | |
function I18n(props: I18nProps) { | |
const { params, count, ...texts } = props | |
const { t } = useI18n() | |
return ( | |
<> | |
{t(texts as Record<I18nLanguage, I18nSingleValue | I18nPluralValue>, { | |
params: params as I18nParams, | |
count: count as number, | |
})} | |
</> | |
) | |
} | |
export function I18nProvider({ | |
children, | |
value, | |
}: PropsWithChildren<{ value: Locale }>) { | |
const { setLanguage } = useI18nContext() | |
setLanguage(value) | |
return <>{children}</> | |
} | |
export default I18n |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'server-only' | |
import { cache } from 'react' | |
const serverOnlyContext = <T>(defaultValue: T): [() => T, (v: T) => void] => { | |
const getRef = cache(() => ({ current: defaultValue })) | |
const getValue = (): T => getRef().current | |
const setValue = (value: T) => { | |
getRef().current = value | |
} | |
return [getValue, setValue] | |
} | |
export default serverOnlyContext |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { cloneElement, isValidElement } from 'react' | |
import { I18nContext } from '../functions/create-i18n-context' | |
import dlv from 'dlv' | |
export type I18nLanguage = Exclude<string, 'count' | 'params'> | |
export type I18nParam = | |
| string | |
| number | |
| Record<string, string | number> | |
| JSX.Element | |
| ((arg: string, args: I18nParams) => string) | |
export type I18nSingleValue = string | |
export type I18nPluralValue = { | |
[n: number]: string | |
n?: string | |
} | |
export type I18nParams = Record<string, I18nParam> & { count?: number } | |
export type TranslateFn = ( | |
languages: Record<I18nLanguage, I18nSingleValue | I18nPluralValue>, | |
args?: { | |
params?: I18nParams | |
count?: number | |
}, | |
) => string | JSX.Element | JSX.Element[] | |
const TAG_REGEXP = /<([a-z0-9_-]+)\b[^>]*>(.*?)<\/\1>/gi | |
const STR_REGEXP = /{{([a-z0-9._-]+)\s*(.*?)}}/gi | |
const LITERAL_REGEXP = /^"(.*)"$/i | |
export function useI18nContext() { | |
const [getValue, setLanguage] = I18nContext | |
return { locale: getValue(), setLanguage } as { | |
locale: Locale | |
setLanguage: typeof setLanguage | |
} | |
} | |
export function useI18n() { | |
const { locale } = useI18nContext() | |
const t: TranslateFn = (languages, args) => { | |
if (!(locale in languages)) { | |
throw new Error(`I18n no string for ${locale}`) | |
} | |
// get value from language | |
const raw = languages[locale] | |
// destruct complex objects like count | |
let value = | |
typeof raw === 'object' | |
? args?.count && args.count in (raw as object) | |
? raw[args.count] | |
: 'n' in raw && raw['n'] | |
? raw['n'] | |
: `${raw}` | |
: raw | |
// substitute any params | |
if (args?.params) { | |
const params = args.params | |
value = value | |
.toString() | |
.replace(STR_REGEXP, function (_, variable: string, arg: string) { | |
const substitute = dlv(params, variable) | |
return typeof substitute === 'function' | |
? substitute( | |
LITERAL_REGEXP.test(arg) | |
? arg.substring(1, arg.length - 1) | |
: dlv(params, arg), | |
) | |
: substitute.toString() | |
}) | |
if (TAG_REGEXP.test(value)) { | |
return value.split(TAG_REGEXP).map((part, i, array) => { | |
// must match 2nd and 3rd param on a group of four | |
// see tests for example | |
if (i % 3 !== 1) return part as unknown as JSX.Element | |
const element = dlv(params, part) | |
if (!isValidElement(element)) { | |
throw new Error(`I18n non JSX in <${part}>.`) | |
} | |
// empty the next one, which is a child element | |
const child = array[i + 1] | |
array[i + 1] = '' | |
return cloneElement(element, { | |
// @ts-expect-error JSX props type is unknown | |
...element.props, | |
key: i, | |
children: child, | |
}) as JSX.Element | |
}) | |
} | |
} | |
return value | |
} | |
return { | |
t, | |
locale, | |
} | |
} | |
export function GetLanguageName(locale: string) { | |
switch (locale) { | |
case 'en': | |
return 'English' | |
case 'pt': | |
return 'Português' | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment