@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,210 @@
1
+ /* ─── I18n Design Tokens (OKLCH) ──────────────────────── */
2
+ :root {
3
+ --i18n-bg: oklch(0.12 0.01 250);
4
+ --i18n-surface: oklch(0.16 0.01 250);
5
+ --i18n-border: oklch(0.22 0.01 250);
6
+ --i18n-text: oklch(0.95 0.01 250);
7
+ --i18n-text-muted: oklch(0.58 0.02 250);
8
+ --i18n-accent: oklch(0.65 0.20 265);
9
+ --i18n-rtl-indicator: oklch(0.72 0.18 60);
10
+ --i18n-missing: oklch(0.60 0.25 25);
11
+ --i18n-complete: oklch(0.72 0.18 155);
12
+ --i18n-radius: 0.75rem;
13
+ }
14
+
15
+ /* ─── Locale Switcher ────────────────────────────────── */
16
+ .i18n__locale-switcher { position: relative; }
17
+ .i18n__locale-switcher-btn {
18
+ display: flex; align-items: center; gap: 0.5rem;
19
+ padding: 0.5rem 0.75rem; border-radius: var(--i18n-radius);
20
+ border: 1px solid var(--i18n-border); background: oklch(1 0 0 / 0.05);
21
+ font-size: 0.875rem; color: var(--i18n-text); cursor: pointer; transition: all 0.15s;
22
+ }
23
+ .i18n__locale-switcher-btn:hover { background: oklch(1 0 0 / 0.1); }
24
+ .i18n__locale-switcher-flag { font-size: 1.125rem; }
25
+ .i18n__locale-switcher-arrow { font-size: 0.625rem; color: oklch(1 0 0 / 0.3); margin-left: 0.25rem; }
26
+ .i18n__locale-dropdown {
27
+ position: absolute; top: 100%; margin-top: 0.25rem; right: 0; z-index: 50;
28
+ min-width: 14rem; border-radius: var(--i18n-radius); border: 1px solid var(--i18n-border);
29
+ background: oklch(0.08 0.01 250); padding: 0.25rem; box-shadow: 0 8px 32px oklch(0 0 0 / 0.5);
30
+ }
31
+
32
+ /* ─── Locale Option ──────────────────────────────────── */
33
+ .i18n__locale-option {
34
+ display: flex; align-items: center; gap: 0.75rem; width: 100%;
35
+ padding: 0.625rem 1rem; border: none; border-radius: calc(var(--i18n-radius) * 0.8);
36
+ font-size: 0.875rem; color: oklch(1 0 0 / 0.7); cursor: pointer; background: transparent; transition: all 0.1s;
37
+ }
38
+ .i18n__locale-option:hover { background: oklch(1 0 0 / 0.05); }
39
+ .i18n__locale-option--active { background: oklch(0.65 0.20 265 / 0.15); color: oklch(0.65 0.20 265); }
40
+ .i18n__locale-option-flag { font-size: 1.125rem; }
41
+ .i18n__locale-option-name { flex: 1; text-align: left; }
42
+ .i18n__locale-option-eng { font-size: 0.6875rem; color: oklch(1 0 0 / 0.3); }
43
+ .i18n__locale-option-rtl { font-size: 0.5625rem; padding: 0.125rem 0.375rem; border-radius: 0.25rem; background: oklch(0.72 0.18 60 / 0.15); color: var(--i18n-rtl-indicator); }
44
+
45
+ /* ─── RTL Wrapper ────────────────────────────────────── */
46
+ .i18n__rtl-wrapper { transition: all 0.2s; }
47
+ .i18n__rtl-wrapper[dir="rtl"] { text-align: right; }
48
+
49
+ /* ─── Translation Table ──────────────────────────────── */
50
+ .i18n__translation-table { width: 100%; font-size: 0.875rem; border-collapse: collapse; }
51
+ .i18n__translation-table th { padding: 0.75rem 1rem; text-align: left; font-size: 0.625rem; font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; color: oklch(1 0 0 / 0.4); border-bottom: 1px solid oklch(1 0 0 / 0.08); }
52
+ .i18n__translation-table td { padding: 0.75rem 1rem; border-bottom: 1px solid oklch(1 0 0 / 0.05); }
53
+ .i18n__translation-row:hover { background: oklch(1 0 0 / 0.02); }
54
+ .i18n__translation-key { font-family: monospace; font-size: 0.6875rem; color: oklch(0.65 0.20 265); }
55
+ .i18n__translation-value { color: oklch(1 0 0 / 0.7); max-width: 20rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
56
+ .i18n__translation-ns { display: inline-block; padding: 0.125rem 0.375rem; border-radius: 0.25rem; background: oklch(1 0 0 / 0.05); font-size: 0.625rem; color: oklch(1 0 0 / 0.4); }
57
+
58
+ /* ─── Translation Editor ─────────────────────────────── */
59
+ .i18n__editor { display: flex; flex-direction: column; gap: 0.75rem; padding: 1.25rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); }
60
+ .i18n__editor-selectors { display: flex; gap: 0.5rem; }
61
+ .i18n__editor-select { padding: 0.375rem 0.5rem; border: 1px solid var(--i18n-border); border-radius: calc(var(--i18n-radius) * 0.8); background: oklch(1 0 0 / 0.05); font-size: 0.6875rem; color: var(--i18n-text); outline: none; }
62
+ .i18n__editor-input { width: 100%; padding: 0.625rem 1rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.03); font-size: 0.875rem; color: var(--i18n-text); outline: none; }
63
+ .i18n__editor-input::placeholder { color: oklch(1 0 0 / 0.3); }
64
+ .i18n__editor-input:focus { border-color: oklch(0.65 0.20 265 / 0.4); }
65
+ .i18n__editor-textarea { resize: none; min-height: 4.5rem; }
66
+
67
+ /* ─── Missing Badge ──────────────────────────────────── */
68
+ .i18n__missing-badge {
69
+ display: flex; align-items: center; gap: 0.75rem;
70
+ padding: 0.625rem 1rem; border-radius: calc(var(--i18n-radius) * 0.8);
71
+ border: 1px solid oklch(0.60 0.25 25 / 0.2); background: oklch(0.60 0.25 25 / 0.05);
72
+ }
73
+ .i18n__missing-badge-icon { font-size: 0.875rem; }
74
+ .i18n__missing-badge-text { font-size: 0.6875rem; color: oklch(0.60 0.25 25); }
75
+
76
+ /* ─── Stats Card ─────────────────────────────────────── */
77
+ .i18n__stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 0.75rem; }
78
+ .i18n__stats-card { padding: 1rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); }
79
+ .i18n__stats-card-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
80
+ .i18n__stats-card-flag { font-size: 1.125rem; }
81
+ .i18n__stats-card-name { font-size: 0.6875rem; font-weight: 500; color: oklch(1 0 0 / 0.8); }
82
+ .i18n__stats-bar { height: 0.375rem; border-radius: 9999px; background: oklch(1 0 0 / 0.05); overflow: hidden; margin-top: 0.25rem; }
83
+ .i18n__stats-bar-fill { height: 100%; border-radius: 9999px; transition: width 0.3s; }
84
+ .i18n__stats-bar-fill--complete { background: var(--i18n-complete); }
85
+ .i18n__stats-bar-fill--partial { background: var(--i18n-rtl-indicator); }
86
+ .i18n__stats-bar-fill--low { background: var(--i18n-missing); }
87
+ .i18n__stats-card-meta { display: flex; justify-content: space-between; margin-top: 0.375rem; font-size: 0.625rem; color: oklch(1 0 0 / 0.4); }
88
+ .i18n__stats-card-missing { font-size: 0.625rem; color: oklch(0.60 0.25 25); margin-top: 0.375rem; }
89
+
90
+ /* ─── Info Cards (Preview) ───────────────────────────── */
91
+ .i18n__info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
92
+ .i18n__info-card { padding: 1rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); }
93
+ .i18n__info-card-label { font-size: 0.625rem; color: oklch(1 0 0 / 0.4); text-transform: uppercase; margin-bottom: 0.25rem; }
94
+ .i18n__info-card-value { font-size: 1.125rem; font-weight: 700; color: oklch(1 0 0 / 0.9); }
95
+ .i18n__info-card-value--rtl { color: var(--i18n-rtl-indicator); }
96
+
97
+ /* ─── RTL Alert ──────────────────────────────────────── */
98
+ .i18n__rtl-alert { padding: 0.75rem 1rem; border-radius: calc(var(--i18n-radius) * 0.8); border: 1px solid oklch(0.72 0.18 60 / 0.2); background: oklch(0.72 0.18 60 / 0.05); font-size: 0.875rem; color: oklch(0.72 0.18 60); }
99
+
100
+ /* ─── Format Row ─────────────────────────────────────── */
101
+ .i18n__format-row { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid oklch(1 0 0 / 0.04); }
102
+ .i18n__format-label { font-size: 0.875rem; color: oklch(1 0 0 / 0.4); }
103
+ .i18n__format-value { font-family: monospace; font-size: 0.6875rem; color: oklch(1 0 0 / 0.8); }
104
+
105
+ /* ─── Section ────────────────────────────────────────── */
106
+ .i18n__section { padding: 1.25rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); }
107
+ .i18n__section-title { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.8); margin-bottom: 1rem; }
108
+
109
+ /* ─── Buttons ────────────────────────────────────────── */
110
+ .i18n__btn { padding: 0.5rem 1rem; border-radius: calc(var(--i18n-radius) * 0.8); font-size: 0.6875rem; font-weight: 500; cursor: pointer; border: none; transition: all 0.15s; }
111
+ .i18n__btn--primary { background: var(--i18n-accent); color: white; }
112
+ .i18n__btn--primary:hover { background: oklch(0.70 0.22 265); }
113
+ .i18n__btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
114
+ .i18n__btn--outline { border: 1px solid var(--i18n-border); background: transparent; color: oklch(1 0 0 / 0.6); }
115
+ .i18n__btn--danger { color: oklch(0.60 0.25 25); }
116
+
117
+ /* ─── Search ─────────────────────────────────────────── */
118
+ .i18n__search { width: 100%; max-width: 12rem; padding: 0.5rem 0.75rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.03); font-size: 0.875rem; color: var(--i18n-text); outline: none; }
119
+ .i18n__search::placeholder { color: oklch(1 0 0 / 0.3); }
120
+ .i18n__search:focus { border-color: oklch(0.65 0.20 265 / 0.4); }
121
+
122
+ /* ─── Skeleton ───────────────────────────────────────── */
123
+ .i18n__skeleton { background: oklch(1 0 0 / 0.05); border-radius: var(--i18n-radius); animation: i18n-pulse 1.5s ease-in-out infinite; }
124
+ @keyframes i18n-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
125
+
126
+ /* ─── Empty ──────────────────────────────────────────── */
127
+ .i18n__empty { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 3rem 1rem; text-align: center; }
128
+ .i18n__empty-icon { font-size: 2.5rem; opacity: 0.2; margin-bottom: 0.75rem; }
129
+ .i18n__empty-text { font-size: 0.875rem; color: oklch(1 0 0 / 0.4); }
130
+
131
+ /* ─── Delete Button ──────────────────────────────────── */
132
+ .i18n__delete-btn { padding: 0.25rem 0.5rem; border: none; border-radius: calc(var(--i18n-radius) * 0.6); background: transparent; color: oklch(1 0 0 / 0.2); font-size: 0.6875rem; cursor: pointer; opacity: 0; transition: all 0.15s; }
133
+ .i18n__translation-row:hover .i18n__delete-btn { opacity: 1; }
134
+ .i18n__delete-btn:hover { color: oklch(0.60 0.25 25); background: oklch(0.60 0.25 25 / 0.1); }
135
+
136
+ /* ─── Namespace Tree View ────────────────────────────── */
137
+ .i18n__namespace-tree { border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); padding: 1rem; max-height: 20rem; overflow-y: auto; }
138
+ .i18n__namespace-item { display: flex; align-items: center; padding: 0.5rem; cursor: pointer; border-radius: calc(var(--i18n-radius) * 0.6); transition: all 0.1s; margin-bottom: 0.25rem; }
139
+ .i18n__namespace-item:hover { background: oklch(1 0 0 / 0.04); }
140
+ .i18n__namespace-item--active { background: oklch(0.65 0.20 265 / 0.1); color: oklch(0.65 0.20 265); }
141
+ .i18n__namespace-icon { width: 1.25rem; height: 1.25rem; display: flex; align-items: center; justify-content: center; margin-right: 0.5rem; font-size: 0.875rem; }
142
+ .i18n__namespace-name { flex: 1; font-size: 0.6875rem; font-weight: 500; }
143
+ .i18n__namespace-count { font-size: 0.625rem; color: oklch(1 0 0 / 0.3); }
144
+
145
+ /* ─── Missing Key Alerts ─────────────────────────────── */
146
+ .i18n__missing-keys-panel { border: 1px solid oklch(0.60 0.25 25 / 0.2); border-radius: var(--i18n-radius); background: oklch(0.60 0.25 25 / 0.05); padding: 1rem; margin-bottom: 1.5rem; }
147
+ .i18n__missing-keys-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; }
148
+ .i18n__missing-keys-title { font-size: 0.875rem; font-weight: 600; color: oklch(0.60 0.25 25); }
149
+ .i18n__missing-keys-count { font-size: 1rem; font-weight: 700; color: oklch(0.60 0.25 25); }
150
+ .i18n__missing-keys-list { display: flex; flex-direction: column; gap: 0.5rem; }
151
+ .i18n__missing-key-item { padding: 0.625rem; border: 1px solid oklch(0.60 0.25 25 / 0.1); border-radius: calc(var(--i18n-radius) * 0.6); background: oklch(1 0 0 / 0.02); }
152
+ .i18n__missing-key-path { font-family: monospace; font-size: 0.625rem; color: oklch(0.60 0.25 25); margin-bottom: 0.25rem; }
153
+ .i18n__missing-key-context { font-size: 0.6875rem; color: oklch(1 0 0 / 0.5); }
154
+
155
+ /* ─── Import/Export Panel ────────────────────────────── */
156
+ .i18n__import-export-panel { border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); padding: 1.25rem; }
157
+ .i18n__import-export-title { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.8); margin-bottom: 1rem; }
158
+ .i18n__import-export-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
159
+ .i18n__import-export-col { display: flex; flex-direction: column; }
160
+ .i18n__import-export-label { font-size: 0.6875rem; font-weight: 500; color: oklch(1 0 0 / 0.6); margin-bottom: 0.5rem; text-transform: uppercase; }
161
+ .i18n__import-btn { padding: 0.75rem 1rem; border: 1px solid var(--i18n-border); border-radius: calc(var(--i18n-radius) * 0.8); background: oklch(1 0 0 / 0.03); font-size: 0.6875rem; font-weight: 500; cursor: pointer; transition: all 0.1s; }
162
+ .i18n__import-btn:hover { background: oklch(1 0 0 / 0.06); }
163
+ .i18n__export-btn { padding: 0.75rem 1rem; border: none; border-radius: calc(var(--i18n-radius) * 0.8); background: var(--i18n-accent); color: white; font-size: 0.6875rem; font-weight: 500; cursor: pointer; transition: all 0.1s; }
164
+ .i18n__export-btn:hover { background: oklch(0.70 0.22 265); }
165
+
166
+ /* ─── RTL Support Toggle ─────────────────────────────── */
167
+ .i18n__rtl-toggle-panel { border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); padding: 1rem; margin-bottom: 1.5rem; }
168
+ .i18n__rtl-toggle-header { display: flex; align-items: center; justify-content: space-between; }
169
+ .i18n__rtl-toggle-label { font-size: 0.875rem; font-weight: 600; color: oklch(1 0 0 / 0.8); }
170
+ .i18n__rtl-toggle-switch { width: 2.5rem; height: 1.5rem; border-radius: 9999px; border: none; background: oklch(1 0 0 / 0.1); cursor: pointer; transition: all 0.2s; position: relative; }
171
+ .i18n__rtl-toggle-switch--active { background: var(--i18n-complete); }
172
+ .i18n__rtl-toggle-knob { position: absolute; width: 1.25rem; height: 1.25rem; border-radius: 50%; background: white; top: 0.125rem; left: 0.125rem; transition: transform 0.2s; box-shadow: 0 1px 3px oklch(0 0 0 / 0.2); }
173
+ .i18n__rtl-toggle-switch--active .i18n__rtl-toggle-knob { transform: translateX(1rem); }
174
+
175
+ /* ─── Coverage Indicator ─────────────────────────────── */
176
+ .i18n__coverage-card { padding: 1rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); text-align: center; }
177
+ .i18n__coverage-percentage { font-size: 2.5rem; font-weight: 900; font-variant-numeric: tabular-nums; color: oklch(1 0 0 / 0.9); }
178
+ .i18n__coverage-label { font-size: 0.6875rem; color: oklch(1 0 0 / 0.4); margin-top: 0.5rem; }
179
+ .i18n__coverage-bar { height: 0.375rem; border-radius: 9999px; background: oklch(1 0 0 / 0.05); overflow: hidden; margin-top: 0.75rem; }
180
+ .i18n__coverage-bar-fill { height: 100%; border-radius: 9999px; background: var(--i18n-complete); transition: width 0.3s; }
181
+
182
+ /* ─── String Table Advanced ──────────────────────────── */
183
+ .i18n__string-table-advanced { width: 100%; border-collapse: collapse; font-size: 0.6875rem; }
184
+ .i18n__string-table-advanced th { padding: 0.75rem 1rem; text-align: left; font-weight: 600; text-transform: uppercase; color: oklch(1 0 0 / 0.4); background: oklch(1 0 0 / 0.01); border-bottom: 1px solid var(--i18n-border); letter-spacing: 0.05em; }
185
+ .i18n__string-table-advanced td { padding: 0.75rem 1rem; border-bottom: 1px solid oklch(1 0 0 / 0.04); }
186
+ .i18n__string-table-advanced tr:hover { background: oklch(1 0 0 / 0.02); }
187
+ .i18n__string-context-badge { display: inline-block; padding: 0.125rem 0.375rem; border-radius: 0.25rem; background: oklch(0.65 0.20 265 / 0.1); font-size: 0.625rem; color: oklch(0.65 0.20 265); }
188
+
189
+ /* ─── Locale Selector Advanced ───────────────────────── */
190
+ .i18n__locale-selector-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.75rem; }
191
+ .i18n__locale-card { padding: 1rem; border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); text-align: center; cursor: pointer; transition: all 0.2s; }
192
+ .i18n__locale-card:hover { border-color: var(--i18n-accent); background: oklch(0.65 0.20 265 / 0.05); }
193
+ .i18n__locale-card--selected { border: 2px solid var(--i18n-accent); background: oklch(0.65 0.20 265 / 0.08); }
194
+ .i18n__locale-card-flag { font-size: 1.5rem; margin-bottom: 0.5rem; }
195
+ .i18n__locale-card-name { font-size: 0.75rem; font-weight: 600; color: oklch(1 0 0 / 0.8); }
196
+ .i18n__locale-card-code { font-size: 0.625rem; color: oklch(1 0 0 / 0.4); margin-top: 0.25rem; font-family: monospace; }
197
+
198
+ /* ─── Format Examples ────────────────────────────────── */
199
+ .i18n__format-examples { border: 1px solid var(--i18n-border); border-radius: var(--i18n-radius); background: oklch(1 0 0 / 0.02); padding: 1rem; }
200
+ .i18n__format-example-title { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; color: oklch(1 0 0 / 0.5); margin-bottom: 0.75rem; }
201
+ .i18n__format-example-list { display: flex; flex-direction: column; gap: 0.5rem; }
202
+ .i18n__format-example-item { padding: 0.5rem; border-left: 2px solid oklch(0.65 0.20 265); border-radius: calc(var(--i18n-radius) * 0.6); background: oklch(0.65 0.20 265 / 0.05); }
203
+ .i18n__format-example-key { font-family: monospace; font-size: 0.625rem; color: oklch(0.65 0.20 265); margin-bottom: 0.25rem; }
204
+ .i18n__format-example-value { font-size: 0.6875rem; color: oklch(1 0 0 / 0.7); }
205
+
206
+ /* ─── Breadcrumb Navigation ──────────────────────────── */
207
+ .i18n__breadcrumb { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; margin-bottom: 1rem; }
208
+ .i18n__breadcrumb-item { color: oklch(1 0 0 / 0.5); }
209
+ .i18n__breadcrumb-item--active { color: oklch(1 0 0 / 0.8); font-weight: 500; }
210
+ .i18n__breadcrumb-sep { color: oklch(1 0 0 / 0.2); margin: 0 0.25rem; }
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "target": "ES2022",
13
+ "module": "ESNext",
14
+ "moduleResolution": "bundler"
15
+ },
16
+ "include": [
17
+ "src"
18
+ ]
19
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts', 'src/styles.css'],
5
+ format: ['cjs', 'esm'],
6
+ dts: true,
7
+ clean: true,
8
+ sourcemap: true,
9
+ external: ['react', 'react-dom'],
10
+ })
@@ -0,0 +1 @@
1
+ # ✦ @geenius-i18n/shared\n\n> Geenius i18n — Shared types & Convex schema\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/shared\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-i18n/shared';\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,44 @@
1
+ {
2
+ "name": "@geenius-i18n/shared",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Geenius i18n \u2014 Shared types & Convex schema",
7
+ "author": "Antigravity HQ",
8
+ "license": "MIT",
9
+ "publishConfig": {
10
+ "access": "public"
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
+ "./convex": "./src/convex.ts"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "clean": "rm -rf dist",
29
+ "type-check": "tsc --noEmit",
30
+ "prepublishOnly": "pnpm clean && pnpm build",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage"
34
+ },
35
+ "devDependencies": {
36
+ "convex": "^1.34.0",
37
+ "tsup": "^8.5.1",
38
+ "typescript": "~6.0.2",
39
+ "vitest": "^4.0.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=20.0.0"
43
+ }
44
+ }
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ interpolate, t, plural, getDirection,
4
+ LOCALE_INFO, ALL_LOCALES, RTL_LOCALES,
5
+ } from '../index'
6
+
7
+ describe('interpolate', () => {
8
+ it('replaces {{key}} with values', () => {
9
+ expect(interpolate('Hello {{name}}!', { name: 'Alice' })).toBe('Hello Alice!')
10
+ })
11
+
12
+ it('handles numeric values', () => {
13
+ expect(interpolate('{{count}} items', { count: 42 })).toBe('42 items')
14
+ })
15
+
16
+ it('preserves unmatched placeholders', () => {
17
+ expect(interpolate('Hello {{name}}', {})).toBe('Hello {{name}}')
18
+ })
19
+ })
20
+
21
+ describe('t (translation lookup)', () => {
22
+ const dict = { greeting: 'Hello', nested: { welcome: 'Welcome {{name}}' } }
23
+
24
+ it('resolves top-level key', () => {
25
+ expect(t('greeting', dict)).toBe('Hello')
26
+ })
27
+
28
+ it('resolves nested key with dot notation', () => {
29
+ expect(t('nested.welcome', dict, { name: 'Bob' })).toBe('Welcome Bob')
30
+ })
31
+
32
+ it('returns key for missing translations', () => {
33
+ expect(t('missing.key', dict)).toBe('missing.key')
34
+ })
35
+ })
36
+
37
+ describe('plural', () => {
38
+ const dict = { items_one: '1 item', items_other: '{{count}} items', items_zero: 'No items' }
39
+
40
+ it('uses _zero form for count 0', () => {
41
+ expect(plural('items', 0, dict)).toBe('No items')
42
+ })
43
+
44
+ it('uses _one form for count 1', () => {
45
+ expect(plural('items', 1, dict)).toBe('1 item')
46
+ })
47
+
48
+ it('uses _other form for count > 1', () => {
49
+ expect(plural('items', 5, dict)).toBe('5 items')
50
+ })
51
+ })
52
+
53
+ describe('Locale Data', () => {
54
+ it('ALL_LOCALES has 14 locales', () => {
55
+ expect(ALL_LOCALES).toHaveLength(14)
56
+ })
57
+
58
+ it('RTL_LOCALES contains ar and he', () => {
59
+ expect(RTL_LOCALES).toContain('ar')
60
+ expect(RTL_LOCALES).toContain('he')
61
+ expect(RTL_LOCALES).toHaveLength(2)
62
+ })
63
+
64
+ it('getDirection returns rtl for Arabic', () => {
65
+ expect(getDirection('ar')).toBe('rtl')
66
+ })
67
+
68
+ it('getDirection returns ltr for English', () => {
69
+ expect(getDirection('en')).toBe('ltr')
70
+ })
71
+
72
+ it('LOCALE_INFO has nativeName for each locale', () => {
73
+ ALL_LOCALES.forEach(locale => {
74
+ expect(LOCALE_INFO[locale].nativeName).toBeDefined()
75
+ expect(LOCALE_INFO[locale].flag.length).toBeGreaterThan(0)
76
+ })
77
+ })
78
+ })