@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 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 | compile error, then a build error naming the slot |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
@@ -32,6 +32,7 @@ export {
32
32
  html,
33
33
  type LinkResolver,
34
34
  type LinkTarget,
35
+ type MessageBindings,
35
36
  type PlainTextFor,
36
37
  plain,
37
38
  type RichText,
@@ -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
- > = Readonly<Record<MessageParamsOf<Catalog, K>, string>> &
204
- Readonly<Record<LinkNamesOf<Catalog, K>, LinkTarget<RouteId>>>;
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
- > = [MessageParamsOf<Catalog, K> | LinkNamesOf<Catalog, K>] extends [never]
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<Catalog, RouteId extends string> = <
228
- K extends StringKeys<Catalog>,
229
- >(
230
- ...args: RichArgsOf<Catalog, K, RouteId>
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> = <K extends StringKeys<Catalog>>(
235
- ...args: TranslateArgsOf<Catalog, K>
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
- >(options: RichTextOptions<L>): RichTextFor<Catalog, RouteId> {
256
- const { catalog, locale, link, origin } = options;
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
- // `Object.hasOwn` rather than reading the index straight, for the reason
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(`${at} is missing a value for "${whole}".`);
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
- const target =
352
- params !== undefined && Object.hasOwn(params, name)
353
- ? params[name]
354
- : undefined;
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<Catalog, L extends string>(
562
- options: RichTextOptions<L>
563
- ): PlainTextFor<Catalog> {
564
- const { catalog, locale } = options;
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" ? " " : fill(span.text, at, params)
632
+ span.kind === "break"
633
+ ? " "
634
+ : fill(span.text, at, params, values)
576
635
  )
577
636
  );
578
637
  };
@@ -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 no placeholders, `[key, params]` otherwise.
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<Catalog, K extends StringKeys<Catalog>> = [
174
- MessageParamsOf<Catalog, K>,
175
- ] extends [never]
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
- : [key: K, params: Readonly<Record<MessageParamsOf<Catalog, K>, string>>];
178
-
179
- export type TranslateFor<Catalog> = <K extends StringKeys<Catalog>>(
180
- ...args: TranslateArgsOf<Catalog, K>
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
- ): TranslateFor<Catalog> {
197
- const raw = createRawTranslate({ catalog, locale });
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
@@ -100,6 +100,7 @@ export {
100
100
  export {
101
101
  html,
102
102
  type LinkTarget,
103
+ type MessageBindings,
103
104
  type PlainTextFor,
104
105
  plain,
105
106
  type RichText,
package/src/site/api.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import type { ResolvedLocaleMeta } from "../config.ts";
2
- import type { PlainTextFor, RichTextFor } from "../content/index.ts";
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
- /** A locale-bound `t()`. */
164
- translate(locale: L): TranslateFor<Catalog>;
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(locale: L): RichTextFor<Catalog, RouteId>;
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(locale: L): PlainTextFor<Catalog>;
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;
@@ -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: (locale) => translateFactory(catalog, locale),
840
- rich: (locale) =>
841
- createRichText<Full, L, RouteId>({
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: (locale) =>
848
- createPlainText<Full, L>({
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,