feat(i18n) : add personal languages and compact weapon exports
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
|
||||
DropdownMenuRadioGroup, DropdownMenuRadioItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { languages, isLocale } from "@/lib/i18n/locales";
|
||||
import { useTranslation } from "./locale-provider";
|
||||
|
||||
export function LanguageDropdown() {
|
||||
const { locale, setLocale, t } = useTranslation();
|
||||
const selected = languages.find((language) => language.code === locale)!;
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />} aria-label={t("Language")}>
|
||||
<Image src={selected.flag} alt="" aria-hidden width={24} height={16} />
|
||||
<span lang={selected.code}>{selected.name}</span>
|
||||
<ChevronDownIcon data-icon="inline-end" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuRadioGroup value={locale} onValueChange={(value) => { if (isLocale(value)) setLocale(value); }} aria-label={t("Language")}>
|
||||
{languages.map((language) => (
|
||||
<DropdownMenuRadioItem key={language.code} value={language.code}>
|
||||
<Image src={language.flag} alt="" aria-hidden width={24} height={16} />
|
||||
<span lang={language.code}>{language.name}</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useCallback, useContext, useEffect, useSyncExternalStore, type ReactNode } from "react";
|
||||
import { createLocaleStore } from "@/lib/i18n/locale-store";
|
||||
import { defaultLocale, type Locale } from "@/lib/i18n/locales";
|
||||
import { translate, type MessageParams } from "@/lib/i18n/messages";
|
||||
import { exportText, type ExportMessageKey } from "@/lib/guides/export-text";
|
||||
|
||||
// This store contains a browser preference only; server rendering always uses Thai.
|
||||
const store = createLocaleStore(() => typeof window === "undefined" ? undefined : window.localStorage);
|
||||
const LocaleContext = createContext<Locale>(defaultLocale);
|
||||
|
||||
export function LocaleProvider({ children }: { children: ReactNode }) {
|
||||
const locale = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getServerSnapshot);
|
||||
useEffect(() => { document.documentElement.lang = locale; }, [locale]);
|
||||
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
|
||||
}
|
||||
|
||||
export function LocaleBoundary({ locale, children }: { locale: Locale; children: ReactNode }) {
|
||||
return <LocaleContext.Provider value={locale}>{children}</LocaleContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTranslation() {
|
||||
const locale = useContext(LocaleContext);
|
||||
const t = useCallback((message: string, params?: MessageParams) => translate(message, params, locale), [locale]);
|
||||
const e = useCallback((key: ExportMessageKey, params?: MessageParams) => exportText(key, params, locale), [locale]);
|
||||
return {
|
||||
locale,
|
||||
setLocale: store.setLocale,
|
||||
t,
|
||||
e,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { cloneElement, type ReactElement } from "react";
|
||||
import { useTranslation } from "./locale-provider";
|
||||
import { exportStatLabel, type ExportMessageKey } from "@/lib/guides/export-text";
|
||||
import type { MessageParams } from "@/lib/i18n/messages";
|
||||
|
||||
export function Text({ message, params }: { message: string; params?: MessageParams }) {
|
||||
const { t } = useTranslation();
|
||||
return t(message, params);
|
||||
}
|
||||
|
||||
export function ExportText({ message, params }: { message: ExportMessageKey; params?: MessageParams }) {
|
||||
const { e } = useTranslation();
|
||||
return e(message, params);
|
||||
}
|
||||
|
||||
export function StatText({ value }: { value: string }) {
|
||||
const { locale } = useTranslation();
|
||||
return exportStatLabel(value, locale);
|
||||
}
|
||||
|
||||
export function Localized({ children, attributes }: {
|
||||
children: ReactElement;
|
||||
attributes: Record<string, string | { message: string; params: MessageParams }>;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const props = Object.fromEntries(Object.entries(attributes).map(([key, value]) => [
|
||||
key, typeof value === "string" ? t(value) : t(value.message, value.params),
|
||||
]));
|
||||
return cloneElement(children, props);
|
||||
}
|
||||
|
||||
export function MetricText({ value }: { value: string }) {
|
||||
const { locale } = useTranslation();
|
||||
return `${Number(value).toLocaleString(locale, { maximumFractionDigits: 2 })}%`;
|
||||
}
|
||||
|
||||
export function SectionTitle({ slug, fallback }: { slug: string; fallback: string }) {
|
||||
const { t } = useTranslation();
|
||||
const titles: Record<string, string> = { overview: "Overview", weapon: "Weapons", artifact: "Artifacts & Stats", constellations: "Constellations", team: "Team Calc" };
|
||||
return titles[slug] ? t(titles[slug]) : fallback;
|
||||
}
|
||||
|
||||
export function ConditionLabel({ name, index }: { name: string | null; index: number }) {
|
||||
const { e } = useTranslation();
|
||||
return name || e("condition", { index });
|
||||
}
|
||||
Reference in New Issue
Block a user