Placements suffix refactor, fix English shown when JP language selected

This commit is contained in:
Kalle
2026-06-06 11:55:23 +03:00
parent 739e6f440b
commit 68b2313660
18 changed files with 59 additions and 77 deletions

View File

@@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next";
import { ordinalSuffix } from "~/utils/i18n";
import {
FIRST_PLACEMENT_ICON_PATH,
SECOND_PLACEMENT_ICON_PATH,
@@ -38,18 +39,11 @@ export function Placement({
showAsSuperscript = true,
plain = false,
}: PlacementProps) {
const { t } = useTranslation(undefined, {});
const { i18n } = useTranslation();
const ordinalSuffix = t("results.placeSuffix", {
count: placement,
ordinal: true,
// no suffix is a better default than english
defaultValue: "",
fallbackLng: [],
});
const isSuperscript = showAsSuperscript && ordinalSuffix.startsWith("^");
const ordinalSuffixText = ordinalSuffix.replace(/^\^/, "");
const suffix = ordinalSuffix(placement, i18n.language);
const isSuperscript = showAsSuperscript && suffix.startsWith("^");
const ordinalSuffixText = suffix.replace(/^\^/, "");
const iconPath = textOnly ? null : getSpecialPlacementIconPath(placement);

View File

@@ -1,3 +1,4 @@
import type { LanguageCode } from "~/modules/i18n/config";
import type { Namespace } from "~/modules/i18n/resources.server";
import { logger } from "./logger";
import { assertType } from "./types";
@@ -76,3 +77,56 @@ export function countryCodeToTranslatedName({
return countryCode; // fallback to the code itself if display name fails
}
}
/**
* Ordinal placement suffixes per language, keyed by the CLDR ordinal plural
* category given by `Intl.PluralRules(language, { type: "ordinal" })`. A leading
* `^` marks a suffix that should render as superscript. Languages without a
* written ordinal suffix use explicit `null` – rendering no suffix is preferred
* over borrowing the English one.
*/
const ORDINAL_SUFFIXES: Record<
LanguageCode,
Partial<Record<Intl.LDMLPluralRule, string>> | null
> = {
da: null,
he: null,
nl: null,
en: { one: "^st", two: "^nd", few: "^rd", other: "^th" },
"es-ES": { other: "º" },
"es-US": { other: "^o" },
"fr-CA": { one: "^er", other: "^ème" },
"fr-EU": { one: "^er", other: "^ème" },
de: { other: "^." },
it: { many: "^o", other: "^o" },
ja: { other: "位" },
ko: { other: "^등" },
pl: { other: "^." },
"pt-BR": { other: "^º" },
ru: { other: "^ое" },
zh: { other: "名" },
};
const pluralRulesCache = new Map<string, Intl.PluralRules>();
/**
* Returns the localized ordinal suffix for a placement number in the given
* language (e.g. `"^st"` for `1` in English). A leading `^` marks a suffix the
* caller should render as superscript. Returns an empty string for languages
* without a written ordinal suffix.
*/
export function ordinalSuffix(placement: number, language: string): string {
const category = ordinalPluralRules(language).select(placement);
return ORDINAL_SUFFIXES[language as LanguageCode]?.[category] ?? "";
}
function ordinalPluralRules(language: string): Intl.PluralRules {
let rules = pluralRulesCache.get(language);
if (!rules) {
rules = new Intl.PluralRules(language, { type: "ordinal" });
pluralRulesCache.set(language, rules);
}
return rules;
}

View File

@@ -174,10 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "Banepuljen er gyldig",
"results": "Resultater",
"results.placeSuffix_one": "",
"results.placeSuffix_other": "",
"results.placeSuffix_two": "",
"results.placeSuffix_few": "",
"forms.name": "Navn",
"forms.description": "Beskrivelse",
"forms.errors.title": "Følgende fejl skal rettes",

View File

@@ -174,10 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "Arenen-Pool ok!",
"results": "Ergebnisse",
"results.placeSuffix_one": "^.",
"results.placeSuffix_other": "^.",
"results.placeSuffix_two": "^.",
"results.placeSuffix_few": "^.",
"forms.name": "Name",
"forms.description": "Beschreibung",
"forms.errors.title": "Diese Fehler müssen behoben werden",

View File

@@ -174,10 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "Can't pick the tiebreaker stage",
"maps.validation.VALID": "Map pool ok!",
"results": "Results",
"results.placeSuffix_one": "^st",
"results.placeSuffix_two": "^nd",
"results.placeSuffix_few": "^rd",
"results.placeSuffix_other": "^th",
"forms.name": "Name",
"forms.description": "Description",
"forms.errors.title": "Following errors need to be fixed",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "No se permite el escenario de desempate",
"maps.validation.VALID": "¡Rotación de mapas OK!",
"results": "Resultados",
"results.placeSuffix_one": "º",
"results.placeSuffix_many": "º",
"results.placeSuffix_other": "º",
"results.placeSuffix_two": "º",
"results.placeSuffix_few": "º",
"forms.name": "Nombre",
"forms.description": "Descripción",
"forms.errors.title": "Deben corregirse los siguientes errores:",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "No se permite el escenario para desempate",
"maps.validation.VALID": "¡Grupo de escenarios OK!",
"results": "Resultados",
"results.placeSuffix_one": "^o",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^o",
"results.placeSuffix_two": "^o",
"results.placeSuffix_few": "^o",
"forms.name": "Nombre",
"forms.description": "Descripción",
"forms.errors.title": "Los siguientes errores deben corregirse",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "Liste de stages valide !",
"results": "Résultats",
"results.placeSuffix_one": "^er",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^ème",
"results.placeSuffix_two": "^ème",
"results.placeSuffix_few": "^ème",
"forms.name": "Nom",
"forms.description": "Description",
"forms.errors.title": "Les erreurs ci-dessous doivent être réglées",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "Vous ne pouvez pas choisir le stage décisif",
"maps.validation.VALID": "Liste de stages valide !",
"results": "Résultats",
"results.placeSuffix_one": "^er",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^ème",
"results.placeSuffix_two": "^ème",
"results.placeSuffix_few": "^ème",
"forms.name": "Nom",
"forms.description": "Description",
"forms.errors.title": "Les erreurs ci-dessous doivent être réglées",

View File

@@ -174,10 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "מאגר המפות בסדר!",
"results": "תוצאות",
"results.placeSuffix_one": "^st",
"results.placeSuffix_two": "^nd",
"results.placeSuffix_other": "^th",
"results.placeSuffix_few": "^rd",
"forms.name": "שם",
"forms.description": "תיאור",
"forms.errors.title": "יש לתקן את השגיאות הבאות",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "Non può venir scelta la mappa per lo spareggio",
"maps.validation.VALID": "Pool scenari OK!",
"results": "Risultati",
"results.placeSuffix_one": "^o",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^o",
"results.placeSuffix_two": "^o",
"results.placeSuffix_few": "^o",
"forms.name": "Nome",
"forms.description": "Descrizione",
"forms.errors.title": "I seguenti errori devono essere corretti",

View File

@@ -174,8 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "タイブレーカ(引き分けの場合)のステージを決めることができません。",
"maps.validation.VALID": "登録できるマップリストです!",
"results": "結果",
"results.placeSuffix_two": "位",
"results.placeSuffix_few": "位",
"forms.name": "名前",
"forms.description": "説明",
"forms.errors.title": "以下のエラーの修正が必要です",

View File

@@ -174,8 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "Map pool ok!",
"results": "결과",
"results.placeSuffix_two": "^등",
"results.placeSuffix_few": "^등",
"forms.name": "이름",
"forms.description": "설명",
"forms.errors.title": "다음 문제들을 해결해야 합니다",

View File

@@ -174,10 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "",
"results": "Uitslagen",
"results.placeSuffix_one": "",
"results.placeSuffix_other": "",
"results.placeSuffix_two": "",
"results.placeSuffix_few": "",
"forms.name": "Naam",
"forms.description": "Beschrijving",
"forms.errors.title": "De volgende errors moeten nog worden opgelost",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "",
"maps.validation.VALID": "Pula map jest okej!",
"results": "Wyniki",
"results.placeSuffix_one": "^.",
"results.placeSuffix_few": "^.",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^.",
"results.placeSuffix_two": "^.",
"forms.name": "Imię",
"forms.description": "Opis",
"forms.errors.title": "Dane błędy muszą zostać naprawione",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "Não é possível escolher o mapa de desempate",
"maps.validation.VALID": "Seleção de mapas OK!",
"results": "Resultados",
"results.placeSuffix_one": "^º",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^º",
"results.placeSuffix_two": "^º",
"results.placeSuffix_few": "^º",
"forms.name": "Nome",
"forms.description": "Descrição",
"forms.errors.title": "Os seguintes erros precisam ser corrigidos",

View File

@@ -174,11 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "Нельзя выбрать арену-тайбрейкер",
"maps.validation.VALID": "Список карт в порядке!",
"results": "Результаты",
"results.placeSuffix_one": "^ое",
"results.placeSuffix_few": "^ье",
"results.placeSuffix_many": "",
"results.placeSuffix_other": "^ое",
"results.placeSuffix_two": "^ое",
"forms.name": "Название",
"forms.description": "Описание",
"forms.errors.title": "Указанные ошибки должны быть исправлены",

View File

@@ -174,8 +174,6 @@
"maps.validation.INCLUDES_TIEBREAKER": "无法选择决胜局地图",
"maps.validation.VALID": "地图池已完成!",
"results": "成绩",
"results.placeSuffix_two": "^nd",
"results.placeSuffix_few": "^rd",
"forms.name": "名称",
"forms.description": "简介",
"forms.errors.title": "以下错误需要修正",