@emdash-cms/admin 0.2.0 → 0.4.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/config-DAVBjz94.d.ts +50 -0
- package/dist/config-DAVBjz94.d.ts.map +1 -0
- package/dist/index.d.ts +20 -33
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7703 -2260
- package/dist/index.js.map +1 -1
- package/dist/locales/ar/messages.mjs +1 -0
- package/dist/locales/de/messages.mjs +1 -1
- package/dist/locales/en/messages.mjs +1 -1
- package/dist/locales/eu/messages.mjs +1 -0
- package/dist/locales/fr/messages.mjs +1 -0
- package/dist/locales/index.d.ts +8 -2
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/index.js +25 -2
- package/dist/locales/index.js.map +1 -0
- package/dist/locales/ja/messages.mjs +1 -0
- package/dist/locales/pseudo/messages.mjs +1 -0
- package/dist/locales/pt-BR/messages.mjs +1 -0
- package/dist/locales/zh-CN/messages.mjs +1 -0
- package/dist/messages-1gc6iQig.js +6 -0
- package/dist/messages-1gc6iQig.js.map +1 -0
- package/dist/messages-B4BeVuVa.js +6 -0
- package/dist/messages-B4BeVuVa.js.map +1 -0
- package/dist/messages-BRsqFuOD.js +6 -0
- package/dist/messages-BRsqFuOD.js.map +1 -0
- package/dist/messages-BZVyg_4s.js +6 -0
- package/dist/messages-BZVyg_4s.js.map +1 -0
- package/dist/messages-BsidfKvO.js +6 -0
- package/dist/messages-BsidfKvO.js.map +1 -0
- package/dist/messages-CGMcVWkD.js +6 -0
- package/dist/messages-CGMcVWkD.js.map +1 -0
- package/dist/messages-D0EpRXuT.js +6 -0
- package/dist/messages-D0EpRXuT.js.map +1 -0
- package/dist/messages-D5m21spG.js +6 -0
- package/dist/messages-D5m21spG.js.map +1 -0
- package/dist/messages-DT2jNITl.js +6 -0
- package/dist/messages-DT2jNITl.js.map +1 -0
- package/dist/styles.css +1 -1
- package/dist/useLocale-CeUy0vje.js +172 -0
- package/dist/useLocale-CeUy0vje.js.map +1 -0
- package/package.json +2 -2
- package/dist/config-BHC21FmY.d.ts +0 -34
- package/dist/config-BHC21FmY.d.ts.map +0 -1
- package/dist/useLocale-CXsoFCFt.js +0 -80
- package/dist/useLocale-CXsoFCFt.js.map +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { loadMessages } from "./locales/index.js";
|
|
2
|
+
import { useLingui } from "@lingui/react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
//#region src/locales/locales.ts
|
|
6
|
+
/**
|
|
7
|
+
* All locales that have (or should have) a PO catalog.
|
|
8
|
+
* First entry is the source/default locale.
|
|
9
|
+
*/
|
|
10
|
+
const LOCALES = [
|
|
11
|
+
{
|
|
12
|
+
code: "en",
|
|
13
|
+
label: "English",
|
|
14
|
+
enabled: true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: "ar",
|
|
18
|
+
label: "العربية",
|
|
19
|
+
enabled: true
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: "eu",
|
|
23
|
+
label: "Euskara",
|
|
24
|
+
enabled: true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: "zh-CN",
|
|
28
|
+
label: "简体中文",
|
|
29
|
+
enabled: true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
code: "fr",
|
|
33
|
+
label: "Français",
|
|
34
|
+
enabled: true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: "de",
|
|
38
|
+
label: "Deutsch",
|
|
39
|
+
enabled: true
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
code: "ja",
|
|
43
|
+
label: "日本語",
|
|
44
|
+
enabled: true
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: "pt-BR",
|
|
48
|
+
label: "Português (Brasil)",
|
|
49
|
+
enabled: true
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: "pseudo",
|
|
53
|
+
label: "Pseudo",
|
|
54
|
+
enabled: false
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
/** The source locale (first entry). */
|
|
58
|
+
const SOURCE_LOCALE = LOCALES[0];
|
|
59
|
+
/** All locale codes (for Lingui extraction / Lunaria tracking). */
|
|
60
|
+
const LOCALE_CODES = LOCALES.map((l) => l.code);
|
|
61
|
+
/** Target locales -- everything except the source (for Lunaria). */
|
|
62
|
+
const TARGET_LOCALES = LOCALES.slice(1);
|
|
63
|
+
/** Locales enabled in the admin UI. */
|
|
64
|
+
const ENABLED_LOCALES = LOCALES.filter((l) => l.enabled);
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/locales/config.ts
|
|
68
|
+
/**
|
|
69
|
+
* Locale configuration for the admin UI runtime.
|
|
70
|
+
*
|
|
71
|
+
* Locale definitions are in `./locales.ts` -- the single source of truth
|
|
72
|
+
* shared by this file, lingui.config.ts and lunaria.config.ts.
|
|
73
|
+
*/
|
|
74
|
+
function isValidLocale(code) {
|
|
75
|
+
try {
|
|
76
|
+
return new Intl.Locale(code).baseName !== "";
|
|
77
|
+
} catch {
|
|
78
|
+
if (import.meta.env.DEV) throw new Error(`Invalid locale code: "${code}"`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The pseudo locale, injected into the supported list only when
|
|
84
|
+
* EMDASH_PSEUDO_LOCALE=1 is set. Never available in production.
|
|
85
|
+
*/
|
|
86
|
+
const PSEUDO_LOCALE = typeof __EMDASH_PSEUDO_LOCALE__ !== "undefined" && __EMDASH_PSEUDO_LOCALE__ ? LOCALES.find((l) => l.code === "pseudo") : void 0;
|
|
87
|
+
/** Available locales at runtime, validated against BCP 47. */
|
|
88
|
+
const SUPPORTED_LOCALES = [...ENABLED_LOCALES.filter((l) => isValidLocale(l.code)), ...PSEUDO_LOCALE ? [PSEUDO_LOCALE] : []];
|
|
89
|
+
const SUPPORTED_LOCALE_CODES = new Set(SUPPORTED_LOCALES.map((l) => l.code));
|
|
90
|
+
const DEFAULT_LOCALE = SOURCE_LOCALE.code;
|
|
91
|
+
/** Maps base language codes to supported locales (e.g. "pt" -> "pt-BR"). */
|
|
92
|
+
const BASE_LANGUAGE_MAP = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const l of SUPPORTED_LOCALES) {
|
|
94
|
+
const base = l.code.split("-")[0].toLowerCase();
|
|
95
|
+
if (!BASE_LANGUAGE_MAP.has(base)) BASE_LANGUAGE_MAP.set(base, l.code);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find the best matching supported locale for a BCP 47 tag.
|
|
99
|
+
* Canonicalizes via Intl.Locale so case differences (e.g. "pt-br" vs "pt-BR")
|
|
100
|
+
* don't prevent matching. Falls back to base language (pt-PT -> pt-BR).
|
|
101
|
+
*/
|
|
102
|
+
function matchLocale(tag) {
|
|
103
|
+
const trimmed = tag.trim();
|
|
104
|
+
if (!trimmed) return void 0;
|
|
105
|
+
let canonical;
|
|
106
|
+
try {
|
|
107
|
+
canonical = new Intl.Locale(trimmed).baseName;
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (SUPPORTED_LOCALE_CODES.has(canonical)) return canonical;
|
|
112
|
+
const base = canonical.split("-")[0].toLowerCase();
|
|
113
|
+
return BASE_LANGUAGE_MAP.get(base);
|
|
114
|
+
}
|
|
115
|
+
const LOCALE_LABELS = new Map(SUPPORTED_LOCALES.map((l) => [l.code, l.label]));
|
|
116
|
+
/** Get a display label for a locale code, falling back to uppercase code. */
|
|
117
|
+
function getLocaleLabel(code) {
|
|
118
|
+
return LOCALE_LABELS.get(code) ?? code.toUpperCase();
|
|
119
|
+
}
|
|
120
|
+
const LOCALE_COOKIE_RE = /(?:^|;\s*)emdash-locale=([^;]+)/;
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the admin locale from a Request.
|
|
123
|
+
* Priority: emdash-locale cookie -> Accept-Language -> DEFAULT_LOCALE.
|
|
124
|
+
*/
|
|
125
|
+
function resolveLocale(request) {
|
|
126
|
+
const cookieLocale = (request.headers.get("cookie") ?? "").match(LOCALE_COOKIE_RE)?.[1]?.trim() ?? "";
|
|
127
|
+
if (SUPPORTED_LOCALE_CODES.has(cookieLocale)) return cookieLocale;
|
|
128
|
+
const acceptLang = request.headers.get("accept-language") ?? "";
|
|
129
|
+
for (const entry of acceptLang.split(",")) {
|
|
130
|
+
const matched = matchLocale(entry.split(";")[0].trim());
|
|
131
|
+
if (matched) return matched;
|
|
132
|
+
}
|
|
133
|
+
return DEFAULT_LOCALE;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/locales/useLocale.ts
|
|
138
|
+
function setCookie(code) {
|
|
139
|
+
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
140
|
+
document.cookie = `emdash-locale=${code}; Path=/_emdash; SameSite=Lax; Max-Age=31536000${secure}`;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get the current locale and a function to switch locales.
|
|
144
|
+
* Loads the new catalog dynamically and sets a cookie for server-side persistence.
|
|
145
|
+
*/
|
|
146
|
+
function useLocale() {
|
|
147
|
+
const { i18n } = useLingui();
|
|
148
|
+
const [locale, setLocaleState] = React.useState(i18n.locale);
|
|
149
|
+
const setLocale = React.useCallback((code) => {
|
|
150
|
+
if (code === i18n.locale || !SUPPORTED_LOCALE_CODES.has(code)) return;
|
|
151
|
+
setCookie(code);
|
|
152
|
+
loadMessages(code).then((messages) => i18n.loadAndActivate({
|
|
153
|
+
locale: code,
|
|
154
|
+
messages
|
|
155
|
+
})).catch(() => {
|
|
156
|
+
setCookie(i18n.locale);
|
|
157
|
+
});
|
|
158
|
+
}, [i18n]);
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
return i18n.on("change", () => {
|
|
161
|
+
setLocaleState(i18n.locale);
|
|
162
|
+
});
|
|
163
|
+
}, [i18n]);
|
|
164
|
+
return {
|
|
165
|
+
locale,
|
|
166
|
+
setLocale
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
export { getLocaleLabel as a, SUPPORTED_LOCALE_CODES as i, DEFAULT_LOCALE as n, resolveLocale as o, SUPPORTED_LOCALES as r, useLocale as t };
|
|
172
|
+
//# sourceMappingURL=useLocale-CeUy0vje.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocale-CeUy0vje.js","names":[],"sources":["../src/locales/locales.ts","../src/locales/config.ts","../src/locales/useLocale.ts"],"sourcesContent":["/**\n * Canonical locale definitions -- the single source of truth.\n *\n * This file is intentionally free of Vite/Astro APIs (`import.meta.env` etc.)\n * so it can be imported from CLI tools (Lingui, Lunaria) running in plain Node.\n *\n * To add a new locale:\n * 1. Add an entry here (with `enabled: false`).\n * 2. Run `pnpm locale:extract` to generate the PO file.\n * 3. Translate the strings in the PO file.\n * 4. Set `enabled: true` once coverage is sufficient.\n *\n * Lingui and Lunaria use all locales (for extraction and tracking).\n * The admin runtime only exposes locales with `enabled: true`.\n */\n\nexport interface LocaleDefinition {\n\t/** BCP 47 locale code (e.g. \"en\", \"pt-BR\"). */\n\tcode: string;\n\t/** Human-readable label in the locale's own language. */\n\tlabel: string;\n\t/** Whether this locale is selectable in the admin UI. */\n\tenabled: boolean;\n}\n\n/**\n * All locales that have (or should have) a PO catalog.\n * First entry is the source/default locale.\n */\nexport const LOCALES: LocaleDefinition[] = [\n\t// Source locale first, then alphabetical by English name.\n\t{ code: \"en\", label: \"English\", enabled: true },\n\t{ code: \"ar\", label: \"العربية\", enabled: true }, // Arabic\n\t{ code: \"eu\", label: \"Euskara\", enabled: true }, // Basque\n\t{ code: \"zh-CN\", label: \"简体中文\", enabled: true }, // Chinese (Simplified)\n\t{ code: \"fr\", label: \"Français\", enabled: true }, // French\n\t{ code: \"de\", label: \"Deutsch\", enabled: true }, // German\n\t{ code: \"ja\", label: \"日本語\", enabled: true }, // Japanese\n\t{ code: \"pt-BR\", label: \"Português (Brasil)\", enabled: true }, // Portuguese (Brazil)\n\t// Pseudo-locale for i18n testing — never enabled in the admin UI by default.\n\t// Set EMDASH_PSEUDO_LOCALE=1 in .env to expose it in the locale switcher (dev only).\n\t{ code: \"pseudo\", label: \"Pseudo\", enabled: false },\n];\n\n/** The source locale (first entry). */\nexport const SOURCE_LOCALE = LOCALES[0]!;\n\n/** All locale codes (for Lingui extraction / Lunaria tracking). */\nexport const LOCALE_CODES = LOCALES.map((l) => l.code);\n\n/** Target locales -- everything except the source (for Lunaria). */\nexport const TARGET_LOCALES = LOCALES.slice(1);\n\n/** Locales enabled in the admin UI. */\nexport const ENABLED_LOCALES = LOCALES.filter((l) => l.enabled);\n","/**\n * Locale configuration for the admin UI runtime.\n *\n * Locale definitions are in `./locales.ts` -- the single source of truth\n * shared by this file, lingui.config.ts and lunaria.config.ts.\n */\n\nimport { ENABLED_LOCALES, LOCALES, SOURCE_LOCALE } from \"./locales.js\";\n\nexport type { LocaleDefinition as SupportedLocale } from \"./locales.js\";\n\nfunction isValidLocale(code: string): boolean {\n\ttry {\n\t\tconst locale = new Intl.Locale(code);\n\t\treturn locale.baseName !== \"\";\n\t} catch {\n\t\tif (import.meta.env.DEV) {\n\t\t\tthrow new Error(`Invalid locale code: \"${code}\"`);\n\t\t}\n\t\treturn false;\n\t}\n}\n\n// Injected by the EmDash Vite integration from process.env.EMDASH_PSEUDO_LOCALE.\n// Only true in dev when EMDASH_PSEUDO_LOCALE=1 is set.\ndeclare const __EMDASH_PSEUDO_LOCALE__: boolean;\n\n/**\n * The pseudo locale, injected into the supported list only when\n * EMDASH_PSEUDO_LOCALE=1 is set. Never available in production.\n */\nconst PSEUDO_LOCALE =\n\ttypeof __EMDASH_PSEUDO_LOCALE__ !== \"undefined\" && __EMDASH_PSEUDO_LOCALE__\n\t\t? LOCALES.find((l) => l.code === \"pseudo\")\n\t\t: undefined;\n\n/** Available locales at runtime, validated against BCP 47. */\nexport const SUPPORTED_LOCALES = [\n\t...ENABLED_LOCALES.filter((l) => isValidLocale(l.code)),\n\t...(PSEUDO_LOCALE ? [PSEUDO_LOCALE] : []),\n];\n\nexport const SUPPORTED_LOCALE_CODES = new Set(SUPPORTED_LOCALES.map((l) => l.code));\n\nexport const DEFAULT_LOCALE = SOURCE_LOCALE.code;\n\n/** Maps base language codes to supported locales (e.g. \"pt\" -> \"pt-BR\"). */\nconst BASE_LANGUAGE_MAP = new Map<string, string>();\nfor (const l of SUPPORTED_LOCALES) {\n\tconst base = l.code.split(\"-\")[0]!.toLowerCase();\n\t// First match wins -- if we have both \"pt\" and \"pt-BR\", exact wins via direct lookup.\n\tif (!BASE_LANGUAGE_MAP.has(base)) {\n\t\tBASE_LANGUAGE_MAP.set(base, l.code);\n\t}\n}\n\n/**\n * Find the best matching supported locale for a BCP 47 tag.\n * Canonicalizes via Intl.Locale so case differences (e.g. \"pt-br\" vs \"pt-BR\")\n * don't prevent matching. Falls back to base language (pt-PT -> pt-BR).\n */\nfunction matchLocale(tag: string): string | undefined {\n\tconst trimmed = tag.trim();\n\tif (!trimmed) return undefined;\n\tlet canonical: string;\n\ttry {\n\t\tcanonical = new Intl.Locale(trimmed).baseName;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (SUPPORTED_LOCALE_CODES.has(canonical)) return canonical;\n\tconst base = canonical.split(\"-\")[0]!.toLowerCase();\n\treturn BASE_LANGUAGE_MAP.get(base);\n}\n\nconst LOCALE_LABELS = new Map(SUPPORTED_LOCALES.map((l) => [l.code, l.label]));\n\n/** Get a display label for a locale code, falling back to uppercase code. */\nexport function getLocaleLabel(code: string): string {\n\treturn LOCALE_LABELS.get(code) ?? code.toUpperCase();\n}\n\nconst LOCALE_COOKIE_RE = /(?:^|;\\s*)emdash-locale=([^;]+)/;\n\n/**\n * Resolve the admin locale from a Request.\n * Priority: emdash-locale cookie -> Accept-Language -> DEFAULT_LOCALE.\n */\nexport function resolveLocale(request: Request): string {\n\tconst cookieHeader = request.headers.get(\"cookie\") ?? \"\";\n\tconst cookieMatch = cookieHeader.match(LOCALE_COOKIE_RE);\n\tconst cookieLocale = cookieMatch?.[1]?.trim() ?? \"\";\n\n\tif (SUPPORTED_LOCALE_CODES.has(cookieLocale)) return cookieLocale;\n\n\tconst acceptLang = request.headers.get(\"accept-language\") ?? \"\";\n\tfor (const entry of acceptLang.split(\",\")) {\n\t\tconst tag = entry.split(\";\")[0]!.trim();\n\t\tconst matched = matchLocale(tag);\n\t\tif (matched) return matched;\n\t}\n\n\treturn DEFAULT_LOCALE;\n}\n","import { useLingui } from \"@lingui/react\";\nimport * as React from \"react\";\n\nimport { SUPPORTED_LOCALE_CODES } from \"./config.js\";\nimport { loadMessages } from \"./index.js\";\n\nfunction setCookie(code: string) {\n\tconst secure = window.location.protocol === \"https:\" ? \"; Secure\" : \"\";\n\tdocument.cookie = `emdash-locale=${code}; Path=/_emdash; SameSite=Lax; Max-Age=31536000${secure}`;\n}\n\n/**\n * Get the current locale and a function to switch locales.\n * Loads the new catalog dynamically and sets a cookie for server-side persistence.\n */\nexport function useLocale() {\n\tconst { i18n } = useLingui();\n\tconst [locale, setLocaleState] = React.useState(i18n.locale);\n\n\tconst setLocale = React.useCallback(\n\t\t(code: string) => {\n\t\t\tif (code === i18n.locale || !SUPPORTED_LOCALE_CODES.has(code)) return;\n\t\t\tsetCookie(code);\n\t\t\tvoid loadMessages(code)\n\t\t\t\t.then((messages) => i18n.loadAndActivate({ locale: code, messages }))\n\t\t\t\t.catch(() => {\n\t\t\t\t\tsetCookie(i18n.locale);\n\t\t\t\t});\n\t\t},\n\t\t[i18n],\n\t);\n\n\t// Subscribe to i18n change events to trigger re-renders\n\tReact.useEffect(() => {\n\t\tconst unsubscribe = i18n.on(\"change\", () => {\n\t\t\tsetLocaleState(i18n.locale);\n\t\t});\n\t\treturn unsubscribe;\n\t}, [i18n]);\n\n\treturn { locale, setLocale };\n}\n"],"mappings":";;;;;;;;;AA6BA,MAAa,UAA8B;CAE1C;EAAE,MAAM;EAAM,OAAO;EAAW,SAAS;EAAM;CAC/C;EAAE,MAAM;EAAM,OAAO;EAAW,SAAS;EAAM;CAC/C;EAAE,MAAM;EAAM,OAAO;EAAW,SAAS;EAAM;CAC/C;EAAE,MAAM;EAAS,OAAO;EAAQ,SAAS;EAAM;CAC/C;EAAE,MAAM;EAAM,OAAO;EAAY,SAAS;EAAM;CAChD;EAAE,MAAM;EAAM,OAAO;EAAW,SAAS;EAAM;CAC/C;EAAE,MAAM;EAAM,OAAO;EAAO,SAAS;EAAM;CAC3C;EAAE,MAAM;EAAS,OAAO;EAAsB,SAAS;EAAM;CAG7D;EAAE,MAAM;EAAU,OAAO;EAAU,SAAS;EAAO;CACnD;;AAGD,MAAa,gBAAgB,QAAQ;;AAGrC,MAAa,eAAe,QAAQ,KAAK,MAAM,EAAE,KAAK;;AAGtD,MAAa,iBAAiB,QAAQ,MAAM,EAAE;;AAG9C,MAAa,kBAAkB,QAAQ,QAAQ,MAAM,EAAE,QAAQ;;;;;;;;;;AC3C/D,SAAS,cAAc,MAAuB;AAC7C,KAAI;AAEH,SADe,IAAI,KAAK,OAAO,KAAK,CACtB,aAAa;SACpB;AACP,MAAI,OAAO,KAAK,IAAI,IACnB,OAAM,IAAI,MAAM,yBAAyB,KAAK,GAAG;AAElD,SAAO;;;;;;;AAYT,MAAM,gBACL,OAAO,6BAA6B,eAAe,2BAChD,QAAQ,MAAM,MAAM,EAAE,SAAS,SAAS,GACxC;;AAGJ,MAAa,oBAAoB,CAChC,GAAG,gBAAgB,QAAQ,MAAM,cAAc,EAAE,KAAK,CAAC,EACvD,GAAI,gBAAgB,CAAC,cAAc,GAAG,EAAE,CACxC;AAED,MAAa,yBAAyB,IAAI,IAAI,kBAAkB,KAAK,MAAM,EAAE,KAAK,CAAC;AAEnF,MAAa,iBAAiB,cAAc;;AAG5C,MAAM,oCAAoB,IAAI,KAAqB;AACnD,KAAK,MAAM,KAAK,mBAAmB;CAClC,MAAM,OAAO,EAAE,KAAK,MAAM,IAAI,CAAC,GAAI,aAAa;AAEhD,KAAI,CAAC,kBAAkB,IAAI,KAAK,CAC/B,mBAAkB,IAAI,MAAM,EAAE,KAAK;;;;;;;AASrC,SAAS,YAAY,KAAiC;CACrD,MAAM,UAAU,IAAI,MAAM;AAC1B,KAAI,CAAC,QAAS,QAAO;CACrB,IAAI;AACJ,KAAI;AACH,cAAY,IAAI,KAAK,OAAO,QAAQ,CAAC;SAC9B;AACP;;AAED,KAAI,uBAAuB,IAAI,UAAU,CAAE,QAAO;CAClD,MAAM,OAAO,UAAU,MAAM,IAAI,CAAC,GAAI,aAAa;AACnD,QAAO,kBAAkB,IAAI,KAAK;;AAGnC,MAAM,gBAAgB,IAAI,IAAI,kBAAkB,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;;AAG9E,SAAgB,eAAe,MAAsB;AACpD,QAAO,cAAc,IAAI,KAAK,IAAI,KAAK,aAAa;;AAGrD,MAAM,mBAAmB;;;;;AAMzB,SAAgB,cAAc,SAA0B;CAGvD,MAAM,gBAFe,QAAQ,QAAQ,IAAI,SAAS,IAAI,IACrB,MAAM,iBAAiB,GACrB,IAAI,MAAM,IAAI;AAEjD,KAAI,uBAAuB,IAAI,aAAa,CAAE,QAAO;CAErD,MAAM,aAAa,QAAQ,QAAQ,IAAI,kBAAkB,IAAI;AAC7D,MAAK,MAAM,SAAS,WAAW,MAAM,IAAI,EAAE;EAE1C,MAAM,UAAU,YADJ,MAAM,MAAM,IAAI,CAAC,GAAI,MAAM,CACP;AAChC,MAAI,QAAS,QAAO;;AAGrB,QAAO;;;;;AClGR,SAAS,UAAA,MAAuB;;AAEhC,UAAS,SAAU,iBAAc,KAAA,iDAAA;;;;;;AAOjC,SAAgB,YAAS;CACvB,MAAA,EACF,SACM,WAAW;CAChB,MAAO,CAAA,QAAQ,kBAAkB,MAAM,SAAS,KAAK,OAAO;;AAE5D,MAAM,SAAU,KAAE,UAAM,CAAA,uBAAW,IAAA,KAAA,CAAA;AACjC,YAAM,KAAU;AAChB,EAAI,aAAc,KAAM,CAAC,MAAI,aAAA,KAAA,gBAAkC;GAC/D,QAAU;GACV;GACE,CAAA,CAAA,CAAA,YAAe;AACf,aAAW,KAAC,OAAA;IACZ;IACA,CAAC,KAAA,CAAA;AAGJ,OAAA,gBAAA;AAIA;AAFE,kBAAiB,KAAC,OAAO;IACvB;IAEH,CAAA,KAAA,CAAA;AACD,QAAE;EACF;EACE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emdash-cms/admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Admin UI for EmDash CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"react-dom": "19.2.4",
|
|
54
54
|
"react-hotkeys-hook": "^5.2.4",
|
|
55
55
|
"tailwind-merge": "^3.3.0",
|
|
56
|
-
"@emdash-cms/blocks": "0.
|
|
56
|
+
"@emdash-cms/blocks": "0.4.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
//#region src/locales/useLocale.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Get the current locale and a function to switch locales.
|
|
4
|
-
* Loads the new catalog dynamically and sets a cookie for server-side persistence.
|
|
5
|
-
*/
|
|
6
|
-
declare function useLocale(): {
|
|
7
|
-
locale: string;
|
|
8
|
-
setLocale: (code: string) => void;
|
|
9
|
-
};
|
|
10
|
-
//#endregion
|
|
11
|
-
//#region src/locales/config.d.ts
|
|
12
|
-
/**
|
|
13
|
-
* Locale configuration — single source of truth for supported locales.
|
|
14
|
-
*
|
|
15
|
-
* Imported by both the Lingui provider (client) and admin.astro (server).
|
|
16
|
-
*/
|
|
17
|
-
interface SupportedLocale {
|
|
18
|
-
code: string;
|
|
19
|
-
label: string;
|
|
20
|
-
}
|
|
21
|
-
/** Available locales — extend this list as translations are added. */
|
|
22
|
-
declare const SUPPORTED_LOCALES: SupportedLocale[];
|
|
23
|
-
declare const SUPPORTED_LOCALE_CODES: Set<string>;
|
|
24
|
-
declare const DEFAULT_LOCALE: string;
|
|
25
|
-
/** Get a display label for a locale code, falling back to uppercase code. */
|
|
26
|
-
declare function getLocaleLabel(code: string): string;
|
|
27
|
-
/**
|
|
28
|
-
* Resolve the admin locale from a Request.
|
|
29
|
-
* Priority: emdash-locale cookie → Accept-Language → DEFAULT_LOCALE.
|
|
30
|
-
*/
|
|
31
|
-
declare function resolveLocale(request: Request): string;
|
|
32
|
-
//#endregion
|
|
33
|
-
export { getLocaleLabel as a, SupportedLocale as i, SUPPORTED_LOCALES as n, resolveLocale as o, SUPPORTED_LOCALE_CODES as r, useLocale as s, DEFAULT_LOCALE as t };
|
|
34
|
-
//# sourceMappingURL=config-BHC21FmY.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"config-BHC21FmY.d.ts","names":[],"sources":["../src/locales/useLocale.ts","../src/locales/config.ts"],"mappings":";;AAcA;;;iBAAgB,SAAA,CAAA;;;;;;;AAAhB;;;;UCRiB,eAAA;EAChB,IAAA;EACA,KAAA;AAAA;;cAeY,iBAAA,EAAmB,eAAA;AAAA,cAMnB,sBAAA,EAAsB,GAAA;AAAA,cAEtB,cAAA;AAzBb;AAAA,iBA8BgB,cAAA,CAAe,IAAA;;;;AAb/B;iBAuBgB,aAAA,CAAc,OAAA,EAAS,OAAA"}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { useLingui } from "@lingui/react";
|
|
2
|
-
import * as React from "react";
|
|
3
|
-
|
|
4
|
-
//#region src/locales/config.ts
|
|
5
|
-
/** Validate a locale code against the Intl.Locale API (BCP 47). */
|
|
6
|
-
function validateLocaleCode(code) {
|
|
7
|
-
try {
|
|
8
|
-
return new Intl.Locale(code).baseName;
|
|
9
|
-
} catch {
|
|
10
|
-
if (import.meta.env.DEV) throw new Error(`Invalid locale code: "${code}"`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
/** Available locales — extend this list as translations are added. */
|
|
14
|
-
const SUPPORTED_LOCALES = [{
|
|
15
|
-
code: "en",
|
|
16
|
-
label: "English"
|
|
17
|
-
}, {
|
|
18
|
-
code: "de",
|
|
19
|
-
label: "Deutsch"
|
|
20
|
-
}].filter((l) => validateLocaleCode(l.code));
|
|
21
|
-
const SUPPORTED_LOCALE_CODES = new Set(SUPPORTED_LOCALES.map((l) => l.code));
|
|
22
|
-
const DEFAULT_LOCALE = SUPPORTED_LOCALES[0].code;
|
|
23
|
-
const LOCALE_LABELS = new Map(SUPPORTED_LOCALES.map((l) => [l.code, l.label]));
|
|
24
|
-
/** Get a display label for a locale code, falling back to uppercase code. */
|
|
25
|
-
function getLocaleLabel(code) {
|
|
26
|
-
return LOCALE_LABELS.get(code) ?? code.toUpperCase();
|
|
27
|
-
}
|
|
28
|
-
const LOCALE_COOKIE_RE = /(?:^|;\s*)emdash-locale=([^;]+)/;
|
|
29
|
-
/**
|
|
30
|
-
* Resolve the admin locale from a Request.
|
|
31
|
-
* Priority: emdash-locale cookie → Accept-Language → DEFAULT_LOCALE.
|
|
32
|
-
*/
|
|
33
|
-
function resolveLocale(request) {
|
|
34
|
-
const cookieLocale = (request.headers.get("cookie") ?? "").match(LOCALE_COOKIE_RE)?.[1]?.trim() ?? "";
|
|
35
|
-
if (SUPPORTED_LOCALE_CODES.has(cookieLocale)) return cookieLocale;
|
|
36
|
-
const acceptLang = request.headers.get("accept-language") ?? "";
|
|
37
|
-
for (const entry of acceptLang.split(",")) {
|
|
38
|
-
const tag = entry.split(";")[0].trim().split("-")[0].toLowerCase();
|
|
39
|
-
if (SUPPORTED_LOCALE_CODES.has(tag)) return tag;
|
|
40
|
-
}
|
|
41
|
-
return DEFAULT_LOCALE;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/locales/useLocale.ts
|
|
46
|
-
function setCookie(code) {
|
|
47
|
-
const secure = window.location.protocol === "https:" ? "; Secure" : "";
|
|
48
|
-
document.cookie = `emdash-locale=${code}; Path=/_emdash; SameSite=Lax; Max-Age=31536000${secure}`;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Get the current locale and a function to switch locales.
|
|
52
|
-
* Loads the new catalog dynamically and sets a cookie for server-side persistence.
|
|
53
|
-
*/
|
|
54
|
-
function useLocale() {
|
|
55
|
-
const { i18n } = useLingui();
|
|
56
|
-
const [locale, setLocaleState] = React.useState(i18n.locale);
|
|
57
|
-
const setLocale = React.useCallback((code) => {
|
|
58
|
-
if (code === i18n.locale || !SUPPORTED_LOCALE_CODES.has(code)) return;
|
|
59
|
-
setCookie(code);
|
|
60
|
-
import(`./${code}/messages.mjs`).then(({ messages }) => i18n.loadAndActivate({
|
|
61
|
-
locale: code,
|
|
62
|
-
messages
|
|
63
|
-
})).catch(() => {
|
|
64
|
-
setCookie(i18n.locale);
|
|
65
|
-
});
|
|
66
|
-
}, [i18n]);
|
|
67
|
-
React.useEffect(() => {
|
|
68
|
-
return i18n.on("change", () => {
|
|
69
|
-
setLocaleState(i18n.locale);
|
|
70
|
-
});
|
|
71
|
-
}, [i18n]);
|
|
72
|
-
return {
|
|
73
|
-
locale,
|
|
74
|
-
setLocale
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
//#endregion
|
|
79
|
-
export { getLocaleLabel as a, SUPPORTED_LOCALE_CODES as i, DEFAULT_LOCALE as n, resolveLocale as o, SUPPORTED_LOCALES as r, useLocale as t };
|
|
80
|
-
//# sourceMappingURL=useLocale-CXsoFCFt.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useLocale-CXsoFCFt.js","names":[],"sources":["../src/locales/config.ts","../src/locales/useLocale.ts"],"sourcesContent":["/**\n * Locale configuration — single source of truth for supported locales.\n *\n * Imported by both the Lingui provider (client) and admin.astro (server).\n */\n\nexport interface SupportedLocale {\n\tcode: string;\n\tlabel: string;\n}\n\n/** Validate a locale code against the Intl.Locale API (BCP 47). */\nfunction validateLocaleCode(code: string): string | void {\n\ttry {\n\t\treturn new Intl.Locale(code).baseName;\n\t} catch {\n\t\tif (import.meta.env.DEV) {\n\t\t\tthrow new Error(`Invalid locale code: \"${code}\"`);\n\t\t}\n\t}\n}\n\n/** Available locales — extend this list as translations are added. */\nexport const SUPPORTED_LOCALES: SupportedLocale[] = [\n\t/* First item is the default locale */\n\t{ code: \"en\", label: \"English\" },\n\t{ code: \"de\", label: \"Deutsch\" },\n].filter((l) => validateLocaleCode(l.code));\n\nexport const SUPPORTED_LOCALE_CODES = new Set(SUPPORTED_LOCALES.map((l) => l.code));\n\nexport const DEFAULT_LOCALE = SUPPORTED_LOCALES[0]!.code;\n\nconst LOCALE_LABELS = new Map(SUPPORTED_LOCALES.map((l) => [l.code, l.label]));\n\n/** Get a display label for a locale code, falling back to uppercase code. */\nexport function getLocaleLabel(code: string): string {\n\treturn LOCALE_LABELS.get(code) ?? code.toUpperCase();\n}\n\nconst LOCALE_COOKIE_RE = /(?:^|;\\s*)emdash-locale=([^;]+)/;\n\n/**\n * Resolve the admin locale from a Request.\n * Priority: emdash-locale cookie → Accept-Language → DEFAULT_LOCALE.\n */\nexport function resolveLocale(request: Request): string {\n\tconst cookieHeader = request.headers.get(\"cookie\") ?? \"\";\n\tconst cookieMatch = cookieHeader.match(LOCALE_COOKIE_RE);\n\tconst cookieLocale = cookieMatch?.[1]?.trim() ?? \"\";\n\n\tif (SUPPORTED_LOCALE_CODES.has(cookieLocale)) return cookieLocale;\n\n\tconst acceptLang = request.headers.get(\"accept-language\") ?? \"\";\n\tfor (const entry of acceptLang.split(\",\")) {\n\t\tconst tag = entry.split(\";\")[0]!.trim().split(\"-\")[0]!.toLowerCase();\n\t\tif (SUPPORTED_LOCALE_CODES.has(tag)) return tag;\n\t}\n\n\treturn DEFAULT_LOCALE;\n}\n","import { useLingui } from \"@lingui/react\";\nimport * as React from \"react\";\n\nimport { SUPPORTED_LOCALE_CODES } from \"./config.js\";\n\nfunction setCookie(code: string) {\n\tconst secure = window.location.protocol === \"https:\" ? \"; Secure\" : \"\";\n\tdocument.cookie = `emdash-locale=${code}; Path=/_emdash; SameSite=Lax; Max-Age=31536000${secure}`;\n}\n\n/**\n * Get the current locale and a function to switch locales.\n * Loads the new catalog dynamically and sets a cookie for server-side persistence.\n */\nexport function useLocale() {\n\tconst { i18n } = useLingui();\n\tconst [locale, setLocaleState] = React.useState(i18n.locale);\n\n\tconst setLocale = React.useCallback(\n\t\t(code: string) => {\n\t\t\tif (code === i18n.locale || !SUPPORTED_LOCALE_CODES.has(code)) return;\n\t\t\tsetCookie(code);\n\t\t\tvoid import(`./${code}/messages.mjs`)\n\t\t\t\t.then(({ messages }) => i18n.loadAndActivate({ locale: code, messages }))\n\t\t\t\t.catch(() => {\n\t\t\t\t\t// Revert cookie on failure so the user isn't stuck\n\t\t\t\t\tsetCookie(i18n.locale);\n\t\t\t\t});\n\t\t},\n\t\t[i18n],\n\t);\n\n\t// Subscribe to i18n change events to trigger re-renders\n\tReact.useEffect(() => {\n\t\tconst unsubscribe = i18n.on(\"change\", () => {\n\t\t\tsetLocaleState(i18n.locale);\n\t\t});\n\t\treturn unsubscribe;\n\t}, [i18n]);\n\n\treturn { locale, setLocale };\n}\n"],"mappings":";;;;;AAYA,SAAS,mBAAmB,MAA6B;AACxD,KAAI;AACH,SAAO,IAAI,KAAK,OAAO,KAAK,CAAC;SACtB;AACP,MAAI,OAAO,KAAK,IAAI,IACnB,OAAM,IAAI,MAAM,yBAAyB,KAAK,GAAG;;;;AAMpD,MAAa,oBAAuC,CAEnD;CAAE,MAAM;CAAM,OAAO;CAAW,EAChC;CAAE,MAAM;CAAM,OAAO;CAAW,CAChC,CAAC,QAAQ,MAAM,mBAAmB,EAAE,KAAK,CAAC;AAE3C,MAAa,yBAAyB,IAAI,IAAI,kBAAkB,KAAK,MAAM,EAAE,KAAK,CAAC;AAEnF,MAAa,iBAAiB,kBAAkB,GAAI;AAEpD,MAAM,gBAAgB,IAAI,IAAI,kBAAkB,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;;AAG9E,SAAgB,eAAe,MAAsB;AACpD,QAAO,cAAc,IAAI,KAAK,IAAI,KAAK,aAAa;;AAGrD,MAAM,mBAAmB;;;;;AAMzB,SAAgB,cAAc,SAA0B;CAGvD,MAAM,gBAFe,QAAQ,QAAQ,IAAI,SAAS,IAAI,IACrB,MAAM,iBAAiB,GACrB,IAAI,MAAM,IAAI;AAEjD,KAAI,uBAAuB,IAAI,aAAa,CAAE,QAAO;CAErD,MAAM,aAAa,QAAQ,QAAQ,IAAI,kBAAkB,IAAI;AAC7D,MAAK,MAAM,SAAS,WAAW,MAAM,IAAI,EAAE;EAC1C,MAAM,MAAM,MAAM,MAAM,IAAI,CAAC,GAAI,MAAM,CAAC,MAAM,IAAI,CAAC,GAAI,aAAa;AACpE,MAAI,uBAAuB,IAAI,IAAI,CAAE,QAAO;;AAG7C,QAAO;;;;;ACxDR,SAAS,UAAA,MAAwB;;AAEjC,UAAS,SAAU,iBAAc,KAAA,iDAAA;;;;;;AAOjC,SAAgB,YAAS;CACvB,MAAA,EACF,SACM,WAAW;CAChB,MAAO,CAAA,QAAQ,kBAAkB,MAAM,SAAS,KAAK,OAAO;;AAE5D,MAAM,SAAU,KAAE,UAAM,CAAA,uBAAW,IAAA,KAAA,CAAA;AACjC,YAAM,KAAU;AAChB,EAAI,OAAS,KAAK,KAAA,gBAAW,MAAA,EAC7B,eACK,KAAO,gBAAW;GACrB,QAAQ;GACR;GACA,CAAC,CAAC,CAAC,YAAO;AAET,aAAA,KAAA,OAAA;IACH;IACA,CAAA,KAAK,CAAA;AAGN,OAAE,gBAAkB;AAIlB,SAHmB,KAAA,GAAA,gBAAA;AACrB,kBAAmB,KAAK,OAAK;IAC5B;IAED,CAAA,KAAO,CAAA;AACP,QAAO;;EAER;EACD"}
|