initial commit
Some checks failed
build / lint (push) Failing after 36s
build / build (push) Has been skipped

This commit is contained in:
2026-01-29 16:13:04 +00:00
commit a32badd025
31 changed files with 6256 additions and 0 deletions

77
app/hooks/useI18n.ts Normal file
View File

@@ -0,0 +1,77 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import de from "@/locales/de.json";
import en from "@/locales/en.json";
import es from "@/locales/es.json";
import fr from "@/locales/fr.json";
import it from "@/locales/it.json";
import sv from "@/locales/sv.json";
const bundles = { en, es, fr, de, it, sv } as const;
export type Locale = keyof typeof bundles;
export type MessageKey = string;
export type LocaleOption = {
code: Locale;
name: string;
emoji: string;
};
const localeOptions: LocaleOption[] = [
{ code: "en", name: "English", emoji: "🇺🇸" },
{ code: "es", name: "Español", emoji: "🇪🇸" },
{ code: "fr", name: "Français", emoji: "🇫🇷" },
{ code: "de", name: "Deutsch", emoji: "🇩🇪" },
{ code: "it", name: "Italiano", emoji: "🇮🇹" },
{ code: "sv", name: "Svenska", emoji: "🇸🇪" },
];
export function useI18n(defaultLocale: Locale = "en") {
const [locale, setLocale] = useState<Locale>(defaultLocale);
const hasDetectedLocale = useRef(false);
useEffect(() => {
if (hasDetectedLocale.current) return;
hasDetectedLocale.current = true;
const navLangs =
typeof navigator !== "undefined" ? navigator.languages || [navigator.language] : [];
const mapToLocale = (lang: string | undefined | null): Locale | null => {
if (!lang) return null;
const lower = lang.toLowerCase();
if (lower.startsWith("es")) return "es";
if (lower.startsWith("fr")) return "fr";
if (lower.startsWith("de")) return "de";
if (lower.startsWith("it")) return "it";
if (lower.startsWith("sv")) return "sv";
if (lower.startsWith("en")) return "en";
return null;
};
const detected = navLangs.map((l) => mapToLocale(l)).find(Boolean);
if (detected && detected !== locale) {
setLocale(detected);
}
}, [locale]);
const t = useCallback(
(key: MessageKey, vars?: Record<string, string>): string => {
const parts = key.split(".");
const dict = bundles[locale] ?? bundles.en;
const fallbackDict = bundles.en;
const resolveKey = (source: unknown) =>
parts.reduce<unknown>(
(acc, part) =>
acc && typeof acc === "object" ? (acc as Record<string, unknown>)[part] : undefined,
source
);
const value = resolveKey(dict) ?? resolveKey(fallbackDict);
const text = typeof value === "string" ? value : key;
if (!vars) return text;
return text.replace(/\{(\w+)\}/g, (_: string, k: string) => (vars[k] ? vars[k] : `{${k}}`));
},
[locale]
);
const options = useMemo(() => localeOptions, []);
return { locale, setLocale, t, localeOptions: options };
}

38
app/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
export type Theme = "light" | "dark";
export function useTheme(initial?: Theme) {
const [theme, setTheme] = useState<Theme>(initial ?? "light");
useEffect(() => {
const stored =
typeof window !== "undefined" && "localStorage" in window
? window.localStorage.getItem("theme")
: null;
if (stored === "light" || stored === "dark") {
setTheme(stored);
return;
}
if (typeof window !== "undefined" && "matchMedia" in window) {
const media = window.matchMedia("(prefers-color-scheme: dark)");
setTheme(media.matches ? "dark" : "light");
const handler = (event: MediaQueryListEvent) => setTheme(event.matches ? "dark" : "light");
media.addEventListener("change", handler);
return () => media.removeEventListener("change", handler);
}
setTheme("light");
}, []);
useEffect(() => {
if (typeof document === "undefined") return;
document.documentElement.setAttribute("data-theme", theme);
if ("localStorage" in window) {
window.localStorage.setItem("theme", theme);
}
}, [theme]);
const toggleTheme = () => setTheme((prev) => (prev === "light" ? "dark" : "light"));
return { theme, setTheme, toggleTheme };
}