@escape-game-over/atlas 0.1.110 → 0.1.111
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 +6 -2
- package/docs/checks.md +2 -1
- package/package.json +1 -1
- package/src/astro/axon-event.ts +1 -1
- package/src/astro/google-event.ts +1 -1
- package/src/astro/meta-event.ts +1 -1
- package/src/astro/snap-event.ts +1 -1
- package/src/astro/tiktok-event.ts +1 -1
- package/src/config.ts +27 -8
- package/src/currency.ts +165 -0
- package/src/index.ts +11 -7
- package/src/money.ts +1 -185
- package/src/price-format.ts +49 -0
- package/src/project.ts +92 -33
- package/src/site/api.ts +14 -0
- package/src/site/create.ts +24 -4
package/README.md
CHANGED
|
@@ -72,7 +72,10 @@ export const { project, redirects } = atlas.project(
|
|
|
72
72
|
|
|
73
73
|
enabledLocales: ["el-GR", "en-US"],
|
|
74
74
|
overrideRouting: { defaultLocale: "el-GR", prefixDefaultLocale: true },
|
|
75
|
-
|
|
75
|
+
formats: {
|
|
76
|
+
clock: { "el-GR": "h23", "en-US": "h12" }, // how each language prints times
|
|
77
|
+
prices: { decimals: "always" }, // "18,00 €", each page in its own locale
|
|
78
|
+
},
|
|
76
79
|
|
|
77
80
|
overrideMessages: {
|
|
78
81
|
// Greek only — the English copy still falls through to the default catalog.
|
|
@@ -244,8 +247,9 @@ regression-tested by an `@ts-expect-error` suite.
|
|
|
244
247
|
| A `robots` directive that is not a real one | error — the vocabulary is a union |
|
|
245
248
|
| A project `url` that is not `https://`, or ends in a slash | error |
|
|
246
249
|
| `defaultLocale` not among the project's `enabledLocales` | error |
|
|
247
|
-
| A `clock` leaving out a published locale, or naming an unpublished one
|
|
250
|
+
| A `formats.clock` leaving out a published locale, or naming an unpublished one | error |
|
|
248
251
|
| `site.clockFor` on a project that declares no clock | throws, naming the project |
|
|
252
|
+
| `site.priceFormatFor` on a project that declares no price format | throws, naming the project |
|
|
249
253
|
| An unknown field in a project declaration | error — a typo cannot sit there unread |
|
|
250
254
|
| Two routes resolving to the same URL | build throws, naming both |
|
|
251
255
|
| A site icon that is not square, a multiple of 48px, and PNG | build throws |
|
package/docs/checks.md
CHANGED
|
@@ -42,7 +42,8 @@ sees.
|
|
|
42
42
|
| unknown message key, or an override dropping a `{placeholder}` | two routes resolving to the same URL |
|
|
43
43
|
| a message the project declared mandatory and never wrote (`CatalogCovers`) | |
|
|
44
44
|
| copy or a locale label written for a locale outside `enabledLocales` | |
|
|
45
|
-
| a `clock` leaving out a published locale, or naming one outside `enabledLocales`
|
|
45
|
+
| a `formats.clock` leaving out a published locale, or naming one outside `enabledLocales` | the same, for a project that was not type-checked first |
|
|
46
|
+
| a `formats.prices.locale` the project does not publish | the same, for a project that was not type-checked first |
|
|
46
47
|
| a slug whose translation sits at a different depth, in the registry or across the overlay | the same, when a slug is not a literal (computed, or from an env var) |
|
|
47
48
|
| unknown route id, or a locale the site does not ship | a locale published twice |
|
|
48
49
|
| a malformed language tag (`en_US`, `en-us`, `EN-US`, bare `en`) | an icon that is not square, a multiple of 48, and PNG |
|
package/package.json
CHANGED
package/src/astro/axon-event.ts
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
GOOGLE_EVENT_GLOBAL,
|
|
3
3
|
type GoogleEventSink,
|
|
4
4
|
} from "../analytics/google.ts";
|
|
5
|
-
import type { CurrencyCode } from "../
|
|
5
|
+
import type { CurrencyCode } from "../currency.ts";
|
|
6
6
|
import type { DeclaredEvent, EventDataArgs } from "./event-data.ts";
|
|
7
7
|
|
|
8
8
|
/** GA4's recommended events that carry items and revenue. */
|
package/src/astro/meta-event.ts
CHANGED
package/src/astro/snap-event.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { CountryCode } from "./countries.ts";
|
|
1
2
|
import type { Fonts } from "./fonts.ts";
|
|
2
3
|
import type { LlmsConfig } from "./llms.ts";
|
|
3
4
|
import type { RobotsConfig } from "./robots.ts";
|
|
4
5
|
import type { SitemapConfig } from "./sitemap.ts";
|
|
5
|
-
import type { NoExcessKeys, StringKeys } from "./types.ts";
|
|
6
|
+
import type { Letter, NoExcessKeys, StringKeys } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
export interface LocaleMeta {
|
|
8
9
|
/** Name of the language, written in that language. */
|
|
@@ -22,29 +23,47 @@ export interface ResolvedLocaleMeta {
|
|
|
22
23
|
readonly ogLocale: string;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** A language subtag: two or three lowercase letters, `en`, `ast`. */
|
|
27
|
+
type LanguageSubtag =
|
|
28
|
+
| `${Lowercase<Letter>}${Lowercase<Letter>}`
|
|
29
|
+
| `${Lowercase<Letter>}${Lowercase<Letter>}${Lowercase<Letter>}`;
|
|
25
30
|
/**
|
|
26
|
-
* A
|
|
31
|
+
* A region subtag: a country, as `CountryCode` lists them, `US`. Not the
|
|
32
|
+
* three-digit areas (`419`, Latin America): a locale names the one country a
|
|
33
|
+
* site serves, and it is a URL segment.
|
|
34
|
+
*/
|
|
35
|
+
type RegionSubtag = CountryCode;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A well-formed language tag, or `never`: a language of two or three
|
|
39
|
+
* lowercase letters and a region that is a country's code, `en-US`, `ast-ES`.
|
|
27
40
|
*
|
|
28
41
|
* The region is required: `en` is rejected, `en-US` accepted. A bare language
|
|
29
42
|
* leaves the territory to the reader, and `en-US` and `en-GB` differ in spelling,
|
|
30
43
|
* currency and date order — decisions worth stating rather than inferring.
|
|
31
44
|
*
|
|
32
|
-
*
|
|
33
|
-
* rejected
|
|
45
|
+
* So `en_US`, `en-us`, `EN-US`, `-US`, `en-U-S`, `es-419` and `en-XX` are all
|
|
46
|
+
* rejected, and so is a script subtag (`sr-Latn-RS`). The region is checked
|
|
47
|
+
* against the real countries; the language only for its shape: the
|
|
34
48
|
* authoritative list is the IANA Language Subtag Registry, thousands of entries
|
|
35
49
|
* that would go stale the moment they were copied into this folder.
|
|
50
|
+
*
|
|
51
|
+
* A check on one literal rather than a type of all tags: as a single type the
|
|
52
|
+
* shape spells out to more strings than TypeScript represents.
|
|
36
53
|
*/
|
|
37
54
|
export type LanguageTag<T extends string> =
|
|
38
|
-
T extends `${infer
|
|
39
|
-
?
|
|
40
|
-
? Region extends
|
|
55
|
+
T extends `${infer Language}-${infer Region}`
|
|
56
|
+
? Language extends LanguageSubtag
|
|
57
|
+
? Region extends RegionSubtag
|
|
41
58
|
? T
|
|
42
59
|
: never
|
|
43
60
|
: never
|
|
44
61
|
: never;
|
|
45
62
|
|
|
63
|
+
/** The same rule at run time, for a project that was not type-checked first. */
|
|
64
|
+
|
|
46
65
|
interface MalformedLanguageTag<T extends string> {
|
|
47
|
-
readonly __MALFORMED_LANGUAGE_TAG__: `"${T}" is not a well-formed language tag: expected a lowercase
|
|
66
|
+
readonly __MALFORMED_LANGUAGE_TAG__: `"${T}" is not a well-formed language tag: expected a language of two or three lowercase letters and a country's code as its region, like "en-US"`;
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
/**
|
package/src/currency.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ISO 4217 currency code: `"EUR"`, `"RON"`, `"GBP"`.
|
|
3
|
+
*
|
|
4
|
+
* A code rather than a symbol — `€`, `lei` — because two currencies can share a
|
|
5
|
+
* symbol, and `priceCurrency` is read as a code.
|
|
6
|
+
*
|
|
7
|
+
* Circulating currencies from ISO 4217 List One (SIX, published 2026-09-17):
|
|
8
|
+
* fund, precious-metal, testing and accounting-unit codes left out.
|
|
9
|
+
*/
|
|
10
|
+
export type CurrencyCode =
|
|
11
|
+
| "AED"
|
|
12
|
+
| "AFN"
|
|
13
|
+
| "ALL"
|
|
14
|
+
| "AMD"
|
|
15
|
+
| "AOA"
|
|
16
|
+
| "ARS"
|
|
17
|
+
| "AUD"
|
|
18
|
+
| "AWG"
|
|
19
|
+
| "AZN"
|
|
20
|
+
| "BAM"
|
|
21
|
+
| "BBD"
|
|
22
|
+
| "BDT"
|
|
23
|
+
| "BHD"
|
|
24
|
+
| "BIF"
|
|
25
|
+
| "BMD"
|
|
26
|
+
| "BND"
|
|
27
|
+
| "BOB"
|
|
28
|
+
| "BRL"
|
|
29
|
+
| "BSD"
|
|
30
|
+
| "BTN"
|
|
31
|
+
| "BWP"
|
|
32
|
+
| "BYN"
|
|
33
|
+
| "BZD"
|
|
34
|
+
| "CAD"
|
|
35
|
+
| "CDF"
|
|
36
|
+
| "CHF"
|
|
37
|
+
| "CLP"
|
|
38
|
+
| "CNY"
|
|
39
|
+
| "COP"
|
|
40
|
+
| "CRC"
|
|
41
|
+
| "CUP"
|
|
42
|
+
| "CVE"
|
|
43
|
+
| "CZK"
|
|
44
|
+
| "DJF"
|
|
45
|
+
| "DKK"
|
|
46
|
+
| "DOP"
|
|
47
|
+
| "DZD"
|
|
48
|
+
| "EGP"
|
|
49
|
+
| "ERN"
|
|
50
|
+
| "ETB"
|
|
51
|
+
| "EUR"
|
|
52
|
+
| "FJD"
|
|
53
|
+
| "FKP"
|
|
54
|
+
| "GBP"
|
|
55
|
+
| "GEL"
|
|
56
|
+
| "GHS"
|
|
57
|
+
| "GIP"
|
|
58
|
+
| "GMD"
|
|
59
|
+
| "GNF"
|
|
60
|
+
| "GTQ"
|
|
61
|
+
| "GYD"
|
|
62
|
+
| "HKD"
|
|
63
|
+
| "HNL"
|
|
64
|
+
| "HTG"
|
|
65
|
+
| "HUF"
|
|
66
|
+
| "IDR"
|
|
67
|
+
| "ILS"
|
|
68
|
+
| "INR"
|
|
69
|
+
| "IQD"
|
|
70
|
+
| "IRR"
|
|
71
|
+
| "ISK"
|
|
72
|
+
| "JMD"
|
|
73
|
+
| "JOD"
|
|
74
|
+
| "JPY"
|
|
75
|
+
| "KES"
|
|
76
|
+
| "KGS"
|
|
77
|
+
| "KHR"
|
|
78
|
+
| "KMF"
|
|
79
|
+
| "KPW"
|
|
80
|
+
| "KRW"
|
|
81
|
+
| "KWD"
|
|
82
|
+
| "KYD"
|
|
83
|
+
| "KZT"
|
|
84
|
+
| "LAK"
|
|
85
|
+
| "LBP"
|
|
86
|
+
| "LKR"
|
|
87
|
+
| "LRD"
|
|
88
|
+
| "LSL"
|
|
89
|
+
| "LYD"
|
|
90
|
+
| "MAD"
|
|
91
|
+
| "MDL"
|
|
92
|
+
| "MGA"
|
|
93
|
+
| "MKD"
|
|
94
|
+
| "MMK"
|
|
95
|
+
| "MNT"
|
|
96
|
+
| "MOP"
|
|
97
|
+
| "MRU"
|
|
98
|
+
| "MUR"
|
|
99
|
+
| "MVR"
|
|
100
|
+
| "MWK"
|
|
101
|
+
| "MXN"
|
|
102
|
+
| "MYR"
|
|
103
|
+
| "MZN"
|
|
104
|
+
| "NAD"
|
|
105
|
+
| "NGN"
|
|
106
|
+
| "NIO"
|
|
107
|
+
| "NOK"
|
|
108
|
+
| "NPR"
|
|
109
|
+
| "NZD"
|
|
110
|
+
| "OMR"
|
|
111
|
+
| "PAB"
|
|
112
|
+
| "PEN"
|
|
113
|
+
| "PGK"
|
|
114
|
+
| "PHP"
|
|
115
|
+
| "PKR"
|
|
116
|
+
| "PLN"
|
|
117
|
+
| "PYG"
|
|
118
|
+
| "QAR"
|
|
119
|
+
| "RON"
|
|
120
|
+
| "RSD"
|
|
121
|
+
| "RUB"
|
|
122
|
+
| "RWF"
|
|
123
|
+
| "SAR"
|
|
124
|
+
| "SBD"
|
|
125
|
+
| "SCR"
|
|
126
|
+
| "SDG"
|
|
127
|
+
| "SEK"
|
|
128
|
+
| "SGD"
|
|
129
|
+
| "SHP"
|
|
130
|
+
| "SLE"
|
|
131
|
+
| "SOS"
|
|
132
|
+
| "SRD"
|
|
133
|
+
| "SSP"
|
|
134
|
+
| "STN"
|
|
135
|
+
| "SVC"
|
|
136
|
+
| "SYP"
|
|
137
|
+
| "SZL"
|
|
138
|
+
| "THB"
|
|
139
|
+
| "TJS"
|
|
140
|
+
| "TMT"
|
|
141
|
+
| "TND"
|
|
142
|
+
| "TOP"
|
|
143
|
+
| "TRY"
|
|
144
|
+
| "TTD"
|
|
145
|
+
| "TWD"
|
|
146
|
+
| "TZS"
|
|
147
|
+
| "UAH"
|
|
148
|
+
| "UGX"
|
|
149
|
+
| "USD"
|
|
150
|
+
| "UYU"
|
|
151
|
+
| "UZS"
|
|
152
|
+
| "VED"
|
|
153
|
+
| "VES"
|
|
154
|
+
| "VND"
|
|
155
|
+
| "VUV"
|
|
156
|
+
| "WST"
|
|
157
|
+
| "XAF"
|
|
158
|
+
| "XCD"
|
|
159
|
+
| "XCG"
|
|
160
|
+
| "XOF"
|
|
161
|
+
| "XPF"
|
|
162
|
+
| "YER"
|
|
163
|
+
| "ZAR"
|
|
164
|
+
| "ZMW"
|
|
165
|
+
| "ZWG";
|
package/src/index.ts
CHANGED
|
@@ -146,6 +146,12 @@ export {
|
|
|
146
146
|
countriesFor,
|
|
147
147
|
type NamedCountry,
|
|
148
148
|
} from "./countries.ts";
|
|
149
|
+
// `quantitiesOf`, `quantityRange` and `isContiguous` are deliberately not here.
|
|
150
|
+
// They are how lib decides between "2–6" and "2, 4, 6", and a consumer holding
|
|
151
|
+
// them writes that decision a second time — which is how `llms.txt` came to
|
|
152
|
+
// advertise a range for a room that skips sizes. `formatQuantities` answers it
|
|
153
|
+
// for prose and `quantitiesFor` for structured data; both read the same tiers.
|
|
154
|
+
export type { CurrencyCode } from "./currency.ts";
|
|
149
155
|
export type { GeneratedFile } from "./file.ts";
|
|
150
156
|
export type { PublicFilePath, PublicFileRegistry } from "./files.ts";
|
|
151
157
|
export type { Font, Fonts } from "./fonts.ts";
|
|
@@ -250,19 +256,12 @@ export {
|
|
|
250
256
|
type ShareImage,
|
|
251
257
|
type SiteVerification,
|
|
252
258
|
} from "./meta/index.ts";
|
|
253
|
-
// `quantitiesOf`, `quantityRange` and `isContiguous` are deliberately not here.
|
|
254
|
-
// They are how lib decides between "2–6" and "2, 4, 6", and a consumer holding
|
|
255
|
-
// them writes that decision a second time — which is how `llms.txt` came to
|
|
256
|
-
// advertise a range for a room that skips sizes. `formatQuantities` answers it
|
|
257
|
-
// for prose and `quantitiesFor` for structured data; both read the same tiers.
|
|
258
259
|
export {
|
|
259
260
|
type AudiencePrice,
|
|
260
261
|
type AudienceTier,
|
|
261
262
|
amountAt,
|
|
262
263
|
assertAudiences,
|
|
263
264
|
assertPriceTiers,
|
|
264
|
-
type CurrencyCode,
|
|
265
|
-
formatPrice,
|
|
266
265
|
formatQuantities,
|
|
267
266
|
type Price,
|
|
268
267
|
type PriceTier,
|
|
@@ -272,6 +271,11 @@ export {
|
|
|
272
271
|
type TieredPrice,
|
|
273
272
|
tierRange,
|
|
274
273
|
} from "./money.ts";
|
|
274
|
+
export {
|
|
275
|
+
formatPrice,
|
|
276
|
+
type PriceDecimals,
|
|
277
|
+
type PriceFormat,
|
|
278
|
+
} from "./price-format.ts";
|
|
275
279
|
export type {
|
|
276
280
|
AddedCatalog,
|
|
277
281
|
DefinedProject,
|
package/src/money.ts
CHANGED
|
@@ -1,171 +1,6 @@
|
|
|
1
|
+
import type { CurrencyCode } from "./currency.ts";
|
|
1
2
|
import type { NonEmpty } from "./types.ts";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* An ISO 4217 currency code: `"EUR"`, `"RON"`, `"GBP"`.
|
|
5
|
-
*
|
|
6
|
-
* A code rather than a symbol — `€`, `lei` — because two currencies can share a
|
|
7
|
-
* symbol, and `priceCurrency` is read as a code.
|
|
8
|
-
*
|
|
9
|
-
* Circulating currencies from ISO 4217 List One (SIX, published 2026-09-17):
|
|
10
|
-
* fund, precious-metal, testing and accounting-unit codes left out.
|
|
11
|
-
*/
|
|
12
|
-
export type CurrencyCode =
|
|
13
|
-
| "AED"
|
|
14
|
-
| "AFN"
|
|
15
|
-
| "ALL"
|
|
16
|
-
| "AMD"
|
|
17
|
-
| "AOA"
|
|
18
|
-
| "ARS"
|
|
19
|
-
| "AUD"
|
|
20
|
-
| "AWG"
|
|
21
|
-
| "AZN"
|
|
22
|
-
| "BAM"
|
|
23
|
-
| "BBD"
|
|
24
|
-
| "BDT"
|
|
25
|
-
| "BHD"
|
|
26
|
-
| "BIF"
|
|
27
|
-
| "BMD"
|
|
28
|
-
| "BND"
|
|
29
|
-
| "BOB"
|
|
30
|
-
| "BRL"
|
|
31
|
-
| "BSD"
|
|
32
|
-
| "BTN"
|
|
33
|
-
| "BWP"
|
|
34
|
-
| "BYN"
|
|
35
|
-
| "BZD"
|
|
36
|
-
| "CAD"
|
|
37
|
-
| "CDF"
|
|
38
|
-
| "CHF"
|
|
39
|
-
| "CLP"
|
|
40
|
-
| "CNY"
|
|
41
|
-
| "COP"
|
|
42
|
-
| "CRC"
|
|
43
|
-
| "CUP"
|
|
44
|
-
| "CVE"
|
|
45
|
-
| "CZK"
|
|
46
|
-
| "DJF"
|
|
47
|
-
| "DKK"
|
|
48
|
-
| "DOP"
|
|
49
|
-
| "DZD"
|
|
50
|
-
| "EGP"
|
|
51
|
-
| "ERN"
|
|
52
|
-
| "ETB"
|
|
53
|
-
| "EUR"
|
|
54
|
-
| "FJD"
|
|
55
|
-
| "FKP"
|
|
56
|
-
| "GBP"
|
|
57
|
-
| "GEL"
|
|
58
|
-
| "GHS"
|
|
59
|
-
| "GIP"
|
|
60
|
-
| "GMD"
|
|
61
|
-
| "GNF"
|
|
62
|
-
| "GTQ"
|
|
63
|
-
| "GYD"
|
|
64
|
-
| "HKD"
|
|
65
|
-
| "HNL"
|
|
66
|
-
| "HTG"
|
|
67
|
-
| "HUF"
|
|
68
|
-
| "IDR"
|
|
69
|
-
| "ILS"
|
|
70
|
-
| "INR"
|
|
71
|
-
| "IQD"
|
|
72
|
-
| "IRR"
|
|
73
|
-
| "ISK"
|
|
74
|
-
| "JMD"
|
|
75
|
-
| "JOD"
|
|
76
|
-
| "JPY"
|
|
77
|
-
| "KES"
|
|
78
|
-
| "KGS"
|
|
79
|
-
| "KHR"
|
|
80
|
-
| "KMF"
|
|
81
|
-
| "KPW"
|
|
82
|
-
| "KRW"
|
|
83
|
-
| "KWD"
|
|
84
|
-
| "KYD"
|
|
85
|
-
| "KZT"
|
|
86
|
-
| "LAK"
|
|
87
|
-
| "LBP"
|
|
88
|
-
| "LKR"
|
|
89
|
-
| "LRD"
|
|
90
|
-
| "LSL"
|
|
91
|
-
| "LYD"
|
|
92
|
-
| "MAD"
|
|
93
|
-
| "MDL"
|
|
94
|
-
| "MGA"
|
|
95
|
-
| "MKD"
|
|
96
|
-
| "MMK"
|
|
97
|
-
| "MNT"
|
|
98
|
-
| "MOP"
|
|
99
|
-
| "MRU"
|
|
100
|
-
| "MUR"
|
|
101
|
-
| "MVR"
|
|
102
|
-
| "MWK"
|
|
103
|
-
| "MXN"
|
|
104
|
-
| "MYR"
|
|
105
|
-
| "MZN"
|
|
106
|
-
| "NAD"
|
|
107
|
-
| "NGN"
|
|
108
|
-
| "NIO"
|
|
109
|
-
| "NOK"
|
|
110
|
-
| "NPR"
|
|
111
|
-
| "NZD"
|
|
112
|
-
| "OMR"
|
|
113
|
-
| "PAB"
|
|
114
|
-
| "PEN"
|
|
115
|
-
| "PGK"
|
|
116
|
-
| "PHP"
|
|
117
|
-
| "PKR"
|
|
118
|
-
| "PLN"
|
|
119
|
-
| "PYG"
|
|
120
|
-
| "QAR"
|
|
121
|
-
| "RON"
|
|
122
|
-
| "RSD"
|
|
123
|
-
| "RUB"
|
|
124
|
-
| "RWF"
|
|
125
|
-
| "SAR"
|
|
126
|
-
| "SBD"
|
|
127
|
-
| "SCR"
|
|
128
|
-
| "SDG"
|
|
129
|
-
| "SEK"
|
|
130
|
-
| "SGD"
|
|
131
|
-
| "SHP"
|
|
132
|
-
| "SLE"
|
|
133
|
-
| "SOS"
|
|
134
|
-
| "SRD"
|
|
135
|
-
| "SSP"
|
|
136
|
-
| "STN"
|
|
137
|
-
| "SVC"
|
|
138
|
-
| "SYP"
|
|
139
|
-
| "SZL"
|
|
140
|
-
| "THB"
|
|
141
|
-
| "TJS"
|
|
142
|
-
| "TMT"
|
|
143
|
-
| "TND"
|
|
144
|
-
| "TOP"
|
|
145
|
-
| "TRY"
|
|
146
|
-
| "TTD"
|
|
147
|
-
| "TWD"
|
|
148
|
-
| "TZS"
|
|
149
|
-
| "UAH"
|
|
150
|
-
| "UGX"
|
|
151
|
-
| "USD"
|
|
152
|
-
| "UYU"
|
|
153
|
-
| "UZS"
|
|
154
|
-
| "VED"
|
|
155
|
-
| "VES"
|
|
156
|
-
| "VND"
|
|
157
|
-
| "VUV"
|
|
158
|
-
| "WST"
|
|
159
|
-
| "XAF"
|
|
160
|
-
| "XCD"
|
|
161
|
-
| "XCG"
|
|
162
|
-
| "XOF"
|
|
163
|
-
| "XPF"
|
|
164
|
-
| "YER"
|
|
165
|
-
| "ZAR"
|
|
166
|
-
| "ZMW"
|
|
167
|
-
| "ZWG";
|
|
168
|
-
|
|
169
4
|
/**
|
|
170
5
|
* An amount and what it is denominated in, kept together.
|
|
171
6
|
*
|
|
@@ -501,22 +336,3 @@ export function amountAt(
|
|
|
501
336
|
}
|
|
502
337
|
return undefined;
|
|
503
338
|
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* A price as a reader in `locale` writes it: the currency's symbol and that
|
|
507
|
-
* language's separators, and no decimals on a whole amount — "£25", not
|
|
508
|
-
* "£25.00", while "€12.50" keeps its cents.
|
|
509
|
-
*
|
|
510
|
-
* Whole amounts drop the decimals because that is how a price list reads; a
|
|
511
|
-
* table of "25.00, 22.00, 20.00" says nothing "25, 22, 20" does not. An amount
|
|
512
|
-
* with cents shows two, so a price is never rounded on the way to the page.
|
|
513
|
-
*/
|
|
514
|
-
export function formatPrice(price: Price, locale: string): string {
|
|
515
|
-
const whole = Number.isInteger(price.amount);
|
|
516
|
-
return new Intl.NumberFormat(locale, {
|
|
517
|
-
style: "currency",
|
|
518
|
-
currency: price.currency,
|
|
519
|
-
minimumFractionDigits: whole ? 0 : 2,
|
|
520
|
-
maximumFractionDigits: whole ? 0 : 2,
|
|
521
|
-
}).format(price.amount);
|
|
522
|
-
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Price } from "./money.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a whole amount shows its decimals: `"always"` writes "18,00 €" and
|
|
5
|
+
* "$18.00"; `"whenNeeded"` writes "£18" and "18 €". Either way an amount with
|
|
6
|
+
* cents shows the currency's digits, so a price is never rounded on the way to
|
|
7
|
+
* the page.
|
|
8
|
+
*/
|
|
9
|
+
export type PriceDecimals = "always" | "whenNeeded";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* How a site writes its prices: a project's `formats.prices`. The decimals are
|
|
13
|
+
* the store's, from its price list and currency, so one rule serves every
|
|
14
|
+
* language. The locale is optional: without it each page writes prices in its
|
|
15
|
+
* own language's way ("€18.00" in English, "18,00 €" in Portuguese); with it,
|
|
16
|
+
* every page writes them the way of that one of the site's languages.
|
|
17
|
+
*/
|
|
18
|
+
export interface PriceFormat<L extends string = string> {
|
|
19
|
+
readonly decimals: PriceDecimals;
|
|
20
|
+
/**
|
|
21
|
+
* The locale every page writes prices in: one the project publishes, so
|
|
22
|
+
* an English page can write them the store's own way, `"pt-PT"`. Each
|
|
23
|
+
* page's own without.
|
|
24
|
+
*/
|
|
25
|
+
readonly locale?: L;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A price as `locale` writes it: the currency's symbol and that locale's
|
|
30
|
+
* separators, and whole amounts with or without their decimals as `decimals`
|
|
31
|
+
* says. Pages take both from `site.priceFormatFor(locale)`.
|
|
32
|
+
*/
|
|
33
|
+
export function formatPrice(
|
|
34
|
+
price: Price,
|
|
35
|
+
locale: string,
|
|
36
|
+
decimals: PriceDecimals
|
|
37
|
+
): string {
|
|
38
|
+
// The currency's own digits otherwise, as Intl knows them: two for the
|
|
39
|
+
// euro, three for the Kuwaiti dinar, none for the yen.
|
|
40
|
+
const whole =
|
|
41
|
+
decimals === "whenNeeded" && Number.isInteger(price.amount)
|
|
42
|
+
? { minimumFractionDigits: 0, maximumFractionDigits: 0 }
|
|
43
|
+
: {};
|
|
44
|
+
return new Intl.NumberFormat(locale, {
|
|
45
|
+
style: "currency",
|
|
46
|
+
currency: price.currency,
|
|
47
|
+
...whole,
|
|
48
|
+
}).format(price.amount);
|
|
49
|
+
}
|
package/src/project.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
} from "./i18n/define.ts";
|
|
17
17
|
import type { ImageAsset } from "./image.ts";
|
|
18
18
|
import type { SiteVerification, ThemeColor } from "./meta/index.ts";
|
|
19
|
+
import type { PriceFormat } from "./price-format.ts";
|
|
19
20
|
import type {
|
|
20
21
|
ProjectRedirects,
|
|
21
22
|
RedirectRule,
|
|
@@ -39,6 +40,28 @@ import type { HttpsUrl, ValidSiteUrl } from "./url.ts";
|
|
|
39
40
|
* here. A project exports those separately, typed by the template's own
|
|
40
41
|
* contract, so lib never carries data it cannot reason about.
|
|
41
42
|
*/
|
|
43
|
+
/** How a project writes what it formats: a project's `formats`. */
|
|
44
|
+
export interface ProjectFormats<L extends string> {
|
|
45
|
+
/**
|
|
46
|
+
* The clock each published language prints times on: `"h23"` for 18:00,
|
|
47
|
+
* `"h12"` for 6:00 PM. Read back with `site.clockFor(locale)`.
|
|
48
|
+
*
|
|
49
|
+
* One entry for every locale in `enabledLocales`, and none for any other,
|
|
50
|
+
* so what each page prints is written down beside the languages that print
|
|
51
|
+
* it: a bilingual Canadian site reads `{ "en-CA": "h12", "fr-CA": "h23" }`.
|
|
52
|
+
* Not taken from the locale's own default (`Intl`'s) on purpose: a site may
|
|
53
|
+
* print a 24-hour clock where its language's default is 12-hour, and that
|
|
54
|
+
* choice is the site's to state.
|
|
55
|
+
*/
|
|
56
|
+
readonly clock?: Readonly<Partial<Record<L, HourCycle>>>;
|
|
57
|
+
/**
|
|
58
|
+
* How the site writes prices, one rule for every page: whether whole
|
|
59
|
+
* amounts show their decimals, and optionally the one locale they are all
|
|
60
|
+
* written in. Read back with `site.priceFormatFor(locale)`.
|
|
61
|
+
*/
|
|
62
|
+
readonly prices?: PriceFormat<L>;
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
export interface ProjectInput<L extends string, Routes> {
|
|
43
66
|
/**
|
|
44
67
|
* Absolute origin, e.g. `https://example.com`.
|
|
@@ -107,20 +130,11 @@ export interface ProjectInput<L extends string, Routes> {
|
|
|
107
130
|
*/
|
|
108
131
|
readonly enabledLocales: readonly L[];
|
|
109
132
|
/**
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* One entry for every locale in `enabledLocales`, and none for any other,
|
|
114
|
-
* so what each page prints is written down beside the languages that print
|
|
115
|
-
* it: a bilingual Canadian site reads `{ "en-CA": "h12", "fr-CA": "h23" }`.
|
|
116
|
-
* Not taken from the locale's own default (`Intl`'s) on purpose: a site may
|
|
117
|
-
* print a 24-hour clock where its language's default is 12-hour, and that
|
|
118
|
-
* choice is the site's to state.
|
|
119
|
-
*
|
|
120
|
-
* Optional: a project that prints no times declares none, and
|
|
121
|
-
* `clockFor` refuses to answer for it.
|
|
133
|
+
* How this site writes what it formats: times and prices. Each format is
|
|
134
|
+
* optional, and its reader refuses to answer for a project that declares
|
|
135
|
+
* none, so one that prints times or prices has to say how.
|
|
122
136
|
*/
|
|
123
|
-
readonly
|
|
137
|
+
readonly formats?: ProjectFormats<L>;
|
|
124
138
|
/**
|
|
125
139
|
* Replaces parts of the site's `defaultRouting` for this project.
|
|
126
140
|
*
|
|
@@ -302,19 +316,41 @@ export type ProjectChecks<
|
|
|
302
316
|
? NoExcessKeys<T["overrideLocaleMeta"][K], keyof LocaleMetaOverride>
|
|
303
317
|
: LocaleNotEnabled<K>;
|
|
304
318
|
};
|
|
305
|
-
// The locales
|
|
319
|
+
// The clock's locales must be ones this project publishes, as for
|
|
306
320
|
// `overrideLocaleMeta` above. That every one it publishes is there is
|
|
307
321
|
// checked in `ProjectOverrideChecks`: a conditional on `T` here would make
|
|
308
322
|
// the constraint circular.
|
|
309
|
-
readonly
|
|
310
|
-
readonly
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
323
|
+
readonly formats?: {
|
|
324
|
+
readonly clock?: {
|
|
325
|
+
readonly [K in StringKeys<
|
|
326
|
+
NonNullable<T["formats"]>["clock"]
|
|
327
|
+
>]: K extends T["enabledLocales"][number]
|
|
328
|
+
? HourCycle
|
|
329
|
+
: LocaleNotEnabled<K>;
|
|
330
|
+
};
|
|
331
|
+
// Its locale too must be one this project publishes.
|
|
332
|
+
readonly prices?: PriceFormat & {
|
|
333
|
+
readonly locale?: PriceLocaleCheck<T>;
|
|
334
|
+
};
|
|
315
335
|
};
|
|
316
336
|
};
|
|
317
337
|
|
|
338
|
+
/** A project's price locale as written, or `never` when it names none. */
|
|
339
|
+
type PriceLocaleOf<T> = T extends {
|
|
340
|
+
readonly formats?: { readonly prices?: { readonly locale?: infer Locale } };
|
|
341
|
+
}
|
|
342
|
+
? Exclude<Locale, undefined> & string
|
|
343
|
+
: never;
|
|
344
|
+
|
|
345
|
+
/** The price locale, if the project publishes it; else an error naming it. */
|
|
346
|
+
type PriceLocaleCheck<T> = T extends {
|
|
347
|
+
readonly enabledLocales: readonly (infer Published)[];
|
|
348
|
+
}
|
|
349
|
+
? PriceLocaleOf<T> extends Published
|
|
350
|
+
? PriceLocaleOf<T>
|
|
351
|
+
: LocaleNotEnabled<PriceLocaleOf<T>>
|
|
352
|
+
: never;
|
|
353
|
+
|
|
318
354
|
/** The same checks that map the whole override objects, in parameter position. */
|
|
319
355
|
export type ProjectOverrideChecks<
|
|
320
356
|
L extends string,
|
|
@@ -331,7 +367,14 @@ export type ProjectOverrideChecks<
|
|
|
331
367
|
Routes,
|
|
332
368
|
T["overrideRoutes"]
|
|
333
369
|
>;
|
|
334
|
-
} &
|
|
370
|
+
} & AdditionsCheck<Catalog, T> &
|
|
371
|
+
ClockCheck<T>;
|
|
372
|
+
|
|
373
|
+
/** A project's own messages, checked like the catalog's: when it adds any. */
|
|
374
|
+
type AdditionsCheck<
|
|
375
|
+
Catalog,
|
|
376
|
+
T extends ProjectInput<string, unknown>,
|
|
377
|
+
> = T extends { readonly addMessages: infer A }
|
|
335
378
|
? {
|
|
336
379
|
readonly addMessages: ValidateAdditions<
|
|
337
380
|
A,
|
|
@@ -339,16 +382,23 @@ export type ProjectOverrideChecks<
|
|
|
339
382
|
T["enabledLocales"][number]
|
|
340
383
|
>;
|
|
341
384
|
}
|
|
342
|
-
: unknown
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
385
|
+
: unknown;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A clock, once declared, is declared for every page: a locale left out would
|
|
389
|
+
* have no clock to print its times on.
|
|
390
|
+
*/
|
|
391
|
+
type ClockCheck<T extends ProjectInput<string, unknown>> = T extends {
|
|
392
|
+
readonly formats: { readonly clock: object };
|
|
393
|
+
}
|
|
394
|
+
? {
|
|
395
|
+
readonly formats: {
|
|
347
396
|
readonly clock: {
|
|
348
397
|
readonly [K in T["enabledLocales"][number]]: HourCycle;
|
|
349
398
|
};
|
|
350
|
-
}
|
|
351
|
-
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
: unknown;
|
|
352
402
|
|
|
353
403
|
/** A project's own messages, as a catalog: none when it adds none. */
|
|
354
404
|
export type AddedCatalog<P> = P extends { readonly addMessages: infer A }
|
|
@@ -406,17 +456,16 @@ export function defineProject<
|
|
|
406
456
|
}
|
|
407
457
|
}
|
|
408
458
|
// The clock's rule, for the same reason.
|
|
409
|
-
|
|
459
|
+
const clock = project.formats?.clock;
|
|
460
|
+
if (clock !== undefined) {
|
|
410
461
|
const published: readonly string[] = project.enabledLocales;
|
|
411
|
-
const missing = published.filter(
|
|
412
|
-
(locale) => !(locale in (project.clock ?? {}))
|
|
413
|
-
);
|
|
462
|
+
const missing = published.filter((locale) => !(locale in clock));
|
|
414
463
|
if (missing.length > 0) {
|
|
415
464
|
throw new Error(
|
|
416
465
|
`Project "${project.siteName}": clock has no ${missing.join(", ")}. A project that declares a clock gives one for every locale it publishes.`
|
|
417
466
|
);
|
|
418
467
|
}
|
|
419
|
-
const extra = Object.keys(
|
|
468
|
+
const extra = Object.keys(clock).filter(
|
|
420
469
|
(locale) => !published.includes(locale)
|
|
421
470
|
);
|
|
422
471
|
if (extra.length > 0) {
|
|
@@ -425,5 +474,15 @@ export function defineProject<
|
|
|
425
474
|
);
|
|
426
475
|
}
|
|
427
476
|
}
|
|
477
|
+
// The price locale's rule, for the same reason.
|
|
478
|
+
const priceLocale: string | undefined = project.formats?.prices?.locale;
|
|
479
|
+
if (
|
|
480
|
+
priceLocale !== undefined &&
|
|
481
|
+
!(project.enabledLocales as readonly string[]).includes(priceLocale)
|
|
482
|
+
) {
|
|
483
|
+
throw new Error(
|
|
484
|
+
`Project "${project.siteName}": formats.prices.locale names ${priceLocale}, which this project does not publish.`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
428
487
|
return project;
|
|
429
488
|
}
|
package/src/site/api.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { HourCycle } from "../hours.ts";
|
|
|
11
11
|
import type { TranslateFor } from "../i18n/translate.ts";
|
|
12
12
|
import type { LlmsSection } from "../llms.ts";
|
|
13
13
|
import type { ThemeColor } from "../meta/index.ts";
|
|
14
|
+
import type { PriceDecimals } from "../price-format.ts";
|
|
14
15
|
import type {
|
|
15
16
|
RedirectRule,
|
|
16
17
|
ResolvedRedirect,
|
|
@@ -229,6 +230,19 @@ export interface Site<
|
|
|
229
230
|
*/
|
|
230
231
|
clockFor(locale: L): HourCycle;
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* How a locale's pages write prices, as the project's `formats.prices`
|
|
235
|
+
* states it: hand both to `formatPrice`. The locale is the page's own,
|
|
236
|
+
* unless the project names one for every page.
|
|
237
|
+
*
|
|
238
|
+
* Throws for a project that declares no price format: one that prints
|
|
239
|
+
* prices has to say how.
|
|
240
|
+
*/
|
|
241
|
+
priceFormatFor(locale: L): {
|
|
242
|
+
readonly decimals: PriceDecimals;
|
|
243
|
+
readonly locale: L;
|
|
244
|
+
};
|
|
245
|
+
|
|
232
246
|
/** Absolute path of a route in one locale, e.g. `/el/sxetika-me-emas`. */
|
|
233
247
|
pathFor(id: RouteId, locale: L, options?: LinkOptions): UrlPath;
|
|
234
248
|
/** The same, prefixed with the site origin — for canonicals and sitemaps. */
|
package/src/site/create.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
buildNotFoundMeta,
|
|
25
25
|
resolveShareImage,
|
|
26
26
|
} from "../meta/index.ts";
|
|
27
|
+
import type { PriceDecimals } from "../price-format.ts";
|
|
27
28
|
import type { AddedCatalog, ProjectInput, ProjectRouteId } from "../project.ts";
|
|
28
29
|
import {
|
|
29
30
|
buildRedirects,
|
|
@@ -194,17 +195,35 @@ export function createSite<
|
|
|
194
195
|
const resolved = mergeRoutes<L, Routes>(routes, project.overrideRoutes);
|
|
195
196
|
|
|
196
197
|
function clockFor(locale: L): HourCycle {
|
|
197
|
-
const
|
|
198
|
+
const clock = project.formats?.clock;
|
|
199
|
+
const cycle = clock?.[locale];
|
|
198
200
|
if (cycle === undefined) {
|
|
199
201
|
throw new Error(
|
|
200
|
-
|
|
201
|
-
? `Project "${project.siteName}" declares no clock, so it cannot print a time. Add \`clock\`, with an hour cycle for each locale it publishes.`
|
|
202
|
-
: `Project "${project.siteName}": clock has no ${locale}.`
|
|
202
|
+
clock === undefined
|
|
203
|
+
? `Project "${project.siteName}" declares no clock, so it cannot print a time. Add \`formats.clock\`, with an hour cycle for each locale it publishes.`
|
|
204
|
+
: `Project "${project.siteName}": formats.clock has no ${locale}.`
|
|
203
205
|
);
|
|
204
206
|
}
|
|
205
207
|
return cycle;
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
function priceFormatFor(locale: L): {
|
|
211
|
+
readonly decimals: PriceDecimals;
|
|
212
|
+
readonly locale: L;
|
|
213
|
+
} {
|
|
214
|
+
const prices = project.formats?.prices;
|
|
215
|
+
if (prices === undefined) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Project "${project.siteName}" declares no price format, so it cannot print a price. Add \`formats.prices\`, with its decimals.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
// Found among the published locales, which the project's checks hold
|
|
221
|
+
// it to, so it is typed as one of them; else the page's own.
|
|
222
|
+
const written =
|
|
223
|
+
locales.find((each) => each === prices.locale) ?? locale;
|
|
224
|
+
return { decimals: prices.decimals, locale: written };
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
function pathFor(id: RouteId, locale: L, options?: LinkOptions): UrlPath {
|
|
209
228
|
const route = resolved[id];
|
|
210
229
|
if (route === undefined) {
|
|
@@ -881,6 +900,7 @@ export function createSite<
|
|
|
881
900
|
}),
|
|
882
901
|
|
|
883
902
|
clockFor,
|
|
903
|
+
priceFormatFor,
|
|
884
904
|
pathFor,
|
|
885
905
|
urlFor,
|
|
886
906
|
fileUrl,
|