src/lib/hangul.ts (2161B)
1 /** Jamo, compatibility jamo, extended jamo and the precomposed syllable block. */
2 const HANGUL = /[\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uAC00-\uD7A3\uD7B0-\uD7FF]/;
3
4 /** True when any of the strings would put a Hangul glyph on the page. */
5 export const hasHangul = (...parts: (string | null | undefined)[]): boolean =>
6 parts.some((s) => !!s && HANGUL.test(s));
7
8 /**
9 * `lang` for a single string, returned only when it differs from the document.
10 * Re-declaring the document language on a child makes a screen reader switch voices
11 * for no reason, so an agreeing value is dropped.
12 */
13 export const langOf = (s: string, doc: 'en' | 'ko'): 'en' | 'ko' | undefined => {
14 const l: 'en' | 'ko' = HANGUL.test(s) ? 'ko' : 'en';
15 return l === doc ? undefined : l;
16 };
17
18 const HANGUL_ALL = new RegExp(HANGUL.source, 'g');
19
20 /**
21 * The language a post is written in: the article's lang, and the baseline satteriLang
22 * compares passages and headings against. Both call this on the same body, so they cannot
23 * disagree.
24 *
25 * It follows the body, not the title, and is measured on prose only. The label drives
26 * leading, line breaking and speech, so it has to describe what is read. Keying off the
27 * title would mislabel the seven Korean posts with Latin titles, VTZero and journalctl
28 * among them; counting Hangul against every non-space character would mislabel the Korean
29 * technical posts, whose shell transcripts and URLs swamp the prose — measured that way
30 * sysmon lands at 0.14 and journalctl at 0.29, both plainly Korean documents. With fences
31 * and link targets dropped and Hangul weighed against Latin letters, the corpus splits with
32 * nothing near the line: the two English essays sit at 0.25 and 0.28, and the other 28
33 * posts all sit at 0.49 or above.
34 */
35 export function docLang(md: string): 'en' | 'ko' {
36 // Linear on any input; see the note on LINK in prose.ts.
37 const prose = md.replace(/```[\s\S]*?```/g, '').replace(/\[[^\][]*\]\((?:[^()]|\([^()]*\))*\)/g, '');
38 const kor = (prose.match(HANGUL_ALL) ?? []).length;
39 const lat = (prose.match(/[A-Za-z]/g) ?? []).length;
40 return kor / Math.max(1, kor + lat) > 0.4 ? 'ko' : 'en';
41 }