Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 77 additions & 17 deletions website/src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,49 @@ interface Props {
description?: string;
}

const currentPath = Astro.url.pathname;
const isRussian = currentPath === '/ru/' || currentPath.startsWith('/ru/');
const locale = isRussian ? 'ru' : 'en';

const {
title,
description = 'Voice Inbox turns accumulated voice recordings into a local Markdown or text inbox.',
description = isRussian
? 'Voice Inbox превращает накопленные голосовые записи в локальные Markdown- или текстовые заметки.'
: 'Voice Inbox turns accumulated voice recordings into a local Markdown or text inbox.',
} = Astro.props;
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const site = Astro.site?.toString().replace(/\/$/, '') ?? 'https://voiceinbox.simpleditor.org';
const gaId = import.meta.env.PUBLIC_GA_ID;
const githubUrl = 'https://github.com/maxistar/Voice-Inbox';
const releaseUrl = `${githubUrl}/releases`;
const nav = [

const trimTrailingSlash = (path: string) => path.length > 1 ? path.replace(/\/$/, '') : path;
const toEnglishPath = (path: string) => {
if (path === '/ru' || path === '/ru/') return '/';
if (path.startsWith('/ru/')) return path.replace('/ru', '');
return path;
};
const toRussianPath = (path: string) => {
if (path === '/') return '/ru/';
if (path === '/ru' || path.startsWith('/ru/')) return path.endsWith('/') ? path : `${path}/`;
return `/ru${path}`;
};
const englishPath = toEnglishPath(currentPath);
const russianPath = toRussianPath(currentPath);
const localized = (path: string) => isRussian ? toRussianPath(path) : path;
const withBase = (path: string) => `${base}${path}`;
const absolute = (path: string) => `${site}${path}`;

const nav = isRussian ? [
['Главная', '/'],
['О проекте', '/about/'],
['Скачать', '/download/'],
['Документация', '/docs/'],
['Настройка', '/setup/'],
['Тестирование', '/testing/'],
['FAQ', '/faq/'],
['Другие проекты', '/projects/'],
] : [
['Home', '/'],
['About', '/about/'],
['Download', '/download/'],
Expand All @@ -22,13 +56,22 @@ const nav = [
['FAQ', '/faq/'],
['Other Projects', '/projects/'],
];
const footerText = isRussian
? 'Voice Inbox — локальная расшифровка голосовых заметок.'
: 'Voice Inbox - local-first voice note transcription.';
const footerLabels = isRussian
? { releases: 'Релизы', contact: 'Контакты', setup: 'Настройка', faq: 'FAQ', privacy: 'Приватность', legal: 'Правовая информация', source: 'Исходный код Voice Inbox на GitHub', language: 'Язык', nav: 'Основная навигация' }
: { releases: 'Releases', contact: 'Contact', setup: 'Setup', faq: 'FAQ', privacy: 'Privacy', legal: 'Legal', source: 'Voice Inbox source code on GitHub', language: 'Language', nav: 'Main navigation' };
---
<!doctype html>
<html lang="en">
<html lang={locale}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="alternate" hreflang="en" href={absolute(englishPath)} />
<link rel="alternate" hreflang="ru" href={absolute(russianPath)} />
<link rel="alternate" hreflang="x-default" href={absolute(englishPath)} />
<title>{title} - Voice Inbox</title>
{
gaId && (
Expand Down Expand Up @@ -117,15 +160,26 @@ const nav = [
flex-wrap: wrap;
gap: 12px 16px;
}
nav a {
nav a, .language-switcher a {
color: var(--muted);
font-size: 0.88rem;
font-weight: 650;
}
nav a:hover {
nav a:hover, .language-switcher a:hover {
color: var(--ink);
text-decoration: none;
}
.language-switcher {
display: inline-flex;
align-items: center;
gap: 8px;
border-left: 1px solid var(--border);
padding-left: 14px;
}
.language-switcher a[aria-current="true"] {
color: var(--ink);
font-weight: 850;
}
main {
flex: 1;
width: 100%;
Expand Down Expand Up @@ -277,19 +331,25 @@ const nav = [
flex-direction: column;
}
nav { justify-content: flex-start; }
.language-switcher { border-left: 0; padding-left: 0; }
.page { padding-top: 38px; }
}
</style>
</head>
<body>
<header>
<div class="topbar">
<a class="logo" href={`${base}/`}>
<a class="logo" href={withBase(localized('/'))}>
<span class="logo-mark" aria-hidden="true">V</span>
<span>Voice Inbox</span>
</a>
<nav aria-label="Main navigation">
{nav.map(([label, href]) => <a href={`${base}${href}`}>{label}</a>)}
<nav aria-label={footerLabels.nav}>
{nav.map(([label, href]) => <a href={withBase(localized(href))}>{label}</a>)}
<span class="language-switcher" aria-label={footerLabels.language}>
<a href={withBase(englishPath)} aria-current={!isRussian ? 'true' : undefined}>EN</a>
<span aria-hidden="true">/</span>
<a href={withBase(russianPath)} aria-current={isRussian ? 'true' : undefined}>RU</a>
</span>
</nav>
</div>
</header>
Expand All @@ -300,16 +360,16 @@ const nav = [
</main>
<footer>
<div class="footer-inner">
<span>Voice Inbox - local-first voice note transcription.</span>
<span>{footerText}</span>
<span class="footer-links">
<a href={releaseUrl} target="_blank" rel="noopener">Releases</a>
<a href={`${base}/contact/`}>Contact</a>
<a href={`${base}/setup/`}>Setup</a>
<a href={`${base}/faq/`}>FAQ</a>
<a href={`${base}/privacy/`}>Privacy</a>
<a href={`${base}/legal/`}>Legal</a>
<a class="github-link" href={githubUrl} target="_blank" rel="noopener" title="Voice Inbox source code on GitHub">
<span class="visually-hidden">Voice Inbox source code on GitHub</span>
<a href={releaseUrl} target="_blank" rel="noopener">{footerLabels.releases}</a>
<a href={withBase(localized('/contact/'))}>{footerLabels.contact}</a>
<a href={withBase(localized('/setup/'))}>{footerLabels.setup}</a>
<a href={withBase(localized('/faq/'))}>{footerLabels.faq}</a>
<a href={withBase(localized('/privacy/'))}>{footerLabels.privacy}</a>
<a href={withBase(localized('/legal/'))}>{footerLabels.legal}</a>
<a class="github-link" href={githubUrl} target="_blank" rel="noopener" title={footerLabels.source}>
<span class="visually-hidden">{footerLabels.source}</span>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2C6.48 2 2 6.59 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.1.68-.22.68-.49 0-.24-.01-.89-.01-1.75-2.78.62-3.37-1.37-3.37-1.37-.46-1.18-1.11-1.5-1.11-1.5-.91-.64.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.35 1.12 2.92.85.09-.66.35-1.12.64-1.37-2.22-.26-4.56-1.14-4.56-5.07 0-1.12.39-2.04 1.03-2.76-.1-.26-.45-1.31.1-2.72 0 0 .84-.28 2.75 1.05A9.3 9.3 0 0 1 12 6.69a9.3 9.3 0 0 1 2.5.34c1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.46.1 2.72.64.72 1.03 1.64 1.03 2.76 0 3.94-2.34 4.8-4.57 5.06.36.32.68.94.68 1.9 0 1.37-.01 2.47-.01 2.81 0 .27.18.59.69.49A10.27 10.27 0 0 0 22 12.25C22 6.59 17.52 2 12 2Z" />
</svg>
Expand Down
15 changes: 15 additions & 0 deletions website/src/pages/ru/about.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import Layout from '../../layouts/Layout.astro';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
---
<Layout title="О Voice Inbox" description="Почему Voice Inbox превращает существующее аудио и короткую Android-диктовку в локальные читаемые расшифровки.">
<p class="eyebrow">О проекте</p>
<h1>Голосовые записи должны становиться полезным текстом без лишней облачной обработки.</h1>
<p>Voice Inbox помогает разобрать накопленные аудиозаписи: сообщения из мессенджеров, записи диктофона, встречи, интервью или лекции. Приложение получает или импортирует уже существующее аудио; оно не позиционируется как универсальный диктофон.</p>
<section class="section grid">
<article class="card"><h2>Локальное распознавание</h2><p>После установки модели распознавание выполняется на устройстве. Сеть нужна для обычной первой загрузки модели, а не для отправки каждого аудио в облако приложения.</p></article>
<article class="card"><h2>Два workflow</h2><p>Основной workflow — расшифровка записей. На Android дополнительно доступна компактная голосовая клавиатура для коротких фраз в активном текстовом поле.</p></article>
<article class="card"><h2>Платформы</h2><p>Android — более зрелая ветка и сейчас распространяется через закрытое тестирование Google Play. iOS — MVP с foreground-импортом, предпросмотром и расшифровкой без фонового расписания.</p></article>
</section>
<section class="section"><h2>Почему это не обычный диктофон</h2><p>Voice Inbox рассчитан на обработку аудио, которое уже существует или пришло через системный импорт/Share. Вы можете сначала прочитать результат в приложении, а экспорт в Markdown или текстовый файл остаётся опциональным способом организации.</p><div class="button-row"><a class="button primary" href={`${base}/ru/setup/`}>Как настроить</a><a class="button" href={`${base}/ru/privacy/`}>Приватность</a></div></section>
</Layout>
13 changes: 13 additions & 0 deletions website/src/pages/ru/contact.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
import Layout from '../../layouts/Layout.astro';
const issuesUrl = 'https://github.com/maxistar/Voice-Inbox/issues';
---
<Layout title="Контакты и поддержка" description="Куда отправлять обратную связь по Voice Inbox и как подготовить приватный отчёт.">
<p class="eyebrow">Поддержка</p>
<h1>Сообщайте о проблемах без раскрытия личных данных.</h1>
<div class="grid section">
<article class="card"><h2>Публичная ошибка</h2><p>Для воспроизводимых публичных проблем используйте GitHub Issues. Укажите версию приложения, устройство, версию Android/iOS, шаги воспроизведения и видимую ошибку.</p><a href={issuesUrl} target="_blank" rel="noopener">Открыть GitHub Issues →</a></article>
<article class="card"><h2>Приватная обратная связь</h2><p>Участникам закрытого тестирования лучше использовать private feedback в Google Play или тот канал, через который пришло приглашение, если отчёт не подходит для публичного issue.</p></article>
<article class="card"><h2>Что не публиковать</h2><p>Не прикладывайте публично аудиозаписи, распознанный текст, credentials, приватные ссылки на документы или другие чувствительные данные.</p></article>
</div>
</Layout>
14 changes: 14 additions & 0 deletions website/src/pages/ru/docs.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
import Layout from '../../layouts/Layout.astro';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
---
<Layout title="Документация" description="Выберите workflow Voice Inbox: расшифровка записей или Android-голосовая клавиатура, затем откройте настройку и помощь.">
<p class="eyebrow">Документация</p>
<h1>Два workflow: записи и короткая Android-диктовка.</h1>
<p>Voice Inbox помогает обрабатывать существующие аудиозаписи в текстовый inbox. На Android также есть отдельная голосовая клавиатура для коротких фраз.</p>
<div class="grid section">
<article class="card"><h2>Расшифровка записей</h2><p>Установите модель, импортируйте аудио или выберите папку, запустите расшифровку и проверьте текст в приложении.</p><a href={`${base}/ru/setup/`}>Настройка →</a></article>
<article class="card"><h2>Голосовая клавиатура Android</h2><p>Включите input method Voice Inbox и диктуйте короткие фразы в активное текстовое поле. Функция Android-only.</p><a href={`${base}/ru/docs/voice-keyboard/`}>Руководство →</a></article>
<article class="card"><h2>Вопросы и приватность</h2><p>FAQ, сведения о локальном распознавании, модели, ограничениях расписания и безопасной отправке обратной связи.</p><a href={`${base}/ru/faq/`}>FAQ →</a></article>
</div>
</Layout>
28 changes: 28 additions & 0 deletions website/src/pages/ru/docs/voice-keyboard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
import Layout from '../../../layouts/Layout.astro';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const keyboardScreenshot = `${base}/screenshots/android-voice-keyboard-guide.png`;
---
<Layout title="Голосовая клавиатура Android" description="Как включить и использовать клавиатуру Voice Inbox для приватной локальной диктовки коротких фраз на Android.">
<p class="eyebrow">Android-only</p>
<h1>Голосовая клавиатура Voice Inbox для коротких фраз.</h1>
<p>Голосовая клавиатура — отдельный Android input method. Она записывает короткую фразу, распознаёт её локально после установки модели и вставляет текст в активное редактируемое поле. Это не полноценная клавиатура и не функция iOS.</p>

<section class="section">
<figure>
<img src={keyboardScreenshot} alt="Руководство по Android-голосовой клавиатуре Voice Inbox" style="width: 100%; height: auto; border: 1px solid var(--border); border-radius: 12px;" />
<figcaption class="note">Скриншот показывает compact voice keyboard workflow с демонстрационным содержимым.</figcaption>
</figure>
</section>

<section class="section grid">
<article class="card"><h2>Перед началом</h2><p>Установите и проверьте модель распознавания в Voice Inbox. Для обычной загрузки модели нужна сеть; после установки распознавание выполняется на устройстве.</p></article>
<article class="card"><h2>Разрешение микрофона</h2><p>Клавиатуре требуется доступ к микрофону для записи короткой фразы. Не выдавайте разрешение, если не планируете использовать диктовку.</p></article>
<article class="card"><h2>Включение клавиатуры</h2><p>Откройте настройки Android input methods, включите Voice Inbox keyboard, затем выберите её в поле ввода, где хотите продиктовать текст.</p></article>
<article class="card"><h2>Использование</h2><p>Нажмите запись, произнесите короткую фразу, дождитесь локального распознавания и вставки текста. Для длинных записей используйте основной workflow импорта аудио.</p></article>
</section>

<section class="section note">
Голосовая клавиатура не отправляет аудио в облако приложения для распознавания после установки модели, но системные клавиатуры, приложения и провайдеры ввода могут иметь собственные правила приватности.
</section>
</Layout>
16 changes: 16 additions & 0 deletions website/src/pages/ru/download.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
import Layout from '../../layouts/Layout.astro';
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
const releasesUrl = 'https://github.com/maxistar/Voice-Inbox/releases';
---
<Layout title="Скачать и получить доступ" description="Доступность Voice Inbox: закрытое тестирование Android, iOS MVP, релизы GitHub и первые требования к использованию.">
<p class="eyebrow">Доступность</p>
<h1>Android доступен через закрытое тестирование Google Play.</h1>
<p>Voice Inbox пока не является публичным production-релизом в Google Play. Для Android нужно вступить в группу тестеров, пройти opt-in и установить приложение из Google Play тем же аккаунтом.</p>
<div class="grid section">
<article class="card"><h2>Android closed testing</h2><p>Поддерживается Android 7.0 (API 24)+ на ARM64-устройстве. После установки потребуется скачать речевую модель примерно 640 MiB или установить совместимую локальную копию.</p><a href={`${base}/ru/testing/`}>Инструкция для тестеров →</a></article>
<article class="card"><h2>GitHub Releases</h2><p>Подписанные APK/AAB и исходный код доступны на GitHub. Это полезно для архива релизов и разработчиков, но основной канал тестирования Android — Google Play closed test.</p><a href={releasesUrl} target="_blank" rel="noopener">Открыть релизы →</a></article>
<article class="card"><h2>iOS MVP</h2><p>iOS сейчас не распространяется через App Store или TestFlight. Это исходно-собираемый MVP с импортом, предпросмотром и foreground-расшифровкой.</p></article>
</div>
<section class="section note">Перед первым использованием подготовьте свободное место и подходящее соединение для загрузки модели. После установки модели распознавание выполняется локально.</section>
</Layout>
Loading
Loading