@enigmax/primitives 0.19.1 → 0.20.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/dist/{chunk-T4F3BWR5.js → chunk-LMZJBOZG.js} +68 -6
- package/dist/{chunk-XPKWJD5O.js → chunk-NPHRS2Q7.js} +1 -1
- package/dist/{flags-fLwIl2hG.d.ts → flags-BQU1V9rE.d.ts} +13 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/index.js +2 -2
- package/dist/react/flag.d.ts +1 -1
- package/dist/react/flag.js +2 -2
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +2 -2
- package/dist/react-router/index.d.ts +1 -1
- package/dist/react-router/index.js +2 -2
- package/package.json +1 -1
- package/src/core/flags.ts +155 -11
|
@@ -22,9 +22,56 @@ function resetFlagConfig() {
|
|
|
22
22
|
var FLAG_RATIO = { rect: 4 / 3, square: 1, circle: 1 };
|
|
23
23
|
var EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
|
|
24
24
|
var LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
|
|
25
|
-
var FILE_CODE = /^[a-z]{2,}
|
|
25
|
+
var FILE_CODE = /^[a-z]{2,}[-_][a-z0-9_-]+$/;
|
|
26
26
|
var COUNTRY_CODE = /^[a-z]{2}$/;
|
|
27
|
-
function
|
|
27
|
+
function fold(value) {
|
|
28
|
+
return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").replace(/['‘’ʼ`´]/g, "").toLowerCase().replace(/&/g, " and ").replace(/\bst\.?\s/g, "saint ").replace(/^the\s+/, "").replace(/[.,]/g, "").replace(/\s+/g, " ").trim();
|
|
29
|
+
}
|
|
30
|
+
var nameIndexes = /* @__PURE__ */ new Map();
|
|
31
|
+
function canonicalRegion(code) {
|
|
32
|
+
try {
|
|
33
|
+
return new Intl.Locale(`und-${code}`).region?.toLowerCase() ?? code.toLowerCase();
|
|
34
|
+
} catch {
|
|
35
|
+
return code.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function unknownRegionName(names) {
|
|
39
|
+
return names.of("ZZ");
|
|
40
|
+
}
|
|
41
|
+
function regionsByName(locale) {
|
|
42
|
+
const cached = nameIndexes.get(locale);
|
|
43
|
+
if (cached !== void 0) return cached;
|
|
44
|
+
let index = null;
|
|
45
|
+
try {
|
|
46
|
+
const names = new Intl.DisplayNames([locale], { type: "region" });
|
|
47
|
+
const unknown = unknownRegionName(names);
|
|
48
|
+
index = /* @__PURE__ */ new Map();
|
|
49
|
+
for (let first = 65; first <= 90; first++) {
|
|
50
|
+
for (let second = 65; second <= 90; second++) {
|
|
51
|
+
const code = String.fromCharCode(first, second);
|
|
52
|
+
const name = names.of(code);
|
|
53
|
+
if (!name || name === code || name === unknown) continue;
|
|
54
|
+
const key = fold(name);
|
|
55
|
+
if (!index.has(key)) index.set(key, canonicalRegion(code));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
index = null;
|
|
60
|
+
}
|
|
61
|
+
nameIndexes.set(locale, index);
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
function isKnownRegion(code) {
|
|
65
|
+
try {
|
|
66
|
+
const names = new Intl.DisplayNames(["en"], { type: "region" });
|
|
67
|
+
const upper = code.toUpperCase();
|
|
68
|
+
const name = names.of(upper);
|
|
69
|
+
return Boolean(name) && name !== upper && name !== unknownRegionName(names);
|
|
70
|
+
} catch {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function normalizeFlagCode(value, locale) {
|
|
28
75
|
if (!value) return null;
|
|
29
76
|
const raw = value.trim();
|
|
30
77
|
if (!raw) return null;
|
|
@@ -32,10 +79,18 @@ function normalizeFlagCode(value) {
|
|
|
32
79
|
const letters = [...raw].map((char) => String.fromCharCode((char.codePointAt(0) ?? 0) - 127462 + 97));
|
|
33
80
|
return letters.join("");
|
|
34
81
|
}
|
|
35
|
-
const
|
|
36
|
-
if (
|
|
37
|
-
const
|
|
38
|
-
|
|
82
|
+
const tag = LOCALE_TAG.exec(raw);
|
|
83
|
+
if (tag) return tag[2].toLowerCase();
|
|
84
|
+
const lower = raw.toLowerCase();
|
|
85
|
+
if (COUNTRY_CODE.test(lower)) return isKnownRegion(lower) ? canonicalRegion(lower) : null;
|
|
86
|
+
if (FILE_CODE.test(lower)) return lower;
|
|
87
|
+
const folded = fold(raw);
|
|
88
|
+
for (const candidate of [locale, documentLocale(), "en", runtimeLocale()]) {
|
|
89
|
+
if (!candidate) continue;
|
|
90
|
+
const found = regionsByName(candidate)?.get(folded);
|
|
91
|
+
if (found) return found;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
39
94
|
}
|
|
40
95
|
function flagName(code, locale) {
|
|
41
96
|
const normalized = normalizeFlagCode(code);
|
|
@@ -49,6 +104,13 @@ function flagName(code, locale) {
|
|
|
49
104
|
return null;
|
|
50
105
|
}
|
|
51
106
|
}
|
|
107
|
+
function runtimeLocale() {
|
|
108
|
+
try {
|
|
109
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
110
|
+
} catch {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
52
114
|
function documentLocale() {
|
|
53
115
|
if (typeof document === "undefined") return void 0;
|
|
54
116
|
return document.documentElement.lang || void 0;
|
|
@@ -71,12 +71,20 @@ declare const FLAG_RATIO: Record<FlagShape, number>;
|
|
|
71
71
|
/**
|
|
72
72
|
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* What it accepts: a code (`es`, `ES`), a locale tag (`en-GB`, `es_ES`), a subdivision or
|
|
75
|
+
* named file (`gb-eng`, `au-nsw`, `easter_island`), the emoji flag you are replacing, and
|
|
76
|
+
* the country's NAME - in English or in the page's own language, accents optional, so
|
|
77
|
+
* `Spain`, `España` and `espana` all resolve. A name is what a database column or an API
|
|
78
|
+
* payload usually holds, and requiring the caller to convert it first is how a flag ends up
|
|
79
|
+
* not being rendered at all.
|
|
80
|
+
*
|
|
81
|
+
* Null rather than a guess, and that is the point of the strictness: `banana` used to pass
|
|
82
|
+
* as a file code and render as a 404 with no alt text, which is worse than no flag. A
|
|
83
|
+
* two-letter code is checked against the runtime's own region list, a bare word is resolved
|
|
84
|
+
* as a name or refused, and only a separator-carrying file name is taken on trust, because
|
|
85
|
+
* nothing can check those without shipping the whole directory listing.
|
|
78
86
|
*/
|
|
79
|
-
declare function normalizeFlagCode(value: string | null | undefined): string | null;
|
|
87
|
+
declare function normalizeFlagCode(value: string | null | undefined, locale?: string): string | null;
|
|
80
88
|
/**
|
|
81
89
|
* The country's name, in the reader's language, or null.
|
|
82
90
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { E as EstimateOptions, G as GeneratePasswordOptions, P as PasswordAlphab
|
|
|
6
6
|
export { N as Notification, a as NotificationAction, b as NotificationInput, c as NotificationTone, d as Notifications, e as NotificationsOptions, P as PromiseMessages, f as createNotifications } from './notifications-BpVV6sel.js';
|
|
7
7
|
export { N as NetworkMonitor, a as NetworkState, S as SERVER_NETWORK_STATE, c as createNetworkMonitor } from './network-D2LsBG_k.js';
|
|
8
8
|
export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePrecision, c as RelativeTimeStyle, d as RelativeTimeTense, e as RelativeTimeView, f as ensureZone, n as normalizeDate, p as parseDuration, r as relativeTimeAttributes, g as relativeTimeView } from './relative-time-YpRTG7YH.js';
|
|
9
|
-
export { F as FLAG_CDN, a as FLAG_RATIO, b as FlagConfig, c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, g as FlagView, h as configureFlags, i as flagAttributes, j as flagConfig, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode, r as resetFlagConfig } from './flags-
|
|
9
|
+
export { F as FLAG_CDN, a as FLAG_RATIO, b as FlagConfig, c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, g as FlagView, h as configureFlags, i as flagAttributes, j as flagConfig, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode, r as resetFlagConfig } from './flags-BQU1V9rE.js';
|
|
10
10
|
export { P as PaletteKey, a as PositionedRow, R as RecentEntry, b as RecentStore, c as RecentStoreOptions, d as RowGroup, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from './palette-D7iuQh_T.js';
|
|
11
11
|
|
|
12
12
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createInput } from './chunk-KJINGUQN.js';
|
|
2
2
|
export { estimatePasswordStrength, generatePassword } from './chunk-HC2ME5PU.js';
|
|
3
|
-
export { FLAG_CDN, FLAG_RATIO, configureFlags, flagAttributes, flagConfig, flagName, flagSrc, flagView, normalizeFlagCode, resetFlagConfig } from './chunk-
|
|
3
|
+
export { FLAG_CDN, FLAG_RATIO, configureFlags, flagAttributes, flagConfig, flagName, flagSrc, flagView, normalizeFlagCode, resetFlagConfig } from './chunk-LMZJBOZG.js';
|
|
4
4
|
export { createMarquee } from './chunk-UOSSNUSC.js';
|
|
5
5
|
export { SERVER_NETWORK_STATE, createNetworkMonitor } from './chunk-2QFTRNAZ.js';
|
|
6
6
|
export { createNotifications } from './chunk-SNYUBXWQ.js';
|
package/dist/next/index.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { RelativeTime, RelativeTimeProps } from '../react/relative-time.js';
|
|
|
18
18
|
export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePrecision, c as RelativeTimeStyle, d as RelativeTimeTense } from '../relative-time-YpRTG7YH.js';
|
|
19
19
|
export { N as Notification, a as NotificationAction, b as NotificationInput, c as NotificationTone, e as NotificationsOptions } from '../notifications-BpVV6sel.js';
|
|
20
20
|
export { Flag, FlagProps } from '../react/flag.js';
|
|
21
|
-
export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-
|
|
21
|
+
export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-BQU1V9rE.js';
|
|
22
22
|
export { PaletteContent, PaletteContextValue, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteListProps, PaletteRoot, PaletteRootProps, PaletteRow, PaletteSection, PaletteTrigger, SearchPalette, SearchPaletteProps, usePaletteContext } from '../react/palette.js';
|
|
23
23
|
export { R as RecentEntry, b as RecentStore, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from '../palette-D7iuQh_T.js';
|
|
24
24
|
|
package/dist/next/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
export { useInput } from '../chunk-U3V4EHOB.js';
|
|
3
3
|
export { PasswordStrength } from '../chunk-XQHCZAPJ.js';
|
|
4
|
-
export { Flag } from '../chunk-
|
|
4
|
+
export { Flag } from '../chunk-NPHRS2Q7.js';
|
|
5
5
|
export { Toaster } from '../chunk-ZCUFYBPB.js';
|
|
6
6
|
export { useMarquee } from '../chunk-ZWR2EXHQ.js';
|
|
7
7
|
export { useSearch } from '../chunk-QQFNAKMY.js';
|
|
@@ -10,7 +10,7 @@ export { createNotificationQueue, defaultQueue, useNotifications } from '../chun
|
|
|
10
10
|
export { RelativeTime } from '../chunk-OCMI7R6H.js';
|
|
11
11
|
import '../chunk-KJINGUQN.js';
|
|
12
12
|
export { estimatePasswordStrength, generatePassword } from '../chunk-HC2ME5PU.js';
|
|
13
|
-
export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-
|
|
13
|
+
export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-LMZJBOZG.js';
|
|
14
14
|
import '../chunk-UOSSNUSC.js';
|
|
15
15
|
import '../chunk-2QFTRNAZ.js';
|
|
16
16
|
import '../chunk-SNYUBXWQ.js';
|
package/dist/react/flag.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ImgHTMLAttributes, ReactNode } from 'react';
|
|
2
|
-
import { e as FlagShape, c as FlagFormat, f as FlagSource } from '../flags-
|
|
2
|
+
import { e as FlagShape, c as FlagFormat, f as FlagSource } from '../flags-BQU1V9rE.js';
|
|
3
3
|
|
|
4
4
|
interface FlagProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "width" | "height"> {
|
|
5
5
|
/** `es`, `ES`, `en-GB`, `gb-eng`, or the emoji flag you are replacing. */
|
package/dist/react/flag.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
export { Flag } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
export { Flag } from '../chunk-NPHRS2Q7.js';
|
|
3
|
+
import '../chunk-LMZJBOZG.js';
|
package/dist/react/index.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export { RelativeTime, RelativeTimeProps } from './relative-time.js';
|
|
|
20
20
|
export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePrecision, c as RelativeTimeStyle, d as RelativeTimeTense } from '../relative-time-YpRTG7YH.js';
|
|
21
21
|
export { N as Notification, a as NotificationAction, b as NotificationInput, c as NotificationTone, e as NotificationsOptions } from '../notifications-BpVV6sel.js';
|
|
22
22
|
export { Flag, FlagProps } from './flag.js';
|
|
23
|
-
export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-
|
|
23
|
+
export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-BQU1V9rE.js';
|
|
24
24
|
export { PaletteContent, PaletteContextValue, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteListProps, PaletteRoot, PaletteRootProps, PaletteRow, PaletteSection, PaletteTrigger, SearchPalette, SearchPaletteProps, usePaletteContext } from './palette.js';
|
|
25
25
|
export { R as RecentEntry, b as RecentStore, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from '../palette-D7iuQh_T.js';
|
|
26
26
|
|
package/dist/react/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
export { useInput } from '../chunk-U3V4EHOB.js';
|
|
3
3
|
export { PasswordStrength } from '../chunk-XQHCZAPJ.js';
|
|
4
|
-
export { Flag } from '../chunk-
|
|
4
|
+
export { Flag } from '../chunk-NPHRS2Q7.js';
|
|
5
5
|
export { Toaster } from '../chunk-ZCUFYBPB.js';
|
|
6
6
|
export { useMarquee } from '../chunk-ZWR2EXHQ.js';
|
|
7
7
|
export { useSearch } from '../chunk-QQFNAKMY.js';
|
|
@@ -10,7 +10,7 @@ export { createNotificationQueue, defaultQueue, useNotifications } from '../chun
|
|
|
10
10
|
export { RelativeTime } from '../chunk-OCMI7R6H.js';
|
|
11
11
|
import '../chunk-KJINGUQN.js';
|
|
12
12
|
export { estimatePasswordStrength, generatePassword } from '../chunk-HC2ME5PU.js';
|
|
13
|
-
export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-
|
|
13
|
+
export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-LMZJBOZG.js';
|
|
14
14
|
import '../chunk-UOSSNUSC.js';
|
|
15
15
|
import '../chunk-2QFTRNAZ.js';
|
|
16
16
|
import '../chunk-SNYUBXWQ.js';
|
|
@@ -18,7 +18,7 @@ export { RelativeTime, RelativeTimeProps } from '../react/relative-time.js';
|
|
|
18
18
|
export { R as RelativeTimeFormat, a as RelativeTimeOptions, b as RelativeTimePrecision, c as RelativeTimeStyle, d as RelativeTimeTense } from '../relative-time-YpRTG7YH.js';
|
|
19
19
|
export { N as Notification, a as NotificationAction, b as NotificationInput, c as NotificationTone, e as NotificationsOptions } from '../notifications-BpVV6sel.js';
|
|
20
20
|
export { Flag, FlagProps } from '../react/flag.js';
|
|
21
|
-
export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-
|
|
21
|
+
export { c as FlagFormat, d as FlagOptions, e as FlagShape, f as FlagSource, h as configureFlags, i as flagAttributes, k as flagName, l as flagSrc, m as flagView, n as normalizeFlagCode } from '../flags-BQU1V9rE.js';
|
|
22
22
|
export { PaletteContent, PaletteContextValue, PaletteField, PaletteFooter, PaletteItem, PaletteList, PaletteListProps, PaletteRoot, PaletteRootProps, PaletteRow, PaletteSection, PaletteTrigger, SearchPalette, SearchPaletteProps, usePaletteContext } from '../react/palette.js';
|
|
23
23
|
export { R as RecentEntry, b as RecentStore, e as createRecentStore, g as groupRows, i as isPaletteShortcut, m as moveActive, r as recentKey, s as shortcutLabel } from '../palette-D7iuQh_T.js';
|
|
24
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
export { useInput } from '../chunk-U3V4EHOB.js';
|
|
3
3
|
export { PasswordStrength } from '../chunk-XQHCZAPJ.js';
|
|
4
|
-
export { Flag } from '../chunk-
|
|
4
|
+
export { Flag } from '../chunk-NPHRS2Q7.js';
|
|
5
5
|
export { Toaster } from '../chunk-ZCUFYBPB.js';
|
|
6
6
|
export { useMarquee } from '../chunk-ZWR2EXHQ.js';
|
|
7
7
|
export { useSearch } from '../chunk-QQFNAKMY.js';
|
|
@@ -10,7 +10,7 @@ export { createNotificationQueue, defaultQueue, useNotifications } from '../chun
|
|
|
10
10
|
export { RelativeTime } from '../chunk-OCMI7R6H.js';
|
|
11
11
|
import '../chunk-KJINGUQN.js';
|
|
12
12
|
export { estimatePasswordStrength, generatePassword } from '../chunk-HC2ME5PU.js';
|
|
13
|
-
export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-
|
|
13
|
+
export { configureFlags, flagAttributes, flagName, flagSrc, flagView, normalizeFlagCode } from '../chunk-LMZJBOZG.js';
|
|
14
14
|
import '../chunk-UOSSNUSC.js';
|
|
15
15
|
import '../chunk-2QFTRNAZ.js';
|
|
16
16
|
import '../chunk-SNYUBXWQ.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enigmax/primitives",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Headless components: the behaviour, the timing and the accessibility, with no styles of their own. Framework-agnostic core plus thin adapters.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
package/src/core/flags.ts
CHANGED
|
@@ -106,20 +106,141 @@ const EMOJI_FLAG = /^[\u{1F1E6}-\u{1F1FF}]{2}$/u;
|
|
|
106
106
|
* two the same string, and then one of them is always wrong.
|
|
107
107
|
*/
|
|
108
108
|
const LOCALE_TAG = /^([A-Za-z]{2,3})[-_]([A-Z]{2})$/;
|
|
109
|
-
/**
|
|
110
|
-
|
|
109
|
+
/**
|
|
110
|
+
* A SUBDIVISION or a named file, which is the only shape that cannot be checked against
|
|
111
|
+
* anything: `gb-eng`, `au-nsw`, `es-ct`, `easter_island`. It must carry a separator - that
|
|
112
|
+
* is what distinguishes a file name from an ordinary word, and without it `banana` was
|
|
113
|
+
* accepted as a code and rendered as a 404.
|
|
114
|
+
*/
|
|
115
|
+
const FILE_CODE = /^[a-z]{2,}[-_][a-z0-9_-]+$/;
|
|
111
116
|
/** A plain country: exactly the shape `Intl.DisplayNames` can put a name to. */
|
|
112
117
|
const COUNTRY_CODE = /^[a-z]{2}$/;
|
|
113
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Accent-, case- and apostrophe-insensitive, so `espana`, `España` and `ESPAÑA` are one
|
|
121
|
+
* string - and so are `Cote d'Ivoire` and `Côte d’Ivoire`.
|
|
122
|
+
*
|
|
123
|
+
* The apostrophe matters more than it looks: CLDR writes the typographic one (U+2019) and
|
|
124
|
+
* every keyboard, database and API writes the straight one, so a lookup that kept them
|
|
125
|
+
* apart failed on exactly the countries whose names contain one.
|
|
126
|
+
*/
|
|
127
|
+
function fold(value: string): string {
|
|
128
|
+
return value
|
|
129
|
+
.normalize("NFD")
|
|
130
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
131
|
+
.replace(/['‘’ʼ`´]/g, "")
|
|
132
|
+
.toLowerCase()
|
|
133
|
+
// Three spellings of the same name, and all three are mechanical - CLDR writes
|
|
134
|
+
// "St. Lucia" and "Trinidad & Tobago", while a form, a database and an API write
|
|
135
|
+
// "Saint Lucia" and "Trinidad and Tobago". Both sides of the lookup go through this,
|
|
136
|
+
// so it is a normalization rather than a table of alternative names.
|
|
137
|
+
.replace(/&/g, " and ")
|
|
138
|
+
.replace(/\bst\.?\s/g, "saint ")
|
|
139
|
+
.replace(/^the\s+/, "")
|
|
140
|
+
.replace(/[.,]/g, "")
|
|
141
|
+
.replace(/\s+/g, " ")
|
|
142
|
+
.trim();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Every two-letter region the runtime can name, folded, mapped back to its code.
|
|
147
|
+
*
|
|
148
|
+
* Built by asking `Intl.DisplayNames` for all 676 letter pairs and keeping the ones it has a
|
|
149
|
+
* name for - which is also how an unknown code is detected, since it echoes the input back.
|
|
150
|
+
* The runtime already holds this data, translated and maintained; shipping a table of 250
|
|
151
|
+
* country names would be a copy of it that goes stale.
|
|
152
|
+
*
|
|
153
|
+
* Memoized per locale and built only when something that is not a code arrives, so a page
|
|
154
|
+
* that passes codes never pays for it.
|
|
155
|
+
*/
|
|
156
|
+
const nameIndexes = new Map<string, Map<string, string> | null>();
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The code a region is FILED under.
|
|
160
|
+
*
|
|
161
|
+
* `Intl.Locale` canonicalizes a deprecated or alias subtag, which is the difference between
|
|
162
|
+
* a flag and a 404: `UK` is not the ISO code for the United Kingdom (`GB` is), `AN` became
|
|
163
|
+
* `CW`, `SU` became `RU`. All three have a name, so a name lookup that skipped this step
|
|
164
|
+
* resolved "United Kingdom" to `uk` and asked for a file that does not exist.
|
|
165
|
+
*/
|
|
166
|
+
function canonicalRegion(code: string): string {
|
|
167
|
+
try {
|
|
168
|
+
return new Intl.Locale(`und-${code}`).region?.toLowerCase() ?? code.toLowerCase();
|
|
169
|
+
} catch {
|
|
170
|
+
return code.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The name the runtime uses for a region it does not know.
|
|
176
|
+
*
|
|
177
|
+
* `ZZ` IS that region by definition, so its name is the sentinel: any code the runtime
|
|
178
|
+
* cannot place comes back either echoing the code (`QQ`) or carrying this exact string, and
|
|
179
|
+
* both have to be refused. Comparing against `of("ZZ")` rather than the English words keeps
|
|
180
|
+
* that true in every language.
|
|
181
|
+
*/
|
|
182
|
+
function unknownRegionName(names: Intl.DisplayNames): string | undefined {
|
|
183
|
+
return names.of("ZZ");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function regionsByName(locale: string): Map<string, string> | null {
|
|
187
|
+
const cached = nameIndexes.get(locale);
|
|
188
|
+
if (cached !== undefined) return cached;
|
|
189
|
+
|
|
190
|
+
let index: Map<string, string> | null = null;
|
|
191
|
+
try {
|
|
192
|
+
const names = new Intl.DisplayNames([locale], { type: "region" });
|
|
193
|
+
const unknown = unknownRegionName(names);
|
|
194
|
+
index = new Map();
|
|
195
|
+
for (let first = 65; first <= 90; first++) {
|
|
196
|
+
for (let second = 65; second <= 90; second++) {
|
|
197
|
+
const code = String.fromCharCode(first, second);
|
|
198
|
+
const name = names.of(code);
|
|
199
|
+
if (!name || name === code || name === unknown) continue;
|
|
200
|
+
const key = fold(name);
|
|
201
|
+
// First wins, and the code is canonical, so an alias can neither overwrite
|
|
202
|
+
// the real entry nor become one.
|
|
203
|
+
if (!index.has(key)) index.set(key, canonicalRegion(code));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
index = null; // No Intl, or no data for this locale.
|
|
208
|
+
}
|
|
209
|
+
nameIndexes.set(locale, index);
|
|
210
|
+
return index;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Whether the runtime knows this two-letter code as a region at all. */
|
|
214
|
+
function isKnownRegion(code: string): boolean {
|
|
215
|
+
try {
|
|
216
|
+
const names = new Intl.DisplayNames(["en"], { type: "region" });
|
|
217
|
+
const upper = code.toUpperCase();
|
|
218
|
+
const name = names.of(upper);
|
|
219
|
+
return Boolean(name) && name !== upper && name !== unknownRegionName(names);
|
|
220
|
+
} catch {
|
|
221
|
+
// Without Intl there is nothing to check against, and refusing every code would be
|
|
222
|
+
// worse than passing one through: two letters is the shape of a flag file.
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
114
227
|
/**
|
|
115
228
|
* Anything a codebase calls a country into the file name the sets use, or null.
|
|
116
229
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
230
|
+
* What it accepts: a code (`es`, `ES`), a locale tag (`en-GB`, `es_ES`), a subdivision or
|
|
231
|
+
* named file (`gb-eng`, `au-nsw`, `easter_island`), the emoji flag you are replacing, and
|
|
232
|
+
* the country's NAME - in English or in the page's own language, accents optional, so
|
|
233
|
+
* `Spain`, `España` and `espana` all resolve. A name is what a database column or an API
|
|
234
|
+
* payload usually holds, and requiring the caller to convert it first is how a flag ends up
|
|
235
|
+
* not being rendered at all.
|
|
236
|
+
*
|
|
237
|
+
* Null rather than a guess, and that is the point of the strictness: `banana` used to pass
|
|
238
|
+
* as a file code and render as a 404 with no alt text, which is worse than no flag. A
|
|
239
|
+
* two-letter code is checked against the runtime's own region list, a bare word is resolved
|
|
240
|
+
* as a name or refused, and only a separator-carrying file name is taken on trust, because
|
|
241
|
+
* nothing can check those without shipping the whole directory listing.
|
|
121
242
|
*/
|
|
122
|
-
export function normalizeFlagCode(value: string | null | undefined): string | null {
|
|
243
|
+
export function normalizeFlagCode(value: string | null | undefined, locale?: string): string | null {
|
|
123
244
|
if (!value) return null;
|
|
124
245
|
const raw = value.trim();
|
|
125
246
|
if (!raw) return null;
|
|
@@ -129,11 +250,25 @@ export function normalizeFlagCode(value: string | null | undefined): string | nu
|
|
|
129
250
|
return letters.join("");
|
|
130
251
|
}
|
|
131
252
|
|
|
132
|
-
const
|
|
133
|
-
if (
|
|
253
|
+
const tag = LOCALE_TAG.exec(raw);
|
|
254
|
+
if (tag) return tag[2].toLowerCase();
|
|
255
|
+
|
|
256
|
+
const lower = raw.toLowerCase();
|
|
257
|
+
// Canonical, so `uk` reaches the file the United Kingdom is actually filed under.
|
|
258
|
+
if (COUNTRY_CODE.test(lower)) return isKnownRegion(lower) ? canonicalRegion(lower) : null;
|
|
259
|
+
if (FILE_CODE.test(lower)) return lower;
|
|
134
260
|
|
|
135
|
-
|
|
136
|
-
|
|
261
|
+
// A name, then, in this order: the locale you asked for, the page's own, English, and
|
|
262
|
+
// finally the runtime's. English is always tried because it is what an API or a database
|
|
263
|
+
// column holds even on a translated page; the runtime's is last so it can rescue a name
|
|
264
|
+
// nothing else placed without ever overriding a match that was more specific.
|
|
265
|
+
const folded = fold(raw);
|
|
266
|
+
for (const candidate of [locale, documentLocale(), "en", runtimeLocale()]) {
|
|
267
|
+
if (!candidate) continue;
|
|
268
|
+
const found = regionsByName(candidate)?.get(folded);
|
|
269
|
+
if (found) return found;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
137
272
|
}
|
|
138
273
|
|
|
139
274
|
/**
|
|
@@ -161,6 +296,15 @@ export function flagName(code: string | null | undefined, locale?: string): stri
|
|
|
161
296
|
}
|
|
162
297
|
}
|
|
163
298
|
|
|
299
|
+
/** Whatever language this runtime is set to - the browser's, or the server's. */
|
|
300
|
+
function runtimeLocale(): string | undefined {
|
|
301
|
+
try {
|
|
302
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
164
308
|
/** The page's own language, which is what a translated page wants the name written in. */
|
|
165
309
|
function documentLocale(): string | undefined {
|
|
166
310
|
if (typeof document === "undefined") return undefined;
|