@gravito/cosmos 1.0.0-alpha.2 → 1.0.0-alpha.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.
@@ -14,17 +14,28 @@ export interface I18nService {
14
14
  getLocale(): string
15
15
  t(key: string, replacements?: Record<string, string | number>): string
16
16
  has(key: string): boolean
17
+ // Create a request-scoped instance
18
+ clone(locale?: string): I18nService
17
19
  }
18
20
 
19
- export class I18nManager implements I18nService {
21
+ /**
22
+ * Request-scoped I18n Instance
23
+ * Holds the state (locale) for a single request, but shares the heavy resources (translations)
24
+ */
25
+ export class I18nInstance implements I18nService {
20
26
  private _locale: string
21
- private translations: Record<string, Record<string, string>> = {}
22
27
 
23
- constructor(private config: I18nConfig) {
24
- this._locale = config.defaultLocale
25
- if (config.translations) {
26
- this.translations = config.translations
27
- }
28
+ /**
29
+ * Create a new I18nInstance.
30
+ *
31
+ * @param manager - The I18nManager instance.
32
+ * @param initialLocale - The initial locale for this instance.
33
+ */
34
+ constructor(
35
+ private manager: I18nManager,
36
+ initialLocale: string
37
+ ) {
38
+ this._locale = initialLocale
28
39
  }
29
40
 
30
41
  get locale(): string {
@@ -35,18 +46,154 @@ export class I18nManager implements I18nService {
35
46
  this.setLocale(value)
36
47
  }
37
48
 
49
+ /**
50
+ * Set the current locale.
51
+ *
52
+ * @param locale - The locale to set.
53
+ */
38
54
  setLocale(locale: string) {
39
- if (this.config.supportedLocales.includes(locale)) {
55
+ if (this.manager.getConfig().supportedLocales.includes(locale)) {
40
56
  this._locale = locale
41
57
  }
42
58
  }
43
59
 
60
+ /**
61
+ * Get the current locale.
62
+ *
63
+ * @returns The current locale string.
64
+ */
44
65
  getLocale(): string {
45
66
  return this._locale
46
67
  }
47
68
 
48
69
  /**
49
- * Add translations for a locale
70
+ * Translate a key.
71
+ *
72
+ * @param key - The translation key (e.g., 'messages.welcome').
73
+ * @param replacements - Optional replacements for parameters in the translation string.
74
+ * @returns The translated string, or the key if not found.
75
+ */
76
+ t(key: string, replacements?: Record<string, string | number>): string {
77
+ return this.manager.translate(this._locale, key, replacements)
78
+ }
79
+
80
+ /**
81
+ * Check if a translation key exists.
82
+ *
83
+ * @param key - The translation key to check.
84
+ * @returns True if the key exists, false otherwise.
85
+ */
86
+ has(key: string): boolean {
87
+ return this.t(key) !== key
88
+ }
89
+
90
+ /**
91
+ * Clone the current instance with a potentially new locale.
92
+ *
93
+ * @param locale - Optional new locale for the cloned instance.
94
+ * @returns A new I18nInstance.
95
+ */
96
+ clone(locale?: string): I18nService {
97
+ return new I18nInstance(this.manager, locale || this._locale)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Global I18n Manager
103
+ * Holds shared configuration and translation resources
104
+ */
105
+ export class I18nManager implements I18nService {
106
+ private translations: Record<string, Record<string, string>> = {}
107
+ // Default instance for global usage (e.g. CLI or background jobs)
108
+ private globalInstance: I18nInstance
109
+
110
+ /**
111
+ * Create a new I18nManager.
112
+ *
113
+ * @param config - The I18n configuration.
114
+ */
115
+ constructor(private config: I18nConfig) {
116
+ if (config.translations) {
117
+ this.translations = config.translations
118
+ }
119
+ this.globalInstance = new I18nInstance(this, config.defaultLocale)
120
+ }
121
+
122
+ // --- I18nService Implementation (Delegates to global instance) ---
123
+
124
+ get locale(): string {
125
+ return this.globalInstance.locale
126
+ }
127
+
128
+ set locale(value: string) {
129
+ this.globalInstance.locale = value
130
+ }
131
+
132
+ /**
133
+ * Set the global locale.
134
+ *
135
+ * @param locale - The locale to set.
136
+ */
137
+ setLocale(locale: string): void {
138
+ this.globalInstance.setLocale(locale)
139
+ }
140
+
141
+ /**
142
+ * Get the global locale.
143
+ *
144
+ * @returns The global locale string.
145
+ */
146
+ getLocale(): string {
147
+ return this.globalInstance.getLocale()
148
+ }
149
+
150
+ /**
151
+ * Translate a key using the global locale.
152
+ *
153
+ * @param key - The translation key.
154
+ * @param replacements - Optional replacements.
155
+ * @returns The translated string.
156
+ */
157
+ t(key: string, replacements?: Record<string, string | number>): string {
158
+ return this.globalInstance.t(key, replacements)
159
+ }
160
+
161
+ /**
162
+ * Check if a translation key exists in the global locale.
163
+ *
164
+ * @param key - The translation key.
165
+ * @returns True if found.
166
+ */
167
+ has(key: string): boolean {
168
+ return this.globalInstance.has(key)
169
+ }
170
+
171
+ /**
172
+ * Clone the global instance.
173
+ *
174
+ * @param locale - Optional locale for the clone.
175
+ * @returns A new I18nInstance.
176
+ */
177
+ clone(locale?: string): I18nService {
178
+ return new I18nInstance(this, locale || this.config.defaultLocale)
179
+ }
180
+
181
+ // --- Manager Internal API ---
182
+
183
+ /**
184
+ * Get the I18n configuration.
185
+ *
186
+ * @returns The configuration object.
187
+ */
188
+ getConfig(): I18nConfig {
189
+ return this.config
190
+ }
191
+
192
+ /**
193
+ * Add a resource bundle for a specific locale.
194
+ *
195
+ * @param locale - The locale string.
196
+ * @param translations - The translations object.
50
197
  */
51
198
  addResource(locale: string, translations: Record<string, string>) {
52
199
  this.translations[locale] = {
@@ -56,16 +203,11 @@ export class I18nManager implements I18nService {
56
203
  }
57
204
 
58
205
  /**
59
- * Translation helper
60
- * t('messages.welcome', { name: 'Carl' })
61
- * Supports nested keys via dot notation: t('auth.errors.invalid')
206
+ * Internal translation logic used by instances
62
207
  */
63
- t(key: string, replacements?: Record<string, string | number>): string {
208
+ translate(locale: string, key: string, replacements?: Record<string, string | number>): string {
64
209
  const keys = key.split('.')
65
- let value: any = this.translations[this._locale]
66
-
67
- // Fallback to default locale if not found in current locale?
68
- // Implementation: Try current locale, then fallback.
210
+ let value: any = this.translations[locale]
69
211
 
70
212
  // 1. Try current locale
71
213
  for (const k of keys) {
@@ -78,7 +220,7 @@ export class I18nManager implements I18nService {
78
220
  }
79
221
 
80
222
  // 2. If not found, try fallback (defaultLocale)
81
- if (value === undefined && this._locale !== this.config.defaultLocale) {
223
+ if (value === undefined && locale !== this.config.defaultLocale) {
82
224
  let fallbackValue: any = this.translations[this.config.defaultLocale]
83
225
  for (const k of keys) {
84
226
  if (fallbackValue && typeof fallbackValue === 'object' && k in fallbackValue) {
@@ -104,11 +246,6 @@ export class I18nManager implements I18nService {
104
246
 
105
247
  return value
106
248
  }
107
-
108
- has(key: string): boolean {
109
- // Simplistic check
110
- return this.t(key) !== key
111
- }
112
249
  }
113
250
 
114
251
  /**
@@ -118,21 +255,25 @@ export class I18nManager implements I18nService {
118
255
  * 1. Route Parameter (e.g. /:locale/foo) - Recommended for SEO
119
256
  * 2. Header (Accept-Language) - Recommended for APIs
120
257
  */
121
- export const localeMiddleware = (i18n: I18nService): MiddlewareHandler => {
258
+ export const localeMiddleware = (i18nManager: I18nService): MiddlewareHandler => {
122
259
  return async (c, next) => {
123
- // 1. Check for route param 'locale'
124
- const paramLocale = c.req.param('locale')
125
- if (paramLocale) {
126
- i18n.setLocale(paramLocale)
260
+ // Determine initial locale
261
+ // Priority: 1. Route Param 2. Query ?lang= 3. Header 4. Default
262
+ let locale = c.req.param('locale') || c.req.query('lang')
263
+
264
+ if (!locale) {
265
+ const acceptLang = c.req.header('Accept-Language')
266
+ if (acceptLang) {
267
+ // Simple extraction: 'en-US,en;q=0.9' -> 'en-US'
268
+ locale = acceptLang.split(',')[0]?.trim()
269
+ }
127
270
  }
128
271
 
129
- // 2. Inject into context (using 'any' for now, or augment PlanetCore variables later)
130
- c.set('i18n', i18n)
272
+ // Clone a request-scoped instance
273
+ const i18n = i18nManager.clone(locale)
131
274
 
132
- // 3. Share with View layer (if Orbit View is present)
133
- // Assuming 'view' service might look at context variables or we explicitly pass it
134
- // For Inertia, we might want to share it as a prop.
135
- // This part depends on how the user sets up their detailed pipeline, but setting it in 'c' is the start.
275
+ // Inject into context
276
+ c.set('i18n', i18n)
136
277
 
137
278
  await next()
138
279
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { GravitoOrbit, PlanetCore } from 'gravito-core'
2
- import { type I18nConfig, I18nManager, type I18nService } from './I18nService'
2
+ import { type I18nConfig, I18nManager, type I18nService, localeMiddleware } from './I18nService'
3
3
 
4
4
  declare module 'gravito-core' {
5
5
  interface Variables {
@@ -11,21 +11,14 @@ export class I18nOrbit implements GravitoOrbit {
11
11
  constructor(private config: I18nConfig) {}
12
12
 
13
13
  install(core: PlanetCore): void {
14
- const i18n = new I18nManager(this.config)
14
+ const i18nManager = new I18nManager(this.config)
15
15
 
16
- // Register globally if needed, or just prepare it to be used.
17
- // There isn't a global "services" container in PlanetCore yet other than 'Variables' injected via Config/Context.
18
- // Ideally we attach it to the core instance or inject it into every request.
16
+ // Register globally if needed (for CLI/Jobs)
17
+ // core.services.set('i18n', i18nManager);
19
18
 
20
- // Inject into every request
21
- core.adapter.use('*', async (c, next) => {
22
- c.set('i18n', i18n)
23
- await next()
24
- })
25
-
26
- // Register a helper if using Orbit View (View Rendering)
27
- // We can check if 'view' exists or we can register a global view helper if that API exists.
28
- // For now, context injection is sufficient.
19
+ // Inject locale middleware into every request
20
+ // This middleware handles cloning the i18n instance per request
21
+ core.adapter.use('*', localeMiddleware(i18nManager) as any)
29
22
 
30
23
  core.logger.info(`I18n Orbit initialized with locale: ${this.config.defaultLocale}`)
31
24
  }