@escape-game-over/atlas 0.1.96 → 0.1.98
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/docs/rich-text.md +39 -1
- package/package.json +1 -1
- package/src/content/index.ts +1 -0
- package/src/content/rich.ts +96 -37
- package/src/i18n/translate.ts +56 -19
- package/src/index.ts +1 -0
- package/src/site/api.ts +31 -5
- package/src/site/create.ts +20 -5
package/docs/rich-text.md
CHANGED
|
@@ -167,6 +167,43 @@ A caller that does can build one — `joinUrl(site.urlFor(id, locale), href)`.
|
|
|
167
167
|
address or number is written once. The number must be in international form —
|
|
168
168
|
`"+30 210 0000000"` — because a local one does not dial from abroad.
|
|
169
169
|
|
|
170
|
+
## Binding slots and values once
|
|
171
|
+
|
|
172
|
+
A view that renders many messages through one call — a FAQ page reading every
|
|
173
|
+
answer in a loop, a description per route — cannot pass a slot or a value to
|
|
174
|
+
some messages and not others: a message that declares none refuses the
|
|
175
|
+
argument, and one that does demands it. Bind them when the reader is made:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const rich = site.rich(locale, {
|
|
179
|
+
links: { contact: "contact", rooms: "rooms" },
|
|
180
|
+
values: { hours: "Mon–Fri 10:00–22:00" },
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
rich("faq.hours.a"); // "…open {hours}, see [a:contact]Contact[/a]." needs nothing
|
|
184
|
+
rich("faq.age.a"); // no slots or placeholders, still no argument
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`site.translate(locale, { values })` and `site.plain(locale, { values })` take
|
|
188
|
+
the same `values`; they have no links to bind.
|
|
189
|
+
|
|
190
|
+
Nothing is loosened:
|
|
191
|
+
|
|
192
|
+
- **A bound destination is checked where it is bound**, against the routes this
|
|
193
|
+
project builds, as a call's would be.
|
|
194
|
+
- **Whatever is left unbound is still required** at the call, and still a
|
|
195
|
+
compile error without it — for a single key and for a union of keys alike, so
|
|
196
|
+
a loop over messages fails to compile if one names a slot or placeholder
|
|
197
|
+
nobody bound.
|
|
198
|
+
- **A bound name is not offered to the call.** Passing it again is a compile
|
|
199
|
+
error; for a key that arrived as a plain `string`, the call's own wins.
|
|
200
|
+
- **A bound name nothing uses is fine.** A reader binds what its messages may
|
|
201
|
+
use, not what each one does.
|
|
202
|
+
- **A value is still text.** It cannot open a mark, bound or passed.
|
|
203
|
+
|
|
204
|
+
Copy still names a slot, never an address. Binding moves where the destination
|
|
205
|
+
or value is written — to the one place that renders the copy — not into it.
|
|
206
|
+
|
|
170
207
|
## What the compiler holds in step
|
|
171
208
|
|
|
172
209
|
Which marks a message uses is checked across locales by the same machinery that
|
|
@@ -266,7 +303,8 @@ class nor attrs still renders its words, and warns at build.
|
|
|
266
303
|
| --------------------------------------------------- | --------------------------------------------------------------------- |
|
|
267
304
|
| A locale dropping `[a:venue]` | compile error — the marks must match across a message |
|
|
268
305
|
| `[a:venue]` in one locale, `[a:booking]` in another | compile error — the argument is part of the token |
|
|
269
|
-
| `rich()` without a slot's target
|
|
306
|
+
| `rich()` without a slot's target, unbound | compile error, then a build error naming the slot |
|
|
307
|
+
| A slot bound to an unbuilt route | compile error where it is bound |
|
|
270
308
|
| A slot filled with an unbuilt route | compile error at the call site |
|
|
271
309
|
| `[Mon-Fri]` | build error, listing the marks and the `[[` escape |
|
|
272
310
|
| `[b]` never closed, `[/b]` never opened | build error naming the message and locale |
|
package/package.json
CHANGED
package/src/content/index.ts
CHANGED
package/src/content/rich.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
lookupTemplate,
|
|
12
12
|
type MergedCatalog,
|
|
13
13
|
render,
|
|
14
|
+
valueFor,
|
|
14
15
|
} from "../i18n/translate.ts";
|
|
15
16
|
import type { StringKeys } from "../types.ts";
|
|
16
17
|
import {
|
|
@@ -179,6 +180,28 @@ export interface RichTextOptions<L extends string> {
|
|
|
179
180
|
* on a bad one. `joinUrl` already answers it once.
|
|
180
181
|
*/
|
|
181
182
|
readonly origin: HttpsUrl;
|
|
183
|
+
/**
|
|
184
|
+
* Destinations bound once, for every message this `rich()` reads: a slot
|
|
185
|
+
* named here is filled without the call passing it. Already checked by
|
|
186
|
+
* the type that built this; strings by the time they reach the runtime.
|
|
187
|
+
*/
|
|
188
|
+
readonly links?: Readonly<Record<string, string>>;
|
|
189
|
+
/** Placeholder values bound the same way. */
|
|
190
|
+
readonly values?: Readonly<Record<string, string>>;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* What a `rich()` binds when it is made: link destinations and placeholder
|
|
195
|
+
* values that every message read through it may use without the call passing
|
|
196
|
+
* them. `plain()` and `t()` take the `values` half.
|
|
197
|
+
*/
|
|
198
|
+
export interface MessageBindings<
|
|
199
|
+
RouteId extends string,
|
|
200
|
+
Links extends string,
|
|
201
|
+
Values extends string,
|
|
202
|
+
> {
|
|
203
|
+
readonly links?: Readonly<Record<Links, LinkTarget<RouteId>>>;
|
|
204
|
+
readonly values?: Readonly<Record<Values, string>>;
|
|
182
205
|
}
|
|
183
206
|
|
|
184
207
|
/** The link slots message `K` declares, e.g. `"venue"` from `[a:venue]`. */
|
|
@@ -200,8 +223,12 @@ export type RichParamsOf<
|
|
|
200
223
|
Catalog,
|
|
201
224
|
K extends StringKeys<Catalog>,
|
|
202
225
|
RouteId extends string,
|
|
203
|
-
|
|
204
|
-
|
|
226
|
+
Links extends string = never,
|
|
227
|
+
Values extends string = never,
|
|
228
|
+
> = Readonly<Record<Exclude<MessageParamsOf<Catalog, K>, Values>, string>> &
|
|
229
|
+
Readonly<
|
|
230
|
+
Record<Exclude<LinkNamesOf<Catalog, K>, Links>, LinkTarget<RouteId>>
|
|
231
|
+
>;
|
|
205
232
|
|
|
206
233
|
/**
|
|
207
234
|
* `[key]` for a message that needs nothing, `[key, params]` otherwise.
|
|
@@ -214,25 +241,36 @@ export type RichArgsOf<
|
|
|
214
241
|
Catalog,
|
|
215
242
|
K extends StringKeys<Catalog>,
|
|
216
243
|
RouteId extends string,
|
|
217
|
-
|
|
244
|
+
Links extends string = never,
|
|
245
|
+
Values extends string = never,
|
|
246
|
+
> = [
|
|
247
|
+
| Exclude<MessageParamsOf<Catalog, K>, Values>
|
|
248
|
+
| Exclude<LinkNamesOf<Catalog, K>, Links>,
|
|
249
|
+
] extends [never]
|
|
218
250
|
? [key: K]
|
|
219
|
-
: [key: K, params: RichParamsOf<Catalog, K, RouteId>];
|
|
251
|
+
: [key: K, params: RichParamsOf<Catalog, K, RouteId, Links, Values>];
|
|
220
252
|
|
|
221
253
|
/**
|
|
222
254
|
* `rich()`, keyed like `t()` and asking for a little more.
|
|
223
255
|
*
|
|
224
256
|
* Generic over the routes as well as the catalog, which is what lets a link
|
|
225
|
-
* slot be typed. `createSite` supplies both.
|
|
257
|
+
* slot be typed. `createSite` supplies both. `Links` and `Values` name what
|
|
258
|
+
* was bound when this `rich()` was made, which a call no longer passes.
|
|
226
259
|
*/
|
|
227
|
-
export type RichTextFor<
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
260
|
+
export type RichTextFor<
|
|
261
|
+
Catalog,
|
|
262
|
+
RouteId extends string,
|
|
263
|
+
Links extends string = never,
|
|
264
|
+
Values extends string = never,
|
|
265
|
+
> = <K extends StringKeys<Catalog>>(
|
|
266
|
+
...args: RichArgsOf<Catalog, K, RouteId, Links, Values>
|
|
231
267
|
) => RichText;
|
|
232
268
|
|
|
233
269
|
/** `site.plain()`, keyed and parameterised exactly as `t()` is. */
|
|
234
|
-
export type PlainTextFor<Catalog
|
|
235
|
-
|
|
270
|
+
export type PlainTextFor<Catalog, Values extends string = never> = <
|
|
271
|
+
K extends StringKeys<Catalog>,
|
|
272
|
+
>(
|
|
273
|
+
...args: TranslateArgsOf<Catalog, K, Values>
|
|
236
274
|
) => string;
|
|
237
275
|
|
|
238
276
|
/**
|
|
@@ -252,11 +290,15 @@ export function createRichText<
|
|
|
252
290
|
Catalog,
|
|
253
291
|
L extends string,
|
|
254
292
|
RouteId extends string = string,
|
|
255
|
-
|
|
256
|
-
|
|
293
|
+
Links extends string = never,
|
|
294
|
+
Values extends string = never,
|
|
295
|
+
>(
|
|
296
|
+
options: RichTextOptions<L> & MessageBindings<RouteId, Links, Values>
|
|
297
|
+
): RichTextFor<Catalog, RouteId, Links, Values> {
|
|
298
|
+
const { catalog, locale, link, origin, links, values } = options;
|
|
257
299
|
|
|
258
300
|
return <K extends StringKeys<Catalog>>(
|
|
259
|
-
...args: RichArgsOf<Catalog, K, RouteId>
|
|
301
|
+
...args: RichArgsOf<Catalog, K, RouteId, Links, Values>
|
|
260
302
|
): RichText => {
|
|
261
303
|
// The conditional tuple cannot be destructured directly; the runtime
|
|
262
304
|
// shape is always `[key]` or `[key, params]`. As in `createTranslate`.
|
|
@@ -267,7 +309,7 @@ export function createRichText<
|
|
|
267
309
|
const template = lookupTemplate(catalog, locale, String(key));
|
|
268
310
|
|
|
269
311
|
return parseMarks(template, at).map((span) =>
|
|
270
|
-
resolve(span, at, params, link, origin)
|
|
312
|
+
resolve(span, at, params, { links, values }, link, origin)
|
|
271
313
|
);
|
|
272
314
|
};
|
|
273
315
|
}
|
|
@@ -276,11 +318,15 @@ function resolve(
|
|
|
276
318
|
span: ParsedSpan,
|
|
277
319
|
at: string,
|
|
278
320
|
params: Readonly<Record<string, string>> | undefined,
|
|
321
|
+
bound: {
|
|
322
|
+
readonly links: Readonly<Record<string, string>> | undefined;
|
|
323
|
+
readonly values: Readonly<Record<string, string>> | undefined;
|
|
324
|
+
},
|
|
279
325
|
link: LinkResolver,
|
|
280
326
|
origin: HttpsUrl
|
|
281
327
|
): Span {
|
|
282
328
|
if (span.kind === "break") return span;
|
|
283
|
-
const text = fill(span.text, at, params);
|
|
329
|
+
const text = fill(span.text, at, params, bound.values);
|
|
284
330
|
|
|
285
331
|
switch (span.kind) {
|
|
286
332
|
case "text":
|
|
@@ -291,7 +337,7 @@ function resolve(
|
|
|
291
337
|
case "link":
|
|
292
338
|
return resolveLink(
|
|
293
339
|
text,
|
|
294
|
-
targetFor(span.name, at, params),
|
|
340
|
+
targetFor(span.name, at, params, bound.links),
|
|
295
341
|
at,
|
|
296
342
|
link,
|
|
297
343
|
origin
|
|
@@ -315,17 +361,15 @@ function resolve(
|
|
|
315
361
|
function fill(
|
|
316
362
|
template: string,
|
|
317
363
|
at: string,
|
|
318
|
-
params: Readonly<Record<string, string>> | undefined
|
|
364
|
+
params: Readonly<Record<string, string>> | undefined,
|
|
365
|
+
values: Readonly<Record<string, string>> | undefined
|
|
319
366
|
): string {
|
|
320
367
|
return render(template, at, (name, whole) => {
|
|
321
|
-
|
|
322
|
-
// `createRawTranslate` uses it — see there.
|
|
323
|
-
const value =
|
|
324
|
-
params !== undefined && Object.hasOwn(params, name)
|
|
325
|
-
? params[name]
|
|
326
|
-
: undefined;
|
|
368
|
+
const value = valueFor(name, params, values);
|
|
327
369
|
if (value === undefined) {
|
|
328
|
-
throw new Error(
|
|
370
|
+
throw new Error(
|
|
371
|
+
`${at} is missing a value for "${whole}". Pass it, or bind it: { values: { ${name}: "…" } }.`
|
|
372
|
+
);
|
|
329
373
|
}
|
|
330
374
|
return value;
|
|
331
375
|
});
|
|
@@ -346,15 +390,22 @@ function fill(
|
|
|
346
390
|
function targetFor(
|
|
347
391
|
name: string,
|
|
348
392
|
at: string,
|
|
349
|
-
params: Readonly<Record<string, string>> | undefined
|
|
393
|
+
params: Readonly<Record<string, string>> | undefined,
|
|
394
|
+
links: Readonly<Record<string, string>> | undefined
|
|
350
395
|
): string {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
396
|
+
// The call's own destination first, then one bound for every message.
|
|
397
|
+
const target = (() => {
|
|
398
|
+
if (params !== undefined && Object.hasOwn(params, name)) {
|
|
399
|
+
return params[name];
|
|
400
|
+
}
|
|
401
|
+
if (links !== undefined && Object.hasOwn(links, name)) {
|
|
402
|
+
return links[name];
|
|
403
|
+
}
|
|
404
|
+
return undefined;
|
|
405
|
+
})();
|
|
355
406
|
if (target === undefined) {
|
|
356
407
|
throw new Error(
|
|
357
|
-
`${at} has no destination for the link slot "[a:${name}]". Pass one: rich(key, { ${name}: "some-route-id" }).`
|
|
408
|
+
`${at} has no destination for the link slot "[a:${name}]". Pass one: rich(key, { ${name}: "some-route-id" }), or bind it: site.rich(locale, { ${name}: "some-route-id" }).`
|
|
358
409
|
);
|
|
359
410
|
}
|
|
360
411
|
return target;
|
|
@@ -558,13 +609,19 @@ function collapse(parts: readonly string[]): string {
|
|
|
558
609
|
* `RichArgsOf` is `rich()`'s and `TranslateArgsOf` is this one's: what a caller
|
|
559
610
|
* is asked for follows from what comes out.
|
|
560
611
|
*/
|
|
561
|
-
export function createPlainText<
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
612
|
+
export function createPlainText<
|
|
613
|
+
Catalog,
|
|
614
|
+
L extends string,
|
|
615
|
+
Values extends string = never,
|
|
616
|
+
>(
|
|
617
|
+
options: RichTextOptions<L> & {
|
|
618
|
+
readonly values?: Readonly<Record<Values, string>>;
|
|
619
|
+
}
|
|
620
|
+
): PlainTextFor<Catalog, Values> {
|
|
621
|
+
const { catalog, locale, values } = options;
|
|
565
622
|
|
|
566
623
|
return <K extends StringKeys<Catalog>>(
|
|
567
|
-
...args: TranslateArgsOf<Catalog, K>
|
|
624
|
+
...args: TranslateArgsOf<Catalog, K, Values>
|
|
568
625
|
): string => {
|
|
569
626
|
const [key, params] = args as [K, Readonly<Record<string, string>>?];
|
|
570
627
|
const at = `Message "${String(key)}" (${locale})`;
|
|
@@ -572,7 +629,9 @@ export function createPlainText<Catalog, L extends string>(
|
|
|
572
629
|
|
|
573
630
|
return collapse(
|
|
574
631
|
parseMarks(template, at).map((span) =>
|
|
575
|
-
span.kind === "break"
|
|
632
|
+
span.kind === "break"
|
|
633
|
+
? " "
|
|
634
|
+
: fill(span.text, at, params, values)
|
|
576
635
|
)
|
|
577
636
|
);
|
|
578
637
|
};
|
package/src/i18n/translate.ts
CHANGED
|
@@ -87,6 +87,12 @@ export type RawTranslate = (
|
|
|
87
87
|
export interface RawTranslateOptions<L extends string> {
|
|
88
88
|
readonly catalog: MergedCatalog<L>;
|
|
89
89
|
readonly locale: L;
|
|
90
|
+
/**
|
|
91
|
+
* Values bound once, for every message this `t()` reads: a placeholder
|
|
92
|
+
* named here is filled without the call passing it. Checked by the type
|
|
93
|
+
* that built this; strings by the time they reach the runtime.
|
|
94
|
+
*/
|
|
95
|
+
readonly values?: Readonly<Record<string, string>>;
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
/**
|
|
@@ -129,7 +135,7 @@ export function lookupTemplate<L extends string>(
|
|
|
129
135
|
export function createRawTranslate<L extends string>(
|
|
130
136
|
options: RawTranslateOptions<L>
|
|
131
137
|
): RawTranslate {
|
|
132
|
-
const { catalog, locale } = options;
|
|
138
|
+
const { catalog, locale, values } = options;
|
|
133
139
|
|
|
134
140
|
return (key, params) => {
|
|
135
141
|
const template = lookupTemplate(catalog, locale, key);
|
|
@@ -143,13 +149,10 @@ export function createRawTranslate<L extends string>(
|
|
|
143
149
|
// pattern above, so `params["__proto__"]` finds `Object.prototype`
|
|
144
150
|
// on any plain object, is not `undefined`, and would be substituted
|
|
145
151
|
// into the page as "[object Object]" instead of throwing.
|
|
146
|
-
const value =
|
|
147
|
-
params !== undefined && Object.hasOwn(params, name)
|
|
148
|
-
? params[name]
|
|
149
|
-
: undefined;
|
|
152
|
+
const value = valueFor(name, params, values);
|
|
150
153
|
if (value === undefined) {
|
|
151
154
|
throw new Error(
|
|
152
|
-
`Message "${key}" (${locale}) is missing a value for "${whole}".`
|
|
155
|
+
`Message "${key}" (${locale}) is missing a value for "${whole}". Pass it, or bind it: site.translate(locale, { values: { ${name}: "…" } }).`
|
|
153
156
|
);
|
|
154
157
|
}
|
|
155
158
|
return value;
|
|
@@ -158,6 +161,24 @@ export function createRawTranslate<L extends string>(
|
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A placeholder's value: the call's own first, then one bound for every
|
|
166
|
+
* message. `undefined` when neither has it, for the caller to report.
|
|
167
|
+
*/
|
|
168
|
+
export function valueFor(
|
|
169
|
+
name: string,
|
|
170
|
+
params: Readonly<Record<string, string>> | undefined,
|
|
171
|
+
values: Readonly<Record<string, string>> | undefined
|
|
172
|
+
): string | undefined {
|
|
173
|
+
if (params !== undefined && Object.hasOwn(params, name)) {
|
|
174
|
+
return params[name];
|
|
175
|
+
}
|
|
176
|
+
if (values !== undefined && Object.hasOwn(values, name)) {
|
|
177
|
+
return values[name];
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
161
182
|
/** The `{placeholders}` message `K` needs from the caller. */
|
|
162
183
|
export type MessageParamsOf<
|
|
163
184
|
Catalog,
|
|
@@ -165,19 +186,30 @@ export type MessageParamsOf<
|
|
|
165
186
|
> = EntryPlaceholders<Catalog[K]>;
|
|
166
187
|
|
|
167
188
|
/**
|
|
168
|
-
* `[key]` for a message with
|
|
189
|
+
* `[key]` for a message with nothing left to pass, `[key, params]` otherwise.
|
|
190
|
+
* `Bound` names the placeholders bound when the `t()` was made, which a call
|
|
191
|
+
* no longer passes.
|
|
169
192
|
*
|
|
170
193
|
* The tuple wrappers around `MessageParamsOf` stop the conditional from
|
|
171
194
|
* distributing, so a union of keys is answered once rather than per member.
|
|
172
195
|
*/
|
|
173
|
-
export type TranslateArgsOf<
|
|
174
|
-
|
|
175
|
-
|
|
196
|
+
export type TranslateArgsOf<
|
|
197
|
+
Catalog,
|
|
198
|
+
K extends StringKeys<Catalog>,
|
|
199
|
+
Bound extends string = never,
|
|
200
|
+
> = [Exclude<MessageParamsOf<Catalog, K>, Bound>] extends [never]
|
|
176
201
|
? [key: K]
|
|
177
|
-
: [
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
: [
|
|
203
|
+
key: K,
|
|
204
|
+
params: Readonly<
|
|
205
|
+
Record<Exclude<MessageParamsOf<Catalog, K>, Bound>, string>
|
|
206
|
+
>,
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
export type TranslateFor<Catalog, Bound extends string = never> = <
|
|
210
|
+
K extends StringKeys<Catalog>,
|
|
211
|
+
>(
|
|
212
|
+
...args: TranslateArgsOf<Catalog, K, Bound>
|
|
181
213
|
) => string;
|
|
182
214
|
|
|
183
215
|
/**
|
|
@@ -190,14 +222,19 @@ export type TranslateFor<Catalog> = <K extends StringKeys<Catalog>>(
|
|
|
190
222
|
* catalog, which only needs to be indexable by key and locale.
|
|
191
223
|
*/
|
|
192
224
|
export function createTranslateFactory<Catalog, L extends string>() {
|
|
193
|
-
return function createTranslate(
|
|
225
|
+
return function createTranslate<Bound extends string = never>(
|
|
194
226
|
catalog: MergedCatalog<L>,
|
|
195
|
-
locale: L
|
|
196
|
-
|
|
197
|
-
|
|
227
|
+
locale: L,
|
|
228
|
+
values?: Readonly<Record<Bound, string>>
|
|
229
|
+
): TranslateFor<Catalog, Bound> {
|
|
230
|
+
const raw = createRawTranslate({
|
|
231
|
+
catalog,
|
|
232
|
+
locale,
|
|
233
|
+
...(values === undefined ? {} : { values }),
|
|
234
|
+
});
|
|
198
235
|
|
|
199
236
|
function t<K extends StringKeys<Catalog>>(
|
|
200
|
-
...args: TranslateArgsOf<Catalog, K>
|
|
237
|
+
...args: TranslateArgsOf<Catalog, K, Bound>
|
|
201
238
|
): string {
|
|
202
239
|
// The conditional tuple cannot be destructured directly; the runtime
|
|
203
240
|
// shape is always `[key]` or `[key, params]`.
|
package/src/index.ts
CHANGED
package/src/site/api.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { ResolvedLocaleMeta } from "../config.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
MessageBindings,
|
|
4
|
+
PlainTextFor,
|
|
5
|
+
RichTextFor,
|
|
6
|
+
} from "../content/index.ts";
|
|
3
7
|
import type { GeneratedFile } from "../file.ts";
|
|
4
8
|
import type { PublicFilePath } from "../files.ts";
|
|
5
9
|
import type { Fonts } from "../fonts.ts";
|
|
@@ -160,8 +164,14 @@ export interface Site<
|
|
|
160
164
|
*/
|
|
161
165
|
readonly routes: readonly RouteId[];
|
|
162
166
|
|
|
163
|
-
/**
|
|
164
|
-
|
|
167
|
+
/**
|
|
168
|
+
* A locale-bound `t()`. `bind.values` fills those placeholders for every
|
|
169
|
+
* message, as `rich()`'s does; the rest are still the call's to pass.
|
|
170
|
+
*/
|
|
171
|
+
translate<const Values extends string = never>(
|
|
172
|
+
locale: L,
|
|
173
|
+
bind?: { readonly values?: Readonly<Record<Values, string>> }
|
|
174
|
+
): TranslateFor<Catalog, Values>;
|
|
165
175
|
|
|
166
176
|
/**
|
|
167
177
|
* A locale-bound `rich()`: the same catalog, read as styled runs.
|
|
@@ -174,8 +184,21 @@ export interface Site<
|
|
|
174
184
|
* Bound to a locale here rather than taking one per call, because a link
|
|
175
185
|
* inside the copy resolves to *this* language's path, and a `rich()` that
|
|
176
186
|
* took the locale late would be a second place to get that wrong.
|
|
187
|
+
*
|
|
188
|
+
* `bind` fixes link destinations and placeholder values once, for every
|
|
189
|
+
* message read through it — for a view that renders many messages through
|
|
190
|
+
* one call: `{ links: { contact: "contact" }, values: { hours } }` lets any
|
|
191
|
+
* of them write `[a:contact]` or `{hours}` with nothing passed per call.
|
|
192
|
+
* Bound targets are checked here, as a call's would be; whatever is left
|
|
193
|
+
* unbound is still required at the call, and a compile error without it.
|
|
177
194
|
*/
|
|
178
|
-
rich
|
|
195
|
+
rich<
|
|
196
|
+
const Links extends string = never,
|
|
197
|
+
const Values extends string = never,
|
|
198
|
+
>(
|
|
199
|
+
locale: L,
|
|
200
|
+
bind?: MessageBindings<RouteId, Links, Values>
|
|
201
|
+
): RichTextFor<Catalog, RouteId, Links, Values>;
|
|
179
202
|
|
|
180
203
|
/**
|
|
181
204
|
* A locale-bound `plain()`: the same catalog, read as words alone.
|
|
@@ -191,7 +214,10 @@ export interface Site<
|
|
|
191
214
|
* does not break the day someone emphasises a clause in the sentence it
|
|
192
215
|
* quotes.
|
|
193
216
|
*/
|
|
194
|
-
plain
|
|
217
|
+
plain<const Values extends string = never>(
|
|
218
|
+
locale: L,
|
|
219
|
+
bind?: { readonly values?: Readonly<Record<Values, string>> }
|
|
220
|
+
): PlainTextFor<Catalog, Values>;
|
|
195
221
|
|
|
196
222
|
/** Absolute path of a route in one locale, e.g. `/el/sxetika-me-emas`. */
|
|
197
223
|
pathFor(id: RouteId, locale: L, options?: LinkOptions): UrlPath;
|
package/src/site/create.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createPlainText,
|
|
10
10
|
createRichText,
|
|
11
11
|
type LinkResolver,
|
|
12
|
+
type MessageBindings,
|
|
12
13
|
} from "../content/index.ts";
|
|
13
14
|
import type { GeneratedFile } from "../file.ts";
|
|
14
15
|
import type { PublicFilePath } from "../files.ts";
|
|
@@ -836,20 +837,34 @@ export function createSite<
|
|
|
836
837
|
fonts,
|
|
837
838
|
routes: enabledRouteIds,
|
|
838
839
|
|
|
839
|
-
translate:
|
|
840
|
-
|
|
841
|
-
|
|
840
|
+
translate: <const Values extends string = never>(
|
|
841
|
+
locale: L,
|
|
842
|
+
bind?: { readonly values?: Readonly<Record<Values, string>> }
|
|
843
|
+
) => translateFactory<Values>(catalog, locale, bind?.values),
|
|
844
|
+
rich: <
|
|
845
|
+
const Links extends string = never,
|
|
846
|
+
const Values extends string = never,
|
|
847
|
+
>(
|
|
848
|
+
locale: L,
|
|
849
|
+
bind?: MessageBindings<RouteId, Links, Values>
|
|
850
|
+
) =>
|
|
851
|
+
createRichText<Full, L, RouteId, Links, Values>({
|
|
842
852
|
catalog,
|
|
843
853
|
locale,
|
|
844
854
|
link: richLink(locale),
|
|
845
855
|
origin: siteUrl,
|
|
856
|
+
...bind,
|
|
846
857
|
}),
|
|
847
|
-
plain: (
|
|
848
|
-
|
|
858
|
+
plain: <const Values extends string = never>(
|
|
859
|
+
locale: L,
|
|
860
|
+
bind?: { readonly values?: Readonly<Record<Values, string>> }
|
|
861
|
+
) =>
|
|
862
|
+
createPlainText<Full, L, Values>({
|
|
849
863
|
catalog,
|
|
850
864
|
locale,
|
|
851
865
|
link: richLink(locale),
|
|
852
866
|
origin: siteUrl,
|
|
867
|
+
...bind,
|
|
853
868
|
}),
|
|
854
869
|
|
|
855
870
|
pathFor,
|