@daanvandenbergh/i18nkit 1.0.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 +285 -0
- package/dist/i18n/catalog.d.ts +18 -0
- package/dist/i18n/catalog.js +17 -0
- package/dist/i18n/detect.d.ts +34 -0
- package/dist/i18n/detect.js +62 -0
- package/dist/i18n/i18n.d.ts +178 -0
- package/dist/i18n/i18n.js +254 -0
- package/dist/i18n/index.d.ts +11 -0
- package/dist/i18n/index.js +10 -0
- package/dist/i18n/routing.d.ts +112 -0
- package/dist/i18n/routing.js +157 -0
- package/dist/i18n/translate.d.ts +26 -0
- package/dist/i18n/translate.js +28 -0
- package/dist/i18n/types.d.ts +155 -0
- package/dist/i18n/types.js +11 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/react/LanguagePicker.d.ts +21 -0
- package/dist/react/LanguagePicker.js +70 -0
- package/dist/react/LocaleLink.d.ts +21 -0
- package/dist/react/LocaleLink.js +23 -0
- package/dist/react/flags.d.ts +21 -0
- package/dist/react/flags.js +348 -0
- package/dist/react/index.d.ts +10 -0
- package/dist/react/index.js +10 -0
- package/dist/react/provider.d.ts +58 -0
- package/dist/react/provider.js +94 -0
- package/dist/react/styles.css +150 -0
- package/package.json +77 -0
- package/skills/i18nkit-add-locale/SKILL.md +182 -0
- package/skills/i18nkit-add-locale/reference/translation-rules.md +207 -0
- package/skills/i18nkit-add-locale/scripts/scan.sh +148 -0
- package/skills/i18nkit-sweep/SKILL.md +208 -0
- package/skills/i18nkit-sweep/reference/rules.md +174 -0
- package/skills/i18nkit-sweep/scripts/find_candidates.sh +99 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* An opt-in set of inline-SVG country flags for {@link LanguagePicker}, so the common case needs no
|
|
4
|
+
* `renderFlag` boilerplate: pass the shipped {@link localeFlag} helper and every locale that maps to
|
|
5
|
+
* a known flag draws one.
|
|
6
|
+
*
|
|
7
|
+
* Inline SVG (not emoji flags) because emoji flags render as bare letters on Windows/Chrome. Every
|
|
8
|
+
* flag is drawn in the same flat, rounded "chip" style - a uniform 4:3 rounded rectangle with a
|
|
9
|
+
* hairline border for edge definition on light surfaces - so a picker of mixed languages reads as
|
|
10
|
+
* one clean, consistent set.
|
|
11
|
+
*
|
|
12
|
+
* Flags are keyed by ISO 3166-1 region code, and a locale string is resolved to a region by
|
|
13
|
+
* {@link resolveFlagRegion}: an explicit region subtag wins (`pt-BR` -> Brazil), otherwise the
|
|
14
|
+
* language's conventional region is used (`pt` -> Portugal, `en` -> United Kingdom). A locale with
|
|
15
|
+
* no known flag renders nothing, so the picker falls back to its label - never a broken image.
|
|
16
|
+
*/
|
|
17
|
+
import { useId } from "react";
|
|
18
|
+
/** Border stroke shared by every chip, for edge definition on light surfaces. */
|
|
19
|
+
const CHIP_BORDER = "rgba(11, 27, 54, 0.14)";
|
|
20
|
+
/**
|
|
21
|
+
* The shared chip frame: a 60x45 `viewBox`, rounded corners that clip the content, and a hairline
|
|
22
|
+
* border. Every flag renders its content inside this so they all share one shape.
|
|
23
|
+
*
|
|
24
|
+
* @param props.size - the rendered width in px (height is 3/4 of it).
|
|
25
|
+
* @param props.children - the flag's inner SVG content, clipped to the rounded rectangle.
|
|
26
|
+
* @returns the framed flag SVG.
|
|
27
|
+
*/
|
|
28
|
+
function FlagChip({ size, children }) {
|
|
29
|
+
const uid = useId();
|
|
30
|
+
const clip = `i18nkit-flag-${uid}`;
|
|
31
|
+
return (_jsxs("svg", { width: size, height: (size * 3) / 4, viewBox: "0 0 60 45", role: "img", "aria-hidden": true, focusable: false, style: { display: "block" }, children: [_jsx("clipPath", { id: clip, children: _jsx("rect", { width: 60, height: 45, rx: 7 }) }), _jsx("g", { clipPath: `url(#${clip})`, children: children }), _jsx("rect", { x: 0.6, y: 0.6, width: 58.8, height: 43.8, rx: 6.4, fill: "none", stroke: CHIP_BORDER, strokeWidth: 1.2 })] }));
|
|
32
|
+
}
|
|
33
|
+
/** Unit five-point star path (outer radius 1, pointing up); scaled/positioned via {@link Star}. */
|
|
34
|
+
const STAR_PATH = "M0,-1 L0.2245,-0.309 L0.9511,-0.309 L0.3633,0.118 L0.5878,0.809 " +
|
|
35
|
+
"L0,0.382 L-0.5878,0.809 L-0.3633,0.118 L-0.9511,-0.309 L-0.2245,-0.309 Z";
|
|
36
|
+
/**
|
|
37
|
+
* A filled five-point star.
|
|
38
|
+
*
|
|
39
|
+
* @param props.cx - centre x in `viewBox` units.
|
|
40
|
+
* @param props.cy - centre y in `viewBox` units.
|
|
41
|
+
* @param props.r - outer radius in `viewBox` units.
|
|
42
|
+
* @param props.fill - fill colour.
|
|
43
|
+
* @param props.rotate - clockwise rotation in degrees (default 0, a point facing up).
|
|
44
|
+
* @returns the star path element.
|
|
45
|
+
*/
|
|
46
|
+
function Star({ cx, cy, r, fill, rotate = 0, }) {
|
|
47
|
+
return _jsx("path", { d: STAR_PATH, fill: fill, transform: `translate(${cx} ${cy}) rotate(${rotate}) scale(${r})` });
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Equal horizontal bands, top to bottom. Bands overlap by a hair to avoid anti-alias seams.
|
|
51
|
+
*
|
|
52
|
+
* @param colors - the band colours, top-first.
|
|
53
|
+
* @returns the band rects.
|
|
54
|
+
*/
|
|
55
|
+
function horizontalBands(colors) {
|
|
56
|
+
const h = 45 / colors.length;
|
|
57
|
+
return (_jsx(_Fragment, { children: colors.map((color, i) => (_jsx("rect", { y: i * h, width: 60, height: h + 0.3, fill: color }, i))) }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Equal vertical bands, hoist (left) to fly (right). Bands overlap by a hair to avoid seams.
|
|
61
|
+
*
|
|
62
|
+
* @param colors - the band colours, hoist-first.
|
|
63
|
+
* @returns the band rects.
|
|
64
|
+
*/
|
|
65
|
+
function verticalBands(colors) {
|
|
66
|
+
const w = 60 / colors.length;
|
|
67
|
+
return (_jsx(_Fragment, { children: colors.map((color, i) => (_jsx("rect", { x: i * w, width: w + 0.3, height: 45, fill: color }, i))) }));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Weighted horizontal bands, top to bottom - for flags whose stripes are unequal (e.g. Spain's
|
|
71
|
+
* 1:2:1, Thailand's 1:1:2:1:1).
|
|
72
|
+
*
|
|
73
|
+
* @param bands - `[color, weight]` pairs, top-first; weights are relative.
|
|
74
|
+
* @returns the band rects.
|
|
75
|
+
*/
|
|
76
|
+
function weightedHorizontalBands(bands) {
|
|
77
|
+
const total = bands.reduce((sum, [, weight]) => sum + weight, 0);
|
|
78
|
+
let y = 0;
|
|
79
|
+
return (_jsx(_Fragment, { children: bands.map(([color, weight], i) => {
|
|
80
|
+
const h = (weight / total) * 45;
|
|
81
|
+
const rect = _jsx("rect", { y: y, width: 60, height: h + 0.3, fill: color }, i);
|
|
82
|
+
y += h;
|
|
83
|
+
return rect;
|
|
84
|
+
}) }));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* A Nordic (off-centre) cross: a full field with a cross whose vertical bar sits left of centre.
|
|
88
|
+
* When `inner` is given the cross is drawn twice - a wide `cross`-coloured border under a narrow
|
|
89
|
+
* `inner` bar - for the bordered crosses (Norway, Iceland).
|
|
90
|
+
*
|
|
91
|
+
* @param field - the field (background) colour.
|
|
92
|
+
* @param cross - the cross colour (the border colour when `inner` is set).
|
|
93
|
+
* @param inner - optional inner-bar colour for a bordered cross.
|
|
94
|
+
* @returns the field and cross rects.
|
|
95
|
+
*/
|
|
96
|
+
function nordicCross(field, cross, inner) {
|
|
97
|
+
return (_jsxs(_Fragment, { children: [_jsx("rect", { width: 60, height: 45, fill: field }), inner !== undefined ? (_jsxs(_Fragment, { children: [_jsx("rect", { x: 12, width: 12, height: 45, fill: cross }), _jsx("rect", { y: 16.5, width: 60, height: 12, fill: cross }), _jsx("rect", { x: 15, width: 6, height: 45, fill: inner }), _jsx("rect", { y: 19.5, width: 60, height: 6, fill: inner })] })) : (_jsxs(_Fragment, { children: [_jsx("rect", { x: 14, width: 8, height: 45, fill: cross }), _jsx("rect", { y: 18.5, width: 60, height: 8, fill: cross })] }))] }));
|
|
98
|
+
}
|
|
99
|
+
/** The United Kingdom (Union Jack): a blue field, white then counterchanged-red saltire, and cross. */
|
|
100
|
+
function UnitedKingdom({ size }) {
|
|
101
|
+
const uid = useId();
|
|
102
|
+
const counter = `i18nkit-uk-${uid}`;
|
|
103
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#012169" }), _jsx("clipPath", { id: counter, children: _jsx("path", { d: "M30,22.5 h30 v22.5 z v22.5 h-30 z h-30 v-22.5 z v-22.5 h30 z" }) }), _jsx("path", { d: "M0,0 L60,45 M60,0 L0,45", stroke: "#fff", strokeWidth: 9 }), _jsx("path", { d: "M0,0 L60,45 M60,0 L0,45", clipPath: `url(#${counter})`, stroke: "#C8102E", strokeWidth: 4 }), _jsx("path", { d: "M30,0 v45 M0,22.5 h60", stroke: "#fff", strokeWidth: 15 }), _jsx("path", { d: "M30,0 v45 M0,22.5 h60", stroke: "#C8102E", strokeWidth: 9 })] }));
|
|
104
|
+
}
|
|
105
|
+
/** The United States: 13 red/white stripes and a blue canton. ponytail: neat star grid, not the exact 50. */
|
|
106
|
+
function UnitedStates({ size }) {
|
|
107
|
+
const stripeH = 45 / 13;
|
|
108
|
+
const cantonH = stripeH * 7;
|
|
109
|
+
const rows = [4, 8, 12, 16, 20];
|
|
110
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#B22234" }), Array.from({ length: 6 }, (_, i) => (_jsx("rect", { y: stripeH * (2 * i + 1), width: 60, height: stripeH + 0.3, fill: "#fff" }, i))), _jsx("rect", { width: 24, height: cantonH, fill: "#3C3B6E" }), rows.flatMap((y, r) => {
|
|
111
|
+
const cols = r % 2 === 0 ? [2.4, 7.2, 12, 16.8, 21.6] : [4.8, 9.6, 14.4, 19.2];
|
|
112
|
+
return cols.map((x, c) => _jsx(Star, { cx: x, cy: y, r: 1.3, fill: "#fff" }, `${r}-${c}`));
|
|
113
|
+
})] }));
|
|
114
|
+
}
|
|
115
|
+
/** Spain: red/yellow/red horizontal bands in a 1:2:1 ratio. ponytail: coat of arms omitted. */
|
|
116
|
+
function Spain({ size }) {
|
|
117
|
+
return _jsx(FlagChip, { size: size, children: weightedHorizontalBands([["#AA151B", 1], ["#F1BF00", 2], ["#AA151B", 1]]) });
|
|
118
|
+
}
|
|
119
|
+
/** Portugal: green/red vertical split with a simplified armillary-and-shield emblem. ponytail: emblem simplified. */
|
|
120
|
+
function Portugal({ size }) {
|
|
121
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 24, height: 45, fill: "#046A38" }), _jsx("rect", { x: 24, width: 36, height: 45, fill: "#DA020E" }), _jsx("circle", { cx: 24, cy: 22.5, r: 6, fill: "none", stroke: "#FFE800", strokeWidth: 1.4 }), _jsx("circle", { cx: 24, cy: 22.5, r: 3.4, fill: "#fff" }), _jsx("rect", { x: 22.3, y: 19.6, width: 3.4, height: 5.8, rx: 0.6, fill: "#DA020E" })] }));
|
|
122
|
+
}
|
|
123
|
+
/** Czechia: white over red with a blue triangle from the hoist. */
|
|
124
|
+
function Czechia({ size }) {
|
|
125
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 22.5, fill: "#fff" }), _jsx("rect", { y: 22.5, width: 60, height: 22.5, fill: "#D7141A" }), _jsx("path", { d: "M0,0 L30,22.5 L0,45 Z", fill: "#11457E" })] }));
|
|
126
|
+
}
|
|
127
|
+
/** Greece: nine blue/white stripes with a blue canton bearing a white cross. */
|
|
128
|
+
function Greece({ size }) {
|
|
129
|
+
const stripe = 45 / 9;
|
|
130
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#0D5EAF" }), Array.from({ length: 4 }, (_, i) => (_jsx("rect", { y: stripe * (2 * i + 1), width: 60, height: stripe + 0.3, fill: "#fff" }, i))), _jsx("rect", { width: stripe * 5, height: stripe * 5, fill: "#0D5EAF" }), _jsx("rect", { x: stripe * 2, width: stripe, height: stripe * 5, fill: "#fff" }), _jsx("rect", { y: stripe * 2, width: stripe * 5, height: stripe, fill: "#fff" })] }));
|
|
131
|
+
}
|
|
132
|
+
/** Switzerland: a bold white cross on red. ponytail: square flag drawn on the shared 4:3 chip. */
|
|
133
|
+
function Switzerland({ size }) {
|
|
134
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#D52B1E" }), _jsx("rect", { x: 26, y: 10.5, width: 8, height: 24, fill: "#fff" }), _jsx("rect", { x: 18, y: 18.5, width: 24, height: 8, fill: "#fff" })] }));
|
|
135
|
+
}
|
|
136
|
+
/** Japan: a red disc centred on white. */
|
|
137
|
+
function Japan({ size }) {
|
|
138
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#fff" }), _jsx("circle", { cx: 30, cy: 22.5, r: 9, fill: "#BC002D" })] }));
|
|
139
|
+
}
|
|
140
|
+
/** China: one large and four small yellow stars in the canton of a red field. */
|
|
141
|
+
function China({ size }) {
|
|
142
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#DE2910" }), _jsx(Star, { cx: 10, cy: 9.5, r: 5.5, fill: "#FFDE00" }), _jsx(Star, { cx: 19.5, cy: 4, r: 1.9, fill: "#FFDE00", rotate: 20 }), _jsx(Star, { cx: 23, cy: 8.5, r: 1.9, fill: "#FFDE00", rotate: 45 }), _jsx(Star, { cx: 23, cy: 14, r: 1.9, fill: "#FFDE00", rotate: 70 }), _jsx(Star, { cx: 19.5, cy: 18, r: 1.9, fill: "#FFDE00", rotate: 20 })] }));
|
|
143
|
+
}
|
|
144
|
+
/** Vietnam: a large yellow star centred on red. */
|
|
145
|
+
function Vietnam({ size }) {
|
|
146
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#DA251D" }), _jsx(Star, { cx: 30, cy: 22.5, r: 12, fill: "#FFFF00" })] }));
|
|
147
|
+
}
|
|
148
|
+
/** Turkey: a white crescent and star on red. */
|
|
149
|
+
function Turkey({ size }) {
|
|
150
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#E30A17" }), _jsx("circle", { cx: 22, cy: 22.5, r: 10, fill: "#fff" }), _jsx("circle", { cx: 25.5, cy: 22.5, r: 8, fill: "#E30A17" }), _jsx(Star, { cx: 37, cy: 22.5, r: 4.6, fill: "#fff", rotate: -18 })] }));
|
|
151
|
+
}
|
|
152
|
+
/** South Korea: the red/blue taegeuk with four black trigrams. ponytail: trigram bars drawn solid. */
|
|
153
|
+
function SouthKorea({ size }) {
|
|
154
|
+
/** Three short parallel black bars (a simplified trigram) at a corner, angled toward centre. */
|
|
155
|
+
function Bars({ x, y, angle }) {
|
|
156
|
+
return (_jsxs("g", { transform: `translate(${x} ${y}) rotate(${angle})`, fill: "#000", children: [_jsx("rect", { x: -4.5, y: -2.7, width: 9, height: 1.3 }), _jsx("rect", { x: -4.5, y: -0.65, width: 9, height: 1.3 }), _jsx("rect", { x: -4.5, y: 1.4, width: 9, height: 1.3 })] }));
|
|
157
|
+
}
|
|
158
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#fff" }), _jsx("circle", { cx: 30, cy: 22.5, r: 9, fill: "#003478" }), _jsx("path", { d: "M30,13.5 A9,9 0 0,1 30,31.5 A4.5,4.5 0 0,1 30,22.5 A4.5,4.5 0 0,0 30,13.5 Z", fill: "#C60C30" }), _jsx(Bars, { x: 12, y: 10, angle: -34 }), _jsx(Bars, { x: 48, y: 10, angle: 34 }), _jsx(Bars, { x: 12, y: 35, angle: 34 }), _jsx(Bars, { x: 48, y: 35, angle: -34 })] }));
|
|
159
|
+
}
|
|
160
|
+
/** India: saffron/white/green bands with a navy Ashoka chakra. */
|
|
161
|
+
function India({ size }) {
|
|
162
|
+
return (_jsxs(FlagChip, { size: size, children: [horizontalBands(["#FF9933", "#fff", "#138808"]), _jsx("g", { stroke: "#000080", strokeWidth: 0.5, children: Array.from({ length: 12 }, (_, k) => (_jsx("line", { x1: 30, y1: 22.5, x2: 30, y2: 18.1, transform: `rotate(${k * 30} 30 22.5)` }, k))) }), _jsx("circle", { cx: 30, cy: 22.5, r: 4.4, fill: "none", stroke: "#000080", strokeWidth: 1 }), _jsx("circle", { cx: 30, cy: 22.5, r: 0.9, fill: "#000080" })] }));
|
|
163
|
+
}
|
|
164
|
+
/** Israel: two blue stripes and a Star of David on white. */
|
|
165
|
+
function Israel({ size }) {
|
|
166
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#fff" }), _jsx("rect", { y: 9, width: 60, height: 4.2, fill: "#0038B8" }), _jsx("rect", { y: 31.8, width: 60, height: 4.2, fill: "#0038B8" }), _jsx("path", { d: "M30,16 L37.5,29 L22.5,29 Z", fill: "none", stroke: "#0038B8", strokeWidth: 1.3 }), _jsx("path", { d: "M30,29 L37.5,16 L22.5,16 Z", fill: "none", stroke: "#0038B8", strokeWidth: 1.3 })] }));
|
|
167
|
+
}
|
|
168
|
+
/** Brazil: a yellow rhombus and blue disc on green. ponytail: constellation and banner omitted. */
|
|
169
|
+
function Brazil({ size }) {
|
|
170
|
+
return (_jsxs(FlagChip, { size: size, children: [_jsx("rect", { width: 60, height: 45, fill: "#009C3B" }), _jsx("path", { d: "M30,6 L54,22.5 L30,39 L6,22.5 Z", fill: "#FFDF00" }), _jsx("circle", { cx: 30, cy: 22.5, r: 8.5, fill: "#002776" })] }));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Every known flag, keyed by lowercase ISO 3166-1 region code. Simple flags are data (bands or a
|
|
174
|
+
* Nordic cross); emblem flags carry a `custom` renderer. Add a region here and, if bare language
|
|
175
|
+
* codes should reach it, an entry in {@link DEFAULT_REGION_BY_LANGUAGE}.
|
|
176
|
+
*/
|
|
177
|
+
const FLAG_BY_REGION = {
|
|
178
|
+
gb: { custom: UnitedKingdom },
|
|
179
|
+
us: { custom: UnitedStates },
|
|
180
|
+
nl: { h: ["#AE1C28", "#fff", "#21468B"] },
|
|
181
|
+
de: { h: ["#000000", "#DD0000", "#FFCE00"] },
|
|
182
|
+
ru: { h: ["#fff", "#0039A6", "#D52B1E"] },
|
|
183
|
+
hu: { h: ["#CD2A3E", "#fff", "#436F4D"] },
|
|
184
|
+
bg: { h: ["#fff", "#00966E", "#D62612"] },
|
|
185
|
+
at: { h: ["#ED2939", "#fff", "#ED2939"] },
|
|
186
|
+
lt: { h: ["#FDB913", "#006A44", "#C1272D"] },
|
|
187
|
+
ee: { h: ["#0072CE", "#000000", "#fff"] },
|
|
188
|
+
pl: { h: ["#fff", "#DC143C"] },
|
|
189
|
+
ua: { h: ["#0057B7", "#FFD700"] },
|
|
190
|
+
id: { h: ["#FF0000", "#fff"] },
|
|
191
|
+
lv: { hw: [["#9E3039", 2], ["#fff", 1], ["#9E3039", 2]] },
|
|
192
|
+
es: { custom: Spain },
|
|
193
|
+
th: { hw: [["#A51931", 1], ["#F4F5F8", 1], ["#2D2A4A", 2], ["#F4F5F8", 1], ["#A51931", 1]] },
|
|
194
|
+
fr: { v: ["#002395", "#fff", "#ED2939"] },
|
|
195
|
+
it: { v: ["#008C45", "#F4F5F0", "#CD212A"] },
|
|
196
|
+
ie: { v: ["#169B62", "#fff", "#FF883E"] },
|
|
197
|
+
be: { v: ["#000000", "#FDDA24", "#EF3340"] },
|
|
198
|
+
ro: { v: ["#002B7F", "#FCD116", "#CE1126"] },
|
|
199
|
+
se: { nordic: { field: "#006AA7", cross: "#FECC00" } },
|
|
200
|
+
dk: { nordic: { field: "#C8102E", cross: "#fff" } },
|
|
201
|
+
fi: { nordic: { field: "#fff", cross: "#003580" } },
|
|
202
|
+
no: { nordic: { field: "#EF2B2D", cross: "#fff", inner: "#002868" } },
|
|
203
|
+
is: { nordic: { field: "#02529C", cross: "#fff", inner: "#DC1E35" } },
|
|
204
|
+
pt: { custom: Portugal },
|
|
205
|
+
cz: { custom: Czechia },
|
|
206
|
+
gr: { custom: Greece },
|
|
207
|
+
ch: { custom: Switzerland },
|
|
208
|
+
jp: { custom: Japan },
|
|
209
|
+
cn: { custom: China },
|
|
210
|
+
vn: { custom: Vietnam },
|
|
211
|
+
tr: { custom: Turkey },
|
|
212
|
+
kr: { custom: SouthKorea },
|
|
213
|
+
in: { custom: India },
|
|
214
|
+
il: { custom: Israel },
|
|
215
|
+
br: { custom: Brazil },
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* The conventional flag region for a bare language subtag (no region in the locale), so `de` shows
|
|
219
|
+
* Germany, `en` the United Kingdom, `pt` Portugal. A region subtag in the locale always overrides
|
|
220
|
+
* this (see {@link resolveFlagRegion}).
|
|
221
|
+
*/
|
|
222
|
+
const DEFAULT_REGION_BY_LANGUAGE = {
|
|
223
|
+
en: "gb",
|
|
224
|
+
nl: "nl",
|
|
225
|
+
de: "de",
|
|
226
|
+
ru: "ru",
|
|
227
|
+
hu: "hu",
|
|
228
|
+
bg: "bg",
|
|
229
|
+
lt: "lt",
|
|
230
|
+
et: "ee",
|
|
231
|
+
pl: "pl",
|
|
232
|
+
uk: "ua",
|
|
233
|
+
id: "id",
|
|
234
|
+
lv: "lv",
|
|
235
|
+
es: "es",
|
|
236
|
+
th: "th",
|
|
237
|
+
fr: "fr",
|
|
238
|
+
it: "it",
|
|
239
|
+
ga: "ie",
|
|
240
|
+
ro: "ro",
|
|
241
|
+
sv: "se",
|
|
242
|
+
da: "dk",
|
|
243
|
+
fi: "fi",
|
|
244
|
+
nb: "no",
|
|
245
|
+
nn: "no",
|
|
246
|
+
no: "no",
|
|
247
|
+
is: "is",
|
|
248
|
+
pt: "pt",
|
|
249
|
+
cs: "cz",
|
|
250
|
+
el: "gr",
|
|
251
|
+
ja: "jp",
|
|
252
|
+
zh: "cn",
|
|
253
|
+
vi: "vn",
|
|
254
|
+
tr: "tr",
|
|
255
|
+
ko: "kr",
|
|
256
|
+
hi: "in",
|
|
257
|
+
he: "il",
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Resolve a locale string to a known flag region, or `undefined` when none matches.
|
|
261
|
+
*
|
|
262
|
+
* A region subtag present in the locale wins when we have that flag (`pt-BR` -> `br`, `de-CH` ->
|
|
263
|
+
* `ch`); a region we do not ship a flag for is ignored and resolution falls back to the language's
|
|
264
|
+
* conventional region (`es-MX` -> `es`, `en-CA` -> `gb`). Script subtags (`zh-Hans`) are skipped.
|
|
265
|
+
*
|
|
266
|
+
* @param locale - a BCP-47-ish locale code (`-` or `_` separated, any case).
|
|
267
|
+
* @returns the lowercase region code of a known flag, or `undefined`.
|
|
268
|
+
*/
|
|
269
|
+
function resolveFlagRegion(locale) {
|
|
270
|
+
const parts = locale.toLowerCase().split(/[-_]/);
|
|
271
|
+
for (let i = 1; i < parts.length; i++) {
|
|
272
|
+
const part = parts[i];
|
|
273
|
+
// A singleton subtag (`t`, `u`, `x`, ...) opens an extension / private-use section; no
|
|
274
|
+
// region follows it, so stop before mistaking e.g. the `de` in `en-t-de` for a region.
|
|
275
|
+
if (part && part.length === 1) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (part && part.length === 2 && /^[a-z]{2}$/.test(part) && part in FLAG_BY_REGION) {
|
|
279
|
+
return part;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const region = DEFAULT_REGION_BY_LANGUAGE[parts[0] ?? ""];
|
|
283
|
+
return region !== undefined && region in FLAG_BY_REGION ? region : undefined;
|
|
284
|
+
}
|
|
285
|
+
/** Render one band/nordic {@link FlagSpec} inside a chip. Custom specs are handled by {@link flagNode}. */
|
|
286
|
+
function BandFlag({ spec, size }) {
|
|
287
|
+
let content;
|
|
288
|
+
if ("h" in spec) {
|
|
289
|
+
content = horizontalBands(spec.h);
|
|
290
|
+
}
|
|
291
|
+
else if ("v" in spec) {
|
|
292
|
+
content = verticalBands(spec.v);
|
|
293
|
+
}
|
|
294
|
+
else if ("hw" in spec) {
|
|
295
|
+
content = weightedHorizontalBands(spec.hw);
|
|
296
|
+
}
|
|
297
|
+
else if ("nordic" in spec) {
|
|
298
|
+
content = nordicCross(spec.nordic.field, spec.nordic.cross, spec.nordic.inner);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
// Custom specs never reach here; flagNode dispatches them.
|
|
302
|
+
content = _jsx(_Fragment, {});
|
|
303
|
+
}
|
|
304
|
+
return _jsx(FlagChip, { size: size, children: content });
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* The flag element for a locale, or `null` when no known flag matches (so callers render nothing).
|
|
308
|
+
*
|
|
309
|
+
* @param code - the locale code to resolve.
|
|
310
|
+
* @param size - the rendered width in px.
|
|
311
|
+
* @returns the flag element, or `null`.
|
|
312
|
+
*/
|
|
313
|
+
function flagNode(code, size) {
|
|
314
|
+
const region = resolveFlagRegion(code);
|
|
315
|
+
if (region === undefined) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const spec = FLAG_BY_REGION[region];
|
|
319
|
+
if (spec === undefined) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
if ("custom" in spec) {
|
|
323
|
+
const Custom = spec.custom;
|
|
324
|
+
return _jsx(Custom, { size: size });
|
|
325
|
+
}
|
|
326
|
+
return _jsx(BandFlag, { spec: spec, size: size });
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The flag for a locale, drawn as a flat rounded chip - or nothing when no known flag matches.
|
|
330
|
+
*
|
|
331
|
+
* @param props.code - the locale code (e.g. `"en"`, `"pt-BR"`, `"de-CH"`).
|
|
332
|
+
* @param props.size - the rendered width in px (default 20; height is 3/4 of it).
|
|
333
|
+
* @returns the flag SVG, or `null` for an unknown locale.
|
|
334
|
+
*/
|
|
335
|
+
export function Flag({ code, size = 20 }) {
|
|
336
|
+
return flagNode(code, size);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* A ready-made `renderFlag` for {@link LanguagePicker}: `<LanguagePicker renderFlag={localeFlag} />`.
|
|
340
|
+
* Returns the built-in flag for the locale, or `null` for locales without one (the picker then
|
|
341
|
+
* shows just the label).
|
|
342
|
+
*
|
|
343
|
+
* @param locale - the locale code to draw a flag for.
|
|
344
|
+
* @returns the flag node, or `null`.
|
|
345
|
+
*/
|
|
346
|
+
export function localeFlag(locale) {
|
|
347
|
+
return flagNode(locale, 20);
|
|
348
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React adapter for i18nkit (the `./react` entry). Framework-agnostic React: a provider, hooks,
|
|
3
|
+
* an accessible language picker, and a locale-aware link - none of which import `next/*`, so they
|
|
4
|
+
* work in any React setup. Pair with the core `@daanvandenbergh/i18nkit` entry (the `I18n` class)
|
|
5
|
+
* and, for the picker, `import "@daanvandenbergh/i18nkit/styles.css"`.
|
|
6
|
+
*/
|
|
7
|
+
export { I18nProvider, useI18n, useLocale, useSetLocale, useTranslator, type I18nProviderProps, } from "./provider.js";
|
|
8
|
+
export { LanguagePicker, type LanguagePickerProps } from "./LanguagePicker.js";
|
|
9
|
+
export { Flag, localeFlag } from "./flags.js";
|
|
10
|
+
export { LocaleLink, type LocaleLinkProps } from "./LocaleLink.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React adapter for i18nkit (the `./react` entry). Framework-agnostic React: a provider, hooks,
|
|
3
|
+
* an accessible language picker, and a locale-aware link - none of which import `next/*`, so they
|
|
4
|
+
* work in any React setup. Pair with the core `@daanvandenbergh/i18nkit` entry (the `I18n` class)
|
|
5
|
+
* and, for the picker, `import "@daanvandenbergh/i18nkit/styles.css"`.
|
|
6
|
+
*/
|
|
7
|
+
export { I18nProvider, useI18n, useLocale, useSetLocale, useTranslator, } from "./provider.js";
|
|
8
|
+
export { LanguagePicker } from "./LanguagePicker.js";
|
|
9
|
+
export { Flag, localeFlag } from "./flags.js";
|
|
10
|
+
export { LocaleLink } from "./LocaleLink.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
|
+
import type { I18n, Translator } from "../i18n/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* Props for {@link I18nProvider}.
|
|
5
|
+
*
|
|
6
|
+
* @typeParam L - the app's locale union (inferred from `i18n`).
|
|
7
|
+
*/
|
|
8
|
+
export interface I18nProviderProps<L extends string> {
|
|
9
|
+
/** The app's {@link I18n} instance. */
|
|
10
|
+
i18n: I18n<L>;
|
|
11
|
+
/** The active locale for this render (the single source of truth). */
|
|
12
|
+
locale: L;
|
|
13
|
+
/**
|
|
14
|
+
* Called after the locale cookie is written when the user picks a new locale. Do the app's
|
|
15
|
+
* navigation/refresh here (e.g. `router.refresh()`, `location.reload()`). Optional.
|
|
16
|
+
*/
|
|
17
|
+
onChange?: ((next: L) => void) | undefined;
|
|
18
|
+
/** The subtree that can read the locale. */
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Provide the active locale to a subtree.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam L - the app's locale union.
|
|
25
|
+
* @param props - see {@link I18nProviderProps}.
|
|
26
|
+
* @returns the provider element.
|
|
27
|
+
*/
|
|
28
|
+
export declare function I18nProvider<L extends string>({ i18n, locale, onChange, children, }: I18nProviderProps<L>): ReactElement;
|
|
29
|
+
/**
|
|
30
|
+
* The active {@link I18n} instance. Handy for reading `i18n.list`, routing helpers, etc. in a
|
|
31
|
+
* component. Pass your `Locale` union as the type argument to recover the precise instance type.
|
|
32
|
+
*
|
|
33
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
34
|
+
* @returns the instance.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useI18n<L extends string = string>(): I18n<L>;
|
|
37
|
+
/**
|
|
38
|
+
* The active locale. Pass your `Locale` union as the type argument for a precise return type.
|
|
39
|
+
*
|
|
40
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
41
|
+
* @returns the active locale.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useLocale<L extends string = string>(): L;
|
|
44
|
+
/**
|
|
45
|
+
* A {@link Translator} bound to the active locale: `const translate = useTranslator();`, then
|
|
46
|
+
* `translate(TX.title)`.
|
|
47
|
+
*
|
|
48
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
49
|
+
* @returns the translator.
|
|
50
|
+
*/
|
|
51
|
+
export declare function useTranslator<L extends string = string>(): Translator<L>;
|
|
52
|
+
/**
|
|
53
|
+
* The locale setter: persists the choice to the cookie and runs the provider's `onChange`.
|
|
54
|
+
*
|
|
55
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
56
|
+
* @returns a function that switches the active locale.
|
|
57
|
+
*/
|
|
58
|
+
export declare function useSetLocale<L extends string = string>(): (next: L) => void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* The React seam. `I18nProvider` carries the active locale (resolved however the app likes -
|
|
5
|
+
* from the URL, a cookie, or the server) down to descendants, which read it via {@link useLocale}
|
|
6
|
+
* / {@link useTranslator} and change it via {@link useSetLocale}.
|
|
7
|
+
*
|
|
8
|
+
* This layer is framework-agnostic: it never imports `next/*`. Navigation on locale change is the
|
|
9
|
+
* app's concern, injected through the provider's `onChange` prop (e.g. a Next `router.refresh`, a
|
|
10
|
+
* React Router navigate, or a full reload). The provider does persist the choice to the configured
|
|
11
|
+
* cookie first, so a server resolver sees it before any navigation the app triggers.
|
|
12
|
+
*/
|
|
13
|
+
import { createContext, useContext, useMemo, useRef } from "react";
|
|
14
|
+
const I18nContext = createContext(null);
|
|
15
|
+
/**
|
|
16
|
+
* Provide the active locale to a subtree.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam L - the app's locale union.
|
|
19
|
+
* @param props - see {@link I18nProviderProps}.
|
|
20
|
+
* @returns the provider element.
|
|
21
|
+
*/
|
|
22
|
+
export function I18nProvider({ i18n, locale, onChange, children, }) {
|
|
23
|
+
// Hold `onChange` in a ref so an inline prop (a new identity each render) does not bust the
|
|
24
|
+
// memo below and re-render every consumer.
|
|
25
|
+
const onChangeRef = useRef(onChange);
|
|
26
|
+
onChangeRef.current = onChange;
|
|
27
|
+
const value = useMemo(() => ({
|
|
28
|
+
i18n: i18n,
|
|
29
|
+
locale,
|
|
30
|
+
translator: i18n.translator(locale),
|
|
31
|
+
setLocale: (next) => {
|
|
32
|
+
if (next === locale) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof document !== "undefined") {
|
|
36
|
+
// Encode the value defensively; a valid BCP-47 locale is unchanged, but an app
|
|
37
|
+
// may forward unvalidated input, and a raw `;` would inject cookie attributes.
|
|
38
|
+
document.cookie = `${i18n.cookie}=${encodeURIComponent(next)}; path=/; max-age=31536000; samesite=lax`;
|
|
39
|
+
}
|
|
40
|
+
onChangeRef.current?.(next);
|
|
41
|
+
},
|
|
42
|
+
}), [i18n, locale]);
|
|
43
|
+
return _jsx(I18nContext.Provider, { value: value, children: children });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the i18n context, throwing a clear error when used outside an {@link I18nProvider}.
|
|
47
|
+
*
|
|
48
|
+
* @returns the context value.
|
|
49
|
+
*/
|
|
50
|
+
function useI18nContext() {
|
|
51
|
+
const ctx = useContext(I18nContext);
|
|
52
|
+
if (!ctx) {
|
|
53
|
+
throw new Error("i18nkit: hooks must be used within <I18nProvider>.");
|
|
54
|
+
}
|
|
55
|
+
return ctx;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The active {@link I18n} instance. Handy for reading `i18n.list`, routing helpers, etc. in a
|
|
59
|
+
* component. Pass your `Locale` union as the type argument to recover the precise instance type.
|
|
60
|
+
*
|
|
61
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
62
|
+
* @returns the instance.
|
|
63
|
+
*/
|
|
64
|
+
export function useI18n() {
|
|
65
|
+
return useI18nContext().i18n;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The active locale. Pass your `Locale` union as the type argument for a precise return type.
|
|
69
|
+
*
|
|
70
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
71
|
+
* @returns the active locale.
|
|
72
|
+
*/
|
|
73
|
+
export function useLocale() {
|
|
74
|
+
return useI18nContext().locale;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A {@link Translator} bound to the active locale: `const translate = useTranslator();`, then
|
|
78
|
+
* `translate(TX.title)`.
|
|
79
|
+
*
|
|
80
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
81
|
+
* @returns the translator.
|
|
82
|
+
*/
|
|
83
|
+
export function useTranslator() {
|
|
84
|
+
return useI18nContext().translator;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The locale setter: persists the choice to the cookie and runs the provider's `onChange`.
|
|
88
|
+
*
|
|
89
|
+
* @typeParam L - the app's locale union (defaults to `string`).
|
|
90
|
+
* @returns a function that switches the active locale.
|
|
91
|
+
*/
|
|
92
|
+
export function useSetLocale() {
|
|
93
|
+
return useI18nContext().setLocale;
|
|
94
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Styles for <LanguagePicker>. Import once in your app:
|
|
3
|
+
* import "@daanvandenbergh/i18nkit/styles.css";
|
|
4
|
+
*
|
|
5
|
+
* Every color is a CSS custom property with a sensible fallback, so the picker looks right with
|
|
6
|
+
* zero configuration and restyles by setting `--i18nkit-*` anywhere up the tree (e.g. on :root):
|
|
7
|
+
* :root { --i18nkit-color-primary: #7c3aed; --i18nkit-color-surface: #0b1220; }
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.i18nkit-picker {
|
|
11
|
+
position: relative;
|
|
12
|
+
display: inline-block;
|
|
13
|
+
flex-shrink: 0;
|
|
14
|
+
font-family: inherit;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.i18nkit-picker *,
|
|
18
|
+
.i18nkit-picker *::before,
|
|
19
|
+
.i18nkit-picker *::after {
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Trigger: a compact bordered chip showing the active locale + a caret. */
|
|
24
|
+
.i18nkit-picker__trigger {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 8px;
|
|
28
|
+
height: 38px;
|
|
29
|
+
padding: 0 12px;
|
|
30
|
+
border: 1px solid var(--i18nkit-color-border, #e2e8f0);
|
|
31
|
+
border-radius: 10px;
|
|
32
|
+
background: var(--i18nkit-color-surface, #ffffff);
|
|
33
|
+
color: var(--i18nkit-color-ink, #1a1a1a);
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
font: inherit;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
font-weight: 500;
|
|
38
|
+
line-height: 1;
|
|
39
|
+
transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.i18nkit-picker__trigger:hover {
|
|
43
|
+
background: var(--i18nkit-color-hover, #f4f7fa);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.i18nkit-picker__trigger:focus-visible {
|
|
47
|
+
outline: none;
|
|
48
|
+
border-color: var(--i18nkit-color-primary, #2563eb);
|
|
49
|
+
box-shadow: 0 0 0 3px var(--i18nkit-color-ring, rgba(37, 99, 235, 0.18));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.i18nkit-picker__flag {
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
line-height: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.i18nkit-picker__current {
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.i18nkit-picker__caret {
|
|
63
|
+
color: var(--i18nkit-color-muted, #64748b);
|
|
64
|
+
transition: transform 0.15s ease;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.i18nkit-picker__caret--open {
|
|
68
|
+
transform: rotate(180deg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Dropdown panel, right-aligned under the trigger. */
|
|
72
|
+
.i18nkit-picker__panel {
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: calc(100% + 8px);
|
|
75
|
+
right: 0;
|
|
76
|
+
min-width: 200px;
|
|
77
|
+
padding: 6px;
|
|
78
|
+
background: var(--i18nkit-color-surface, #ffffff);
|
|
79
|
+
border: 1px solid var(--i18nkit-color-border, #e2e8f0);
|
|
80
|
+
border-radius: 13px;
|
|
81
|
+
box-shadow: var(--i18nkit-shadow, 0 16px 38px rgba(11, 27, 54, 0.16));
|
|
82
|
+
z-index: 80;
|
|
83
|
+
animation: i18nkit-popdown 0.14s ease;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.i18nkit-picker__heading {
|
|
87
|
+
padding: 9px 10px 7px;
|
|
88
|
+
font-size: 10.5px;
|
|
89
|
+
font-weight: 600;
|
|
90
|
+
letter-spacing: 0.06em;
|
|
91
|
+
text-transform: uppercase;
|
|
92
|
+
color: var(--i18nkit-color-muted, #9aa5b5);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.i18nkit-picker__item {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 10px;
|
|
99
|
+
width: 100%;
|
|
100
|
+
padding: 9px 10px;
|
|
101
|
+
border: none;
|
|
102
|
+
border-radius: 9px;
|
|
103
|
+
background: transparent;
|
|
104
|
+
color: var(--i18nkit-color-ink, #1a1a1a);
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
font: inherit;
|
|
107
|
+
font-size: 13.5px;
|
|
108
|
+
font-weight: 500;
|
|
109
|
+
text-align: left;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.i18nkit-picker__item:hover {
|
|
113
|
+
background: var(--i18nkit-color-hover, #f4f7fa);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.i18nkit-picker__item:focus-visible {
|
|
117
|
+
outline: none;
|
|
118
|
+
box-shadow: 0 0 0 2px var(--i18nkit-color-primary, #2563eb) inset;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.i18nkit-picker__label {
|
|
122
|
+
flex: 1;
|
|
123
|
+
min-width: 0;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.i18nkit-picker__check {
|
|
128
|
+
color: var(--i18nkit-color-primary, #2563eb);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@keyframes i18nkit-popdown {
|
|
132
|
+
from {
|
|
133
|
+
opacity: 0;
|
|
134
|
+
transform: translateY(-6px) scale(0.985);
|
|
135
|
+
}
|
|
136
|
+
to {
|
|
137
|
+
opacity: 1;
|
|
138
|
+
transform: none;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@media (prefers-reduced-motion: reduce) {
|
|
143
|
+
.i18nkit-picker__panel {
|
|
144
|
+
animation: none;
|
|
145
|
+
}
|
|
146
|
+
.i18nkit-picker__caret,
|
|
147
|
+
.i18nkit-picker__trigger {
|
|
148
|
+
transition: none;
|
|
149
|
+
}
|
|
150
|
+
}
|