-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathLocaleSwitcher.tsx
More file actions
147 lines (138 loc) · 3.74 KB
/
Copy pathLocaleSwitcher.tsx
File metadata and controls
147 lines (138 loc) · 3.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* Locale switcher component for i18n-enabled sites.
*
* Used in both the content list (to filter by locale) and the content editor
* (to switch between locale versions of a content item).
*
* Only renders when i18n is configured (manifest.i18n is present).
*/
import { Select } from "@cloudflare/kumo";
import { useLingui } from "@lingui/react/macro";
import { GlobeSimple } from "@phosphor-icons/react";
import React from "react";
import { cn } from "../lib/utils.js";
interface LocaleSwitcherProps {
locales: string[];
defaultLocale: string;
value: string;
onChange: (locale: string) => void;
/** Show "All locales" option (for list filtering) */
showAll?: boolean;
className?: string;
/** Size variant */
size?: "sm" | "md";
}
/**
* Get a display label for a locale code.
* Uses Intl.DisplayNames when available, falls back to uppercase code.
*/
function getLocaleLabel(code: string): string {
try {
const names = new Intl.DisplayNames(["en"], { type: "language" });
return names.of(code) ?? code.toUpperCase();
} catch {
return code.toUpperCase();
}
}
export function LocaleSwitcher({
locales,
defaultLocale,
value,
onChange,
showAll = false,
className,
size = "md",
}: LocaleSwitcherProps) {
const { t } = useLingui();
const formatLocaleLabel = (locale: string) => {
const code = locale.toUpperCase();
return locale === defaultLocale ? (
<>
{code}
{t` (default)`}
</>
) : (
code
);
};
return (
<div className={cn("flex items-center gap-1.5", className)}>
<GlobeSimple
className={cn("text-kumo-subtle shrink-0", size === "sm" ? "size-3.5" : "size-4")}
weight="bold"
/>
<Select<string>
value={value}
onValueChange={(nextValue) => {
if (typeof nextValue === "string" && nextValue !== value) onChange(nextValue);
}}
placeholder={showAll ? t`All locales` : undefined}
renderValue={formatLocaleLabel}
aria-label={t`Locale`}
size={size === "sm" ? "sm" : "base"}
>
{showAll && <Select.Option value="">{t`All locales`}</Select.Option>}
{locales.map((locale) => (
<Select.Option key={locale} value={locale}>
{formatLocaleLabel(locale)}
</Select.Option>
))}
</Select>
</div>
);
}
/**
* Compact locale badges showing which translations exist for a content item.
* Renders as a row of small locale codes, with existing translations highlighted.
*/
export function LocaleBadges({
locales,
existingLocales,
onLocaleClick,
}: {
locales: string[];
existingLocales: string[];
onLocaleClick?: (locale: string) => void;
}) {
const { t } = useLingui();
const existingSet = new Set(existingLocales);
return (
<div className="flex items-center gap-0.5">
{locales.map((locale) => {
const exists = existingSet.has(locale);
const label = getLocaleLabel(locale);
return (
<button
key={locale}
type="button"
onClick={() => onLocaleClick?.(locale)}
disabled={!onLocaleClick}
title={exists ? t`${label} \u2014 view translation` : t`${label} \u2014 no translation`}
className={cn(
"rounded px-1 py-0.5 text-[10px] font-semibold uppercase leading-none transition-colors",
exists
? "bg-kumo-brand/10 text-kumo-link hover:bg-kumo-brand/20"
: "bg-kumo-tint text-kumo-subtle/50",
onLocaleClick && exists && "cursor-pointer",
(!onLocaleClick || !exists) && "cursor-default",
)}
>
{locale}
</button>
);
})}
</div>
);
}
/**
* Hook to get i18n config from the manifest query.
* Returns null if i18n is not configured.
*/
export function useI18nConfig(
manifest: { i18n?: { defaultLocale: string; locales: string[] } } | undefined,
) {
return React.useMemo(() => {
if (!manifest?.i18n) return null;
return manifest.i18n;
}, [manifest?.i18n]);
}