@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.
- package/README.md +266 -280
- package/demo/README.md +119 -0
- package/demo/index.html +12 -1
- package/demo/node_modules/.bin/kill-port +17 -0
- package/demo/node_modules/.bin/tsc +5 -9
- package/demo/node_modules/.bin/tsserver +5 -9
- package/demo/node_modules/.bin/vite +5 -9
- package/demo/package.json +7 -4
- package/demo/public/404.html +24 -0
- package/demo/public/favicon.svg +4 -4
- package/demo/public/logo.svg +24 -24
- package/demo/src/App.tsx +178 -129
- package/demo/src/components/CountryLanguageSelectorsPage.tsx +240 -0
- package/demo/src/components/GetStartedSection.tsx +56 -56
- package/demo/src/components/KeyTable.tsx +29 -29
- package/demo/src/components/LanguageBar.tsx +145 -103
- package/demo/src/components/LanguageSwitcherDemo.module.css +114 -114
- package/demo/src/components/LanguageSwitchersPage.tsx +245 -0
- package/demo/src/components/Logo.tsx +6 -6
- package/demo/src/components/OverviewSection.tsx +58 -43
- package/demo/src/components/Panel.tsx +15 -15
- package/demo/src/components/RoutingLabPage.tsx +147 -0
- package/demo/src/components/StatusCard.tsx +109 -109
- package/demo/src/data/countries.ts +48 -0
- package/demo/src/i18n/localeAdapter.ts +91 -0
- package/demo/src/i18n/localeRouting.ts +77 -0
- package/demo/src/index.css +1075 -644
- package/demo/src/locales/de/demo.json +202 -84
- package/demo/src/locales/en/demo.json +201 -85
- package/demo/src/locales/fr/demo.json +203 -85
- package/demo/src/locales/it/demo.json +202 -84
- package/demo/src/locales/lb/demo.json +201 -0
- package/demo/src/locales/nl/demo.json +203 -85
- package/demo/src/main.tsx +32 -29
- package/demo/tsconfig.json +18 -18
- package/demo/tsconfig.node.json +10 -10
- package/demo/tsconfig.tsbuildinfo +1 -1
- package/demo/vite-env.d.ts +7 -7
- package/demo/vite.config.d.ts +2 -2
- package/demo/vite.config.js +10 -10
- package/dist/components/LanguageSwitcher.module.css +303 -303
- package/dist/country-language-selector.css +431 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +87 -85
- package/demo/dist/Icon Dropdown_Limited Languages.png +0 -0
- package/demo/dist/Select Dropdown_Text Only.png +0 -0
- package/demo/dist/assets/favicon-BZYZvBLo.svg +0 -4
- package/demo/dist/assets/index-BdjqKw_N.css +0 -1
- package/demo/dist/assets/index-C1Tq1uEr.js +0 -191
- package/demo/dist/favicon.svg +0 -4
- package/demo/dist/index.html +0 -27
- package/demo/dist/logo.svg +0 -24
- package/demo/node_modules/.bin/browserslist +0 -21
- package/demo/node_modules/.bin/browserslist.CMD +0 -12
- package/demo/node_modules/.bin/browserslist.ps1 +0 -41
- package/demo/node_modules/.bin/tsc.CMD +0 -12
- package/demo/node_modules/.bin/tsc.ps1 +0 -41
- package/demo/node_modules/.bin/tsserver.CMD +0 -12
- package/demo/node_modules/.bin/tsserver.ps1 +0 -41
- package/demo/node_modules/.bin/vite.CMD +0 -12
- package/demo/node_modules/.bin/vite.ps1 +0 -41
- package/demo/node_modules/.vite/deps/@asafarim_country-language-selector.js +0 -848
- package/demo/node_modules/.vite/deps/@asafarim_country-language-selector.js.map +0 -7
- package/demo/node_modules/.vite/deps/_metadata.json +0 -76
- package/demo/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
- package/demo/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
- package/demo/node_modules/.vite/deps/chunk-B3AHR5EX.js +0 -1004
- package/demo/node_modules/.vite/deps/chunk-B3AHR5EX.js.map +0 -7
- package/demo/node_modules/.vite/deps/chunk-E6BG6WAU.js +0 -292
- package/demo/node_modules/.vite/deps/chunk-E6BG6WAU.js.map +0 -7
- package/demo/node_modules/.vite/deps/chunk-MVARZQEG.js +0 -280
- package/demo/node_modules/.vite/deps/chunk-MVARZQEG.js.map +0 -7
- package/demo/node_modules/.vite/deps/i18next-browser-languagedetector.js +0 -400
- package/demo/node_modules/.vite/deps/i18next-browser-languagedetector.js.map +0 -7
- package/demo/node_modules/.vite/deps/i18next.js +0 -2392
- package/demo/node_modules/.vite/deps/i18next.js.map +0 -7
- package/demo/node_modules/.vite/deps/package.json +0 -3
- package/demo/node_modules/.vite/deps/react-dom.js +0 -6
- package/demo/node_modules/.vite/deps/react-dom.js.map +0 -7
- package/demo/node_modules/.vite/deps/react-dom_client.js +0 -20217
- package/demo/node_modules/.vite/deps/react-dom_client.js.map +0 -7
- package/demo/node_modules/.vite/deps/react-i18next.js +0 -869
- package/demo/node_modules/.vite/deps/react-i18next.js.map +0 -7
- package/demo/node_modules/.vite/deps/react.js +0 -5
- package/demo/node_modules/.vite/deps/react.js.map +0 -7
- package/demo/node_modules/.vite/deps/react_jsx-dev-runtime.js +0 -278
- package/demo/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +0 -7
- package/demo/node_modules/.vite/deps/react_jsx-runtime.js +0 -6
- package/demo/node_modules/.vite/deps/react_jsx-runtime.js.map +0 -7
- package/demo/src/components/CountryLanguageDemo.tsx +0 -140
- package/demo/src/components/LanguageSwitcherDemo.tsx +0 -256
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
useTranslation,
|
|
4
|
-
SUPPORTED_LANGUAGES,
|
|
5
|
-
LANGUAGE_NAMES,
|
|
6
|
-
LANGUAGE_COOKIE_NAME,
|
|
7
|
-
getLanguageFromCookie,
|
|
8
|
-
getInitialLanguage,
|
|
9
|
-
updateUserLanguagePreference,
|
|
10
|
-
isSupportedLanguage
|
|
11
|
-
} from '@asafarim/shared-i18n'
|
|
12
|
-
import Panel from './Panel'
|
|
13
|
-
|
|
14
|
-
export default function StatusCard() {
|
|
15
|
-
const { t, i18n } = useTranslation('demo')
|
|
16
|
-
const [cookieValue, setCookieValue] = useState<string | null>(null)
|
|
17
|
-
const [initialLang, setInitialLang] = useState<string>('')
|
|
18
|
-
const [syncResult, setSyncResult] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
|
|
19
|
-
const [isSyncing, setIsSyncing] = useState(false)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
setCookieValue(getLanguageFromCookie())
|
|
23
|
-
setInitialLang(getInitialLanguage())
|
|
24
|
-
}, [i18n.language])
|
|
25
|
-
|
|
26
|
-
const backendUrl = import.meta.env.VITE_IDENTITY_API_URL || 'not set'
|
|
27
|
-
|
|
28
|
-
const handleSync = async () => {
|
|
29
|
-
setIsSyncing(true)
|
|
30
|
-
setSyncResult(null)
|
|
31
|
-
try {
|
|
32
|
-
const currentLanguage = i18n.language
|
|
33
|
-
if (isSupportedLanguage(currentLanguage)) {
|
|
34
|
-
await updateUserLanguagePreference(currentLanguage)
|
|
35
|
-
} else {
|
|
36
|
-
throw new Error(`Unsupported language: ${currentLanguage}`)
|
|
37
|
-
}
|
|
38
|
-
setSyncResult({
|
|
39
|
-
type: 'success',
|
|
40
|
-
message: t('status.resultOk')
|
|
41
|
-
})
|
|
42
|
-
} catch (error) {
|
|
43
|
-
setSyncResult({
|
|
44
|
-
type: 'error',
|
|
45
|
-
message: `${t('status.resultFail')}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
46
|
-
})
|
|
47
|
-
} finally {
|
|
48
|
-
setIsSyncing(false)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<Panel title={t('status.heading')}>
|
|
54
|
-
<div className="status-grid">
|
|
55
|
-
<div className="status-card">
|
|
56
|
-
<h4>Current Language</h4>
|
|
57
|
-
<p>{i18n.language}</p>
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
<div className="status-card">
|
|
61
|
-
<h4>{t('status.cookie')}</h4>
|
|
62
|
-
<p>
|
|
63
|
-
<strong>Name:</strong> {LANGUAGE_COOKIE_NAME}
|
|
64
|
-
</p>
|
|
65
|
-
<p>
|
|
66
|
-
<strong>Value:</strong> {cookieValue || '(not set)'}
|
|
67
|
-
</p>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div className="status-card">
|
|
71
|
-
<h4>Initial Language</h4>
|
|
72
|
-
<p>{initialLang}</p>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div className="status-card">
|
|
76
|
-
<h4>Supported Languages</h4>
|
|
77
|
-
<ul className="status-list">
|
|
78
|
-
{SUPPORTED_LANGUAGES.map((lang: string) => (
|
|
79
|
-
<li key={lang}>
|
|
80
|
-
{lang}: {LANGUAGE_NAMES[lang as keyof typeof LANGUAGE_NAMES]}
|
|
81
|
-
</li>
|
|
82
|
-
))}
|
|
83
|
-
</ul>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div className="status-card">
|
|
87
|
-
<h4>{t('status.backend')}</h4>
|
|
88
|
-
<p>{backendUrl}</p>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<div className="status-card">
|
|
92
|
-
<h4>Backend Sync</h4>
|
|
93
|
-
<button
|
|
94
|
-
className="sync-button"
|
|
95
|
-
onClick={handleSync}
|
|
96
|
-
disabled={isSyncing}
|
|
97
|
-
>
|
|
98
|
-
{isSyncing ? 'Syncing...' : t('status.simulate')}
|
|
99
|
-
</button>
|
|
100
|
-
{syncResult && (
|
|
101
|
-
<div className={`sync-result ${syncResult.type}`}>
|
|
102
|
-
{syncResult.message}
|
|
103
|
-
</div>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
107
|
-
</Panel>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
useTranslation,
|
|
4
|
+
SUPPORTED_LANGUAGES,
|
|
5
|
+
LANGUAGE_NAMES,
|
|
6
|
+
LANGUAGE_COOKIE_NAME,
|
|
7
|
+
getLanguageFromCookie,
|
|
8
|
+
getInitialLanguage,
|
|
9
|
+
updateUserLanguagePreference,
|
|
10
|
+
isSupportedLanguage
|
|
11
|
+
} from '@asafarim/shared-i18n'
|
|
12
|
+
import Panel from './Panel'
|
|
13
|
+
|
|
14
|
+
export default function StatusCard() {
|
|
15
|
+
const { t, i18n } = useTranslation('demo')
|
|
16
|
+
const [cookieValue, setCookieValue] = useState<string | null>(null)
|
|
17
|
+
const [initialLang, setInitialLang] = useState<string>('')
|
|
18
|
+
const [syncResult, setSyncResult] = useState<{ type: 'success' | 'error'; message: string } | null>(null)
|
|
19
|
+
const [isSyncing, setIsSyncing] = useState(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setCookieValue(getLanguageFromCookie())
|
|
23
|
+
setInitialLang(getInitialLanguage())
|
|
24
|
+
}, [i18n.language])
|
|
25
|
+
|
|
26
|
+
const backendUrl = import.meta.env.VITE_IDENTITY_API_URL || 'not set'
|
|
27
|
+
|
|
28
|
+
const handleSync = async () => {
|
|
29
|
+
setIsSyncing(true)
|
|
30
|
+
setSyncResult(null)
|
|
31
|
+
try {
|
|
32
|
+
const currentLanguage = i18n.language
|
|
33
|
+
if (isSupportedLanguage(currentLanguage)) {
|
|
34
|
+
await updateUserLanguagePreference(currentLanguage)
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error(`Unsupported language: ${currentLanguage}`)
|
|
37
|
+
}
|
|
38
|
+
setSyncResult({
|
|
39
|
+
type: 'success',
|
|
40
|
+
message: t('status.resultOk')
|
|
41
|
+
})
|
|
42
|
+
} catch (error) {
|
|
43
|
+
setSyncResult({
|
|
44
|
+
type: 'error',
|
|
45
|
+
message: `${t('status.resultFail')}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
46
|
+
})
|
|
47
|
+
} finally {
|
|
48
|
+
setIsSyncing(false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Panel title={t('status.heading')}>
|
|
54
|
+
<div className="status-grid">
|
|
55
|
+
<div className="status-card">
|
|
56
|
+
<h4>Current Language</h4>
|
|
57
|
+
<p>{i18n.language}</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="status-card">
|
|
61
|
+
<h4>{t('status.cookie')}</h4>
|
|
62
|
+
<p>
|
|
63
|
+
<strong>Name:</strong> {LANGUAGE_COOKIE_NAME}
|
|
64
|
+
</p>
|
|
65
|
+
<p>
|
|
66
|
+
<strong>Value:</strong> {cookieValue || '(not set)'}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="status-card">
|
|
71
|
+
<h4>Initial Language</h4>
|
|
72
|
+
<p>{initialLang}</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="status-card">
|
|
76
|
+
<h4>Supported Languages</h4>
|
|
77
|
+
<ul className="status-list">
|
|
78
|
+
{SUPPORTED_LANGUAGES.map((lang: string) => (
|
|
79
|
+
<li key={lang}>
|
|
80
|
+
{lang}: {LANGUAGE_NAMES[lang as keyof typeof LANGUAGE_NAMES]}
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="status-card">
|
|
87
|
+
<h4>{t('status.backend')}</h4>
|
|
88
|
+
<p>{backendUrl}</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="status-card">
|
|
92
|
+
<h4>Backend Sync</h4>
|
|
93
|
+
<button
|
|
94
|
+
className="sync-button"
|
|
95
|
+
onClick={handleSync}
|
|
96
|
+
disabled={isSyncing}
|
|
97
|
+
>
|
|
98
|
+
{isSyncing ? 'Syncing...' : t('status.simulate')}
|
|
99
|
+
</button>
|
|
100
|
+
{syncResult && (
|
|
101
|
+
<div className={`sync-result ${syncResult.type}`}>
|
|
102
|
+
{syncResult.message}
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</Panel>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Country } from '@asafarim/shared-i18n'
|
|
2
|
+
|
|
3
|
+
/** Benelux + United Kingdom — the scope for this demo. */
|
|
4
|
+
export const countries: Country[] = [
|
|
5
|
+
{
|
|
6
|
+
code: 'BE',
|
|
7
|
+
name: 'Belgium',
|
|
8
|
+
nativeName: 'België',
|
|
9
|
+
flag: '🇧🇪',
|
|
10
|
+
languages: [
|
|
11
|
+
{ code: 'en', label: 'English', nativeLabel: 'English' },
|
|
12
|
+
{ code: 'nl', label: 'Dutch', nativeLabel: 'Nederlands' },
|
|
13
|
+
{ code: 'fr', label: 'French', nativeLabel: 'Français' },
|
|
14
|
+
{ code: 'de', label: 'German', nativeLabel: 'Deutsch' }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
code: 'NL',
|
|
19
|
+
name: 'Netherlands',
|
|
20
|
+
nativeName: 'Nederland',
|
|
21
|
+
flag: '��',
|
|
22
|
+
languages: [
|
|
23
|
+
{ code: 'nl', label: 'Dutch', nativeLabel: 'Nederlands' },
|
|
24
|
+
{ code: 'en', label: 'English', nativeLabel: 'English' }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: 'LU',
|
|
29
|
+
name: 'Luxembourg',
|
|
30
|
+
nativeName: 'Lëtzebuerg',
|
|
31
|
+
flag: '🇱🇺',
|
|
32
|
+
languages: [
|
|
33
|
+
{ code: 'lb', label: 'Luxembourgish', nativeLabel: 'Lëtzebuergesch' },
|
|
34
|
+
{ code: 'fr', label: 'French', nativeLabel: 'Français' },
|
|
35
|
+
{ code: 'de', label: 'German', nativeLabel: 'Deutsch' },
|
|
36
|
+
{ code: 'en', label: 'English', nativeLabel: 'English' }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
code: 'GB',
|
|
41
|
+
name: 'United Kingdom',
|
|
42
|
+
nativeName: 'United Kingdom',
|
|
43
|
+
flag: '��',
|
|
44
|
+
languages: [
|
|
45
|
+
{ code: 'en', label: 'English', nativeLabel: 'English' }
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Locale, Country } from '@asafarim/shared-i18n'
|
|
2
|
+
|
|
3
|
+
export type ResolveReason = 'same-country' | 'fallback-country' | 'unsupported'
|
|
4
|
+
|
|
5
|
+
export interface ResolveLocaleResult {
|
|
6
|
+
locale: Locale
|
|
7
|
+
reason: ResolveReason
|
|
8
|
+
/** Human-readable notice when a fallback was applied. */
|
|
9
|
+
message?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Preferred first-choice country code for each language when the current
|
|
14
|
+
* country does not support the selected language.
|
|
15
|
+
* If the language isn't listed here, we fall through to the first country
|
|
16
|
+
* in `countries` that supports it.
|
|
17
|
+
*/
|
|
18
|
+
const PREFERRED_FALLBACK: Record<string, string> = {
|
|
19
|
+
nl: 'NL',
|
|
20
|
+
fr: 'BE',
|
|
21
|
+
de: 'BE',
|
|
22
|
+
lb: 'LU',
|
|
23
|
+
en: 'BE',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a full Locale from a language-only change.
|
|
28
|
+
*
|
|
29
|
+
* Rules:
|
|
30
|
+
* 1. If `currentLocale.country` supports `nextLanguage` → keep country.
|
|
31
|
+
* 2. Else find the preferred fallback country (see `PREFERRED_FALLBACK`),
|
|
32
|
+
* then any other country that supports it.
|
|
33
|
+
* 3. If no country supports the language → return current locale unchanged.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveLocaleFromLanguage(
|
|
36
|
+
currentLocale: Locale,
|
|
37
|
+
nextLanguage: string,
|
|
38
|
+
countries: Country[],
|
|
39
|
+
): ResolveLocaleResult {
|
|
40
|
+
const lang = nextLanguage.toLowerCase()
|
|
41
|
+
|
|
42
|
+
// 1. Current country already supports the language?
|
|
43
|
+
const currentCountry = countries.find(
|
|
44
|
+
c => c.code.toUpperCase() === currentLocale.country.toUpperCase(),
|
|
45
|
+
)
|
|
46
|
+
if (currentCountry?.languages.some(l => l.code.toLowerCase() === lang)) {
|
|
47
|
+
return {
|
|
48
|
+
locale: { country: currentLocale.country, language: lang },
|
|
49
|
+
reason: 'same-country',
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Try preferred fallback country.
|
|
54
|
+
const preferredCode = PREFERRED_FALLBACK[lang]
|
|
55
|
+
if (preferredCode) {
|
|
56
|
+
const preferred = countries.find(
|
|
57
|
+
c => c.code.toUpperCase() === preferredCode.toUpperCase(),
|
|
58
|
+
)
|
|
59
|
+
if (preferred?.languages.some(l => l.code.toLowerCase() === lang)) {
|
|
60
|
+
const fromCountryName = currentCountry?.name ?? currentLocale.country
|
|
61
|
+
const langLabel = preferred.languages.find(l => l.code.toLowerCase() === lang)?.label ?? lang
|
|
62
|
+
return {
|
|
63
|
+
locale: { country: preferred.code, language: lang },
|
|
64
|
+
reason: 'fallback-country',
|
|
65
|
+
message: `${langLabel} is not available for ${fromCountryName}, so the locale moved to ${preferred.name}.`,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Any country that supports it.
|
|
71
|
+
for (const country of countries) {
|
|
72
|
+
if (country.languages.some(l => l.code.toLowerCase() === lang)) {
|
|
73
|
+
const fromCountryName = currentCountry?.name ?? currentLocale.country
|
|
74
|
+
const langLabel = country.languages.find(l => l.code.toLowerCase() === lang)?.label ?? lang
|
|
75
|
+
return {
|
|
76
|
+
locale: { country: country.code, language: lang },
|
|
77
|
+
reason: 'fallback-country',
|
|
78
|
+
message: `${langLabel} is not available for ${fromCountryName}, so the locale moved to ${country.name}.`,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. Unsupported — no country found.
|
|
84
|
+
const langLabel = nextLanguage
|
|
85
|
+
const fromCountryName = currentCountry?.name ?? currentLocale.country
|
|
86
|
+
return {
|
|
87
|
+
locale: currentLocale,
|
|
88
|
+
reason: 'unsupported',
|
|
89
|
+
message: `${langLabel} is not supported by any country in this demo. Keeping ${fromCountryName}.`,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Locale } from '@asafarim/shared-i18n'
|
|
2
|
+
import { countries } from '../data/countries'
|
|
3
|
+
|
|
4
|
+
export type DemoPage =
|
|
5
|
+
| 'overview'
|
|
6
|
+
| 'get-started'
|
|
7
|
+
| 'language-switchers'
|
|
8
|
+
| 'country-language-selectors'
|
|
9
|
+
| 'routing-lab'
|
|
10
|
+
|
|
11
|
+
export const PAGES: DemoPage[] = [
|
|
12
|
+
'overview',
|
|
13
|
+
'get-started',
|
|
14
|
+
'language-switchers',
|
|
15
|
+
'country-language-selectors',
|
|
16
|
+
'routing-lab',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_LOCALE: Locale = { country: 'BE', language: 'en' }
|
|
20
|
+
export const DEFAULT_PAGE: DemoPage = 'overview'
|
|
21
|
+
|
|
22
|
+
/** Vite base path. Mirrors `vite.config.ts` `base`. */
|
|
23
|
+
export const BASE_PATH = '/shared-i18n'
|
|
24
|
+
|
|
25
|
+
export function isSupportedLocale(locale: Locale): boolean {
|
|
26
|
+
const country = countries.find(c => c.code.toUpperCase() === locale.country.toUpperCase())
|
|
27
|
+
if (!country) return false
|
|
28
|
+
return country.languages.some(l => l.code.toLowerCase() === locale.language.toLowerCase())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isSupportedPage(page: string): page is DemoPage {
|
|
32
|
+
return (PAGES as string[]).includes(page)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function localeToSlug(locale: Locale): string {
|
|
36
|
+
return `${locale.country.toLowerCase()}-${locale.language.toLowerCase()}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function slugToLocale(slug: string): Locale | null {
|
|
40
|
+
const match = /^([a-z]{2})-([a-z]{2,3})$/i.exec(slug.trim())
|
|
41
|
+
if (!match) return null
|
|
42
|
+
const candidate: Locale = { country: match[1].toUpperCase(), language: match[2].toLowerCase() }
|
|
43
|
+
return isSupportedLocale(candidate) ? candidate : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Build a path like `/shared-i18n/be-en/overview`. */
|
|
47
|
+
export function buildLocalizedPath(locale: Locale, page: DemoPage): string {
|
|
48
|
+
return `${BASE_PATH}/${localeToSlug(locale)}/${page}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ParsedRoute {
|
|
52
|
+
locale: Locale
|
|
53
|
+
page: DemoPage
|
|
54
|
+
/** True if the input did not cleanly map to a known locale + page. */
|
|
55
|
+
needsRedirect: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse a window.location.pathname into a `{ locale, page }` route.
|
|
60
|
+
* Falls back to `DEFAULT_LOCALE` / `DEFAULT_PAGE` and signals redirect when needed.
|
|
61
|
+
*/
|
|
62
|
+
export function parsePathname(pathname: string): ParsedRoute {
|
|
63
|
+
let path = pathname
|
|
64
|
+
if (path.startsWith(BASE_PATH)) path = path.slice(BASE_PATH.length)
|
|
65
|
+
path = path.replace(/^\/+/, '').replace(/\/+$/, '')
|
|
66
|
+
|
|
67
|
+
if (!path) {
|
|
68
|
+
return { locale: DEFAULT_LOCALE, page: DEFAULT_PAGE, needsRedirect: true }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [slugPart, pagePart] = path.split('/')
|
|
72
|
+
const parsedLocale = slugToLocale(slugPart)
|
|
73
|
+
const locale = parsedLocale ?? DEFAULT_LOCALE
|
|
74
|
+
const page = pagePart && isSupportedPage(pagePart) ? pagePart : DEFAULT_PAGE
|
|
75
|
+
const needsRedirect = !parsedLocale || !pagePart || !isSupportedPage(pagePart)
|
|
76
|
+
return { locale, page, needsRedirect }
|
|
77
|
+
}
|