Skip to content

Instantly share code, notes, and snippets.

@lightningspirit
Last active February 6, 2024 17:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lightningspirit/7508f635d083851b4611bd6884987ecf to your computer and use it in GitHub Desktop.
Save lightningspirit/7508f635d083851b4611bd6884987ecf to your computer and use it in GitHub Desktop.
React I18n only server side
import 'server-only'
import serverOnlyContext from './server-only-context'
export type Locale = string
export const I18nContext = serverOnlyContext<Locale>('en')
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
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
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