mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
168 lines
5.7 KiB
TypeScript
168 lines
5.7 KiB
TypeScript
import { en } from '@/locales/en.ts';
|
|
import { ru } from '@/locales/ru.ts';
|
|
import { tt } from '@/locales/tt.ts';
|
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
import { z } from 'zod';
|
|
|
|
type Language = 'en' | 'ru' | 'tt';
|
|
|
|
// Translation value can be string, object, or array
|
|
// Using z.lazy() for recursive union types (still recommended in Zod v4 for unions)
|
|
// Note: z.interface() is for object schemas with recursive properties, not union types
|
|
const TranslationValueSchema: z.ZodTypeAny = z.lazy(() =>
|
|
z.union([
|
|
z.string(),
|
|
z.record(z.string(), TranslationValueSchema),
|
|
z.array(TranslationValueSchema),
|
|
])
|
|
);
|
|
|
|
// Translation object schema
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const TranslationSchema = z.record(z.string(), TranslationValueSchema);
|
|
|
|
// Replacements schema for interpolation
|
|
const ReplacementsSchema = z
|
|
.object({
|
|
count: z.number().optional(),
|
|
})
|
|
.catchall(z.union([z.string(), z.number()]));
|
|
|
|
interface I18nContextType {
|
|
lang: Language;
|
|
setLang: (lang: Language) => void;
|
|
t: (key: string, replacements?: z.infer<typeof ReplacementsSchema>) => string;
|
|
t_raw: (key: string) => z.infer<typeof TranslationValueSchema>;
|
|
}
|
|
|
|
const translations = { en, ru, tt };
|
|
const LANG_STORAGE_KEY = 'tugan-yak-lang';
|
|
|
|
const I18nContext = createContext<I18nContextType | undefined>(undefined);
|
|
|
|
// Helper to resolve nested keys like 'a.b.c'
|
|
const resolvePath = (
|
|
obj: z.infer<typeof TranslationSchema>,
|
|
path: string
|
|
): z.infer<typeof TranslationValueSchema> => {
|
|
return path.split('.').reduce((acc: z.infer<typeof TranslationValueSchema>, part: string) => {
|
|
if (acc && typeof acc === 'object' && !Array.isArray(acc)) {
|
|
return (acc as Record<string, z.infer<typeof TranslationValueSchema>>)[part];
|
|
}
|
|
return undefined;
|
|
}, obj);
|
|
};
|
|
|
|
export const I18nProvider = ({ children }: { children?: React.ReactNode }) => {
|
|
const [lang, setLang] = useState<Language>(() => {
|
|
try {
|
|
const storedLang = window.localStorage.getItem(LANG_STORAGE_KEY);
|
|
if (storedLang === 'en' || storedLang === 'ru' || storedLang === 'tt') {
|
|
return storedLang;
|
|
}
|
|
const browserLang = navigator.language.split(/[-_]/)[0];
|
|
if (browserLang === 'ru') return 'ru';
|
|
if (browserLang === 'tt') return 'tt';
|
|
} catch (e) {
|
|
console.error('Could not read language preference.', e);
|
|
}
|
|
return 'en'; // Default to English
|
|
});
|
|
|
|
useEffect(() => {
|
|
document.documentElement.lang = lang;
|
|
try {
|
|
window.localStorage.setItem(LANG_STORAGE_KEY, lang);
|
|
} catch (e) {
|
|
console.error('Could not save language to localStorage', e);
|
|
}
|
|
}, [lang]);
|
|
|
|
const t_raw = useCallback(
|
|
(key: string): z.infer<typeof TranslationValueSchema> => {
|
|
let translation = resolvePath(translations[lang], key);
|
|
if (translation === undefined) {
|
|
// Fallback to English if translation is missing in the current language
|
|
translation = resolvePath(translations.en, key);
|
|
}
|
|
return translation;
|
|
},
|
|
[lang]
|
|
);
|
|
|
|
// Memoize plural rules to avoid recreating on every call
|
|
const pluralRules = useMemo(() => new Intl.PluralRules(lang), [lang]);
|
|
|
|
const t = useCallback(
|
|
(key: string, replacements?: z.infer<typeof ReplacementsSchema>): string => {
|
|
let translation: z.infer<typeof TranslationValueSchema>;
|
|
let usedKey = key;
|
|
|
|
if (replacements?.count !== undefined) {
|
|
const pluralCategory = pluralRules.select(replacements.count);
|
|
|
|
const pluralKey = `${key}.${pluralCategory}`;
|
|
const pluralTranslation = t_raw(pluralKey);
|
|
|
|
if (typeof pluralTranslation === 'string') {
|
|
translation = pluralTranslation;
|
|
usedKey = pluralKey;
|
|
} else {
|
|
// Fallback to the '.other' key which is standard for i18next-style pluralization
|
|
const otherKey = `${key}.other`;
|
|
const otherTranslation = t_raw(otherKey);
|
|
if (typeof otherTranslation === 'string') {
|
|
translation = otherTranslation;
|
|
usedKey = otherKey;
|
|
} else {
|
|
// Final fallback to non-plural version
|
|
translation = t_raw(key);
|
|
}
|
|
}
|
|
} else {
|
|
translation = t_raw(key);
|
|
}
|
|
|
|
if (typeof translation !== 'string') {
|
|
// If translation is an object with a 'name' property, use that (for sectors.X.name)
|
|
if (translation && typeof translation === 'object' && 'name' in translation && typeof translation.name === 'string') {
|
|
return translation.name;
|
|
}
|
|
// If translation is an object with a 'desc' property, use that (for sectors.X.desc)
|
|
if (translation && typeof translation === 'object' && 'desc' in translation && typeof translation.desc === 'string') {
|
|
return translation.desc;
|
|
}
|
|
console.warn(
|
|
`Translation for key "${usedKey}" is not a string. Found: ${typeof translation}. Falling back to key.`
|
|
);
|
|
return key;
|
|
}
|
|
|
|
if (replacements) {
|
|
// Replace placeholders - create regex only once per placeholder
|
|
let result = translation;
|
|
Object.keys(replacements).forEach((placeholder) => {
|
|
const regex = new RegExp(`{{${placeholder}}}`, 'g');
|
|
result = result.replace(regex, String(replacements[placeholder]));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
return translation;
|
|
},
|
|
[t_raw, pluralRules]
|
|
);
|
|
|
|
return (
|
|
<I18nContext.Provider value={{ lang, setLang, t, t_raw }}>{children}</I18nContext.Provider>
|
|
);
|
|
};
|
|
|
|
export const useTranslation = (): I18nContextType => {
|
|
const context = useContext(I18nContext);
|
|
if (!context) {
|
|
throw new Error('useTranslation must be used within an I18nProvider');
|
|
}
|
|
return context;
|
|
};
|