55 * its response in the standard envelope: `{ data: { locale, translations } }`.
66 * We extract `data.translations` when present, and fall back to the raw JSON
77 * for mock / local-dev environments that may return flat translation objects.
8+ *
9+ * When the response uses the @objectstack/spec `TranslationData` format
10+ * (objects with nested fields), we automatically transform it into the flat
11+ * format expected by @object-ui/i18n's `useObjectLabel` hook:
12+ * - `objects.{name}.fields.{field}.label` → `fields.{name}.{field}` (string)
13+ * - `objects.{name}.fields.{field}.options` → `fieldOptions.{name}.{field}`
14+ * - Wrapped under an `app` namespace key for hook discovery
815 */
916export async function loadLanguage ( lang : string ) : Promise < Record < string , unknown > > {
1017 try {
@@ -15,13 +22,88 @@ export async function loadLanguage(lang: string): Promise<Record<string, unknown
1522 }
1623 const json = await res . json ( ) ;
1724 // Unwrap the spec REST API envelope when present
25+ let translations : Record < string , unknown > ;
1826 if ( json ?. data ?. translations && typeof json . data . translations === 'object' ) {
19- return json . data . translations as Record < string , unknown > ;
27+ translations = json . data . translations as Record < string , unknown > ;
28+ } else {
29+ // Fallback: mock server / local dev returns flat translation objects
30+ translations = json ;
31+ }
32+ // Auto-transform @objectstack/spec TranslationData format when detected
33+ if ( isSpecTranslationData ( translations ) ) {
34+ return transformSpecTranslations ( translations ) ;
2035 }
21- // Fallback: mock server / local dev returns flat translation objects
22- return json ;
36+ return translations ;
2337 } catch ( err ) {
2438 console . warn ( `[i18n] Failed to load translations for '${ lang } ':` , err ) ;
2539 return { } ;
2640 }
2741}
42+
43+ /**
44+ * Detect whether the data uses the @objectstack/spec `TranslationData` format.
45+ *
46+ * TranslationData has: `objects.{name}.fields.{field}.label` (nested objects).
47+ * The flat format has: `{namespace}.objects.{name}.label` + `{namespace}.fields.{name}.{field}` (string).
48+ *
49+ * We check if `data.objects` exists AND at least one object has a nested `fields`
50+ * key whose values are objects (not strings).
51+ */
52+ function isSpecTranslationData ( data : Record < string , unknown > ) : boolean {
53+ const objects = data . objects ;
54+ if ( ! objects || typeof objects !== 'object' || Array . isArray ( objects ) ) return false ;
55+ for ( const obj of Object . values ( objects as Record < string , unknown > ) ) {
56+ if ( obj && typeof obj === 'object' && ! Array . isArray ( obj ) && 'fields' in obj ) {
57+ return true ;
58+ }
59+ }
60+ return false ;
61+ }
62+
63+ /**
64+ * Transform `TranslationData` (objectstack/spec) into the flat format
65+ * expected by `useObjectLabel` (object-ui/i18n).
66+ *
67+ * The result is wrapped under an `app` namespace key so that
68+ * `useObjectLabel.getAppNamespaces()` discovers it via the presence
69+ * of `objects` and `fields` sub-keys.
70+ */
71+ function transformSpecTranslations ( data : Record < string , unknown > ) : Record < string , unknown > {
72+ const objects : Record < string , unknown > = { } ;
73+ const fields : Record < string , Record < string , string > > = { } ;
74+ const fieldOptions : Record < string , Record < string , Record < string , string > > > = { } ;
75+
76+ const srcObjects = data . objects as Record < string , any > | undefined ;
77+ if ( srcObjects ) {
78+ for ( const [ objName , objData ] of Object . entries ( srcObjects ) ) {
79+ if ( ! objData || typeof objData !== 'object' ) continue ;
80+
81+ // Object-level metadata
82+ const obj : Record < string , unknown > = { label : objData . label } ;
83+ if ( objData . pluralLabel ) obj . pluralLabel = objData . pluralLabel ;
84+ objects [ objName ] = obj ;
85+
86+ // Flatten fields: objects.X.fields.Y.label → fields.X.Y = string
87+ if ( objData . fields && typeof objData . fields === 'object' ) {
88+ fields [ objName ] = { } ;
89+ for ( const [ fieldName , fieldData ] of Object . entries ( objData . fields as Record < string , any > ) ) {
90+ if ( fieldData ?. label ) fields [ objName ] [ fieldName ] = fieldData . label ;
91+ if ( fieldData ?. options && typeof fieldData . options === 'object' && Object . keys ( fieldData . options ) . length > 0 ) {
92+ if ( ! fieldOptions [ objName ] ) fieldOptions [ objName ] = { } ;
93+ fieldOptions [ objName ] [ fieldName ] = fieldData . options ;
94+ }
95+ }
96+ }
97+ }
98+ }
99+
100+ const appNs : Record < string , unknown > = { } ;
101+ if ( Object . keys ( objects ) . length > 0 ) appNs . objects = objects ;
102+ if ( Object . keys ( fields ) . length > 0 ) appNs . fields = fields ;
103+ if ( Object . keys ( fieldOptions ) . length > 0 ) appNs . fieldOptions = fieldOptions ;
104+ if ( data . apps ) appNs . apps = data . apps ;
105+ if ( data . messages ) appNs . messages = data . messages ;
106+ if ( data . validationMessages ) appNs . validationMessages = data . validationMessages ;
107+
108+ return { app : appNs } ;
109+ }
0 commit comments