@escape-game-over/atlas 0.1.5 → 0.1.7
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 +52 -3
- package/docs/NOT-BUILT.md +67 -0
- package/docs/rich-text.md +261 -0
- package/package.json +4 -3
- package/src/analytics/google.ts +45 -7
- package/src/analytics/index.ts +38 -1
- package/src/analytics/tags.ts +27 -0
- package/src/analytics/umami.ts +5 -0
- package/src/contact.ts +84 -3
- package/src/content/index.ts +40 -0
- package/src/content/marks.ts +413 -0
- package/src/content/rich.ts +550 -0
- package/src/i18n/define.ts +11 -10
- package/src/i18n/placeholders.ts +37 -10
- package/src/i18n/translate.ts +34 -11
- package/src/index.ts +21 -0
- package/src/meta/tag.ts +9 -0
- package/src/site/api.ts +31 -0
- package/src/site/create.ts +49 -0
- package/src/url.ts +20 -0
package/src/i18n/translate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertNoMarks } from "../content/marks.ts";
|
|
1
2
|
import type { StringKeys } from "../types.ts";
|
|
2
3
|
import type { EntryPlaceholders } from "./placeholders.ts";
|
|
3
4
|
|
|
@@ -32,7 +33,7 @@ const NAME = /^[a-zA-Z0-9_]+$/;
|
|
|
32
33
|
* unmatched closer to be ambiguous about, and rejecting it would fail copy that
|
|
33
34
|
* is merely writing a bracket.
|
|
34
35
|
*/
|
|
35
|
-
function render(
|
|
36
|
+
export function render(
|
|
36
37
|
template: string,
|
|
37
38
|
at: string,
|
|
38
39
|
value: (name: string, whole: string) => string
|
|
@@ -88,12 +89,42 @@ export interface RawTranslateOptions<L extends string> {
|
|
|
88
89
|
readonly locale: L;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* One message's raw copy, before anything is substituted or parsed.
|
|
94
|
+
*
|
|
95
|
+
* Shared with `content/rich.ts`, which needs the template *unrendered* — marks
|
|
96
|
+
* are parsed out of it first and placeholders filled per run afterwards, so it
|
|
97
|
+
* cannot go through `t()`. Both failures are stated once here rather than in
|
|
98
|
+
* each caller, which is what keeps the two error messages identical.
|
|
99
|
+
*/
|
|
100
|
+
export function lookupTemplate<L extends string>(
|
|
101
|
+
catalog: MergedCatalog<L>,
|
|
102
|
+
locale: L,
|
|
103
|
+
key: string
|
|
104
|
+
): string {
|
|
105
|
+
const entry = catalog[key];
|
|
106
|
+
if (entry === undefined) {
|
|
107
|
+
throw new Error(`Unknown message key "${key}".`);
|
|
108
|
+
}
|
|
109
|
+
const template = entry[locale];
|
|
110
|
+
if (typeof template !== "string") {
|
|
111
|
+
throw new Error(`Message "${key}" has no copy for locale "${locale}".`);
|
|
112
|
+
}
|
|
113
|
+
return template;
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
/**
|
|
92
117
|
* Builds the runtime half of `t()`.
|
|
93
118
|
*
|
|
94
119
|
* Every failure here is a build-time failure by design: a static site should not
|
|
95
120
|
* ship a page with a literal `{name}` in the markup, so a missing message or an
|
|
96
121
|
* unfilled placeholder throws rather than degrading.
|
|
122
|
+
*
|
|
123
|
+
* That rule is why a message carrying `[marks]` is refused here too. `t()`
|
|
124
|
+
* returns a string, so the only thing it could do with them is print them, and
|
|
125
|
+
* `[v:accent]Game Over[/v]` rendered into a `<title>` is the literal-brace
|
|
126
|
+
* failure wearing different brackets. `rich()` reads those, and `plain()` gives
|
|
127
|
+
* back the sentence without them.
|
|
97
128
|
*/
|
|
98
129
|
export function createRawTranslate<L extends string>(
|
|
99
130
|
options: RawTranslateOptions<L>
|
|
@@ -101,16 +132,8 @@ export function createRawTranslate<L extends string>(
|
|
|
101
132
|
const { catalog, locale } = options;
|
|
102
133
|
|
|
103
134
|
return (key, params) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
throw new Error(`Unknown message key "${key}".`);
|
|
107
|
-
}
|
|
108
|
-
const template = entry[locale];
|
|
109
|
-
if (typeof template !== "string") {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`Message "${key}" has no copy for locale "${locale}".`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
135
|
+
const template = lookupTemplate(catalog, locale, key);
|
|
136
|
+
assertNoMarks(template, `Message "${key}" (${locale})`);
|
|
114
137
|
return render(
|
|
115
138
|
template,
|
|
116
139
|
`Message "${key}" (${locale})`,
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export {
|
|
|
26
26
|
CONSENT_UPDATE_GLOBAL,
|
|
27
27
|
type ConsentDefaults,
|
|
28
28
|
type ConsentState,
|
|
29
|
+
consentRequired,
|
|
29
30
|
type GoogleSettings,
|
|
30
31
|
type UmamiReplay,
|
|
31
32
|
type UmamiSettings,
|
|
@@ -45,12 +46,17 @@ export {
|
|
|
45
46
|
type CallingCode,
|
|
46
47
|
type Coordinates,
|
|
47
48
|
type CountryCode,
|
|
49
|
+
type E164,
|
|
48
50
|
type EmailAddress,
|
|
49
51
|
e164,
|
|
52
|
+
e164Of,
|
|
50
53
|
formatPhone,
|
|
54
|
+
isEmailAddress,
|
|
55
|
+
type MailtoUrl,
|
|
51
56
|
mailtoHref,
|
|
52
57
|
type PhoneNumber,
|
|
53
58
|
type PostalAddress,
|
|
59
|
+
type TelUrl,
|
|
54
60
|
telHref,
|
|
55
61
|
} from "./contact.ts";
|
|
56
62
|
export {
|
|
@@ -60,6 +66,19 @@ export {
|
|
|
60
66
|
type MailEndpoint,
|
|
61
67
|
sendContactMessage,
|
|
62
68
|
} from "./contact-form.ts";
|
|
69
|
+
// `parseMarks`, `assertNoMarks` and `createRichText` are deliberately not here.
|
|
70
|
+
// They are how lib gets from a message to a `Span[]`, and a consumer holding
|
|
71
|
+
// them has a second `rich()` that is not bound to the site's route table — so
|
|
72
|
+
// a slot filled with a route id resolves against nothing there. `site.rich(locale)` is
|
|
73
|
+
// the only way in, and `Span` is what a renderer switches on.
|
|
74
|
+
export {
|
|
75
|
+
type LinkTarget,
|
|
76
|
+
type PlainTextFor,
|
|
77
|
+
plain,
|
|
78
|
+
type RichText,
|
|
79
|
+
type RichTextFor,
|
|
80
|
+
type Span,
|
|
81
|
+
} from "./content/index.ts";
|
|
63
82
|
export {
|
|
64
83
|
COUNTRY_CODES,
|
|
65
84
|
countriesFor,
|
|
@@ -227,7 +246,9 @@ export type { Sitemap, SitemapConfig } from "./sitemap.ts";
|
|
|
227
246
|
export type { IsoDate, Letter, StringKeys } from "./types.ts";
|
|
228
247
|
export {
|
|
229
248
|
absoluteUrl,
|
|
249
|
+
type Hash,
|
|
230
250
|
type HttpsUrl,
|
|
251
|
+
isHash,
|
|
231
252
|
isHttpsUrl,
|
|
232
253
|
isUrlPath,
|
|
233
254
|
joinUrl,
|
package/src/meta/tag.ts
CHANGED
|
@@ -92,6 +92,15 @@ export function asMetaTag(script: AnalyticsTag): MetaTag {
|
|
|
92
92
|
if (script.kind === "noscriptFrame") {
|
|
93
93
|
return { kind: "noscriptFrame", src: script.src };
|
|
94
94
|
}
|
|
95
|
+
if (script.kind === "preconnect") {
|
|
96
|
+
// A `link` rather than a kind of its own: the head vocabulary already
|
|
97
|
+
// has the shape, and a preconnect is a link in every sense the renderer
|
|
98
|
+
// cares about. See `AnalyticsTag` for why no `crossorigin`.
|
|
99
|
+
return {
|
|
100
|
+
kind: "link",
|
|
101
|
+
attrs: { rel: "preconnect", href: script.origin },
|
|
102
|
+
};
|
|
103
|
+
}
|
|
95
104
|
const unhandled: never = script;
|
|
96
105
|
throw new Error(`Unhandled analytics tag: ${JSON.stringify(unhandled)}`);
|
|
97
106
|
}
|
package/src/site/api.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedLocaleMeta } from "../config.ts";
|
|
2
|
+
import type { PlainTextFor, RichTextFor } from "../content/index.ts";
|
|
2
3
|
import type { GeneratedFile } from "../file.ts";
|
|
3
4
|
import type { PublicFilePath } from "../files.ts";
|
|
4
5
|
import type { TranslateFor } from "../i18n/translate.ts";
|
|
@@ -161,6 +162,36 @@ export interface Site<
|
|
|
161
162
|
/** A locale-bound `t()`. */
|
|
162
163
|
translate(locale: L): TranslateFor<Catalog>;
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* A locale-bound `rich()`: the same catalog, read as styled runs.
|
|
167
|
+
*
|
|
168
|
+
* For copy that is one sentence with a bold clause, an accent colour or a
|
|
169
|
+
* link in the middle of it. The marks live in the message so every language
|
|
170
|
+
* can move them — see `content/marks.ts`, which is mostly about why they
|
|
171
|
+
* cannot live in config instead.
|
|
172
|
+
*
|
|
173
|
+
* Bound to a locale here rather than taking one per call, because a link
|
|
174
|
+
* inside the copy resolves to *this* language's path, and a `rich()` that
|
|
175
|
+
* took the locale late would be a second place to get that wrong.
|
|
176
|
+
*/
|
|
177
|
+
rich(locale: L): RichTextFor<Catalog, RouteId>;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A locale-bound `plain()`: the same catalog, read as words alone.
|
|
181
|
+
*
|
|
182
|
+
* For everywhere a string is what fits and markup is not — `llms()`'s
|
|
183
|
+
* `describe`, a page description, a structured-data `description`. Marks are
|
|
184
|
+
* dropped, a `[br]` becomes a space, and the result is the sentence a reader
|
|
185
|
+
* would hear.
|
|
186
|
+
*
|
|
187
|
+
* The reason it exists next to `translate` rather than being folded into it:
|
|
188
|
+
* `t()` refuses a message carrying marks, because the only thing it could do
|
|
189
|
+
* with them is print them. This is the reading that copes, so a description
|
|
190
|
+
* does not break the day someone emphasises a clause in the sentence it
|
|
191
|
+
* quotes.
|
|
192
|
+
*/
|
|
193
|
+
plain(locale: L): PlainTextFor<Catalog>;
|
|
194
|
+
|
|
164
195
|
/** Absolute path of a route in one locale, e.g. `/el/sxetika-me-emas`. */
|
|
165
196
|
pathFor(id: RouteId, locale: L, options?: LinkOptions): UrlPath;
|
|
166
197
|
/** The same, prefixed with the site origin — for canonicals and sitemaps. */
|
package/src/site/create.ts
CHANGED
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
mergeLocaleMeta,
|
|
5
5
|
type SiteConfigShape,
|
|
6
6
|
} from "../config.ts";
|
|
7
|
+
import {
|
|
8
|
+
createPlainText,
|
|
9
|
+
createRichText,
|
|
10
|
+
type LinkResolver,
|
|
11
|
+
} from "../content/index.ts";
|
|
7
12
|
import type { GeneratedFile } from "../file.ts";
|
|
8
13
|
import type { PublicFilePath } from "../files.ts";
|
|
9
14
|
import { type BaseCatalog, mergeCatalog } from "../i18n/define.ts";
|
|
@@ -729,6 +734,36 @@ export function createSite<
|
|
|
729
734
|
});
|
|
730
735
|
}
|
|
731
736
|
|
|
737
|
+
/**
|
|
738
|
+
* How a slot filled with a route id becomes this locale's path.
|
|
739
|
+
*
|
|
740
|
+
* `pathFor` already refuses an unknown or disabled route, and its message is
|
|
741
|
+
* right for a call site that named the id — but a mark comes from a
|
|
742
|
+
* *translation*, so the useful part is which message to open. That is what
|
|
743
|
+
* this adds; the original is kept as the cause rather than restated, since
|
|
744
|
+
* "disabled for this project" and "unknown" are different fixes.
|
|
745
|
+
*/
|
|
746
|
+
function richLink(locale: L): LinkResolver {
|
|
747
|
+
return (routeId, at, hash) => {
|
|
748
|
+
try {
|
|
749
|
+
// Through `pathFor` rather than concatenated on afterwards, so
|
|
750
|
+
// a fragment lands after the page segment and the query — the
|
|
751
|
+
// one order that is right — and so `#` is added in exactly one
|
|
752
|
+
// place in this package.
|
|
753
|
+
return pathFor(
|
|
754
|
+
routeId as RouteId,
|
|
755
|
+
locale,
|
|
756
|
+
hash === undefined ? undefined : { hash }
|
|
757
|
+
);
|
|
758
|
+
} catch (cause) {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`${at} links to "${routeId}", which is not a page this project builds. A link target is a route id — optionally with a fragment, as "about#team" — a "/path", an https:// URL, or a "#fragment" on the page it is rendered on.`,
|
|
761
|
+
{ cause }
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
732
767
|
return {
|
|
733
768
|
locales,
|
|
734
769
|
defaultLocale,
|
|
@@ -740,6 +775,20 @@ export function createSite<
|
|
|
740
775
|
routes: enabledRouteIds,
|
|
741
776
|
|
|
742
777
|
translate: (locale) => translateFactory(catalog, locale),
|
|
778
|
+
rich: (locale) =>
|
|
779
|
+
createRichText<Catalog, L, RouteId>({
|
|
780
|
+
catalog,
|
|
781
|
+
locale,
|
|
782
|
+
link: richLink(locale),
|
|
783
|
+
origin: siteUrl,
|
|
784
|
+
}),
|
|
785
|
+
plain: (locale) =>
|
|
786
|
+
createPlainText<Catalog, L>({
|
|
787
|
+
catalog,
|
|
788
|
+
locale,
|
|
789
|
+
link: richLink(locale),
|
|
790
|
+
origin: siteUrl,
|
|
791
|
+
}),
|
|
743
792
|
|
|
744
793
|
pathFor,
|
|
745
794
|
urlFor,
|
package/src/url.ts
CHANGED
|
@@ -46,6 +46,26 @@ export function isUrlPath(path: string): path is UrlPath {
|
|
|
46
46
|
return path.startsWith("/");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A fragment on whatever page is being rendered: `#booking`, `#Scenarios`.
|
|
51
|
+
*
|
|
52
|
+
* The one link target lib cannot resolve and does not need to. A route id
|
|
53
|
+
* becomes a different path in every language and an `https://` URL points
|
|
54
|
+
* somewhere else entirely; a fragment names an element of the document it is
|
|
55
|
+
* written in, so it is already the whole answer and passes through untouched.
|
|
56
|
+
*
|
|
57
|
+
* Typed as its own shape rather than folded into `UrlPath` because they are not
|
|
58
|
+
* interchangeable: `/booking` navigates and `#booking` scrolls, and a leading
|
|
59
|
+
* character is the only thing that distinguishes them. See `LinkTarget`, which
|
|
60
|
+
* tells the four apart by exactly that.
|
|
61
|
+
*/
|
|
62
|
+
export type Hash = `#${string}`;
|
|
63
|
+
|
|
64
|
+
/** The runtime half of `Hash`. See `isHttpsUrl`. */
|
|
65
|
+
export function isHash(value: string): value is Hash {
|
|
66
|
+
return value.startsWith("#");
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
/**
|
|
50
70
|
* An origin without its trailing slash.
|
|
51
71
|
*
|