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:
Your Name 2026-01-20 10:01:55 +08:00
parent e792dfedf1
commit 33301033ae
21 changed files with 1205 additions and 89 deletions

View file

@ -12,6 +12,7 @@ import {
HTTP_ALLOWED,
WEBROOT,
} from "../helpers/env";
import { localeService } from "../i18n/service";
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
@ -65,51 +66,52 @@ export const userService = new Elysia({ name: "user/service" })
export const user = new Elysia()
.use(userService)
.get("/setup", ({ redirect }) => {
.use(localeService)
.get("/setup", ({ redirect, locale, t }) => {
if (!FIRST_RUN) {
return redirect(`${WEBROOT}/login`, 302);
}
return (
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT}>
<BaseHtml title="ConvertX | Setup" webroot={WEBROOT} locale={locale}>
<main
class={`
mx-auto w-full max-w-4xl flex-1 px-2
sm:px-4
`}
>
<h1 class="my-8 text-3xl">Welcome to ConvertX!</h1>
<h1 class="my-8 text-3xl">{t("setup", "welcome")}</h1>
<article class="article p-0">
<header class="w-full bg-neutral-800 p-4">Create your account</header>
<header class="w-full bg-neutral-800 p-4">{t("setup", "createYourAccount")}</header>
<form method="post" action={`${WEBROOT}/register`} class="p-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
{t("auth", "email")}
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
placeholder={t("auth", "email")}
autocomplete="email"
required
/>
</label>
<label class="flex flex-col gap-1">
Password
{t("auth", "password")}
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
placeholder={t("auth", "password")}
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Create account" class="btn-primary" />
<input type="submit" value={t("auth", "createAccount")} class="btn-primary" />
</form>
<footer class="p-4">
Report any issues on{" "}
{t("setup", "reportIssues")}{" "}
<a
class={`
text-accent-500 underline
@ -117,7 +119,7 @@ export const user = new Elysia()
`}
href="https://github.com/C4illin/ConvertX"
>
GitHub
{t("setup", "github")}
</a>
.
</footer>
@ -126,19 +128,21 @@ export const user = new Elysia()
</BaseHtml>
);
})
.get("/register", ({ redirect }) => {
.get("/register", ({ redirect, locale, t }) => {
if (!ACCOUNT_REGISTRATION) {
return redirect(`${WEBROOT}/login`, 302);
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Register">
<BaseHtml webroot={WEBROOT} title="ConvertX | Register" locale={locale}>
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
locale={locale}
t={t}
/>
<main
class={`
@ -150,29 +154,29 @@ export const user = new Elysia()
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
{t("auth", "email")}
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
placeholder={t("auth", "email")}
autocomplete="email"
required
/>
</label>
<label class="flex flex-col gap-1">
Password
{t("auth", "password")}
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
placeholder={t("auth", "password")}
autocomplete="current-password"
required
/>
</label>
</fieldset>
<input type="submit" value="Register" class="w-full btn-primary" />
<input type="submit" value={t("auth", "registerButton")} class="w-full btn-primary" />
</form>
</article>
</main>
@ -237,7 +241,7 @@ export const user = new Elysia()
)
.get(
"/login",
async ({ jwt, redirect, cookie: { auth } }) => {
async ({ jwt, redirect, cookie: { auth }, locale, t }) => {
if (FIRST_RUN) {
return redirect(`${WEBROOT}/setup`, 302);
}
@ -254,13 +258,15 @@ export const user = new Elysia()
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Login">
<BaseHtml webroot={WEBROOT} title="ConvertX | Login" locale={locale}>
<>
<Header
webroot={WEBROOT}
accountRegistration={ACCOUNT_REGISTRATION}
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
locale={locale}
t={t}
/>
<main
class={`
@ -272,23 +278,23 @@ export const user = new Elysia()
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
{t("auth", "email")}
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
placeholder={t("auth", "email")}
autocomplete="email"
required
/>
</label>
<label class="flex flex-col gap-1">
Password
{t("auth", "password")}
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
placeholder={t("auth", "password")}
autocomplete="current-password"
required
/>
@ -301,10 +307,10 @@ export const user = new Elysia()
role="button"
class="w-full btn-secondary text-center"
>
Register
{t("nav", "register")}
</a>
) : null}
<input type="submit" value="Login" class="w-full btn-primary" />
<input type="submit" value={t("auth", "loginButton")} class="w-full btn-primary" />
</div>
</form>
</article>
@ -376,7 +382,7 @@ export const user = new Elysia()
})
.get(
"/account",
async ({ user, redirect }) => {
async ({ user, redirect, locale, t }) => {
if (!user) {
return redirect(`${WEBROOT}/`, 302);
}
@ -388,7 +394,7 @@ export const user = new Elysia()
}
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Account">
<BaseHtml webroot={WEBROOT} title="ConvertX | Account" locale={locale}>
<>
<Header
webroot={WEBROOT}
@ -396,6 +402,8 @@ export const user = new Elysia()
allowUnauthenticated={ALLOW_UNAUTHENTICATED}
hideHistory={HIDE_HISTORY}
loggedIn
locale={locale}
t={t}
/>
<main
class={`
@ -407,41 +415,41 @@ export const user = new Elysia()
<form method="post" class="flex flex-col gap-4">
<fieldset class="mb-4 flex flex-col gap-4">
<label class="flex flex-col gap-1">
Email
{t("auth", "email")}
<input
type="email"
name="email"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Email"
placeholder={t("auth", "email")}
autocomplete="email"
value={userData.email}
required
/>
</label>
<label class="flex flex-col gap-1">
Password (leave blank for unchanged)
{t("auth", "newPassword")}
<input
type="password"
name="newPassword"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
placeholder={t("auth", "password")}
autocomplete="new-password"
/>
</label>
<label class="flex flex-col gap-1">
Current Password
{t("auth", "currentPassword")}
<input
type="password"
name="password"
class="rounded-sm bg-neutral-800 p-3"
placeholder="Password"
placeholder={t("auth", "password")}
autocomplete="current-password"
required
/>
</label>
</fieldset>
<div role="group">
<input type="submit" value="Update" class="w-full btn-primary" />
<input type="submit" value={t("auth", "updateButton")} class="w-full btn-primary" />
</div>
</form>
</article>