@raclettejs/core 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE.md +661 -1
  3. package/dist/cli.js +10 -10
  4. package/dist/index.js +4 -4
  5. package/package.json +1 -1
  6. package/services/backend/src/core/crud/crudTypes.ts +5 -5
  7. package/services/backend/src/core/payload/payloadRegistrar.ts +15 -15
  8. package/services/backend/src/core/payload/payloadTypes.ts +4 -4
  9. package/services/backend/src/core/pluginSystem/pluginFastifyInstance.ts +4 -4
  10. package/services/backend/src/core/pluginSystem/pluginTypes.ts +4 -4
  11. package/services/backend/src/core/sockets/createSystemEventHandlers.ts +18 -16
  12. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/account.service.ts +16 -16
  13. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/events/index.ts +4 -4
  14. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/account/helpers/payload.ts +2 -2
  15. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +14 -14
  16. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/events/index.ts +3 -3
  17. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/helpers/payload.ts +2 -2
  18. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/events/index.ts +3 -3
  19. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/helpers/payload.ts +2 -2
  20. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +18 -18
  21. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/events/index.ts +4 -4
  22. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/helpers/payload.ts +2 -2
  23. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.service.ts +14 -14
  24. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/events/index.ts +5 -5
  25. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/helpers/payload.ts +2 -2
  26. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +14 -14
  27. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/events/index.ts +4 -4
  28. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/helpers/payload.ts +2 -2
  29. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.service.ts +14 -14
  30. package/services/backend/src/types/custom-fastify.d.ts +2 -2
  31. package/services/backend/src/utils/request.utils.ts +2 -2
  32. package/services/frontend/src/core/lib/data/dataApi.ts +6 -5
  33. package/services/frontend/src/core/lib/data/fetchDataHandler.ts +51 -21
  34. package/services/frontend/src/core/lib/data/queryApi.ts +20 -27
  35. package/services/frontend/src/core/lib/projectDataHelper.ts +1 -5
  36. package/services/frontend/src/core/setup/plugin-system/api/plugin-data.ts +17 -13
  37. package/services/frontend/src/core/types/DataApi.ts +13 -1
  38. package/services/frontend/src/orchestrator/assets/iconMappings/flags.json +2 -2
  39. package/services/frontend/src/orchestrator/composables/useVueQueryObservableHelper.ts +21 -11
  40. package/services/frontend/src/orchestrator/composables/useVueWriteOperationHelper.ts +27 -18
  41. package/services/frontend/src/orchestrator/i18n/de-DE.json +2 -2
  42. package/services/frontend/src/orchestrator/i18n/en-EU.json +2 -2
  43. package/services/frontend/src/orchestrator/i18n/{sk-SK.json → sk.json} +2 -2
  44. package/services/frontend/src/orchestrator/setup/i18n.ts +413 -270
  45. package/services/frontend/src/orchestrator/types/PluginApi.ts +7 -3
@@ -6,12 +6,12 @@ import defaultNumberFormats from "../i18n/numberFormats"
6
6
  import configService from "@racletteCore/lib/configService"
7
7
  import logger from "@racletteCore/lib/logger"
8
8
  import { mergeDeepRight } from "ramda"
9
+ import { $store } from "@racletteCore"
9
10
 
10
11
  // Types
11
12
  type LocaleFileImports = Record<string, () => Promise<any>>
12
13
  type Messages = Record<string, any>
13
14
  type DateLocales = Record<string, any>
14
- type LocaleEnrichmentMap = Record<string, string[]>
15
15
  type SectionPriorities = Record<string, number>
16
16
 
17
17
  interface I18nInstance {
@@ -48,359 +48,502 @@ let instanceAdapter: { adapter: any } | null = null
48
48
  let dateLocale: DateLocales = {}
49
49
  let messages: Messages = {}
50
50
 
51
- // Utility functions
52
- const extractLocaleFromFilePath = (filePath: string): string => {
51
+ // ============================================================================
52
+ // UTILITY FUNCTIONS
53
+ // ============================================================================
54
+
55
+ const extractLocaleFromPath = (filePath: string): string => {
53
56
  const fileName = filePath.split("/").pop()!
54
57
  return fileName.replace(/\.(json|js)$/, "")
55
58
  }
56
59
 
57
- const getShortLocale = (longLocale: string): string => {
58
- return longLocale.split("-")[0]
60
+ const getLanguageCode = (locale: string): string => {
61
+ return locale.split("-")[0]
59
62
  }
60
63
 
61
64
  const getAvailableLocales = (files: LocaleFileImports): string[] => {
62
- return Object.keys(files).map(extractLocaleFromFilePath)
65
+ return Object.keys(files).map(extractLocaleFromPath)
63
66
  }
64
67
 
65
- const getAvailableDateFnsLocales = (files: LocaleFileImports): string[] => {
66
- return Object.keys(files)
67
- .map(extractLocaleFromFilePath)
68
- .filter((locale) => !locale.includes("cdn") && locale !== "types")
68
+ const filterValidDateFnsLocales = (locales: string[]): string[] => {
69
+ return locales.filter(
70
+ (locale) => !locale.includes("cdn") && locale !== "types",
71
+ )
69
72
  }
70
73
 
71
- const createFormatsForLocales = (
74
+ const createFormatsForAllLocales = (
72
75
  locales: string[],
73
- defaultFormats: any
76
+ defaultFormats: any,
74
77
  ): Record<string, any> => {
75
- return locales.reduce((formats, locale) => {
76
- formats[locale] = defaultFormats.EU
77
- return formats
78
- }, {} as Record<string, any>)
78
+ return locales.reduce(
79
+ (formats, locale) => {
80
+ formats[locale] = defaultFormats.EU
81
+ return formats
82
+ },
83
+ {} as Record<string, any>,
84
+ )
79
85
  }
80
86
 
81
- // Locale filtering functions
82
- const createLocaleFilter = (targetLocales: string[]) => {
83
- return (files: LocaleFileImports): LocaleFileImports => {
84
- const filtered: LocaleFileImports = {}
85
-
86
- Object.entries(files).forEach(([filePath, importFn]) => {
87
- const fileLocale = extractLocaleFromFilePath(filePath)
88
- if (targetLocales.includes(fileLocale)) {
89
- filtered[filePath] = importFn
90
- }
91
- })
92
-
93
- return filtered
87
+ // ============================================================================
88
+ // LOCALE ENRICHMENT (FALLBACK CHAIN)
89
+ // ============================================================================
90
+
91
+ class LocaleEnrichment {
92
+ private requestedLocales: string[]
93
+ private availableLocales: string[]
94
+ private fallbackLocale: string
95
+
96
+ constructor(
97
+ requestedLocales: string[],
98
+ availableLocales: string[],
99
+ fallbackLocale: string,
100
+ ) {
101
+ this.requestedLocales = requestedLocales
102
+ this.availableLocales = availableLocales
103
+ this.fallbackLocale = fallbackLocale
94
104
  }
95
- }
96
105
 
97
- const createDateFnsLocaleFilter = (requestedLocales: string[]) => {
98
- return (files: LocaleFileImports): LocaleFileImports => {
99
- const filtered: LocaleFileImports = {}
100
- const availableDateFnsLocales = getAvailableDateFnsLocales(files)
101
-
102
- requestedLocales.forEach((locale) => {
103
- const targetLocale = findBestDateFnsLocale(locale, availableDateFnsLocales)
104
-
105
- if (targetLocale) {
106
- const matchingFilePath = Object.keys(files).find((filePath) => {
107
- return extractLocaleFromFilePath(filePath) === targetLocale
108
- })
109
-
110
- if (matchingFilePath) {
111
- filtered[matchingFilePath] = files[matchingFilePath]
112
- }
106
+ private buildFallbackChain(targetLocale: string): string[] {
107
+ const chain: string[] = []
108
+ const languageCode = getLanguageCode(targetLocale)
109
+
110
+ // 1. Try exact match
111
+ if (this.availableLocales.includes(targetLocale)) {
112
+ chain.push(targetLocale)
113
+ }
114
+
115
+ // 2. Try same language, different region
116
+ const sameLanguageLocales = this.availableLocales.filter(
117
+ (locale) =>
118
+ locale !== targetLocale &&
119
+ getLanguageCode(locale) === languageCode &&
120
+ locale.includes("-"),
121
+ )
122
+ chain.push(...sameLanguageLocales)
123
+
124
+ // 3. Try language code only
125
+ if (
126
+ this.availableLocales.includes(languageCode) &&
127
+ !chain.includes(languageCode)
128
+ ) {
129
+ chain.push(languageCode)
130
+ }
131
+
132
+ return chain
133
+ }
134
+
135
+ createEnrichmentMap(): Record<string, string[]> {
136
+ const enrichmentMap: Record<string, string[]> = {}
137
+
138
+ for (const locale of this.requestedLocales) {
139
+ let fallbackChain = this.buildFallbackChain(locale)
140
+
141
+ // Add fallback locale if nothing found
142
+ if (
143
+ fallbackChain.length === 0 &&
144
+ this.availableLocales.includes(this.fallbackLocale)
145
+ ) {
146
+ fallbackChain = [this.fallbackLocale]
113
147
  }
114
- })
115
-
116
- return filtered
148
+
149
+ if (fallbackChain.length > 0) {
150
+ enrichmentMap[locale] = fallbackChain
151
+ logger.orchestrator(`Fallback chain for ${locale}:`, fallbackChain)
152
+ }
153
+ }
154
+
155
+ return enrichmentMap
117
156
  }
118
157
  }
119
158
 
120
- const findBestDateFnsLocale = (
121
- requestedLocale: string,
122
- availableLocales: string[]
123
- ): string | null => {
124
- // Try exact match first
125
- if (availableLocales.includes(requestedLocale)) {
126
- return requestedLocale
127
- }
128
-
129
- // Try short locale
130
- const shortLocale = getShortLocale(requestedLocale)
131
- if (availableLocales.includes(shortLocale)) {
132
- return shortLocale
133
- }
134
-
135
- // Fallback to "eu"
136
- if (availableLocales.includes("eu")) {
137
- return "eu"
159
+ // ============================================================================
160
+ // MESSAGE LOADING AND PROCESSING
161
+ // ============================================================================
162
+
163
+ class MessageProcessor {
164
+ private priorities: SectionPriorities
165
+
166
+ constructor(priorities: SectionPriorities) {
167
+ this.priorities = priorities
138
168
  }
139
-
140
- return null
141
- }
142
169
 
143
- // Locale enrichment logic
144
- const buildFallbackChain = (
145
- requestedLocale: string,
146
- availableLocales: string[]
147
- ): string[] => {
148
- const fallbackChain: string[] = []
149
- const requestedLang = getShortLocale(requestedLocale)
150
-
151
- // 1. Exact match
152
- if (availableLocales.includes(requestedLocale)) {
153
- fallbackChain.push(requestedLocale)
170
+ private extractSectionFromPath(filePath: string): string {
171
+ const pathParts = filePath.split("/")
172
+ const i18nIndex = pathParts.findIndex((part) => part === "i18n")
173
+ const folderPath = pathParts.slice(i18nIndex + 1, -1).join("/")
174
+ return folderPath || "."
154
175
  }
155
176
 
156
- // 2. Same language, different region
157
- const sameLanguageDifferentRegion = availableLocales.filter(
158
- (locale) =>
159
- locale !== requestedLocale &&
160
- getShortLocale(locale) === requestedLang &&
161
- locale.includes("-")
162
- )
163
- fallbackChain.push(...sameLanguageDifferentRegion)
177
+ private async loadI18nFiles(
178
+ files: LocaleFileImports,
179
+ ): Promise<Record<string, LocaleSection[]>> {
180
+ const sectionsByLocale: Record<string, LocaleSection[]> = {}
181
+
182
+ for (const [filePath, importFn] of Object.entries(files)) {
183
+ try {
184
+ const module = await importFn()
185
+ const content = module.default || module
186
+ const locale = extractLocaleFromPath(filePath)
187
+ const section = this.extractSectionFromPath(filePath)
188
+ const priority = this.priorities[section]
189
+
190
+ if (priority === undefined) {
191
+ logger.warning(
192
+ `Section "${section}" for locale "${locale}" has no priority - skipping`,
193
+ )
194
+ continue
195
+ }
196
+
197
+ if (!sectionsByLocale[locale]) {
198
+ sectionsByLocale[locale] = []
199
+ }
164
200
 
165
- // 3. Short locale
166
- if (availableLocales.includes(requestedLang) && !fallbackChain.includes(requestedLang)) {
167
- fallbackChain.push(requestedLang)
201
+ sectionsByLocale[locale].push({ section, content, priority })
202
+ } catch (error) {
203
+ logger.error(`Failed to load i18n file ${filePath}:`, error)
204
+ }
205
+ }
206
+
207
+ return sectionsByLocale
168
208
  }
169
209
 
170
- return fallbackChain
171
- }
210
+ private mergeSectionsByPriority(
211
+ sectionsByLocale: Record<string, LocaleSection[]>,
212
+ ): Record<string, any> {
213
+ const mergedByLocale: Record<string, any> = {}
172
214
 
173
- const createLocaleEnrichmentMap = (
174
- requestedLocales: string[],
175
- availableLocales: string[],
176
- fallbackLocale: string
177
- ): LocaleEnrichmentMap => {
178
- const enrichmentMap: LocaleEnrichmentMap = {}
179
-
180
- requestedLocales.forEach((requestedLocale) => {
181
- let fallbackChain = buildFallbackChain(requestedLocale, availableLocales)
182
-
183
- // Add fallback locale if nothing else was found
184
- if (fallbackChain.length === 0 && availableLocales.includes(fallbackLocale)) {
185
- fallbackChain = [fallbackLocale]
215
+ for (const [locale, sections] of Object.entries(sectionsByLocale)) {
216
+ // Sort by priority (highest number = lowest priority, so merge first)
217
+ const sortedSections = sections.sort((a, b) => b.priority - a.priority)
218
+
219
+ // Merge from lowest to highest priority
220
+ mergedByLocale[locale] = sortedSections.reduce((merged, { content }) => {
221
+ return mergeDeepRight(merged, content || {})
222
+ }, {})
186
223
  }
187
224
 
188
- if (fallbackChain.length > 0) {
189
- enrichmentMap[requestedLocale] = fallbackChain
190
- logger.orchestrator(`Enrichment chain for ${requestedLocale}:`, fallbackChain)
225
+ return mergedByLocale
226
+ }
227
+
228
+ private enrichWithFallbacks(
229
+ enrichmentMap: Record<string, string[]>,
230
+ mergedByLocale: Record<string, any>,
231
+ ): Messages {
232
+ const enrichedMessages: Messages = {}
233
+
234
+ for (const [requestedLocale, fallbackChain] of Object.entries(
235
+ enrichmentMap,
236
+ )) {
237
+ let localeMessages = {}
238
+
239
+ // Merge in reverse order (lowest priority first)
240
+ const reversedChain = [...fallbackChain].reverse()
241
+ for (const sourceLocale of reversedChain) {
242
+ if (mergedByLocale[sourceLocale]) {
243
+ localeMessages = mergeDeepRight(
244
+ localeMessages,
245
+ mergedByLocale[sourceLocale],
246
+ )
247
+ logger.orchestrator(`Merged ${sourceLocale} into ${requestedLocale}`)
248
+ }
249
+ }
250
+
251
+ enrichedMessages[requestedLocale] = localeMessages
191
252
  }
192
- })
193
253
 
194
- return enrichmentMap
254
+ return enrichedMessages
255
+ }
256
+
257
+ async processMessages(
258
+ files: LocaleFileImports,
259
+ enrichmentMap: Record<string, string[]>,
260
+ ): Promise<Messages> {
261
+ const sectionsByLocale = await this.loadI18nFiles(files)
262
+ const mergedByLocale = this.mergeSectionsByPriority(sectionsByLocale)
263
+ return this.enrichWithFallbacks(enrichmentMap, mergedByLocale)
264
+ }
195
265
  }
196
266
 
197
- // Message loading functions
198
- const extractSectionFromFilePath = (filePath: string): string => {
199
- const pathParts = filePath.split("/")
200
- const i18nIndex = pathParts.findIndex((part) => part === "i18n")
201
- const folderPath = pathParts.slice(i18nIndex + 1, -1).join("/")
202
- return folderPath || "."
267
+ // ============================================================================
268
+ // VUETIFY LOCALE LOADER
269
+ // ============================================================================
270
+
271
+ class VuetifyLocaleLoader {
272
+ async loadVuetifyLocales(
273
+ files: LocaleFileImports,
274
+ requestedLocales: string[],
275
+ messages: Messages,
276
+ ): Promise<void> {
277
+ for (const [filePath, importFn] of Object.entries(files)) {
278
+ try {
279
+ const module = await importFn()
280
+ const vuetifyLocale = module.default || module
281
+ const shortLocale = extractLocaleFromPath(filePath)
282
+
283
+ // Find matching long locales
284
+ const matchingLocales = requestedLocales.filter(
285
+ (locale) => getLanguageCode(locale) === shortLocale,
286
+ )
287
+
288
+ for (const locale of matchingLocales) {
289
+ if (messages[locale]) {
290
+ messages[locale].$vuetify = vuetifyLocale
291
+ }
292
+ }
293
+ } catch (error) {
294
+ logger.error(`Failed to load Vuetify locale ${filePath}:`, error)
295
+ }
296
+ }
297
+ }
203
298
  }
204
299
 
205
- const loadAndCategorizeI18nFiles = async (
206
- files: LocaleFileImports,
207
- priorities: SectionPriorities
208
- ): Promise<Record<string, LocaleSection[]>> => {
209
- const sectionsByLocale: Record<string, LocaleSection[]> = {}
210
-
211
- for (const [filePath, importFn] of Object.entries(files)) {
212
- const module = await importFn()
213
- const content = module.default || module
214
- const fileLocale = extractLocaleFromFilePath(filePath)
215
- const section = extractSectionFromFilePath(filePath)
216
- const priority = priorities[section]
217
-
218
- if (priority === undefined) {
219
- logger.warning(
220
- `Section "${section}" for locale "${fileLocale}" has no priority defined and will be skipped`
221
- )
222
- continue
300
+ // ============================================================================
301
+ // DATE-FNS LOCALE LOADER
302
+ // ============================================================================
303
+
304
+ class DateFnsLocaleLoader {
305
+ private findBestDateFnsLocale(
306
+ requestedLocale: string,
307
+ availableLocales: string[],
308
+ ): string | null {
309
+ // Try exact match
310
+ if (availableLocales.includes(requestedLocale)) {
311
+ return requestedLocale
223
312
  }
224
313
 
225
- if (!sectionsByLocale[fileLocale]) {
226
- sectionsByLocale[fileLocale] = []
314
+ // Try language code
315
+ const languageCode = getLanguageCode(requestedLocale)
316
+ if (availableLocales.includes(languageCode)) {
317
+ return languageCode
227
318
  }
228
319
 
229
- sectionsByLocale[fileLocale].push({ section, content, priority })
230
- }
320
+ // Fallback to en-GB (English with European formatting)
321
+ if (availableLocales.includes("en-GB")) {
322
+ return "en-GB"
323
+ }
231
324
 
232
- return sectionsByLocale
233
- }
325
+ // Final fallback to en
326
+ if (availableLocales.includes("en")) {
327
+ return "en"
328
+ }
234
329
 
235
- const mergeSectionsByPriority = (
236
- sectionsByLocale: Record<string, LocaleSection[]>
237
- ): Record<string, any> => {
238
- const mergedByLocale: Record<string, any> = {}
330
+ return null
331
+ }
239
332
 
240
- Object.entries(sectionsByLocale).forEach(([locale, sections]) => {
241
- // Sort by priority descending (highest number = lowest priority)
242
- const sortedSections = sections.sort((a, b) => b.priority - a.priority)
333
+ async loadDateFnsLocales(requestedLocales: string[]): Promise<DateLocales> {
334
+ const availableDateFnsLocales = filterValidDateFnsLocales(
335
+ getAvailableLocales(dateFnsLocaleFiles),
336
+ )
243
337
 
244
- // Deep merge from lowest to highest priority
245
- mergedByLocale[locale] = sortedSections.reduce((merged, { content }) => {
246
- return mergeDeepRight(merged, content || {})
247
- }, {})
248
- })
338
+ // Create mapping of requested locale -> best available locale
339
+ const localeMapping: Record<string, string> = {}
340
+ for (const requestedLocale of requestedLocales) {
341
+ const bestMatch = this.findBestDateFnsLocale(
342
+ requestedLocale,
343
+ availableDateFnsLocales,
344
+ )
345
+ if (bestMatch) {
346
+ localeMapping[requestedLocale] = bestMatch
347
+ }
348
+ }
249
349
 
250
- return mergedByLocale
251
- }
350
+ // Load only the needed locale files
351
+ const neededLocales = [...new Set(Object.values(localeMapping))]
352
+ const dateLocales: DateLocales = {}
252
353
 
253
- const enrichMessagesWithFallbacks = (
254
- enrichmentMap: LocaleEnrichmentMap,
255
- mergedByLocale: Record<string, any>
256
- ): Messages => {
257
- const enrichedMessages: Messages = {}
258
-
259
- Object.entries(enrichmentMap).forEach(([requestedLocale, fallbackChain]) => {
260
- let localeMessages = {}
261
-
262
- // Merge in reverse order (lowest priority first)
263
- const reversedChain = [...fallbackChain].reverse()
264
- reversedChain.forEach((sourceLocale) => {
265
- if (mergedByLocale[sourceLocale]) {
266
- localeMessages = mergeDeepRight(localeMessages, mergedByLocale[sourceLocale])
267
- logger.orchestrator(`Merged ${sourceLocale} into ${requestedLocale}`)
268
- }
269
- })
354
+ for (const neededLocale of neededLocales) {
355
+ const filePath = Object.keys(dateFnsLocaleFiles).find(
356
+ (path) => extractLocaleFromPath(path) === neededLocale,
357
+ )
270
358
 
271
- enrichedMessages[requestedLocale] = localeMessages
272
- })
359
+ if (filePath) {
360
+ try {
361
+ const module = await dateFnsLocaleFiles[filePath]()
362
+ const dateFnsLocale = module.default || module
363
+
364
+ // Map this loaded locale to all requesting locales
365
+ for (const [requestedLocale, targetLocale] of Object.entries(
366
+ localeMapping,
367
+ )) {
368
+ if (targetLocale === neededLocale) {
369
+ dateLocales[requestedLocale] = dateFnsLocale
370
+ }
371
+ }
372
+ } catch (error) {
373
+ logger.error(`Failed to load date-fns locale ${filePath}:`, error)
374
+ }
375
+ }
376
+ }
273
377
 
274
- return enrichedMessages
378
+ return dateLocales
379
+ }
275
380
  }
276
381
 
277
- const loadVuetifyLocales = async (
278
- files: LocaleFileImports,
279
- locales: string[],
280
- messages: Messages
281
- ): Promise<void> => {
282
- for (const [filePath, importFn] of Object.entries(files)) {
283
- const module = await importFn()
284
- const vuetifyLocale = module.default || module
285
- const shortLocale = extractLocaleFromFilePath(filePath)
286
-
287
- // Find corresponding long locale(s)
288
- const matchingLocales = locales.filter(
289
- (locale) => getShortLocale(locale) === shortLocale
290
- )
382
+ // ============================================================================
383
+ // FILE FILTERING
384
+ // ============================================================================
291
385
 
292
- matchingLocales.forEach((locale) => {
293
- if (messages[locale]) {
294
- messages[locale].$vuetify = vuetifyLocale
386
+ class FileFilter {
387
+ static filterByLocales(targetLocales: string[]) {
388
+ return (files: LocaleFileImports): LocaleFileImports => {
389
+ const filtered: LocaleFileImports = {}
390
+
391
+ for (const [filePath, importFn] of Object.entries(files)) {
392
+ const fileLocale = extractLocaleFromPath(filePath)
393
+ if (targetLocales.includes(fileLocale)) {
394
+ filtered[filePath] = importFn
395
+ }
295
396
  }
296
- })
397
+
398
+ return filtered
399
+ }
297
400
  }
298
401
  }
299
402
 
300
- const loadMessages = async (
301
- locales: string[],
302
- priorities: SectionPriorities
303
- ): Promise<Messages> => {
304
- const availableLocales = getAvailableLocales(i18nFiles)
305
- const enrichmentMap = createLocaleEnrichmentMap(locales, availableLocales, "en-EU")
306
-
307
- // Get all unique locales to load
308
- const allLocalesToLoad = new Set<string>()
309
- Object.values(enrichmentMap).forEach((fallbackChain) => {
310
- fallbackChain.forEach((locale) => allLocalesToLoad.add(locale))
311
- })
312
-
313
- // Filter and load i18n files
314
- const filteredI18nFiles = createLocaleFilter(Array.from(allLocalesToLoad))(i18nFiles)
315
- const sectionsByLocale = await loadAndCategorizeI18nFiles(filteredI18nFiles, priorities)
316
- const mergedByLocale = mergeSectionsByPriority(sectionsByLocale)
317
- const messages = enrichMessagesWithFallbacks(enrichmentMap, mergedByLocale)
318
-
319
- // Load Vuetify locales
320
- const shortLocales = locales.map(getShortLocale)
321
- const filteredVuetifyFiles = createLocaleFilter(shortLocales)(vuetifyLocaleFiles)
322
- await loadVuetifyLocales(filteredVuetifyFiles, locales, messages)
323
-
324
- return messages
325
- }
403
+ // ============================================================================
404
+ // MAIN ORCHESTRATOR
405
+ // ============================================================================
326
406
 
327
- // Date-fns locale loading
328
- const loadDateFnsLocales = async (locales: string[]): Promise<DateLocales> => {
329
- const filteredDateFnsFiles = createDateFnsLocaleFilter(locales)(dateFnsLocaleFiles)
330
- const dateLocales: DateLocales = {}
331
- const availableDateFnsLocales = getAvailableDateFnsLocales(dateFnsLocaleFiles)
332
-
333
- for (const [filePath, importFn] of Object.entries(filteredDateFnsFiles)) {
334
- const module = await importFn()
335
- const dateFnsLocale = module.default || module
336
- const actualLocale = extractLocaleFromFilePath(filePath)
337
-
338
- // Map to all requesting locales that should use this date-fns locale
339
- locales.forEach((requestedLocale) => {
340
- const shouldUseThisLocale =
341
- requestedLocale === actualLocale ||
342
- (getShortLocale(requestedLocale) === actualLocale &&
343
- !availableDateFnsLocales.includes(requestedLocale)) ||
344
- (actualLocale === "eu" &&
345
- !availableDateFnsLocales.includes(requestedLocale) &&
346
- !availableDateFnsLocales.includes(getShortLocale(requestedLocale)))
347
-
348
- if (shouldUseThisLocale) {
349
- dateLocales[requestedLocale] = dateFnsLocale
350
- }
407
+ class I18nOrchestrator {
408
+ private fallbackLocale = "en-EU"
409
+
410
+ private async determineInitialLocale(
411
+ availableLocales: string[],
412
+ ): Promise<string> {
413
+ const browserLocale = navigator.language
414
+ let initialLocale = this.fallbackLocale
415
+
416
+ // Use the same fallback logic as message loading
417
+ const enrichment = new LocaleEnrichment(
418
+ [browserLocale],
419
+ availableLocales,
420
+ this.fallbackLocale,
421
+ )
422
+ const enrichmentMap = enrichment.createEnrichmentMap()
423
+
424
+ if (
425
+ enrichmentMap[browserLocale] &&
426
+ enrichmentMap[browserLocale].length > 0
427
+ ) {
428
+ initialLocale = enrichmentMap[browserLocale][0] // Take the first (best) match
429
+ logger.orchestrator(`Initial locale: ${browserLocale} → ${initialLocale}`)
430
+ } else {
431
+ logger.orchestrator(
432
+ `No match found for ${browserLocale}, using fallback: ${initialLocale}`,
433
+ )
434
+ }
435
+
436
+ await $store.dispatch({
437
+ type: "ui/update",
438
+ payload: { locale: initialLocale },
351
439
  })
352
- }
353
440
 
354
- return dateLocales
355
- }
441
+ return initialLocale
442
+ }
356
443
 
357
- // Main initialization function
358
- const initI18n = async (): Promise<I18nInstance> => {
359
- if (!i18nInstance) {
360
- const fallbackLocale = "en-EU"
444
+ async initialize(): Promise<I18nInstance> {
361
445
  const { locales, priorities } = configService.getI18nConfig()
362
446
  const availableLocales = getAvailableLocales(i18nFiles)
363
-
447
+
364
448
  logger.orchestrator("Configured locales:", locales)
365
449
  logger.orchestrator("Available locale files:", availableLocales)
366
450
 
367
- // Load messages and date locales concurrently
451
+ // Create enrichment map for fallback chains
452
+ const enrichment = new LocaleEnrichment(
453
+ locales,
454
+ availableLocales,
455
+ this.fallbackLocale,
456
+ )
457
+ const enrichmentMap = enrichment.createEnrichmentMap()
458
+
459
+ // Get all locales we need to load (including fallbacks)
460
+ const allLocalesToLoad = new Set<string>()
461
+ Object.values(enrichmentMap).forEach((fallbackChain) => {
462
+ fallbackChain.forEach((locale) => allLocalesToLoad.add(locale))
463
+ })
464
+
465
+ // Load messages, Vuetify locales, and date-fns locales concurrently
368
466
  const [loadedMessages, loadedDateLocales] = await Promise.all([
369
- loadMessages(locales, priorities),
370
- loadDateFnsLocales(locales)
467
+ this.loadMessages(
468
+ Array.from(allLocalesToLoad),
469
+ enrichmentMap,
470
+ priorities,
471
+ locales,
472
+ ),
473
+ new DateFnsLocaleLoader().loadDateFnsLocales(locales),
371
474
  ])
372
475
 
373
476
  messages = loadedMessages
374
477
  dateLocale = loadedDateLocales
375
-
478
+
376
479
  logger.orchestrator("Loaded messages:", Object.keys(messages))
377
480
  logger.orchestrator("Loaded date locales:", Object.keys(dateLocale))
378
481
 
379
- // Create formats
380
- const dateTimeFormats = createFormatsForLocales(locales, defaultDateTimeFormats)
381
- const numberFormatsForLocales = createFormatsForLocales(locales, defaultNumberFormats)
382
-
383
- // Initialize i18n instance
482
+ // Create i18n instance
483
+ const initialLocale = await this.determineInitialLocale(
484
+ Object.keys(messages),
485
+ )
384
486
  i18nInstance = createI18n({
385
487
  legacy: false,
386
488
  globalInjection: true,
387
- locale: navigator.language,
388
- fallbackLocale: fallbackLocale,
489
+ locale: initialLocale,
490
+ fallbackLocale: this.fallbackLocale,
389
491
  messages,
390
- datetimeFormats: dateTimeFormats,
391
- numberFormats: numberFormatsForLocales,
492
+ datetimeFormats: createFormatsForAllLocales(
493
+ locales,
494
+ defaultDateTimeFormats,
495
+ ),
496
+ numberFormats: createFormatsForAllLocales(locales, defaultNumberFormats),
392
497
  })
393
498
 
394
499
  instanceAdapter = {
395
- adapter: createVueI18nAdapter({
396
- i18n: i18nInstance,
397
- useI18n,
398
- }),
500
+ adapter: createVueI18nAdapter({ i18n: i18nInstance, useI18n }),
399
501
  }
502
+
503
+ return { i18nInstance, instanceAdapter, dateLocale }
400
504
  }
401
505
 
506
+ private async loadMessages(
507
+ localesToLoad: string[],
508
+ enrichmentMap: Record<string, string[]>,
509
+ priorities: SectionPriorities,
510
+ requestedLocales: string[],
511
+ ): Promise<Messages> {
512
+ // Filter i18n files and process messages
513
+ const filteredI18nFiles =
514
+ FileFilter.filterByLocales(localesToLoad)(i18nFiles)
515
+ const messageProcessor = new MessageProcessor(priorities)
516
+ const messages = await messageProcessor.processMessages(
517
+ filteredI18nFiles,
518
+ enrichmentMap,
519
+ )
520
+
521
+ // Load Vuetify locales
522
+ const shortLocales = requestedLocales.map(getLanguageCode)
523
+ const filteredVuetifyFiles =
524
+ FileFilter.filterByLocales(shortLocales)(vuetifyLocaleFiles)
525
+ const vuetifyLoader = new VuetifyLocaleLoader()
526
+ await vuetifyLoader.loadVuetifyLocales(
527
+ filteredVuetifyFiles,
528
+ requestedLocales,
529
+ messages,
530
+ )
531
+
532
+ return messages
533
+ }
534
+ }
535
+
536
+ // ============================================================================
537
+ // PUBLIC API
538
+ // ============================================================================
539
+
540
+ const initI18n = async (): Promise<I18nInstance> => {
541
+ if (!i18nInstance) {
542
+ const orchestrator = new I18nOrchestrator()
543
+ return await orchestrator.initialize()
544
+ }
402
545
  return { i18nInstance, instanceAdapter, dateLocale }
403
546
  }
404
547
 
405
548
  export default initI18n
406
- export { i18nInstance, instanceAdapter, dateLocale, initI18n }
549
+ export { i18nInstance, instanceAdapter, dateLocale, initI18n }