Skip to content

🌐 feat: Add Simplified Chinese (zh-Hans) localization + language setting - #148

Open
SwakinX wants to merge 68 commits into
LibreChat-AI:mainfrom
SwakinX:feat/zh-hans-localization
Open

SwakinX wants to merge 68 commits into
LibreChat-AI:mainfrom
SwakinX:feat/zh-hans-localization

Conversation

@SwakinX

@SwakinX SwakinX commented Sep 24, 2026

Copy link
Copy Markdown

Summary

  • Add a complete Simplified Chinese (zh-Hans) locale covering the full English key set,
    with terminology aligned to the LibreChat main zh-Hans translation (shared com_*
    strings reuse the official wording so the panel and chat UI read consistently)
  • Add a language setting to the settings dialog — a Select dropdown (English /
    中文(简体)) below the theme row, persisting the choice via the i18nextLng
    cache key and keeping document.documentElement.lang in sync
  • Localize two dropdowns that rendered raw enum values (create-user role select
    showed USER/ADMIN; audit-log target-type filter showed raw principal types)

Locale detection

Detection is adapted from the LibreChat client implementation
(client/src/locales/i18n.ts): explicit stored choice wins, then navigator language,
normalized through an alias table (zh, zh-CN, zh-SG → zh-Hans). This replaces
i18next-browser-languagedetector, whose nonExplicitSupportedLngs handling strips
zh-Hans to zh before the whitelist check — meaning a registered zh-Hans locale
could never actually be resolved.

SSR renders the pre-hydration loading shell in the default locale deterministically,
and the shell text suppresses the expected hydration mismatch (<html lang> already
had suppressHydrationWarning).

Tests

  • New src/locales/locales.test.ts guards en/zh key-set parity, interpolation
    placeholder parity, language switching (incl. zh-CN/zh resolution), and
    locale normalization
  • Full suite passes (bun run test, 831 tests)

Notes for reviewers

Three commits, intentionally split for reviewability:

  1. 🌐 feat: Add Simplified Chinese (zh-Hans) locale
  2. 🌐 feat: Add language setting to settings dialog
  3. 🐛 fix: Localize role and target type dropdown options

Happy to adjust the translation wording or split/merge commits as preferred.

- Add src/locales/zh-Hans/translation.json covering the full en key set
  (terminology aligned with the LibreChat main zh-Hans translation)
- Register zh-Hans in i18n init with LibreChat-style locale normalization
  and detection (stored choice > navigator, alias mapping zh/zh-CN/zh-SG),
  replacing the browser-languagedetector whose nonExplicitSupportedLngs
  handling rejected zh-Hans
- Render the SSR loading shell in the default locale deterministically and
  suppress the expected hydration text mismatch
- Guard en/zh key-set and placeholder parity plus language switching in
  locales.test.ts
- Add a language row with a Select dropdown (English / 中文(简体)) below
  the theme row, reusing the existing click-ui Select component
- Persist the choice via i18nextLng (the detection cache key) and keep
  document.documentElement.lang in sync
- CreateUserDialog role select rendered raw SystemRoles values
  (USER/ADMIN); use localized labels instead
- Audit log target-type filter rendered raw PrincipalType enum values;
  map them to localized labels
New /tools route lists the full Tool Gateway catalog (disabled
included) via the LibreChat proxy /api/terravox/tools/all: tool id,
version, exposure fronts, allowed groups, renderer and enable state,
with search and zh-Hans/en localization.
- Add GiteaImportDialog: owner/repo dropdowns from the anonymous repo
  listing, check report (error blocks, warn allows continuing), prefill
  into the edit dialog; manual-create secondary path
- ToolEditDialog: desktop execution now registers owner/repo
  distribution (tool.json fields as optional fallback), prefill support
- Desktop manifest assembly uses the distribution form (catalog_tool_id
  contract change); owner+repo required, sha256 format validated
- Tool create/update toasts; com_tools_gitea_* / dist field locales
Misclicks on the overlay discarded filled-in forms. All admin-panel
dialogs now ignore pointer-downs outside the content; explicit close,
cancel buttons and Escape still close.
Pending updates governance (contracts 2.8.0): repo releases ahead of the
approved manifest are hidden from users until an admin confirms them.

- getPendingUpdatesFn + pendingUpdatesQueryOptions (60s poll, aligned
  with the gateway's resolver cache TTL)
- ToolCatalogTab: amber pending section above the table; Review & confirm
  runs the existing Gitea check endpoint, prefills the edit dialog via
  buildPrefill with repo content fields while governance fields
  (tool_id/expose/allowed_groups/enabled) stay as approved — submitting
  the dialog is the existing PUT confirm
- export buildPrefill from GiteaImportDialog (shared by import + update)
- com_tools_pending_* locale keys (en/zh-Hans)
- vitest: panel render/empty, confirm merge, check failure, query
  failure, catalog loading
…ds repo omits

Confirming a pending update prefilled the dialog from tool.json@tag, so a
stale tool.json version clobbered the release-tag version and the pending
row looped forever. buildUpdateMerge now:

- sets version (top-level + distribution) from the pending target (tag);
  the tag is the distribution identity resolver/Toolhost install by
- keeps approved values for anything the repo does not provide
  (parameters/form/result/dangerous/description/display_name/sha256/
  launcher/runtime) instead of overwriting with undefined/fallbacks
- keeps governance fields (tool_id/expose/allowed_groups/enabled)

Check results are now surfaced in the pending panel: hard errors block
the dialog with a notice; warns (e.g. version_mismatch) show as a notice.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
…o error)

Gateway check_repo returns version_mismatch as a hard error when
tool.json@tag carries a version that differs from the tag, so the
confirm flow blocks with the reason instead of opening a dialog for a
version the Toolhost would refuse to install. Missing version stays a
warn (manifest fallback at install).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
- server fns: paged GET + full export (200/page, 10k cap) against the
  gateway admin run-reports endpoint, zod-parsed like the rest
- /usage route, sidebar entry, en/zh-Hans locales (parity-tested)
- AuditLogTab pattern reuse: DatePickerCell extracted to shared
- CSV built client-side with UTF-8 BOM + RFC 4180 escaping
toolsQueryOptions is a static queryOptions object in the server module,
but UsagePage invoked it as a factory (toolsQueryOptions()), throwing
"is not a function" in the production bundle. The spec's @/server mock
shaped it as a function, which is why tests stayed green — the mock now
mirrors the real export shape.
- ToolEditDialog: exec kind extends to plugin/web; web gets a URL
  field, plugin gets host_launcher + host-root candidates + install/
  uninstall script fields; desktop distribution block shared
- GiteaImportDialog: import-kind selector — desktop/plugin run the
  existing check flow (plugin prefill carries host fields from
  tool.json), web skips the check and takes a required URL with the
  repo as optional name/description prefill
- locales +13 keys en/zh-Hans (parity-checked); 859 tests green
…oup visibility); tool editor gains display_group/help_url/usage_stats; dashboard quick links
…alls -> IntegrityCheckFailed); bun's cache persists across loop rounds so retries converge
…ded); groups dimension unified on display_group — catalog rows get an inline group <select>, listing sections by group, edit dialog gains a datalist of existing groups; /usage redirects to /tools?tab=usage
…d by the tool's group, 2.17.0); group editor gains allowed-groups chips; catalog group options come from the registered group list; dashboard drops the usage quick link
…l style as the edit dialog; commit on blur/Enter)
…services, per-datasource binding) and map_name field on map bindings
… shows tool names; binding edit = single full-URL field with list-maps/datasources buttons and multi-select basemap group
- ServicesPage: tab 系统(服务绑定/底图管理),底图管理挂 BasemapsManager
- BasemapsManager(新):当前底图组列表(序/名/URL/移除)+ 从已绑定地图服务
  多选地图加入组(组内原序保留、新增追加;搜索框筛选)
- ServiceEditDialog: 移除底图设置区(底图归底图管理标签)
- DiscoverDialog: 探测结果列表加搜索框(结果为空时不渲染)
- ToolCatalogTab/ToolEditDialog: 暴露面(ui/mcp)改为工具清单行内即时开关,
  编辑对话框移除「暴露给」区块
- locales: com_services_tab_*、com_basemaps_* 新增
搜索框此前只过滤显示,点绑定时仍遍历全部预勾选服务——过滤后绑定
会把被隐藏的服务一并绑入(2026-09-30 误绑 49 服务事故根因)。
替换原 select 下拉 + 单列表:grid 双列,左列服务行点击选中(选中态高亮,
切换清空本服务勾选),右列该服务地图多选勾选;两列各自搜索框。i18n 增
pick_service/pick_layer/pick_layer_of/search 四 key(en/zh-Hans)
表格行内分组单元格与编辑对话框展示分组均改为 SelectField 同款
click-ui Select;「未分组」以哨兵项表示(选回即清空分组);移除
datalist 自由文本入口(新分组经分组页管理)
- ParamBuilder:参数卡片(折叠行头:拖拽把手+名称+类型徽章+必填+删除;
  展开态:参数名(可改名)/类型/标题/占位/描述/默认值(按类型)/控件类型/
  枚举选项/静态选项与动态 choices_from)
- 双向绑定:可视化编辑即时 serialize 回 parametersJson+formJson;
  外部 JSON 变化反向 parse;round-trip 保真(未知键 raw/rawHint 原样保留);
  JSON 语法错误锁定可视化并显示错误
- 添加参数 ▾ 预设菜单:字符串/数字/整数/布尔/多选数组/多行文本/日期/
  日期时间/文件/目录/云盘文件/隐藏
- 拖拽排序写 form.order;i18n 40 key(en/zh-Hans)
SwakinX added 30 commits October 4, 2026 15:03
自定义 portal 菜单在弹窗内反复踩坑(裁剪/透明/坐标包含块),换组件库
官方 Dropdown:Trigger asChild 包 Button,Content 由 Radix 自动定位与
portal,Item onSelect 即添加;移除手写定位/配色状态
选中 chip 高亮(accent 边框+计数反色),列表按所选类型过滤;
新增 com_services_type_all locale key(en/zh-Hans)
- 新建绑定默认 iserver_map(不再默认数据服务)
- 切换类型即时清空另一类型字段(地图清 datasource、数据清 mapName)
- 提交按类型归一剥离无关字段(空串覆盖旧值),修复数据服务改地图服务
  时旧数据源残留被网关 400 拒绝保存
- 手写方框 tab 栏换 click-ui Tabs(工具管理等设置页同款下划线风格)
- 底图管理双列表搜索框换共享 SearchInput(SearchField 同款)
- 新增 shared/CheckIndicator:去重 BasemapsManager/DiscoverDialog 各自私有的勾选指示
- DiscoverDialog:补 modal-frost(唯一缺失的弹窗)、手写按钮→click-ui Button、原生搜索框→SearchInput
- ServiceEditDialog:enabled 原生 checkbox→click-ui Switch(对齐 ToolEditDialog 标志位)
- ToolEditDialog:4 处原生 select→SelectField(前缀/执行类型/handler/运行时,空值走 __none__ 哨兵)
- ToolCatalogTab:暴露面原生 checkbox→click-ui Checkbox;修正 datalist 陈旧注释;去无用 useEffect 导入
- ParamBuilder:required checkbox→Checkbox、静态/动态选项 radio→RadioGroup、aria-label 本地化、inferWidget 死分支清理
- BasemapsManager:删除重复的 splitServiceUrl(复用 ServiceEditDialog 导出)、删除按钮→InlineAction
- ServicesPage:类型筛选 chips 计数提升为普通常量,去掉 JSX 内 IIFE
- choices_from 解析 {tool,value_field?,label_field?} 对象(value/label 字段随行
  保全);旧版产出的纯字符串救援为 tool 名,round-trip 后升格为合法对象
- accept 解析数组、序列化为带前导点扩展名数组(schema ^\.[A-Za-z0-9_-]+$,
  逗号/中文逗号输入均归一)
- 修复「动态来源」单选把本地化占位文案当真实数据种子(改为 placeholder)
- TYPE_LABEL 类型文案与 accept 字段名走 locale(com_param_type_* / com_param_field_accept)
- SelectField 改 barrel 导入(admin-panel CLAUDE.md 规范)
- 导出 parseManifest/serialize 并新增 5 个 round-trip 单测
19 个领域类型(manifest/分组/Gitea 检查/用量/服务绑定/底图/发现)从
server/terravox.ts 迁入 src/types/ 并入 barrel;server 模块 type-only
再导出保持 '@/server' 导入方零改动(纯类型变更,无运行时影响)。
SelectField 外层只有 max-w 约束,在 flex/grid 里收缩到当前值的自然宽度
(前缀分组实测 77px,选项稍长即截断)。按 KeyValueField 惯例外包宽度
容器:前缀分组 w-44 shrink-0(原原生 select 为 w-40),exec-kind/handler/
distRuntime 恢复原生 w-full 语义。
- 工具分组:显式分组卡片拖拽调整先后,落库 sort_order(×10 间隔,仅改动项
  逐个 PATCH);隐式分组(前缀自动合成)无把手固定沉底,增删后本地序回退
  服务端序
- 工具清单:分组节内工具行拖拽排序,落库 manifest.display_order(乐观覆盖
  保持拖拽视觉稳定,成功后 invalidate 清空);列表排序改为 display_order
  升序、未设置沉底再按 tool_id
- 编辑弹窗保存透传 display_order(不走表单,防重建 manifest 抹掉已设顺序)
- 契约/网关侧见 terravox-contracts e03e660、terravox-gateway 43933ee
分组卡片新增 ☰ 入口:弹窗内上半区列组内工具(⠿ 拖拽定 display_order、
✕ 移除=清 display_group+display_order 归未分组),下半区搜索点选添加
(设 display_group、顺序留空自然沉底)。改动即时落库并失效 ['terravox']
缓存,分组卡的工具计数随 invalidate 刷新。隐式分组同样可管理组内工具。
- @dnd-kit/core+sortable(PointerSensor distance 约束,点击不受影响)
- 分组工具详情弹窗重设计为双列表:左=未加入工具(可搜索)、右=组内工具
  (display_order 序);跨列拖拽=添加/移除、右列内拖拽=排序,droppable 列
  容器拖入高亮,提交时统一 diff 落库后失效缓存
- 分组卡/清单工具行换 useSortable(网格 rectSortingStrategy/表格
  verticalListSortingStrategy),清单支持跨分组节拖动=直接改 display_group
  (拖入「未分组」节即清空分组);移除全部手写指针交换代码
- ToolCatalogTab.test 补 dnd-kit 桩(vitest CJS 互操作下命名导出为 undefined,
  与 click-ui 白名单 mock 同款约定)
…t 挂载即抛错

sortableKeyboardCoordinates 是坐标函数,直接 useSensor(它) 产出坏传感器描述,
useCombineActivators 里 sensors.map 抛 TypeError,整页落错误边界(「出了点问题」)。
改为 useSensor(KeyboardSensor, { coordinateGetter }) 标准用法(分组详情弹窗同修);
新增真实 dnd-kit(importOriginal 不桩化)渲染回归测试,防止桩化测试掩盖此类崩溃。
…渲染重跑

弹窗打开瞬间 toolsQuery 尚在加载,data?.tools ?? [] 每轮渲染产生新数组引用,
byId 派生失效,重建两列的 effect 无限 setState(Maximum update depth,
React 整页崩到错误边界)。修复:模块级稳定 EMPTY_TOOLS 常量 + 初始列改
useMemo 派生、同步 effect 以派生数组身份为依赖(身份稳定即不重跑)。
分组详情弹窗纳入真实 dnd-kit 回归测试(渲染双列表断言行内容)。
分组详情双列表保留跨列拖拽语义(dragOver 换列判定),restrictToVerticalAxis
仅锁被拖项的视觉位移轴,跨列判定不受影响。分组卡网格排序暂不加锁
(网格两维都有意义)。
ToolCatalogTab 表格与 ToolGroupsTab 详情弹窗的 restrictToVerticalAxis
此前未传入 DndContext(replace 静默 no-op,prettier 折行所致),竖轴锁定
实际未生效。本次以 grep 断言接线完成。
…再度丢失(prettier 重排吞行),realDnd 测试补 modifiers 真模块 mock 防回归
closestCorners 按被拖项矩形算碰撞,锁轴后矩形永远到不了另一列,
dragOver 跨列判定失效(实测无法双向拖)。移除弹窗的 modifiers 与
失效的导入;其他位置(清单表格/图层树)竖轴锁定保持不变。
- 表格新增来源列(此前仅 CSV 导出含 source),colSpan 6→7
- 值经 SOURCE_LABEL_KEY 本地化映射,未知值原样回显
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant