diff --git a/.unipi/docs/specs/2026-05-25-frontend-backend-api-requirements.md b/.unipi/docs/specs/2026-05-25-frontend-backend-api-requirements.md index e1f9c90..b53eb26 100644 --- a/.unipi/docs/specs/2026-05-25-frontend-backend-api-requirements.md +++ b/.unipi/docs/specs/2026-05-25-frontend-backend-api-requirements.md @@ -15,7 +15,7 @@ status: draft - API base:前端通过 `VITE_API_URL` 指向后端;本地可同源 `/api`。 - 上传文件可通过 `/uploads/...` 或完整 URL 返回;前端会用 `assetUrl()` 处理相对路径。 - 所有时间字段使用 ISO 8601 字符串,例如 `2026-05-24T14:42:00.000Z`。 -- 语言字段:`zh` / `en` / `ja` / `ko` / `vi` / `id` / `ms`;默认语言为 `en`。中文只有简体 `zh`,没有繁体中文。 +- 语言字段:`zh-CN` / `en` / `ja` / `ko` / `vi` / `id` / `ms`;默认语言为 `en`。中文只有简体 `zh-CN`,没有繁体中文。 - 错误格式:非 2xx + text/plain 或 JSON 均可;前端会显示错误文本。 - Admin 接口需要 `Authorization: Bearer `。 @@ -57,7 +57,7 @@ type Post = { id: string; categoryId: number; categorySlug: string; - language: "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; + language: "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; text?: string; attachments: Attachment[]; isRecommended: boolean; @@ -449,7 +449,7 @@ Request: ```ts type UpsertPostPayload = { categoryId: number; - language: "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; + language: "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; text?: string; attachments: Attachment[]; isPublic: boolean; diff --git a/.unipi/docs/specs/2026-05-25-posts-api-contract.md b/.unipi/docs/specs/2026-05-25-posts-api-contract.md index 7f084eb..477fc37 100644 --- a/.unipi/docs/specs/2026-05-25-posts-api-contract.md +++ b/.unipi/docs/specs/2026-05-25-posts-api-contract.md @@ -33,7 +33,7 @@ type Post = { id: string; categoryId: number; categorySlug: string; - language: string; // "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms" + language: string; // "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms" text?: string; // 可选,纯文本/图说;前端做 https → 链接自动识别 attachments: Attachment[]; // 0~N;text-only post 时为 [] isRecommended: boolean; @@ -72,7 +72,7 @@ Query 参数: | `lang` | 是 | UI 语言;后端可据此选择不同语言版本的 `text` | | `category` | 否 | category slug;不传 = 全部分类 | | `type` | 否 | `all` / `image` / `video` / `pdf` / `ppt` / `text` / `link` / `archive`;语义见 §3 | -| `language` | 否 | 资源语言:`zh` / `en` / `ja` / `ko` / `vi` / `id` / `ms` | +| `language` | 否 | 资源语言:`zh-CN` / `en` / `ja` / `ko` / `vi` / `id` / `ms` | | `cursor` | 否 | 上一次返回的 `nextCursor`;不传 = 第一页 | | `limit` | 否 | 默认 20,最大 50 | diff --git a/README.md b/README.md index c909612..2eee7a4 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ src/ App.tsx # public app + optional admin routes AppAdminOnly.tsx # admin-only app entry api.ts # fetch helpers and shared API types - i18n.tsx # zh / en / ja / ko / vi / id / ms dictionary + i18n.tsx # zh-CN / en / ja / ko / vi / id / ms dictionary adminPaths.ts # admin UI prefix logic adminRouteTree.tsx # admin routes components/ # reusable public components diff --git a/src/admin/useAdminT.ts b/src/admin/useAdminT.ts index 81904cf..39fb670 100644 --- a/src/admin/useAdminT.ts +++ b/src/admin/useAdminT.ts @@ -3,5 +3,5 @@ import { tLang } from "../i18n"; /** Admin area always uses Chinese, independent of site language. */ export function useAdminT() { - return useCallback((key: string) => tLang("zh", key), []); + return useCallback((key: string) => tLang("zh-CN", key), []); } diff --git a/src/components/messageStream/MessageStream.tsx b/src/components/messageStream/MessageStream.tsx index b473f27..e938473 100644 --- a/src/components/messageStream/MessageStream.tsx +++ b/src/components/messageStream/MessageStream.tsx @@ -26,7 +26,7 @@ export function MessageStream({ scope }: MessageStreamProps) { const { items, isLoading, error, hasMore, loadMore, reset } = usePostStream(params); const groups = useGroupedByDay(items, lang); - const retryLabel = lang === "zh" ? "重试" : "Retry"; + const retryLabel = lang === "zh-CN" ? "重试" : "Retry"; const sentinelRef = useRef(null); const hasMoreRef = useRef(hasMore); diff --git a/src/components/messageStream/hooks/useGroupedByDay.test.ts b/src/components/messageStream/hooks/useGroupedByDay.test.ts index 487c1c4..6c33ee5 100644 --- a/src/components/messageStream/hooks/useGroupedByDay.test.ts +++ b/src/components/messageStream/hooks/useGroupedByDay.test.ts @@ -8,7 +8,7 @@ function makePost(id: string, isoDate: string): Post { id, categoryId: 1, categorySlug: "x", - language: "zh", + language: "zh-CN", attachments: [], isRecommended: false, publishedAt: isoDate, @@ -25,7 +25,7 @@ describe("useGroupedByDay", () => { makePost("c", "2026-02-28T01:00:00.000Z"), makePost("d", "2026-05-16T12:00:00.000Z"), ]; - const { result } = renderHook(() => useGroupedByDay(posts, "zh")); + const { result } = renderHook(() => useGroupedByDay(posts, "zh-CN")); expect(result.current.length).toBeGreaterThanOrEqual(2); const allIds = result.current.flatMap((g) => g.items.map((p) => p.id)); expect(allIds).toEqual(["a", "b", "c", "d"]); @@ -47,7 +47,7 @@ describe("useGroupedByDay", () => { }); it("returns empty array for empty input", () => { - const { result } = renderHook(() => useGroupedByDay([], "zh")); + const { result } = renderHook(() => useGroupedByDay([], "zh-CN")); expect(result.current).toEqual([]); }); }); diff --git a/src/components/messageStream/hooks/useGroupedByDay.ts b/src/components/messageStream/hooks/useGroupedByDay.ts index 2837a99..02fb618 100644 --- a/src/components/messageStream/hooks/useGroupedByDay.ts +++ b/src/components/messageStream/hooks/useGroupedByDay.ts @@ -35,10 +35,10 @@ function dayLabel(iso: string, lang: string): string { a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); if (isSameDay(d, today)) { - return lang === "zh" ? "今天" : "Today"; + return lang === "zh-CN" ? "今天" : "Today"; } if (isSameDay(d, yesterday)) { - return lang === "zh" ? "昨天" : "Yesterday"; + return lang === "zh-CN" ? "昨天" : "Yesterday"; } return new Intl.DateTimeFormat(localeFor(lang), { month: "long", diff --git a/src/i18n.tsx b/src/i18n.tsx index 9fb7869..7a00e17 100644 --- a/src/i18n.tsx +++ b/src/i18n.tsx @@ -6,7 +6,7 @@ import React, { useState, } from "react"; -export type Lang = "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; +export type Lang = "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; type Dict = Record; @@ -82,7 +82,7 @@ const zhDict: Dict = { walletMissingProjectId: "请配置 VITE_WALLETCONNECT_PROJECT_ID(Reown Cloud),否则无法使用 WalletConnect/扫码。", walletSetupNeeded: "钱包扫码未启用(请在服务器配置环境变量)", - lang_zh: "中文", + lang_zh_CN: "中文", lang_en: "English", lang_ja: "日本語", lang_ko: "한국어", @@ -208,7 +208,7 @@ const enDict: Dict = { walletMissingProjectId: "Set VITE_WALLETCONNECT_PROJECT_ID (free on Reown Cloud). Required for WalletConnect / QR login.", walletSetupNeeded: "Wallet QR login disabled (set env on server)", - lang_zh: "Chinese", + lang_zh_CN: "Chinese", lang_en: "English", lang_ja: "Japanese", lang_ko: "Korean", @@ -262,8 +262,8 @@ const enDict: Dict = { }; const languageNames: Record = { - zh: { - lang_zh: "中文", + "zh-CN": { + lang_zh_CN: "中文", lang_en: "English", lang_ja: "日本語", lang_ko: "한국어", @@ -272,7 +272,7 @@ const languageNames: Record = { lang_ms: "Bahasa Melayu", }, en: { - lang_zh: "Chinese", + lang_zh_CN: "Chinese", lang_en: "English", lang_ja: "Japanese", lang_ko: "Korean", @@ -281,7 +281,7 @@ const languageNames: Record = { lang_ms: "Malay", }, ja: { - lang_zh: "中国語", + lang_zh_CN: "中国語", lang_en: "英語", lang_ja: "日本語", lang_ko: "韓国語", @@ -290,7 +290,7 @@ const languageNames: Record = { lang_ms: "マレー語", }, ko: { - lang_zh: "중국어", + lang_zh_CN: "중국어", lang_en: "영어", lang_ja: "일본어", lang_ko: "한국어", @@ -299,7 +299,7 @@ const languageNames: Record = { lang_ms: "말레이어", }, vi: { - lang_zh: "Tiếng Trung", + lang_zh_CN: "Tiếng Trung", lang_en: "Tiếng Anh", lang_ja: "Tiếng Nhật", lang_ko: "Tiếng Hàn", @@ -308,7 +308,7 @@ const languageNames: Record = { lang_ms: "Tiếng Mã Lai", }, id: { - lang_zh: "Bahasa Tionghoa", + lang_zh_CN: "Bahasa Tionghoa", lang_en: "Bahasa Inggris", lang_ja: "Bahasa Jepang", lang_ko: "Bahasa Korea", @@ -317,7 +317,7 @@ const languageNames: Record = { lang_ms: "Bahasa Melayu", }, ms: { - lang_zh: "Bahasa Cina", + lang_zh_CN: "Bahasa Cina", lang_en: "Bahasa Inggeris", lang_ja: "Bahasa Jepun", lang_ko: "Bahasa Korea", @@ -328,7 +328,7 @@ const languageNames: Record = { }; const dict: Record = { - zh: { ...zhDict, ...languageNames.zh }, + "zh-CN": { ...zhDict, ...languageNames["zh-CN"] }, en: { ...enDict, ...languageNames.en }, ja: { ...enDict, ...languageNames.ja }, ko: { ...enDict, ...languageNames.ko }, @@ -351,9 +351,9 @@ const LANG_KEY = "ark_lang"; export function I18nProvider({ children }: { children: React.ReactNode }) { const [lang, setLangState] = useState(() => { const s = localStorage.getItem(LANG_KEY); - if (s === "zh-CN" || s === "zh-TW") return "zh"; + if (s === "zh" || s === "zh-TW") return "zh-CN"; if ( - s === "zh" || + s === "zh-CN" || s === "en" || s === "ja" || s === "ko" || @@ -383,5 +383,5 @@ export function useI18n() { } export function langQuery(lang: Lang) { - return lang === "zh" ? "zh-CN" : lang; + return lang; } diff --git a/src/i18nLanguages.ts b/src/i18nLanguages.ts index 261ad1d..c5997d3 100644 --- a/src/i18nLanguages.ts +++ b/src/i18nLanguages.ts @@ -1,7 +1,7 @@ import type { Lang } from "./i18n"; export const LANG_OPTIONS: { code: Lang; label: string }[] = [ - { code: "zh", label: "中文" }, + { code: "zh-CN", label: "中文" }, { code: "en", label: "English" }, { code: "ja", label: "日本語" }, { code: "ko", label: "한국어" }, @@ -12,6 +12,7 @@ export const LANG_OPTIONS: { code: Lang; label: string }[] = [ export function languageLabel(t: (key: string) => string, code: string) { if (!code) return t("filterLanguageAll"); - const label = t(`lang_${code}`); - return label === `lang_${code}` ? code : label; + const key = `lang_${code.replace("-", "_")}`; + const label = t(key); + return label === key ? code : label; } diff --git a/src/mocks/mockPosts.ts b/src/mocks/mockPosts.ts index 7193286..8e86149 100644 --- a/src/mocks/mockPosts.ts +++ b/src/mocks/mockPosts.ts @@ -31,7 +31,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-001", categoryId: 1, categorySlug: "project", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-001", @@ -55,7 +55,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-002", categoryId: 1, categorySlug: "project", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-002", @@ -79,7 +79,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-013", categoryId: 1, categorySlug: "project", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-013", @@ -100,7 +100,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-003", categoryId: 2, categorySlug: "guide", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-003", @@ -121,7 +121,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-004", categoryId: 2, categorySlug: "guide", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-004", @@ -142,7 +142,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-005", categoryId: 3, categorySlug: "data", - language: "zh", + language: "zh-CN", text: "📊 ARK DeFAI 各大平台现已上线 🔥\n\n" + "🔷 市场数据平台\n" + @@ -163,7 +163,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-006", categoryId: 3, categorySlug: "data", - language: "zh", + language: "zh-CN", text: "📌 收取协议固定 2.5% 手续费。\n\n" + "🔷 贡献值合约\n0x7736b5B84cADDB7661D250D10e60E31F3C905c99\n📌 用于新贡献值机制的 USDT 购买与资金流向管理(通缩销毁 / 储备 RBS)", @@ -178,7 +178,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-007", categoryId: 4, categorySlug: "videos", - language: "zh", + language: "zh-CN", text: "ARK 山东·东营社区 招商复盘·势位重塑\n🔥 ARK DeFai 相位偏移锁死增值弧度。质能裂变诱发认知风暴,海岱动能正于中原合围!🚀", attachments: [ { @@ -204,7 +204,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-008", categoryId: 5, categorySlug: "poster", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-008", @@ -228,7 +228,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-009", categoryId: 5, categorySlug: "poster", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-009", @@ -252,7 +252,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-010", categoryId: 6, categorySlug: "meeting", - language: "zh", + language: "zh-CN", text: "📌 ARK DeFAI 方舟晨间时刻\n\n🧠 会议主题:市场概况交流 & 市场问题讨论。\n🕙 会议时间:3月1日(日)10:00\n🎬 直播腾讯会议链接:https://meeting.tencent.com/l/G718S4Sedm38", attachments: [ { @@ -277,7 +277,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-011", categoryId: 5, categorySlug: "poster", - language: "zh", + language: "zh-CN", attachments: [ { id: "a-011a", @@ -323,7 +323,7 @@ export const MOCK_POSTS: Post[] = [ id: "p-012", categoryId: 5, categorySlug: "poster", - language: "zh", + language: "zh-CN", attachments: Array.from({ length: 7 }).map((_, i) => ({ id: `a-012-${i}`, kind: "image" as const, diff --git a/src/pages/admin/AdminResourceForm.tsx b/src/pages/admin/AdminResourceForm.tsx index 2993a12..735b538 100644 --- a/src/pages/admin/AdminResourceForm.tsx +++ b/src/pages/admin/AdminResourceForm.tsx @@ -37,7 +37,7 @@ export function AdminResourceForm() { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [rtype, setRtype] = useState("image"); - const [language, setLanguage] = useState("zh"); + const [language, setLanguage] = useState("zh-CN"); const [categoryId, setCategoryId] = useState(1); const [coverImage, setCoverImage] = useState(""); const [fileUrl, setFileUrl] = useState(""); @@ -66,7 +66,7 @@ export function AdminResourceForm() { setTitle(r.title || ""); setDescription(r.description || ""); setRtype(r.type || "image"); - setLanguage(r.language || "zh"); + setLanguage(r.language || "zh-CN"); setCategoryId(r.categoryId || 1); setCoverImage(r.coverImage || ""); setFileUrl(r.fileUrl || ""); @@ -183,7 +183,7 @@ export function AdminResourceForm() { value={language} onChange={(e) => setLanguage(e.target.value)} > - + diff --git a/src/resourceTypeLabels.test.ts b/src/resourceTypeLabels.test.ts index 54b4e6f..4188551 100644 --- a/src/resourceTypeLabels.test.ts +++ b/src/resourceTypeLabels.test.ts @@ -12,7 +12,7 @@ const t = (key: string) => type_image: "图片", type_video: "视频", type_music: "音乐", - lang_zh: "中文", + lang_zh_CN: "中文", lang_en: "English", lang_ja: "日本語", })[key] ?? key; diff --git a/src/resourceTypeLabels.ts b/src/resourceTypeLabels.ts index 348cf75..70a4786 100644 --- a/src/resourceTypeLabels.ts +++ b/src/resourceTypeLabels.ts @@ -34,8 +34,10 @@ export function resourceLanguageLabel( ): string { const lc = langCode.trim().toLowerCase(); const normalized = - lc === "zh-cn" || lc === "zh-tw" || lc === "zh-hans" ? "zh" : lc; - const key = `lang_${normalized}`; + lc === "zh" || lc === "zh-cn" || lc === "zh-tw" || lc === "zh-hans" + ? "zh-CN" + : lc; + const key = `lang_${normalized.replace("-", "_")}`; const label = t(key); return label !== key ? label : langCode.trim(); }