Skip to content

Commit c35fed1

Browse files
fix(service-datasource): serve the datasource driver form's labels and help in the requester's language (#30685)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DGazHPumEc1dceMjE3J1eEi)_ Fixes #30563 Clause-②: no Metadata-protocol: none domain:server#1 card thread · session `session_016WV4czJVaL8SDBxjng4RLk` · branch `claude/project-thread-b5t886` (the card thread's platform-assigned branch, named in claim amendment 6670256081). ## 维护者速读 - 改了什么:新建数据源对话框里的驱动名称、驱动说明和每个连接字段的标题与说明(文件名、自动迁移表结构、主机、端口等),现在由服务端按请求语言返回;平台语言表上的 11 种语言全部齐全(简体、繁体、西、阿、法、葡(巴西)、俄、德、日、韩,英文为源文),英文界面不变。 - 为什么改:这些文字全部来自 `GET /api/v1/datasources/drivers`,控制台自己没有对应的翻译键,所以中文界面里这一块一直是英文,和对话框其他中文文字混在一起。 - 风险与回滚:只改返回的文字,驱动 id、图标、字段类型、默认值和校验都不变;繁体读者(zh-TW、zh-HK)拿到繁体,不会落到简体;平台表之外的语言和产品名照旧返回英文。回滚即还原本 PR。 - 先看:`packages/services/service-datasource/src/driver-catalog.ts` 的 `driverCatalogLocaleOf` 与 `driverCatalogFor`;`src/driver-catalog-translations.ts` 的十种语言表;`src/admin-routes.ts` 里 drivers 路由那一行。 - 两分钟验证:`pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/driver-catalog.test.ts src/__tests__/admin-routes.test.ts`,预期 2 个文件 70 例全绿,其中 zh-CN 读到「文件名」,zh-TW 读到「檔案名稱」,fr-FR 读到 Nom de fichier,en-US 读到 Filename。 - 需要你做的:无,席位落地。 ## What changes - `GET /api/v1/datasources/drivers` reads the requester's language the way the REST metadata door and the settings routes read it (the top `Accept-Language` tag, else a string `?locale=`) and serves `driverCatalogFor(locale)` instead of the fixed English `DRIVER_CATALOG`. The console already sends its own language as `Accept-Language` on every API call (`createAuthenticatedFetch` reads the html `lang` attribute), so no `ui/` change is needed. - `driverCatalogLocaleOf` (new, `driver-catalog.ts`) reads the requested code through the platform locale table's own reader, `platformLocaleTagOf` (`@objectstack/spec/system`), shortened one subtag at a time (RFC 4647 lookup), the shape the storage connection test uses (PR #30627). So `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-TW` reach `zh-Hant` and never the Simplified text; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-CN` reach `zh-Hans`; `pt` and `pt-PT` reach `pt-BR`; `fr-CA` reaches `fr`. `driverCatalogFor` overlays that language's entry `label` / `description` and each top-level field's JSON-Schema `title` / `description` on the English catalog. The spec's projected schema is copied, never written. - `driver-catalog-translations.ts` (new) holds the translations, service-local beside the catalog's English curation, keyed by the platform table's tags. Every non-English language on the table (`zh-Hans`, `zh-Hant`, `es`, `ar`, `fr`, `pt-BR`, `ru`, `de`, `ja`, `ko`) covers all five offered drivers (In-Memory, SQLite, PostgreSQL, MySQL / MariaDB, MongoDB) and every field word the form draws. Each language is written once as its distinct words and laid out per driver, so a field the drivers share (host, port, password) reads one way. Product names and technical terms (SQLite, URL, URI, SSL/TLS, `knex searchPath`, `application_name`) are kept as they are. - Fallback: no locale, `en`, a code the platform table does not name (`it`, `nl-NL`), and the product-name labels are served the English text, never a key. A translation only replaces a word the English form shows; it never adds a title to a field that has none. - Changeset: `@objectstack/service-datasource` patch. ## Tests - `src/__tests__/driver-catalog.test.ts`, two new top-level describes. `driverCatalogFor`: Simplified Chinese words for SQLite (`文件名`, `自动迁移表结构`, the driver description) and the network drivers (`主机`, `端口`, `密码`); a SQLite `filename` title of its own in each of the ten languages, asserted against `PLATFORM_LOCALE_TAGS`; `zh-TW`, `zh-HK`, `zh-MO`, `zh-Hant`, `zh-hant`, `zh-Hant-TW`, `zh-Hant-HK` reach `zh-Hant` (`檔案名稱`) and `zh`, `zh-CN`, `zh-cn`, `zh-SG`, `zh-Hans`, `zh-Hans-CN` reach `zh-Hans`; ten alias and regional codes (`ja-JP`, `es-MX`, `pt-PT`, `fr-CA`, …) reach their table language; ids, icons, field keys, types and defaults equal the English entry's in all eleven languages; `undefined`, empty, `en`, `en-US`, `en-GB`, `it`, `nl-NL`, `constructor`, `__proto__` return `DRIVER_CATALOG` itself; product-name labels stay English; the spec projection and `DRIVER_CATALOG` still read `Filename` after every language is served. `DRIVER_CATALOG_TRANSLATIONS coverage` walks `PLATFORM_LOCALE_TAGS`, not the table's keys: the table carries exactly the ten non-English languages; each names only drivers and fields the catalog serves; each translates every driver description, every non-product label and every field word the English form shows, so a missing language or a field the spec adds goes red. - `src/__tests__/admin-routes.test.ts`, one new case on the real `HonoHttpServer`: `Accept-Language: zh-CN,zh;q=0.9,en;q=0.8` serves `文件名`, `自动迁移表结构`, `主机`, `端口` and the zh MySQL description with the English `SQLite` label; `?locale=zh-CN` with no header serves zh; `zh-TW,zh;q=0.9` serves `檔案名稱` and `連接埠`; `fr-FR,fr;q=0.9` serves `Nom de fichier`; `en-US` and `it-IT` serve `Filename` and `Host`. ## Acceptance notes - **Languages.** All eleven languages on the platform locale table (`PLATFORM_LOCALES`), English as the source, per the maintainer's one-list ruling (no language tiers). - **Out-of-scope finding (class a).** The dialog's own strings (the console's `studio` namespace: the dialog title, Name, Display name, Driver, Schema mode and its hints) exist in English and Chinese only (`ui/packages/i18n/src/namespaces/studio/` holds `en.ts` and `zh.ts`), so a console in any of the other nine languages now reads these driver words in its own language inside an English dialog. Reach: the New datasource dialog under Setup, console language set to any table language other than en / zh-Hans; read from the tree, not measured live. Not fixed here (`ui/**` is outside this card's surface). Dedupe words: studio namespace locales, datasource dialog i18n, studio en zh only. - **`?locale=` read.** The route now reads `?locale=` (string only, as the settings routes and the metadata door do). It had no query read before and declares no closed query set; converting this route to `refuseUnknownQueryParams` is not done here. - **Stale translations are not detected.** If the spec revises an English field description, the zh-CN text stays as written; the coverage test catches added and removed fields, not revised wording. Noted, not filed. - Nested schema words (the memory driver's persistence variants) are not translated: the dialog draws only top-level properties. - `DRIVER_CATALOG` keeps its value and stays the English catalog; the QA checklist row that verifies the drivers body against it (`docs/qa/platform-checklist/areas/integration-system.json`) still holds for an English request. ## Verification At head `2289fe00` (round 1 rework), after the `@objectstack/service-datasource` dependency closure build. <details><summary>Package suite and typecheck</summary> - `pnpm --filter @objectstack/service-datasource test` (under `os-verify-lock`): Test Files 72 passed (72), Tests 1239 passed (1239), VERDICT command-exit 0. - `pnpm --filter @objectstack/service-datasource typecheck`: VERDICT command-exit 0 (`tsc --listFiles` includes both changed test files). - `pnpm lint` (eslint . --no-inline-config): exit 0. </details> <details><summary>Ablations (scripts/ablation-replace.mjs, each restored to blob == HEAD, git diff HEAD empty)</summary> - Route ignores the requester (`driverCatalogFor(requestLocaleOf(req))` to `driverCatalogFor(undefined)`): 1 failed / 69 passed, the route case. - Title overlay deleted in `localizeField`: 4 failed / 66 passed (Simplified words, every-language titles, Traditional codes, route case). - Schema written in place (`Object.assign(configSchema, …)` instead of a copy): 3 failed / 67 passed (English fallback, spec projection untouched, route case). - Subtag shortening removed (only the whole code is looked up): 3 failed / 67 passed (Traditional codes via `zh-Hant-TW`, regional codes such as `fr-CA` and `es-MX`, route case via `fr-FR`). </details> <details><summary>Derived gates (dispatch-gates --commands)</summary> - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 61 families derived at `2289fe00`; every one exit 0 on the first pass (the round-0 prerequisites, the teardown fixture commit and the whole-tree build, were still in place). - `--ran` reconciliation: 61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN. - `pnpm lint`: exit 0. `check-commit-card-trailers --range origin/main..HEAD`: exit 0 (2 commits). - Families CI runs beyond the derived set: read from this PR's check-runs. The round-0 `TypeScript Type Check` red at `b475fae0` was the aggregator over a lane cancelled by the push of `2289fe00` (run 42121322456: consumer gates `cancelled`, the other three lanes `success`), not a type error. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4czJVaL8SDBxjng4RLk)_ <!-- signed: domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ef7f58 commit c35fed1

6 files changed

Lines changed: 1029 additions & 3 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
---
4+
5+
fix(service-datasource): the datasource driver catalog serves its words in the requester's language, so the New datasource dialog reads in one language
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`GET /api/v1/datasources/drivers` now serves each driver's label and description, and each connection field's `title` and `description` (Filename, Auto-migrate schema, Host, Port, …), in the language the request names: the top `Accept-Language` tag, else `?locale=`. The code is read through the platform locale table (`platformLocaleTagOf`, shortened one subtag at a time), so `zh-TW` and `zh-HK` get Traditional Chinese, `zh`, `zh-CN` and `zh-SG` Simplified, `pt` Brazilian Portuguese and `fr-CA` French. Every language on the table is translated for every offered driver: Simplified and Traditional Chinese, Spanish, Arabic, French, Brazilian Portuguese, Russian, German, Japanese and Korean. English requests, codes the table does not name, and product names (SQLite, PostgreSQL, MySQL / MariaDB, MongoDB) are served the English text, never a key. `DRIVER_CATALOG` stays the English catalog, and the new `driverCatalogFor(locale)` in the package source builds the localized one; ids, icons, types and defaults are unchanged in every language.

‎packages/services/service-datasource/src/__tests__/admin-routes.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,47 @@ describe('registerDatasourceAdminRoutes (real HonoHttpServer)', () => {
122122
expect(sqlite!.configSchema?.properties?.filename?.type).toBe('string');
123123
});
124124

125+
it('GET /api/v1/datasources/drivers serves the words in the requester\'s language', async () => {
126+
const app = mount({});
127+
type Served = { data: { drivers: Array<{ id: string; label: string; description?: string; configSchema: any }> } };
128+
const read = async (init?: RequestInit, path = '/api/v1/datasources/drivers') => {
129+
const res = await app.fetch(json(path, init));
130+
expect(res.status).toBe(200);
131+
const drivers = ((await res.json()) as Served).data.drivers;
132+
return (id: string) => drivers.find((d) => d.id === id)!;
133+
};
134+
135+
// The console sends its language as `Accept-Language` on every API call.
136+
const zh = await read({ headers: { 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8' } });
137+
expect(zh('sqlite').configSchema.properties.filename.title).toBe('文件名');
138+
expect(zh('sqlite').configSchema.properties.autoMigrate.title).toBe('自动迁移表结构');
139+
expect(zh('postgres').configSchema.properties.host.title).toBe('主机');
140+
expect(zh('postgres').configSchema.properties.port.title).toBe('端口');
141+
expect(zh('mysql').description).toBe('MySQL 或 MariaDB 连接。');
142+
// A word the locale does not translate is the English one, not a key.
143+
expect(zh('sqlite').label).toBe('SQLite');
144+
145+
// `?locale=` when no header names a language.
146+
const query = await read(undefined, '/api/v1/datasources/drivers?locale=zh-CN');
147+
expect(query('sqlite').configSchema.properties.filename.title).toBe('文件名');
148+
149+
// A Traditional Chinese reader gets Traditional, never the Simplified text.
150+
const zhTW = await read({ headers: { 'accept-language': 'zh-TW,zh;q=0.9' } });
151+
expect(zhTW('sqlite').configSchema.properties.filename.title).toBe('檔案名稱');
152+
expect(zhTW('postgres').configSchema.properties.port.title).toBe('連接埠');
153+
154+
// Every other language on the platform table is served too.
155+
const fr = await read({ headers: { 'accept-language': 'fr-FR,fr;q=0.9' } });
156+
expect(fr('sqlite').configSchema.properties.filename.title).toBe('Nom de fichier');
157+
158+
// English, and a language the platform table does not name, are served the English words.
159+
for (const lang of ['en-US', 'it-IT']) {
160+
const en = await read({ headers: { 'accept-language': lang } });
161+
expect(en('sqlite').configSchema.properties.filename.title, lang).toBe('Filename');
162+
expect(en('postgres').configSchema.properties.host.title, lang).toBe('Host');
163+
}
164+
});
165+
125166
it('GET /api/v1/datasources/:name/remote-tables lists remote tables', async () => {
126167
const listRemoteTables = vi.fn().mockResolvedValue([{ name: 'customers', columnCount: 4 }]);
127168
const app = mount({ listRemoteTables });

‎packages/services/service-datasource/src/__tests__/driver-catalog.test.ts‎

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import {
2323
type BuiltinDriverId,
2424
} from '@objectstack/spec/data';
2525

26-
import { DRIVER_CATALOG } from '../driver-catalog.js';
26+
import { PLATFORM_LOCALE_TAGS } from '@objectstack/spec/system';
27+
28+
import { DRIVER_CATALOG, driverCatalogFor, driverCatalogLocaleOf } from '../driver-catalog.js';
29+
import { DRIVER_CATALOG_TRANSLATIONS } from '../driver-catalog-translations.js';
2730

2831
describe('DRIVER_CATALOG', () => {
2932
it('serves the spec projection for every offered driver', () => {
@@ -82,3 +85,172 @@ describe('DRIVER_CATALOG', () => {
8285
}
8386
});
8487
});
88+
89+
type JsonProp = { title?: string; description?: string; type?: string };
90+
const propsOf = (entry: { configSchema: Record<string, unknown> }) =>
91+
(entry.configSchema as { properties: Record<string, JsonProp> }).properties;
92+
const entryOf = (catalog: typeof DRIVER_CATALOG, id: string) => {
93+
const entry = catalog.find((d) => d.id === id);
94+
if (!entry) throw new Error(`no ${id} entry`);
95+
return entry;
96+
};
97+
const filenameTitle = (tag: string | undefined) => propsOf(entryOf(driverCatalogFor(tag), 'sqlite')).filename.title;
98+
99+
/** The labels that are product names, served as written in every language. */
100+
const PRODUCT_NAME_LABELS = new Set(['sqlite', 'postgres', 'mysql', 'mongodb']);
101+
102+
/**
103+
* The catalog's words in the requester's language. The New datasource dialog
104+
* draws exactly what the drivers route serves and has no key of its own for
105+
* these words, so a Chinese console read `Filename`, `Auto-migrate schema`,
106+
* `Host` and `Port` in English until the route served them translated.
107+
*/
108+
describe('driverCatalogFor', () => {
109+
it('serves Simplified Chinese words for SQLite and the network drivers', () => {
110+
const zh = driverCatalogFor('zh-CN');
111+
112+
const sqlite = entryOf(zh, 'sqlite');
113+
expect(sqlite.description).toBe('基于文件(或内存)的 SQL 数据库,适合本地开发和小型部署。');
114+
expect(propsOf(sqlite).filename.title).toBe('文件名');
115+
expect(propsOf(sqlite).filename.description).toBe('数据库文件路径,或填 ":memory:" 使用临时内存数据库');
116+
expect(propsOf(sqlite).autoMigrate.title).toBe('自动迁移表结构');
117+
118+
const postgres = entryOf(zh, 'postgres');
119+
expect(propsOf(postgres).host).toMatchObject({ title: '主机', description: '主机地址' });
120+
expect(propsOf(postgres).port).toMatchObject({ title: '端口', description: '端口号' });
121+
expect(propsOf(postgres).password.title).toBe('密码');
122+
123+
expect(propsOf(entryOf(zh, 'mysql')).host.title).toBe('主机');
124+
expect(propsOf(entryOf(zh, 'mongodb')).port.title).toBe('端口');
125+
expect(entryOf(zh, 'memory').label).toBe('内存');
126+
});
127+
128+
it('serves a word of its own in every language on the platform table', () => {
129+
const expected: Record<string, string> = {
130+
'zh-Hans': '文件名',
131+
'zh-Hant': '檔案名稱',
132+
es: 'Nombre de archivo',
133+
ar: 'اسم الملف',
134+
fr: 'Nom de fichier',
135+
'pt-BR': 'Nome do arquivo',
136+
ru: 'Имя файла',
137+
de: 'Dateiname',
138+
ja: 'ファイル名',
139+
ko: '파일 이름',
140+
};
141+
expect(Object.keys(expected).sort()).toEqual(PLATFORM_LOCALE_TAGS.filter((t) => t !== 'en').sort());
142+
for (const [tag, title] of Object.entries(expected)) expect(filenameTitle(tag), tag).toBe(title);
143+
});
144+
145+
it('reads Traditional Chinese codes as Traditional, never Simplified', () => {
146+
for (const tag of ['zh-TW', 'zh-HK', 'zh-MO', 'zh-Hant', 'zh-hant', 'zh-Hant-TW', 'zh-Hant-HK']) {
147+
expect(driverCatalogLocaleOf(tag), tag).toBe('zh-Hant');
148+
expect(filenameTitle(tag), tag).toBe('檔案名稱');
149+
}
150+
for (const tag of ['zh', 'zh-CN', 'zh-cn', 'zh-SG', 'zh-Hans', 'zh-Hans-CN']) {
151+
expect(driverCatalogLocaleOf(tag), tag).toBe('zh-Hans');
152+
expect(filenameTitle(tag), tag).toBe('文件名');
153+
}
154+
});
155+
156+
it('reads aliases and regional codes through the platform table', () => {
157+
const cases: Array<[string, string]> = [
158+
['ja-JP', 'ja'], ['es-ES', 'es'], ['es-MX', 'es'], ['pt', 'pt-BR'], ['pt-PT', 'pt-BR'],
159+
['fr-CA', 'fr'], ['de-AT', 'de'], ['ko-KR', 'ko'], ['ar-EG', 'ar'], ['ru-RU', 'ru'],
160+
];
161+
for (const [code, tag] of cases) expect(driverCatalogLocaleOf(code), code).toBe(tag);
162+
});
163+
164+
it('keeps everything but the words: ids, icons, types and defaults are the English entry\'s', () => {
165+
for (const tag of PLATFORM_LOCALE_TAGS) {
166+
const served = driverCatalogFor(tag);
167+
expect(served.map((d) => [d.id, d.icon]), tag).toEqual(DRIVER_CATALOG.map((d) => [d.id, d.icon]));
168+
for (const entry of served) {
169+
const english = propsOf(entryOf(DRIVER_CATALOG, entry.id));
170+
const translated = propsOf(entry);
171+
expect(Object.keys(translated), `${tag}.${entry.id}`).toEqual(Object.keys(english));
172+
for (const key of Object.keys(english)) {
173+
const { title: _t, description: _d, ...rest } = translated[key];
174+
const { title: _et, description: _ed, ...englishRest } = english[key];
175+
expect(rest, `${tag}.${entry.id}.${key}`).toEqual(englishRest);
176+
}
177+
}
178+
}
179+
});
180+
181+
it('serves English for no locale, English, or a code the platform table does not name', () => {
182+
for (const tag of [undefined, '', 'en', 'en-US', 'en-GB', 'it', 'nl-NL', 'constructor', '__proto__']) {
183+
expect(driverCatalogFor(tag), String(tag)).toBe(DRIVER_CATALOG);
184+
}
185+
expect(propsOf(entryOf(DRIVER_CATALOG, 'sqlite')).filename.title).toBe('Filename');
186+
expect(propsOf(entryOf(DRIVER_CATALOG, 'postgres')).host.title).toBe('Host');
187+
});
188+
189+
it('falls back to the English word where a language has none, never to a key', () => {
190+
const zh = driverCatalogFor('zh-CN');
191+
// Product names are not translated: the English label is served.
192+
expect(entryOf(zh, 'sqlite').label).toBe('SQLite');
193+
expect(entryOf(zh, 'postgres').label).toBe('PostgreSQL');
194+
// The memory driver's fields carry a description and no title in English;
195+
// the translation adds no title, so the form still names the field by key.
196+
expect(propsOf(entryOf(zh, 'memory')).strictMode.title).toBeUndefined();
197+
expect(propsOf(entryOf(zh, 'memory')).strictMode.description).toBe('记录不存在时抛出错误,而不是返回 null');
198+
});
199+
200+
it('never writes the spec projection it overlays', () => {
201+
for (const tag of PLATFORM_LOCALE_TAGS) driverCatalogFor(tag);
202+
const sqlite = getDriverConfigJsonSchemaById('sqlite') as { properties: Record<string, JsonProp> };
203+
expect(sqlite.properties.filename.title).toBe('Filename');
204+
expect(propsOf(entryOf(DRIVER_CATALOG, 'sqlite')).filename.title).toBe('Filename');
205+
expect(entryOf(DRIVER_CATALOG, 'memory').label).toBe('In-Memory');
206+
});
207+
});
208+
209+
/**
210+
* Every language on the platform locale table against the served shape. It
211+
* walks `PLATFORM_LOCALE_TAGS`, not the translation table's own keys, so a
212+
* language the table lacks goes red here: the platform has one list of
213+
* languages, each complete. A driver or field a language names must exist, so
214+
* a renamed field cannot leave a translation that silently stops applying; and
215+
* every word the English form shows must be translated, so a field the spec
216+
* adds goes red here rather than reading English in a translated dialog.
217+
*/
218+
describe('DRIVER_CATALOG_TRANSLATIONS coverage', () => {
219+
const offered = new Set(DRIVER_CATALOG.map((d) => d.id));
220+
const languages = PLATFORM_LOCALE_TAGS.filter((tag) => tag !== 'en');
221+
222+
it('carries every non-English language on the platform table, and nothing else', () => {
223+
expect(PLATFORM_LOCALE_TAGS).toContain('en');
224+
expect(Object.keys(DRIVER_CATALOG_TRANSLATIONS).sort()).toEqual([...languages].sort());
225+
});
226+
227+
for (const locale of languages) {
228+
it(`${locale}: names only drivers and fields the catalog serves`, () => {
229+
const table = DRIVER_CATALOG_TRANSLATIONS[locale];
230+
expect(table, locale).toBeDefined();
231+
for (const [id, text] of Object.entries(table ?? {})) {
232+
expect(offered.has(id), `${locale}.${id}`).toBe(true);
233+
const props = propsOf(entryOf(DRIVER_CATALOG, id));
234+
for (const key of Object.keys(text?.fields ?? {})) {
235+
expect(Object.prototype.hasOwnProperty.call(props, key), `${locale}.${id}.fields.${key}`).toBe(true);
236+
}
237+
}
238+
});
239+
240+
it(`${locale}: translates every word the English form shows`, () => {
241+
const table = DRIVER_CATALOG_TRANSLATIONS[locale];
242+
for (const entry of DRIVER_CATALOG) {
243+
const text = table?.[entry.id as keyof typeof table];
244+
expect(text?.description, `${locale}.${entry.id}.description`).toBeTruthy();
245+
if (!PRODUCT_NAME_LABELS.has(entry.id)) expect(text?.label, `${locale}.${entry.id}.label`).toBeTruthy();
246+
for (const [key, prop] of Object.entries(propsOf(entry))) {
247+
const field = text?.fields?.[key];
248+
if (prop.title !== undefined) expect(field?.title, `${locale}.${entry.id}.${key}.title`).toBeTruthy();
249+
if (prop.description !== undefined) {
250+
expect(field?.description, `${locale}.${entry.id}.${key}.description`).toBeTruthy();
251+
}
252+
}
253+
}
254+
});
255+
}
256+
});

‎packages/services/service-datasource/src/admin-routes.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
} from '@objectstack/core';
3636
import type { TenancyPosture } from '@objectstack/spec/security';
3737
import type { ErrorCode } from '@objectstack/spec/api';
38+
import { preferredLocaleFromHeader } from '@objectstack/spec/system';
3839
// The slots this module resolves, by their declared contracts. Erasing a
3940
// lookup to `any` is banned (#4127/#4176/#4202/#4251) and the ban is right
4041
// here: `IAuthService` is what declares BOTH shapes of the session accessor
@@ -44,7 +45,7 @@ import type { ErrorCode } from '@objectstack/spec/api';
4445
import type { IAuthService, IDataEngine, IHttpServer } from '@objectstack/spec/contracts';
4546
// The declared envelope is written in ONE place for the whole platform (#3973).
4647
import { inProcessSessionReadInput, sendOk, sendError } from '@objectstack/types';
47-
import { DRIVER_CATALOG } from './driver-catalog.js';
48+
import { driverCatalogFor } from './driver-catalog.js';
4849
import { DRIVER_CATALOG_SEGMENT } from './datasource-route-reserved-names.js';
4950
// [#27618] The package's one "no such datasource" refusal: GET answers an
5051
// unknown name in its words, as every other by-name door does.
@@ -201,6 +202,19 @@ function toWebHeaders(raw: unknown): Headers | undefined {
201202
return undefined;
202203
}
203204

205+
/**
206+
* The requester's display language, read the way the REST metadata door and
207+
* the settings routes read it: the top `Accept-Language` tag, else a string
208+
* `?locale=`. The console sends its own language as `Accept-Language` on every
209+
* API call. It picks the language of served words only and authorizes nothing.
210+
*/
211+
function requestLocaleOf(req: { headers?: unknown; query?: Record<string, unknown> } | undefined): string | undefined {
212+
const preferred = preferredLocaleFromHeader(toWebHeaders(req?.headers)?.get('accept-language'));
213+
if (preferred) return preferred;
214+
const query = req?.query?.locale;
215+
return typeof query === 'string' && query.length > 0 ? query : undefined;
216+
}
217+
204218
/**
205219
* A `getSession(headers)` bound to the kernel's `auth` service, or `undefined`
206220
* when this deployment registers none.
@@ -705,9 +719,10 @@ export function registerDatasourceAdminRoutes(
705719
// Catalog of connection drivers + their JSON-Schema config (drives the
706720
// Studio connection form). Static metadata — no service dependency, so it
707721
// is always available even before any datasource-admin service is wired.
722+
// Its words are served in the requester's language (`driverCatalogFor`).
708723
server.get(`${root}/${DRIVER_CATALOG_SEGMENT}`, async (req: any, res: any) => {
709724
if (await requireDatasourceAdmin(req, res)) return;
710-
sendOk(res, { drivers: DRIVER_CATALOG });
725+
sendOk(res, { drivers: driverCatalogFor(requestLocaleOf(req)) });
711726
});
712727

713728
// Read-only schema introspection for the Studio "sync objects" flow.

0 commit comments

Comments
 (0)