Skip to content

Commit 82eb6cf

Browse files
committed
fix(i18n): locale fallback, app/dashboard localization, es-ES settings
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
1 parent fa64f4b commit 82eb6cf

7 files changed

Lines changed: 699 additions & 5 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/rest": patch
4+
"@objectstack/platform-objects": patch
5+
"@objectstack/service-settings": patch
6+
---
7+
8+
Fix system-metadata translations: locale fallback, app/dashboard localization, and coverage gaps.
9+
10+
Switching the UI language left many surfaces in English. Three root causes
11+
are addressed:
12+
13+
- **Locale fallback (server).** The metadata translation resolver
14+
(`@objectstack/spec` `i18n-resolver`) now resolves a requested locale
15+
against the locales actually present in the bundle (exact →
16+
case-insensitive → base-language → variant), so a request for `zh`
17+
correctly hits the `zh-CN` bundle instead of falling back to English.
18+
This mirrors `resolveLocale` in `@objectstack/core` and benefits every
19+
resolver (objects, views, actions, settings, metadata forms).
20+
21+
- **App & dashboard localization (server).** Added `translateApp` and
22+
`translateDashboard` resolvers and wired `app`/`dashboard` into the REST
23+
`/meta` translation path. App labels, sidebar/navigation group labels,
24+
and dashboard titles/widgets were previously never localized at the API
25+
boundary even though the translation data existed.
26+
27+
- **Coverage & quality (data).** Added translations for the previously
28+
untranslated platform objects `sys_share_link`, `sys_view_definition`,
29+
and `sys_metadata_audit` (and registered them in the i18n-extract config
30+
so future extractions keep them). Replaced English placeholder strings
31+
left in the `zh-CN` / `ja-JP` / `es-ES` object and metadata-form bundles
32+
(notably action `confirmText` / `successMessage` prompts). Added the
33+
missing `es-ES` built-in Settings bundle in `@objectstack/service-settings`.

packages/platform-objects/scripts/i18n-extract.config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import {
5858
SysRolePermissionSet,
5959
SysRecordShare,
6060
SysSharingRule,
61+
SysShareLink,
6162
} from '../src/security/index.js';
6263

6364
// ── Audit ─────────────────────────────────────────────────────────────────
@@ -84,7 +85,12 @@ import {
8485
import { SysWebhook } from '../src/integration/index.js';
8586

8687
// ── Metadata ──────────────────────────────────────────────────────────────
87-
import { SysMetadataObject, SysMetadataHistoryObject } from '../src/metadata/index.js';
88+
import {
89+
SysMetadataObject,
90+
SysMetadataHistoryObject,
91+
SysViewDefinitionObject,
92+
SysMetadataAuditObject,
93+
} from '../src/metadata/index.js';
8894

8995
// ── System ────────────────────────────────────────────────────────────────
9096
import { SysSetting, SysSecret, SysSettingAudit } from '../src/system/index.js';
@@ -147,6 +153,7 @@ export default defineStack({
147153
SysRolePermissionSet,
148154
SysRecordShare,
149155
SysSharingRule,
156+
SysShareLink,
150157

151158
// Audit
152159
SysAuditLog,
@@ -172,6 +179,8 @@ export default defineStack({
172179
// Metadata
173180
SysMetadataObject,
174181
SysMetadataHistoryObject,
182+
SysViewDefinitionObject,
183+
SysMetadataAuditObject,
175184

176185
// System
177186
SysSetting,

packages/rest/src/rest-server.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import { ObjectStackProtocol } from '@objectstack/spec/api';
88
// Node-safe logger — avoids importing 'console' which is absent from ES2020 lib typings.
99
const logError = (...args: unknown[]) => (globalThis as any).console?.error(...args);
1010

11+
/**
12+
* Metadata types whose user-facing labels are localized at the REST boundary
13+
* via `translateMetadataDocument`. Keep in sync with the type dispatch in
14+
* `@objectstack/spec/system`'s `translateMetadataDocument`.
15+
*/
16+
const TRANSLATABLE_META_TYPES = new Set(['view', 'action', 'object', 'app', 'dashboard']);
17+
1118
/**
1219
* Map a data-layer error to a clean HTTP response. Unknown-object errors
1320
* (SQLite "no such table", PG "relation does not exist", protocol
@@ -917,7 +924,7 @@ export class RestServer {
917924
*/
918925
private async translateMetaItem(req: any, type: string, environmentId: string | undefined, item: any, i18nService?: any): Promise<any> {
919926
if (!item || typeof item !== 'object') return item;
920-
if (type !== 'view' && type !== 'action' && type !== 'object') return item;
927+
if (!TRANSLATABLE_META_TYPES.has(type)) return item;
921928
// The cached read path resolves the i18n service up-front (to build a
922929
// locale-aware ETag) and passes it here so we don't repeat the
923930
// potentially registry-hitting lookup on every request.
@@ -935,7 +942,7 @@ export class RestServer {
935942
*/
936943
private async translateMetaItems(req: any, type: string, environmentId: string | undefined, items: any): Promise<any> {
937944
if (!Array.isArray(items)) return items;
938-
if (type !== 'view' && type !== 'action' && type !== 'object') return items;
945+
if (!TRANSLATABLE_META_TYPES.has(type)) return items;
939946
const i18n = await this.resolveI18nService(environmentId, req);
940947
const bundle = this.buildTranslationBundle(i18n);
941948
if (!bundle) return items;
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { TranslationData } from '@objectstack/spec/system';
4+
5+
/**
6+
* Español (es-ES) — built-in settings manifest translations.
7+
*/
8+
export const esES: TranslationData = {
9+
settingsCommon: {
10+
sourceLabels: {
11+
env: 'Entorno',
12+
global: 'Global',
13+
tenant: 'Inquilino',
14+
user: 'Usuario',
15+
default: 'Predeterminado',
16+
},
17+
},
18+
settings: {
19+
mail: {
20+
title: 'Envío de correo',
21+
description: 'Configuración de SMTP y del proveedor de correo transaccional.',
22+
groups: {
23+
provider: { title: 'Proveedor', description: 'Elige cómo envía correo saliente este espacio de trabajo.' },
24+
smtp: { title: 'SMTP' },
25+
api_key: { title: 'Clave de API' },
26+
from_address: { title: 'Dirección de remitente' },
27+
},
28+
keys: {
29+
provider: {
30+
label: 'Proveedor',
31+
options: {
32+
smtp: 'SMTP',
33+
sendgrid: 'SendGrid',
34+
ses: 'Amazon SES',
35+
postmark: 'Postmark',
36+
},
37+
},
38+
smtp_host: { label: 'Host', help: 'Ejemplo: smtp.example.com' },
39+
smtp_port: { label: 'Puerto' },
40+
smtp_secure: { label: 'Usar TLS' },
41+
smtp_user: { label: 'Usuario' },
42+
smtp_password: { label: 'Contraseña' },
43+
api_key: { label: 'Clave de API' },
44+
from_email: { label: 'Correo del remitente', help: 'Ejemplo: no-reply@example.com' },
45+
from_name: { label: 'Nombre del remitente' },
46+
},
47+
actions: {
48+
test: { label: 'Enviar correo de prueba' },
49+
},
50+
},
51+
52+
branding: {
53+
title: 'Marca',
54+
description: 'Nombre del espacio de trabajo, logotipo y color de acento.',
55+
groups: {
56+
identity: { title: 'Identidad' },
57+
appearance: { title: 'Apariencia' },
58+
},
59+
keys: {
60+
workspace_name: { label: 'Nombre del espacio de trabajo' },
61+
support_email: { label: 'Correo de soporte', help: 'Ejemplo: support@example.com' },
62+
theme_mode: {
63+
label: 'Tema predeterminado',
64+
options: { light: 'Claro', dark: 'Oscuro', system: 'Según el sistema' },
65+
},
66+
accent_color: { label: 'Color de acento' },
67+
logo_url: { label: 'URL del logotipo', help: 'Ejemplo: https://…/logo.svg' },
68+
},
69+
},
70+
71+
feature_flags: {
72+
title: 'Indicadores de función',
73+
description: 'Activa funciones experimentales y en beta para este espacio de trabajo.',
74+
groups: {
75+
productivity: { title: 'Productividad' },
76+
collaboration: { title: 'Colaboración' },
77+
},
78+
keys: {
79+
ai_enabled: {
80+
label: 'Asistente de IA',
81+
help: 'Habilita el panel del asistente de IA dentro de la aplicación.',
82+
},
83+
kanban_swimlanes: { label: 'Carriles de Kanban' },
84+
realtime_cursors: { label: 'Cursores en tiempo real' },
85+
inline_comments: { label: 'Comentarios en línea' },
86+
},
87+
},
88+
89+
storage: {
90+
title: 'Almacenamiento de archivos',
91+
description:
92+
'Backend usado para adjuntos, exportaciones y subidas de usuarios. ' +
93+
'⚠ Cambiar de adaptador no migra los archivos existentes: los archivos ' +
94+
'subidos con el adaptador anterior dejan de ser accesibles a través ' +
95+
'del nuevo.',
96+
groups: {
97+
adapter: { title: 'Backend', description: 'Elige dónde se almacenan los archivos subidos.' },
98+
local: { title: 'Local' },
99+
s3: { title: 'S3' },
100+
limits: { title: 'Límites' },
101+
},
102+
keys: {
103+
adapter: {
104+
label: 'Adaptador',
105+
options: { local: 'Sistema de archivos local', s3: 'S3 / compatible con S3' },
106+
},
107+
local_root: { label: 'Directorio raíz',
108+
help: 'Ruta del sistema de archivos donde se almacenan los archivos. Las rutas relativas se resuelven desde el directorio de trabajo del servidor.' },
109+
s3_bucket: { label: 'Bucket',
110+
help: 'Bucket compartido del host. Los archivos de cada entorno se aíslan mediante el prefijo projects/<environmentId>/.' },
111+
s3_region: { label: 'Región', help: 'Ejemplo: us-east-1' },
112+
s3_endpoint: { label: 'Endpoint',
113+
help: 'Endpoint personalizado para proveedores compatibles con S3 (R2, MinIO, Wasabi). Déjalo en blanco para AWS S3.' },
114+
s3_access_key_id: { label: 'Access Key ID' },
115+
s3_secret_access_key: { label: 'Secret Access Key' },
116+
s3_force_path_style: { label: 'Forzar URLs de tipo path',
117+
help: 'Actívalo para MinIO y la mayoría de proveedores compatibles con S3; desactívalo para AWS S3.' },
118+
presigned_ttl: { label: 'TTL de URL prefirmada (segundos)' },
119+
session_ttl: { label: 'TTL de sesión de subida (segundos)',
120+
help: 'Tiempo durante el cual una sesión de subida por fragmentos sigue siendo reanudable.' },
121+
max_upload_mb: { label: 'Tamaño máximo de subida (MB)' },
122+
},
123+
actions: {
124+
test: { label: 'Probar conexión' },
125+
},
126+
},
127+
128+
ai: {
129+
title: 'IA y Embedder',
130+
description:
131+
'Proveedor de LLM, modelo, credenciales y configuración del embedder usados por ' +
132+
'los servicios de IA y de conocimiento de la plataforma.',
133+
groups: {
134+
provider: { title: 'Proveedor',
135+
description: 'Elige el backend de LLM. El modo Memory repite la entrada: útil para pruebas, nunca para producción.' },
136+
gateway: { title: 'Vercel AI Gateway',
137+
description: 'Enrutador multiproveedor. La especificación del modelo sigue `provider/model`, p. ej. `openai/gpt-4o`.' },
138+
openai: { title: 'OpenAI' },
139+
anthropic: { title: 'Anthropic' },
140+
google: { title: 'Google' },
141+
defaults: { title: 'Valores predeterminados de generación',
142+
description: 'Se aplican cuando un agente o una solicitud de chat no especifica su propio valor.' },
143+
observability: { title: 'Observabilidad' },
144+
embedder: { title: 'Embedder',
145+
description:
146+
'Proveedor de texto → vector usado por las fuentes de conocimiento y RAG. ' +
147+
'Independiente del proveedor de chat anterior.' },
148+
},
149+
keys: {
150+
provider: {
151+
label: 'Proveedor',
152+
options: {
153+
memory: 'Memory (eco — solo pruebas)',
154+
gateway: 'Vercel AI Gateway',
155+
openai: 'OpenAI',
156+
anthropic: 'Anthropic',
157+
google: 'Google Generative AI',
158+
},
159+
},
160+
gateway_model: { label: 'Modelo de Gateway',
161+
help: 'Se reenvía como AI_GATEWAY_MODEL. Ejemplo: openai/gpt-4o' },
162+
gateway_api_key: { label: 'Clave de API de Gateway',
163+
help: 'Opcional: solo se requiere si el gateway exige autenticación.' },
164+
openai_api_key: { label: 'Clave de API de OpenAI',
165+
help: 'Se reenvía como OPENAI_API_KEY. Se almacena cifrada en reposo.' },
166+
openai_model: { label: 'Modelo',
167+
help: 'ID de modelo predeterminado. Las anulaciones por agente tienen prioridad.' },
168+
openai_base_url: { label: 'Base URL',
169+
help: 'Anulación para Azure OpenAI o gateways autoalojados. Déjalo en blanco para api.openai.com.' },
170+
anthropic_api_key: { label: 'Clave de API de Anthropic',
171+
help: 'Se reenvía como ANTHROPIC_API_KEY. Se almacena cifrada en reposo.' },
172+
anthropic_model: { label: 'Modelo' },
173+
google_api_key: { label: 'Clave de API de Google',
174+
help: 'Se reenvía como GOOGLE_GENERATIVE_AI_API_KEY. Se almacena cifrada en reposo.' },
175+
google_model: { label: 'Modelo' },
176+
temperature: { label: 'Temperatura',
177+
help: '0 = determinista, 2 = muy creativo.' },
178+
max_tokens: { label: 'Máximo de tokens de salida',
179+
help: 'Límite estricto de tokens generados por respuesta.' },
180+
request_timeout_ms: { label: 'Tiempo de espera de la solicitud (ms)' },
181+
trace_enabled: { label: 'Registrar trazas',
182+
help: 'Persiste las trazas de prompt/respuesta en sys_ai_trace para depuración y reproducción.' },
183+
log_prompts: { label: 'Registrar prompts completos',
184+
help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza. ⚠ Puede filtrar PII: desactívalo en entornos regulados.' },
185+
embedder_provider: {
186+
label: 'Proveedor',
187+
options: {
188+
none: 'Deshabilitado (sin embeddings)',
189+
openai: 'OpenAI',
190+
azure: 'Azure OpenAI',
191+
dashscope: '阿里通义 DashScope',
192+
zhipu: '智谱 BigModel',
193+
siliconflow: '硅基流动 SiliconFlow',
194+
doubao: '火山引擎 Doubao',
195+
minimax: 'MiniMax',
196+
ollama: 'Ollama (local)',
197+
custom: 'Personalizado (compatible con OpenAI)',
198+
},
199+
},
200+
embedder_api_key: { label: 'Clave de API del embedder',
201+
help: 'Token bearer enviado en la cabecera Authorization. Para Ollama sirve cualquier valor no vacío.' },
202+
embedder_model: { label: 'Modelo',
203+
help: 'Ejemplos — OpenAI: text-embedding-3-small · 阿里通义: text-embedding-v3 · 智谱: embedding-3 · 硅基流动: BAAI/bge-m3 · Ollama: bge-m3' },
204+
embedder_base_url: { label: 'Base URL',
205+
help: 'Raíz del endpoint (sin /embeddings). Se autocompleta desde el preset; anúlalo para proxys o gateways autoalojados.' },
206+
embedder_dimensions: { label: 'Dimensiones',
207+
help: 'Anula la dimensionalidad de salida (solo modelos Matryoshka). Déjalo en blanco para usar el valor predeterminado del modelo.' },
208+
embedder_batch_size: { label: 'Tamaño de lote',
209+
help: 'Fragmentos por llamada a embed(). Redúcelo si alcanzas los límites de tasa o tamaño del proveedor.' },
210+
},
211+
actions: {
212+
test: { label: 'Probar conexión' },
213+
test_embedder: { label: 'Probar embedder' },
214+
},
215+
},
216+
217+
knowledge: {
218+
title: 'Conocimiento',
219+
description:
220+
'Backend de almacén de vectores para RAG / fuentes de conocimiento. ' +
221+
'⚠ Cambiar de adaptador NO migra los índices existentes.',
222+
groups: {
223+
adapter: { title: 'Backend',
224+
description: 'Elige dónde se almacenan los fragmentos de documento y sus vectores.' },
225+
turso: { title: 'Turso / libSQL',
226+
description: 'Funciona con Turso gestionado, archivo local o en memoria.' },
227+
ragflow: { title: 'RAGFlow',
228+
description: 'Despliegue externo de RAGFlow. Consulta https://ragflow.io para instrucciones de autoalojamiento.' },
229+
indexing: { title: 'Valores predeterminados de indexación',
230+
description: 'Los valores por fuente en KnowledgeSource.adapterConfig tienen prioridad.' },
231+
permissions: { title: 'Permisos' },
232+
},
233+
keys: {
234+
adapter: {
235+
label: 'Adaptador',
236+
options: {
237+
memory: 'En memoria (solo desarrollo / pruebas)',
238+
turso: 'Turso / libSQL (nube o local)',
239+
ragflow: 'RAGFlow (externo)',
240+
},
241+
},
242+
turso_url: { label: 'URL de conexión',
243+
help: 'Ejemplos: libsql://your-tenant.turso.io · file:./.objectstack/knowledge.db · :memory:' },
244+
turso_auth_token: { label: 'Token de autenticación',
245+
help: 'Solo se requiere para URLs de Turso gestionado.' },
246+
ragflow_base_url: { label: 'Base URL', help: 'Ejemplo: http://localhost:9380' },
247+
ragflow_api_key: { label: 'Clave de API' },
248+
ragflow_default_dataset: { label: 'ID de dataset predeterminado',
249+
help: 'Se usa cuando una KnowledgeSource no especifica su propio dataset de RAGFlow.' },
250+
chunk_target: { label: 'Tamaño objetivo de fragmento (caracteres)',
251+
help: 'Límite flexible del tamaño de fragmento antes de que actúe la división consciente de tokens.' },
252+
chunk_overlap: { label: 'Solapamiento de fragmentos (caracteres)',
253+
help: 'Caracteres conservados del fragmento anterior para que el contexto sobreviva al límite.' },
254+
over_fetch: { label: 'Multiplicador de sobre-obtención',
255+
help: 'Se obtienen topK × overFetch candidatos internos para que el filtrado de metadatos en JS siga teniendo filas.' },
256+
enforce_rls: { label: 'Aplicar RLS en la búsqueda',
257+
help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante. ⚠ Desactivarlo omite la salvaguarda exclusiva de la plataforma.' },
258+
},
259+
actions: {
260+
test: { label: 'Probar conexión' },
261+
},
262+
},
263+
},
264+
};

packages/services/service-settings/src/translations/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import type { TranslationBundle } from '@objectstack/spec/system';
1515
import { en } from './en.js';
1616
import { zhCN } from './zh-CN.js';
1717
import { jaJP } from './ja-JP.js';
18+
import { esES } from './es-ES.js';
1819

19-
export { en, zhCN, jaJP };
20+
export { en, zhCN, jaJP, esES };
2021

2122
export const settingsBuiltinTranslations: TranslationBundle = {
2223
en,
2324
'zh-CN': zhCN,
2425
'ja-JP': jaJP,
26+
'es-ES': esES,
2527
};

0 commit comments

Comments
 (0)