ReviewOS

also looking at this

stacks/analyticshq

fix(dashboard): the regions panel says California, not CA

#63
Merged glennmichael123 wants to merge fix/region-panel-shows-names into main
4 files +145 -9
app/Analytics/regions.tsmodified+77-0
Changes to app/Analytics/regions.ts
@@ -27,6 +27,8 @@
2727 */
2828
2929/** What a grouped region query hands back, before folding. */
30import SUBDIVISIONS from './subdivisions.json'
31
3032export interface RegionCount {
3133 region: string | null
3234 views: number | string
@@ -109,3 +111,78 @@ export function foldRegions(rows: readonly RegionCount[], floor: number, limit:
109111 withheld,
110112 }
111113}
114
115/**
116 * `US-CA` -> `California`, `PH-07` -> `Central Visayas`.
117 *
118 * ## Where the names come from
119 *
120 * `subdivisions.json` is a name -> code table per country, generated from the
121 * DB-IP City Lite file so `geo.ts` can turn the English name that database
122 * carries into an ISO code on the way in. Read the other way it is a code ->
123 * name table, which is what a reader needs on the way out.
124 *
125 * The panel used to print the bare code, and the comment where it did explained
126 * why: spelling a subdivision out "would mean shipping a table of every ISO
127 * 3166-2 subdivision name — some four thousand of them". That table is already
128 * shipped, and has been since region geo started working at all. `US-CA` at
129 * least reads as California by luck; `PH-07` reads as nothing, and `CA` beside a
130 * flag is equally the code for Canada.
131 *
132 * ## Why a name can be ambiguous, and which one wins
133 *
134 * DB-IP carries aliases, so 384 codes have more than one spelling — `AL-03` is
135 * both `Elbasan` and `Elbasan County`, `DE-BW` both `Baden-Württemberg` and the
136 * unaccented `Baden-Wurttemberg`. Reversing the map therefore needs a rule, and
137 * the obvious one — shortest wins — picks the mangled German.
138 *
139 * So: the spelling with the most non-ASCII characters wins, and the shortest
140 * breaks the tie. That is "prefer the properly accented name, then the plainest
141 * form of it", which gives Baden-Württemberg, Berlin over State of Berlin,
142 * Elbasan over Elbasan County, and Bicol over Bicol Region.
143 *
144 * Built once on first use: 194 countries and 3,234 subdivisions is not work to
145 * repeat per row, and a dashboard that never opens the panel never pays for it.
146 */
147let subdivisionNames: Map<string, string> | null = null
148
149function namesByCode(): Map<string, string> {
150 if (subdivisionNames)
151 return subdivisionNames
152
153 // Accented spelling first, then the shortest, so the winner is deterministic
154 // whatever order the table is in.
155 const preferred = (a: string, b: string): string => {
156 const accents = (s: string) => [...s].filter(c => c.charCodeAt(0) > 127).length
157 if (accents(a) !== accents(b))
158 return accents(a) > accents(b) ? a : b
159 return a.length <= b.length ? a : b
160 }
161
162 const table = SUBDIVISIONS as Record<string, Record<string, string>>
163 const built = new Map<string, string>()
164 for (const [country, names] of Object.entries(table)) {
165 for (const [name, code] of Object.entries(names)) {
166 const key = `${country}-${code}`
167 const held = built.get(key)
168 built.set(key, held ? preferred(held, name) : name)
169 }
170 }
171
172 subdivisionNames = built
173 return built
174}
175
176/**
177 * The English name for an ISO 3166-2 code, or null when the table has none.
178 *
179 * Null rather than a guess, and the caller keeps showing the code: a subdivision
180 * the table does not know is still a real place the visitor came from, and a row
181 * reading `PH-XX` is honest where an invented name would not be.
182 */
183export function subdivisionName(code: string | null | undefined): string | null {
184 const parts = splitRegion(code)
185 if (!parts)
186 return null
187 return namesByCode().get(`${parts.country}-${parts.subdivision}`) ?? null
188}