@asafarim/shared-i18n 0.5.2 → 0.6.1
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 +170 -168
- package/demo/index.html +13 -0
- package/demo/node_modules/.bin/tsc +21 -0
- package/demo/node_modules/.bin/tsserver +21 -0
- package/demo/node_modules/.bin/vite +21 -0
- package/demo/package.json +24 -0
- package/demo/public/favicon.svg +4 -0
- package/demo/public/logo.svg +24 -0
- package/demo/src/App.tsx +112 -0
- package/demo/src/components/GetStartedSection.tsx +56 -0
- package/demo/src/components/KeyTable.tsx +29 -0
- package/demo/src/components/LanguageBar.tsx +19 -0
- package/demo/src/components/LanguageSwitcherDemo.module.css +113 -0
- package/demo/src/components/LanguageSwitcherDemo.tsx +184 -0
- package/demo/src/components/OverviewSection.tsx +43 -0
- package/demo/src/components/Panel.tsx +15 -0
- package/demo/src/components/StatusCard.tsx +109 -0
- package/demo/src/index.css +569 -0
- package/demo/src/locales/en/demo.json +85 -0
- package/demo/src/locales/fr/demo.json +85 -0
- package/demo/src/locales/nl/demo.json +85 -0
- package/demo/src/main.tsx +24 -0
- package/demo/tsconfig.json +18 -0
- package/demo/tsconfig.node.json +10 -0
- package/demo/vite-env.d.ts +7 -0
- package/demo/vite.config.ts +11 -0
- package/dist/components/LanguageSwitcher.d.ts +20 -0
- package/dist/components/LanguageSwitcher.d.ts.map +1 -0
- package/dist/components/LanguageSwitcher.js +72 -0
- package/dist/components/LanguageSwitcher.module.css +205 -0
- package/dist/config/i18n.d.ts +46 -0
- package/dist/config/i18n.d.ts.map +1 -0
- package/dist/config/i18n.js +66 -0
- package/dist/hooks/useLanguage.d.ts +12 -0
- package/dist/hooks/useLanguage.d.ts.map +1 -0
- package/dist/hooks/useLanguage.js +61 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/locales/en/common.json +68 -0
- package/{locales → dist/locales}/en/identity-portal.json +69 -69
- package/dist/locales/nl/common.json +68 -0
- package/{locales → dist/locales}/nl/identity-portal.json +69 -69
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils/languageIcons.d.ts +4 -0
- package/dist/utils/languageIcons.d.ts.map +1 -0
- package/dist/utils/languageIcons.js +12 -0
- package/dist/utils/languageUtils.d.ts +45 -0
- package/dist/utils/languageUtils.d.ts.map +1 -0
- package/dist/utils/languageUtils.js +144 -0
- package/package.json +34 -22
- package/LICENSE +0 -23
- package/config/i18n.ts +0 -89
- package/hooks/useLanguage.ts +0 -80
- package/index.ts +0 -21
- package/locales/en/common.json +0 -66
- package/locales/en/web.json +0 -343
- package/locales/nl/common.json +0 -66
- package/locales/nl/web.json +0 -343
- package/tsconfig.json +0 -32
- package/utils/languageUtils.ts +0 -141
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Démonstration Shared i18n",
|
|
3
|
+
"subtitle": "Un wrapper React i18next léger avec persistance des cookies.",
|
|
4
|
+
"cta": "Changer de langue",
|
|
5
|
+
"overview": {
|
|
6
|
+
"heading": "Aperçu",
|
|
7
|
+
"description": "@asafarim/shared-i18n est un package d'internationalisation (i18n) de qualité production pour les applications React.",
|
|
8
|
+
"features": {
|
|
9
|
+
"title": "Fonctionnalités Principales",
|
|
10
|
+
"items": [
|
|
11
|
+
"Support multi-langues avec persistance des cookies",
|
|
12
|
+
"Détection de langue et fallback intégrés",
|
|
13
|
+
"Support TypeScript avec sécurité typage complet",
|
|
14
|
+
"Espaces de noms common et identity-portal préconfigurés",
|
|
15
|
+
"Intégration transparente avec react-i18next",
|
|
16
|
+
"Synchronisation des préférences de langue avec le backend",
|
|
17
|
+
"Utilitaires SSR-safe pour le rendu côté serveur"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"useCases": {
|
|
21
|
+
"title": "Idéal Pour",
|
|
22
|
+
"items": [
|
|
23
|
+
"Applications globales nécessitant un support multi-langues",
|
|
24
|
+
"Plateformes enterprise avec préférences de langue utilisateur",
|
|
25
|
+
"Systèmes d'identité et d'authentification",
|
|
26
|
+
"Systèmes de gestion de contenu avec localisation",
|
|
27
|
+
"Applications SaaS servant des utilisateurs internationaux"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"getStarted": {
|
|
32
|
+
"heading": "Premiers Pas",
|
|
33
|
+
"intro": "Apprenez à intégrer @asafarim/shared-i18n dans votre application React en quelques minutes.",
|
|
34
|
+
"steps": [
|
|
35
|
+
{
|
|
36
|
+
"title": "1. Installer le Package",
|
|
37
|
+
"description": "Ajoutez le package aux dépendances de votre projet.",
|
|
38
|
+
"code": "pnpm add @asafarim/shared-i18n"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"title": "2. Initialiser i18n dans Votre App",
|
|
42
|
+
"description": "Appelez initI18n au démarrage de l'application pour configurer les paramètres de langue.",
|
|
43
|
+
"code": "import { initI18n } from '@asafarim/shared-i18n';\n\ninitI18n({\n defaultNS: 'common',\n ns: ['common', 'identityPortal'],\n resources: /* vos traductions */\n});"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"title": "3. Utiliser le Hook useLanguage",
|
|
47
|
+
"description": "Accédez au changement de langue et à l'état de langue actuel dans vos composants.",
|
|
48
|
+
"code": "const { changeLanguage, isChanging } = useLanguage();\n\n<button onClick={() => changeLanguage('en')}>\n Vers Anglais\n</button>"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"title": "4. Traduire avec useTranslation",
|
|
52
|
+
"description": "Utilisez le hook useTranslation pour accéder aux chaînes traduites.",
|
|
53
|
+
"code": "const { t } = useTranslation('common');\n\n<h1>{t('welcome')}</h1>"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"title": "5. Profiter de la Persistance des Cookies",
|
|
57
|
+
"description": "Les préférences de langue des utilisateurs sont automatiquement sauvegardées et restaurées.",
|
|
58
|
+
"code": "// Sauvegardé automatiquement via cookies\n// Aucune configuration supplémentaire requise !"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"title": "6. Synchroniser avec le Backend (Optionnel)",
|
|
62
|
+
"description": "Mettez à jour les préférences de langue des utilisateurs sur votre backend.",
|
|
63
|
+
"code": "import { updateUserLanguagePreference } from '@asafarim/shared-i18n';\n\nawait updateUserLanguagePreference('fr');"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"tips": {
|
|
67
|
+
"title": "Conseils Pro",
|
|
68
|
+
"items": [
|
|
69
|
+
"Utilisez les espaces de noms pour organiser les traductions par fonctionnalité",
|
|
70
|
+
"Profitez du composant Trans pour les traductions complexes avec variables",
|
|
71
|
+
"Configurez VITE_IDENTITY_API_URL pour la synchronisation backend",
|
|
72
|
+
"Vérifiez la détection de langue du navigateur avec getInitialLanguage()",
|
|
73
|
+
"Combinez avec les design tokens pour des thèmes cohérents dans toutes les langues"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"status": {
|
|
78
|
+
"heading": "Statut en direct",
|
|
79
|
+
"cookie": "Cookie",
|
|
80
|
+
"backend": "Backend",
|
|
81
|
+
"simulate": "Simuler la synchronisation backend",
|
|
82
|
+
"resultOk": "Synchronisation backend réussie",
|
|
83
|
+
"resultFail": "Échec de la synchronisation backend"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "Shared i18n demo",
|
|
3
|
+
"subtitle": "Een lichtgewicht React i18next wrapper met cookie persistentie.",
|
|
4
|
+
"cta": "Taal wisselen",
|
|
5
|
+
"overview": {
|
|
6
|
+
"heading": "Overzicht",
|
|
7
|
+
"description": "@asafarim/shared-i18n is een production-grade internationalisatie (i18n) pakket voor React-applicaties.",
|
|
8
|
+
"features": {
|
|
9
|
+
"title": "Belangrijkste Functies",
|
|
10
|
+
"items": [
|
|
11
|
+
"Ondersteuning voor meerdere talen met cookie persistentie",
|
|
12
|
+
"Ingebouwde taaldetectie en fallback",
|
|
13
|
+
"TypeScript-ondersteuning met volledige type veiligheid",
|
|
14
|
+
"Vooraf geconfigureerde common en identity-portal naamruimten",
|
|
15
|
+
"Naadloze integratie met react-i18next",
|
|
16
|
+
"Synchronisatie van taalvoorkeur met backend",
|
|
17
|
+
"SSR-veilige hulpprogramma's voor server-side rendering"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"useCases": {
|
|
21
|
+
"title": "Ideaal Voor",
|
|
22
|
+
"items": [
|
|
23
|
+
"Globale applicaties die ondersteuning voor meerdere talen vereisen",
|
|
24
|
+
"Enterprise-platforms met taalvoorkeuren van gebruikers",
|
|
25
|
+
"Identiteits- en verificatiesystemen",
|
|
26
|
+
"Content management systemen met lokalisatie",
|
|
27
|
+
"SaaS-applicaties die internationale gebruikers bedienen"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"getStarted": {
|
|
32
|
+
"heading": "Aan de Slag",
|
|
33
|
+
"intro": "Leer hoe je @asafarim/shared-i18n in minuten in je React-applicatie integreert.",
|
|
34
|
+
"steps": [
|
|
35
|
+
{
|
|
36
|
+
"title": "1. Installeer het Pakket",
|
|
37
|
+
"description": "Voeg het pakket toe aan je projectafhankelijkheden.",
|
|
38
|
+
"code": "pnpm add @asafarim/shared-i18n"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"title": "2. Initialiseer i18n in Je App",
|
|
42
|
+
"description": "Roep initI18n aan bij het opstarten van de applicatie om taalinstellingen te configureren.",
|
|
43
|
+
"code": "import { initI18n } from '@asafarim/shared-i18n';\n\ninitI18n({\n defaultNS: 'common',\n ns: ['common', 'identityPortal'],\n resources: { /* je vertalingen */ }\n});"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"title": "3. Gebruik de useLanguage Hook",
|
|
47
|
+
"description": "Krijg toegang tot taalwisseling en huidige taaltoestand in je componenten.",
|
|
48
|
+
"code": "const { changeLanguage, isChanging } = useLanguage();\n\n<button onClick={() => changeLanguage('en')}>\n Naar Engels\n</button>"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"title": "4. Vertaal met useTranslation",
|
|
52
|
+
"description": "Gebruik de useTranslation hook om toegang te krijgen tot vertaalde strings.",
|
|
53
|
+
"code": "const { t } = useTranslation('common');\n\n<h1>{t('welcome')}</h1>"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"title": "5. Maak Gebruik van Cookie Persistentie",
|
|
57
|
+
"description": "Taalvoorkeuren van gebruikers worden automatisch opgeslagen en hersteld.",
|
|
58
|
+
"code": "// Automatisch opgeslagen via cookies\n// Geen extra setup vereist!"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"title": "6. Synchroniseer met Backend (Optioneel)",
|
|
62
|
+
"description": "Werk taalvoorkeuren van gebruikers bij op je backend.",
|
|
63
|
+
"code": "import { updateUserLanguagePreference } from '@asafarim/shared-i18n';\n\nawait updateUserLanguagePreference('nl');"
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"tips": {
|
|
67
|
+
"title": "Pro Tips",
|
|
68
|
+
"items": [
|
|
69
|
+
"Gebruik naamruimten om vertalingen per functie in te delen",
|
|
70
|
+
"Maak gebruik van de Trans-component voor complexe vertalingen met variabelen",
|
|
71
|
+
"Stel VITE_IDENTITY_API_URL in voor backend-synchronisatie",
|
|
72
|
+
"Controleer browsertaldetectie met getInitialLanguage()",
|
|
73
|
+
"Combineer met design tokens voor consistent thema's in alle talen"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"status": {
|
|
78
|
+
"heading": "Live status",
|
|
79
|
+
"cookie": "Cookie",
|
|
80
|
+
"backend": "Backend",
|
|
81
|
+
"simulate": "Backend synchronisatie simuleren",
|
|
82
|
+
"resultOk": "Backend synchronisatie geslaagd",
|
|
83
|
+
"resultFail": "Backend synchronisatie mislukt"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { initI18n } from '@asafarim/shared-i18n'
|
|
4
|
+
import App from './App'
|
|
5
|
+
import './index.css'
|
|
6
|
+
import enDemo from './locales/en/demo.json'
|
|
7
|
+
import nlDemo from './locales/nl/demo.json'
|
|
8
|
+
import frDemo from './locales/fr/demo.json'
|
|
9
|
+
|
|
10
|
+
initI18n({
|
|
11
|
+
defaultNS: 'common',
|
|
12
|
+
ns: ['common', 'demo'],
|
|
13
|
+
resources: {
|
|
14
|
+
en: { demo: enDemo },
|
|
15
|
+
nl: { demo: nlDemo },
|
|
16
|
+
fr: { demo: frDemo }
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
21
|
+
<React.StrictMode>
|
|
22
|
+
<App />
|
|
23
|
+
</React.StrictMode>,
|
|
24
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"jsx": "react-jsx"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "../src/types", "../vite-env.d.ts"],
|
|
17
|
+
"references": [{ "path": "./tsconfig.node.json" }, { "path": "../tsconfig.json" }]
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SupportedLanguage } from "../config/i18n.js";
|
|
2
|
+
export interface LanguageSwitcherProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
style?: React.CSSProperties;
|
|
5
|
+
languages?: readonly SupportedLanguage[];
|
|
6
|
+
variant?: "buttons" | "select" | "icon-dropdown";
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
getLabel?: (lang: SupportedLanguage) => string;
|
|
9
|
+
getIcon?: (lang: SupportedLanguage) => React.ReactNode;
|
|
10
|
+
onChanged?: (lang: SupportedLanguage) => void;
|
|
11
|
+
buttonClassName?: string;
|
|
12
|
+
selectClassName?: string;
|
|
13
|
+
showLabel?: boolean;
|
|
14
|
+
showIcon?: boolean;
|
|
15
|
+
showEmoji?: boolean;
|
|
16
|
+
unstyled?: boolean;
|
|
17
|
+
isToggler?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare const LanguageSwitcher: ({ className, style, languages, variant, disabled, getLabel, getIcon, onChanged, buttonClassName, selectClassName, showLabel, showIcon, showEmoji, unstyled, isToggler, }: LanguageSwitcherProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
//# sourceMappingURL=LanguageSwitcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageSwitcher.d.ts","sourceRoot":"","sources":["../../components/LanguageSwitcher.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA0E,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAInI,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,SAAS,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACzC,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,eAAe,CAAC;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,MAAM,CAAC;IAC/C,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,KAAK,CAAC,SAAS,CAAC;IACvD,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,eAAO,MAAM,gBAAgB,GAAI,0KAgB9B,qBAAqB,4CAyJvB,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect } from "react";
|
|
3
|
+
import { useLanguage } from "../hooks/useLanguage.js";
|
|
4
|
+
import { LANGUAGE_CONFIGS, LANGUAGE_NAMES, LANGUAGE_EMOJIS, SUPPORTED_LANGUAGES } from "../config/i18n.js";
|
|
5
|
+
import { getLanguageFlag } from "../utils/languageIcons.js";
|
|
6
|
+
import styles from "./LanguageSwitcher.module.css";
|
|
7
|
+
export const LanguageSwitcher = ({ className, style, languages = SUPPORTED_LANGUAGES, variant = "buttons", disabled = false, getLabel, getIcon, onChanged, buttonClassName, selectClassName, showLabel = true, showIcon = false, showEmoji = true, unstyled = false, isToggler = true, }) => {
|
|
8
|
+
const { language, changeLanguage, isChanging } = useLanguage();
|
|
9
|
+
const handleChange = async (lang) => {
|
|
10
|
+
await changeLanguage(lang);
|
|
11
|
+
onChanged?.(lang);
|
|
12
|
+
};
|
|
13
|
+
const isDisabled = isChanging || disabled;
|
|
14
|
+
const shouldUseToggler = isToggler && languages.length === 2 && variant === "select";
|
|
15
|
+
const getSelectClassName = () => {
|
|
16
|
+
if (selectClassName)
|
|
17
|
+
return selectClassName;
|
|
18
|
+
if (unstyled)
|
|
19
|
+
return className || "";
|
|
20
|
+
return `${styles.switcher} ${className || ""}`.trim();
|
|
21
|
+
};
|
|
22
|
+
const getButtonsClassName = () => {
|
|
23
|
+
if (unstyled)
|
|
24
|
+
return className || "";
|
|
25
|
+
return `${styles.switcher} ${className || ""}`.trim();
|
|
26
|
+
};
|
|
27
|
+
if (variant === "icon-dropdown") {
|
|
28
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
29
|
+
const dropdownRef = useRef(null);
|
|
30
|
+
const currentLang = languages.find((lang) => lang === language);
|
|
31
|
+
const currentLabel = currentLang ? (getLabel ? getLabel(currentLang) : LANGUAGE_NAMES[currentLang]) : "";
|
|
32
|
+
const currentFlag = currentLang ? getLanguageFlag(currentLang) : "🌐";
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const handleClickOutside = (event) => {
|
|
35
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
36
|
+
setIsOpen(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (isOpen) {
|
|
40
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
41
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
42
|
+
}
|
|
43
|
+
}, [isOpen]);
|
|
44
|
+
const handleSelect = async (lang) => {
|
|
45
|
+
await handleChange(lang);
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
};
|
|
48
|
+
return (_jsxs("div", { ref: dropdownRef, className: `${styles.iconDropdownContainer} ${selectClassName || className || ""}`.trim(), style: style, children: [_jsx("button", { className: styles.iconDropdownButton, onClick: () => setIsOpen(!isOpen), disabled: isDisabled, title: currentLabel, "aria-label": `Select language: ${currentLabel}`, "aria-expanded": isOpen, "aria-haspopup": "listbox", children: currentFlag }), isOpen && (_jsx("div", { className: styles.iconDropdownMenu, role: "listbox", children: languages.map((lang) => (_jsx("button", { className: `${styles.iconDropdownOption} ${lang === language ? styles.active : ""}`, onClick: () => handleSelect(lang), role: "option", "aria-selected": lang === language, title: getLabel ? getLabel(lang) : LANGUAGE_NAMES[lang], children: showEmoji ? LANGUAGE_EMOJIS[lang] : (getLabel ? getLabel(lang) : LANGUAGE_NAMES[lang]) }, lang))) }))] }));
|
|
49
|
+
}
|
|
50
|
+
if (shouldUseToggler) {
|
|
51
|
+
const otherLang = languages.find((lang) => lang !== language);
|
|
52
|
+
const currentLangConfig = LANGUAGE_CONFIGS.find(config => config.code === language);
|
|
53
|
+
return (_jsx("button", { className: `${styles.toggler} ${selectClassName || className || ""}`.trim(), style: style, onClick: () => handleChange(otherLang), disabled: isDisabled, "aria-label": `Switch to ${getLabel ? getLabel(otherLang) : LANGUAGE_NAMES[otherLang]}`, title: `Switch to ${getLabel ? getLabel(otherLang) : LANGUAGE_NAMES[otherLang]}`, children: showEmoji ? (currentLangConfig && LANGUAGE_EMOJIS[currentLangConfig.code]) : (getLabel ? getLabel(language) : LANGUAGE_NAMES[language]) }));
|
|
54
|
+
}
|
|
55
|
+
if (variant === "select") {
|
|
56
|
+
return (_jsx("select", { className: getSelectClassName(), style: style, value: language, onChange: (e) => handleChange(e.target.value), disabled: isDisabled, children: languages.map((lang) => {
|
|
57
|
+
const displayText = showEmoji ? LANGUAGE_EMOJIS[lang] : (getLabel ? getLabel(lang) : LANGUAGE_NAMES[lang]);
|
|
58
|
+
return (_jsx("option", { value: lang, children: displayText }, lang));
|
|
59
|
+
}) }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsx("div", { className: getButtonsClassName(), style: style, children: languages.map((lang) => {
|
|
62
|
+
const isActive = lang === language;
|
|
63
|
+
const btnClass = buttonClassName
|
|
64
|
+
? `${buttonClassName}${isActive ? " active" : ""}`
|
|
65
|
+
: isActive
|
|
66
|
+
? "active"
|
|
67
|
+
: "";
|
|
68
|
+
const label = getLabel ? getLabel(lang) : LANGUAGE_NAMES[lang];
|
|
69
|
+
const icon = getIcon ? getIcon(lang) : null;
|
|
70
|
+
return (_jsxs("button", { className: btnClass, onClick: () => handleChange(lang), disabled: isDisabled, "aria-pressed": isActive, "data-active": isActive, "aria-label": !showLabel ? label : undefined, title: !showLabel ? label : undefined, children: [showIcon && icon, showLabel && label] }, lang));
|
|
71
|
+
}) }));
|
|
72
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
@import "@asafarim/design-tokens/css";
|
|
2
|
+
|
|
3
|
+
.switcher {
|
|
4
|
+
display: flex;
|
|
5
|
+
gap: var(--asm-space-2);
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
align-items: center;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.switcher button {
|
|
11
|
+
padding: var(--asm-space-2) var(--asm-space-4);
|
|
12
|
+
border: 1px solid var(--asm-color-border);
|
|
13
|
+
background: var(--asm-color-surface);
|
|
14
|
+
border-radius: var(--asm-radius-md);
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
font-size: var(--asm-font-size-sm);
|
|
17
|
+
font-weight: 500;
|
|
18
|
+
transition: all var(--asm-motion-duration-fast) var(--asm-motion-easing-standard);
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: var(--asm-space-2);
|
|
22
|
+
min-width: 2.5rem;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
color: var(--asm-color-text);
|
|
25
|
+
white-space: nowrap;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.switcher button:hover:not(:disabled) {
|
|
29
|
+
background: var(--asm-color-primary-600);
|
|
30
|
+
color: var(--asm-color-bg);
|
|
31
|
+
border-color: var(--asm-color-primary-600);
|
|
32
|
+
box-shadow: var(--asm-effect-shadow-sm);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.switcher button:disabled {
|
|
36
|
+
opacity: 0.5;
|
|
37
|
+
cursor: not-allowed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.switcher button.active {
|
|
41
|
+
background: var(--asm-color-primary-600);
|
|
42
|
+
color: var(--asm-color-bg);
|
|
43
|
+
border-color: var(--asm-color-primary-600);
|
|
44
|
+
box-shadow: var(--asm-effect-shadow-md);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.switcher button.active:hover:not(:disabled) {
|
|
48
|
+
background: var(--asm-color-primary-700);
|
|
49
|
+
border-color: var(--asm-color-primary-700);
|
|
50
|
+
box-shadow: var(--asm-effect-shadow-lg);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.switcher select {
|
|
54
|
+
padding: var(--asm-space-2) calc(var(--asm-space-4) * 2) var(--asm-space-2) var(--asm-space-3);
|
|
55
|
+
border: 1px solid var(--asm-color-border);
|
|
56
|
+
background: var(--asm-color-surface);
|
|
57
|
+
border-radius: var(--asm-radius-md);
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
font-size: var(--asm-font-size-sm);
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
appearance: none;
|
|
62
|
+
color: var(--asm-color-text);
|
|
63
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
|
64
|
+
background-repeat: no-repeat;
|
|
65
|
+
background-position: right var(--asm-space-3) center;
|
|
66
|
+
transition: all var(--asm-motion-duration-fast) var(--asm-motion-easing-standard);
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.switcher select:hover:not(:disabled) {
|
|
71
|
+
border-color: var(--asm-color-primary-600);
|
|
72
|
+
background-color: var(--asm-color-surface-muted);
|
|
73
|
+
box-shadow: var(--asm-effect-shadow-sm);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.switcher select:disabled {
|
|
77
|
+
opacity: 0.5;
|
|
78
|
+
cursor: not-allowed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.iconDropdownContainer {
|
|
82
|
+
position: relative;
|
|
83
|
+
display: inline-block;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.iconDropdownButton {
|
|
87
|
+
padding: 0;
|
|
88
|
+
border: 1px solid var(--asm-color-border);
|
|
89
|
+
background: var(--asm-color-surface);
|
|
90
|
+
border-radius: var(--asm-radius-md);
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
font-size: 1.25rem;
|
|
93
|
+
color: var(--asm-color-text);
|
|
94
|
+
width: 2.75rem;
|
|
95
|
+
height: 2.75rem;
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
transition: all var(--asm-motion-duration-fast) var(--asm-motion-easing-standard);
|
|
100
|
+
flex-shrink: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.iconDropdownButton:hover:not(:disabled) {
|
|
104
|
+
border-color: var(--asm-color-primary-600);
|
|
105
|
+
background: var(--asm-color-surface-muted);
|
|
106
|
+
box-shadow: var(--asm-effect-shadow-sm);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.iconDropdownButton:disabled {
|
|
110
|
+
opacity: 0.5;
|
|
111
|
+
cursor: not-allowed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.iconDropdownMenu {
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: calc(100% + var(--asm-space-2));
|
|
117
|
+
left: 0;
|
|
118
|
+
background: var(--asm-color-surface);
|
|
119
|
+
border: 1px solid var(--asm-color-border);
|
|
120
|
+
border-radius: var(--asm-radius-md);
|
|
121
|
+
box-shadow: var(--asm-effect-shadow-lg);
|
|
122
|
+
z-index: 1000;
|
|
123
|
+
min-width: 10rem;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.iconDropdownOption {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
gap: var(--asm-space-2);
|
|
131
|
+
width: 100%;
|
|
132
|
+
padding: var(--asm-space-2) var(--asm-space-3);
|
|
133
|
+
border: none;
|
|
134
|
+
background: transparent;
|
|
135
|
+
color: var(--asm-color-text);
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
font-size: var(--asm-font-size-sm);
|
|
138
|
+
font-weight: 500;
|
|
139
|
+
text-align: left;
|
|
140
|
+
transition: all var(--asm-motion-duration-fast) var(--asm-motion-easing-standard);
|
|
141
|
+
white-space: nowrap;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.iconDropdownOption:hover:not(:disabled) {
|
|
145
|
+
background: var(--asm-color-surface-muted);
|
|
146
|
+
color: var(--asm-color-text);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.iconDropdownOption.active {
|
|
150
|
+
background: var(--asm-color-primary-600);
|
|
151
|
+
color: var(--asm-color-bg);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.iconDropdownOption.active:hover {
|
|
155
|
+
background: var(--asm-color-primary-700);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.toggler {
|
|
159
|
+
padding: var(--asm-space-2) var(--asm-space-4);
|
|
160
|
+
border: 1px solid var(--asm-color-border);
|
|
161
|
+
background: var(--asm-color-transparent);
|
|
162
|
+
border-radius: var(--asm-radius-md);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
font-size: var(--asm-font-size-sm);
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
transition: all var(--asm-motion-duration-fast) var(--asm-motion-easing-standard);
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: var(--asm-space-2);
|
|
170
|
+
color: var(--asm-color-text);
|
|
171
|
+
white-space: nowrap;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.toggler:hover:not(:disabled) {
|
|
175
|
+
background: var(--asm-color-primary-600);
|
|
176
|
+
color: var(--asm-color-bg);
|
|
177
|
+
border-color: var(--asm-color-primary-600);
|
|
178
|
+
box-shadow: var(--asm-effect-shadow-sm);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.toggler:disabled {
|
|
182
|
+
opacity: 0.5;
|
|
183
|
+
cursor: not-allowed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@media (max-width: 640px) {
|
|
187
|
+
.switcher button {
|
|
188
|
+
padding: var(--asm-space-1) var(--asm-space-3);
|
|
189
|
+
font-size: var(--asm-font-size-xs);
|
|
190
|
+
min-width: 2.25rem;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.switcher select {
|
|
194
|
+
font-size: var(--asm-font-size-xs);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.toggler {
|
|
198
|
+
padding: var(--asm-space-1) var(--asm-space-2);
|
|
199
|
+
font-size: var(--asm-font-size-xs);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.togglerLabel {
|
|
203
|
+
padding: var(--asm-space-1) var(--asm-space-1);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
export declare const LANGUAGE_CONFIGS: readonly [{
|
|
3
|
+
readonly code: "en";
|
|
4
|
+
readonly name: "English";
|
|
5
|
+
readonly emoji: "🌍";
|
|
6
|
+
}, {
|
|
7
|
+
readonly code: "nl";
|
|
8
|
+
readonly name: "Nederlands";
|
|
9
|
+
readonly emoji: "🇳🇱";
|
|
10
|
+
}, {
|
|
11
|
+
readonly code: "fr";
|
|
12
|
+
readonly name: "Français";
|
|
13
|
+
readonly emoji: "🇫🇷";
|
|
14
|
+
}, {
|
|
15
|
+
readonly code: "de";
|
|
16
|
+
readonly name: "Deutsch";
|
|
17
|
+
readonly emoji: "🇩🇪";
|
|
18
|
+
}, {
|
|
19
|
+
readonly code: "es";
|
|
20
|
+
readonly name: "Español";
|
|
21
|
+
readonly emoji: "🇪🇸";
|
|
22
|
+
}, {
|
|
23
|
+
readonly code: "it";
|
|
24
|
+
readonly name: "Italiano";
|
|
25
|
+
readonly emoji: "🇮🇹";
|
|
26
|
+
}, {
|
|
27
|
+
readonly code: "pt";
|
|
28
|
+
readonly name: "Português";
|
|
29
|
+
readonly emoji: "🇵🇹";
|
|
30
|
+
}];
|
|
31
|
+
export type SupportedLanguage = typeof LANGUAGE_CONFIGS[number]['code'];
|
|
32
|
+
export declare const SUPPORTED_LANGUAGES: readonly SupportedLanguage[];
|
|
33
|
+
export declare const LANGUAGE_NAMES: Record<SupportedLanguage, string>;
|
|
34
|
+
export declare const LANGUAGE_EMOJIS: Record<SupportedLanguage, string>;
|
|
35
|
+
export declare const DEFAULT_LANGUAGE: SupportedLanguage;
|
|
36
|
+
export declare const LANGUAGE_COOKIE_NAME = "preferredLanguage";
|
|
37
|
+
export interface I18nConfig {
|
|
38
|
+
defaultNS?: string;
|
|
39
|
+
ns?: string[];
|
|
40
|
+
resources?: Record<string, Record<string, any>>;
|
|
41
|
+
supportedLngs?: string[];
|
|
42
|
+
defaultLanguage?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare const initI18n: (config?: I18nConfig) => import("i18next").i18n;
|
|
45
|
+
export default i18n;
|
|
46
|
+
//# sourceMappingURL=i18n.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../config/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,SAAS,CAAC;AAY3B,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAiW,CAAC;AAC/X,MAAM,MAAM,iBAAiB,GAAG,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AACxE,eAAO,MAAM,mBAAmB,EAAkD,SAAS,iBAAiB,EAAE,CAAC;AAE/G,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAEvB,CAAC;AAEvC,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAExB,CAAC;AAEvC,eAAO,MAAM,gBAAgB,EAAE,iBAAwB,CAAC;AACxD,eAAO,MAAM,oBAAoB,sBAAsB,CAAC;AAExD,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAChD,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,UAAU,2BAuD3C,CAAC;AAEF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import i18n from 'i18next';
|
|
2
|
+
import { initReactI18next } from 'react-i18next';
|
|
3
|
+
import LanguageDetector from 'i18next-browser-languagedetector';
|
|
4
|
+
// Import common translations
|
|
5
|
+
import enCommon from '../locales/en/common.json';
|
|
6
|
+
import nlCommon from '../locales/nl/common.json';
|
|
7
|
+
// Import identity-portal translations
|
|
8
|
+
import enIdentityPortal from '../locales/en/identity-portal.json';
|
|
9
|
+
import nlIdentityPortal from '../locales/nl/identity-portal.json';
|
|
10
|
+
export const LANGUAGE_CONFIGS = [{ code: 'en', name: 'English', emoji: '🌍' }, { code: 'nl', name: 'Nederlands', emoji: '🇳🇱' }, { code: 'fr', name: 'Français', emoji: '🇫🇷' }, { code: 'de', name: 'Deutsch', emoji: '🇩🇪' }, { code: 'es', name: 'Español', emoji: '🇪🇸' }, { code: 'it', name: 'Italiano', emoji: '🇮🇹' }, { code: 'pt', name: 'Português', emoji: '🇵🇹' }];
|
|
11
|
+
export const SUPPORTED_LANGUAGES = LANGUAGE_CONFIGS.map(config => config.code);
|
|
12
|
+
export const LANGUAGE_NAMES = Object.fromEntries(LANGUAGE_CONFIGS.map(config => [config.code, config.name]));
|
|
13
|
+
export const LANGUAGE_EMOJIS = Object.fromEntries(LANGUAGE_CONFIGS.map(config => [config.code, config.emoji]));
|
|
14
|
+
export const DEFAULT_LANGUAGE = 'en';
|
|
15
|
+
export const LANGUAGE_COOKIE_NAME = 'preferredLanguage';
|
|
16
|
+
export const initI18n = (config) => {
|
|
17
|
+
const resources = config?.resources;
|
|
18
|
+
const supportedLngs = config?.supportedLngs;
|
|
19
|
+
const defaultLanguage = config?.defaultLanguage;
|
|
20
|
+
const defaultNS = config?.defaultNS;
|
|
21
|
+
const ns = config?.ns;
|
|
22
|
+
// const {
|
|
23
|
+
// defaultNS = 'common',
|
|
24
|
+
// ns = ['common', 'identityPortal'],
|
|
25
|
+
// resources,
|
|
26
|
+
// supportedLngs,
|
|
27
|
+
// defaultLanguage,
|
|
28
|
+
// } = config || {};
|
|
29
|
+
// Merge common translations with app-specific resources
|
|
30
|
+
const mergedResources = { ...config?.resources };
|
|
31
|
+
mergedResources.en = {
|
|
32
|
+
common: enCommon,
|
|
33
|
+
identityPortal: enIdentityPortal,
|
|
34
|
+
...(resources?.en || {}),
|
|
35
|
+
};
|
|
36
|
+
mergedResources.nl = {
|
|
37
|
+
common: nlCommon,
|
|
38
|
+
identityPortal: nlIdentityPortal,
|
|
39
|
+
...(resources?.nl || {}),
|
|
40
|
+
};
|
|
41
|
+
const finalSupportedLngs = supportedLngs ?? Object.keys(mergedResources);
|
|
42
|
+
const fallbackLng = defaultLanguage ?? DEFAULT_LANGUAGE;
|
|
43
|
+
i18n
|
|
44
|
+
.use(initReactI18next)
|
|
45
|
+
.use(LanguageDetector)
|
|
46
|
+
.init({
|
|
47
|
+
resources: mergedResources,
|
|
48
|
+
fallbackLng,
|
|
49
|
+
defaultNS,
|
|
50
|
+
ns,
|
|
51
|
+
supportedLngs: finalSupportedLngs,
|
|
52
|
+
detection: {
|
|
53
|
+
order: ['cookie', 'navigator'],
|
|
54
|
+
caches: ['cookie'],
|
|
55
|
+
lookupCookie: LANGUAGE_COOKIE_NAME
|
|
56
|
+
},
|
|
57
|
+
interpolation: {
|
|
58
|
+
escapeValue: false // React already escapes
|
|
59
|
+
},
|
|
60
|
+
react: {
|
|
61
|
+
useSuspense: false
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return i18n;
|
|
65
|
+
};
|
|
66
|
+
export default i18n;
|