@@ -22,11 +22,9 @@ export interface I18nOptions {
2222 locales : Record <
2323 string ,
2424 {
25- file : string ;
26- format ?: string ;
27- translation ?: unknown ;
25+ files : { path : string ; integrity ?: string ; format ?: string } [ ] ;
26+ translation ?: Record < string , unknown > ;
2827 dataPath ?: string ;
29- integrity ?: string ;
3028 baseHref ?: string ;
3129 }
3230 > ;
@@ -35,6 +33,29 @@ export interface I18nOptions {
3533 veCompatLocale ?: string ;
3634}
3735
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+
3859export function createI18nOptions (
3960 metadata : json . JsonObject ,
4061 inline ?: boolean | string [ ] ,
@@ -75,32 +96,24 @@ export function createI18nOptions(
7596 }
7697
7798 i18n . locales [ i18n . sourceLocale ] = {
78- file : '' ,
99+ files : [ ] ,
79100 baseHref : rawSourceLocaleBaseHref ,
80101 } ;
81102
82103 if ( metadata . locales !== undefined && ! json . isJsonObject ( metadata . locales ) ) {
83104 throw new Error ( 'Project i18n locales field is malformed. Expected an object.' ) ;
84105 } else if ( metadata . locales ) {
85106 for ( const [ locale , options ] of Object . entries ( metadata . locales ) ) {
86- let translationFile ;
107+ let translationFiles ;
87108 let baseHref ;
88109 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+
95112 if ( typeof options . baseHref === 'string' ) {
96113 baseHref = options . baseHref ;
97114 }
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- ) ;
102115 } else {
103- translationFile = options ;
116+ translationFiles = normalizeTranslationFileOption ( options , locale , true ) ;
104117 }
105118
106119 if ( locale === i18n . sourceLocale ) {
@@ -110,7 +123,7 @@ export function createI18nOptions(
110123 }
111124
112125 i18n . locales [ locale ] = {
113- file : translationFile ,
126+ files : translationFiles . map ( ( file ) => ( { path : file } ) ) ,
114127 baseHref,
115128 } ;
116129 }
@@ -226,33 +239,55 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
226239 desc . dataPath = localeDataPath ;
227240 }
228241
229- if ( ! desc . file ) {
242+ if ( ! desc . files . length ) {
230243 continue ;
231244 }
232245
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 ) ) ;
234248
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)
237268 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.' ,
239270 ) ;
240- } else {
241- context . logger . warn ( `WARNING [${ desc . file } ]: ${ diagnostics . message } ` ) ;
242271 }
243- }
244272
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+ }
251290 }
252-
253- desc . format = result . format ;
254- desc . translation = result . translation ;
255- desc . integrity = result . integrity ;
256291 }
257292
258293 // Legacy message id's require the format of the translations
@@ -265,7 +300,12 @@ export async function configureI18nBuild<T extends BrowserBuilderSchema | Server
265300 i18n . veCompatLocale = buildOptions . i18nLocale = [ ...i18n . inlineLocales ] [ 0 ] ;
266301
267302 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 ;
269309 }
270310
271311 // Clear inline locales to prevent any new i18n related processing
@@ -306,12 +346,12 @@ function mergeDeprecatedI18nOptions(
306346 i18n . inlineLocales . add ( i18nLocale ) ;
307347
308348 if ( i18nFile !== undefined ) {
309- i18n . locales [ i18nLocale ] = { file : i18nFile , baseHref : '' } ;
349+ i18n . locales [ i18nLocale ] = { files : [ { path : i18nFile } ] , baseHref : '' } ;
310350 } else {
311351 // If no file, treat the locale as the source locale
312352 // This mimics deprecated behavior
313353 i18n . sourceLocale = i18nLocale ;
314- i18n . locales [ i18nLocale ] = { file : '' , baseHref : '' } ;
354+ i18n . locales [ i18nLocale ] = { files : [ ] , baseHref : '' } ;
315355 }
316356
317357 i18n . flatOutput = true ;
0 commit comments