@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.
Files changed (101) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  6. package/.github/dependabot.yml +11 -0
  7. package/.github/workflows/ci.yml +23 -0
  8. package/.github/workflows/release.yml +29 -0
  9. package/.nvmrc +1 -0
  10. package/.project/ACCOUNT.yaml +4 -0
  11. package/.project/IDEAS.yaml +7 -0
  12. package/.project/PROJECT.yaml +11 -0
  13. package/.project/ROADMAP.yaml +15 -0
  14. package/CHANGELOG.md +8 -0
  15. package/CODE_OF_CONDUCT.md +16 -0
  16. package/CONTRIBUTING.md +26 -0
  17. package/LICENSE +2 -0
  18. package/README.md +1 -0
  19. package/SECURITY.md +15 -0
  20. package/SUPPORT.md +8 -0
  21. package/package.json +75 -0
  22. package/packages/convex/package.json +42 -0
  23. package/packages/convex/src/index.ts +3 -0
  24. package/packages/convex/src/mutations.ts +65 -0
  25. package/packages/convex/src/queries.ts +54 -0
  26. package/packages/convex/src/schema.ts +26 -0
  27. package/packages/convex/tsconfig.json +18 -0
  28. package/packages/convex/tsup.config.ts +17 -0
  29. package/packages/react/README.md +1 -0
  30. package/packages/react/package.json +51 -0
  31. package/packages/react/src/components/index.tsx +87 -0
  32. package/packages/react/src/hooks/index.ts +4 -0
  33. package/packages/react/src/hooks/useI18n.tsx +50 -0
  34. package/packages/react/src/hooks/useI18nAdmin.ts +12 -0
  35. package/packages/react/src/hooks/useLocaleDetect.ts +10 -0
  36. package/packages/react/src/hooks/useTranslations.ts +11 -0
  37. package/packages/react/src/index.tsx +8 -0
  38. package/packages/react/src/pages/I18nAdminPage.tsx +42 -0
  39. package/packages/react/src/pages/LocalePreviewPage.tsx +54 -0
  40. package/packages/react/src/pages/index.ts +2 -0
  41. package/packages/react/tsconfig.json +19 -0
  42. package/packages/react/tsup.config.ts +12 -0
  43. package/packages/react-css/README.md +1 -0
  44. package/packages/react-css/package.json +36 -0
  45. package/packages/react-css/src/components/index.tsx +66 -0
  46. package/packages/react-css/src/hooks/index.ts +4 -0
  47. package/packages/react-css/src/index.tsx +4 -0
  48. package/packages/react-css/src/pages/LocaleSettingsPage.tsx +74 -0
  49. package/packages/react-css/src/pages/TranslationsPage.tsx +98 -0
  50. package/packages/react-css/src/styles.css +210 -0
  51. package/packages/react-css/tsconfig.json +19 -0
  52. package/packages/react-css/tsup.config.ts +10 -0
  53. package/packages/shared/README.md +1 -0
  54. package/packages/shared/package.json +44 -0
  55. package/packages/shared/src/__tests__/i18n.test.ts +78 -0
  56. package/packages/shared/src/config.ts +344 -0
  57. package/packages/shared/src/index.ts +106 -0
  58. package/packages/shared/src/types.ts +51 -0
  59. package/packages/shared/tsconfig.json +18 -0
  60. package/packages/shared/tsup.config.ts +11 -0
  61. package/packages/shared/vitest.config.ts +4 -0
  62. package/packages/solidjs/README.md +1 -0
  63. package/packages/solidjs/package.json +47 -0
  64. package/packages/solidjs/src/components/LocaleCard.tsx +44 -0
  65. package/packages/solidjs/src/components/LocaleStatsCard.tsx +35 -0
  66. package/packages/solidjs/src/components/LocaleSwitcher.tsx +65 -0
  67. package/packages/solidjs/src/components/MissingKeyAlert.tsx +21 -0
  68. package/packages/solidjs/src/components/RTLWrapper.tsx +13 -0
  69. package/packages/solidjs/src/components/TranslationKeyRow.tsx +41 -0
  70. package/packages/solidjs/src/components/index.ts +6 -0
  71. package/packages/solidjs/src/index.tsx +8 -0
  72. package/packages/solidjs/src/pages/I18nAdminPage.tsx +188 -0
  73. package/packages/solidjs/src/pages/LocalePreviewPage.tsx +99 -0
  74. package/packages/solidjs/src/pages/index.ts +2 -0
  75. package/packages/solidjs/src/primitives/I18nProvider.tsx +56 -0
  76. package/packages/solidjs/src/primitives/createI18nAdmin.ts +7 -0
  77. package/packages/solidjs/src/primitives/createLocaleDetect.ts +8 -0
  78. package/packages/solidjs/src/primitives/createTranslations.ts +22 -0
  79. package/packages/solidjs/src/primitives/index.ts +4 -0
  80. package/packages/solidjs/tsconfig.json +20 -0
  81. package/packages/solidjs/tsup.config.ts +12 -0
  82. package/packages/solidjs-css/README.md +1 -0
  83. package/packages/solidjs-css/package.json +33 -0
  84. package/packages/solidjs-css/src/components/LocaleCard.tsx +45 -0
  85. package/packages/solidjs-css/src/components/LocaleStatsCard.tsx +43 -0
  86. package/packages/solidjs-css/src/components/LocaleSwitcher.tsx +51 -0
  87. package/packages/solidjs-css/src/components/MissingKeyAlert.tsx +24 -0
  88. package/packages/solidjs-css/src/components/RTLWrapper.tsx +16 -0
  89. package/packages/solidjs-css/src/components/TranslationKeyRow.tsx +47 -0
  90. package/packages/solidjs-css/src/components/index.ts +6 -0
  91. package/packages/solidjs-css/src/i18n.css +1322 -0
  92. package/packages/solidjs-css/src/index.tsx +3 -0
  93. package/packages/solidjs-css/src/pages/I18nAdminPage.tsx +134 -0
  94. package/packages/solidjs-css/src/pages/LocalePreviewPage.tsx +116 -0
  95. package/packages/solidjs-css/src/pages/index.ts +2 -0
  96. package/packages/solidjs-css/src/primitives/index.ts +1 -0
  97. package/packages/solidjs-css/tsconfig.json +20 -0
  98. package/packages/solidjs-css/tsup.config.bundled_dcjc4sct21j.mjs +18 -0
  99. package/packages/solidjs-css/tsup.config.ts +14 -0
  100. package/pnpm-workspace.yaml +2 -0
  101. package/tsconfig.json +23 -0
@@ -0,0 +1,65 @@
1
+ import { createSignal, Show, For } 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
+ locales: Locale[]
8
+ current: Locale
9
+ onChange: (l: Locale) => void
10
+ }
11
+
12
+ export const LocaleSwitcher: Component<Props> = (props) => {
13
+ const [open, setOpen] = createSignal(false)
14
+
15
+ return (
16
+ <div style={{ position: 'relative' }}>
17
+ <button
18
+ type="button"
19
+ onClick={() => setOpen(!open())}
20
+ style={{
21
+ display: 'flex', alignItems: 'center', gap: '0.5rem',
22
+ borderRadius: '0.75rem', border: '1px solid oklch(1 0 0 / 0.1)',
23
+ background: 'oklch(1 0 0 / 0.05)', padding: '0.5rem 0.75rem',
24
+ fontSize: '0.875rem', color: 'white', cursor: 'pointer',
25
+ }}
26
+ >
27
+ <span style={{ 'font-size': '1.125rem' }}>{LOCALE_INFO[props.current]?.flag}</span>
28
+ {LOCALE_INFO[props.current]?.nativeName}
29
+ <span style={{ color: 'oklch(1 0 0 / 0.3)', 'font-size': '0.75rem' }}>▼</span>
30
+ </button>
31
+
32
+ <Show when={open()}>
33
+ <div style={{
34
+ position: 'absolute', top: '100%', 'margin-top': '0.25rem', right: '0', 'z-index': '50',
35
+ 'min-width': '12.5rem', 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.1)',
36
+ background: '#111118', padding: '0.25rem 0', 'box-shadow': '0 10px 30px oklch(0 0 0 / 0.5)',
37
+ }}>
38
+ <For each={props.locales}>
39
+ {(l) => {
40
+ const info = LOCALE_INFO[l]
41
+ return (
42
+ <button
43
+ type="button"
44
+ onClick={() => { props.onChange(l); setOpen(false) }}
45
+ style={{
46
+ display: 'flex', width: '100%', 'align-items': 'center', gap: '0.75rem',
47
+ padding: '0.625rem 1rem', 'font-size': '0.875rem', border: 'none', cursor: 'pointer',
48
+ background: props.current === l ? 'oklch(0.55 0.25 265 / 0.15)' : 'transparent',
49
+ color: props.current === l ? 'oklch(0.72 0.18 265)' : 'oklch(1 0 0 / 0.7)',
50
+ }}
51
+ >
52
+ <span style={{ 'font-size': '1.125rem' }}>{info?.flag}</span>
53
+ <span>{info?.nativeName}</span>
54
+ <Show when={info?.direction === 'rtl'}>
55
+ <span style={{ 'font-size': '0.625rem', padding: '0.125rem 0.25rem', 'border-radius': '0.25rem', background: 'oklch(0.72 0.18 60 / 0.15)', color: 'oklch(0.72 0.18 60)' }}>RTL</span>
56
+ </Show>
57
+ </button>
58
+ )
59
+ }}
60
+ </For>
61
+ </div>
62
+ </Show>
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,21 @@
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
+ export const MissingKeyAlert: Component<{ count: number; locale: Locale }> = (props) => {
7
+ return (
8
+ <Show when={props.count > 0}>
9
+ <div style={{
10
+ display: 'flex', 'align-items': 'center', gap: '0.75rem',
11
+ 'border-radius': '0.5rem', border: '1px solid oklch(0.60 0.25 25 / 0.2)',
12
+ background: 'oklch(0.60 0.25 25 / 0.05)', padding: '0.5rem 1rem',
13
+ }}>
14
+ <span style={{ 'font-size': '0.875rem' }}>⚠️</span>
15
+ <span style={{ 'font-size': '0.75rem', color: 'oklch(0.70 0.20 25)' }}>
16
+ {props.count} missing key{props.count > 1 ? 's' : ''} for <strong>{LOCALE_INFO[props.locale]?.name}</strong>
17
+ </span>
18
+ </div>
19
+ </Show>
20
+ )
21
+ }
@@ -0,0 +1,13 @@
1
+ import type { ParentComponent } from 'solid-js'
2
+ import type { Locale } from '@geenius-i18n/shared'
3
+ import { LOCALE_INFO } from '@geenius-i18n/shared'
4
+
5
+ export const RTLWrapper: ParentComponent<{ locale?: Locale }> = (props) => {
6
+ const dir = () => props.locale ? LOCALE_INFO[props.locale]?.direction ?? 'ltr' : 'ltr'
7
+
8
+ return (
9
+ <div dir={dir()} style={{ 'text-align': dir() === 'rtl' ? 'right' : 'left' }}>
10
+ {props.children}
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,41 @@
1
+ import type { Component } from 'solid-js'
2
+ import type { Locale } from '@geenius-i18n/shared'
3
+ import { LOCALE_INFO } from '@geenius-i18n/shared'
4
+
5
+ interface Props {
6
+ translationKey: string
7
+ value: string
8
+ locale: Locale
9
+ namespace?: string
10
+ onEdit?: (key: string) => void
11
+ onDelete?: (key: string) => void
12
+ }
13
+
14
+ export const TranslationKeyRow: Component<Props> = (props) => {
15
+ return (
16
+ <tr style={{ 'border-bottom': '1px solid oklch(1 0 0 / 0.05)' }}>
17
+ <td style={{ padding: '0.75rem 1rem', 'font-family': 'monospace', 'font-size': '0.75rem', color: 'oklch(0.65 0.20 265)' }}>
18
+ {props.translationKey}
19
+ </td>
20
+ <td style={{ padding: '0.75rem 1rem', 'font-size': '0.6875rem', color: 'oklch(1 0 0 / 0.7)', 'max-width': '16rem', overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap' }}>
21
+ {props.value}
22
+ </td>
23
+ <td style={{ padding: '0.75rem 1rem' }}>
24
+ <span style={{ 'border-radius': '0.25rem', background: 'oklch(1 0 0 / 0.05)', padding: '0.125rem 0.375rem', 'font-size': '0.625rem', color: 'oklch(1 0 0 / 0.4)' }}>
25
+ {props.namespace ?? 'common'}
26
+ </span>
27
+ </td>
28
+ <td style={{ padding: '0.75rem 1rem', 'text-align': 'right' }}>
29
+ {props.onDelete && (
30
+ <button
31
+ type="button"
32
+ onClick={() => props.onDelete?.(props.translationKey)}
33
+ style={{ border: 'none', background: 'transparent', 'font-size': '0.75rem', color: 'oklch(1 0 0 / 0.2)', cursor: 'pointer' }}
34
+ >
35
+ Delete
36
+ </button>
37
+ )}
38
+ </td>
39
+ </tr>
40
+ )
41
+ }
@@ -0,0 +1,6 @@
1
+ export { LocaleSwitcher } from './LocaleSwitcher'
2
+ export { RTLWrapper } from './RTLWrapper'
3
+ export { LocaleStatsCard } from './LocaleStatsCard'
4
+ export { MissingKeyAlert } from './MissingKeyAlert'
5
+ export { TranslationKeyRow } from './TranslationKeyRow'
6
+ export { LocaleCard } from './LocaleCard'
@@ -0,0 +1,8 @@
1
+ export * from './primitives'
2
+ export * from './components'
3
+ export * from './pages'
4
+
5
+ export type {
6
+ Locale, Direction, I18nNamespace, TranslationDict, I18nConfig,
7
+ LocaleInfo, TranslationEntry, MissingKey, LocaleStat,
8
+ } from '@geenius-i18n/shared'
@@ -0,0 +1,188 @@
1
+ import { createSignal, createMemo, Show, For } from 'solid-js'
2
+ import type { Component } from 'solid-js'
3
+ import type { Locale, TranslationEntry, MissingKey, LocaleStat } from '@geenius-i18n/shared'
4
+ import { LOCALE_INFO, ALL_LOCALES } from '@geenius-i18n/shared'
5
+ import { LocaleStatsCard } from '../components/LocaleStatsCard'
6
+ import { MissingKeyAlert } from '../components/MissingKeyAlert'
7
+ import { TranslationKeyRow } from '../components/TranslationKeyRow'
8
+
9
+ interface Props {
10
+ translations?: TranslationEntry[]
11
+ missingKeys?: MissingKey[]
12
+ localeStats?: LocaleStat[]
13
+ onUpsert?: (locale: string, namespace: string, key: string, value: string) => void
14
+ onDelete?: (locale: string, key: string, namespace?: string) => void
15
+ }
16
+
17
+ const NAMESPACES = ['common', 'auth', 'dashboard', 'billing', 'errors']
18
+
19
+ export const I18nAdminPage: Component<Props> = (props) => {
20
+ const [selectedLocale, setSelectedLocale] = createSignal<Locale>('en')
21
+ const [selectedNs, setSelectedNs] = createSignal('common')
22
+ const [search, setSearch] = createSignal('')
23
+
24
+ // Editor state
25
+ const [editorKey, setEditorKey] = createSignal('')
26
+ const [editorValue, setEditorValue] = createSignal('')
27
+
28
+ const allTranslations = () => props.translations ?? []
29
+ const allLocaleStats = () => props.localeStats ?? []
30
+ const allMissingKeys = () => props.missingKeys ?? []
31
+
32
+ const filteredTranslations = createMemo(() => {
33
+ let t = allTranslations().filter(
34
+ (tr) => tr.locale === selectedLocale() && tr.namespace === selectedNs()
35
+ )
36
+ const q = search().toLowerCase()
37
+ if (q) t = t.filter((tr) => tr.key.toLowerCase().includes(q) || tr.value.toLowerCase().includes(q))
38
+ return t
39
+ })
40
+
41
+ const missingCount = createMemo(() =>
42
+ allMissingKeys().filter(m => m.locale === selectedLocale()).length
43
+ )
44
+
45
+ const isLoading = () => props.translations === undefined
46
+
47
+ // Skeleton
48
+ if (isLoading()) {
49
+ return (
50
+ <div style={{ 'min-height': '100vh', background: '#090a0f', padding: '3rem 1.5rem' }}>
51
+ <div style={{ 'max-width': '72rem', margin: '0 auto' }}>
52
+ <div style={{ height: '2.5rem', width: '12rem', 'border-radius': '0.5rem', background: 'oklch(1 0 0 / 0.05)', 'margin-bottom': '2rem' }} />
53
+ <div style={{ display: 'grid', 'grid-template-columns': 'repeat(4, 1fr)', gap: '0.75rem', 'margin-bottom': '1.5rem' }}>
54
+ <For each={[1,2,3,4,5,6,7,8]}>{() => <div style={{ height: '5rem', 'border-radius': '0.75rem', background: 'oklch(1 0 0 / 0.05)' }} />}</For>
55
+ </div>
56
+ <div style={{ height: '24rem', 'border-radius': '0.75rem', background: 'oklch(1 0 0 / 0.05)' }} />
57
+ </div>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ return (
63
+ <div style={{ 'min-height': '100vh', background: '#090a0f', color: 'white' }}>
64
+ <div style={{ 'max-width': '72rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
65
+ <h1 style={{ 'font-size': '1.5rem', 'font-weight': '700', 'letter-spacing': '-0.02em', 'margin-bottom': '2rem' }}>Translation Admin</h1>
66
+
67
+ {/* Locale Coverage */}
68
+ <Show when={allLocaleStats().length > 0}>
69
+ <div style={{ 'margin-bottom': '2rem' }}>
70
+ <h2 style={{ 'font-size': '0.6875rem', 'font-weight': '600', color: 'oklch(1 0 0 / 0.4)', 'text-transform': 'uppercase', 'margin-bottom': '0.75rem' }}>Locale Coverage</h2>
71
+ <LocaleStatsCard stats={allLocaleStats()} />
72
+ </div>
73
+ </Show>
74
+
75
+ {/* Missing Keys Alert */}
76
+ <Show when={missingCount() > 0}>
77
+ <div style={{ 'margin-bottom': '1.5rem' }}>
78
+ <MissingKeyAlert count={missingCount()} locale={selectedLocale()} />
79
+ </div>
80
+ </Show>
81
+
82
+ {/* Filters + Table + Editor */}
83
+ <div style={{ display: 'grid', gap: '1.5rem', 'grid-template-columns': '2fr 1fr' }}>
84
+ {/* Left: Filters + Table */}
85
+ <div>
86
+ <div style={{ display: 'flex', 'flex-wrap': 'wrap', 'align-items': 'center', gap: '0.75rem', 'margin-bottom': '1rem' }}>
87
+ <select
88
+ value={selectedLocale()}
89
+ onChange={(e) => setSelectedLocale(e.currentTarget.value as Locale)}
90
+ style={{ 'border-radius': '0.5rem', border: '1px solid oklch(1 0 0 / 0.1)', background: 'oklch(1 0 0 / 0.05)', padding: '0.5rem 0.75rem', 'font-size': '0.875rem', color: 'white' }}
91
+ >
92
+ <For each={ALL_LOCALES}>{(l) => <option value={l}>{LOCALE_INFO[l].flag} {LOCALE_INFO[l].name}</option>}</For>
93
+ </select>
94
+ <select
95
+ value={selectedNs()}
96
+ onChange={(e) => setSelectedNs(e.currentTarget.value)}
97
+ style={{ 'border-radius': '0.5rem', border: '1px solid oklch(1 0 0 / 0.1)', background: 'oklch(1 0 0 / 0.05)', padding: '0.5rem 0.75rem', 'font-size': '0.875rem', color: 'white' }}
98
+ >
99
+ <For each={NAMESPACES}>{(n) => <option value={n}>{n}</option>}</For>
100
+ </select>
101
+ <input
102
+ type="text"
103
+ placeholder="Search…"
104
+ value={search()}
105
+ onInput={(e) => setSearch(e.currentTarget.value)}
106
+ style={{ 'margin-left': 'auto', width: '12rem', 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.1)', background: 'oklch(1 0 0 / 0.05)', padding: '0.5rem 0.75rem', 'font-size': '0.875rem', color: 'white' }}
107
+ />
108
+ </div>
109
+
110
+ {/* Table */}
111
+ <Show
112
+ when={filteredTranslations().length > 0}
113
+ fallback={
114
+ <div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', padding: '3rem 1rem' }}>
115
+ <div style={{ 'font-size': '2.5rem', opacity: '0.2', 'margin-bottom': '0.75rem' }}>🌐</div>
116
+ <p style={{ 'font-size': '0.875rem', color: 'oklch(1 0 0 / 0.4)' }}>No translations for this locale</p>
117
+ </div>
118
+ }
119
+ >
120
+ <div style={{ overflow: 'auto', 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)' }}>
121
+ <table style={{ width: '100%', 'font-size': '0.875rem', 'border-collapse': 'collapse' }}>
122
+ <thead>
123
+ <tr style={{ 'border-bottom': '1px solid oklch(1 0 0 / 0.08)', background: 'oklch(1 0 0 / 0.02)' }}>
124
+ <th style={{ padding: '0.75rem 1rem', 'text-align': 'left', 'font-size': '0.625rem', 'font-weight': '500', color: 'oklch(1 0 0 / 0.4)', 'text-transform': 'uppercase' }}>Key</th>
125
+ <th style={{ padding: '0.75rem 1rem', 'text-align': 'left', 'font-size': '0.625rem', 'font-weight': '500', color: 'oklch(1 0 0 / 0.4)', 'text-transform': 'uppercase' }}>Value</th>
126
+ <th style={{ padding: '0.75rem 1rem', 'text-align': 'left', 'font-size': '0.625rem', 'font-weight': '500', color: 'oklch(1 0 0 / 0.4)', 'text-transform': 'uppercase' }}>Namespace</th>
127
+ <th style={{ padding: '0.75rem 1rem', 'text-align': 'right', 'font-size': '0.625rem', 'font-weight': '500', color: 'oklch(1 0 0 / 0.4)', 'text-transform': 'uppercase' }}>Actions</th>
128
+ </tr>
129
+ </thead>
130
+ <tbody>
131
+ <For each={filteredTranslations()}>
132
+ {(tr) => (
133
+ <TranslationKeyRow
134
+ translationKey={tr.key}
135
+ value={tr.value}
136
+ locale={selectedLocale()}
137
+ namespace={tr.namespace}
138
+ onDelete={(key) => props.onDelete?.(selectedLocale(), key, selectedNs())}
139
+ />
140
+ )}
141
+ </For>
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ </Show>
146
+ </div>
147
+
148
+ {/* Right: Editor */}
149
+ <div style={{ 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)', background: 'oklch(1 0 0 / 0.02)', padding: '1.25rem' }}>
150
+ <h3 style={{ 'font-size': '0.6875rem', 'font-weight': '600', color: 'oklch(1 0 0 / 0.6)', 'margin-bottom': '0.75rem' }}>Add/Edit Translation</h3>
151
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '0.5rem' }}>
152
+ <input
153
+ type="text"
154
+ placeholder="Translation key (e.g. auth.login_button)"
155
+ value={editorKey()}
156
+ onInput={(e) => setEditorKey(e.currentTarget.value)}
157
+ style={{ 'border-radius': '0.5rem', border: '1px solid oklch(1 0 0 / 0.1)', background: 'oklch(1 0 0 / 0.05)', padding: '0.625rem 1rem', 'font-size': '0.875rem', color: 'white' }}
158
+ />
159
+ <textarea
160
+ placeholder="Translation value…"
161
+ value={editorValue()}
162
+ onInput={(e) => setEditorValue(e.currentTarget.value)}
163
+ rows={3}
164
+ style={{ 'border-radius': '0.5rem', border: '1px solid oklch(1 0 0 / 0.1)', background: 'oklch(1 0 0 / 0.05)', padding: '0.625rem 1rem', 'font-size': '0.875rem', color: 'white', resize: 'none' }}
165
+ />
166
+ <button
167
+ type="button"
168
+ disabled={!editorKey() || !editorValue()}
169
+ onClick={() => {
170
+ props.onUpsert?.(selectedLocale(), selectedNs(), editorKey(), editorValue())
171
+ setEditorKey(''); setEditorValue('')
172
+ }}
173
+ style={{
174
+ 'border-radius': '0.5rem', border: 'none',
175
+ background: editorKey() && editorValue() ? 'oklch(0.55 0.25 265)' : 'oklch(1 0 0 / 0.1)',
176
+ padding: '0.5rem 1rem', 'font-size': '0.75rem', 'font-weight': '500',
177
+ color: editorKey() && editorValue() ? 'white' : 'oklch(1 0 0 / 0.3)', cursor: 'pointer',
178
+ }}
179
+ >
180
+ Save Translation
181
+ </button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ )
188
+ }
@@ -0,0 +1,99 @@
1
+ import { Show } from 'solid-js'
2
+ import type { Component } from 'solid-js'
3
+ import type { I18nConfig, TranslationDict } from '@geenius-i18n/shared'
4
+ import { LOCALE_INFO, ALL_LOCALES } from '@geenius-i18n/shared'
5
+ import { I18nProvider, createI18n } from '../primitives/I18nProvider'
6
+ import { LocaleSwitcher } from '../components/LocaleSwitcher'
7
+ import { RTLWrapper } from '../components/RTLWrapper'
8
+
9
+ function InfoCard(props: { label: string; value: string; highlight?: boolean }) {
10
+ return (
11
+ <div style={{ 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)', background: 'oklch(1 0 0 / 0.02)', padding: '1rem' }}>
12
+ <p style={{ 'font-size': '0.625rem', color: 'oklch(1 0 0 / 0.4)', 'text-transform': 'uppercase', 'margin-bottom': '0.25rem' }}>{props.label}</p>
13
+ <p style={{ 'font-size': '1.125rem', 'font-weight': '700', color: props.highlight ? 'oklch(0.72 0.18 60)' : 'oklch(1 0 0 / 0.9)' }}>{props.value}</p>
14
+ </div>
15
+ )
16
+ }
17
+
18
+ function FormatRow(props: { label: string; value: string }) {
19
+ return (
20
+ <div style={{ display: 'flex', 'justify-content': 'space-between' }}>
21
+ <span style={{ color: 'oklch(1 0 0 / 0.4)' }}>{props.label}</span>
22
+ <span style={{ color: 'oklch(1 0 0 / 0.8)', 'font-family': 'monospace', 'font-size': '0.75rem' }}>{props.value}</span>
23
+ </div>
24
+ )
25
+ }
26
+
27
+ function PreviewContent() {
28
+ const { locale, t, direction, isRTL, setLocale, formatDate, formatNumber, formatCurrency } = createI18n()
29
+ const info = () => LOCALE_INFO[locale()]
30
+
31
+ return (
32
+ <div style={{ 'min-height': '100vh', background: '#090a0f', color: 'white' }}>
33
+ <div style={{ 'max-width': '48rem', margin: '0 auto', padding: '3rem 1.5rem' }}>
34
+ {/* Header */}
35
+ <div style={{ display: 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'margin-bottom': '2rem' }}>
36
+ <h1 style={{ 'font-size': '1.5rem', 'font-weight': '700', 'letter-spacing': '-0.02em' }}>Locale Preview</h1>
37
+ <LocaleSwitcher locales={ALL_LOCALES} current={locale()} onChange={setLocale} />
38
+ </div>
39
+
40
+ {/* Info cards */}
41
+ <div style={{ display: 'grid', 'grid-template-columns': 'repeat(4, 1fr)', gap: '0.75rem', 'margin-bottom': '1.5rem' }}>
42
+ <InfoCard label="Locale" value={locale().toUpperCase()} />
43
+ <InfoCard label="Direction" value={direction()} highlight={isRTL()} />
44
+ <InfoCard label="Native" value={info()?.nativeName ?? '?'} />
45
+ <InfoCard label="Flag" value={info()?.flag ?? '?'} />
46
+ </div>
47
+
48
+ {/* RTL-aware content */}
49
+ <RTLWrapper locale={locale()}>
50
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '1.5rem' }}>
51
+ {/* Formatting */}
52
+ <div style={{ 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)', background: 'oklch(1 0 0 / 0.02)', padding: '1.25rem' }}>
53
+ <h3 style={{ 'font-size': '0.875rem', 'font-weight': '600', color: 'oklch(1 0 0 / 0.8)', 'margin-bottom': '1rem' }}>Formatting Examples</h3>
54
+ <div style={{ display: 'flex', 'flex-direction': 'column', gap: '0.75rem', 'font-size': '0.875rem' }}>
55
+ <FormatRow label="Date" value={formatDate(new Date())} />
56
+ <FormatRow label="Date (full)" value={formatDate(new Date(), { dateStyle: 'full' })} />
57
+ <FormatRow label="Number" value={formatNumber(1234567.89)} />
58
+ <FormatRow label="Currency (USD)" value={formatCurrency(9999.99, 'USD')} />
59
+ <FormatRow label="Currency (EUR)" value={formatCurrency(4250.50, 'EUR')} />
60
+ <FormatRow label="Percentage" value={formatNumber(0.85, { style: 'percent' })} />
61
+ </div>
62
+ </div>
63
+
64
+ {/* RTL notice */}
65
+ <Show when={isRTL()}>
66
+ <div style={{ 'border-radius': '0.5rem', border: '1px solid oklch(0.72 0.18 60 / 0.2)', background: 'oklch(0.72 0.18 60 / 0.05)', padding: '0.75rem 1rem', 'font-size': '0.875rem', color: 'oklch(0.72 0.18 60)' }}>
67
+ ⚡ RTL mode active — text flows right-to-left
68
+ </div>
69
+ </Show>
70
+
71
+ {/* Sample text */}
72
+ <div style={{ 'border-radius': '0.75rem', border: '1px solid oklch(1 0 0 / 0.08)', background: 'oklch(1 0 0 / 0.02)', padding: '1.25rem' }}>
73
+ <h3 style={{ 'font-size': '0.875rem', 'font-weight': '600', color: 'oklch(1 0 0 / 0.8)', 'margin-bottom': '0.75rem' }}>Sample Text</h3>
74
+ <p style={{ 'font-size': '0.875rem', color: 'oklch(1 0 0 / 0.6)', 'line-height': '1.625' }}>
75
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
76
+ </p>
77
+ </div>
78
+ </div>
79
+ </RTLWrapper>
80
+ </div>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ export const LocalePreviewPage: Component<{ config?: Partial<I18nConfig>; translations?: TranslationDict }> = (props) => {
86
+ const fullConfig: I18nConfig = {
87
+ defaultLocale: 'en',
88
+ supportedLocales: ALL_LOCALES,
89
+ detectBrowser: true,
90
+ persistLocale: true,
91
+ ...props.config,
92
+ }
93
+
94
+ return (
95
+ <I18nProvider config={fullConfig} translations={props.translations}>
96
+ <PreviewContent />
97
+ </I18nProvider>
98
+ )
99
+ }
@@ -0,0 +1,2 @@
1
+ export { I18nAdminPage } from './I18nAdminPage'
2
+ export { LocalePreviewPage } from './LocalePreviewPage'
@@ -0,0 +1,56 @@
1
+ import { createSignal, createContext, useContext, type ParentComponent } from 'solid-js'
2
+ import type { Locale, Direction, TranslationDict, I18nConfig } from '@geenius-i18n/shared'
3
+ import {
4
+ t as tFn,
5
+ getDirection,
6
+ isRTL as checkRTL,
7
+ detectLocale,
8
+ formatDate as fmtDate,
9
+ formatNumber as fmtNum,
10
+ formatCurrency as fmtCur,
11
+ LOCALE_INFO,
12
+ } from '@geenius-i18n/shared'
13
+
14
+ interface I18nCtx {
15
+ locale: () => Locale
16
+ direction: () => Direction
17
+ isRTL: () => boolean
18
+ setLocale: (l: Locale) => void
19
+ t: (key: string, params?: Record<string, string | number>) => string
20
+ formatDate: (d: Date | string, o?: Intl.DateTimeFormatOptions) => string
21
+ formatNumber: (n: number, o?: Intl.NumberFormatOptions) => string
22
+ formatCurrency: (a: number, c: string) => string
23
+ }
24
+
25
+ const I18nContext = createContext<I18nCtx>()
26
+
27
+ export const I18nProvider: ParentComponent<{ config: I18nConfig; translations?: TranslationDict }> = (props) => {
28
+ const [locale, setLocaleSignal] = createSignal<Locale>(
29
+ props.config.detectBrowser ? detectLocale(props.config.supportedLocales) : props.config.defaultLocale
30
+ )
31
+
32
+ const value: I18nCtx = {
33
+ locale,
34
+ direction: () => getDirection(locale()),
35
+ isRTL: () => checkRTL(locale()),
36
+ setLocale: (l) => {
37
+ setLocaleSignal(l)
38
+ if (typeof document !== 'undefined') {
39
+ document.documentElement.dir = getDirection(l)
40
+ document.documentElement.lang = l
41
+ }
42
+ },
43
+ t: (key, params) => tFn(key, props.translations ?? {}, params),
44
+ formatDate: (d, o) => fmtDate(d, locale(), o),
45
+ formatNumber: (n, o) => fmtNum(n, locale(), o),
46
+ formatCurrency: (a, c) => fmtCur(a, c, locale()),
47
+ }
48
+
49
+ return <I18nContext.Provider value={value}>{props.children}</I18nContext.Provider>
50
+ }
51
+
52
+ export function createI18n() {
53
+ const ctx = useContext(I18nContext)
54
+ if (!ctx) throw new Error('createI18n must be used inside I18nProvider')
55
+ return ctx
56
+ }
@@ -0,0 +1,7 @@
1
+ export function createI18nAdmin(mutations: {
2
+ upsertTranslation: (locale: string, key: string, value: string, namespace?: string) => Promise<void>
3
+ deleteTranslation: (locale: string, key: string, namespace?: string) => Promise<void>
4
+ importTranslations: (locale: string, translations: Record<string, string>, namespace?: string) => Promise<void>
5
+ }) {
6
+ return mutations
7
+ }
@@ -0,0 +1,8 @@
1
+ import { createSignal } from 'solid-js'
2
+ import type { Locale } from '@geenius-i18n/shared'
3
+ import { detectLocale } from '@geenius-i18n/shared'
4
+
5
+ export function createLocaleDetect(supportedLocales: Locale[]) {
6
+ const [detectedLocale] = createSignal(detectLocale(supportedLocales))
7
+ return { detectedLocale }
8
+ }
@@ -0,0 +1,22 @@
1
+ import { createSignal, createEffect, type Accessor } from 'solid-js'
2
+ import type { Locale, TranslationDict } from '@geenius-i18n/shared'
3
+ import { t as tFn, loadNamespace } from '@geenius-i18n/shared'
4
+
5
+ export function createTranslations(locale: Accessor<Locale>, namespace: string) {
6
+ const [dict, setDict] = createSignal<TranslationDict>({})
7
+ const [isLoading, setLoading] = createSignal(true)
8
+ const [error, setError] = createSignal<string | undefined>(undefined)
9
+
10
+ createEffect(() => {
11
+ const loc = locale()
12
+ setLoading(true)
13
+ setError(undefined)
14
+ loadNamespace(loc, namespace)
15
+ .then((d) => { setDict(d); setLoading(false) })
16
+ .catch((e: Error) => { setError(e.message); setLoading(false) })
17
+ })
18
+
19
+ const t = (key: string, params?: Record<string, string | number>) => tFn(key, dict(), params)
20
+
21
+ return { dict, isLoading, error, t }
22
+ }
@@ -0,0 +1,4 @@
1
+ export { I18nProvider, createI18n } from './I18nProvider'
2
+ export { createLocaleDetect } from './createLocaleDetect'
3
+ export { createTranslations } from './createTranslations'
4
+ export { createI18nAdmin } from './createI18nAdmin'
@@ -0,0 +1,20 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "solid-js",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "target": "ES2022",
14
+ "module": "ESNext",
15
+ "moduleResolution": "bundler"
16
+ },
17
+ "include": [
18
+ "src"
19
+ ]
20
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: { index: 'src/index.tsx' },
5
+ outDir: 'dist',
6
+ format: ['esm'],
7
+ dts: true,
8
+ sourcemap: true,
9
+ clean: true,
10
+ treeshake: true,
11
+ external: ['solid-js'],
12
+ })
@@ -0,0 +1 @@
1
+ # ✦ @geenius-i18n/solidjs-css\n\n> Geenius i18n — SolidJS primitives (vanilla CSS variant)\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-css\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-i18n/solidjs-css';\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,33 @@
1
+ {
2
+ "name": "@geenius-i18n/solidjs-css",
3
+ "version": "0.1.0",
4
+ "description": "Geenius i18n — SolidJS primitives (vanilla CSS variant)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "restricted"
9
+ },
10
+ "main": "./src/index.tsx",
11
+ "types": "./src/index.tsx",
12
+ "exports": {
13
+ ".": "./src/index.tsx"
14
+ },
15
+ "scripts": {
16
+ "type-check": "tsc --noEmit"
17
+ },
18
+ "peerDependencies": {
19
+ "solid-js": ">=1.0.0"
20
+ },
21
+ "dependencies": {
22
+ "@geenius-i18n/shared": "workspace:*",
23
+ "@geenius-i18n/solidjs": "workspace:*"
24
+ },
25
+ "devDependencies": {
26
+ "solid-js": "^1.9.4",
27
+ "typescript": "~6.0.2"
28
+ },
29
+ "author": "Antigravity HQ",
30
+ "engines": {
31
+ "node": ">=20.0.0"
32
+ }
33
+ }