diff --git a/client/apps/voicememos.lua b/client/apps/voicememos.lua
index 1faa62dbc..6d553293b 100644
--- a/client/apps/voicememos.lua
+++ b/client/apps/voicememos.lua
@@ -27,8 +27,8 @@ end)
---Server push: our upload was rejected (bad payload, too long, or the upstream upload
---failed); relays the reason to the app.
---@param message string human-readable failure reason from server/voicememos/init.lua
-RegisterNetEvent('sd-phone:client:voice:uploadFailed', function(message)
- SendNUIMessage({ action = 'sd-phone:voice:uploadFailed', data = { message = message } })
+RegisterNetEvent('sd-phone:client:voice:uploadFailed', function(message, messageKey)
+ SendNUIMessage({ action = 'sd-phone:voice:uploadFailed', data = { message = message, messageKey = messageKey } })
end)
-- Direct upload. The event path above hands the whole recording to the server in one ordinary
diff --git a/locales/en.json b/locales/en.json
index 3f6e12b61..764433739 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -1188,6 +1188,7 @@
"passwordsNoMatch": "Passwords do not match.",
"pleaseCheckField": "Please check this field",
"pleaseWait": "Please wait…",
+ "pluralSuffix": "s",
"poster": "poster",
"previous": "Previous",
"recoverAccount": "Forgot your password? Tap here to recover your account",
@@ -2145,6 +2146,7 @@
"noKeyHolders": "No one else has a key.",
"owned": "Owned",
"properties": "properties",
+ "propertiesFew": "properties",
"property": "property",
"remove": "Remove",
"removeKeyHolder": "Remove Key Holder?",
@@ -4085,6 +4087,8 @@
"appTitle": "Notes",
"attachPhoto": "Attach photo",
"attachmentAlt": "Attachment {n}",
+ "attachmentDrawing": "drawing",
+ "attachmentPhoto": "photo",
"badNoteId": "Bad note id",
"cancel": "Cancel",
"clearCanvas": "Clear canvas",
@@ -4539,6 +4543,7 @@
"storyRoomLeft": "Your story only has room for {n} more today",
"storySlide": "Slide {n}",
"storyTextEmpty": "Write something first",
+ "submitComment": "Post",
"tagPeople": "Tag people",
"tagSearchHint": "Search for anyone on Photogram to tag them.",
"tagline": "Sign up to see photos from your friends.",
@@ -6194,6 +6199,14 @@
"pasteYoutubeNotification": "Paste any YouTube link to use it as a notification tone.",
"pasteYoutubeRingtone": "Paste any YouTube link to use it as a ringtone.",
"phone": "Phone",
+ "phoneColorBlack": "Black",
+ "phoneColorBlue": "Blue",
+ "phoneColorGreen": "Green",
+ "phoneColorOrange": "Orange",
+ "phoneColorPink": "Pink",
+ "phoneColorPurple": "Purple",
+ "phoneColorRed": "Red",
+ "phoneColorYellow": "Yellow",
"phonePosition": "Phone Position",
"phonePositionHint": "Pick where the phone snaps to inside the game window. The phone scales toward this anchor so it stays pinned when you change the size above.",
"phoneScale": "Phone Scale",
@@ -6484,9 +6497,21 @@
},
"share": {
"acceptedYourShare": "{name} accepted your {kind}.",
+ "couldNotSaveSharedItem": "That {kind} could not be saved.",
"couldNotSaveYourShare": "{name} could not save your {kind}.",
"declinedYourShare": "{name} declined your {kind}.",
"invalidRecipient": "Invalid recipient",
+ "kindContact": "contact",
+ "kindDocument": "document",
+ "kindIdCard": "ID card",
+ "kindNote": "note",
+ "kindPhoto": "photo",
+ "kindPhotogramProfile": "Photogram profile",
+ "kindPin": "map pin",
+ "kindPlaylist": "playlist",
+ "kindSignatureRequest": "signature request",
+ "kindSong": "song",
+ "kindVoice": "voice memo",
"recipientHasTooManyPending": "Recipient has too many pending shares",
"recipientNoLongerNearby": "Recipient is no longer nearby",
"requestExpired": "Request expired",
@@ -7134,10 +7159,12 @@
"voicemailTitle": "Voicemail"
},
"voicememos": {
+ "badAudioPayload": "Bad audio payload",
"badMemoId": "Bad memo id",
"cancel": "Cancel",
"copied": "Copied!",
"copyLink": "Copy Link",
+ "couldNotSaveMemo": "Could not save memo",
"couldNotSendRequest": "Could not send request",
"delete": "Delete",
"deleteMessage": "“{name}” will be permanently deleted.",
@@ -7156,6 +7183,7 @@
"pause": "Pause",
"play": "Play",
"record": "Record",
+ "recordingTooLong": "Recording is too long",
"recordingUnsupported": "Recording is not supported here.",
"rename": "Rename",
"renameMessage": "Enter a new name for this recording.",
@@ -7169,7 +7197,10 @@
"title": "Voice Memos",
"today": "Today {time}",
"toneLimit": "You can save up to {n} custom tones. Remove one in Settings first.",
+ "uploadBusy": "Upload already in progress",
"uploadFailed": "Upload failed",
+ "uploadLimitReached": "Upload limit reached, try again later",
+ "uploadSlowDown": "Slow down a moment",
"useAsNotification": "Use as Notification Sound",
"useAsRingtone": "Use as Ringtone",
"yesterday": "Yesterday {time}"
diff --git a/locales/ru.json b/locales/ru.json
index fbb36ade5..e41988285 100644
--- a/locales/ru.json
+++ b/locales/ru.json
@@ -1226,8 +1226,9 @@
"wantsYouToFollowHandle": "{name} хочет, чтобы вы подписались на @{handle} в Photogram",
"welcomeBack": "С возвращением",
"wiFiUnavailable": "Wi-Fi недоступен",
- "wouldLikeToShare": "{name} хочет поделиться {kind}",
- "yes": "Да"
+ "wouldLikeToShare": "{name} хочет отправить вам объект: {kind}",
+ "yes": "Да",
+ "pluralSuffix": ""
},
"compass": {
"dirE": "В",
@@ -2140,8 +2141,8 @@
"locked": "Заблокировано",
"noKeyHolders": "Больше ни у кого нет ключа.",
"owned": "В собственности",
- "properties": "дома",
- "property": "дом",
+ "properties": "объектов недвижимости",
+ "property": "объект недвижимости",
"remove": "Удалить",
"removeKeyHolder": "Удалить владельца ключа?",
"removeKeyMessage": "{name} больше не будет иметь ключа к {address}.",
@@ -2150,9 +2151,10 @@
"sourceId": "Идентификатор источника",
"title": "Дома",
"type": "Тип",
- "unlock": "Разблокировать",
- "unlocked": "Разблокировано",
- "waypoint": "Путевая точка"
+ "unlock": "Отпереть",
+ "unlocked": "Не заперто",
+ "waypoint": "Путевая точка",
+ "propertiesFew": "объекта недвижимости"
},
"id": {
"copied": "Скопировано",
@@ -2306,7 +2308,7 @@
"moveToSpam": "Переместить в Спам",
"moveToSpamConfirm": "Переместить это письмо в папку «Спам»?",
"nameMustCharactersFewer": "Имя не длиннее {n} символов",
- "nameMustLeastCharacters": "Имя должно быть не короче {n} символ{s}",
+ "nameMustLeastCharacters": "Минимальная длина имени: {n} симв.{s}",
"newMessage": "Новое сообщение",
"next": "Далее",
"noDocuments": "Никаких документов для прикрепления. Сначала создайте его в Файлах.",
@@ -4044,7 +4046,7 @@
"playPause": "Воспроизведение/Пауза",
"playlist": "Плейлист",
"playlistName": "Название плейлиста",
- "playlistSongCount": "Плейлист · {count} песня{plural}",
+ "playlistSongCount": "Плейлист · Треков: {count}{plural}",
"playlists": "Плейлисты",
"previous": "Предыдущий",
"recentlyAdded": "Недавно добавлено",
@@ -4063,8 +4065,8 @@
"shareTitle": "Поделиться {title}",
"sharedArtist": "Отправлено",
"sharedPlaylist": "Общий плейлист",
- "shuffle": "Перетасовать",
- "songCount": "{count} песня{plural}",
+ "shuffle": "Перемешать",
+ "songCount": "Треков: {count}{plural}",
"songs": "Песни",
"sourceBlocked": "Эта ссылка не из разрешенного источника.",
"stop": "Стоп",
@@ -4131,7 +4133,9 @@
"strokeWidthAria": "Ширина штриха {w}",
"undoStroke": "Отменить последний штрих",
"viewDrawing": "Посмотреть рисунок {n}",
- "viewPhoto": "Посмотреть фото {n}"
+ "viewPhoto": "Посмотреть фото {n}",
+ "attachmentDrawing": "рисунок",
+ "attachmentPhoto": "фото"
},
"pages": {
"alreadyPublished": "Этот пост уже опубликован",
@@ -4489,7 +4493,7 @@
"password": "Пароль",
"phone": "Телефон",
"photoLabel": "📷 Фото",
- "post": "Сообщение",
+ "post": "Публикация",
"postLabel": "📷 Публикация",
"postNotFound": "Пост не найден",
"posts": "Сообщения",
@@ -4551,7 +4555,8 @@
"you": "ты",
"youAreTheHost": "Вы ведущий(-ая)",
"youColon": "Вы: {body}",
- "yourStory": "Ваша история"
+ "yourStory": "Ваша история",
+ "submitComment": "Отправить"
},
"photos": {
"add": "Добавить",
@@ -5191,8 +5196,8 @@
"platePlaceholder": "например 12 АВС 34",
"rateYourDriver": "Оцените своего водителя",
"rating": "Рейтинг",
- "ratingRider": "{rating} · Райдер",
- "ratingsCount": "{n} рейтинг{plural}",
+ "ratingRider": "{rating} · Пассажир",
+ "ratingsCount": "Оценок: {n}{plural}",
"requestARide": "Запросить поездку?",
"requestRide": "Запросить поездку",
"requestRideMessage": "Ближайшему водителю будет предложено отвезти вас до {name}, а затем предложить приемлемый для вас тариф.",
@@ -5241,9 +5246,9 @@
"totalTrips": "Всего поездок",
"trips": "Поездки",
"tripsCount": "{n} поездок",
- "tripsEarned": "{n} поездка{plural} · {amount} заработано",
+ "tripsEarned": "Поездок: {n}{plural} · Заработано: {amount}",
"tripsTaken": "Совершено поездок",
- "tripsTodayCount": "{n} поездка{plural} сегодня",
+ "tripsTodayCount": "Поездок сегодня: {n}{plural}",
"unregister": "Отменить регистрацию",
"unregisterMessage": "{vehicle}{plate} будет удален. Ваш рейтинг и количество поездок сохраняются, и вы сразу можете зарегистрировать новый автомобиль.",
"unregisterTitle": "Отменить регистрацию автомобиля?",
@@ -6434,7 +6439,15 @@
"youtubeLink": "Ссылка YouTube",
"fromVoiceMemos": "Из Голосовых заметок",
"toneLimit": "Можно сохранить до {n} собственных мелодий. Сначала удалите одну выше.",
- "voiceMemoToneHint": "Выбор записи переносит её в ваши мелодии выше."
+ "voiceMemoToneHint": "Выбор записи переносит её в ваши мелодии выше.",
+ "phoneColorBlack": "Чёрный",
+ "phoneColorBlue": "Синий",
+ "phoneColorGreen": "Зелёный",
+ "phoneColorOrange": "Оранжевый",
+ "phoneColorPink": "Розовый",
+ "phoneColorPurple": "Фиолетовый",
+ "phoneColorRed": "Красный",
+ "phoneColorYellow": "Жёлтый"
},
"setup": {
"allSet": "Все готово",
@@ -6485,9 +6498,21 @@
"recipientNoLongerNearby": "Получателя больше нет рядом",
"requestExpired": "Срок запроса истёк",
"slowDownMoment": "Не так быстро",
- "tooLargeShare": "Это слишком большое, чтобы поделиться",
- "tooManyPendingShares": "Слишком много ожидающих отправлений",
- "unknownShareType": "Неизвестный тип отправления"
+ "tooLargeShare": "Объект слишком большой для передачи",
+ "tooManyPendingShares": "Слишком много запросов на передачу без ответа",
+ "unknownShareType": "Неизвестный тип передаваемого объекта",
+ "couldNotSaveSharedItem": "Не удалось сохранить: {kind}.",
+ "kindContact": "контакт",
+ "kindDocument": "документ",
+ "kindIdCard": "удостоверение",
+ "kindNote": "заметка",
+ "kindPhoto": "фото",
+ "kindPhotogramProfile": "профиль Photogram",
+ "kindPin": "метка на карте",
+ "kindPlaylist": "плейлист",
+ "kindSignatureRequest": "запрос подписи",
+ "kindSong": "песня",
+ "kindVoice": "голосовая заметка"
},
"shell": {
"airplaneMode": "Режим полета",
@@ -7166,7 +7191,13 @@
"ringtoneSet": "Рингтон установлен",
"toneLimit": "Можно сохранить до {n} собственных мелодий. Сначала удалите одну в Настройках.",
"useAsNotification": "Использовать как звук уведомления",
- "useAsRingtone": "Использовать как рингтон"
+ "useAsRingtone": "Использовать как рингтон",
+ "badAudioPayload": "Некорректные аудиоданные",
+ "couldNotSaveMemo": "Не удалось сохранить запись",
+ "recordingTooLong": "Запись слишком длинная",
+ "uploadBusy": "Запись уже загружается",
+ "uploadLimitReached": "Достигнут лимит загрузок. Попробуйте позже.",
+ "uploadSlowDown": "Подождите немного перед повторной загрузкой"
},
"weather": {
"closeWeather": "Закрыть Погоду",
@@ -7280,9 +7311,9 @@
"standfirstPlaceholder": "Однострочное резюме, отображаемое под заголовком (необязательно).",
"stories": "Истории",
"storyHeadlinePlaceholder": "Заголовок материала",
- "tapGearToPublish": "Нажмите на шестеренку, чтобы опубликовать первую историю.",
- "tapNewPost": "Нажмите «Новая запись», чтобы опубликовать первую историю.",
- "tickerSummary": "{count} заголовок{plural} · {first}",
+ "tapGearToPublish": "Нажмите на шестерёнку, чтобы опубликовать первый материал.",
+ "tapNewPost": "Нажмите «Новый пост», чтобы опубликовать первый материал.",
+ "tickerSummary": "Заголовков: {count}{plural} · {first}",
"timeAgo": "{time} назад",
"url": "URL-адрес",
"viewsMillions": "{n}M",
@@ -7385,4 +7416,4 @@
"noNumberToShare": "У вашего телефона нет номера, которым можно поделиться.",
"theyHaveNoPhone": "У этого человека нет телефона."
}
-}
\ No newline at end of file
+}
diff --git a/server/share/core.lua b/server/share/core.lua
index 6d8327cf1..6ce38ec88 100644
--- a/server/share/core.lua
+++ b/server/share/core.lua
@@ -204,10 +204,16 @@ function core.respond(src, id, accept)
-- just watch the prompt disappear: the recipient's client fires the respond callback and
-- drops the card without reading the result, and the sender was only ever told on success.
-- The recipient gets the handler's specific reason; the sender only needs to know it missed.
- TriggerClientEvent('sd-phone:client:notify', src, {
+ local recipientNotice = {
app = 'phone', title = 'AirShare',
- body = reason or ('That %s could not be saved.'):format(what),
- })
+ bodyKey = 'share.couldNotSaveSharedItem', body = ('That %s could not be saved.'):format(what),
+ bodyVars = { kind = what },
+ }
+ -- Handler-provided reasons are custom content, not the generic kind template.
+ if reason then
+ recipientNotice.body, recipientNotice.bodyKey, recipientNotice.bodyVars = reason, nil, nil
+ end
+ TriggerClientEvent('sd-phone:client:notify', src, recipientNotice)
TriggerClientEvent('sd-phone:client:notify', req.fromSrc, {
app = 'phone', title = 'AirShare',
bodyKey = 'share.couldNotSaveYourShare', body = ('%s could not save your %s.'):format(who, what),
diff --git a/server/voicememos/init.lua b/server/voicememos/init.lua
index 242309bed..5fb1cd1ac 100644
--- a/server/voicememos/init.lua
+++ b/server/voicememos/init.lua
@@ -115,9 +115,9 @@ util.onCleanup(function(src) pendingDirect[src] = nil end)
---@param why string|nil 'cooldown'|'busy'|'identity'|'budget'|'server'
---@return string message
local function refusal(why)
- if why == 'cooldown' then return 'Slow down a moment' end
- if why == 'busy' then return 'Upload already in progress' end
- return 'Upload limit reached, try again later'
+ if why == 'cooldown' then return 'Slow down a moment', 'voicememos.uploadSlowDown' end
+ if why == 'busy' then return 'Upload already in progress', 'voicememos.uploadBusy' end
+ return 'Upload limit reached, try again later', 'voicememos.uploadLimitReached'
end
---Takes a recorded memo as a base64 audio data-URL, pushes it to the media provider and saves it.
@@ -129,15 +129,15 @@ end
---@param prepaid boolean|nil true when an HTTP slot already holds the budget for it
local function ingest(src, audio, name, duration, prepaid)
if type(audio) ~= 'string' or not lib.string.startsWith(audio, 'data:audio/') then
- TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Bad audio payload')
+ TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Bad audio payload', 'voicememos.badAudioPayload')
return
end
if #audio > VM.MaxAudioBytes then
- TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Recording is too long')
+ TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Recording is too long', 'voicememos.recordingTooLong')
return
end
if uploading[src] then
- TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Upload already in progress')
+ TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Upload already in progress', 'voicememos.uploadBusy')
return
end
if not prepaid then
@@ -159,14 +159,14 @@ local function ingest(src, audio, name, duration, prepaid)
uploading[src] = nil
if not url then
print(('^1[sd-phone:voice]^0 upload failed: %s'):format(tostring(err)))
- TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, err or 'Upload failed')
+ TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Upload failed', 'voicememos.uploadFailed')
return
end
local memo = actions.saveUploaded(src, url, name, duration)
if memo then
TriggerClientEvent('sd-phone:client:voice:added', src, memo)
else
- TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Could not save memo')
+ TriggerClientEvent('sd-phone:client:voice:uploadFailed', src, 'Could not save memo', 'voicememos.couldNotSaveMemo')
end
end)
end
diff --git a/web/src/apps/banking/bankingApi.ts b/web/src/apps/banking/bankingApi.ts
index 32f66b7ac..af2621bb9 100644
--- a/web/src/apps/banking/bankingApi.ts
+++ b/web/src/apps/banking/bankingApi.ts
@@ -1,5 +1,5 @@
import { fetchNui, isFiveM } from '@/core/nui';
-import { t } from '@/i18n';
+import { getLocaleTag, t } from '@/i18n';
import { ACCOUNTS, TRANSACTIONS } from './data';
import { apiData, type Envelope } from '@/core/api';
import { formatClockTime } from '@/lib/time';
@@ -243,7 +243,7 @@ export function groupTx(txs: BankTx[]): BankDay[] {
let label: string;
if (key === todayK) label = t('banking.today', 'Today');
else if (key === yestK) label = t('banking.yesterday', 'Yesterday');
- else label = new Date(key + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
+ else label = new Date(key + 'T00:00:00').toLocaleDateString(getLocaleTag(), { weekday: 'short', month: 'short', day: 'numeric' });
return { key, label, items: items.sort((a, b) => b.date.localeCompare(a.date)) };
});
}
@@ -254,5 +254,5 @@ export function txTimeLabel(dateStr: string): string {
if (dateStr.slice(0, 10) === todayK) {
return formatClockTime(d, true);
}
- return d.toLocaleDateString('en-US', { day: 'numeric', month: 'short' });
+ return d.toLocaleDateString(getLocaleTag(), { day: 'numeric', month: 'short' });
}
diff --git a/web/src/apps/homes/Homes.tsx b/web/src/apps/homes/Homes.tsx
index bcc661044..4c0c34192 100644
--- a/web/src/apps/homes/Homes.tsx
+++ b/web/src/apps/homes/Homes.tsx
@@ -13,7 +13,7 @@ import { PromptDialog } from '@/ui/PromptDialog';
import { EmptyState } from '@/ui/EmptyState';
import { NavBar } from '@/ui/NavBar';
import { HOMES, DEV_CAPS, type Home, type HomesCaps, type KeyHolder } from './data';
-import { t } from '@/i18n';
+import { getLocaleTag, t } from '@/i18n';
import { Pill } from '@/ui/Pill';
import { StatusBarSpacer } from '@/ui/StatusBarSpacer';
@@ -43,6 +43,19 @@ export function Homes({ onClose: _onClose }: { onClose: () => void }) {
refetch();
}, [refetch]));
const homes = list?.homes ?? (isFiveM ? [] : HOMES);
+ const pluralProperties = t('homes.properties', 'properties');
+ const fewProperties = t('homes.propertiesFew', 'properties');
+ // Only catalogs with a dedicated few form opt into locale plural rules.
+ // Existing catalogs retain their original count === 1 singular behavior.
+ const propertyForm = fewProperties !== 'properties'
+ ? new Intl.PluralRules(getLocaleTag()).select(homes.length)
+ : homes.length === 1 ? 'one' : 'other';
+ const propertyLabel = propertyForm === 'one'
+ ? t('homes.property', 'property')
+ // Catalogs without a separate few form retain their existing localized plural.
+ : propertyForm === 'few' && fewProperties !== 'properties'
+ ? fewProperties
+ : pluralProperties;
const open = homes.find(h => h.id === openId) ?? null;
@@ -65,7 +78,7 @@ export function Homes({ onClose: _onClose }: { onClose: () => void }) {
{t('homes.title','Homes')}
-
{homes.length} {homes.length === 1 ? t('homes.property','property') : t('homes.properties','properties')}
+
{homes.length} {propertyLabel}
{loading && homes.length === 0 ? null : homes.length === 0 ? (
diff --git a/web/src/apps/music/Music.tsx b/web/src/apps/music/Music.tsx
index 34b431272..2fc5d422b 100644
--- a/web/src/apps/music/Music.tsx
+++ b/web/src/apps/music/Music.tsx
@@ -6,7 +6,7 @@ import {
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
-import { t } from '@/i18n';
+import { pluralSuffix, t } from '@/i18n';
import { useSessionState, clearSessionState } from '@/hooks/useSessionState';
import { useTheme } from '@/stores/themeStore';
import { useDidEnter } from '@/hooks/useDidEnter';
@@ -233,7 +233,7 @@ export function Music({ onClose: _onClose }: { onClose: () => void }) {
key={f.id}
art={}
title={f.name}
- subtitle={t('music.playlistSongCount', 'Playlist · {count} song{plural}', { count: f.trackIds.length, plural: f.trackIds.length === 1 ? '' : 's' })}
+ subtitle={t('music.playlistSongCount', 'Playlist · {count} song{plural}', { count: f.trackIds.length, plural: pluralSuffix(f.trackIds.length) })}
onPress={() => push({ kind: 'playlist', id: f.id })}
divider={i < folders.length - 1}
action={f.trackIds.length > 0 ? setSharing({ kind: 'playlist', folder: f })} label={t('music.shareName', 'Share {name}', { name: f.name })} /> : undefined}
@@ -285,7 +285,7 @@ export function Music({ onClose: _onClose }: { onClose: () => void }) {
{artistHits.map((a, i) => (
push({ kind: 'artist', name: a.name })} divider={i < artistHits.length - 1}
art={}
- title={a.name} subtitle={t('music.songCount', '{count} song{plural}', { count: a.tracks.length, plural: a.tracks.length === 1 ? '' : 's' })} />
+ title={a.name} subtitle={t('music.songCount', '{count} song{plural}', { count: a.tracks.length, plural: pluralSuffix(a.tracks.length) })} />
))}
{albumHits.length > 0 && {t('music.albums', 'Albums')}}
@@ -320,7 +320,7 @@ export function Music({ onClose: _onClose }: { onClose: () => void }) {
) : artists.map((a, i) => (
push({ kind: 'artist', name: a.name })} divider={i < artists.length - 1}
art={}
- title={a.name} subtitle={t('music.songCount', '{count} song{plural}', { count: a.tracks.length, plural: a.tracks.length === 1 ? '' : 's' })} />
+ title={a.name} subtitle={t('music.songCount', '{count} song{plural}', { count: a.tracks.length, plural: pluralSuffix(a.tracks.length) })} />
))}
>
@@ -338,7 +338,7 @@ export function Music({ onClose: _onClose }: { onClose: () => void }) {
}
title={f.name}
- subtitle={t('music.playlistSongCount', 'Playlist · {count} song{plural}', { count: f.trackIds.length, plural: f.trackIds.length === 1 ? '' : 's' })}
+ subtitle={t('music.playlistSongCount', 'Playlist · {count} song{plural}', { count: f.trackIds.length, plural: pluralSuffix(f.trackIds.length) })}
onPress={() => push({ kind: 'playlist', id: f.id })}
divider={i < folders.length - 1}
action={f.trackIds.length > 0 ? setSharing({ kind: 'playlist', folder: f })} label={t('music.shareName', 'Share {name}', { name: f.name })} /> : undefined}
@@ -448,7 +448,7 @@ export function Music({ onClose: _onClose }: { onClose: () => void }) {
) : (
{folder.name}
)}
- {t('music.songCount', '{count} song{plural}', { count: folderTracks.length, plural: folderTracks.length === 1 ? '' : 's' })}
+ {t('music.songCount', '{count} song{plural}', { count: folderTracks.length, plural: pluralSuffix(folderTracks.length) })}
diff --git a/web/src/apps/notes/NoteEditor.tsx b/web/src/apps/notes/NoteEditor.tsx
index 12891443a..e95eb4ad5 100644
--- a/web/src/apps/notes/NoteEditor.tsx
+++ b/web/src/apps/notes/NoteEditor.tsx
@@ -369,7 +369,11 @@ export function NoteEditor({
message={t(
"notes.removeAttachmentBody",
"This {kind} will be removed from the note.",
- { kind: pendingRemove.kind },
+ {
+ kind: pendingRemove.kind === "photo"
+ ? t("notes.attachmentPhoto", "photo")
+ : t("notes.attachmentDrawing", "drawing"),
+ },
)}
confirmLabel={t("notes.remove", "Remove")}
cancelLabel={t("notes.cancel", "Cancel")}
diff --git a/web/src/apps/photogram/feed/Comments.tsx b/web/src/apps/photogram/feed/Comments.tsx
index 50ae94526..d0a5b7640 100644
--- a/web/src/apps/photogram/feed/Comments.tsx
+++ b/web/src/apps/photogram/feed/Comments.tsx
@@ -86,7 +86,9 @@ export function Comments({ post, me, comments, onBack, onSubmit, onToggleLike, o
className="min-w-0 flex-1 bg-transparent text-[18px] text-black outline-none placeholder:text-[#9b9b9b]"
/>
diff --git a/web/src/apps/ryde/driver/Driver.tsx b/web/src/apps/ryde/driver/Driver.tsx
index f35fa04c8..b3eadf99b 100644
--- a/web/src/apps/ryde/driver/Driver.tsx
+++ b/web/src/apps/ryde/driver/Driver.tsx
@@ -4,7 +4,7 @@ import { Car, ChevronRight, Download, Inbox, Navigation, Power, Star, TrendingUp
import { useSessionState } from '@/hooks/useSessionState';
import { SegmentedControl } from '@/ui/SegmentedControl';
import { AlertDialog } from '@/ui/AlertDialog';
-import { t } from '@/i18n';
+import { pluralSuffix, t } from '@/i18n';
import { driverStats, useRyde } from '../store';
import { CAR_COLORS, money } from '../data';
import { BigButton } from '../ui';
@@ -140,7 +140,7 @@ function Dashboard() {
{t('ryde.todaysEarnings', 'Today’s earnings')}
{money(s.earnToday)}
- {t('ryde.tripsTodayCount', '{n} trip{plural} today', { n: s.tripsToday, plural: s.tripsToday === 1 ? '' : 's' })}
+ {t('ryde.tripsTodayCount', '{n} trip{plural} today', { n: s.tripsToday, plural: pluralSuffix(s.tripsToday) })}