@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,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,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,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,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 @@
|
|
|
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
|
+
}
|