feat: add i18n support

This commit is contained in:
hamster1963 2024-09-26 10:23:56 +08:00
parent 624ccc75d9
commit 01f1646fe9
38 changed files with 416 additions and 298 deletions

View File

@ -1,32 +0,0 @@
"use client";
import { ServerApi } from "@/app/types/nezha-api";
import ServerCard from "@/components/ServerCard";
import { nezhaFetcher } from "@/lib/utils";
import useSWR from "swr";
import getEnv from "@/lib/env-entry";
export default function ServerListClient() {
const { data } = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000,
});
if (!data) return null;
const sortedServers = data.result.sort((a, b) => {
if (a.display_index && b.display_index) {
return b.display_index - a.display_index;
}
if (a.display_index) return -1;
if (b.display_index) return 1;
return a.id - b.id;
});
return (
<section className="grid grid-cols-1 gap-2 md:grid-cols-2">
{sortedServers.map((serverInfo) => (
<ServerCard key={serverInfo.id} serverInfo={serverInfo} />
))}
</section>
);
}

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
export default function Footer() {
return (
<footer className="mx-auto w-full max-w-5xl">
<section className="flex flex-col">
<p className="mt-3 flex gap-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
Find the code on{" "}
<a
href="https://github.com/hamster1963/nezha-dash"
target="_blank"
className="cursor-pointer font-normal underline decoration-yellow-500 decoration-2 underline-offset-2 dark:decoration-yellow-500/50"
>
GitHub
</a>
</p>
<section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
© 2020-{new Date().getFullYear()}{" "}
<a href={"https://buycoffee.top"}>@Hamster1963</a>
</section>
</section>
</footer>
);
}

View File

@ -1,20 +0,0 @@
import React from "react";
import Header from "@/app/(main)/header";
import Footer from "./footer";
type DashboardProps = {
children: React.ReactNode;
};
export default function MainLayout({ children }: DashboardProps) {
return (
<div className="flex min-h-screen w-full flex-col">
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:p-10 md:pt-8">
<Header />
{children}
<Footer />
</main>
</div>
);
}

View File

@ -1,29 +0,0 @@
import ServerList from "@/components/ServerList";
import ServerOverview from "@/components/ServerOverview";
import getEnv from "@/lib/env-entry";
import { GetNezhaData } from "@/lib/serverFetch";
import { SWRConfig } from "swr";
const disablePrefetch = getEnv("ServerDisablePrefetch") === "true";
const fallback = disablePrefetch
? {}
: {
"/api/server": GetNezhaData(),
};
export default function Home() {
return (
<SWRConfig
value={{
fallback: fallback,
}}
>
<div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6">
<ServerOverview />
<ServerList />
</div>
</SWRConfig>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import { useTranslations } from 'next-intl';
import { ServerApi } from "../../types/nezha-api";
import ServerCard from "../../../../components/ServerCard";
import { nezhaFetcher } from "../../../../lib/utils";
import useSWR from "swr";
import getEnv from "../../../../lib/env-entry";
export default function ServerListClient() {
const {
data
} = useSWR<ServerApi>("/api/server", nezhaFetcher, {
refreshInterval: Number(getEnv("NEXT_PUBLIC_NezhaFetchInterval")) || 2000
});
if (!data) return null;
const sortedServers = data.result.sort((a, b) => {
if (a.display_index && b.display_index) {
return b.display_index - a.display_index;
}
if (a.display_index) return -1;
if (b.display_index) return 1;
return a.id - b.id;
});
return <section className="grid grid-cols-1 gap-2 md:grid-cols-2">
{sortedServers.map(serverInfo => <ServerCard key={serverInfo.id} serverInfo={serverInfo} />)}
</section>;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
import { useTranslations } from 'next-intl';
export default function Footer() {
const t = useTranslations("Footer");
return <footer className="mx-auto w-full max-w-5xl">
<section className="flex flex-col">
<p className="mt-3 flex gap-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">{t('p_146-598_Findthecodeon')}{" "}
<a href="https://github.com/hamster1963/nezha-dash" target="_blank" className="cursor-pointer font-normal underline decoration-yellow-500 decoration-2 underline-offset-2 dark:decoration-yellow-500/50">{t('a_303-585_GitHub')}</a>
</p>
<section className="mt-1 flex items-center gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">{t('section_607-869_2020')}{new Date().getFullYear()}{" "}
<a href={"https://buycoffee.top"}>{t('a_800-850_Hamster1963')}</a>
</section>
</section>
</footer>;
}

View File

@ -1,86 +1,61 @@
"use client";
import { useTranslations } from 'next-intl';
import React, { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { Separator } from "@/components/ui/separator";
import { Separator } from "../../../components/ui/separator";
import { DateTime } from "luxon";
import { ModeToggle } from "@/components/ThemeSwitcher";
import { ModeToggle } from "../../../components/ThemeSwitcher";
function Header() {
return (
<div className="mx-auto w-full max-w-5xl">
const t = useTranslations("Header");
return <div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between">
<section className="flex items-center text-base font-medium">
<div className="mr-1 flex flex-row items-center justify-start">
<Image
width={40}
height={40}
unoptimized
alt="apple-touch-icon"
src={"/apple-touch-icon.png"}
className="relative !m-0 h-6 w-6 border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
/>
<Image width={40} height={40} unoptimized alt="apple-touch-icon" src={"/apple-touch-icon.png"} className="relative !m-0 h-6 w-6 border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105" />
</div>
NezhaDash
<Separator
orientation="vertical"
className="mx-2 hidden h-4 w-[1px] md:block"
/>
<p className="hidden text-sm font-medium opacity-40 md:block">
Simple and beautiful dashboard
</p>
<Separator orientation="vertical" className="mx-2 hidden h-4 w-[1px] md:block" />
<p className="hidden text-sm font-medium opacity-40 md:block">{t('p_1079-1199_Simpleandbeautifuldashbo')}</p>
</section>
<ModeToggle />
</section>
<Overview />
</div>
);
</div>;
}
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: Function, delay?: number | null) => {
const savedCallback = useRef<Function>(() => {});
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
if (delay !== null) {
const interval = setInterval(() => savedCallback.current(), delay || 0);
return () => clearInterval(interval);
}
return undefined;
}, [delay]);
};
function Overview() {
const t = useTranslations("Overview");
const [mouted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const timeOption = DateTime.TIME_SIMPLE;
timeOption.hour12 = true;
const [timeString, setTimeString] = useState(
DateTime.now().setLocale("en-US").toLocaleString(timeOption),
);
const [timeString, setTimeString] = useState(DateTime.now().setLocale("en-US").toLocaleString(timeOption));
useInterval(() => {
setTimeString(DateTime.now().setLocale("en-US").toLocaleString(timeOption));
}, 1000);
return (
<section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">👋 Overview</p>
return <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">{t('p_2277-2331_Overview')}</p>
<div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50">where the time is</p>
{mouted && (
<p className="opacity-1 text-sm font-medium">{timeString}</p>
)}
<p className="text-sm font-medium opacity-50">{t('p_2390-2457_wherethetimeis')}</p>
{mouted && <p className="opacity-1 text-sm font-medium">{timeString}</p>}
</div>
</section>
);
</section>;
}
export default Header;

View File

@ -0,0 +1,18 @@
import { useTranslations } from 'next-intl';
import React from "react";
import Header from "@/app/[locale]/(main)/header";
import Footer from "./footer";
type DashboardProps = {
children: React.ReactNode;
};
export default function MainLayout({
children
}: DashboardProps) {
return <div className="flex min-h-screen w-full flex-col">
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:p-10 md:pt-8">
<Header />
{children}
<Footer />
</main>
</div>;
}

View File

@ -0,0 +1,20 @@
import { useTranslations } from 'next-intl';
import ServerList from "../../../components/ServerList";
import ServerOverview from "../../../components/ServerOverview";
import getEnv from "../../../lib/env-entry";
import { GetNezhaData } from "../../../lib/serverFetch";
import { SWRConfig } from "swr";
const disablePrefetch = getEnv("ServerDisablePrefetch") === "true";
const fallback = disablePrefetch ? {} : {
"/api/server": GetNezhaData()
};
export default function Home() {
return <SWRConfig value={{
fallback: fallback
}}>
<div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6">
<ServerOverview />
<ServerList />
</div>
</SWRConfig>;
}

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

View File

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 324 B

22
app/[locale]/layout.tsx Normal file
View File

@ -0,0 +1,22 @@
// @auto-i18n-check. Please do not delete the line.
import React from "react";
import {NextIntlClientProvider, useMessages} from 'next-intl';
export default function LocaleLayout({
children,
params: {locale}
}: {
children: React.ReactNode; params: { locale: string };
}) {
const messages = useMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}

View File

@ -0,0 +1,16 @@
import { useTranslations } from 'next-intl';
import Image from "next/image";
import Link from "next/link";
export default function NotFoundPage() {
const t = useTranslations("NotFoundPage");
return <main className="relative h-screen w-full">
<div className="absolute inset-0 m-4 flex items-center justify-center">
<Image priority className="rounded-3xl object-cover" src="/tardis.jpg" fill={true} alt="TARDIS" />
<div className="text-container absolute right-4 p-4 md:right-20">
<h1 className="text-2xl font-bold opacity-80 md:text-5xl">{t('h1_490-590_404NotFound')}</h1>
<p className="text-lg opacity-60 md:text-base">{t('p_601-665_TARDISERROR')}</p>
<Link href={"/"} className="text-2xl opacity-80 md:text-3xl">{t('Link_676-775_Doctor')}</Link>
</div>
</div>
</main>;
}

View File

@ -1,27 +0,0 @@
import Image from "next/image";
import Link from "next/link";
export default function NotFoundPage() {
return (
<main className="relative h-screen w-full">
<div className="absolute inset-0 m-4 flex items-center justify-center">
<Image
priority
className="rounded-3xl object-cover"
src="/tardis.jpg"
fill={true}
alt="TARDIS"
/>
<div className="text-container absolute right-4 p-4 md:right-20">
<h1 className="text-2xl font-bold opacity-80 md:text-5xl">
404 Not Found
</h1>
<p className="text-lg opacity-60 md:text-base">TARDIS ERROR!</p>
<Link href={"/"} className="text-2xl opacity-80 md:text-3xl">
Doctor?
</Link>
</div>
</div>
</main>
);
}

15
auto-i18n-config.json Normal file
View File

@ -0,0 +1,15 @@
{
"defaultLang": "en",
"translatorServerName": "azure",
"needLangs": [
"en",
"zh",
"zh-t",
"ja"
],
"brandWords": [],
"unMoveToLocaleDirFiles": [],
"enableStaticRendering": false,
"enableSubPageRedirectToLocale": false,
"disableDefaultLangRedirect": true
}

BIN
bun.lockb

Binary file not shown.

View File

@ -1,4 +1,4 @@
import { NezhaAPISafe } from "@/app/types/nezha-api";
import { NezhaAPISafe } from "../app/[locale]/types/nezha-api";
import ServerUsageBar from "@/components/ServerUsageBar";
import { Card } from "@/components/ui/card";
import {

View File

@ -1,4 +1,4 @@
import { NezhaAPISafe } from "@/app/types/nezha-api";
import { NezhaAPISafe } from "../app/[locale]/types/nezha-api";
import { cn, formatBytes } from "@/lib/utils";
export function ServerCardPopoverCard({

View File

@ -1,6 +1,6 @@
import React from "react";
import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient";
import ServerListClient from "../app/[locale]/(main)/ClientComponents/ServerListClient";
export default async function ServerList() {
return <ServerListClient />;

View File

@ -1,4 +1,4 @@
import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient";
import ServerOverviewClient from "../app/[locale]/(main)/ClientComponents/ServerOverviewClient";
export default async function ServerOverview() {
return <ServerOverviewClient />;

24
i18n-metadata.ts Normal file
View File

@ -0,0 +1,24 @@
// @auto-i18n-check. Please do not delete the line.
export const localeItems = [
{code: 'en', name: 'English'},
{code: 'ja', name: '日本語'},
{code: 'zh-t', name: '中文繁體'},
{code: 'zh', name: '中文简体'},
//{code: 'ar', name: 'العربية'},
//{code: 'de', name: 'Deutsch'},
//{code: 'es', name: 'Español'},
//{code: 'fr', name: 'Français'},
//{code: 'hi', name: 'हिन्दी'},
//{code: 'id', name: 'Bahasa Indonesia'},
//{code: 'it', name: 'Italiano'},
//{code: 'ko', name: '한국어'},
//{code: 'ms', name: 'Bahasa Melayu'},
//{code: 'pt', name: 'Português'},
//{code: 'ru', name: 'Русский'},
//{code: 'th', name: 'ไทย'},
//{code: 'vi', name: 'Tiếng Việt'},
];
export const locales = localeItems.map((item) => item.code);
export const defaultLocale = 'en';

14
i18n.ts Normal file
View File

@ -0,0 +1,14 @@
// @auto-i18n-check. Please do not delete the line.
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {locales} from "./i18n-metadata";
export default getRequestConfig(async ({locale}) => {
// Validate that the incoming `locale` parameter is valid
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`./messages/${locale}.json`)).default
};
});

View File

@ -1,7 +1,7 @@
"use server";
import { NezhaAPI, ServerApi } from "@/app/types/nezha-api";
import { MakeOptional } from "@/app/types/utils";
import { NezhaAPI, ServerApi } from "../app/[locale]/types/nezha-api";
import { MakeOptional } from "../app/[locale]/types/utils";
import { error } from "console";
import { unstable_noStore as noStore } from "next/cache";
import getEnv from "./env-entry";

View File

@ -1,4 +1,4 @@
import { NezhaAPISafe } from "@/app/types/nezha-api";
import { NezhaAPISafe } from "../app/[locale]/types/nezha-api";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

26
messages/en.json Normal file
View File

@ -0,0 +1,26 @@
{
"ServerOverviewClient": {
"p_816-881_Totalservers": "Total servers",
"p_1610-1676_Onlineservers": "Online servers",
"p_2532-2599_Offlineservers": "Offline servers",
"p_3463-3530_Totalbandwidth": "Total bandwidth"
},
"Footer": {
"p_146-598_Findthecodeon": "Find the code on",
"a_303-585_GitHub": "GitHub",
"section_607-869_2020": "© 2020-",
"a_800-850_Hamster1963": "@Hamster1963"
},
"Header": {
"p_1079-1199_Simpleandbeautifuldashbo": "Simple and beautiful dashboard"
},
"Overview": {
"p_2277-2331_Overview": "👋 Overview",
"p_2390-2457_wherethetimeis": "where the time is"
},
"NotFoundPage": {
"h1_490-590_404NotFound": "404 Not Found",
"p_601-665_TARDISERROR": "TARDIS ERROR!",
"Link_676-775_Doctor": "Doctor?"
}
}

26
messages/ja.json Normal file
View File

@ -0,0 +1,26 @@
{
"ServerOverviewClient": {
"p_816-881_Totalservers": "サーバーの総数",
"p_1610-1676_Onlineservers": "オンラインサーバー",
"p_2532-2599_Offlineservers": "オフラインサーバー",
"p_3463-3530_Totalbandwidth": "合計帯域幅"
},
"Footer": {
"p_146-598_Findthecodeon": "でコードを見つけます",
"a_303-585_GitHub": "GitHubの",
"section_607-869_2020": "© 2020年〜",
"a_800-850_Hamster1963": "@Hamster1963"
},
"Header": {
"p_1079-1199_Simpleandbeautifuldashbo": "シンプルで美しいダッシュボード"
},
"Overview": {
"p_2277-2331_Overview": "👋 概要",
"p_2390-2457_wherethetimeis": "時間はどこにあるのか"
},
"NotFoundPage": {
"h1_490-590_404NotFound": "404 見つかりませんでした",
"p_601-665_TARDISERROR": "ターディスエラー!",
"Link_676-775_Doctor": "医者。"
}
}

26
messages/zh-t.json Normal file
View File

@ -0,0 +1,26 @@
{
"ServerOverviewClient": {
"p_816-881_Totalservers": "伺服器總數",
"p_1610-1676_Onlineservers": "在線伺服器",
"p_2532-2599_Offlineservers": "離線伺服器",
"p_3463-3530_Totalbandwidth": "總頻寬"
},
"Footer": {
"p_146-598_Findthecodeon": "查找代碼",
"a_303-585_GitHub": "GitHub的",
"section_607-869_2020": "© 2020 年-",
"a_800-850_Hamster1963": "@Hamster1963"
},
"Header": {
"p_1079-1199_Simpleandbeautifuldashbo": "簡單美觀的儀錶板"
},
"Overview": {
"p_2277-2331_Overview": "👋 概述",
"p_2390-2457_wherethetimeis": "時間在哪裡"
},
"NotFoundPage": {
"h1_490-590_404NotFound": "404 未找到",
"p_601-665_TARDISERROR": "TARDIS 錯誤!",
"Link_676-775_Doctor": "醫生?"
}
}

26
messages/zh.json Normal file
View File

@ -0,0 +1,26 @@
{
"ServerOverviewClient": {
"p_816-881_Totalservers": "服务器总数",
"p_1610-1676_Onlineservers": "在线服务器",
"p_2532-2599_Offlineservers": "离线服务器",
"p_3463-3530_Totalbandwidth": "总带宽"
},
"Footer": {
"p_146-598_Findthecodeon": "查找代码",
"a_303-585_GitHub": "GitHub的",
"section_607-869_2020": "© 2020 年-",
"a_800-850_Hamster1963": "@Hamster1963"
},
"Header": {
"p_1079-1199_Simpleandbeautifuldashbo": "简单美观的仪表板"
},
"Overview": {
"p_2277-2331_Overview": "👋 概述",
"p_2390-2457_wherethetimeis": "时间在哪里"
},
"NotFoundPage": {
"h1_490-590_404NotFound": "404 未找到",
"p_601-665_TARDISERROR": "TARDIS 错误!",
"Link_676-775_Doctor": "医生?"
}
}

21
middleware.ts Normal file
View File

@ -0,0 +1,21 @@
// @auto-i18n-check. Please do not delete the line.
import createMiddleware from 'next-intl/middleware';
import {locales} from "./i18n-metadata";
export default createMiddleware({
// A list of all locales that are supported
locales: locales,
// Used when no locale matches
defaultLocale: 'en',
// 'always': This is the default, The home page will also be redirected to the default language, such as www.abc.com to www.abc.com/en
// 'as-needed': The default page is not redirected. For example, if you open www.abc.com, it is still www.abc.com
localePrefix: 'as-needed',
});
export const config = {
// Match only internationalized pathnames
matcher: ['/', '/(en|zh|zh-t|ja)/:path*']
};

View File

@ -1,5 +1,6 @@
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin();
import withPWAInit from "@ducanh2912/next-pwa";
const withPWA = withPWAInit({
dest: "public",
cacheOnFrontEndNav: true,
@ -7,14 +8,13 @@ const withPWA = withPWAInit({
reloadOnOnline: true,
disable: false,
workboxOptions: {
disableDevLogs: true,
},
disableDevLogs: true
}
});
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
reactStrictMode: true,
// output: "standalone",
reactStrictMode: true
};
export default withPWA(nextConfig);
export default withPWA(withNextIntl(nextConfig));

View File

@ -6,7 +6,9 @@
"dev": "next dev -p 3020",
"start": "node .next/standalone/server.js",
"lint": "next lint",
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/"
"build": "next build && cp -r .next/static .next/standalone/.next/ && cp -r public .next/standalone/",
"build-dev": "next build",
"start-dev": "next start"
},
"dependencies": {
"@ducanh2912/next-pwa": "^10.2.9",
@ -28,6 +30,7 @@
"lucide-react": "^0.414.0",
"luxon": "^3.5.0",
"next": "^14.2.13",
"next-intl": "^3.20.0",
"next-runtime-env": "^3.2.2",
"next-themes": "^0.3.0",
"react": "^18.3.1",