@asafarim/shared-i18n 0.5.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ # License
2
+
3
+ This project is licensed under the **Creative Commons Attribution 4.0 International (CC BY 4.0)** license.
4
+
5
+ You are free to:
6
+
7
+ - **Share** — copy and redistribute the material in any medium or format
8
+ - **Adapt** — remix, transform, and build upon the material for any purpose, even commercially
9
+
10
+ Under the following terms:
11
+
12
+ - **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made.
13
+ You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
14
+
15
+ ## 📌 Attribution Requirement
16
+
17
+ If you use or modify this project, please include the following attribution:
18
+
19
+ > Based on work by [Ali Safari](https://github.com/AliSafari-IT/asafarim-dot-be)
20
+
21
+ ## 🔗 Full License Text
22
+
23
+ For full details, see the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/).
package/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # @asafarim/shared-i18n
2
+
3
+ Lightweight, simple translation module for any React + TypeScript app, built on top of i18next and react-i18next. It ships with sensible defaults (English and Dutch) but can support any language by adding JSON files to your locales folder.
4
+
5
+ ## Features
6
+
7
+ - 🌍 Works in any React + TypeScript project (monorepo or standalone)
8
+ - 🗂️ JSON-based translations per language and namespace
9
+ - 🔄 Cookie-based language persistence (browser) with automatic detection
10
+ - ⚙️ Optional backend sync for user language preferences
11
+ - ⚡ Lazy loading support for app-specific translations
12
+ - 🪝 React hooks for language management (useLanguage) and translations (useTranslation)
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pnpm add @asafarim/shared-i18n
18
+ # or
19
+ npm i @asafarim/shared-i18n
20
+ # or
21
+ yarn add @asafarim/shared-i18n
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### 1) Initialize i18n in your app
27
+
28
+ In your app's main entry point (e.g., `main.tsx`):
29
+
30
+ ```tsx
31
+ import { StrictMode } from 'react';
32
+ import { createRoot } from 'react-dom/client';
33
+ import { initI18n } from '@asafarim/shared-i18n';
34
+ import App from './App';
35
+
36
+ // Optional: import your app-specific translations
37
+ import enApp from './locales/en/app.json';
38
+ import nlApp from './locales/nl/app.json';
39
+
40
+ initI18n({
41
+ defaultNS: 'common',
42
+ ns: ['common', 'app'],
43
+ resources: {
44
+ en: { app: enApp },
45
+ nl: { app: nlApp }
46
+ }
47
+ });
48
+
49
+ createRoot(document.getElementById('root')!).render(
50
+ <StrictMode>
51
+ <App />
52
+ </StrictMode>
53
+ );
54
+ ```
55
+
56
+ ### 2) Use translations in components
57
+
58
+ ```tsx
59
+ import { useTranslation } from '@asafarim/shared-i18n';
60
+
61
+ function MyComponent() {
62
+ const { t } = useTranslation();
63
+ return (
64
+ <div>
65
+ <h1>{t('welcome')}</h1>
66
+ <p>{t('app:customKey')}</p>
67
+ </div>
68
+ );
69
+ }
70
+ ```
71
+
72
+ ### 3) Add a language switcher
73
+
74
+ If you use @asafarim/shared-ui-react you can leverage its LanguageSwitcher component. Otherwise, call useLanguage directly.
75
+
76
+ ```tsx
77
+ import { useLanguage } from '@asafarim/shared-i18n';
78
+
79
+ export function LanguageToggle() {
80
+ const { language, changeLanguage, isChanging } = useLanguage();
81
+ return (
82
+ <button onClick={() => changeLanguage(language === 'en' ? 'nl' : 'en')} disabled={isChanging}>
83
+ Switch language (current: {language})
84
+ </button>
85
+ );
86
+ }
87
+ ```
88
+
89
+ ## Add more languages
90
+
91
+ Yes — you can support any language by adding the required JSON files to your locales folder. For example:
92
+
93
+ ```
94
+ your-app/
95
+ src/
96
+ locales/
97
+ en/
98
+ app.json
99
+ nl/
100
+ app.json
101
+ anotherLang/
102
+ new.json
103
+ ```
104
+
105
+ Then include them when initializing:
106
+
107
+ ```tsx
108
+ import anotherLang from './locales/anotherLang/new.json';
109
+
110
+ initI18n({
111
+ ns: ['common', 'app', 'new'],
112
+ resources: {
113
+ anotherLang: { new: anotherLang }
114
+ },
115
+ supportedLngs: ['en', 'nl', 'anotherLang'],
116
+ defaultLanguage: 'en'
117
+ });
118
+ ```
119
+
120
+ Notes:
121
+ - If you pass supportedLngs, it will override the default supported languages.
122
+ - defaultLanguage overrides the default fallback (which is English).
123
+
124
+ ## API Reference
125
+
126
+ ### initI18n(config?: I18nConfig)
127
+
128
+ Initialize i18next with the shared configuration.
129
+
130
+ Parameters:
131
+ - config.defaultNS — Default namespace (default: 'common')
132
+ - config.ns — Namespaces to load (default: ['common'])
133
+ - config.resources — App-specific translation resources
134
+ - config.supportedLngs — Optional list of supported language codes to enable
135
+ - config.defaultLanguage — Optional fallback language code
136
+
137
+ ### useLanguage()
138
+
139
+ Hook for managing language preferences.
140
+
141
+ Returns:
142
+ - language — Current language code
143
+ - changeLanguage(lang) — Function to change language
144
+ - isChanging — Boolean indicating if language change is in progress
145
+
146
+ ### useTranslation()
147
+
148
+ Re-exported from react-i18next. See official docs.
149
+
150
+ ## Cookie and backend integration
151
+
152
+ - A preferredLanguage cookie is used to persist the selected language in the browser.
153
+ - If your environment provides an Identity API, updateUserLanguagePreference can sync the preference server-side. If not, the library still works fully client-side.
154
+
155
+ To point to a backend, optionally set:
156
+
157
+ ```env
158
+ VITE_IDENTITY_API_URL=https://your-identity.example.com
159
+ ```
160
+
161
+ ## Built-in translations
162
+
163
+ This package ships with default English and Dutch common translations. You can ignore them and supply your own resources if preferred.
164
+
165
+ ## License
166
+
167
+ MIT © ASafariM
168
+
package/config/i18n.ts ADDED
@@ -0,0 +1,89 @@
1
+ import i18n from 'i18next';
2
+ import { initReactI18next } from 'react-i18next';
3
+ import LanguageDetector from 'i18next-browser-languagedetector';
4
+
5
+ // Import common translations
6
+ import enCommon from '../locales/en/common.json';
7
+ import nlCommon from '../locales/nl/common.json';
8
+
9
+ // Import web app translations
10
+ import enWeb from '../locales/en/web.json';
11
+ import nlWeb from '../locales/nl/web.json';
12
+
13
+ // Import identity-portal translations
14
+ import enIdentityPortal from '../locales/en/identity-portal.json';
15
+ import nlIdentityPortal from '../locales/nl/identity-portal.json';
16
+
17
+ export const SUPPORTED_LANGUAGES = ['en', 'nl'] as const;
18
+ export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number];
19
+
20
+ export const LANGUAGE_NAMES: Record<SupportedLanguage, string> = {
21
+ en: 'English',
22
+ nl: 'Nederlands'
23
+ };
24
+
25
+ export const DEFAULT_LANGUAGE: SupportedLanguage = 'en';
26
+ export const LANGUAGE_COOKIE_NAME = 'preferredLanguage';
27
+
28
+ export interface I18nConfig {
29
+ defaultNS?: string;
30
+ ns?: string[];
31
+ resources?: Record<string, Record<string, any>>;
32
+ supportedLngs?: string[];
33
+ defaultLanguage?: string;
34
+ }
35
+
36
+ export const initI18n = (config?: I18nConfig) => {
37
+ const {
38
+ defaultNS = 'common',
39
+ ns = ['common', 'web', 'identityPortal'],
40
+ resources = {},
41
+ supportedLngs,
42
+ defaultLanguage,
43
+ } = config || {};
44
+
45
+ // Merge common translations with app-specific resources
46
+ const mergedResources: Record<string, Record<string, any>> = { ...resources };
47
+
48
+ mergedResources.en = {
49
+ common: enCommon,
50
+ web: enWeb,
51
+ identityPortal: enIdentityPortal,
52
+ ...(resources.en || {}),
53
+ };
54
+ mergedResources.nl = {
55
+ common: nlCommon,
56
+ web: nlWeb,
57
+ identityPortal: nlIdentityPortal,
58
+ ...(resources.nl || {}),
59
+ };
60
+
61
+ const finalSupportedLngs = supportedLngs ?? Object.keys(mergedResources);
62
+ const fallbackLng = defaultLanguage ?? DEFAULT_LANGUAGE;
63
+
64
+ i18n
65
+ .use(initReactI18next)
66
+ .use(LanguageDetector)
67
+ .init({
68
+ resources: mergedResources,
69
+ fallbackLng,
70
+ defaultNS,
71
+ ns,
72
+ supportedLngs: finalSupportedLngs,
73
+ detection: {
74
+ order: ['cookie', 'navigator'],
75
+ caches: ['cookie'],
76
+ lookupCookie: LANGUAGE_COOKIE_NAME
77
+ },
78
+ interpolation: {
79
+ escapeValue: false // React already escapes
80
+ },
81
+ react: {
82
+ useSuspense: false
83
+ }
84
+ });
85
+
86
+ return i18n;
87
+ };
88
+
89
+ export default i18n;
@@ -0,0 +1,80 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import {
4
+ setLanguageCookie,
5
+ updateUserLanguagePreference,
6
+ fetchUserLanguagePreference,
7
+ isSupportedLanguage
8
+ } from '../utils/languageUtils';
9
+ import type { SupportedLanguage } from '../config/i18n';
10
+
11
+ export interface UseLanguageReturn {
12
+ language: SupportedLanguage;
13
+ changeLanguage: (lang: SupportedLanguage) => Promise<void>;
14
+ isChanging: boolean;
15
+ }
16
+
17
+ /**
18
+ * Hook for managing language preferences
19
+ * Handles both frontend (cookie) and backend (API) synchronization
20
+ */
21
+ export const useLanguage = (): UseLanguageReturn => {
22
+ const { i18n } = useTranslation();
23
+ const [isChanging, setIsChanging] = useState(false);
24
+
25
+ // Safely get current language with fallback
26
+ const currentLanguage = (i18n?.language && isSupportedLanguage(i18n.language)
27
+ ? i18n.language
28
+ : 'en') as SupportedLanguage;
29
+
30
+ // Sync with backend on mount (for authenticated users)
31
+ useEffect(() => {
32
+ const syncLanguageWithBackend = async () => {
33
+ if (!i18n?.changeLanguage) return;
34
+
35
+ const backendLang = await fetchUserLanguagePreference();
36
+ if (backendLang && backendLang !== currentLanguage) {
37
+ await i18n.changeLanguage(backendLang);
38
+ setLanguageCookie(backendLang);
39
+ }
40
+ };
41
+
42
+ syncLanguageWithBackend();
43
+ }, []); // Only run once on mount
44
+
45
+ const changeLanguage = useCallback(async (lang: SupportedLanguage) => {
46
+ if (lang === currentLanguage) return;
47
+
48
+ setIsChanging(true);
49
+ try {
50
+ // Check if i18n is properly initialized
51
+ if (!i18n || typeof i18n.changeLanguage !== 'function') {
52
+ console.error('i18n is not properly initialized');
53
+ // Still update cookie even if i18n fails
54
+ setLanguageCookie(lang);
55
+ return;
56
+ }
57
+
58
+ // Update i18next
59
+ await i18n.changeLanguage(lang);
60
+
61
+ // Update cookie immediately
62
+ setLanguageCookie(lang);
63
+
64
+ // Update backend (fire and forget for better UX)
65
+ updateUserLanguagePreference(lang).catch(err => {
66
+ console.warn('Failed to sync language preference with backend:', err);
67
+ });
68
+ } catch (error) {
69
+ console.error('Failed to change language:', error);
70
+ } finally {
71
+ setIsChanging(false);
72
+ }
73
+ }, [currentLanguage]);
74
+
75
+ return {
76
+ language: currentLanguage,
77
+ changeLanguage,
78
+ isChanging
79
+ };
80
+ };
package/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Main exports
2
+ export { initI18n, SUPPORTED_LANGUAGES, LANGUAGE_NAMES, DEFAULT_LANGUAGE, LANGUAGE_COOKIE_NAME } from './config/i18n';
3
+ export type { SupportedLanguage, I18nConfig } from './config/i18n';
4
+
5
+ // Hooks
6
+ export { useLanguage } from './hooks/useLanguage';
7
+ export type { UseLanguageReturn } from './hooks/useLanguage';
8
+
9
+ // Utils
10
+ export {
11
+ getLanguageFromCookie,
12
+ setLanguageCookie,
13
+ isSupportedLanguage,
14
+ getBrowserLanguage,
15
+ getInitialLanguage,
16
+ updateUserLanguagePreference,
17
+ fetchUserLanguagePreference
18
+ } from './utils/languageUtils';
19
+
20
+ // Re-export react-i18next for convenience
21
+ export { useTranslation, Trans, Translation } from 'react-i18next';
@@ -0,0 +1,66 @@
1
+ {
2
+ "welcome": "Welcome",
3
+ "language": "Language",
4
+ "settings": "Settings",
5
+ "profile": "Profile",
6
+ "logout": "Logout",
7
+ "login": "Login",
8
+ "register": "Register",
9
+ "email": "Email",
10
+ "password": "Password",
11
+ "confirmPassword": "Confirm Password",
12
+ "forgotPassword": "Forgot Password?",
13
+ "rememberMe": "Remember Me",
14
+ "submit": "Submit",
15
+ "cancel": "Cancel",
16
+ "save": "Save",
17
+ "delete": "Delete",
18
+ "edit": "Edit",
19
+ "close": "Close",
20
+ "search": "Search",
21
+ "loading": "Loading...",
22
+ "error": "Error",
23
+ "success": "Success",
24
+ "warning": "Warning",
25
+ "info": "Information",
26
+ "yes": "Yes",
27
+ "no": "No",
28
+ "back": "Back",
29
+ "next": "Next",
30
+ "previous": "Previous",
31
+ "home": "Home",
32
+ "about": "About",
33
+ "contact": "Contact",
34
+ "services": "Services",
35
+ "blog": "Blog",
36
+ "careers": "Careers",
37
+ "privacy": "Privacy Policy",
38
+ "terms": "Terms of Service",
39
+ "copyright": " ASafariM. All rights reserved.",
40
+ "languageChanged": "Language changed successfully",
41
+ "preferencesSaved": "Preferences saved successfully",
42
+ "apps": {
43
+ "appName": {
44
+ "web": "ASafariM web",
45
+ "blog": "Blog & documentation",
46
+ "ai": "AI Tools",
47
+ "core": "Core App",
48
+ "jobs": "Job Applications",
49
+ "identity": "Identity Portal",
50
+ "testora": "Testora",
51
+ "taskmanagement": "Task Management",
52
+ "smartops": "SmartOps"
53
+ },
54
+ "description": {
55
+ "web": "ASafariM web portal",
56
+ "blog": "Documentation and blog",
57
+ "ai": "AI-powered tools and services",
58
+ "core": "Core application features",
59
+ "jobs": "Job application tracking",
60
+ "identity": "User management and authentication",
61
+ "testora": "Test Automation System - Testora",
62
+ "taskmanagement": "Task Management",
63
+ "smartops": "SmartOps"
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "toast": {
3
+ "notAuthorized": "You do not have permission to access this page",
4
+ "notAdmin": "You do not have permission to access this page",
5
+ "loading": "Loading..."
6
+ },
7
+ "admin-area": {
8
+ "dashboard": {
9
+ "title": "Admin Dashboard",
10
+ "description": "Manage Identity Portal entities",
11
+ "actions": {
12
+ "open": "Open"
13
+ },
14
+ "user-management": {
15
+ "title": "User Management",
16
+ "description": "Manage platform users and their permissions.",
17
+ "icon": "👥",
18
+ "link": "/admin-area/users"
19
+ },
20
+ "role-management": {
21
+ "title": "Role Management",
22
+ "description": "Define and update system roles and privileges.",
23
+ "icon": "🛡️",
24
+ "link": "/admin-area/roles"
25
+ }
26
+ }
27
+ },
28
+ "navbar": {
29
+ "admin-area": "Admin Area",
30
+ "me": "My Profile",
31
+ "contact": "Contact",
32
+ "about": "About",
33
+ "auth": {
34
+ "notSignedIn": "Not signed in!",
35
+ "signIn": "Sign In",
36
+ "signOut": "Sign Out",
37
+ "welcome": "Welcome, {{userName}}!"
38
+ }
39
+ },
40
+ "dashboard": {
41
+ "title": "Dashboard",
42
+ "description": "Manage Identity Portal entities",
43
+ "actions": {
44
+ "title": "Actions",
45
+ "open": "Open",
46
+ "editProfile": "Edit profile",
47
+ "changePassword": "Change password",
48
+ "manageUsers": "Manage users"
49
+ },
50
+ "user-management": {
51
+ "title": "User Management",
52
+ "description": "Manage platform users and their permissions.",
53
+ "icon": "👥",
54
+ "link": "/admin-area/users",
55
+ "email": "Email",
56
+ "username": "Username"
57
+ },
58
+ "role-management": {
59
+ "title": "Role Management",
60
+ "description": "Define and update system roles and privileges.",
61
+ "icon": "🛡️",
62
+ "link": "/admin-area/roles"
63
+ },
64
+ "access": {
65
+ "title": "Access",
66
+ "roles": "Roles"
67
+ }
68
+ }
69
+ }