- Enhanced tests to ensure consistent messageId generation for legacy inbox rows lacking a messageId. - Updated test descriptions for better clarity regarding the new messageId handling. - Adjusted test expectations to align with the updated behavior of relaying legacy inbox rows with generated messageIds.
1027 lines
40 KiB
Markdown
1027 lines
40 KiB
Markdown
# План разработки лендинга для Voice-to-Text
|
||
|
||
## Обзор проекта
|
||
|
||
**Voice-to-Text** — десктопное приложение для преобразования голоса в текст с фокусом на приватность и офлайн-поддержку. Построено на Tauri, Rust и Vue 3.
|
||
|
||
### Ключевые особенности приложения:
|
||
- 🎤 **Real-time транскрипция** с поддержкой нескольких провайдеров (Deepgram, AssemblyAI, Whisper)
|
||
- 🔒 **Privacy-focused** — API ключи хранятся локально, нет облачного хранилища
|
||
- 🌍 **Кроссплатформенность** — macOS, Windows, Linux
|
||
- ⚡ **Глобальные хоткеи** — быстрый доступ через горячие клавиши
|
||
- 📋 **Автоматическое копирование** в буфер обмена
|
||
- 🎨 **Современный UI** с glass morphism эффектами
|
||
- 🌐 **Мультиязычность** — поддержка 6 языков (en, ru, es, fr, de, uk)
|
||
|
||
---
|
||
|
||
## Технологический стек
|
||
|
||
### Основные технологии:
|
||
- **Nuxt 3** — фреймворк для Vue.js с SSR/SSG
|
||
- **Vuetify 3** — Material Design компоненты
|
||
- **Vue I18n** — локализация (совместимо с текущим `vue-i18n`)
|
||
- **TypeScript** — типизация
|
||
- **Vite** — сборщик (встроен в Nuxt)
|
||
- **Pinia** — управление общим состоянием (через `@pinia/nuxt`)
|
||
|
||
### Дополнительные библиотеки:
|
||
- **@nuxtjs/i18n** — интеграция i18n с Nuxt
|
||
- **@nuxtjs/seo** — SEO оптимизация
|
||
- **@vueuse/nuxt** — композаблы для Vue (например `usePreferredDark`)
|
||
- **nuxt-icon** — иконки
|
||
- **@nuxtjs/ipx** — обработка изображений (опционально)
|
||
- **swiper** — карусель скриншотов (Vue интеграция)
|
||
- **@nuxt/eslint** + **Prettier** — линтинг и форматирование кода
|
||
|
||
Примечания по зависимостям:
|
||
- Google Fonts лучше не подключать как внешнюю зависимость. Либо системный шрифт, либо self-host (проще с приватностью и стабильностью).
|
||
|
||
---
|
||
|
||
## Архитектура проекта
|
||
|
||
```
|
||
landing/
|
||
├── data/ # статические данные/конфиги секций (без Nuxt контекста)
|
||
├── types/ # TS типы (без Nuxt контекста)
|
||
├── utils/ # чистые функции (без Nuxt контекста, легко тестировать)
|
||
├── nuxt.config.ts # Конфигурация Nuxt
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── .env # Переменные окружения
|
||
│
|
||
├── locales/ # Файлы локализации
|
||
│ ├── en.json
|
||
│ ├── ru.json
|
||
│ ├── es.json
|
||
│ ├── fr.json
|
||
│ ├── de.json
|
||
│ └── uk.json
|
||
│
|
||
├── assets/ # Статические ресурсы
|
||
│ ├── images/
|
||
│ │ ├── hero-bg.jpg
|
||
│ │ ├── screenshot-dark.png
|
||
│ │ ├── screenshot-light.png
|
||
│ │ ├── features/
|
||
│ │ └── platforms/
|
||
│ ├── videos/ # Демо-видео (опционально)
|
||
│ └── styles/
|
||
│ └── main.scss # Глобальные стили
|
||
│
|
||
├── components/ # Vue компоненты
|
||
│ ├── layout/
|
||
│ │ ├── AppHeader.vue # Шапка с навигацией
|
||
│ │ ├── AppFooter.vue # Подвал
|
||
│ │ └── LanguageSwitcher.vue # Переключатель языков
|
||
│ │
|
||
│ ├── sections/
|
||
│ │ ├── HeroSection.vue # Главный экран
|
||
│ │ ├── FeaturesSection.vue # Особенности
|
||
│ │ ├── ProvidersSection.vue # STT провайдеры
|
||
│ │ ├── ScreenshotsSection.vue # Скриншоты
|
||
│ │ ├── DownloadSection.vue # Секция загрузки
|
||
│ │ ├── PrivacySection.vue # Приватность
|
||
│ │ └── FAQSection.vue # Частые вопросы
|
||
│ │
|
||
│ ├── ui/
|
||
│ │ ├── DownloadButton.vue # Кнопка загрузки
|
||
│ │ ├── FeatureCard.vue # Карточка фичи
|
||
│ │ ├── PlatformBadge.vue # Бейдж платформы
|
||
│ │ └── ScreenshotCarousel.vue # Карусель скриншотов
|
||
│ │
|
||
│ └── common/
|
||
│ ├── AppLogo.vue
|
||
│ └── ThemeToggle.vue # Переключатель темы (если нужен)
|
||
│
|
||
├── composables/ # Композаблы
|
||
│ ├── useDownload.ts # Логика загрузки
|
||
│ ├── useAnalytics.ts # Аналитика (опционально)
|
||
│ ├── usePlatform.ts # Определение платформы
|
||
│ ├── useBrowserTheme.ts # Определение темы браузера
|
||
│ └── useLocation.ts # Определение локации пользователя
|
||
│
|
||
├── layouts/
|
||
│ └── default.vue # Основной layout
|
||
│
|
||
├── plugins/ # Плагины Nuxt
|
||
│ ├── vuetify.ts # Инициализация Vuetify
|
||
│ └── init-theme-locale.client.ts # Автоинициализация темы и локали
|
||
│
|
||
├── pages/
|
||
│ ├── index.vue # Главная страница
|
||
│ ├── download.vue # Страница загрузки
|
||
│ └── privacy.vue # Политика приватности (опционально)
|
||
│
|
||
├── public/ # Публичные файлы
|
||
│ ├── favicon.ico
|
||
│ ├── robots.txt
|
||
│ └── sitemap.xml
|
||
│
|
||
└── server/ # Server API (если нужен)
|
||
```
|
||
|
||
---
|
||
|
||
## Правила разделения логики
|
||
|
||
- **composables/**: завязаны на Nuxt/Vue (реактивность, `useCookie`, `navigateTo`, `useRoute`, `useFetch`, `useHead` и т.д.)
|
||
- **utils/**: чистые функции без Nuxt контекста (легко тестировать, переиспользовать)
|
||
|
||
## Структура страниц
|
||
|
||
### 1. Главная страница (`/`)
|
||
|
||
#### Hero Section
|
||
- **Заголовок**: "Voice to Text — Privacy-Focused Transcription"
|
||
- **Подзаголовок**: Краткое описание приложения
|
||
- **CTA кнопки**:
|
||
- "Download for [Platform]" (определяется автоматически)
|
||
- "View Features" (скролл к секции)
|
||
- **Фоновое изображение/видео**: Демонстрация приложения
|
||
|
||
#### Features Section
|
||
**6 основных фич в виде карточек:**
|
||
|
||
1. **Real-time Transcription**
|
||
- Иконка: 🎤
|
||
- Описание: Мгновенная транскрипция с частичными результатами
|
||
|
||
2. **Privacy-Focused**
|
||
- Иконка: 🔒
|
||
- Описание: API ключи хранятся локально, нет облачного хранилища
|
||
|
||
3. **Multiple Providers**
|
||
- Иконка: 🌐
|
||
- Описание: Deepgram, AssemblyAI, Whisper (офлайн)
|
||
|
||
4. **Cross-Platform**
|
||
- Иконка: 💻
|
||
- Описание: macOS, Windows, Linux
|
||
|
||
5. **Global Hotkeys**
|
||
- Иконка: ⌨️
|
||
- Описание: Быстрый доступ через горячие клавиши
|
||
|
||
6. **Auto-Copy**
|
||
- Иконка: 📋
|
||
- Описание: Автоматическое копирование в буфер обмена
|
||
|
||
#### Providers Section
|
||
**Детальное описание STT провайдеров:**
|
||
|
||
- **Deepgram** (Nova-2/3)
|
||
- Низкая задержка
|
||
- Высокое качество
|
||
- Автоматический выбор модели (Nova-3 для английского, Nova-2 для русского)
|
||
|
||
- **AssemblyAI** (Universal-Streaming v3)
|
||
- Высокое качество
|
||
- Облачный сервис
|
||
|
||
- **Whisper Local** (офлайн)
|
||
- Полностью офлайн
|
||
- Требует cmake и загрузки модели
|
||
|
||
#### Screenshots Section
|
||
**Карусель скриншотов (Swiper):**
|
||
- Используем Swiper для Vue: `https://swiperjs.com/vue`
|
||
- На **десктопе** должно быть видно **сразу несколько** скриншотов на экране (не один с обязательным “далее”)
|
||
- Пример: `slidesPerView: 2-4` на широких экранах с `breakpoints`
|
||
- Допускается режим с “частичным” превью следующего слайда
|
||
- На мобильных: 1 скрин, свайп, пагинация
|
||
- Набор скринов:
|
||
- Темная тема
|
||
- Светлая тема
|
||
- Настройки
|
||
- Процесс записи
|
||
|
||
#### Download Section
|
||
**Платформо-специфичные кнопки загрузки с автоопределением ОС:**
|
||
- Определяем текущую ОС пользователя и показываем **приоритетно** релевантную загрузку
|
||
- Если ОС определить не удалось — показываем **все** ОС
|
||
- Для macOS учитывать архитектуру (Apple Silicon vs Intel):
|
||
- Если получилось определить — выбираем нужную сборку по умолчанию
|
||
- Если нет — показываем оба варианта и даём выбрать вручную
|
||
|
||
**Дополнительно:**
|
||
- Версия приложения
|
||
- Размер файла
|
||
- Системные требования
|
||
- Changelog ссылка
|
||
|
||
#### Privacy Section
|
||
**Ключевые моменты приватности:**
|
||
- Локальное хранение API ключей
|
||
- Нет облачного хранилища транскрипций
|
||
- Опциональное использование собственных API ключей
|
||
|
||
**Open Source:**
|
||
- Да: часть компонентов/модулей планируем сделать open-source (для маркетинга и доверия)
|
||
|
||
#### FAQ Section
|
||
**Частые вопросы:**
|
||
1. Какие платформы поддерживаются?
|
||
2. Нужен ли интернет для работы?
|
||
3. Как настроить API ключи?
|
||
4. Можно ли использовать офлайн?
|
||
5. Как изменить горячие клавиши?
|
||
6. Безопасны ли мои данные?
|
||
|
||
### 2. Страница загрузки (`/download`)
|
||
|
||
- **Определение платформы** автоматически
|
||
- **Кнопки загрузки** для всех платформ
|
||
- **Инструкции по установке** для каждой ОС
|
||
- **Системные требования**
|
||
- **Changelog** (последние версии)
|
||
|
||
---
|
||
|
||
## Локализация (i18n)
|
||
|
||
### Поддерживаемые языки:
|
||
- 🇺🇸 English (`en`)
|
||
- 🇷🇺 Русский (`ru`)
|
||
- 🇪🇸 Español (`es`)
|
||
- 🇫🇷 Français (`fr`)
|
||
- 🇩🇪 Deutsch (`de`)
|
||
- 🇺🇦 Українська (`uk`)
|
||
|
||
### Структура файлов локализации:
|
||
|
||
```json
|
||
// locales/en.json
|
||
{
|
||
"meta": {
|
||
"title": "Voice to Text - Privacy-Focused Transcription",
|
||
"description": "..."
|
||
},
|
||
"nav": {
|
||
"features": "Features",
|
||
"download": "Download",
|
||
"privacy": "Privacy"
|
||
},
|
||
"hero": {
|
||
"title": "Voice to Text",
|
||
"subtitle": "Privacy-focused voice-to-text application with offline support",
|
||
"download": "Download for {platform}",
|
||
"viewFeatures": "View Features"
|
||
},
|
||
"features": {
|
||
"realtime": {
|
||
"title": "Real-time Transcription",
|
||
"description": "..."
|
||
},
|
||
"privacy": {
|
||
"title": "Privacy-Focused",
|
||
"description": "..."
|
||
}
|
||
// ... остальные фичи
|
||
},
|
||
"providers": {
|
||
"title": "Multiple STT Providers",
|
||
"deepgram": {
|
||
"name": "Deepgram",
|
||
"description": "..."
|
||
}
|
||
// ... остальные провайдеры
|
||
},
|
||
"download": {
|
||
"title": "Download",
|
||
"forPlatform": "Download for {platform}",
|
||
"systemRequirements": "System Requirements",
|
||
"version": "Version {version}"
|
||
},
|
||
"faq": {
|
||
"title": "Frequently Asked Questions",
|
||
"items": [
|
||
{
|
||
"question": "...",
|
||
"answer": "..."
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### Модель контента (чтобы i18n не превращалась в ад)
|
||
|
||
Проблема классическая: если хранить “структуру секций” в `data/*`, а весь контент раскидать по ключам i18n, то любая правка превращается в квест “найди 20 ключей в 6 языках”.
|
||
|
||
Решение — разделить два слоя:
|
||
- **Микрокопирайт** (кнопки, лейблы, мелкие подписи) — остаётся в `landing/locales/*`.
|
||
- **Контент секций** (FAQ, список фич, провайдеры, тексты блоков) — лежит в **локализованных контент-файлах** с одинаковой структурой по всем языкам.
|
||
|
||
Рекомендуемая схема:
|
||
- `landing/content/en.ts`, `landing/content/ru.ts`, ... (или `.json`, если удобнее).
|
||
- Внутри — один типизированный объект `LandingContent`, где:
|
||
- элементы имеют **стабильные `id`** (например `faq.items[].id`, `features.items[].id`)
|
||
- порядок можно менять без рефакторинга компонентов
|
||
- длинные тексты редактируются “в одном месте” для каждой локали
|
||
|
||
Минимальные правила дисциплины:
|
||
- `id` неизменяемы (меняем текст, но не идентификатор).
|
||
- Если добавили/удалили элемент — правим **все локали** (это легко проверяется автоматикой).
|
||
- Для контента не используем “вложенные ключи на 10 уровней”, держим структуру простой и читаемой.
|
||
|
||
Проверка качества:
|
||
- Добавляем маленькую проверку (скрипт/тест), которая сравнивает структуру контента между локалями и падает, если где-то не хватает ключей/элементов.
|
||
|
||
Минимальный “контракт” этой проверки:
|
||
- сравниваем структуру (ключи/массивы по `id`), а не тексты
|
||
- ошибка должна показывать, **какой `id`/ключ отсутствует** и **в какой локали**
|
||
- проверка запускается локально и в CI (чтобы не ловить это уже на проде)
|
||
|
||
### Настройка i18n в Nuxt:
|
||
|
||
```typescript
|
||
// nuxt.config.ts
|
||
export default defineNuxtConfig({
|
||
modules: [
|
||
'@nuxtjs/i18n',
|
||
'vuetify/nuxt'
|
||
],
|
||
i18n: {
|
||
locales: [
|
||
{ code: 'en', iso: 'en-US', file: 'en.json', name: 'English' },
|
||
{ code: 'ru', iso: 'ru-RU', file: 'ru.json', name: 'Русский' },
|
||
{ code: 'es', iso: 'es-ES', file: 'es.json', name: 'Español' },
|
||
{ code: 'fr', iso: 'fr-FR', file: 'fr.json', name: 'Français' },
|
||
{ code: 'de', iso: 'de-DE', file: 'de.json', name: 'Deutsch' },
|
||
{ code: 'uk', iso: 'uk-UA', file: 'uk.json', name: 'Українська' }
|
||
],
|
||
defaultLocale: 'en',
|
||
strategy: 'prefix_except_default',
|
||
detectBrowserLanguage: {
|
||
useCookie: true,
|
||
cookieKey: 'i18n_redirected',
|
||
redirectOn: 'root',
|
||
// Определение языка по локации (через composable)
|
||
alwaysRedirect: false,
|
||
fallbackLocale: 'en'
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### Автоматическое определение языка по локации:
|
||
|
||
Логика определения языка:
|
||
1. **Проверка cookie** — если пользователь уже выбирал язык, использовать его
|
||
2. **Определение по браузеру** — `navigator.language` или `navigator.languages`
|
||
3. **Fallback** — английский язык по умолчанию
|
||
|
||
Важно:
|
||
- Лендинг планируется как **SSG (статический)**. Значит IP-геолокация на сервере здесь неуместна: негде “серверу” исполняться на каждый запрос.
|
||
- Если когда-то понадобится geo-редирект — это отдельная задача (edge/runtime), не часть текущего лендинга.
|
||
|
||
**Маппинг стран к языкам:**
|
||
- 🇷🇺 Россия, Беларусь, Казахстан → `ru`
|
||
- 🇺🇦 Украина → `uk`
|
||
- 🇪🇸 Испания, Латинская Америка → `es`
|
||
- 🇫🇷 Франция, Бельгия, Швейцария (французский) → `fr`
|
||
- 🇩🇪 Германия, Австрия, Швейцария (немецкий) → `de`
|
||
- 🇺🇸 Остальные → `en`
|
||
|
||
---
|
||
|
||
## Компоненты Vuetify
|
||
|
||
### Используемые компоненты:
|
||
|
||
1. **v-app-bar** — шапка сайта
|
||
2. **v-container**, **v-row**, **v-col** — сетка
|
||
3. **v-card** — карточки фич
|
||
4. **v-btn** — кнопки
|
||
5. **v-carousel** — карусель скриншотов
|
||
6. **v-expansion-panels** — FAQ аккордеон
|
||
7. **v-chip** — бейджи платформ
|
||
8. **v-select** — выбор языка
|
||
9. **v-icon** — иконки
|
||
10. **v-divider** — разделители
|
||
|
||
### Кастомизация темы:
|
||
|
||
```typescript
|
||
// plugins/vuetify.ts
|
||
import { createVuetify } from 'vuetify'
|
||
|
||
export default defineNuxtPlugin((nuxtApp) => {
|
||
// Определение темы браузера при инициализации
|
||
const getInitialTheme = (): 'dark' | 'light' => {
|
||
if (process.client) {
|
||
// Проверка сохраненной темы в localStorage
|
||
const savedTheme = localStorage.getItem('vuetify-theme')
|
||
if (savedTheme === 'dark' || savedTheme === 'light') {
|
||
return savedTheme
|
||
}
|
||
|
||
// Определение по системным настройкам браузера
|
||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
return 'dark'
|
||
}
|
||
}
|
||
return 'light' // Fallback для SSR
|
||
}
|
||
|
||
const vuetify = createVuetify({
|
||
theme: {
|
||
defaultTheme: getInitialTheme(),
|
||
themes: {
|
||
dark: {
|
||
colors: {
|
||
primary: '#6366f1', // indigo
|
||
secondary: '#8b5cf6', // purple
|
||
accent: '#ec4899', // pink
|
||
background: '#0f172a', // slate-900
|
||
surface: '#1e293b', // slate-800
|
||
}
|
||
},
|
||
light: {
|
||
colors: {
|
||
primary: '#6366f1',
|
||
secondary: '#8b5cf6',
|
||
accent: '#ec4899',
|
||
background: '#ffffff',
|
||
surface: '#f8fafc', // slate-50
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
// Слушатель изменений системной темы
|
||
if (process.client) {
|
||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||
const handleThemeChange = (e: MediaQueryListEvent) => {
|
||
const savedTheme = localStorage.getItem('vuetify-theme')
|
||
// Автоматически менять только если пользователь не выбирал тему вручную
|
||
if (!savedTheme) {
|
||
vuetify.theme.global.name.value = e.matches ? 'dark' : 'light'
|
||
}
|
||
}
|
||
mediaQuery.addEventListener('change', handleThemeChange)
|
||
}
|
||
|
||
nuxtApp.vueApp.use(vuetify)
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## SEO оптимизация
|
||
|
||
### Meta теги:
|
||
|
||
```vue
|
||
<!-- layouts/default.vue -->
|
||
<template>
|
||
<Html :lang="$i18n.locale">
|
||
<Head>
|
||
<Title>{{ $t('meta.title') }}</Title>
|
||
<Meta name="description" :content="$t('meta.description')" />
|
||
<Meta property="og:title" :content="$t('meta.title')" />
|
||
<Meta property="og:description" :content="$t('meta.description')" />
|
||
<Meta property="og:image" content="/og-image.png" />
|
||
<Meta name="twitter:card" content="summary_large_image" />
|
||
</Head>
|
||
<!-- ... -->
|
||
</Html>
|
||
</template>
|
||
```
|
||
|
||
### Структурированные данные (JSON-LD):
|
||
|
||
```typescript
|
||
// composables/useStructuredData.ts
|
||
export const useStructuredData = () => {
|
||
const { $i18n } = useNuxtApp()
|
||
|
||
const softwareApplication = {
|
||
"@context": "https://schema.org",
|
||
"@type": "SoftwareApplication",
|
||
"name": "Voice to Text",
|
||
"applicationCategory": "UtilityApplication",
|
||
"operatingSystem": ["macOS", "Windows", "Linux"],
|
||
"offers": {
|
||
"@type": "Offer",
|
||
"price": "0",
|
||
"priceCurrency": "USD"
|
||
}
|
||
}
|
||
|
||
return { softwareApplication }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Функциональность
|
||
|
||
### 1. Определение платформы
|
||
|
||
```typescript
|
||
// composables/usePlatform.ts
|
||
export const usePlatform = () => {
|
||
const platform = ref<'macos' | 'windows' | 'linux' | 'unknown'>('unknown')
|
||
|
||
if (process.client) {
|
||
const userAgent = navigator.userAgent.toLowerCase()
|
||
if (userAgent.includes('mac')) platform.value = 'macos'
|
||
else if (userAgent.includes('win')) platform.value = 'windows'
|
||
else if (userAgent.includes('linux')) platform.value = 'linux'
|
||
}
|
||
|
||
const downloadUrl = computed(() => {
|
||
// Логика формирования URL для загрузки
|
||
const baseUrl = 'https://github.com/777genius/voice-to-text/releases'
|
||
// ...
|
||
})
|
||
|
||
return { platform, downloadUrl }
|
||
}
|
||
```
|
||
|
||
### 2. Определение темы браузера
|
||
|
||
```typescript
|
||
// composables/useBrowserTheme.ts
|
||
import { usePreferredDark } from '@vueuse/core'
|
||
import { useTheme } from 'vuetify'
|
||
|
||
export const useBrowserTheme = () => {
|
||
const { $vuetify } = useNuxtApp()
|
||
const preferredDark = usePreferredDark()
|
||
const theme = useTheme()
|
||
|
||
// Инициализация темы при первом посещении
|
||
const initTheme = () => {
|
||
if (process.client) {
|
||
const savedTheme = localStorage.getItem('vuetify-theme')
|
||
|
||
if (savedTheme) {
|
||
// Использовать сохраненную тему
|
||
theme.global.name.value = savedTheme as 'dark' | 'light'
|
||
} else {
|
||
// Использовать системную тему браузера
|
||
theme.global.name.value = preferredDark.value ? 'dark' : 'light'
|
||
localStorage.setItem('vuetify-theme', theme.global.name.value)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Переключение темы
|
||
const toggleTheme = () => {
|
||
const newTheme = theme.global.name.value === 'dark' ? 'light' : 'dark'
|
||
theme.global.name.value = newTheme
|
||
localStorage.setItem('vuetify-theme', newTheme)
|
||
}
|
||
|
||
// Установка конкретной темы
|
||
const setTheme = (themeName: 'dark' | 'light') => {
|
||
theme.global.name.value = themeName
|
||
localStorage.setItem('vuetify-theme', themeName)
|
||
}
|
||
|
||
// Слушатель изменений системной темы (только если пользователь не выбирал вручную)
|
||
if (process.client) {
|
||
watch(preferredDark, (isDark) => {
|
||
const savedTheme = localStorage.getItem('vuetify-theme')
|
||
if (!savedTheme) {
|
||
theme.global.name.value = isDark ? 'dark' : 'light'
|
||
}
|
||
})
|
||
}
|
||
|
||
return {
|
||
currentTheme: computed(() => theme.global.name.value),
|
||
isDark: computed(() => theme.global.name.value === 'dark'),
|
||
initTheme,
|
||
toggleTheme,
|
||
setTheme
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. Определение локации пользователя
|
||
|
||
```typescript
|
||
// composables/useLocation.ts
|
||
export const useLocation = () => {
|
||
const { $i18n } = useNuxtApp()
|
||
const locale = useCookie('i18n_redirected', { default: () => 'en' })
|
||
|
||
// Маппинг стран к языкам
|
||
const countryToLocale: Record<string, string> = {
|
||
'RU': 'ru', // Россия
|
||
'BY': 'ru', // Беларусь
|
||
'KZ': 'ru', // Казахстан
|
||
'UA': 'uk', // Украина
|
||
'ES': 'es', // Испания
|
||
'MX': 'es', // Мексика
|
||
'AR': 'es', // Аргентина
|
||
'CO': 'es', // Колумбия
|
||
'CL': 'es', // Чили
|
||
'PE': 'es', // Перу
|
||
'FR': 'fr', // Франция
|
||
'BE': 'fr', // Бельгия (французский)
|
||
'CH': 'de', // Швейцария (по умолчанию немецкий, можно улучшить)
|
||
'DE': 'de', // Германия
|
||
'AT': 'de', // Австрия
|
||
}
|
||
|
||
// Определение языка по браузеру
|
||
const getBrowserLocale = (): string => {
|
||
if (process.client) {
|
||
const browserLang = navigator.language || (navigator as any).userLanguage
|
||
const langCode = browserLang.split('-')[0].toLowerCase()
|
||
|
||
// Проверка поддерживаемых языков
|
||
const supportedLocales = ['en', 'ru', 'es', 'fr', 'de', 'uk']
|
||
if (supportedLocales.includes(langCode)) {
|
||
return langCode
|
||
}
|
||
|
||
// Проверка полного кода (например, ru-RU)
|
||
const fullCode = browserLang.toLowerCase()
|
||
if (fullCode.startsWith('ru')) return 'ru'
|
||
if (fullCode.startsWith('uk')) return 'uk'
|
||
if (fullCode.startsWith('es')) return 'es'
|
||
if (fullCode.startsWith('fr')) return 'fr'
|
||
if (fullCode.startsWith('de')) return 'de'
|
||
}
|
||
return 'en'
|
||
}
|
||
|
||
// Важно: лендинг статический (SSG), поэтому IP-геолокацию не используем.
|
||
|
||
// Инициализация языка при первом посещении
|
||
const initLocale = async () => {
|
||
// Если язык уже выбран пользователем, не менять
|
||
if (locale.value && locale.value !== 'en') {
|
||
return
|
||
}
|
||
|
||
// Приоритет: cookie > браузер > fallback
|
||
let detectedLocale = locale.value || 'en'
|
||
|
||
// На клиенте определяем по браузеру
|
||
detectedLocale = getBrowserLocale()
|
||
|
||
// Устанавливаем язык только если он отличается от текущего
|
||
if (detectedLocale !== $i18n.locale.value) {
|
||
const { switchLocalePath } = useI18n()
|
||
await navigateTo(switchLocalePath(detectedLocale))
|
||
}
|
||
}
|
||
|
||
return {
|
||
currentLocale: computed(() => $i18n.locale.value),
|
||
initLocale,
|
||
getBrowserLocale
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. Плагин для автоматической инициализации
|
||
|
||
```typescript
|
||
// plugins/init-theme-locale.client.ts
|
||
export default defineNuxtPlugin(async () => {
|
||
const { initTheme } = useBrowserTheme()
|
||
const { initLocale } = useLocation()
|
||
|
||
// Инициализация темы
|
||
initTheme()
|
||
|
||
// Инициализация локали (только при первом посещении)
|
||
const hasVisited = useCookie('has_visited', { default: () => false })
|
||
if (!hasVisited.value) {
|
||
await initLocale()
|
||
hasVisited.value = true
|
||
}
|
||
})
|
||
```
|
||
|
||
### 5. Статистика загрузок (опционально)
|
||
|
||
Не делаем.
|
||
|
||
### 6. Аналитика (опционально)
|
||
|
||
```typescript
|
||
// composables/useAnalytics.ts
|
||
export const useAnalytics = () => {
|
||
const trackDownload = (platform: string) => {
|
||
if (process.client) {
|
||
// Google Analytics, Plausible, или другая аналитика
|
||
gtag('event', 'download', { platform })
|
||
}
|
||
}
|
||
|
||
return { trackDownload }
|
||
}
|
||
```
|
||
|
||
**Решение по аналитике:**
|
||
- Делаем **GA4** (Google Analytics) + события:
|
||
- `download_click` (platform, arch, version, locale)
|
||
- `download_page_view`
|
||
- `language_change`
|
||
- `theme_change`
|
||
- `faq_open`
|
||
- `cta_view_features_click`
|
||
- Для подключения нужен `GA4 Measurement ID` (например, `G-XXXXXXXXXX`) и/или доступы к аккаунту. В коде предусматриваем конфиг через env, а фактическое подключение выполняется владельцем аккаунта.
|
||
|
||
---
|
||
|
||
## Дизайн и стилизация
|
||
|
||
### Цветовая схема:
|
||
|
||
**Темная тема:**
|
||
- Фон: `#0f172a` (slate-900)
|
||
- Поверхности: `#1e293b` (slate-800)
|
||
- Акцент: `#6366f1` (indigo-500)
|
||
- Текст: `#f1f5f9` (slate-100)
|
||
|
||
**Светлая тема:**
|
||
- Фон: `#ffffff`
|
||
- Поверхности: `#f8fafc` (slate-50)
|
||
- Акцент: `#6366f1` (indigo-500)
|
||
- Текст: `#0f172a` (slate-900)
|
||
|
||
### Типографика:
|
||
|
||
- **Заголовки**: Inter или System Font Stack
|
||
- **Текст**: Inter или System Font Stack
|
||
- **Моноширинный**: JetBrains Mono (для кода)
|
||
|
||
### Анимации:
|
||
|
||
- Плавные переходы при скролле
|
||
- Hover эффекты на карточках
|
||
- Параллакс для hero секции (опционально)
|
||
|
||
---
|
||
|
||
## Деплой
|
||
|
||
### Рекомендуемые платформы:
|
||
|
||
1. **Vercel** (рекомендуется)
|
||
- Автоматический деплой из Git
|
||
- SSR/SSG поддержка
|
||
- CDN по умолчанию
|
||
|
||
2. **Netlify**
|
||
- Аналогично Vercel
|
||
- Хорошая поддержка Nuxt
|
||
|
||
3. **GitHub Pages** (только SSG)
|
||
- Бесплатный хостинг
|
||
- Требует `nuxt generate`
|
||
|
||
### Target деплой:
|
||
- **Render**
|
||
- Режим: **Static Site Generation (SSG)** (статическая генерация)
|
||
|
||
### Конфигурация для деплоя:
|
||
|
||
```typescript
|
||
// nuxt.config.ts
|
||
export default defineNuxtConfig({
|
||
// Для SSG (пререндер страниц, а не SPA):
|
||
ssr: true,
|
||
nitro: { preset: 'static' },
|
||
routeRules: {
|
||
'/': { prerender: true },
|
||
'/download': { prerender: true },
|
||
},
|
||
})
|
||
```
|
||
|
||
### Важно про i18n + SSG (критично для SEO)
|
||
|
||
`routeRules` выше — это только базовый пример. Для i18n нужно гарантировать, что **пререндерятся все локали**.
|
||
|
||
Правило:
|
||
- Источник правды — список локалей + список страниц.
|
||
- На их основе формируем список путей для пререндера и для sitemap.
|
||
|
||
Пример логики (идея, не финальный код):
|
||
- Локали: `['en', 'ru', 'es', 'fr', 'de', 'uk']`, default = `en`
|
||
- Страницы: `['/', '/download']`
|
||
- Генерация путей:
|
||
- для `en`: `['/', '/download']`
|
||
- для остальных: `['/ru', '/ru/download']`, `['/es', '/es/download']`, ...
|
||
|
||
Это решает сразу две проблемы:
|
||
- не теряем локали при деплое (все страницы реально существуют как статик)
|
||
- sitemap/alternate можно генерировать из той же таблицы
|
||
|
||
См. также: `landing/docs/ARCHITECTURE_GUARDRAILS.md` (раздел про “источник правды по URL”).
|
||
|
||
---
|
||
|
||
## Чеклист разработки
|
||
|
||
### Фаза 1: Настройка проекта
|
||
- [ ] Инициализация Nuxt 3 проекта
|
||
- [ ] Установка Vuetify 3
|
||
- [ ] Настройка TypeScript
|
||
- [ ] Настройка Pinia (`@pinia/nuxt`) и базовых stores (общие состояния не держим “размазанными” по компонентам)
|
||
- [ ] Настройка i18n с определением локации
|
||
- [ ] Настройка Vuetify с определением темы браузера
|
||
- [ ] Установка @vueuse/nuxt для usePreferredDark
|
||
- [ ] ESLint (`@nuxt/eslint`) + Prettier (единый стиль форматирования)
|
||
- [ ] Базовая структура папок
|
||
|
||
### Фаза 2: Layout и навигация
|
||
- [ ] Создание `default.vue` layout
|
||
- [ ] Компонент `AppHeader.vue` с навигацией
|
||
- [ ] Компонент `AppFooter.vue`
|
||
- [ ] Компонент `LanguageSwitcher.vue`
|
||
- [ ] Адаптивная навигация (мобильное меню)
|
||
|
||
### Фаза 3: Главная страница
|
||
- [ ] Hero Section
|
||
- [ ] Features Section (6 карточек)
|
||
- [ ] Providers Section
|
||
- [ ] Screenshots Section (карусель)
|
||
- [ ] Download Section
|
||
- [ ] Privacy Section
|
||
- [ ] FAQ Section
|
||
|
||
### Фаза 4: Локализация и тема
|
||
- [ ] Переводы для всех 6 языков
|
||
- [ ] Композабл `useBrowserTheme.ts` для определения темы
|
||
- [ ] Композабл `useLocation.ts` для определения локации
|
||
- [ ] Плагин `init-theme-locale.client.ts` для автоинициализации
|
||
- [ ] Маппинг стран к языкам
|
||
- [ ] Определение языка по браузеру (client-side)
|
||
- [ ] Определение темы по системным настройкам
|
||
- [ ] Сохранение выбора пользователя в cookies/localStorage
|
||
- [ ] Тестирование переключения языков и темы
|
||
- [ ] SEO мета-теги для каждого языка
|
||
- [ ] Правильные URL для каждого языка
|
||
|
||
### Фаза 5: Функциональность
|
||
- [ ] Определение платформы пользователя
|
||
- [ ] Кнопки загрузки с правильными ссылками
|
||
- [ ] Страница `/download` с инструкциями
|
||
- [ ] Аналитика (GA4) + события (скачивание, смена языка/темы, FAQ, CTA)
|
||
|
||
### Фаза 6: Оптимизация
|
||
- [ ] Оптимизация изображений
|
||
- [ ] Lazy loading для секций
|
||
- [ ] SEO оптимизация
|
||
- [ ] Структурированные данные
|
||
- [ ] Sitemap и robots.txt
|
||
|
||
### Фаза 7: Тестирование
|
||
- [ ] Тестирование на разных устройствах
|
||
- [ ] Тестирование всех языков
|
||
- [ ] Тестирование автоматического определения языка по браузеру
|
||
- [ ] Тестирование автоматического определения темы
|
||
- [ ] Тестирование переключения темы вручную
|
||
- [ ] Тестирование сохранения выбора пользователя
|
||
- [ ] Проверка производительности
|
||
- [ ] Проверка доступности (a11y)
|
||
|
||
### Фаза 8: Деплой
|
||
- [ ] Настройка CI/CD
|
||
- [ ] Деплой на выбранную платформу
|
||
- [ ] Настройка домена
|
||
- [ ] SSL сертификат
|
||
|
||
---
|
||
|
||
## Дополнительные рекомендации
|
||
|
||
### Определение темы и локации:
|
||
- **Приоритет определения языка:**
|
||
1. Cookie (если пользователь уже выбирал)
|
||
2. Браузерные настройки (`navigator.language`)
|
||
3. Fallback на английский
|
||
|
||
- **Приоритет определения темы:**
|
||
1. localStorage (если пользователь выбирал вручную)
|
||
2. Системные настройки браузера (`prefers-color-scheme`)
|
||
3. Fallback на светлую тему
|
||
|
||
- **Альтернативные API для геолокации:**
|
||
- Не используем в рамках текущего статического лендинга (SSG).
|
||
|
||
- **Обработка ошибок:**
|
||
- Таймаут для API запросов (3 секунды)
|
||
- Graceful fallback на браузерные настройки
|
||
- Логирование ошибок для отладки
|
||
|
||
### Производительность:
|
||
- Использовать `nuxt/image` для оптимизации изображений
|
||
- Lazy loading для компонентов ниже fold
|
||
- Code splitting для больших компонентов
|
||
- Никаких внешних geo-запросов: меньше точек отказа и проще с приватностью.
|
||
|
||
### Доступность:
|
||
- Семантический HTML
|
||
- ARIA атрибуты
|
||
- Keyboard navigation
|
||
- Контрастность цветов (WCAG AA)
|
||
|
||
### Аналитика:
|
||
- Google Analytics 4
|
||
- Plausible (privacy-focused)
|
||
- Yandex Metrika (для русскоязычной аудитории)
|
||
|
||
### Мониторинг:
|
||
- Sentry для отслеживания ошибок
|
||
- Uptime monitoring
|
||
|
||
---
|
||
|
||
## Примеры реализации компонентов
|
||
|
||
### Hero Section
|
||
|
||
```vue
|
||
<template>
|
||
<v-container class="hero-section">
|
||
<v-row align="center" justify="center" class="fill-height">
|
||
<v-col cols="12" md="8" class="text-center">
|
||
<h1 class="text-h2 font-weight-bold mb-4">
|
||
{{ $t('hero.title') }}
|
||
</h1>
|
||
<p class="text-h6 text-medium-emphasis mb-8">
|
||
{{ $t('hero.subtitle') }}
|
||
</p>
|
||
<div class="d-flex gap-4 justify-center flex-wrap">
|
||
<DownloadButton :platform="platform" />
|
||
<v-btn
|
||
size="large"
|
||
variant="outlined"
|
||
@click="scrollToFeatures"
|
||
>
|
||
{{ $t('hero.viewFeatures') }}
|
||
</v-btn>
|
||
</div>
|
||
</v-col>
|
||
</v-row>
|
||
</v-container>
|
||
</template>
|
||
```
|
||
|
||
### Feature Card
|
||
|
||
```vue
|
||
<template>
|
||
<v-card class="feature-card" elevation="2">
|
||
<v-card-text class="text-center pa-6">
|
||
<v-icon
|
||
:icon="icon"
|
||
size="64"
|
||
color="primary"
|
||
class="mb-4"
|
||
/>
|
||
<h3 class="text-h6 mb-2">{{ title }}</h3>
|
||
<p class="text-body-2 text-medium-emphasis">
|
||
{{ description }}
|
||
</p>
|
||
</v-card-text>
|
||
</v-card>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
## Контакты и ресурсы
|
||
|
||
- **GitHub**: https://github.com/777genius/voice-to-text
|
||
- **Документация**: (если есть)
|
||
- **Поддержка**: (если есть)
|
||
|
||
---
|
||
|
||
**Версия плана**: 1.0
|
||
**Дата создания**: 2025-01-17
|
||
**Статус**: Готов к реализации
|
||
|
||
---
|
||
|
||
## План итераций (сначала планируем, затем перепроверяем, потом реализуем)
|
||
|
||
Процесс:
|
||
1) Сначала готовим максимально подробные планы итераций.
|
||
2) Затем **несколько раз** перепроверяем планы (полнота, несостыковки, риски, критерии готовности).
|
||
3) Только после этого начинаем реализацию пошагово.
|
||
4) После реализации — сверяемся с планами и ещё раз перепроверяем соответствие.
|
||
|
||
Файлы итераций:
|
||
- `landing/docs/iterations/ITERATION_00_REQUIREMENTS.md`
|
||
- `landing/docs/iterations/ITERATION_01_SCAFFOLDING.md`
|
||
- `landing/docs/iterations/ITERATION_02_UI_SECTIONS.md`
|
||
- `landing/docs/iterations/ITERATION_03_SWIPER_AND_DOWNLOAD.md`
|
||
- `landing/docs/iterations/ITERATION_04_ANALYTICS_SEO_SSG_RENDER.md`
|