From f7a849ddcb468e0cbce2a7e30de65280b30d4431 Mon Sep 17 00:00:00 2001 From: vsecoder Date: Sat, 18 Apr 2026 23:39:38 +0300 Subject: [PATCH] chore(client): translate all user-visible strings to English MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mixed-language UI was confusing — onboarding said "Why DChain / How it works / Your keys" in English headings but feature descriptions and CTAs were in Russian; compose's confirm dialog was Russian; feed tabs were Russian; error messages in humanizeTxError were Russian. Everything user-facing is now English. Files touched (only string literals, not comments): app/index.tsx onboarding slides + CTA buttons app/(app)/compose.tsx composer alerts, header button, placeholder, attachment-size hint app/(app)/feed/index.tsx tab labels (Following/For you/Trending), empty-state hints, retry button app/(app)/feed/[id].tsx post detail header + stats rows (Views, Likes, Size, Paid to publish, Hosted on, Hashtags) app/(app)/feed/tag/[tag].tsx empty-state copy app/(app)/profile/[address].tsx Profile header, Follow/Following, Edit, Open chat, Address, Copied, Encryption, Added, Members, unknown-contact hint app/(app)/new-contact.tsx Search title, placeholder, Search button, empty-state hint, E2E-ready indicator, Intro label + placeholder, fee-tier labels (Min / Standard / Priority), Send request, Insufficient-balance alert, Request-sent alert app/(app)/requests.tsx Notifications title, empty-state, Accept / Decline buttons, decline-confirm alert, "wants to add you" line components/SearchBar.tsx default placeholder components/feed/PostCard.tsx long-press menu (Delete post, confirm, Actions / Cancel) components/feed/ShareSheet.tsx sheet title, contact-search placeholder, empty state, Select contacts / Send button, plural helper rewritten for English components/chat/PostRefCard.tsx "POST" ribbon, "photo" indicator lib/api.ts humanizeTxError (rate-limit, clock skew, bad signature, 400/5xx/network-error messages) lib/dates.ts dateBucket now returns Today/Yesterday/ "Jun 17, 2025"; month array switched to English short forms Code comments left in Russian intentionally — they're developer context, not user-facing. This commit is purely display-string. Co-Authored-By: Claude Opus 4.7 (1M context) --- client-app/app/(app)/compose.tsx | 34 +++++++++---------- client-app/app/(app)/feed/[id].tsx | 18 +++++----- client-app/app/(app)/feed/index.tsx | 20 ++++++------ client-app/app/(app)/feed/tag/[tag].tsx | 4 +-- client-app/app/(app)/new-contact.tsx | 38 +++++++++++----------- client-app/app/(app)/profile/[address].tsx | 24 +++++++------- client-app/app/(app)/requests.tsx | 22 ++++++------- client-app/app/index.tsx | 38 +++++++++++----------- client-app/components/SearchBar.tsx | 2 +- client-app/components/chat/PostRefCard.tsx | 4 +-- client-app/components/feed/PostCard.tsx | 16 ++++----- client-app/components/feed/ShareSheet.tsx | 23 +++++-------- client-app/lib/api.ts | 12 +++---- client-app/lib/dates.ts | 18 +++++----- 14 files changed, 134 insertions(+), 139 deletions(-) diff --git a/client-app/app/(app)/compose.tsx b/client-app/app/(app)/compose.tsx index 41ac210..e3ca43a 100644 --- a/client-app/app/(app)/compose.tsx +++ b/client-app/app/(app)/compose.tsx @@ -98,11 +98,11 @@ export default function ComposeScreen() { const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (!perm.granted) { Alert.alert( - 'Нужен доступ к фото', - 'Откройте настройки и разрешите доступ к галерее.', + 'Photo access required', + 'Please enable photo library access in Settings.', [ - { text: 'Отмена' }, - { text: 'Настройки', onPress: () => Linking.openSettings() }, + { text: 'Cancel' }, + { text: 'Settings', onPress: () => Linking.openSettings() }, ], ); return; @@ -132,8 +132,8 @@ export default function ComposeScreen() { if (bytes.length > MAX_POST_BYTES - 512) { Alert.alert( - 'Слишком большое', - `Картинка ${Math.round(bytes.length / 1024)} KB — лимит ${MAX_POST_BYTES / 1024} KB. Попробуйте выбрать поменьше.`, + 'Image too large', + `Image is ${Math.round(bytes.length / 1024)} KB but the limit is ${MAX_POST_BYTES / 1024} KB. Try picking a smaller one.`, ); return; } @@ -147,7 +147,7 @@ export default function ComposeScreen() { height: manipulated.height, }); } catch (e: any) { - Alert.alert('Не удалось', String(e?.message ?? e)); + Alert.alert('Failed', String(e?.message ?? e)); } finally { setPicking(false); } @@ -159,19 +159,19 @@ export default function ComposeScreen() { // Balance guard. if (balance !== null && balance < estimatedFee) { Alert.alert( - 'Недостаточно средств', - `Нужно ${formatFee(estimatedFee)}, на балансе ${formatFee(balance)}.`, + 'Insufficient balance', + `Need ${formatFee(estimatedFee)}, have ${formatFee(balance)}.`, ); return; } Alert.alert( - 'Опубликовать пост?', - `Цена: ${formatFee(estimatedFee)}\nРазмер: ${Math.round(totalBytes / 1024 * 10) / 10} KB`, + 'Publish post?', + `Cost: ${formatFee(estimatedFee)}\nSize: ${Math.round(totalBytes / 1024 * 10) / 10} KB`, [ - { text: 'Отмена', style: 'cancel' }, + { text: 'Cancel', style: 'cancel' }, { - text: 'Опубликовать', + text: 'Publish', onPress: async () => { setBusy(true); try { @@ -185,7 +185,7 @@ export default function ComposeScreen() { // Close composer and open the new post. router.replace(`/(app)/feed/${postID}` as never); } catch (e: any) { - Alert.alert('Не удалось опубликовать', humanizeTxError(e)); + Alert.alert('Failed to publish', humanizeTxError(e)); } finally { setBusy(false); } @@ -235,7 +235,7 @@ export default function ComposeScreen() { fontSize: 14, }} > - Опубликовать + Publish )} @@ -251,7 +251,7 @@ export default function ComposeScreen() { - {Math.round(attach.size / 1024)} KB · метаданные удалят на сервере + {Math.round(attach.size / 1024)} KB · metadata stripped on server )} diff --git a/client-app/app/(app)/feed/[id].tsx b/client-app/app/(app)/feed/[id].tsx index 355baa9..46fb1af 100644 --- a/client-app/app/(app)/feed/[id].tsx +++ b/client-app/app/(app)/feed/[id].tsx @@ -79,7 +79,7 @@ export default function PostDetailScreen() {
router.back()} />} - title="Пост" + title="Post" /> {loading ? ( @@ -94,7 +94,7 @@ export default function PostDetailScreen() { - Пост удалён или больше недоступен + Post deleted or no longer available ) : ( @@ -131,18 +131,18 @@ export default function PostDetailScreen() { textTransform: 'uppercase', marginBottom: 10, }}> - Информация о посте + Post details - - - + + + @@ -151,7 +151,7 @@ export default function PostDetailScreen() { <> - Хештеги + Hashtags {post.hashtags.map(tag => ( diff --git a/client-app/app/(app)/feed/index.tsx b/client-app/app/(app)/feed/index.tsx index 4102028..82c7563 100644 --- a/client-app/app/(app)/feed/index.tsx +++ b/client-app/app/(app)/feed/index.tsx @@ -31,9 +31,9 @@ import { type TabKey = 'following' | 'foryou' | 'trending'; const TAB_LABELS: Record = { - following: 'Подписки', - foryou: 'Для вас', - trending: 'В тренде', + following: 'Following', + foryou: 'For you', + trending: 'Trending', }; export default function FeedScreen() { @@ -201,15 +201,15 @@ export default function FeedScreen() { const emptyHint = useMemo(() => { switch (tab) { - case 'following': return 'Подпишитесь на кого-нибудь, чтобы увидеть их посты здесь.'; - case 'foryou': return 'Пока нет рекомендаций — возвращайтесь позже.'; - case 'trending': return 'В этой ленте пока тихо.'; + case 'following': return 'Follow someone to see their posts here.'; + case 'foryou': return 'No recommendations yet — come back later.'; + case 'trending': return 'Nothing trending yet.'; } }, [tab]); return ( - + {/* Tab strip — три таба, равномерно распределены по ширине (justifyContent: space-between). Каждый Pressable hug'ает @@ -305,14 +305,14 @@ export default function FeedScreen() { ) : error ? ( loadPosts(false)} /> ) : ( ) @@ -409,7 +409,7 @@ function EmptyState({ })} > - Попробовать снова + Try again )} diff --git a/client-app/app/(app)/feed/tag/[tag].tsx b/client-app/app/(app)/feed/tag/[tag].tsx index 026de04..fb8b42a 100644 --- a/client-app/app/(app)/feed/tag/[tag].tsx +++ b/client-app/app/(app)/feed/tag/[tag].tsx @@ -119,10 +119,10 @@ export default function HashtagScreen() { }}> - Пока нет постов с этим тегом + No posts with this tag yet - Будьте первым — напишите пост с #{tag} + Be the first — write a post with #{tag} ) diff --git a/client-app/app/(app)/new-contact.tsx b/client-app/app/(app)/new-contact.tsx index 2526804..ca73657 100644 --- a/client-app/app/(app)/new-contact.tsx +++ b/client-app/app/(app)/new-contact.tsx @@ -27,9 +27,9 @@ import { SearchBar } from '@/components/SearchBar'; const MIN_CONTACT_FEE = 5000; const FEE_TIERS = [ - { value: 5_000, label: 'Базовая' }, - { value: 10_000, label: 'Стандарт' }, - { value: 50_000, label: 'Приоритет' }, + { value: 5_000, label: 'Min' }, + { value: 10_000, label: 'Standard' }, + { value: 50_000, label: 'Priority' }, ]; interface Resolved { @@ -61,7 +61,7 @@ export default function NewContactScreen() { if (q.startsWith('@') || (!q.match(/^[0-9a-f]{64}$/i) && !q.startsWith('DC'))) { const name = q.replace('@', ''); const addr = await resolveUsername(settings.contractId, name); - if (!addr) { setError(`@${name} не зарегистрирован в этой сети`); return; } + if (!addr) { setError(`@${name} is not registered on this chain`); return; } address = addr; } const identity = await getIdentity(address); @@ -71,7 +71,7 @@ export default function NewContactScreen() { x25519: identity?.x25519_pub || undefined, }); } catch (e: any) { - setError(e?.message ?? 'Не удалось найти пользователя'); + setError(e?.message ?? 'Lookup failed'); } finally { setSearching(false); } @@ -80,7 +80,7 @@ export default function NewContactScreen() { async function sendRequest() { if (!resolved || !keyFile) return; if (balance < fee + 1000) { - Alert.alert('Недостаточно средств', `Нужно ${formatAmount(fee + 1000)} (плата + сетевая комиссия).`); + Alert.alert('Insufficient balance', `Need ${formatAmount(fee + 1000)} (request fee + network).`); return; } setSending(true); setError(null); @@ -94,8 +94,8 @@ export default function NewContactScreen() { }); await submitTx(tx); Alert.alert( - 'Запрос отправлен', - `Запрос на общение отправлен ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`, + 'Request sent', + `A contact request has been sent to ${resolved.nickname ? '@' + resolved.nickname : shortAddr(resolved.address)}.`, [{ text: 'OK', onPress: () => router.back() }], ); } catch (e: any) { @@ -112,7 +112,7 @@ export default function NewContactScreen() { return (
router.back()} />} /> @@ -124,7 +124,7 @@ export default function NewContactScreen() { { setResolved(null); setError(null); }} @@ -143,7 +143,7 @@ export default function NewContactScreen() { {searching ? ( ) : ( - Найти + Search )} )} @@ -163,11 +163,11 @@ export default function NewContactScreen() { - Найдите собеседника + Find someone to message - Введите @username, - если человек зарегистрировал ник, либо полный hex pubkey или DC… адрес. + Enter an @username if + the person registered one, or paste a full hex pubkey or DC… address. )} @@ -218,7 +218,7 @@ export default function NewContactScreen() { color: resolved.x25519 ? '#3ba55d' : '#f0b35a', fontSize: 11, fontWeight: '500', }}> - {resolved.x25519 ? 'E2E готов' : 'Ключ ещё не опубликован'} + {resolved.x25519 ? 'E2E ready' : 'Key not published yet'} @@ -227,12 +227,12 @@ export default function NewContactScreen() { {/* Intro */} - Сообщение (опционально, видно в открытом виде на chain) + Intro (optional, plaintext on-chain) - Плата за запрос (уходит получателю, anti-spam) + Anti-spam fee (goes to the recipient) {/* Fee-tier pills. Layout (background, border, padding) lives on a static @@ -314,7 +314,7 @@ export default function NewContactScreen() { ) : ( - Отправить запрос · {formatAmount(fee + 1000)} + Send request · {formatAmount(fee + 1000)} )} diff --git a/client-app/app/(app)/profile/[address].tsx b/client-app/app/(app)/profile/[address].tsx index 35e8391..1b4e634 100644 --- a/client-app/app/(app)/profile/[address].tsx +++ b/client-app/app/(app)/profile/[address].tsx @@ -48,7 +48,7 @@ export default function ProfileScreen() { const isMe = !!keyFile && keyFile.pub_key === address; const displayName = contact?.username ? `@${contact.username}` - : contact?.alias ?? (isMe ? 'Вы' : shortAddr(address ?? '', 6)); + : contact?.alias ?? (isMe ? 'You' : shortAddr(address ?? '', 6)); const copyAddress = async () => { if (!address) return; @@ -85,7 +85,7 @@ export default function ProfileScreen() { return (
router.back()} />} /> @@ -124,7 +124,7 @@ export default function ProfileScreen() { fontSize: 13, }} > - {following ? 'Вы подписаны' : 'Подписаться'} + {following ? 'Following' : 'Follow'} )} @@ -139,7 +139,7 @@ export default function ProfileScreen() { })} > - Редактировать + Edit )} @@ -177,7 +177,7 @@ export default function ProfileScreen() { > - Открыть чат + Open chat )} @@ -203,7 +203,7 @@ export default function ProfileScreen() { })} > - Адрес + Address - {copied ? 'Скопировано' : shortAddr(address ?? '')} + {copied ? 'Copied' : shortAddr(address ?? '')} @@ -251,7 +251,7 @@ export default function ProfileScreen() { <> @@ -270,7 +270,7 @@ export default function ProfileScreen() { paddingHorizontal: 24, lineHeight: 17, }}> - Этот пользователь пока не в ваших контактах. Нажмите «Подписаться», чтобы видеть его посты в ленте, или добавьте в чаты через @username. + This user isn't in your contacts yet. Tap "Follow" to see their posts in your feed, or add them as a chat contact via @username. )} diff --git a/client-app/app/(app)/requests.tsx b/client-app/app/(app)/requests.tsx index dad308f..ced7848 100644 --- a/client-app/app/(app)/requests.tsx +++ b/client-app/app/(app)/requests.tsx @@ -51,7 +51,7 @@ export default function RequestsScreen() { setRequests(requests.filter(r => r.txHash !== req.txHash)); router.replace(`/(app)/chats/${req.from}` as never); } catch (e: any) { - Alert.alert('Не удалось принять', humanizeTxError(e)); + Alert.alert('Accept failed', humanizeTxError(e)); } finally { setAccepting(null); } @@ -59,12 +59,12 @@ export default function RequestsScreen() { function decline(req: ContactRequest) { Alert.alert( - 'Отклонить запрос', - `Отклонить запрос от ${req.username ? '@' + req.username : shortAddr(req.from)}?`, + 'Decline request', + `Decline request from ${req.username ? '@' + req.username : shortAddr(req.from)}?`, [ - { text: 'Отмена', style: 'cancel' }, + { text: 'Cancel', style: 'cancel' }, { - text: 'Отклонить', + text: 'Decline', style: 'destructive', onPress: () => setRequests(requests.filter(r => r.txHash !== req.txHash)), }, @@ -91,7 +91,7 @@ export default function RequestsScreen() { {name} - хочет добавить вас в контакты · {relativeTime(req.timestamp)} + wants to add you as a contact · {relativeTime(req.timestamp)} {req.intro ? ( ) : ( - Принять + Accept )} - Отклонить + Decline @@ -147,16 +147,16 @@ export default function RequestsScreen() { return ( - + {requests.length === 0 ? ( - Всё прочитано + All caught up - Запросы на общение и события сети появятся здесь. + Contact requests and network events will appear here. ) : ( diff --git a/client-app/app/index.tsx b/client-app/app/index.tsx index 263e58a..7cf5d6a 100644 --- a/client-app/app/index.tsx +++ b/client-app/app/index.tsx @@ -187,17 +187,17 @@ export default function WelcomeScreen() { @@ -206,7 +206,7 @@ export default function WelcomeScreen() { flexDirection: 'row', justifyContent: 'flex-end', paddingHorizontal: 24, paddingBottom: 8, }}> - goToPage(1)} /> + goToPage(1)} /> @@ -223,22 +223,22 @@ export default function WelcomeScreen() { keyboardShouldPersistTaps="handled" > - Как это работает + How it works - Сообщения проходят через релей-ноду в зашифрованном виде. - Выбери публичную или подключи свою. + Messages travel through a relay node in encrypted form. + Pick a public one or run your own. Linking.openURL(GITEA_URL).catch(() => {})} /> - goToPage(2)} /> + goToPage(2)} /> @@ -334,11 +334,11 @@ export default function WelcomeScreen() { - Твой аккаунт + Your account - Создай новую пару ключей или импортируй существующую. - Ключи хранятся только на этом устройстве. + Generate a fresh keypair or import an existing one. + Keys stay on this device only. @@ -349,11 +349,11 @@ export default function WelcomeScreen() { paddingHorizontal: 24, paddingBottom: 8, }}> router.push('/(auth)/import' as never)} /> router.push('/(auth)/create' as never)} /> diff --git a/client-app/components/SearchBar.tsx b/client-app/components/SearchBar.tsx index fe972c9..ac2651c 100644 --- a/client-app/components/SearchBar.tsx +++ b/client-app/components/SearchBar.tsx @@ -18,7 +18,7 @@ export interface SearchBarProps { } export function SearchBar({ - value, onChangeText, placeholder = 'Поиск', autoFocus, onSubmitEditing, onClear, + value, onChangeText, placeholder = 'Search', autoFocus, onSubmitEditing, onClear, }: SearchBarProps) { return ( - ПОСТ + POST @@ -132,7 +132,7 @@ export function PostRefCard({ postID, author, excerpt, hasImage, own }: PostRefC > - с фото + photo )} diff --git a/client-app/components/feed/PostCard.tsx b/client-app/components/feed/PostCard.tsx index 6fbc8d0..b7b9548 100644 --- a/client-app/components/feed/PostCard.tsx +++ b/client-app/components/feed/PostCard.tsx @@ -109,7 +109,7 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }: // Roll back optimistic update. setLocalLiked(wasLiked); setLocalLikeCount(c => c + (wasLiked ? 1 : -1)); - Alert.alert('Не удалось', String(e?.message ?? e)); + Alert.alert('Failed', String(e?.message ?? e)); } finally { setBusy(false); } @@ -128,13 +128,13 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }: const options: Array<{ label: string; destructive?: boolean; onPress: () => void }> = []; if (mine) { options.push({ - label: 'Удалить пост', + label: 'Delete post', destructive: true, onPress: () => { - Alert.alert('Удалить пост?', 'Это действие нельзя отменить.', [ - { text: 'Отмена', style: 'cancel' }, + Alert.alert('Delete post?', 'This action cannot be undone.', [ + { text: 'Cancel', style: 'cancel' }, { - text: 'Удалить', + text: 'Delete', style: 'destructive', onPress: async () => { try { @@ -145,7 +145,7 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }: }); onDeleted?.(post.post_id); } catch (e: any) { - Alert.alert('Ошибка', String(e?.message ?? e)); + Alert.alert('Error', String(e?.message ?? e)); } }, }, @@ -160,9 +160,9 @@ function PostCardInner({ post, likedByMe, onStatsChanged, onDeleted, compact }: style: (o.destructive ? 'destructive' : 'default') as 'default' | 'destructive', onPress: o.onPress, })), - { text: 'Отмена', style: 'cancel' as const }, + { text: 'Cancel', style: 'cancel' as const }, ]; - Alert.alert('Действия', '', buttons); + Alert.alert('Actions', '', buttons); }, [keyFile, mine, post.post_id, onDeleted]); // Attachment preview URL — native Image can stream straight from the diff --git a/client-app/components/feed/ShareSheet.tsx b/client-app/components/feed/ShareSheet.tsx index 6e99588..5705144 100644 --- a/client-app/components/feed/ShareSheet.tsx +++ b/client-app/components/feed/ShareSheet.tsx @@ -74,14 +74,14 @@ export function ShareSheet({ visible, post, onClose }: ShareSheetProps) { post, contacts: targets, keyFile, }); if (failed > 0) { - Alert.alert('Готово', `Отправлено в ${ok} из ${ok + failed} чат${plural(ok + failed)}.`); + Alert.alert('Done', `Sent to ${ok} of ${ok + failed} ${plural(ok + failed)}.`); } // Close + reset regardless — done is done. setPicked(new Set()); setQuery(''); onClose(); } catch (e: any) { - Alert.alert('Не удалось', String(e?.message ?? e)); + Alert.alert('Failed', String(e?.message ?? e)); } finally { setSending(false); } @@ -136,7 +136,7 @@ export function ShareSheet({ visible, post, onClose }: ShareSheetProps) { paddingHorizontal: 16, marginBottom: 10, }}> - Поделиться постом + Share post @@ -158,7 +158,7 @@ export function ShareSheet({ visible, post, onClose }: ShareSheetProps) { {query.length > 0 - ? 'Нет контактов по такому запросу' - : 'Контакты с ключами шифрования отсутствуют'} + ? 'No contacts match this search' + : 'No contacts with encryption keys yet'} } @@ -227,8 +227,8 @@ export function ShareSheet({ visible, post, onClose }: ShareSheetProps) { fontSize: 14, }}> {picked.size === 0 - ? 'Выберите контакты' - : `Отправить (${picked.size})`} + ? 'Select contacts' + : `Send (${picked.size})`} )} @@ -298,10 +298,5 @@ function shortAddr(a: string, n = 6): string { } function plural(n: number): string { - const mod100 = n % 100; - const mod10 = n % 10; - if (mod100 >= 11 && mod100 <= 19) return 'ов'; - if (mod10 === 1) return ''; - if (mod10 >= 2 && mod10 <= 4) return 'а'; - return 'ов'; + return n === 1 ? 'chat' : 'chats'; } diff --git a/client-app/lib/api.ts b/client-app/lib/api.ts index 859af26..62d6d33 100644 --- a/client-app/lib/api.ts +++ b/client-app/lib/api.ts @@ -79,23 +79,23 @@ async function post(path: string, body: unknown): Promise { export function humanizeTxError(e: unknown): string { const raw = e instanceof Error ? e.message : String(e); if (raw.startsWith('429')) { - return 'Слишком много запросов к ноде. Подождите пару секунд и попробуйте снова.'; + return 'Too many requests to the node. Wait a couple of seconds and try again.'; } if (raw.startsWith('400') && raw.includes('timestamp')) { - return 'Часы устройства не синхронизированы с нодой. Проверьте время на телефоне (±1 час).'; + return 'Device clock is out of sync with the node. Check the time on your phone (±1 hour).'; } if (raw.startsWith('400') && raw.includes('signature')) { - return 'Подпись транзакции невалидна. Попробуйте ещё раз; если не помогает — вероятна несовместимость версий клиента и ноды.'; + return 'Transaction signature is invalid. Try again; if this persists the client and node versions may be incompatible.'; } if (raw.startsWith('400')) { - return `Нода отклонила транзакцию: ${raw.replace(/^400:\s*/, '')}`; + return `Node rejected transaction: ${raw.replace(/^400:\s*/, '')}`; } if (raw.startsWith('5')) { - return `Ошибка ноды (${raw}). Попробуйте позже.`; + return `Node error (${raw}). Please try again later.`; } // Network-level if (raw.toLowerCase().includes('network request failed')) { - return 'Нет связи с нодой. Проверьте URL в настройках и доступность сервера.'; + return 'Cannot reach the node. Check the URL in settings and that the server is online.'; } return raw; } diff --git a/client-app/lib/dates.ts b/client-app/lib/dates.ts index 8368e2b..989a36a 100644 --- a/client-app/lib/dates.ts +++ b/client-app/lib/dates.ts @@ -5,10 +5,10 @@ * который тоже в секундах). Для ms-таймштампов делаем нормализацию внутри. */ -// ─── Русские месяцы (genitive для "17 июня 2025") ──────────────────────────── -const RU_MONTHS_GEN = [ - 'января', 'февраля', 'марта', 'апреля', 'мая', 'июня', - 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря', +// English short month names ("Jun 17, 2025"). +const MONTHS_SHORT = [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', ]; function sameDay(a: Date, b: Date): boolean { @@ -20,8 +20,8 @@ function sameDay(a: Date, b: Date): boolean { } /** - * Day-bucket label для сепараторов внутри чата. - * "Сегодня" / "Вчера" / "17 июня 2025" + * Day-bucket label for chat separators. + * "Today" / "Yesterday" / "Jun 17, 2025" * * @param ts unix-seconds */ @@ -29,9 +29,9 @@ export function dateBucket(ts: number): string { const d = new Date(ts * 1000); const now = new Date(); const yday = new Date(); yday.setDate(now.getDate() - 1); - if (sameDay(d, now)) return 'Сегодня'; - if (sameDay(d, yday)) return 'Вчера'; - return `${d.getDate()} ${RU_MONTHS_GEN[d.getMonth()]} ${d.getFullYear()}`; + if (sameDay(d, now)) return 'Today'; + if (sameDay(d, yday)) return 'Yesterday'; + return `${MONTHS_SHORT[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } /**