chore(client): translate all user-visible strings to English

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) <noreply@anthropic.com>
This commit is contained in:
vsecoder
2026-04-18 23:39:38 +03:00
parent 060ac6c2c9
commit f7a849ddcb
14 changed files with 134 additions and 139 deletions

View File

@@ -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
</Text>
)}
</Pressable>
@@ -251,7 +251,7 @@ export default function ComposeScreen() {
<TextInput
value={content}
onChangeText={setContent}
placeholder="Что происходит?"
placeholder="What's happening?"
placeholderTextColor="#5a5a5a"
multiline
maxLength={MAX_CONTENT_LENGTH}
@@ -328,7 +328,7 @@ export default function ComposeScreen() {
</Pressable>
</View>
<Text style={{ color: '#6a6a6a', fontSize: 11, marginTop: 6 }}>
{Math.round(attach.size / 1024)} KB · метаданные удалят на сервере
{Math.round(attach.size / 1024)} KB · metadata stripped on server
</Text>
</View>
)}

View File

@@ -79,7 +79,7 @@ export default function PostDetailScreen() {
<Header
divider
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
title="Пост"
title="Post"
/>
{loading ? (
@@ -94,7 +94,7 @@ export default function PostDetailScreen() {
<View style={{ padding: 24, alignItems: 'center' }}>
<Ionicons name="trash-outline" size={32} color="#6a6a6a" />
<Text style={{ color: '#8b8b8b', marginTop: 10 }}>
Пост удалён или больше недоступен
Post deleted or no longer available
</Text>
</View>
) : (
@@ -131,18 +131,18 @@ export default function PostDetailScreen() {
textTransform: 'uppercase',
marginBottom: 10,
}}>
Информация о посте
Post details
</Text>
<DetailRow label="Просмотров" value={formatCount(stats?.views ?? post.views)} />
<DetailRow label="Лайков" value={formatCount(stats?.likes ?? post.likes)} />
<DetailRow label="Размер" value={`${Math.round(post.size / 1024 * 10) / 10} KB`} />
<DetailRow label="Views" value={formatCount(stats?.views ?? post.views)} />
<DetailRow label="Likes" value={formatCount(stats?.likes ?? post.likes)} />
<DetailRow label="Size" value={`${Math.round(post.size / 1024 * 10) / 10} KB`} />
<DetailRow
label="Стоимость публикации"
label="Paid to publish"
value={formatFee(1000 + post.size)}
/>
<DetailRow
label="Хостинг"
label="Hosted on"
value={shortAddr(post.hosting_relay)}
mono
/>
@@ -151,7 +151,7 @@ export default function PostDetailScreen() {
<>
<View style={{ height: 1, backgroundColor: '#1f1f1f', marginVertical: 10 }} />
<Text style={{ color: '#5a5a5a', fontSize: 11, marginBottom: 6 }}>
Хештеги
Hashtags
</Text>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 6 }}>
{post.hashtags.map(tag => (

View File

@@ -31,9 +31,9 @@ import {
type TabKey = 'following' | 'foryou' | 'trending';
const TAB_LABELS: Record<TabKey, string> = {
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 (
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<TabHeader title="Лента" />
<TabHeader title="Feed" />
{/* Tab strip — три таба, равномерно распределены по ширине
(justifyContent: space-between). Каждый Pressable hug'ает
@@ -305,14 +305,14 @@ export default function FeedScreen() {
) : error ? (
<EmptyState
icon="alert-circle-outline"
title="Не удалось загрузить ленту"
title="Couldn't load feed"
subtitle={error}
onRetry={() => loadPosts(false)}
/>
) : (
<EmptyState
icon="newspaper-outline"
title="Здесь пока пусто"
title="Nothing to show yet"
subtitle={emptyHint}
/>
)
@@ -409,7 +409,7 @@ function EmptyState({
})}
>
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>
Попробовать снова
Try again
</Text>
</Pressable>
)}

View File

@@ -119,10 +119,10 @@ export default function HashtagScreen() {
}}>
<Ionicons name="pricetag-outline" size={32} color="#6a6a6a" />
<Text style={{ color: '#ffffff', fontWeight: '700', marginTop: 10 }}>
Пока нет постов с этим тегом
No posts with this tag yet
</Text>
<Text style={{ color: '#8b8b8b', textAlign: 'center', fontSize: 13, marginTop: 6 }}>
Будьте первым напишите пост с #{tag}
Be the first write a post with #{tag}
</Text>
</View>
)

View File

@@ -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 (
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<Header
title="Поиск"
title="Search"
divider={false}
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
/>
@@ -124,7 +124,7 @@ export default function NewContactScreen() {
<SearchBar
value={query}
onChangeText={setQuery}
placeholder="@alice, hex pubkey или DC-адрес"
placeholder="@alice, hex pubkey or DC address"
onSubmitEditing={search}
autoFocus
onClear={() => { setResolved(null); setError(null); }}
@@ -143,7 +143,7 @@ export default function NewContactScreen() {
{searching ? (
<ActivityIndicator color="#ffffff" size="small" />
) : (
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Найти</Text>
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>Search</Text>
)}
</Pressable>
)}
@@ -163,11 +163,11 @@ export default function NewContactScreen() {
<Ionicons name="person-add-outline" size={24} color="#6a6a6a" />
</View>
<Text style={{ color: '#ffffff', fontSize: 15, fontWeight: '700', marginBottom: 6 }}>
Найдите собеседника
Find someone to message
</Text>
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', lineHeight: 19 }}>
Введите <Text style={{ color: '#ffffff', fontWeight: '600' }}>@username</Text>,
если человек зарегистрировал ник, либо полный hex pubkey или <Text style={{ color: '#ffffff', fontWeight: '600' }}>DC</Text> адрес.
Enter an <Text style={{ color: '#ffffff', fontWeight: '600' }}>@username</Text> if
the person registered one, or paste a full hex pubkey or <Text style={{ color: '#ffffff', fontWeight: '600' }}>DC</Text> address.
</Text>
</View>
)}
@@ -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'}
</Text>
</View>
</View>
@@ -227,12 +227,12 @@ export default function NewContactScreen() {
{/* Intro */}
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 18, marginBottom: 6 }}>
Сообщение (опционально, видно в открытом виде на chain)
Intro (optional, plaintext on-chain)
</Text>
<TextInput
value={intro}
onChangeText={setIntro}
placeholder="Привет! Это Влад со встречи в среду"
placeholder="Hey, it's Jordan from the conference"
placeholderTextColor="#5a5a5a"
multiline
maxLength={140}
@@ -250,7 +250,7 @@ export default function NewContactScreen() {
{/* Fee tier */}
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 14, marginBottom: 6 }}>
Плата за запрос (уходит получателю, anti-spam)
Anti-spam fee (goes to the recipient)
</Text>
{/* Fee-tier pills.
Layout (background, border, padding) lives on a static
@@ -314,7 +314,7 @@ export default function NewContactScreen() {
<ActivityIndicator color="#ffffff" size="small" />
) : (
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 14 }}>
Отправить запрос · {formatAmount(fee + 1000)}
Send request · {formatAmount(fee + 1000)}
</Text>
)}
</Pressable>

View File

@@ -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 (
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<Header
title="Профиль"
title="Profile"
divider
left={<IconButton icon="chevron-back" size={36} onPress={() => router.back()} />}
/>
@@ -124,7 +124,7 @@ export default function ProfileScreen() {
fontSize: 13,
}}
>
{following ? 'Вы подписаны' : 'Подписаться'}
{following ? 'Following' : 'Follow'}
</Text>
)}
</Pressable>
@@ -139,7 +139,7 @@ export default function ProfileScreen() {
})}
>
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>
Редактировать
Edit
</Text>
</Pressable>
)}
@@ -177,7 +177,7 @@ export default function ProfileScreen() {
>
<Ionicons name="chatbubble-outline" size={15} color="#ffffff" />
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 14 }}>
Открыть чат
Open chat
</Text>
</Pressable>
)}
@@ -203,7 +203,7 @@ export default function ProfileScreen() {
})}
>
<Text style={{ color: '#8b8b8b', fontSize: 13, flex: 1 }}>
Адрес
Address
</Text>
<Text
style={{
@@ -214,7 +214,7 @@ export default function ProfileScreen() {
}}
numberOfLines={1}
>
{copied ? 'Скопировано' : shortAddr(address ?? '')}
{copied ? 'Copied' : shortAddr(address ?? '')}
</Text>
<Ionicons
name={copied ? 'checkmark' : 'copy-outline'}
@@ -229,17 +229,17 @@ export default function ProfileScreen() {
<>
<Divider />
<InfoRow
label="Шифрование"
label="Encryption"
value={contact.x25519Pub
? 'end-to-end (NaCl)'
: 'ключ ещё не опубликован'}
: 'key not published yet'}
danger={!contact.x25519Pub}
icon={contact.x25519Pub ? 'lock-closed' : 'lock-open'}
/>
<Divider />
<InfoRow
label="Добавлен"
label="Added"
value={new Date(contact.addedAt).toLocaleDateString()}
icon="calendar-outline"
/>
@@ -251,7 +251,7 @@ export default function ProfileScreen() {
<>
<Divider />
<InfoRow
label="Участников"
label="Members"
value="—"
icon="people-outline"
/>
@@ -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.
</Text>
)}
</ScrollView>

View File

@@ -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}
</Text>
<Text style={{ color: '#8b8b8b', fontSize: 12, marginTop: 2 }}>
хочет добавить вас в контакты · {relativeTime(req.timestamp)}
wants to add you as a contact · {relativeTime(req.timestamp)}
</Text>
{req.intro ? (
<Text
@@ -123,7 +123,7 @@ export default function RequestsScreen() {
{isAccepting ? (
<ActivityIndicator size="small" color="#ffffff" />
) : (
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>Принять</Text>
<Text style={{ color: '#ffffff', fontWeight: '700', fontSize: 13 }}>Accept</Text>
)}
</Pressable>
<Pressable
@@ -137,7 +137,7 @@ export default function RequestsScreen() {
borderWidth: 1, borderColor: '#1f1f1f',
})}
>
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 13 }}>Отклонить</Text>
<Text style={{ color: '#ffffff', fontWeight: '600', fontSize: 13 }}>Decline</Text>
</Pressable>
</View>
</View>
@@ -147,16 +147,16 @@ export default function RequestsScreen() {
return (
<View style={{ flex: 1, backgroundColor: '#000000', paddingTop: insets.top }}>
<TabHeader title="Уведомления" />
<TabHeader title="Notifications" />
{requests.length === 0 ? (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32 }}>
<Ionicons name="notifications-outline" size={42} color="#3a3a3a" />
<Text style={{ color: '#ffffff', fontSize: 16, fontWeight: '700', marginTop: 10 }}>
Всё прочитано
All caught up
</Text>
<Text style={{ color: '#8b8b8b', fontSize: 13, textAlign: 'center', marginTop: 6, lineHeight: 19 }}>
Запросы на общение и события сети появятся здесь.
Contact requests and network events will appear here.
</Text>
</View>
) : (

View File

@@ -187,17 +187,17 @@ export default function WelcomeScreen() {
<FeatureRow
icon="lock-closed"
title="End-to-end encryption"
text="X25519 + NaCl на каждом сообщении. Даже релей-нода не может прочитать переписку."
text="X25519 + NaCl on every message. Not even the relay node can read your conversations."
/>
<FeatureRow
icon="key"
title="Твои ключи — твой аккаунт"
text="Без телефона, email и серверных паролей. Ключи никогда не покидают устройство."
title="Your keys, your account"
text="No phone, email, or server passwords. Keys never leave your device."
/>
<FeatureRow
icon="git-network"
title="Decentralised"
text="Любой может поднять свою ноду. Нет единой точки отказа и цензуры."
text="Anyone can run a node. No single point of failure or censorship."
/>
</ScrollView>
@@ -206,7 +206,7 @@ export default function WelcomeScreen() {
flexDirection: 'row', justifyContent: 'flex-end',
paddingHorizontal: 24, paddingBottom: 8,
}}>
<CTAPrimary label="Продолжить" onPress={() => goToPage(1)} />
<CTAPrimary label="Continue" onPress={() => goToPage(1)} />
</View>
</View>
@@ -223,22 +223,22 @@ export default function WelcomeScreen() {
keyboardShouldPersistTaps="handled"
>
<Text style={{ color: '#ffffff', fontSize: 24, fontWeight: '800', letterSpacing: -0.5 }}>
Как это работает
How it works
</Text>
<Text style={{ color: '#8b8b8b', fontSize: 14, lineHeight: 20, marginTop: 8, marginBottom: 22 }}>
Сообщения проходят через релей-ноду в зашифрованном виде.
Выбери публичную или подключи свою.
Messages travel through a relay node in encrypted form.
Pick a public one or run your own.
</Text>
<OptionCard
icon="globe"
title="Публичная нода"
text="Удобно и быстро — нода хостится комьюнити, небольшая комиссия за каждое отправленное сообщение."
title="Public node"
text="Quick and easy — community-hosted relay, small fee per delivered message."
/>
<OptionCard
icon="hardware-chip"
title="Своя нода"
text="Максимальный контроль. Исходники открыты — подними на своём сервере за 5 минут."
title="Self-hosted"
text="Maximum control. Source is open — spin up your own in five minutes."
/>
<Text style={{
@@ -302,11 +302,11 @@ export default function WelcomeScreen() {
paddingHorizontal: 24, paddingBottom: 8,
}}>
<CTASecondary
label="Исходники"
label="Source"
icon="logo-github"
onPress={() => Linking.openURL(GITEA_URL).catch(() => {})}
/>
<CTAPrimary label="Продолжить" onPress={() => goToPage(2)} />
<CTAPrimary label="Continue" onPress={() => goToPage(2)} />
</View>
</View>
@@ -334,11 +334,11 @@ export default function WelcomeScreen() {
<Ionicons name="key" size={44} color="#1d9bf0" />
</View>
<Text style={{ color: '#ffffff', fontSize: 24, fontWeight: '800', letterSpacing: -0.5, textAlign: 'center' }}>
Твой аккаунт
Your account
</Text>
<Text style={{ color: '#8b8b8b', fontSize: 14, lineHeight: 20, marginTop: 8, textAlign: 'center', maxWidth: 280 }}>
Создай новую пару ключей или импортируй существующую.
Ключи хранятся только на этом устройстве.
Generate a fresh keypair or import an existing one.
Keys stay on this device only.
</Text>
</View>
</ScrollView>
@@ -349,11 +349,11 @@ export default function WelcomeScreen() {
paddingHorizontal: 24, paddingBottom: 8,
}}>
<CTASecondary
label="Импорт"
label="Import"
onPress={() => router.push('/(auth)/import' as never)}
/>
<CTAPrimary
label="Создать аккаунт"
label="Create account"
onPress={() => router.push('/(auth)/create' as never)}
/>
</View>