67 lines
3.2 KiB
JavaScript
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
|