feat(i18n): add multi-language UI support with 5 languages
- Add i18n framework with locale detection and cookie persistence - Support English, Traditional Chinese (zh-TW), Simplified Chinese (zh-CN), Japanese (ja), Korean (ko) - Add LanguageSelector component in navigation header - Create locale JSON files with full translations - Update all page components (root, user, results, history) with i18n support - Add client-side translation helpers for dynamic content - Auto-detect user's preferred language from browser settings - Bump version to v0.1.3
This commit is contained in:
parent
e792dfedf1
commit
33301033ae
21 changed files with 1205 additions and 89 deletions
|
|
@ -1,31 +1,39 @@
|
|||
import { version } from "../../package.json";
|
||||
import { type SupportedLocale, defaultLocale, getTranslations } from "../i18n";
|
||||
|
||||
export const BaseHtml = ({
|
||||
children,
|
||||
title = "ConvertX",
|
||||
webroot = "",
|
||||
locale = defaultLocale,
|
||||
}: {
|
||||
children: JSX.Element;
|
||||
title?: string;
|
||||
webroot?: string;
|
||||
locale?: SupportedLocale;
|
||||
}) => (
|
||||
<html lang="en">
|
||||
<html lang={locale}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="webroot" content={webroot} />
|
||||
<meta name="locale" content={locale} />
|
||||
<title safe>{title}</title>
|
||||
<link rel="stylesheet" href={`${webroot}/generated.css`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${webroot}/apple-touch-icon.png`} />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href={`${webroot}/favicon-32x32.png`} />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href={`${webroot}/favicon-16x16.png`} />
|
||||
<link rel="manifest" href={`${webroot}/site.webmanifest`} />
|
||||
<script>
|
||||
{`window.__TRANSLATIONS__ = ${JSON.stringify(getTranslations(locale))};`}
|
||||
</script>
|
||||
<script src={`${webroot}/i18n.js`} defer />
|
||||
</head>
|
||||
<body class={`flex min-h-screen w-full flex-col bg-neutral-900 text-neutral-200`}>
|
||||
{children}
|
||||
<footer class="w-full">
|
||||
<div class="p-4 text-center text-sm text-neutral-500">
|
||||
<span>Powered by </span>
|
||||
<span>{locale === "en" ? "Powered by " : `${getTranslations(locale).common.poweredBy} `}</span>
|
||||
<a
|
||||
href="https://github.com/C4illin/ConvertX"
|
||||
class={`
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
import { type SupportedLocale, type Translator, createTranslator, defaultLocale } from "../i18n";
|
||||
import { LanguageSelector } from "./languageSelector";
|
||||
|
||||
export const Header = ({
|
||||
loggedIn,
|
||||
accountRegistration,
|
||||
allowUnauthenticated,
|
||||
hideHistory,
|
||||
webroot = "",
|
||||
locale = defaultLocale,
|
||||
t = createTranslator(defaultLocale),
|
||||
}: {
|
||||
loggedIn?: boolean;
|
||||
accountRegistration?: boolean;
|
||||
allowUnauthenticated?: boolean;
|
||||
hideHistory?: boolean;
|
||||
webroot?: string;
|
||||
locale?: SupportedLocale;
|
||||
t?: Translator;
|
||||
}) => {
|
||||
let rightNav: JSX.Element;
|
||||
if (loggedIn) {
|
||||
rightNav = (
|
||||
<ul class="flex gap-4">
|
||||
<ul class="flex items-center gap-4">
|
||||
{!hideHistory && (
|
||||
<li>
|
||||
<a
|
||||
|
|
@ -24,7 +31,7 @@ export const Header = ({
|
|||
`}
|
||||
href={`${webroot}/history`}
|
||||
>
|
||||
History
|
||||
{t("nav", "history")}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
|
|
@ -37,7 +44,7 @@ export const Header = ({
|
|||
`}
|
||||
href={`${webroot}/account`}
|
||||
>
|
||||
Account
|
||||
{t("nav", "account")}
|
||||
</a>
|
||||
</li>
|
||||
) : null}
|
||||
|
|
@ -50,15 +57,18 @@ export const Header = ({
|
|||
`}
|
||||
href={`${webroot}/logoff`}
|
||||
>
|
||||
Logout
|
||||
{t("nav", "logout")}
|
||||
</a>
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<LanguageSelector currentLocale={locale} webroot={webroot} t={t} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
} else {
|
||||
rightNav = (
|
||||
<ul class="flex gap-4">
|
||||
<ul class="flex items-center gap-4">
|
||||
<li>
|
||||
<a
|
||||
class={`
|
||||
|
|
@ -67,7 +77,7 @@ export const Header = ({
|
|||
`}
|
||||
href={`${webroot}/login`}
|
||||
>
|
||||
Login
|
||||
{t("nav", "login")}
|
||||
</a>
|
||||
</li>
|
||||
{accountRegistration ? (
|
||||
|
|
@ -79,10 +89,13 @@ export const Header = ({
|
|||
`}
|
||||
href={`${webroot}/register`}
|
||||
>
|
||||
Register
|
||||
{t("nav", "register")}
|
||||
</a>
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<LanguageSelector currentLocale={locale} webroot={webroot} t={t} />
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
92
src/components/languageSelector.tsx
Normal file
92
src/components/languageSelector.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { supportedLocales, type SupportedLocale, type Translator } from "../i18n";
|
||||
|
||||
export const LanguageSelector = ({
|
||||
currentLocale,
|
||||
webroot = "",
|
||||
t,
|
||||
}: {
|
||||
currentLocale: SupportedLocale;
|
||||
webroot?: string;
|
||||
t: Translator;
|
||||
}) => {
|
||||
return (
|
||||
<div class="language-selector relative">
|
||||
<button
|
||||
type="button"
|
||||
class={`
|
||||
flex items-center gap-1 text-accent-600 transition-all
|
||||
hover:text-accent-500
|
||||
`}
|
||||
id="language-toggle"
|
||||
aria-label={t("nav", "language")}
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-5 w-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M10.5 21l5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 016-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 01-3.827-5.802"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hidden sm:inline">{t("nav", "language")}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<ul
|
||||
id="language-dropdown"
|
||||
class={`
|
||||
absolute right-0 top-full z-50 mt-2 hidden min-w-[140px] flex-col overflow-hidden rounded
|
||||
border border-neutral-700 bg-neutral-800 shadow-lg
|
||||
`}
|
||||
role="menu"
|
||||
>
|
||||
{supportedLocales.map((locale) => (
|
||||
<li role="none">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
class={`
|
||||
language-option flex w-full items-center gap-2 px-4 py-2 text-left text-sm
|
||||
transition-colors
|
||||
hover:bg-neutral-700
|
||||
${locale.code === currentLocale ? "bg-neutral-700 text-accent-500" : "text-neutral-200"}
|
||||
`}
|
||||
data-locale={locale.code}
|
||||
data-webroot={webroot}
|
||||
>
|
||||
{locale.code === currentLocale && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="h-4 w-4 text-accent-500"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
)}
|
||||
<span class={locale.code === currentLocale ? "" : "ml-6"}>{locale.nativeName}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue