mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(i18n): say "sign in again" or "too many requests" on a refused save
A save after the session ran out, or into a rate limit, showed the form's generic "could not save". The server already names both refusals in meta.errorCode (auth.missing for an ended web session, the status codes http.rate_limited, http.forbidden and http.invalid otherwise), but the catalogs had no sentence for them, so the lookup fell through to the fallback. They have one now in all ten locales, and a refusal that reaches the client without a code (a reverse proxy's 429) is read by its status the way the server labels it.
This commit is contained in:
1 parent
4721914cac
commit
0639d28f6a
12 files changed
+137
-15
No files matched your search
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Bestätige dein aktuelles Passwort, um das zu ändern.",
|
||||
"failed": "Die Bestätigung ist fehlgeschlagen. Es wurde nichts geändert."
|
||||
}
|
||||
},
|
||||
"missing": "Deine Sitzung ist abgelaufen, daher wurde nichts geändert. Melde dich erneut an, um weiterzumachen."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "Der Dokumentleser ist gerade mit anderen Aufgaben beschäftigt. Versuch es in einer Minute noch einmal."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Deine Sitzung ist abgelaufen, daher wurde nichts geändert. Melde dich erneut an, um weiterzumachen.",
|
||||
"rate_limited": "Gerade gibt es zu viele Anfragen, daher wurde nichts geändert. Warte kurz und versuche es erneut.",
|
||||
"forbidden": "Dafür fehlt dir die Berechtigung, daher wurde nichts geändert.",
|
||||
"invalid": "Einige Angaben wurden nicht angenommen, daher wurde nichts geändert. Prüfe sie und versuche es erneut."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Confirm your current password to change this.",
|
||||
"failed": "That did not verify. Nothing was changed."
|
||||
}
|
||||
},
|
||||
"missing": "Your session has ended, so nothing was changed. Sign in again to continue."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "The document reader is busy with other work. Try again in a minute."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Your session has ended, so nothing was changed. Sign in again to continue.",
|
||||
"rate_limited": "There are too many requests just now, so nothing was changed. Wait a moment and try again.",
|
||||
"forbidden": "You don't have permission for this, so nothing was changed.",
|
||||
"invalid": "Some of the entries were not accepted, so nothing was changed. Check them and try again."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Confirma tu contraseña actual para cambiar esto.",
|
||||
"failed": "La verificación ha fallado. No se ha cambiado nada."
|
||||
}
|
||||
},
|
||||
"missing": "Tu sesión ha caducado, así que no se ha cambiado nada. Inicia sesión de nuevo para continuar."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "El lector de documentos está ocupado con otras tareas. Inténtalo de nuevo en un minuto."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Tu sesión ha caducado, así que no se ha cambiado nada. Inicia sesión de nuevo para continuar.",
|
||||
"rate_limited": "Hay demasiadas solicitudes ahora mismo, así que no se ha cambiado nada. Espera un momento y vuelve a intentarlo.",
|
||||
"forbidden": "No tienes permiso para esto, así que no se ha cambiado nada.",
|
||||
"invalid": "Algunos datos no se aceptaron, así que no se ha cambiado nada. Revísalos y vuelve a intentarlo."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Confirmez votre mot de passe actuel pour modifier ceci.",
|
||||
"failed": "La vérification a échoué. Rien n'a été modifié."
|
||||
}
|
||||
},
|
||||
"missing": "Votre session a expiré, rien n'a donc été modifié. Reconnectez-vous pour continuer."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "Le lecteur de documents est occupé par d'autres tâches. Réessayez dans une minute."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Votre session a expiré, rien n'a donc été modifié. Reconnectez-vous pour continuer.",
|
||||
"rate_limited": "Il y a trop de requêtes pour le moment, rien n'a donc été modifié. Patientez un instant puis réessayez.",
|
||||
"forbidden": "Vous n'avez pas l'autorisation pour cette action, rien n'a donc été modifié.",
|
||||
"invalid": "Certaines saisies n'ont pas été acceptées, rien n'a donc été modifié. Vérifiez-les puis réessayez."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Conferma la password attuale per modificarlo.",
|
||||
"failed": "La verifica non è riuscita. Non è stato modificato nulla."
|
||||
}
|
||||
},
|
||||
"missing": "La tua sessione è scaduta, quindi non è stato modificato nulla. Accedi di nuovo per continuare."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "Il lettore di documenti è occupato con altre attività. Riprova tra un minuto."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "La tua sessione è scaduta, quindi non è stato modificato nulla. Accedi di nuovo per continuare.",
|
||||
"rate_limited": "Ci sono troppe richieste in questo momento, quindi non è stato modificato nulla. Attendi un attimo e riprova.",
|
||||
"forbidden": "Non hai l'autorizzazione per farlo, quindi non è stato modificato nulla.",
|
||||
"invalid": "Alcuni dati non sono stati accettati, quindi non è stato modificato nulla. Controllali e riprova."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "변경하려면 현재 비밀번호를 확인하세요.",
|
||||
"failed": "확인에 실패했습니다. 아무것도 변경되지 않았습니다."
|
||||
}
|
||||
},
|
||||
"missing": "세션이 만료되어 아무것도 변경되지 않았어요. 계속하려면 다시 로그인해 주세요."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "문서 판독기가 다른 작업으로 바빠요. 1분 후에 다시 시도하세요."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "세션이 만료되어 아무것도 변경되지 않았어요. 계속하려면 다시 로그인해 주세요.",
|
||||
"rate_limited": "지금은 요청이 너무 많아 아무것도 변경되지 않았어요. 잠시 후 다시 시도해 주세요.",
|
||||
"forbidden": "이 작업을 할 권한이 없어 아무것도 변경되지 않았어요.",
|
||||
"invalid": "일부 입력값이 받아들여지지 않아 아무것도 변경되지 않았어요. 확인한 후 다시 시도해 주세요."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Bevestig je huidige wachtwoord om dit te wijzigen.",
|
||||
"failed": "De verificatie is mislukt. Er is niets gewijzigd."
|
||||
}
|
||||
},
|
||||
"missing": "Je sessie is verlopen, dus er is niets gewijzigd. Meld je opnieuw aan om verder te gaan."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "De documentlezer is met ander werk bezig. Probeer het over een minuut opnieuw."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Je sessie is verlopen, dus er is niets gewijzigd. Meld je opnieuw aan om verder te gaan.",
|
||||
"rate_limited": "Er zijn net te veel verzoeken, dus er is niets gewijzigd. Wacht even en probeer het opnieuw.",
|
||||
"forbidden": "Je hebt hier geen toestemming voor, dus er is niets gewijzigd.",
|
||||
"invalid": "Sommige gegevens zijn niet geaccepteerd, dus er is niets gewijzigd. Controleer ze en probeer het opnieuw."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Potwierdź obecne hasło, aby to zmienić.",
|
||||
"failed": "Weryfikacja nie powiodła się. Nic nie zostało zmienione."
|
||||
}
|
||||
},
|
||||
"missing": "Twoja sesja wygasła, więc nic nie zostało zmienione. Zaloguj się ponownie, aby kontynuować."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "Czytnik dokumentów jest zajęty innymi zadaniami. Spróbuj ponownie za minutę."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Twoja sesja wygasła, więc nic nie zostało zmienione. Zaloguj się ponownie, aby kontynuować.",
|
||||
"rate_limited": "W tej chwili jest zbyt wiele zapytań, więc nic nie zostało zmienione. Zaczekaj chwilę i spróbuj ponownie.",
|
||||
"forbidden": "Nie masz do tego uprawnień, więc nic nie zostało zmienione.",
|
||||
"invalid": "Niektóre dane nie zostały przyjęte, więc nic nie zostało zmienione. Sprawdź je i spróbuj ponownie."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Confirme sua senha atual para alterar isto.",
|
||||
"failed": "A verificação não foi concluída. Nada foi alterado."
|
||||
}
|
||||
},
|
||||
"missing": "Sua sessão terminou, então nada foi alterado. Entre novamente para continuar."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "O leitor de documentos está ocupado com outras tarefas. Tente novamente em um minuto."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Sua sessão terminou, então nada foi alterado. Entre novamente para continuar.",
|
||||
"rate_limited": "Há solicitações demais neste momento, então nada foi alterado. Aguarde um pouco e tente de novo.",
|
||||
"forbidden": "Você não tem permissão para isso, então nada foi alterado.",
|
||||
"invalid": "Alguns dados não foram aceitos, então nada foi alterado. Confira e tente de novo."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
+8
-1
@@ -143,7 +143,8 @@
|
||||
"reproof": {
|
||||
"required": "Подтвердите текущий пароль, чтобы изменить это.",
|
||||
"failed": "Не подтверждено. Ничего не изменено."
|
||||
}
|
||||
},
|
||||
"missing": "Сеанс завершён, поэтому ничего не изменено. Войдите снова, чтобы продолжить."
|
||||
},
|
||||
"biomarkers": {
|
||||
"unit": {
|
||||
@@ -178,6 +179,12 @@
|
||||
},
|
||||
"aiRuns": {
|
||||
"readerBusy": "Средство чтения документов занято другой работой. Попробуйте через минуту."
|
||||
},
|
||||
"http": {
|
||||
"unauthenticated": "Сеанс завершён, поэтому ничего не изменено. Войдите снова, чтобы продолжить.",
|
||||
"rate_limited": "Сейчас слишком много запросов, поэтому ничего не изменено. Подождите немного и попробуйте снова.",
|
||||
"forbidden": "У вас нет прав на это действие, поэтому ничего не изменено.",
|
||||
"invalid": "Некоторые данные не приняты, поэтому ничего не изменено. Проверьте их и попробуйте снова."
|
||||
}
|
||||
},
|
||||
"doctorReport": {
|
||||
|
||||
@@ -113,4 +113,51 @@ describe("localizedApiError", () => {
|
||||
),
|
||||
).toBe(translator(locale as Catalog)("measurements.saveError"));
|
||||
});
|
||||
|
||||
describe("a refusal the client can act on", () => {
|
||||
const enT = translator(en as Catalog);
|
||||
|
||||
it.each([
|
||||
["an ended session", 401, "auth.missing", "Sign in again"],
|
||||
["a rate limit", 429, "http.rate_limited", "too many requests"],
|
||||
["a missing right", 403, "http.forbidden", "permission"],
|
||||
["a rejected entry", 422, "http.invalid", "Check them"],
|
||||
])("names %s instead of the save fallback", (_, status, code, text) => {
|
||||
const error = new ApiError("raw English server prose", status, {
|
||||
errorCode: code,
|
||||
});
|
||||
expect(localizedApiError(error, enT, "measurements.saveError")).toMatch(
|
||||
new RegExp(text, "i"),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads a refusal without a code by its status", () => {
|
||||
// A reverse proxy's 429 carries no envelope at all.
|
||||
const error = new ApiError("Request failed (429)", 429);
|
||||
expect(localizedApiError(error, enT, "measurements.saveError")).toBe(
|
||||
enT("apiErrors.http.rate_limited"),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["de", de],
|
||||
["es", es],
|
||||
["fr", fr],
|
||||
["it", itCatalog],
|
||||
["pl", pl],
|
||||
])("has its own sentence in %s", (_, locale) => {
|
||||
const localT = translator(locale as Catalog);
|
||||
for (const code of [
|
||||
"auth.missing",
|
||||
"http.unauthenticated",
|
||||
"http.rate_limited",
|
||||
"http.forbidden",
|
||||
"http.invalid",
|
||||
]) {
|
||||
const sentence = localT(`apiErrors.${code}`);
|
||||
expect(sentence).not.toBe(`apiErrors.${code}`);
|
||||
expect(sentence).not.toBe(enT(`apiErrors.${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,26 @@
|
||||
import { ApiError } from "./api-fetch";
|
||||
import { defaultErrorCodeForStatus } from "./default-error-codes";
|
||||
import { translateCode } from "@/lib/i18n/translate-code";
|
||||
|
||||
type Translate = (key: string) => string;
|
||||
|
||||
/**
|
||||
* Resolve an API envelope's stable error code through the locale catalog.
|
||||
* Missing/unknown codes, network failures, and non-API errors deliberately
|
||||
* collapse to the caller's safe domain fallback; server prose is never shown.
|
||||
* A refusal without a code of its own (a proxy's 429, an answer without the
|
||||
* envelope) is read by its status, the way the server labels one, so an
|
||||
* ended session or a rate limit is named as such. Unknown codes, network
|
||||
* failures, and non-API errors deliberately collapse to the caller's safe
|
||||
* domain fallback; server prose is never shown.
|
||||
*/
|
||||
export function localizedApiError(
|
||||
error: unknown,
|
||||
t: Translate,
|
||||
fallbackKey: string,
|
||||
): string {
|
||||
const errorCode =
|
||||
error instanceof ApiError && typeof error.meta?.errorCode === "string"
|
||||
const errorCode = !(error instanceof ApiError)
|
||||
? null
|
||||
: typeof error.meta?.errorCode === "string"
|
||||
? error.meta.errorCode
|
||||
: null;
|
||||
: defaultErrorCodeForStatus(error.status);
|
||||
return translateCode(t, "apiErrors", errorCode, fallbackKey);
|
||||
}
|
||||
Reference in new issue
Block a user