@naeil/ui 0.9.0 → 0.11.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -5
  3. package/dist/{chunk-XRO3NJAB.js → chunk-KWEWR46R.js} +40 -154
  4. package/dist/{chunk-JDHGKM25.js → chunk-U3IZCFM4.js} +267 -2
  5. package/dist/components/index.js +12 -3
  6. package/dist/components/naeil-footer.d.ts +3 -3
  7. package/dist/components/naeil-footer.js +14 -11
  8. package/dist/components/naeil-header.d.ts +7 -9
  9. package/dist/components/naeil-header.js +2 -2
  10. package/dist/lib/ui-release.d.ts +1 -1
  11. package/dist/lib/ui-release.js +1 -1
  12. package/dist/naeil-locale/checker.d.ts +16 -0
  13. package/dist/naeil-locale/checker.js +91 -0
  14. package/dist/naeil-locale/chrome.d.ts +150 -0
  15. package/dist/naeil-locale/chrome.js +2385 -0
  16. package/dist/naeil-locale/chunk-4U64K4J3.js +56 -0
  17. package/dist/naeil-locale/chunk-N3ETBM74.js +24 -0
  18. package/dist/naeil-locale/chunk-U65OQHZY.js +167 -0
  19. package/dist/naeil-locale/chunk-YQ5OJWBF.js +53 -0
  20. package/dist/naeil-locale/client.d.ts +70 -0
  21. package/dist/naeil-locale/client.js +388 -0
  22. package/dist/naeil-locale/en-ZC66VDHP.js +86 -0
  23. package/dist/naeil-locale/index.d.ts +40 -0
  24. package/dist/naeil-locale/index.js +111 -0
  25. package/dist/naeil-locale/ja-DKIEQUG7.js +86 -0
  26. package/dist/naeil-locale/ko-CY6663RZ.js +86 -0
  27. package/dist/naeil-locale/messages-BuQvB-u2.d.ts +167 -0
  28. package/dist/naeil-locale/messages.d.ts +1 -0
  29. package/dist/naeil-locale/messages.js +14 -0
  30. package/dist/naeil-locale/server.d.ts +75 -0
  31. package/dist/naeil-locale/server.js +172 -0
  32. package/dist/source/components/naeil-footer-core.d.ts +16 -0
  33. package/dist/source/components/naeil-footer-core.js +93 -0
  34. package/dist/source/components/naeil-footer.d.ts +6 -11
  35. package/dist/source/components/naeil-footer.js +4 -75
  36. package/dist/source/components/naeil-header-account-core.d.ts +26 -0
  37. package/dist/source/components/naeil-header-account-core.js +98 -0
  38. package/dist/source/components/naeil-header-account.d.ts +6 -22
  39. package/dist/source/components/naeil-header-account.js +5 -81
  40. package/dist/source/components/naeil-header-content.d.ts +102 -109
  41. package/dist/source/components/naeil-header-content.js +6 -142
  42. package/dist/source/components/naeil-header-core.d.ts +59 -0
  43. package/dist/source/components/naeil-header-core.js +231 -0
  44. package/dist/source/components/naeil-header-data.d.ts +11 -0
  45. package/dist/source/components/naeil-header-data.js +14 -0
  46. package/dist/source/components/naeil-header.d.ts +10 -51
  47. package/dist/source/components/naeil-header.js +9 -200
  48. package/dist/source/components/naeil-messages-context.d.ts +94 -0
  49. package/dist/source/components/naeil-messages-context.js +16 -0
  50. package/dist/source/components/naeil-standalone-messages.d.ts +11 -0
  51. package/dist/source/components/naeil-standalone-messages.js +13 -0
  52. package/dist/source/i18n/routing.d.ts +22 -20
  53. package/dist/source/lib/ui-release.d.ts +1 -1
  54. package/dist/source/lib/ui-release.js +1 -1
  55. package/dist/source/naeil-locale/messages/en.json +78 -0
  56. package/dist/source/naeil-locale/messages/ja.json +78 -0
  57. package/dist/source/naeil-locale/messages/ko.json +78 -0
  58. package/docs/components/naeil-locale.md +52 -0
  59. package/docs/package-release.md +31 -6
  60. package/package.json +35 -7
  61. /package/dist/{naeil-header-content-BvISJ4RV.d.ts → naeil-header-data-BvISJ4RV.d.ts} +0 -0
@@ -0,0 +1,388 @@
1
+ "use client";
2
+ "use client";
3
+ import {
4
+ NaeilMessagesProvider,
5
+ __spreadProps,
6
+ __spreadValues
7
+ } from "./chunk-YQ5OJWBF.js";
8
+
9
+ // src/naeil-locale/client.tsx
10
+ import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
11
+ import { usePathname, useRouter } from "next/navigation";
12
+ import { NextIntlClientProvider } from "next-intl";
13
+
14
+ // src/naeil-locale/routing.ts
15
+ var naeilLocales = ["en", "ko", "ja"];
16
+ function isNaeilLocale(value) {
17
+ return typeof value === "string" && naeilLocales.includes(value);
18
+ }
19
+ function assertLocale(value) {
20
+ if (!isNaeilLocale(value)) throw new Error("Unsupported naeil locale");
21
+ return value;
22
+ }
23
+ var origins = {
24
+ abstorm: "https://abstorm.naeil.dev",
25
+ main: "https://naeil.dev",
26
+ jev: "https://jev.naeil.dev",
27
+ estimate: "https://abestimate.naeil.dev"
28
+ };
29
+ var protocolPrefixes = ["/api", "/auth", "/_next", "/_vercel", "/.well-known", "/health", "/invite", "/return", "/webhook", "/images", "/fonts"];
30
+ var protocolFiles = ["/favicon.ico", "/icon.svg", "/apple-icon.png", "/robots.txt", "/sitemap.xml"];
31
+ function defineLocaleService(service) {
32
+ var _a, _b, _c, _d;
33
+ if (origins[service.id] !== service.origin) throw new Error("Service origin must match the exact owned origin");
34
+ const paths = service.routes.map((route) => typeof route === "string" ? route : route.path);
35
+ if (!paths.length || new Set(paths).size !== paths.length) throw new Error("Route inventory is empty or duplicated");
36
+ for (const path of paths) {
37
+ if (!/^\/(?:[A-Za-z0-9._~-]+|\[[A-Za-z][A-Za-z0-9_]*\])(?:\/(?:[A-Za-z0-9._~-]+|\[[A-Za-z][A-Za-z0-9_]*\]))*$/.test(path) && path !== "/") throw new Error(`Invalid registered route: ${path}`);
38
+ if (/^\/(?:en|ko|ja)(?:\/|$)/.test(path) || isProtocolPath(path, service) && !((_a = service.uiProtocolExceptions) == null ? void 0 : _a.includes(path)) || path.split("/").some((segment) => segment === "." || segment === "..")) throw new Error(`Locale/protocol path cannot be a UI route: ${path}`);
39
+ }
40
+ for (const exception of (_b = service.uiProtocolExceptions) != null ? _b : []) if (exception.includes("[") || !paths.includes(exception)) throw new Error("Protocol UI exceptions must be exact registered routes");
41
+ return Object.freeze(__spreadProps(__spreadValues({}, service), { routes: Object.freeze(service.routes.map((route) => typeof route === "string" ? route : Object.freeze(__spreadValues({}, route)))), protocolPrefixes: Object.freeze([...(_c = service.protocolPrefixes) != null ? _c : []]), uiProtocolExceptions: Object.freeze([...(_d = service.uiProtocolExceptions) != null ? _d : []]) }));
42
+ }
43
+ var main = defineLocaleService({ id: "main", origin: origins.main, routes: ["/", "/projects", "/design", "/blog", "/blog/[slug]", "/login", { path: "/login/change-password", protected: true }] });
44
+ var naeilServices = {
45
+ abstorm: defineLocaleService({
46
+ id: "abstorm",
47
+ origin: origins.abstorm,
48
+ routes: ["/", "/new", "/runs/[id]", "/runs/[id]/report", "/experts"].map((path) => ({ path, protected: true }))
49
+ }),
50
+ main,
51
+ site: main,
52
+ jev: defineLocaleService({ id: "jev", origin: origins.jev, routes: [{ path: "/", protected: true }, "/login"] }),
53
+ estimate: defineLocaleService({ id: "estimate", origin: origins.estimate, uiProtocolExceptions: ["/auth/complete"], routes: ["/", "/estimates", "/estimates/[id]", "/compare", "/settings", "/calendar", "/calendar-preview", "/calendar-preview/estimates", "/calendar-preview/estimates/[id]"].map((path) => ({ path, protected: true })).concat([{ path: "/login", protected: false }, { path: "/access-denied", protected: false }, { path: "/auth/complete", protected: false }]) })
54
+ };
55
+ function getLocaleService(service) {
56
+ return typeof service === "string" ? naeilServices[service] : service;
57
+ }
58
+ function isProtocolPath(path, service) {
59
+ var _a;
60
+ return [...protocolPrefixes, ...(_a = service.protocolPrefixes) != null ? _a : []].some((prefix) => path === prefix || path.startsWith(prefix + "/")) || protocolFiles.includes(path);
61
+ }
62
+ function matchRoute(service, pathname) {
63
+ const segments = pathname.split("/");
64
+ for (const route of service.routes) {
65
+ const record = typeof route === "string" ? { path: route } : route;
66
+ const expected = record.path.split("/");
67
+ if (expected.length === segments.length && expected.every((part, index) => part === segments[index] || /^\[[A-Za-z][A-Za-z0-9_]*\]$/.test(part) && !!segments[index])) return record;
68
+ }
69
+ }
70
+ function invalid(reason, status = 404, locale) {
71
+ return __spreadValues({ kind: "invalid", status, reason }, locale ? { locale } : {});
72
+ }
73
+ function splitInput(input, origin) {
74
+ if (!input || /[\u0000-\u0020\u007f\\]/.test(input)) return;
75
+ let path = input;
76
+ if (!path.startsWith("/")) {
77
+ if (!path.startsWith(origin + "/") && path !== origin) return;
78
+ path = path.slice(origin.length) || "/";
79
+ }
80
+ if (path.startsWith("//")) return;
81
+ const hashAt = path.indexOf("#");
82
+ const hash = hashAt < 0 ? "" : path.slice(hashAt);
83
+ const withoutHash = hashAt < 0 ? path : path.slice(0, hashAt);
84
+ const queryAt = withoutHash.indexOf("?");
85
+ return { pathname: queryAt < 0 ? withoutHash : withoutHash.slice(0, queryAt), search: queryAt < 0 ? "" : withoutHash.slice(queryAt), hash };
86
+ }
87
+ function safePath(path) {
88
+ if (!path.startsWith("/") || path.includes("//")) return false;
89
+ return path.split("/").every((segment) => {
90
+ try {
91
+ const decoded = decodeURIComponent(segment);
92
+ return !/[\u0000-\u001f\u007f/\\%?#]/.test(decoded) && decoded !== "." && decoded !== ".." && (!/^(?:en|ko|ja|api|auth|_next|invite|return|webhook)$/.test(decoded) || decoded === segment);
93
+ } catch (e) {
94
+ return false;
95
+ }
96
+ });
97
+ }
98
+ function stripLegacyLang(search) {
99
+ const retained = search.slice(1).split("&").filter((part) => {
100
+ try {
101
+ return decodeURIComponent(part.split("=")[0].replace(/\+/g, " ")) !== "lang";
102
+ } catch (e) {
103
+ return true;
104
+ }
105
+ });
106
+ return retained.length && retained.join("&") ? "?" + retained.join("&") : "";
107
+ }
108
+ function prefixPath(path, locale) {
109
+ return locale === "en" ? path : `/${locale}${path === "/" ? "" : path}`;
110
+ }
111
+ function resolveLocaleUrl(inputService, input) {
112
+ var _a, _b;
113
+ const service = getLocaleService(inputService);
114
+ const parts = splitInput(input, service.origin);
115
+ if (!parts || !safePath(parts.pathname)) return invalid("Malformed or encoded path");
116
+ const rawPathname = parts.pathname;
117
+ const pathname = rawPathname !== "/" && rawPathname.endsWith("/") ? rawPathname.slice(0, -1) : rawPathname;
118
+ if (isProtocolPath(pathname, service) && !((_a = service.uiProtocolExceptions) == null ? void 0 : _a.includes(pathname))) return { kind: "protocol", pathname };
119
+ const segments = pathname.split("/");
120
+ const explicit = isNaeilLocale(segments[1]);
121
+ const pathLocale = explicit ? segments[1] : void 0;
122
+ const logicalPath = explicit ? "/" + segments.slice(2).join("/") : pathname;
123
+ if (/^\/(?:en|ko|ja)(?:\/|$)/.test(logicalPath) || isProtocolPath(logicalPath, service) && !((_b = service.uiProtocolExceptions) == null ? void 0 : _b.includes(logicalPath))) return invalid("Nested locale or localized protocol path", 404, pathLocale);
124
+ const route = matchRoute(service, logicalPath);
125
+ if (!route) return invalid("Unregistered UI route", 404, pathLocale);
126
+ const legacy = service.id !== "main";
127
+ const languages = new URLSearchParams(parts.search).getAll("lang");
128
+ if (legacy && languages.length && (languages.length !== 1 || !isNaeilLocale(languages[0]))) return invalid("Invalid legacy language", 400, pathLocale);
129
+ const locale = pathLocale != null ? pathLocale : legacy && languages.length ? languages[0] : "en";
130
+ const search = legacy ? stripLegacyLang(parts.search) : parts.search;
131
+ const canonicalPath = prefixPath(logicalPath, locale) + search + parts.hash;
132
+ const originalPath = rawPathname + parts.search + parts.hash;
133
+ return { kind: "ui", locale, logicalPath, route, canonicalPath, originalPath, alias: canonicalPath !== originalPath };
134
+ }
135
+ function localePath(inputService, unlocalizedRoute, locale) {
136
+ assertLocale(locale);
137
+ const service = getLocaleService(inputService);
138
+ const parts = splitInput(unlocalizedRoute, service.origin);
139
+ if (!parts || /^\/(?:en|ko|ja)(?:\/|$)/.test(parts.pathname)) throw new Error("localePath requires an unlocalized route");
140
+ const result = resolveLocaleUrl(service, parts.pathname);
141
+ if (result.kind !== "ui") throw new Error("Unregistered UI destination");
142
+ return prefixPath(result.logicalPath, locale) + (service.id === "main" ? parts.search : stripLegacyLang(parts.search)) + parts.hash;
143
+ }
144
+
145
+ // src/naeil-locale/intent.ts
146
+ function createLocaleIntentGate(service) {
147
+ let serial = 0;
148
+ let pending;
149
+ return {
150
+ begin(href, locale, now = Date.now()) {
151
+ if (pending) return;
152
+ const resolved = resolveLocaleUrl(service, href);
153
+ if (resolved.kind !== "ui" || resolved.alias || resolved.locale !== locale) throw new Error("Invalid locale intent target");
154
+ pending = { token: ++serial, locale, href: resolved.canonicalPath, startedAt: now };
155
+ return pending;
156
+ },
157
+ current() {
158
+ return pending;
159
+ },
160
+ cancel(token) {
161
+ if (token === void 0 || (pending == null ? void 0 : pending.token) === token) pending = void 0;
162
+ },
163
+ arrive(token, href, locale) {
164
+ const resolved = resolveLocaleUrl(service, href);
165
+ if (!pending || pending.token !== token || pending.locale !== locale || resolved.kind !== "ui" || resolved.alias || resolved.canonicalPath !== pending.href) return;
166
+ const committed = pending;
167
+ pending = void 0;
168
+ return committed;
169
+ }
170
+ };
171
+ }
172
+
173
+ // src/naeil-locale/preference.ts
174
+ var localePreferenceCookie = "naeil_locale_v1";
175
+ var developmentPreferenceCookie = "naeil_locale_dev_v1";
176
+ function preferenceCookieWrites(locale, location) {
177
+ assertLocale(locale);
178
+ const production = location.hostname === "naeil.dev" || location.hostname.endsWith(".naeil.dev");
179
+ if (production) {
180
+ if (location.protocol !== "https:") return [];
181
+ return [
182
+ `${localePreferenceCookie}=; Path=/; Max-Age=0; Secure; SameSite=Lax`,
183
+ `${localePreferenceCookie}=${locale}; Domain=naeil.dev; Path=/; Max-Age=31536000; Secure; SameSite=Lax`
184
+ ];
185
+ }
186
+ return [`${developmentPreferenceCookie}=${locale}; Path=/; Max-Age=31536000; SameSite=Lax${location.protocol === "https:" ? "; Secure" : ""}`];
187
+ }
188
+ function writeLocalePreference(locale, environment, enabled = false) {
189
+ if (!enabled) return false;
190
+ try {
191
+ const writes = preferenceCookieWrites(locale, environment.location);
192
+ for (const cookie of writes) environment.writeCookie(cookie);
193
+ return writes.length > 0;
194
+ } catch (e) {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ // src/naeil-locale/client.tsx
200
+ import { jsx } from "react/jsx-runtime";
201
+ function captureFocus() {
202
+ const active = document.activeElement;
203
+ if (!(active instanceof HTMLElement)) return;
204
+ if (active.closest("[data-naeil-locale-menu]")) return { attribute: "data-naeil-focus-id", value: "locale-control" };
205
+ const attribute = ["data-naeil-focus-id", "id", "name"].find((key) => active.hasAttribute(key));
206
+ if (!attribute) return;
207
+ const snapshot = { attribute, value: active.getAttribute(attribute) };
208
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement) {
209
+ snapshot.start = active.selectionStart;
210
+ snapshot.end = active.selectionEnd;
211
+ snapshot.direction = active.selectionDirection;
212
+ }
213
+ return snapshot;
214
+ }
215
+ function restoreFocus(snapshot) {
216
+ var _a;
217
+ if (!snapshot) return;
218
+ const element = Array.from(document.querySelectorAll(`[${snapshot.attribute}]`)).find((node) => node.getAttribute(snapshot.attribute) === snapshot.value && node.getClientRects().length > 0);
219
+ if (!element) return;
220
+ element.focus({ preventScroll: true });
221
+ if ((element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) && snapshot.start !== void 0 && snapshot.start !== null && snapshot.end !== void 0 && snapshot.end !== null) {
222
+ try {
223
+ element.setSelectionRange(snapshot.start, snapshot.end, (_a = snapshot.direction) != null ? _a : void 0);
224
+ } catch (e) {
225
+ }
226
+ }
227
+ }
228
+ function browserHref() {
229
+ return window.location.pathname + window.location.search + window.location.hash;
230
+ }
231
+ var LocaleControllerContext = createContext(null);
232
+ function NaeilLocaleController({ service, children, preferenceWritesEnabled = false, transitionTimeoutMs = 2e4 }) {
233
+ const router = useRouter();
234
+ const pathname = usePathname();
235
+ const [gate] = useState(() => createLocaleIntentGate(service));
236
+ const [intent, setIntent] = useState();
237
+ const [error, setError] = useState();
238
+ const lastRequest = useRef(void 0);
239
+ const focus = useRef(void 0);
240
+ const timer = useRef(void 0);
241
+ const guards = useRef(/* @__PURE__ */ new Set());
242
+ const clearTimer = useCallback(() => {
243
+ if (timer.current !== void 0) clearTimeout(timer.current);
244
+ timer.current = void 0;
245
+ }, []);
246
+ const cancel = useCallback(() => {
247
+ gate.cancel();
248
+ clearTimer();
249
+ setIntent(void 0);
250
+ setError(void 0);
251
+ focus.current = void 0;
252
+ }, [gate, clearTimer]);
253
+ const fail = useCallback((token, reason) => {
254
+ var _a;
255
+ if (((_a = gate.current()) == null ? void 0 : _a.token) !== token) return;
256
+ gate.cancel(token);
257
+ clearTimer();
258
+ setIntent(void 0);
259
+ setError(reason);
260
+ }, [gate, clearTimer]);
261
+ const changeLocale = useCallback((language, options) => {
262
+ var _a;
263
+ const locale = assertLocale(language);
264
+ if (gate.current()) return false;
265
+ const current = resolveLocaleUrl(service, browserHref());
266
+ if (current.kind !== "ui") {
267
+ setError("unsupported-route");
268
+ return false;
269
+ }
270
+ if (current.locale === locale && !(options == null ? void 0 : options.route) && !(options == null ? void 0 : options.href)) return false;
271
+ const suffix = current.canonicalPath.slice(localePath(service, current.logicalPath, current.locale).length);
272
+ let href;
273
+ try {
274
+ if (options == null ? void 0 : options.href) {
275
+ if (options.route) throw new Error("Use either route or href");
276
+ const supplied = resolveLocaleUrl(service, options.href);
277
+ if (supplied.kind !== "ui" || supplied.locale !== locale || supplied.alias) throw new Error("Invalid locale destination");
278
+ href = supplied.canonicalPath;
279
+ } else href = localePath(service, (_a = options == null ? void 0 : options.route) != null ? _a : current.logicalPath + suffix, locale);
280
+ } catch (e) {
281
+ setError("unsupported-route");
282
+ return false;
283
+ }
284
+ const request = gate.begin(href, locale);
285
+ if (!request) return false;
286
+ lastRequest.current = { locale, options };
287
+ focus.current = captureFocus();
288
+ setIntent(request);
289
+ setError(void 0);
290
+ timer.current = setTimeout(() => fail(request.token, "timeout"), transitionTimeoutMs);
291
+ try {
292
+ router.replace(href, { scroll: false });
293
+ return true;
294
+ } catch (e) {
295
+ fail(request.token, "navigation-failed");
296
+ return false;
297
+ }
298
+ }, [service, gate, router, fail, transitionTimeoutMs]);
299
+ const retry = useCallback(() => lastRequest.current ? changeLocale(lastRequest.current.locale, lastRequest.current.options) : false, [changeLocale]);
300
+ const arrive = useCallback((token, locale, committedPathname) => {
301
+ if (token === void 0 || committedPathname !== window.location.pathname) return;
302
+ const committed = gate.arrive(token, browserHref(), locale);
303
+ if (!committed) return;
304
+ clearTimer();
305
+ setIntent(void 0);
306
+ setError(void 0);
307
+ writeLocalePreference(locale, { location: window.location, writeCookie: (cookie) => {
308
+ document.cookie = cookie;
309
+ } }, preferenceWritesEnabled);
310
+ const savedFocus = focus.current;
311
+ focus.current = void 0;
312
+ requestAnimationFrame(() => restoreFocus(savedFocus));
313
+ }, [gate, clearTimer, preferenceWritesEnabled]);
314
+ const registerDepartureGuard = useCallback((guard) => {
315
+ guards.current.add(guard);
316
+ return () => {
317
+ guards.current.delete(guard);
318
+ };
319
+ }, []);
320
+ useEffect(() => {
321
+ const onPopState = () => cancel();
322
+ const beforeUnload = (event) => {
323
+ if ([...guards.current].some((guard) => {
324
+ try {
325
+ return guard();
326
+ } catch (e) {
327
+ return true;
328
+ }
329
+ })) {
330
+ event.preventDefault();
331
+ event.returnValue = "";
332
+ }
333
+ };
334
+ window.addEventListener("popstate", onPopState);
335
+ window.addEventListener("beforeunload", beforeUnload);
336
+ return () => {
337
+ window.removeEventListener("popstate", onPopState);
338
+ window.removeEventListener("beforeunload", beforeUnload);
339
+ gate.cancel();
340
+ clearTimer();
341
+ };
342
+ }, [cancel, gate, clearTimer]);
343
+ useEffect(() => {
344
+ const pending = gate.current();
345
+ if (!pending) return;
346
+ const target = pending.href.split(/[?#]/, 1)[0];
347
+ const current = resolveLocaleUrl(service, pathname != null ? pathname : "/");
348
+ const destination = resolveLocaleUrl(service, target);
349
+ if (current.kind !== "ui" || destination.kind !== "ui" || current.logicalPath !== destination.logicalPath) cancel();
350
+ }, [pathname, service, gate, cancel]);
351
+ const context = useMemo(() => ({ pending: !!intent, intent, error, changeLocale, retry, cancel, service, arrive, registerDepartureGuard }), [intent, error, changeLocale, retry, cancel, service, arrive, registerDepartureGuard]);
352
+ return /* @__PURE__ */ jsx(LocaleControllerContext.Provider, { value: context, children });
353
+ }
354
+ function useLocaleController() {
355
+ const controller = useContext(LocaleControllerContext);
356
+ if (!controller) throw new Error("NaeilLocaleController is required above the locale route");
357
+ return controller;
358
+ }
359
+ function NaeilLocaleProvider({ locale: input, messages, children, timeZone = "UTC", now }) {
360
+ var _a;
361
+ const locale = assertLocale(input);
362
+ const controller = useContext(LocaleControllerContext);
363
+ const pathname = usePathname();
364
+ const token = (_a = controller == null ? void 0 : controller.intent) == null ? void 0 : _a.token;
365
+ const arrive = controller == null ? void 0 : controller.arrive;
366
+ useEffect(() => {
367
+ document.documentElement.lang = locale;
368
+ if (pathname) arrive == null ? void 0 : arrive(token, locale, pathname);
369
+ }, [locale, pathname, token, arrive]);
370
+ if (!messages.naeil || typeof messages.naeil !== "object") throw new Error("NaeilLocaleProvider requires the shared naeil namespace");
371
+ return /* @__PURE__ */ jsx(NextIntlClientProvider, { locale, messages, timeZone, now, children: /* @__PURE__ */ jsx(NaeilMessagesProvider, { locale, messages: messages.naeil, children }) });
372
+ }
373
+ function useLocaleBeforeUnload(unsafeToLeave) {
374
+ const controller = useContext(LocaleControllerContext);
375
+ if (!controller) throw new Error("NaeilLocaleController is required for departure protection");
376
+ const current = useRef(unsafeToLeave);
377
+ useLayoutEffect(() => {
378
+ current.current = unsafeToLeave;
379
+ }, [unsafeToLeave]);
380
+ const register = controller.registerDepartureGuard;
381
+ useEffect(() => register(() => typeof current.current === "function" ? current.current() : current.current), [register]);
382
+ }
383
+ export {
384
+ NaeilLocaleController,
385
+ NaeilLocaleProvider,
386
+ useLocaleBeforeUnload,
387
+ useLocaleController
388
+ };
@@ -0,0 +1,86 @@
1
+ import "./chunk-N3ETBM74.js";
2
+
3
+ // src/naeil-locale/messages/en.json
4
+ var naeil = {
5
+ header: {
6
+ nav: {
7
+ home: "Home",
8
+ projects: "Projects",
9
+ blog: "Blog",
10
+ navigation: "Main navigation",
11
+ menu: "Menu",
12
+ closeMenu: "Close menu"
13
+ },
14
+ theme: {
15
+ switchToLight: "Switch to light theme",
16
+ switchToDark: "Switch to dark theme"
17
+ },
18
+ locale: {
19
+ switchLabel: "Language"
20
+ },
21
+ projects: {
22
+ cc: {
23
+ name: "Content Collector",
24
+ description: "Automated content collection from RSS, YouTube, Reddit + AI summarization"
25
+ },
26
+ pkm: {
27
+ name: "PKM",
28
+ description: "Personal knowledge management \u2014 document parsing, embeddings, semantic search"
29
+ },
30
+ naeilUi: {
31
+ name: "naeil-ui",
32
+ description: "Minimal design system \u2014 OKLCH tokens, Tailwind v4, shadcn"
33
+ },
34
+ sa: {
35
+ name: "Sustainability Analyzer",
36
+ description: "AI-powered ESG report analysis \u2014 PDF parsing, qualitative assessment, comparison tables"
37
+ },
38
+ babyAgent: {
39
+ name: "Baby Agent",
40
+ description: "Parenting AI agent \u2014 LINE-based Q&A, records, reminders"
41
+ },
42
+ status: {
43
+ active: "Active",
44
+ coming: "Coming Soon"
45
+ }
46
+ }
47
+ },
48
+ account: {
49
+ menu: "Account menu",
50
+ account: "Account",
51
+ login: "Log in",
52
+ logout: "Log out",
53
+ busy: "Logging out\u2026"
54
+ },
55
+ footer: {
56
+ navigation: "Navigation",
57
+ projects: "Projects",
58
+ blog: "Blog",
59
+ social: "Social",
60
+ description: "Tomorrow starts here."
61
+ },
62
+ locale: {
63
+ names: {
64
+ en: "English",
65
+ ko: "\uD55C\uAD6D\uC5B4",
66
+ ja: "\u65E5\u672C\u8A9E"
67
+ },
68
+ pending: "Changing language\u2026",
69
+ failed: "The language could not be changed. Your work is still here.",
70
+ retry: "Try again",
71
+ errors: {
72
+ badRequest: "This address is not valid.",
73
+ notFound: "Page not found.",
74
+ unauthenticated: "Sign in to continue.",
75
+ forbidden: "You do not have access to this page.",
76
+ unavailable: "The service is temporarily unavailable. Please try again."
77
+ }
78
+ }
79
+ };
80
+ var en_default = {
81
+ naeil
82
+ };
83
+ export {
84
+ en_default as default,
85
+ naeil
86
+ };
@@ -0,0 +1,40 @@
1
+ import { N as NaeilLocale, a as LocaleServiceInput } from './messages-BuQvB-u2.js';
2
+ export { b as LocaleMessageLoaders, L as LocaleMessages, c as LocaleUrlResolution, d as NaeilLocaleService, e as NaeilRoute, f as NaeilServiceId, R as ResolvedLocaleRoute, S as SharedMessages, g as assertLocale, h as defaultLocale, i as defineLocaleService, j as getLocaleService, k as isNaeilLocale, l as localeHref, m as localePath, n as naeilLocales, o as naeilRouting, p as naeilServices, r as resolveLocaleUrl, s as sameLogicalRoute } from './messages-BuQvB-u2.js';
3
+
4
+ declare const localePreferenceCookie: string;
5
+ declare const developmentPreferenceCookie: string;
6
+ declare function readLocalePreference(cookie: string, development?: boolean): NaeilLocale | undefined;
7
+ declare function preferenceCookieWrites(locale: NaeilLocale, location: {
8
+ hostname: string;
9
+ protocol: string;
10
+ }): string[];
11
+ declare function writeLocalePreference(locale: NaeilLocale, environment: {
12
+ location: {
13
+ hostname: string;
14
+ protocol: string;
15
+ };
16
+ writeCookie: (cookie: string) => void;
17
+ }, enabled?: boolean): boolean;
18
+ declare function browserLocaleHint(languages: readonly string[]): NaeilLocale | undefined;
19
+ /** Legacy reads are hint-only and expire against the recorded activation date. */
20
+ declare function legacyLocaleHint(options: {
21
+ activatedAt: number | undefined;
22
+ now: number;
23
+ values: readonly (string | null | undefined)[];
24
+ }): NaeilLocale | undefined;
25
+
26
+ interface LocaleIntent {
27
+ token: number;
28
+ locale: NaeilLocale;
29
+ href: string;
30
+ startedAt: number;
31
+ }
32
+ /** Pure arrival gate. Router invocation/prefetch alone is never a commit. */
33
+ declare function createLocaleIntentGate(service: LocaleServiceInput): {
34
+ begin(href: string, locale: NaeilLocale, now?: number): LocaleIntent | undefined;
35
+ current(): LocaleIntent | undefined;
36
+ cancel(token?: number): void;
37
+ arrive(token: number, href: string, locale: NaeilLocale): LocaleIntent | undefined;
38
+ };
39
+
40
+ export { type LocaleIntent, LocaleServiceInput, NaeilLocale, browserLocaleHint, createLocaleIntentGate, developmentPreferenceCookie, legacyLocaleHint, localePreferenceCookie, preferenceCookieWrites, readLocalePreference, writeLocalePreference };
@@ -0,0 +1,111 @@
1
+ import {
2
+ assertLocale,
3
+ defaultLocale,
4
+ defineLocaleService,
5
+ getLocaleService,
6
+ isNaeilLocale,
7
+ localeHref,
8
+ localePath,
9
+ naeilLocales,
10
+ naeilRouting,
11
+ naeilServices,
12
+ resolveLocaleUrl,
13
+ sameLogicalRoute
14
+ } from "./chunk-U65OQHZY.js";
15
+ import "./chunk-N3ETBM74.js";
16
+
17
+ // src/naeil-locale/preference.ts
18
+ var localePreferenceCookie = "naeil_locale_v1";
19
+ var developmentPreferenceCookie = "naeil_locale_dev_v1";
20
+ function readLocalePreference(cookie, development = false) {
21
+ const name = development ? developmentPreferenceCookie : localePreferenceCookie;
22
+ const matches = cookie.split(";").map((item) => item.trim()).filter((item) => item.split("=", 1)[0] === name);
23
+ if (matches.length !== 1) return;
24
+ const value = matches[0].slice(name.length + 1);
25
+ return isNaeilLocale(value) ? value : void 0;
26
+ }
27
+ function preferenceCookieWrites(locale, location) {
28
+ assertLocale(locale);
29
+ const production = location.hostname === "naeil.dev" || location.hostname.endsWith(".naeil.dev");
30
+ if (production) {
31
+ if (location.protocol !== "https:") return [];
32
+ return [
33
+ `${localePreferenceCookie}=; Path=/; Max-Age=0; Secure; SameSite=Lax`,
34
+ `${localePreferenceCookie}=${locale}; Domain=naeil.dev; Path=/; Max-Age=31536000; Secure; SameSite=Lax`
35
+ ];
36
+ }
37
+ return [`${developmentPreferenceCookie}=${locale}; Path=/; Max-Age=31536000; SameSite=Lax${location.protocol === "https:" ? "; Secure" : ""}`];
38
+ }
39
+ function writeLocalePreference(locale, environment, enabled = false) {
40
+ if (!enabled) return false;
41
+ try {
42
+ const writes = preferenceCookieWrites(locale, environment.location);
43
+ for (const cookie of writes) environment.writeCookie(cookie);
44
+ return writes.length > 0;
45
+ } catch (e) {
46
+ return false;
47
+ }
48
+ }
49
+ function browserLocaleHint(languages) {
50
+ for (const language of languages) {
51
+ try {
52
+ const base = new Intl.Locale(language).language;
53
+ if (isNaeilLocale(base)) return base;
54
+ } catch (e) {
55
+ }
56
+ }
57
+ }
58
+ function legacyLocaleHint(options) {
59
+ if (options.activatedAt === void 0 || options.now >= options.activatedAt + 90 * 864e5 || options.now < options.activatedAt) return;
60
+ return options.values.find(isNaeilLocale);
61
+ }
62
+
63
+ // src/naeil-locale/intent.ts
64
+ function createLocaleIntentGate(service) {
65
+ let serial = 0;
66
+ let pending;
67
+ return {
68
+ begin(href, locale, now = Date.now()) {
69
+ if (pending) return;
70
+ const resolved = resolveLocaleUrl(service, href);
71
+ if (resolved.kind !== "ui" || resolved.alias || resolved.locale !== locale) throw new Error("Invalid locale intent target");
72
+ pending = { token: ++serial, locale, href: resolved.canonicalPath, startedAt: now };
73
+ return pending;
74
+ },
75
+ current() {
76
+ return pending;
77
+ },
78
+ cancel(token) {
79
+ if (token === void 0 || (pending == null ? void 0 : pending.token) === token) pending = void 0;
80
+ },
81
+ arrive(token, href, locale) {
82
+ const resolved = resolveLocaleUrl(service, href);
83
+ if (!pending || pending.token !== token || pending.locale !== locale || resolved.kind !== "ui" || resolved.alias || resolved.canonicalPath !== pending.href) return;
84
+ const committed = pending;
85
+ pending = void 0;
86
+ return committed;
87
+ }
88
+ };
89
+ }
90
+ export {
91
+ assertLocale,
92
+ browserLocaleHint,
93
+ createLocaleIntentGate,
94
+ defaultLocale,
95
+ defineLocaleService,
96
+ developmentPreferenceCookie,
97
+ getLocaleService,
98
+ isNaeilLocale,
99
+ legacyLocaleHint,
100
+ localeHref,
101
+ localePath,
102
+ localePreferenceCookie,
103
+ naeilLocales,
104
+ naeilRouting,
105
+ naeilServices,
106
+ preferenceCookieWrites,
107
+ readLocalePreference,
108
+ resolveLocaleUrl,
109
+ sameLogicalRoute,
110
+ writeLocalePreference
111
+ };