From 68b23136606f8b33db971107368994a5336640d5 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:55:23 +0300 Subject: [PATCH] Placements suffix refactor, fix English shown when JP language selected --- app/components/Placement.tsx | 16 ++++------- app/utils/i18n.ts | 54 ++++++++++++++++++++++++++++++++++++ locales/da/common.json | 4 --- locales/de/common.json | 4 --- locales/en/common.json | 4 --- locales/es-ES/common.json | 5 ---- locales/es-US/common.json | 5 ---- locales/fr-CA/common.json | 5 ---- locales/fr-EU/common.json | 5 ---- locales/he/common.json | 4 --- locales/it/common.json | 5 ---- locales/ja/common.json | 2 -- locales/ko/common.json | 2 -- locales/nl/common.json | 4 --- locales/pl/common.json | 5 ---- locales/pt-BR/common.json | 5 ---- locales/ru/common.json | 5 ---- locales/zh/common.json | 2 -- 18 files changed, 59 insertions(+), 77 deletions(-) diff --git a/app/components/Placement.tsx b/app/components/Placement.tsx index ce7059614..711dee8b5 100644 --- a/app/components/Placement.tsx +++ b/app/components/Placement.tsx @@ -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); diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts index dc25b0859..68d571585 100644 --- a/app/utils/i18n.ts +++ b/app/utils/i18n.ts @@ -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> | 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(); + +/** + * 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; +} diff --git a/locales/da/common.json b/locales/da/common.json index 82321cd82..d73d14822 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -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", diff --git a/locales/de/common.json b/locales/de/common.json index 9f0b39a70..ccf374b2e 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -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", diff --git a/locales/en/common.json b/locales/en/common.json index 384e3be53..c311aa3b1 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -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", diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 87eb5a2d5..51ffea346 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -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:", diff --git a/locales/es-US/common.json b/locales/es-US/common.json index 052a6d10f..3d53e3c36 100644 --- a/locales/es-US/common.json +++ b/locales/es-US/common.json @@ -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", diff --git a/locales/fr-CA/common.json b/locales/fr-CA/common.json index 30a1e0513..429c1f47a 100644 --- a/locales/fr-CA/common.json +++ b/locales/fr-CA/common.json @@ -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", diff --git a/locales/fr-EU/common.json b/locales/fr-EU/common.json index bdf92d376..de6ce5947 100644 --- a/locales/fr-EU/common.json +++ b/locales/fr-EU/common.json @@ -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", diff --git a/locales/he/common.json b/locales/he/common.json index b5f48b4f5..983e4c56b 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -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": "יש לתקן את השגיאות הבאות", diff --git a/locales/it/common.json b/locales/it/common.json index c1f20953a..6a447dd7a 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -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", diff --git a/locales/ja/common.json b/locales/ja/common.json index 6eaeea1f3..a3a43cf30 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -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": "以下のエラーの修正が必要です", diff --git a/locales/ko/common.json b/locales/ko/common.json index 027b3e15e..81f7c856f 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -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": "다음 문제들을 해결해야 합니다", diff --git a/locales/nl/common.json b/locales/nl/common.json index 7a6226392..8352a60e9 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -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", diff --git a/locales/pl/common.json b/locales/pl/common.json index 0eefe660e..2804d8995 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -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", diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json index 2c01e2ea4..2007156f0 100644 --- a/locales/pt-BR/common.json +++ b/locales/pt-BR/common.json @@ -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", diff --git a/locales/ru/common.json b/locales/ru/common.json index 37dbf1baf..481aa5348 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -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": "Указанные ошибки должны быть исправлены", diff --git a/locales/zh/common.json b/locales/zh/common.json index 39502c107..d8c934698 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -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": "以下错误需要修正",