localization

This commit is contained in:
2026-02-17 21:36:12 +00:00
parent 3733e99ba8
commit 680f623b41
17 changed files with 341 additions and 71 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"name": "i18n",
"version": "0.0.1",
"license": "MIT",
"type": "module",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
}
}
+25
View File
@@ -0,0 +1,25 @@
import { createContext, useMemo } from 'react';
import type { ReactNode } from 'react';
import type { I18nContextValue, InterpolationParams, TranslationMap } from './types';
import { interpolate, resolveKey } from './resolve';
export const I18nContext = createContext<I18nContextValue>({
t: (key: string) => key,
locale: 'en',
});
type I18nProviderProps = {
translations: TranslationMap;
locale: string;
children: ReactNode;
};
export const I18nProvider = ({ translations, locale, children }: I18nProviderProps) => {
const value = useMemo<I18nContextValue>(() => {
const t = (key: string, params?: InterpolationParams) =>
interpolate(resolveKey(translations, locale, key), params);
return { t, locale };
}, [translations, locale]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
};
+4
View File
@@ -0,0 +1,4 @@
export { I18nProvider, I18nContext } from './I18nProvider';
export { useTranslation } from './useTranslation';
export { resolveKey, interpolate } from './resolve';
export type { TranslationMap, InterpolationParams, I18nContextValue, TranslationKeys } from './types';
+20
View File
@@ -0,0 +1,20 @@
import type { InterpolationParams, TranslationMap } from './types';
export const resolveKey = (translations: TranslationMap, locale: string, key: string): string => {
const value = walkPath(translations[locale], key) ?? walkPath(translations['en'], key);
return typeof value === 'string' ? value : key;
};
const walkPath = (obj: unknown, path: string): unknown => {
let current = obj;
for (const segment of path.split('.')) {
if (current == null || typeof current !== 'object') return undefined;
current = (current as Record<string, unknown>)[segment];
}
return current;
};
export const interpolate = (template: string, params?: InterpolationParams): string => {
if (!params) return template;
return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => String(params[key] ?? `{{${key}}}`));
};
+16
View File
@@ -0,0 +1,16 @@
export type TranslationMap = Record<string, Record<string, unknown>>;
export type InterpolationParams = Record<string, string | number>;
export type I18nContextValue = {
t: (key: string, params?: InterpolationParams) => string;
locale: string;
};
export type TranslationKeys<T, Prefix extends string = ''> = T extends string
? Prefix
: T extends Record<string, unknown>
? {
[K in keyof T & string]: TranslationKeys<T[K], Prefix extends '' ? K : `${Prefix}.${K}`>;
}[keyof T & string]
: never;
@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { I18nContext } from './I18nProvider';
export const useTranslation = () => useContext(I18nContext);