forked from andreykobal/Amber-mint-page
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
176 lines (161 loc) · 4.89 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import fs from "fs"
import path from "path"
import { execSync } from "child_process"
import {
Locale,
requiredHeroFields,
optionalHeroFields,
saleStatuses,
userStatuses,
} from "./Locale"
import { placeholderStrings } from "./runtimeUtils"
import type { Hero, RawHeroTree } from "./Locale"
// re-create `Locale.validator.ts` based of current contents of `Locale.ts`
execSync(`yarn create-validator-ts --skipTypeCheck ${__dirname}/Locale.ts`)
// now that we re-created the file we can import the latest version
const validate: (x: unknown) => Locale =
require("./Locale.validator").validateLocale
class LocaleError extends Error {}
interface HeroSaleState {
signedOut: Hero
signedIn: Hero
vip: Hero
}
export interface ExpandedHeroTree {
saleClosed: HeroSaleState
presale: HeroSaleState
saleOpen: HeroSaleState
allSold: HeroSaleState
}
export interface DecoratedLocale extends Locale {
id: string
hero: ExpandedHeroTree
}
function computeField({
rawHeroTree,
saleStatus,
userStatus,
required,
}: {
rawHeroTree: RawHeroTree
saleStatus: keyof ExpandedHeroTree
userStatus: keyof HeroSaleState
required: boolean
}) {
return (hero: Partial<Hero>, field: keyof Hero) => {
// @ts-expect-error Type 'string | undefined' is not assignable to type 'Action'
hero[field] =
rawHeroTree[saleStatus]?.[userStatus]?.[field] ??
rawHeroTree[saleStatus]?.[field] ??
rawHeroTree[field]
if (required && typeof hero[field] === "undefined") {
throw new LocaleError(
`"hero" must include computable "${field}" in each branch; please include at least one of:\n` +
` • "hero.${field}"\n` +
` • "hero.${saleStatus}.${field}"\n` +
` • "hero.${saleStatus}.${userStatus}.${field}"\n` +
`(if set in more than one of these, a more specific setting overrides a more general)`
)
}
// warn if it looks like there might be an unknown placeholder string
// ('action' field values are validated as part of the schema, so we can skip them here)
const allCapsSubStrings =
field !== "action" && hero[field]?.matchAll(/\b[A-Z_]+\b/g)
Array.from(allCapsSubStrings || []).forEach(([possiblePlaceholder]) => {
// TODO: update above regex to only match strings with underscores in them to avoid `.match('_')`
if (
possiblePlaceholder.match("_") &&
!placeholderStrings.includes(possiblePlaceholder)
) {
console.warn(
`"hero" field "${field}" contains what looks like a placeholder string "${possiblePlaceholder}", ` +
`but no substitution is available for this string. Did you mean to include one of the following?\n\n` +
` • ${placeholderStrings.join("\n • ")}\n\n` +
`The full text given for this field was:\n\n ${hero[field]}\n\n`
)
}
})
return hero
}
}
function hoistHeroFields(rawHeroTree: RawHeroTree): ExpandedHeroTree {
return saleStatuses.reduce(
(a, saleStatus) => ({
...a,
[saleStatus]: userStatuses.reduce(
(b, userStatus) => ({
...b,
[userStatus]: {
...requiredHeroFields.reduce(
computeField({
rawHeroTree,
saleStatus,
userStatus,
required: true,
}),
{}
),
...optionalHeroFields.reduce(
computeField({
rawHeroTree,
saleStatus,
userStatus,
required: false,
}),
{}
),
} as Hero,
}),
{} as HeroSaleState
),
}),
{} as ExpandedHeroTree
)
}
// for use with `sort`
function alphabeticOrder(
{ id: a }: DecoratedLocale,
{ id: b }: DecoratedLocale
): -1 | 0 | 1 {
if (a < b) {
return -1
} else if (a > b) {
return 1
} else {
return 0
}
}
const localesDirectory = path.resolve(process.cwd(), "config/i18n")
let fileNames: string[]
try {
fileNames = fs.readdirSync(localesDirectory)
} catch {
fileNames = []
}
const IS_JSON = /.json$/
export const locales: DecoratedLocale[] = fileNames
.filter(f => IS_JSON.test(f))
.map(fileName => {
// Remove ".json" from file name to get id
// TODO: validate that `id` is valid according to https://www.npmjs.com/package/iso-639-1
const id = fileName.replace(/\.json$/, "")
const fullPath = path.join(localesDirectory, fileName)
const fileContents: unknown = JSON.parse(fs.readFileSync(fullPath, "utf8"))
const i18n = validate(fileContents)
let hero: ExpandedHeroTree
try {
hero = hoistHeroFields(i18n.hero)
} catch (e: unknown) {
if (e instanceof LocaleError) {
throw new Error(`Error parsing ${fileName}:\n\n${e.message}`)
}
throw e
}
// Combine the data with the id
return {
id,
...i18n,
hero,
}
})
.sort(alphabeticOrder)