Skip to content

Commit e7f8e56

Browse files
authored
fix(i18n): translate Setup System Settings menu labels and pages (#1670)
1 parent 2c3c0cd commit e7f8e56

9 files changed

Lines changed: 391 additions & 2 deletions

File tree

packages/platform-objects/src/apps/translations/en.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const en: TranslationData = {
5050
group_access_control: { label: 'Access Control' },
5151
group_approvals: { label: 'Approvals' },
5252
group_configuration: { label: 'Configuration' },
53+
group_integrations: { label: 'Integrations' },
5354
group_diagnostics: { label: 'Diagnostics' },
5455
group_advanced: { label: 'Advanced' },
5556

@@ -84,7 +85,14 @@ export const en: TranslationData = {
8485
nav_settings_mail: { label: 'Email' },
8586
nav_settings_branding: { label: 'Branding' },
8687
nav_settings_auth: { label: 'Authentication' },
88+
nav_settings_storage: { label: 'File Storage' },
89+
nav_settings_ai: { label: 'AI & Embedder' },
90+
nav_settings_knowledge: { label: 'Knowledge' },
8791
nav_settings_feature_flags: { label: 'Feature Flags' },
92+
// Notifications (contributed by @objectstack/service-messaging)
93+
nav_notification_preferences: { label: 'Notification Preferences' },
94+
nav_notification_subscriptions: { label: 'Notification Subscriptions' },
95+
nav_notification_templates: { label: 'Notification Templates' },
8896

8997
// Diagnostics
9098
nav_sessions: { label: 'Sessions' },

packages/platform-objects/src/apps/translations/es-ES.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const esES: TranslationData = {
3838
group_access_control: { label: 'Control de Acceso' },
3939
group_approvals: { label: 'Aprobaciones' },
4040
group_configuration: { label: 'Configuración' },
41+
group_integrations: { label: 'Integraciones' },
4142
group_diagnostics: { label: 'Diagnóstico' },
4243
group_advanced: { label: 'Avanzado' },
4344

@@ -62,7 +63,14 @@ export const esES: TranslationData = {
6263
nav_settings_hub: { label: 'Todos los Ajustes' },
6364
nav_settings_mail: { label: 'Correo' },
6465
nav_settings_branding: { label: 'Marca' },
66+
nav_settings_auth: { label: 'Autenticación' },
67+
nav_settings_storage: { label: 'Almacenamiento de Archivos' },
68+
nav_settings_ai: { label: 'IA y Embedder' },
69+
nav_settings_knowledge: { label: 'Conocimiento' },
6570
nav_settings_feature_flags: { label: 'Indicadores de Funcionalidad' },
71+
nav_notification_preferences: { label: 'Preferencias de Notificación' },
72+
nav_notification_subscriptions: { label: 'Suscripciones de Notificación' },
73+
nav_notification_templates: { label: 'Plantillas de Notificación' },
6674

6775
nav_sessions: { label: 'Sesiones' },
6876
nav_audit_logs: { label: 'Registros de Auditoría' },

packages/platform-objects/src/apps/translations/ja-JP.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const jaJP: TranslationData = {
3838
group_access_control: { label: 'アクセス制御' },
3939
group_approvals: { label: '承認' },
4040
group_configuration: { label: '構成' },
41+
group_integrations: { label: '統合' },
4142
group_diagnostics: { label: '診断' },
4243
group_advanced: { label: '詳細' },
4344

@@ -62,7 +63,14 @@ export const jaJP: TranslationData = {
6263
nav_settings_hub: { label: 'すべての設定' },
6364
nav_settings_mail: { label: 'メール' },
6465
nav_settings_branding: { label: 'ブランディング' },
66+
nav_settings_auth: { label: '認証' },
67+
nav_settings_storage: { label: 'ファイルストレージ' },
68+
nav_settings_ai: { label: 'AI と Embedder' },
69+
nav_settings_knowledge: { label: 'ナレッジ' },
6570
nav_settings_feature_flags: { label: '機能フラグ' },
71+
nav_notification_preferences: { label: '通知設定' },
72+
nav_notification_subscriptions: { label: '通知購読' },
73+
nav_notification_templates: { label: '通知テンプレート' },
6674

6775
nav_sessions: { label: 'セッション' },
6876
nav_audit_logs: { label: '監査ログ' },

packages/platform-objects/src/apps/translations/zh-CN.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const zhCN: TranslationData = {
3838
group_access_control: { label: '访问控制' },
3939
group_approvals: { label: '审批' },
4040
group_configuration: { label: '配置' },
41+
group_integrations: { label: '集成' },
4142
group_diagnostics: { label: '诊断' },
4243
group_advanced: { label: '高级' },
4344

@@ -62,7 +63,14 @@ export const zhCN: TranslationData = {
6263
nav_settings_hub: { label: '全部设置' },
6364
nav_settings_mail: { label: '邮件' },
6465
nav_settings_branding: { label: '品牌' },
66+
nav_settings_auth: { label: '认证' },
67+
nav_settings_storage: { label: '文件存储' },
68+
nav_settings_ai: { label: 'AI 与 Embedder' },
69+
nav_settings_knowledge: { label: '知识库' },
6570
nav_settings_feature_flags: { label: '功能开关' },
71+
nav_notification_preferences: { label: '通知偏好' },
72+
nav_notification_subscriptions: { label: '通知订阅' },
73+
nav_notification_templates: { label: '通知模板' },
6674

6775
nav_sessions: { label: '会话' },
6876
nav_audit_logs: { label: '审计日志' },

packages/rest/src/rest-server.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ const logError = (...args: unknown[]) => (globalThis as any).console?.error(...a
1515
*/
1616
const TRANSLATABLE_META_TYPES = new Set(['view', 'action', 'object', 'app', 'dashboard']);
1717

18+
/**
19+
* Detect the `getMetaItem` response envelope (`{ type, name, item, lock, … }`)
20+
* whose translatable metadata document is nested at `.item`. The cached read
21+
* path and `getMetaItems` element shape hand back the already-unwrapped
22+
* document instead, so translation helpers must distinguish the two: an
23+
* envelope carries a nested `item` object alongside its own `type`/`name`,
24+
* which a bare metadata document never does.
25+
*/
26+
function isMetaEnvelope(value: any): boolean {
27+
return !!value
28+
&& typeof value === 'object'
29+
&& typeof value.type === 'string'
30+
&& typeof value.name === 'string'
31+
&& value.item != null
32+
&& typeof value.item === 'object'
33+
&& !Array.isArray(value.item);
34+
}
35+
1836
/**
1937
* Map a data-layer error to a clean HTTP response. Unknown-object errors
2038
* (SQLite "no such table", PG "relation does not exist", protocol
@@ -1037,22 +1055,43 @@ export class RestServer {
10371055
const locale = this.extractLocale(req, i18n);
10381056
if (!locale) return item;
10391057
const { translateMetadataDocument } = await import('@objectstack/spec/system');
1058+
// `getMetaItem` returns an envelope `{ type, name, item, lock, ... }`
1059+
// whose translatable document is nested at `.item`; the cached read
1060+
// path hands us the already-unwrapped document. Translate whichever
1061+
// shape we received — nav/field labels live on the inner doc, so
1062+
// translating the envelope's top level (which has no `navigation`)
1063+
// would leave the menu untranslated.
1064+
if (isMetaEnvelope(item)) {
1065+
return { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) };
1066+
}
10401067
return translateMetadataDocument(type, item, bundle, { locale });
10411068
}
10421069

10431070
/**
10441071
* Translate a list of metadata documents using `translateMetaItem`.
10451072
*/
10461073
private async translateMetaItems(req: any, type: string, environmentId: string | undefined, items: any): Promise<any> {
1047-
if (!Array.isArray(items)) return items;
10481074
if (!TRANSLATABLE_META_TYPES.has(type)) return items;
1075+
// `getMetaItems` may hand back a bare array or an `{ items: [...] }`
1076+
// envelope. Unwrap so list responses are localized the same way the
1077+
// single-item route is; a non-array, non-envelope value is returned
1078+
// untouched.
1079+
const arr: any[] | null = Array.isArray(items)
1080+
? items
1081+
: (items && typeof items === 'object' && Array.isArray(items.items) ? items.items : null);
1082+
if (!arr) return items;
10491083
const i18n = await this.resolveI18nService(environmentId, req);
10501084
const bundle = this.buildTranslationBundle(i18n);
10511085
if (!bundle) return items;
10521086
const locale = this.extractLocale(req, i18n);
10531087
if (!locale) return items;
10541088
const { translateMetadataDocument } = await import('@objectstack/spec/system');
1055-
return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
1089+
const translated = arr.map((item) =>
1090+
isMetaEnvelope(item)
1091+
? { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) }
1092+
: translateMetadataDocument(type, item, bundle, { locale }),
1093+
);
1094+
return Array.isArray(items) ? translated : { ...items, items: translated };
10561095
}
10571096

10581097
/**

packages/rest/src/rest.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,3 +1595,88 @@ describe('discovery — routes.mcp (ADR-0036, #152)', () => {
15951595
expect(body.routes.mcp).toBeUndefined();
15961596
});
15971597
});
1598+
1599+
// ──────────────────────────────────────────────────────────────────────────
1600+
// Metadata translation — envelope unwrapping
1601+
//
1602+
// Regression guard for the Setup-app i18n gap: `getMetaItem` returns an
1603+
// envelope `{ type, name, item, ... }` whose translatable document (and its
1604+
// `navigation` tree) is nested at `.item`. Translating the envelope's top
1605+
// level instead of the inner doc left every nav label in English. The
1606+
// single-item app route bypasses the HTTP cache (for per-user RBAC
1607+
// filtering), so it only ever sees the envelope shape.
1608+
// ──────────────────────────────────────────────────────────────────────────
1609+
describe('RestServer metadata translation — envelope unwrap', () => {
1610+
// Minimal i18n service exposing one zh-CN bundle for the `setup` app.
1611+
const fakeI18n = {
1612+
getLocales: () => ['zh-CN'],
1613+
getDefaultLocale: () => 'zh-CN',
1614+
getTranslations: (locale: string) =>
1615+
locale === 'zh-CN'
1616+
? {
1617+
apps: {
1618+
setup: {
1619+
label: '系统设置',
1620+
navigation: {
1621+
group_configuration: { label: '配置' },
1622+
nav_settings_storage: { label: '文件存储' },
1623+
},
1624+
},
1625+
},
1626+
}
1627+
: undefined,
1628+
};
1629+
const zhReq = { headers: { 'accept-language': 'zh-CN' } };
1630+
// A fresh app document each call (helpers must not mutate the input).
1631+
const makeDoc = () => ({
1632+
name: 'setup',
1633+
label: 'Setup',
1634+
navigation: [
1635+
{
1636+
id: 'group_configuration',
1637+
type: 'group',
1638+
label: 'Configuration',
1639+
children: [{ id: 'nav_settings_storage', type: 'url', label: 'File Storage' }],
1640+
},
1641+
],
1642+
});
1643+
1644+
it('translates the inner document of a getMetaItem envelope', async () => {
1645+
const rest = new RestServer(createMockServer() as any, createMockProtocol() as any);
1646+
const envelope = { type: 'app', name: 'setup', item: makeDoc(), lock: null };
1647+
const out = await (rest as any).translateMetaItem(zhReq, 'app', undefined, envelope, fakeI18n);
1648+
// Envelope shape preserved …
1649+
expect(out.type).toBe('app');
1650+
expect(out.name).toBe('setup');
1651+
expect(out.lock).toBeNull();
1652+
// … and the nested doc — not the envelope top level — is translated.
1653+
expect(out.item.label).toBe('系统设置');
1654+
expect(out.item.navigation[0].label).toBe('配置');
1655+
expect(out.item.navigation[0].children[0].label).toBe('文件存储');
1656+
});
1657+
1658+
it('still translates a bare (already-unwrapped) document', async () => {
1659+
const rest = new RestServer(createMockServer() as any, createMockProtocol() as any);
1660+
const out = await (rest as any).translateMetaItem(zhReq, 'app', undefined, makeDoc(), fakeI18n);
1661+
expect(out.label).toBe('系统设置');
1662+
expect(out.navigation[0].children[0].label).toBe('文件存储');
1663+
});
1664+
1665+
it('translates list responses in the `{ items: [...] }` envelope shape', async () => {
1666+
const rest = new RestServer(createMockServer() as any, createMockProtocol() as any);
1667+
// translateMetaItems resolves the i18n service itself; stub the lookup.
1668+
(rest as any).resolveI18nService = async () => fakeI18n;
1669+
const listEnvelope = { items: [{ type: 'app', name: 'setup', item: makeDoc() }] };
1670+
const out = await (rest as any).translateMetaItems(zhReq, 'app', undefined, listEnvelope);
1671+
expect(Array.isArray(out.items)).toBe(true);
1672+
expect(out.items[0].item.navigation[0].children[0].label).toBe('文件存储');
1673+
});
1674+
1675+
it('translates a bare array of unwrapped documents', async () => {
1676+
const rest = new RestServer(createMockServer() as any, createMockProtocol() as any);
1677+
(rest as any).resolveI18nService = async () => fakeI18n;
1678+
const out = await (rest as any).translateMetaItems(zhReq, 'app', undefined, [makeDoc()]);
1679+
expect(Array.isArray(out)).toBe(true);
1680+
expect(out[0].navigation[0].children[0].label).toBe('文件存储');
1681+
});
1682+
});

packages/services/service-settings/src/translations/es-ES.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,39 @@ export const esES: TranslationData = {
6868
},
6969
},
7070

71+
auth: {
72+
title: 'Autenticación',
73+
description: 'Inicio de sesión, registro y controles de las funciones de autenticación integradas.',
74+
groups: {
75+
email_password: {
76+
title: 'Correo y contraseña',
77+
description: 'Controla el inicio de sesión local con correo/contraseña y el registro de autoservicio.',
78+
},
79+
social: {
80+
title: 'Inicio de sesión social',
81+
description:
82+
'Configura el proveedor de inicio de sesión de Google integrado. Las variables de entorno del despliegue siguen teniendo prioridad.',
83+
},
84+
},
85+
keys: {
86+
email_password_enabled: { label: 'Habilitar inicio de sesión con correo/contraseña' },
87+
signup_enabled: { label: 'Permitir registro de autoservicio' },
88+
require_email_verification: { label: 'Requerir verificación de correo' },
89+
google_enabled: {
90+
label: 'Habilitar inicio de sesión con Google',
91+
help: 'Requiere un ID de cliente y un secreto de OAuth de Google desde Google Cloud Console.',
92+
},
93+
google_client_id: {
94+
label: 'ID de cliente de Google',
95+
help: 'ID de cliente de OAuth desde Google Cloud Console. También se puede definir GOOGLE_CLIENT_ID en el servidor.',
96+
},
97+
google_client_secret: {
98+
label: 'Secreto de cliente de Google',
99+
help: 'Se almacena cifrado en reposo. También se puede definir GOOGLE_CLIENT_SECRET en el servidor.',
100+
},
101+
},
102+
},
103+
71104
feature_flags: {
72105
title: 'Indicadores de función',
73106
description: 'Activa funciones experimentales y en beta para este espacio de trabajo.',

0 commit comments

Comments
 (0)