@escape-game-over/atlas 0.1.83 → 0.1.85
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 +4 -0
- package/package.json +5 -5
- package/src/i18n/per-language.ts +45 -0
- package/src/jsonld/organization.ts +17 -0
- package/src/llms.ts +15 -6
- package/src/site/create.ts +12 -0
- package/src/site/define.ts +12 -0
package/README.md
CHANGED
|
@@ -109,6 +109,10 @@ export const { project, redirects } = atlas.project(baseMessages, baseRoutes, {.
|
|
|
109
109
|
export const site = atlas.site(baseMessages, baseRoutes, project)
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
A site that publishes one language under several regions writes the words once:
|
|
113
|
+
`atlas.perLanguage({ en: "Home", fr: "Accueil" })` fills in `en-GB`, `en-IE` and
|
|
114
|
+
`fr-FR` for one entry of `atlas.messages`, with placeholders still checked.
|
|
115
|
+
|
|
112
116
|
Step 4 returns `t()`, `rich()`, `plain()`, `pathFor()`, `urlFor()`, `fileUrl()`,
|
|
113
117
|
`localeLinksFor()`, `metaFor()`, `breadcrumbFor()`,
|
|
114
118
|
`getStaticPaths()`, `routes`, `sitemap()`, `robots()` and `llms()` already
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escape-game-over/atlas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.85",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
|
|
6
6
|
"private": false,
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"vite": ">= 8"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@biomejs/biome": "2.6.
|
|
61
|
+
"@biomejs/biome": "2.6.5",
|
|
62
62
|
"@types/node": "26.6.6",
|
|
63
|
-
"@vitest/coverage-istanbul": "5.0
|
|
64
|
-
"astro": "7.4.
|
|
63
|
+
"@vitest/coverage-istanbul": "5.1.0",
|
|
64
|
+
"astro": "7.4.5",
|
|
65
65
|
"typescript": "6.0.3",
|
|
66
|
-
"vitest": "5.0
|
|
66
|
+
"vitest": "5.1.0"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** `"en"` for `"en-GB"`: everything before the first dash. */
|
|
2
|
+
export type LanguageOf<L extends string> =
|
|
3
|
+
L extends `${infer Language}-${string}` ? Language : never;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Every locale, holding its language's text.
|
|
7
|
+
*
|
|
8
|
+
* Keeps each string's literal type, so a message built this way has its
|
|
9
|
+
* placeholders checked exactly like one written out by hand.
|
|
10
|
+
*/
|
|
11
|
+
export type PerLanguage<L extends string, T> = {
|
|
12
|
+
readonly [K in L]: T[LanguageOf<K> & keyof T];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One message, written once per language and filled in for each of its tags.
|
|
17
|
+
*
|
|
18
|
+
* For a site that publishes one language under several regions, where `en-GB`
|
|
19
|
+
* and `en-IE` are different tags for `hreflang` but the same words. Checked here
|
|
20
|
+
* as well as in the types, because the cast below stops the compiler seeing
|
|
21
|
+
* this body, and a caller's `as any` would otherwise publish `undefined`.
|
|
22
|
+
*/
|
|
23
|
+
export function expandLanguages<L extends string, T>(
|
|
24
|
+
locales: readonly L[],
|
|
25
|
+
texts: T
|
|
26
|
+
): PerLanguage<L, T> {
|
|
27
|
+
const given = texts as Readonly<Record<string, string>>;
|
|
28
|
+
const languages = new Set(locales.map((tag) => tag.split("-")[0]));
|
|
29
|
+
for (const language of Object.keys(given)) {
|
|
30
|
+
if (!languages.has(language)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`perLanguage: "${language}" is not a language this site declares.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const entries = locales.map((tag) => {
|
|
37
|
+
const language = tag.split("-")[0] as string;
|
|
38
|
+
const text = given[language];
|
|
39
|
+
if (text === undefined) {
|
|
40
|
+
throw new Error(`perLanguage: no text for "${language}" (${tag}).`);
|
|
41
|
+
}
|
|
42
|
+
return [tag, text];
|
|
43
|
+
});
|
|
44
|
+
return Object.fromEntries(entries) as PerLanguage<L, T>;
|
|
45
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type PostalAddress,
|
|
6
6
|
} from "../contact.ts";
|
|
7
7
|
import type { ImageAsset } from "../image.ts";
|
|
8
|
+
import type { IsoDate } from "../types.ts";
|
|
8
9
|
import { absoluteUrl, type HttpsUrl, joinUrl, type UrlPath } from "../url.ts";
|
|
9
10
|
import { organizationId } from "./ids.ts";
|
|
10
11
|
import { alternateName, type JsonLdNode } from "./node.ts";
|
|
@@ -15,6 +16,16 @@ export interface OrganizationInput {
|
|
|
15
16
|
readonly name: string;
|
|
16
17
|
/** The brand's own home. Also its identity, see `@id` below. */
|
|
17
18
|
readonly url: HttpsUrl;
|
|
19
|
+
/**
|
|
20
|
+
* What the brand is, in a sentence. Google lists it as a recommended
|
|
21
|
+
* property of an organization.
|
|
22
|
+
*/
|
|
23
|
+
readonly description?: string;
|
|
24
|
+
/**
|
|
25
|
+
* When the brand was founded: a full date, or just the year where that is
|
|
26
|
+
* all anyone can honestly state. Both are valid ISO 8601.
|
|
27
|
+
*/
|
|
28
|
+
readonly foundingDate?: IsoDate | `${number}`;
|
|
18
29
|
/**
|
|
19
30
|
* Another name the brand actually goes by — an acronym, a short form.
|
|
20
31
|
*
|
|
@@ -162,6 +173,12 @@ export function organization(brand: OrganizationInput): JsonLdNode {
|
|
|
162
173
|
name: brand.name,
|
|
163
174
|
url: brand.url,
|
|
164
175
|
...alternateName(brand.name, brand.alternateName),
|
|
176
|
+
...(brand.description === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { description: brand.description }),
|
|
179
|
+
...(brand.foundingDate === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { foundingDate: brand.foundingDate }),
|
|
165
182
|
...(brand.logo === undefined
|
|
166
183
|
? {}
|
|
167
184
|
: { logo: joinUrl(brand.url, brand.logo) }),
|
package/src/llms.ts
CHANGED
|
@@ -68,6 +68,11 @@ export interface LlmsInput {
|
|
|
68
68
|
readonly tag: string;
|
|
69
69
|
readonly url: HttpsUrl;
|
|
70
70
|
}[];
|
|
71
|
+
/**
|
|
72
|
+
* Whether any page's slug differs between languages. Decides what the file
|
|
73
|
+
* tells a reader about reaching the same page in another one.
|
|
74
|
+
*/
|
|
75
|
+
readonly translatedSlugs?: boolean;
|
|
71
76
|
readonly sections: readonly LlmsSection[];
|
|
72
77
|
}
|
|
73
78
|
|
|
@@ -104,14 +109,18 @@ export function buildLlms(input: LlmsInput): GeneratedFile {
|
|
|
104
109
|
// Which language the links are in, and how to get to the others — stated
|
|
105
110
|
// only when there *are* others.
|
|
106
111
|
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
+
// Whether "swap the prefix" works depends on the site, so it is stated only
|
|
113
|
+
// where it holds. Where a slug is translated, the Greek `/about-us` is
|
|
114
|
+
// `/el-GR/sxetika-me-emas` and a reader told to substitute the prefix would
|
|
115
|
+
// construct a URL that 404s; the sitemap is where that mapping lives, one
|
|
116
|
+
// `xhtml:link` per page per language.
|
|
112
117
|
if (input.otherLanguages !== undefined && input.otherLanguages.length > 0) {
|
|
113
118
|
lines.push(
|
|
114
|
-
`Every URL below is the ${input.language.tag} version.
|
|
119
|
+
`Every URL below is the ${input.language.tag} version. ${
|
|
120
|
+
input.translatedSlugs === true
|
|
121
|
+
? "Other languages might translate their slugs, so the same page is not always this URL with a different prefix —"
|
|
122
|
+
: "Other languages use the same path under their own address above —"
|
|
123
|
+
} the sitemap lists each page in every language.`,
|
|
115
124
|
""
|
|
116
125
|
);
|
|
117
126
|
}
|
package/src/site/create.ts
CHANGED
|
@@ -761,6 +761,18 @@ export function createSite<
|
|
|
761
761
|
tag: localeMeta[other].htmlLang,
|
|
762
762
|
url: joinUrl(siteUrl, buildPath("", other, pathContext)),
|
|
763
763
|
})),
|
|
764
|
+
// Read off the routes, so the file cannot claim something the URLs
|
|
765
|
+
// do not do.
|
|
766
|
+
translatedSlugs: enabledRouteIds.some((id) => {
|
|
767
|
+
const route = resolved[id];
|
|
768
|
+
return (
|
|
769
|
+
route !== undefined &&
|
|
770
|
+
others.some(
|
|
771
|
+
(other) =>
|
|
772
|
+
slugFor(route, other) !== slugFor(route, locale)
|
|
773
|
+
)
|
|
774
|
+
);
|
|
775
|
+
}),
|
|
764
776
|
sections,
|
|
765
777
|
});
|
|
766
778
|
}
|
package/src/site/define.ts
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
type NoExtraLocales,
|
|
11
11
|
type ValidateBase,
|
|
12
12
|
} from "../i18n/define.ts";
|
|
13
|
+
import {
|
|
14
|
+
expandLanguages,
|
|
15
|
+
type LanguageOf,
|
|
16
|
+
type PerLanguage,
|
|
17
|
+
} from "../i18n/per-language.ts";
|
|
13
18
|
import {
|
|
14
19
|
bindRedirects,
|
|
15
20
|
type DefinedProject,
|
|
@@ -86,6 +91,13 @@ export function defineSite<
|
|
|
86
91
|
return defineMessages(config, catalog as never) as T;
|
|
87
92
|
},
|
|
88
93
|
|
|
94
|
+
/** One message per language, filled in for each of that language's tags. */
|
|
95
|
+
perLanguage<const T extends Readonly<Record<LanguageOf<L>, string>>>(
|
|
96
|
+
texts: T & Readonly<Record<Exclude<keyof T, LanguageOf<L>>, never>>
|
|
97
|
+
): PerLanguage<L, T> {
|
|
98
|
+
return expandLanguages(Object.keys(config.locales) as L[], texts);
|
|
99
|
+
},
|
|
100
|
+
|
|
89
101
|
/** One deployment: what differs from the defaults. */
|
|
90
102
|
project<
|
|
91
103
|
const Catalog extends BaseCatalog<L>,
|