diff --git a/infrastructure/eid-wallet/src/routes/(app)/ePassport/+page.svelte b/infrastructure/eid-wallet/src/routes/(app)/ePassport/+page.svelte index 42a222d48..f363ff60c 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/ePassport/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/ePassport/+page.svelte @@ -7,6 +7,7 @@ import { import { AppNav, IdentityCard } from "$lib/fragments"; import type { GlobalState } from "$lib/global"; import { m } from "$lib/i18n"; +import { getLocale } from "$lib/paraglide/runtime"; import { ButtonAction, CopyableEName } from "$lib/ui"; import { addCounterpartySignature, @@ -178,7 +179,7 @@ async function startKycUpgrade() { const { data } = await axios.post( new URL("/verification/v2", PUBLIC_PROVISIONER_URL).toString(), - {}, + { language: getLocale() }, { headers: { "x-shared-secret": PUBLIC_PROVISIONER_SHARED_SECRET, diff --git a/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte index 19c2ff2cf..7b8b1c581 100644 --- a/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte +++ b/infrastructure/eid-wallet/src/routes/(app)/main/legacy/KycUpgradeOverlay.svelte @@ -14,6 +14,7 @@ import { } from "$env/static/public"; import type { GlobalState } from "$lib/global"; import { m } from "$lib/i18n"; +import { getLocale } from "$lib/paraglide/runtime"; import { CopyableEName } from "$lib/ui"; import * as Button from "$lib/ui/Button"; import { capitalize } from "$lib/utils"; @@ -164,7 +165,7 @@ async function startKycUpgrade() { const { data } = await axios.post( new URL("/verification/v2", PUBLIC_PROVISIONER_URL).toString(), - {}, + { language: getLocale() }, { headers: { "x-shared-secret": PUBLIC_PROVISIONER_SHARED_SECRET, diff --git a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte index 8dbde4543..98f0de741 100644 --- a/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(auth)/onboarding/+page.svelte @@ -9,6 +9,7 @@ import { import { Hero } from "$lib/fragments"; import { GlobalState } from "$lib/global"; import { m } from "$lib/i18n"; +import { getLocale } from "$lib/paraglide/runtime"; import { pendingRecovery } from "$lib/stores/pendingRecovery"; import { ButtonAction, CopyableEName, LoadingSheet } from "$lib/ui"; import { capitalize, getCanonicalBindingDocString } from "$lib/utils"; @@ -458,7 +459,7 @@ const handleKycNext = async () => { const { data } = await axios.post( new URL("/verification/v2", PUBLIC_PROVISIONER_URL).toString(), - {}, + { language: getLocale() }, { headers: { "x-shared-secret": PUBLIC_PROVISIONER_SHARED_SECRET, diff --git a/infrastructure/eid-wallet/src/routes/(public)/recover/+page.svelte b/infrastructure/eid-wallet/src/routes/(public)/recover/+page.svelte index 4f966446b..770814676 100644 --- a/infrastructure/eid-wallet/src/routes/(public)/recover/+page.svelte +++ b/infrastructure/eid-wallet/src/routes/(public)/recover/+page.svelte @@ -206,6 +206,7 @@ async function startVerifiedRecovery() { "/recovery/start-session", PUBLIC_PROVISIONER_URL, ).toString(), + { language: getLocale() }, ); if (!data.verificationUrl) { diff --git a/infrastructure/evault-core/src/controllers/RecoveryController.ts b/infrastructure/evault-core/src/controllers/RecoveryController.ts index 96fb631f6..4a0edf582 100644 --- a/infrastructure/evault-core/src/controllers/RecoveryController.ts +++ b/infrastructure/evault-core/src/controllers/RecoveryController.ts @@ -4,6 +4,7 @@ import FormData from "form-data"; import { validate as uuidValidate } from "uuid"; import type { VerificationService } from "../services/VerificationService"; import { signAsProvisioner } from "../core/utils/provisioner-signer"; +import { verificationLanguage } from "../utils/verificationLanguage"; const diditClient = Axios.create({ baseURL: "https://verification.didit.me" }); @@ -225,11 +226,16 @@ export class RecoveryController { } try { + const language = verificationLanguage(req.body?.language); const verification = await this.verificationService.create({}); const { data: diditSession } = await diditClient.post( "/v3/session/", - { workflow_id: workflowId, vendor_data: verification.id }, + { + workflow_id: workflowId, + vendor_data: verification.id, + language, + }, { headers: { "x-api-key": apiKey, "Content-Type": "application/json" } }, ); @@ -238,7 +244,7 @@ export class RecoveryController { const verificationUrl: string = diditSession.verification_url ?? diditSession.url ?? - `https://verify.didit.me/session/${sessionToken}`; + `https://verify.didit.me/${language}/session/${sessionToken}`; await this.verificationService.findByIdAndUpdate(verification.id, { diditSessionId: sessionId, diff --git a/infrastructure/evault-core/src/controllers/VerificationController.ts b/infrastructure/evault-core/src/controllers/VerificationController.ts index cfcaca7de..e8c9d2f10 100644 --- a/infrastructure/evault-core/src/controllers/VerificationController.ts +++ b/infrastructure/evault-core/src/controllers/VerificationController.ts @@ -3,6 +3,7 @@ import { default as Axios } from "axios"; import { validate as uuidValidate } from "uuid"; import { VerificationService } from "../services/VerificationService"; import type { ProvisioningService } from "../services/ProvisioningService"; +import { verificationLanguage } from "../utils/verificationLanguage"; const diditClient = Axios.create({ baseURL: "https://verification.didit.me", @@ -50,6 +51,7 @@ export class VerificationController { if (!requireSharedSecret(req, res)) return; console.log("Creating new Didit verification session"); const { referenceId } = req.body; + const language = verificationLanguage(req.body?.language); if (referenceId) { const existing = await this.verificationService.findOne({ referenceId }); @@ -73,6 +75,7 @@ export class VerificationController { { workflow_id: workflowId, vendor_data: verification.id, + language, }, { headers: { @@ -99,7 +102,7 @@ export class VerificationController { const verificationUrl: string = diditSession.verification_url ?? diditSession.url ?? - `https://verify.didit.me/session/${sessionToken}`; + `https://verify.didit.me/${language}/session/${sessionToken}`; await this.verificationService.findByIdAndUpdate(verification.id, { diditSessionId: sessionId, diff --git a/infrastructure/evault-core/src/utils/verificationLanguage.spec.ts b/infrastructure/evault-core/src/utils/verificationLanguage.spec.ts new file mode 100644 index 000000000..7db370629 --- /dev/null +++ b/infrastructure/evault-core/src/utils/verificationLanguage.spec.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { verificationLanguage } from "./verificationLanguage"; + +describe("verificationLanguage", () => { + it.each(["en", "ru", "uk", "pt-BR", "zh-TW"])( + "passes the supported code %s through", + (code) => { + expect(verificationLanguage(code)).toBe(code); + }, + ); + + // Well-formed but not offered by Didit. Forwarding these risks a rejected + // session, which would stop verification starting at all. + it.each(["es-MX", "zz", "en-GB", "uk-UA"])( + "falls back to en for the unsupported code %s", + (code) => { + expect(verificationLanguage(code)).toBe("en"); + }, + ); + + it.each([undefined, null, "", "english", "RU", 42, {}])( + "falls back to en for %s", + (value) => { + expect(verificationLanguage(value)).toBe("en"); + }, + ); +}); diff --git a/infrastructure/evault-core/src/utils/verificationLanguage.ts b/infrastructure/evault-core/src/utils/verificationLanguage.ts new file mode 100644 index 000000000..110acbf69 --- /dev/null +++ b/infrastructure/evault-core/src/utils/verificationLanguage.ts @@ -0,0 +1,23 @@ +/** + * Didit renders its verification UI in the language passed at session + * creation. Without it Didit falls back to detecting the browser language, + * which does not work inside the wallet's webview, so the screens come out in + * English however the app is set. + * + * Codes Didit documents, as of 2026-09. An unlisted code falls back to `en` + * rather than reaching Didit: a rejected session creation would stop + * verification and recovery from starting at all. + * See https://docs.didit.me/sessions-api/create-session + */ + +const SUPPORTED = new Set([ + "ar", "bg", "bn", "bs", "ca", "cnr", "cs", "da", "de", "el", "en", "es", + "et", "fa", "fi", "fr", "he", "hi", "hr", "hu", "hy", "id", "it", "ja", + "ka", "kk", "ko", "ky", "lt", "lv", "mk", "mn", "ms", "nl", "no", "pl", + "pt", "pt-BR", "ro", "ru", "sk", "sl", "so", "sq", "sr", "sv", "th", "tr", + "uk", "uz", "vi", "zh", "zh-CN", "zh-TW", +]); + +export function verificationLanguage(value: unknown): string { + return typeof value === "string" && SUPPORTED.has(value) ? value : "en"; +}