@@ -22,11 +22,9 @@ export interface I18nOptions {
22
22
locales : Record <
23
23
string ,
24
24
{
25
- file : string ;
26
- format ?: string ;
27
- translation ?: unknown ;
25
+ files : { path : string ; integrity ?: string ; format ?: string } [ ] ;
26
+ translation ?: Record < string , unknown > ;
28
27
dataPath ?: string ;
29
- integrity ?: string ;
30
28
baseHref ?: string ;
31
29
}
32
30
> ;
@@ -35,6 +33,29 @@ export interface I18nOptions {
35
33
veCompatLocale ?: string ;
36
34
}
37
35
36
+ function normalizeTranslationFileOption (
37
+ option : json . JsonValue ,
38
+ locale : string ,
39
+ expectObjectInError : boolean ,
40
+ ) : string [ ] {
41
+ if ( typeof option === 'string' ) {
42
+ return [ option ] ;
43
+ }
44
+
45
+ if ( Array . isArray ( option ) && option . every ( ( element ) => typeof element === 'string' ) ) {
46
+ return option as string [ ] ;
47
+ }
48
+
49
+ let errorMessage = `Project i18n locales translation field value for '${ locale } ' is malformed. ` ;
50
+ if ( expectObjectInError ) {
51
+ errorMessage += 'Expected a string, array of strings, or object.' ;
52
+ } else {
53
+ errorMessage += 'Expected a string or array of strings.' ;
54
+ }
55
+
56
+ throw new Error ( errorMessage ) ;
57
+ }
58
+
38
59
export function createI18nOptions (
39
60
metadata : json . JsonObject ,
40
61
inline ?: boolean | string [ ] ,
@@ -75,32 +96,24 @@ export function createI18nOptions(
75
96
}
76
97
77
98
i18n . locales [ i18n . sourceLocale ] = {
78
- file : '' ,
99
+ files : [ ] ,
79
100
baseHref : rawSourceLocaleBaseHref ,
80
101
} ;
81
102
82
103
if ( metadata . locales !== undefined && ! json . isJsonObject ( metadata . locales ) ) {
83
104
throw new Error ( 'Project i18n locales field is malformed. Expected an object.' ) ;
84
105
} else if ( metadata . locales ) {
85
106
for ( const [ locale , options ] of Object . entries ( metadata . locales ) ) {
86
- let translationFile ;
107
+ let translationFiles ;
87
108
let baseHref ;
88
109
if ( json . isJsonObject ( options ) ) {
89
- if ( typeof options . translation !== 'string' ) {
90
- throw new Error (
91
- `Project i18n locales translation field value for '${ locale } ' is malformed. Expected a string.` ,
92
- ) ;
93
- }
94
- translationFile = options . translation ;
110
+ translationFiles = normalizeTranslationFileOption ( options . translation , locale , false ) ;
111
+
95
112
if ( typeof options . baseHref === 'string' ) {
96
113
baseHref = options . baseHref ;
97
114
}
98
- } else if ( typeof options !== 'string' ) {
99
- throw new Error (
100
- `Project i18n locales field value for '${ locale } ' is malformed. Expected a string or object.` ,
101
- ) ;
102
115
} else {
103
- translationFile = options ;
116
+ translationFiles = normalizeTranslationFileOption ( options , locale , true ) ;
104
117
}
105
118
106
119
if ( locale === i18n . sourceLocale ) {
@@ -110,7 +123,7 @@ export function createI18nOptions(
110
123
}
111
124
112
125
i18n . locales [ locale ] = {
113
- file : translationFile ,
126
+ files : translationFiles . map ( ( file ) => ( { path : file } ) ) ,
114
127
baseHref,
115
128
} ;
116
129
}
@@ -226,33 +239,55 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
226
239
desc . dataPath = localeDataPath ;
227
240
}
228
241
229
- if ( ! desc . file ) {
242
+ if ( ! desc . files . length ) {
230
243
continue ;
231
244
}
232
245
233
- const result = loader ( path . join ( context . workspaceRoot , desc . file ) ) ;
246
+ for ( const file of desc . files ) {
247
+ const loadResult = loader ( path . join ( context . workspaceRoot , file . path ) ) ;
234
248
235
- for ( const diagnostics of result . diagnostics . messages ) {
236
- if ( diagnostics . type === 'error' ) {
249
+ for ( const diagnostics of loadResult . diagnostics . messages ) {
250
+ if ( diagnostics . type === 'error' ) {
251
+ throw new Error (
252
+ `Error parsing translation file '${ file . path } ': ${ diagnostics . message } ` ,
253
+ ) ;
254
+ } else {
255
+ context . logger . warn ( `WARNING [${ file . path } ]: ${ diagnostics . message } ` ) ;
256
+ }
257
+ }
258
+
259
+ if ( loadResult . locale !== undefined && loadResult . locale !== locale ) {
260
+ context . logger . warn (
261
+ `WARNING [${ file . path } ]: File target locale ('${ loadResult . locale } ') does not match configured locale ('${ locale } ')` ,
262
+ ) ;
263
+ }
264
+
265
+ usedFormats . add ( loadResult . format ) ;
266
+ if ( usedFormats . size > 1 && tsConfig . options . enableI18nLegacyMessageIdFormat !== false ) {
267
+ // This limitation is only for legacy message id support (defaults to true as of 9.0)
237
268
throw new Error (
238
- `Error parsing translation file ' ${ desc . file } ': ${ diagnostics . message } ` ,
269
+ 'Localization currently only supports using one type of translation file format for the entire application.' ,
239
270
) ;
240
- } else {
241
- context . logger . warn ( `WARNING [${ desc . file } ]: ${ diagnostics . message } ` ) ;
242
271
}
243
- }
244
272
245
- usedFormats . add ( result . format ) ;
246
- if ( usedFormats . size > 1 && tsConfig . options . enableI18nLegacyMessageIdFormat !== false ) {
247
- // This limitation is only for legacy message id support (defaults to true as of 9.0)
248
- throw new Error (
249
- 'Localization currently only supports using one type of translation file format for the entire application.' ,
250
- ) ;
273
+ file . format = loadResult . format ;
274
+ file . integrity = loadResult . integrity ;
275
+
276
+ if ( desc . translation ) {
277
+ // Merge translations
278
+ for ( const [ id , message ] of Object . entries ( loadResult . translations ) ) {
279
+ if ( desc . translation [ id ] !== undefined ) {
280
+ context . logger . warn (
281
+ `WARNING [${ file . path } ]: Duplicate translations for message '${ id } ' when merging` ,
282
+ ) ;
283
+ }
284
+ desc . translation [ id ] = message ;
285
+ }
286
+ } else {
287
+ // First or only translation file
288
+ desc . translation = loadResult . translations ;
289
+ }
251
290
}
252
-
253
- desc . format = result . format ;
254
- desc . translation = result . translation ;
255
- desc . integrity = result . integrity ;
256
291
}
257
292
258
293
// Legacy message id's require the format of the translations
@@ -265,7 +300,12 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
265
300
i18n . veCompatLocale = buildOptions . i18nLocale = [ ...i18n . inlineLocales ] [ 0 ] ;
266
301
267
302
if ( buildOptions . i18nLocale !== i18n . sourceLocale ) {
268
- buildOptions . i18nFile = i18n . locales [ buildOptions . i18nLocale ] . file ;
303
+ if ( i18n . locales [ buildOptions . i18nLocale ] . files . length > 1 ) {
304
+ throw new Error (
305
+ 'Localization with View Engine only supports using a single translation file per locale.' ,
306
+ ) ;
307
+ }
308
+ buildOptions . i18nFile = i18n . locales [ buildOptions . i18nLocale ] . files [ 0 ] . path ;
269
309
}
270
310
271
311
// Clear inline locales to prevent any new i18n related processing
@@ -306,12 +346,12 @@ function mergeDeprecatedI18nOptions(
306
346
i18n . inlineLocales . add ( i18nLocale ) ;
307
347
308
348
if ( i18nFile !== undefined ) {
309
- i18n . locales [ i18nLocale ] = { file : i18nFile , baseHref : '' } ;
349
+ i18n . locales [ i18nLocale ] = { files : [ { path : i18nFile } ] , baseHref : '' } ;
310
350
} else {
311
351
// If no file, treat the locale as the source locale
312
352
// This mimics deprecated behavior
313
353
i18n . sourceLocale = i18nLocale ;
314
- i18n . locales [ i18nLocale ] = { file : '' , baseHref : '' } ;
354
+ i18n . locales [ i18nLocale ] = { files : [ ] , baseHref : '' } ;
315
355
}
316
356
317
357
i18n . flatOutput = true ;
0 commit comments