terry-staging #1

Merged
terry merged 8 commits from terry-staging into main 2026-05-26 06:53:11 +00:00
13 changed files with 53 additions and 50 deletions
Showing only changes of commit f482a2ec38 - Show all commits

View File

@@ -15,7 +15,7 @@ status: draft
- API base前端通过 `VITE_API_URL` 指向后端;本地可同源 `/api` - API base前端通过 `VITE_API_URL` 指向后端;本地可同源 `/api`
- 上传文件可通过 `/uploads/...` 或完整 URL 返回;前端会用 `assetUrl()` 处理相对路径。 - 上传文件可通过 `/uploads/...` 或完整 URL 返回;前端会用 `assetUrl()` 处理相对路径。
- 所有时间字段使用 ISO 8601 字符串,例如 `2026-05-24T14:42:00.000Z` - 所有时间字段使用 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 均可;前端会显示错误文本。 - 错误格式:非 2xx + text/plain 或 JSON 均可;前端会显示错误文本。
- Admin 接口需要 `Authorization: Bearer <token>` - Admin 接口需要 `Authorization: Bearer <token>`
@@ -57,7 +57,7 @@ type Post = {
id: string; id: string;
categoryId: number; categoryId: number;
categorySlug: string; categorySlug: string;
language: "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; language: "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
text?: string; text?: string;
attachments: Attachment[]; attachments: Attachment[];
isRecommended: boolean; isRecommended: boolean;
@@ -449,7 +449,7 @@ Request:
```ts ```ts
type UpsertPostPayload = { type UpsertPostPayload = {
categoryId: number; categoryId: number;
language: "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms"; language: "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms";
text?: string; text?: string;
attachments: Attachment[]; attachments: Attachment[];
isPublic: boolean; isPublic: boolean;

View File

@@ -33,7 +33,7 @@ type Post = {
id: string; id: string;
categoryId: number; categoryId: number;
categorySlug: string; categorySlug: string;
language: string; // "zh" | "en" | "ja" | "ko" | "vi" | "id" | "ms" language: string; // "zh-CN" | "en" | "ja" | "ko" | "vi" | "id" | "ms"
text?: string; // 可选,纯文本/图说;前端做 https → 链接自动识别 text?: string; // 可选,纯文本/图说;前端做 https → 链接自动识别
attachments: Attachment[]; // 0~Ntext-only post 时为 [] attachments: Attachment[]; // 0~Ntext-only post 时为 []
isRecommended: boolean; isRecommended: boolean;
@@ -72,7 +72,7 @@ Query 参数:
| `lang` | 是 | UI 语言;后端可据此选择不同语言版本的 `text` | | `lang` | 是 | UI 语言;后端可据此选择不同语言版本的 `text` |
| `category` | 否 | category slug不传 = 全部分类 | | `category` | 否 | category slug不传 = 全部分类 |
| `type` | 否 | `all` / `image` / `video` / `pdf` / `ppt` / `text` / `link` / `archive`;语义见 §3 | | `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`;不传 = 第一页 | | `cursor` | 否 | 上一次返回的 `nextCursor`;不传 = 第一页 |
| `limit` | 否 | 默认 20最大 50 | | `limit` | 否 | 默认 20最大 50 |

View File

@@ -68,7 +68,7 @@ src/
App.tsx # public app + optional admin routes App.tsx # public app + optional admin routes
AppAdminOnly.tsx # admin-only app entry AppAdminOnly.tsx # admin-only app entry
api.ts # fetch helpers and shared API types 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 adminPaths.ts # admin UI prefix logic
adminRouteTree.tsx # admin routes adminRouteTree.tsx # admin routes
components/ # reusable public components components/ # reusable public components

View File

@@ -3,5 +3,5 @@ import { tLang } from "../i18n";
/** Admin area always uses Chinese, independent of site language. */ /** Admin area always uses Chinese, independent of site language. */
export function useAdminT() { export function useAdminT() {
return useCallback((key: string) => tLang("zh", key), []); return useCallback((key: string) => tLang("zh-CN", key), []);
} }

View File

@@ -26,7 +26,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
const { items, isLoading, error, hasMore, loadMore, reset } = const { items, isLoading, error, hasMore, loadMore, reset } =
usePostStream(params); usePostStream(params);
const groups = useGroupedByDay(items, lang); const groups = useGroupedByDay(items, lang);
const retryLabel = lang === "zh" ? "重试" : "Retry"; const retryLabel = lang === "zh-CN" ? "重试" : "Retry";
const sentinelRef = useRef<HTMLDivElement>(null); const sentinelRef = useRef<HTMLDivElement>(null);
const hasMoreRef = useRef(hasMore); const hasMoreRef = useRef(hasMore);

View File

@@ -8,7 +8,7 @@ function makePost(id: string, isoDate: string): Post {
id, id,
categoryId: 1, categoryId: 1,
categorySlug: "x", categorySlug: "x",
language: "zh", language: "zh-CN",
attachments: [], attachments: [],
isRecommended: false, isRecommended: false,
publishedAt: isoDate, publishedAt: isoDate,
@@ -25,7 +25,7 @@ describe("useGroupedByDay", () => {
makePost("c", "2026-02-28T01:00:00.000Z"), makePost("c", "2026-02-28T01:00:00.000Z"),
makePost("d", "2026-05-16T12: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); expect(result.current.length).toBeGreaterThanOrEqual(2);
const allIds = result.current.flatMap((g) => g.items.map((p) => p.id)); const allIds = result.current.flatMap((g) => g.items.map((p) => p.id));
expect(allIds).toEqual(["a", "b", "c", "d"]); expect(allIds).toEqual(["a", "b", "c", "d"]);
@@ -47,7 +47,7 @@ describe("useGroupedByDay", () => {
}); });
it("returns empty array for empty input", () => { it("returns empty array for empty input", () => {
const { result } = renderHook(() => useGroupedByDay([], "zh")); const { result } = renderHook(() => useGroupedByDay([], "zh-CN"));
expect(result.current).toEqual([]); expect(result.current).toEqual([]);
}); });
}); });

View File

@@ -35,10 +35,10 @@ function dayLabel(iso: string, lang: string): string {
a.getMonth() === b.getMonth() && a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate(); a.getDate() === b.getDate();
if (isSameDay(d, today)) { if (isSameDay(d, today)) {
return lang === "zh" ? "今天" : "Today"; return lang === "zh-CN" ? "今天" : "Today";
} }
if (isSameDay(d, yesterday)) { if (isSameDay(d, yesterday)) {
return lang === "zh" ? "昨天" : "Yesterday"; return lang === "zh-CN" ? "昨天" : "Yesterday";
} }
return new Intl.DateTimeFormat(localeFor(lang), { return new Intl.DateTimeFormat(localeFor(lang), {
month: "long", month: "long",

View File

@@ -6,7 +6,7 @@ import React, {
useState, useState,
} from "react"; } 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<string, string>; type Dict = Record<string, string>;
@@ -82,7 +82,7 @@ const zhDict: Dict = {
walletMissingProjectId: walletMissingProjectId:
"请配置 VITE_WALLETCONNECT_PROJECT_IDReown Cloud否则无法使用 WalletConnect/扫码。", "请配置 VITE_WALLETCONNECT_PROJECT_IDReown Cloud否则无法使用 WalletConnect/扫码。",
walletSetupNeeded: "钱包扫码未启用(请在服务器配置环境变量)", walletSetupNeeded: "钱包扫码未启用(请在服务器配置环境变量)",
lang_zh: "中文", lang_zh_CN: "中文",
lang_en: "English", lang_en: "English",
lang_ja: "日本語", lang_ja: "日本語",
lang_ko: "한국어", lang_ko: "한국어",
@@ -208,7 +208,7 @@ const enDict: Dict = {
walletMissingProjectId: walletMissingProjectId:
"Set VITE_WALLETCONNECT_PROJECT_ID (free on Reown Cloud). Required for WalletConnect / QR login.", "Set VITE_WALLETCONNECT_PROJECT_ID (free on Reown Cloud). Required for WalletConnect / QR login.",
walletSetupNeeded: "Wallet QR login disabled (set env on server)", walletSetupNeeded: "Wallet QR login disabled (set env on server)",
lang_zh: "Chinese", lang_zh_CN: "Chinese",
lang_en: "English", lang_en: "English",
lang_ja: "Japanese", lang_ja: "Japanese",
lang_ko: "Korean", lang_ko: "Korean",
@@ -262,8 +262,8 @@ const enDict: Dict = {
}; };
const languageNames: Record<Lang, Dict> = { const languageNames: Record<Lang, Dict> = {
zh: { "zh-CN": {
lang_zh: "中文", lang_zh_CN: "中文",
lang_en: "English", lang_en: "English",
lang_ja: "日本語", lang_ja: "日本語",
lang_ko: "한국어", lang_ko: "한국어",
@@ -272,7 +272,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Bahasa Melayu", lang_ms: "Bahasa Melayu",
}, },
en: { en: {
lang_zh: "Chinese", lang_zh_CN: "Chinese",
lang_en: "English", lang_en: "English",
lang_ja: "Japanese", lang_ja: "Japanese",
lang_ko: "Korean", lang_ko: "Korean",
@@ -281,7 +281,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Malay", lang_ms: "Malay",
}, },
ja: { ja: {
lang_zh: "中国語", lang_zh_CN: "中国語",
lang_en: "英語", lang_en: "英語",
lang_ja: "日本語", lang_ja: "日本語",
lang_ko: "韓国語", lang_ko: "韓国語",
@@ -290,7 +290,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "マレー語", lang_ms: "マレー語",
}, },
ko: { ko: {
lang_zh: "중국어", lang_zh_CN: "중국어",
lang_en: "영어", lang_en: "영어",
lang_ja: "일본어", lang_ja: "일본어",
lang_ko: "한국어", lang_ko: "한국어",
@@ -299,7 +299,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "말레이어", lang_ms: "말레이어",
}, },
vi: { vi: {
lang_zh: "Tiếng Trung", lang_zh_CN: "Tiếng Trung",
lang_en: "Tiếng Anh", lang_en: "Tiếng Anh",
lang_ja: "Tiếng Nhật", lang_ja: "Tiếng Nhật",
lang_ko: "Tiếng Hàn", lang_ko: "Tiếng Hàn",
@@ -308,7 +308,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Tiếng Mã Lai", lang_ms: "Tiếng Mã Lai",
}, },
id: { id: {
lang_zh: "Bahasa Tionghoa", lang_zh_CN: "Bahasa Tionghoa",
lang_en: "Bahasa Inggris", lang_en: "Bahasa Inggris",
lang_ja: "Bahasa Jepang", lang_ja: "Bahasa Jepang",
lang_ko: "Bahasa Korea", lang_ko: "Bahasa Korea",
@@ -317,7 +317,7 @@ const languageNames: Record<Lang, Dict> = {
lang_ms: "Bahasa Melayu", lang_ms: "Bahasa Melayu",
}, },
ms: { ms: {
lang_zh: "Bahasa Cina", lang_zh_CN: "Bahasa Cina",
lang_en: "Bahasa Inggeris", lang_en: "Bahasa Inggeris",
lang_ja: "Bahasa Jepun", lang_ja: "Bahasa Jepun",
lang_ko: "Bahasa Korea", lang_ko: "Bahasa Korea",
@@ -328,7 +328,7 @@ const languageNames: Record<Lang, Dict> = {
}; };
const dict: Record<Lang, Dict> = { const dict: Record<Lang, Dict> = {
zh: { ...zhDict, ...languageNames.zh }, "zh-CN": { ...zhDict, ...languageNames["zh-CN"] },
en: { ...enDict, ...languageNames.en }, en: { ...enDict, ...languageNames.en },
ja: { ...enDict, ...languageNames.ja }, ja: { ...enDict, ...languageNames.ja },
ko: { ...enDict, ...languageNames.ko }, ko: { ...enDict, ...languageNames.ko },
@@ -351,9 +351,9 @@ const LANG_KEY = "ark_lang";
export function I18nProvider({ children }: { children: React.ReactNode }) { export function I18nProvider({ children }: { children: React.ReactNode }) {
const [lang, setLangState] = useState<Lang>(() => { const [lang, setLangState] = useState<Lang>(() => {
const s = localStorage.getItem(LANG_KEY); 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 ( if (
s === "zh" || s === "zh-CN" ||
s === "en" || s === "en" ||
s === "ja" || s === "ja" ||
s === "ko" || s === "ko" ||
@@ -383,5 +383,5 @@ export function useI18n() {
} }
export function langQuery(lang: Lang) { export function langQuery(lang: Lang) {
return lang === "zh" ? "zh-CN" : lang; return lang;
} }

View File

@@ -1,7 +1,7 @@
import type { Lang } from "./i18n"; import type { Lang } from "./i18n";
export const LANG_OPTIONS: { code: Lang; label: string }[] = [ export const LANG_OPTIONS: { code: Lang; label: string }[] = [
{ code: "zh", label: "中文" }, { code: "zh-CN", label: "中文" },
{ code: "en", label: "English" }, { code: "en", label: "English" },
{ code: "ja", label: "日本語" }, { code: "ja", label: "日本語" },
{ code: "ko", 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) { export function languageLabel(t: (key: string) => string, code: string) {
if (!code) return t("filterLanguageAll"); if (!code) return t("filterLanguageAll");
const label = t(`lang_${code}`); const key = `lang_${code.replace("-", "_")}`;
return label === `lang_${code}` ? code : label; const label = t(key);
return label === key ? code : label;
} }

View File

@@ -31,7 +31,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-001", id: "p-001",
categoryId: 1, categoryId: 1,
categorySlug: "project", categorySlug: "project",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-001", id: "a-001",
@@ -55,7 +55,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-002", id: "p-002",
categoryId: 1, categoryId: 1,
categorySlug: "project", categorySlug: "project",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-002", id: "a-002",
@@ -79,7 +79,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-013", id: "p-013",
categoryId: 1, categoryId: 1,
categorySlug: "project", categorySlug: "project",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-013", id: "a-013",
@@ -100,7 +100,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-003", id: "p-003",
categoryId: 2, categoryId: 2,
categorySlug: "guide", categorySlug: "guide",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-003", id: "a-003",
@@ -121,7 +121,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-004", id: "p-004",
categoryId: 2, categoryId: 2,
categorySlug: "guide", categorySlug: "guide",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-004", id: "a-004",
@@ -142,7 +142,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-005", id: "p-005",
categoryId: 3, categoryId: 3,
categorySlug: "data", categorySlug: "data",
language: "zh", language: "zh-CN",
text: text:
"📊 ARK DeFAI 各大平台现已上线 🔥\n\n" + "📊 ARK DeFAI 各大平台现已上线 🔥\n\n" +
"🔷 市场数据平台\n" + "🔷 市场数据平台\n" +
@@ -163,7 +163,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-006", id: "p-006",
categoryId: 3, categoryId: 3,
categorySlug: "data", categorySlug: "data",
language: "zh", language: "zh-CN",
text: text:
"📌 收取协议固定 2.5% 手续费。\n\n" + "📌 收取协议固定 2.5% 手续费。\n\n" +
"🔷 贡献值合约\n0x7736b5B84cADDB7661D250D10e60E31F3C905c99\n📌 用于新贡献值机制的 USDT 购买与资金流向管理(通缩销毁 / 储备 RBS", "🔷 贡献值合约\n0x7736b5B84cADDB7661D250D10e60E31F3C905c99\n📌 用于新贡献值机制的 USDT 购买与资金流向管理(通缩销毁 / 储备 RBS",
@@ -178,7 +178,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-007", id: "p-007",
categoryId: 4, categoryId: 4,
categorySlug: "videos", categorySlug: "videos",
language: "zh", language: "zh-CN",
text: "ARK 山东·东营社区 招商复盘·势位重塑\n🔥 ARK DeFai 相位偏移锁死增值弧度。质能裂变诱发认知风暴,海岱动能正于中原合围!🚀", text: "ARK 山东·东营社区 招商复盘·势位重塑\n🔥 ARK DeFai 相位偏移锁死增值弧度。质能裂变诱发认知风暴,海岱动能正于中原合围!🚀",
attachments: [ attachments: [
{ {
@@ -204,7 +204,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-008", id: "p-008",
categoryId: 5, categoryId: 5,
categorySlug: "poster", categorySlug: "poster",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-008", id: "a-008",
@@ -228,7 +228,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-009", id: "p-009",
categoryId: 5, categoryId: 5,
categorySlug: "poster", categorySlug: "poster",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-009", id: "a-009",
@@ -252,7 +252,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-010", id: "p-010",
categoryId: 6, categoryId: 6,
categorySlug: "meeting", categorySlug: "meeting",
language: "zh", language: "zh-CN",
text: "📌 ARK DeFAI 方舟晨间时刻\n\n🧠 会议主题:市场概况交流 & 市场问题讨论。\n🕙 会议时间3月1日10:00\n🎬 直播腾讯会议链接https://meeting.tencent.com/l/G718S4Sedm38", text: "📌 ARK DeFAI 方舟晨间时刻\n\n🧠 会议主题:市场概况交流 & 市场问题讨论。\n🕙 会议时间3月1日10:00\n🎬 直播腾讯会议链接https://meeting.tencent.com/l/G718S4Sedm38",
attachments: [ attachments: [
{ {
@@ -277,7 +277,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-011", id: "p-011",
categoryId: 5, categoryId: 5,
categorySlug: "poster", categorySlug: "poster",
language: "zh", language: "zh-CN",
attachments: [ attachments: [
{ {
id: "a-011a", id: "a-011a",
@@ -323,7 +323,7 @@ export const MOCK_POSTS: Post[] = [
id: "p-012", id: "p-012",
categoryId: 5, categoryId: 5,
categorySlug: "poster", categorySlug: "poster",
language: "zh", language: "zh-CN",
attachments: Array.from({ length: 7 }).map((_, i) => ({ attachments: Array.from({ length: 7 }).map((_, i) => ({
id: `a-012-${i}`, id: `a-012-${i}`,
kind: "image" as const, kind: "image" as const,

View File

@@ -37,7 +37,7 @@ export function AdminResourceForm() {
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [rtype, setRtype] = useState<string>("image"); const [rtype, setRtype] = useState<string>("image");
const [language, setLanguage] = useState("zh"); const [language, setLanguage] = useState("zh-CN");
const [categoryId, setCategoryId] = useState(1); const [categoryId, setCategoryId] = useState(1);
const [coverImage, setCoverImage] = useState(""); const [coverImage, setCoverImage] = useState("");
const [fileUrl, setFileUrl] = useState(""); const [fileUrl, setFileUrl] = useState("");
@@ -66,7 +66,7 @@ export function AdminResourceForm() {
setTitle(r.title || ""); setTitle(r.title || "");
setDescription(r.description || ""); setDescription(r.description || "");
setRtype(r.type || "image"); setRtype(r.type || "image");
setLanguage(r.language || "zh"); setLanguage(r.language || "zh-CN");
setCategoryId(r.categoryId || 1); setCategoryId(r.categoryId || 1);
setCoverImage(r.coverImage || ""); setCoverImage(r.coverImage || "");
setFileUrl(r.fileUrl || ""); setFileUrl(r.fileUrl || "");
@@ -183,7 +183,7 @@ export function AdminResourceForm() {
value={language} value={language}
onChange={(e) => setLanguage(e.target.value)} onChange={(e) => setLanguage(e.target.value)}
> >
<option value="zh">{t("lang_zh")}</option> <option value="zh-CN">{t("lang_zh_CN")}</option>
<option value="en">{t("lang_en")}</option> <option value="en">{t("lang_en")}</option>
<option value="ja">{t("lang_ja")}</option> <option value="ja">{t("lang_ja")}</option>
<option value="ko">{t("lang_ko")}</option> <option value="ko">{t("lang_ko")}</option>

View File

@@ -12,7 +12,7 @@ const t = (key: string) =>
type_image: "图片", type_image: "图片",
type_video: "视频", type_video: "视频",
type_music: "音乐", type_music: "音乐",
lang_zh: "中文", lang_zh_CN: "中文",
lang_en: "English", lang_en: "English",
lang_ja: "日本語", lang_ja: "日本語",
})[key] ?? key; })[key] ?? key;

View File

@@ -34,8 +34,10 @@ export function resourceLanguageLabel(
): string { ): string {
const lc = langCode.trim().toLowerCase(); const lc = langCode.trim().toLowerCase();
const normalized = const normalized =
lc === "zh-cn" || lc === "zh-tw" || lc === "zh-hans" ? "zh" : lc; lc === "zh" || lc === "zh-cn" || lc === "zh-tw" || lc === "zh-hans"
const key = `lang_${normalized}`; ? "zh-CN"
: lc;
const key = `lang_${normalized.replace("-", "_")}`;
const label = t(key); const label = t(key);
return label !== key ? label : langCode.trim(); return label !== key ? label : langCode.trim();
} }