diff --git a/messages/de.json b/messages/de.json index affff6eed..81db8b629 100644 --- a/messages/de.json +++ b/messages/de.json @@ -11064,7 +11064,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (rund um die Uhr, kostenlos, vertraulich)" + "name": "CVV, Centro de Valorização da Vida (Brasilien, rund um die Uhr, kostenlos, vertraulich)" + }, + "samu": { + "name": "SAMU, medizinischer Notruf (Brasilien)" + }, + "sns24": { + "name": "SNS 24, psychologische Beratung (Portugal, rund um die Uhr, Option 4 wählen)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugal, täglich 15:30 bis 0:30 Uhr)" } }, "instrumentDescription": { diff --git a/messages/en.json b/messages/en.json index 6af65dcf7..2820dd01f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -11074,7 +11074,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (24/7, free, confidential)" + "name": "CVV, Centro de Valorização da Vida (Brazil, 24/7, free, confidential)" + }, + "samu": { + "name": "SAMU medical emergency service (Brazil)" + }, + "sns24": { + "name": "SNS 24 psychological counselling (Portugal, 24/7, choose option 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugal, daily 3.30 pm to 12.30 am)" } }, "instrumentDescription": { diff --git a/messages/es.json b/messages/es.json index 0eb0dbb64..594405877 100644 --- a/messages/es.json +++ b/messages/es.json @@ -11064,7 +11064,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (24/7, gratuito y confidencial)" + "name": "CVV, Centro de Valorização da Vida (Brasil, 24/7, gratuito y confidencial)" + }, + "samu": { + "name": "SAMU, urgencias médicas (Brasil)" + }, + "sns24": { + "name": "SNS 24, asesoramiento psicológico (Portugal, 24/7, opción 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugal, todos los días de 15:30 a 0:30)" } }, "instrumentDescription": { diff --git a/messages/fr.json b/messages/fr.json index 36c24f9a8..a15af2316 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -11064,7 +11064,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (24h/24, 7j/7, gratuit et confidentiel)" + "name": "CVV, Centro de Valorização da Vida (Brésil, 24h/24, 7j/7, gratuit et confidentiel)" + }, + "samu": { + "name": "SAMU, urgences médicales (Brésil)" + }, + "sns24": { + "name": "SNS 24, soutien psychologique (Portugal, 24h/24, 7j/7, option 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugal, tous les jours de 15h30 à 0h30)" } }, "instrumentDescription": { diff --git a/messages/it.json b/messages/it.json index 435ecb0eb..74164001e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -11064,7 +11064,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (24/7, gratuito e riservato)" + "name": "CVV, Centro de Valorização da Vida (Brasile, 24/7, gratuito e riservato)" + }, + "samu": { + "name": "SAMU, emergenza sanitaria (Brasile)" + }, + "sns24": { + "name": "SNS 24, supporto psicologico (Portogallo, 24/7, opzione 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portogallo, tutti i giorni dalle 15:30 alle 0:30)" } }, "instrumentDescription": { diff --git a/messages/ko.json b/messages/ko.json index 71c18317e..5462b7e39 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -11064,7 +11064,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (24시간 연중무휴, 무료, 비밀 보장)" + "name": "CVV, Centro de Valorização da Vida (브라질, 24시간 연중무휴, 무료, 비밀 보장)" + }, + "samu": { + "name": "SAMU 응급의료 서비스 (브라질)" + }, + "sns24": { + "name": "SNS 24 심리 상담 (포르투갈, 24시간 연중무휴, 4번 선택)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (포르투갈, 매일 15:30~00:30)" } }, "instrumentDescription": { diff --git a/messages/nl.json b/messages/nl.json index f913ab86a..f5877d23f 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -11022,7 +11022,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (dag en nacht, gratis, vertrouwelijk)" + "name": "CVV, Centro de Valorização da Vida (Brazilië, dag en nacht, gratis, vertrouwelijk)" + }, + "samu": { + "name": "SAMU, medische spoedhulp (Brazilië)" + }, + "sns24": { + "name": "SNS 24, psychologische hulp (Portugal, dag en nacht, keuze 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugal, dagelijks van 15.30 tot 0.30 uur)" } }, "instrumentDescription": { diff --git a/messages/pl.json b/messages/pl.json index 21b5dca8b..08a8e9e82 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -11064,7 +11064,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (całodobowo, bezpłatnie i poufnie)" + "name": "CVV, Centro de Valorização da Vida (Brazylia, całodobowo, bezpłatnie i poufnie)" + }, + "samu": { + "name": "SAMU, pogotowie ratunkowe (Brazylia)" + }, + "sns24": { + "name": "SNS 24, wsparcie psychologiczne (Portugalia, całodobowo, opcja 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugalia, codziennie od 15:30 do 0:30)" } }, "instrumentDescription": { diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 107e7505a..f57008858 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -11022,7 +11022,16 @@ "name": "Crisis Text Line" }, "cvv": { - "name": "CVV, Centro de Valorização da Vida (24 horas, gratuito, sigiloso)" + "name": "CVV, Centro de Valorização da Vida (Brasil, 24 horas, gratuito, sigiloso)" + }, + "samu": { + "name": "SAMU, urgência médica (Brasil)" + }, + "sns24": { + "name": "SNS 24, aconselhamento psicológico (Portugal, 24 horas, opção 4)" + }, + "sosVozAmiga": { + "name": "SOS Voz Amiga (Portugal, todos os dias das 15h30 às 0h30)" } }, "instrumentDescription": { diff --git a/src/lib/mental-health/__tests__/crisis-resources.test.ts b/src/lib/mental-health/__tests__/crisis-resources.test.ts index af7c5df12..535faf119 100644 --- a/src/lib/mental-health/__tests__/crisis-resources.test.ts +++ b/src/lib/mental-health/__tests__/crisis-resources.test.ts @@ -6,8 +6,31 @@ import { getServerTranslator } from "@/lib/i18n/server-translator"; import { crisisResourcesForLocale } from "../crisis-resources"; describe("crisisResourcesForLocale", () => { - it("gives a Brazilian Portuguese reader CVV and SAMU", () => { - const set = crisisResourcesForLocale("pt-BR"); + it("gives a Portuguese reader with no region 112 first, then each country's lines by name", () => { + // The web stores the folded app locale: someone in Portugal reads the + // pt-BR bundle too, so "pt-BR" is no evidence of Brazil. + for (const locale of ["pt-BR", "pt_BR", "pt", "pt-AO"]) { + const set = crisisResourcesForLocale(locale); + expect(set.emergencyNumber, locale).toBe("112"); + expect(set.resources.map((r) => r.id)).toEqual([ + "findahelpline", + "cvv", + "samu", + "sns24", + "sosVozAmiga", + ]); + } + const { t } = getServerTranslator("pt-BR"); + expect(t("mentalHealth.crisisResource.cvv.name")).toContain("Brasil"); + expect(t("mentalHealth.crisisResource.samu.name")).toContain("Brasil"); + expect(t("mentalHealth.crisisResource.sns24.name")).toContain("Portugal"); + expect(t("mentalHealth.crisisResource.sosVozAmiga.name")).toContain( + "Portugal", + ); + }); + + it("gives an explicit Brazilian region CVV and SAMU", () => { + const set = crisisResourcesForLocale("br"); expect(set.emergencyNumber).toBe("192"); expect(set.resources[0]).toEqual({ id: "cvv", @@ -15,12 +38,35 @@ describe("crisisResourcesForLocale", () => { }); }); - it("keeps the international set for Portuguese outside Brazil", () => { - // 188 does not connect from Portugal. - for (const locale of ["pt", "pt-PT"]) { + it("gives an explicit Portuguese region 112, SNS 24 and SOS Voz Amiga, never Brazil's lines", () => { + for (const locale of ["pt-PT", "pt_PT", "pt-pt"]) { const set = crisisResourcesForLocale(locale); expect(set.emergencyNumber).toBe("112"); - expect(set.resources.map((r) => r.id)).not.toContain("cvv"); + expect(set.resources[0]).toEqual({ + id: "sns24", + contacts: ["808 24 24 24"], + }); + const ids = set.resources.map((r) => r.id); + expect(ids).toContain("sosVozAmiga"); + expect(ids).not.toContain("cvv"); + expect(ids).not.toContain("samu"); + } + }); + + 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. + const ids = new Set( + ["pt-BR", "pt-PT", "br", "de", "de-AT", "de-CH", "en-US", "fr"].flatMap( + (l) => crisisResourcesForLocale(l).resources.map((r) => r.id), + ), + ); + for (const locale of locales) { + const { t } = getServerTranslator(locale); + for (const id of ids) { + const key = `mentalHealth.crisisResource.${id}.name`; + expect(t(key), `${locale} ${key}`).not.toBe(key); + } } }); diff --git a/src/lib/mental-health/crisis-resources.ts b/src/lib/mental-health/crisis-resources.ts index 386d2680b..e498b23d5 100644 --- a/src/lib/mental-health/crisis-resources.ts +++ b/src/lib/mental-health/crisis-resources.ts @@ -99,9 +99,10 @@ const US: CrisisResourceSet = { }; /** - * Brazil (pt-BR) — CVV by phone and chat, SAMU as the emergency number. - * Both numbers are free and answer around the clock (cvv.org.br; the - * Ministry of Health's SAMU 192 page). + * Brazil (an explicit region signal: a bare "br" or "pt_BR"-style tag a + * native client sends from the device region) — CVV by phone and chat, SAMU + * as the emergency number. Both numbers are free and answer around the clock + * (cvv.org.br; the Ministry of Health's SAMU 192 page). */ const BR: CrisisResourceSet = { emergencyNumber: "192", @@ -111,27 +112,65 @@ const BR: CrisisResourceSet = { ], }; +/** + * Portugal (explicit "pt-PT" / "pt_PT" / "pt-pt"). 112 is the emergency + * number. SNS 24 (808 24 24 24) runs the national psychological counselling + * line around the clock as menu option 4 and transfers a call at risk to + * INEM; SOS Voz Amiga answers daily from 15:30 to 00:30 (sosvozamiga.org). + */ +const PT: CrisisResourceSet = { + emergencyNumber: "112", + resources: [ + { id: "sns24", contacts: ["808 24 24 24"] }, + { id: "sosVozAmiga", contacts: ["213 544 545", "sosvozamiga.org"] }, + { id: "findahelpline", contacts: ["findahelpline.com"] }, + ], +}; + +/** + * Portuguese with no region signal. The app ships one Portuguese bundle, + * `pt-BR`, and `localeFromLanguageTag` folds `pt-PT`, `pt-AO` and bare `pt` + * into it, so a stored "pt-BR" says only that the reader reads Portuguese: + * it is as likely to be someone in Lisbon as in São Paulo. The account keeps + * no country to tell them apart. Following DE_AT_CH, the set leads with 112 + * (the emergency number across Portugal and the EU, which mobile networks in + * Brazil also route to the police) and the worldwide directory, then names + * every country's own lines with the country in the label, so nothing reads + * as if it reached everyone. + */ +const PT_BR_AMBIGUOUS: CrisisResourceSet = { + emergencyNumber: "112", + resources: [ + { id: "findahelpline", contacts: ["findahelpline.com"] }, + ...BR.resources.filter((r) => r.id !== "findahelpline"), + { id: "samu", contacts: ["192"] }, + ...PT.resources.filter((r) => r.id !== "findahelpline"), + ], +}; + /** * 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. * - * A region-qualified tag ("de-AT", "de_CH", …) resolves to the exact country - * set when the caller has one to give us. `User.locale` today only ever - * stores the bare short code, so in practice every "de" hits the DE_AT_CH - * branch below — that is the safe default for an unqualified "de": never - * assume Germany when the region is unknown. + * 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" or the folded "pt-BR", so it lands on the + * combined sets: never assume Germany or Brazil when the region is unknown. */ export function crisisResourcesForLocale( locale: string | null | undefined, ): CrisisResourceSet { - const lc = (locale ?? "").toLowerCase(); - if (lc === "de-at" || lc === "de_at" || lc === "at") return AT; - if (lc === "de-ch" || lc === "de_ch" || lc === "ch") return CH; + 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 === "en-us" || lc.startsWith("en_us") || lc === "us") return US; - // Brazilian Portuguese only: CVV's 188 does not connect from Portugal, so a - // bare "pt" or "pt-PT" keeps the international set. - if (lc === "pt-br" || lc === "pt_br" || lc === "br") return BR; + 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; return INTERNATIONAL; }