@raclettejs/core 0.1.3 โ 0.1.5
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.
- package/CHANGELOG.md +14 -0
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/services/frontend/src/core/lib/data/fetchDataHandler.ts +2 -2
- package/services/frontend/src/core/lib/projectDataHelper.ts +1 -5
- package/services/frontend/src/orchestrator/assets/iconMappings/flags.json +2 -2
- package/services/frontend/src/orchestrator/i18n/de-DE.json +2 -2
- package/services/frontend/src/orchestrator/i18n/en-EU.json +2 -2
- package/services/frontend/src/orchestrator/i18n/{sk-SK.json โ sk.json} +2 -2
- package/services/frontend/src/orchestrator/setup/i18n.ts +413 -270
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.5] - 11.09.2025
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- "refresh all queries" functionallity in user menu now works as expected
|
|
15
|
+
- issue was introduced during query api update in v0.1.3
|
|
16
|
+
|
|
17
|
+
## [0.1.4] - 10.09.2025
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- aliases "@app", "@raclettejs/core/frontend" & "@raclettejs/core/backend" are now resolved properly in the IDE
|
|
22
|
+
- fixed unspecific locale fallback
|
|
23
|
+
|
|
10
24
|
## [0.1.3] - 10.09.2025
|
|
11
25
|
|
|
12
26
|
### Fixed
|
package/dist/cli.js
CHANGED
|
@@ -636,7 +636,7 @@ export default {
|
|
|
636
636
|
`)),h.debug(`\u{1F512}\u2705 Created ${te.basename(r)}`)}}h.success("Lock files ready for volume mounting")},Ck=async(e,t,r,n)=>{let o=te.join(t,"yarn.lock");h.debug(`\u{1F512} Setting up yarn.lock for ${r} build context`);let i;if((r==="frontend"||r==="backend")&&(i=gr(r,n)),i&&await J.pathExists(i))h.debug(`\u{1F512} Copying configured lock file to build context: ${i}`),await J.copy(i,o);else{let s=te.join(e,"yarn.lock");await J.pathExists(s)?(h.debug(`\u{1F512} Using core yarn.lock for ${r} build context`),await J.copy(s,o)):(h.debug(`\u{1F512} Creating minimal yarn.lock for ${r} build context`),await J.writeFile(o,`# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
637
637
|
# yarn lockfile v1
|
|
638
638
|
|
|
639
|
-
`))}};import jd from"fs-extra";import Fd from"path";var jn=async(e,t)=>{let r=Fd.join(t,"tsconfig.json"),n={compilerOptions:{target:"ES2020",module:"ESNext",moduleResolution:"node",esModuleInterop:!0,resolveJsonModule:!0,outDir:"dist",declaration:!0,emitDeclarationOnly:!0,strict:!0,paths:{"@app":["../frontend/src/app"],"@app/*":["../frontend/src/app/*"],"@shared/app/*":["../shared/*"],"@shared/*":["./virtual/backend/src/shared/*"],"@raclettejs/core/backend":["../node_modules/@raclettejs/core/services/backend/src"],"@raclettejs/core/backend/*":["../node_modules/@raclettejs/core/services/backend/src/*"],"@raclettejs/workbench/app/*":["../node_modules/@raclettejs/workbench/frontend/src/app/*"],"@raclettejs/workbench/*":["../node_modules/@raclettejs/workbench/*"],"@raclettejs/core/frontend":["../node_modules/@raclettejs/core/services/frontend/src/core"],"@raclettejs/core/frontend/*":["../node_modules/@raclettejs/core/services/frontend/src/core/*"],"@raclettejs/core/orchestrator":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator"],"@raclettejs/core/orchestrator/*":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/*"],"@raclettejs/core/orchestrator/components":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/components"],"@raclettejs/core/orchestrator/components/*":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/components/*"],"@raclettejs/core/orchestrator/composables":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/composables"],"@raclettejs/core/orchestrator/composables/*":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/composables/*"],"@raclettejs/core":["../node_modules/@raclettejs/core"]},typeRoots:["./node_modules/@types","./node_modules/@raclettejs/core/types"]},include:["./src/**/*","types/**/*","../**/*","../node_modules/@raclettejs/core/*"],exclude:["../node_modules","../dist","./**","../node_modules/@raclettejs/core/node_modules"]};e.typescript?.compilerOptions&&Object.assign(n.compilerOptions,e.typescript.compilerOptions),e?.frontend?.framework==="vue"&&(Ak(t),n.include.push("shims.vue.d.ts")),await jd.writeFile(r,JSON.stringify(n,null,2)),h.debug(`Generated tsconfig.json in ${t}`)},Ak=async e=>{await jd.writeFile(Fd.join(e,"shims.vue.d.ts"),`declare module "*.vue" {
|
|
639
|
+
`))}};import jd from"fs-extra";import Fd from"path";var jn=async(e,t)=>{let r=Fd.join(t,"tsconfig.json"),n={compilerOptions:{target:"ES2020",module:"ESNext",moduleResolution:"node",esModuleInterop:!0,resolveJsonModule:!0,outDir:"dist",declaration:!0,emitDeclarationOnly:!0,strict:!0,paths:{"@app":["../services/frontend/src/app"],"@app/*":["../services/frontend/src/app/*"],"@shared/app/*":["../shared/*"],"@shared/*":["./virtual/backend/src/shared/*"],"@raclettejs/core/backend":["../node_modules/@raclettejs/core/services/backend/src"],"@raclettejs/core/backend/*":["../node_modules/@raclettejs/core/services/backend/src/*"],"@raclettejs/workbench/app/*":["../node_modules/@raclettejs/workbench/frontend/src/app/*"],"@raclettejs/workbench/*":["../node_modules/@raclettejs/workbench/*"],"@raclettejs/core/frontend":["../node_modules/@raclettejs/core/services/frontend/src/core"],"@raclettejs/core/frontend/*":["../node_modules/@raclettejs/core/services/frontend/src/core/*"],"@raclettejs/core/orchestrator":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator"],"@raclettejs/core/orchestrator/*":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/*"],"@raclettejs/core/orchestrator/components":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/components"],"@raclettejs/core/orchestrator/components/*":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/components/*"],"@raclettejs/core/orchestrator/composables":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/composables"],"@raclettejs/core/orchestrator/composables/*":["../node_modules/@raclettejs/core/services/frontend/src/orchestrator/composables/*"],"@raclettejs/core":["../node_modules/@raclettejs/core"]},typeRoots:["./node_modules/@types","./node_modules/@raclettejs/core/types"]},include:["./src/**/*","types/**/*","../**/*","../node_modules/@raclettejs/core/*"],exclude:["../node_modules","../dist","./**","../node_modules/@raclettejs/core/node_modules"]};e.typescript?.compilerOptions&&Object.assign(n.compilerOptions,e.typescript.compilerOptions),e?.frontend?.framework==="vue"&&(Ak(t),n.include.push("shims.vue.d.ts")),await jd.writeFile(r,JSON.stringify(n,null,2)),h.debug(`Generated tsconfig.json in ${t}`)},Ak=async e=>{await jd.writeFile(Fd.join(e,"shims.vue.d.ts"),`declare module "*.vue" {
|
|
640
640
|
import { DefineComponent } from "vue"
|
|
641
641
|
const component: DefineComponent<{}, {}, any>
|
|
642
642
|
export default component
|
package/package.json
CHANGED
|
@@ -108,7 +108,7 @@ const itemDispatcher = async (
|
|
|
108
108
|
// Cache Management Functions
|
|
109
109
|
const cacheOperations = {
|
|
110
110
|
async setupCache(actionId, target, params, options) {
|
|
111
|
-
if (options
|
|
111
|
+
if (options?.useStore === false)
|
|
112
112
|
return { cachedQuery: null, actionIdentifier: { actionId } }
|
|
113
113
|
const cachedQuery = getCacheQueryAndHash(target, params)
|
|
114
114
|
const actionIdentifier = {
|
|
@@ -175,7 +175,7 @@ const finishRemoteQuery = (actionIdentifier, result, params, options) => {
|
|
|
175
175
|
resultBody.items.forEach((item) => resultObj.result.push(item))
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
if (options
|
|
178
|
+
if (options?.useStore === false) {
|
|
179
179
|
$store.dispatch({
|
|
180
180
|
type: "queries/update/loading",
|
|
181
181
|
payload: {
|
|
@@ -12,7 +12,6 @@ const setupSpace = async (
|
|
|
12
12
|
const userLanguage = useLocalStorage<string>("user-language", null)
|
|
13
13
|
const userTheme = useLocalStorage<string>("user-theme", null)
|
|
14
14
|
let sessionTheme = userTheme.value
|
|
15
|
-
let sessionLocale = userLanguage.value
|
|
16
15
|
// set compositions
|
|
17
16
|
if (Array.isArray(compositions) && compositions.length > 0) {
|
|
18
17
|
await $store.dispatch({
|
|
@@ -36,9 +35,6 @@ const setupSpace = async (
|
|
|
36
35
|
})
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
if (!userLanguage.value) {
|
|
40
|
-
sessionLocale = navigator.language || navigator.userLanguage
|
|
41
|
-
}
|
|
42
38
|
if (!userTheme.value) {
|
|
43
39
|
let theme = "light"
|
|
44
40
|
if (
|
|
@@ -52,7 +48,7 @@ const setupSpace = async (
|
|
|
52
48
|
}
|
|
53
49
|
await $store.dispatch({
|
|
54
50
|
type: "ui/update",
|
|
55
|
-
payload: { theme: sessionTheme
|
|
51
|
+
payload: { theme: sessionTheme},
|
|
56
52
|
})
|
|
57
53
|
}
|
|
58
54
|
|
|
@@ -160,8 +160,8 @@
|
|
|
160
160
|
"vi-VN": "Vietnamฤina (Vietnam)",
|
|
161
161
|
"yi-IL": "Jidiลก (Izrael)",
|
|
162
162
|
"zu-ZA": "Zulu (Juลพnรก Afrika)",
|
|
163
|
-
"sk
|
|
163
|
+
"sk": "Slovenฤina (Slovensko)",
|
|
164
164
|
"sl-SL": "Slovinฤina (Slovinsko)"
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
-
}
|
|
167
|
+
}
|
|
@@ -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
|
-
//
|
|
52
|
-
|
|
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
|
|
58
|
-
return
|
|
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(
|
|
65
|
+
return Object.keys(files).map(extractLocaleFromPath)
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
const
|
|
66
|
-
return
|
|
67
|
-
.
|
|
68
|
-
|
|
68
|
+
const filterValidDateFnsLocales = (locales: string[]): string[] => {
|
|
69
|
+
return locales.filter(
|
|
70
|
+
(locale) => !locale.includes("cdn") && locale !== "types",
|
|
71
|
+
)
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
const
|
|
74
|
+
const createFormatsForAllLocales = (
|
|
72
75
|
locales: string[],
|
|
73
|
-
defaultFormats: any
|
|
76
|
+
defaultFormats: any,
|
|
74
77
|
): Record<string, any> => {
|
|
75
|
-
return locales.reduce(
|
|
76
|
-
formats
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
210
|
+
private mergeSectionsByPriority(
|
|
211
|
+
sectionsByLocale: Record<string, LocaleSection[]>,
|
|
212
|
+
): Record<string, any> {
|
|
213
|
+
const mergedByLocale: Record<string, any> = {}
|
|
172
214
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
226
|
-
|
|
314
|
+
// Try language code
|
|
315
|
+
const languageCode = getLanguageCode(requestedLocale)
|
|
316
|
+
if (availableLocales.includes(languageCode)) {
|
|
317
|
+
return languageCode
|
|
227
318
|
}
|
|
228
319
|
|
|
229
|
-
|
|
230
|
-
|
|
320
|
+
// Fallback to en-GB (English with European formatting)
|
|
321
|
+
if (availableLocales.includes("en-GB")) {
|
|
322
|
+
return "en-GB"
|
|
323
|
+
}
|
|
231
324
|
|
|
232
|
-
|
|
233
|
-
|
|
325
|
+
// Final fallback to en
|
|
326
|
+
if (availableLocales.includes("en")) {
|
|
327
|
+
return "en"
|
|
328
|
+
}
|
|
234
329
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
): Record<string, any> => {
|
|
238
|
-
const mergedByLocale: Record<string, any> = {}
|
|
330
|
+
return null
|
|
331
|
+
}
|
|
239
332
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
333
|
+
async loadDateFnsLocales(requestedLocales: string[]): Promise<DateLocales> {
|
|
334
|
+
const availableDateFnsLocales = filterValidDateFnsLocales(
|
|
335
|
+
getAvailableLocales(dateFnsLocaleFiles),
|
|
336
|
+
)
|
|
243
337
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
251
|
-
|
|
350
|
+
// Load only the needed locale files
|
|
351
|
+
const neededLocales = [...new Set(Object.values(localeMapping))]
|
|
352
|
+
const dateLocales: DateLocales = {}
|
|
252
353
|
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
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
|
-
|
|
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
|
-
|
|
378
|
+
return dateLocales
|
|
379
|
+
}
|
|
275
380
|
}
|
|
276
381
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
355
|
-
}
|
|
441
|
+
return initialLocale
|
|
442
|
+
}
|
|
356
443
|
|
|
357
|
-
|
|
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
|
-
//
|
|
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(
|
|
370
|
-
|
|
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
|
|
380
|
-
const
|
|
381
|
-
|
|
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:
|
|
388
|
-
fallbackLocale: fallbackLocale,
|
|
489
|
+
locale: initialLocale,
|
|
490
|
+
fallbackLocale: this.fallbackLocale,
|
|
389
491
|
messages,
|
|
390
|
-
datetimeFormats:
|
|
391
|
-
|
|
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 }
|