mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(mental-health): read the country from the region subtag for every language
A tag such as en-BE, fr-CH or en-AT fell through to the international crisis set because region detection hung on the language. The region subtag now decides first for every language; bare language codes keep the combined sets, and the web's folded pt-BR bundle stays ambiguous. Refs #1338
This commit is contained in:
1 parent
d3401b5b89
commit
a812fb699f
3 files changed
+106
-22
No files matched your search
@@ -10,6 +10,12 @@
|
||||
range, so the first digit turned into 1900. Both fields now keep what is
|
||||
typed and commit it once it is a valid value, and leaving the month field
|
||||
pads "2019-3" to "2019-03". (#1334)
|
||||
- **Crisis lines for the country you are in.** A questionnaire answered
|
||||
from a phone set to English in Belgium (`en-BE`), French in Switzerland,
|
||||
or English in Austria got the international crisis set, because the
|
||||
country was only read for some languages. The region in the language tag
|
||||
now picks the country's lines whatever the language; a bare language
|
||||
code keeps the combined sets as before. (#1338)
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -86,6 +86,49 @@ describe("crisisResourcesForLocale", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves the country from the region subtag whatever the language", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["en-BE", "zelfmoordlijn1813"],
|
||||
["fr-BE", "zelfmoordlijn1813"],
|
||||
["en-NL", "zelfmoordpreventie113"],
|
||||
["en-AT", "telefonSeelsorgeAt"],
|
||||
["fr-CH", "dargeboteneHand"],
|
||||
["it_CH", "dargeboteneHand"],
|
||||
["en-DE", "telefonSeelsorge"],
|
||||
["de-DE", "telefonSeelsorge"],
|
||||
["en-PT", "sns24"],
|
||||
["en-BR", "cvv"],
|
||||
["es-US", "lifeline988"],
|
||||
["zh-Hant-CH", "dargeboteneHand"],
|
||||
];
|
||||
for (const [locale, first] of cases) {
|
||||
const set = crisisResourcesForLocale(locale);
|
||||
expect(set.resources[0]?.id, locale).toBe(first);
|
||||
}
|
||||
// A country set never carries another country's lines.
|
||||
expect(
|
||||
crisisResourcesForLocale("en-DE").resources.map((r) => r.id),
|
||||
).not.toContain("dargeboteneHand");
|
||||
expect(crisisResourcesForLocale("en-AT").emergencyNumber).toBe("112");
|
||||
expect(crisisResourcesForLocale("es-US").emergencyNumber).toBe("911");
|
||||
});
|
||||
|
||||
it("keeps the language rules when the region has no set of its own", () => {
|
||||
expect(
|
||||
crisisResourcesForLocale("de-LI").resources.map((r) => r.id),
|
||||
).toEqual(crisisResourcesForLocale("de").resources.map((r) => r.id));
|
||||
expect(crisisResourcesForLocale("nl-SR").resources[0]?.id).toBe(
|
||||
"zelfmoordpreventie113",
|
||||
);
|
||||
expect(crisisResourcesForLocale("nl-SR").resources).toHaveLength(3);
|
||||
for (const locale of ["en", "en-GB", "fr-FR", "", null, undefined]) {
|
||||
expect(
|
||||
crisisResourcesForLocale(locale).resources.map((r) => r.id),
|
||||
String(locale),
|
||||
).toEqual(crisisResourcesForLocale("xx").resources.map((r) => r.id));
|
||||
}
|
||||
});
|
||||
|
||||
it("names every crisis resource in every language", () => {
|
||||
// History re-derives the set from the row's stored locale and renders it
|
||||
// in whatever language the reader uses now.
|
||||
|
||||
@@ -195,33 +195,68 @@ const NL_BE: CrisisResourceSet = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a crisis-resource set from the user's locale. Region detection is
|
||||
* coarse (locale prefix) and defaults to the international set — the goal is to
|
||||
* always surface SOMETHING actionable, never to be exhaustive.
|
||||
* Country sets reached by a region subtag, whatever the language. Someone in
|
||||
* Belgium with the phone in English sends "en-BE" and gets Belgium's line,
|
||||
* not the international set.
|
||||
*/
|
||||
const BY_REGION: Readonly<Record<string, CrisisResourceSet>> = {
|
||||
at: AT,
|
||||
be: BE,
|
||||
br: BR,
|
||||
ch: CH,
|
||||
de: DE,
|
||||
nl: NL,
|
||||
pt: PT,
|
||||
us: US,
|
||||
};
|
||||
|
||||
/**
|
||||
* The region subtag of a BCP 47 tag ("en-BE" -> "be", "zh-Hant-TW" -> "tw"),
|
||||
* or null for a bare language code. Only a two-letter subtag after the
|
||||
* language counts; a script subtag ("Hant") is skipped.
|
||||
*/
|
||||
function regionSubtag(tag: string): string | null {
|
||||
const parts = tag.split("-");
|
||||
for (const part of parts.slice(1)) {
|
||||
if (/^[a-z]{2}$/.test(part)) return part;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a crisis-resource set from the user's locale. The region subtag
|
||||
* decides first: any language with a region we have a set for ("en-BE",
|
||||
* "fr-CH", "en-AT", "de-DE") gets that country's set. Without a region the
|
||||
* language decides, coarsely, and anything unknown gets the international
|
||||
* set: the goal is to always surface SOMETHING actionable, never to be
|
||||
* exhaustive.
|
||||
*
|
||||
* A region-qualified tag ("de-AT", "de_CH", "pt-PT", …) resolves to the exact
|
||||
* country set when the caller has one to give us. The web stores the app
|
||||
* locale, which is a bare "de", "nl" or the folded "pt-BR", so it lands on the
|
||||
* combined sets: never assume Germany, the Netherlands or Brazil when the
|
||||
* region is unknown.
|
||||
* One tag is not a region signal: "pt-BR" is the web's folded Portuguese
|
||||
* bundle (see PT_BR_AMBIGUOUS), so it keeps the ambiguous set. The web stores
|
||||
* the app locale, which is otherwise a bare "de", "nl", "en", so it lands on
|
||||
* the combined sets: never assume Germany, the Netherlands or Brazil when the
|
||||
* region is unknown. A bare country code ("at", "ch", "us", "br", "be")
|
||||
* still resolves to that country.
|
||||
*/
|
||||
export function crisisResourcesForLocale(
|
||||
locale: string | null | undefined,
|
||||
): CrisisResourceSet {
|
||||
const lc = (locale ?? "").toLowerCase().replace("_", "-");
|
||||
if (lc === "de-at" || lc === "at") return AT;
|
||||
if (lc === "de-ch" || lc === "ch") return CH;
|
||||
if (lc.startsWith("de")) return DE_AT_CH;
|
||||
if (lc.startsWith("en-us") || lc === "us") return US;
|
||||
if (lc === "br") return BR;
|
||||
if (lc === "pt-pt") return PT;
|
||||
// "pt-BR" from the web is the folded app locale, not a region signal; see
|
||||
// PT_BR_AMBIGUOUS. Any other Portuguese tag (pt, pt-AO, pt-MZ) lands here
|
||||
// too, and still gets 112 and the worldwide directory first.
|
||||
if (lc === "pt" || lc.startsWith("pt-")) return PT_BR_AMBIGUOUS;
|
||||
if (lc === "nl-nl") return NL;
|
||||
if (lc === "nl-be" || lc === "be") return BE;
|
||||
const lc = (locale ?? "").trim().toLowerCase().replaceAll("_", "-");
|
||||
if (lc === "pt-br") return PT_BR_AMBIGUOUS;
|
||||
const region = regionSubtag(lc);
|
||||
if (region !== null) {
|
||||
const byRegion = BY_REGION[region];
|
||||
if (byRegion) return byRegion;
|
||||
}
|
||||
if (lc === "at" || lc === "ch" || lc === "us" || lc === "br" || lc === "be") {
|
||||
return BY_REGION[lc];
|
||||
}
|
||||
const language = lc.split("-")[0];
|
||||
if (language === "de") return DE_AT_CH;
|
||||
// Any other Portuguese tag (pt, pt-AO, pt-MZ) still gets 112 and the
|
||||
// worldwide directory first.
|
||||
if (language === "pt") return PT_BR_AMBIGUOUS;
|
||||
// The bare "nl" the web stores, and any other Dutch tag: both countries.
|
||||
if (lc === "nl" || lc.startsWith("nl-")) return NL_BE;
|
||||
if (language === "nl") return NL_BE;
|
||||
return INTERNATIONAL;
|
||||
}
|
||||
Reference in new issue
Block a user