@djangocfg/i18n 2.1.111

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/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # @djangocfg/i18n
2
+
3
+ Lightweight, type-safe i18n library for @djangocfg component packages. Provides built-in translations for English, Russian, and Korean with easy extensibility.
4
+
5
+ ## Features
6
+
7
+ - **Type-safe** - Full TypeScript support with autocomplete for translation keys
8
+ - **Lightweight** - No heavy dependencies, pure React
9
+ - **Extensible** - Easy to add custom translations or override defaults
10
+ - **Works standalone** - Components work without provider using English defaults
11
+ - **3 languages built-in** - English, Russian, Korean
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add @djangocfg/i18n
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```tsx
22
+ import { I18nProvider, useT, ru } from '@djangocfg/i18n'
23
+
24
+ // Wrap your app (optional - works without provider too)
25
+ <I18nProvider locale="ru" translations={ru}>
26
+ <App />
27
+ </I18nProvider>
28
+
29
+ // Use in components - simplest way
30
+ function MyComponent() {
31
+ const t = useT()
32
+ return <span>{t('ui.form.save')}</span>
33
+ }
34
+ ```
35
+
36
+ ## Hooks
37
+
38
+ | Hook | Description |
39
+ |------|-------------|
40
+ | `useT()` | Returns translation function, works with/without provider (recommended) |
41
+ | `useI18n()` | Full context: `{ t, locale, setLocale, translations }` |
42
+ | `useTranslation()` | Alias for `useT()` |
43
+ | `useLocale()` | Returns current locale string |
44
+ | `useTypedT<T>()` | Type-safe translation function with compile-time key validation |
45
+
46
+ ### useT() - Recommended
47
+
48
+ ```tsx
49
+ function MyComponent() {
50
+ const t = useT()
51
+ return <button>{t('ui.form.save')}</button>
52
+ }
53
+ ```
54
+
55
+ ### useI18n() - Full Context
56
+
57
+ ```tsx
58
+ function LocaleSwitcher() {
59
+ const { t, locale, setLocale } = useI18n()
60
+
61
+ return (
62
+ <select value={locale} onChange={(e) => setLocale(e.target.value)}>
63
+ <option value="en">English</option>
64
+ <option value="ru">Russian</option>
65
+ <option value="ko">Korean</option>
66
+ </select>
67
+ )
68
+ }
69
+ ```
70
+
71
+ ### useTypedT<T>() - Type-Safe Keys
72
+
73
+ ```tsx
74
+ import { useTypedT } from '@djangocfg/i18n'
75
+ import type { I18nTranslations } from '@djangocfg/i18n'
76
+
77
+ function MyComponent() {
78
+ const t = useTypedT<I18nTranslations>()
79
+ return <span>{t('ui.form.save')}</span> // OK
80
+ // t('ui.form.typo') // Compile error!
81
+ }
82
+ ```
83
+
84
+ ### getT() - Non-Hook Context
85
+
86
+ For class components or outside React:
87
+
88
+ ```tsx
89
+ import { getT } from '@djangocfg/i18n'
90
+
91
+ const label = getT('ui.form.save')
92
+ const message = getT('ui.pagination.showing', { from: 1, to: 10, total: 100 })
93
+ ```
94
+
95
+ ## Extending Translations
96
+
97
+ ### mergeTranslations()
98
+
99
+ Deep merge base translations with custom overrides. Supports generic type parameter for app-specific namespaces:
100
+
101
+ ```tsx
102
+ import { mergeTranslations, ru } from '@djangocfg/i18n'
103
+
104
+ // Override specific keys
105
+ const customRu = mergeTranslations(ru, {
106
+ ui: {
107
+ select: { placeholder: 'Выберите марку авто...' }
108
+ }
109
+ })
110
+
111
+ // Add app-specific namespace with type safety
112
+ type AppNamespace = { app: { title: string; welcome: string } }
113
+
114
+ const appRu = mergeTranslations<AppNamespace>(ru, {
115
+ app: {
116
+ title: 'Мое приложение',
117
+ welcome: 'Добро пожаловать!'
118
+ }
119
+ })
120
+ // Result type: I18nTranslations & AppNamespace
121
+ ```
122
+
123
+ ### createTranslations()
124
+
125
+ Create translations from multiple partial sources:
126
+
127
+ ```tsx
128
+ import { createTranslations, en } from '@djangocfg/i18n'
129
+
130
+ const translations = createTranslations(
131
+ en, // base
132
+ uiOverrides, // UI customizations
133
+ appStrings // App-specific strings
134
+ )
135
+ ```
136
+
137
+ ## Built-in Locales
138
+
139
+ ```tsx
140
+ import { en, ru, ko } from '@djangocfg/i18n'
141
+ ```
142
+
143
+ - `en` - English (default)
144
+ - `ru` - Russian
145
+ - `ko` - Korean
146
+
147
+ ## Interpolation
148
+
149
+ ```tsx
150
+ t('ui.pagination.showing', { from: 1, to: 10, total: 100 })
151
+ // => "1-10 of 100"
152
+
153
+ t('ui.select.moreItems', { count: 5 })
154
+ // => "+5 more"
155
+ ```
156
+
157
+ ## Type Utilities
158
+
159
+ ```tsx
160
+ import type { PathKeys, I18nKeys, I18nTranslations } from '@djangocfg/i18n'
161
+
162
+ // Get all valid keys from any translations type
163
+ type MyKeys = PathKeys<MyTranslations>
164
+ // = "form.title" | "form.submit" | "messages.error" | ...
165
+
166
+ // Built-in keys for base translations
167
+ const key: I18nKeys = 'ui.form.save' // OK
168
+ const bad: I18nKeys = 'ui.form.typo' // Error
169
+ ```
170
+
171
+ ## Translation Key Paths
172
+
173
+ All translation keys use dot notation:
174
+
175
+ ### UI Components
176
+
177
+ ```
178
+ ui.select.placeholder - "Select option..."
179
+ ui.select.search - "Search..."
180
+ ui.select.noResults - "No results found."
181
+ ui.select.selectAll - "Select all"
182
+ ui.select.clearAll - "Clear all"
183
+ ui.select.moreItems - "+{count} more"
184
+
185
+ ui.pagination.previous - "Previous"
186
+ ui.pagination.next - "Next"
187
+ ui.pagination.showing - "{from}-{to} of {total}"
188
+
189
+ ui.form.submit - "Submit"
190
+ ui.form.save - "Save"
191
+ ui.form.cancel - "Cancel"
192
+ ui.form.delete - "Delete"
193
+ ui.form.loading - "Loading..."
194
+ ui.form.required - "This field is required"
195
+
196
+ ui.dialog.close - "Close"
197
+ ui.dialog.confirm - "Confirm"
198
+
199
+ ui.errors.generic - "Something went wrong"
200
+ ui.errors.network - "Network error..."
201
+ ```
202
+
203
+ ### Layouts
204
+
205
+ ```
206
+ layouts.sidebar.toggle - "Toggle sidebar"
207
+ layouts.profile.settings - "Settings"
208
+ layouts.profile.logout - "Log out"
209
+ layouts.theme.light - "Light"
210
+ layouts.theme.dark - "Dark"
211
+ ```
212
+
213
+ ### API
214
+
215
+ ```
216
+ api.errors.networkError - "Network error"
217
+ api.status.connecting - "Connecting..."
218
+ api.status.connected - "Connected"
219
+ ```
220
+
221
+ ## Works Without Provider
222
+
223
+ Components using `useT()` or `useI18n()` work without provider - they fall back to English defaults. This allows gradual adoption in existing projects.
224
+
225
+ ## Integration with @djangocfg packages
226
+
227
+ This package is designed to work with:
228
+ - `@djangocfg/ui-core` - Base React components
229
+ - `@djangocfg/ui-nextjs` - Next.js specific components
230
+ - `@djangocfg/layouts` - Layout components
231
+ - `@djangocfg/nextjs` - Next.js App Router integration with next-intl
232
+
233
+ ## License
234
+
235
+ MIT
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "@djangocfg/i18n",
3
+ "version": "2.1.111",
4
+ "description": "Lightweight i18n library for @djangocfg packages with built-in translations for English, Russian, and Korean",
5
+ "keywords": [
6
+ "i18n",
7
+ "internationalization",
8
+ "localization",
9
+ "translations",
10
+ "react",
11
+ "nextjs",
12
+ "typescript"
13
+ ],
14
+ "author": {
15
+ "name": "DjangoCFG",
16
+ "url": "https://djangocfg.com"
17
+ },
18
+ "homepage": "https://djangocfg.com",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/markolofsen/django-cfg.git",
22
+ "directory": "packages/i18n"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/markolofsen/django-cfg/issues"
26
+ },
27
+ "license": "MIT",
28
+ "main": "./src/index.ts",
29
+ "module": "./src/index.ts",
30
+ "types": "./src/index.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./src/index.ts",
34
+ "import": "./src/index.ts",
35
+ "require": "./src/index.ts"
36
+ },
37
+ "./locales": {
38
+ "types": "./src/locales/index.ts",
39
+ "import": "./src/locales/index.ts",
40
+ "require": "./src/locales/index.ts"
41
+ },
42
+ "./locales/*": "./src/locales/*.ts",
43
+ "./utils": {
44
+ "types": "./src/utils/index.ts",
45
+ "import": "./src/utils/index.ts",
46
+ "require": "./src/utils/index.ts"
47
+ }
48
+ },
49
+ "files": [
50
+ "src",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "scripts": {
55
+ "lint": "eslint .",
56
+ "check": "tsc --noEmit"
57
+ },
58
+ "peerDependencies": {
59
+ "react": "^18.0.0 || ^19.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@djangocfg/typescript-config": "^2.1.111",
63
+ "@types/react": "^19.1.0",
64
+ "eslint": "^9.37.0",
65
+ "typescript": "^5.9.3"
66
+ },
67
+ "publishConfig": {
68
+ "access": "public"
69
+ }
70
+ }
@@ -0,0 +1,293 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ import { en } from './locales/en'
6
+ import type {
7
+ I18nContextValue,
8
+ I18nProviderProps,
9
+ I18nTranslations,
10
+ LocaleCode,
11
+ } from './types'
12
+ import { getNestedValue, interpolate, mergeTranslations } from './utils'
13
+
14
+ /**
15
+ * I18n Context
16
+ */
17
+ const I18nContext = React.createContext<I18nContextValue | null>(null)
18
+
19
+ /**
20
+ * Default translations (English)
21
+ */
22
+ const defaultTranslations: I18nTranslations = en
23
+
24
+ /**
25
+ * I18n Provider
26
+ *
27
+ * @example
28
+ * // Basic usage with single locale
29
+ * import { I18nProvider, ru } from '@djangocfg/i18n'
30
+ *
31
+ * <I18nProvider locale="ru" translations={ru}>
32
+ * <App />
33
+ * </I18nProvider>
34
+ *
35
+ * @example
36
+ * // With all translations for dynamic switching
37
+ * import { I18nProvider, en, ru, ko } from '@djangocfg/i18n'
38
+ *
39
+ * <I18nProvider
40
+ * locale={currentLocale}
41
+ * allTranslations={{ en, ru, ko }}
42
+ * onLocaleChange={(locale) => setCurrentLocale(locale)}
43
+ * >
44
+ * <App />
45
+ * </I18nProvider>
46
+ *
47
+ * @example
48
+ * // With custom overrides
49
+ * import { I18nProvider, ru, mergeTranslations } from '@djangocfg/i18n'
50
+ *
51
+ * const customRu = mergeTranslations(ru, {
52
+ * ui: { select: { placeholder: 'Выберите марку...' } }
53
+ * })
54
+ *
55
+ * <I18nProvider locale="ru" translations={customRu}>
56
+ * <App />
57
+ * </I18nProvider>
58
+ */
59
+ export function I18nProvider({
60
+ children,
61
+ locale: initialLocale = 'en',
62
+ translations: initialTranslations,
63
+ allTranslations,
64
+ onLocaleChange,
65
+ }: I18nProviderProps) {
66
+ const [locale, setLocaleState] = React.useState<LocaleCode>(initialLocale)
67
+
68
+ // Resolve current translations
69
+ const translations = React.useMemo<I18nTranslations>(() => {
70
+ // If allTranslations provided, use it to get translations for current locale
71
+ if (allTranslations && allTranslations[locale]) {
72
+ const localeTranslations = allTranslations[locale]
73
+ // Merge with defaults to fill any missing keys
74
+ return mergeTranslations(defaultTranslations, localeTranslations)
75
+ }
76
+
77
+ // If single translations provided, use them
78
+ if (initialTranslations) {
79
+ return mergeTranslations(
80
+ defaultTranslations,
81
+ initialTranslations as I18nTranslations
82
+ )
83
+ }
84
+
85
+ // Fallback to defaults
86
+ return defaultTranslations
87
+ }, [locale, allTranslations, initialTranslations])
88
+
89
+ // Translation function
90
+ const t = React.useCallback(
91
+ (key: string, params?: Record<string, string | number>): string => {
92
+ const value = getNestedValue(
93
+ translations as unknown as Record<string, unknown>,
94
+ key
95
+ )
96
+
97
+ if (value === undefined) {
98
+ // Development warning
99
+ if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
100
+ console.warn(`[i18n] Missing translation key: "${key}"`)
101
+ }
102
+ // Return the key as fallback
103
+ return key
104
+ }
105
+
106
+ return interpolate(value, params)
107
+ },
108
+ [translations]
109
+ )
110
+
111
+ // Locale change handler
112
+ const setLocale = React.useCallback(
113
+ (newLocale: LocaleCode) => {
114
+ setLocaleState(newLocale)
115
+ onLocaleChange?.(newLocale)
116
+ },
117
+ [onLocaleChange]
118
+ )
119
+
120
+ // Sync with external locale changes
121
+ React.useEffect(() => {
122
+ if (initialLocale !== locale) {
123
+ setLocaleState(initialLocale)
124
+ }
125
+ }, [initialLocale])
126
+
127
+ const value = React.useMemo<I18nContextValue>(
128
+ () => ({
129
+ locale,
130
+ translations,
131
+ t,
132
+ setLocale,
133
+ }),
134
+ [locale, translations, t, setLocale]
135
+ )
136
+
137
+ return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>
138
+ }
139
+
140
+ /**
141
+ * Hook to access i18n context
142
+ *
143
+ * @example
144
+ * function MyComponent() {
145
+ * const { t, locale, setLocale } = useI18n()
146
+ *
147
+ * return (
148
+ * <div>
149
+ * <p>{t('ui.select.placeholder')}</p>
150
+ * <p>{t('ui.pagination.showing', { from: 1, to: 10, total: 100 })}</p>
151
+ * <button onClick={() => setLocale('ru')}>Switch to Russian</button>
152
+ * </div>
153
+ * )
154
+ * }
155
+ */
156
+ export function useI18n(): I18nContextValue {
157
+ const context = React.useContext(I18nContext)
158
+
159
+ if (!context) {
160
+ // Return default implementation if no provider
161
+ // This allows components to work without provider (using English defaults)
162
+ return {
163
+ locale: 'en',
164
+ translations: defaultTranslations,
165
+ t: (key: string, params?: Record<string, string | number>) => {
166
+ const value = getNestedValue(
167
+ defaultTranslations as unknown as Record<string, unknown>,
168
+ key
169
+ )
170
+ if (value === undefined) return key
171
+ return interpolate(value, params)
172
+ },
173
+ setLocale: () => {
174
+ if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
175
+ console.warn(
176
+ '[i18n] setLocale called but no I18nProvider found. Wrap your app with I18nProvider to enable locale switching.'
177
+ )
178
+ }
179
+ },
180
+ }
181
+ }
182
+
183
+ return context
184
+ }
185
+
186
+ /**
187
+ * Hook to get the translation function only
188
+ * Useful when you only need translations without other context values
189
+ *
190
+ * @example
191
+ * function MyComponent() {
192
+ * const t = useTranslation()
193
+ * return <span>{t('ui.form.save')}</span>
194
+ * }
195
+ */
196
+ export function useTranslation() {
197
+ const { t } = useI18n()
198
+ return t
199
+ }
200
+
201
+ /**
202
+ * Hook to get current locale
203
+ *
204
+ * @example
205
+ * function MyComponent() {
206
+ * const locale = useLocale()
207
+ * return <span>Current: {locale}</span>
208
+ * }
209
+ */
210
+ export function useLocale(): LocaleCode {
211
+ const { locale } = useI18n()
212
+ return locale
213
+ }
214
+
215
+ /**
216
+ * Alias for useTranslation
217
+ * Lightweight hook that just returns the t function
218
+ * Works with or without I18nProvider (falls back to English)
219
+ *
220
+ * @example
221
+ * function MyComponent() {
222
+ * const t = useT()
223
+ * return <span>{t('ui.form.save')}</span>
224
+ * }
225
+ */
226
+ export const useT = useTranslation
227
+
228
+ /**
229
+ * Type-safe translation hook
230
+ *
231
+ * Returns a translation function that only accepts valid keys from your translations type.
232
+ * Use this when you want compile-time checking of translation keys.
233
+ *
234
+ * @example
235
+ * ```typescript
236
+ * import { useTypedT } from '@djangocfg/i18n'
237
+ * import type { I18nTranslations } from '@djangocfg/i18n'
238
+ *
239
+ * function MyComponent() {
240
+ * const t = useTypedT<I18nTranslations>()
241
+ * return <span>{t('ui.form.save')}</span> // OK
242
+ * return <span>{t('ui.form.typo')}</span> // Compile error!
243
+ * }
244
+ * ```
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * // With merged translations (app + extensions)
249
+ * import type { AppTranslations } from '@/i18n/types'
250
+ *
251
+ * function MyComponent() {
252
+ * const t = useTypedT<AppTranslations>()
253
+ * return <span>{t('leads.form.title')}</span> // OK if leads is in AppTranslations
254
+ * }
255
+ * ```
256
+ */
257
+ export function useTypedT<T extends object>(): (
258
+ key: import('./utils/path-keys').PathKeys<T>,
259
+ params?: Record<string, string | number>
260
+ ) => string {
261
+ const { t } = useI18n()
262
+ return t as (
263
+ key: import('./utils/path-keys').PathKeys<T>,
264
+ params?: Record<string, string | number>
265
+ ) => string
266
+ }
267
+
268
+ /**
269
+ * Get translation without hook (for class components or non-React contexts)
270
+ * Always uses English defaults, no context support
271
+ *
272
+ * @example
273
+ * // In a class component
274
+ * render() {
275
+ * const title = getT('layouts.errors.somethingWentWrong')
276
+ * return <h1>{title}</h1>
277
+ * }
278
+ *
279
+ * @example
280
+ * // With interpolation
281
+ * const message = getT('ui.pagination.showing', { from: 1, to: 10, total: 100 })
282
+ */
283
+ export function getT(
284
+ key: string,
285
+ params?: Record<string, string | number>
286
+ ): string {
287
+ const value = getNestedValue(
288
+ defaultTranslations as unknown as Record<string, unknown>,
289
+ key
290
+ )
291
+ if (value === undefined) return key
292
+ return interpolate(value, params)
293
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Context & Hooks
2
+ export { I18nProvider, useI18n, useTranslation, useLocale, useT, useTypedT, getT } from './context'
3
+
4
+ // Types
5
+ export type {
6
+ I18nTranslations,
7
+ PartialTranslations,
8
+ LocaleCode,
9
+ I18nContextValue,
10
+ I18nProviderProps,
11
+ DeepPartial,
12
+ I18nKeys,
13
+ I18nTranslationFn,
14
+ } from './types'
15
+
16
+ // Type-safe keys utilities
17
+ export type {
18
+ PathKeys,
19
+ TranslationFn,
20
+ TypedTranslationFn,
21
+ TranslationKeys,
22
+ } from './utils/path-keys'
23
+
24
+ // Locales
25
+ export { en, ru, ko } from './locales'
26
+
27
+ // Utils
28
+ export { interpolate, mergeTranslations, createTranslations } from './utils'