@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,87 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { Locale, Direction, TranslationEntry, MissingKey, LocaleStat } from '@geenius-i18n/shared'
|
|
3
|
+
import { LOCALE_INFO } from '@geenius-i18n/shared'
|
|
4
|
+
|
|
5
|
+
export function LocaleSwitcher({ locales, current, onChange }: { locales: Locale[]; current: Locale; onChange: (l: Locale) => void }) {
|
|
6
|
+
const [open, setOpen] = useState(false)
|
|
7
|
+
const info = LOCALE_INFO[current]
|
|
8
|
+
return (
|
|
9
|
+
<div className="relative">
|
|
10
|
+
<button type="button" onClick={() => setOpen(!open)} className="flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white hover:bg-white/10 transition-all">
|
|
11
|
+
<span className="text-lg">{info?.flag}</span><span>{info?.nativeName}</span><span className="text-white/30 text-xs ml-1">▼</span>
|
|
12
|
+
</button>
|
|
13
|
+
{open && (
|
|
14
|
+
<div className="absolute top-full mt-1 right-0 z-50 min-w-[200px] rounded-xl border border-white/10 bg-[#111118] py-1 shadow-xl">
|
|
15
|
+
{locales.map(l => { const li = LOCALE_INFO[l]; return (
|
|
16
|
+
<button key={l} type="button" onClick={() => { onChange(l); setOpen(false) }}
|
|
17
|
+
className={`flex w-full items-center gap-3 px-4 py-2.5 text-sm transition-colors ${current === l ? 'bg-indigo-500/15 text-indigo-300' : 'text-white/70 hover:bg-white/5'}`}>
|
|
18
|
+
<span className="text-lg">{li?.flag}</span><span>{li?.nativeName}</span><span className="text-xs text-white/30 ml-auto">{li?.name}</span>
|
|
19
|
+
{li?.direction === 'rtl' && <span className="text-[10px] px-1 py-0.5 rounded bg-amber-500/15 text-amber-400">RTL</span>}
|
|
20
|
+
</button>
|
|
21
|
+
)})}
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function RTLWrapper({ children, locale }: { children: React.ReactNode; locale?: Locale }) {
|
|
29
|
+
const dir = locale ? LOCALE_INFO[locale]?.direction ?? 'ltr' : 'ltr'
|
|
30
|
+
return <div dir={dir} style={{ textAlign: dir === 'rtl' ? 'right' : 'left' }}>{children}</div>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function TranslationEditor({ onSave, initialKey, initialLocale, namespaces }: { onSave: (locale: string, ns: string, key: string, value: string) => void; initialKey?: string; initialLocale?: Locale; namespaces?: string[] }) {
|
|
34
|
+
const [locale, setLocale] = useState<string>(initialLocale ?? 'en')
|
|
35
|
+
const [ns, setNs] = useState(namespaces?.[0] ?? 'common')
|
|
36
|
+
const [key, setKey] = useState(initialKey ?? ''); const [value, setValue] = useState('')
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-3 rounded-xl border border-white/8 bg-white/[0.02] p-5">
|
|
39
|
+
<div className="flex gap-2">
|
|
40
|
+
<select value={locale} onChange={e => setLocale(e.target.value)} className="rounded-lg border border-white/10 bg-white/5 px-2 py-1.5 text-xs text-white outline-none">{Object.entries(LOCALE_INFO).map(([code, info]) => <option key={code} value={code}>{info.flag} {info.name}</option>)}</select>
|
|
41
|
+
<select value={ns} onChange={e => setNs(e.target.value)} className="rounded-lg border border-white/10 bg-white/5 px-2 py-1.5 text-xs text-white outline-none">{(namespaces ?? ['common', 'auth', 'dashboard', 'billing', 'errors']).map(n => <option key={n} value={n}>{n}</option>)}</select>
|
|
42
|
+
</div>
|
|
43
|
+
<input type="text" placeholder="Translation key (e.g. auth.login_button)" value={key} onChange={e => setKey(e.target.value)} className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40" />
|
|
44
|
+
<textarea placeholder="Translation value…" value={value} onChange={e => setValue(e.target.value)} rows={3} className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none focus:border-indigo-500/40 resize-none" />
|
|
45
|
+
<button type="button" onClick={() => { onSave(locale, ns, key, value); setKey(''); setValue('') }} disabled={!key || !value} className="rounded-lg bg-indigo-600 px-4 py-2 text-xs font-medium text-white hover:bg-indigo-500 disabled:opacity-50">Save Translation</button>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function TranslationTable({ translations, locale, onUpdate, onDelete }: { translations: TranslationEntry[]; locale: Locale; onUpdate?: (key: string, value: string) => void; onDelete?: (key: string) => void }) {
|
|
51
|
+
if (translations.length === 0) return <div className="flex flex-col items-center py-12"><div className="mb-3 text-4xl opacity-20">🌐</div><p className="text-sm text-white/40">No translations for this locale</p></div>
|
|
52
|
+
return (
|
|
53
|
+
<div className="overflow-x-auto rounded-xl border border-white/8">
|
|
54
|
+
<table className="w-full text-sm"><thead><tr className="border-b border-white/8 bg-white/[0.02]"><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Key</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Value</th><th className="px-4 py-3 text-left text-xs font-medium text-white/40 uppercase">Namespace</th><th className="px-4 py-3 text-right text-xs font-medium text-white/40 uppercase">Actions</th></tr></thead>
|
|
55
|
+
<tbody className="divide-y divide-white/5">{translations.map(t => (
|
|
56
|
+
<tr key={t.id} className="group hover:bg-white/[0.02]">
|
|
57
|
+
<td className="px-4 py-3 font-mono text-xs text-indigo-300">{t.key}</td>
|
|
58
|
+
<td className="px-4 py-3 text-white/70 max-w-xs truncate">{t.value}</td>
|
|
59
|
+
<td className="px-4 py-3"><span className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] text-white/40">{t.namespace}</span></td>
|
|
60
|
+
<td className="px-4 py-3 text-right">{onDelete && <button type="button" onClick={() => onDelete(t.key)} className="rounded px-2 py-1 text-xs text-white/20 hover:text-red-400 opacity-0 group-hover:opacity-100">Delete</button>}</td>
|
|
61
|
+
</tr>
|
|
62
|
+
))}</tbody></table>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function MissingKeyAlert({ count, locale }: { count: number; locale: Locale }) {
|
|
68
|
+
if (count === 0) return null
|
|
69
|
+
return <div className="flex items-center gap-3 rounded-lg border border-red-500/20 bg-red-500/[0.05] px-4 py-2.5"><span className="text-sm">⚠️</span><span className="text-xs text-red-300">{count} missing key{count > 1 ? 's' : ''} for <strong>{LOCALE_INFO[locale]?.name}</strong></span></div>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function LocaleStatsCard({ stats }: { stats: LocaleStat[] }) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">{stats.map(s => {
|
|
75
|
+
const info = LOCALE_INFO[s.locale]
|
|
76
|
+
const color = s.coverage >= 90 ? 'bg-emerald-500' : s.coverage >= 70 ? 'bg-amber-500' : 'bg-red-500'
|
|
77
|
+
return (
|
|
78
|
+
<div key={s.locale} className="rounded-xl border border-white/8 bg-white/[0.02] p-4">
|
|
79
|
+
<div className="flex items-center gap-2 mb-2"><span className="text-lg">{info?.flag}</span><span className="text-xs font-medium text-white/80">{info?.nativeName}</span></div>
|
|
80
|
+
<div className="mb-1 flex justify-between text-[10px] text-white/40"><span>{s.totalKeys} keys</span><span className="font-bold">{s.coverage}%</span></div>
|
|
81
|
+
<div className="h-1.5 rounded-full bg-white/5 overflow-hidden"><div className={`h-full rounded-full ${color}`} style={{ width: `${s.coverage}%` }} /></div>
|
|
82
|
+
{s.missingKeys > 0 && <p className="mt-1.5 text-[10px] text-red-400">{s.missingKeys} missing</p>}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
})}</div>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useCallback, useEffect, useMemo } from 'react'
|
|
2
|
+
import type { Locale, Direction, TranslationDict, I18nConfig } from '@geenius-i18n/shared'
|
|
3
|
+
import { t as tFn, plural, formatDate as fmtDate, formatNumber as fmtNum, formatCurrency as fmtCur, getDirection, isRTL as checkRTL, detectLocale, LOCALE_INFO } from '@geenius-i18n/shared'
|
|
4
|
+
|
|
5
|
+
interface I18nContextValue {
|
|
6
|
+
locale: Locale; direction: Direction; isRTL: boolean
|
|
7
|
+
setLocale: (l: Locale) => void; t: (key: string, params?: Record<string, string | number>) => string
|
|
8
|
+
formatDate: (date: Date | string, opts?: Intl.DateTimeFormatOptions) => string
|
|
9
|
+
formatNumber: (n: number, opts?: Intl.NumberFormatOptions) => string
|
|
10
|
+
formatCurrency: (amount: number, currency: string) => string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const I18nContext = createContext<I18nContextValue | null>(null)
|
|
14
|
+
|
|
15
|
+
export function I18nProvider({ children, config, translations }: { children: React.ReactNode; config: I18nConfig; translations?: TranslationDict }) {
|
|
16
|
+
const [locale, setLocaleState] = useState<Locale>(() => {
|
|
17
|
+
if (config.persistLocale && typeof localStorage !== 'undefined') {
|
|
18
|
+
const saved = localStorage.getItem('geenius-locale')
|
|
19
|
+
if (saved && config.supportedLocales.includes(saved as Locale)) return saved as Locale
|
|
20
|
+
}
|
|
21
|
+
if (config.detectBrowser) return detectLocale(config.supportedLocales)
|
|
22
|
+
return config.defaultLocale
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const setLocale = useCallback((l: Locale) => {
|
|
26
|
+
setLocaleState(l)
|
|
27
|
+
if (config.persistLocale && typeof localStorage !== 'undefined') localStorage.setItem('geenius-locale', l)
|
|
28
|
+
if (typeof document !== 'undefined') { document.documentElement.dir = getDirection(l); document.documentElement.lang = l }
|
|
29
|
+
}, [config.persistLocale])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (typeof document !== 'undefined') { document.documentElement.dir = getDirection(locale); document.documentElement.lang = locale }
|
|
33
|
+
}, [locale])
|
|
34
|
+
|
|
35
|
+
const value = useMemo<I18nContextValue>(() => ({
|
|
36
|
+
locale, direction: getDirection(locale), isRTL: checkRTL(locale), setLocale,
|
|
37
|
+
t: (key, params) => tFn(key, translations ?? {}, params),
|
|
38
|
+
formatDate: (date, opts) => fmtDate(date, locale, opts),
|
|
39
|
+
formatNumber: (n, opts) => fmtNum(n, locale, opts),
|
|
40
|
+
formatCurrency: (amount, currency) => fmtCur(amount, currency, locale),
|
|
41
|
+
}), [locale, setLocale, translations])
|
|
42
|
+
|
|
43
|
+
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useI18n() {
|
|
47
|
+
const ctx = useContext(I18nContext)
|
|
48
|
+
if (!ctx) throw new Error('useI18n must be used within I18nProvider')
|
|
49
|
+
return ctx
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react'
|
|
2
|
+
import type { TranslationEntry, MissingKey, LocaleStat } from '@geenius-i18n/shared'
|
|
3
|
+
|
|
4
|
+
export function useI18nAdmin(data: { translations?: TranslationEntry[]; missingKeys?: MissingKey[]; localeStats?: LocaleStat[] }, mutations: { upsert: (locale: string, ns: string, key: string, value: string) => Promise<void>; deleteKey: (locale: string, key: string) => Promise<void>; importDict: (locale: string, ns: string, dict: Record<string, string>) => Promise<void>; clearMissing: (id: string) => Promise<void> }) {
|
|
5
|
+
const [search, setSearch] = useState('')
|
|
6
|
+
const filteredTranslations = useMemo(() => {
|
|
7
|
+
if (!search) return data.translations ?? []
|
|
8
|
+
const q = search.toLowerCase()
|
|
9
|
+
return (data.translations ?? []).filter(t => t.key.toLowerCase().includes(q) || t.value.toLowerCase().includes(q))
|
|
10
|
+
}, [data.translations, search])
|
|
11
|
+
return { translations: filteredTranslations, missingKeys: data.missingKeys ?? [], localeStats: data.localeStats ?? [], search, setSearch, isLoading: data.translations === undefined, ...mutations }
|
|
12
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import type { Locale } from '@geenius-i18n/shared'
|
|
3
|
+
import { detectLocale } from '@geenius-i18n/shared'
|
|
4
|
+
|
|
5
|
+
export function useLocaleDetect(supportedLocales: Locale[]) {
|
|
6
|
+
const [detectedLocale, setDetected] = useState<Locale>(supportedLocales[0])
|
|
7
|
+
const [isDetecting, setDetecting] = useState(true)
|
|
8
|
+
useEffect(() => { setDetected(detectLocale(supportedLocales)); setDetecting(false) }, [supportedLocales])
|
|
9
|
+
return { detectedLocale, isDetecting }
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import type { Locale, TranslationDict, I18nNamespace } from '@geenius-i18n/shared'
|
|
3
|
+
import { t as tFn, loadNamespace } from '@geenius-i18n/shared'
|
|
4
|
+
|
|
5
|
+
export function useTranslations(locale: Locale, namespace: I18nNamespace) {
|
|
6
|
+
const [dict, setDict] = useState<TranslationDict>({})
|
|
7
|
+
const [isLoading, setLoading] = useState(true)
|
|
8
|
+
useEffect(() => { setLoading(true); loadNamespace(locale, namespace).then(d => { setDict(d); setLoading(false) }) }, [locale, namespace])
|
|
9
|
+
const t = useCallback((key: string, params?: Record<string, string | number>) => tFn(key, dict, params), [dict])
|
|
10
|
+
return { dict, isLoading, t }
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { I18nProvider, useI18n } from './hooks/useI18n'
|
|
2
|
+
export { useLocaleDetect } from './hooks/useLocaleDetect'
|
|
3
|
+
export { useTranslations } from './hooks/useTranslations'
|
|
4
|
+
export { useI18nAdmin } from './hooks/useI18nAdmin'
|
|
5
|
+
export { LocaleSwitcher, RTLWrapper, TranslationEditor, TranslationTable, MissingKeyAlert, LocaleStatsCard } from './components'
|
|
6
|
+
export { I18nAdminPage } from './pages/I18nAdminPage'
|
|
7
|
+
export { LocalePreviewPage } from './pages/LocalePreviewPage'
|
|
8
|
+
export type { Locale, Direction, I18nNamespace, TranslationDict, I18nConfig, LocaleInfo, TranslationEntry, MissingKey, LocaleStat } from '@geenius-i18n/shared'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { Locale, TranslationEntry, MissingKey, LocaleStat } from '@geenius-i18n/shared'
|
|
3
|
+
import { LOCALE_INFO, ALL_LOCALES } from '@geenius-i18n/shared'
|
|
4
|
+
import { useI18nAdmin } from '../hooks/useI18nAdmin'
|
|
5
|
+
import { LocaleStatsCard, MissingKeyAlert, TranslationTable, TranslationEditor } from '../components'
|
|
6
|
+
|
|
7
|
+
interface I18nAdminPageProps { translations?: TranslationEntry[]; missingKeys?: MissingKey[]; localeStats?: LocaleStat[]; mutations: Parameters<typeof useI18nAdmin>[1] }
|
|
8
|
+
|
|
9
|
+
export function I18nAdminPage({ translations, missingKeys, localeStats, mutations }: I18nAdminPageProps) {
|
|
10
|
+
const admin = useI18nAdmin({ translations, missingKeys, localeStats }, mutations)
|
|
11
|
+
const [selectedLocale, setSelectedLocale] = useState<Locale>('en')
|
|
12
|
+
const [selectedNs, setSelectedNs] = useState('common')
|
|
13
|
+
|
|
14
|
+
const filteredTranslations = admin.translations.filter(t => t.locale === selectedLocale && t.namespace === selectedNs)
|
|
15
|
+
|
|
16
|
+
if (admin.isLoading) return (
|
|
17
|
+
<div className="min-h-screen bg-[#090a0f] px-6 py-12"><div className="mx-auto max-w-6xl">
|
|
18
|
+
<div className="mb-8 h-10 w-48 animate-pulse rounded-lg bg-white/5" />
|
|
19
|
+
<div className="mb-6 grid grid-cols-4 gap-3">{Array.from({ length: 8 }).map((_, i) => <div key={i} className="h-20 animate-pulse rounded-xl bg-white/5" />)}</div>
|
|
20
|
+
<div className="h-96 animate-pulse rounded-xl bg-white/5" />
|
|
21
|
+
</div></div>
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="min-h-screen bg-[#090a0f] text-white"><div className="mx-auto max-w-6xl px-6 py-12">
|
|
26
|
+
<h1 className="text-2xl font-bold tracking-tight mb-8">Translation Admin</h1>
|
|
27
|
+
{admin.localeStats.length > 0 && <div className="mb-8"><h2 className="text-sm font-semibold text-white/60 mb-4">Locale Coverage</h2><LocaleStatsCard stats={admin.localeStats} /></div>}
|
|
28
|
+
{admin.missingKeys.length > 0 && <div className="mb-6"><MissingKeyAlert count={admin.missingKeys.filter(m => m.locale === selectedLocale).length} locale={selectedLocale} /></div>}
|
|
29
|
+
<div className="grid gap-6 lg:grid-cols-3">
|
|
30
|
+
<div className="lg:col-span-2 space-y-4">
|
|
31
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
32
|
+
<select value={selectedLocale} onChange={e => setSelectedLocale(e.target.value as Locale)} className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none">{ALL_LOCALES.map(l => <option key={l} value={l}>{LOCALE_INFO[l].flag} {LOCALE_INFO[l].name}</option>)}</select>
|
|
33
|
+
<select value={selectedNs} onChange={e => setSelectedNs(e.target.value)} className="rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white outline-none">{['common', 'auth', 'dashboard', 'billing', 'errors'].map(n => <option key={n} value={n}>{n}</option>)}</select>
|
|
34
|
+
<input type="text" placeholder="Search…" value={admin.search} onChange={e => admin.setSearch(e.target.value)} className="ml-auto w-48 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder-white/30 outline-none" />
|
|
35
|
+
</div>
|
|
36
|
+
<TranslationTable translations={filteredTranslations} locale={selectedLocale} onDelete={(key) => admin.deleteKey(selectedLocale, key)} />
|
|
37
|
+
</div>
|
|
38
|
+
<div><TranslationEditor onSave={(l, ns, k, v) => admin.upsert(l, ns, k, v)} initialLocale={selectedLocale} /></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div></div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { I18nConfig, TranslationDict } from '@geenius-i18n/shared'
|
|
3
|
+
import { LOCALE_INFO, ALL_LOCALES } from '@geenius-i18n/shared'
|
|
4
|
+
import { useI18n, I18nProvider } from '../hooks/useI18n'
|
|
5
|
+
import { LocaleSwitcher, RTLWrapper } from '../components'
|
|
6
|
+
|
|
7
|
+
function PreviewContent() {
|
|
8
|
+
const { locale, t, direction, isRTL, setLocale, formatDate, formatNumber, formatCurrency } = useI18n()
|
|
9
|
+
const info = LOCALE_INFO[locale]
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-screen bg-[#090a0f] text-white"><div className="mx-auto max-w-3xl px-6 py-12">
|
|
12
|
+
<div className="mb-8 flex items-center justify-between"><h1 className="text-2xl font-bold tracking-tight">Locale Preview</h1><LocaleSwitcher locales={ALL_LOCALES} current={locale} onChange={setLocale} /></div>
|
|
13
|
+
<div className="mb-6 grid grid-cols-2 gap-4 md:grid-cols-4">
|
|
14
|
+
<InfoCard label="Locale" value={locale.toUpperCase()} />
|
|
15
|
+
<InfoCard label="Direction" value={direction} highlight={isRTL} />
|
|
16
|
+
<InfoCard label="Native" value={info?.nativeName ?? '?'} />
|
|
17
|
+
<InfoCard label="Flag" value={info?.flag ?? '?'} />
|
|
18
|
+
</div>
|
|
19
|
+
<RTLWrapper locale={locale}>
|
|
20
|
+
<div className="space-y-6">
|
|
21
|
+
<div className="rounded-xl border border-white/8 bg-white/[0.02] p-5">
|
|
22
|
+
<h3 className="text-sm font-semibold text-white/80 mb-4">Formatting Examples</h3>
|
|
23
|
+
<div className="space-y-3 text-sm">
|
|
24
|
+
<Row label="Date" value={formatDate(new Date())} />
|
|
25
|
+
<Row label="Date (full)" value={formatDate(new Date(), { dateStyle: 'full' })} />
|
|
26
|
+
<Row label="Number" value={formatNumber(1234567.89)} />
|
|
27
|
+
<Row label="Currency (USD)" value={formatCurrency(9999.99, 'USD')} />
|
|
28
|
+
<Row label="Currency (EUR)" value={formatCurrency(4250.50, 'EUR')} />
|
|
29
|
+
<Row label="Percentage" value={formatNumber(0.85, { style: 'percent' })} />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
{isRTL && <div className="rounded-lg border border-amber-500/20 bg-amber-500/[0.05] px-4 py-3 text-sm text-amber-300">⚡ RTL mode active — text flows right-to-left</div>}
|
|
33
|
+
<div className="rounded-xl border border-white/8 bg-white/[0.02] p-5">
|
|
34
|
+
<h3 className="text-sm font-semibold text-white/80 mb-3">Sample Text</h3>
|
|
35
|
+
<p className="text-sm text-white/60 leading-relaxed">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</RTLWrapper>
|
|
39
|
+
</div></div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function InfoCard({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) {
|
|
44
|
+
return <div className="rounded-xl border border-white/8 bg-white/[0.02] p-4"><p className="text-[10px] text-white/40 mb-1 uppercase">{label}</p><p className={`text-lg font-bold ${highlight ? 'text-amber-400' : 'text-white/90'}`}>{value}</p></div>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function Row({ label, value }: { label: string; value: string }) {
|
|
48
|
+
return <div className="flex justify-between"><span className="text-white/40">{label}</span><span className="text-white/80 font-mono text-xs">{value}</span></div>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function LocalePreviewPage({ config, translations }: { config?: Partial<import('@geenius-i18n/shared').I18nConfig>; translations?: TranslationDict }) {
|
|
52
|
+
const fullConfig: import('@geenius-i18n/shared').I18nConfig = { defaultLocale: 'en', supportedLocales: ALL_LOCALES, detectBrowser: true, persistLocale: true, ...config }
|
|
53
|
+
return <I18nProvider config={fullConfig} translations={translations}><PreviewContent /></I18nProvider>
|
|
54
|
+
}
|
|
@@ -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 @@
|
|
|
1
|
+
# ✦ @geenius-i18n/react-css\n\n> Geenius i18n — React components & hooks (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/react-css\n```\n\n## Usage\n\n```typescript\nimport { init } from '@geenius-i18n/react-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,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geenius-i18n/react-css",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Geenius i18n — React components & hooks (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
|
+
"react": ">=18.0.0",
|
|
20
|
+
"react-dom": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@geenius-i18n/shared": "workspace:*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/react": "^19.0.7",
|
|
27
|
+
"@types/react-dom": "^19.0.3",
|
|
28
|
+
"react": "^19.0.0",
|
|
29
|
+
"react-dom": "^19.0.0",
|
|
30
|
+
"typescript": "~6.0.2"
|
|
31
|
+
},
|
|
32
|
+
"author": "Antigravity HQ",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=20.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import type { Locale, TranslationEntry, LocaleStat, MissingKey } from '@geenius-i18n/shared'
|
|
3
|
+
import { LOCALE_INFO, ALL_LOCALES } from '@geenius-i18n/shared'
|
|
4
|
+
|
|
5
|
+
export function LocaleSwitcher({ locales, current, onChange }: { locales: Locale[]; current: Locale; onChange: (l: Locale) => void }) {
|
|
6
|
+
const [open, setOpen] = useState(false)
|
|
7
|
+
const info = LOCALE_INFO[current]
|
|
8
|
+
return (
|
|
9
|
+
<div className="i18n__locale-switcher">
|
|
10
|
+
<button type="button" className="i18n__locale-switcher-btn" onClick={() => setOpen(!open)}>
|
|
11
|
+
<span className="i18n__locale-switcher-flag">{info?.flag}</span><span>{info?.nativeName}</span><span className="i18n__locale-switcher-arrow">▼</span>
|
|
12
|
+
</button>
|
|
13
|
+
{open && <div className="i18n__locale-dropdown">{locales.map(l => { const li = LOCALE_INFO[l]; return (
|
|
14
|
+
<button key={l} type="button" onClick={() => { onChange(l); setOpen(false) }} className={`i18n__locale-option ${current === l ? 'i18n__locale-option--active' : ''}`}>
|
|
15
|
+
<span className="i18n__locale-option-flag">{li?.flag}</span><span className="i18n__locale-option-name">{li?.nativeName}</span><span className="i18n__locale-option-eng">{li?.name}</span>
|
|
16
|
+
{li?.direction === 'rtl' && <span className="i18n__locale-option-rtl">RTL</span>}
|
|
17
|
+
</button>
|
|
18
|
+
)})}</div>}
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function RTLWrapper({ children, locale }: { children: React.ReactNode; locale?: Locale }) {
|
|
24
|
+
const dir = locale ? LOCALE_INFO[locale]?.direction ?? 'ltr' : 'ltr'
|
|
25
|
+
return <div className="i18n__rtl-wrapper" dir={dir}>{children}</div>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function TranslationEditor({ onSave, initialLocale, namespaces }: { onSave: (l: string, ns: string, k: string, v: string) => void; initialLocale?: Locale; namespaces?: string[] }) {
|
|
29
|
+
const [locale, setLocale] = useState<string>(initialLocale ?? 'en'); const [ns, setNs] = useState(namespaces?.[0] ?? 'common')
|
|
30
|
+
const [key, setKey] = useState(''); const [value, setValue] = useState('')
|
|
31
|
+
return (
|
|
32
|
+
<div className="i18n__editor">
|
|
33
|
+
<div className="i18n__editor-selectors">
|
|
34
|
+
<select value={locale} onChange={e => setLocale(e.target.value)} className="i18n__editor-select">{Object.entries(LOCALE_INFO).map(([c, i]) => <option key={c} value={c}>{i.flag} {i.name}</option>)}</select>
|
|
35
|
+
<select value={ns} onChange={e => setNs(e.target.value)} className="i18n__editor-select">{(namespaces ?? ['common','auth','dashboard','billing','errors']).map(n => <option key={n} value={n}>{n}</option>)}</select>
|
|
36
|
+
</div>
|
|
37
|
+
<input type="text" className="i18n__editor-input" placeholder="Translation key…" value={key} onChange={e => setKey(e.target.value)} />
|
|
38
|
+
<textarea className="i18n__editor-input i18n__editor-textarea" placeholder="Translation value…" value={value} onChange={e => setValue(e.target.value)} />
|
|
39
|
+
<button type="button" className="i18n__btn i18n__btn--primary" disabled={!key || !value} onClick={() => { onSave(locale, ns, key, value); setKey(''); setValue('') }}>Save Translation</button>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function TranslationTable({ translations, locale, onDelete }: { translations: TranslationEntry[]; locale: Locale; onDelete?: (key: string) => void }) {
|
|
45
|
+
if (translations.length === 0) return <div className="i18n__empty"><div className="i18n__empty-icon">🌐</div><div className="i18n__empty-text">No translations for this locale</div></div>
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ overflowX: 'auto', borderRadius: 'var(--i18n-radius)', border: '1px solid var(--i18n-border)' }}>
|
|
48
|
+
<table className="i18n__translation-table"><thead><tr><th>Key</th><th>Value</th><th>NS</th><th style={{ textAlign: 'right' }}>Actions</th></tr></thead>
|
|
49
|
+
<tbody>{translations.map(t => <tr key={t.id} className="i18n__translation-row"><td className="i18n__translation-key">{t.key}</td><td className="i18n__translation-value">{t.value}</td><td><span className="i18n__translation-ns">{t.namespace}</span></td><td style={{ textAlign: 'right' }}>{onDelete && <button type="button" className="i18n__delete-btn" onClick={() => onDelete(t.key)}>Delete</button>}</td></tr>)}</tbody></table>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function MissingKeyAlert({ count, locale }: { count: number; locale: Locale }) {
|
|
55
|
+
if (count === 0) return null
|
|
56
|
+
return <div className="i18n__missing-badge"><span className="i18n__missing-badge-icon">⚠️</span><span className="i18n__missing-badge-text">{count} missing key{count > 1 ? 's' : ''} for <strong>{LOCALE_INFO[locale]?.name}</strong></span></div>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function LocaleStatsCard({ stats }: { stats: LocaleStat[] }) {
|
|
60
|
+
return (
|
|
61
|
+
<div className="i18n__stats-grid">{stats.map(s => {
|
|
62
|
+
const info = LOCALE_INFO[s.locale]; const barClass = s.coverage >= 90 ? 'i18n__stats-bar-fill--complete' : s.coverage >= 70 ? 'i18n__stats-bar-fill--partial' : 'i18n__stats-bar-fill--low'
|
|
63
|
+
return <div key={s.locale} className="i18n__stats-card"><div className="i18n__stats-card-header"><span className="i18n__stats-card-flag">{info?.flag}</span><span className="i18n__stats-card-name">{info?.nativeName}</span></div><div className="i18n__stats-card-meta"><span>{s.totalKeys} keys</span><span style={{ fontWeight: 700 }}>{s.coverage}%</span></div><div className="i18n__stats-bar"><div className={`i18n__stats-bar-fill ${barClass}`} style={{ width: `${s.coverage}%` }} /></div>{s.missingKeys > 0 && <div className="i18n__stats-card-missing">{s.missingKeys} missing</div>}</div>
|
|
64
|
+
})}</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { I18nProvider, useI18n, useLocaleDetect, useTranslations, useI18nAdmin } from './hooks'
|
|
2
|
+
export { LocaleSwitcher, RTLWrapper, TranslationEditor, TranslationTable, MissingKeyAlert, LocaleStatsCard } from './components'
|
|
3
|
+
import './styles.css'
|
|
4
|
+
export type { Locale, Direction, I18nNamespace, TranslationDict, I18nConfig, LocaleInfo, TranslationEntry, MissingKey, LocaleStat } from '@geenius-i18n/shared'
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../styles.css';
|
|
3
|
+
|
|
4
|
+
const LocaleSettingsPage: React.FC = () => {
|
|
5
|
+
const locales = [
|
|
6
|
+
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
|
7
|
+
{ code: 'es', name: 'Spanish', flag: '🇪🇸' },
|
|
8
|
+
{ code: 'ja', name: 'Japanese', flag: '🇯🇵' },
|
|
9
|
+
{ code: 'de', name: 'German', flag: '🇩🇪' },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div style={{ padding: '1.5rem' }}>
|
|
14
|
+
<div className="i18n__breadcrumb">
|
|
15
|
+
<span className="i18n__breadcrumb-item">i18n</span>
|
|
16
|
+
<span className="i18n__breadcrumb-sep">/</span>
|
|
17
|
+
<span className="i18n__breadcrumb-item--active">Locale Settings</span>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div className="i18n__rtl-toggle-panel">
|
|
21
|
+
<div className="i18n__rtl-toggle-header">
|
|
22
|
+
<span className="i18n__rtl-toggle-label">Enable RTL Support</span>
|
|
23
|
+
<button className="i18n__rtl-toggle-switch">
|
|
24
|
+
<span className="i18n__rtl-toggle-knob" />
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<h2 style={{ fontSize: '0.875rem', fontWeight: '600', marginBottom: '1rem', color: 'oklch(1 0 0 / 0.8)' }}>
|
|
30
|
+
Available Locales
|
|
31
|
+
</h2>
|
|
32
|
+
|
|
33
|
+
<div className="i18n__locale-selector-grid">
|
|
34
|
+
{locales.map(locale => (
|
|
35
|
+
<div key={locale.code} className="i18n__locale-card">
|
|
36
|
+
<div className="i18n__locale-card-flag">{locale.flag}</div>
|
|
37
|
+
<div className="i18n__locale-card-name">{locale.name}</div>
|
|
38
|
+
<div className="i18n__locale-card-code">{locale.code}</div>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div className="i18n__import-export-panel" style={{ marginTop: '2rem' }}>
|
|
44
|
+
<div className="i18n__import-export-title">Import / Export Translations</div>
|
|
45
|
+
<div className="i18n__import-export-row">
|
|
46
|
+
<div className="i18n__import-export-col">
|
|
47
|
+
<label className="i18n__import-export-label">Import from File</label>
|
|
48
|
+
<button className="i18n__import-btn">Select File</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="i18n__import-export-col">
|
|
51
|
+
<label className="i18n__import-export-label">Export All Translations</label>
|
|
52
|
+
<button className="i18n__export-btn">Download JSON</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="i18n__format-examples" style={{ marginTop: '2rem' }}>
|
|
58
|
+
<div className="i18n__format-example-title">Translation Format Examples</div>
|
|
59
|
+
<div className="i18n__format-example-list">
|
|
60
|
+
<div className="i18n__format-example-item">
|
|
61
|
+
<div className="i18n__format-example-key">common.greeting</div>
|
|
62
|
+
<div className="i18n__format-example-value">Hello, {'{name}'}</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="i18n__format-example-item">
|
|
65
|
+
<div className="i18n__format-example-key">common.farewell</div>
|
|
66
|
+
<div className="i18n__format-example-value">Goodbye</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default LocaleSettingsPage;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import '../styles.css';
|
|
3
|
+
|
|
4
|
+
const TranslationsPage: React.FC = () => {
|
|
5
|
+
const translations = [
|
|
6
|
+
{ key: 'welcome.title', en: 'Welcome to our app', es: 'Bienvenido a nuestra app', status: 'complete' },
|
|
7
|
+
{ key: 'welcome.subtitle', en: 'Get started here', es: '', status: 'missing' },
|
|
8
|
+
{ key: 'button.save', en: 'Save', es: 'Guardar', status: 'complete' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div style={{ padding: '1.5rem' }}>
|
|
13
|
+
<div className="i18n__breadcrumb">
|
|
14
|
+
<span className="i18n__breadcrumb-item">i18n</span>
|
|
15
|
+
<span className="i18n__breadcrumb-sep">/</span>
|
|
16
|
+
<span className="i18n__breadcrumb-item--active">Translations</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className="i18n__stats-grid">
|
|
20
|
+
<div className="i18n__stats-card">
|
|
21
|
+
<div className="i18n__stats-card-header">
|
|
22
|
+
<div className="i18n__stats-card-flag">🌍</div>
|
|
23
|
+
<div className="i18n__stats-card-name">English</div>
|
|
24
|
+
</div>
|
|
25
|
+
<div className="i18n__stats-bar">
|
|
26
|
+
<div className="i18n__stats-bar-fill i18n__stats-bar-fill--complete" style={{ width: '100%' }} />
|
|
27
|
+
</div>
|
|
28
|
+
<div className="i18n__stats-card-meta">
|
|
29
|
+
<span>45 strings</span>
|
|
30
|
+
<span>100%</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="i18n__stats-card">
|
|
34
|
+
<div className="i18n__stats-card-header">
|
|
35
|
+
<div className="i18n__stats-card-flag">🇪🇸</div>
|
|
36
|
+
<div className="i18n__stats-card-name">Spanish</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="i18n__stats-bar">
|
|
39
|
+
<div className="i18n__stats-bar-fill i18n__stats-bar-fill--partial" style={{ width: '85%' }} />
|
|
40
|
+
</div>
|
|
41
|
+
<div className="i18n__stats-card-meta">
|
|
42
|
+
<span>38 strings</span>
|
|
43
|
+
<span>85%</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="i18n__stats-card">
|
|
47
|
+
<div className="i18n__stats-card-header">
|
|
48
|
+
<div className="i18n__stats-card-flag">🇯🇵</div>
|
|
49
|
+
<div className="i18n__stats-card-name">Japanese</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="i18n__stats-bar">
|
|
52
|
+
<div className="i18n__stats-bar-fill i18n__stats-bar-fill--low" style={{ width: '45%' }} />
|
|
53
|
+
</div>
|
|
54
|
+
<div className="i18n__stats-card-meta">
|
|
55
|
+
<span>20 strings</span>
|
|
56
|
+
<span>45%</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="i18n__stats-card-missing">7 missing keys</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="i18n__search" style={{ marginTop: '1.5rem', marginBottom: '1.5rem' }}>
|
|
63
|
+
<input type="text" className="i18n__search" placeholder="Search translations..." />
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<table className="i18n__translation-table">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr>
|
|
69
|
+
<th>Key</th>
|
|
70
|
+
<th>English</th>
|
|
71
|
+
<th>Spanish</th>
|
|
72
|
+
<th>Status</th>
|
|
73
|
+
</tr>
|
|
74
|
+
</thead>
|
|
75
|
+
<tbody>
|
|
76
|
+
{translations.map(t => (
|
|
77
|
+
<tr key={t.key} className="i18n__translation-row">
|
|
78
|
+
<td>
|
|
79
|
+
<div className="i18n__translation-key">{t.key}</div>
|
|
80
|
+
</td>
|
|
81
|
+
<td className="i18n__translation-value">{t.en}</td>
|
|
82
|
+
<td className="i18n__translation-value">{t.es || <span style={{ color: 'oklch(0.60 0.25 25)' }}>Missing</span>}</td>
|
|
83
|
+
<td>
|
|
84
|
+
<span className={`i18n__status-badge i18n__status-badge--${t.status === 'complete' ? 'open' : 'open'}`}>
|
|
85
|
+
{t.status}
|
|
86
|
+
</span>
|
|
87
|
+
</td>
|
|
88
|
+
</tr>
|
|
89
|
+
))}
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
|
|
93
|
+
<button className="i18n__delete-btn">🗑️ Delete</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export default TranslationsPage;
|