@k8ordo/i18n 0.1.0 → 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 +73 -28
- package/dist/accept-language.d.mts +7 -7
- package/dist/accept-language.mjs +5 -4
- package/dist/cookie.d.mts +10 -0
- package/dist/cookie.mjs +20 -0
- package/dist/current.d.mts +26 -23
- package/dist/current.mjs +31 -5
- package/dist/format.d.mts +32 -0
- package/dist/format.mjs +31 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +2 -1
- package/dist/locales.d.mts +76 -19
- package/dist/locales.mjs +55 -12
- package/dist/message.d.mts +5 -6
- package/dist/message.mjs +2 -2
- package/dist/register.d.mts +6 -6
- package/docs/GUIDE.md +394 -113
- package/docs/llms.txt +37 -14
- package/package.json +7 -7
package/dist/locales.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseAcceptLanguage } from "./accept-language.mjs";
|
|
2
|
+
import { readCookie } from "./cookie.mjs";
|
|
3
|
+
import { browserPathname, inBrowser, localeStorage, register } from "./current.mjs";
|
|
4
|
+
import { intlFormats } from "./format.mjs";
|
|
2
5
|
//#region src/locales.ts
|
|
3
6
|
/** `Intl.Locale` throws a RangeError on a tag that is not BCP 47; that is the only check a tag needs. */
|
|
4
7
|
const parseTag = (tag) => {
|
|
@@ -14,12 +17,33 @@ const localize = (pathname, locale) => {
|
|
|
14
17
|
if (rest !== "" && !rest.startsWith("/")) throw new TypeError(`localize: ${JSON.stringify(pathname)} is not a pathname — it does not start with "/"`);
|
|
15
18
|
return `/${locale}${rest}`;
|
|
16
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Where a server keeps the locale. Without one this throws rather than
|
|
22
|
+
* falling back: the locale set would be ignored and the page rendered in the
|
|
23
|
+
* default.
|
|
24
|
+
*/
|
|
25
|
+
const serverStorage = (caller) => {
|
|
26
|
+
const storage = localeStorage();
|
|
27
|
+
if (storage === null) throw new Error(`${caller}: this runtime has no AsyncLocalStorage to scope a locale to`);
|
|
28
|
+
return storage;
|
|
29
|
+
};
|
|
17
30
|
/** Server only: the browser has no request, and its URL already is the locale. */
|
|
18
31
|
const run = (locale, fn) => {
|
|
19
32
|
if (inBrowser) throw new Error("locales.run: in the browser the URL is the locale — navigate instead");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
return serverStorage("locales.run").run(locale, fn);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* `Intl.DateTimeFormat` throws a RangeError on a time zone it does not know,
|
|
37
|
+
* and quietly takes the runtime's own for a missing one — the very value
|
|
38
|
+
* that differs between a server and a browser — so both are `null` here.
|
|
39
|
+
*/
|
|
40
|
+
const resolveTimeZone = (value) => {
|
|
41
|
+
if (typeof value !== "string") return null;
|
|
42
|
+
try {
|
|
43
|
+
return new Intl.DateTimeFormat("en", { timeZone: value }).resolvedOptions().timeZone;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
23
47
|
};
|
|
24
48
|
/**
|
|
25
49
|
* Declares the locale set once. Everything that needs to know which locales
|
|
@@ -28,18 +52,24 @@ const run = (locale, fn) => {
|
|
|
28
52
|
* every message — reads this value, so the list is spelled in one place.
|
|
29
53
|
*
|
|
30
54
|
* ```ts
|
|
31
|
-
* export const locales = defineLocales(
|
|
32
|
-
*
|
|
55
|
+
* export const locales = defineLocales({
|
|
56
|
+
* ja: { timeZone: 'Asia/Tokyo', dir: 'ltr' },
|
|
57
|
+
* en: { timeZone: 'America/New_York', dir: 'ltr' },
|
|
58
|
+
* });
|
|
33
59
|
* ```
|
|
34
60
|
*/
|
|
35
|
-
const defineLocales = (
|
|
36
|
-
const
|
|
37
|
-
|
|
61
|
+
const defineLocales = (definitions, options) => {
|
|
62
|
+
const byTag = definitions;
|
|
63
|
+
const list = Object.keys(byTag);
|
|
64
|
+
if (list.length === 0) throw new TypeError("defineLocales: no locale is defined");
|
|
38
65
|
const fallback = options?.default ?? list[0];
|
|
39
66
|
if (!list.includes(fallback)) throw new TypeError(`defineLocales: the default ${JSON.stringify(fallback)} is not in ${JSON.stringify(list)}`);
|
|
40
67
|
const supported = list.map((tag) => {
|
|
41
68
|
const parsed = parseTag(tag);
|
|
42
69
|
if (parsed === null) throw new TypeError(`defineLocales: ${JSON.stringify(tag)} is not a BCP 47 language tag`);
|
|
70
|
+
const { timeZone, dir } = byTag[tag];
|
|
71
|
+
if (resolveTimeZone(timeZone) === null) throw new TypeError(`defineLocales: the timeZone of ${JSON.stringify(tag)}, ${JSON.stringify(timeZone)}, is not a time zone`);
|
|
72
|
+
if (dir !== "ltr" && dir !== "rtl") throw new TypeError(`defineLocales: the dir of ${JSON.stringify(tag)}, ${JSON.stringify(dir)}, is neither "ltr" nor "rtl"`);
|
|
43
73
|
return {
|
|
44
74
|
tag,
|
|
45
75
|
language: parsed.language
|
|
@@ -58,6 +88,15 @@ const defineLocales = (all, options) => {
|
|
|
58
88
|
}
|
|
59
89
|
return fallback;
|
|
60
90
|
};
|
|
91
|
+
const negotiateRequest = (request, { cookie } = {}) => {
|
|
92
|
+
const chosen = cookie === void 0 ? null : readCookie(request.headers.get("cookie"), cookie);
|
|
93
|
+
return negotiate([...chosen === null ? [] : [chosen], ...parseAcceptLanguage(request.headers.get("accept-language"))]);
|
|
94
|
+
};
|
|
95
|
+
const paths = (patterns) => patterns.flatMap((pattern) => {
|
|
96
|
+
const segments = pattern.split("/");
|
|
97
|
+
if (!segments.includes(":locale")) return [pattern];
|
|
98
|
+
return list.map((locale) => segments.map((segment) => segment === ":locale" ? locale : segment).join("/"));
|
|
99
|
+
});
|
|
61
100
|
const delocalize = (pathname) => {
|
|
62
101
|
const first = pathname.split("/")[1] ?? "";
|
|
63
102
|
if (!is(first)) return {
|
|
@@ -71,7 +110,7 @@ const defineLocales = (all, options) => {
|
|
|
71
110
|
};
|
|
72
111
|
};
|
|
73
112
|
const getLocale = () => {
|
|
74
|
-
if (inBrowser) return delocalize(
|
|
113
|
+
if (inBrowser) return delocalize(browserPathname()).locale ?? fallback;
|
|
75
114
|
const current = localeStorage()?.getStore();
|
|
76
115
|
return is(current) ? current : fallback;
|
|
77
116
|
};
|
|
@@ -84,7 +123,7 @@ const defineLocales = (all, options) => {
|
|
|
84
123
|
message: `expected one of ${JSON.stringify(list)}, received ${JSON.stringify(locale)}`,
|
|
85
124
|
path: ["locale"]
|
|
86
125
|
}] };
|
|
87
|
-
|
|
126
|
+
if (!inBrowser) serverStorage("locales.paramsSchema").enterWith(locale);
|
|
88
127
|
return { value: { locale } };
|
|
89
128
|
}
|
|
90
129
|
} };
|
|
@@ -94,14 +133,18 @@ const defineLocales = (all, options) => {
|
|
|
94
133
|
});
|
|
95
134
|
return {
|
|
96
135
|
all: list,
|
|
136
|
+
definitions: byTag,
|
|
97
137
|
default: fallback,
|
|
98
138
|
is,
|
|
99
139
|
negotiate,
|
|
140
|
+
negotiateRequest,
|
|
100
141
|
localize,
|
|
101
142
|
delocalize,
|
|
143
|
+
paths,
|
|
102
144
|
paramsSchema,
|
|
103
145
|
getLocale,
|
|
104
|
-
run
|
|
146
|
+
run,
|
|
147
|
+
...intlFormats(getLocale, byTag)
|
|
105
148
|
};
|
|
106
149
|
};
|
|
107
150
|
//#endregion
|
package/dist/message.d.mts
CHANGED
|
@@ -5,9 +5,9 @@ import { RegisteredLocale } from "./register.mjs";
|
|
|
5
5
|
* string), returning the text in the current locale. What a component that
|
|
6
6
|
* takes a message as a prop should accept.
|
|
7
7
|
*/
|
|
8
|
-
type Message<A extends readonly unknown[] = []> = (...args: A) => string;
|
|
8
|
+
export type Message<A extends readonly unknown[] = []> = (...args: A) => string;
|
|
9
9
|
/** One value per locale — every locale, no locale twice. */
|
|
10
|
-
type Variants<V> = Readonly<Record<RegisteredLocale, V>>;
|
|
10
|
+
export type Variants<V> = Readonly<Record<RegisteredLocale, V>>;
|
|
11
11
|
/**
|
|
12
12
|
* Declares one message with its text in every locale. The result reads the
|
|
13
13
|
* locale where it is called — the request on a server, the URL in the
|
|
@@ -25,7 +25,6 @@ type Variants<V> = Readonly<Record<RegisteredLocale, V>>;
|
|
|
25
25
|
* When nothing names a locale, the text is the default locale's — the one
|
|
26
26
|
* `defineLocales` registered, else the first variant written.
|
|
27
27
|
*/
|
|
28
|
-
declare function message(variants: Variants<string>): Message;
|
|
29
|
-
declare function message<A extends readonly unknown[]>(variants: Variants<(...args: A) => string>): Message<A>;
|
|
30
|
-
//#endregion
|
|
31
|
-
export { Message, Variants, message };
|
|
28
|
+
export declare function message(variants: Variants<string>): Message;
|
|
29
|
+
export declare function message<A extends readonly unknown[]>(variants: Variants<(...args: A) => string>): Message<A>;
|
|
30
|
+
//#endregion
|
package/dist/message.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { namedLocale, registeredDefault, setRegistered } from "./current.mjs";
|
|
2
2
|
//#region src/message.ts
|
|
3
3
|
function message(variants) {
|
|
4
4
|
return (...args) => {
|
|
5
|
-
const named =
|
|
5
|
+
const named = namedLocale();
|
|
6
6
|
const locale = named !== null && (setRegistered() || Object.hasOwn(variants, named)) ? named : registeredDefault();
|
|
7
7
|
const variant = locale === null ? variants[Object.keys(variants)[0] ?? ""] : variants[locale];
|
|
8
8
|
if (variant === void 0) throw new TypeError(`message: no text for ${JSON.stringify(locale)} in ${JSON.stringify(Object.keys(variants))}`);
|
package/dist/register.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
//#region src/register.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* Merged
|
|
4
|
-
*
|
|
3
|
+
* Merged once, so `message()` knows which locales every message must carry —
|
|
4
|
+
* without each message naming the set. `@k8ordo/framework` generates it from
|
|
5
|
+
* the `locales` export of `src/i18n.ts`; any other application writes it:
|
|
5
6
|
*
|
|
6
7
|
* ```ts
|
|
7
8
|
* declare module '@k8ordo/i18n' {
|
|
@@ -14,10 +15,9 @@
|
|
|
14
15
|
* The one `interface` in this package, kept so because it exists to be
|
|
15
16
|
* merged (see the repository's conventions).
|
|
16
17
|
*/
|
|
17
|
-
interface Register {}
|
|
18
|
+
export interface Register {}
|
|
18
19
|
/** The application's locale union once `Register` is merged; `string` before. */
|
|
19
|
-
type RegisteredLocale = Register extends {
|
|
20
|
+
export type RegisteredLocale = Register extends {
|
|
20
21
|
locale: infer L extends string;
|
|
21
22
|
} ? L : string;
|
|
22
|
-
//#endregion
|
|
23
|
-
export { Register, RegisteredLocale };
|
|
23
|
+
//#endregion
|