-
Notifications
You must be signed in to change notification settings - Fork 6
feat: add /i18n route dispatch to HttpDispatcher #861
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -475,6 +475,64 @@ export class HttpDispatcher { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { handled: false }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Handles i18n requests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * path: sub-path after /i18n/ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Routes: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /locales → getLocales | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /translations/:locale → getTranslations (locale from path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /translations?locale=xx → getTranslations (locale from query) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /labels/:object/:locale → getFieldLabels (both from path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * GET /labels/:object?locale=xx → getFieldLabels (locale from query) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async handleI18n(path: string, method: string, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const i18nService = await this.getService(CoreServiceName.enum.i18n); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!i18nService) return { handled: true, response: this.error('i18n service not available', 501) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m = method.toUpperCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parts = path.replace(/^\/+/, '').split('/').filter(Boolean); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (m !== 'GET') return { handled: false }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GET /i18n/locales | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parts[0] === 'locales' && parts.length === 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const locales = i18nService.getLocales(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { handled: true, response: this.success({ locales }) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+498
to
+502
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GET /i18n/translations/:locale OR /i18n/translations?locale=xx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parts[0] === 'translations') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const locale = parts[1] ? decodeURIComponent(parts[1]) : query?.locale; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!locale) return { handled: true, response: this.error('Missing locale parameter', 400) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const translations = i18nService.getTranslations(locale); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const translations = i18nService.getTranslations(locale); | |
| // Base translation bundle for the requested locale | |
| const fullTranslations = i18nService.getTranslations(locale) || {}; | |
| // Optional filtering via namespace / keys query params | |
| const rawNamespace = query?.namespace; | |
| const rawKeys = query?.keys; | |
| // Validate basic types for namespace | |
| if (rawNamespace !== undefined && typeof rawNamespace !== 'string' && !Array.isArray(rawNamespace)) { | |
| return { handled: true, response: this.error('Invalid namespace parameter', 400) }; | |
| } | |
| // Validate basic types for keys | |
| if (rawKeys !== undefined && typeof rawKeys !== 'string' && !Array.isArray(rawKeys)) { | |
| return { handled: true, response: this.error('Invalid keys parameter', 400) }; | |
| } | |
| // Normalize namespace(s) to a string array | |
| const namespaces: string[] = []; | |
| if (typeof rawNamespace === 'string') { | |
| for (const ns of rawNamespace.split(',')) { | |
| const trimmed = ns.trim(); | |
| if (trimmed) namespaces.push(trimmed); | |
| } | |
| } else if (Array.isArray(rawNamespace)) { | |
| for (const ns of rawNamespace) { | |
| if (typeof ns === 'string') { | |
| const trimmed = ns.trim(); | |
| if (trimmed) namespaces.push(trimmed); | |
| } | |
| } | |
| } | |
| // Normalize keys to a string array (client sends comma-separated list) | |
| const keyFilters: string[] = []; | |
| if (typeof rawKeys === 'string') { | |
| for (const k of rawKeys.split(',')) { | |
| const trimmed = k.trim(); | |
| if (trimmed) keyFilters.push(trimmed); | |
| } | |
| } else if (Array.isArray(rawKeys)) { | |
| for (const k of rawKeys) { | |
| if (typeof k === 'string') { | |
| const trimmed = k.trim(); | |
| if (trimmed) keyFilters.push(trimmed); | |
| } | |
| } | |
| } | |
| // If both filters are supplied, treat as unsupported to avoid ambiguity | |
| if (namespaces.length > 0 && keyFilters.length > 0) { | |
| return { handled: true, response: this.error('Cannot combine namespace and keys filters', 400) }; | |
| } | |
| let translations = fullTranslations; | |
| if (namespaces.length > 0) { | |
| const filtered: Record<string, any> = {}; | |
| for (const [key, value] of Object.entries(fullTranslations)) { | |
| for (const ns of namespaces) { | |
| if ( | |
| key === ns || | |
| key.startsWith(ns + '.') || | |
| key.startsWith(ns + ':') | |
| ) { | |
| filtered[key] = value; | |
| break; | |
| } | |
| } | |
| } | |
| translations = filtered; | |
| } else if (keyFilters.length > 0) { | |
| const filtered: Record<string, any> = {}; | |
| for (const k of keyFilters) { | |
| if (Object.prototype.hasOwnProperty.call(fullTranslations, k)) { | |
| filtered[k] = (fullTranslations as any)[k]; | |
| } | |
| } | |
| translations = filtered; | |
| } |
Copilot
AI
Mar 3, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The /labels fallback derives labels by scanning for flat keys with prefix o.${objectName}.fields. and casting values to string. This won’t work with the repo’s TranslationDataSchema / FileI18nAdapter, where translations are nested under objects.{object}.fields.{field} and field values are objects (label/help/options). In production, this likely returns an empty labels map or incorrect values; consider extracting from translations.objects?.[objectName]?.fields and shaping the response accordingly.
| const prefix = `o.${objectName}.fields.`; | |
| const labels: Record<string, string> = {}; | |
| for (const [key, value] of Object.entries(translations)) { | |
| if (key.startsWith(prefix)) { | |
| labels[key.substring(prefix.length)] = value as string; | |
| } | |
| } | |
| const labels: Record<string, string> = {}; | |
| // Preferred: TranslationDataSchema / FileI18nAdapter nested structure: | |
| // translations.objects?.[objectName]?.fields?.[fieldName]?.label | |
| const objectTranslations = (translations as any)?.objects?.[objectName]; | |
| const fieldTranslations = objectTranslations && typeof objectTranslations === 'object' | |
| ? (objectTranslations as any).fields | |
| : undefined; | |
| if (fieldTranslations && typeof fieldTranslations === 'object') { | |
| for (const [fieldName, fieldTranslation] of Object.entries(fieldTranslations)) { | |
| const label = (fieldTranslation as any)?.label; | |
| if (typeof label === 'string') { | |
| labels[fieldName] = label; | |
| } | |
| } | |
| } else { | |
| // Legacy fallback: flat keys like "o.${objectName}.fields.${field}" | |
| const prefix = `o.${objectName}.fields.`; | |
| for (const [key, value] of Object.entries(translations as any)) { | |
| if (key.startsWith(prefix) && typeof value === 'string') { | |
| labels[key.substring(prefix.length)] = value; | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback test for missing getFieldLabels uses flat translation keys like
o.contact.fields.first_nameand expects string values, but the default TranslationDataSchema / FileI18nAdapter returns nested objects (e.g.translations.objects.contact.fields.first_name.label). As written, this test can pass while the production fallback remains broken. Adjust the test fixture to use the real nested structure and assert the expected response shape.