From 2f8351a81a26c49b1a2a033c78d99235a4313c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Tue, 9 Jun 2026 14:35:57 +0800 Subject: [PATCH 1/4] fix(i18n): translate Setup System Settings menu labels and pages The Setup app's Configuration menu gained Authentication, File Storage, AI & Embedder, and Knowledge entries, but their nav labels were never added to the translation bundles, and several settings pages had no localized content. Untranslated locales fell back to the raw English literal (or key). Setup nav menu labels (platform-objects): - Add nav_settings_storage / nav_settings_ai / nav_settings_knowledge to en, zh-CN, ja-JP, es-ES. - Add the missing nav_settings_auth label to zh-CN, ja-JP, es-ES. Settings page content (service-settings): - Add the auth page to zh-CN, ja-JP, es-ES. - Add the ai and knowledge pages to ja-JP. en remains the source of truth; keys mirror the manifest field names. --- .../src/apps/translations/en.ts | 3 + .../src/apps/translations/es-ES.ts | 4 + .../src/apps/translations/ja-JP.ts | 4 + .../src/apps/translations/zh-CN.ts | 4 + .../src/translations/es-ES.ts | 33 ++++ .../src/translations/ja-JP.ts | 168 ++++++++++++++++++ .../src/translations/zh-CN.ts | 32 ++++ 7 files changed, 248 insertions(+) diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index fc41e342f..53d4f28c6 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -84,6 +84,9 @@ export const en: TranslationData = { nav_settings_mail: { label: 'Email' }, nav_settings_branding: { label: 'Branding' }, nav_settings_auth: { label: 'Authentication' }, + nav_settings_storage: { label: 'File Storage' }, + nav_settings_ai: { label: 'AI & Embedder' }, + nav_settings_knowledge: { label: 'Knowledge' }, nav_settings_feature_flags: { label: 'Feature Flags' }, // Diagnostics diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index f067923fb..ab36b03df 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -62,6 +62,10 @@ export const esES: TranslationData = { nav_settings_hub: { label: 'Todos los Ajustes' }, nav_settings_mail: { label: 'Correo' }, nav_settings_branding: { label: 'Marca' }, + nav_settings_auth: { label: 'Autenticación' }, + nav_settings_storage: { label: 'Almacenamiento de Archivos' }, + nav_settings_ai: { label: 'IA y Embedder' }, + nav_settings_knowledge: { label: 'Conocimiento' }, nav_settings_feature_flags: { label: 'Indicadores de Funcionalidad' }, nav_sessions: { label: 'Sesiones' }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index e1a4cf83f..7622c09d7 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -62,6 +62,10 @@ export const jaJP: TranslationData = { nav_settings_hub: { label: 'すべての設定' }, nav_settings_mail: { label: 'メール' }, nav_settings_branding: { label: 'ブランディング' }, + nav_settings_auth: { label: '認証' }, + nav_settings_storage: { label: 'ファイルストレージ' }, + nav_settings_ai: { label: 'AI と Embedder' }, + nav_settings_knowledge: { label: 'ナレッジ' }, nav_settings_feature_flags: { label: '機能フラグ' }, nav_sessions: { label: 'セッション' }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index 96b86ec80..ea7ff6c62 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -62,6 +62,10 @@ export const zhCN: TranslationData = { nav_settings_hub: { label: '全部设置' }, nav_settings_mail: { label: '邮件' }, nav_settings_branding: { label: '品牌' }, + nav_settings_auth: { label: '认证' }, + nav_settings_storage: { label: '文件存储' }, + nav_settings_ai: { label: 'AI 与 Embedder' }, + nav_settings_knowledge: { label: '知识库' }, nav_settings_feature_flags: { label: '功能开关' }, nav_sessions: { label: '会话' }, diff --git a/packages/services/service-settings/src/translations/es-ES.ts b/packages/services/service-settings/src/translations/es-ES.ts index 66e90574b..2f170cd5a 100644 --- a/packages/services/service-settings/src/translations/es-ES.ts +++ b/packages/services/service-settings/src/translations/es-ES.ts @@ -68,6 +68,39 @@ export const esES: TranslationData = { }, }, + auth: { + title: 'Autenticación', + description: 'Inicio de sesión, registro y controles de las funciones de autenticación integradas.', + groups: { + email_password: { + title: 'Correo y contraseña', + description: 'Controla el inicio de sesión local con correo/contraseña y el registro de autoservicio.', + }, + social: { + title: 'Inicio de sesión social', + description: + 'Configura el proveedor de inicio de sesión de Google integrado. Las variables de entorno del despliegue siguen teniendo prioridad.', + }, + }, + keys: { + email_password_enabled: { label: 'Habilitar inicio de sesión con correo/contraseña' }, + signup_enabled: { label: 'Permitir registro de autoservicio' }, + require_email_verification: { label: 'Requerir verificación de correo' }, + google_enabled: { + label: 'Habilitar inicio de sesión con Google', + help: 'Requiere un ID de cliente y un secreto de OAuth de Google desde Google Cloud Console.', + }, + google_client_id: { + label: 'ID de cliente de Google', + help: 'ID de cliente de OAuth desde Google Cloud Console. También se puede definir GOOGLE_CLIENT_ID en el servidor.', + }, + google_client_secret: { + label: 'Secreto de cliente de Google', + help: 'Se almacena cifrado en reposo. También se puede definir GOOGLE_CLIENT_SECRET en el servidor.', + }, + }, + }, + feature_flags: { title: 'Indicadores de función', description: 'Activa funciones experimentales y en beta para este espacio de trabajo.', diff --git a/packages/services/service-settings/src/translations/ja-JP.ts b/packages/services/service-settings/src/translations/ja-JP.ts index d3389f70c..d83f4d3ee 100644 --- a/packages/services/service-settings/src/translations/ja-JP.ts +++ b/packages/services/service-settings/src/translations/ja-JP.ts @@ -68,6 +68,39 @@ export const jaJP: TranslationData = { }, }, + auth: { + title: '認証', + description: 'サインイン、登録、組み込み認証機能の制御。', + groups: { + email_password: { + title: 'メールとパスワード', + description: 'ローカルのメール/パスワードサインインとセルフサービス登録を制御します。', + }, + social: { + title: 'ソーシャルサインイン', + description: + '組み込みの Google サインインプロバイダーを設定します。デプロイの環境変数が優先されます。', + }, + }, + keys: { + email_password_enabled: { label: 'メール/パスワードログインを有効化' }, + signup_enabled: { label: 'セルフサービス登録を許可' }, + require_email_verification: { label: 'メール確認を必須にする' }, + google_enabled: { + label: 'Google ログインを有効化', + help: 'Google Cloud Console の Google OAuth クライアント ID とシークレットが必要です。', + }, + google_client_id: { + label: 'Google クライアント ID', + help: 'Google Cloud Console の OAuth クライアント ID。サーバー側で GOOGLE_CLIENT_ID を設定することもできます。', + }, + google_client_secret: { + label: 'Google クライアントシークレット', + help: '保存時に暗号化されます。サーバー側で GOOGLE_CLIENT_SECRET を設定することもできます。', + }, + }, + }, + feature_flags: { title: '機能フラグ', description: 'このワークスペースで実験的・ベータ機能を切替えます。', @@ -122,5 +155,140 @@ export const jaJP: TranslationData = { test: { label: '接続テスト' }, }, }, + + ai: { + title: 'AI と Embedder', + description: + 'プラットフォームの AI およびナレッジサービスが使用する LLM プロバイダー、モデル、認証情報、Embedder 設定。', + groups: { + provider: { title: 'プロバイダー', + description: 'LLM バックエンドを選択します。Memory モードは入力をそのまま返します。テスト用であり、本番では使用しないでください。' }, + gateway: { title: 'Vercel AI Gateway', + description: 'マルチプロバイダールーター。モデル指定は `provider/model` 形式に従います(例: `openai/gpt-4o`)。' }, + openai: { title: 'OpenAI' }, + anthropic: { title: 'Anthropic' }, + google: { title: 'Google' }, + defaults: { title: '生成のデフォルト値', + description: 'エージェントまたはチャットリクエストが独自の値を指定しない場合に適用されます。' }, + observability: { title: '可観測性' }, + embedder: { title: 'Embedder', + description: + 'ナレッジソースと RAG が使用するテキスト → ベクトルプロバイダー。' + + '上記のチャットプロバイダーとは独立しています。' }, + }, + keys: { + provider: { + label: 'プロバイダー', + options: { + memory: 'Memory(エコー — テスト専用)', + gateway: 'Vercel AI Gateway', + openai: 'OpenAI', + anthropic: 'Anthropic', + google: 'Google Generative AI', + }, + }, + gateway_model: { label: 'Gateway モデル', + help: 'AI_GATEWAY_MODEL として転送されます。例: openai/gpt-4o' }, + gateway_api_key: { label: 'Gateway API キー', + help: '任意 — Gateway が認証を要求する場合のみ必要です。' }, + openai_api_key: { label: 'OpenAI API キー', + help: 'OPENAI_API_KEY として転送されます。保存時に暗号化されます。' }, + openai_model: { label: 'モデル', + help: 'デフォルトのモデル ID。エージェント単位の上書きが優先されます。' }, + openai_base_url: { label: 'Base URL', + help: 'Azure OpenAI や自己ホスト型ゲートウェイ用の上書き。api.openai.com の場合は空欄にします。' }, + anthropic_api_key: { label: 'Anthropic API キー', + help: 'ANTHROPIC_API_KEY として転送されます。保存時に暗号化されます。' }, + anthropic_model: { label: 'モデル' }, + google_api_key: { label: 'Google API キー', + help: 'GOOGLE_GENERATIVE_AI_API_KEY として転送されます。保存時に暗号化されます。' }, + google_model: { label: 'モデル' }, + temperature: { label: 'Temperature', + help: '0 = 決定的、2 = 非常に創造的。' }, + max_tokens: { label: '最大出力トークン数', + help: 'レスポンスごとに生成されるトークンの上限。' }, + request_timeout_ms: { label: 'リクエストタイムアウト (ms)' }, + trace_enabled: { label: 'トレースを記録', + help: 'デバッグと再生のため prompt/response トレースを sys_ai_trace に保存します。' }, + log_prompts: { label: '完全なプロンプトを記録', + help: 'メタデータだけでなくレンダリングされたプロンプトをトレース行に含めます。⚠ PII が漏えいする可能性があります。規制環境では無効にしてください。' }, + embedder_provider: { + label: 'プロバイダー', + options: { + none: '無効(埋め込みなし)', + openai: 'OpenAI', + azure: 'Azure OpenAI', + dashscope: '阿里通义 DashScope', + zhipu: '智谱 BigModel', + siliconflow: '硅基流动 SiliconFlow', + doubao: '火山引擎 Doubao', + minimax: 'MiniMax', + ollama: 'Ollama(ローカル)', + custom: 'カスタム(OpenAI 互換)', + }, + }, + embedder_api_key: { label: 'Embedder API キー', + help: 'Authorization ヘッダーとして送信される Bearer トークン。Ollama では空でない任意の値で動作します。' }, + embedder_model: { label: 'モデル', + help: '例 — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3' }, + embedder_base_url: { label: 'Base URL', + help: 'エンドポイントのルート(/embeddings を含まない)。プリセットから自動入力されます。プロキシや自己ホスト型ゲートウェイ用に上書きできます。' }, + embedder_dimensions: { label: '次元数', + help: '出力次元数を上書きします(Matryoshka モデルのみ)。空欄の場合はモデルのデフォルトを使用します。' }, + embedder_batch_size: { label: 'バッチサイズ', + help: 'embed() 呼び出しごとのチャンク数。プロバイダーのレート/サイズ制限に達する場合は減らします。' }, + }, + actions: { + test: { label: '接続テスト' }, + test_embedder: { label: 'Embedder をテスト' }, + }, + }, + + knowledge: { + title: 'ナレッジ', + description: + 'RAG / ナレッジソース用のベクトルストアバックエンド。' + + '⚠ アダプターを切替えても既存のインデックスは移行されません。', + groups: { + adapter: { title: 'バックエンド', + description: 'ドキュメントチャンクとそのベクトルの保存先を選択します。' }, + turso: { title: 'Turso / libSQL', + description: 'マネージド Turso、ローカルファイル、インメモリのいずれでも動作します。' }, + ragflow: { title: 'RAGFlow', + description: '外部 RAGFlow デプロイ。セルフホストの手順は https://ragflow.io を参照してください。' }, + indexing: { title: 'インデックスのデフォルト値', + description: 'KnowledgeSource.adapterConfig のソース単位の値が優先されます。' }, + permissions: { title: '権限' }, + }, + keys: { + adapter: { + label: 'アダプター', + options: { + memory: 'インメモリ(開発/テスト専用)', + turso: 'Turso / libSQL(クラウドまたはローカル)', + ragflow: 'RAGFlow(外部)', + }, + }, + turso_url: { label: '接続 URL', + help: '例: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:' }, + turso_auth_token: { label: '認証トークン', + help: 'マネージド Turso URL の場合のみ必要です。' }, + ragflow_base_url: { label: 'Base URL', help: '例: http://localhost:9380' }, + ragflow_api_key: { label: 'API キー' }, + ragflow_default_dataset: { label: 'デフォルトデータセット ID', + help: 'KnowledgeSource が独自の RAGFlow データセットを指定しない場合に使用されます。' }, + chunk_target: { label: '目標チャンクサイズ(文字数)', + help: 'トークン単位の分割が行われる前のチャンクサイズのソフト上限。' }, + chunk_overlap: { label: 'チャンクの重なり(文字数)', + help: '境界を越えてコンテキストを維持するため、前のチャンクから保持する文字数。' }, + over_fetch: { label: 'オーバーフェッチ倍率', + help: 'JS 側のメタデータフィルタリングでも行が残るよう、内部で topK × overFetch 件の候補を取得します。' }, + enforce_rls: { label: '検索時に RLS を強制', + help: '各ヒットを呼び出し元のレコードレベル権限に対して再確認します。⚠ 無効化するとプラットフォーム固有のセーフガードがスキップされます。' }, + }, + actions: { + test: { label: '接続テスト' }, + }, + }, }, }; diff --git a/packages/services/service-settings/src/translations/zh-CN.ts b/packages/services/service-settings/src/translations/zh-CN.ts index 43e5e32e9..60e8f657e 100644 --- a/packages/services/service-settings/src/translations/zh-CN.ts +++ b/packages/services/service-settings/src/translations/zh-CN.ts @@ -68,6 +68,38 @@ export const zhCN: TranslationData = { }, }, + auth: { + title: '认证', + description: '登录、注册以及内置认证功能的控制项。', + groups: { + email_password: { + title: '邮箱与密码', + description: '控制本地邮箱/密码登录与自助注册。', + }, + social: { + title: '社交登录', + description: '配置内置的 Google 登录提供商。部署环境变量仍优先生效。', + }, + }, + keys: { + email_password_enabled: { label: '启用邮箱/密码登录' }, + signup_enabled: { label: '允许自助注册' }, + require_email_verification: { label: '要求邮箱验证' }, + google_enabled: { + label: '启用 Google 登录', + help: '需要在 Google Cloud Console 中创建的 Google OAuth 客户端 ID 与密钥。', + }, + google_client_id: { + label: 'Google 客户端 ID', + help: '来自 Google Cloud Console 的 OAuth 客户端 ID。也可在服务器上设置 GOOGLE_CLIENT_ID。', + }, + google_client_secret: { + label: 'Google 客户端密钥', + help: '加密存储。也可在服务器上设置 GOOGLE_CLIENT_SECRET。', + }, + }, + }, + feature_flags: { title: '功能开关', description: '为当前工作区开启实验性与测试功能。', From 61fb34686da02a3ecc8ec56dfda5360b995a14f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Tue, 9 Jun 2026 15:26:21 +0800 Subject: [PATCH 2/4] fix(rest): unwrap getMetaItem envelope before translating metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-item app route bypasses the HTTP cache (for per-user RBAC filtering), so it received the raw `getMetaItem` envelope `{ type, name, item, ... }` and passed it straight to `translateMetadataDocument`. Because the translatable document — and its `navigation` tree — lives nested at `.item`, the resolver found no `navigation` to walk and left every Setup menu label in English; only the envelope's own top-level label got translated, which consumers reading `.item` never see. Add `isMetaEnvelope` and unwrap the inner document before translating in both `translateMetaItem` and `translateMetaItems`. Also handle the `{ items: [...] }` list-envelope shape in `translateMetaItems`, which the bare `Array.isArray` guard previously dropped untranslated. --- packages/rest/src/rest-server.ts | 43 ++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index 6ff9e07c3..650684d1f 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -15,6 +15,24 @@ const logError = (...args: unknown[]) => (globalThis as any).console?.error(...a */ const TRANSLATABLE_META_TYPES = new Set(['view', 'action', 'object', 'app', 'dashboard']); +/** + * Detect the `getMetaItem` response envelope (`{ type, name, item, lock, … }`) + * whose translatable metadata document is nested at `.item`. The cached read + * path and `getMetaItems` element shape hand back the already-unwrapped + * document instead, so translation helpers must distinguish the two: an + * envelope carries a nested `item` object alongside its own `type`/`name`, + * which a bare metadata document never does. + */ +function isMetaEnvelope(value: any): boolean { + return !!value + && typeof value === 'object' + && typeof value.type === 'string' + && typeof value.name === 'string' + && value.item != null + && typeof value.item === 'object' + && !Array.isArray(value.item); +} + /** * Map a data-layer error to a clean HTTP response. Unknown-object errors * (SQLite "no such table", PG "relation does not exist", protocol @@ -1037,6 +1055,15 @@ export class RestServer { const locale = this.extractLocale(req, i18n); if (!locale) return item; const { translateMetadataDocument } = await import('@objectstack/spec/system'); + // `getMetaItem` returns an envelope `{ type, name, item, lock, ... }` + // whose translatable document is nested at `.item`; the cached read + // path hands us the already-unwrapped document. Translate whichever + // shape we received — nav/field labels live on the inner doc, so + // translating the envelope's top level (which has no `navigation`) + // would leave the menu untranslated. + if (isMetaEnvelope(item)) { + return { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) }; + } return translateMetadataDocument(type, item, bundle, { locale }); } @@ -1044,15 +1071,27 @@ export class RestServer { * Translate a list of metadata documents using `translateMetaItem`. */ private async translateMetaItems(req: any, type: string, environmentId: string | undefined, items: any): Promise { - if (!Array.isArray(items)) return items; if (!TRANSLATABLE_META_TYPES.has(type)) return items; + // `getMetaItems` may hand back a bare array or an `{ items: [...] }` + // envelope. Unwrap so list responses are localized the same way the + // single-item route is; a non-array, non-envelope value is returned + // untouched. + const arr: any[] | null = Array.isArray(items) + ? items + : (items && typeof items === 'object' && Array.isArray(items.items) ? items.items : null); + if (!arr) return items; const i18n = await this.resolveI18nService(environmentId, req); const bundle = this.buildTranslationBundle(i18n); if (!bundle) return items; const locale = this.extractLocale(req, i18n); if (!locale) return items; const { translateMetadataDocument } = await import('@objectstack/spec/system'); - return items.map((item) => translateMetadataDocument(type, item, bundle, { locale })); + const translated = arr.map((item) => + isMetaEnvelope(item) + ? { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) } + : translateMetadataDocument(type, item, bundle, { locale }), + ); + return Array.isArray(items) ? translated : { ...items, items: translated }; } /** From 851a0dff5574e21747106ff3202c9ea43f6df10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Tue, 9 Jun 2026 15:34:05 +0800 Subject: [PATCH 3/4] fix(i18n): add missing Setup nav keys (group_integrations, notifications) The Setup app left four nav labels in English because no bundle key existed for them across any locale: - group_integrations (the empty integrations anchor owned by the Setup app itself; only the singular group_integration for Studio was translated) - nav_notification_{preferences,subscriptions,templates} (contributed to group_configuration by @objectstack/service-messaging, which ships no translation bundle) These merge into the Setup app by stable node id, so their labels belong in the central SetupAppTranslations alongside nav_notifications. Add all four to en, zh-CN, ja-JP and es-ES. --- packages/platform-objects/src/apps/translations/en.ts | 5 +++++ packages/platform-objects/src/apps/translations/es-ES.ts | 4 ++++ packages/platform-objects/src/apps/translations/ja-JP.ts | 4 ++++ packages/platform-objects/src/apps/translations/zh-CN.ts | 4 ++++ 4 files changed, 17 insertions(+) diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index 53d4f28c6..13a68f44b 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -50,6 +50,7 @@ export const en: TranslationData = { group_access_control: { label: 'Access Control' }, group_approvals: { label: 'Approvals' }, group_configuration: { label: 'Configuration' }, + group_integrations: { label: 'Integrations' }, group_diagnostics: { label: 'Diagnostics' }, group_advanced: { label: 'Advanced' }, @@ -88,6 +89,10 @@ export const en: TranslationData = { nav_settings_ai: { label: 'AI & Embedder' }, nav_settings_knowledge: { label: 'Knowledge' }, nav_settings_feature_flags: { label: 'Feature Flags' }, + // Notifications (contributed by @objectstack/service-messaging) + nav_notification_preferences: { label: 'Notification Preferences' }, + nav_notification_subscriptions: { label: 'Notification Subscriptions' }, + nav_notification_templates: { label: 'Notification Templates' }, // Diagnostics nav_sessions: { label: 'Sessions' }, diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index ab36b03df..fe74fc537 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -38,6 +38,7 @@ export const esES: TranslationData = { group_access_control: { label: 'Control de Acceso' }, group_approvals: { label: 'Aprobaciones' }, group_configuration: { label: 'Configuración' }, + group_integrations: { label: 'Integraciones' }, group_diagnostics: { label: 'Diagnóstico' }, group_advanced: { label: 'Avanzado' }, @@ -67,6 +68,9 @@ export const esES: TranslationData = { nav_settings_ai: { label: 'IA y Embedder' }, nav_settings_knowledge: { label: 'Conocimiento' }, nav_settings_feature_flags: { label: 'Indicadores de Funcionalidad' }, + nav_notification_preferences: { label: 'Preferencias de Notificación' }, + nav_notification_subscriptions: { label: 'Suscripciones de Notificación' }, + nav_notification_templates: { label: 'Plantillas de Notificación' }, nav_sessions: { label: 'Sesiones' }, nav_audit_logs: { label: 'Registros de Auditoría' }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index 7622c09d7..490516637 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -38,6 +38,7 @@ export const jaJP: TranslationData = { group_access_control: { label: 'アクセス制御' }, group_approvals: { label: '承認' }, group_configuration: { label: '構成' }, + group_integrations: { label: '統合' }, group_diagnostics: { label: '診断' }, group_advanced: { label: '詳細' }, @@ -67,6 +68,9 @@ export const jaJP: TranslationData = { nav_settings_ai: { label: 'AI と Embedder' }, nav_settings_knowledge: { label: 'ナレッジ' }, nav_settings_feature_flags: { label: '機能フラグ' }, + nav_notification_preferences: { label: '通知設定' }, + nav_notification_subscriptions: { label: '通知購読' }, + nav_notification_templates: { label: '通知テンプレート' }, nav_sessions: { label: 'セッション' }, nav_audit_logs: { label: '監査ログ' }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index ea7ff6c62..3c54fb268 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -38,6 +38,7 @@ export const zhCN: TranslationData = { group_access_control: { label: '访问控制' }, group_approvals: { label: '审批' }, group_configuration: { label: '配置' }, + group_integrations: { label: '集成' }, group_diagnostics: { label: '诊断' }, group_advanced: { label: '高级' }, @@ -67,6 +68,9 @@ export const zhCN: TranslationData = { nav_settings_ai: { label: 'AI 与 Embedder' }, nav_settings_knowledge: { label: '知识库' }, nav_settings_feature_flags: { label: '功能开关' }, + nav_notification_preferences: { label: '通知偏好' }, + nav_notification_subscriptions: { label: '通知订阅' }, + nav_notification_templates: { label: '通知模板' }, nav_sessions: { label: '会话' }, nav_audit_logs: { label: '审计日志' }, From 23f1aca8ec977b9ce20a27db9f6a2ce4b83f039c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Tue, 9 Jun 2026 15:44:40 +0800 Subject: [PATCH 4/4] test(rest): guard metadata envelope-unwrap translation path Cover the regression directly: a getMetaItem envelope must have its nested .item translated (not the envelope top level), the bare already-unwrapped document path must still work, and both list shapes ({items:[...]} and a raw array) must localize their elements. --- packages/rest/src/rest.test.ts | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/packages/rest/src/rest.test.ts b/packages/rest/src/rest.test.ts index d31ca0a91..c84668c93 100644 --- a/packages/rest/src/rest.test.ts +++ b/packages/rest/src/rest.test.ts @@ -1595,3 +1595,88 @@ describe('discovery — routes.mcp (ADR-0036, #152)', () => { expect(body.routes.mcp).toBeUndefined(); }); }); + +// ────────────────────────────────────────────────────────────────────────── +// Metadata translation — envelope unwrapping +// +// Regression guard for the Setup-app i18n gap: `getMetaItem` returns an +// envelope `{ type, name, item, ... }` whose translatable document (and its +// `navigation` tree) is nested at `.item`. Translating the envelope's top +// level instead of the inner doc left every nav label in English. The +// single-item app route bypasses the HTTP cache (for per-user RBAC +// filtering), so it only ever sees the envelope shape. +// ────────────────────────────────────────────────────────────────────────── +describe('RestServer metadata translation — envelope unwrap', () => { + // Minimal i18n service exposing one zh-CN bundle for the `setup` app. + const fakeI18n = { + getLocales: () => ['zh-CN'], + getDefaultLocale: () => 'zh-CN', + getTranslations: (locale: string) => + locale === 'zh-CN' + ? { + apps: { + setup: { + label: '系统设置', + navigation: { + group_configuration: { label: '配置' }, + nav_settings_storage: { label: '文件存储' }, + }, + }, + }, + } + : undefined, + }; + const zhReq = { headers: { 'accept-language': 'zh-CN' } }; + // A fresh app document each call (helpers must not mutate the input). + const makeDoc = () => ({ + name: 'setup', + label: 'Setup', + navigation: [ + { + id: 'group_configuration', + type: 'group', + label: 'Configuration', + children: [{ id: 'nav_settings_storage', type: 'url', label: 'File Storage' }], + }, + ], + }); + + it('translates the inner document of a getMetaItem envelope', async () => { + const rest = new RestServer(createMockServer() as any, createMockProtocol() as any); + const envelope = { type: 'app', name: 'setup', item: makeDoc(), lock: null }; + const out = await (rest as any).translateMetaItem(zhReq, 'app', undefined, envelope, fakeI18n); + // Envelope shape preserved … + expect(out.type).toBe('app'); + expect(out.name).toBe('setup'); + expect(out.lock).toBeNull(); + // … and the nested doc — not the envelope top level — is translated. + expect(out.item.label).toBe('系统设置'); + expect(out.item.navigation[0].label).toBe('配置'); + expect(out.item.navigation[0].children[0].label).toBe('文件存储'); + }); + + it('still translates a bare (already-unwrapped) document', async () => { + const rest = new RestServer(createMockServer() as any, createMockProtocol() as any); + const out = await (rest as any).translateMetaItem(zhReq, 'app', undefined, makeDoc(), fakeI18n); + expect(out.label).toBe('系统设置'); + expect(out.navigation[0].children[0].label).toBe('文件存储'); + }); + + it('translates list responses in the `{ items: [...] }` envelope shape', async () => { + const rest = new RestServer(createMockServer() as any, createMockProtocol() as any); + // translateMetaItems resolves the i18n service itself; stub the lookup. + (rest as any).resolveI18nService = async () => fakeI18n; + const listEnvelope = { items: [{ type: 'app', name: 'setup', item: makeDoc() }] }; + const out = await (rest as any).translateMetaItems(zhReq, 'app', undefined, listEnvelope); + expect(Array.isArray(out.items)).toBe(true); + expect(out.items[0].item.navigation[0].children[0].label).toBe('文件存储'); + }); + + it('translates a bare array of unwrapped documents', async () => { + const rest = new RestServer(createMockServer() as any, createMockProtocol() as any); + (rest as any).resolveI18nService = async () => fakeI18n; + const out = await (rest as any).translateMetaItems(zhReq, 'app', undefined, [makeDoc()]); + expect(Array.isArray(out)).toBe(true); + expect(out[0].navigation[0].children[0].label).toBe('文件存储'); + }); +});