|
| 1 | +/** |
| 2 | + * Input-script detection for detect(): the walk only transliterates |
| 3 | + * systems whose source script matches the input — loading all 288 maps |
| 4 | + * per request exceeds the Workers CPU budget (1102s in production). |
| 5 | + * Ranges cover exactly the source scripts present in the corpus. |
| 6 | + */ |
| 7 | + |
| 8 | +type ScriptName = string |
| 9 | + |
| 10 | +const SCRIPT_RANGES: ReadonlyArray< |
| 11 | + readonly [ScriptName, ReadonlyArray<readonly [number, number]>] |
| 12 | +> = [ |
| 13 | + [ |
| 14 | + "Latn", |
| 15 | + [ |
| 16 | + [0x41, 0x5a], |
| 17 | + [0x61, 0x7a], |
| 18 | + [0xc0, 0xff], |
| 19 | + [0x100, 0x24f], |
| 20 | + ], |
| 21 | + ], |
| 22 | + [ |
| 23 | + "Grek", |
| 24 | + [ |
| 25 | + [0x370, 0x3ff], |
| 26 | + [0x1f00, 0x1fff], |
| 27 | + ], |
| 28 | + ], |
| 29 | + ["Cyrl", [[0x400, 0x52f]]], |
| 30 | + ["Armn", [[0x530, 0x58f]]], |
| 31 | + ["Hebr", [[0x590, 0x5ff]]], |
| 32 | + [ |
| 33 | + "Arab", |
| 34 | + [ |
| 35 | + [0x600, 0x6ff], |
| 36 | + [0x750, 0x77f], |
| 37 | + [0xfb50, 0xfdff], |
| 38 | + [0xfe70, 0xfeff], |
| 39 | + ], |
| 40 | + ], |
| 41 | + ["Deva", [[0x900, 0x97f]]], |
| 42 | + ["Beng", [[0x980, 0x9ff]]], |
| 43 | + ["Guru", [[0xa00, 0xa7f]]], |
| 44 | + ["Gujr", [[0xa80, 0xaff]]], |
| 45 | + ["Orya", [[0xb00, 0xb7f]]], |
| 46 | + ["Taml", [[0xb80, 0xbff]]], |
| 47 | + ["Telu", [[0xc00, 0xc7f]]], |
| 48 | + ["Kana", [[0xc80, 0xcff]]], |
| 49 | + ["Mlym", [[0xd00, 0xd7f]]], |
| 50 | + ["Sinh", [[0xd80, 0xdff]]], |
| 51 | + ["Thaa", [[0x780, 0x7bf]]], |
| 52 | + ["Thai", [[0xe00, 0xe7f]]], |
| 53 | + [ |
| 54 | + "Geor", |
| 55 | + [ |
| 56 | + [0x10a0, 0x10ff], |
| 57 | + [0x1c90, 0x1cbf], |
| 58 | + ], |
| 59 | + ], |
| 60 | + ["Ethi", [[0x1200, 0x137f]]], |
| 61 | + ["Mong", [[0x1800, 0x18af]]], |
| 62 | + [ |
| 63 | + "Hang", |
| 64 | + [ |
| 65 | + [0x1100, 0x11ff], |
| 66 | + [0x3130, 0x318f], |
| 67 | + [0xac00, 0xd7af], |
| 68 | + ], |
| 69 | + ], |
| 70 | + [ |
| 71 | + "Hrkt", |
| 72 | + [ |
| 73 | + [0x3040, 0x309f], |
| 74 | + [0x30a0, 0x30ff], |
| 75 | + [0x31f0, 0x31ff], |
| 76 | + ], |
| 77 | + ], |
| 78 | + [ |
| 79 | + "Hani", |
| 80 | + [ |
| 81 | + [0x2e80, 0x2eff], |
| 82 | + [0x3400, 0x4dbf], |
| 83 | + [0x4e00, 0x9fff], |
| 84 | + [0xf900, 0xfaff], |
| 85 | + [0x20000, 0x2a6df], |
| 86 | + ], |
| 87 | + ], |
| 88 | +] |
| 89 | + |
| 90 | +function scriptOfCodePoint(cp: number): ScriptName | null { |
| 91 | + for (const [script, ranges] of SCRIPT_RANGES) { |
| 92 | + for (const [lo, hi] of ranges) { |
| 93 | + if (cp >= lo && cp <= hi) return script |
| 94 | + } |
| 95 | + } |
| 96 | + return null |
| 97 | +} |
| 98 | + |
| 99 | +export function detectInputScript(input: string): ScriptName | null { |
| 100 | + const counts = new Map<ScriptName, number>() |
| 101 | + for (const ch of input) { |
| 102 | + const script = scriptOfCodePoint(ch.codePointAt(0)!) |
| 103 | + if (script) counts.set(script, (counts.get(script) ?? 0) + 1) |
| 104 | + } |
| 105 | + let best: ScriptName | null = null |
| 106 | + let bestWeighted = 0 |
| 107 | + for (const [script, n] of counts) { |
| 108 | + // A Latin majority is the weakest signal: one stray non-Latin letter |
| 109 | + // outranks any amount of Latin. |
| 110 | + const weighted = script === "Latn" ? n - 0.5 : n |
| 111 | + if (weighted > bestWeighted) { |
| 112 | + best = script |
| 113 | + bestWeighted = weighted |
| 114 | + } |
| 115 | + } |
| 116 | + return best |
| 117 | +} |
| 118 | + |
| 119 | +// Detected script → corpus source_script values that can legitimately |
| 120 | +// process it ("Kana" is Kannada in this corpus; "Kore" is the mixed |
| 121 | +// Hangul+Latin jamo family; "Hrkt" is Japanese kana). |
| 122 | +const FAMILIES: Readonly<Record<ScriptName, readonly string[]>> = { |
| 123 | + Latn: ["Latn"], |
| 124 | + Grek: ["Grek"], |
| 125 | + Cyrl: ["Cyrl"], |
| 126 | + Armn: ["Armn"], |
| 127 | + Hebr: ["Hebr"], |
| 128 | + Arab: ["Arab"], |
| 129 | + Deva: ["Deva"], |
| 130 | + Beng: ["Beng"], |
| 131 | + Guru: ["Guru"], |
| 132 | + Gujr: ["Gujr"], |
| 133 | + Orya: ["Orya"], |
| 134 | + Taml: ["Taml"], |
| 135 | + Telu: ["Telu"], |
| 136 | + Kana: ["Kana"], |
| 137 | + Mlym: ["Mlym"], |
| 138 | + Sinh: ["Sinh"], |
| 139 | + Thai: ["Thai"], |
| 140 | + Thaa: ["Thaa"], |
| 141 | + Ethi: ["Ethi"], |
| 142 | + Mong: ["Mong"], |
| 143 | + Geor: ["Geor"], |
| 144 | + Hang: ["Hang", "Kore"], |
| 145 | + Hrkt: ["Hrkt", "Kore"], |
| 146 | + Hani: ["Hani", "Hans", "Kore", "Hrkt"], |
| 147 | +} |
| 148 | + |
| 149 | +export function sourceMatchesScript(sourceScript: string | undefined, detected: string): boolean { |
| 150 | + const family = FAMILIES[detected] |
| 151 | + return !!sourceScript && !!family && family.includes(sourceScript) |
| 152 | +} |
0 commit comments