@asafarim/shared-i18n 0.8.0 → 0.9.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.
Files changed (94) hide show
  1. package/README.md +266 -280
  2. package/demo/README.md +119 -0
  3. package/demo/index.html +12 -1
  4. package/demo/node_modules/.bin/kill-port +17 -0
  5. package/demo/node_modules/.bin/tsc +5 -9
  6. package/demo/node_modules/.bin/tsserver +5 -9
  7. package/demo/node_modules/.bin/vite +5 -9
  8. package/demo/package.json +7 -4
  9. package/demo/public/404.html +24 -0
  10. package/demo/public/favicon.svg +4 -4
  11. package/demo/public/logo.svg +24 -24
  12. package/demo/src/App.tsx +178 -129
  13. package/demo/src/components/CountryLanguageSelectorsPage.tsx +240 -0
  14. package/demo/src/components/GetStartedSection.tsx +56 -56
  15. package/demo/src/components/KeyTable.tsx +29 -29
  16. package/demo/src/components/LanguageBar.tsx +145 -103
  17. package/demo/src/components/LanguageSwitcherDemo.module.css +114 -114
  18. package/demo/src/components/LanguageSwitchersPage.tsx +245 -0
  19. package/demo/src/components/Logo.tsx +6 -6
  20. package/demo/src/components/OverviewSection.tsx +58 -43
  21. package/demo/src/components/Panel.tsx +15 -15
  22. package/demo/src/components/RoutingLabPage.tsx +147 -0
  23. package/demo/src/components/StatusCard.tsx +109 -109
  24. package/demo/src/data/countries.ts +48 -0
  25. package/demo/src/i18n/localeAdapter.ts +91 -0
  26. package/demo/src/i18n/localeRouting.ts +77 -0
  27. package/demo/src/index.css +1075 -644
  28. package/demo/src/locales/de/demo.json +202 -84
  29. package/demo/src/locales/en/demo.json +201 -85
  30. package/demo/src/locales/fr/demo.json +203 -85
  31. package/demo/src/locales/it/demo.json +202 -84
  32. package/demo/src/locales/lb/demo.json +201 -0
  33. package/demo/src/locales/nl/demo.json +203 -85
  34. package/demo/src/main.tsx +32 -29
  35. package/demo/tsconfig.json +18 -18
  36. package/demo/tsconfig.node.json +10 -10
  37. package/demo/tsconfig.tsbuildinfo +1 -1
  38. package/demo/vite-env.d.ts +7 -7
  39. package/demo/vite.config.d.ts +2 -2
  40. package/demo/vite.config.js +10 -10
  41. package/dist/components/LanguageSwitcher.module.css +303 -303
  42. package/dist/country-language-selector.css +431 -0
  43. package/dist/index.d.ts +2 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +2 -0
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/package.json +87 -85
  48. package/demo/dist/Icon Dropdown_Limited Languages.png +0 -0
  49. package/demo/dist/Select Dropdown_Text Only.png +0 -0
  50. package/demo/dist/assets/favicon-BZYZvBLo.svg +0 -4
  51. package/demo/dist/assets/index-BdjqKw_N.css +0 -1
  52. package/demo/dist/assets/index-C1Tq1uEr.js +0 -191
  53. package/demo/dist/favicon.svg +0 -4
  54. package/demo/dist/index.html +0 -27
  55. package/demo/dist/logo.svg +0 -24
  56. package/demo/node_modules/.bin/browserslist +0 -21
  57. package/demo/node_modules/.bin/browserslist.CMD +0 -12
  58. package/demo/node_modules/.bin/browserslist.ps1 +0 -41
  59. package/demo/node_modules/.bin/tsc.CMD +0 -12
  60. package/demo/node_modules/.bin/tsc.ps1 +0 -41
  61. package/demo/node_modules/.bin/tsserver.CMD +0 -12
  62. package/demo/node_modules/.bin/tsserver.ps1 +0 -41
  63. package/demo/node_modules/.bin/vite.CMD +0 -12
  64. package/demo/node_modules/.bin/vite.ps1 +0 -41
  65. package/demo/node_modules/.vite/deps/@asafarim_country-language-selector.js +0 -848
  66. package/demo/node_modules/.vite/deps/@asafarim_country-language-selector.js.map +0 -7
  67. package/demo/node_modules/.vite/deps/_metadata.json +0 -76
  68. package/demo/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
  69. package/demo/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
  70. package/demo/node_modules/.vite/deps/chunk-B3AHR5EX.js +0 -1004
  71. package/demo/node_modules/.vite/deps/chunk-B3AHR5EX.js.map +0 -7
  72. package/demo/node_modules/.vite/deps/chunk-E6BG6WAU.js +0 -292
  73. package/demo/node_modules/.vite/deps/chunk-E6BG6WAU.js.map +0 -7
  74. package/demo/node_modules/.vite/deps/chunk-MVARZQEG.js +0 -280
  75. package/demo/node_modules/.vite/deps/chunk-MVARZQEG.js.map +0 -7
  76. package/demo/node_modules/.vite/deps/i18next-browser-languagedetector.js +0 -400
  77. package/demo/node_modules/.vite/deps/i18next-browser-languagedetector.js.map +0 -7
  78. package/demo/node_modules/.vite/deps/i18next.js +0 -2392
  79. package/demo/node_modules/.vite/deps/i18next.js.map +0 -7
  80. package/demo/node_modules/.vite/deps/package.json +0 -3
  81. package/demo/node_modules/.vite/deps/react-dom.js +0 -6
  82. package/demo/node_modules/.vite/deps/react-dom.js.map +0 -7
  83. package/demo/node_modules/.vite/deps/react-dom_client.js +0 -20217
  84. package/demo/node_modules/.vite/deps/react-dom_client.js.map +0 -7
  85. package/demo/node_modules/.vite/deps/react-i18next.js +0 -869
  86. package/demo/node_modules/.vite/deps/react-i18next.js.map +0 -7
  87. package/demo/node_modules/.vite/deps/react.js +0 -5
  88. package/demo/node_modules/.vite/deps/react.js.map +0 -7
  89. package/demo/node_modules/.vite/deps/react_jsx-dev-runtime.js +0 -278
  90. package/demo/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +0 -7
  91. package/demo/node_modules/.vite/deps/react_jsx-runtime.js +0 -6
  92. package/demo/node_modules/.vite/deps/react_jsx-runtime.js.map +0 -7
  93. package/demo/src/components/CountryLanguageDemo.tsx +0 -140
  94. package/demo/src/components/LanguageSwitcherDemo.tsx +0 -256
@@ -0,0 +1,245 @@
1
+ import { useState } from 'react'
2
+ import { LanguageSwitcher, useTranslation } from '@asafarim/shared-i18n'
3
+ import type { Locale } from '@asafarim/shared-i18n'
4
+ import type { SupportedLanguage } from '@asafarim/shared-i18n'
5
+ import { countries } from '../data/countries'
6
+ import { resolveLocaleFromLanguage } from '../i18n/localeAdapter'
7
+ import type { ResolveLocaleResult } from '../i18n/localeAdapter'
8
+
9
+ interface Props {
10
+ locale: Locale
11
+ onLocaleChange: (locale: Locale) => void
12
+ }
13
+
14
+ interface DemoItemProps {
15
+ title: string
16
+ description: string
17
+ code: string
18
+ notice?: ResolveLocaleResult | null
19
+ children: React.ReactNode
20
+ previewLabel?: string
21
+ codeLabel?: string
22
+ note?: string
23
+ }
24
+
25
+ function DemoItem({ title, description, code, notice, children, previewLabel = 'Preview', codeLabel = 'Code', note }: DemoItemProps) {
26
+ return (
27
+ <article className="lab-item">
28
+ <header className="lab-item__head">
29
+ <h3 className="lab-item__title">{title}</h3>
30
+ <p className="lab-item__desc">{description}</p>
31
+ </header>
32
+ <div className="lab-item__body">
33
+ <div className="lab-item__preview">
34
+ <span className="lab-item__caption">{previewLabel}</span>
35
+ {children}
36
+ {notice?.reason === 'fallback-country' && (
37
+ <p className="notice notice--warn">{notice.message}</p>
38
+ )}
39
+ {notice?.reason === 'unsupported' && (
40
+ <p className="notice notice--err">{notice.message}</p>
41
+ )}
42
+ </div>
43
+ <div className="lab-item__code">
44
+ <span className="lab-item__caption">{codeLabel}</span>
45
+ <pre><code>{code}</code></pre>
46
+ </div>
47
+ </div>
48
+ {note && (
49
+ <p className="lab-item__note">
50
+ ℹ️ {note}
51
+ </p>
52
+ )}
53
+ </article>
54
+ )
55
+ }
56
+
57
+ export default function LanguageSwitchersPage({ locale, onLocaleChange }: Props) {
58
+ const { t } = useTranslation('demo')
59
+ const [lastResult, setLastResult] = useState<ResolveLocaleResult | null>(null)
60
+
61
+ const makeHandler = () => (lang: SupportedLanguage) => {
62
+ const result = resolveLocaleFromLanguage(locale, lang, countries)
63
+ setLastResult(result)
64
+ if (result.reason !== 'unsupported') onLocaleChange(result.locale)
65
+ }
66
+
67
+ return (
68
+ <div className="lab">
69
+ <header className="section-head">
70
+ <h2>{t('languageSwitchers.heading')}</h2>
71
+ <p>{t('languageSwitchers.intro')}</p>
72
+ </header>
73
+
74
+ <div className="switcher-distinction surface">
75
+ <h3>{t('languageSwitchers.distinction.title')}</h3>
76
+ <p>{t('languageSwitchers.distinction.p1')}</p>
77
+ <ul className="slug-list">
78
+ <li><code>be-en</code> — Belgium English</li>
79
+ <li><code>nl-en</code> — Netherlands English</li>
80
+ <li><code>lu-en</code> — Luxembourg English</li>
81
+ <li><code>gb-en</code> — UK English</li>
82
+ </ul>
83
+ <p>
84
+ {t('languageSwitchers.distinction.p2')}{' '}
85
+ <strong>{t('languageSwitchers.distinction.link')}</strong>
86
+ </p>
87
+ </div>
88
+
89
+ <div className="comparison-table surface">
90
+ <h3>{t('languageSwitchers.comparison.title')}</h3>
91
+ <div className="tbl-wrap">
92
+ <table>
93
+ <thead>
94
+ <tr>
95
+ <th>{t('languageSwitchers.comparison.capability')}</th>
96
+ <th>{t('languageSwitchers.comparison.ls')}</th>
97
+ <th>{t('languageSwitchers.comparison.cls')}</th>
98
+ </tr>
99
+ </thead>
100
+ <tbody>
101
+ <tr><td>{t('languageSwitchers.comparison.changesLang')}</td><td className="yes">{t('languageSwitchers.comparison.yes')}</td><td className="yes">{t('languageSwitchers.comparison.yesVia')}</td></tr>
102
+ <tr><td>{t('languageSwitchers.comparison.knowsCountry')}</td><td className="no">{t('languageSwitchers.comparison.no')}</td><td className="yes">{t('languageSwitchers.comparison.yes')}</td></tr>
103
+ <tr><td>{t('languageSwitchers.comparison.representsBeEn')}</td><td className="no">{t('languageSwitchers.comparison.no')}</td><td className="yes">{t('languageSwitchers.comparison.yes')}</td></tr>
104
+ <tr><td>{t('languageSwitchers.comparison.distinguishes')}</td><td className="no">{t('languageSwitchers.comparison.no')}</td><td className="yes">{t('languageSwitchers.comparison.yes')}</td></tr>
105
+ <tr><td>{t('languageSwitchers.comparison.bestUrls')}</td><td className="no">{t('languageSwitchers.comparison.needsAdapter')}</td><td className="yes">{t('languageSwitchers.comparison.yes')}</td></tr>
106
+ <tr><td>{t('languageSwitchers.comparison.bestTransOnly')}</td><td className="yes">{t('languageSwitchers.comparison.yes')}</td><td>{t('languageSwitchers.comparison.optional')}</td></tr>
107
+ </tbody>
108
+ </table>
109
+ </div>
110
+ </div>
111
+
112
+ <div className="lab__grid">
113
+ <DemoItem
114
+ title={t('languageSwitchers.variants.0.title')}
115
+ description={t('languageSwitchers.variants.0.desc')}
116
+ notice={lastResult}
117
+ previewLabel={t('languageSwitchers.preview')}
118
+ codeLabel={t('languageSwitchers.code')}
119
+ note={t('languageSwitchers.note')}
120
+ code={`import { LanguageSwitcher } from '@asafarim/shared-i18n'
121
+
122
+ <LanguageSwitcher
123
+ variant="buttons"
124
+ onChanged={(lang) => {
125
+ const { locale } = resolveLocaleFromLanguage(currentLocale, lang, countries)
126
+ navigate(locale)
127
+ }}
128
+ />`}
129
+ >
130
+ <LanguageSwitcher
131
+ variant="buttons"
132
+ onChanged={makeHandler()}
133
+ />
134
+ </DemoItem>
135
+
136
+ <DemoItem
137
+ title={t('languageSwitchers.variants.1.title')}
138
+ description={t('languageSwitchers.variants.1.desc')}
139
+ notice={lastResult}
140
+ previewLabel={t('languageSwitchers.preview')}
141
+ codeLabel={t('languageSwitchers.code')}
142
+ note={t('languageSwitchers.note')}
143
+ code={`<LanguageSwitcher
144
+ variant="select"
145
+ showEmoji={false}
146
+ onChanged={makeHandler()}
147
+ />`}
148
+ >
149
+ <LanguageSwitcher
150
+ variant="select"
151
+ showEmoji={false}
152
+ showLabel={true}
153
+ unstyled={true}
154
+ onChanged={makeHandler()}
155
+ />
156
+ </DemoItem>
157
+
158
+ <DemoItem
159
+ title={t('languageSwitchers.variants.2.title')}
160
+ description={t('languageSwitchers.variants.2.desc')}
161
+ notice={lastResult}
162
+ previewLabel={t('languageSwitchers.preview')}
163
+ codeLabel={t('languageSwitchers.code')}
164
+ note={t('languageSwitchers.note')}
165
+ code={`<LanguageSwitcher
166
+ variant="icon-dropdown"
167
+ onChanged={makeHandler()}
168
+ />`}
169
+ >
170
+ <LanguageSwitcher
171
+ variant="icon-dropdown"
172
+ onChanged={makeHandler()}
173
+ />
174
+ </DemoItem>
175
+
176
+ <DemoItem
177
+ title={t('languageSwitchers.variants.3.title')}
178
+ description={t('languageSwitchers.variants.3.desc')}
179
+ notice={lastResult}
180
+ previewLabel={t('languageSwitchers.preview')}
181
+ codeLabel={t('languageSwitchers.code')}
182
+ note={t('languageSwitchers.note')}
183
+ code={`<LanguageSwitcher
184
+ variant="icon-dropdown"
185
+ languages={['en', 'nl', 'fr', 'de']}
186
+ onChanged={makeHandler()}
187
+ />`}
188
+ >
189
+ <LanguageSwitcher
190
+ variant="icon-dropdown"
191
+ languages={['en', 'nl', 'fr', 'de']}
192
+ onChanged={makeHandler()}
193
+ />
194
+ </DemoItem>
195
+
196
+ <DemoItem
197
+ title={t('languageSwitchers.variants.4.title')}
198
+ description={t('languageSwitchers.variants.4.desc')}
199
+ notice={lastResult}
200
+ previewLabel={t('languageSwitchers.preview')}
201
+ codeLabel={t('languageSwitchers.code')}
202
+ note={t('languageSwitchers.note')}
203
+ code={`<LanguageSwitcher
204
+ variant="icon-dropdown"
205
+ languages={['en', 'nl', 'fr', 'de']}
206
+ showLabelInIconDropdown={true}
207
+ onChanged={makeHandler()}
208
+ />`}
209
+ >
210
+ <LanguageSwitcher
211
+ variant="icon-dropdown"
212
+ languages={['en', 'nl', 'fr', 'de']}
213
+ showLabelInIconDropdown={true}
214
+ onChanged={makeHandler()}
215
+ />
216
+ </DemoItem>
217
+
218
+ <DemoItem
219
+ title={t('languageSwitchers.variants.5.title')}
220
+ description={t('languageSwitchers.variants.5.desc')}
221
+ notice={lastResult}
222
+ previewLabel={t('languageSwitchers.preview')}
223
+ codeLabel={t('languageSwitchers.code')}
224
+ note={t('languageSwitchers.note')}
225
+ code={`<LanguageSwitcher
226
+ variant="select"
227
+ languages={['en', 'nl']}
228
+ isToggler={true}
229
+ showEmoji={true}
230
+ onChanged={makeHandler()}
231
+ />`}
232
+ >
233
+ <LanguageSwitcher
234
+ variant="select"
235
+ languages={['en', 'nl']}
236
+ isToggler={true}
237
+ showEmoji={true}
238
+ unstyled={true}
239
+ onChanged={makeHandler()}
240
+ />
241
+ </DemoItem>
242
+ </div>
243
+ </div>
244
+ )
245
+ }
@@ -1,7 +1,7 @@
1
- export default function Logo({width = 70}: {width?: number}) {
2
- return (
3
- <div className="logo">
4
- <img src={import.meta.env.BASE_URL + "logo.svg"} alt="Logo" width={width} />
5
- </div>
6
- )
1
+ export default function Logo({width = 70}: {width?: number}) {
2
+ return (
3
+ <div className="logo">
4
+ <img src={import.meta.env.BASE_URL + "logo.svg"} alt="Logo" width={width} />
5
+ </div>
6
+ )
7
7
  }
@@ -1,43 +1,58 @@
1
- import { useTranslation } from '@asafarim/shared-i18n'
2
-
3
- export default function OverviewSection() {
4
- const { t } = useTranslation('demo')
5
- const overview = t('overview', { returnObjects: true }) as any
6
-
7
- return (
8
- <div className="overview-section">
9
- <div className="overview-header">
10
- <h2 className="overview-title">{overview.heading}</h2>
11
- <p className="overview-description">{overview.description}</p>
12
- </div>
13
-
14
- <div className="overview-grid">
15
- <div className="feature-card">
16
- <div className="feature-icon">✨</div>
17
- <h3>{overview.features.title}</h3>
18
- <ul className="feature-list">
19
- {overview.features.items.map((item: string, idx: number) => (
20
- <li key={idx}>
21
- <span className="feature-dot">•</span>
22
- {item}
23
- </li>
24
- ))}
25
- </ul>
26
- </div>
27
-
28
- <div className="feature-card">
29
- <div className="feature-icon">🎯</div>
30
- <h3>{overview.useCases.title}</h3>
31
- <ul className="feature-list">
32
- {overview.useCases.items.map((item: string, idx: number) => (
33
- <li key={idx}>
34
- <span className="feature-dot">→</span>
35
- {item}
36
- </li>
37
- ))}
38
- </ul>
39
- </div>
40
- </div>
41
- </div>
42
- )
43
- }
1
+ import { useTranslation } from '@asafarim/shared-i18n'
2
+
3
+ export default function OverviewSection() {
4
+ const { t } = useTranslation('demo')
5
+ const overview = t('overview', { returnObjects: true }) as any
6
+
7
+ return (
8
+ <div className="overview-section">
9
+ <div className="overview-header">
10
+ <h2 className="overview-title">{overview.heading}</h2>
11
+ <p className="overview-description">{overview.description}</p>
12
+ </div>
13
+
14
+ <div className="overview-grid">
15
+ <div className="feature-card">
16
+ <div className="feature-icon">🌍</div>
17
+ <h3>{overview.features.title}</h3>
18
+ <ul className="feature-list">
19
+ {overview.features.items.map((item: string, idx: number) => (
20
+ <li key={idx}>
21
+ <span className="feature-dot">•</span>
22
+ {item}
23
+ </li>
24
+ ))}
25
+ </ul>
26
+ </div>
27
+
28
+ {overview.selectors && (
29
+ <div className="feature-card">
30
+ <div className="feature-icon">🗺️</div>
31
+ <h3>{overview.selectors.title}</h3>
32
+ <ul className="feature-list">
33
+ {overview.selectors.items.map((item: string, idx: number) => (
34
+ <li key={idx}>
35
+ <span className="feature-dot">→</span>
36
+ {item}
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ </div>
41
+ )}
42
+
43
+ <div className="feature-card">
44
+ <div className="feature-icon">🎯</div>
45
+ <h3>{overview.useCases.title}</h3>
46
+ <ul className="feature-list">
47
+ {overview.useCases.items.map((item: string, idx: number) => (
48
+ <li key={idx}>
49
+ <span className="feature-dot">→</span>
50
+ {item}
51
+ </li>
52
+ ))}
53
+ </ul>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ )
58
+ }
@@ -1,15 +1,15 @@
1
- import { ReactNode } from 'react'
2
-
3
- interface PanelProps {
4
- title: string
5
- children: ReactNode
6
- }
7
-
8
- export default function Panel({ title, children }: PanelProps) {
9
- return (
10
- <div className="panel">
11
- <h2 className="panel-title">{title}</h2>
12
- {children}
13
- </div>
14
- )
15
- }
1
+ import { ReactNode } from 'react'
2
+
3
+ interface PanelProps {
4
+ title: string
5
+ children: ReactNode
6
+ }
7
+
8
+ export default function Panel({ title, children }: PanelProps) {
9
+ return (
10
+ <div className="panel">
11
+ <h2 className="panel-title">{title}</h2>
12
+ {children}
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,147 @@
1
+ import { useState } from 'react'
2
+ import { LanguageSwitcher, CountryLanguageSelector, useTranslation } from '@asafarim/shared-i18n'
3
+ import type { Locale } from '@asafarim/shared-i18n'
4
+ import type { SupportedLanguage } from '@asafarim/shared-i18n'
5
+ import { countries } from '../data/countries'
6
+ import { localeToSlug } from '../i18n/localeRouting'
7
+ import { resolveLocaleFromLanguage } from '../i18n/localeAdapter'
8
+ import type { ResolveLocaleResult } from '../i18n/localeAdapter'
9
+
10
+ interface Props {
11
+ locale: Locale
12
+ onLocaleChange: (locale: Locale) => void
13
+ }
14
+
15
+ interface ScenarioRow {
16
+ id: string
17
+ startLocale: Locale
18
+ action: string
19
+ expectedSlug: string
20
+ run: (onLocaleChange: (l: Locale) => void) => void
21
+ }
22
+
23
+ const SCENARIOS: ScenarioRow[] = [
24
+ {
25
+ id: 'be-en-fr',
26
+ startLocale: { country: 'BE', language: 'en' },
27
+ action: 'LanguageSwitcher selects French',
28
+ expectedSlug: 'be-fr',
29
+ run: (cb) => {
30
+ const { locale } = resolveLocaleFromLanguage({ country: 'BE', language: 'en' }, 'fr', countries)
31
+ cb(locale)
32
+ },
33
+ },
34
+ {
35
+ id: 'gb-en-nl',
36
+ startLocale: { country: 'GB', language: 'en' },
37
+ action: 'LanguageSwitcher selects Dutch (UK → fallback to NL)',
38
+ expectedSlug: 'nl-nl',
39
+ run: (cb) => {
40
+ const { locale } = resolveLocaleFromLanguage({ country: 'GB', language: 'en' }, 'nl', countries)
41
+ cb(locale)
42
+ },
43
+ },
44
+ {
45
+ id: 'cls-gb-en',
46
+ startLocale: { country: 'BE', language: 'en' },
47
+ action: 'CountryLanguageSelector selects UK English',
48
+ expectedSlug: 'gb-en',
49
+ run: (cb) => cb({ country: 'GB', language: 'en' }),
50
+ },
51
+ {
52
+ id: 'cls-lu-de',
53
+ startLocale: { country: 'BE', language: 'en' },
54
+ action: 'CountryLanguageSelector selects Luxembourg German',
55
+ expectedSlug: 'lu-de',
56
+ run: (cb) => cb({ country: 'LU', language: 'de' }),
57
+ },
58
+ ]
59
+
60
+ export default function RoutingLabPage({ locale, onLocaleChange }: Props) {
61
+ const { t } = useTranslation('demo')
62
+ const [lsResult, setLsResult] = useState<ResolveLocaleResult | null>(null)
63
+
64
+ const handleLangSwitch = (lang: SupportedLanguage) => {
65
+ const result = resolveLocaleFromLanguage(locale, lang, countries)
66
+ setLsResult(result)
67
+ if (result.reason !== 'unsupported') onLocaleChange(result.locale)
68
+ }
69
+
70
+ return (
71
+ <div className="lab">
72
+ <header className="section-head">
73
+ <h2>{t('routingLab.heading')}</h2>
74
+ <p>{t('routingLab.intro')}</p>
75
+ </header>
76
+
77
+ <div className="lab__status surface">
78
+ <div><span className="lab__caption">{t('routingLab.currentLocale')}</span><strong className="lab__value">{locale.country} / {locale.language.toUpperCase()}</strong></div>
79
+ <div><span className="lab__caption">{t('routingLab.slug')}</span><code className="lab__value">{localeToSlug(locale)}</code></div>
80
+ <div><span className="lab__caption">{t('routingLab.urlPath')}</span><code className="lab__value">/shared-i18n/{localeToSlug(locale)}/routing-lab</code></div>
81
+ </div>
82
+
83
+ <div className="lab__side-by-side">
84
+ <div className="lab__side surface">
85
+ <h3 className="lab__side-title">{t('routingLab.langOnly.title')}</h3>
86
+ <p className="lab__side-desc">{t('routingLab.langOnly.desc')}</p>
87
+ <LanguageSwitcher
88
+ variant="buttons"
89
+ languages={['en', 'nl', 'fr', 'de']}
90
+ onChanged={handleLangSwitch}
91
+ />
92
+ {lsResult && (
93
+ <div className={`notice ${lsResult.reason === 'fallback-country' ? 'notice--warn' : lsResult.reason === 'unsupported' ? 'notice--err' : 'notice--ok'}`}>
94
+ {lsResult.reason === 'same-country' && (
95
+ <>✓ Same country — locale: <code>{localeToSlug(lsResult.locale)}</code></>
96
+ )}
97
+ {lsResult.reason === 'fallback-country' && lsResult.message}
98
+ {lsResult.reason === 'unsupported' && lsResult.message}
99
+ </div>
100
+ )}
101
+ <div className="lab__outcome">
102
+ {t('routingLab.langOnly.resolved')} <code>{localeToSlug(locale)}</code>
103
+ </div>
104
+ </div>
105
+
106
+ <div className="lab__side surface">
107
+ <h3 className="lab__side-title">{t('routingLab.countryLang.title')}</h3>
108
+ <p className="lab__side-desc">{t('routingLab.countryLang.desc')}</p>
109
+ <CountryLanguageSelector
110
+ countries={countries}
111
+ value={locale}
112
+ onChange={onLocaleChange}
113
+ triggerVariant="full"
114
+ flagMode="image"
115
+ ariaLabel="Routing lab — country language selector"
116
+ />
117
+ <div className="lab__outcome">
118
+ {t('routingLab.countryLang.resolved')} <code>{localeToSlug(locale)}</code>
119
+ </div>
120
+ </div>
121
+ </div>
122
+
123
+ <section className="lab__scenarios">
124
+ <h3>{t('routingLab.scenarios.title')}</h3>
125
+ <p>{t('routingLab.scenarios.desc')}</p>
126
+ <div className="scenario-grid">
127
+ {SCENARIOS.map(s => (
128
+ <button
129
+ key={s.id}
130
+ type="button"
131
+ className="scenario-card"
132
+ onClick={() => s.run(onLocaleChange)}
133
+ >
134
+ <div className="scenario-card__action">{s.action}</div>
135
+ <div className="scenario-card__result">
136
+ {t('routingLab.scenarios.expected')} <code>{s.expectedSlug}</code>
137
+ </div>
138
+ <div className="scenario-card__start">
139
+ Start: <code>{localeToSlug(s.startLocale)}</code>
140
+ </div>
141
+ </button>
142
+ ))}
143
+ </div>
144
+ </section>
145
+ </div>
146
+ )
147
+ }