From 0f59298f3035020894031d8962cb48bd83214761 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 25 Sep 2023 12:18:15 +0100 Subject: [PATCH] Consolidate conjugation i18n strings (#11660) --- cypress/tsconfig.json | 2 +- src/MatrixClientPeg.ts | 11 +- .../views/elements/EventListSummary.tsx | 6 +- src/components/views/messages/MPollBody.tsx | 4 +- .../views/messages/ReactionsRowButton.tsx | 4 +- .../messages/ReactionsRowButtonTooltip.tsx | 4 +- src/components/views/rooms/RoomKnocksBar.tsx | 85 +++++++-------- src/i18n/strings/bg.json | 6 +- src/i18n/strings/ca.json | 6 +- src/i18n/strings/cs.json | 6 +- src/i18n/strings/da.json | 6 +- src/i18n/strings/de_DE.json | 6 +- src/i18n/strings/el.json | 6 +- src/i18n/strings/en_EN.json | 26 +---- src/i18n/strings/eo.json | 6 +- src/i18n/strings/es.json | 6 +- src/i18n/strings/et.json | 6 +- src/i18n/strings/eu.json | 6 +- src/i18n/strings/fa.json | 7 +- src/i18n/strings/fi.json | 6 +- src/i18n/strings/fr.json | 6 +- src/i18n/strings/gl.json | 6 +- src/i18n/strings/he.json | 7 +- src/i18n/strings/hu.json | 6 +- src/i18n/strings/id.json | 6 +- src/i18n/strings/is.json | 7 +- src/i18n/strings/it.json | 6 +- src/i18n/strings/ja.json | 6 +- src/i18n/strings/kab.json | 6 +- src/i18n/strings/ko.json | 6 +- src/i18n/strings/lo.json | 7 +- src/i18n/strings/lt.json | 6 +- src/i18n/strings/lv.json | 6 +- src/i18n/strings/nb_NO.json | 6 +- src/i18n/strings/nl.json | 6 +- src/i18n/strings/nn.json | 6 +- src/i18n/strings/pl.json | 6 +- src/i18n/strings/pt_BR.json | 6 +- src/i18n/strings/ru.json | 6 +- src/i18n/strings/sk.json | 6 +- src/i18n/strings/sq.json | 6 +- src/i18n/strings/sr.json | 6 +- src/i18n/strings/sv.json | 6 +- src/i18n/strings/tr.json | 6 +- src/i18n/strings/uk.json | 6 +- src/i18n/strings/vi.json | 6 +- src/i18n/strings/vls.json | 6 +- src/i18n/strings/zh_Hans.json | 6 +- src/i18n/strings/zh_Hant.json | 6 +- src/utils/FormattingUtils.ts | 52 +++++---- src/utils/i18n-helpers.ts | 5 +- .../views/elements/EventListSummary-test.tsx | 2 + .../views/messages/MPollBody-test.tsx | 2 + .../views/rooms/RoomKnocksBar-test.tsx | 2 + test/utils/FormattingUtils-test.ts | 42 ------- test/utils/FormattingUtils-test.tsx | 103 ++++++++++++++++++ .../FormattingUtils-test.tsx.snap | 48 ++++++++ test/utils/i18n-helpers-test.ts | 4 +- tsconfig.json | 2 +- 59 files changed, 371 insertions(+), 283 deletions(-) delete mode 100644 test/utils/FormattingUtils-test.ts create mode 100644 test/utils/FormattingUtils-test.tsx create mode 100644 test/utils/__snapshots__/FormattingUtils-test.tsx.snap diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index bfff3775a4..d8a522d417 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es2016", "jsx": "react", - "lib": ["es2020", "dom", "dom.iterable"], + "lib": ["es2021", "dom", "dom.iterable"], "types": ["cypress", "cypress-axe", "@percy/cypress", "@testing-library/cypress"], "resolveJsonModule": true, "esModuleInterop": true, diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 6a5eb25b2f..5ae2d16dba 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -49,6 +49,7 @@ import { SettingLevel } from "./settings/SettingLevel"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import PlatformPeg from "./PlatformPeg"; +import { formatList } from "./utils/FormattingUtils"; export interface IMatrixClientCreds { homeserverUrl: string; @@ -356,15 +357,9 @@ class MatrixClientPegClass implements IMatrixClientPeg { if (name) return name; if (names.length === 2 && count === 2) { - return _t("user1_and_user2", { - user1: names[0], - user2: names[1], - }); + return formatList(names); } - return _t("user_and_n_others", { - user: names[0], - count: count - 1, - }); + return formatList(names, 1); } private inviteeNamesToRoomName(names: string[], count: number): string { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 9ed1345a0b..4d9bbe35c9 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -20,7 +20,7 @@ import React, { ComponentProps, ReactNode } from "react"; import { MatrixEvent, RoomMember, EventType } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../languageHandler"; -import { formatCommaSeparatedList } from "../../../utils/FormattingUtils"; +import { formatList } from "../../../utils/FormattingUtils"; import { isValid3pidInvite } from "../../../RoomInvite"; import GenericEventListSummary from "./GenericEventListSummary"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; @@ -131,7 +131,7 @@ export default class EventListSummary extends React.Component< return EventListSummary.getDescriptionForTransition(t.transitionType, userNames.length, t.repeats); }); - const desc = formatCommaSeparatedList(descs); + const desc = formatList(descs); return _t("timeline|summary|format", { nameList, transitionList: desc }); }); @@ -150,7 +150,7 @@ export default class EventListSummary extends React.Component< * included before "and [n] others". */ private renderNameList(users: string[]): string { - return formatCommaSeparatedList(users, this.props.summaryLength); + return formatList(users, this.props.summaryLength); } /** diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index e26c1faf4b..ca4baaed1c 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -33,7 +33,7 @@ import { PollResponseEvent } from "matrix-js-sdk/src/extensible_events_v1/PollRe import { _t } from "../../../languageHandler"; import Modal from "../../../Modal"; import { IBodyProps } from "./IBodyProps"; -import { formatCommaSeparatedList } from "../../../utils/FormattingUtils"; +import { formatList } from "../../../utils/FormattingUtils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import ErrorDialog from "../dialogs/ErrorDialog"; import { GetRelationsForEvent } from "../rooms/EventTile"; @@ -100,7 +100,7 @@ export function findTopAnswer(pollEvent: MatrixEvent, voteRelations: Relations): const bestAnswerTexts = bestAnswerIds.map(findAnswerText); - return formatCommaSeparatedList(bestAnswerTexts, 3); + return formatList(bestAnswerTexts, 3); } export function isPollEnded(pollEvent: MatrixEvent, matrixClient: MatrixClient): boolean { diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 70cd48e3f6..3deae400f0 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { mediaFromMxc } from "../../../customisations/Media"; import { _t } from "../../../languageHandler"; -import { formatCommaSeparatedList } from "../../../utils/FormattingUtils"; +import { formatList } from "../../../utils/FormattingUtils"; import dis from "../../../dispatcher/dispatcher"; import ReactionsRowButtonTooltip from "./ReactionsRowButtonTooltip"; import AccessibleButton from "../elements/AccessibleButton"; @@ -123,7 +123,7 @@ export default class ReactionsRowButton extends React.PureComponent { - return
{formatCommaSeparatedList(senders, 6)}
; + return
{formatList(senders, 6)}
; }, reactedWith: (sub) => { if (!shortName) { diff --git a/src/components/views/rooms/RoomKnocksBar.tsx b/src/components/views/rooms/RoomKnocksBar.tsx index f24db6ed2e..6439014df9 100644 --- a/src/components/views/rooms/RoomKnocksBar.tsx +++ b/src/components/views/rooms/RoomKnocksBar.tsx @@ -28,6 +28,7 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; import AccessibleButton from "../elements/AccessibleButton"; import Heading from "../typography/Heading"; +import { formatList } from "../../../utils/FormattingUtils"; export const RoomKnocksBar: VFC<{ room: Room }> = ({ room }) => { const [disabled, setDisabled] = useState(false); @@ -82,58 +83,46 @@ export const RoomKnocksBar: VFC<{ room: Room }> = ({ room }) => { {_t("action|view")} ); - let names: string = knockMembers - .slice(0, 2) - .map((knockMember) => knockMember.name) - .join(", "); + let names = formatList( + knockMembers.map((knockMember) => knockMember.name), + 3, + true, + ); let link: ReactNode = null; - switch (knockMembersCount) { - case 1: { - buttons = ( - <> - handleDeny(knockMembers[0].userId)} - title={_t("action|deny")} - > - - - handleApprove(knockMembers[0].userId)} - title={_t("action|approve")} - > - - - - ); - names = `${knockMembers[0].name} (${knockMembers[0].userId})`; - link = knockMembers[0].events.member?.getContent().reason && ( + if (knockMembersCount === 1) { + buttons = ( + <> handleDeny(knockMembers[0].userId)} + title={_t("action|deny")} > - {_t("action|view_message")} + - ); - break; - } - case 2: { - names = _t("%(names)s and %(name)s", { names: knockMembers[0].name, name: knockMembers[1].name }); - break; - } - case 3: { - names = _t("%(names)s and %(name)s", { names, name: knockMembers[2].name }); - break; - } - default: - names = _t("%(names)s and %(count)s others", { names, count: knockMembersCount - 2 }); + handleApprove(knockMembers[0].userId)} + title={_t("action|approve")} + > + + + + ); + names = `${knockMembers[0].name} (${knockMembers[0].userId})`; + link = knockMembers[0].events.member?.getContent().reason && ( + + {_t("action|view_message")} + + ); } return ( diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 360a0aee3f..d4f57a50bc 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -95,9 +95,9 @@ "Delete widget": "Изтрий приспособлението", "Create new room": "Създай нова стая", "Home": "Начална страница", - "%(items)s and %(count)s others": { - "other": "%(items)s и %(count)s други", - "one": "%(items)s и още един" + " and %(count)s others": { + "other": " и %(count)s други", + "one": " и още един" }, "%(items)s and %(lastItem)s": "%(items)s и %(lastItem)s", "collapse": "свий", diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json index 33c24495e7..498fd32ed0 100644 --- a/src/i18n/strings/ca.json +++ b/src/i18n/strings/ca.json @@ -99,9 +99,9 @@ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "La supressió d'un giny l'elimina per a tots els usuaris d'aquesta sala. Esteu segur que voleu eliminar aquest giny?", "Delete widget": "Suprimeix el giny", "Home": "Inici", - "%(items)s and %(count)s others": { - "other": "%(items)s i %(count)s altres", - "one": "%(items)s i un altre" + " and %(count)s others": { + "other": " i %(count)s altres", + "one": " i un altre" }, "%(items)s and %(lastItem)s": "%(items)s i %(lastItem)s", "collapse": "col·lapsa", diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 26066aec8b..3e1c1795d7 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -132,9 +132,9 @@ "Invalid file%(extra)s": "Neplatný soubor%(extra)s", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "Budete přesměrováni na stránku třetí strany k ověření svého účtu pro používání s %(integrationsUrl)s. Chcete pokračovat?", "Something went wrong!": "Něco se nepodařilo!", - "%(items)s and %(count)s others": { - "other": "%(items)s a %(count)s další", - "one": "%(items)s a jeden další" + " and %(count)s others": { + "other": " a %(count)s další", + "one": " a jeden další" }, "%(items)s and %(lastItem)s": "%(items)s a také %(lastItem)s", "And %(count)s more...": { diff --git a/src/i18n/strings/da.json b/src/i18n/strings/da.json index 48f262c8e3..6dc6a72c96 100644 --- a/src/i18n/strings/da.json +++ b/src/i18n/strings/da.json @@ -73,9 +73,9 @@ "Preparing to send logs": "Forbereder afsendelse af logfiler", "Permission Required": "Tilladelse påkrævet", "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s. %(monthName)s %(fullYear)s", - "%(items)s and %(count)s others": { - "other": "%(items)s og %(count)s andre", - "one": "%(items)s og en anden" + " and %(count)s others": { + "other": " og %(count)s andre", + "one": " og en anden" }, "%(items)s and %(lastItem)s": "%(items)s og %(lastItem)s", "Please contact your homeserver administrator.": "Kontakt venligst din homeserver administrator.", diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 7967f8b6e8..380939360a 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -145,9 +145,9 @@ }, "Delete Widget": "Widget löschen", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Das Löschen des Widgets entfernt es für alle in diesem Raum. Wirklich löschen?", - "%(items)s and %(count)s others": { - "other": "%(items)s und %(count)s andere", - "one": "%(items)s und ein weiteres Raummitglied" + " and %(count)s others": { + "other": " und %(count)s andere", + "one": " und ein weiteres Raummitglied" }, "Restricted": "Eingeschränkt", "%(duration)ss": "%(duration)ss", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 85fa69f15c..de9173bd70 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -167,9 +167,9 @@ "Your homeserver has exceeded its user limit.": "Ο διακομιστής σας ξεπέρασε το όριο χρηστών.", "Use app": "Χρησιμοποιήστε την εφαρμογή", "Use app for a better experience": "Χρησιμοποιήστε την εφαρμογή για καλύτερη εμπειρία", - "%(items)s and %(count)s others": { - "one": "%(items)s και ένα ακόμα", - "other": "%(items)s και %(count)s άλλα" + " and %(count)s others": { + "one": " και ένα ακόμα", + "other": " και %(count)s άλλα" }, "Ask this user to verify their session, or manually verify it below.": "Ζητήστε από αυτόν τον χρήστη να επιβεβαιώσει την συνεδρία του, ή επιβεβαιώστε την χειροκίνητα παρακάτω.", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "Ο %(name)s (%(userId)s) συνδέθηκε σε μία νέα συνεδρία χωρίς να την επιβεβαιώσει:", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 260a505a78..a0196b5297 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -764,15 +764,10 @@ "error_database_closed_title": "Database unexpectedly closed", "error_database_closed_description": "This may be caused by having the app open in multiple tabs or due to clearing browser data.", "empty_room": "Empty room", - "user1_and_user2": "%(user1)s and %(user2)s", - "user_and_n_others": { - "other": "%(user)s and %(count)s others", - "one": "%(user)s and 1 other" - }, "inviting_user1_and_user2": "Inviting %(user1)s and %(user2)s", "inviting_user_and_n_others": { "other": "Inviting %(user)s and %(count)s others", - "one": "Inviting %(user)s and 1 other" + "one": "Inviting %(user)s and one other" }, "empty_room_was_name": "Empty room (was %(oldName)s)", "notifier": { @@ -1403,20 +1398,14 @@ "mixed_content": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", "tls": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests." }, - "%(items)s and %(count)s others": { - "other": "%(items)s and %(count)s others", - "one": "%(items)s and one other" + " and %(count)s others": { + "other": " and %(count)s others", + "one": " and one other" }, - "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", - "%(space1Name)s and %(space2Name)s": "%(space1Name)s and %(space2Name)s", "in_space1_and_space2": "In spaces %(space1Name)s and %(space2Name)s.", - "%(spaceName)s and %(count)s others": { - "other": "%(spaceName)s and %(count)s others", - "one": "%(spaceName)s and %(count)s other" - }, "in_space_and_n_other_spaces": { "other": "In %(spaceName)s and %(count)s other spaces.", - "one": "In %(spaceName)s and %(count)s other space." + "one": "In %(spaceName)s and one other space." }, "in_space": "In %(spaceName)s.", "name_and_id": "%(name)s (%(userId)s)", @@ -2571,11 +2560,6 @@ "Public space": "Public space", "Private space": "Private space", "Private room": "Private room", - "%(names)s and %(name)s": "%(names)s and %(name)s", - "%(names)s and %(count)s others": { - "other": "%(names)s and %(count)s others", - "one": "%(names)s and %(count)s other" - }, "%(count)s people asking to join": { "other": "%(count)s people asking to join", "one": "Asking to join" diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 6d12b15c6d..53bca72663 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -91,9 +91,9 @@ "Delete widget": "Forigi fenestraĵon", "Create new room": "Krei novan ĉambron", "Home": "Hejmo", - "%(items)s and %(count)s others": { - "other": "%(items)s kaj %(count)s aliaj", - "one": "%(items)s kaj unu alia" + " and %(count)s others": { + "other": " kaj %(count)s aliaj", + "one": " kaj unu alia" }, "%(items)s and %(lastItem)s": "%(items)s kaj %(lastItem)s", "collapse": "maletendi", diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 1b3dee8c97..0ffdfbc769 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -177,9 +177,9 @@ "Delete Widget": "Eliminar accesorio", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Al borrar un accesorio, este se elimina para todos usuarios de la sala. ¿Estás seguro?", "Popout widget": "Abrir accesorio en una ventana emergente", - "%(items)s and %(count)s others": { - "other": "%(items)s y otros %(count)s", - "one": "%(items)s y otro más" + " and %(count)s others": { + "other": " y otros %(count)s", + "one": " y otro más" }, "collapse": "encoger", "expand": "desplegar", diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 149245bee9..ed62c202ae 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -442,9 +442,9 @@ "Identity server URL does not appear to be a valid identity server": "Isikutuvastusserveri aadress ei tundu viitama kehtivale isikutuvastusserverile", "Looks good!": "Tundub õige!", "Failed to re-authenticate due to a homeserver problem": "Uuesti autentimine ei õnnestunud koduserveri vea tõttu", - "%(items)s and %(count)s others": { - "other": "%(items)s ja %(count)s muud", - "one": "%(items)s ja üks muu" + " and %(count)s others": { + "other": " ja %(count)s muud", + "one": " ja üks muu" }, "%(items)s and %(lastItem)s": "%(items)s ja %(lastItem)s", "Please contact your homeserver administrator.": "Palun võta ühendust koduserveri haldajaga.", diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 31e41973f9..c03bbef12b 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -153,9 +153,9 @@ }, "Delete Widget": "Ezabatu trepeta", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Trepeta ezabatzean gelako kide guztientzat kentzen da. Ziur trepeta ezabatu nahi duzula?", - "%(items)s and %(count)s others": { - "other": "%(items)s eta beste %(count)s", - "one": "%(items)s eta beste bat" + " and %(count)s others": { + "other": " eta beste %(count)s", + "one": " eta beste bat" }, "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Ezin izango duzu hau aldatu zure burua mailaz jaisten ari zarelako, zu bazara gelan baimenak dituen azken erabiltzailea ezin izango dira baimenak berreskuratu.", "Replying": "Erantzuten", diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index ed0d6bd534..c3e0faf903 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -1011,10 +1011,9 @@ "IRC display name width": "عرض نمایش نام‌های IRC", "This event could not be displayed": "امکان نمایش این رخداد وجود ندارد", "Edit message": "ویرایش پیام", - "%(items)s and %(lastItem)s": "%(items)s و %(lastItem)s", - "%(items)s and %(count)s others": { - "one": "%(items)s و یکی دیگر", - "other": "%(items)s و %(count)s دیگر" + " and %(count)s others": { + "one": " و یکی دیگر", + "other": " و %(count)s دیگر" }, "Clear personal data": "پاک‌کردن داده‌های شخصی", "Verify your identity to access encrypted messages and prove your identity to others.": "با تائید هویت خود به پیام‌های رمزشده دسترسی یافته و هویت خود را به دیگران ثابت می‌کنید.", diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 09b2987d9c..266d467091 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -151,9 +151,9 @@ "expand": "laajenna", "collapse": "supista", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Sovelman poistaminen poistaa sen kaikilta huoneen käyttäjiltä. Haluatko varmasti poistaa tämän sovelman?", - "%(items)s and %(count)s others": { - "other": "%(items)s ja %(count)s muuta", - "one": "%(items)s ja yksi muu" + " and %(count)s others": { + "other": " ja %(count)s muuta", + "one": " ja yksi muu" }, "Sunday": "Sunnuntai", "Notification targets": "Ilmoituksen kohteet", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index eb33ea9b7f..89d1c1a766 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -142,9 +142,9 @@ "Jump to read receipt": "Aller à l’accusé de lecture", "Delete Widget": "Supprimer le widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Supprimer un widget le supprime pour tous les utilisateurs du salon. Voulez-vous vraiment supprimer ce widget ?", - "%(items)s and %(count)s others": { - "other": "%(items)s et %(count)s autres", - "one": "%(items)s et un autre" + " and %(count)s others": { + "other": " et %(count)s autres", + "one": " et un autre" }, "And %(count)s more...": { "other": "Et %(count)s autres…" diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 9bf03c9488..3ee6ac99dc 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -94,9 +94,9 @@ "Delete widget": "Eliminar widget", "Create new room": "Crear unha nova sala", "Home": "Inicio", - "%(items)s and %(count)s others": { - "other": "%(items)s e %(count)s outras", - "one": "%(items)s e outra máis" + " and %(count)s others": { + "other": " e %(count)s outras", + "one": " e outra máis" }, "%(items)s and %(lastItem)s": "%(items)s e %(lastItem)s", "collapse": "comprimir", diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 03952ee1c0..4add0c2fee 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -307,10 +307,9 @@ "Vietnam": "וייטנאם", "Venezuela": "ונצואלה", "Vatican City": "ותיקן", - "%(items)s and %(lastItem)s": "%(items)s ו%(lastItem)s אחרון", - "%(items)s and %(count)s others": { - "one": "%(items)s ועוד אחד אחר", - "other": "%(items)s ו%(count)s אחרים" + " and %(count)s others": { + "one": " ועוד אחד אחר", + "other": " ו%(count)s אחרים" }, "Not Trusted": "לא אמין", "Ask this user to verify their session, or manually verify it below.": "בקש ממשתמש זה לאמת את ההתחברות שלו, או לאמת אותה באופן ידני למטה.", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index d4d530ac33..adecb2bd10 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -145,9 +145,9 @@ }, "Delete Widget": "Kisalkalmazás törlése", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "A kisalkalmazás törlése minden felhasználót érint a szobában. Biztos, hogy törli a kisalkalmazást?", - "%(items)s and %(count)s others": { - "other": "%(items)s és még %(count)s másik", - "one": "%(items)s és még egy másik" + " and %(count)s others": { + "other": " és még %(count)s másik", + "one": " és még egy másik" }, "Restricted": "Korlátozott", "%(duration)ss": "%(duration)s mp", diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 51178de22e..47dc4cb8b1 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -526,9 +526,9 @@ "Sending": "Mengirim", "Spaces": "Space", "Connecting": "Menghubungkan", - "%(items)s and %(count)s others": { - "one": "%(items)s dan satu lainnya", - "other": "%(items)s dan %(count)s lainnya" + " and %(count)s others": { + "one": " dan satu lainnya", + "other": " dan %(count)s lainnya" }, "Disconnect from the identity server ?": "Putuskan hubungan dari server identitas ?", "Disconnect identity server": "Putuskan hubungan server identitas", diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index 2c0d2baca0..8c71c950d1 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -686,10 +686,9 @@ "Enable desktop notifications": "Virkja tilkynningar á skjáborði", "Don't miss a reply": "Ekki missa af svari", "Review to ensure your account is safe": "Yfirfarðu þetta til að tryggja að aðgangurinn þinn sé öruggur", - "%(items)s and %(lastItem)s": "%(items)s og %(lastItem)s", - "%(items)s and %(count)s others": { - "one": "%(items)s og einn til viðbótar", - "other": "%(items)s og %(count)s til viðbótar" + " and %(count)s others": { + "one": " og einn til viðbótar", + "other": " og %(count)s til viðbótar" }, "Private space": "Einkasvæði", "Mentions only": "Aðeins minnst á", diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 3bcfeb8614..1deeb67daf 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -97,9 +97,9 @@ "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "L'eliminazione di un widget lo rimuove per tutti gli utenti della stanza. Sei sicuro di eliminare il widget?", "Delete widget": "Elimina widget", "Home": "Pagina iniziale", - "%(items)s and %(count)s others": { - "other": "%(items)s e altri %(count)s", - "one": "%(items)s e un altro" + " and %(count)s others": { + "other": " e altri %(count)s", + "one": " e un altro" }, "%(items)s and %(lastItem)s": "%(items)s e %(lastItem)s", "collapse": "richiudi", diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index f1c9db450d..2796c88d34 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -135,9 +135,9 @@ "Delete widget": "ウィジェットを削除", "Popout widget": "ウィジェットをポップアウト", "Home": "ホーム", - "%(items)s and %(count)s others": { - "other": "%(items)sと他%(count)s人", - "one": "%(items)sともう1人" + " and %(count)s others": { + "other": "と他%(count)s人", + "one": "ともう1人" }, "%(items)s and %(lastItem)s": "%(items)s, %(lastItem)s", "collapse": "折りたたむ", diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index d4e57077c7..3e79a7e07c 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -115,9 +115,9 @@ "Set up Secure Messages": "Sbadu iznan iɣelsanen", "Logs sent": "Iɣmisen ttewaznen", "Not Trusted": "Ur yettwattkal ara", - "%(items)s and %(count)s others": { - "other": "%(items)s d %(count)s wiyaḍ", - "one": "%(items)s d wayeḍ-nniḍen" + " and %(count)s others": { + "other": " d %(count)s wiyaḍ", + "one": " d wayeḍ-nniḍen" }, "%(items)s and %(lastItem)s": "%(items)s d %(lastItem)s", "Encryption upgrade available": "Yella uleqqem n uwgelhen", diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index a92fbd420e..910aa51a66 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -173,9 +173,9 @@ "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "자기 자신을 강등하는 것은 되돌릴 수 없고, 자신이 마지막으로 이 방에서 특권을 가진 사용자라면 다시 특권을 얻는 건 불가능합니다.", "Jump to read receipt": "읽은 기록으로 건너뛰기", "Share room": "방 공유하기", - "%(items)s and %(count)s others": { - "one": "%(items)s님 외 한 명", - "other": "%(items)s님 외 %(count)s명" + " and %(count)s others": { + "one": "님 외 한 명", + "other": "님 외 %(count)s명" }, "Permission Required": "권한 필요", "Copied!": "복사했습니다!", diff --git a/src/i18n/strings/lo.json b/src/i18n/strings/lo.json index 91017c58b4..a95b3f0a81 100644 --- a/src/i18n/strings/lo.json +++ b/src/i18n/strings/lo.json @@ -1190,10 +1190,9 @@ "other": "%(spaceName)s ແລະ %(count)s ອື່ນໆ" }, "%(space1Name)s and %(space2Name)s": "%(space1Name)s ແລະ %(space2Name)s", - "%(items)s and %(lastItem)s": "%(items)s ແລະ %(lastItem)s", - "%(items)s and %(count)s others": { - "one": "%(items)s ແລະ ອີກນຶ່ງລາຍການ", - "other": "%(items)s ແລະ %(count)s ອື່ນໆ" + " and %(count)s others": { + "one": " ແລະ ອີກນຶ່ງລາຍການ", + "other": " ແລະ %(count)s ອື່ນໆ" }, "Email (optional)": "ອີເມວ (ທາງເລືອກ)", "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "ກະລຸນາຮັບຊາບວ່າ, ຖ້າທ່ານບໍ່ເພີ່ມອີເມວ ແລະ ລືມລະຫັດຜ່ານຂອງທ່ານ, ທ່ານອາດ ສູນເສຍການເຂົ້າເຖິງບັນຊີຂອງທ່ານຢ່າງຖາວອນ.", diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 44559c16aa..846b4f165b 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -177,9 +177,9 @@ "No backup found!": "Nerasta jokios atsarginės kopijos!", "Failed to decrypt %(failedCount)s sessions!": "Nepavyko iššifruoti %(failedCount)s seansų!", "Explore rooms": "Žvalgyti kambarius", - "%(items)s and %(count)s others": { - "other": "%(items)s ir %(count)s kiti(-ų)", - "one": "%(items)s ir dar vienas" + " and %(count)s others": { + "other": " ir %(count)s kiti(-ų)", + "one": " ir dar vienas" }, "%(items)s and %(lastItem)s": "%(items)s ir %(lastItem)s", "General": "Bendrieji", diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index ade2a5c450..f0b1862212 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -158,9 +158,9 @@ "other": "Un par %(count)s vairāk..." }, "This room is not public. You will not be able to rejoin without an invite.": "Šī istaba nav publiska un jūs nevarēsiet atkārtoti pievienoties bez uzaicinājuma.", - "%(items)s and %(count)s others": { - "one": "%(items)s un viens cits", - "other": "%(items)s un %(count)s citus" + " and %(count)s others": { + "one": " un viens cits", + "other": " un %(count)s citus" }, "Sunday": "Svētdiena", "Notification targets": "Paziņojumu adresāti", diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index 8ac64fd897..34789b6d51 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -248,9 +248,9 @@ "Upgrade your encryption": "Oppgrader krypteringen din", "Verify this session": "Verifiser denne økten", "Not Trusted": "Ikke betrodd", - "%(items)s and %(count)s others": { - "other": "%(items)s og %(count)s andre", - "one": "%(items)s og én annen" + " and %(count)s others": { + "other": " og %(count)s andre", + "one": " og én annen" }, "%(items)s and %(lastItem)s": "%(items)s og %(lastItem)s", "Show more": "Vis mer", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index a04f029670..1e84a9a92a 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -151,9 +151,9 @@ "Replying": "Aan het beantwoorden", "Unnamed room": "Naamloze kamer", "Banned by %(displayName)s": "Verbannen door %(displayName)s", - "%(items)s and %(count)s others": { - "other": "%(items)s en %(count)s andere", - "one": "%(items)s en één ander" + " and %(count)s others": { + "other": " en %(count)s andere", + "one": " en één ander" }, "collapse": "dichtvouwen", "expand": "uitvouwen", diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index 4b25f58e55..bdd00e0dcb 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -109,9 +109,9 @@ "Delete widget": "Slett widgeten", "Create new room": "Lag nytt rom", "Home": "Heim", - "%(items)s and %(count)s others": { - "other": "%(items)s og %(count)s til", - "one": "%(items)s og ein til" + " and %(count)s others": { + "other": " og %(count)s til", + "one": " og ein til" }, "%(items)s and %(lastItem)s": "%(items)s og %(lastItem)s", "collapse": "Slå saman", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 48e174b685..85308cae8d 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -210,9 +210,9 @@ "other": "I %(count)s więcej…" }, "Delete Backup": "Usuń kopię zapasową", - "%(items)s and %(count)s others": { - "other": "%(items)s i %(count)s innych", - "one": "%(items)s i jedna inna osoba" + " and %(count)s others": { + "other": " i %(count)s innych", + "one": " i jedna inna osoba" }, "Add some now": "Dodaj teraz kilka", "Continue With Encryption Disabled": "Kontynuuj Z Wyłączonym Szyfrowaniem", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 6410d1bf4e..d7a4d20c09 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -151,9 +151,9 @@ "Delete Widget": "Apagar widget", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Remover um widget o remove para todas as pessoas desta sala. Tem certeza que quer remover este widget?", "Delete widget": "Remover widget", - "%(items)s and %(count)s others": { - "other": "%(items)s e %(count)s outras", - "one": "%(items)s e uma outra" + " and %(count)s others": { + "other": " e %(count)s outras", + "one": " e uma outra" }, "collapse": "recolher", "expand": "expandir", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index f928529d33..60a0b71371 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -145,9 +145,9 @@ }, "Delete Widget": "Удалить виджет", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Удаление виджета действует для всех участников этой комнаты. Вы действительно хотите удалить этот виджет?", - "%(items)s and %(count)s others": { - "other": "%(items)s и ещё %(count)s участника(-ов)", - "one": "%(items)s и ещё кто-то" + " and %(count)s others": { + "other": " и ещё %(count)s участника(-ов)", + "one": " и ещё кто-то" }, "Restricted": "Ограниченный пользователь", "%(duration)ss": "%(duration)s сек", diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 4a1d639ad7..d9115bb2d7 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -85,9 +85,9 @@ "Delete widget": "Vymazať widget", "Create new room": "Vytvoriť novú miestnosť", "Home": "Domov", - "%(items)s and %(count)s others": { - "other": "%(items)s a %(count)s ďalší", - "one": "%(items)s a jeden ďalší" + " and %(count)s others": { + "other": " a %(count)s ďalší", + "one": " a jeden ďalší" }, "%(items)s and %(lastItem)s": "%(items)s a tiež %(lastItem)s", "Custom level": "Vlastná úroveň", diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index ad38d7b1c6..3e2ab2716f 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -94,9 +94,9 @@ "Something went wrong!": "Diçka shkoi ters!", "Create new room": "Krijoni dhomë të re", "Home": "Kreu", - "%(items)s and %(count)s others": { - "other": "%(items)s dhe %(count)s të tjerë", - "one": "%(items)s dhe një tjetër" + " and %(count)s others": { + "other": " dhe %(count)s të tjerë", + "one": " dhe një tjetër" }, "%(items)s and %(lastItem)s": "%(items)s dhe %(lastItem)s", "collapse": "tkurre", diff --git a/src/i18n/strings/sr.json b/src/i18n/strings/sr.json index 73221aa35a..f5813c75ae 100644 --- a/src/i18n/strings/sr.json +++ b/src/i18n/strings/sr.json @@ -94,9 +94,9 @@ "Delete widget": "Обриши виџет", "Create new room": "Направи нову собу", "Home": "Почетна", - "%(items)s and %(count)s others": { - "other": "%(items)s и %(count)s других", - "one": "%(items)s и још један" + " and %(count)s others": { + "other": " и %(count)s других", + "one": " и још један" }, "%(items)s and %(lastItem)s": "%(items)s и %(lastItem)s", "collapse": "скупи", diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index 3836dc6976..eecaeac21b 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -173,9 +173,9 @@ "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Denna process låter dig exportera nycklarna för meddelanden som du har fått i krypterade rum till en lokal fil. Du kommer sedan att kunna importera filen i en annan Matrix-klient i framtiden, så att den klienten också kan avkryptera meddelandena.", "Confirm Removal": "Bekräfta borttagning", "Reject all %(invitedRooms)s invites": "Avböj alla %(invitedRooms)s inbjudningar", - "%(items)s and %(count)s others": { - "other": "%(items)s och %(count)s till", - "one": "%(items)s och en till" + " and %(count)s others": { + "other": " och %(count)s till", + "one": " och en till" }, "collapse": "fäll ihop", "expand": "fäll ut", diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index b71fe57cc1..2b7a9d901d 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -228,9 +228,9 @@ "Your password has been reset.": "Parolanız sıfırlandı.", "General failure": "Genel başarısızlık", "Create account": "Yeni hesap", - "%(items)s and %(count)s others": { - "other": "%(items)s ve diğer %(count)s", - "one": "%(items)s ve bir diğeri" + " and %(count)s others": { + "other": " ve diğer %(count)s", + "one": " ve bir diğeri" }, "Clear personal data": "Kişisel veri temizle", "That matches!": "Eşleşti!", diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 6cb3f0c5f2..5c68427f87 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -172,9 +172,9 @@ "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) починає новий сеанс без його звірення:", "Ask this user to verify their session, or manually verify it below.": "Попросіть цього користувача звірити сеанс, або звірте його власноруч унизу.", "Not Trusted": "Не довірений", - "%(items)s and %(count)s others": { - "other": "%(items)s та ще %(count)s учасників", - "one": "%(items)s і ще хтось" + " and %(count)s others": { + "other": " та ще %(count)s учасників", + "one": " і ще хтось" }, "Your homeserver has exceeded its user limit.": "Ваш домашній сервер перевищив свій ліміт користувачів.", "Your homeserver has exceeded one of its resource limits.": "Ваш домашній сервер перевищив одне із своїх обмежень ресурсів.", diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index d0828a51c3..091eb1ef42 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -30,9 +30,9 @@ "Restricted": "Bị hạn chế", "Moderator": "Điều phối viên", "Reason": "Lý do", - "%(items)s and %(count)s others": { - "other": "%(items)s và %(count)s mục khác", - "one": "%(items)s và một mục khác" + " and %(count)s others": { + "other": " và %(count)s mục khác", + "one": " và một mục khác" }, "%(items)s and %(lastItem)s": "%(items)s và %(lastItem)s", "Please contact your homeserver administrator.": "Vui lòng liên hệ quản trị viên homeserver của bạn.", diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index f5655ba998..46825393f4 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -30,9 +30,9 @@ "Restricted": "Beperkten toegank", "Moderator": "Moderator", "Reason": "Reedn", - "%(items)s and %(count)s others": { - "other": "%(items)s en %(count)s andere", - "one": "%(items)s en één ander" + " and %(count)s others": { + "other": " en %(count)s andere", + "one": " en één ander" }, "%(items)s and %(lastItem)s": "%(items)s en %(lastItem)s", "Please contact your homeserver administrator.": "Gelieve contact ip te neemn me den beheerder van je thuusserver.", diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 14c970f3e0..35cba95c2e 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -124,9 +124,9 @@ "Jump to read receipt": "跳到阅读回执", "Unnamed room": "未命名的房间", "Delete Widget": "删除挂件", - "%(items)s and %(count)s others": { - "other": "%(items)s 和其他 %(count)s 人", - "one": "%(items)s 与另一个人" + " and %(count)s others": { + "other": " 和其他 %(count)s 人", + "one": " 与另一个人" }, "collapse": "折叠", "expand": "展开", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 309a33b439..39b7b0e098 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -150,9 +150,9 @@ "Banned by %(displayName)s": "被 %(displayName)s 封鎖", "Delete Widget": "刪除小工具", "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "刪除小工具會將它從此聊天室中所有使用者的收藏中移除。您確定您要刪除這個小工具嗎?", - "%(items)s and %(count)s others": { - "other": "%(items)s 與其他 %(count)s 個人", - "one": "%(items)s 與另一個人" + " and %(count)s others": { + "other": " 與其他 %(count)s 個人", + "one": " 與另一個人" }, "collapse": "收折", "expand": "展開", diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts index d12a4c704a..0d1231cb28 100644 --- a/src/utils/FormattingUtils.ts +++ b/src/utils/FormattingUtils.ts @@ -18,7 +18,7 @@ limitations under the License. import { ReactElement, ReactNode } from "react"; import { useIdColorHash } from "@vector-im/compound-web"; -import { _t, getCurrentLanguage } from "../languageHandler"; +import { _t, getCurrentLanguage, getUserLanguage } from "../languageHandler"; import { jsxJoin } from "./ReactUtils"; const locale = getCurrentLanguage(); @@ -92,34 +92,42 @@ export function getUserNameColorClass(userId: string): string { * @returns {string} a string constructed by joining `items` with a comma * between each item, but with the last item appended as " and [lastItem]". */ -export function formatCommaSeparatedList(items: string[], itemLimit?: number): string; -export function formatCommaSeparatedList(items: ReactElement[], itemLimit?: number): ReactElement; -export function formatCommaSeparatedList(items: ReactNode[], itemLimit?: number): ReactNode; -export function formatCommaSeparatedList(items: ReactNode[], itemLimit?: number): ReactNode { - const remaining = itemLimit === undefined ? 0 : Math.max(items.length - itemLimit, 0); - if (items.length === 0) { - return ""; - } else if (items.length === 1) { - return items[0]; - } else { - let lastItem; - if (remaining > 0) { - items = items.slice(0, itemLimit); - } else { - lastItem = items.pop(); +export function formatList(items: string[], itemLimit?: number, includeCount?: boolean): string; +export function formatList(items: ReactElement[], itemLimit?: number, includeCount?: boolean): ReactElement; +export function formatList(items: ReactNode[], itemLimit?: number, includeCount?: boolean): ReactNode; +export function formatList(items: ReactNode[], itemLimit = items.length, includeCount = false): ReactNode { + let remaining = Math.max(items.length - itemLimit, 0); + if (items.length <= 1) { + return items[0] ?? ""; + } + + const formatter = new Intl.ListFormat(getUserLanguage(), { style: "long", type: "conjunction" }); + if (remaining > 0) { + if (includeCount) { + itemLimit--; + remaining++; } - let joinedItems; + items = items.slice(0, itemLimit); + let joinedItems: ReactNode; if (items.every((e) => typeof e === "string")) { joinedItems = items.join(", "); } else { joinedItems = jsxJoin(items, ", "); } - if (remaining > 0) { - return _t("%(items)s and %(count)s others", { items: joinedItems, count: remaining }); - } else { - return _t("%(items)s and %(lastItem)s", { items: joinedItems, lastItem }); - } + return _t(" and %(count)s others", { count: remaining }, { Items: () => joinedItems }); } + + if (items.every((e) => typeof e === "string")) { + return formatter.format(items as string[]); + } + + const parts = formatter.formatToParts(items.map((_, i) => `${i}`)); + return jsxJoin( + parts.map((part) => { + if (part.type === "literal") return part.value; + return items[parseInt(part.value, 10)]; + }), + ); } diff --git a/src/utils/i18n-helpers.ts b/src/utils/i18n-helpers.ts index dd87b4767a..81576040f9 100644 --- a/src/utils/i18n-helpers.ts +++ b/src/utils/i18n-helpers.ts @@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/matrix"; import SpaceStore from "../stores/spaces/SpaceStore"; import { _t } from "../languageHandler"; import DMRoomMap from "./DMRoomMap"; +import { formatList } from "./FormattingUtils"; export interface RoomContextDetails { details: string | null; @@ -39,7 +40,7 @@ export function roomContextDetails(room: Room): RoomContextDetails | null { const space1Name = room.client.getRoom(parent)?.name; const space2Name = room.client.getRoom(secondParent)?.name; return { - details: _t("%(space1Name)s and %(space2Name)s", { space1Name, space2Name }), + details: formatList([space1Name ?? "", space2Name ?? ""]), ariaLabel: _t("in_space1_and_space2", { space1Name, space2Name }), }; } else if (parent) { @@ -47,7 +48,7 @@ export function roomContextDetails(room: Room): RoomContextDetails | null { const count = otherParents.length; if (count > 0) { return { - details: _t("%(spaceName)s and %(count)s others", { spaceName, count }), + details: formatList([spaceName, ...otherParents], 1), ariaLabel: _t("in_space_and_n_other_spaces", { spaceName, count }), }; } diff --git a/test/components/views/elements/EventListSummary-test.tsx b/test/components/views/elements/EventListSummary-test.tsx index 03c01c453e..c57303dff2 100644 --- a/test/components/views/elements/EventListSummary-test.tsx +++ b/test/components/views/elements/EventListSummary-test.tsx @@ -28,6 +28,7 @@ import { import EventListSummary from "../../../../src/components/views/elements/EventListSummary"; import { Layout } from "../../../../src/settings/enums/Layout"; import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; +import * as languageHandler from "../../../../src/languageHandler"; describe("EventListSummary", function () { const roomId = "!room:server.org"; @@ -136,6 +137,7 @@ describe("EventListSummary", function () { beforeEach(function () { jest.clearAllMocks(); + jest.spyOn(languageHandler, "getUserLanguage").mockReturnValue("en-GB"); }); afterAll(() => { diff --git a/test/components/views/messages/MPollBody-test.tsx b/test/components/views/messages/MPollBody-test.tsx index 7e9c2a542f..0ab99f63fb 100644 --- a/test/components/views/messages/MPollBody-test.tsx +++ b/test/components/views/messages/MPollBody-test.tsx @@ -40,6 +40,7 @@ import { import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper"; +import * as languageHandler from "../../../../src/languageHandler"; const CHECKED = "mx_PollOption_checked"; const userId = "@me:example.com"; @@ -58,6 +59,7 @@ describe("MPollBody", () => { mockClient.getRoom.mockReturnValue(null); mockClient.relations.mockResolvedValue({ events: [] }); + jest.spyOn(languageHandler, "getUserLanguage").mockReturnValue("en-GB"); }); it("finds no votes if there are none", () => { diff --git a/test/components/views/rooms/RoomKnocksBar-test.tsx b/test/components/views/rooms/RoomKnocksBar-test.tsx index 1f5a89036b..89a1863cbc 100644 --- a/test/components/views/rooms/RoomKnocksBar-test.tsx +++ b/test/components/views/rooms/RoomKnocksBar-test.tsx @@ -39,6 +39,7 @@ import { getMockClientWithEventEmitter, mockClientMethodsUser, } from "../../../test-utils"; +import * as languageHandler from "../../../../src/languageHandler"; describe("RoomKnocksBar", () => { const userId = "@alice:example.org"; @@ -127,6 +128,7 @@ describe("RoomKnocksBar", () => { jest.spyOn(state, "hasSufficientPowerLevelFor").mockReturnValue(true); jest.spyOn(Modal, "createDialog"); jest.spyOn(dis, "dispatch"); + jest.spyOn(languageHandler, "getUserLanguage").mockReturnValue("en-GB"); }); it("does not render if user can neither approve nor deny", () => { diff --git a/test/utils/FormattingUtils-test.ts b/test/utils/FormattingUtils-test.ts deleted file mode 100644 index bbe49053c3..0000000000 --- a/test/utils/FormattingUtils-test.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* -Copyright 2023 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { formatCount, formatCountLong } from "../../src/utils/FormattingUtils"; - -jest.mock("../../src/dispatcher/dispatcher"); - -describe("FormattingUtils", () => { - describe("formatCount", () => { - it.each([ - { count: 999, expectedCount: "999" }, - { count: 9999, expectedCount: "10K" }, - { count: 99999, expectedCount: "100K" }, - { count: 999999, expectedCount: "1M" }, - { count: 9999999, expectedCount: "10M" }, - { count: 99999999, expectedCount: "100M" }, - { count: 999999999, expectedCount: "1B" }, - { count: 9999999999, expectedCount: "10B" }, - ])("formats $count as $expectedCount", ({ count, expectedCount }) => { - expect(formatCount(count)).toBe(expectedCount); - }); - }); - - describe("formatCountLong", () => { - it("formats numbers according to the locale", () => { - expect(formatCountLong(1000)).toBe("1,000"); - }); - }); -}); diff --git a/test/utils/FormattingUtils-test.tsx b/test/utils/FormattingUtils-test.tsx new file mode 100644 index 0000000000..c1bb2d6b58 --- /dev/null +++ b/test/utils/FormattingUtils-test.tsx @@ -0,0 +1,103 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; + +import { formatList, formatCount, formatCountLong } from "../../src/utils/FormattingUtils"; +import SettingsStore from "../../src/settings/SettingsStore"; + +jest.mock("../../src/dispatcher/dispatcher"); + +describe("FormattingUtils", () => { + describe("formatCount", () => { + it.each([ + { count: 999, expectedCount: "999" }, + { count: 9999, expectedCount: "10K" }, + { count: 99999, expectedCount: "100K" }, + { count: 999999, expectedCount: "1M" }, + { count: 9999999, expectedCount: "10M" }, + { count: 99999999, expectedCount: "100M" }, + { count: 999999999, expectedCount: "1B" }, + { count: 9999999999, expectedCount: "10B" }, + ])("formats $count as $expectedCount", ({ count, expectedCount }) => { + expect(formatCount(count)).toBe(expectedCount); + }); + }); + + describe("formatCountLong", () => { + it("formats numbers according to the locale", () => { + expect(formatCountLong(1000)).toBe("1,000"); + }); + }); + + describe("formatList", () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.spyOn(SettingsStore, "getValue").mockReturnValue("en-GB"); + }); + + it("should return empty string when given empty list", () => { + expect(formatList([])).toEqual(""); + }); + + it("should return only item when given list of length 1", () => { + expect(formatList(["abc"])).toEqual("abc"); + }); + + it("should return expected sentence in English without item limit", () => { + expect(formatList(["abc", "def", "ghi"])).toEqual("abc, def and ghi"); + }); + + it("should return expected sentence in German without item limit", () => { + jest.spyOn(SettingsStore, "getValue").mockReturnValue("de"); + expect(formatList(["abc", "def", "ghi"])).toEqual("abc, def und ghi"); + }); + + it("should return expected sentence in English with item limit", () => { + expect(formatList(["abc", "def", "ghi", "jkl"], 2)).toEqual("abc, def and 2 others"); + expect(formatList(["abc", "def", "ghi", "jkl"], 3)).toEqual("abc, def, ghi and one other"); + }); + + it("should return expected sentence in English with item limit and includeCount", () => { + expect(formatList(["abc", "def", "ghi", "jkl"], 3, true)).toEqual("abc, def and 2 others"); + expect(formatList(["abc", "def", "ghi", "jkl"], 4, true)).toEqual("abc, def, ghi and jkl"); + }); + + it("should return expected sentence in ReactNode when given 2 React children", () => { + expect(formatList([a, b])).toMatchSnapshot(); + }); + + it("should return expected sentence in ReactNode when given more React children", () => { + expect( + formatList([ + a, + b, + c, + d, + ]), + ).toMatchSnapshot(); + }); + + it("should return expected sentence in ReactNode when using itemLimit", () => { + expect( + formatList( + [a, b, c, d], + 2, + ), + ).toMatchSnapshot(); + }); + }); +}); diff --git a/test/utils/__snapshots__/FormattingUtils-test.tsx.snap b/test/utils/__snapshots__/FormattingUtils-test.tsx.snap new file mode 100644 index 0000000000..32f0ec443c --- /dev/null +++ b/test/utils/__snapshots__/FormattingUtils-test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FormattingUtils formatList should return expected sentence in ReactNode when given 2 React children 1`] = ` + + + a + + and + + b + + +`; + +exports[`FormattingUtils formatList should return expected sentence in ReactNode when given more React children 1`] = ` + + + a + + , + + b + + , + + c + + and + + d + + +`; + +exports[`FormattingUtils formatList should return expected sentence in ReactNode when using itemLimit 1`] = ` + + + + a + + , + + b + + + and 2 others + +`; diff --git a/test/utils/i18n-helpers-test.ts b/test/utils/i18n-helpers-test.ts index 3e120588d2..df512a2306 100644 --- a/test/utils/i18n-helpers-test.ts +++ b/test/utils/i18n-helpers-test.ts @@ -56,7 +56,7 @@ describe("roomContextDetails", () => { new Set([parent1.roomId, parent2.roomId, parent3.roomId]), ); const res = roomContextDetails(room); - expect(res!.details).toMatchInlineSnapshot(`"Alpha and 1 other"`); - expect(res!.ariaLabel).toMatchInlineSnapshot(`"In Alpha and 1 other space."`); + expect(res!.details).toMatchInlineSnapshot(`"Alpha and one other"`); + expect(res!.ariaLabel).toMatchInlineSnapshot(`"In Alpha and one other space."`); }); }); diff --git a/tsconfig.json b/tsconfig.json index 814718f4d3..e7689727ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "outDir": "./lib", "declaration": true, "jsx": "react", - "lib": ["es2020", "dom", "dom.iterable"], + "lib": ["es2021", "dom", "dom.iterable"], "strict": true }, "include": [