Phase 3-4: Goals AI planner, Team tasks, Push notifications, i18n (CZ/HE/RU/UA)
- Goals CRUD API + AI study plan generator + progress reports - Goals frontend page with progress bars - Team tasks: assign, transfer, collaborate endpoints - Push notifications: web-push, VAPID, subscribe/send - i18n: 4 languages (cs, he, ru, ua) translation files - notifications.js + goals.js routes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
99
apps/tasks/lib/i18n.tsx
Normal file
99
apps/tasks/lib/i18n.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from "react";
|
||||
|
||||
import cs from "@/messages/cs.json";
|
||||
import he from "@/messages/he.json";
|
||||
import ru from "@/messages/ru.json";
|
||||
import ua from "@/messages/ua.json";
|
||||
|
||||
export type Locale = "cs" | "he" | "ru" | "ua";
|
||||
|
||||
export const LOCALES: { code: Locale; label: string; flag: string; dir: "ltr" | "rtl" }[] = [
|
||||
{ code: "cs", label: "\u010ce\u0161tina", flag: "\ud83c\udde8\ud83c\uddff", dir: "ltr" },
|
||||
{ code: "he", label: "\u05e2\u05d1\u05e8\u05d9\u05ea", flag: "\ud83c\uddee\ud83c\uddf1", dir: "rtl" },
|
||||
{ code: "ru", label: "\u0420\u0443\u0441\u0441\u043a\u0438\u0439", flag: "\ud83c\uddf7\ud83c\uddfa", dir: "ltr" },
|
||||
{ code: "ua", label: "\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430", flag: "\ud83c\uddfa\ud83c\udde6", dir: "ltr" },
|
||||
];
|
||||
|
||||
type Messages = typeof cs;
|
||||
|
||||
const MESSAGES: Record<Locale, Messages> = { cs, he, ru, ua };
|
||||
|
||||
const STORAGE_KEY = "taskteam_language";
|
||||
|
||||
function getNestedValue(obj: unknown, path: string): string {
|
||||
const keys = path.split(".");
|
||||
let current: unknown = obj;
|
||||
for (const key of keys) {
|
||||
if (current == null || typeof current !== "object") return path;
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return typeof current === "string" ? current : path;
|
||||
}
|
||||
|
||||
interface I18nContextType {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: (key: string) => string;
|
||||
dir: "ltr" | "rtl";
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextType>({
|
||||
locale: "cs",
|
||||
setLocale: () => {},
|
||||
t: (key: string) => key,
|
||||
dir: "ltr",
|
||||
isRTL: false,
|
||||
});
|
||||
|
||||
export function useTranslation() {
|
||||
return useContext(I18nContext);
|
||||
}
|
||||
|
||||
export function I18nProvider({ children }: { children: ReactNode }) {
|
||||
const [locale, setLocaleState] = useState<Locale>("cs");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
if (typeof window !== "undefined") {
|
||||
const stored = localStorage.getItem(STORAGE_KEY) as Locale | null;
|
||||
if (stored && MESSAGES[stored]) {
|
||||
setLocaleState(stored);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setLocale = useCallback((newLocale: Locale) => {
|
||||
setLocaleState(newLocale);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, newLocale);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const t = useCallback(
|
||||
(key: string): string => {
|
||||
return getNestedValue(MESSAGES[locale], key);
|
||||
},
|
||||
[locale]
|
||||
);
|
||||
|
||||
const localeInfo = LOCALES.find((l) => l.code === locale) || LOCALES[0];
|
||||
const dir = localeInfo.dir;
|
||||
const isRTL = dir === "rtl";
|
||||
|
||||
// Update html attributes when locale changes
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
document.documentElement.lang = locale;
|
||||
document.documentElement.dir = dir;
|
||||
}, [locale, dir, mounted]);
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={{ locale, setLocale, t, dir, isRTL }}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user