@geenius/i18n 0.1.0
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/.changeset/config.json +11 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/ci.yml +23 -0
- package/.github/workflows/release.yml +29 -0
- package/.nvmrc +1 -0
- package/.project/ACCOUNT.yaml +4 -0
- package/.project/IDEAS.yaml +7 -0
- package/.project/PROJECT.yaml +11 -0
- package/.project/ROADMAP.yaml +15 -0
- package/CHANGELOG.md +8 -0
- package/CODE_OF_CONDUCT.md +16 -0
- package/CONTRIBUTING.md +26 -0
- package/LICENSE +2 -0
- package/README.md +1 -0
- package/SECURITY.md +15 -0
- package/SUPPORT.md +8 -0
- package/package.json +75 -0
- package/packages/convex/package.json +42 -0
- package/packages/convex/src/index.ts +3 -0
- package/packages/convex/src/mutations.ts +65 -0
- package/packages/convex/src/queries.ts +54 -0
- package/packages/convex/src/schema.ts +26 -0
- package/packages/convex/tsconfig.json +18 -0
- package/packages/convex/tsup.config.ts +17 -0
- package/packages/react/README.md +1 -0
- package/packages/react/package.json +51 -0
- package/packages/react/src/components/index.tsx +87 -0
- package/packages/react/src/hooks/index.ts +4 -0
- package/packages/react/src/hooks/useI18n.tsx +50 -0
- package/packages/react/src/hooks/useI18nAdmin.ts +12 -0
- package/packages/react/src/hooks/useLocaleDetect.ts +10 -0
- package/packages/react/src/hooks/useTranslations.ts +11 -0
- package/packages/react/src/index.tsx +8 -0
- package/packages/react/src/pages/I18nAdminPage.tsx +42 -0
- package/packages/react/src/pages/LocalePreviewPage.tsx +54 -0
- package/packages/react/src/pages/index.ts +2 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/react/tsup.config.ts +12 -0
- package/packages/react-css/README.md +1 -0
- package/packages/react-css/package.json +36 -0
- package/packages/react-css/src/components/index.tsx +66 -0
- package/packages/react-css/src/hooks/index.ts +4 -0
- package/packages/react-css/src/index.tsx +4 -0
- package/packages/react-css/src/pages/LocaleSettingsPage.tsx +74 -0
- package/packages/react-css/src/pages/TranslationsPage.tsx +98 -0
- package/packages/react-css/src/styles.css +210 -0
- package/packages/react-css/tsconfig.json +19 -0
- package/packages/react-css/tsup.config.ts +10 -0
- package/packages/shared/README.md +1 -0
- package/packages/shared/package.json +44 -0
- package/packages/shared/src/__tests__/i18n.test.ts +78 -0
- package/packages/shared/src/config.ts +344 -0
- package/packages/shared/src/index.ts +106 -0
- package/packages/shared/src/types.ts +51 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/shared/tsup.config.ts +11 -0
- package/packages/shared/vitest.config.ts +4 -0
- package/packages/solidjs/README.md +1 -0
- package/packages/solidjs/package.json +47 -0
- package/packages/solidjs/src/components/LocaleCard.tsx +44 -0
- package/packages/solidjs/src/components/LocaleStatsCard.tsx +35 -0
- package/packages/solidjs/src/components/LocaleSwitcher.tsx +65 -0
- package/packages/solidjs/src/components/MissingKeyAlert.tsx +21 -0
- package/packages/solidjs/src/components/RTLWrapper.tsx +13 -0
- package/packages/solidjs/src/components/TranslationKeyRow.tsx +41 -0
- package/packages/solidjs/src/components/index.ts +6 -0
- package/packages/solidjs/src/index.tsx +8 -0
- package/packages/solidjs/src/pages/I18nAdminPage.tsx +188 -0
- package/packages/solidjs/src/pages/LocalePreviewPage.tsx +99 -0
- package/packages/solidjs/src/pages/index.ts +2 -0
- package/packages/solidjs/src/primitives/I18nProvider.tsx +56 -0
- package/packages/solidjs/src/primitives/createI18nAdmin.ts +7 -0
- package/packages/solidjs/src/primitives/createLocaleDetect.ts +8 -0
- package/packages/solidjs/src/primitives/createTranslations.ts +22 -0
- package/packages/solidjs/src/primitives/index.ts +4 -0
- package/packages/solidjs/tsconfig.json +20 -0
- package/packages/solidjs/tsup.config.ts +12 -0
- package/packages/solidjs-css/README.md +1 -0
- package/packages/solidjs-css/package.json +33 -0
- package/packages/solidjs-css/src/components/LocaleCard.tsx +45 -0
- package/packages/solidjs-css/src/components/LocaleStatsCard.tsx +43 -0
- package/packages/solidjs-css/src/components/LocaleSwitcher.tsx +51 -0
- package/packages/solidjs-css/src/components/MissingKeyAlert.tsx +24 -0
- package/packages/solidjs-css/src/components/RTLWrapper.tsx +16 -0
- package/packages/solidjs-css/src/components/TranslationKeyRow.tsx +47 -0
- package/packages/solidjs-css/src/components/index.ts +6 -0
- package/packages/solidjs-css/src/i18n.css +1322 -0
- package/packages/solidjs-css/src/index.tsx +3 -0
- package/packages/solidjs-css/src/pages/I18nAdminPage.tsx +134 -0
- package/packages/solidjs-css/src/pages/LocalePreviewPage.tsx +116 -0
- package/packages/solidjs-css/src/pages/index.ts +2 -0
- package/packages/solidjs-css/src/primitives/index.ts +1 -0
- package/packages/solidjs-css/tsconfig.json +20 -0
- package/packages/solidjs-css/tsup.config.bundled_dcjc4sct21j.mjs +18 -0
- package/packages/solidjs-css/tsup.config.ts +14 -0
- package/pnpm-workspace.yaml +2 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18n configuration and setup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Locale, I18nConfig, TranslationDict } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* I18n configuration builder
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const i18nConfig = createI18nConfig()
|
|
13
|
+
* .withDefaultLocale('en')
|
|
14
|
+
* .withSupportedLocales(['en', 'fr', 'de'])
|
|
15
|
+
* .withFallbackLocale('en')
|
|
16
|
+
* .withNamespace('common')
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export class I18nConfigBuilder {
|
|
20
|
+
private config: I18nConfig = {
|
|
21
|
+
defaultLocale: 'en',
|
|
22
|
+
supportedLocales: ['en'],
|
|
23
|
+
fallbackLocale: 'en',
|
|
24
|
+
namespace: 'translations',
|
|
25
|
+
debug: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sets default locale
|
|
30
|
+
*/
|
|
31
|
+
withDefaultLocale(locale: Locale): this {
|
|
32
|
+
this.config.defaultLocale = locale
|
|
33
|
+
return this
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sets supported locales
|
|
38
|
+
*/
|
|
39
|
+
withSupportedLocales(locales: Locale[]): this {
|
|
40
|
+
this.config.supportedLocales = locales
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sets fallback locale when translation is missing
|
|
46
|
+
*/
|
|
47
|
+
withFallbackLocale(locale: Locale): this {
|
|
48
|
+
this.config.fallbackLocale = locale
|
|
49
|
+
return this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets translation namespace
|
|
54
|
+
*/
|
|
55
|
+
withNamespace(namespace: string): this {
|
|
56
|
+
this.config.namespace = namespace
|
|
57
|
+
return this
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sets debug mode
|
|
62
|
+
*/
|
|
63
|
+
withDebug(enabled: boolean): this {
|
|
64
|
+
this.config.debug = enabled
|
|
65
|
+
return this
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Enables missing key warnings
|
|
70
|
+
*/
|
|
71
|
+
withMissingKeyWarnings(enabled: boolean): this {
|
|
72
|
+
this.config.missingKeyWarnings = enabled
|
|
73
|
+
return this
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sets missing key prefix
|
|
78
|
+
*/
|
|
79
|
+
withMissingKeyPrefix(prefix: string): this {
|
|
80
|
+
this.config.missingKeyPrefix = prefix
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Builds the configuration
|
|
86
|
+
*/
|
|
87
|
+
build(): I18nConfig {
|
|
88
|
+
if (!this.config.supportedLocales.includes(this.config.defaultLocale)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Default locale ${this.config.defaultLocale} must be in supported locales`
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!this.config.supportedLocales.includes(this.config.fallbackLocale)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Fallback locale ${this.config.fallbackLocale} must be in supported locales`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return this.config
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Creates a new I18n configuration builder
|
|
106
|
+
*/
|
|
107
|
+
export function createI18nConfig(): I18nConfigBuilder {
|
|
108
|
+
return new I18nConfigBuilder()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Translation loader configuration
|
|
113
|
+
*/
|
|
114
|
+
export interface TranslationLoaderConfig {
|
|
115
|
+
/** Base URL for loading translations */
|
|
116
|
+
baseUrl?: string
|
|
117
|
+
/** Whether to cache loaded translations */
|
|
118
|
+
cache?: boolean
|
|
119
|
+
/** Cache duration in milliseconds */
|
|
120
|
+
cacheDuration?: number
|
|
121
|
+
/** Whether to load all locales on init */
|
|
122
|
+
preloadAll?: boolean
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Creates a translation loader
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* const loader = createTranslationLoader({
|
|
131
|
+
* baseUrl: '/locales',
|
|
132
|
+
* cache: true,
|
|
133
|
+
* cacheDuration: 3600000
|
|
134
|
+
* })
|
|
135
|
+
*
|
|
136
|
+
* const translations = await loader.load('en', 'common')
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function createTranslationLoader(config: TranslationLoaderConfig = {}) {
|
|
140
|
+
const cache = new Map<string, { data: TranslationDict; timestamp: number }>()
|
|
141
|
+
const {
|
|
142
|
+
baseUrl = '/locales',
|
|
143
|
+
cache: enableCache = true,
|
|
144
|
+
cacheDuration = 3600000, // 1 hour
|
|
145
|
+
} = config
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
/**
|
|
149
|
+
* Loads translations for a locale and namespace
|
|
150
|
+
*/
|
|
151
|
+
async load(locale: Locale, namespace: string): Promise<TranslationDict> {
|
|
152
|
+
const cacheKey = `${locale}:${namespace}`
|
|
153
|
+
|
|
154
|
+
// Check cache
|
|
155
|
+
if (enableCache) {
|
|
156
|
+
const cached = cache.get(cacheKey)
|
|
157
|
+
if (cached && Date.now() - cached.timestamp < cacheDuration) {
|
|
158
|
+
return cached.data
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const url = `${baseUrl}/${locale}/${namespace}.json`
|
|
164
|
+
const response = await fetch(url)
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
console.warn(
|
|
168
|
+
`Failed to load translations from ${url}: ${response.statusText}`
|
|
169
|
+
)
|
|
170
|
+
return {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const data = (await response.json()) as TranslationDict
|
|
174
|
+
|
|
175
|
+
// Cache result
|
|
176
|
+
if (enableCache) {
|
|
177
|
+
cache.set(cacheKey, {
|
|
178
|
+
data,
|
|
179
|
+
timestamp: Date.now(),
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return data
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`Error loading translations for ${locale}/${namespace}:`, error)
|
|
186
|
+
return {}
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Preloads all locales for a namespace
|
|
192
|
+
*/
|
|
193
|
+
async preload(locales: Locale[], namespace: string): Promise<void> {
|
|
194
|
+
await Promise.all(locales.map((locale) => this.load(locale, namespace)))
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clears cache
|
|
199
|
+
*/
|
|
200
|
+
clearCache(locale?: Locale, namespace?: string) {
|
|
201
|
+
if (locale && namespace) {
|
|
202
|
+
cache.delete(`${locale}:${namespace}`)
|
|
203
|
+
} else {
|
|
204
|
+
cache.clear()
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Gets cache stats
|
|
210
|
+
*/
|
|
211
|
+
getCacheStats() {
|
|
212
|
+
return {
|
|
213
|
+
size: cache.size,
|
|
214
|
+
entries: Array.from(cache.keys()),
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Namespace loader for managing multiple translation namespaces
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* const loader = createNamespaceLoader(createTranslationLoader())
|
|
226
|
+
* const translations = await loader.loadMultiple('en', ['common', 'errors'])
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export function createNamespaceLoader(
|
|
230
|
+
translationLoader: ReturnType<typeof createTranslationLoader>
|
|
231
|
+
) {
|
|
232
|
+
const namespaces = new Map<string, TranslationDict>()
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
/**
|
|
236
|
+
* Loads a single namespace
|
|
237
|
+
*/
|
|
238
|
+
async load(locale: Locale, namespace: string): Promise<TranslationDict> {
|
|
239
|
+
const translations = await translationLoader.load(locale, namespace)
|
|
240
|
+
const key = `${locale}:${namespace}`
|
|
241
|
+
namespaces.set(key, translations)
|
|
242
|
+
return translations
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Loads multiple namespaces
|
|
247
|
+
*/
|
|
248
|
+
async loadMultiple(
|
|
249
|
+
locale: Locale,
|
|
250
|
+
namespaceList: string[]
|
|
251
|
+
): Promise<Record<string, TranslationDict>> {
|
|
252
|
+
const result: Record<string, TranslationDict> = {}
|
|
253
|
+
|
|
254
|
+
for (const ns of namespaceList) {
|
|
255
|
+
result[ns] = await this.load(locale, ns)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Gets loaded namespace
|
|
263
|
+
*/
|
|
264
|
+
get(locale: Locale, namespace: string): TranslationDict | undefined {
|
|
265
|
+
return namespaces.get(`${locale}:${namespace}`)
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Merges multiple translations into one
|
|
270
|
+
*/
|
|
271
|
+
merge(
|
|
272
|
+
locale: Locale,
|
|
273
|
+
namespaceList: string[]
|
|
274
|
+
): TranslationDict {
|
|
275
|
+
const merged: TranslationDict = {}
|
|
276
|
+
|
|
277
|
+
for (const ns of namespaceList) {
|
|
278
|
+
const trans = this.get(locale, ns)
|
|
279
|
+
if (trans) {
|
|
280
|
+
Object.assign(merged, trans)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return merged
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Default i18n configurations for different scenarios
|
|
291
|
+
*/
|
|
292
|
+
export const i18nPresets = {
|
|
293
|
+
/**
|
|
294
|
+
* Simple single-language setup
|
|
295
|
+
*/
|
|
296
|
+
simple: (locale: Locale = 'en'): I18nConfig => {
|
|
297
|
+
return createI18nConfig()
|
|
298
|
+
.withDefaultLocale(locale)
|
|
299
|
+
.withSupportedLocales([locale])
|
|
300
|
+
.build()
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Multi-language with common locales
|
|
305
|
+
*/
|
|
306
|
+
multi: (defaultLocale: Locale = 'en'): I18nConfig => {
|
|
307
|
+
return createI18nConfig()
|
|
308
|
+
.withDefaultLocale(defaultLocale)
|
|
309
|
+
.withSupportedLocales(['en', 'fr', 'de', 'es', 'it'])
|
|
310
|
+
.build()
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Global localization
|
|
315
|
+
*/
|
|
316
|
+
global: (defaultLocale: Locale = 'en'): I18nConfig => {
|
|
317
|
+
return createI18nConfig()
|
|
318
|
+
.withDefaultLocale(defaultLocale)
|
|
319
|
+
.withSupportedLocales([
|
|
320
|
+
'en',
|
|
321
|
+
'fr',
|
|
322
|
+
'de',
|
|
323
|
+
'es',
|
|
324
|
+
'it',
|
|
325
|
+
'pt',
|
|
326
|
+
'ru',
|
|
327
|
+
'zh',
|
|
328
|
+
'ja',
|
|
329
|
+
'ko',
|
|
330
|
+
] as Locale[])
|
|
331
|
+
.build()
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Development configuration with debug output
|
|
336
|
+
*/
|
|
337
|
+
development: (): I18nConfig => {
|
|
338
|
+
return createI18nConfig()
|
|
339
|
+
.withDebug(true)
|
|
340
|
+
.withMissingKeyWarnings(true)
|
|
341
|
+
.withMissingKeyPrefix('[MISSING] ')
|
|
342
|
+
.build()
|
|
343
|
+
},
|
|
344
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
Locale, Direction, I18nNamespace, TranslationDict, I18nConfig,
|
|
3
|
+
LocaleInfo, TranslationEntry, MissingKey, LocaleStat,
|
|
4
|
+
} from './types'
|
|
5
|
+
|
|
6
|
+
import type { Locale, Direction, LocaleInfo, TranslationDict } from './types'
|
|
7
|
+
|
|
8
|
+
// ─── LOCALE_INFO (14 locales) ─────────────────────────
|
|
9
|
+
export const LOCALE_INFO: Record<Locale, LocaleInfo> = {
|
|
10
|
+
en: { code: 'en', name: 'English', nativeName: 'English', direction: 'ltr', flag: '🇬🇧' },
|
|
11
|
+
fr: { code: 'fr', name: 'French', nativeName: 'Français', direction: 'ltr', flag: '🇫🇷' },
|
|
12
|
+
de: { code: 'de', name: 'German', nativeName: 'Deutsch', direction: 'ltr', flag: '🇩🇪' },
|
|
13
|
+
es: { code: 'es', name: 'Spanish', nativeName: 'Español', direction: 'ltr', flag: '🇪🇸' },
|
|
14
|
+
pt: { code: 'pt', name: 'Portuguese', nativeName: 'Português', direction: 'ltr', flag: '🇵🇹' },
|
|
15
|
+
it: { code: 'it', name: 'Italian', nativeName: 'Italiano', direction: 'ltr', flag: '🇮🇹' },
|
|
16
|
+
nl: { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', direction: 'ltr', flag: '🇳🇱' },
|
|
17
|
+
ru: { code: 'ru', name: 'Russian', nativeName: 'Русский', direction: 'ltr', flag: '🇷🇺' },
|
|
18
|
+
zh: { code: 'zh', name: 'Chinese', nativeName: '中文', direction: 'ltr', flag: '🇨🇳' },
|
|
19
|
+
ja: { code: 'ja', name: 'Japanese', nativeName: '日本語', direction: 'ltr', flag: '🇯🇵' },
|
|
20
|
+
ko: { code: 'ko', name: 'Korean', nativeName: '한국어', direction: 'ltr', flag: '🇰🇷' },
|
|
21
|
+
ar: { code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', flag: '🇸🇦' },
|
|
22
|
+
he: { code: 'he', name: 'Hebrew', nativeName: 'עברית', direction: 'rtl', flag: '🇮🇱' },
|
|
23
|
+
tr: { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', direction: 'ltr', flag: '🇹🇷' },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ALL_LOCALES: Locale[] = Object.keys(LOCALE_INFO) as Locale[]
|
|
27
|
+
export const RTL_LOCALES: Locale[] = ['ar', 'he']
|
|
28
|
+
|
|
29
|
+
// ─── Translation Utilities ────────────────────────────
|
|
30
|
+
export function interpolate(template: string, params: Record<string, string | number>): string {
|
|
31
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => String(params[key] ?? `{{${key}}}`))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function t(key: string, dict: TranslationDict, params?: Record<string, string | number>): string {
|
|
35
|
+
const parts = key.split('.')
|
|
36
|
+
let current: string | TranslationDict = dict
|
|
37
|
+
for (const part of parts) {
|
|
38
|
+
if (typeof current !== 'object' || current === null) return key
|
|
39
|
+
current = (current as TranslationDict)[part]
|
|
40
|
+
if (current === undefined) return key
|
|
41
|
+
}
|
|
42
|
+
if (typeof current !== 'string') return key
|
|
43
|
+
return params ? interpolate(current, params) : current
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function plural(key: string, count: number, dict: TranslationDict): string {
|
|
47
|
+
if (count === 0) { const zero = t(`${key}_zero`, dict); if (zero !== `${key}_zero`) return zero }
|
|
48
|
+
if (count === 1) { const one = t(`${key}_one`, dict); if (one !== `${key}_one`) return one }
|
|
49
|
+
const other = t(`${key}_other`, dict)
|
|
50
|
+
if (other !== `${key}_other`) return interpolate(other, { count })
|
|
51
|
+
return t(key, dict, { count })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── Formatting ───────────────────────────────────────
|
|
55
|
+
export function formatDate(date: Date | string, locale: Locale, format?: Intl.DateTimeFormatOptions): string {
|
|
56
|
+
const d = typeof date === 'string' ? new Date(date) : date
|
|
57
|
+
return new Intl.DateTimeFormat(locale, format ?? { dateStyle: 'medium' }).format(d)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatNumber(n: number, locale: Locale, opts?: Intl.NumberFormatOptions): string {
|
|
61
|
+
return new Intl.NumberFormat(locale, opts).format(n)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function formatCurrency(amount: number, currency: string, locale: Locale): string {
|
|
65
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Detection & Direction ────────────────────────────
|
|
69
|
+
export function detectLocale(supportedLocales: Locale[]): Locale {
|
|
70
|
+
if (typeof navigator === 'undefined') return supportedLocales[0] ?? 'en'
|
|
71
|
+
const browserLang = navigator.language.split('-')[0] as Locale
|
|
72
|
+
if (supportedLocales.includes(browserLang)) return browserLang
|
|
73
|
+
const full = navigator.language as Locale
|
|
74
|
+
if (supportedLocales.includes(full)) return full
|
|
75
|
+
return supportedLocales[0] ?? 'en'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getDirection(locale: Locale): Direction {
|
|
79
|
+
return LOCALE_INFO[locale]?.direction ?? 'ltr'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isRTL(locale: Locale): boolean {
|
|
83
|
+
return getDirection(locale) === 'rtl'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Namespace Loader (stub) ──────────────────────────
|
|
87
|
+
const namespaceCache = new Map<string, TranslationDict>()
|
|
88
|
+
|
|
89
|
+
export async function loadNamespace(locale: Locale, ns: string): Promise<TranslationDict> {
|
|
90
|
+
const cacheKey = `${locale}:${ns}`
|
|
91
|
+
if (namespaceCache.has(cacheKey)) return namespaceCache.get(cacheKey)!
|
|
92
|
+
// In real usage this would fetch from API/CDN — stub returns empty dict
|
|
93
|
+
const dict: TranslationDict = {}
|
|
94
|
+
namespaceCache.set(cacheKey, dict)
|
|
95
|
+
return dict
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function flattenDict(dict: TranslationDict, prefix = ''): Record<string, string> {
|
|
99
|
+
const result: Record<string, string> = {}
|
|
100
|
+
for (const [key, value] of Object.entries(dict)) {
|
|
101
|
+
const fullKey = prefix ? `${prefix}.${key}` : key
|
|
102
|
+
if (typeof value === 'string') result[fullKey] = value
|
|
103
|
+
else Object.assign(result, flattenDict(value, fullKey))
|
|
104
|
+
}
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type Locale = 'en' | 'fr' | 'de' | 'es' | 'pt' | 'it' | 'nl' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'he' | 'tr'
|
|
2
|
+
export type Direction = 'ltr' | 'rtl'
|
|
3
|
+
export type I18nNamespace = 'common' | 'auth' | 'dashboard' | 'billing' | 'errors' | string
|
|
4
|
+
|
|
5
|
+
export type TranslationDict = Record<string, string | TranslationDict>
|
|
6
|
+
|
|
7
|
+
export interface I18nConfig {
|
|
8
|
+
defaultLocale: Locale
|
|
9
|
+
supportedLocales: Locale[]
|
|
10
|
+
fallbackLocale?: Locale
|
|
11
|
+
namespaces?: I18nNamespace[]
|
|
12
|
+
detectBrowser?: boolean
|
|
13
|
+
persistLocale?: boolean
|
|
14
|
+
dateFormat?: string
|
|
15
|
+
numberFormat?: Intl.NumberFormatOptions
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LocaleInfo {
|
|
19
|
+
code: Locale
|
|
20
|
+
name: string
|
|
21
|
+
nativeName: string
|
|
22
|
+
direction: Direction
|
|
23
|
+
flag: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TranslationEntry {
|
|
27
|
+
id: string
|
|
28
|
+
locale: Locale
|
|
29
|
+
namespace: string
|
|
30
|
+
key: string
|
|
31
|
+
value: string
|
|
32
|
+
lastEditedBy?: string
|
|
33
|
+
updatedAt: string
|
|
34
|
+
createdAt: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MissingKey {
|
|
38
|
+
id: string
|
|
39
|
+
locale: Locale
|
|
40
|
+
namespace: string
|
|
41
|
+
key: string
|
|
42
|
+
detectedAt: string
|
|
43
|
+
count: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface LocaleStat {
|
|
47
|
+
locale: Locale
|
|
48
|
+
totalKeys: number
|
|
49
|
+
missingKeys: number
|
|
50
|
+
coverage: number
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"target": "ES2022",
|
|
12
|
+
"module": "ESNext",
|
|
13
|
+
"moduleResolution": "bundler"
|
|
14
|
+
},
|
|
15
|
+
"include": [
|
|
16
|
+
"src"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# ✦ @geenius-i18n/solidjs\n\n> Geenius I18n — SolidJS components & primitives\n\n---\n\n## Overview\nBuilt with Steve Jobs-level minimalism and Jony Ive-level craftsmanship, this package is designed to deliver unparalleled developer experience (DX) and rock-solid performance.\n\n## Installation\n\n```bash\npnpm add @geenius-i18n/solidjs\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-i18n/solidjs';\n\n// Initialize the module with absolute precision\ninit({\n mode: 'premium',\n});\n```\n\n## Architecture\n- **Zero-config**: It just works.\n- **Strictly Typed**: Fully written in TypeScript for flawless IntelliSense.\n- **Framework Agnostic**: seamlessly integrates into the Geenius ecosystem.\n\n---\n\n*Designed by Antigravity HQ*\n
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geenius-i18n/solidjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Geenius I18n — SolidJS components & primitives",
|
|
7
|
+
"author": "Antigravity HQ",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "restricted"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"type-check": "tsc --noEmit",
|
|
29
|
+
"prepublishOnly": "pnpm clean && pnpm build"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@geenius-i18n/shared": "workspace:*"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"solid-js": "^1.9.0",
|
|
36
|
+
"tsup": "^8.5.1",
|
|
37
|
+
"typescript": "~6.0.2",
|
|
38
|
+
"@tanstack/solid-router": "^1.111.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"solid-js": "^1.8.0 || ^1.9.0",
|
|
42
|
+
"@tanstack/solid-router": "^1.0.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Show } from 'solid-js'
|
|
2
|
+
import type { Component } from 'solid-js'
|
|
3
|
+
import type { Locale } from '@geenius-i18n/shared'
|
|
4
|
+
import { LOCALE_INFO } from '@geenius-i18n/shared'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale: Locale
|
|
8
|
+
coverage?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const LocaleCard: Component<Props> = (props) => {
|
|
12
|
+
const info = () => LOCALE_INFO[props.locale]
|
|
13
|
+
const barColor = () =>
|
|
14
|
+
(props.coverage ?? 0) >= 90 ? 'oklch(0.72 0.18 155)' :
|
|
15
|
+
(props.coverage ?? 0) >= 70 ? 'oklch(0.72 0.18 60)' :
|
|
16
|
+
'oklch(0.60 0.25 25)'
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div style={{
|
|
20
|
+
'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)',
|
|
21
|
+
background: 'oklch(1 0 0 / 0.02)', padding: '1rem',
|
|
22
|
+
}}>
|
|
23
|
+
<div style={{ display: 'flex', 'align-items': 'center', gap: '0.75rem', 'margin-bottom': '0.75rem' }}>
|
|
24
|
+
<span style={{ 'font-size': '1.5rem' }}>{info()?.flag}</span>
|
|
25
|
+
<div>
|
|
26
|
+
<p style={{ 'font-size': '0.875rem', 'font-weight': '600', color: 'oklch(1 0 0 / 0.85)' }}>{info()?.nativeName}</p>
|
|
27
|
+
<p style={{ 'font-size': '0.625rem', color: 'oklch(1 0 0 / 0.4)' }}>{info()?.name} — {props.locale.toUpperCase()}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<Show when={info()?.direction === 'rtl'}>
|
|
30
|
+
<span style={{
|
|
31
|
+
'margin-left': 'auto', 'font-size': '0.625rem', padding: '0.125rem 0.375rem',
|
|
32
|
+
'border-radius': '0.25rem', background: 'oklch(0.72 0.18 60 / 0.15)', color: 'oklch(0.72 0.18 60)',
|
|
33
|
+
}}>RTL</span>
|
|
34
|
+
</Show>
|
|
35
|
+
</div>
|
|
36
|
+
<Show when={props.coverage !== undefined}>
|
|
37
|
+
<div style={{ height: '0.375rem', 'border-radius': '9999px', background: 'oklch(1 0 0 / 0.05)', overflow: 'hidden' }}>
|
|
38
|
+
<div style={{ height: '100%', 'border-radius': '9999px', background: barColor(), width: `${props.coverage}%` }} />
|
|
39
|
+
</div>
|
|
40
|
+
<p style={{ 'margin-top': '0.25rem', 'font-size': '0.625rem', color: 'oklch(1 0 0 / 0.4)', 'text-align': 'right' }}>{props.coverage}%</p>
|
|
41
|
+
</Show>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js'
|
|
2
|
+
import type { Component } from 'solid-js'
|
|
3
|
+
import type { LocaleStat } from '@geenius-i18n/shared'
|
|
4
|
+
import { LOCALE_INFO } from '@geenius-i18n/shared'
|
|
5
|
+
|
|
6
|
+
export const LocaleStatsCard: Component<{ stats: LocaleStat[] }> = (props) => {
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ display: 'grid', 'grid-template-columns': 'repeat(4, 1fr)', gap: '0.75rem' }}>
|
|
9
|
+
<For each={props.stats}>
|
|
10
|
+
{(s) => {
|
|
11
|
+
const info = LOCALE_INFO[s.locale]
|
|
12
|
+
const barColor = s.coverage >= 90 ? 'oklch(0.72 0.18 155)' : s.coverage >= 70 ? 'oklch(0.72 0.18 60)' : 'oklch(0.60 0.25 25)'
|
|
13
|
+
return (
|
|
14
|
+
<div style={{ 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)', background: 'oklch(1 0 0 / 0.02)', padding: '1rem' }}>
|
|
15
|
+
<div style={{ display: 'flex', 'align-items': 'center', gap: '0.5rem', 'margin-bottom': '0.5rem' }}>
|
|
16
|
+
<span style={{ 'font-size': '1.125rem' }}>{info?.flag}</span>
|
|
17
|
+
<span style={{ 'font-size': '0.75rem', 'font-weight': '500', color: 'oklch(1 0 0 / 0.8)' }}>{info?.nativeName}</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div style={{ display: 'flex', 'justify-content': 'space-between', 'font-size': '0.625rem', color: 'oklch(1 0 0 / 0.4)', 'margin-bottom': '0.25rem' }}>
|
|
20
|
+
<span>{s.totalKeys} keys</span>
|
|
21
|
+
<span style={{ 'font-weight': '700' }}>{s.coverage}%</span>
|
|
22
|
+
</div>
|
|
23
|
+
<div style={{ height: '0.375rem', 'border-radius': '9999px', background: 'oklch(1 0 0 / 0.05)', overflow: 'hidden' }}>
|
|
24
|
+
<div style={{ height: '100%', 'border-radius': '9999px', background: barColor, width: `${s.coverage}%` }} />
|
|
25
|
+
</div>
|
|
26
|
+
<Show when={s.missingKeys > 0}>
|
|
27
|
+
<p style={{ 'margin-top': '0.375rem', 'font-size': '0.625rem', color: 'oklch(0.60 0.25 25)' }}>{s.missingKeys} missing</p>
|
|
28
|
+
</Show>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}}
|
|
32
|
+
</For>
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|