-
Notifications
You must be signed in to change notification settings - Fork 79
Expand file tree
/
Copy pathlocales.ts
More file actions
236 lines (221 loc) · 6.31 KB
/
Copy pathlocales.ts
File metadata and controls
236 lines (221 loc) · 6.31 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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* Central locale registry — the single source of truth for every supported language.
*
* To add a new language:
* 1. Add one entry to LOCALES below.
* 2. Add the matching `messages/<code>.json` file (full translation of en.json).
* That is the whole checklist — message files are loaded by path from the code
* below (`src/lib/messages.ts`), and everything else (switcher, html dir/lang,
* hreflang, openGraph, cookie handling) derives from this registry too.
*/
export type Dir = "ltr" | "rtl";
export const LOCALES = [
{
code: "en",
label: "English",
short: "EN",
dir: "ltr",
ogLocale: "en_US",
},
{
code: "ar",
label: "العربية",
short: "عر",
dir: "rtl",
ogLocale: "ar_SA",
},
{
code: "es",
label: "Español",
short: "ES",
dir: "ltr",
ogLocale: "es_ES",
},
{
code: "pt-BR",
label: "Português",
short: "PT",
dir: "ltr",
ogLocale: "pt_BR",
},
{
code: "fr",
label: "Français",
short: "FR",
dir: "ltr",
ogLocale: "fr_FR",
},
{
code: "de",
label: "Deutsch",
short: "DE",
dir: "ltr",
ogLocale: "de_DE",
},
{
code: "ru",
label: "Русский",
short: "RU",
dir: "ltr",
ogLocale: "ru_RU",
},
{
code: "id",
label: "Bahasa Indonesia",
short: "ID",
dir: "ltr",
ogLocale: "id_ID",
},
{
code: "zh-CN",
label: "简体中文",
short: "zh",
dir: "ltr",
ogLocale: "zh_CN",
},
{
code: "tr",
label: "Türkçe",
short: "TR",
dir: "ltr",
ogLocale: "tr_TR",
},
{
code: "hi",
label: "हिन्दी",
short: "HI",
dir: "ltr",
ogLocale: "hi_IN",
},
{
code: "vi",
label: "Tiếng Việt",
short: "VI",
dir: "ltr",
ogLocale: "vi_VN",
},
{
code: "ja",
label: "日本語",
short: "JA",
dir: "ltr",
ogLocale: "ja_JP",
},
{
// Second RTL locale after Arabic. Everything directional already derives
// from `dir`, so this needs no separate handling.
code: "fa",
label: "فارسی",
short: "FA",
dir: "rtl",
ogLocale: "fa_IR",
},
{
code: "ko",
label: "한국어",
short: "KO",
dir: "ltr",
ogLocale: "ko_KR",
},
{
code: "pl",
label: "Polski",
short: "PL",
dir: "ltr",
ogLocale: "pl_PL",
},
{
code: "it",
label: "Italiano",
short: "IT",
dir: "ltr",
ogLocale: "it_IT",
},
] as const;
export type LocaleConfig = (typeof LOCALES)[number];
export type Locale = LocaleConfig["code"];
export const defaultLocale: Locale = "en";
export const localeCodes = LOCALES.map((l) => l.code) as Locale[];
const byCode = new Map<string, LocaleConfig>(LOCALES.map((l) => [l.code, l]));
export function getLocaleConfig(code: Locale): LocaleConfig {
return byCode.get(code) ?? byCode.get(defaultLocale)!;
}
export function isLocale(value: unknown): value is Locale {
return typeof value === "string" && byCode.has(value);
}
export function getDir(code: Locale): Dir {
return getLocaleConfig(code).dir;
}
/**
* Match a raw browser language string (e.g. "pt-BR", "es-419", "ar-EG", "en-GB")
* to a supported locale. Tries an exact match first, then a primary-subtag match.
*/
export function matchLocale(browserLang: string | undefined | null): Locale | null {
if (!browserLang) return null;
const lower = browserLang.toLowerCase();
const exact = localeCodes.find((c) => c.toLowerCase() === lower);
if (exact) return exact;
const prefix = lower.split("-")[0];
const byPrefix = localeCodes.find((c) => c.toLowerCase().split("-")[0] === prefix);
return byPrefix ?? null;
}
export const SITE_URL = "https://browserytools.com";
/**
* Request headers the middleware sets so server components (which cannot see
* the URL directly) know which locale to render. `LOCALE_SOURCE_HEADER` is
* "path" when the locale came from a `/{locale}/…` prefix and "cookie"
* otherwise — a path locale is pinned and must not be overridden by client
* state after hydration.
*/
export const LOCALE_HEADER = "x-browsery-locale";
export const LOCALE_SOURCE_HEADER = "x-browsery-locale-source";
export const LOCALE_COOKIE = "browsery-locale";
/**
* Prefix an app-relative path with a locale segment.
*
* English is the unprefixed default — `/tools/x` stays `/tools/x` — because
* those URLs are already indexed and must never move. Every other locale gets
* `/{code}` in front: `localePath("/tools/x", "es") === "/es/tools/x"`.
*/
export function localePath(path: string, locale: Locale): string {
const clean = path === "" ? "/" : path.startsWith("/") ? path : `/${path}`;
if (locale === defaultLocale) return clean;
return clean === "/" ? `/${locale}` : `/${locale}${clean}`;
}
/**
* Split a pathname into its locale prefix (if any) and the remaining path.
* `/es/tools/x` -> { locale: "es", path: "/tools/x" }
* `/tools/x` -> { locale: "en", path: "/tools/x" }
*/
export function splitLocalePath(pathname: string): { locale: Locale; path: string } {
const segments = pathname.split("/");
const first = segments[1];
if (isLocale(first)) {
const rest = `/${segments.slice(2).join("/")}`;
return { locale: first, path: rest === "/" ? "/" : rest.replace(/\/$/, "") || "/" };
}
return { locale: defaultLocale, path: pathname || "/" };
}
/**
* Build the hreflang `alternates.languages` map for an app-relative path.
*
* Each entry is a genuinely distinct, resolvable URL: English at the
* unprefixed path and every other locale at its `/{code}` prefix. `x-default`
* points at the English URL, which is the one served when no locale is
* negotiated. Pass the *unprefixed* path (e.g. "/tools/json-formatter") — the
* same map is correct for every locale variant of that page, since hreflang
* sets must be identical and reciprocal across the whole cluster.
*/
export function hreflangLanguages(path: string): Record<string, string> {
const canonicalEn = `${SITE_URL}${localePath(path, defaultLocale)}`;
return {
"x-default": canonicalEn,
...Object.fromEntries(
localeCodes.map((c) => [c, `${SITE_URL}${localePath(path, c)}`])
),
};
}
/** OpenGraph alternateLocale list — all locales except the primary one. */
export function ogAlternateLocales(current: Locale = defaultLocale): string[] {
return LOCALES.filter((l) => l.code !== current).map((l) => l.ogLocale);
}