doxa-maps/library/utils/languageLabel.js
Doxa Maps 891f7535d8
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Update from source 53d386262
2026-09-28 14:27:42 +00:00

67 lines
3.2 KiB
JavaScript

/**
* languageLabel.js — turn the API's comma-inverted language label into the natural reading.
*
* The pray-tools API returns primary_language.label in an INDEX ordering, surname-first:
* "Arabic, Shihhi" where a reader expects "Shihhi Arabic". It is the ordering a printed language
* catalogue uses so that every Arabic variety files together, and it is wrong in a sentence.
* 725 of the 12,379 rows carry one (measured on data/people-groups/people_groups.csv, LanguageName,
* the same vocabulary), so this is not an edge case: it is one row in seventeen.
*
* ★ THE SPLIT IS PARENTHESIS-AWARE, AND THAT IS THE WHOLE REASON THIS IS A MODULE. 13 names carry a
* comma INSIDE a parenthetical -- "Boano (Indonesia, Central Sulawesi)", "Kango (DRC, Tshopo district)",
* "Mari (PNG, Madang Province)". A naive label.split(',') reverses those into
* "Central Sulawesi) Boano (Indonesia": unbalanced, unreadable, and printed. The comma in those names
* is not an inversion marker, it separates a disambiguating region, so only a TOP-LEVEL comma counts.
*
* ★ THIS IS HARMLESS IN A LOOKUP AND FATAL IN A SENTENCE, which is why the rule lives here rather than
* staying where it was. useLanguageFamilyLegendData built the same reversal to produce a KEY for
* langFamilyByLanguage, where a garbage candidate simply misses and the next one is tried -- the
* corruption never reaches a reader. describeGroup interpolates the result into printed prose, where
* there is no such safety net. Both now split through splitLanguageLabelParts so the two cannot drift.
*/
/**
* Split a label on its TOP-LEVEL commas — commas inside parentheses are text, not separators.
* "Arabic, Shihhi" -> ['Arabic', 'Shihhi']; "Boano (Indonesia, Central Sulawesi)" -> one part.
* @param {string} label
* @returns {string[]} trimmed, empty parts dropped ("Kango (DRC, , Bas-Uélé district)" is real data)
*/
export function splitLanguageLabelParts(label) {
if (!label || typeof label !== 'string') return []
const parts = []
let depth = 0
let cur = ''
for (const ch of label) {
if (ch === '(') depth++
else if (ch === ')') depth = Math.max(0, depth - 1)
if (ch === ',' && depth === 0) {
parts.push(cur)
cur = ''
} else {
cur += ch
}
}
parts.push(cur)
return parts.map((s) => s.trim()).filter(Boolean)
}
/**
* The natural reading of a language label, for display.
* Un-inverted when the label is inverted; returned unchanged when it is not, so it is safe to call on
* any label from any source. A trailing parenthetical stays at the END, where it qualifies the whole
* name: "Naga, Tangkhul (India)" -> "Tangkhul Naga (India)", not "Tangkhul (India) Naga".
* @param {string} label
* @returns {string} '' for a missing or non-string label
*/
export function naturalLanguageLabel(label) {
if (!label || typeof label !== 'string') return ''
const parts = splitLanguageLabelParts(label)
if (parts.length < 2) return label.trim()
const last = parts[parts.length - 1]
const paren = last.match(/\s*(\(.*\))$/)
if (paren) parts[parts.length - 1] = last.slice(0, paren.index).trim()
const reversed = parts.filter(Boolean).reverse().join(' ')
return paren ? `${reversed} ${paren[1]}` : reversed
}
export default naturalLanguageLabel