From 82eb6cfc63e008cf20555ffb113a0bce29b94e1e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 02:36:58 +0000 Subject: [PATCH 1/2] fix(i18n): locale fallback, app/dashboard localization, es-ES settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server-side metadata translation root-cause fixes: - i18n-resolver: resolve the requested locale against the locales present in the bundle (exact → case-insensitive → base-language → variant) so a request for `zh` hits the `zh-CN` bundle instead of falling back to English. Benefits every resolver (objects/views/actions/settings/forms). - Add translateApp / translateDashboard and wire `app`/`dashboard` into the REST `/meta` translation path so app labels, navigation/sidebar group labels, and dashboard titles/widgets are localized at the API boundary. - service-settings: add the missing es-ES built-in Settings bundle. - i18n-extract.config: register sys_share_link, sys_view_definition and sys_metadata_audit so future extractions include them. https://claude.ai/code/session_01ASWzynvrsx5WRU2fUR116h --- .changeset/metadata-translation-audit.md | 33 +++ .../scripts/i18n-extract.config.ts | 11 +- packages/rest/src/rest-server.ts | 11 +- .../src/translations/es-ES.ts | 264 ++++++++++++++++++ .../src/translations/index.ts | 4 +- .../spec/src/system/i18n-resolver.test.ts | 151 ++++++++++ packages/spec/src/system/i18n-resolver.ts | 230 ++++++++++++++- 7 files changed, 699 insertions(+), 5 deletions(-) create mode 100644 .changeset/metadata-translation-audit.md create mode 100644 packages/services/service-settings/src/translations/es-ES.ts diff --git a/.changeset/metadata-translation-audit.md b/.changeset/metadata-translation-audit.md new file mode 100644 index 0000000000..1a35989abf --- /dev/null +++ b/.changeset/metadata-translation-audit.md @@ -0,0 +1,33 @@ +--- +"@objectstack/spec": patch +"@objectstack/rest": patch +"@objectstack/platform-objects": patch +"@objectstack/service-settings": patch +--- + +Fix system-metadata translations: locale fallback, app/dashboard localization, and coverage gaps. + +Switching the UI language left many surfaces in English. Three root causes +are addressed: + +- **Locale fallback (server).** The metadata translation resolver + (`@objectstack/spec` `i18n-resolver`) now resolves a requested locale + against the locales actually present in the bundle (exact → + case-insensitive → base-language → variant), so a request for `zh` + correctly hits the `zh-CN` bundle instead of falling back to English. + This mirrors `resolveLocale` in `@objectstack/core` and benefits every + resolver (objects, views, actions, settings, metadata forms). + +- **App & dashboard localization (server).** Added `translateApp` and + `translateDashboard` resolvers and wired `app`/`dashboard` into the REST + `/meta` translation path. App labels, sidebar/navigation group labels, + and dashboard titles/widgets were previously never localized at the API + boundary even though the translation data existed. + +- **Coverage & quality (data).** Added translations for the previously + untranslated platform objects `sys_share_link`, `sys_view_definition`, + and `sys_metadata_audit` (and registered them in the i18n-extract config + so future extractions keep them). Replaced English placeholder strings + left in the `zh-CN` / `ja-JP` / `es-ES` object and metadata-form bundles + (notably action `confirmText` / `successMessage` prompts). Added the + missing `es-ES` built-in Settings bundle in `@objectstack/service-settings`. diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index b1eb040727..7a4de1a96e 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -58,6 +58,7 @@ import { SysRolePermissionSet, SysRecordShare, SysSharingRule, + SysShareLink, } from '../src/security/index.js'; // ── Audit ───────────────────────────────────────────────────────────────── @@ -84,7 +85,12 @@ import { import { SysWebhook } from '../src/integration/index.js'; // ── Metadata ────────────────────────────────────────────────────────────── -import { SysMetadataObject, SysMetadataHistoryObject } from '../src/metadata/index.js'; +import { + SysMetadataObject, + SysMetadataHistoryObject, + SysViewDefinitionObject, + SysMetadataAuditObject, +} from '../src/metadata/index.js'; // ── System ──────────────────────────────────────────────────────────────── import { SysSetting, SysSecret, SysSettingAudit } from '../src/system/index.js'; @@ -147,6 +153,7 @@ export default defineStack({ SysRolePermissionSet, SysRecordShare, SysSharingRule, + SysShareLink, // Audit SysAuditLog, @@ -172,6 +179,8 @@ export default defineStack({ // Metadata SysMetadataObject, SysMetadataHistoryObject, + SysViewDefinitionObject, + SysMetadataAuditObject, // System SysSetting, diff --git a/packages/rest/src/rest-server.ts b/packages/rest/src/rest-server.ts index cb4fa9c704..498d6070fd 100644 --- a/packages/rest/src/rest-server.ts +++ b/packages/rest/src/rest-server.ts @@ -8,6 +8,13 @@ import { ObjectStackProtocol } from '@objectstack/spec/api'; // Node-safe logger — avoids importing 'console' which is absent from ES2020 lib typings. const logError = (...args: unknown[]) => (globalThis as any).console?.error(...args); +/** + * Metadata types whose user-facing labels are localized at the REST boundary + * via `translateMetadataDocument`. Keep in sync with the type dispatch in + * `@objectstack/spec/system`'s `translateMetadataDocument`. + */ +const TRANSLATABLE_META_TYPES = new Set(['view', 'action', 'object', 'app', 'dashboard']); + /** * Map a data-layer error to a clean HTTP response. Unknown-object errors * (SQLite "no such table", PG "relation does not exist", protocol @@ -917,7 +924,7 @@ export class RestServer { */ private async translateMetaItem(req: any, type: string, environmentId: string | undefined, item: any, i18nService?: any): Promise { if (!item || typeof item !== 'object') return item; - if (type !== 'view' && type !== 'action' && type !== 'object') return item; + if (!TRANSLATABLE_META_TYPES.has(type)) return item; // The cached read path resolves the i18n service up-front (to build a // locale-aware ETag) and passes it here so we don't repeat the // potentially registry-hitting lookup on every request. @@ -935,7 +942,7 @@ export class RestServer { */ private async translateMetaItems(req: any, type: string, environmentId: string | undefined, items: any): Promise { if (!Array.isArray(items)) return items; - if (type !== 'view' && type !== 'action' && type !== 'object') return items; + if (!TRANSLATABLE_META_TYPES.has(type)) return items; const i18n = await this.resolveI18nService(environmentId, req); const bundle = this.buildTranslationBundle(i18n); if (!bundle) return items; diff --git a/packages/services/service-settings/src/translations/es-ES.ts b/packages/services/service-settings/src/translations/es-ES.ts new file mode 100644 index 0000000000..66e90574b4 --- /dev/null +++ b/packages/services/service-settings/src/translations/es-ES.ts @@ -0,0 +1,264 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import type { TranslationData } from '@objectstack/spec/system'; + +/** + * Español (es-ES) — built-in settings manifest translations. + */ +export const esES: TranslationData = { + settingsCommon: { + sourceLabels: { + env: 'Entorno', + global: 'Global', + tenant: 'Inquilino', + user: 'Usuario', + default: 'Predeterminado', + }, + }, + settings: { + mail: { + title: 'Envío de correo', + description: 'Configuración de SMTP y del proveedor de correo transaccional.', + groups: { + provider: { title: 'Proveedor', description: 'Elige cómo envía correo saliente este espacio de trabajo.' }, + smtp: { title: 'SMTP' }, + api_key: { title: 'Clave de API' }, + from_address: { title: 'Dirección de remitente' }, + }, + keys: { + provider: { + label: 'Proveedor', + options: { + smtp: 'SMTP', + sendgrid: 'SendGrid', + ses: 'Amazon SES', + postmark: 'Postmark', + }, + }, + smtp_host: { label: 'Host', help: 'Ejemplo: smtp.example.com' }, + smtp_port: { label: 'Puerto' }, + smtp_secure: { label: 'Usar TLS' }, + smtp_user: { label: 'Usuario' }, + smtp_password: { label: 'Contraseña' }, + api_key: { label: 'Clave de API' }, + from_email: { label: 'Correo del remitente', help: 'Ejemplo: no-reply@example.com' }, + from_name: { label: 'Nombre del remitente' }, + }, + actions: { + test: { label: 'Enviar correo de prueba' }, + }, + }, + + branding: { + title: 'Marca', + description: 'Nombre del espacio de trabajo, logotipo y color de acento.', + groups: { + identity: { title: 'Identidad' }, + appearance: { title: 'Apariencia' }, + }, + keys: { + workspace_name: { label: 'Nombre del espacio de trabajo' }, + support_email: { label: 'Correo de soporte', help: 'Ejemplo: support@example.com' }, + theme_mode: { + label: 'Tema predeterminado', + options: { light: 'Claro', dark: 'Oscuro', system: 'Según el sistema' }, + }, + accent_color: { label: 'Color de acento' }, + logo_url: { label: 'URL del logotipo', help: 'Ejemplo: https://…/logo.svg' }, + }, + }, + + feature_flags: { + title: 'Indicadores de función', + description: 'Activa funciones experimentales y en beta para este espacio de trabajo.', + groups: { + productivity: { title: 'Productividad' }, + collaboration: { title: 'Colaboración' }, + }, + keys: { + ai_enabled: { + label: 'Asistente de IA', + help: 'Habilita el panel del asistente de IA dentro de la aplicación.', + }, + kanban_swimlanes: { label: 'Carriles de Kanban' }, + realtime_cursors: { label: 'Cursores en tiempo real' }, + inline_comments: { label: 'Comentarios en línea' }, + }, + }, + + storage: { + title: 'Almacenamiento de archivos', + description: + 'Backend usado para adjuntos, exportaciones y subidas de usuarios. ' + + '⚠ Cambiar de adaptador no migra los archivos existentes: los archivos ' + + 'subidos con el adaptador anterior dejan de ser accesibles a través ' + + 'del nuevo.', + groups: { + adapter: { title: 'Backend', description: 'Elige dónde se almacenan los archivos subidos.' }, + local: { title: 'Local' }, + s3: { title: 'S3' }, + limits: { title: 'Límites' }, + }, + keys: { + adapter: { + label: 'Adaptador', + options: { local: 'Sistema de archivos local', s3: 'S3 / compatible con S3' }, + }, + local_root: { label: 'Directorio raíz', + help: 'Ruta del sistema de archivos donde se almacenan los archivos. Las rutas relativas se resuelven desde el directorio de trabajo del servidor.' }, + s3_bucket: { label: 'Bucket', + help: 'Bucket compartido del host. Los archivos de cada entorno se aíslan mediante el prefijo projects//.' }, + s3_region: { label: 'Región', help: 'Ejemplo: us-east-1' }, + s3_endpoint: { label: 'Endpoint', + help: 'Endpoint personalizado para proveedores compatibles con S3 (R2, MinIO, Wasabi). Déjalo en blanco para AWS S3.' }, + s3_access_key_id: { label: 'Access Key ID' }, + s3_secret_access_key: { label: 'Secret Access Key' }, + s3_force_path_style: { label: 'Forzar URLs de tipo path', + help: 'Actívalo para MinIO y la mayoría de proveedores compatibles con S3; desactívalo para AWS S3.' }, + presigned_ttl: { label: 'TTL de URL prefirmada (segundos)' }, + session_ttl: { label: 'TTL de sesión de subida (segundos)', + help: 'Tiempo durante el cual una sesión de subida por fragmentos sigue siendo reanudable.' }, + max_upload_mb: { label: 'Tamaño máximo de subida (MB)' }, + }, + actions: { + test: { label: 'Probar conexión' }, + }, + }, + + ai: { + title: 'IA y Embedder', + description: + 'Proveedor de LLM, modelo, credenciales y configuración del embedder usados por ' + + 'los servicios de IA y de conocimiento de la plataforma.', + groups: { + provider: { title: 'Proveedor', + description: 'Elige el backend de LLM. El modo Memory repite la entrada: útil para pruebas, nunca para producción.' }, + gateway: { title: 'Vercel AI Gateway', + description: 'Enrutador multiproveedor. La especificación del modelo sigue `provider/model`, p. ej. `openai/gpt-4o`.' }, + openai: { title: 'OpenAI' }, + anthropic: { title: 'Anthropic' }, + google: { title: 'Google' }, + defaults: { title: 'Valores predeterminados de generación', + description: 'Se aplican cuando un agente o una solicitud de chat no especifica su propio valor.' }, + observability: { title: 'Observabilidad' }, + embedder: { title: 'Embedder', + description: + 'Proveedor de texto → vector usado por las fuentes de conocimiento y RAG. ' + + 'Independiente del proveedor de chat anterior.' }, + }, + keys: { + provider: { + label: 'Proveedor', + options: { + memory: 'Memory (eco — solo pruebas)', + gateway: 'Vercel AI Gateway', + openai: 'OpenAI', + anthropic: 'Anthropic', + google: 'Google Generative AI', + }, + }, + gateway_model: { label: 'Modelo de Gateway', + help: 'Se reenvía como AI_GATEWAY_MODEL. Ejemplo: openai/gpt-4o' }, + gateway_api_key: { label: 'Clave de API de Gateway', + help: 'Opcional: solo se requiere si el gateway exige autenticación.' }, + openai_api_key: { label: 'Clave de API de OpenAI', + help: 'Se reenvía como OPENAI_API_KEY. Se almacena cifrada en reposo.' }, + openai_model: { label: 'Modelo', + help: 'ID de modelo predeterminado. Las anulaciones por agente tienen prioridad.' }, + openai_base_url: { label: 'Base URL', + help: 'Anulación para Azure OpenAI o gateways autoalojados. Déjalo en blanco para api.openai.com.' }, + anthropic_api_key: { label: 'Clave de API de Anthropic', + help: 'Se reenvía como ANTHROPIC_API_KEY. Se almacena cifrada en reposo.' }, + anthropic_model: { label: 'Modelo' }, + google_api_key: { label: 'Clave de API de Google', + help: 'Se reenvía como GOOGLE_GENERATIVE_AI_API_KEY. Se almacena cifrada en reposo.' }, + google_model: { label: 'Modelo' }, + temperature: { label: 'Temperatura', + help: '0 = determinista, 2 = muy creativo.' }, + max_tokens: { label: 'Máximo de tokens de salida', + help: 'Límite estricto de tokens generados por respuesta.' }, + request_timeout_ms: { label: 'Tiempo de espera de la solicitud (ms)' }, + trace_enabled: { label: 'Registrar trazas', + help: 'Persiste las trazas de prompt/respuesta en sys_ai_trace para depuración y reproducción.' }, + log_prompts: { label: 'Registrar prompts completos', + help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza. ⚠ Puede filtrar PII: desactívalo en entornos regulados.' }, + embedder_provider: { + label: 'Proveedor', + options: { + none: 'Deshabilitado (sin embeddings)', + openai: 'OpenAI', + azure: 'Azure OpenAI', + dashscope: '阿里通义 DashScope', + zhipu: '智谱 BigModel', + siliconflow: '硅基流动 SiliconFlow', + doubao: '火山引擎 Doubao', + minimax: 'MiniMax', + ollama: 'Ollama (local)', + custom: 'Personalizado (compatible con OpenAI)', + }, + }, + embedder_api_key: { label: 'Clave de API del embedder', + help: 'Token bearer enviado en la cabecera Authorization. Para Ollama sirve cualquier valor no vacío.' }, + embedder_model: { label: 'Modelo', + help: 'Ejemplos — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3' }, + embedder_base_url: { label: 'Base URL', + help: 'Raíz del endpoint (sin /embeddings). Se autocompleta desde el preset; anúlalo para proxys o gateways autoalojados.' }, + embedder_dimensions: { label: 'Dimensiones', + help: 'Anula la dimensionalidad de salida (solo modelos Matryoshka). Déjalo en blanco para usar el valor predeterminado del modelo.' }, + embedder_batch_size: { label: 'Tamaño de lote', + help: 'Fragmentos por llamada a embed(). Redúcelo si alcanzas los límites de tasa o tamaño del proveedor.' }, + }, + actions: { + test: { label: 'Probar conexión' }, + test_embedder: { label: 'Probar embedder' }, + }, + }, + + knowledge: { + title: 'Conocimiento', + description: + 'Backend de almacén de vectores para RAG / fuentes de conocimiento. ' + + '⚠ Cambiar de adaptador NO migra los índices existentes.', + groups: { + adapter: { title: 'Backend', + description: 'Elige dónde se almacenan los fragmentos de documento y sus vectores.' }, + turso: { title: 'Turso / libSQL', + description: 'Funciona con Turso gestionado, archivo local o en memoria.' }, + ragflow: { title: 'RAGFlow', + description: 'Despliegue externo de RAGFlow. Consulta https://ragflow.io para instrucciones de autoalojamiento.' }, + indexing: { title: 'Valores predeterminados de indexación', + description: 'Los valores por fuente en KnowledgeSource.adapterConfig tienen prioridad.' }, + permissions: { title: 'Permisos' }, + }, + keys: { + adapter: { + label: 'Adaptador', + options: { + memory: 'En memoria (solo desarrollo / pruebas)', + turso: 'Turso / libSQL (nube o local)', + ragflow: 'RAGFlow (externo)', + }, + }, + turso_url: { label: 'URL de conexión', + help: 'Ejemplos: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:' }, + turso_auth_token: { label: 'Token de autenticación', + help: 'Solo se requiere para URLs de Turso gestionado.' }, + ragflow_base_url: { label: 'Base URL', help: 'Ejemplo: http://localhost:9380' }, + ragflow_api_key: { label: 'Clave de API' }, + ragflow_default_dataset: { label: 'ID de dataset predeterminado', + help: 'Se usa cuando una KnowledgeSource no especifica su propio dataset de RAGFlow.' }, + chunk_target: { label: 'Tamaño objetivo de fragmento (caracteres)', + help: 'Límite flexible del tamaño de fragmento antes de que actúe la división consciente de tokens.' }, + chunk_overlap: { label: 'Solapamiento de fragmentos (caracteres)', + help: 'Caracteres conservados del fragmento anterior para que el contexto sobreviva al límite.' }, + over_fetch: { label: 'Multiplicador de sobre-obtención', + help: 'Se obtienen topK × overFetch candidatos internos para que el filtrado de metadatos en JS siga teniendo filas.' }, + enforce_rls: { label: 'Aplicar RLS en la búsqueda', + help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante. ⚠ Desactivarlo omite la salvaguarda exclusiva de la plataforma.' }, + }, + actions: { + test: { label: 'Probar conexión' }, + }, + }, + }, +}; diff --git a/packages/services/service-settings/src/translations/index.ts b/packages/services/service-settings/src/translations/index.ts index 8ee45c376d..8f90f32cb5 100644 --- a/packages/services/service-settings/src/translations/index.ts +++ b/packages/services/service-settings/src/translations/index.ts @@ -15,11 +15,13 @@ import type { TranslationBundle } from '@objectstack/spec/system'; import { en } from './en.js'; import { zhCN } from './zh-CN.js'; import { jaJP } from './ja-JP.js'; +import { esES } from './es-ES.js'; -export { en, zhCN, jaJP }; +export { en, zhCN, jaJP, esES }; export const settingsBuiltinTranslations: TranslationBundle = { en, 'zh-CN': zhCN, 'ja-JP': jaJP, + 'es-ES': esES, }; diff --git a/packages/spec/src/system/i18n-resolver.test.ts b/packages/spec/src/system/i18n-resolver.test.ts index 028f97e244..ab5da56709 100644 --- a/packages/spec/src/system/i18n-resolver.test.ts +++ b/packages/spec/src/system/i18n-resolver.test.ts @@ -481,3 +481,154 @@ describe('resolveMetadataFormLabels', () => { expect(out.sections[0].label).toBe('Basics (en)'); }); }); + +import { + translateApp, + translateDashboard, + resolveViewLabel as _resolveViewLabel, +} from './i18n-resolver'; + +describe('locale fallback resolution (BCP-47)', () => { + const bundle: TranslationBundle = { + 'zh-CN': { + objects: { account: { label: '客户' } }, + }, + }; + + it('resolves base language to a registered region variant (zh → zh-CN)', () => { + const out = translateMetadataDocument( + 'object', + { name: 'account', label: 'Account' }, + bundle, + { locale: 'zh' }, + ); + expect(out.label).toBe('客户'); + }); + + it('resolves case-insensitively (zh-cn → zh-CN)', () => { + const out = translateMetadataDocument( + 'object', + { name: 'account', label: 'Account' }, + bundle, + { locale: 'zh-cn' }, + ); + expect(out.label).toBe('客户'); + }); + + it('resolves a region-qualified request down to base/other variant (zh-TW → zh-CN)', () => { + const out = translateMetadataDocument( + 'object', + { name: 'account', label: 'Account' }, + bundle, + { locale: 'zh-TW' }, + ); + expect(out.label).toBe('客户'); + }); + + it('falls back to literal when no related locale is registered', () => { + const out = translateMetadataDocument( + 'object', + { name: 'account', label: 'Account' }, + bundle, + { locale: 'fr', fallbackChain: [] }, + ); + expect(out.label).toBe('Account'); + }); +}); + +describe('translateApp', () => { + const bundle: TranslationBundle = { + 'zh-CN': { + apps: { + setup: { + label: '系统设置', + description: '平台设置与管理', + navigation: { + group_overview: { label: '总览' }, + nav_users: { label: '用户' }, + }, + }, + }, + }, + }; + + const app = { + name: 'setup', + label: 'Setup', + description: 'Platform settings and administration', + navigation: [ + { + id: 'group_overview', + type: 'group', + label: 'Overview', + children: [{ id: 'nav_users', type: 'object', label: 'Users' }], + }, + ], + }; + + it('translates app label/description and nested navigation labels', () => { + const out = translateApp(app, bundle, { locale: 'zh-CN' }); + expect(out.label).toBe('系统设置'); + expect(out.description).toBe('平台设置与管理'); + expect(out.navigation[0].label).toBe('总览'); + expect(out.navigation[0].children[0].label).toBe('用户'); + }); + + it('works through translateMetadataDocument with app type', () => { + const out = translateMetadataDocument('app', app, bundle, { locale: 'zh' }); + expect(out.navigation[0].children[0].label).toBe('用户'); + }); + + it('does not mutate the input app', () => { + const snapshot = JSON.parse(JSON.stringify(app)); + translateApp(app, bundle, { locale: 'zh-CN' }); + expect(app).toEqual(snapshot); + }); + + it('falls back to literal labels when no translation present', () => { + const out = translateApp(app, undefined, { locale: 'zh-CN' }); + expect(out.label).toBe('Setup'); + expect(out.navigation[0].label).toBe('Overview'); + }); +}); + +describe('translateDashboard', () => { + const bundle: TranslationBundle = { + 'zh-CN': { + dashboards: { + system_overview: { + label: '系统概览', + widgets: { + widget_total_users: { title: '用户总数', description: '系统中注册的用户总数' }, + }, + }, + }, + }, + }; + + const dashboard = { + name: 'system_overview', + label: 'System Overview', + widgets: [ + { id: 'widget_total_users', title: 'Total Users', description: 'Total registered users' }, + { id: 'widget_other', title: 'Other' }, + ], + }; + + it('translates dashboard label and widget title/description', () => { + const out = translateDashboard(dashboard, bundle, { locale: 'zh-CN' }); + expect(out.label).toBe('系统概览'); + expect(out.widgets[0].title).toBe('用户总数'); + expect(out.widgets[0].description).toBe('系统中注册的用户总数'); + }); + + it('leaves widgets without a translation entry unchanged', () => { + const out = translateDashboard(dashboard, bundle, { locale: 'zh-CN' }); + expect(out.widgets[1].title).toBe('Other'); + }); + + it('works through translateMetadataDocument with dashboard type', () => { + const out = translateMetadataDocument('dashboard', dashboard, bundle, { locale: 'zh-CN' }); + expect(out.label).toBe('系统概览'); + }); +}); diff --git a/packages/spec/src/system/i18n-resolver.ts b/packages/spec/src/system/i18n-resolver.ts index cb7a035e83..50647edbc6 100644 --- a/packages/spec/src/system/i18n-resolver.ts +++ b/packages/spec/src/system/i18n-resolver.ts @@ -59,12 +59,53 @@ export interface ResolveOptions { fallbackChain?: string[]; } +/** + * Resolve a requested locale code against the locales actually present in a + * bundle, applying BCP-47 fallback so callers that pass a base language + * (e.g. `zh`) or a differently-cased / region-qualified variant still hit the + * available data (e.g. `zh-CN`). Mirrors `resolveLocale` in + * `@objectstack/core` but is inlined here so `@objectstack/spec` stays + * dependency-free. + * + * Order: exact → case-insensitive → base-language → variant-expansion. + * Returns the matched bundle key, or `undefined` when nothing matches. + */ +function resolveBundleLocale( + bundle: TranslationBundle, + requested: string, +): string | undefined { + // 1. Exact match (fast path). + if (bundle[requested] !== undefined) return requested; + + const available = Object.keys(bundle); + if (available.length === 0) return undefined; + + const lower = requested.toLowerCase(); + // 2. Case-insensitive match (e.g. `zh-cn` → `zh-CN`). + const caseMatch = available.find((code) => code.toLowerCase() === lower); + if (caseMatch) return caseMatch; + + const base = lower.split('-')[0]; + // 3. Base-language match (e.g. `zh-CN` → `zh`). + const baseMatch = available.find((code) => code.toLowerCase() === base); + if (baseMatch) return baseMatch; + + // 4. Variant expansion (e.g. `zh` → `zh-CN`; first registered variant wins). + const variantMatch = available.find((code) => code.toLowerCase().split('-')[0] === base); + if (variantMatch) return variantMatch; + + return undefined; +} + function pickData( bundle: TranslationBundle | undefined, locale: string, ): TranslationData | undefined { if (!bundle) return undefined; - return bundle[locale]; + const exact = bundle[locale]; + if (exact !== undefined) return exact; + const resolved = resolveBundleLocale(bundle, locale); + return resolved !== undefined ? bundle[resolved] : undefined; } function localeChain(opts?: ResolveOptions): string[] { @@ -248,9 +289,196 @@ export function translateMetadataDocument( if (type === 'view') return translateView(doc, bundle, opts); if (type === 'action') return translateAction(doc, bundle, opts); if (type === 'object') return translateObject(doc, bundle, opts); + if (type === 'app') return translateApp(doc, bundle, opts); + if (type === 'dashboard') return translateDashboard(doc, bundle, opts); return doc; } +// ──────────────────────────────────────────────────────────────────────────── +// App metadata resolvers (label / description / navigation labels) +// ──────────────────────────────────────────────────────────────────────────── + +/** Minimal navigation-node shape consumed by `translateApp`. */ +export interface NavNodeLike { + id?: string; + label?: string; + children?: NavNodeLike[]; + [key: string]: any; +} + +/** Minimal app metadata shape consumed by `translateApp`. */ +export interface AppLike { + name: string; + label?: string; + description?: string; + navigation?: NavNodeLike[]; + [key: string]: any; +} + +function lookupAppAttr( + bundle: TranslationBundle | undefined, + appName: string, + attr: 'label' | 'description', + opts?: ResolveOptions, +): string | undefined { + if (!bundle) return undefined; + for (const code of localeChain(opts)) { + const candidate = pickData(bundle, code)?.apps?.[appName]?.[attr]; + if (typeof candidate === 'string' && candidate.length > 0) return candidate; + } + return undefined; +} + +function lookupNavLabel( + bundle: TranslationBundle | undefined, + appName: string, + navId: string, + opts?: ResolveOptions, +): string | undefined { + if (!bundle) return undefined; + for (const code of localeChain(opts)) { + const candidate = pickData(bundle, code)?.apps?.[appName]?.navigation?.[navId]?.label; + if (typeof candidate === 'string' && candidate.length > 0) return candidate; + } + return undefined; +} + +/** + * Apply the active locale to an app metadata document — translates the app's + * `label` / `description` and walks the (possibly nested) `navigation` tree, + * replacing each node's `label` with `apps..navigation..label` when a + * translation exists. The input document is not mutated. + * + * Translation keys are addressed by the stable navigation-node `id` + * (e.g. `group_overview`, `nav_users`) — the same flat keyspace used by the + * `apps..navigation` map, regardless of tree depth. + */ +export function translateApp( + doc: T, + bundle: TranslationBundle | undefined, + opts?: ResolveOptions, +): T { + if (!doc || typeof doc !== 'object') return doc; + const appName = doc.name; + if (!appName || !bundle) return doc; + + const label = lookupAppAttr(bundle, appName, 'label', opts) ?? doc.label; + const description = lookupAppAttr(bundle, appName, 'description', opts) ?? doc.description; + + const translateNav = (node: NavNodeLike): NavNodeLike => { + if (!node || typeof node !== 'object') return node; + const next: NavNodeLike = { ...node }; + if (typeof node.id === 'string') { + const translated = lookupNavLabel(bundle, appName, node.id, opts); + if (translated) next.label = translated; + } + if (Array.isArray(node.children)) { + next.children = node.children.map(translateNav); + } + return next; + }; + + const navigation = Array.isArray(doc.navigation) + ? doc.navigation.map(translateNav) + : doc.navigation; + + return { + ...doc, + ...(label !== undefined ? { label } : {}), + ...(description !== undefined ? { description } : {}), + ...(navigation !== undefined ? { navigation } : {}), + }; +} + +// ──────────────────────────────────────────────────────────────────────────── +// Dashboard metadata resolvers (label / description / widget titles) +// ──────────────────────────────────────────────────────────────────────────── + +/** Minimal widget shape consumed by `translateDashboard`. */ +export interface WidgetLike { + id?: string; + title?: string; + description?: string; + [key: string]: any; +} + +/** Minimal dashboard metadata shape consumed by `translateDashboard`. */ +export interface DashboardLike { + name: string; + label?: string; + description?: string; + widgets?: WidgetLike[]; + [key: string]: any; +} + +function lookupDashboardAttr( + bundle: TranslationBundle | undefined, + name: string, + attr: 'label' | 'description', + opts?: ResolveOptions, +): string | undefined { + if (!bundle) return undefined; + for (const code of localeChain(opts)) { + const candidate = pickData(bundle, code)?.dashboards?.[name]?.[attr]; + if (typeof candidate === 'string' && candidate.length > 0) return candidate; + } + return undefined; +} + +function lookupWidgetAttr( + bundle: TranslationBundle | undefined, + dashboardName: string, + widgetId: string, + attr: 'title' | 'description', + opts?: ResolveOptions, +): string | undefined { + if (!bundle) return undefined; + for (const code of localeChain(opts)) { + const candidate = + pickData(bundle, code)?.dashboards?.[dashboardName]?.widgets?.[widgetId]?.[attr]; + if (typeof candidate === 'string' && candidate.length > 0) return candidate; + } + return undefined; +} + +/** + * Apply the active locale to a dashboard metadata document — translates the + * dashboard's `label` / `description` and each widget's `title` / + * `description` against `dashboards..widgets..*`. The input document + * is not mutated. + */ +export function translateDashboard( + doc: T, + bundle: TranslationBundle | undefined, + opts?: ResolveOptions, +): T { + if (!doc || typeof doc !== 'object') return doc; + const name = doc.name; + if (!name || !bundle) return doc; + + const label = lookupDashboardAttr(bundle, name, 'label', opts) ?? doc.label; + const description = lookupDashboardAttr(bundle, name, 'description', opts) ?? doc.description; + + const widgets = Array.isArray(doc.widgets) + ? doc.widgets.map((w) => { + if (!w || typeof w !== 'object' || typeof w.id !== 'string') return w; + const next: WidgetLike = { ...w }; + const title = lookupWidgetAttr(bundle, name, w.id, 'title', opts); + if (title) next.title = title; + const desc = lookupWidgetAttr(bundle, name, w.id, 'description', opts); + if (desc) next.description = desc; + return next; + }) + : doc.widgets; + + return { + ...doc, + ...(label !== undefined ? { label } : {}), + ...(description !== undefined ? { description } : {}), + ...(widgets !== undefined ? { widgets } : {}), + }; +} + // ──────────────────────────────────────────────────────────────────────────── // Object metadata resolvers (label / pluralLabel / description / fields / options) // ──────────────────────────────────────────────────────────────────────────── From ad7cf1631e678cc5d2d20200d8c881903cd3cfaf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 02:45:55 +0000 Subject: [PATCH 2/2] i18n(platform-objects): add missing objects, translate placeholder strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add translations for sys_share_link, sys_view_definition and sys_metadata_audit across en/zh-CN/ja-JP/es-ES (label, fields, help, picklist options, views) — previously absent from every bundle. - Translate English placeholder strings left in the zh-CN/ja-JP/es-ES object and metadata-form bundles: action confirmText/successMessage prompts, permission-set field labels/help, picklist option labels for the new objects, and metadata-type form labels. - Remaining identical leaves are intentional (acronyms/loanwords/format hints: URL, Token, Webhook, API, snake_case, GET/POST/..., and Spanish-identical words like Actor/Error/Global). https://claude.ai/code/session_01ASWzynvrsx5WRU2fUR116h --- .../apps/translations/en.objects.generated.ts | 235 +++++++++++++ .../es-ES.metadata-forms.generated.ts | 2 +- .../translations/es-ES.objects.generated.ts | 321 +++++++++++++++--- .../translations/ja-JP.objects.generated.ts | 301 ++++++++++++++-- .../zh-CN.metadata-forms.generated.ts | 48 +-- .../translations/zh-CN.objects.generated.ts | 301 ++++++++++++++-- 6 files changed, 1074 insertions(+), 134 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index bfc3ff659e..57d4745b5f 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -1556,6 +1556,100 @@ export const enObjects: NonNullable = { } } }, + sys_share_link: { + label: "Share Link", + pluralLabel: "Share Links", + description: "Opaque capability token granting access to a single record. Notion/Figma-style public link sharing.", + fields: { + id: { + label: "Link ID" + }, + token: { + label: "Token", + help: "Opaque URL-safe random token (≥ 22 chars). The only secret in this row." + }, + object_name: { + label: "Object", + help: "Short object name of the shared record (e.g. ai_conversation, contracts_contract)" + }, + record_id: { + label: "Record", + help: "Primary key of the shared record within object_name" + }, + permission: { + label: "Permission", + help: "What the link holder can do with the record", + options: { + view: "View", + comment: "Comment", + edit: "Edit" + } + }, + audience: { + label: "Audience", + help: "Gating layer applied on top of the token check", + options: { + public: "Public (indexable)", + link_only: "Anyone with the link", + signed_in: "Signed-in users", + email: "Specific emails" + } + }, + expires_at: { + label: "Expires At", + help: "When set, resolveToken returns null after this timestamp" + }, + email_allowlist: { + label: "Email Allowlist", + help: "Lowercased addresses checked when audience=email" + }, + password_hash: { + label: "Password Hash", + help: "Argon2/bcrypt hash. When set, the UI prompts for a password before rendering." + }, + redact_fields: { + label: "Per-Link Redactions", + help: "Extra fields stripped from the response, on top of the object-default set" + }, + label: { + label: "Label", + help: "Free-text shown in the share dialog (e.g. \"ACME Q3 contract\")" + }, + revoked_at: { + label: "Revoked At", + help: "When set, the link is permanently disabled" + }, + created_by: { + label: "Created By", + help: "Issuer of the link" + }, + created_at: { + label: "Created At" + }, + last_used_at: { + label: "Last Used At", + help: "Stamped by resolveToken; used by the dashboard to highlight active links" + }, + use_count: { + label: "Use Count", + help: "Incremented by resolveToken on every successful resolution" + } + }, + _views: { + active_links: { + label: "Active" + }, + by_me: { + label: "Created by Me" + }, + revoked: { + label: "Revoked" + }, + all_links: { + label: "All" + } + } + }, sys_audit_log: { label: "Audit Log", pluralLabel: "Audit Logs", @@ -2861,6 +2955,147 @@ export const enObjects: NonNullable = { } } }, + sys_view_definition: { + label: "View Definition", + pluralLabel: "View Definitions", + description: "Runtime-authored view definitions (shared / personal layers). The package layer ships from source.", + fields: { + id: { + label: "ID" + }, + name: { + label: "Name" + }, + object: { + label: "Object" + }, + view_kind: { + label: "View Kind", + options: { + list: "list", + form: "form" + } + }, + label: { + label: "Label" + }, + is_default: { + label: "Is Default" + }, + view_order: { + label: "Order" + }, + scope: { + label: "Scope", + options: { + shared: "shared", + personal: "personal" + } + }, + owner: { + label: "Owner" + }, + hidden: { + label: "Hidden" + }, + config: { + label: "Config", + help: "ListView or FormView configuration (matches spec ViewItem.config)." + }, + organization_id: { + label: "Organization", + help: "Organization for multi-tenant isolation." + }, + state: { + label: "State", + options: { + draft: "draft", + active: "active", + archived: "archived" + } + }, + created_by: { + label: "Created By" + }, + created_at: { + label: "Created At" + }, + updated_by: { + label: "Updated By" + }, + updated_at: { + label: "Updated At" + } + } + }, + sys_metadata_audit: { + label: "Metadata Audit", + pluralLabel: "Metadata Audit", + description: "Append-only audit trail of metadata write decisions (ADR-0010).", + fields: { + id: { + label: "ID" + }, + occurred_at: { + label: "Occurred At" + }, + actor: { + label: "Actor", + help: "Acting principal — user id, system id, or \"system\"." + }, + source: { + label: "Source" + }, + type: { + label: "Metadata Type" + }, + name: { + label: "Name" + }, + organization_id: { + label: "Organization" + }, + operation: { + label: "Operation", + options: { + save: "save", + publish: "publish", + rollback: "rollback", + delete: "delete", + reset: "reset" + } + }, + outcome: { + label: "Outcome", + options: { + allowed: "allowed", + denied: "denied", + forced: "forced" + } + }, + code: { + label: "Code" + }, + lock_state: { + label: "Lock State", + options: { + none: "none", + "no-overlay": "no-overlay", + "no-delete": "no-delete", + full: "full" + } + }, + lock_overridden: { + label: "Lock Overridden" + }, + request_id: { + label: "Request ID" + }, + note: { + label: "Note" + } + } + }, sys_setting: { label: "Setting", pluralLabel: "Settings", diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index 492a05f1a9..8420620f2f 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -857,7 +857,7 @@ export const esESMetadataForms: NonNullable = label: "Traducción" }, router: { - label: "Router" + label: "Enrutador" }, function: { label: "Función" diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index e852abbe01..91ac34f6d0 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -110,7 +110,7 @@ export const esESObjects: NonNullable = { }, delete_my_account: { label: "Eliminar mi cuenta", - confirmText: "Permanently delete your account? This cannot be undone — all your sessions will be terminated and all data you own will be removed per the configured retention policy.", + confirmText: "¿Eliminar tu cuenta de forma permanente? Esta acción no se puede deshacer: se cerrarán todas tus sesiones y se eliminarán todos los datos de tu propiedad según la política de retención configurada.", successMessage: "Cuenta eliminada" } } @@ -243,7 +243,7 @@ export const esESObjects: NonNullable = { }, unlink_account: { label: "Desvincular cuenta", - confirmText: "Unlink this identity link? The user will no longer be able to sign in with this provider until they re-link it from their account settings.", + confirmText: "¿Desvincular este vínculo de identidad? El usuario ya no podrá iniciar sesión con este proveedor hasta que lo vuelva a vincular desde la configuración de su cuenta.", successMessage: "Vínculo de identidad eliminado" } } @@ -377,7 +377,7 @@ export const esESObjects: NonNullable = { }, transfer_ownership: { label: "Transferir propiedad", - confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.", + confirmText: "¿Transferir la propiedad de esta organización al miembro seleccionado? Pasarás a ser administrador y perderás los privilegios exclusivos del propietario.", successMessage: "Propiedad transferida" } } @@ -465,7 +465,7 @@ export const esESObjects: NonNullable = { }, reject_invitation: { label: "Rechazar invitación", - confirmText: "Decline this invitation? The inviter will be notified and you will need a new invitation to join.", + confirmText: "¿Rechazar esta invitación? Se notificará a quien la envió y necesitarás una nueva invitación para unirte.", successMessage: "Invitación rechazada" } } @@ -781,7 +781,7 @@ export const esESObjects: NonNullable = { }, regenerate_backup_codes: { label: "Regenerar códigos de respaldo", - confirmText: "Regenerate backup codes? All previous backup codes will stop working immediately." + confirmText: "¿Regenerar los códigos de respaldo? Todos los códigos de respaldo anteriores dejarán de funcionar de inmediato." } } }, @@ -1010,26 +1010,26 @@ export const esESObjects: NonNullable = { }, _actions: { disable_oauth_application: { - label: "Disable OAuth Application", - confirmText: "Disable this OAuth application? Active access/refresh tokens issued to it will continue to be rejected at the token, authorize, and introspect endpoints. Existing integrations will stop working immediately.", - successMessage: "OAuth application disabled" + label: "Deshabilitar aplicación OAuth", + confirmText: "¿Deshabilitar esta aplicación OAuth? Los tokens de acceso/actualización activos emitidos para ella seguirán siendo rechazados en los endpoints token, authorize e introspect. Las integraciones existentes dejarán de funcionar de inmediato.", + successMessage: "Aplicación OAuth deshabilitada" }, enable_oauth_application: { - label: "Enable OAuth Application", - confirmText: "Re-enable this OAuth application? Token issuance, authorization, and introspection will resume immediately.", - successMessage: "OAuth application enabled" + label: "Habilitar aplicación OAuth", + confirmText: "¿Volver a habilitar esta aplicación OAuth? La emisión de tokens, la autorización y la introspección se reanudarán de inmediato.", + successMessage: "Aplicación OAuth habilitada" }, create_oauth_application: { - label: "Register OAuth Application" + label: "Registrar aplicación OAuth" }, rotate_client_secret: { label: "Rotar Client Secret", - confirmText: "Rotate this OAuth client's secret? The previous secret will stop working immediately and any integrations using it will break until they are updated with the new secret. The new secret is shown only once." + confirmText: "¿Rotar el secreto de este cliente OAuth? El secreto anterior dejará de funcionar de inmediato y cualquier integración que lo utilice fallará hasta que se actualice con el nuevo secreto. El nuevo secreto se muestra una sola vez." }, delete_oauth_application: { - label: "Delete OAuth Application", - confirmText: "Permanently delete this OAuth application? All issued tokens and consents will be invalidated and integrations using this client_id will stop working immediately. This cannot be undone.", - successMessage: "OAuth application deleted" + label: "Eliminar aplicación OAuth", + confirmText: "¿Eliminar de forma permanente esta aplicación OAuth? Todos los tokens y consentimientos emitidos quedarán invalidados y las integraciones que usen este client_id dejarán de funcionar de inmediato. Esta acción no se puede deshacer.", + successMessage: "Aplicación OAuth eliminada" } } }, @@ -1240,12 +1240,12 @@ export const esESObjects: NonNullable = { }, deactivate_role: { label: "Desactivar rol", - confirmText: "Deactivate this role? Users with the role keep their assignment but the role stops granting permissions until re-activated.", + confirmText: "¿Desactivar este rol? Los usuarios con el rol conservan su asignación, pero el rol deja de otorgar permisos hasta que se vuelva a activar.", successMessage: "Rol desactivado" }, set_default_role: { label: "Establecer como predeterminado", - confirmText: "Make this the default role for new users? Existing users are unaffected.", + confirmText: "¿Convertir este en el rol predeterminado para los nuevos usuarios? Los usuarios existentes no se ven afectados.", successMessage: "Rol predeterminado actualizado" }, clone_role: { @@ -1278,16 +1278,16 @@ export const esESObjects: NonNullable = { help: "Permisos de lectura/escritura a nivel de campo serializados en JSON." }, system_permissions: { - label: "System Permissions", - help: "JSON-serialized array of system capability names (e.g. [\"setup.access\",\"studio.access\",\"manage_users\"])" + label: "Permisos del sistema", + help: "Array serializado en JSON de nombres de capacidades del sistema (p. ej. [\"setup.access\",\"studio.access\",\"manage_users\"])" }, row_level_security: { - label: "Row-Level Security", - help: "JSON-serialized array of row-level security policies (USING/CHECK clauses)" + label: "Seguridad a nivel de fila", + help: "Array serializado en JSON de políticas de seguridad a nivel de fila (cláusulas USING/CHECK)" }, tab_permissions: { - label: "Tab Permissions", - help: "JSON-serialized map of app tab visibility (visible | hidden | default_on | default_off)" + label: "Permisos de pestañas", + help: "Mapa serializado en JSON de la visibilidad de las pestañas de la app (visible | hidden | default_on | default_off)" }, active: { label: "Activo" @@ -1320,7 +1320,7 @@ export const esESObjects: NonNullable = { }, deactivate_permission_set: { label: "Desactivar", - confirmText: "Deactivate this permission set? Existing assignments stay in place but stop granting access until re-activated.", + confirmText: "¿Desactivar este conjunto de permisos? Las asignaciones existentes se mantienen, pero dejan de otorgar acceso hasta que se vuelva a activar.", successMessage: "Conjunto de permisos desactivado" }, clone_permission_set: { @@ -1556,6 +1556,100 @@ export const esESObjects: NonNullable = { } } }, + sys_share_link: { + label: "Enlace de uso compartido", + pluralLabel: "Enlaces de uso compartido", + description: "Token de capacidad opaco que concede acceso a un único registro. Uso compartido mediante enlace público al estilo de Notion/Figma.", + fields: { + id: { + label: "ID del enlace" + }, + token: { + label: "Token", + help: "Token aleatorio opaco seguro para URL (≥ 22 caracteres). El único secreto de esta fila." + }, + object_name: { + label: "Objeto", + help: "Nombre corto del objeto del registro compartido (p. ej. ai_conversation, contracts_contract)" + }, + record_id: { + label: "Registro", + help: "Clave principal del registro compartido dentro de object_name" + }, + permission: { + label: "Permiso", + help: "Lo que el titular del enlace puede hacer con el registro", + options: { + view: "Ver", + comment: "Comentar", + edit: "Editar" + } + }, + audience: { + label: "Audiencia", + help: "Capa de control aplicada por encima de la verificación del token", + options: { + public: "Público (indexable)", + link_only: "Cualquier persona con el enlace", + signed_in: "Usuarios con sesión iniciada", + email: "Correos específicos" + } + }, + expires_at: { + label: "Caduca el", + help: "Cuando se establece, resolveToken devuelve null después de esta marca de tiempo" + }, + email_allowlist: { + label: "Lista de correos permitidos", + help: "Direcciones en minúsculas que se comprueban cuando audience=email" + }, + password_hash: { + label: "Hash de contraseña", + help: "Hash Argon2/bcrypt. Cuando se establece, la interfaz solicita una contraseña antes de mostrar el contenido." + }, + redact_fields: { + label: "Campos ocultos por enlace", + help: "Campos adicionales que se eliminan de la respuesta, además del conjunto predeterminado del objeto" + }, + label: { + label: "Etiqueta", + help: "Texto libre que se muestra en el cuadro de diálogo de uso compartido (p. ej. \"ACME Q3 contract\")" + }, + revoked_at: { + label: "Revocado el", + help: "Cuando se establece, el enlace queda deshabilitado permanentemente" + }, + created_by: { + label: "Creado por", + help: "Emisor del enlace" + }, + created_at: { + label: "Creado el" + }, + last_used_at: { + label: "Último uso el", + help: "Lo registra resolveToken; el panel lo usa para resaltar los enlaces activos" + }, + use_count: { + label: "Número de usos", + help: "Lo incrementa resolveToken en cada resolución correcta" + } + }, + _views: { + active_links: { + label: "Activos" + }, + by_me: { + label: "Creados por mí" + }, + revoked: { + label: "Revocados" + }, + all_links: { + label: "Todos" + } + } + }, sys_audit_log: { label: "Registro de auditoría", pluralLabel: "Registros de auditoría", @@ -1607,7 +1701,7 @@ export const esESObjects: NonNullable = { label: "Agente de usuario" }, tenant_id: { - label: "Tenant", + label: "Inquilino", help: "Contexto del tenant para el aislamiento multi-tenant." }, metadata: { @@ -2343,8 +2437,8 @@ export const esESObjects: NonNullable = { help: "Instantánea del registro en el momento del envío." }, process_hash: { - label: "Process Hash", - help: "sha256 of the approval process body at submit time (ADR-0009 execution pinning). Resolved through sys_metadata_history so process upgrades do not affect in-flight requests." + label: "Hash del proceso", + help: "sha256 del cuerpo del proceso de aprobación en el momento del envío (fijación de ejecución de ADR-0009). Se resuelve a través de sys_metadata_history para que las actualizaciones del proceso no afecten a las solicitudes en curso." }, completed_at: { label: "Completado el" @@ -2361,7 +2455,7 @@ export const esESObjects: NonNullable = { label: "Mis pendientes" }, submitted_by_me: { - label: "I Submitted" + label: "Enviadas por mí" }, completed: { label: "Completadas" @@ -2580,7 +2674,7 @@ export const esESObjects: NonNullable = { label: "Intentos máximos" }, backoff_type: { - label: "Backoff", + label: "Retroceso", options: { fixed: "Fijo", exponential: "Exponencial" @@ -2791,13 +2885,13 @@ export const esESObjects: NonNullable = { }, _views: { only_objects: { - label: "Objects" + label: "Objetos" }, only_fields: { - label: "Fields" + label: "Campos" }, all_metadata: { - label: "All" + label: "Todos" } } }, @@ -2810,8 +2904,8 @@ export const esESObjects: NonNullable = { label: "ID" }, event_seq: { - label: "Event Seq", - help: "Per-organization monotonic event log cursor." + label: "Secuencia de eventos", + help: "Cursor monotónico del registro de eventos por organización." }, name: { label: "Nombre" @@ -2847,7 +2941,7 @@ export const esESObjects: NonNullable = { help: "Descripción de lo que cambió en esta versión." }, source: { - label: "Source" + label: "Origen" }, organization_id: { label: "Organización", @@ -2861,6 +2955,147 @@ export const esESObjects: NonNullable = { } } }, + sys_view_definition: { + label: "Definición de vista", + pluralLabel: "Definiciones de vista", + description: "Definiciones de vista creadas en tiempo de ejecución (capas compartida / personal). La capa de paquete se distribuye desde el código fuente.", + fields: { + id: { + label: "ID" + }, + name: { + label: "Nombre" + }, + object: { + label: "Objeto" + }, + view_kind: { + label: "Tipo de vista", + options: { + list: "Lista", + form: "Formulario" + } + }, + label: { + label: "Etiqueta" + }, + is_default: { + label: "Es predeterminada" + }, + view_order: { + label: "Orden" + }, + scope: { + label: "Ámbito", + options: { + shared: "Compartida", + personal: "Personal" + } + }, + owner: { + label: "Propietario" + }, + hidden: { + label: "Oculta" + }, + config: { + label: "Configuración", + help: "Configuración de ListView o FormView (coincide con ViewItem.config de la especificación)." + }, + organization_id: { + label: "Organización", + help: "Organización para el aislamiento multiinquilino." + }, + state: { + label: "Estado", + options: { + draft: "Borrador", + active: "Activa", + archived: "Archivada" + } + }, + created_by: { + label: "Creado por" + }, + created_at: { + label: "Creado el" + }, + updated_by: { + label: "Actualizado por" + }, + updated_at: { + label: "Actualizado el" + } + } + }, + sys_metadata_audit: { + label: "Auditoría de metadatos", + pluralLabel: "Auditoría de metadatos", + description: "Registro de auditoría de solo adición de las decisiones de escritura de metadatos (ADR-0010).", + fields: { + id: { + label: "ID" + }, + occurred_at: { + label: "Ocurrido el" + }, + actor: { + label: "Actor", + help: "Principal que actúa: ID de usuario, ID de sistema o \"system\"." + }, + source: { + label: "Origen" + }, + type: { + label: "Tipo de metadatos" + }, + name: { + label: "Nombre" + }, + organization_id: { + label: "Organización" + }, + operation: { + label: "Operación", + options: { + save: "Guardar", + publish: "Publicar", + rollback: "Revertir", + delete: "Eliminar", + reset: "Restablecer" + } + }, + outcome: { + label: "Resultado", + options: { + allowed: "Permitido", + denied: "Denegado", + forced: "Forzado" + } + }, + code: { + label: "Código" + }, + lock_state: { + label: "Estado de bloqueo", + options: { + none: "Ninguno", + "no-overlay": "Sin superposición", + "no-delete": "Sin eliminación", + full: "Bloqueo total" + } + }, + lock_overridden: { + label: "Bloqueo anulado" + }, + request_id: { + label: "ID de solicitud" + }, + note: { + label: "Nota" + } + } + }, sys_setting: { label: "Ajuste", pluralLabel: "Ajustes", @@ -2888,9 +3123,9 @@ export const esESObjects: NonNullable = { help: "Capa de la jerarquía de resolución de configuración a la que pertenece esta fila.", options: { global: "Global", - tenant: "Tenant", + tenant: "Inquilino", user: "Usuario", - runtime: "Runtime" + runtime: "Tiempo de ejecución" } }, user_id: { @@ -2927,7 +3162,7 @@ export const esESObjects: NonNullable = { label: "Por espacio de nombres" }, tenant_only: { - label: "Tenant" + label: "Inquilino" }, user_only: { label: "Usuario" @@ -3003,7 +3238,7 @@ export const esESObjects: NonNullable = { help: "Capa de cascada en la que se escribió la fila.", options: { global: "Global", - tenant: "Tenant", + tenant: "Inquilino", user: "Usuario" } }, @@ -3030,7 +3265,7 @@ export const esESObjects: NonNullable = { api: "API", migration: "Migración", import: "Importar", - system: "System" + system: "Sistema" } }, reason: { @@ -3038,11 +3273,11 @@ export const esESObjects: NonNullable = { help: "Justificación en texto libre proporcionada por el actor (opcional)." }, old_hash: { - label: "Old Hash", + label: "Hash anterior", help: "SHA-256 del valor anterior (canonicalizado). Null cuando antes no estaba establecido." }, new_hash: { - label: "New Hash", + label: "Hash nuevo", help: "SHA-256 del valor nuevo (canonicalizado). Null al restablecer." }, encrypted: { diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index fe0e5dbacd..3384746037 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -110,7 +110,7 @@ export const jaJPObjects: NonNullable = { }, delete_my_account: { label: "アカウント削除", - confirmText: "Permanently delete your account? This cannot be undone — all your sessions will be terminated and all data you own will be removed per the configured retention policy.", + confirmText: "アカウントを完全に削除しますか?この操作は元に戻せません。すべてのセッションが終了され、設定された保持ポリシーに従って所有するすべてのデータが削除されます。", successMessage: "アカウントを削除しました" } } @@ -243,7 +243,7 @@ export const jaJPObjects: NonNullable = { }, unlink_account: { label: "連携解除", - confirmText: "Unlink this identity link? The user will no longer be able to sign in with this provider until they re-link it from their account settings.", + confirmText: "このID連携を解除しますか?ユーザーがアカウント設定から再度連携するまで、このプロバイダーでサインインできなくなります。", successMessage: "アイデンティティ連携を解除しました" } } @@ -377,7 +377,7 @@ export const jaJPObjects: NonNullable = { }, transfer_ownership: { label: "所有権の移譲", - confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.", + confirmText: "この組織の所有権を選択したメンバーに移譲しますか?あなたは管理者に降格され、所有者のみの権限を失います。", successMessage: "所有権を移譲しました" } } @@ -465,7 +465,7 @@ export const jaJPObjects: NonNullable = { }, reject_invitation: { label: "招待を辞退", - confirmText: "Decline this invitation? The inviter will be notified and you will need a new invitation to join.", + confirmText: "この招待を辞退しますか?招待者に通知され、参加するには新しい招待が必要になります。", successMessage: "招待を辞退しました" } } @@ -781,7 +781,7 @@ export const jaJPObjects: NonNullable = { }, regenerate_backup_codes: { label: "バックアップコード再生成", - confirmText: "Regenerate backup codes? All previous backup codes will stop working immediately." + confirmText: "バックアップコードを再生成しますか?以前のバックアップコードはすべて直ちに使用できなくなります。" } } }, @@ -1010,26 +1010,26 @@ export const jaJPObjects: NonNullable = { }, _actions: { disable_oauth_application: { - label: "Disable OAuth Application", - confirmText: "Disable this OAuth application? Active access/refresh tokens issued to it will continue to be rejected at the token, authorize, and introspect endpoints. Existing integrations will stop working immediately.", - successMessage: "OAuth application disabled" + label: "OAuthアプリケーションを無効化", + confirmText: "このOAuthアプリケーションを無効化しますか?発行済みの有効なアクセストークン/リフレッシュトークンは、token、authorize、introspect の各エンドポイントで引き続き拒否されます。既存の連携は直ちに動作しなくなります。", + successMessage: "OAuthアプリケーションを無効化しました" }, enable_oauth_application: { - label: "Enable OAuth Application", - confirmText: "Re-enable this OAuth application? Token issuance, authorization, and introspection will resume immediately.", - successMessage: "OAuth application enabled" + label: "OAuthアプリケーションを有効化", + confirmText: "このOAuthアプリケーションを再度有効化しますか?トークンの発行、認可、イントロスペクションが直ちに再開されます。", + successMessage: "OAuthアプリケーションを有効化しました" }, create_oauth_application: { - label: "Register OAuth Application" + label: "OAuthアプリケーションを登録" }, rotate_client_secret: { label: "クライアントシークレット更新", - confirmText: "Rotate this OAuth client's secret? The previous secret will stop working immediately and any integrations using it will break until they are updated with the new secret. The new secret is shown only once." + confirmText: "このOAuthクライアントのシークレットをローテーションしますか?以前のシークレットは直ちに使用できなくなり、それを使用している連携は新しいシークレットに更新されるまで動作しなくなります。新しいシークレットは一度しか表示されません。" }, delete_oauth_application: { - label: "Delete OAuth Application", - confirmText: "Permanently delete this OAuth application? All issued tokens and consents will be invalidated and integrations using this client_id will stop working immediately. This cannot be undone.", - successMessage: "OAuth application deleted" + label: "OAuthアプリケーションを削除", + confirmText: "このOAuthアプリケーションを完全に削除しますか?発行済みのすべてのトークンと同意が無効化され、この client_id を使用している連携は直ちに動作しなくなります。この操作は元に戻せません。", + successMessage: "OAuthアプリケーションを削除しました" } } }, @@ -1240,12 +1240,12 @@ export const jaJPObjects: NonNullable = { }, deactivate_role: { label: "ロールを無効化", - confirmText: "Deactivate this role? Users with the role keep their assignment but the role stops granting permissions until re-activated.", + confirmText: "このロールを無効化しますか?このロールを持つユーザーの割り当ては維持されますが、再度有効化するまで権限の付与は停止されます。", successMessage: "ロールが無効化されました" }, set_default_role: { label: "デフォルトに設定", - confirmText: "Make this the default role for new users? Existing users are unaffected.", + confirmText: "このロールを新規ユーザーのデフォルトロールにしますか?既存のユーザーには影響しません。", successMessage: "デフォルトロールを更新しました" }, clone_role: { @@ -1278,16 +1278,16 @@ export const jaJPObjects: NonNullable = { help: "JSON シリアライズされたフィールドレベルの読み取り/書き込み権限" }, system_permissions: { - label: "System Permissions", - help: "JSON-serialized array of system capability names (e.g. [\"setup.access\",\"studio.access\",\"manage_users\"])" + label: "システム権限", + help: "システムケーパビリティ名のJSONシリアライズ配列(例: [\"setup.access\",\"studio.access\",\"manage_users\"])" }, row_level_security: { - label: "Row-Level Security", - help: "JSON-serialized array of row-level security policies (USING/CHECK clauses)" + label: "行レベルセキュリティ", + help: "行レベルセキュリティポリシーのJSONシリアライズ配列(USING/CHECK 句)" }, tab_permissions: { - label: "Tab Permissions", - help: "JSON-serialized map of app tab visibility (visible | hidden | default_on | default_off)" + label: "タブ権限", + help: "アプリのタブ表示のJSONシリアライズマップ(visible | hidden | default_on | default_off)" }, active: { label: "有効" @@ -1320,7 +1320,7 @@ export const jaJPObjects: NonNullable = { }, deactivate_permission_set: { label: "無効化", - confirmText: "Deactivate this permission set? Existing assignments stay in place but stop granting access until re-activated.", + confirmText: "この権限セットを無効化しますか?既存の割り当ては維持されますが、再度有効化するまでアクセスの付与は停止されます。", successMessage: "権限セットが無効化されました" }, clone_permission_set: { @@ -1556,6 +1556,100 @@ export const jaJPObjects: NonNullable = { } } }, + sys_share_link: { + label: "共有リンク", + pluralLabel: "共有リンク", + description: "単一レコードへのアクセスを許可する不透明なケーパビリティトークン。Notion / Figma スタイルの公開リンク共有。", + fields: { + id: { + label: "リンク ID" + }, + token: { + label: "トークン", + help: "URL セーフな不透明ランダムトークン(22 文字以上)。この行で唯一の機密情報です。" + }, + object_name: { + label: "オブジェクト", + help: "共有対象レコードのオブジェクト短縮名(例: ai_conversation、contracts_contract)" + }, + record_id: { + label: "レコード", + help: "object_name 内における共有対象レコードの主キー" + }, + permission: { + label: "権限", + help: "リンク保持者がレコードに対して実行できる操作", + options: { + view: "閲覧", + comment: "コメント", + edit: "編集" + } + }, + audience: { + label: "対象者", + help: "トークン検証の上に適用されるアクセス制御レイヤー", + options: { + public: "公開(インデックス可能)", + link_only: "リンクを知っている全員", + signed_in: "サインイン済みユーザー", + email: "特定のメールアドレス" + } + }, + expires_at: { + label: "有効期限", + help: "設定すると、このタイムスタンプ以降は resolveToken が null を返します" + }, + email_allowlist: { + label: "メール許可リスト", + help: "audience=email のときに照合される小文字のメールアドレス" + }, + password_hash: { + label: "パスワードハッシュ", + help: "Argon2/bcrypt ハッシュ。設定すると、表示前に UI がパスワードの入力を求めます。" + }, + redact_fields: { + label: "リンク単位のマスキング", + help: "オブジェクト既定のマスキング集合に加えて、レスポンスから除外する追加フィールド" + }, + label: { + label: "ラベル", + help: "共有ダイアログに表示される自由記述テキスト(例: \"ACME Q3 contract\")" + }, + revoked_at: { + label: "失効日時", + help: "設定すると、リンクは恒久的に無効化されます" + }, + created_by: { + label: "作成者", + help: "リンクの発行者" + }, + created_at: { + label: "作成日時" + }, + last_used_at: { + label: "最終使用日時", + help: "resolveToken によって記録されます。ダッシュボードでアクティブなリンクを強調表示するために使用されます" + }, + use_count: { + label: "使用回数", + help: "解決が成功するたびに resolveToken によって加算されます" + } + }, + _views: { + active_links: { + label: "アクティブ" + }, + by_me: { + label: "自分が作成" + }, + revoked: { + label: "失効済み" + }, + all_links: { + label: "すべて" + } + } + }, sys_audit_log: { label: "監査ログ", pluralLabel: "監査ログ", @@ -2343,8 +2437,8 @@ export const jaJPObjects: NonNullable = { help: "送信時のレコードスナップショット" }, process_hash: { - label: "Process Hash", - help: "sha256 of the approval process body at submit time (ADR-0009 execution pinning). Resolved through sys_metadata_history so process upgrades do not affect in-flight requests." + label: "プロセスハッシュ", + help: "送信時点の承認プロセス本体の sha256(ADR-0009 の実行ピン留め)。sys_metadata_history を介して解決されるため、プロセスのアップグレードは処理中のリクエストに影響しません。" }, completed_at: { label: "完了日時" @@ -2791,13 +2885,13 @@ export const jaJPObjects: NonNullable = { }, _views: { only_objects: { - label: "Objects" + label: "オブジェクト" }, only_fields: { - label: "Fields" + label: "フィールド" }, all_metadata: { - label: "All" + label: "すべて" } } }, @@ -2810,8 +2904,8 @@ export const jaJPObjects: NonNullable = { label: "ID" }, event_seq: { - label: "Event Seq", - help: "Per-organization monotonic event log cursor." + label: "イベントシーケンス", + help: "組織ごとの単調増加するイベントログカーソル。" }, name: { label: "名前" @@ -2847,7 +2941,7 @@ export const jaJPObjects: NonNullable = { help: "このバージョンで変更された内容の説明" }, source: { - label: "Source" + label: "ソース" }, organization_id: { label: "組織", @@ -2861,6 +2955,147 @@ export const jaJPObjects: NonNullable = { } } }, + sys_view_definition: { + label: "ビュー定義", + pluralLabel: "ビュー定義", + description: "実行時に作成されたビュー定義(共有/個人レイヤー)。パッケージレイヤーはソースから提供されます。", + fields: { + id: { + label: "ID" + }, + name: { + label: "名前" + }, + object: { + label: "オブジェクト" + }, + view_kind: { + label: "ビュー種別", + options: { + list: "リスト", + form: "フォーム" + } + }, + label: { + label: "ラベル" + }, + is_default: { + label: "デフォルト" + }, + view_order: { + label: "並び順" + }, + scope: { + label: "スコープ", + options: { + shared: "共有", + personal: "個人" + } + }, + owner: { + label: "所有者" + }, + hidden: { + label: "非表示" + }, + config: { + label: "構成", + help: "ListView または FormView の構成(仕様の ViewItem.config に対応)。" + }, + organization_id: { + label: "組織", + help: "マルチテナント分離のための組織。" + }, + state: { + label: "状態", + options: { + draft: "下書き", + active: "有効", + archived: "アーカイブ済み" + } + }, + created_by: { + label: "作成者" + }, + created_at: { + label: "作成日時" + }, + updated_by: { + label: "更新者" + }, + updated_at: { + label: "更新日時" + } + } + }, + sys_metadata_audit: { + label: "メタデータ監査", + pluralLabel: "メタデータ監査", + description: "メタデータ書き込み決定の追記専用監査証跡(ADR-0010)。", + fields: { + id: { + label: "ID" + }, + occurred_at: { + label: "発生日時" + }, + actor: { + label: "実行主体", + help: "操作を行った主体——ユーザー ID、システム ID、または \"system\"。" + }, + source: { + label: "ソース" + }, + type: { + label: "メタデータ種別" + }, + name: { + label: "名前" + }, + organization_id: { + label: "組織" + }, + operation: { + label: "操作", + options: { + save: "保存", + publish: "公開", + rollback: "ロールバック", + delete: "削除", + reset: "リセット" + } + }, + outcome: { + label: "結果", + options: { + allowed: "許可", + denied: "拒否", + forced: "強制" + } + }, + code: { + label: "コード" + }, + lock_state: { + label: "ロック状態", + options: { + none: "なし", + "no-overlay": "オーバーレイ禁止", + "no-delete": "削除禁止", + full: "完全ロック" + } + }, + lock_overridden: { + label: "ロックの上書き" + }, + request_id: { + label: "リクエスト ID" + }, + note: { + label: "メモ" + } + } + }, sys_setting: { label: "設定", pluralLabel: "設定", diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index aba597971e..38bc394498 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -203,10 +203,10 @@ export const zhCNMetadataForms: NonNullable = } }, trigger: { - label: "Trigger" + label: "触发器" }, validation: { - label: "Validation Rule" + label: "验证规则" }, hook: { label: "钩子", @@ -425,7 +425,7 @@ export const zhCNMetadataForms: NonNullable = }, fields: { name: { - helpText: "snake_case unique identifier" + helpText: "snake_case 唯一标识符" }, label: { helpText: "显示名" @@ -848,22 +848,22 @@ export const zhCNMetadataForms: NonNullable = } }, job: { - label: "Background Job" + label: "后台作业" }, datasource: { - label: "Datasource" + label: "数据源" }, translation: { - label: "Translation" + label: "翻译" }, router: { - label: "Router" + label: "路由器" }, function: { - label: "Function" + label: "函数" }, service: { - label: "Service" + label: "服务" }, email_template: { label: "邮件模板", @@ -951,37 +951,37 @@ export const zhCNMetadataForms: NonNullable = } }, profile: { - label: "Profile", + label: "配置文件", sections: { identity: { - label: "Identity", - description: "Permission Sets stack on top of a Profile to grant additional access. Profiles are the base set assigned 1:1 to each user." + label: "标识", + description: "权限集叠加在配置文件之上以授予额外的访问权限。配置文件是按 1:1 分配给每个用户的基础集合。" }, system_permissions: { - label: "System Permissions", - description: "High-level capabilities not tied to a specific object — e.g. manage_users, view_audit_logs." + label: "系统权限", + description: "与特定对象无关的高级能力——例如 manage_users、view_audit_logs。" }, object_and_field_permissions: { - label: "Object & Field Permissions", - description: "Per-object CRUD + per-field FLS. Edit via the matrix editor or paste JSON here." + label: "对象与字段权限", + description: "按对象的增删改查 + 按字段的字段级安全(FLS)。可通过矩阵编辑器编辑,或在此处粘贴 JSON。" }, tab_and_row_level_security: { - label: "Tab & Row-Level Security", - description: "Tab visibility, RLS policies, and custom context variables for predicate evaluation." + label: "标签页与行级安全", + description: "标签页可见性、RLS 策略,以及用于谓词求值的自定义上下文变量。" } }, fields: { name: { - helpText: "Machine name (snake_case)" + helpText: "机器名(snake_case)" }, label: { - helpText: "Display label for admins" + helpText: "面向管理员显示的标签" }, isProfile: { - helpText: "Profile = base set assigned to users. Permission Set = additive grant." + helpText: "配置文件 = 分配给用户的基础集合。权限集 = 附加授予。" }, systemPermissions: { - helpText: "List of system capability keys" + helpText: "系统能力键列表" }, objects: { helpText: "{ \"account\": { allowRead: true, allowEdit: true, ... } }" @@ -993,10 +993,10 @@ export const zhCNMetadataForms: NonNullable = helpText: "{ \"app_crm\": \"visible\", \"app_admin\": \"hidden\" }" }, rowLevelSecurity: { - helpText: "Array of RLS policies (see rls.zod.ts)" + helpText: "RLS 策略数组(参见 rls.zod.ts)" }, contextVariables: { - helpText: "Custom variables referenced in RLS predicates" + helpText: "RLS 谓词中引用的自定义变量" } } }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 9f6822f819..e937c04a82 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -110,7 +110,7 @@ export const zhCNObjects: NonNullable = { }, delete_my_account: { label: "删除我的账号", - confirmText: "Permanently delete your account? This cannot be undone — all your sessions will be terminated and all data you own will be removed per the configured retention policy.", + confirmText: "确定要永久删除您的账户吗?此操作无法撤销——您的所有会话都将被终止,并将按照配置的保留策略移除您拥有的所有数据。", successMessage: "已删除账号" } } @@ -243,7 +243,7 @@ export const zhCNObjects: NonNullable = { }, unlink_account: { label: "解除关联", - confirmText: "Unlink this identity link? The user will no longer be able to sign in with this provider until they re-link it from their account settings.", + confirmText: "确定要解除此身份关联吗?在用户从账户设置中重新关联之前,将无法再使用此提供方登录。", successMessage: "已解除身份关联" } } @@ -377,7 +377,7 @@ export const zhCNObjects: NonNullable = { }, transfer_ownership: { label: "转移所有权", - confirmText: "Transfer ownership of this organization to the selected member? You will be demoted to admin and lose owner-only privileges.", + confirmText: "确定要将该组织的所有权转移给所选成员吗?您将被降级为管理员,并失去仅所有者拥有的权限。", successMessage: "已转移所有权" } } @@ -465,7 +465,7 @@ export const zhCNObjects: NonNullable = { }, reject_invitation: { label: "拒绝邀请", - confirmText: "Decline this invitation? The inviter will be notified and you will need a new invitation to join.", + confirmText: "确定要拒绝此邀请吗?邀请人将收到通知,您需要新的邀请才能加入。", successMessage: "已拒绝邀请" } } @@ -781,7 +781,7 @@ export const zhCNObjects: NonNullable = { }, regenerate_backup_codes: { label: "重新生成备用码", - confirmText: "Regenerate backup codes? All previous backup codes will stop working immediately." + confirmText: "确定要重新生成备份码吗?此前的所有备份码将立即失效。" } } }, @@ -1010,26 +1010,26 @@ export const zhCNObjects: NonNullable = { }, _actions: { disable_oauth_application: { - label: "Disable OAuth Application", - confirmText: "Disable this OAuth application? Active access/refresh tokens issued to it will continue to be rejected at the token, authorize, and introspect endpoints. Existing integrations will stop working immediately.", - successMessage: "OAuth application disabled" + label: "停用 OAuth 应用", + confirmText: "确定要停用此 OAuth 应用吗?已为其签发的有效访问令牌/刷新令牌将继续在 token、authorize 和 introspect 端点被拒绝。现有集成将立即停止工作。", + successMessage: "OAuth 应用已停用" }, enable_oauth_application: { - label: "Enable OAuth Application", - confirmText: "Re-enable this OAuth application? Token issuance, authorization, and introspection will resume immediately.", - successMessage: "OAuth application enabled" + label: "启用 OAuth 应用", + confirmText: "确定要重新启用此 OAuth 应用吗?令牌签发、授权和内省将立即恢复。", + successMessage: "OAuth 应用已启用" }, create_oauth_application: { - label: "Register OAuth Application" + label: "注册 OAuth 应用" }, rotate_client_secret: { label: "轮换 Client Secret", - confirmText: "Rotate this OAuth client's secret? The previous secret will stop working immediately and any integrations using it will break until they are updated with the new secret. The new secret is shown only once." + confirmText: "确定要轮换此 OAuth 客户端的密钥吗?旧密钥将立即失效,任何使用它的集成都将中断,直到更新为新密钥为止。新密钥仅显示一次。" }, delete_oauth_application: { - label: "Delete OAuth Application", - confirmText: "Permanently delete this OAuth application? All issued tokens and consents will be invalidated and integrations using this client_id will stop working immediately. This cannot be undone.", - successMessage: "OAuth application deleted" + label: "删除 OAuth 应用", + confirmText: "确定要永久删除此 OAuth 应用吗?所有已签发的令牌和授权同意都将失效,使用此 client_id 的集成将立即停止工作。此操作无法撤销。", + successMessage: "OAuth 应用已删除" } } }, @@ -1240,12 +1240,12 @@ export const zhCNObjects: NonNullable = { }, deactivate_role: { label: "停用角色", - confirmText: "Deactivate this role? Users with the role keep their assignment but the role stops granting permissions until re-activated.", + confirmText: "确定要停用此角色吗?拥有该角色的用户仍保留其分配,但在重新激活之前该角色将不再授予权限。", successMessage: "角色已停用" }, set_default_role: { label: "设为默认", - confirmText: "Make this the default role for new users? Existing users are unaffected.", + confirmText: "将此角色设为新用户的默认角色吗?现有用户不受影响。", successMessage: "已更新默认角色" }, clone_role: { @@ -1278,16 +1278,16 @@ export const zhCNObjects: NonNullable = { help: "字段级读写权限的 JSON 序列化内容" }, system_permissions: { - label: "System Permissions", - help: "JSON-serialized array of system capability names (e.g. [\"setup.access\",\"studio.access\",\"manage_users\"])" + label: "系统权限", + help: "系统能力名称的 JSON 序列化数组(例如 [\"setup.access\",\"studio.access\",\"manage_users\"])" }, row_level_security: { - label: "Row-Level Security", - help: "JSON-serialized array of row-level security policies (USING/CHECK clauses)" + label: "行级安全", + help: "行级安全策略的 JSON 序列化数组(USING/CHECK 子句)" }, tab_permissions: { - label: "Tab Permissions", - help: "JSON-serialized map of app tab visibility (visible | hidden | default_on | default_off)" + label: "标签页权限", + help: "应用标签页可见性的 JSON 序列化映射(visible | hidden | default_on | default_off)" }, active: { label: "启用" @@ -1320,7 +1320,7 @@ export const zhCNObjects: NonNullable = { }, deactivate_permission_set: { label: "停用", - confirmText: "Deactivate this permission set? Existing assignments stay in place but stop granting access until re-activated.", + confirmText: "确定要停用此权限集吗?现有分配仍将保留,但在重新激活之前将不再授予访问权限。", successMessage: "权限集已停用" }, clone_permission_set: { @@ -1556,6 +1556,100 @@ export const zhCNObjects: NonNullable = { } } }, + sys_share_link: { + label: "共享链接", + pluralLabel: "共享链接", + description: "授予对单条记录访问权限的不透明能力令牌。类似 Notion/Figma 的公开链接共享。", + fields: { + id: { + label: "链接 ID" + }, + token: { + label: "令牌", + help: "URL 安全的不透明随机令牌(≥ 22 个字符)。本记录中唯一的机密信息。" + }, + object_name: { + label: "对象", + help: "所共享记录的对象短名称(例如 ai_conversation、contracts_contract)" + }, + record_id: { + label: "记录", + help: "object_name 内所共享记录的主键" + }, + permission: { + label: "权限", + help: "链接持有者可对该记录执行的操作", + options: { + view: "查看", + comment: "评论", + edit: "编辑" + } + }, + audience: { + label: "受众", + help: "在令牌校验之上额外施加的访问限制层", + options: { + public: "公开(可被索引)", + link_only: "任何持有链接的人", + signed_in: "已登录用户", + email: "指定邮箱" + } + }, + expires_at: { + label: "过期时间", + help: "设置后,超过此时间点 resolveToken 将返回 null" + }, + email_allowlist: { + label: "邮箱白名单", + help: "当 audience=email 时校验的小写邮箱地址" + }, + password_hash: { + label: "密码哈希", + help: "Argon2/bcrypt 哈希值。设置后,界面会在呈现内容前提示输入密码。" + }, + redact_fields: { + label: "按链接脱敏字段", + help: "在对象默认脱敏集之上,从响应中额外剔除的字段" + }, + label: { + label: "标签", + help: "在共享对话框中显示的自由文本(例如 \"ACME Q3 合同\")" + }, + revoked_at: { + label: "撤销时间", + help: "设置后,该链接将被永久停用" + }, + created_by: { + label: "创建人", + help: "链接的签发者" + }, + created_at: { + label: "创建时间" + }, + last_used_at: { + label: "最近使用时间", + help: "由 resolveToken 标记;仪表盘据此高亮显示活跃链接" + }, + use_count: { + label: "使用次数", + help: "每次成功解析时由 resolveToken 递增" + } + }, + _views: { + active_links: { + label: "活跃" + }, + by_me: { + label: "我创建的" + }, + revoked: { + label: "已撤销" + }, + all_links: { + label: "全部" + } + } + }, sys_audit_log: { label: "审计日志", pluralLabel: "审计日志", @@ -2343,8 +2437,8 @@ export const zhCNObjects: NonNullable = { help: "提交时的记录快照" }, process_hash: { - label: "Process Hash", - help: "sha256 of the approval process body at submit time (ADR-0009 execution pinning). Resolved through sys_metadata_history so process upgrades do not affect in-flight requests." + label: "流程哈希", + help: "提交时审批流程主体的 sha256(ADR-0009 执行固定)。通过 sys_metadata_history 解析,因此流程升级不会影响进行中的请求。" }, completed_at: { label: "完成时间" @@ -2791,13 +2885,13 @@ export const zhCNObjects: NonNullable = { }, _views: { only_objects: { - label: "Objects" + label: "对象" }, only_fields: { - label: "Fields" + label: "字段" }, all_metadata: { - label: "All" + label: "全部" } } }, @@ -2810,8 +2904,8 @@ export const zhCNObjects: NonNullable = { label: "ID" }, event_seq: { - label: "Event Seq", - help: "Per-organization monotonic event log cursor." + label: "事件序号", + help: "按组织的单调递增事件日志游标。" }, name: { label: "名称" @@ -2847,7 +2941,7 @@ export const zhCNObjects: NonNullable = { help: "对该版本变更内容的说明" }, source: { - label: "Source" + label: "来源" }, organization_id: { label: "组织", @@ -2861,6 +2955,147 @@ export const zhCNObjects: NonNullable = { } } }, + sys_view_definition: { + label: "视图定义", + pluralLabel: "视图定义", + description: "运行时创建的视图定义(共享/个人层)。软件包层由源代码提供。", + fields: { + id: { + label: "ID" + }, + name: { + label: "名称" + }, + object: { + label: "对象" + }, + view_kind: { + label: "视图类型", + options: { + list: "列表", + form: "表单" + } + }, + label: { + label: "标签" + }, + is_default: { + label: "默认视图" + }, + view_order: { + label: "排序" + }, + scope: { + label: "范围", + options: { + shared: "共享", + personal: "个人" + } + }, + owner: { + label: "所有者" + }, + hidden: { + label: "隐藏" + }, + config: { + label: "配置", + help: "ListView 或 FormView 配置(与规范 ViewItem.config 一致)。" + }, + organization_id: { + label: "组织", + help: "用于多租户隔离的组织。" + }, + state: { + label: "状态", + options: { + draft: "草稿", + active: "活动", + archived: "已归档" + } + }, + created_by: { + label: "创建人" + }, + created_at: { + label: "创建时间" + }, + updated_by: { + label: "更新人" + }, + updated_at: { + label: "更新时间" + } + } + }, + sys_metadata_audit: { + label: "元数据审计", + pluralLabel: "元数据审计", + description: "元数据写入决策的仅追加审计记录(ADR-0010)。", + fields: { + id: { + label: "ID" + }, + occurred_at: { + label: "发生时间" + }, + actor: { + label: "操作者", + help: "操作主体——用户 ID、系统 ID 或 \"system\"。" + }, + source: { + label: "来源" + }, + type: { + label: "元数据类型" + }, + name: { + label: "名称" + }, + organization_id: { + label: "组织" + }, + operation: { + label: "操作", + options: { + save: "保存", + publish: "发布", + rollback: "回滚", + delete: "删除", + reset: "重置" + } + }, + outcome: { + label: "结果", + options: { + allowed: "允许", + denied: "拒绝", + forced: "强制执行" + } + }, + code: { + label: "代码" + }, + lock_state: { + label: "锁定状态", + options: { + none: "无", + "no-overlay": "禁止覆盖", + "no-delete": "禁止删除", + full: "完全锁定" + } + }, + lock_overridden: { + label: "锁定已被覆盖" + }, + request_id: { + label: "请求 ID" + }, + note: { + label: "备注" + } + } + }, sys_setting: { label: "设置", pluralLabel: "设置",