describeGroup(group, locale, opts) -> one localized sentence pair for a people
group's profile row: where they live, what they speak, and how reached they are.
Pure, exported, no refs or fetch; strings and the country resolver are injected.
The level is phrased from GSEC, a number, never from GSECDescription, which is
English prose the API hands us -- interpolating it would leave the widest column
of every non-English packet in English.
THE LANGUAGE FIELD WAS BOTH WRONG FIELDS AT ONCE.
primary_language is in normalize.js's NESTED_FIELDS, so the API's { value, label }
is flattened to its VALUE: an ISO 639-3 code. Reading it prints "and speak abf".
The name is in primary_language_label, and it arrives INDEX-ORDERED -- "Arabic,
Shihhi" for what a reader calls "Shihhi Arabic". 725 of the 12,379 rows carry an
inverted label, one row in seventeen.
The un-inversion is not a second copy: useLanguageFamilyLegendData already built
one to make a lookup key, so the split moves to library/utils/languageLabel.js and
both call it. Doing that surfaced why the two uses are not the same use -- 13 real
names carry a comma INSIDE a parenthetical ("Boano (Indonesia, Central Sulawesi)"),
and a naive split reverses those into "Central Sulawesi) Boano (Indonesia". As a
lookup key that garbage simply misses and the next candidate is tried; printed in a
sentence there is no such safety net. The shared split is top-level-only, and a
trailing parenthetical is hoisted past the reversal so "Naga, Tangkhul (India)"
reads "Tangkhul Naga (India)".
The legend's own behaviour is unchanged, checked rather than assumed: old and new
lookup compared over all 3,498 labels in langFamilyByLanguage plus every
LanguageName in the dataset, 0 differences.
countryName.js is NOT imported. It resolves only through the Vite '@map/' alias and
imports two JSON files without an import attribute -- fine in a bundle, fatal to a
node gate. Importing it would make this module unloadable by the test that proves it
right in eleven languages, so the renderer passes it in, and a missing injection
degrades down countryName.js's own ladder (localized -> English label -> alpha-3)
rather than blanking the clause.
check-describe-group asserts every GSEC level, each missing field, both locales,
truncation, the inverted label, the parenthetical that must not be shredded, and
that a bare language code is dropped rather than printed. Negative-tested: reading
primary_language, skipping the un-inversion, a naive split, and dropping the
parenthetical hoist each turn it red; restored, it is green.
Not yet wired into the renderer and not yet in gates:pre -- register with
"check:describe-group": "node internal/check-describe-group.mjs".
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
|