Этап 1b: сайт
- Страницы обоих брендов из блоков Payload (D-029): главная, цены, локации, поддержка, 4 юридические, 404 и 500. Коллекции pages (черновики и версии) и faq с местами показа, 12 типов блоков; поля sites — почта и Telegram поддержки, часы работы, SEO по умолчанию. - Карта и глобус рисуются на сервере (d3-geo), контуры во внешнем SVG через <use>; выбор локации синхронизирован между картой и сеткой. - Тексты сайта и юридические черновики на 5 языках: de, fr, es — перевод с глоссарием; seed создаёт страницы и FAQ, только если их нет, SEED_CONTENT=reset пересоздаёт. - SEO: title и описания, canonical, hreflang с x-default, sitemap.xml и robots.txt на каждый домен, картинки для соцсетей. - Статическая сборка и пересборка через revalidatePath из хуков Payload (D-030); /svc/internal/revalidate для проверки. - 404 — статичная страница со статусом 404 через proxy; на сайте только опубликованные страницы. - Производительность: Client Hints темы Payload только в админке, предзагрузка 2 файлов шрифтов вместо 6; Lighthouse mobile 97–99. - Контраст --ink-3 по WCAG AA, радиокнопки вместо div role=button. - Документация: итоги 1b, глоссарий de/fr/es, трактовки юридических черновиков подтверждены, переносимые вопросы — в этапах 3e–3h. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
515acfb69d
commit
20491d1ba4
134 files changed
+11232
-212
No files matched your search
@@ -1,12 +1,17 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
import { cache } from 'react'
|
||||
|
||||
import { buildPlaceholders } from '@/core/content/catalog'
|
||||
import { getSiteSettings, listFaq, listLocations, listTariffs } from '@/core/content/server'
|
||||
import { listCurrencies } from '@/core/currencies/server'
|
||||
import { getDomain, getPrimaryDomain } from '@/core/domains/server'
|
||||
import type { DomainConfig } from '@/core/domains/types'
|
||||
import { localizedPath } from '@/core/routing/paths'
|
||||
import { SITE_CODES } from '@/core/sites'
|
||||
import { isLocale, type Locale } from '@/i18n/locales'
|
||||
import type { CabinetShell } from '@/ui/cabinet/types'
|
||||
import type { BlockContext } from '@/ui/marketing/blocks/context'
|
||||
import type { MarketingShell } from '@/ui/marketing/types'
|
||||
|
||||
/** Адрес страницы на другом домене бренда; язык — текущий, если он там включён */
|
||||
@@ -51,6 +56,51 @@ export const marketingShell = cache(
|
||||
},
|
||||
)
|
||||
|
||||
/** Данные блоков страницы сайта: настройки, локации, тарифы, FAQ, подстановки и ссылки */
|
||||
export const siteBlockContext = cache(
|
||||
async (
|
||||
host: string,
|
||||
rawLocale: string,
|
||||
rawCurrency: string,
|
||||
): Promise<Omit<BlockContext, 'page'>> => {
|
||||
const { domain, locale, currency } = await marketingContext(host, rawLocale, rawCurrency)
|
||||
const site = domain.site
|
||||
const other = SITE_CODES.find((code) => code !== site) ?? site
|
||||
const [settings, locations, tariffs, faq, cabinet, otherSite, t] = await Promise.all([
|
||||
getSiteSettings(site, locale),
|
||||
listLocations(site, locale),
|
||||
listTariffs(site, locale),
|
||||
listFaq(site, locale),
|
||||
getPrimaryDomain(site, 'cabinet'),
|
||||
getPrimaryDomain(other, 'marketing'),
|
||||
getTranslations({ locale, namespace: 'count' }),
|
||||
])
|
||||
return {
|
||||
site,
|
||||
locale,
|
||||
currency,
|
||||
settings,
|
||||
locations,
|
||||
tariffs,
|
||||
faq,
|
||||
placeholders: buildPlaceholders({
|
||||
site,
|
||||
locations,
|
||||
tariffs,
|
||||
currency,
|
||||
locale,
|
||||
countriesLabel: (count) => t('countries', { count }),
|
||||
}),
|
||||
href: (path) => localizedPath(path, locale, domain.defaultLocale),
|
||||
orderUrl: urlOn(cabinet, '/order', locale),
|
||||
otherBrand: {
|
||||
home: urlOn(otherSite, '/', locale),
|
||||
pricing: urlOn(otherSite, '/pricing', locale),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** Страница кабинета: /c/{хост}/{язык}/… */
|
||||
export const cabinetContext = cache(async (host: string, locale: string) => {
|
||||
const domain = await getDomain(host)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
// неизвестные адреса не кэшируем: иначе случайные пути от ботов копились бы в кэше страниц
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/** Любой другой путь сайта — 404 в оформлении бренда */
|
||||
export default function UnknownPage() {
|
||||
notFound()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
/** Ошибка страницы сайта (500) в оформлении бренда: шапка и подвал остаются на месте */
|
||||
export default function SiteError({
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
const t = useTranslations('error')
|
||||
return (
|
||||
<div className="mx-auto max-w-[1200px] px-6 pt-16 pb-24 leading-[normal]">
|
||||
<h1 className="mb-1.5 text-[34px] font-semibold tracking-[-0.03em] text-ink">{t('title')}</h1>
|
||||
<p className="mb-6 max-w-[40em] text-[15px] leading-[1.55] font-light text-ink-2">
|
||||
{t('text')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="inline-flex h-[46px] items-center rounded-xl bg-accent px-[22px] text-[15px] font-medium text-on-accent hover:bg-accent-hover"
|
||||
>
|
||||
{t('retry')}
|
||||
</button>
|
||||
<a
|
||||
href="/"
|
||||
className="inline-flex h-[46px] items-center rounded-xl border border-border px-[22px] text-[15px] text-ink hover:border-ink-3 hover:text-ink"
|
||||
>
|
||||
{t('home')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('footer.cookies')
|
||||
const page = sitePage('cookies')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('footer.privacy')
|
||||
const page = sitePage('privacy')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('footer.refund')
|
||||
const page = sitePage('refund')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('footer.terms')
|
||||
const page = sitePage('terms')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('nav.locations')
|
||||
const page = sitePage('locations')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
|
||||
import { marketingContext } from '@/app/(front)/context'
|
||||
import { NotFound } from '@/ui/NotFound'
|
||||
|
||||
type Props = { params: Promise<{ host: string; locale: string; currency: string }> }
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { host, locale, currency } = await params
|
||||
const context = await marketingContext(host, locale, currency)
|
||||
const t = await getTranslations({ locale: context.locale, namespace: 'notFound' })
|
||||
return { title: t('title'), robots: { index: false, follow: false } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Страница 404 сайта: сюда proxy ведёт неизвестные адреса со статусом 404 (decide.ts).
|
||||
* Обычная статичная страница — поэтому отрисована на сервере, в шапке и подвале бренда.
|
||||
*/
|
||||
export default async function SiteNotFoundPage({ params }: Props) {
|
||||
const { host, locale, currency } = await params
|
||||
const context = await marketingContext(host, locale, currency)
|
||||
setRequestLocale(context.locale)
|
||||
return <NotFound size="site" />
|
||||
}
|
||||
@@ -1,22 +1,7 @@
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
import { marketingContext } from '@/app/(front)/context'
|
||||
import { Stub } from '@/ui/Stub'
|
||||
const page = sitePage('home')
|
||||
|
||||
type Props = { params: Promise<{ host: string; locale: string; currency: string }> }
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
/** Главная бренда — заглушка до этапа 1b */
|
||||
export default async function HomePage({ params }: Props) {
|
||||
const { host, locale, currency } = await params
|
||||
const context = await marketingContext(host, locale, currency)
|
||||
setRequestLocale(context.locale)
|
||||
const t = await getTranslations()
|
||||
const site = context.domain.site
|
||||
return (
|
||||
<Stub
|
||||
size="site"
|
||||
title={t(`brand.${site}.name`)}
|
||||
text={`${t(`brand.${site}.tagline`)} ${t('stub.text')}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default page.Page
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('nav.pricing')
|
||||
const page = sitePage('pricing')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { siteStub } from '@/app/(front)/stubs'
|
||||
import { sitePage } from '@/app/(front)/site-page'
|
||||
|
||||
const page = siteStub('nav.support')
|
||||
const page = sitePage('support')
|
||||
|
||||
export const generateMetadata = page.generateMetadata
|
||||
|
||||
|
||||
@@ -60,7 +60,9 @@ export default async function MarketingRootLayout({
|
||||
</head>
|
||||
{/* расширения браузера (ColorZilla, Grammarly) дописывают атрибуты в <body> до гидратации */}
|
||||
<body suppressHydrationWarning>
|
||||
<NextIntlClientProvider messages={pick(messages, ['nav', 'switch'])}>
|
||||
<NextIntlClientProvider
|
||||
messages={pick(messages, ['nav', 'switch', 'lead', 'pricing', 'contact', 'error'])}
|
||||
>
|
||||
<ThemeWatcher />
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { setRequestLocale } from 'next-intl/server'
|
||||
|
||||
import { fillPlaceholders } from '@/core/content/placeholders'
|
||||
import { getPage, getSiteSettings } from '@/core/content/server'
|
||||
import { getPrimaryDomain } from '@/core/domains/server'
|
||||
import { isLegalSlug, pagePath, type PageSlug } from '@/core/pages'
|
||||
import { H1_BLOCKS, RenderBlocks } from '@/ui/marketing/blocks/RenderBlocks'
|
||||
|
||||
import { marketingContext, siteBlockContext, urlOn } from './context'
|
||||
|
||||
type Params = { params: Promise<{ host: string; locale: string; currency: string }> }
|
||||
|
||||
const mediaPath = (value: unknown): string | null =>
|
||||
typeof value === 'object' && value !== null && 'url' in value && typeof value.url === 'string'
|
||||
? value.url
|
||||
: null
|
||||
|
||||
/** Файлы медиатеки на доменах брендов открываются по /media/…, а не по REST-пути Payload */
|
||||
const publicMediaPath = (url: string) => url.replace(/^\/api\/media\/file\//, '/media/')
|
||||
|
||||
/** Ширина и отступы страницы — как у `<main>` в макетах */
|
||||
const CONTAINER: Record<PageSlug, string> = {
|
||||
home: 'max-w-[1200px] px-6',
|
||||
pricing: 'max-w-[1200px] px-6 pb-20',
|
||||
locations: 'max-w-[1200px] px-[clamp(14px,4vw,24px)] pb-20',
|
||||
support: 'max-w-[1000px] px-6 pb-20',
|
||||
terms: 'max-w-[1000px] px-6 pb-20',
|
||||
privacy: 'max-w-[1000px] px-6 pb-20',
|
||||
refund: 'max-w-[1000px] px-6 pb-20',
|
||||
cookies: 'max-w-[1000px] px-6 pb-20',
|
||||
}
|
||||
|
||||
/**
|
||||
* Страница сайта из коллекции «Страницы» (D-029): метаданные, H1 и блоки.
|
||||
* Адрес канонический на основном домене сайта, `hreflang` — на все его языки.
|
||||
*/
|
||||
export const sitePage = (slug: PageSlug) => ({
|
||||
async generateMetadata({ params }: Params): Promise<Metadata> {
|
||||
const { host, locale, currency } = await params
|
||||
const { domain, locale: pageLocale } = await marketingContext(host, locale, currency)
|
||||
const [page, settings, primary, blockContext] = await Promise.all([
|
||||
getPage(domain.site, slug, pageLocale),
|
||||
getSiteSettings(domain.site, pageLocale),
|
||||
getPrimaryDomain(domain.site, 'marketing'),
|
||||
siteBlockContext(host, locale, currency),
|
||||
])
|
||||
if (!page) return {}
|
||||
|
||||
const fill = (text: string) => fillPlaceholders(text, blockContext.placeholders)
|
||||
const name = fill(page.seo?.title || page.title)
|
||||
const template = settings.seo.titleTemplate
|
||||
// у главной заголовок целиком свой, у остальных страниц — по шаблону сайта
|
||||
const title = slug !== 'home' && template?.includes('%s') ? template.replace('%s', name) : name
|
||||
const seoDescription = page.seo?.description || settings.seo.description
|
||||
const description = seoDescription ? fill(seoDescription) : undefined
|
||||
const path = pagePath(slug)
|
||||
const target = primary ?? domain
|
||||
const languages = Object.fromEntries(
|
||||
target.locales.map((code) => [code, urlOn(target, path, code) ?? '']),
|
||||
)
|
||||
|
||||
// своя картинка из админки (страницы или сайта), иначе — созданная в цветах бренда
|
||||
const ownImage = mediaPath(page.seo?.image) ?? settings.seo.ogImage
|
||||
const origin = `https://${target.hostname}`
|
||||
const image = ownImage
|
||||
? `${origin}${publicMediaPath(ownImage)}`
|
||||
: `${origin}/svc/og/${domain.site}/${pageLocale}/${slug}`
|
||||
|
||||
return {
|
||||
title: { absolute: title },
|
||||
description,
|
||||
alternates: {
|
||||
canonical: urlOn(target, path, pageLocale),
|
||||
languages: { ...languages, 'x-default': urlOn(target, path, target.defaultLocale) ?? '' },
|
||||
},
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
url: urlOn(target, path, pageLocale) ?? undefined,
|
||||
siteName: settings.name,
|
||||
locale: pageLocale,
|
||||
type: 'website',
|
||||
images: [{ url: image, width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: { card: 'summary_large_image', title, description, images: [image] },
|
||||
}
|
||||
},
|
||||
|
||||
async Page({ params }: Params) {
|
||||
const { host, locale, currency } = await params
|
||||
const context = await marketingContext(host, locale, currency)
|
||||
setRequestLocale(context.locale)
|
||||
const page = await getPage(context.domain.site, slug, context.locale)
|
||||
if (!page) notFound()
|
||||
|
||||
const blocks = page.layout ?? []
|
||||
const blockContext = await siteBlockContext(host, locale, currency)
|
||||
const legal = isLegalSlug(slug)
|
||||
const hasH1 = blocks.some((block) => H1_BLOCKS.includes(block.blockType))
|
||||
|
||||
return (
|
||||
// межстрочный интервал как в макетах: там он нигде не задан, то есть normal
|
||||
<div data-page={slug} className={`mx-auto leading-[normal] ${CONTAINER[slug]}`}>
|
||||
{legal ? (
|
||||
// длинные составные слова (нем. «Rückerstattungsrichtlinie») на узком экране переносим
|
||||
<h1 className="pt-16 pb-6 text-[clamp(30px,4vw,44px)] leading-[1.1] font-semibold tracking-[-0.03em] break-words hyphens-auto">
|
||||
{page.title}
|
||||
</h1>
|
||||
) : (
|
||||
!hasH1 && <h1 className="sr-only">{page.title}</h1>
|
||||
)}
|
||||
<RenderBlocks blocks={blocks} context={{ ...blockContext, page: slug }} />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -3,16 +3,7 @@ import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||||
|
||||
import { Stub } from '@/ui/Stub'
|
||||
|
||||
import { cabinetContext, marketingContext } from './context'
|
||||
|
||||
type SiteKey =
|
||||
| 'nav.pricing'
|
||||
| 'nav.locations'
|
||||
| 'nav.support'
|
||||
| 'footer.terms'
|
||||
| 'footer.privacy'
|
||||
| 'footer.refund'
|
||||
| 'footer.cookies'
|
||||
import { cabinetContext } from './context'
|
||||
|
||||
type CabinetKey =
|
||||
| 'cabinet.home'
|
||||
@@ -24,27 +15,9 @@ type CabinetKey =
|
||||
| 'cabinet.login'
|
||||
| 'cabinet.order'
|
||||
|
||||
type SiteParams = { params: Promise<{ host: string; locale: string; currency: string }> }
|
||||
type CabinetParams = { params: Promise<{ host: string; locale: string }> }
|
||||
|
||||
/** Заглушка страницы сайта до этапа 1b: заголовок и короткая подпись */
|
||||
export const siteStub = (key: SiteKey) => ({
|
||||
async generateMetadata({ params }: SiteParams): Promise<Metadata> {
|
||||
const { host, locale, currency } = await params
|
||||
const context = await marketingContext(host, locale, currency)
|
||||
const t = await getTranslations({ locale: context.locale })
|
||||
return { title: t(key) }
|
||||
},
|
||||
async Page({ params }: SiteParams) {
|
||||
const { host, locale, currency } = await params
|
||||
const context = await marketingContext(host, locale, currency)
|
||||
setRequestLocale(context.locale)
|
||||
const t = await getTranslations()
|
||||
return <Stub size="site" title={t(key)} text={t('stub.text')} />
|
||||
},
|
||||
})
|
||||
|
||||
/** Заглушка страницы кабинета до этапов 1c–1g; card — для страниц без навигации */
|
||||
/** Заглушка страницы кабинета до этапов 1c–1h; card — для страниц без навигации */
|
||||
export const cabinetStub = (key: CabinetKey, size: 'cabinet' | 'card' = 'cabinet') => ({
|
||||
async generateMetadata({ params }: CabinetParams): Promise<Metadata> {
|
||||
const { host, locale } = await params
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { CurrencyCodeField as CurrencyCodeField_5f6e45c2d44d3ceaa88e6ac4e2162ad8 } from '../../../payload/components/CurrencyCodeField'
|
||||
import { AssignTenantFieldTrigger as AssignTenantFieldTrigger_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
|
||||
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { WatchTenantCollection as WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelector as TenantSelector_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
@@ -11,6 +34,29 @@ export const importMap = {
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"/payload/components/CurrencyCodeField#CurrencyCodeField": CurrencyCodeField_5f6e45c2d44d3ceaa88e6ac4e2162ad8,
|
||||
"@payloadcms/plugin-multi-tenant/client#AssignTenantFieldTrigger": AssignTenantFieldTrigger_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
|
||||
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/plugin-multi-tenant/client#WatchTenantCollection": WatchTenantCollection_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelector": TenantSelector_d6d5f193a167989e2ee7d14202901e62,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client'
|
||||
|
||||
import '@/styles/globals.css'
|
||||
|
||||
import de from '@/i18n/messages/de.json'
|
||||
import en from '@/i18n/messages/en.json'
|
||||
import es from '@/i18n/messages/es.json'
|
||||
import fr from '@/i18n/messages/fr.json'
|
||||
import ru from '@/i18n/messages/ru.json'
|
||||
import { DEFAULT_LOCALE, isLocale, type Locale } from '@/i18n/locales'
|
||||
|
||||
type ErrorTexts = typeof en.error
|
||||
|
||||
const TEXTS: Record<Locale, Partial<{ error: Partial<ErrorTexts> }>> = { en, ru, de, fr, es }
|
||||
|
||||
/**
|
||||
* Ошибка, из-за которой не отрисовался корневой layout (например, недоступна база).
|
||||
* Провайдера next-intl здесь нет: язык берётся из префикса адреса, бренд — из домена.
|
||||
*/
|
||||
export default function GlobalError({
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
reset: () => void
|
||||
}) {
|
||||
const prefix = typeof window === 'undefined' ? '' : window.location.pathname.split('/')[1]
|
||||
const locale = isLocale(prefix) ? prefix : DEFAULT_LOCALE
|
||||
const t: ErrorTexts = { ...en.error, ...TEXTS[locale].error }
|
||||
const site =
|
||||
typeof window !== 'undefined' && window.location.hostname.includes('veilixvpn')
|
||||
? 'mass'
|
||||
: 'dedicated'
|
||||
|
||||
return (
|
||||
<html lang={locale} data-site={site}>
|
||||
<body>
|
||||
<div className="mx-auto max-w-[1200px] px-6 pt-16 pb-24 leading-[normal]">
|
||||
<h1 className="mb-1.5 text-[34px] font-semibold tracking-[-0.03em] text-ink">
|
||||
{t.title}
|
||||
</h1>
|
||||
<p className="mb-6 max-w-[40em] text-[15px] leading-[1.55] font-light text-ink-2">
|
||||
{t.text}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="inline-flex h-[46px] items-center rounded-xl bg-accent px-[22px] text-[15px] font-medium text-on-accent hover:bg-accent-hover"
|
||||
>
|
||||
{t.retry}
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { GEO_FILES } from '@/ui/marketing/geo'
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
|
||||
export const generateStaticParams = () => Object.keys(GEO_FILES).map((file) => ({ file }))
|
||||
|
||||
/** Контуры стран для карты и глобуса. Адрес с версией, поэтому кэш — на год */
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ file: string }> }) {
|
||||
const { file } = await params
|
||||
const body = GEO_FILES[file]
|
||||
if (!body) return new Response('Not Found', { status: 404 })
|
||||
return new Response(body, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml; charset=utf-8',
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
|
||||
/** Служебные эндпоинты /svc/internal/* отвечают только с INTERNAL_TOKEN из .env */
|
||||
export const isInternalRequest = (request: Request) => {
|
||||
const expected = process.env.INTERNAL_TOKEN
|
||||
const given = request.headers.get('x-vx-internal-token')
|
||||
if (!expected || !given) return false
|
||||
const a = Buffer.from(given)
|
||||
const b = Buffer.from(expected)
|
||||
return a.length === b.length && timingSafeEqual(a, b)
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { timingSafeEqual } from 'node:crypto'
|
||||
|
||||
import { listActiveDomains } from '@/core/domains/server'
|
||||
|
||||
import { isInternalRequest } from '../auth'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const isAuthorized = (request: Request) => {
|
||||
const expected = process.env.INTERNAL_TOKEN
|
||||
const given = request.headers.get('x-vx-internal-token')
|
||||
if (!expected || !given) return false
|
||||
const a = Buffer.from(given)
|
||||
const b = Buffer.from(expected)
|
||||
return a.length === b.length && timingSafeEqual(a, b)
|
||||
}
|
||||
|
||||
/** Список активных доменов для proxy: тот не ходит в Payload сам, а берёт его здесь */
|
||||
export async function GET(request: Request) {
|
||||
if (!isAuthorized(request)) return new Response('Not Found', { status: 404 })
|
||||
if (!isInternalRequest(request)) return new Response('Not Found', { status: 404 })
|
||||
return Response.json(await listActiveDomains(), { headers: { 'Cache-Control': 'no-store' } })
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { revalidateMarketingPages } from '@/core/content/revalidate'
|
||||
|
||||
import { isInternalRequest } from '../auth'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
/**
|
||||
* Пересобрать маркетинговые страницы вручную — например, после правки прямо в базе.
|
||||
* Обычно пересборку запускают хуки Payload при сохранении в админке (D-030).
|
||||
*/
|
||||
export async function POST(request: Request) {
|
||||
if (!isInternalRequest(request)) return new Response('Not Found', { status: 404 })
|
||||
revalidateMarketingPages()
|
||||
return Response.json({ revalidated: true })
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import { ImageResponse } from 'next/og'
|
||||
|
||||
import { buildPlaceholders } from '@/core/content/catalog'
|
||||
import { fillPlaceholders } from '@/core/content/placeholders'
|
||||
import { getPage, getSiteSettings, listLocations, listTariffs } from '@/core/content/server'
|
||||
import { getPrimaryDomain } from '@/core/domains/server'
|
||||
import { PAGE_SLUGS, type PageSlug } from '@/core/pages'
|
||||
import { isSiteCode, type SiteCode } from '@/core/sites'
|
||||
import { isLocale } from '@/i18n/locales'
|
||||
|
||||
// картинка собирается при первом запросе и обновляется раз в сутки
|
||||
export const dynamic = 'force-static'
|
||||
export const revalidate = 86400
|
||||
|
||||
/** Цвета брендов для картинок: Satori не понимает OKLCH, значения — из tokens.css в sRGB */
|
||||
const COLORS: Record<SiteCode, Record<string, string>> = {
|
||||
dedicated: {
|
||||
bg: '#fcfbff',
|
||||
ink: '#211b30',
|
||||
ink2: '#565264',
|
||||
accent: '#6137a6',
|
||||
accentBorder: 'rgba(97, 55, 166, 0.35)',
|
||||
glow1: 'rgba(214, 196, 255, 1)',
|
||||
glow1Clear: 'rgba(214, 196, 255, 0)',
|
||||
glow3: 'rgba(255, 206, 249, 1)',
|
||||
glow3Clear: 'rgba(255, 206, 249, 0)',
|
||||
},
|
||||
mass: {
|
||||
bg: '#fdfefe',
|
||||
ink: '#012528',
|
||||
ink2: '#425a5d',
|
||||
accent: '#00705e',
|
||||
accentBorder: 'rgba(0, 112, 94, 0.35)',
|
||||
glow1: 'rgba(140, 227, 221, 1)',
|
||||
glow1Clear: 'rgba(140, 227, 221, 0)',
|
||||
glow3: 'rgba(171, 234, 213, 1)',
|
||||
glow3Clear: 'rgba(171, 234, 213, 0)',
|
||||
},
|
||||
}
|
||||
|
||||
const fontFile = (pkg: string, file: string) =>
|
||||
readFile(path.join(process.cwd(), 'node_modules', '@fontsource', pkg, 'files', file))
|
||||
|
||||
const fonts = async () => {
|
||||
const [latin, cyrillic, latinBold, cyrillicBold, logoLatin, logoCyrillic] = await Promise.all([
|
||||
fontFile('commissioner', 'commissioner-latin-400-normal.woff'),
|
||||
fontFile('commissioner', 'commissioner-cyrillic-400-normal.woff'),
|
||||
fontFile('commissioner', 'commissioner-latin-600-normal.woff'),
|
||||
fontFile('commissioner', 'commissioner-cyrillic-600-normal.woff'),
|
||||
fontFile('unbounded', 'unbounded-latin-600-normal.woff'),
|
||||
fontFile('unbounded', 'unbounded-cyrillic-600-normal.woff'),
|
||||
])
|
||||
// у Satori один шрифт на имя и насыщенность, поэтому наборы символов — под разными именами
|
||||
return [
|
||||
{ name: 'Commissioner', data: latin, weight: 400 as const },
|
||||
{ name: 'CommissionerCyr', data: cyrillic, weight: 400 as const },
|
||||
{ name: 'Commissioner', data: latinBold, weight: 600 as const },
|
||||
{ name: 'CommissionerCyr', data: cyrillicBold, weight: 600 as const },
|
||||
{ name: 'Unbounded', data: logoLatin, weight: 600 as const },
|
||||
{ name: 'UnboundedCyr', data: logoCyrillic, weight: 600 as const },
|
||||
]
|
||||
}
|
||||
|
||||
const TEXT_FONT = 'Commissioner, CommissionerCyr'
|
||||
const LOGO_FONT = 'Unbounded, UnboundedCyr'
|
||||
|
||||
/**
|
||||
* Картинка для соцсетей (1200×630) в цветах бренда: логотип «Слои», название, заголовок
|
||||
* страницы и домен. Если в админке загружена своя картинка, страница ссылается на неё.
|
||||
*/
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ site: string; locale: string; slug: string }> },
|
||||
) {
|
||||
const { site, locale, slug } = await params
|
||||
if (!isSiteCode(site) || !isLocale(locale) || !(PAGE_SLUGS as readonly string[]).includes(slug)) {
|
||||
return new Response('Not Found', { status: 404 })
|
||||
}
|
||||
const [page, settings, primary, locations, tariffs] = await Promise.all([
|
||||
getPage(site, slug as PageSlug, locale),
|
||||
getSiteSettings(site, locale),
|
||||
getPrimaryDomain(site, 'marketing'),
|
||||
listLocations(site, locale),
|
||||
listTariffs(site, locale),
|
||||
])
|
||||
if (!page) return new Response('Not Found', { status: 404 })
|
||||
|
||||
const currency = primary?.localeCurrency[locale] ?? primary?.defaultCurrency ?? 'USD'
|
||||
const placeholders = buildPlaceholders({
|
||||
site,
|
||||
locations,
|
||||
tariffs,
|
||||
currency,
|
||||
locale,
|
||||
countriesLabel: (count) => String(count),
|
||||
})
|
||||
// у главной — заголовок первого экрана: название бренда и так стоит рядом с логотипом
|
||||
const hero = page.layout?.find((block) => block.blockType === 'hero')
|
||||
const title = fillPlaceholders(hero?.title || page.seo?.title || page.title, placeholders)
|
||||
const color = COLORS[site]
|
||||
|
||||
return new ImageResponse(
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
padding: 72,
|
||||
background: color.bg,
|
||||
fontFamily: TEXT_FONT,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: -160,
|
||||
top: -200,
|
||||
width: 700,
|
||||
height: 700,
|
||||
borderRadius: 9999,
|
||||
backgroundImage: `radial-gradient(circle, ${color.glow1} 0%, ${color.glow1Clear} 70%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 120,
|
||||
bottom: -320,
|
||||
width: 640,
|
||||
height: 640,
|
||||
borderRadius: 9999,
|
||||
backgroundImage: `radial-gradient(circle, ${color.glow3} 0%, ${color.glow3Clear} 70%)`,
|
||||
}}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 22 }}>
|
||||
<div style={{ display: 'flex', position: 'relative', width: 64, height: 64 }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 7,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 16,
|
||||
backgroundImage: `linear-gradient(140deg, ${color.accent}, ${color.glow3})`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 7,
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 16,
|
||||
background: 'rgba(255,255,255,0.6)',
|
||||
border: `2px solid ${color.accentBorder}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontFamily: LOGO_FONT, fontSize: 40, color: color.ink }}>{settings.name}</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
maxWidth: 940,
|
||||
fontSize: 68,
|
||||
fontWeight: 600,
|
||||
lineHeight: 1.08,
|
||||
letterSpacing: '-0.03em',
|
||||
color: color.ink,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{ fontSize: 30, color: color.ink2 }}>{primary?.hostname ?? ''}</div>
|
||||
</div>,
|
||||
{ width: 1200, height: 630, fonts: await fonts() },
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { listPublishedPages } from '@/core/content/server'
|
||||
import { getDomain } from '@/core/domains/server'
|
||||
import { pagePath } from '@/core/pages'
|
||||
import { buildRobots, buildSitemap } from '@/core/seo/sitemap'
|
||||
|
||||
// по файлу на хост собирается при первом запросе и обновляется раз в 5 минут
|
||||
export const dynamic = 'force-static'
|
||||
export const revalidate = 300
|
||||
|
||||
/**
|
||||
* robots.txt и sitemap.xml домена: proxy ведёт сюда /robots.txt и /sitemap.xml любого хоста.
|
||||
* Индексируется только маркетинговый домен с флагом «индексация»; sitemap — у маркетинговых.
|
||||
*/
|
||||
export async function GET(
|
||||
_request: Request,
|
||||
{ params }: { params: Promise<{ host: string; file: string }> },
|
||||
) {
|
||||
const { host, file } = await params
|
||||
const domain = await getDomain(host)
|
||||
const origin = `https://${host}`
|
||||
|
||||
if (file === 'robots.txt') {
|
||||
const indexable = Boolean(domain && domain.app === 'marketing' && domain.indexable)
|
||||
return new Response(buildRobots({ origin, indexable }), {
|
||||
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||
})
|
||||
}
|
||||
|
||||
if (file === 'sitemap.xml' && domain?.app === 'marketing') {
|
||||
const pages = await listPublishedPages(domain.site)
|
||||
const xml = buildSitemap({
|
||||
origin,
|
||||
locales: domain.locales,
|
||||
defaultLocale: domain.defaultLocale,
|
||||
pages: pages.map((page) => ({ path: pagePath(page.slug), updatedAt: page.updatedAt })),
|
||||
})
|
||||
return new Response(xml, { headers: { 'Content-Type': 'application/xml; charset=utf-8' } })
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 })
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildPlaceholders, defaultTariff, includedLocations, tariffForCountry } from './catalog'
|
||||
import { formatMoney, perMonth, yearSavings } from './money'
|
||||
import { fillPlaceholders } from './placeholders'
|
||||
import type { CatalogLocation, CatalogTariff } from './types'
|
||||
|
||||
const tariff = (overrides: Partial<CatalogTariff>): CatalogTariff => ({
|
||||
code: 'base',
|
||||
name: 'Base',
|
||||
description: null,
|
||||
badge: null,
|
||||
period: { unit: 'month', count: 1 },
|
||||
devicesLimit: 5,
|
||||
isDefault: false,
|
||||
prices: {},
|
||||
countries: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const location = (iso2: string, status: CatalogLocation['status']): CatalogLocation => ({
|
||||
iso2,
|
||||
country: iso2,
|
||||
city: iso2,
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
status,
|
||||
freeServers: null,
|
||||
})
|
||||
|
||||
const dedicated = [
|
||||
tariff({
|
||||
code: 'base',
|
||||
isDefault: true,
|
||||
countries: ['NL', 'NO'],
|
||||
prices: {
|
||||
RUB: { first: 490000, renewal: 249900 },
|
||||
USD: { first: 4900, renewal: 2400 },
|
||||
},
|
||||
}),
|
||||
tariff({ code: 'rare', countries: ['CH'], prices: { RUB: { first: 690000, renewal: 349900 } } }),
|
||||
]
|
||||
|
||||
const mass = [
|
||||
tariff({
|
||||
code: 'month',
|
||||
countries: ['NL'],
|
||||
prices: { RUB: { first: 69900, renewal: 69900 }, USD: { first: 699, renewal: 699 } },
|
||||
}),
|
||||
tariff({
|
||||
code: 'year',
|
||||
isDefault: true,
|
||||
period: { unit: 'year', count: 1 },
|
||||
countries: ['NL'],
|
||||
prices: { RUB: { first: 699000, renewal: 699000 }, USD: { first: 6990, renewal: 6990 } },
|
||||
}),
|
||||
]
|
||||
|
||||
describe('деньги', () => {
|
||||
it('рубли без копеек, доллары с центами только когда они есть', () => {
|
||||
expect(formatMoney(490000, 'RUB', 'ru')).toBe('4 900 ₽')
|
||||
expect(formatMoney(4900, 'USD', 'en')).toBe('$49')
|
||||
expect(formatMoney(699, 'USD', 'en')).toBe('$6.99')
|
||||
})
|
||||
|
||||
it('цена «в месяц» у года округляется вниз: 582 ₽ и $5.82', () => {
|
||||
expect(perMonth(699000, 69900)).toBe(58200)
|
||||
expect(perMonth(6990, 699)).toBe(582)
|
||||
})
|
||||
|
||||
it('экономия года — точно, а без экономии — null', () => {
|
||||
expect(yearSavings(699000, 69900)).toBe(139800)
|
||||
expect(yearSavings(6990, 699)).toBe(1398)
|
||||
expect(yearSavings(120000, 10000)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('каталог', () => {
|
||||
it('цена локации — у тарифа, в странах которого она есть, и только в его валютах', () => {
|
||||
expect(tariffForCountry(dedicated, 'NL', 'RUB')?.code).toBe('base')
|
||||
expect(tariffForCountry(dedicated, 'CH', 'RUB')?.code).toBe('rare')
|
||||
expect(tariffForCountry(dedicated, 'CH', 'USD')).toBeNull()
|
||||
expect(tariffForCountry(dedicated, 'BR', 'RUB')).toBeNull()
|
||||
})
|
||||
|
||||
it('тариф по умолчанию — отмеченный, а без цены в валюте — первый с ценой', () => {
|
||||
expect(defaultTariff(dedicated, 'RUB')?.code).toBe('base')
|
||||
expect(defaultTariff([dedicated[1]], 'RUB')?.code).toBe('rare')
|
||||
expect(defaultTariff(dedicated, 'EUR')).toBeNull()
|
||||
})
|
||||
|
||||
it('страны: у dedicated — те, что продаются, у mass — только «Включено»', () => {
|
||||
const locations = [
|
||||
location('NL', 'available'),
|
||||
location('NO', 'on_demand'),
|
||||
location('CH', 'available'),
|
||||
]
|
||||
expect(includedLocations('dedicated', locations, dedicated, 'USD').map((l) => l.iso2)).toEqual([
|
||||
'NL',
|
||||
'NO',
|
||||
])
|
||||
const massLocations = [location('NL', 'available'), location('NO', 'soon')]
|
||||
expect(includedLocations('mass', massLocations, mass, 'RUB').map((l) => l.iso2)).toEqual(['NL'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('подстановки', () => {
|
||||
it('цены и страны mass на русском', () => {
|
||||
const values = buildPlaceholders({
|
||||
site: 'mass',
|
||||
locations: [location('NL', 'available'), location('NO', 'soon')],
|
||||
tariffs: mass,
|
||||
currency: 'RUB',
|
||||
locale: 'ru',
|
||||
countriesLabel: (count) => `${count} стран`,
|
||||
})
|
||||
expect(
|
||||
fillPlaceholders(
|
||||
'{priceMonth} или {priceYear} — {pricePerMonth} в месяц, экономия {savings}',
|
||||
values,
|
||||
),
|
||||
).toBe('699 ₽ или 6 990 ₽ — 582 ₽ в месяц, экономия 1 398 ₽')
|
||||
expect(values.countries).toBe('1 стран')
|
||||
expect(values.devices).toBe('5')
|
||||
})
|
||||
|
||||
it('неизвестная подстановка остаётся в тексте', () => {
|
||||
expect(fillPlaceholders('{priceFirst} и {unknown}', { priceFirst: '$49' })).toBe(
|
||||
'$49 и {unknown}',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { SiteCode } from '@/core/sites'
|
||||
|
||||
import { formatMoney, perMonth, yearSavings } from './money'
|
||||
import type { Placeholders } from './placeholders'
|
||||
import type { CatalogLocation, CatalogTariff } from './types'
|
||||
|
||||
const hasPrice = (tariff: CatalogTariff, currency: string) => Boolean(tariff.prices[currency])
|
||||
|
||||
/** Тариф dedicated, в странах которого локация. Нет цены в валюте страницы — тариф не продаётся */
|
||||
export const tariffForCountry = (
|
||||
tariffs: CatalogTariff[],
|
||||
iso2: string,
|
||||
currency: string,
|
||||
): CatalogTariff | null =>
|
||||
tariffs.find((tariff) => tariff.countries.includes(iso2) && hasPrice(tariff, currency)) ?? null
|
||||
|
||||
/** Тариф по умолчанию (`isDefault`), иначе первый с ценой в валюте страницы */
|
||||
export const defaultTariff = (tariffs: CatalogTariff[], currency: string): CatalogTariff | null =>
|
||||
tariffs.find((tariff) => tariff.isDefault && hasPrice(tariff, currency)) ??
|
||||
tariffs.find((tariff) => hasPrice(tariff, currency)) ??
|
||||
null
|
||||
|
||||
const plan = (tariffs: CatalogTariff[], unit: 'month' | 'year', currency: string) =>
|
||||
tariffs.find(
|
||||
(tariff) =>
|
||||
tariff.period.unit === unit && tariff.period.count === 1 && hasPrice(tariff, currency),
|
||||
) ?? null
|
||||
|
||||
/** Планы mass: «Месяц» и «Год» */
|
||||
export const massPlans = (tariffs: CatalogTariff[], currency: string) => ({
|
||||
month: plan(tariffs, 'month', currency),
|
||||
year: plan(tariffs, 'year', currency),
|
||||
})
|
||||
|
||||
/**
|
||||
* Локации, которые даёт сайт: у dedicated — те, что можно купить в валюте страницы,
|
||||
* у mass — работающие («Включено»). По ним считается число стран в текстах.
|
||||
*/
|
||||
export const includedLocations = (
|
||||
site: SiteCode,
|
||||
locations: CatalogLocation[],
|
||||
tariffs: CatalogTariff[],
|
||||
currency: string,
|
||||
): CatalogLocation[] =>
|
||||
site === 'dedicated'
|
||||
? locations.filter((location) => tariffForCountry(tariffs, location.iso2, currency))
|
||||
: locations.filter((location) => location.status === 'available')
|
||||
|
||||
/** Значения подстановок для текстов блоков на языке и в валюте страницы */
|
||||
export const buildPlaceholders = ({
|
||||
site,
|
||||
locations,
|
||||
tariffs,
|
||||
currency,
|
||||
locale,
|
||||
countriesLabel,
|
||||
}: {
|
||||
site: SiteCode
|
||||
locations: CatalogLocation[]
|
||||
tariffs: CatalogTariff[]
|
||||
currency: string
|
||||
locale: string
|
||||
/** «30 стран» с правильной формой слова: формирует next-intl */
|
||||
countriesLabel: (count: number) => string
|
||||
}): Placeholders => {
|
||||
const money = (amount: number) => formatMoney(amount, currency, locale)
|
||||
const values: Placeholders = {
|
||||
countries: countriesLabel(includedLocations(site, locations, tariffs, currency).length),
|
||||
}
|
||||
|
||||
const base = defaultTariff(tariffs, currency)
|
||||
if (base) {
|
||||
values.priceFirst = money(base.prices[currency].first)
|
||||
values.priceRenewal = money(base.prices[currency].renewal)
|
||||
values.devices = String(base.devicesLimit)
|
||||
}
|
||||
|
||||
const { month, year } = massPlans(tariffs, currency)
|
||||
if (month) values.priceMonth = money(month.prices[currency].first)
|
||||
if (year) values.priceYear = money(year.prices[currency].first)
|
||||
if (month && year) {
|
||||
const monthAmount = month.prices[currency].first
|
||||
const yearAmount = year.prices[currency].first
|
||||
values.pricePerMonth = money(perMonth(yearAmount, monthAmount))
|
||||
const savings = yearSavings(yearAmount, monthAmount)
|
||||
if (savings) values.savings = money(savings)
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/** Деньги на страницах: суммы в минимальных единицах (копейки, центы), формат — Intl */
|
||||
|
||||
/** «4 900 ₽» в русском, «₽4,900» в английском, «$6.99»; копейки и центы — только если они есть */
|
||||
export const formatMoney = (amount: number, currency: string, locale: string): string =>
|
||||
new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
currencyDisplay: 'narrowSymbol',
|
||||
minimumFractionDigits: amount % 100 === 0 ? 0 : 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount / 100)
|
||||
|
||||
/**
|
||||
* Цена «в месяц» у годового тарифа: округление вниз. Если цены тарифов целые (рубли),
|
||||
* то до целых — 582 ₽ из 6 990 ₽, иначе до центов — $5.82 из $69.90.
|
||||
*/
|
||||
export const perMonth = (yearAmount: number, monthAmount: number): number => {
|
||||
const step = yearAmount % 100 === 0 && monthAmount % 100 === 0 ? 100 : 1
|
||||
return Math.floor(yearAmount / 12 / step) * step
|
||||
}
|
||||
|
||||
/** Экономия годового тарифа против 12 месяцев; не положительная — экономии нет */
|
||||
export const yearSavings = (yearAmount: number, monthAmount: number): number | null => {
|
||||
const savings = monthAmount * 12 - yearAmount
|
||||
return savings > 0 ? savings : null
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Подстановки в текстах блоков: {priceFirst}, {countries} и т. п. (D-029).
|
||||
* Неизвестная подстановка остаётся как есть — так ошибку в админке видно на странице.
|
||||
*/
|
||||
export type Placeholders = Partial<Record<PlaceholderName, string>>
|
||||
|
||||
export const PLACEHOLDER_NAMES = [
|
||||
'priceFirst',
|
||||
'priceRenewal',
|
||||
'priceMonth',
|
||||
'priceYear',
|
||||
'pricePerMonth',
|
||||
'savings',
|
||||
'countries',
|
||||
'devices',
|
||||
] as const
|
||||
|
||||
export type PlaceholderName = (typeof PLACEHOLDER_NAMES)[number]
|
||||
|
||||
export const fillPlaceholders = (text: string, values: Placeholders): string =>
|
||||
text.replace(/\{(\w+)\}/g, (match, name: string) => values[name as PlaceholderName] ?? match)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
|
||||
/** Layout всех маркетинговых страниц: правка любого бренда пересобирает оба (D-030) */
|
||||
const MARKETING_LAYOUT = '/(front)/m/[host]/[locale]/[currency]'
|
||||
|
||||
/**
|
||||
* Пометить маркетинговые страницы устаревшими: они пересоберутся при следующем заходе.
|
||||
* Работает только внутри запроса Next; вне его (скрипты Payload) бросает ошибку.
|
||||
*/
|
||||
export const revalidateMarketingPages = () => revalidatePath(MARKETING_LAYOUT, 'layout')
|
||||
@@ -0,0 +1,201 @@
|
||||
import config from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { cache } from 'react'
|
||||
|
||||
import { FAQ_PLACEMENTS, type FaqPlacement, PAGE_SLUGS, type PageSlug } from '@/core/pages'
|
||||
import type { SiteCode } from '@/core/sites'
|
||||
import type { Locale } from '@/i18n/locales'
|
||||
import type { Country, Media, Page } from '@/payload-types'
|
||||
|
||||
import type { CatalogLocation, CatalogTariff, FaqItem, LocationStatus, SiteSettings } from './types'
|
||||
|
||||
/**
|
||||
* Данные маркетинговых страниц из Payload. Страницы публичные, поэтому чтение без правил
|
||||
* доступа (`overrideAccess: true`), но только опубликованное.
|
||||
*/
|
||||
|
||||
const siteRecord = cache(async (code: SiteCode) => {
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'sites',
|
||||
where: { code: { equals: code } },
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
overrideAccess: true,
|
||||
})
|
||||
return docs[0] ?? null
|
||||
})
|
||||
|
||||
const mediaUrl = (value: unknown): string | null =>
|
||||
typeof value === 'object' && value !== null && 'url' in value
|
||||
? ((value as Media).url ?? null)
|
||||
: null
|
||||
|
||||
export const getSiteSettings = cache(
|
||||
async (code: SiteCode, locale: Locale): Promise<SiteSettings> => {
|
||||
const record = await siteRecord(code)
|
||||
if (!record) throw new Error(`site ${code} not found`)
|
||||
const payload = await getPayload({ config })
|
||||
const site = await payload.findByID({
|
||||
collection: 'sites',
|
||||
id: record.id,
|
||||
locale,
|
||||
depth: 1,
|
||||
overrideAccess: true,
|
||||
})
|
||||
return {
|
||||
site: code,
|
||||
name: site.name,
|
||||
supportEmail: site.supportEmail ?? null,
|
||||
supportBot: site.supportBot ?? null,
|
||||
supportHours: site.supportHours ?? null,
|
||||
seo: {
|
||||
titleTemplate: site.seo?.titleTemplate ?? null,
|
||||
description: site.seo?.description ?? null,
|
||||
ogImage: mediaUrl(site.seo?.ogImage),
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** Опубликованная страница сайта; черновики на сайт не попадают */
|
||||
export const getPage = cache(
|
||||
async (code: SiteCode, slug: PageSlug, locale: Locale): Promise<Page | null> => {
|
||||
const record = await siteRecord(code)
|
||||
if (!record) return null
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
and: [
|
||||
{ site: { equals: record.id } },
|
||||
{ slug: { equals: slug } },
|
||||
{ _status: { equals: 'published' } },
|
||||
],
|
||||
},
|
||||
locale,
|
||||
draft: false,
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
overrideAccess: true,
|
||||
})
|
||||
return docs[0] ?? null
|
||||
},
|
||||
)
|
||||
|
||||
const countryOf = (value: unknown): Country | null =>
|
||||
typeof value === 'object' && value !== null && 'iso2' in value ? (value as Country) : null
|
||||
|
||||
export const listLocations = cache(
|
||||
async (code: SiteCode, locale: Locale): Promise<CatalogLocation[]> => {
|
||||
const record = await siteRecord(code)
|
||||
if (!record) return []
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'locations',
|
||||
where: { and: [{ site: { equals: record.id } }, { visible: { equals: true } }] },
|
||||
locale,
|
||||
sort: 'sort',
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
overrideAccess: true,
|
||||
})
|
||||
return docs.flatMap((doc) => {
|
||||
const country = countryOf(doc.country)
|
||||
if (!country) return []
|
||||
return [
|
||||
{
|
||||
iso2: country.iso2,
|
||||
country: country.name,
|
||||
city: doc.city,
|
||||
lat: doc.lat ?? country.lat ?? 0,
|
||||
lng: doc.lng ?? country.lng ?? 0,
|
||||
status: doc.status as LocationStatus,
|
||||
freeServers: doc.freeServers ?? null,
|
||||
},
|
||||
]
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export const listTariffs = cache(
|
||||
async (code: SiteCode, locale: Locale): Promise<CatalogTariff[]> => {
|
||||
const record = await siteRecord(code)
|
||||
if (!record) return []
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'tariffs',
|
||||
where: { and: [{ site: { equals: record.id } }, { active: { equals: true } }] },
|
||||
locale,
|
||||
sort: 'sort',
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
overrideAccess: true,
|
||||
})
|
||||
return docs.map((doc) => ({
|
||||
code: doc.code,
|
||||
name: doc.name,
|
||||
description: doc.description ?? null,
|
||||
badge: doc.badge ?? null,
|
||||
period: { unit: doc.period.unit, count: doc.period.count },
|
||||
devicesLimit: doc.devicesLimit,
|
||||
isDefault: Boolean(doc.isDefault),
|
||||
prices: Object.fromEntries(
|
||||
(doc.prices ?? []).map((row) => [
|
||||
row.currency,
|
||||
{ first: row.firstPeriodAmount, renewal: row.renewalAmount },
|
||||
]),
|
||||
),
|
||||
countries: (doc.countries ?? []).flatMap((value) => {
|
||||
const country = countryOf(value)
|
||||
return country ? [country.iso2] : []
|
||||
}),
|
||||
}))
|
||||
},
|
||||
)
|
||||
|
||||
const isPlacement = (value: unknown): value is FaqPlacement =>
|
||||
typeof value === 'string' && (FAQ_PLACEMENTS as readonly string[]).includes(value)
|
||||
|
||||
export const listFaq = cache(async (code: SiteCode, locale: Locale): Promise<FaqItem[]> => {
|
||||
const record = await siteRecord(code)
|
||||
if (!record) return []
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'faq',
|
||||
where: { and: [{ site: { equals: record.id } }, { active: { equals: true } }] },
|
||||
locale,
|
||||
sort: 'sort',
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
overrideAccess: true,
|
||||
})
|
||||
return docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
question: doc.question,
|
||||
answer: doc.answer,
|
||||
placements: (doc.placements ?? []).filter(isPlacement),
|
||||
}))
|
||||
})
|
||||
|
||||
/** Опубликованные страницы сайта для sitemap.xml: адрес и дата правки */
|
||||
export const listPublishedPages = cache(
|
||||
async (code: SiteCode): Promise<{ slug: PageSlug; updatedAt: string | null }[]> => {
|
||||
const record = await siteRecord(code)
|
||||
if (!record) return []
|
||||
const payload = await getPayload({ config })
|
||||
const { docs } = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { and: [{ site: { equals: record.id } }, { _status: { equals: 'published' } }] },
|
||||
draft: false,
|
||||
depth: 0,
|
||||
pagination: false,
|
||||
select: { slug: true, updatedAt: true },
|
||||
overrideAccess: true,
|
||||
})
|
||||
const order = new Map(PAGE_SLUGS.map((slug, index) => [slug, index]))
|
||||
return docs
|
||||
.map((doc) => ({ slug: doc.slug, updatedAt: doc.updatedAt ?? null }))
|
||||
.sort((a, b) => (order.get(a.slug) ?? 0) - (order.get(b.slug) ?? 0))
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
||||
|
||||
import type { FaqPlacement } from '@/core/pages'
|
||||
import type { SiteCode } from '@/core/sites'
|
||||
|
||||
/** Статус локации: у dedicated `available` и `on_demand`, у mass `available` и `soon` */
|
||||
export type LocationStatus = 'available' | 'on_demand' | 'soon'
|
||||
|
||||
/** Тариф сайта на языке страницы. Цены — по валютам, в минимальных единицах */
|
||||
export type CatalogTariff = {
|
||||
code: string
|
||||
name: string
|
||||
description: string | null
|
||||
badge: string | null
|
||||
period: { unit: 'month' | 'year'; count: number }
|
||||
devicesLimit: number
|
||||
isDefault: boolean
|
||||
prices: Record<string, { first: number; renewal: number }>
|
||||
/** коды стран ISO 3166-1 */
|
||||
countries: string[]
|
||||
}
|
||||
|
||||
/** Локация сайта на языке страницы */
|
||||
export type CatalogLocation = {
|
||||
iso2: string
|
||||
country: string
|
||||
city: string
|
||||
lat: number
|
||||
lng: number
|
||||
status: LocationStatus
|
||||
freeServers: number | null
|
||||
}
|
||||
|
||||
export type SiteSettings = {
|
||||
site: SiteCode
|
||||
name: string
|
||||
supportEmail: string | null
|
||||
supportBot: string | null
|
||||
supportHours: string | null
|
||||
seo: { titleTemplate: string | null; description: string | null; ogImage: string | null }
|
||||
}
|
||||
|
||||
export type FaqItem = {
|
||||
id: string
|
||||
question: string
|
||||
answer: SerializedEditorState
|
||||
placements: FaqPlacement[]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/** Страницы сайта: адреса заданы в коде, содержимое — в коллекции «Страницы» (D-029) */
|
||||
export const PAGE_SLUGS = [
|
||||
'home',
|
||||
'pricing',
|
||||
'locations',
|
||||
'support',
|
||||
'terms',
|
||||
'privacy',
|
||||
'refund',
|
||||
'cookies',
|
||||
] as const
|
||||
|
||||
export type PageSlug = (typeof PAGE_SLUGS)[number]
|
||||
|
||||
/** Юридические страницы открываются по адресу /legal/{slug} */
|
||||
export const LEGAL_SLUGS = ['terms', 'privacy', 'refund', 'cookies'] as const satisfies PageSlug[]
|
||||
|
||||
export type LegalSlug = (typeof LEGAL_SLUGS)[number]
|
||||
|
||||
export const isLegalSlug = (value: string): value is LegalSlug =>
|
||||
(LEGAL_SLUGS as readonly string[]).includes(value)
|
||||
|
||||
/** Путь страницы на сайте без префикса языка */
|
||||
export const pagePath = (slug: PageSlug): string => {
|
||||
if (slug === 'home') return '/'
|
||||
return isLegalSlug(slug) ? `/legal/${slug}` : `/${slug}`
|
||||
}
|
||||
|
||||
/** Места показа вопросов FAQ (07-frontend, раздел 7.7) */
|
||||
export const FAQ_PLACEMENTS = [
|
||||
'home',
|
||||
'pricing',
|
||||
'support_connection',
|
||||
'support_billing',
|
||||
'support_locations',
|
||||
'cabinet',
|
||||
'miniapp',
|
||||
] as const
|
||||
|
||||
export type FaqPlacement = (typeof FAQ_PLACEMENTS)[number]
|
||||
@@ -60,9 +60,11 @@ describe('домены брендов', () => {
|
||||
it('/admin и /api закрыты', () => {
|
||||
expect(decide(input({ pathname: '/admin' }))).toEqual({ type: 'not-found' })
|
||||
expect(decide(input({ pathname: '/api/admins' }))).toEqual({ type: 'not-found' })
|
||||
// похожий адрес — обычная неизвестная страница сайта
|
||||
expect(decide(input({ pathname: '/administrator' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/m/dev.veilix.app/en/usd/administrator',
|
||||
pathname: '/m/dev.veilix.app/en/usd/page-not-found',
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -77,10 +79,23 @@ describe('домены брендов', () => {
|
||||
expect(decide(input({ pathname: '/media' }))).toEqual({ type: 'not-found' })
|
||||
})
|
||||
|
||||
it('прямой запрос к внутреннему маршруту получает ещё один префикс и попадает в 404', () => {
|
||||
it('прямой запрос к внутреннему маршруту — неизвестный адрес, страница 404 бренда', () => {
|
||||
expect(decide(input({ pathname: '/m/dev.veilix.app/ru/rub/pricing' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/m/dev.veilix.app/en/usd/m/dev.veilix.app/ru/rub/pricing',
|
||||
pathname: '/m/dev.veilix.app/en/usd/page-not-found',
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it('неизвестный адрес сайта — страница 404 бренда со статусом 404, на языке из адреса', () => {
|
||||
expect(decide(input({ pathname: '/ru/nope', cookieLocale: 'ru' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/m/dev.veilix.app/ru/rub/page-not-found',
|
||||
status: 404,
|
||||
})
|
||||
expect(decide(input({ pathname: '/legal/terms' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/m/dev.veilix.app/en/usd/legal/terms',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -176,6 +191,37 @@ describe('валюта', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('robots.txt и sitemap.xml', () => {
|
||||
it('у каждого домена свои, без префикса языка и валюты', () => {
|
||||
expect(decide(input({ pathname: '/robots.txt' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/svc/seo/dev.veilix.app/robots.txt',
|
||||
})
|
||||
expect(decide(input({ domain: cabinet, pathname: '/sitemap.xml' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/svc/seo/dev.my.veilix.app/sitemap.xml',
|
||||
})
|
||||
})
|
||||
|
||||
it('у админки и неизвестного хоста — тоже, там они запрещают индексацию', () => {
|
||||
expect(decide(input({ isAdminHost: true, domain: null, pathname: '/robots.txt' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/svc/seo/admin/robots.txt',
|
||||
})
|
||||
expect(decide(input({ domain: null, pathname: '/robots.txt' }))).toEqual({
|
||||
type: 'rewrite',
|
||||
pathname: '/svc/seo/unknown/robots.txt',
|
||||
})
|
||||
})
|
||||
|
||||
it('только GET и HEAD', () => {
|
||||
expect(decide(input({ method: 'POST', pathname: '/robots.txt' }))).toMatchObject({
|
||||
type: 'rewrite',
|
||||
pathname: '/m/dev.veilix.app/en/usd/page-not-found',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('кабинет', () => {
|
||||
it('без валюты в маршруте', () => {
|
||||
expect(decide(input({ domain: cabinet, pathname: '/ru/keys' }))).toEqual({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DomainConfig } from '@/core/domains/types'
|
||||
import { PAGE_SLUGS, pagePath } from '@/core/pages'
|
||||
import { isLocale, type Locale } from '@/i18n/locales'
|
||||
|
||||
import { detectLocale } from './locale'
|
||||
@@ -21,7 +22,7 @@ export type RouteInput = {
|
||||
|
||||
export type RouteDecision =
|
||||
| { type: 'next' }
|
||||
| { type: 'rewrite'; pathname: string }
|
||||
| { type: 'rewrite'; pathname: string; status?: 404 }
|
||||
| { type: 'redirect'; location: string }
|
||||
| { type: 'not-found' }
|
||||
|
||||
@@ -30,6 +31,18 @@ const startsWith = (pathname: string, segment: string) =>
|
||||
|
||||
const isNavigation = (method: string) => method === 'GET' || method === 'HEAD'
|
||||
|
||||
/** Файлы для поисковиков: у каждого домена свои, отдаёт /svc/seo/{хост}/… */
|
||||
const SEO_FILES = ['/robots.txt', '/sitemap.xml']
|
||||
|
||||
/** Адреса страниц сайта; остальные получают страницу 404 бренда */
|
||||
const SITE_PATHS = new Set<string>(PAGE_SLUGS.map(pagePath))
|
||||
|
||||
/**
|
||||
* Страница 404 сайта — обычная статичная страница, отрисованная на сервере. notFound() в
|
||||
* Next 16 отдаёт пустую оболочку и рисует 404 уже в браузере (07-frontend, раздел 7.16).
|
||||
*/
|
||||
export const NOT_FOUND_SEGMENT = 'page-not-found'
|
||||
|
||||
/** Валюта страницы: выбор из cookie, если она есть на домене; иначе валюта для языка или по умолчанию */
|
||||
export const pickCurrency = (
|
||||
domain: DomainConfig,
|
||||
@@ -53,6 +66,12 @@ export const decide = (input: RouteInput): RouteDecision => {
|
||||
// свои эндпоинты работают на любом хосте
|
||||
if (startsWith(pathname, 'svc')) return { type: 'next' }
|
||||
|
||||
// robots.txt и sitemap.xml — на каждом хосте, включая админку и неизвестные (там — запрет)
|
||||
if (SEO_FILES.includes(pathname) && isNavigation(input.method)) {
|
||||
const host = input.domain?.hostname ?? (input.isAdminHost ? 'admin' : 'unknown')
|
||||
return { type: 'rewrite', pathname: `/svc/seo/${host}${pathname}` }
|
||||
}
|
||||
|
||||
if (input.isAdminHost) {
|
||||
if (pathname === '/') return { type: 'redirect', location: '/admin' }
|
||||
if (startsWith(pathname, 'admin') || startsWith(pathname, 'api')) return { type: 'next' }
|
||||
@@ -114,8 +133,9 @@ export const decide = (input: RouteInput): RouteDecision => {
|
||||
return { type: 'rewrite', pathname: `/c/${domain.hostname}/${locale}${tail}` }
|
||||
}
|
||||
const currency = pickCurrency(domain, locale, input.cookieCurrency, input.isBot)
|
||||
return {
|
||||
type: 'rewrite',
|
||||
pathname: `/m/${domain.hostname}/${locale}/${currency.toLowerCase()}${tail}`,
|
||||
const base = `/m/${domain.hostname}/${locale}/${currency.toLowerCase()}`
|
||||
if (!SITE_PATHS.has(rest)) {
|
||||
return { type: 'rewrite', pathname: `${base}/${NOT_FOUND_SEGMENT}`, status: 404 }
|
||||
}
|
||||
return { type: 'rewrite', pathname: `${base}${tail}` }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { buildRobots, buildSitemap } from './sitemap'
|
||||
|
||||
describe('sitemap.xml', () => {
|
||||
const xml = buildSitemap({
|
||||
origin: 'https://veilix.app',
|
||||
locales: ['en', 'ru'],
|
||||
defaultLocale: 'en',
|
||||
pages: [
|
||||
{ path: '/', updatedAt: '2026-10-22T03:00:00.000Z' },
|
||||
{ path: '/pricing', updatedAt: null },
|
||||
],
|
||||
})
|
||||
|
||||
it('каждая страница на каждом языке', () => {
|
||||
const locs = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((match) => match[1])
|
||||
expect(locs).toEqual([
|
||||
'https://veilix.app/',
|
||||
'https://veilix.app/ru',
|
||||
'https://veilix.app/pricing',
|
||||
'https://veilix.app/ru/pricing',
|
||||
])
|
||||
})
|
||||
|
||||
it('у записи — все языковые версии и x-default на английскую', () => {
|
||||
const first = xml.split('<url>')[1]
|
||||
expect(first).toContain('hreflang="en" href="https://veilix.app/"')
|
||||
expect(first).toContain('hreflang="ru" href="https://veilix.app/ru"')
|
||||
expect(first).toContain('hreflang="x-default" href="https://veilix.app/"')
|
||||
expect(first).toContain('<lastmod>2026-10-22</lastmod>')
|
||||
})
|
||||
|
||||
it('пространства имён sitemap и xhtml', () => {
|
||||
expect(xml).toContain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"')
|
||||
expect(xml).toContain('xmlns:xhtml="http://www.w3.org/1999/xhtml"')
|
||||
})
|
||||
})
|
||||
|
||||
describe('robots.txt', () => {
|
||||
it('индексируемый домен открыт и ссылается на sitemap', () => {
|
||||
expect(buildRobots({ origin: 'https://veilix.app', indexable: true })).toBe(
|
||||
'User-agent: *\nAllow: /\n\nSitemap: https://veilix.app/sitemap.xml\n',
|
||||
)
|
||||
})
|
||||
|
||||
it('остальные закрыты', () => {
|
||||
expect(buildRobots({ origin: 'https://dev.veilix.app', indexable: false })).toBe(
|
||||
'User-agent: *\nDisallow: /\n',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
import { localizedPath } from '@/core/routing/paths'
|
||||
import type { Locale } from '@/i18n/locales'
|
||||
|
||||
/** Страница сайта в sitemap: путь без префикса языка и дата правки */
|
||||
export type SitemapPage = { path: string; updatedAt: string | null }
|
||||
|
||||
const escape = (value: string) =>
|
||||
value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
|
||||
/**
|
||||
* sitemap.xml домена (06-i18n, раздел 5): каждая страница на каждом языке домена,
|
||||
* у каждой записи — все языковые версии и x-default (английская, без префикса).
|
||||
*/
|
||||
export const buildSitemap = ({
|
||||
origin,
|
||||
locales,
|
||||
defaultLocale,
|
||||
pages,
|
||||
}: {
|
||||
origin: string
|
||||
locales: Locale[]
|
||||
defaultLocale: Locale
|
||||
pages: SitemapPage[]
|
||||
}): string => {
|
||||
const url = (path: string, locale: Locale) =>
|
||||
escape(`${origin}${localizedPath(path, locale, defaultLocale)}`)
|
||||
|
||||
const entries = pages.flatMap((page) => {
|
||||
const alternates = [
|
||||
...locales.map(
|
||||
(locale) =>
|
||||
` <xhtml:link rel="alternate" hreflang="${locale}" href="${url(page.path, locale)}"/>`,
|
||||
),
|
||||
` <xhtml:link rel="alternate" hreflang="x-default" href="${url(page.path, defaultLocale)}"/>`,
|
||||
]
|
||||
const lastmod = page.updatedAt ? ` <lastmod>${page.updatedAt.slice(0, 10)}</lastmod>\n` : ''
|
||||
return locales.map(
|
||||
(locale) =>
|
||||
` <url>\n <loc>${url(page.path, locale)}</loc>\n${lastmod}${alternates.join('\n')}\n </url>`,
|
||||
)
|
||||
})
|
||||
|
||||
return [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
|
||||
...entries,
|
||||
'</urlset>',
|
||||
'',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** robots.txt: индексируемый сайт — всё открыто и ссылка на sitemap, иначе — запрет */
|
||||
export const buildRobots = ({ origin, indexable }: { origin: string; indexable: boolean }) =>
|
||||
indexable
|
||||
? `User-agent: *\nAllow: /\n\nSitemap: ${origin}/sitemap.xml\n`
|
||||
: 'User-agent: *\nDisallow: /\n'
|
||||
+120
-1
@@ -1 +1,120 @@
|
||||
{}
|
||||
{
|
||||
"brand": {
|
||||
"dedicated": {
|
||||
"name": "Veilix",
|
||||
"tagline": "Persönliches VPN mit dedizierter IP-Adresse. Seit 2022 am Markt."
|
||||
},
|
||||
"mass": {
|
||||
"name": "VeilixVPN",
|
||||
"tagline": "Ein VPN, das Sie nie ausschalten müssen. Seit 2022 am Markt."
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"main": "Hauptnavigation",
|
||||
"home": "Startseite",
|
||||
"pricing": "Preise",
|
||||
"locations": "Standorte",
|
||||
"support": "Support",
|
||||
"logIn": "Anmelden",
|
||||
"getVpn": "VPN holen",
|
||||
"menu": "Menü"
|
||||
},
|
||||
"footer": {
|
||||
"service": "Service",
|
||||
"legal": "Rechtliches",
|
||||
"terms": "Nutzungs\u00ADbedingungen",
|
||||
"privacy": "Datenschutz",
|
||||
"refund": "Rückerstattung",
|
||||
"cookies": "Cookie-Richtlinie",
|
||||
"copyright": "© {year} {brand}"
|
||||
},
|
||||
"switch": {
|
||||
"language": "Sprache",
|
||||
"currency": "Währung",
|
||||
"theme": "Design",
|
||||
"light": "Helles Design",
|
||||
"dark": "Dunkles Design",
|
||||
"system": "Systemeinstellung verwenden"
|
||||
},
|
||||
"cabinet": {
|
||||
"home": "Übersicht",
|
||||
"keys": "Alle Schlüssel",
|
||||
"keysShort": "Schlüssel",
|
||||
"key": "Schlüssel",
|
||||
"buy": "Schlüssel kaufen",
|
||||
"buyShort": "Kaufen",
|
||||
"billing": "Guthaben & Zahlungen",
|
||||
"referral": "Empfehlungsprogramm",
|
||||
"more": "Mehr",
|
||||
"support": "Support",
|
||||
"login": "Anmelden",
|
||||
"order": "Bestellung",
|
||||
"terms": "Nutzungsbedingungen",
|
||||
"privacy": "Datenschutz"
|
||||
},
|
||||
"stub": {
|
||||
"text": "Dieser Bereich folgt in den nächsten Ausbaustufen."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Seite nicht gefunden",
|
||||
"text": "Die Seite wurde möglicherweise verschoben oder hat nie existiert.",
|
||||
"home": "Zur Startseite"
|
||||
},
|
||||
"status": {
|
||||
"dedicated": {
|
||||
"available": "Server verfügbar",
|
||||
"on_demand": "In 24 Stunden bereit"
|
||||
},
|
||||
"mass": {
|
||||
"available": "Inklusive",
|
||||
"soon": "In Kürze"
|
||||
},
|
||||
"withCount": "{label} · {count}"
|
||||
},
|
||||
"count": {
|
||||
"countries": "{count, plural, one {# Land} other {# Länder}}"
|
||||
},
|
||||
"pricing": {
|
||||
"firstMonth": "im ersten Monat",
|
||||
"thenPerMonth": "danach {price} pro Monat",
|
||||
"then": "danach",
|
||||
"perMonth": "pro Monat",
|
||||
"perYear": "pro Jahr",
|
||||
"buy": "Kaufen",
|
||||
"buyIn": "Kaufen: {country}",
|
||||
"locations": "Standorte",
|
||||
"plans": "Tarif",
|
||||
"close": "Schließen"
|
||||
},
|
||||
"lead": {
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"telegram": "Telegram",
|
||||
"whatsapp": "WhatsApp",
|
||||
"task": "Kurz zu Ihrem Anliegen",
|
||||
"send": "Senden",
|
||||
"sent": "Anfrage gesendet",
|
||||
"needContact": "Bitte geben Sie eine E-Mail-Adresse, Telegram oder WhatsApp an",
|
||||
"badEmail": "Bitte prüfen Sie die E-Mail-Adresse",
|
||||
"consent": "Mit dem Absenden des Formulars stimmen Sie der <link>Datenschutzerklärung</link> zu."
|
||||
},
|
||||
"support": {
|
||||
"telegram": "Telegram",
|
||||
"email": "E-Mail"
|
||||
},
|
||||
"contact": {
|
||||
"email": "Ihre E-Mail-Adresse",
|
||||
"topic": "Thema",
|
||||
"message": "Was ist passiert?",
|
||||
"send": "Senden",
|
||||
"sent": "Gesendet, wir antworten per E-Mail",
|
||||
"needEmail": "Bitte geben Sie Ihre E-Mail-Adresse an, damit wir antworten können",
|
||||
"needMessage": "Bitte beschreiben Sie, was passiert ist"
|
||||
},
|
||||
"error": {
|
||||
"title": "Etwas ist schiefgelaufen",
|
||||
"text": "Der Fehler ist uns bereits bekannt. Versuchen Sie in einer Minute, die Seite neu zu laden.",
|
||||
"retry": "Seite neu laden",
|
||||
"home": "Zur Startseite"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"brand": {
|
||||
"dedicated": {
|
||||
"name": "Veilix",
|
||||
"tagline": "Personal VPN servers with a dedicated IP. Running since 2022."
|
||||
"tagline": "A private VPN with a dedicated IP. Running since 2022."
|
||||
},
|
||||
"mass": {
|
||||
"name": "VeilixVPN",
|
||||
@@ -59,5 +59,62 @@
|
||||
"title": "Page not found",
|
||||
"text": "The page may have moved or never existed.",
|
||||
"home": "Go to the home page"
|
||||
},
|
||||
"status": {
|
||||
"dedicated": {
|
||||
"available": "Servers available",
|
||||
"on_demand": "Ready within 24 hours"
|
||||
},
|
||||
"mass": {
|
||||
"available": "Included",
|
||||
"soon": "Coming soon"
|
||||
},
|
||||
"withCount": "{label} · {count}"
|
||||
},
|
||||
"count": {
|
||||
"countries": "{count, plural, one {# country} other {# countries}}"
|
||||
},
|
||||
"pricing": {
|
||||
"firstMonth": "first month",
|
||||
"thenPerMonth": "then {price} per month",
|
||||
"then": "then",
|
||||
"perMonth": "per month",
|
||||
"perYear": "per year",
|
||||
"buy": "Buy",
|
||||
"buyIn": "Buy: {country}",
|
||||
"locations": "Locations",
|
||||
"plans": "Plan",
|
||||
"close": "Close"
|
||||
},
|
||||
"lead": {
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"telegram": "Telegram",
|
||||
"whatsapp": "WhatsApp",
|
||||
"task": "Briefly about your case",
|
||||
"send": "Send",
|
||||
"sent": "Request sent",
|
||||
"needContact": "Add an email, Telegram or WhatsApp",
|
||||
"badEmail": "Check the email address",
|
||||
"consent": "By sending the form, you agree to the <link>privacy policy</link>."
|
||||
},
|
||||
"support": {
|
||||
"telegram": "Telegram",
|
||||
"email": "Email"
|
||||
},
|
||||
"contact": {
|
||||
"email": "Your email",
|
||||
"topic": "Topic",
|
||||
"message": "What happened",
|
||||
"send": "Send",
|
||||
"sent": "Sent, we will reply by email",
|
||||
"needEmail": "Add your email so we can reply",
|
||||
"needMessage": "Describe what happened"
|
||||
},
|
||||
"error": {
|
||||
"title": "Something went wrong",
|
||||
"text": "We already know about the error. Try reloading the page in a minute.",
|
||||
"retry": "Reload the page",
|
||||
"home": "Go to home page"
|
||||
}
|
||||
}
|
||||
+120
-1
@@ -1 +1,120 @@
|
||||
{}
|
||||
{
|
||||
"brand": {
|
||||
"dedicated": {
|
||||
"name": "Veilix",
|
||||
"tagline": "Una VPN personal con IP dedicada. En funcionamiento desde 2022."
|
||||
},
|
||||
"mass": {
|
||||
"name": "VeilixVPN",
|
||||
"tagline": "Una VPN que nunca tendrás que desactivar. En funcionamiento desde 2022."
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"main": "Navegación principal",
|
||||
"home": "Inicio",
|
||||
"pricing": "Precios",
|
||||
"locations": "Ubicaciones",
|
||||
"support": "Soporte",
|
||||
"logIn": "Entrar",
|
||||
"getVpn": "Obtener VPN",
|
||||
"menu": "Menú"
|
||||
},
|
||||
"footer": {
|
||||
"service": "Servicio",
|
||||
"legal": "Legal",
|
||||
"terms": "Condiciones de uso",
|
||||
"privacy": "Política de privacidad",
|
||||
"refund": "Reembolsos",
|
||||
"cookies": "Política de cookies",
|
||||
"copyright": "© {year} {brand}"
|
||||
},
|
||||
"switch": {
|
||||
"language": "Idioma",
|
||||
"currency": "Moneda",
|
||||
"theme": "Tema",
|
||||
"light": "Tema claro",
|
||||
"dark": "Tema oscuro",
|
||||
"system": "Según el sistema"
|
||||
},
|
||||
"cabinet": {
|
||||
"home": "Resumen",
|
||||
"keys": "Todas las claves",
|
||||
"keysShort": "Claves",
|
||||
"key": "Clave",
|
||||
"buy": "Comprar una clave",
|
||||
"buyShort": "Comprar",
|
||||
"billing": "Saldo y pagos",
|
||||
"referral": "Programa de referidos",
|
||||
"more": "Más",
|
||||
"support": "Soporte",
|
||||
"login": "Iniciar sesión",
|
||||
"order": "Finalizar compra",
|
||||
"terms": "Condiciones",
|
||||
"privacy": "Privacidad"
|
||||
},
|
||||
"stub": {
|
||||
"text": "Esta sección estará disponible en las próximas etapas."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Página no encontrada",
|
||||
"text": "Es posible que la página se haya movido o que nunca haya existido.",
|
||||
"home": "Ir a la página de inicio"
|
||||
},
|
||||
"status": {
|
||||
"dedicated": {
|
||||
"available": "Servidores disponibles",
|
||||
"on_demand": "Listo en 24 horas"
|
||||
},
|
||||
"mass": {
|
||||
"available": "Incluido",
|
||||
"soon": "Próximamente"
|
||||
},
|
||||
"withCount": "{label} · {count}"
|
||||
},
|
||||
"count": {
|
||||
"countries": "{count, plural, one {# país} other {# países}}"
|
||||
},
|
||||
"pricing": {
|
||||
"firstMonth": "el primer mes",
|
||||
"thenPerMonth": "después {price} al mes",
|
||||
"then": "después",
|
||||
"perMonth": "al mes",
|
||||
"perYear": "al año",
|
||||
"buy": "Comprar",
|
||||
"buyIn": "Comprar: {country}",
|
||||
"locations": "Ubicaciones",
|
||||
"plans": "Plan",
|
||||
"close": "Cerrar"
|
||||
},
|
||||
"lead": {
|
||||
"name": "Nombre",
|
||||
"email": "Correo electrónico",
|
||||
"telegram": "Telegram",
|
||||
"whatsapp": "WhatsApp",
|
||||
"task": "Cuéntanos brevemente tu caso",
|
||||
"send": "Enviar",
|
||||
"sent": "Solicitud enviada",
|
||||
"needContact": "Indica un correo electrónico, Telegram o WhatsApp",
|
||||
"badEmail": "Revisa la dirección de correo electrónico",
|
||||
"consent": "Al enviar el formulario, aceptas la <link>política de privacidad</link>."
|
||||
},
|
||||
"support": {
|
||||
"telegram": "Telegram",
|
||||
"email": "Correo electrónico"
|
||||
},
|
||||
"contact": {
|
||||
"email": "Tu correo electrónico",
|
||||
"topic": "Tema",
|
||||
"message": "¿Qué ocurre?",
|
||||
"send": "Enviar",
|
||||
"sent": "Enviado, te responderemos por correo",
|
||||
"needEmail": "Indica tu correo electrónico para que podamos responderte",
|
||||
"needMessage": "Cuéntanos qué ocurre"
|
||||
},
|
||||
"error": {
|
||||
"title": "Algo salió mal",
|
||||
"text": "Ya estamos al tanto del error. Intenta recargar la página dentro de un minuto.",
|
||||
"retry": "Recargar la página",
|
||||
"home": "Ir a la página de inicio"
|
||||
}
|
||||
}
|
||||
+120
-1
@@ -1 +1,120 @@
|
||||
{}
|
||||
{
|
||||
"brand": {
|
||||
"dedicated": {
|
||||
"name": "Veilix",
|
||||
"tagline": "Un VPN personnel avec une IP dédiée. En service depuis 2022."
|
||||
},
|
||||
"mass": {
|
||||
"name": "VeilixVPN",
|
||||
"tagline": "Un VPN que vous n’avez jamais à désactiver. En service depuis 2022."
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"main": "Navigation principale",
|
||||
"home": "Accueil",
|
||||
"pricing": "Tarifs",
|
||||
"locations": "Emplacements",
|
||||
"support": "Assistance",
|
||||
"logIn": "Se connecter",
|
||||
"getVpn": "Obtenir un VPN",
|
||||
"menu": "Menu"
|
||||
},
|
||||
"footer": {
|
||||
"service": "Service",
|
||||
"legal": "Informations légales",
|
||||
"terms": "Conditions d’utilisation",
|
||||
"privacy": "Politique de confidentialité",
|
||||
"refund": "Remboursements",
|
||||
"cookies": "Politique relative aux cookies",
|
||||
"copyright": "© {year} {brand}"
|
||||
},
|
||||
"switch": {
|
||||
"language": "Langue",
|
||||
"currency": "Devise",
|
||||
"theme": "Thème",
|
||||
"light": "Thème clair",
|
||||
"dark": "Thème sombre",
|
||||
"system": "Thème du système"
|
||||
},
|
||||
"cabinet": {
|
||||
"home": "Accueil",
|
||||
"keys": "Toutes les clés",
|
||||
"keysShort": "Clés",
|
||||
"key": "Clé",
|
||||
"buy": "Acheter une clé",
|
||||
"buyShort": "Acheter",
|
||||
"billing": "Facturation",
|
||||
"referral": "Programme de parrainage",
|
||||
"more": "Plus",
|
||||
"support": "Assistance",
|
||||
"login": "Connexion",
|
||||
"order": "Passer commande",
|
||||
"terms": "Conditions",
|
||||
"privacy": "Confidentialité"
|
||||
},
|
||||
"stub": {
|
||||
"text": "Cette section sera disponible lors des prochaines étapes."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page introuvable",
|
||||
"text": "La page a peut-être été déplacée, ou elle n’a jamais existé.",
|
||||
"home": "Aller à l’accueil"
|
||||
},
|
||||
"status": {
|
||||
"dedicated": {
|
||||
"available": "Serveurs disponibles",
|
||||
"on_demand": "Prêt sous 24 heures"
|
||||
},
|
||||
"mass": {
|
||||
"available": "Inclus",
|
||||
"soon": "Bientôt"
|
||||
},
|
||||
"withCount": "{label} · {count}"
|
||||
},
|
||||
"count": {
|
||||
"countries": "{count, plural, one {# pays} other {# pays}}"
|
||||
},
|
||||
"pricing": {
|
||||
"firstMonth": "le premier mois",
|
||||
"thenPerMonth": "puis {price} par mois",
|
||||
"then": "puis",
|
||||
"perMonth": "par mois",
|
||||
"perYear": "par an",
|
||||
"buy": "Acheter",
|
||||
"buyIn": "Acheter : {country}",
|
||||
"locations": "Emplacements",
|
||||
"plans": "Forfait",
|
||||
"close": "Fermer"
|
||||
},
|
||||
"lead": {
|
||||
"name": "Nom",
|
||||
"email": "E-mail",
|
||||
"telegram": "Telegram",
|
||||
"whatsapp": "WhatsApp",
|
||||
"task": "Votre besoin en quelques mots",
|
||||
"send": "Envoyer",
|
||||
"sent": "Demande envoyée",
|
||||
"needContact": "Indiquez un e-mail, Telegram ou WhatsApp",
|
||||
"badEmail": "Vérifiez l’adresse e-mail",
|
||||
"consent": "En envoyant ce formulaire, vous acceptez la <link>politique de confidentialité</link>."
|
||||
},
|
||||
"support": {
|
||||
"telegram": "Telegram",
|
||||
"email": "E-mail"
|
||||
},
|
||||
"contact": {
|
||||
"email": "Votre e-mail",
|
||||
"topic": "Sujet",
|
||||
"message": "Que s’est-il passé ?",
|
||||
"send": "Envoyer",
|
||||
"sent": "Message envoyé, nous vous répondrons par e-mail",
|
||||
"needEmail": "Indiquez votre e-mail pour recevoir notre réponse",
|
||||
"needMessage": "Décrivez ce qui s’est passé"
|
||||
},
|
||||
"error": {
|
||||
"title": "Un problème est survenu",
|
||||
"text": "Nous sommes déjà informés de l’erreur. Essayez de recharger la page dans une minute.",
|
||||
"retry": "Recharger la page",
|
||||
"home": "Aller à l’accueil"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"brand": {
|
||||
"dedicated": {
|
||||
"name": "Вейликс",
|
||||
"tagline": "Персональные VPN-серверы с индивидуальным IP. Работаем с 2022 года."
|
||||
"tagline": "Личный VPN с индивидуальным IP. Работаем с 2022 года."
|
||||
},
|
||||
"mass": {
|
||||
"name": "ВейликсВПН",
|
||||
@@ -59,5 +59,62 @@
|
||||
"title": "Страница не найдена",
|
||||
"text": "Возможно, она переехала или её никогда не было.",
|
||||
"home": "На главную"
|
||||
},
|
||||
"status": {
|
||||
"dedicated": {
|
||||
"available": "Серверы в наличии",
|
||||
"on_demand": "Установим за 24 часа"
|
||||
},
|
||||
"mass": {
|
||||
"available": "Включено",
|
||||
"soon": "Скоро"
|
||||
},
|
||||
"withCount": "{label} · {count}"
|
||||
},
|
||||
"count": {
|
||||
"countries": "{count, plural, one {# страна} few {# страны} many {# стран} other {# страны}}"
|
||||
},
|
||||
"pricing": {
|
||||
"firstMonth": "первый месяц",
|
||||
"thenPerMonth": "далее {price} в месяц",
|
||||
"then": "далее",
|
||||
"perMonth": "в месяц",
|
||||
"perYear": "в год",
|
||||
"buy": "Купить",
|
||||
"buyIn": "Купить: {country}",
|
||||
"locations": "Локации",
|
||||
"plans": "Тариф",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"lead": {
|
||||
"name": "Имя",
|
||||
"email": "Email",
|
||||
"telegram": "Telegram",
|
||||
"whatsapp": "WhatsApp",
|
||||
"task": "Коротко о задаче",
|
||||
"send": "Отправить",
|
||||
"sent": "Заявка отправлена",
|
||||
"needContact": "Укажите почту, Telegram или WhatsApp",
|
||||
"badEmail": "Проверьте адрес почты",
|
||||
"consent": "Отправляя форму, вы соглашаетесь с <link>политикой конфиденциальности</link>."
|
||||
},
|
||||
"support": {
|
||||
"telegram": "Telegram",
|
||||
"email": "Почта"
|
||||
},
|
||||
"contact": {
|
||||
"email": "Ваша почта",
|
||||
"topic": "Тема",
|
||||
"message": "Что произошло",
|
||||
"send": "Отправить",
|
||||
"sent": "Отправлено, ответим на почту",
|
||||
"needEmail": "Укажите почту, на неё придёт ответ",
|
||||
"needMessage": "Опишите, что произошло"
|
||||
},
|
||||
"error": {
|
||||
"title": "Что-то пошло не так",
|
||||
"text": "Мы уже знаем об ошибке. Попробуйте обновить страницу через минуту.",
|
||||
"retry": "Обновить страницу",
|
||||
"home": "На главную"
|
||||
}
|
||||
}
|
||||
+770
-12
@@ -71,6 +71,8 @@ export interface Config {
|
||||
locations: Location;
|
||||
countries: Country;
|
||||
tags: Tag;
|
||||
pages: Page;
|
||||
faq: Faq;
|
||||
media: Media;
|
||||
sites: Site;
|
||||
domains: Domain;
|
||||
@@ -88,6 +90,8 @@ export interface Config {
|
||||
locations: LocationsSelect<false> | LocationsSelect<true>;
|
||||
countries: CountriesSelect<false> | CountriesSelect<true>;
|
||||
tags: TagsSelect<false> | TagsSelect<true>;
|
||||
pages: PagesSelect<false> | PagesSelect<true>;
|
||||
faq: FaqSelect<false> | FaqSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
sites: SitesSelect<false> | SitesSelect<true>;
|
||||
domains: DomainsSelect<false> | DomainsSelect<true>;
|
||||
@@ -205,9 +209,45 @@ export interface Site {
|
||||
* Each domain of the site shows some of these languages.
|
||||
*/
|
||||
locales: ('en' | 'ru' | 'de' | 'fr' | 'es')[];
|
||||
supportEmail?: string | null;
|
||||
/**
|
||||
* Username for the t.me link, without @. Empty — the card is hidden.
|
||||
*/
|
||||
supportBot?: string | null;
|
||||
supportHours?: string | null;
|
||||
seo?: {
|
||||
/**
|
||||
* %s is replaced with the page title, e.g. “%s · Veilix”
|
||||
*/
|
||||
titleTemplate?: string | null;
|
||||
description?: string | null;
|
||||
/**
|
||||
* Empty — generated automatically in the brand colors
|
||||
*/
|
||||
ogImage?: (string | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
alt?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* Shared by both brands
|
||||
*
|
||||
@@ -266,22 +306,417 @@ export interface Location {
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
* via the `definition` "pages".
|
||||
*/
|
||||
export interface Media {
|
||||
export interface Page {
|
||||
id: string;
|
||||
alt?: string | null;
|
||||
site?: (string | null) | Site;
|
||||
/**
|
||||
* Page name in the title bar and in search results
|
||||
*/
|
||||
title: string;
|
||||
slug: 'home' | 'pricing' | 'locations' | 'support' | 'terms' | 'privacy' | 'refund' | 'cookies';
|
||||
layout?:
|
||||
| (
|
||||
| HeroBlock
|
||||
| AdvantagesBlock
|
||||
| PricingBlock
|
||||
| CrossLinkBlock
|
||||
| FaqBlock
|
||||
| PricingHeroBlock
|
||||
| LocationGridBlock
|
||||
| CardsBlock
|
||||
| MapHeroBlock
|
||||
| ContactsBlock
|
||||
| ContactFormBlock
|
||||
| RichTextBlock
|
||||
)[]
|
||||
| null;
|
||||
seo?: {
|
||||
/**
|
||||
* Empty — the page name is used
|
||||
*/
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
/**
|
||||
* Empty — generated automatically
|
||||
*/
|
||||
image?: (string | null) | Media;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HeroBlock".
|
||||
*/
|
||||
export interface HeroBlock {
|
||||
badges?:
|
||||
| {
|
||||
text: string;
|
||||
style: 'outline' | 'soft';
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
title: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
primary?: {
|
||||
label?: string | null;
|
||||
/**
|
||||
* #anchor, /path or https://…
|
||||
*/
|
||||
href?: string | null;
|
||||
};
|
||||
secondary?: {
|
||||
label?: string | null;
|
||||
/**
|
||||
* #anchor, /path or https://…
|
||||
*/
|
||||
href?: string | null;
|
||||
};
|
||||
plate?: {
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
items?:
|
||||
| {
|
||||
text: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'hero';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "AdvantagesBlock".
|
||||
*/
|
||||
export interface AdvantagesBlock {
|
||||
title?: string | null;
|
||||
main?: {
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
items?:
|
||||
| {
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
cards?:
|
||||
| {
|
||||
icon: 'devices' | 'ru' | 'protocols';
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
chips?:
|
||||
| {
|
||||
text: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
routes?: {
|
||||
title?: string | null;
|
||||
text?: string | null;
|
||||
schemes?:
|
||||
| {
|
||||
title: string;
|
||||
rows?:
|
||||
| {
|
||||
label: string;
|
||||
mode: 'direct' | 'tunnel';
|
||||
text: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
note?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'advantages';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "PricingBlock".
|
||||
*/
|
||||
export interface PricingBlock {
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
points?:
|
||||
| {
|
||||
text: string;
|
||||
sub?:
|
||||
| {
|
||||
text: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
/**
|
||||
* Veilix: above the locations; VeilixVPN: above the countries
|
||||
*/
|
||||
listTitle?: string | null;
|
||||
buyLabel: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
note?: string | null;
|
||||
lead?: {
|
||||
buttonLabel?: string | null;
|
||||
title?: string | null;
|
||||
};
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'pricing';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CrossLinkBlock".
|
||||
*/
|
||||
export interface CrossLinkBlock {
|
||||
title: string;
|
||||
text?: string | null;
|
||||
buttonLabel: string;
|
||||
target: 'home' | 'pricing';
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'crossLink';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "FaqBlock".
|
||||
*/
|
||||
export interface FaqBlock {
|
||||
title: string;
|
||||
/**
|
||||
* Questions of each placement; several sections are shown as tabs
|
||||
*/
|
||||
groups?:
|
||||
| {
|
||||
placement:
|
||||
'home' | 'pricing' | 'support_connection' | 'support_billing' | 'support_locations' | 'cabinet' | 'miniapp';
|
||||
label?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'faq';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "PricingHeroBlock".
|
||||
*/
|
||||
export interface PricingHeroBlock {
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
subline?: string | null;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
note?: string | null;
|
||||
includedTitle?: string | null;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
included?:
|
||||
| {
|
||||
text: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
buttonLabel?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'pricingHero';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "LocationGridBlock".
|
||||
*/
|
||||
export interface LocationGridBlock {
|
||||
title: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
paragraphs?:
|
||||
| {
|
||||
text: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'locationGrid';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CardsBlock".
|
||||
*/
|
||||
export interface CardsBlock {
|
||||
cards?:
|
||||
| {
|
||||
kind: 'info' | 'lead' | 'crossLink';
|
||||
title: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
buttonLabel?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'cards';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "MapHeroBlock".
|
||||
*/
|
||||
export interface MapHeroBlock {
|
||||
title: string;
|
||||
/**
|
||||
* Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.
|
||||
*/
|
||||
text?: string | null;
|
||||
hint?: string | null;
|
||||
scrollHint?: string | null;
|
||||
ruLegend?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'mapHero';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ContactsBlock".
|
||||
*/
|
||||
export interface ContactsBlock {
|
||||
title: string;
|
||||
telegramText?: string | null;
|
||||
emailText?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'contacts';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ContactFormBlock".
|
||||
*/
|
||||
export interface ContactFormBlock {
|
||||
title: string;
|
||||
text?: string | null;
|
||||
topics?:
|
||||
| {
|
||||
value: 'connection' | 'billing' | 'locations' | 'other';
|
||||
label: string;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'contactForm';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "RichTextBlock".
|
||||
*/
|
||||
export interface RichTextBlock {
|
||||
content: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'richText';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "faq".
|
||||
*/
|
||||
export interface Faq {
|
||||
id: string;
|
||||
site?: (string | null) | Site;
|
||||
question: string;
|
||||
answer: {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
direction: ('ltr' | 'rtl') | null;
|
||||
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
|
||||
indent: number;
|
||||
version: number;
|
||||
};
|
||||
[k: string]: unknown;
|
||||
};
|
||||
placements: (
|
||||
'home' | 'pricing' | 'support_connection' | 'support_billing' | 'support_locations' | 'cabinet' | 'miniapp'
|
||||
)[];
|
||||
active?: boolean | null;
|
||||
sort?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* A new mirror is a record here plus an nginx block and a certificate.
|
||||
@@ -455,6 +890,14 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'tags';
|
||||
value: string | Tag;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'pages';
|
||||
value: string | Page;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'faq';
|
||||
value: string | Faq;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
@@ -601,6 +1044,311 @@ export interface TagsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "pages_select".
|
||||
*/
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
site?: T;
|
||||
title?: T;
|
||||
slug?: T;
|
||||
layout?:
|
||||
| T
|
||||
| {
|
||||
hero?: T | HeroBlockSelect<T>;
|
||||
advantages?: T | AdvantagesBlockSelect<T>;
|
||||
pricing?: T | PricingBlockSelect<T>;
|
||||
crossLink?: T | CrossLinkBlockSelect<T>;
|
||||
faq?: T | FaqBlockSelect<T>;
|
||||
pricingHero?: T | PricingHeroBlockSelect<T>;
|
||||
locationGrid?: T | LocationGridBlockSelect<T>;
|
||||
cards?: T | CardsBlockSelect<T>;
|
||||
mapHero?: T | MapHeroBlockSelect<T>;
|
||||
contacts?: T | ContactsBlockSelect<T>;
|
||||
contactForm?: T | ContactFormBlockSelect<T>;
|
||||
richText?: T | RichTextBlockSelect<T>;
|
||||
};
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
description?: T;
|
||||
image?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HeroBlock_select".
|
||||
*/
|
||||
export interface HeroBlockSelect<T extends boolean = true> {
|
||||
badges?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
style?: T;
|
||||
id?: T;
|
||||
};
|
||||
title?: T;
|
||||
text?: T;
|
||||
primary?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
href?: T;
|
||||
};
|
||||
secondary?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
href?: T;
|
||||
};
|
||||
plate?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "AdvantagesBlock_select".
|
||||
*/
|
||||
export interface AdvantagesBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
main?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
text?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
cards?:
|
||||
| T
|
||||
| {
|
||||
icon?: T;
|
||||
title?: T;
|
||||
text?: T;
|
||||
chips?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
routes?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
text?: T;
|
||||
schemes?:
|
||||
| T
|
||||
| {
|
||||
title?: T;
|
||||
rows?:
|
||||
| T
|
||||
| {
|
||||
label?: T;
|
||||
mode?: T;
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
note?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "PricingBlock_select".
|
||||
*/
|
||||
export interface PricingBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
text?: T;
|
||||
points?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
sub?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
};
|
||||
listTitle?: T;
|
||||
buyLabel?: T;
|
||||
note?: T;
|
||||
lead?:
|
||||
| T
|
||||
| {
|
||||
buttonLabel?: T;
|
||||
title?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CrossLinkBlock_select".
|
||||
*/
|
||||
export interface CrossLinkBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
text?: T;
|
||||
buttonLabel?: T;
|
||||
target?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "FaqBlock_select".
|
||||
*/
|
||||
export interface FaqBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
groups?:
|
||||
| T
|
||||
| {
|
||||
placement?: T;
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "PricingHeroBlock_select".
|
||||
*/
|
||||
export interface PricingHeroBlockSelect<T extends boolean = true> {
|
||||
subline?: T;
|
||||
note?: T;
|
||||
includedTitle?: T;
|
||||
included?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
buttonLabel?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "LocationGridBlock_select".
|
||||
*/
|
||||
export interface LocationGridBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
paragraphs?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CardsBlock_select".
|
||||
*/
|
||||
export interface CardsBlockSelect<T extends boolean = true> {
|
||||
cards?:
|
||||
| T
|
||||
| {
|
||||
kind?: T;
|
||||
title?: T;
|
||||
text?: T;
|
||||
buttonLabel?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "MapHeroBlock_select".
|
||||
*/
|
||||
export interface MapHeroBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
text?: T;
|
||||
hint?: T;
|
||||
scrollHint?: T;
|
||||
ruLegend?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ContactsBlock_select".
|
||||
*/
|
||||
export interface ContactsBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
telegramText?: T;
|
||||
emailText?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ContactFormBlock_select".
|
||||
*/
|
||||
export interface ContactFormBlockSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
text?: T;
|
||||
topics?:
|
||||
| T
|
||||
| {
|
||||
value?: T;
|
||||
label?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "RichTextBlock_select".
|
||||
*/
|
||||
export interface RichTextBlockSelect<T extends boolean = true> {
|
||||
content?: T;
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "faq_select".
|
||||
*/
|
||||
export interface FaqSelect<T extends boolean = true> {
|
||||
site?: T;
|
||||
question?: T;
|
||||
answer?: T;
|
||||
placements?: T;
|
||||
active?: T;
|
||||
sort?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
@@ -628,6 +1376,16 @@ export interface SitesSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
orderPrefix?: T;
|
||||
locales?: T;
|
||||
supportEmail?: T;
|
||||
supportBot?: T;
|
||||
supportHours?: T;
|
||||
seo?:
|
||||
| T
|
||||
| {
|
||||
titleTemplate?: T;
|
||||
description?: T;
|
||||
ogImage?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ import { Admins } from './payload/collections/Admins'
|
||||
import { Countries } from './payload/collections/Countries'
|
||||
import { Currencies } from './payload/collections/Currencies'
|
||||
import { Domains } from './payload/collections/Domains'
|
||||
import { Faq } from './payload/collections/Faq'
|
||||
import { Locations } from './payload/collections/Locations'
|
||||
import { Media } from './payload/collections/Media'
|
||||
import { Pages } from './payload/collections/Pages'
|
||||
import { PaymentSystems } from './payload/collections/PaymentSystems'
|
||||
import { Sites } from './payload/collections/Sites'
|
||||
import { Tags } from './payload/collections/Tags'
|
||||
@@ -38,6 +40,8 @@ export default buildConfig({
|
||||
Locations,
|
||||
Countries,
|
||||
Tags,
|
||||
Pages,
|
||||
Faq,
|
||||
Media,
|
||||
Sites,
|
||||
Domains,
|
||||
@@ -76,7 +80,9 @@ export default buildConfig({
|
||||
},
|
||||
collections: {
|
||||
domains: {},
|
||||
faq: {},
|
||||
locations: {},
|
||||
pages: {},
|
||||
tariffs: {},
|
||||
},
|
||||
userHasAccessToAllTenants: (user) => hasRole(user, 'owner', 'admin'),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { ArrayField, Field, SelectField, TextareaField, TextField } from 'payload'
|
||||
|
||||
import { label } from '../i18n'
|
||||
|
||||
type Label = { en: string; ru: string }
|
||||
|
||||
/** Подстановки в текстах блоков: значения берутся из тарифов и локаций сайта */
|
||||
export const PLACEHOLDERS_HINT = label(
|
||||
'Placeholders: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.',
|
||||
'Подстановки: {priceFirst}, {priceRenewal}, {priceMonth}, {priceYear}, {pricePerMonth}, {savings}, {countries}, {devices}.',
|
||||
)
|
||||
|
||||
export const text = (
|
||||
name: string,
|
||||
fieldLabel: Label,
|
||||
options: Partial<Pick<TextField, 'required' | 'admin'>> = {},
|
||||
): TextField => ({ name, type: 'text', localized: true, label: fieldLabel, ...options })
|
||||
|
||||
export const textarea = (
|
||||
name: string,
|
||||
fieldLabel: Label,
|
||||
options: Partial<Pick<TextareaField, 'required' | 'admin'>> = {},
|
||||
): TextareaField => ({ name, type: 'textarea', localized: true, label: fieldLabel, ...options })
|
||||
|
||||
/** Список строк: пункты, чипы, абзацы */
|
||||
export const lines = (
|
||||
name: string,
|
||||
fieldLabel: Label,
|
||||
options: Partial<Pick<ArrayField, 'minRows' | 'maxRows' | 'admin'>> = {},
|
||||
extra: Field[] = [],
|
||||
): ArrayField => ({
|
||||
name,
|
||||
type: 'array',
|
||||
label: fieldLabel,
|
||||
labels: { singular: label('Line', 'Строка'), plural: label('Lines', 'Строки') },
|
||||
...options,
|
||||
fields: [text('text', label('Text', 'Текст'), { required: true }), ...extra],
|
||||
})
|
||||
|
||||
/** Ссылка кнопки: «#якорь», «/путь» (язык добавится сам) или полный адрес */
|
||||
export const button = (name: string, fieldLabel: Label): Field => ({
|
||||
name,
|
||||
type: 'group',
|
||||
label: fieldLabel,
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
text('label', label('Label', 'Подпись')),
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
label: label('Link', 'Ссылка'),
|
||||
admin: {
|
||||
description: label('#anchor, /path or https://…', '#якорь, /путь или https://…'),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const select = (
|
||||
name: string,
|
||||
fieldLabel: Label,
|
||||
options: { value: string; label: Label }[],
|
||||
extra: Partial<Pick<SelectField, 'required' | 'defaultValue' | 'admin'>> = {},
|
||||
): SelectField => ({ name, type: 'select', label: fieldLabel, options, ...extra })
|
||||
@@ -0,0 +1,418 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
import { FAQ_PLACEMENTS, type FaqPlacement } from '@/core/pages'
|
||||
|
||||
import { label } from '../i18n'
|
||||
import { button, lines, PLACEHOLDERS_HINT, select, text, textarea } from './fields'
|
||||
|
||||
const withHint = { admin: { description: PLACEHOLDERS_HINT } }
|
||||
|
||||
/** Первый экран главной: плашки, H1, кнопки, карточка с глобусом */
|
||||
export const HeroBlock: Block = {
|
||||
slug: 'hero',
|
||||
interfaceName: 'HeroBlock',
|
||||
labels: { singular: label('Hero', 'Первый экран'), plural: label('Heroes', 'Первые экраны') },
|
||||
fields: [
|
||||
{
|
||||
name: 'badges',
|
||||
type: 'array',
|
||||
maxRows: 3,
|
||||
label: label('Badges', 'Плашки'),
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
text('text', label('Text', 'Текст'), { required: true }),
|
||||
select(
|
||||
'style',
|
||||
label('Style', 'Вид'),
|
||||
[
|
||||
{ value: 'outline', label: label('Outline with dot', 'Рамка с точкой') },
|
||||
{ value: 'soft', label: label('Accent fill', 'Заливка акцентом') },
|
||||
],
|
||||
{ required: true, defaultValue: 'outline' },
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
text('title', label('Title (H1)', 'Заголовок (H1)'), { required: true }),
|
||||
textarea('text', label('Text', 'Текст'), withHint),
|
||||
button('primary', label('Main button', 'Основная кнопка')),
|
||||
button('secondary', label('Second button', 'Вторая кнопка')),
|
||||
{
|
||||
name: 'plate',
|
||||
type: 'group',
|
||||
label: label('Card under the globe', 'Плашка под глобусом'),
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), withHint),
|
||||
lines('items', label('Items', 'Пункты'), { maxRows: 4, ...withHint }),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const ICONS = [
|
||||
{ value: 'devices', label: label('Devices', 'Устройства') },
|
||||
{ value: 'ru', label: label('RU address', 'RU-адрес') },
|
||||
{ value: 'protocols', label: label('Protocols', 'Протоколы') },
|
||||
]
|
||||
|
||||
/** «Что это даёт на практике»: большая карточка, три малые и схема маршрутов */
|
||||
export const AdvantagesBlock: Block = {
|
||||
slug: 'advantages',
|
||||
interfaceName: 'AdvantagesBlock',
|
||||
labels: {
|
||||
singular: label('Advantages', 'Преимущества'),
|
||||
plural: label('Advantages', 'Преимущества'),
|
||||
},
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок')),
|
||||
{
|
||||
name: 'main',
|
||||
type: 'group',
|
||||
label: label('Large card', 'Большая карточка'),
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), withHint),
|
||||
textarea('text', label('Text', 'Текст'), withHint),
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
maxRows: 4,
|
||||
label: label('Points', 'Пункты'),
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), { required: true, ...withHint }),
|
||||
textarea('text', label('Text', 'Текст'), withHint),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cards',
|
||||
type: 'array',
|
||||
maxRows: 3,
|
||||
label: label('Small cards', 'Малые карточки'),
|
||||
fields: [
|
||||
select('icon', label('Icon', 'Значок'), ICONS, { required: true, defaultValue: 'devices' }),
|
||||
text('title', label('Title', 'Заголовок'), { required: true, ...withHint }),
|
||||
textarea('text', label('Text', 'Текст'), withHint),
|
||||
lines('chips', label('Chips', 'Чипы'), { maxRows: 4 }),
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'routes',
|
||||
type: 'group',
|
||||
label: label('Routes card', 'Карточка маршрутов'),
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок')),
|
||||
textarea('text', label('Text', 'Текст')),
|
||||
{
|
||||
name: 'schemes',
|
||||
type: 'array',
|
||||
maxRows: 2,
|
||||
label: label('Schemes', 'Схемы'),
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), { required: true }),
|
||||
{
|
||||
name: 'rows',
|
||||
type: 'array',
|
||||
maxRows: 3,
|
||||
label: label('Routes', 'Маршруты'),
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
text('label', label('Where', 'Куда'), { required: true }),
|
||||
select(
|
||||
'mode',
|
||||
label('Line', 'Линия'),
|
||||
[
|
||||
{ value: 'direct', label: label('Direct', 'Напрямую') },
|
||||
{ value: 'tunnel', label: label('Tunnel', 'Туннель') },
|
||||
],
|
||||
{ required: true, defaultValue: 'direct' },
|
||||
),
|
||||
text('text', label('How', 'Как'), { required: true }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
textarea('note', label('Note', 'Сноска')),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Блок цен на главной: у dedicated — выбор локации, у mass — планы «Месяц» и «Год» */
|
||||
export const PricingBlock: Block = {
|
||||
slug: 'pricing',
|
||||
interfaceName: 'PricingBlock',
|
||||
labels: { singular: label('Pricing', 'Цены'), plural: label('Pricing', 'Цены') },
|
||||
fields: [
|
||||
text('title', label('Title (VeilixVPN)', 'Заголовок (ВейликсВПН)'), withHint),
|
||||
textarea('text', label('Text (VeilixVPN)', 'Текст (ВейликсВПН)'), withHint),
|
||||
lines('points', label('Points', 'Пункты'), { maxRows: 6, ...withHint }, [
|
||||
lines('sub', label('Sub-points', 'Подпункты'), { maxRows: 4 }),
|
||||
]),
|
||||
text('listTitle', label('List title', 'Заголовок списка'), {
|
||||
admin: {
|
||||
description: label(
|
||||
'Veilix: above the locations; VeilixVPN: above the countries',
|
||||
'Вейликс — над локациями, ВейликсВПН — над странами',
|
||||
),
|
||||
},
|
||||
}),
|
||||
text('buyLabel', label('Buy button', 'Кнопка покупки'), { required: true }),
|
||||
textarea('note', label('Note under the button', 'Сноска под кнопкой'), withHint),
|
||||
{
|
||||
name: 'lead',
|
||||
type: 'group',
|
||||
label: label('Custom terms request (Veilix)', 'Заявка на особые условия (Вейликс)'),
|
||||
fields: [
|
||||
text('buttonLabel', label('Button', 'Кнопка')),
|
||||
text('title', label('Form title', 'Заголовок формы')),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Ссылка на другой бренд: «Нужен адрес, закреплённый только за вами» */
|
||||
export const CrossLinkBlock: Block = {
|
||||
slug: 'crossLink',
|
||||
interfaceName: 'CrossLinkBlock',
|
||||
labels: {
|
||||
singular: label('Other brand', 'Другой бренд'),
|
||||
plural: label('Other brand', 'Другой бренд'),
|
||||
},
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), { required: true }),
|
||||
textarea('text', label('Text', 'Текст')),
|
||||
text('buttonLabel', label('Button', 'Кнопка'), { required: true }),
|
||||
select(
|
||||
'target',
|
||||
label('Page of the other brand', 'Страница другого бренда'),
|
||||
[
|
||||
{ value: 'home', label: label('Home', 'Главная') },
|
||||
{ value: 'pricing', label: label('Pricing', 'Цены') },
|
||||
],
|
||||
{ required: true, defaultValue: 'home' },
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
const PLACEMENT_LABELS: Record<FaqPlacement, { en: string; ru: string }> = {
|
||||
home: label('Home', 'Главная'),
|
||||
pricing: label('Pricing', 'Цены'),
|
||||
support_connection: label('Support: connection', 'Поддержка: подключение'),
|
||||
support_billing: label('Support: billing', 'Поддержка: оплата'),
|
||||
support_locations: label('Support: locations', 'Поддержка: локации'),
|
||||
cabinet: label('Cabinet', 'Кабинет'),
|
||||
miniapp: label('MiniApp', 'MiniApp'),
|
||||
}
|
||||
|
||||
export const FAQ_PLACEMENT_OPTIONS = FAQ_PLACEMENTS.map((value) => ({
|
||||
value,
|
||||
label: PLACEMENT_LABELS[value],
|
||||
}))
|
||||
|
||||
/** Вопросы и ответы: одно место показа — список, несколько — вкладки */
|
||||
export const FaqBlock: Block = {
|
||||
slug: 'faq',
|
||||
interfaceName: 'FaqBlock',
|
||||
labels: { singular: label('FAQ', 'Вопросы'), plural: label('FAQ', 'Вопросы') },
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), { required: true }),
|
||||
{
|
||||
name: 'groups',
|
||||
type: 'array',
|
||||
minRows: 1,
|
||||
maxRows: 4,
|
||||
label: label('Sections', 'Разделы'),
|
||||
admin: {
|
||||
description: label(
|
||||
'Questions of each placement; several sections are shown as tabs',
|
||||
'Вопросы из места показа; несколько разделов показываются вкладками',
|
||||
),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
select('placement', label('Placement', 'Место показа'), FAQ_PLACEMENT_OPTIONS, {
|
||||
required: true,
|
||||
}),
|
||||
text('label', label('Tab title', 'Название вкладки')),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Верх страницы цен: крупная цена и «Что входит» */
|
||||
export const PricingHeroBlock: Block = {
|
||||
slug: 'pricingHero',
|
||||
interfaceName: 'PricingHeroBlock',
|
||||
labels: {
|
||||
singular: label('Price card', 'Карточка цены'),
|
||||
plural: label('Price cards', 'Карточки цены'),
|
||||
},
|
||||
fields: [
|
||||
text('subline', label('Line under the price', 'Строка под ценой'), withHint),
|
||||
textarea('note', label('Note', 'Пояснение'), withHint),
|
||||
text('includedTitle', label('List title', 'Заголовок списка')),
|
||||
lines('included', label('What is included', 'Что входит'), { maxRows: 6, ...withHint }),
|
||||
text('buttonLabel', label('Button (VeilixVPN)', 'Кнопка (ВейликсВПН)')),
|
||||
],
|
||||
}
|
||||
|
||||
/** Сетка локаций: у dedicated с ценами и «Купить», у mass — со статусами */
|
||||
export const LocationGridBlock: Block = {
|
||||
slug: 'locationGrid',
|
||||
interfaceName: 'LocationGridBlock',
|
||||
labels: {
|
||||
singular: label('Locations grid', 'Сетка локаций'),
|
||||
plural: label('Locations grids', 'Сетки локаций'),
|
||||
},
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), { required: true }),
|
||||
lines('paragraphs', label('Paragraphs', 'Абзацы'), { maxRows: 3, ...withHint }),
|
||||
],
|
||||
}
|
||||
|
||||
/** Две карточки внизу страниц цен и локаций */
|
||||
export const CardsBlock: Block = {
|
||||
slug: 'cards',
|
||||
interfaceName: 'CardsBlock',
|
||||
labels: { singular: label('Cards', 'Карточки'), plural: label('Cards', 'Карточки') },
|
||||
fields: [
|
||||
{
|
||||
name: 'cards',
|
||||
type: 'array',
|
||||
minRows: 1,
|
||||
maxRows: 2,
|
||||
label: label('Cards', 'Карточки'),
|
||||
fields: [
|
||||
select(
|
||||
'kind',
|
||||
label('Kind', 'Вид'),
|
||||
[
|
||||
{ value: 'info', label: label('Text', 'Текст') },
|
||||
{ value: 'lead', label: label('Custom terms request', 'Заявка на особые условия') },
|
||||
{
|
||||
value: 'crossLink',
|
||||
label: label('Link to the other brand', 'Ссылка на другой бренд'),
|
||||
},
|
||||
],
|
||||
{ required: true, defaultValue: 'info' },
|
||||
),
|
||||
text('title', label('Title', 'Заголовок'), { required: true }),
|
||||
textarea('text', label('Text', 'Текст'), withHint),
|
||||
text('buttonLabel', label('Button', 'Кнопка')),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Верх страницы локаций: H1, карта, легенда */
|
||||
export const MapHeroBlock: Block = {
|
||||
slug: 'mapHero',
|
||||
interfaceName: 'MapHeroBlock',
|
||||
labels: { singular: label('Map', 'Карта'), plural: label('Maps', 'Карты') },
|
||||
fields: [
|
||||
text('title', label('Title (H1)', 'Заголовок (H1)'), { required: true }),
|
||||
textarea('text', label('Text', 'Текст'), withHint),
|
||||
text('hint', label('Hint under the map', 'Подсказка под картой')),
|
||||
text('scrollHint', label('Hint on narrow screens', 'Подсказка на узком экране')),
|
||||
text('ruLegend', label('RU address in the legend', 'RU-адрес в легенде')),
|
||||
],
|
||||
}
|
||||
|
||||
/** Верх страницы поддержки: H1, Telegram и почта из настроек сайта, часы работы */
|
||||
export const ContactsBlock: Block = {
|
||||
slug: 'contacts',
|
||||
interfaceName: 'ContactsBlock',
|
||||
labels: { singular: label('Contacts', 'Контакты'), plural: label('Contacts', 'Контакты') },
|
||||
fields: [
|
||||
text('title', label('Title (H1)', 'Заголовок (H1)'), { required: true }),
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
textarea('telegramText', label('Telegram card', 'Карточка Telegram')),
|
||||
textarea('emailText', label('Email card', 'Карточка почты')),
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Форма «Написать нам» */
|
||||
export const ContactFormBlock: Block = {
|
||||
slug: 'contactForm',
|
||||
interfaceName: 'ContactFormBlock',
|
||||
labels: {
|
||||
singular: label('Contact form', 'Форма связи'),
|
||||
plural: label('Contact forms', 'Формы связи'),
|
||||
},
|
||||
fields: [
|
||||
text('title', label('Title', 'Заголовок'), { required: true }),
|
||||
textarea('text', label('Text', 'Текст')),
|
||||
{
|
||||
name: 'topics',
|
||||
type: 'array',
|
||||
maxRows: 6,
|
||||
label: label('Topics', 'Темы'),
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
select(
|
||||
'value',
|
||||
label('Topic', 'Тема'),
|
||||
[
|
||||
{ value: 'connection', label: label('Connection', 'Подключение') },
|
||||
{ value: 'billing', label: label('Billing', 'Оплата') },
|
||||
{ value: 'locations', label: label('Locations', 'Локации') },
|
||||
{ value: 'other', label: label('Other', 'Другое') },
|
||||
],
|
||||
{ required: true },
|
||||
),
|
||||
text('label', label('Label', 'Подпись'), { required: true }),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** Текст: юридические страницы */
|
||||
export const RichTextBlock: Block = {
|
||||
slug: 'richText',
|
||||
interfaceName: 'RichTextBlock',
|
||||
labels: { singular: label('Text', 'Текст'), plural: label('Texts', 'Тексты') },
|
||||
fields: [
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
required: true,
|
||||
label: label('Text', 'Текст'),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const PAGE_BLOCKS: Block[] = [
|
||||
HeroBlock,
|
||||
AdvantagesBlock,
|
||||
PricingBlock,
|
||||
CrossLinkBlock,
|
||||
FaqBlock,
|
||||
PricingHeroBlock,
|
||||
LocationGridBlock,
|
||||
CardsBlock,
|
||||
MapHeroBlock,
|
||||
ContactsBlock,
|
||||
ContactFormBlock,
|
||||
RichTextBlock,
|
||||
]
|
||||
@@ -3,6 +3,7 @@ import type { CollectionConfig, FieldHook, TextFieldSingleValidation } from 'pay
|
||||
import { anyRole, managers } from '../access/roles'
|
||||
import { sortField } from '../fields/common'
|
||||
import { groups, label, message } from '../i18n'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
|
||||
const upperCase: FieldHook = ({ value }) =>
|
||||
typeof value === 'string' ? value.trim().toUpperCase() : value
|
||||
@@ -28,6 +29,7 @@ export const Countries: CollectionConfig = {
|
||||
description: label('Shared by both brands', 'Общий справочник обоих брендов'),
|
||||
},
|
||||
defaultSort: 'sort',
|
||||
hooks: revalidateSiteHooks,
|
||||
access: {
|
||||
read: anyRole,
|
||||
create: managers,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { APIError } from 'payload'
|
||||
import { anyRole, managers } from '../access/roles'
|
||||
import { integerField, sortField } from '../fields/common'
|
||||
import { groups, label, message } from '../i18n'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
|
||||
const upperCase: FieldHook = ({ value }) =>
|
||||
typeof value === 'string' ? value.trim().toUpperCase() : value
|
||||
@@ -81,6 +82,7 @@ export const Currencies: CollectionConfig = {
|
||||
},
|
||||
hooks: {
|
||||
beforeDelete: [keepUsedCurrency],
|
||||
...revalidateSiteHooks,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ import { currenciesField, currencyField } from '../fields/currency'
|
||||
import { localeOptions } from '../fields/locales'
|
||||
import { groups, label, message } from '../i18n'
|
||||
import { relationId } from '../utils'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
|
||||
const HOSTNAME = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/
|
||||
|
||||
@@ -152,6 +153,7 @@ export const Domains: CollectionConfig = {
|
||||
},
|
||||
hooks: {
|
||||
beforeChange: [validateDomain],
|
||||
...revalidateSiteHooks,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { anyRole, contentEditors } from '../access/roles'
|
||||
import { FAQ_PLACEMENT_OPTIONS } from '../blocks'
|
||||
import { sortField } from '../fields/common'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
import { groups, label } from '../i18n'
|
||||
|
||||
/** Вопросы и ответы. Один вопрос показывается в нескольких местах */
|
||||
export const Faq: CollectionConfig = {
|
||||
slug: 'faq',
|
||||
labels: {
|
||||
singular: label('Question', 'Вопрос'),
|
||||
plural: label('FAQ', 'Вопросы и ответы'),
|
||||
},
|
||||
admin: {
|
||||
group: groups.content,
|
||||
useAsTitle: 'question',
|
||||
defaultColumns: ['question', 'placements', 'active', 'sort'],
|
||||
},
|
||||
defaultSort: 'sort',
|
||||
access: {
|
||||
read: anyRole,
|
||||
create: contentEditors,
|
||||
update: contentEditors,
|
||||
delete: contentEditors,
|
||||
},
|
||||
hooks: revalidateSiteHooks,
|
||||
fields: [
|
||||
{
|
||||
name: 'question',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: label('Question', 'Вопрос'),
|
||||
},
|
||||
{
|
||||
name: 'answer',
|
||||
type: 'richText',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: label('Answer', 'Ответ'),
|
||||
},
|
||||
{
|
||||
name: 'placements',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
required: true,
|
||||
options: FAQ_PLACEMENT_OPTIONS,
|
||||
label: label('Where to show', 'Где показывать'),
|
||||
admin: { position: 'sidebar' },
|
||||
},
|
||||
{
|
||||
name: 'active',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
label: label('Shown', 'Показывать'),
|
||||
admin: { position: 'sidebar' },
|
||||
},
|
||||
sortField,
|
||||
],
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { integerField, sortField } from '../fields/common'
|
||||
import { uniqueInSite } from '../hooks/uniqueInSite'
|
||||
import { groups, label, message } from '../i18n'
|
||||
import { relationId } from '../utils'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
|
||||
export const LOCATION_STATUSES = ['available', 'on_demand', 'soon'] as const
|
||||
|
||||
@@ -79,6 +80,7 @@ export const Locations: CollectionConfig = {
|
||||
ru: 'У сайта уже есть локация в этой стране',
|
||||
}),
|
||||
],
|
||||
...revalidateSiteHooks,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
import { PAGE_SLUGS } from '@/core/pages'
|
||||
|
||||
import { anyRole, contentEditors } from '../access/roles'
|
||||
import { PAGE_BLOCKS } from '../blocks'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
import { uniqueInSite } from '../hooks/uniqueInSite'
|
||||
import { groups, label } from '../i18n'
|
||||
|
||||
const SLUG_LABELS: Record<(typeof PAGE_SLUGS)[number], { en: string; ru: string }> = {
|
||||
home: label('Home', 'Главная'),
|
||||
pricing: label('Pricing', 'Цены'),
|
||||
locations: label('Locations', 'Локации'),
|
||||
support: label('Support', 'Поддержка'),
|
||||
terms: label('Terms of use', 'Условия использования'),
|
||||
privacy: label('Privacy policy', 'Политика конфиденциальности'),
|
||||
refund: label('Refund policy', 'Возврат средств'),
|
||||
cookies: label('Cookie policy', 'Политика cookies'),
|
||||
}
|
||||
|
||||
/** Страницы сайта из блоков (D-029). Адреса страниц заданы в коде, поэтому slug — из списка */
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
labels: {
|
||||
singular: label('Page', 'Страница'),
|
||||
plural: label('Pages', 'Страницы'),
|
||||
},
|
||||
admin: {
|
||||
group: groups.content,
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', '_status', 'updatedAt'],
|
||||
},
|
||||
access: {
|
||||
read: anyRole,
|
||||
create: contentEditors,
|
||||
update: contentEditors,
|
||||
delete: contentEditors,
|
||||
},
|
||||
versions: { drafts: true, maxPerDoc: 30 },
|
||||
indexes: [{ fields: ['site', 'slug'], unique: true }],
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
uniqueInSite('slug', {
|
||||
en: 'The site already has this page',
|
||||
ru: 'У сайта уже есть эта страница',
|
||||
}),
|
||||
],
|
||||
...revalidateSiteHooks,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: label('Title', 'Название'),
|
||||
admin: {
|
||||
description: label(
|
||||
'Page name in the title bar and in search results',
|
||||
'Название страницы во вкладке браузера и в поиске',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: PAGE_SLUGS.map((value) => ({ value, label: SLUG_LABELS[value] })),
|
||||
label: label('Page', 'Страница'),
|
||||
admin: { position: 'sidebar' },
|
||||
},
|
||||
{
|
||||
name: 'layout',
|
||||
type: 'blocks',
|
||||
blocks: PAGE_BLOCKS,
|
||||
label: label('Sections', 'Секции'),
|
||||
labels: { singular: label('Section', 'Секция'), plural: label('Sections', 'Секции') },
|
||||
},
|
||||
{
|
||||
name: 'seo',
|
||||
type: 'group',
|
||||
label: label('Search and social networks', 'Поиск и соцсети'),
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
label: label('Title', 'Заголовок'),
|
||||
admin: {
|
||||
description: label(
|
||||
'Empty — the page name is used',
|
||||
'Пусто — используется название страницы',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
localized: true,
|
||||
label: label('Description', 'Описание'),
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label: label('Social image', 'Картинка для соцсетей'),
|
||||
admin: {
|
||||
description: label(
|
||||
'Empty — generated automatically',
|
||||
'Пусто — создаётся автоматически',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { DEFAULT_LOCALE, LOCALES } from '@/i18n/locales'
|
||||
|
||||
import { anyRole, managers, owners } from '../access/roles'
|
||||
import { localeOptions } from '../fields/locales'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
import { groups, label, message } from '../i18n'
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,15 @@ const validateLocales: SelectFieldManyValidation = async (value, args) => {
|
||||
return true
|
||||
}
|
||||
|
||||
/** Имя для ссылки t.me/…: латиница, цифры и подчёркивание, без @ */
|
||||
const validateTelegram: TextFieldSingleValidation = (value, { req }) =>
|
||||
!value || (typeof value === 'string' && /^[A-Za-z][A-Za-z0-9_]{4,31}$/.test(value))
|
||||
? true
|
||||
: message(req, {
|
||||
en: 'Telegram username without @, e.g. veilix_support',
|
||||
ru: 'Имя в Telegram без @, например veilix_support',
|
||||
})
|
||||
|
||||
const validateOrderPrefix: TextFieldSingleValidation = (value, { req }) =>
|
||||
typeof value === 'string' && /^[A-Z]{2,4}$/.test(value)
|
||||
? true
|
||||
@@ -74,50 +84,131 @@ export const Sites: CollectionConfig = {
|
||||
// вместе с сайтом плагин удаляет все его документы
|
||||
delete: owners,
|
||||
},
|
||||
hooks: revalidateSiteHooks,
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'select',
|
||||
required: true,
|
||||
unique: true,
|
||||
options: [
|
||||
{ value: 'dedicated', label: label('Veilix (dedicated)', 'Вейликс (dedicated)') },
|
||||
{ value: 'mass', label: label('VeilixVPN (mass)', 'ВейликсВПН (mass)') },
|
||||
type: 'tabs',
|
||||
tabs: [
|
||||
{
|
||||
label: label('Main', 'Основное'),
|
||||
fields: [
|
||||
{
|
||||
name: 'code',
|
||||
type: 'select',
|
||||
required: true,
|
||||
unique: true,
|
||||
options: [
|
||||
{ value: 'dedicated', label: label('Veilix (dedicated)', 'Вейликс (dedicated)') },
|
||||
{ value: 'mass', label: label('VeilixVPN (mass)', 'ВейликсВПН (mass)') },
|
||||
],
|
||||
label: label('Brand', 'Бренд'),
|
||||
// по коду выбираются тема и страницы бренда, после создания не меняется
|
||||
access: { update: () => false },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: label('Name', 'Название'),
|
||||
},
|
||||
{
|
||||
name: 'orderPrefix',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
label: label('Order number prefix', 'Префикс номеров'),
|
||||
validate: validateOrderPrefix,
|
||||
},
|
||||
{
|
||||
name: 'locales',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
required: true,
|
||||
options: localeOptions,
|
||||
defaultValue: [...LOCALES],
|
||||
label: label('Content languages', 'Языки контента'),
|
||||
validate: validateLocales,
|
||||
admin: {
|
||||
description: label(
|
||||
'Each domain of the site shows some of these languages.',
|
||||
'Каждый домен сайта показывает часть этих языков.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: label('Support', 'Поддержка'),
|
||||
fields: [
|
||||
{
|
||||
name: 'supportEmail',
|
||||
type: 'email',
|
||||
label: label('Support email', 'Почта поддержки'),
|
||||
},
|
||||
{
|
||||
name: 'supportBot',
|
||||
type: 'text',
|
||||
label: label('Support Telegram', 'Telegram поддержки'),
|
||||
validate: validateTelegram,
|
||||
admin: {
|
||||
description: label(
|
||||
'Username for the t.me link, without @. Empty — the card is hidden.',
|
||||
'Имя для ссылки t.me, без @. Пусто — карточка не показывается.',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'supportHours',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
label: label('Support hours', 'Часы работы поддержки'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: label('SEO', 'SEO'),
|
||||
fields: [
|
||||
{
|
||||
name: 'seo',
|
||||
type: 'group',
|
||||
label: false,
|
||||
fields: [
|
||||
{
|
||||
name: 'titleTemplate',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
label: label('Title template', 'Шаблон заголовка'),
|
||||
admin: {
|
||||
description: label(
|
||||
'%s is replaced with the page title, e.g. “%s · Veilix”',
|
||||
'%s заменяется названием страницы, например «%s · Вейликс»',
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
localized: true,
|
||||
label: label('Default description', 'Описание по умолчанию'),
|
||||
},
|
||||
{
|
||||
name: 'ogImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label: label('Social image', 'Картинка для соцсетей'),
|
||||
admin: {
|
||||
description: label(
|
||||
'Empty — generated automatically in the brand colors',
|
||||
'Пусто — создаётся автоматически в цветах бренда',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
label: label('Brand', 'Бренд'),
|
||||
// по коду выбираются тема и страницы бренда, после создания не меняется
|
||||
access: { update: () => false },
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: label('Name', 'Название'),
|
||||
},
|
||||
{
|
||||
name: 'orderPrefix',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
label: label('Order number prefix', 'Префикс номеров'),
|
||||
validate: validateOrderPrefix,
|
||||
},
|
||||
{
|
||||
name: 'locales',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
required: true,
|
||||
options: localeOptions,
|
||||
defaultValue: [...LOCALES],
|
||||
label: label('Content languages', 'Языки контента'),
|
||||
validate: validateLocales,
|
||||
admin: {
|
||||
description: label(
|
||||
'Each domain of the site shows some of these languages.',
|
||||
'Каждый домен сайта показывает часть этих языков.',
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,11 +1,80 @@
|
||||
import type { ArrayFieldValidation, CollectionConfig, TextFieldSingleValidation } from 'payload'
|
||||
import type {
|
||||
ArrayFieldValidation,
|
||||
CollectionBeforeChangeHook,
|
||||
CollectionConfig,
|
||||
TextFieldSingleValidation,
|
||||
} from 'payload'
|
||||
import { ValidationError } from 'payload'
|
||||
import { array } from 'payload/shared'
|
||||
|
||||
import { anyRole, managers } from '../access/roles'
|
||||
import { amountField, integerField, sortField } from '../fields/common'
|
||||
import { currencyField } from '../fields/currency'
|
||||
import { revalidateSiteHooks } from '../hooks/revalidateSite'
|
||||
import { uniqueInSite } from '../hooks/uniqueInSite'
|
||||
import { groups, label, message } from '../i18n'
|
||||
import { relationId } from '../utils'
|
||||
|
||||
/**
|
||||
* У dedicated цена локации — цена тарифа, в странах которого она есть. Страна в двух
|
||||
* активных тарифах сайта дала бы две цены, поэтому такое сохранение отклоняется.
|
||||
*/
|
||||
const oneDedicatedTariffPerCountry: CollectionBeforeChangeHook = async ({
|
||||
collection,
|
||||
data,
|
||||
originalDoc,
|
||||
req,
|
||||
}) => {
|
||||
const doc = { ...originalDoc, ...data }
|
||||
const siteId = relationId(doc.site)
|
||||
const countries = ((doc.countries ?? []) as unknown[])
|
||||
.map(relationId)
|
||||
.filter((id): id is string => id !== null)
|
||||
if (doc.type !== 'dedicated' || doc.active === false || !siteId || countries.length === 0) {
|
||||
return data
|
||||
}
|
||||
|
||||
const { docs } = await req.payload.find({
|
||||
collection: 'tariffs',
|
||||
where: {
|
||||
and: [
|
||||
{ site: { equals: siteId } },
|
||||
{ type: { equals: 'dedicated' } },
|
||||
{ active: { equals: true } },
|
||||
{ countries: { in: countries } },
|
||||
...(originalDoc?.id ? [{ id: { not_equals: originalDoc.id } }] : []),
|
||||
],
|
||||
},
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
overrideAccess: true,
|
||||
req,
|
||||
})
|
||||
const taken = new Set(
|
||||
docs.flatMap((tariff) =>
|
||||
(tariff.countries ?? []).flatMap((country) =>
|
||||
typeof country === 'object' && countries.includes(country.id) ? [country.iso2] : [],
|
||||
),
|
||||
),
|
||||
)
|
||||
if (taken.size > 0) {
|
||||
const list = [...taken].sort().join(', ')
|
||||
throw new ValidationError({
|
||||
collection: collection.slug,
|
||||
errors: [
|
||||
{
|
||||
path: 'countries',
|
||||
message: message(req, {
|
||||
en: `Already in another active tariff of the site: ${list}`,
|
||||
ru: `Уже входят в другой активный тариф сайта: ${list}`,
|
||||
}),
|
||||
},
|
||||
],
|
||||
req,
|
||||
})
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
const validateCode: TextFieldSingleValidation = (value, { req }) =>
|
||||
typeof value === 'string' && /^[a-z0-9][a-z0-9_-]*$/.test(value)
|
||||
@@ -56,7 +125,9 @@ export const Tariffs: CollectionConfig = {
|
||||
en: 'The site already has a tariff with this code',
|
||||
ru: 'У сайта уже есть тариф с таким кодом',
|
||||
}),
|
||||
oneDedicatedTariffPerCountry,
|
||||
],
|
||||
...revalidateSiteHooks,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import type { PayloadRequest } from 'payload'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { revalidateAfterChange, revalidateAfterDelete, SKIP_REVALIDATE } from './revalidateSite'
|
||||
|
||||
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }))
|
||||
|
||||
const req = { payload: { logger: { debug: vi.fn() } } } as unknown as PayloadRequest
|
||||
|
||||
// хукам нужны только doc, previousDoc, req и context
|
||||
const change = (doc: object, previousDoc: object = {}, context: object = {}) =>
|
||||
revalidateAfterChange({ doc, previousDoc, req, context } as never)
|
||||
|
||||
describe('пересборка маркетинговых страниц', () => {
|
||||
beforeEach(() => vi.mocked(revalidatePath).mockReset())
|
||||
|
||||
it('правка без черновиков пересобирает layout всех маркетинговых страниц', () => {
|
||||
change({ id: '1' })
|
||||
expect(revalidatePath).toHaveBeenCalledWith('/(front)/m/[host]/[locale]/[currency]', 'layout')
|
||||
})
|
||||
|
||||
it('черновик страницы не пересобирает, публикация и снятие с публикации — да', () => {
|
||||
change({ _status: 'draft' }, { _status: 'draft' })
|
||||
expect(revalidatePath).not.toHaveBeenCalled()
|
||||
change({ _status: 'published' }, { _status: 'draft' })
|
||||
change({ _status: 'draft' }, { _status: 'published' })
|
||||
expect(revalidatePath).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('seed и пакетные правки пропускают пересборку', () => {
|
||||
change({ id: '1' }, {}, { [SKIP_REVALIDATE]: true })
|
||||
revalidateAfterDelete({ doc: { id: '1' }, req, context: { [SKIP_REVALIDATE]: true } } as never)
|
||||
expect(revalidatePath).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('вне запроса Next ошибка пересборки не мешает сохранению', () => {
|
||||
vi.mocked(revalidatePath).mockImplementationOnce(() => {
|
||||
throw new Error('static generation store missing')
|
||||
})
|
||||
expect(() => change({ id: '1' })).not.toThrow()
|
||||
expect(req.payload.logger.debug).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { CollectionAfterChangeHook, CollectionAfterDeleteHook, PayloadRequest } from 'payload'
|
||||
|
||||
import { revalidateMarketingPages } from '@/core/content/revalidate'
|
||||
|
||||
/** Флаг в `context` операции: seed и пакетные правки пересборку не запускают */
|
||||
export const SKIP_REVALIDATE = 'skipRevalidate'
|
||||
|
||||
const revalidate = (req: PayloadRequest, context: Record<string, unknown>) => {
|
||||
if (context[SKIP_REVALIDATE]) return
|
||||
try {
|
||||
revalidateMarketingPages()
|
||||
} catch (error) {
|
||||
// вне запроса Next (скрипты Payload) пересобирать нечего: кэш страниц живёт в процессе Next
|
||||
req.payload.logger.debug({ msg: 'revalidatePath пропущен', err: error })
|
||||
}
|
||||
}
|
||||
|
||||
/** У коллекций с черновиками пересобираем только на публикацию и снятие с публикации */
|
||||
const affectsPublished = (doc: unknown, previousDoc: unknown): boolean => {
|
||||
const status = (value: unknown) =>
|
||||
typeof value === 'object' && value !== null && '_status' in value ? value._status : undefined
|
||||
const current = status(doc)
|
||||
if (current === undefined) return true
|
||||
return current === 'published' || status(previousDoc) === 'published'
|
||||
}
|
||||
|
||||
export const revalidateAfterChange: CollectionAfterChangeHook = ({
|
||||
doc,
|
||||
previousDoc,
|
||||
req,
|
||||
context,
|
||||
}) => {
|
||||
if (affectsPublished(doc, previousDoc)) revalidate(req, context)
|
||||
return doc
|
||||
}
|
||||
|
||||
export const revalidateAfterDelete: CollectionAfterDeleteHook = ({ doc, req, context }) => {
|
||||
revalidate(req, context)
|
||||
return doc
|
||||
}
|
||||
|
||||
/** Хуки для коллекций, чьи данные показывают маркетинговые страницы */
|
||||
export const revalidateSiteHooks = {
|
||||
afterChange: [revalidateAfterChange],
|
||||
afterDelete: [revalidateAfterDelete],
|
||||
}
|
||||
+4
-1
@@ -73,7 +73,10 @@ export async function proxy(request: NextRequest) {
|
||||
case 'next':
|
||||
return NextResponse.next()
|
||||
case 'rewrite':
|
||||
return NextResponse.rewrite(new URL(decision.pathname + search, request.url))
|
||||
// статус 404 Next применяет к ответу переписанной страницы: так 404 бренда — с сервера
|
||||
return NextResponse.rewrite(new URL(decision.pathname + search, request.url), {
|
||||
status: decision.status,
|
||||
})
|
||||
case 'redirect': {
|
||||
// за nginx запрос приходит по http, а снаружи сайт открыт по https
|
||||
const proto = request.headers.get('x-forwarded-proto')?.split(',')[0]?.trim() || 'https'
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Тексты начального контента: словари src/seed/content/{язык}.json с одинаковыми ключами.
|
||||
* В структурах страниц строка — ссылка `t('dedicated.home.hero.title')`; seed подставляет
|
||||
* её значение на каждом языке. Нет перевода — берётся английская строка: так в словаре
|
||||
* языка не нужно повторять названия вроде «VLESS», а обязательные поля проходят проверку.
|
||||
*/
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { DEFAULT_LOCALE, type Locale, LOCALES } from '@/i18n/locales'
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
type Dictionary = { [key: string]: string | Dictionary }
|
||||
|
||||
const dictionaries: Partial<Record<Locale, Dictionary>> = Object.fromEntries(
|
||||
LOCALES.flatMap((locale) => {
|
||||
const file = path.join(dirname, 'content', `${locale}.json`)
|
||||
return existsSync(file) ? [[locale, JSON.parse(readFileSync(file, 'utf8')) as Dictionary]] : []
|
||||
}),
|
||||
)
|
||||
|
||||
/** Ссылка на строку словаря */
|
||||
export type Text = { $t: string }
|
||||
|
||||
export const t = (key: string): Text => ({ $t: key })
|
||||
|
||||
const isText = (value: unknown): value is Text =>
|
||||
typeof value === 'object' && value !== null && '$t' in value
|
||||
|
||||
/** Значение, которое seed вычисляет на каждом языке: например, текст юридической страницы */
|
||||
export type Local = { $local: (locale: Locale) => unknown }
|
||||
|
||||
export const local = (resolve: (locale: Locale) => unknown): Local => ({ $local: resolve })
|
||||
|
||||
const isLocal = (value: unknown): value is Local =>
|
||||
typeof value === 'object' && value !== null && '$local' in value
|
||||
|
||||
const lookup = (locale: Locale, key: string): string | undefined => {
|
||||
let node: string | Dictionary | undefined = dictionaries[locale]
|
||||
for (const part of key.split('.')) {
|
||||
if (typeof node !== 'object') return undefined
|
||||
node = node[part]
|
||||
}
|
||||
return typeof node === 'string' ? node : undefined
|
||||
}
|
||||
|
||||
/** Строка на языке, без перевода — английская; в en.json строка обязана быть */
|
||||
export const text = (value: Text, locale: Locale): string => {
|
||||
const result = lookup(locale, value.$t) ?? lookup(DEFAULT_LOCALE, value.$t)
|
||||
if (result === undefined) throw new Error(`seed: нет строки «${value.$t}» в en.json`)
|
||||
return result
|
||||
}
|
||||
|
||||
/** Ключи словаря под префиксом, по порядку: например, вопросы FAQ раздела */
|
||||
export const keysUnder = (prefix: string): string[] => {
|
||||
let node: string | Dictionary | undefined = dictionaries[DEFAULT_LOCALE]
|
||||
for (const part of prefix.split('.')) {
|
||||
if (typeof node !== 'object') return []
|
||||
node = node[part]
|
||||
}
|
||||
return typeof node === 'object' ? Object.keys(node) : []
|
||||
}
|
||||
|
||||
/** Языки, для которых в словарях есть файл */
|
||||
export const contentLocales = (): Locale[] => LOCALES.filter((locale) => dictionaries[locale])
|
||||
|
||||
type Tree = unknown
|
||||
|
||||
/**
|
||||
* Дерево seed на языке: ссылки на строки заменяются значениями. Для языков, кроме
|
||||
* английского, нужны id блоков и строк массивов из уже созданного документа: локализованные
|
||||
* поля внутри блоков Payload хранит по этим id.
|
||||
*/
|
||||
export const resolveTree = (node: Tree, locale: Locale, created?: Tree): Tree => {
|
||||
if (isText(node)) return text(node, locale)
|
||||
if (isLocal(node)) return node.$local(locale)
|
||||
if (Array.isArray(node)) {
|
||||
const rows = Array.isArray(created) ? created : []
|
||||
return node.map((item, index) => {
|
||||
const resolved = resolveTree(item, locale, rows[index])
|
||||
const id = (rows[index] as { id?: string } | undefined)?.id
|
||||
return id && typeof resolved === 'object' && resolved !== null
|
||||
? { ...resolved, id }
|
||||
: resolved
|
||||
})
|
||||
}
|
||||
if (typeof node === 'object' && node !== null) {
|
||||
const existing = (typeof created === 'object' && created !== null ? created : {}) as Record<
|
||||
string,
|
||||
Tree
|
||||
>
|
||||
return Object.fromEntries(
|
||||
Object.entries(node).map(([key, value]) => [key, resolveTree(value, locale, existing[key])]),
|
||||
)
|
||||
}
|
||||
return node
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"chips": {
|
||||
"vless": "VLESS",
|
||||
"hysteria2": "Hysteria2",
|
||||
"xhttp": "XHTTP",
|
||||
"wireguard": "WireGuard"
|
||||
},
|
||||
"site": {
|
||||
"dedicated": {
|
||||
"hours": "Wir antworten an Arbeitstagen von 10:00 bis 20:00 Uhr Moskauer Zeit.",
|
||||
"titleTemplate": "%s · Veilix",
|
||||
"description": "Persönliches VPN mit dedizierter IP-Adresse im Land Ihrer Wahl: eine Adresse auf allen Ihren Geräten, RU-Adresse inklusive."
|
||||
},
|
||||
"mass": {
|
||||
"hours": "Wir antworten an Arbeitstagen von 10:00 bis 20:00 Uhr Moskauer Zeit.",
|
||||
"titleTemplate": "%s · VeilixVPN",
|
||||
"description": "VPN mit allen Ländern in einem Abonnement: russische Adresse, Xray und WireGuard, bis zu 5 Geräte."
|
||||
}
|
||||
},
|
||||
"tariffs": {
|
||||
"mass": {
|
||||
"month": {
|
||||
"description": "Monatliche Abbuchung, jederzeit kündbar"
|
||||
},
|
||||
"year": {
|
||||
"description": "{pricePerMonth} pro Monat, Sie sparen {savings} im Jahr",
|
||||
"badge": "günstiger"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"title": "Veilix – Ihr persönliches VPN mit dedizierter IP-Adresse",
|
||||
"description": "Persönliches VPN mit dedizierter IP-Adresse: eine Adresse auf allen Ihren Geräten, das Land Ihrer Wahl, RU-Adresse inklusive, WireGuard und Xray.",
|
||||
"hero": {
|
||||
"badge": "Dedizierte IP-Adresse, keine geteilte",
|
||||
"since": "Seit 2022 am Markt",
|
||||
"title": "Ihr persönliches VPN mit dedizierter IP-Adresse",
|
||||
"text": "Die Adresse gehört nur Ihnen: dieselbe IP-Adresse auf jedem Gerät, in jedem Land. Niemand sonst nutzt Ihre Adresse, und Sie erben keinen fremden Ruf.",
|
||||
"primary": "VPN holen",
|
||||
"secondary": "Standorte ansehen",
|
||||
"plateTitle": "Ihre IP-Adresse ist nur für Sie reserviert",
|
||||
"platePrice": "{priceFirst} im ersten Monat, danach {priceRenewal}",
|
||||
"plateDevices": "Bis zu {devices} Geräte",
|
||||
"plateRu": "RU-Adresse inklusive"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Was das in der Praxis bedeutet",
|
||||
"mainTitle": "Dedizierte IP-Adresse",
|
||||
"mainText": "Die Adresse ist nur Ihnen zugewiesen. Dienste sehen eine gewöhnliche Person aus dem gewählten Land, nicht hundert Menschen hinter einer einzigen Adresse.",
|
||||
"aiTitle": "Westliche KI-Dienste",
|
||||
"aiText": "Geringeres Risiko einer Sperrung als bei öffentlichen VPNs, deren Adressen längst auf Sperrlisten stehen.",
|
||||
"sameTitle": "Eine Adresse auf allen Geräten",
|
||||
"sameText": "Laptop, Smartphone und Tablet sehen nach außen gleich aus. Sie bleiben immer an einem Standort mit Ihrer eigenen IP-Adresse.",
|
||||
"captchaTitle": "Weniger Prüfungen und Captchas",
|
||||
"captchaText": "Massenhaft genutzte Adressen lösen den Bot-Schutz aus. Bei einer eigenen Adresse verhalten sich Dienste berechenbarer.",
|
||||
"encryptedTitle": "Verschlüsselter Datenverkehr",
|
||||
"encryptedText": "Ob zu Hause, im Hotel oder im Café: Die Verbindung ist überall auf der Welt geschützt.",
|
||||
"devicesTitle": "Bis zu {devices} Geräte pro Schlüssel",
|
||||
"devicesText": "Für mehr Geräte oder ein anderes Land fügen Sie einen zweiten Schlüssel mit eigener IP-Adresse hinzu.",
|
||||
"ruTitle": "Dazu eine dedizierte RU-Adresse",
|
||||
"ruText": "Gehört zu jedem Tarif. Funktioniert sowohl als direkte Verbindung als auch als Brücke in beide Richtungen.",
|
||||
"protocolsTitle": "Zwei Verbindungsarten",
|
||||
"protocolsText": "Funktioniert eine Methode nicht mehr, ist die zweite bereits eingerichtet. Die Liste wird nach und nach erweitert und aktualisiert.",
|
||||
"chipWg": "WireGuard / Amnezia WG",
|
||||
"chipXray": "Xray: VLESS, Hysteria2, XHTTP",
|
||||
"routesTitle": "Der Datenverkehr nimmt den kurzen Weg",
|
||||
"routesText": "Die Route wird automatisch anhand der Adresse der Website gewählt. Russische Dienste funktionieren weiter, ausländische öffnen sich, und Sie müssen nie etwas umschalten.",
|
||||
"inRussia": "Sie sind in Russland",
|
||||
"abroad": "Sie sind im Ausland",
|
||||
"ruSites": "Russische Websites",
|
||||
"restOfWorld": "Rest der Welt",
|
||||
"globalSites": "Weltweite Websites",
|
||||
"viaRuDirect": "direkt über den RU-Server",
|
||||
"tunnelToLocation": "per Tunnel zu Ihrem Standort",
|
||||
"fromLocation": "direkt von Ihrem Standort aus",
|
||||
"tunnelViaRu": "per Tunnel über den RU-Server",
|
||||
"abroadNote": "Für alle, die aus einem anderen Land mit Russland arbeiten."
|
||||
},
|
||||
"pricing": {
|
||||
"ip": "Dedizierte IP-Adresse im Land Ihrer Wahl",
|
||||
"ru": "Dedizierte RU-Adresse inklusive",
|
||||
"devices": "Bis zu {devices} Geräte",
|
||||
"protocols": "2 Verbindungsarten:",
|
||||
"listTitle": "Standort wählen",
|
||||
"buy": "Kaufen",
|
||||
"lead": "Individuelle Konditionen anfragen"
|
||||
},
|
||||
"faqTitle": "Fragen"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Preise",
|
||||
"description": "Dedizierte IP-Adresse im Land Ihrer Wahl: {priceFirst} im ersten Monat, danach {priceRenewal} pro Monat. RU-Adresse und bis zu {devices} Geräte inklusive.",
|
||||
"subline": "danach {priceRenewal} pro Monat",
|
||||
"note": "Der erste Monat umfasst die Vorbereitung Ihrer Adresse und Verbindungen. Danach zahlen Sie nur noch die monatliche Gebühr. Kündigen können Sie im Kundenkonto.",
|
||||
"includedTitle": "Im Preis enthalten",
|
||||
"included1": "Dedizierte IP-Adresse im Land Ihrer Wahl, nur für Sie",
|
||||
"included2": "Dedizierte RU-Adresse: direkte Verbindung und Brücke in beide Richtungen",
|
||||
"included3": "Bis zu {devices} Geräte mit derselben Adresse",
|
||||
"included4": "Zwei Verbindungsarten: WireGuard und Xray",
|
||||
"included5": "Intelligentes Routing RU / Global ohne Umschalten",
|
||||
"gridTitle": "Standort wählen",
|
||||
"gridText1": "In manchen Ländern wartet bereits eine freie Adresse, und Sie erhalten den Zugang sofort. Ist keine frei, bereiten wir eine Adresse eigens für Sie vor – das dauert bis zu 24 Stunden. Diesen Status zeigen wir Ihnen ehrlich, noch vor der Zahlung.",
|
||||
"gridText2": "Der Grundpreis beträgt {priceFirst} im ersten Monat und danach {priceRenewal}. Seltene und weit entfernte Standorte kosten mehr, der Preis ist beim jeweiligen Standort angegeben.",
|
||||
"severalTitle": "Benötigen Sie mehrere Standorte?",
|
||||
"severalText": "Jeder Schlüssel ist eine eigene dedizierte IP-Adresse. Nehmen Sie einen zweiten Schlüssel in einem anderen Land – beide liegen in einem Kundenkonto und werden unabhängig voneinander verlängert.",
|
||||
"leadTitle": "Benötigen Sie individuelle Konditionen?",
|
||||
"leadText": "Ein Team, viele Adressen, ein seltenes Land oder Ihr eigenes Routing-Szenario. Hinterlassen Sie eine Anfrage, wir melden uns bei Ihnen.",
|
||||
"leadButton": "Anfrage senden"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Standorte",
|
||||
"description": "Veilix-Standorte: {countries} zur Auswahl, mit Verfügbarkeit und Preisen. Eine russische Adresse gehört zu jedem Tarif.",
|
||||
"mapText": "{countries} zur Auswahl, dazu eine russische Adresse in jedem Tarif.",
|
||||
"hint": "Tippen Sie auf einen Punkt auf der Karte, um Land und Preis zu sehen",
|
||||
"scrollHint": "Die Karte lässt sich seitwärts scrollen",
|
||||
"ruLegend": "Jeder Tarif enthält eine RU-Adresse",
|
||||
"gridTitle": "Land wählen",
|
||||
"gridText": "Der Grundpreis beträgt {priceFirst} im ersten Monat und danach {priceRenewal}. Seltene und weit entfernte Standorte kosten mehr, der Preis ist beim jeweiligen Standort angegeben.",
|
||||
"ruTitle": "Russische Adresse in jedem Tarif",
|
||||
"ruText": "Die Adresse funktioniert in beide Richtungen: Aus Russland sehen Banken und Behördenportale eine gewöhnliche lokale Verbindung, aus dem Ausland öffnen sich dieselben Dienste, als wären Sie zu Hause.",
|
||||
"leadTitle": "Benötigen Sie individuelle Konditionen?",
|
||||
"leadText": "Ihr Land fehlt, Sie brauchen viele Adressen oder ein eigenes Routing-Szenario? Schreiben Sie uns – wir nennen Ihnen den Zeitrahmen und bereiten alles für Sie vor.",
|
||||
"leadButton": "Anfrage senden"
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"description": "Veilix-Support: Telegram und E-Mail, Antworten zu Verbindung, Zahlung und Standorten.",
|
||||
"telegramText": "Der schnellste Weg. Sprachnachrichten und Screenshots sind willkommen.",
|
||||
"emailText": "Für Rechnungen, Rückerstattungen und alles, was schriftlich sein muss.",
|
||||
"faqTitle": "Häufige Fragen",
|
||||
"tabConnection": "Verbindung",
|
||||
"tabBilling": "Zahlung und Verlängerung",
|
||||
"tabLocations": "Standorte und IP",
|
||||
"formTitle": "Schreiben Sie uns",
|
||||
"formText": "Wenn Sie bereits einen Schlüssel gekauft haben, geben Sie die E-Mail-Adresse an, auf die er registriert ist – so sehen wir Ihre Konfiguration sofort.",
|
||||
"topicConnection": "Thema: Verbindung",
|
||||
"topicBilling": "Thema: Zahlung und Verlängerung",
|
||||
"topicLocations": "Thema: Standorte und IP",
|
||||
"topicOther": "Thema: Sonstiges"
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"title": "VeilixVPN – ein VPN, das Sie nie ausschalten müssen",
|
||||
"description": "Alle Länder in einem Abonnement: russische Adresse, Xray und WireGuard, bis zu 5 Geräte. Wechseln Sie das Land, so oft Sie möchten.",
|
||||
"hero": {
|
||||
"badge": "Alle Länder in einem Abonnement",
|
||||
"since": "Seit 2022 am Markt",
|
||||
"title": "Ein VPN, das Sie nie ausschalten müssen",
|
||||
"text": "{countries} und eine russische Adresse sind im Abonnement enthalten. Wechseln Sie das Land, so oft Sie möchten – auf {devices} Geräten gleichzeitig.",
|
||||
"primary": "VPN holen",
|
||||
"secondary": "Standorte ansehen",
|
||||
"plateTitle": "{countries}, Wechsel mit einem Fingertipp",
|
||||
"platePrice": "{priceMonth} pro Monat oder {priceYear} pro Jahr",
|
||||
"plateDevices": "Bis zu {devices} Geräte",
|
||||
"plateRu": "Russische Adresse inklusive"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Was das in der Praxis bedeutet",
|
||||
"mainTitle": "Alle Standorte im Abonnement",
|
||||
"mainText": "Die gesamte Länderliste steht Ihnen offen. Wählen Sie ein Land in der App, die Verbindung baut sich in wenigen Sekunden neu auf, und der Wechsel kostet nichts extra.",
|
||||
"countriesTitle": "{countries}",
|
||||
"countriesText": "Europa, die USA, Asien, der Nahe Osten und Südamerika. Die Liste wächst weiter.",
|
||||
"devicesTitle": "{devices} Geräte gleichzeitig",
|
||||
"devicesText": "Smartphone, Laptop, Tablet und zwei weitere Geräte laufen gleichzeitig.",
|
||||
"ruSitesTitle": "Russische Websites ohne Umweg",
|
||||
"ruSitesText": "Banken und Behördenportale funktionieren wie gewohnt, ohne dass Sie das VPN ausschalten müssen.",
|
||||
"encryptedTitle": "Verschlüsselter Datenverkehr",
|
||||
"encryptedText": "Ob zu Hause, im Hotel oder im Café: Die Verbindung ist überall auf der Welt geschützt.",
|
||||
"devicesCardTitle": "{devices} Geräte pro Abonnement",
|
||||
"devicesCardText": "Ein Abonnement für alle Ihre Geräte. Die Geräte laufen gleichzeitig und in verschiedenen Ländern.",
|
||||
"ruTitle": "Russische Adresse inklusive",
|
||||
"ruText": "Die russische Adresse ist Teil des Abonnements: Banken, Behördenportale und Streamingdienste öffnen sich wie zu Hause.",
|
||||
"protocolsTitle": "Xray und WireGuard",
|
||||
"protocolsText": "Xray lässt den Datenverkehr wie gewöhnliches HTTPS aussehen und bleibt dort stabil, wo ältere Protokolle nicht mehr funktionieren. WireGuard ist schnell und einfach, wo nichts gesperrt wird.",
|
||||
"routesTitle": "Der Datenverkehr nimmt den kurzen Weg",
|
||||
"routesText": "Die Route wird automatisch anhand der Adresse der Website gewählt. Russische Dienste funktionieren weiter, ausländische öffnen sich, und Sie müssen nie etwas umschalten.",
|
||||
"inRussia": "Sie sind in Russland",
|
||||
"abroad": "Sie sind im Ausland",
|
||||
"ruSites": "Russische Websites",
|
||||
"restOfWorld": "Rest der Welt",
|
||||
"globalSites": "Weltweite Websites",
|
||||
"viaRuDirect": "direkt über den RU-Server",
|
||||
"tunnelToLocation": "per Tunnel zum gewählten Standort",
|
||||
"fromLocation": "direkt vom gewählten Standort aus",
|
||||
"tunnelViaRu": "per Tunnel über den RU-Server",
|
||||
"abroadNote": "Für alle, die aus einem anderen Land mit Russland arbeiten."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Ein Abonnement, alle Länder",
|
||||
"text": "{countries}, eine russische Adresse und {devices} Geräte.",
|
||||
"countries": "{countries} und eine russische Adresse",
|
||||
"devices": "Bis zu {devices} Geräte gleichzeitig",
|
||||
"protocols": "Xray und WireGuard",
|
||||
"listTitle": "Länder im Abonnement",
|
||||
"buy": "Abonnement abschließen",
|
||||
"note": "Russische und internationale Bankkarten sowie Kryptowährungen. Der Zugang wird direkt nach der Zahlung freigeschaltet."
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Benötigen Sie eine Adresse, die nur Ihnen gehört?",
|
||||
"text": "Hier teilen Sie sich die IP-Adresse mit anderen: Das kostet weniger und reicht den meisten aus. Wenn ein Dienst eine feste IP-Adresse verlangt, sehen Sie sich die Veilix-Tarife mit dedizierter IP-Adresse an.",
|
||||
"button": "Dedizierte IP-Adresse"
|
||||
},
|
||||
"faqTitle": "Fragen"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Preise",
|
||||
"description": "Alle Länder in einem Abonnement: {priceMonth} pro Monat oder {priceYear} pro Jahr. Russische Adresse und bis zu {devices} Geräte.",
|
||||
"subline": "oder {priceYear} pro Jahr – {pricePerMonth} pro Monat",
|
||||
"note": "Ein Preis für alles. Die Verlängerung können Sie im Kundenkonto beenden.",
|
||||
"button": "Abonnement abschließen",
|
||||
"includedTitle": "Im Preis enthalten",
|
||||
"included1": "{countries}, jederzeit wechselbar",
|
||||
"included2": "Russische Adresse: direkte Verbindung und Brücke in beide Richtungen",
|
||||
"included3": "Bis zu {devices} Geräte gleichzeitig",
|
||||
"included4": "Xray und WireGuard",
|
||||
"gridTitle": "Länder im Abonnement",
|
||||
"gridText1": "Länder mit dem Status „Inklusive“ sind direkt nach der Zahlung verfügbar, alle zu einem Preis. Neue Länder kommen ohne Aufpreis zum Abonnement hinzu.",
|
||||
"crossTitle": "Benötigen Sie eine Adresse, die nur Ihnen gehört?",
|
||||
"crossText": "Hier teilen Sie sich die IP-Adresse mit anderen: Das kostet weniger und reicht den meisten aus. Wenn ein Dienst eine feste IP-Adresse verlangt, sehen Sie sich die Veilix-Tarife mit dedizierter IP-Adresse an.",
|
||||
"crossButton": "Dedizierte IP-Adresse",
|
||||
"leadTitle": "Benötigen Sie besondere Konditionen?",
|
||||
"leadText": "Abonnements für Teams, Zahlung auf Rechnung oder ein seltenes Land. Hinterlassen Sie eine Anfrage, wir melden uns bei Ihnen.",
|
||||
"leadButton": "Anfrage senden"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Standorte",
|
||||
"description": "Im VeilixVPN-Abonnement: {countries} und eine russische Adresse, mit dem Status jedes Landes.",
|
||||
"mapText": "{countries} und eine russische Adresse sind im Abonnement enthalten.",
|
||||
"hint": "Tippen Sie auf einen Punkt auf der Karte, um Land und Stadt zu sehen",
|
||||
"scrollHint": "Die Karte lässt sich seitwärts scrollen",
|
||||
"ruLegend": "Russische Adresse inklusive",
|
||||
"gridTitle": "Alle Länder im Abonnement",
|
||||
"gridText": "Ein Preis für alles: {priceMonth} pro Monat oder {priceYear} pro Jahr.",
|
||||
"ruTitle": "Russische Adresse im Abonnement",
|
||||
"ruText": "Die Adresse funktioniert in beide Richtungen: Aus Russland sehen Banken und Behördenportale eine gewöhnliche lokale Verbindung, aus dem Ausland öffnen sich dieselben Dienste, als wären Sie zu Hause.",
|
||||
"leadTitle": "Benötigen Sie besondere Konditionen?",
|
||||
"leadText": "Das gewünschte Land fehlt oder Sie möchten ein Abonnement für ein Team? Schreiben Sie uns, wir melden uns bei Ihnen.",
|
||||
"leadButton": "Anfrage senden"
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"description": "VeilixVPN-Support: Telegram und E-Mail, Antworten zu Verbindung, Zahlung und den Ländern in Ihrem Abonnement.",
|
||||
"telegramText": "Der schnellste Weg. Sprachnachrichten und Screenshots sind willkommen.",
|
||||
"emailText": "Für Rechnungen, Rückerstattungen und alles, was schriftlich sein muss.",
|
||||
"faqTitle": "Häufige Fragen",
|
||||
"tabConnection": "Verbindung",
|
||||
"tabBilling": "Zahlung und Verlängerung",
|
||||
"tabLocations": "Standorte und Abonnement",
|
||||
"formTitle": "Schreiben Sie uns",
|
||||
"formText": "Wenn Sie bereits ein Abonnement haben, geben Sie die E-Mail-Adresse an, auf die es registriert ist – so sehen wir Ihre Konfiguration sofort.",
|
||||
"topicConnection": "Thema: Verbindung",
|
||||
"topicBilling": "Thema: Zahlung und Verlängerung",
|
||||
"topicLocations": "Thema: Standorte und Abonnement",
|
||||
"topicOther": "Thema: Sonstiges"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"differs": {
|
||||
"q": "Worin unterscheidet sich das von einem gewöhnlichen VPN für 3 Dollar?",
|
||||
"a": "Bei einem Massen-VPN teilen sich Hunderte Menschen Ihre Adresse. Landet einer von ihnen auf einer Sperrliste, gelten die Einschränkungen für alle. Hier gehört die Adresse nur Ihnen, deshalb behandeln Dienste sie wie einen gewöhnlichen Heimanschluss."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Warum sind manche Standorte erst in 24 Stunden bereit?",
|
||||
"a": "Wir halten keinen Pool gemeinsam genutzter Adressen vor. Gibt es im gewünschten Land keine freie Adresse, bereiten wir eine eigens für Sie vor und übergeben Ihnen den Zugang innerhalb eines Tages. Der bezahlte Zeitraum beginnt mit der Freischaltung des Zugangs."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Muss ich selbst etwas einrichten?",
|
||||
"a": "Nein. Im Kundenkonto laden Sie eine fertige Datei herunter oder scannen einen QR-Code mit dem Smartphone. Das Routing ist bereits eingerichtet: Russische Websites laufen direkt, ausländische über Ihren Standort."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Wie viele Geräte kann ich verbinden?",
|
||||
"a": "Bis zu fünf pro Schlüssel, alle mit derselben Adresse. Für mehr Geräte oder ein zweites Land nehmen Sie einen weiteren Schlüssel mit eigener dedizierter IP-Adresse."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Wozu brauche ich zusätzlich eine russische Adresse?",
|
||||
"a": "Die Adresse funktioniert in beide Richtungen. Aus Russland, damit Banken, Behördenportale und Streamingdienste eine gewöhnliche lokale Verbindung sehen. Aus dem Ausland, damit sich dieselben Dienste öffnen, als wären Sie zu Hause."
|
||||
},
|
||||
"blocked": {
|
||||
"q": "Was, wenn eine Verbindungsmethode nicht mehr funktioniert?",
|
||||
"a": "Ihr Schlüssel enthält beide Verbindungsarten gleichzeitig. Wird eine davon blockiert, wechseln Sie im selben Kundenkonto zur anderen. Die Auswahl an Protokollen erweitern und aktualisieren wir laufend."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "Was tue ich direkt nach der Zahlung?",
|
||||
"a": "Melden Sie sich per E-Mail im Kundenkonto an, öffnen Sie Ihren Schlüssel und laden Sie die Konfiguration herunter oder scannen Sie den QR-Code mit dem Smartphone. Das Routing ist bereits eingerichtet, Sie müssen nichts von Hand eingeben."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Wie viele Geräte kann ich verbinden?",
|
||||
"a": "Bis zu fünf pro Schlüssel, alle mit derselben Adresse. Für mehr Geräte oder ein zweites Land nehmen Sie einen weiteren Schlüssel mit eigener dedizierter IP-Adresse."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Eine Verbindungsmethode funktioniert nicht mehr.",
|
||||
"a": "In Ihrem Schlüssel sind immer beide Arten eingerichtet: WireGuard und Xray. Öffnen Sie Ihr Kundenkonto und verbinden Sie sich mit der zweiten Konfiguration, eine Anfrage ist nicht nötig. Die Auswahl an Protokollen erweitern und aktualisieren wir laufend."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Muss ich etwas manuell einrichten?",
|
||||
"a": "Nein. In der Konfiguration ist bereits festgelegt, dass russische Websites direkt laufen und ausländische über Ihren Standort. Während der Arbeit müssen Sie keine Modi umschalten."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"firstMonth": {
|
||||
"q": "Warum ist der erste Monat teurer?",
|
||||
"a": "Der erste Monat umfasst die Vorbereitung: Zuweisung der Adresse sowie Einrichtung beider Protokolle und des Routings. Danach zahlen Sie nur noch die monatliche Gebühr."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "Wie funktioniert die Verlängerung?",
|
||||
"a": "Automatisch, einmal im Monat: zuerst vom Guthaben, dann von der hinterlegten Karte. Das Ablaufdatum sehen Sie auf der Seite des Schlüssels im Kundenkonto, dort können Sie die automatische Verlängerung auch deaktivieren."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Kann ich mein Geld zurückbekommen?",
|
||||
"a": "Wenn der Schlüssel durch unser Verschulden nicht funktioniert hat, erstatten wir in den ersten sieben Tagen den vollen Betrag. Wenn Sie es sich einfach anders überlegt haben, erstatten wir den nicht genutzten Teil des Monats."
|
||||
},
|
||||
"methods": {
|
||||
"q": "Wie kann ich bezahlen?",
|
||||
"a": "Mit Karten russischer und ausländischer Banken sowie mit Kryptowährungen. Alle Zahlungsarten sehen Sie bei der Bestellung."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"vsVpn": {
|
||||
"q": "Worin unterscheidet sich eine dedizierte IP-Adresse von einem gewöhnlichen VPN?",
|
||||
"a": "Bei einem Massen-VPN teilen sich Hunderte Menschen eine Adresse, und jede Einschränkung, die sich einer von ihnen einhandelt, gilt für alle. Ihre Adresse gehört nur Ihnen, deshalb behandeln Dienste sie wie einen gewöhnlichen Heimanschluss."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Warum sind manche Standorte erst in 24 Stunden bereit?",
|
||||
"a": "Wir halten keinen Pool gemeinsam genutzter Adressen vor. Gibt es im gewünschten Land keine freie Adresse, bereiten wir eine eigens für Sie vor. Der bezahlte Zeitraum beginnt mit der Freischaltung des Zugangs."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Warum gehört eine russische Adresse zum Tarif?",
|
||||
"a": "Die Adresse funktioniert in beide Richtungen. Aus Russland, damit Banken, Behördenportale und Streamingdienste eine gewöhnliche lokale Verbindung sehen. Aus dem Ausland, damit sich dieselben Dienste öffnen, als wären Sie zu Hause."
|
||||
},
|
||||
"changeCountry": {
|
||||
"q": "Kann ich das Land später wechseln?",
|
||||
"a": "Ein Schlüssel ist an sein Land gebunden, deshalb ändern wir es nicht. Wenn Sie einen anderen Standort brauchen, nehmen Sie einen zweiten Schlüssel: Er erscheint im selben Kundenkonto und wird separat verlängert. Den ersten können Sie behalten oder jederzeit kündigen."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "Wie verbinde ich ein neues Gerät?",
|
||||
"a": "Öffnen Sie den Schlüssel, laden Sie eine beliebige freie WireGuard-Konfiguration herunter oder scannen Sie ihren QR-Code. Jede Konfiguration ist für ein Gerät gedacht."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Was tun, wenn die Verbindung nicht mehr funktioniert?",
|
||||
"a": "Wechseln Sie im selben Schlüssel zu Xray – es ist bereits eingerichtet. Wenn das nicht hilft, schreiben Sie an den Support."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "Wie beende ich die Verlängerung?",
|
||||
"a": "Deaktivieren Sie die automatische Verlängerung auf der Seite des Schlüssels. Der Schlüssel funktioniert bis zum Ende des bezahlten Zeitraums weiter."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"included": {
|
||||
"q": "Was ist im Abonnement enthalten?",
|
||||
"a": "Alle Länder auf der Liste, eine russische Adresse und fünf Geräte. Für einzelne Länder oder Geräte fallen keine zusätzlichen Kosten an."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Kann ich das Land so oft wechseln, wie ich möchte?",
|
||||
"a": "Ja. Der Wechsel dauert wenige Sekunden und kostet nichts: Die komplette Standortliste steht während des gesamten Abonnements offen."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Muss ich selbst etwas einrichten?",
|
||||
"a": "Nein. Im Kundenkonto laden Sie eine fertige Datei herunter oder scannen einen QR-Code mit dem Smartphone. Russische Websites laufen direkt, alle anderen über den gewählten Standort."
|
||||
},
|
||||
"protocol": {
|
||||
"q": "Welches Protokoll nutzt die Verbindung?",
|
||||
"a": "Xray mit VLESS, Hysteria2 und XHTTP: Der Datenverkehr sieht aus wie gewöhnliches HTTPS, deshalb bleibt die Verbindung auch dort stabil, wo ältere Protokolle blockiert werden. Für einfache Fälle gibt es zusätzlich WireGuard."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "Was ist der Unterschied zu einem Tarif mit dedizierter IP-Adresse?",
|
||||
"a": "Hier ist die Adresse geteilt: Mehrere Personen nutzen einen Server, deshalb ist das Abonnement deutlich günstiger. Wenn Sie eine Adresse nur für sich brauchen, gibt es dafür separate Tarife mit dedizierter IP-Adresse."
|
||||
},
|
||||
"payment": {
|
||||
"q": "Wie wird bezahlt und verlängert?",
|
||||
"a": "Monatlich oder jährlich. Das Abonnement verlängert sich automatisch: zuerst vom Guthaben, dann von der hinterlegten Karte. Der Jahrestarif ist günstiger als die monatliche Zahlung. Die Verlängerung können Sie jederzeit im Kundenkonto deaktivieren."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "Was tue ich direkt nach der Zahlung?",
|
||||
"a": "Melden Sie sich mit Ihrer E-Mail-Adresse an und öffnen Sie Ihren Schlüssel: Fügen Sie den Link in die App Happ ein oder laden Sie die WireGuard-Konfiguration für das gewünschte Land herunter. Sie können auch den QR-Code mit dem Smartphone scannen. Das Routing ist bereits eingerichtet."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Wie viele Geräte kann ich verbinden?",
|
||||
"a": "Bis zu fünf pro Schlüssel. Die Geräte können verschiedene Länder nutzen: Jedes verbindet sich mit seinem eigenen Standort."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Eine Verbindungsmethode funktioniert nicht mehr.",
|
||||
"a": "Wechseln Sie in der App zu einem anderen Protokoll oder Standort – das dauert wenige Sekunden. Die Protokolleinstellungen passen wir an, sobald neue Sperren auftreten."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Muss ich etwas manuell einrichten?",
|
||||
"a": "Nein. In der Konfiguration ist bereits festgelegt, dass russische Websites direkt laufen und ausländische über den gewählten Standort. Während der Arbeit müssen Sie keine Modi umschalten."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"price": {
|
||||
"q": "Was kostet das Abonnement?",
|
||||
"a": "Die Preise finden Sie auf der Preisseite: monatlich oder jährlich, der Jahrestarif ist günstiger. Es gibt keine Einrichtungsgebühr und keine Aufpreise für einzelne Länder."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "Wie verlängert sich das Abonnement?",
|
||||
"a": "Automatisch, monatlich oder jährlich: zuerst vom Guthaben, dann von der hinterlegten Karte. Das Enddatum sehen Sie im Kundenkonto, dort können Sie die automatische Verlängerung auch deaktivieren."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Kann ich mein Geld zurückbekommen?",
|
||||
"a": "Wenn das Abonnement durch unser Verschulden nicht funktioniert hat, erstatten wir in den ersten sieben Tagen den vollen Betrag. Wenn Sie es sich einfach anders überlegt haben, erstatten wir den nicht genutzten Teil des Zeitraums."
|
||||
},
|
||||
"methods": {
|
||||
"q": "Wie kann ich bezahlen?",
|
||||
"a": "Mit Karten russischer und ausländischer Banken sowie mit Kryptowährungen. Alle Zahlungsarten sehen Sie bei der Bestellung."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"countries": {
|
||||
"q": "Wie viele Länder sind enthalten?",
|
||||
"a": "Alle Länder der Standortliste mit dem Status „Inklusive“ sowie eine russische Adresse. Die Länder werden nicht einzeln berechnet, und neue kommen ohne Aufpreis hinzu."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Kann ich das Land so oft wechseln, wie ich möchte?",
|
||||
"a": "Ja. Der Wechsel dauert wenige Sekunden und kostet nichts: Die komplette Standortliste steht während des gesamten Abonnements offen."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Warum gehört eine russische Adresse zum Abonnement?",
|
||||
"a": "Die Adresse funktioniert in beide Richtungen. Aus Russland, damit Banken, Behördenportale und Streamingdienste eine gewöhnliche lokale Verbindung sehen. Aus dem Ausland, damit sich dieselben Dienste öffnen, als wären Sie zu Hause."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "Was ist der Unterschied zu einer dedizierten IP-Adresse?",
|
||||
"a": "Hier ist die Adresse geteilt: Mehrere Personen nutzen einen Server, deshalb kostet das Abonnement deutlich weniger. Wenn Sie eine Adresse nur für sich brauchen, bietet Veilix separate Tarife mit dedizierter IP-Adresse."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "Wie verbinde ich ein neues Gerät?",
|
||||
"a": "Öffnen Sie den Schlüssel und fügen Sie den Link in die App Happ ein oder laden Sie die WireGuard-Konfiguration für das gewünschte Land herunter. Sie können auch den QR-Code scannen."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Was tun, wenn die Verbindung nicht mehr funktioniert?",
|
||||
"a": "Wechseln Sie in der App den Standort oder das Protokoll – das dauert wenige Sekunden. Wenn das nicht hilft, schreiben Sie an den Support."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "Wie beende ich die Verlängerung?",
|
||||
"a": "Deaktivieren Sie die automatische Verlängerung auf der Seite des Schlüssels. Der Schlüssel funktioniert bis zum Ende des bezahlten Zeitraums weiter."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "Nutzungsbedingungen",
|
||||
"description": "Nutzungsbedingungen des Dienstes: Kundenkonto, Schlüssel und Geräte, Zahlung, Verlängerung und zulässige Nutzung."
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Datenschutzerklärung",
|
||||
"description": "Welche Daten wir speichern und warum. Die VPN-Server führen keine Logs."
|
||||
},
|
||||
"refund": {
|
||||
"title": "Rückerstattungsrichtlinie",
|
||||
"description": "Wann und wie wir Zahlungen für einen Schlüssel erstatten."
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Cookie-Richtlinie",
|
||||
"description": "Welche Cookies die Website verwendet und warum."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"chips": {
|
||||
"vless": "VLESS",
|
||||
"hysteria2": "Hysteria2",
|
||||
"xhttp": "XHTTP",
|
||||
"wireguard": "WireGuard"
|
||||
},
|
||||
"site": {
|
||||
"dedicated": {
|
||||
"hours": "We reply from 10:00 to 20:00 Moscow time on business days.",
|
||||
"titleTemplate": "%s · Veilix",
|
||||
"description": "A private VPN with a dedicated IP in the country you choose: one address on all your devices, a RU address included."
|
||||
},
|
||||
"mass": {
|
||||
"hours": "We reply from 10:00 to 20:00 Moscow time on business days.",
|
||||
"titleTemplate": "%s · VeilixVPN",
|
||||
"description": "A VPN with every country in one subscription: a Russian address, Xray and WireGuard, up to 5 devices."
|
||||
}
|
||||
},
|
||||
"tariffs": {
|
||||
"mass": {
|
||||
"month": {
|
||||
"description": "Charged monthly, cancel any time"
|
||||
},
|
||||
"year": {
|
||||
"description": "{pricePerMonth} a month, you save {savings} a year",
|
||||
"badge": "better value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"title": "Veilix — your private VPN with a dedicated IP",
|
||||
"description": "A private VPN with a dedicated IP: one address on all your devices, the country you choose, a RU address included, WireGuard and Xray.",
|
||||
"hero": {
|
||||
"badge": "A dedicated IP, not a shared one",
|
||||
"since": "Running since 2022",
|
||||
"title": "Your private VPN with a dedicated IP",
|
||||
"text": "The address belongs to you alone: the same IP on every device, in any country. No neighbours on your address and no reputation to inherit.",
|
||||
"primary": "Get a VPN",
|
||||
"secondary": "See locations",
|
||||
"plateTitle": "Your IP is reserved for you alone",
|
||||
"platePrice": "{priceFirst} for the first month, then {priceRenewal}",
|
||||
"plateDevices": "Up to {devices} devices",
|
||||
"plateRu": "RU address included"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "What this means in practice",
|
||||
"mainTitle": "Dedicated IP",
|
||||
"mainText": "The address is assigned to you alone. Services see one ordinary person from the country you chose, not a hundred people behind a single address.",
|
||||
"aiTitle": "Western AI services",
|
||||
"aiText": "Lower risk of being blocked than with public VPNs, whose addresses have long been blacklisted.",
|
||||
"sameTitle": "One address across all devices",
|
||||
"sameText": "Laptop, phone and tablet all look the same. You always stay in one location with your own IP.",
|
||||
"captchaTitle": "Fewer checks and captchas",
|
||||
"captchaText": "Mass addresses trigger anti-bot protection. On a personal one, services behave more predictably.",
|
||||
"encryptedTitle": "Encrypted traffic",
|
||||
"encryptedText": "At home, in a hotel or in a café, the connection is protected anywhere in the world.",
|
||||
"devicesTitle": "Up to {devices} devices per key",
|
||||
"devicesText": "Need more, or another country — add a second key with its own separate IP.",
|
||||
"ruTitle": "Plus a dedicated RU address",
|
||||
"ruText": "Comes with every plan. Works both as a direct connection and as a two-way bridge.",
|
||||
"protocolsTitle": "Two connection types",
|
||||
"protocolsText": "If one method stops working, the second is already set up. The list is extended and updated over time.",
|
||||
"chipWg": "WireGuard / Amnezia WG",
|
||||
"chipXray": "Xray: VLESS, Hysteria2, XHTTP",
|
||||
"routesTitle": "Traffic takes the short route",
|
||||
"routesText": "The route is chosen automatically by the site address. Russian services keep working, foreign ones open, and you never switch anything.",
|
||||
"inRussia": "You are in Russia",
|
||||
"abroad": "You are abroad",
|
||||
"ruSites": "RU sites",
|
||||
"restOfWorld": "The rest of the world",
|
||||
"globalSites": "Global sites",
|
||||
"viaRuDirect": "directly through the RU server",
|
||||
"tunnelToLocation": "tunnelled to your location",
|
||||
"fromLocation": "straight from your location",
|
||||
"tunnelViaRu": "tunnelled through the RU server",
|
||||
"abroadNote": "For people who work with Russia from another country."
|
||||
},
|
||||
"pricing": {
|
||||
"ip": "Dedicated IP in the country you choose",
|
||||
"ru": "Dedicated RU address included",
|
||||
"devices": "Up to {devices} devices",
|
||||
"protocols": "2 connection types:",
|
||||
"listTitle": "Choose a location",
|
||||
"buy": "Buy",
|
||||
"lead": "Need custom terms"
|
||||
},
|
||||
"faqTitle": "Questions"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Pricing",
|
||||
"description": "A dedicated IP in the country you choose: {priceFirst} for the first month, then {priceRenewal} a month. A RU address and up to {devices} devices included.",
|
||||
"subline": "then {priceRenewal} per month",
|
||||
"note": "The first month covers preparing your address and connections. After that it is only the subscription fee, and you can cancel in your account.",
|
||||
"includedTitle": "What is included",
|
||||
"included1": "A dedicated IP in the country you choose, yours alone",
|
||||
"included2": "A dedicated RU address: direct connection and a two-way bridge",
|
||||
"included3": "Up to {devices} devices sharing the same address",
|
||||
"included4": "Two connection types: WireGuard and Xray",
|
||||
"included5": "Smart RU / global routing with no switching",
|
||||
"gridTitle": "Choose a location",
|
||||
"gridText1": "In some countries a free address is already waiting and access is issued immediately. If none is free, we prepare one for you personally — that takes up to 24 hours, and you see this status honestly, before paying.",
|
||||
"gridText2": "The base price is {priceFirst} for the first month and {priceRenewal} after that. Rare and distant locations cost more, and the price is shown on the card.",
|
||||
"severalTitle": "Need several locations",
|
||||
"severalText": "Each key is a separate dedicated IP. Take a second key in another country and both live in one account, renewing independently.",
|
||||
"leadTitle": "Need custom terms",
|
||||
"leadText": "A team, many addresses, a rare country or your own routing scenario. Leave a request and we will get in touch.",
|
||||
"leadButton": "Send a request"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Locations",
|
||||
"description": "Veilix locations: {countries} to choose from, with availability and prices. A Russian address comes with every plan.",
|
||||
"mapText": "{countries} to choose from, with a Russian address included in every plan.",
|
||||
"hint": "Tap a dot on the map to see the country and its price",
|
||||
"scrollHint": "Scroll the map sideways",
|
||||
"ruLegend": "A RU address comes with every plan",
|
||||
"gridTitle": "Choose a country",
|
||||
"gridText": "The base price is {priceFirst} for the first month and {priceRenewal} after that. Rare and distant locations cost more, and the price is shown on the card.",
|
||||
"ruTitle": "A Russian address in every plan",
|
||||
"ruText": "It works both ways: from Russia, banks and government services see an ordinary local connection; from abroad, those same services open as if you were at home.",
|
||||
"leadTitle": "Need custom terms",
|
||||
"leadText": "If your country is missing, you need many addresses, or you have a custom routing setup — write to us and we will give you timing and prepare it for you.",
|
||||
"leadButton": "Send a request"
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"description": "Veilix support: Telegram and email, answers about connecting, payment and locations.",
|
||||
"telegramText": "The fastest way. Voice messages and screenshots welcome.",
|
||||
"emailText": "For invoices, refunds and anything that needs to be in writing.",
|
||||
"faqTitle": "Frequent questions",
|
||||
"tabConnection": "Connecting",
|
||||
"tabBilling": "Payment and renewal",
|
||||
"tabLocations": "Locations and IP",
|
||||
"formTitle": "Write to us",
|
||||
"formText": "If you already bought a key, use the email it is registered to so we can see your configuration straight away.",
|
||||
"topicConnection": "Topic: connecting",
|
||||
"topicBilling": "Topic: payment and renewal",
|
||||
"topicLocations": "Topic: locations and IP",
|
||||
"topicOther": "Topic: something else"
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"title": "VeilixVPN — a VPN you never have to switch off",
|
||||
"description": "Every country in one subscription: a Russian address, Xray and WireGuard, up to 5 devices. Switch countries as often as you like.",
|
||||
"hero": {
|
||||
"badge": "Every country in one subscription",
|
||||
"since": "Running since 2022",
|
||||
"title": "A VPN you never have to switch off",
|
||||
"text": "{countries} and a Russian address come with the subscription. Switch countries as often as you like, on {devices} devices at once.",
|
||||
"primary": "Get a VPN",
|
||||
"secondary": "See locations",
|
||||
"plateTitle": "{countries}, one tap to switch",
|
||||
"platePrice": "{priceMonth} a month or {priceYear} a year",
|
||||
"plateDevices": "Up to {devices} devices",
|
||||
"plateRu": "Russian address included"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "What this means in practice",
|
||||
"mainTitle": "Every location in the subscription",
|
||||
"mainText": "The whole country list is open. Pick a country in the app, the connection rebuilds in a couple of seconds, and switching costs nothing extra.",
|
||||
"countriesTitle": "{countries}",
|
||||
"countriesText": "Europe, the US, Asia, the Middle East and South America. The list keeps growing.",
|
||||
"devicesTitle": "{devices} devices at once",
|
||||
"devicesText": "Phone, laptop, tablet and two more devices all run at the same time.",
|
||||
"ruSitesTitle": "Russian sites go direct",
|
||||
"ruSitesText": "Banks and government services keep working as usual, with no need to switch the VPN off.",
|
||||
"encryptedTitle": "Encrypted traffic",
|
||||
"encryptedText": "At home, in a hotel or in a café, the connection is protected anywhere in the world.",
|
||||
"devicesCardTitle": "{devices} devices per subscription",
|
||||
"devicesCardText": "One subscription for all your devices. They run at the same time and in different countries.",
|
||||
"ruTitle": "Russian address included",
|
||||
"ruText": "The Russian address is part of the subscription: banks, government services and streaming open as if you were home.",
|
||||
"protocolsTitle": "Xray and WireGuard",
|
||||
"protocolsText": "Xray makes traffic look like ordinary HTTPS and holds where older protocols no longer work. WireGuard is fast and simple where nothing is blocked.",
|
||||
"routesTitle": "Traffic takes the short route",
|
||||
"routesText": "The route is chosen automatically by the site address. Russian services keep working, foreign ones open, and you never switch anything.",
|
||||
"inRussia": "You are in Russia",
|
||||
"abroad": "You are abroad",
|
||||
"ruSites": "RU sites",
|
||||
"restOfWorld": "The rest of the world",
|
||||
"globalSites": "Global sites",
|
||||
"viaRuDirect": "directly through the RU server",
|
||||
"tunnelToLocation": "tunnelled to the chosen location",
|
||||
"fromLocation": "straight from the chosen location",
|
||||
"tunnelViaRu": "tunnelled through the RU server",
|
||||
"abroadNote": "For people who work with Russia from another country."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "One subscription, every country",
|
||||
"text": "{countries}, a Russian address and {devices} devices.",
|
||||
"countries": "{countries} and a Russian address",
|
||||
"devices": "Up to {devices} devices at once",
|
||||
"protocols": "Xray and WireGuard",
|
||||
"listTitle": "Countries included",
|
||||
"buy": "Get the subscription",
|
||||
"note": "Russian and international cards, plus crypto. Access opens right after payment."
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Need an address that is yours alone",
|
||||
"text": "The IP here is shared: it costs less and is enough for most people. If a service needs a permanent IP, look at Veilix dedicated plans.",
|
||||
"button": "Dedicated IP"
|
||||
},
|
||||
"faqTitle": "Questions"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Pricing",
|
||||
"description": "Every country in one subscription: {priceMonth} a month or {priceYear} a year. A Russian address and up to {devices} devices.",
|
||||
"subline": "or {priceYear} a year — {pricePerMonth} per month",
|
||||
"note": "One price for everything. You can cancel renewal in your account.",
|
||||
"button": "Get the subscription",
|
||||
"includedTitle": "What is included",
|
||||
"included1": "{countries}, switch any time",
|
||||
"included2": "A Russian address: direct connection and a two-way bridge",
|
||||
"included3": "Up to {devices} devices at once",
|
||||
"included4": "Xray and WireGuard",
|
||||
"gridTitle": "Countries included",
|
||||
"gridText1": "Countries marked “Included” are available right after payment for one price. New countries join the subscription at no extra cost.",
|
||||
"crossTitle": "Need an address that is yours alone",
|
||||
"crossText": "The IP here is shared: it costs less and is enough for most people. If a service needs a permanent IP, look at Veilix dedicated plans.",
|
||||
"crossButton": "Dedicated IP",
|
||||
"leadTitle": "Need special terms",
|
||||
"leadText": "Team subscriptions, invoice payment or a rare country. Leave a request and we will get in touch.",
|
||||
"leadButton": "Send a request"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Locations",
|
||||
"description": "Countries in the VeilixVPN subscription: {countries} and a Russian address, with the status of each country.",
|
||||
"mapText": "{countries} and a Russian address come with the subscription.",
|
||||
"hint": "Tap a dot on the map to see the country and city",
|
||||
"scrollHint": "Scroll the map sideways",
|
||||
"ruLegend": "Russian address included",
|
||||
"gridTitle": "Every country in the subscription",
|
||||
"gridText": "One price for everything: {priceMonth} a month or {priceYear} a year.",
|
||||
"ruTitle": "Russian address in the subscription",
|
||||
"ruText": "It works both ways: from Russia, banks and government services see an ordinary local connection; from abroad, those same services open as if you were at home.",
|
||||
"leadTitle": "Need special terms",
|
||||
"leadText": "If the country you need is missing or you want a team subscription, write to us and we will get in touch.",
|
||||
"leadButton": "Send a request"
|
||||
},
|
||||
"support": {
|
||||
"title": "Support",
|
||||
"description": "VeilixVPN support: Telegram and email, answers about connecting, payment and the countries in your subscription.",
|
||||
"telegramText": "The fastest way. Voice messages and screenshots welcome.",
|
||||
"emailText": "For invoices, refunds and anything that needs to be in writing.",
|
||||
"faqTitle": "Frequent questions",
|
||||
"tabConnection": "Connecting",
|
||||
"tabBilling": "Payment and renewal",
|
||||
"tabLocations": "Locations and subscription",
|
||||
"formTitle": "Write to us",
|
||||
"formText": "If you already have a subscription, give the email it is registered to and we will see your configuration right away.",
|
||||
"topicConnection": "Topic: connecting",
|
||||
"topicBilling": "Topic: payment and renewal",
|
||||
"topicLocations": "Topic: locations and subscription",
|
||||
"topicOther": "Topic: something else"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"differs": {
|
||||
"q": "How is this different from an ordinary $3 VPN?",
|
||||
"a": "In a mass-market VPN hundreds of people share your address. If any of them ends up blacklisted, the restrictions apply to everyone. Here the address is yours alone, so services treat it like a home connection."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Why are some locations only ready in 24 hours?",
|
||||
"a": "We do not keep a pool of shared addresses. If there is no free address in the country you need, we prepare one for you personally and hand over access within a day. Your paid period starts when access is issued."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Do I have to configure anything myself?",
|
||||
"a": "No. In your account you download a ready-made file or scan a QR code with your phone. Routing is already set up: Russian sites go direct, foreign ones through your location."
|
||||
},
|
||||
"devices": {
|
||||
"q": "How many devices can I connect?",
|
||||
"a": "Up to five per key, all sharing the same address. If you need more devices or a second country, take another key with its own dedicated IP."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Why do I also need a Russian address?",
|
||||
"a": "It works both ways. From Russia, so that banks, government services and streaming see an ordinary local connection. From abroad, so the same services open as if you were at home."
|
||||
},
|
||||
"blocked": {
|
||||
"q": "What if a connection method stops working?",
|
||||
"a": "Your key includes both connection types at once. If one starts getting blocked, you switch to the other in the same account. We keep extending and updating the set of protocols."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "What do I do right after paying?",
|
||||
"a": "Sign in to your account by email, open your key and download the configuration or scan the QR code with your phone. Routing is already configured, nothing to type in by hand."
|
||||
},
|
||||
"devices": {
|
||||
"q": "How many devices can I connect?",
|
||||
"a": "Up to five per key, all sharing the same address. If you need more devices or a second country, take another key with its own dedicated IP."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "One connection method stopped working.",
|
||||
"a": "Both types are always configured in your key: WireGuard and Xray. Open your account and connect with the second configuration, no request needed. We keep extending and updating the set of protocols."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Do I need to configure anything manually?",
|
||||
"a": "No. The configuration already states that Russian sites go direct and foreign ones go through your location. There are no modes to switch while you work."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"firstMonth": {
|
||||
"q": "Why is the first month more expensive?",
|
||||
"a": "The first month covers preparation: assigning the address and configuring both protocols and the routing. After that you only pay the monthly fee."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "How does renewal work?",
|
||||
"a": "Automatically, once a month: from your balance first, then from the linked card. The expiry date is shown on the key page in your account, where you can also turn auto-renewal off."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Can I get a refund?",
|
||||
"a": "If the key failed to work through our fault, we refund in full within the first seven days. If you simply changed your mind, we refund the unused part of the month."
|
||||
},
|
||||
"methods": {
|
||||
"q": "What can I pay with?",
|
||||
"a": "Cards from Russian and foreign banks, and cryptocurrency. All methods are shown at checkout."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"vsVpn": {
|
||||
"q": "How does a dedicated IP differ from an ordinary VPN?",
|
||||
"a": "In a mass-market VPN hundreds of people share one address, and any restriction one of them earns applies to everyone. Your address belongs to you alone, so services treat it like a home connection."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Why are some locations only ready in 24 hours?",
|
||||
"a": "We do not keep a pool of shared addresses. If there is no free address in the country you need, we prepare one for you personally. Your paid period starts when access is issued."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Why does the plan include a Russian address?",
|
||||
"a": "It works both ways. From Russia, so that banks, government services and streaming see an ordinary local connection. From abroad, so the same services open as if you were at home."
|
||||
},
|
||||
"changeCountry": {
|
||||
"q": "Can I change the country later?",
|
||||
"a": "A key is tied to its country, so we don't change it. If you need another location, take a second key: it appears in the same account and renews separately. Keep the first one or cancel it whenever you like."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "How do I connect a new device?",
|
||||
"a": "Open the key, download any free WireGuard configuration or scan its QR code. Each configuration is for one device."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "What if the connection stops working?",
|
||||
"a": "Switch to Xray in the same key — it is set up in advance. If that does not help, write to support."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "How do I cancel renewal?",
|
||||
"a": "Turn off auto-renewal on the key page. The key keeps working until the end of the paid period."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"included": {
|
||||
"q": "What is included in the subscription?",
|
||||
"a": "Every country on the list, a Russian address and five devices. There is no separate charge per country or device."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Can I change the country as often as I want?",
|
||||
"a": "Yes. Switching takes a couple of seconds and costs nothing: the full location list stays open for the whole subscription."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Do I have to configure anything myself?",
|
||||
"a": "No. In your account you download a ready file or scan a QR code with your phone. Russian sites go direct, the rest travel through the chosen location."
|
||||
},
|
||||
"protocol": {
|
||||
"q": "Which protocol does the connection use?",
|
||||
"a": "Xray, with VLESS, Hysteria2 and XHTTP: the traffic looks like ordinary HTTPS, so the connection holds where older protocols get blocked. For simple cases there is WireGuard too."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "How is this different from a dedicated plan?",
|
||||
"a": "The address here is shared: several people use one server, which makes the subscription much cheaper. If you need an address reserved for you alone, that is the dedicated IP plan."
|
||||
},
|
||||
"payment": {
|
||||
"q": "How is it paid and renewed?",
|
||||
"a": "Monthly or yearly. The subscription renews automatically: from your balance first, then from the linked card. The yearly plan works out cheaper than paying monthly. You can turn off renewal in your account at any time."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "What do I do right after paying?",
|
||||
"a": "Sign in with your email and open your key: add the link to the Happ app or download the WireGuard configuration for the country you need. You can also scan the QR code with your phone. Routing is already set up."
|
||||
},
|
||||
"devices": {
|
||||
"q": "How many devices can I connect?",
|
||||
"a": "Up to five per key. Devices can use different countries: each connects to its own location."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "One connection method stopped working.",
|
||||
"a": "Switch to another protocol or change the location in the app — it takes a couple of seconds. We update the protocol settings as blocks appear."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Do I need to configure anything manually?",
|
||||
"a": "No. The configuration already says that Russian sites go direct and foreign ones through the chosen location. There are no modes to switch while you work."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"price": {
|
||||
"q": "How much does the subscription cost?",
|
||||
"a": "Prices are on the pricing page: monthly or yearly, and the yearly plan works out cheaper. There is no setup fee and no per-country charge."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "How is the subscription renewed?",
|
||||
"a": "Automatically, monthly or yearly: from your balance first, then from the linked card. The end date is shown in your account, where you can also turn auto-renewal off."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Can I get a refund?",
|
||||
"a": "If the subscription failed to work through our fault, we refund in full within the first seven days. If you simply changed your mind, we refund the unused part of the period."
|
||||
},
|
||||
"methods": {
|
||||
"q": "What can I pay with?",
|
||||
"a": "Cards from Russian and foreign banks, and cryptocurrency. All methods are shown at checkout."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"countries": {
|
||||
"q": "How many countries are included?",
|
||||
"a": "Every country on the locations list marked “Included”, plus a Russian address. They are not charged separately, and new countries join at no extra cost."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Can I change the country as often as I want?",
|
||||
"a": "Yes. Switching takes a couple of seconds and costs nothing: the full location list stays open for the whole subscription."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Why does the subscription include a Russian address?",
|
||||
"a": "It works both ways. From Russia, so that banks, government services and streaming see an ordinary local connection. From abroad, so the same services open as if you were at home."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "How is this different from a dedicated IP?",
|
||||
"a": "The address here is shared: several people use one server, so the subscription costs much less. If you need an address reserved for you alone, Veilix has separate dedicated-IP plans."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "How do I connect a new device?",
|
||||
"a": "Open the key and add the link to the Happ app or download the WireGuard configuration for the country you need. You can also scan the QR code."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "What if the connection stops working?",
|
||||
"a": "Change the location or protocol in the app — it takes a couple of seconds. If that does not help, write to support."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "How do I cancel renewal?",
|
||||
"a": "Turn off auto-renewal on the key page. The key keeps working until the end of the paid period."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "Terms of use",
|
||||
"description": "Terms of use of the service: account, keys and devices, payment, renewal and acceptable use."
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy policy",
|
||||
"description": "What data we keep and why. VPN servers keep no logs."
|
||||
},
|
||||
"refund": {
|
||||
"title": "Refund policy",
|
||||
"description": "When and how we refund a key."
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Cookie policy",
|
||||
"description": "Which cookies the site uses and why."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"chips": {
|
||||
"vless": "VLESS",
|
||||
"hysteria2": "Hysteria2",
|
||||
"xhttp": "XHTTP",
|
||||
"wireguard": "WireGuard"
|
||||
},
|
||||
"site": {
|
||||
"dedicated": {
|
||||
"hours": "Respondemos de 10:00 a 20:00, hora de Moscú, en días laborables.",
|
||||
"titleTemplate": "%s · Veilix",
|
||||
"description": "Una VPN personal con IP dedicada en el país que elijas: una sola dirección en todos tus dispositivos y una dirección RU incluida."
|
||||
},
|
||||
"mass": {
|
||||
"hours": "Respondemos de 10:00 a 20:00, hora de Moscú, en días laborables.",
|
||||
"titleTemplate": "%s · VeilixVPN",
|
||||
"description": "Una VPN con todos los países en una sola suscripción: dirección rusa, Xray y WireGuard, hasta 5 dispositivos."
|
||||
}
|
||||
},
|
||||
"tariffs": {
|
||||
"mass": {
|
||||
"month": {
|
||||
"description": "Cobro mensual, cancela cuando quieras"
|
||||
},
|
||||
"year": {
|
||||
"description": "{pricePerMonth} al mes, ahorras {savings} al año",
|
||||
"badge": "más ventajoso"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"title": "Veilix — tu VPN personal con IP dedicada",
|
||||
"description": "Una VPN personal con IP dedicada: una sola dirección en todos tus dispositivos, el país que elijas, dirección RU incluida, WireGuard y Xray.",
|
||||
"hero": {
|
||||
"badge": "IP dedicada, no compartida",
|
||||
"since": "En funcionamiento desde 2022",
|
||||
"title": "Tu VPN personal con IP dedicada",
|
||||
"text": "La dirección es solo tuya: la misma IP en todos tus dispositivos, en el país que elijas. Sin vecinos en tu dirección ni reputación ajena que heredar.",
|
||||
"primary": "Obtener VPN",
|
||||
"secondary": "Ver ubicaciones",
|
||||
"plateTitle": "Tu IP está reservada solo para ti",
|
||||
"platePrice": "{priceFirst} el primer mes, después {priceRenewal}",
|
||||
"plateDevices": "Hasta {devices} dispositivos",
|
||||
"plateRu": "Dirección RU incluida"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Qué significa en la práctica",
|
||||
"mainTitle": "IP dedicada",
|
||||
"mainText": "La dirección se asigna solo a ti. Los servicios ven a una persona normal del país elegido, no a cien personas detrás de una misma dirección.",
|
||||
"aiTitle": "Servicios de IA occidentales",
|
||||
"aiText": "Menor riesgo de bloqueo que con las VPN públicas, cuyas direcciones llevan tiempo en listas negras.",
|
||||
"sameTitle": "Una sola dirección en todos tus dispositivos",
|
||||
"sameText": "Portátil, teléfono y tableta se ven igual. Siempre estás en la misma ubicación, con tu propia IP.",
|
||||
"captchaTitle": "Menos verificaciones y captchas",
|
||||
"captchaText": "Las direcciones que comparte mucha gente activan la protección contra bots. Con una dirección propia, los servicios se comportan de forma más predecible.",
|
||||
"encryptedTitle": "Tráfico cifrado",
|
||||
"encryptedText": "En casa, en un hotel o en una cafetería, la conexión está protegida en cualquier parte del mundo.",
|
||||
"devicesTitle": "Hasta {devices} dispositivos por clave",
|
||||
"devicesText": "¿Necesitas más dispositivos u otro país? Añade una segunda clave con su propia IP.",
|
||||
"ruTitle": "Además, una dirección RU dedicada",
|
||||
"ruText": "Incluida en todos los planes. Funciona como conexión directa y como puente en ambos sentidos.",
|
||||
"protocolsTitle": "Dos tipos de conexión",
|
||||
"protocolsText": "Si un método deja de funcionar, el otro ya está configurado. La lista se amplía y se actualiza con el tiempo.",
|
||||
"chipWg": "WireGuard / Amnezia WG",
|
||||
"chipXray": "Xray: VLESS, Hysteria2, XHTTP",
|
||||
"routesTitle": "El tráfico va por el camino más corto",
|
||||
"routesText": "La ruta se elige automáticamente según la dirección del sitio. Los servicios rusos siguen funcionando, los extranjeros se abren y no tienes que cambiar nada.",
|
||||
"inRussia": "Estás en Rusia",
|
||||
"abroad": "Estás en el extranjero",
|
||||
"ruSites": "Sitios RU",
|
||||
"restOfWorld": "Resto del mundo",
|
||||
"globalSites": "Sitios globales",
|
||||
"viaRuDirect": "directamente a través del servidor RU",
|
||||
"tunnelToLocation": "por túnel hasta tu ubicación",
|
||||
"fromLocation": "directamente desde tu ubicación",
|
||||
"tunnelViaRu": "por túnel a través del servidor RU",
|
||||
"abroadNote": "Para quienes trabajan con Rusia desde otro país."
|
||||
},
|
||||
"pricing": {
|
||||
"ip": "IP dedicada en el país que elijas",
|
||||
"ru": "Dirección RU dedicada incluida",
|
||||
"devices": "Hasta {devices} dispositivos",
|
||||
"protocols": "2 tipos de conexión:",
|
||||
"listTitle": "Elige una ubicación",
|
||||
"buy": "Comprar",
|
||||
"lead": "Necesito condiciones personalizadas"
|
||||
},
|
||||
"faqTitle": "Preguntas"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Precios",
|
||||
"description": "IP dedicada en el país que elijas: {priceFirst} el primer mes y después {priceRenewal} al mes. Incluye dirección RU y hasta {devices} dispositivos.",
|
||||
"subline": "después {priceRenewal} al mes",
|
||||
"note": "El primer mes incluye la preparación de tu dirección y tus conexiones. Después solo pagas la cuota mensual, y puedes cancelar desde tu área de cliente.",
|
||||
"includedTitle": "Qué incluye",
|
||||
"included1": "IP dedicada en el país que elijas, solo para ti",
|
||||
"included2": "Dirección RU dedicada: conexión directa y puente en ambos sentidos",
|
||||
"included3": "Hasta {devices} dispositivos con la misma dirección",
|
||||
"included4": "Dos tipos de conexión: WireGuard y Xray",
|
||||
"included5": "Enrutamiento inteligente RU / internacional, sin cambios manuales",
|
||||
"gridTitle": "Elige una ubicación",
|
||||
"gridText1": "En algunos países ya hay una dirección libre esperándote y recibes el acceso al instante. Si no hay ninguna libre, preparamos una solo para ti: tarda hasta 24 horas, y verás este estado con claridad antes de pagar.",
|
||||
"gridText2": "El precio base es de {priceFirst} el primer mes y {priceRenewal} los meses siguientes. Las ubicaciones poco comunes y lejanas cuestan más; el precio aparece en cada tarjeta.",
|
||||
"severalTitle": "¿Necesitas varias ubicaciones?",
|
||||
"severalText": "Cada clave es una IP dedicada distinta. Compra una segunda clave en otro país: las dos estarán en la misma área de cliente y se renovarán de forma independiente.",
|
||||
"leadTitle": "¿Necesitas condiciones personalizadas?",
|
||||
"leadText": "Un equipo, muchas direcciones, un país poco común o tu propio esquema de enrutamiento. Déjanos una solicitud y nos pondremos en contacto contigo.",
|
||||
"leadButton": "Enviar solicitud"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Ubicaciones",
|
||||
"description": "Ubicaciones de Veilix: {countries} para elegir, con disponibilidad y precios. Todos los planes incluyen una dirección rusa.",
|
||||
"mapText": "{countries} para elegir y una dirección rusa incluida en todos los planes.",
|
||||
"hint": "Toca un punto del mapa para ver el país y su precio",
|
||||
"scrollHint": "Desliza el mapa hacia los lados",
|
||||
"ruLegend": "Todos los planes incluyen una dirección RU",
|
||||
"gridTitle": "Elige un país",
|
||||
"gridText": "El precio base es de {priceFirst} el primer mes y {priceRenewal} los meses siguientes. Las ubicaciones poco comunes y lejanas cuestan más; el precio aparece en cada tarjeta.",
|
||||
"ruTitle": "Una dirección rusa en todos los planes",
|
||||
"ruText": "Funciona en ambos sentidos: desde Rusia, los bancos y los servicios gubernamentales ven una conexión local normal; desde el extranjero, esos mismos servicios se abren como si estuvieras en casa.",
|
||||
"leadTitle": "¿Necesitas condiciones personalizadas?",
|
||||
"leadText": "Si tu país no está en la lista, necesitas muchas direcciones o tienes un esquema de enrutamiento propio, escríbenos: te indicaremos los plazos y lo prepararemos para ti.",
|
||||
"leadButton": "Enviar solicitud"
|
||||
},
|
||||
"support": {
|
||||
"title": "Soporte",
|
||||
"description": "Soporte de Veilix: Telegram y correo electrónico, respuestas sobre la conexión, el pago y las ubicaciones.",
|
||||
"telegramText": "La vía más rápida. Puedes enviar mensajes de voz y capturas de pantalla.",
|
||||
"emailText": "Para facturas, reembolsos y todo lo que deba quedar por escrito.",
|
||||
"faqTitle": "Preguntas frecuentes",
|
||||
"tabConnection": "Conexión",
|
||||
"tabBilling": "Pago y renovación",
|
||||
"tabLocations": "Ubicaciones e IP",
|
||||
"formTitle": "Escríbenos",
|
||||
"formText": "Si ya tienes una clave, indica el correo electrónico con el que está registrada: así veremos tu configuración enseguida.",
|
||||
"topicConnection": "Tema: conexión",
|
||||
"topicBilling": "Tema: pago y renovación",
|
||||
"topicLocations": "Tema: ubicaciones e IP",
|
||||
"topicOther": "Tema: otro asunto"
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"title": "VeilixVPN — una VPN que nunca tendrás que desactivar",
|
||||
"description": "Todos los países en una sola suscripción: dirección rusa, Xray y WireGuard, hasta 5 dispositivos. Cambia de país tantas veces como quieras.",
|
||||
"hero": {
|
||||
"badge": "Todos los países en una suscripción",
|
||||
"since": "En funcionamiento desde 2022",
|
||||
"title": "Una VPN que nunca tendrás que desactivar",
|
||||
"text": "La suscripción incluye {countries} y una dirección rusa. Cambia de país tantas veces como quieras, en {devices} dispositivos a la vez.",
|
||||
"primary": "Obtener VPN",
|
||||
"secondary": "Ver ubicaciones",
|
||||
"plateTitle": "{countries}, cambia con un solo toque",
|
||||
"platePrice": "{priceMonth} al mes o {priceYear} al año",
|
||||
"plateDevices": "Hasta {devices} dispositivos",
|
||||
"plateRu": "Dirección rusa incluida"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Qué significa en la práctica",
|
||||
"mainTitle": "Todas las ubicaciones en la suscripción",
|
||||
"mainText": "Toda la lista de países está disponible. Elige un país en la aplicación: la conexión se restablece en un par de segundos y cambiar no cuesta nada extra.",
|
||||
"countriesTitle": "{countries}",
|
||||
"countriesText": "Europa, Estados Unidos, Asia, Oriente Medio y Sudamérica. La lista sigue creciendo.",
|
||||
"devicesTitle": "{devices} dispositivos a la vez",
|
||||
"devicesText": "Teléfono, portátil, tableta y dos dispositivos más funcionan al mismo tiempo.",
|
||||
"ruSitesTitle": "Acceso directo a sitios rusos",
|
||||
"ruSitesText": "Los bancos y los servicios gubernamentales funcionan con normalidad, sin necesidad de desactivar la VPN.",
|
||||
"encryptedTitle": "Tráfico cifrado",
|
||||
"encryptedText": "En casa, en un hotel o en una cafetería, la conexión está protegida en cualquier parte del mundo.",
|
||||
"devicesCardTitle": "{devices} dispositivos por suscripción",
|
||||
"devicesCardText": "Una sola suscripción para todos tus dispositivos. Funcionan al mismo tiempo y en países distintos.",
|
||||
"ruTitle": "Dirección rusa incluida",
|
||||
"ruText": "La dirección rusa forma parte de la suscripción: los bancos, los servicios gubernamentales y las plataformas de streaming se abren como si estuvieras en casa.",
|
||||
"protocolsTitle": "Xray y WireGuard",
|
||||
"protocolsText": "Xray hace que el tráfico parezca HTTPS normal y sigue funcionando donde los protocolos más antiguos ya no lo hacen. WireGuard es rápido y sencillo donde no hay bloqueos.",
|
||||
"routesTitle": "El tráfico va por el camino más corto",
|
||||
"routesText": "La ruta se elige automáticamente según la dirección del sitio. Los servicios rusos siguen funcionando, los extranjeros se abren y no tienes que cambiar nada.",
|
||||
"inRussia": "Estás en Rusia",
|
||||
"abroad": "Estás en el extranjero",
|
||||
"ruSites": "Sitios RU",
|
||||
"restOfWorld": "Resto del mundo",
|
||||
"globalSites": "Sitios globales",
|
||||
"viaRuDirect": "directamente a través del servidor RU",
|
||||
"tunnelToLocation": "por túnel hasta la ubicación elegida",
|
||||
"fromLocation": "directamente desde la ubicación elegida",
|
||||
"tunnelViaRu": "por túnel a través del servidor RU",
|
||||
"abroadNote": "Para quienes trabajan con Rusia desde otro país."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Una suscripción, todos los países",
|
||||
"text": "{countries}, una dirección rusa y {devices} dispositivos.",
|
||||
"countries": "{countries} y una dirección rusa",
|
||||
"devices": "Hasta {devices} dispositivos a la vez",
|
||||
"protocols": "Xray y WireGuard",
|
||||
"listTitle": "Países incluidos",
|
||||
"buy": "Suscribirse",
|
||||
"note": "Tarjetas rusas e internacionales, además de criptomonedas. El acceso se activa justo después del pago."
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "¿Necesitas una dirección solo para ti?",
|
||||
"text": "Aquí la IP es compartida: cuesta menos y es suficiente para la mayoría. Si un servicio requiere una IP fija, consulta los planes con IP dedicada de Veilix.",
|
||||
"button": "IP dedicada"
|
||||
},
|
||||
"faqTitle": "Preguntas"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Precios",
|
||||
"description": "Todos los países en una sola suscripción: {priceMonth} al mes o {priceYear} al año. Dirección rusa y hasta {devices} dispositivos.",
|
||||
"subline": "o {priceYear} al año — {pricePerMonth} al mes",
|
||||
"note": "Un solo precio por todo. Puedes cancelar la renovación en tu área de cliente.",
|
||||
"button": "Suscribirse",
|
||||
"includedTitle": "Qué incluye",
|
||||
"included1": "{countries}, cambia cuando quieras",
|
||||
"included2": "Dirección rusa: conexión directa y puente en ambos sentidos",
|
||||
"included3": "Hasta {devices} dispositivos a la vez",
|
||||
"included4": "Xray y WireGuard",
|
||||
"gridTitle": "Países incluidos",
|
||||
"gridText1": "Los países marcados como «Incluido» están disponibles justo después del pago, por un único precio. Los países nuevos se añaden a la suscripción sin cargo adicional.",
|
||||
"crossTitle": "¿Necesitas una dirección solo para ti?",
|
||||
"crossText": "Aquí la IP es compartida: cuesta menos y es suficiente para la mayoría. Si un servicio requiere una IP fija, consulta los planes con IP dedicada de Veilix.",
|
||||
"crossButton": "IP dedicada",
|
||||
"leadTitle": "¿Necesitas condiciones especiales?",
|
||||
"leadText": "Suscripciones para equipos, pago mediante factura o un país poco común. Déjanos una solicitud y nos pondremos en contacto contigo.",
|
||||
"leadButton": "Enviar solicitud"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Ubicaciones",
|
||||
"description": "Qué incluye la suscripción de VeilixVPN: {countries} y una dirección rusa, con el estado de cada país.",
|
||||
"mapText": "La suscripción incluye {countries} y una dirección rusa.",
|
||||
"hint": "Toca un punto del mapa para ver el país y la ciudad",
|
||||
"scrollHint": "Desliza el mapa hacia los lados",
|
||||
"ruLegend": "Dirección rusa incluida",
|
||||
"gridTitle": "Todos los países de la suscripción",
|
||||
"gridText": "Un solo precio por todo: {priceMonth} al mes o {priceYear} al año.",
|
||||
"ruTitle": "Dirección rusa en la suscripción",
|
||||
"ruText": "Funciona en ambos sentidos: desde Rusia, los bancos y los servicios gubernamentales ven una conexión local normal; desde el extranjero, esos mismos servicios se abren como si estuvieras en casa.",
|
||||
"leadTitle": "¿Necesitas condiciones especiales?",
|
||||
"leadText": "Si el país que necesitas no está en la lista o quieres una suscripción para tu equipo, escríbenos y nos pondremos en contacto contigo.",
|
||||
"leadButton": "Enviar solicitud"
|
||||
},
|
||||
"support": {
|
||||
"title": "Soporte",
|
||||
"description": "Soporte de VeilixVPN: Telegram y correo electrónico, respuestas sobre la conexión, el pago y los países de tu suscripción.",
|
||||
"telegramText": "La vía más rápida. Puedes enviar mensajes de voz y capturas de pantalla.",
|
||||
"emailText": "Para facturas, reembolsos y todo lo que deba quedar por escrito.",
|
||||
"faqTitle": "Preguntas frecuentes",
|
||||
"tabConnection": "Conexión",
|
||||
"tabBilling": "Pago y renovación",
|
||||
"tabLocations": "Ubicaciones y suscripción",
|
||||
"formTitle": "Escríbenos",
|
||||
"formText": "Si ya tienes una suscripción, indica el correo electrónico con el que está registrada: así veremos tu configuración enseguida.",
|
||||
"topicConnection": "Tema: conexión",
|
||||
"topicBilling": "Tema: pago y renovación",
|
||||
"topicLocations": "Tema: ubicaciones y suscripción",
|
||||
"topicOther": "Tema: otro asunto"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"differs": {
|
||||
"q": "¿En qué se diferencia de una VPN normal de 3 dólares?",
|
||||
"a": "En una VPN de uso masivo, cientos de personas comparten tu dirección. Si alguna de ellas acaba en una lista negra, las restricciones afectan a todos. Aquí la dirección es solo tuya, así que los servicios la tratan como una conexión doméstica."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "¿Por qué algunas ubicaciones tardan 24 horas en estar listas?",
|
||||
"a": "No mantenemos una reserva de direcciones compartidas. Si en el país que necesitas no hay ninguna dirección libre, preparamos una solo para ti y te damos acceso en un plazo de un día. El período pagado empieza cuando recibes el acceso."
|
||||
},
|
||||
"setup": {
|
||||
"q": "¿Tengo que configurar algo por mi cuenta?",
|
||||
"a": "No. En tu área de cliente descargas un archivo ya preparado o escaneas un código QR con el teléfono. El enrutamiento ya está configurado: los sitios rusos se abren directamente y los extranjeros pasan por tu ubicación."
|
||||
},
|
||||
"devices": {
|
||||
"q": "¿Cuántos dispositivos puedo conectar?",
|
||||
"a": "Hasta cinco por clave, todos con la misma dirección. Si necesitas más dispositivos u otro país, compra otra clave con su propia IP dedicada."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "¿Para qué necesito también una dirección rusa?",
|
||||
"a": "Funciona en ambos sentidos. Desde Rusia, para que los bancos, los servicios gubernamentales y las plataformas de streaming vean una conexión local normal. Desde el extranjero, para que esos mismos servicios se abran como si estuvieras en casa."
|
||||
},
|
||||
"blocked": {
|
||||
"q": "¿Qué pasa si un método de conexión deja de funcionar?",
|
||||
"a": "Tu clave incluye los dos tipos de conexión a la vez. Si uno empieza a bloquearse, cambias al otro desde la misma área de cliente. Seguimos ampliando y actualizando el conjunto de protocolos."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "¿Qué hago justo después de pagar?",
|
||||
"a": "Inicia sesión en tu área de cliente con tu correo electrónico, abre tu clave y descarga la configuración o escanea el código QR con el teléfono. El enrutamiento ya está configurado: no tienes que escribir nada a mano."
|
||||
},
|
||||
"devices": {
|
||||
"q": "¿Cuántos dispositivos puedo conectar?",
|
||||
"a": "Hasta cinco por clave, todos con la misma dirección. Si necesitas más dispositivos u otro país, compra otra clave con su propia IP dedicada."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Un método de conexión ya no funciona.",
|
||||
"a": "Tu clave siempre tiene configurados los dos tipos: WireGuard y Xray. Abre tu área de cliente y conéctate con la otra configuración, sin tener que pedir nada. Seguimos ampliando y actualizando el conjunto de protocolos."
|
||||
},
|
||||
"manual": {
|
||||
"q": "¿Tengo que configurar algo manualmente?",
|
||||
"a": "No. La configuración ya indica que los sitios rusos se abren directamente y los extranjeros pasan por tu ubicación. No hay modos que cambiar mientras trabajas."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"firstMonth": {
|
||||
"q": "¿Por qué el primer mes es más caro?",
|
||||
"a": "El primer mes incluye la preparación: asignar la dirección y configurar ambos protocolos y el enrutamiento. Después solo pagas la cuota mensual."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "¿Cómo funciona la renovación?",
|
||||
"a": "Es automática, una vez al mes: primero se cobra de tu saldo y después de la tarjeta vinculada. La fecha de vencimiento aparece en la página de la clave, en tu área de cliente; ahí también puedes desactivar la renovación automática."
|
||||
},
|
||||
"refund": {
|
||||
"q": "¿Puedo pedir un reembolso?",
|
||||
"a": "Si la clave no funciona por un fallo nuestro, te devolvemos todo el dinero en los primeros siete días. Si simplemente cambias de opinión, te devolvemos la parte no utilizada del mes."
|
||||
},
|
||||
"methods": {
|
||||
"q": "¿Con qué puedo pagar?",
|
||||
"a": "Con tarjetas de bancos rusos y extranjeros, y con criptomonedas. Todos los métodos se muestran al finalizar la compra."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"vsVpn": {
|
||||
"q": "¿En qué se diferencia una IP dedicada de una VPN normal?",
|
||||
"a": "En una VPN de uso masivo, cientos de personas comparten una misma dirección, y cualquier restricción que reciba una de ellas afecta a todas. Tu dirección es solo tuya, así que los servicios la tratan como una conexión doméstica."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "¿Por qué algunas ubicaciones tardan 24 horas en estar listas?",
|
||||
"a": "No mantenemos una reserva de direcciones compartidas. Si en el país que necesitas no hay ninguna dirección libre, preparamos una solo para ti. El período pagado empieza cuando recibes el acceso."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "¿Por qué el plan incluye una dirección rusa?",
|
||||
"a": "Funciona en ambos sentidos. Desde Rusia, para que los bancos, los servicios gubernamentales y las plataformas de streaming vean una conexión local normal. Desde el extranjero, para que esos mismos servicios se abran como si estuvieras en casa."
|
||||
},
|
||||
"changeCountry": {
|
||||
"q": "¿Puedo cambiar de país más adelante?",
|
||||
"a": "Cada clave está vinculada a su país, así que no lo cambiamos. Si necesitas otra ubicación, compra una segunda clave: aparecerá en la misma área de cliente y se renovará por separado. Puedes conservar la primera o cancelarla cuando quieras."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "¿Cómo conecto un dispositivo nuevo?",
|
||||
"a": "Abre la clave y descarga cualquier configuración de WireGuard libre o escanea su código QR. Cada configuración es para un solo dispositivo."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "¿Qué hago si la conexión deja de funcionar?",
|
||||
"a": "Cambia a Xray en la misma clave: ya está configurado. Si no se soluciona, escribe a soporte."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "¿Cómo cancelo la renovación?",
|
||||
"a": "Desactiva la renovación automática en la página de la clave. La clave seguirá funcionando hasta el final del período pagado."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"included": {
|
||||
"q": "¿Qué incluye la suscripción?",
|
||||
"a": "Todos los países de la lista, una dirección rusa y cinco dispositivos. No se cobra aparte por país ni por dispositivo."
|
||||
},
|
||||
"switch": {
|
||||
"q": "¿Puedo cambiar de país tantas veces como quiera?",
|
||||
"a": "Sí. El cambio tarda un par de segundos y no cuesta nada: toda la lista de ubicaciones está disponible mientras dure la suscripción."
|
||||
},
|
||||
"setup": {
|
||||
"q": "¿Tengo que configurar algo por mi cuenta?",
|
||||
"a": "No. En tu área de cliente descargas un archivo ya preparado o escaneas un código QR con el teléfono. Los sitios rusos se abren directamente y el resto pasa por la ubicación elegida."
|
||||
},
|
||||
"protocol": {
|
||||
"q": "¿Qué protocolo usa la conexión?",
|
||||
"a": "Xray, con VLESS, Hysteria2 y XHTTP: el tráfico parece HTTPS normal, así que la conexión se mantiene donde los protocolos más antiguos se bloquean. Para casos sencillos también está WireGuard."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "¿En qué se diferencia de un plan con IP dedicada?",
|
||||
"a": "Aquí la dirección es compartida: varias personas usan un mismo servidor, por eso la suscripción es mucho más barata. Si necesitas una dirección reservada solo para ti, para eso está el plan con IP dedicada."
|
||||
},
|
||||
"payment": {
|
||||
"q": "¿Cómo se paga y se renueva?",
|
||||
"a": "Cada mes o cada año. La suscripción se renueva automáticamente: primero se cobra de tu saldo y después de la tarjeta vinculada. El plan anual sale más barato que pagar mes a mes. Puedes desactivar la renovación en tu área de cliente en cualquier momento."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "¿Qué hago justo después de pagar?",
|
||||
"a": "Inicia sesión con tu correo electrónico y abre tu clave: añade el enlace a la aplicación Happ o descarga la configuración de WireGuard del país que necesites. También puedes escanear el código QR con el teléfono. El enrutamiento ya está configurado."
|
||||
},
|
||||
"devices": {
|
||||
"q": "¿Cuántos dispositivos puedo conectar?",
|
||||
"a": "Hasta cinco por clave. Los dispositivos pueden usar países distintos: cada uno se conecta a su propia ubicación."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Un método de conexión ya no funciona.",
|
||||
"a": "Cambia a otro protocolo o a otra ubicación en la aplicación: tarda un par de segundos. Actualizamos la configuración de los protocolos a medida que aparecen bloqueos."
|
||||
},
|
||||
"manual": {
|
||||
"q": "¿Tengo que configurar algo manualmente?",
|
||||
"a": "No. La configuración ya indica que los sitios rusos se abren directamente y los extranjeros pasan por la ubicación elegida. No hay modos que cambiar mientras trabajas."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"price": {
|
||||
"q": "¿Cuánto cuesta la suscripción?",
|
||||
"a": "Consulta la página de precios: puedes pagar cada mes o cada año, y el plan anual sale más barato. No hay cargo de activación ni se cobra por país."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "¿Cómo se renueva la suscripción?",
|
||||
"a": "Automáticamente, cada mes o cada año: primero se cobra de tu saldo y después de la tarjeta vinculada. La fecha de finalización aparece en tu área de cliente; ahí también puedes desactivar la renovación automática."
|
||||
},
|
||||
"refund": {
|
||||
"q": "¿Puedo pedir un reembolso?",
|
||||
"a": "Si la suscripción no funciona por un fallo nuestro, te devolvemos todo el dinero en los primeros siete días. Si simplemente cambias de opinión, te devolvemos la parte no utilizada del período."
|
||||
},
|
||||
"methods": {
|
||||
"q": "¿Con qué puedo pagar?",
|
||||
"a": "Con tarjetas de bancos rusos y extranjeros, y con criptomonedas. Todos los métodos se muestran al finalizar la compra."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"countries": {
|
||||
"q": "¿Cuántos países están incluidos?",
|
||||
"a": "Todos los países de la lista de ubicaciones marcados como «Incluido», además de una dirección rusa. No se cobran por separado, y los países nuevos se añaden sin cargo adicional."
|
||||
},
|
||||
"switch": {
|
||||
"q": "¿Puedo cambiar de país tantas veces como quiera?",
|
||||
"a": "Sí. El cambio tarda un par de segundos y no cuesta nada: toda la lista de ubicaciones está disponible mientras dure la suscripción."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "¿Por qué la suscripción incluye una dirección rusa?",
|
||||
"a": "Funciona en ambos sentidos. Desde Rusia, para que los bancos, los servicios gubernamentales y las plataformas de streaming vean una conexión local normal. Desde el extranjero, para que esos mismos servicios se abran como si estuvieras en casa."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "¿En qué se diferencia de una IP dedicada?",
|
||||
"a": "Aquí la dirección es compartida: varias personas usan un mismo servidor, por eso la suscripción cuesta mucho menos. Si necesitas una dirección reservada solo para ti, Veilix tiene planes aparte con IP dedicada."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "¿Cómo conecto un dispositivo nuevo?",
|
||||
"a": "Abre la clave y añade el enlace a la aplicación Happ o descarga la configuración de WireGuard del país que necesites. También puedes escanear el código QR."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "¿Qué hago si la conexión deja de funcionar?",
|
||||
"a": "Cambia de ubicación o de protocolo en la aplicación: tarda un par de segundos. Si no se soluciona, escribe a soporte."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "¿Cómo cancelo la renovación?",
|
||||
"a": "Desactiva la renovación automática en la página de la clave. La clave seguirá funcionando hasta el final del período pagado."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "Condiciones de uso",
|
||||
"description": "Condiciones de uso del servicio: cuenta, claves y dispositivos, pago, renovación y uso aceptable."
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Política de privacidad",
|
||||
"description": "Qué datos guardamos y para qué. Los servidores VPN no guardan registros."
|
||||
},
|
||||
"refund": {
|
||||
"title": "Política de reembolsos",
|
||||
"description": "Cuándo y cómo devolvemos el dinero de una clave."
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Política de cookies",
|
||||
"description": "Qué cookies usa el sitio y para qué."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"chips": {
|
||||
"vless": "VLESS",
|
||||
"hysteria2": "Hysteria2",
|
||||
"xhttp": "XHTTP",
|
||||
"wireguard": "WireGuard"
|
||||
},
|
||||
"site": {
|
||||
"dedicated": {
|
||||
"hours": "Nous répondons de 10 h à 20 h, heure de Moscou, les jours ouvrés.",
|
||||
"titleTemplate": "%s · Veilix",
|
||||
"description": "Un VPN personnel avec une IP dédiée dans le pays de votre choix : une seule adresse sur tous vos appareils, adresse RU incluse."
|
||||
},
|
||||
"mass": {
|
||||
"hours": "Nous répondons de 10 h à 20 h, heure de Moscou, les jours ouvrés.",
|
||||
"titleTemplate": "%s · VeilixVPN",
|
||||
"description": "Un VPN avec tous les pays dans un seul abonnement : une adresse russe, Xray et WireGuard, jusqu’à 5 appareils."
|
||||
}
|
||||
},
|
||||
"tariffs": {
|
||||
"mass": {
|
||||
"month": {
|
||||
"description": "Facturation mensuelle, résiliation à tout moment"
|
||||
},
|
||||
"year": {
|
||||
"description": "{pricePerMonth} par mois, {savings} d’économie par an",
|
||||
"badge": "plus avantageux"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"title": "Veilix — votre VPN personnel avec une IP dédiée",
|
||||
"description": "Un VPN personnel avec une IP dédiée : une seule adresse sur tous vos appareils, le pays de votre choix, une adresse RU incluse, WireGuard et Xray.",
|
||||
"hero": {
|
||||
"badge": "Une IP dédiée, pas partagée",
|
||||
"since": "En service depuis 2022",
|
||||
"title": "Votre VPN personnel avec une IP dédiée",
|
||||
"text": "L’adresse n’appartient qu’à vous : la même IP sur tous vos appareils, dans le pays de votre choix. Pas de voisins sur votre adresse, pas de réputation héritée d’autrui.",
|
||||
"primary": "Obtenir un VPN",
|
||||
"secondary": "Voir les emplacements",
|
||||
"plateTitle": "Votre IP n’est attribuée qu’à vous",
|
||||
"platePrice": "{priceFirst} le premier mois, puis {priceRenewal}",
|
||||
"plateDevices": "Jusqu’à {devices} appareils",
|
||||
"plateRu": "Adresse RU incluse"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Ce que cela change en pratique",
|
||||
"mainTitle": "IP dédiée",
|
||||
"mainText": "L’adresse n’est attribuée qu’à vous. Les services voient une personne ordinaire du pays choisi, et non une centaine de personnes derrière une seule adresse.",
|
||||
"aiTitle": "Services d’IA occidentaux",
|
||||
"aiText": "Moins de risques de blocage qu’avec les VPN publics, dont les adresses figurent depuis longtemps sur des listes noires.",
|
||||
"sameTitle": "Une seule adresse sur tous vos appareils",
|
||||
"sameText": "Ordinateur portable, téléphone et tablette apparaissent de la même manière. Vous restez toujours au même emplacement, avec votre propre IP.",
|
||||
"captchaTitle": "Moins de vérifications et de captchas",
|
||||
"captchaText": "Les adresses utilisées par de nombreuses personnes déclenchent les protections anti-bots. Avec une adresse personnelle, les services réagissent de façon plus prévisible.",
|
||||
"encryptedTitle": "Trafic chiffré",
|
||||
"encryptedText": "À la maison, à l’hôtel ou au café, la connexion est protégée partout dans le monde.",
|
||||
"devicesTitle": "Jusqu’à {devices} appareils par clé",
|
||||
"devicesText": "Besoin de plus d’appareils ou d’un autre pays ? Ajoutez une deuxième clé avec sa propre IP.",
|
||||
"ruTitle": "Une adresse RU dédiée en plus",
|
||||
"ruText": "Incluse dans chaque forfait. Elle sert à la fois de connexion directe et de pont dans les deux sens.",
|
||||
"protocolsTitle": "Deux types de connexion",
|
||||
"protocolsText": "Si une méthode cesse de fonctionner, la seconde est déjà configurée. La liste est complétée et mise à jour au fil du temps.",
|
||||
"chipWg": "WireGuard / Amnezia WG",
|
||||
"chipXray": "Xray : VLESS, Hysteria2, XHTTP",
|
||||
"routesTitle": "Le trafic prend le chemin le plus court",
|
||||
"routesText": "Le chemin est choisi automatiquement selon l’adresse du site. Les services russes continuent de fonctionner, les services étrangers s’ouvrent, et vous n’avez jamais rien à changer.",
|
||||
"inRussia": "Vous êtes en Russie",
|
||||
"abroad": "Vous êtes à l’étranger",
|
||||
"ruSites": "Sites RU",
|
||||
"restOfWorld": "Reste du monde",
|
||||
"globalSites": "Sites mondiaux",
|
||||
"viaRuDirect": "directement via le serveur RU",
|
||||
"tunnelToLocation": "par tunnel vers votre emplacement",
|
||||
"fromLocation": "directement depuis votre emplacement",
|
||||
"tunnelViaRu": "par tunnel via le serveur RU",
|
||||
"abroadNote": "Pour ceux qui travaillent avec la Russie depuis un autre pays."
|
||||
},
|
||||
"pricing": {
|
||||
"ip": "IP dédiée dans le pays de votre choix",
|
||||
"ru": "Adresse RU dédiée incluse",
|
||||
"devices": "Jusqu’à {devices} appareils",
|
||||
"protocols": "2 types de connexion :",
|
||||
"listTitle": "Choisissez un emplacement",
|
||||
"buy": "Acheter",
|
||||
"lead": "Besoin de conditions sur mesure ?"
|
||||
},
|
||||
"faqTitle": "Questions"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Tarifs",
|
||||
"description": "Une IP dédiée dans le pays de votre choix : {priceFirst} le premier mois, puis {priceRenewal} par mois. Une adresse RU et jusqu’à {devices} appareils inclus.",
|
||||
"subline": "puis {priceRenewal} par mois",
|
||||
"note": "Le premier mois couvre la préparation de votre adresse et de vos connexions. Ensuite, vous ne payez que la mensualité, et vous pouvez résilier depuis votre espace client.",
|
||||
"includedTitle": "Ce qui est inclus",
|
||||
"included1": "Une IP dédiée dans le pays de votre choix, rien qu’à vous",
|
||||
"included2": "Une adresse RU dédiée : connexion directe et pont dans les deux sens",
|
||||
"included3": "Jusqu’à {devices} appareils avec la même adresse",
|
||||
"included4": "Deux types de connexion : WireGuard et Xray",
|
||||
"included5": "Routage intelligent RU / international, sans bascule manuelle",
|
||||
"gridTitle": "Choisissez un emplacement",
|
||||
"gridText1": "Dans certains pays, une adresse libre vous attend déjà et l’accès est délivré immédiatement. S’il n’y en a aucune, nous en préparons une spécialement pour vous : cela prend jusqu’à 24 heures, et ce statut est affiché en toute transparence avant le paiement.",
|
||||
"gridText2": "Le prix de base est de {priceFirst} le premier mois, puis de {priceRenewal}. Les emplacements rares et lointains sont plus chers : leur prix est indiqué sur leur fiche.",
|
||||
"severalTitle": "Besoin de plusieurs emplacements ?",
|
||||
"severalText": "Chaque clé correspond à une IP dédiée distincte. Prenez une deuxième clé dans un autre pays : les deux sont réunies dans le même espace client et se renouvellent indépendamment.",
|
||||
"leadTitle": "Besoin de conditions sur mesure ?",
|
||||
"leadText": "Une équipe, de nombreuses adresses, un pays rare ou votre propre scénario de routage. Laissez une demande et nous vous contacterons.",
|
||||
"leadButton": "Envoyer une demande"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Emplacements",
|
||||
"description": "Emplacements Veilix : {countries} au choix, avec leur disponibilité et leurs prix. Une adresse russe est incluse dans chaque forfait.",
|
||||
"mapText": "{countries} au choix, et une adresse russe incluse dans chaque forfait.",
|
||||
"hint": "Sélectionnez un point sur la carte pour voir le pays et son prix",
|
||||
"scrollHint": "Faites défiler la carte horizontalement",
|
||||
"ruLegend": "Une adresse RU est incluse dans chaque forfait",
|
||||
"gridTitle": "Choisissez un pays",
|
||||
"gridText": "Le prix de base est de {priceFirst} le premier mois, puis de {priceRenewal}. Les emplacements rares et lointains sont plus chers : leur prix est indiqué sur leur fiche.",
|
||||
"ruTitle": "Une adresse russe dans chaque forfait",
|
||||
"ruText": "Elle fonctionne dans les deux sens : depuis la Russie, les banques et les services publics voient une connexion locale ordinaire ; depuis l’étranger, ces mêmes services s’ouvrent comme si vous étiez chez vous.",
|
||||
"leadTitle": "Besoin de conditions sur mesure ?",
|
||||
"leadText": "Votre pays ne figure pas dans la liste, vous avez besoin de nombreuses adresses ou d’un routage sur mesure ? Écrivez-nous : nous vous indiquerons les délais et préparerons tout pour vous.",
|
||||
"leadButton": "Envoyer une demande"
|
||||
},
|
||||
"support": {
|
||||
"title": "Assistance",
|
||||
"description": "Assistance Veilix : Telegram et e-mail, réponses aux questions sur la connexion, le paiement et les emplacements.",
|
||||
"telegramText": "Le moyen le plus rapide. Messages vocaux et captures d’écran bienvenus.",
|
||||
"emailText": "Pour les factures, les remboursements et tout ce qui doit se faire par écrit.",
|
||||
"faqTitle": "Questions fréquentes",
|
||||
"tabConnection": "Connexion",
|
||||
"tabBilling": "Paiement et renouvellement",
|
||||
"tabLocations": "Emplacements et IP",
|
||||
"formTitle": "Écrivez-nous",
|
||||
"formText": "Si vous avez déjà acheté une clé, indiquez l’e-mail utilisé pour l’achat : nous verrons ainsi directement votre configuration.",
|
||||
"topicConnection": "Sujet : connexion",
|
||||
"topicBilling": "Sujet : paiement et renouvellement",
|
||||
"topicLocations": "Sujet : emplacements et IP",
|
||||
"topicOther": "Sujet : autre"
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"title": "VeilixVPN — un VPN que vous n’avez jamais à désactiver",
|
||||
"description": "Tous les pays dans un seul abonnement : une adresse russe, Xray et WireGuard, jusqu’à 5 appareils. Changez de pays aussi souvent que vous le souhaitez.",
|
||||
"hero": {
|
||||
"badge": "Tous les pays dans un seul abonnement",
|
||||
"since": "En service depuis 2022",
|
||||
"title": "Un VPN que vous n’avez jamais à désactiver",
|
||||
"text": "{countries} et une adresse russe sont inclus dans l’abonnement. Changez de pays aussi souvent que vous le souhaitez, sur {devices} appareils à la fois.",
|
||||
"primary": "Obtenir un VPN",
|
||||
"secondary": "Voir les emplacements",
|
||||
"plateTitle": "{countries}, changement en un clic",
|
||||
"platePrice": "{priceMonth} par mois ou {priceYear} par an",
|
||||
"plateDevices": "Jusqu’à {devices} appareils",
|
||||
"plateRu": "Adresse russe incluse"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Ce que cela change en pratique",
|
||||
"mainTitle": "Tous les emplacements dans l’abonnement",
|
||||
"mainText": "Toute la liste des pays est ouverte. Choisissez un pays dans l’application : la connexion se reconfigure en quelques secondes, et le changement ne coûte rien de plus.",
|
||||
"countriesTitle": "{countries}",
|
||||
"countriesText": "Europe, États-Unis, Asie, Moyen-Orient et Amérique du Sud. La liste continue de s’allonger.",
|
||||
"devicesTitle": "{devices} appareils à la fois",
|
||||
"devicesText": "Téléphone, ordinateur portable, tablette et deux autres appareils fonctionnent en même temps.",
|
||||
"ruSitesTitle": "Sites russes en direct",
|
||||
"ruSitesText": "Les banques et les services publics fonctionnent comme d’habitude, sans avoir à désactiver le VPN.",
|
||||
"encryptedTitle": "Trafic chiffré",
|
||||
"encryptedText": "À la maison, à l’hôtel ou au café, la connexion est protégée partout dans le monde.",
|
||||
"devicesCardTitle": "{devices} appareils par abonnement",
|
||||
"devicesCardText": "Un seul abonnement pour tous vos appareils. Ils fonctionnent en même temps, y compris dans des pays différents.",
|
||||
"ruTitle": "Adresse russe incluse",
|
||||
"ruText": "L’adresse russe fait partie de l’abonnement : banques, services publics et plateformes de streaming s’ouvrent comme si vous étiez chez vous.",
|
||||
"protocolsTitle": "Xray et WireGuard",
|
||||
"protocolsText": "Xray fait passer le trafic pour du HTTPS ordinaire et tient bon là où les anciens protocoles ne fonctionnent plus. WireGuard est rapide et simple là où rien n’est bloqué.",
|
||||
"routesTitle": "Le trafic prend le chemin le plus court",
|
||||
"routesText": "Le chemin est choisi automatiquement selon l’adresse du site. Les services russes continuent de fonctionner, les services étrangers s’ouvrent, et vous n’avez jamais rien à changer.",
|
||||
"inRussia": "Vous êtes en Russie",
|
||||
"abroad": "Vous êtes à l’étranger",
|
||||
"ruSites": "Sites RU",
|
||||
"restOfWorld": "Reste du monde",
|
||||
"globalSites": "Sites mondiaux",
|
||||
"viaRuDirect": "directement via le serveur RU",
|
||||
"tunnelToLocation": "par tunnel vers l’emplacement choisi",
|
||||
"fromLocation": "directement depuis l’emplacement choisi",
|
||||
"tunnelViaRu": "par tunnel via le serveur RU",
|
||||
"abroadNote": "Pour ceux qui travaillent avec la Russie depuis un autre pays."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Un abonnement, tous les pays",
|
||||
"text": "{countries}, une adresse russe et {devices} appareils.",
|
||||
"countries": "{countries} et une adresse russe",
|
||||
"devices": "Jusqu’à {devices} appareils à la fois",
|
||||
"protocols": "Xray et WireGuard",
|
||||
"listTitle": "Pays inclus",
|
||||
"buy": "S’abonner",
|
||||
"note": "Cartes bancaires russes et internationales, ainsi que les cryptomonnaies. L’accès est ouvert dès le paiement."
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Besoin d’une adresse rien qu’à vous ?",
|
||||
"text": "Ici, l’IP est partagée : c’est moins cher, et cela suffit à la plupart des gens. Si un service exige une IP permanente, consultez les forfaits Veilix avec IP dédiée.",
|
||||
"button": "IP dédiée"
|
||||
},
|
||||
"faqTitle": "Questions"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Tarifs",
|
||||
"description": "Tous les pays dans un seul abonnement : {priceMonth} par mois ou {priceYear} par an. Une adresse russe et jusqu’à {devices} appareils.",
|
||||
"subline": "ou {priceYear} par an, soit {pricePerMonth} par mois",
|
||||
"note": "Un prix unique pour tout. Vous pouvez annuler le renouvellement depuis votre espace client.",
|
||||
"button": "S’abonner",
|
||||
"includedTitle": "Ce qui est inclus",
|
||||
"included1": "{countries}, changement à tout moment",
|
||||
"included2": "Une adresse russe : connexion directe et pont dans les deux sens",
|
||||
"included3": "Jusqu’à {devices} appareils à la fois",
|
||||
"included4": "Xray et WireGuard",
|
||||
"gridTitle": "Pays inclus",
|
||||
"gridText1": "Les pays marqués « Inclus » sont disponibles dès le paiement et compris dans un prix unique. Les nouveaux pays rejoignent l’abonnement sans supplément.",
|
||||
"crossTitle": "Besoin d’une adresse rien qu’à vous ?",
|
||||
"crossText": "Ici, l’IP est partagée : c’est moins cher, et cela suffit à la plupart des gens. Si un service exige une IP permanente, consultez les forfaits Veilix avec IP dédiée.",
|
||||
"crossButton": "IP dédiée",
|
||||
"leadTitle": "Besoin de conditions particulières ?",
|
||||
"leadText": "Abonnements pour une équipe, paiement sur facture ou pays rare. Laissez une demande et nous vous contacterons.",
|
||||
"leadButton": "Envoyer une demande"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Emplacements",
|
||||
"description": "Pays inclus dans l’abonnement VeilixVPN : {countries} et une adresse russe, avec le statut de chaque pays.",
|
||||
"mapText": "{countries} et une adresse russe sont inclus dans l’abonnement.",
|
||||
"hint": "Sélectionnez un point sur la carte pour voir le pays et la ville",
|
||||
"scrollHint": "Faites défiler la carte horizontalement",
|
||||
"ruLegend": "Adresse russe incluse",
|
||||
"gridTitle": "Tous les pays de l’abonnement",
|
||||
"gridText": "Un prix unique pour tout : {priceMonth} par mois ou {priceYear} par an.",
|
||||
"ruTitle": "Une adresse russe dans l’abonnement",
|
||||
"ruText": "Elle fonctionne dans les deux sens : depuis la Russie, les banques et les services publics voient une connexion locale ordinaire ; depuis l’étranger, ces mêmes services s’ouvrent comme si vous étiez chez vous.",
|
||||
"leadTitle": "Besoin de conditions particulières ?",
|
||||
"leadText": "Le pays dont vous avez besoin ne figure pas dans la liste, ou vous souhaitez un abonnement pour une équipe ? Écrivez-nous et nous vous contacterons.",
|
||||
"leadButton": "Envoyer une demande"
|
||||
},
|
||||
"support": {
|
||||
"title": "Assistance",
|
||||
"description": "Assistance VeilixVPN : Telegram et e-mail, réponses aux questions sur la connexion, le paiement et les pays de votre abonnement.",
|
||||
"telegramText": "Le moyen le plus rapide. Messages vocaux et captures d’écran bienvenus.",
|
||||
"emailText": "Pour les factures, les remboursements et tout ce qui doit se faire par écrit.",
|
||||
"faqTitle": "Questions fréquentes",
|
||||
"tabConnection": "Connexion",
|
||||
"tabBilling": "Paiement et renouvellement",
|
||||
"tabLocations": "Emplacements et abonnement",
|
||||
"formTitle": "Écrivez-nous",
|
||||
"formText": "Si vous avez déjà un abonnement, indiquez l’e-mail associé : nous verrons ainsi directement votre configuration.",
|
||||
"topicConnection": "Sujet : connexion",
|
||||
"topicBilling": "Sujet : paiement et renouvellement",
|
||||
"topicLocations": "Sujet : emplacements et abonnement",
|
||||
"topicOther": "Sujet : autre"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"differs": {
|
||||
"q": "En quoi est-ce différent d’un VPN ordinaire à 3 $ ?",
|
||||
"a": "Dans un VPN grand public, des centaines de personnes partagent votre adresse. Si l’une d’elles se retrouve sur une liste noire, les restrictions s’appliquent à tout le monde. Ici, l’adresse n’appartient qu’à vous : les services la traitent donc comme une connexion à domicile."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Pourquoi certains emplacements demandent-ils jusqu’à 24 heures ?",
|
||||
"a": "Nous ne gardons pas de réserve d’adresses partagées. S’il n’y a pas d’adresse libre dans le pays souhaité, nous en préparons une spécialement pour vous et vous remettons l’accès sous 24 heures. La période payée commence au moment où l’accès vous est remis."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Dois-je configurer quelque chose moi-même ?",
|
||||
"a": "Non. Dans votre espace client, vous téléchargez un fichier prêt à l’emploi ou scannez un QR code avec votre téléphone. Le routage est déjà configuré : les sites russes passent en direct, les sites étrangers par votre emplacement."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Combien d’appareils puis-je connecter ?",
|
||||
"a": "Jusqu’à cinq par clé, tous avec la même adresse. Pour plus d’appareils ou un deuxième pays, prenez une autre clé avec sa propre IP dédiée."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Pourquoi une adresse russe en plus ?",
|
||||
"a": "Elle fonctionne dans les deux sens. Depuis la Russie, pour que les banques, les services publics et les plateformes de streaming voient une connexion locale ordinaire. Depuis l’étranger, pour que ces mêmes services s’ouvrent comme si vous étiez chez vous."
|
||||
},
|
||||
"blocked": {
|
||||
"q": "Et si une méthode de connexion cesse de fonctionner ?",
|
||||
"a": "Votre clé inclut les deux types de connexion à la fois. Si l’un commence à être bloqué, vous passez à l’autre dans le même espace client. Nous complétons et mettons régulièrement à jour la liste des protocoles."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "Que faire juste après le paiement ?",
|
||||
"a": "Connectez-vous à votre espace client avec votre e-mail, ouvrez votre clé et téléchargez la configuration ou scannez le QR code avec votre téléphone. Le routage est déjà configuré, vous n’avez rien à saisir à la main."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Combien d’appareils puis-je connecter ?",
|
||||
"a": "Jusqu’à cinq par clé, tous avec la même adresse. Pour plus d’appareils ou un deuxième pays, prenez une autre clé avec sa propre IP dédiée."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Une méthode de connexion a cessé de fonctionner.",
|
||||
"a": "Les deux types sont toujours configurés dans votre clé : WireGuard et Xray. Ouvrez votre espace client et connectez-vous avec la seconde configuration, sans rien avoir à demander. Nous complétons et mettons régulièrement à jour la liste des protocoles."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Dois-je configurer quelque chose manuellement ?",
|
||||
"a": "Non. La configuration prévoit déjà que les sites russes passent en direct et les sites étrangers par votre emplacement. Vous n’avez aucun mode à changer pendant que vous travaillez."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"firstMonth": {
|
||||
"q": "Pourquoi le premier mois coûte-t-il plus cher ?",
|
||||
"a": "Le premier mois couvre la préparation : attribution de l’adresse, configuration des deux protocoles et du routage. Ensuite, vous ne payez que la mensualité."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "Comment fonctionne le renouvellement ?",
|
||||
"a": "Automatiquement, une fois par mois : le montant est prélevé d’abord sur votre solde, puis sur la carte enregistrée. La date d’expiration figure sur la page de la clé dans votre espace client, où vous pouvez aussi désactiver le renouvellement automatique."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Puis-je obtenir un remboursement ?",
|
||||
"a": "Si la clé n’a pas fonctionné par notre faute, nous vous remboursons intégralement au cours des sept premiers jours. Si vous avez simplement changé d’avis, nous remboursons la partie non utilisée du mois."
|
||||
},
|
||||
"methods": {
|
||||
"q": "Quels moyens de paiement sont acceptés ?",
|
||||
"a": "Les cartes de banques russes et étrangères, ainsi que les cryptomonnaies. Tous les moyens de paiement sont affichés au moment de la commande."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"vsVpn": {
|
||||
"q": "En quoi une IP dédiée diffère-t-elle d’un VPN ordinaire ?",
|
||||
"a": "Dans un VPN grand public, des centaines de personnes partagent une même adresse, et toute restriction visant l’une d’elles s’applique à tous. Votre adresse n’appartient qu’à vous : les services la traitent donc comme une connexion à domicile."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Pourquoi certains emplacements demandent-ils jusqu’à 24 heures ?",
|
||||
"a": "Nous ne gardons pas de réserve d’adresses partagées. S’il n’y a pas d’adresse libre dans le pays souhaité, nous en préparons une spécialement pour vous. La période payée commence au moment où l’accès vous est remis."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Pourquoi le forfait inclut-il une adresse russe ?",
|
||||
"a": "Elle fonctionne dans les deux sens. Depuis la Russie, pour que les banques, les services publics et les plateformes de streaming voient une connexion locale ordinaire. Depuis l’étranger, pour que ces mêmes services s’ouvrent comme si vous étiez chez vous."
|
||||
},
|
||||
"changeCountry": {
|
||||
"q": "Puis-je changer de pays plus tard ?",
|
||||
"a": "Une clé est liée à son pays, nous ne le changeons donc pas. S’il vous faut un autre emplacement, prenez une deuxième clé : elle apparaîtra dans le même espace client et se renouvellera séparément. Vous pouvez garder la première ou la résilier quand vous le souhaitez."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "Comment connecter un nouvel appareil ?",
|
||||
"a": "Ouvrez la clé, téléchargez une configuration WireGuard disponible ou scannez son QR code. Chaque configuration est prévue pour un seul appareil."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Que faire si la connexion ne fonctionne plus ?",
|
||||
"a": "Passez à Xray dans la même clé : il est configuré à l’avance. Si cela ne suffit pas, écrivez à l’assistance."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "Comment annuler le renouvellement ?",
|
||||
"a": "Désactivez le renouvellement automatique sur la page de la clé. La clé continuera de fonctionner jusqu’à la fin de la période payée."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"included": {
|
||||
"q": "Que comprend l’abonnement ?",
|
||||
"a": "Tous les pays de la liste, une adresse russe et cinq appareils. Il n’y a pas de frais séparés par pays ou par appareil."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Puis-je changer de pays aussi souvent que je le souhaite ?",
|
||||
"a": "Oui. Le changement prend quelques secondes et ne coûte rien : la liste complète des emplacements reste ouverte pendant toute la durée de l’abonnement."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Dois-je configurer quelque chose moi-même ?",
|
||||
"a": "Non. Dans votre espace client, vous téléchargez un fichier prêt à l’emploi ou scannez un QR code avec votre téléphone. Les sites russes passent en direct, les autres par l’emplacement choisi."
|
||||
},
|
||||
"protocol": {
|
||||
"q": "Quel protocole la connexion utilise-t-elle ?",
|
||||
"a": "Xray, avec VLESS, Hysteria2 et XHTTP : le trafic ressemble à du HTTPS ordinaire, la connexion tient donc là où les anciens protocoles sont bloqués. Pour les cas simples, WireGuard est aussi disponible."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "En quoi est-ce différent d’un forfait avec IP dédiée ?",
|
||||
"a": "Ici, l’adresse est partagée : plusieurs personnes utilisent le même serveur, ce qui rend l’abonnement bien moins cher. Si vous avez besoin d’une adresse qui vous soit réservée, c’est le forfait avec IP dédiée qui convient."
|
||||
},
|
||||
"payment": {
|
||||
"q": "Comment se passent le paiement et le renouvellement ?",
|
||||
"a": "Au mois ou à l’année. L’abonnement se renouvelle automatiquement : le montant est prélevé d’abord sur votre solde, puis sur la carte enregistrée. Le forfait annuel revient moins cher que le paiement mensuel. Vous pouvez désactiver le renouvellement à tout moment depuis votre espace client."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "Que faire juste après le paiement ?",
|
||||
"a": "Connectez-vous avec votre e-mail et ouvrez votre clé : ajoutez le lien dans l’application Happ ou téléchargez la configuration WireGuard du pays souhaité. Vous pouvez aussi scanner le QR code avec votre téléphone. Le routage est déjà configuré."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Combien d’appareils puis-je connecter ?",
|
||||
"a": "Jusqu’à cinq par clé. Les appareils peuvent utiliser des pays différents : chacun se connecte à son propre emplacement."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Une méthode de connexion a cessé de fonctionner.",
|
||||
"a": "Passez à un autre protocole ou changez d’emplacement dans l’application : cela prend quelques secondes. Nous mettons à jour les réglages des protocoles au fur et à mesure des blocages."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Dois-je configurer quelque chose manuellement ?",
|
||||
"a": "Non. La configuration prévoit déjà que les sites russes passent en direct et les sites étrangers par l’emplacement choisi. Vous n’avez aucun mode à changer pendant que vous travaillez."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"price": {
|
||||
"q": "Combien coûte l’abonnement ?",
|
||||
"a": "Les prix sont indiqués sur la page Tarifs : au mois ou à l’année, le forfait annuel revenant moins cher. Il n’y a ni frais de mise en service, ni supplément par pays."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "Comment l’abonnement est-il renouvelé ?",
|
||||
"a": "Automatiquement, chaque mois ou chaque année : le montant est prélevé d’abord sur votre solde, puis sur la carte enregistrée. La date de fin figure dans votre espace client, où vous pouvez aussi désactiver le renouvellement automatique."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Puis-je obtenir un remboursement ?",
|
||||
"a": "Si l’abonnement n’a pas fonctionné par notre faute, nous vous remboursons intégralement au cours des sept premiers jours. Si vous avez simplement changé d’avis, nous remboursons la partie non utilisée de la période."
|
||||
},
|
||||
"methods": {
|
||||
"q": "Quels moyens de paiement sont acceptés ?",
|
||||
"a": "Les cartes de banques russes et étrangères, ainsi que les cryptomonnaies. Tous les moyens de paiement sont affichés au moment de la commande."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"countries": {
|
||||
"q": "Combien de pays sont inclus ?",
|
||||
"a": "Tous les pays de la liste des emplacements marqués « Inclus », plus une adresse russe. Ils ne sont pas facturés séparément, et les nouveaux pays sont ajoutés sans supplément."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Puis-je changer de pays aussi souvent que je le souhaite ?",
|
||||
"a": "Oui. Le changement prend quelques secondes et ne coûte rien : la liste complète des emplacements reste ouverte pendant toute la durée de l’abonnement."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Pourquoi l’abonnement inclut-il une adresse russe ?",
|
||||
"a": "Elle fonctionne dans les deux sens. Depuis la Russie, pour que les banques, les services publics et les plateformes de streaming voient une connexion locale ordinaire. Depuis l’étranger, pour que ces mêmes services s’ouvrent comme si vous étiez chez vous."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "En quoi est-ce différent d’une IP dédiée ?",
|
||||
"a": "Ici, l’adresse est partagée : plusieurs personnes utilisent le même serveur, l’abonnement coûte donc bien moins cher. Si vous avez besoin d’une adresse qui vous soit réservée, Veilix propose des forfaits distincts avec IP dédiée."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "Comment connecter un nouvel appareil ?",
|
||||
"a": "Ouvrez la clé et ajoutez le lien dans l’application Happ, ou téléchargez la configuration WireGuard du pays souhaité. Vous pouvez aussi scanner le QR code."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Que faire si la connexion ne fonctionne plus ?",
|
||||
"a": "Changez d’emplacement ou de protocole dans l’application : cela prend quelques secondes. Si cela ne suffit pas, écrivez à l’assistance."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "Comment annuler le renouvellement ?",
|
||||
"a": "Désactivez le renouvellement automatique sur la page de la clé. La clé continuera de fonctionner jusqu’à la fin de la période payée."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "Conditions d’utilisation",
|
||||
"description": "Conditions d’utilisation du service : compte, clés et appareils, paiement, renouvellement et usages autorisés."
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Politique de confidentialité",
|
||||
"description": "Quelles données nous conservons et pourquoi. Les serveurs VPN ne conservent aucun journal."
|
||||
},
|
||||
"refund": {
|
||||
"title": "Politique de remboursement",
|
||||
"description": "Quand et comment nous remboursons une clé."
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Politique relative aux cookies",
|
||||
"description": "Quels cookies le site utilise et pourquoi."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
{
|
||||
"site": {
|
||||
"dedicated": {
|
||||
"hours": "Отвечаем с 10 до 20 по московскому времени в рабочие дни.",
|
||||
"titleTemplate": "%s · Вейликс",
|
||||
"description": "Личный VPN с индивидуальным IP в выбранной стране: один адрес на всех устройствах, RU-адрес в комплекте."
|
||||
},
|
||||
"mass": {
|
||||
"hours": "Отвечаем с 10 до 20 по московскому времени в рабочие дни.",
|
||||
"titleTemplate": "%s · ВейликсВПН",
|
||||
"description": "VPN со всеми странами в одной подписке: российский адрес, Xray и WireGuard, до 5 устройств."
|
||||
}
|
||||
},
|
||||
"tariffs": {
|
||||
"mass": {
|
||||
"month": {
|
||||
"description": "Списание раз в месяц, отмена в любой момент"
|
||||
},
|
||||
"year": {
|
||||
"description": "{pricePerMonth} в месяц, экономия {savings} за год",
|
||||
"badge": "выгоднее"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"title": "Вейликс — личный VPN с индивидуальным IP",
|
||||
"description": "Личный VPN с индивидуальным IP: один адрес на всех устройствах, страна на выбор, RU-адрес в комплекте, WireGuard и Xray.",
|
||||
"hero": {
|
||||
"badge": "Индивидуальный IP, не общий",
|
||||
"since": "Работаем с 2022 года",
|
||||
"title": "Ваш личный VPN с индивидуальным IP",
|
||||
"text": "Адрес принадлежит только вам: один и тот же IP на всех устройствах, в любой стране. Без соседей по адресу и чужой репутации.",
|
||||
"primary": "Получить VPN",
|
||||
"secondary": "Посмотреть локации",
|
||||
"plateTitle": "Ваш IP закреплён только за вами",
|
||||
"platePrice": "{priceFirst} первый месяц, далее {priceRenewal}",
|
||||
"plateDevices": "До {devices} устройств",
|
||||
"plateRu": "RU-адрес в комплекте"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Что это даёт на практике",
|
||||
"mainTitle": "Индивидуальный IP",
|
||||
"mainText": "Адрес выделен под вас одного. Сервисы видят обычного человека из выбранной страны, а не сотню людей за одним адресом.",
|
||||
"aiTitle": "Западные AI-сервисы",
|
||||
"aiText": "Риск блокировки ниже, чем у публичных VPN: их адреса уже давно в чёрных списках.",
|
||||
"sameTitle": "Один адрес на всех устройствах",
|
||||
"sameText": "Ноутбук, телефон, планшет выглядят одинаково. Вы всегда в одной локации со своим IP.",
|
||||
"captchaTitle": "Меньше проверок и капч",
|
||||
"captchaText": "На массовых адресах срабатывает антибот-защита. На личном сервисы работают стабильнее.",
|
||||
"encryptedTitle": "Трафик под шифрованием",
|
||||
"encryptedText": "Дома, в отеле или в кафе соединение защищено в любой точке мира.",
|
||||
"devicesTitle": "До {devices} устройств на ключ",
|
||||
"devicesText": "Нужно больше или другая страна — добавляете второй ключ со своим отдельным IP.",
|
||||
"ruTitle": "Плюс выделенный RU-адрес",
|
||||
"ruText": "Идёт к любому тарифу. Работает и как прямое соединение, и как мост в обе стороны.",
|
||||
"protocolsTitle": "Два типа подключения",
|
||||
"protocolsText": "Если один способ перестал работать, второй уже настроен. Список дополняется и обновляется.",
|
||||
"chipWg": "WireGuard / Amnezia WG",
|
||||
"chipXray": "Xray: VLESS, Hysteria2, XHTTP",
|
||||
"routesTitle": "Трафик идёт коротким путём",
|
||||
"routesText": "Маршрут выбирается сам, по адресу сайта. Российские сервисы не ломаются, зарубежные открываются, переключать ничего не нужно.",
|
||||
"inRussia": "Вы в России",
|
||||
"abroad": "Вы за границей",
|
||||
"ruSites": "RU-сайты",
|
||||
"restOfWorld": "Остальной мир",
|
||||
"globalSites": "Мировые сайты",
|
||||
"viaRuDirect": "напрямую через RU-сервер",
|
||||
"tunnelToLocation": "туннелем в вашу локацию",
|
||||
"fromLocation": "прямо из вашей локации",
|
||||
"tunnelViaRu": "туннелем через RU-сервер",
|
||||
"abroadNote": "Для тех, кто работает с Россией из другой страны."
|
||||
},
|
||||
"pricing": {
|
||||
"ip": "Индивидуальный IP в выбранной стране",
|
||||
"ru": "Выделенный RU-адрес в комплекте",
|
||||
"devices": "До {devices} устройств",
|
||||
"protocols": "2 типа подключения:",
|
||||
"listTitle": "Выберите локацию",
|
||||
"buy": "Купить",
|
||||
"lead": "Нужны индивидуальные условия"
|
||||
},
|
||||
"faqTitle": "Вопросы"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Цены",
|
||||
"description": "Индивидуальный IP в выбранной стране: {priceFirst} первый месяц, далее {priceRenewal} в месяц. RU-адрес и до {devices} устройств в комплекте.",
|
||||
"subline": "далее {priceRenewal} в месяц",
|
||||
"note": "В первый месяц входит подготовка адреса и подключений под вас. Дальше — только абонентская плата, отменить можно в кабинете.",
|
||||
"includedTitle": "Что входит",
|
||||
"included1": "Индивидуальный IP в выбранной стране, только ваш",
|
||||
"included2": "Выделенный RU-адрес: прямое соединение и мост в обе стороны",
|
||||
"included3": "До {devices} устройств с одним и тем же адресом",
|
||||
"included4": "Два типа подключения: WireGuard и Xray",
|
||||
"included5": "Умная маршрутизация RU / Глобал без переключений",
|
||||
"gridTitle": "Выберите локацию",
|
||||
"gridText1": "Где-то свободные адреса уже есть, и доступ выдаётся сразу. Если свободного нет, мы готовим адрес лично под вас — на это уходит до 24 часов, и вы видите этот статус честно, до оплаты.",
|
||||
"gridText2": "Базовая цена — {priceFirst} первый месяц и {priceRenewal} далее. В редких и дальних локациях она выше и указана на карточке.",
|
||||
"severalTitle": "Нужно несколько локаций",
|
||||
"severalText": "Каждый ключ — отдельный индивидуальный IP. Берёте второй ключ в другой стране, оба лежат в одном кабинете и продлеваются независимо.",
|
||||
"leadTitle": "Нужны индивидуальные условия",
|
||||
"leadText": "Команда, много адресов, редкая страна или свой сценарий маршрутизации. Оставьте заявку — мы свяжемся с вами.",
|
||||
"leadButton": "Оставить заявку"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Локации",
|
||||
"description": "Локации Вейликс: {countries} на выбор, статус наличия и цены. Российский адрес — в комплекте с любым тарифом.",
|
||||
"mapText": "{countries} на выбор и российский адрес в комплекте с любым тарифом.",
|
||||
"hint": "Нажмите точку на карте — покажем страну и цену",
|
||||
"scrollHint": "Карту можно прокрутить в сторону",
|
||||
"ruLegend": "RU-адрес входит в каждый тариф",
|
||||
"gridTitle": "Выберите страну",
|
||||
"gridText": "Базовая цена — {priceFirst} первый месяц и {priceRenewal} далее. В редких и дальних локациях она выше и указана на карточке.",
|
||||
"ruTitle": "Российский адрес в каждом тарифе",
|
||||
"ruText": "Он работает в обе стороны: из России банки и госуслуги видят обычное местное подключение, из другой страны те же сервисы открываются так, будто вы дома.",
|
||||
"leadTitle": "Нужны индивидуальные условия",
|
||||
"leadText": "Нужной страны нет в списке, нужно много адресов или свой сценарий маршрутизации — напишите, скажем сроки и подготовим под вас.",
|
||||
"leadButton": "Оставить заявку"
|
||||
},
|
||||
"support": {
|
||||
"title": "Поддержка",
|
||||
"description": "Поддержка Вейликс: Telegram и почта, ответы на частые вопросы о подключении, оплате и локациях.",
|
||||
"telegramText": "Быстрее всего. Можно писать голосом и скидывать скриншоты.",
|
||||
"emailText": "Для счетов, возвратов и всего, что нужно письменно.",
|
||||
"faqTitle": "Частые вопросы",
|
||||
"tabConnection": "Подключение",
|
||||
"tabBilling": "Оплата и продление",
|
||||
"tabLocations": "Локации и IP",
|
||||
"formTitle": "Написать нам",
|
||||
"formText": "Если уже купили ключ, укажите почту, на которую он оформлен — так мы сразу увидим вашу конфигурацию.",
|
||||
"topicConnection": "Тема: подключение",
|
||||
"topicBilling": "Тема: оплата и продление",
|
||||
"topicLocations": "Тема: локации и IP",
|
||||
"topicOther": "Тема: другое"
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"title": "ВейликсВПН — VPN, который не нужно отключать",
|
||||
"description": "Все страны в одной подписке: российский адрес, Xray и WireGuard, до 5 устройств. Переключайте страну сколько угодно.",
|
||||
"hero": {
|
||||
"badge": "Все страны в одной подписке",
|
||||
"since": "Работаем с 2022 года",
|
||||
"title": "VPN, который не нужно отключать",
|
||||
"text": "{countries} и российский адрес входят в подписку. Переключайтесь между странами сколько угодно, на {devices} устройствах сразу.",
|
||||
"primary": "Получить VPN",
|
||||
"secondary": "Посмотреть локации",
|
||||
"plateTitle": "{countries}, переключение в один тап",
|
||||
"platePrice": "{priceMonth} в месяц или {priceYear} в год",
|
||||
"plateDevices": "До {devices} устройств",
|
||||
"plateRu": "RU-адрес включён"
|
||||
},
|
||||
"advantages": {
|
||||
"title": "Что это даёт на практике",
|
||||
"mainTitle": "Все локации в подписке",
|
||||
"mainText": "Список стран открыт целиком. Выбираете страну в приложении, соединение перестраивается за пару секунд, доплачивать за переключение не нужно.",
|
||||
"countriesTitle": "{countries}",
|
||||
"countriesText": "Европа, США, Азия, Ближний Восток и Южная Америка. Список пополняется.",
|
||||
"devicesTitle": "{devices} устройств сразу",
|
||||
"devicesText": "Телефон, ноутбук, планшет и ещё два устройства работают одновременно.",
|
||||
"ruSitesTitle": "Российские сайты напрямую",
|
||||
"ruSitesText": "Банки и госуслуги работают как обычно, отключать VPN не нужно.",
|
||||
"encryptedTitle": "Трафик под шифрованием",
|
||||
"encryptedText": "Дома, в отеле или в кафе соединение защищено в любой точке мира.",
|
||||
"devicesCardTitle": "{devices} устройств в подписке",
|
||||
"devicesCardText": "Одна подписка на всю технику. Устройства работают одновременно и в разных странах.",
|
||||
"ruTitle": "Российский адрес включён",
|
||||
"ruText": "Российский адрес входит в подписку: банки, госуслуги и стриминг открываются как дома.",
|
||||
"protocolsTitle": "Xray и WireGuard",
|
||||
"protocolsText": "Xray маскирует трафик под обычный HTTPS и держится там, где старые протоколы уже не работают. WireGuard — быстрый и простой, когда блокировок нет.",
|
||||
"routesTitle": "Трафик идёт коротким путём",
|
||||
"routesText": "Маршрут выбирается сам, по адресу сайта. Российские сервисы не ломаются, зарубежные открываются, переключать ничего не нужно.",
|
||||
"inRussia": "Вы в России",
|
||||
"abroad": "Вы за границей",
|
||||
"ruSites": "RU-сайты",
|
||||
"restOfWorld": "Остальной мир",
|
||||
"globalSites": "Мировые сайты",
|
||||
"viaRuDirect": "напрямую через RU-сервер",
|
||||
"tunnelToLocation": "туннелем в выбранную локацию",
|
||||
"fromLocation": "прямо из выбранной локации",
|
||||
"tunnelViaRu": "туннелем через RU-сервер",
|
||||
"abroadNote": "Для тех, кто работает с Россией из другой страны."
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Одна подписка, все страны",
|
||||
"text": "{countries}, российский адрес и {devices} устройств.",
|
||||
"countries": "{countries} и российский адрес",
|
||||
"devices": "До {devices} устройств одновременно",
|
||||
"protocols": "Xray и WireGuard",
|
||||
"listTitle": "Страны в подписке",
|
||||
"buy": "Оформить подписку",
|
||||
"note": "Карты российских и зарубежных банков, криптовалюта. Доступ открывается сразу после оплаты."
|
||||
},
|
||||
"crossLink": {
|
||||
"title": "Нужен адрес, закреплённый только за вами",
|
||||
"text": "Здесь IP общий: так дешевле, и этого достаточно большинству. Если сервисы требуют постоянный IP, посмотрите индивидуальные тарифы Вейликс.",
|
||||
"button": "Индивидуальный IP"
|
||||
},
|
||||
"faqTitle": "Вопросы"
|
||||
},
|
||||
"pricing": {
|
||||
"title": "Цены",
|
||||
"description": "Все страны в одной подписке: {priceMonth} в месяц или {priceYear} в год. Российский адрес и до {devices} устройств.",
|
||||
"subline": "или {priceYear} в год — {pricePerMonth} в месяц",
|
||||
"note": "Одна цена за всё. Отменить продление можно в кабинете.",
|
||||
"button": "Оформить подписку",
|
||||
"includedTitle": "Что входит",
|
||||
"included1": "{countries}, переключение в любой момент",
|
||||
"included2": "Российский адрес: прямое соединение и мост в обе стороны",
|
||||
"included3": "До {devices} устройств одновременно",
|
||||
"included4": "Xray и WireGuard",
|
||||
"gridTitle": "Страны в подписке",
|
||||
"gridText1": "Страны со статусом «Включено» доступны сразу после оплаты и входят в одну цену. Новые страны добавляются в подписку без доплаты.",
|
||||
"crossTitle": "Нужен адрес, закреплённый только за вами",
|
||||
"crossText": "Здесь IP общий: так дешевле, и этого достаточно большинству. Если сервисы требуют постоянный IP, посмотрите индивидуальные тарифы Вейликс.",
|
||||
"crossButton": "Индивидуальный IP",
|
||||
"leadTitle": "Нужны особые условия",
|
||||
"leadText": "Подписки на команду, оплата по счёту или редкая страна. Оставьте заявку — мы свяжемся с вами.",
|
||||
"leadButton": "Оставить заявку"
|
||||
},
|
||||
"locations": {
|
||||
"title": "Локации",
|
||||
"description": "Страны в подписке ВейликсВПН: {countries} и российский адрес, статус каждой страны.",
|
||||
"mapText": "{countries} и российский адрес входят в подписку.",
|
||||
"hint": "Нажмите точку на карте — покажем страну и город",
|
||||
"scrollHint": "Карту можно прокрутить в сторону",
|
||||
"ruLegend": "RU-адрес входит в подписку",
|
||||
"gridTitle": "Все страны подписки",
|
||||
"gridText": "Одна цена за всё: {priceMonth} в месяц или {priceYear} в год.",
|
||||
"ruTitle": "Российский адрес в подписке",
|
||||
"ruText": "Он работает в обе стороны: из России банки и госуслуги видят обычное местное подключение, из другой страны те же сервисы открываются так, будто вы дома.",
|
||||
"leadTitle": "Нужны особые условия",
|
||||
"leadText": "Нужной страны нет в списке или нужна подписка на команду — напишите, мы свяжемся с вами.",
|
||||
"leadButton": "Оставить заявку"
|
||||
},
|
||||
"support": {
|
||||
"title": "Поддержка",
|
||||
"description": "Поддержка ВейликсВПН: Telegram и почта, ответы на частые вопросы о подключении, оплате и странах подписки.",
|
||||
"telegramText": "Быстрее всего. Можно писать голосом и скидывать скриншоты.",
|
||||
"emailText": "Для счетов, возвратов и всего, что нужно письменно.",
|
||||
"faqTitle": "Частые вопросы",
|
||||
"tabConnection": "Подключение",
|
||||
"tabBilling": "Оплата и продление",
|
||||
"tabLocations": "Локации и подписка",
|
||||
"formTitle": "Написать нам",
|
||||
"formText": "Если подписка уже оформлена, укажите почту, на которую она оформлена — так мы сразу увидим вашу конфигурацию.",
|
||||
"topicConnection": "Тема: подключение",
|
||||
"topicBilling": "Тема: оплата и продление",
|
||||
"topicLocations": "Тема: локации и подписка",
|
||||
"topicOther": "Тема: другое"
|
||||
}
|
||||
},
|
||||
"faq": {
|
||||
"dedicated": {
|
||||
"home": {
|
||||
"differs": {
|
||||
"q": "Чем это отличается от обычного VPN за 300 рублей?",
|
||||
"a": "В массовом VPN ваш адрес делят сотни людей. Если кто-то из них попал в чёрный список, ограничения касаются всех. У вас адрес один и только ваш, поэтому сервисы относятся к нему как к домашнему интернету."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Почему некоторые локации готовы только через 24 часа?",
|
||||
"a": "Мы не держим пул общих адресов. Если в нужной стране свободного адреса нет, мы готовим его лично под вас и выдаём доступ в течение суток. Оплаченный период начинается с момента выдачи."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Нужно ли что-то настраивать самому?",
|
||||
"a": "Нет. В личном кабинете вы скачиваете готовый файл или сканируете QR-код с телефона. Маршрутизация уже настроена: российские сайты идут напрямую, зарубежные — через вашу локацию."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Сколько устройств можно подключить?",
|
||||
"a": "До пяти на один ключ, все с одним и тем же адресом. Нужно больше устройств или вторая страна — берёте ещё один ключ с отдельным индивидуальным IP."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Зачем мне ещё и российский адрес?",
|
||||
"a": "Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома."
|
||||
},
|
||||
"blocked": {
|
||||
"q": "Что будет, если способ подключения перестанет работать?",
|
||||
"a": "В вашем ключе одновременно работают два типа подключения. Если один способ начал блокироваться, вы переключаетесь на второй в том же кабинете. Набор протоколов мы дополняем и обновляем."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "Что делать сразу после оплаты?",
|
||||
"a": "Зайдите в кабинет по почте, откройте свой ключ и скачайте конфигурацию или отсканируйте QR-код с телефона. Маршрутизация уже настроена, вручную ничего вписывать не нужно."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Сколько устройств можно подключить?",
|
||||
"a": "До пяти на один ключ, все с одним и тем же адресом. Если нужно больше устройств или вторая страна — берёте ещё один ключ с отдельным индивидуальным IP."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Перестал работать один способ подключения.",
|
||||
"a": "В вашем ключе всегда настроены оба типа: WireGuard и Xray. Откройте кабинет и подключитесь по второй конфигурации, ничего запрашивать не нужно. Набор протоколов мы дополняем и обновляем."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Нужно ли что-то настраивать вручную?",
|
||||
"a": "Нет. В конфигурации уже прописано, что российские сайты идут напрямую, а зарубежные — через вашу локацию. Переключать режимы во время работы не нужно."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"firstMonth": {
|
||||
"q": "Почему первый месяц дороже?",
|
||||
"a": "В первый месяц входит подготовка: выделение адреса, настройка обоих протоколов и маршрутизации. Дальше вы платите только абонентскую плату."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "Как продлевается ключ?",
|
||||
"a": "Автоматически, раз в месяц: сначала с баланса, затем с привязанной карты. Дату окончания видно на странице ключа в кабинете, там же автопродление можно выключить."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Можно вернуть деньги?",
|
||||
"a": "Если ключ не заработал по нашей вине, возвращаем полностью в течение первых семи дней. Если вы просто передумали — возвращаем остаток неиспользованного месяца."
|
||||
},
|
||||
"methods": {
|
||||
"q": "Чем можно платить?",
|
||||
"a": "Картами российских и зарубежных банков и криптовалютой. Все способы видны на странице оформления заказа."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"vsVpn": {
|
||||
"q": "Чем индивидуальный IP отличается от обычного VPN?",
|
||||
"a": "В массовом VPN один адрес делят сотни людей, и ограничения, которые получил любой из них, касаются всех. Ваш адрес принадлежит только вам, поэтому сервисы относятся к нему как к домашнему интернету."
|
||||
},
|
||||
"ready24": {
|
||||
"q": "Почему часть локаций готова только через 24 часа?",
|
||||
"a": "Мы не держим пул общих адресов. Если в нужной стране свободного адреса нет, мы готовим его лично под вас. Оплаченный период начинается с момента выдачи доступа."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Зачем к тарифу российский адрес?",
|
||||
"a": "Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома."
|
||||
},
|
||||
"changeCountry": {
|
||||
"q": "Можно сменить страну позже?",
|
||||
"a": "Ключ привязан к своей стране, поэтому её не меняем. Если нужна другая локация, возьмите второй ключ: он появится в том же кабинете и будет продлеваться отдельно. Первый можно оставить или отключить в любой момент."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "Как подключить новое устройство?",
|
||||
"a": "Откройте ключ, скачайте любую свободную конфигурацию WireGuard или отсканируйте её QR-код. Каждая конфигурация рассчитана на одно устройство."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Что делать, если подключение перестало работать?",
|
||||
"a": "Переключитесь на Xray в том же ключе — он настроен заранее. Если не помогло, напишите в поддержку."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "Как отменить продление?",
|
||||
"a": "На странице ключа выключите автопродление. Ключ доработает оплаченный период."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mass": {
|
||||
"home": {
|
||||
"included": {
|
||||
"q": "Что входит в подписку?",
|
||||
"a": "Все страны из списка, российский адрес и пять устройств. Отдельной платы за страну или устройство нет."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Можно менять страну сколько угодно?",
|
||||
"a": "Да. Переключение занимает пару секунд и ничего не стоит: список локаций открыт целиком на всё время подписки."
|
||||
},
|
||||
"setup": {
|
||||
"q": "Нужно ли что-то настраивать самому?",
|
||||
"a": "Нет. В кабинете вы скачиваете готовый файл или сканируете QR-код с телефона. Российские сайты идут напрямую, зарубежные — через выбранную локацию."
|
||||
},
|
||||
"protocol": {
|
||||
"q": "На каком протоколе работает подключение?",
|
||||
"a": "На Xray с VLESS, Hysteria2 и XHTTP: трафик выглядит как обычный HTTPS, поэтому соединение держится там, где старые протоколы блокируются. Для простых случаев есть WireGuard."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "Чем это отличается от индивидуального тарифа?",
|
||||
"a": "Здесь адрес общий: на сервере несколько человек, поэтому подписка стоит заметно дешевле. Если нужен адрес, закреплённый только за вами, это отдельные тарифы с индивидуальным IP."
|
||||
},
|
||||
"payment": {
|
||||
"q": "Как оплачивается и продлевается?",
|
||||
"a": "Помесячно или за год. Подписка продлевается автоматически: сначала с баланса, затем с привязанной карты. Годовая выходит дешевле помесячной. Отменить продление можно в кабинете в любой момент."
|
||||
}
|
||||
},
|
||||
"support_connection": {
|
||||
"afterPayment": {
|
||||
"q": "Что делать сразу после оплаты?",
|
||||
"a": "Зайдите в кабинет по почте и откройте свой ключ: добавьте ссылку в приложение Happ или скачайте конфигурацию WireGuard нужной страны. Можно и отсканировать QR-код с телефона. Маршрутизация уже настроена."
|
||||
},
|
||||
"devices": {
|
||||
"q": "Сколько устройств можно подключить?",
|
||||
"a": "До пяти на один ключ. Страны у устройств могут быть разные: каждое подключается к своей локации."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Перестал работать один способ подключения.",
|
||||
"a": "Переключитесь на другой протокол или смените локацию в приложении — это занимает пару секунд. Настройки протоколов мы обновляем по мере блокировок."
|
||||
},
|
||||
"manual": {
|
||||
"q": "Нужно ли что-то настраивать вручную?",
|
||||
"a": "Нет. В конфигурации уже прописано, что российские сайты идут напрямую, а зарубежные — через выбранную локацию. Переключать режимы во время работы не нужно."
|
||||
}
|
||||
},
|
||||
"support_billing": {
|
||||
"price": {
|
||||
"q": "Сколько стоит подписка?",
|
||||
"a": "Цены — на странице тарифов: помесячно или за год, годовая подписка выходит дешевле. Платы за подключение и доплат за страну нет."
|
||||
},
|
||||
"renewal": {
|
||||
"q": "Как продлевается подписка?",
|
||||
"a": "Автоматически, раз в месяц или раз в год: сначала с баланса, затем с привязанной карты. Дату окончания видно в кабинете, там же автопродление можно выключить."
|
||||
},
|
||||
"refund": {
|
||||
"q": "Можно вернуть деньги?",
|
||||
"a": "Если подписка не заработала по нашей вине, возвращаем полностью в течение первых семи дней. Если вы просто передумали — возвращаем остаток неиспользованного периода."
|
||||
},
|
||||
"methods": {
|
||||
"q": "Чем можно платить?",
|
||||
"a": "Картами российских и зарубежных банков и криптовалютой. Все способы видны на странице оформления заказа."
|
||||
}
|
||||
},
|
||||
"support_locations": {
|
||||
"countries": {
|
||||
"q": "Сколько стран входит в подписку?",
|
||||
"a": "Все страны из списка локаций со статусом «Включено» и российский адрес. Отдельно они не тарифицируются, а новые страны добавляются без доплаты."
|
||||
},
|
||||
"switch": {
|
||||
"q": "Можно менять страну сколько угодно?",
|
||||
"a": "Да. Переключение занимает пару секунд и ничего не стоит: список локаций открыт целиком на всё время подписки."
|
||||
},
|
||||
"ruAddress": {
|
||||
"q": "Зачем в подписке российский адрес?",
|
||||
"a": "Он работает в обе стороны. Из России — чтобы банки, госуслуги и стриминг видели обычное местное подключение. Из другой страны — чтобы те же сервисы открывались так, будто вы дома."
|
||||
},
|
||||
"vsDedicated": {
|
||||
"q": "Чем это отличается от индивидуального IP?",
|
||||
"a": "Здесь адрес общий: на сервере несколько человек, поэтому подписка стоит заметно дешевле. Если нужен адрес, закреплённый только за вами, это отдельные тарифы Вейликс с индивидуальным IP."
|
||||
}
|
||||
},
|
||||
"cabinet": {
|
||||
"newDevice": {
|
||||
"q": "Как подключить новое устройство?",
|
||||
"a": "Откройте ключ и добавьте ссылку в приложение Happ или скачайте конфигурацию WireGuard нужной страны. Можно и отсканировать QR-код."
|
||||
},
|
||||
"stopped": {
|
||||
"q": "Что делать, если подключение перестало работать?",
|
||||
"a": "Смените локацию или протокол в приложении — это занимает пару секунд. Если не помогло, напишите в поддержку."
|
||||
},
|
||||
"cancel": {
|
||||
"q": "Как отменить продление?",
|
||||
"a": "На странице ключа выключите автопродление. Ключ доработает оплаченный период."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"legal": {
|
||||
"terms": {
|
||||
"title": "Условия использования",
|
||||
"description": "Условия использования сервиса: аккаунт, ключи и устройства, оплата, продление и правила использования."
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Политика конфиденциальности",
|
||||
"description": "Какие данные мы храним и зачем. VPN-серверы не ведут журналов."
|
||||
},
|
||||
"refund": {
|
||||
"title": "Политика возврата средств",
|
||||
"description": "Когда и как мы возвращаем деньги за ключ."
|
||||
},
|
||||
"cookies": {
|
||||
"title": "Политика cookies",
|
||||
"description": "Какие cookies использует сайт и зачем."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,3 +164,48 @@ export const APPS = [
|
||||
{ app: 'cabinet', subdomain: 'my.' },
|
||||
{ app: 'miniapp', subdomain: 'tg.' },
|
||||
] as const
|
||||
|
||||
type MoreNames = { de: string; fr: string; es: string }
|
||||
|
||||
/** Названия городов на de, fr, es; en и ru — в LOCATIONS */
|
||||
export const CITY_NAMES: Record<string, MoreNames> = {
|
||||
NL: { de: 'Amsterdam', fr: 'Amsterdam', es: 'Ámsterdam' },
|
||||
DE: { de: 'Frankfurt', fr: 'Francfort', es: 'Fráncfort' },
|
||||
US: { de: 'New York', fr: 'New York', es: 'Nueva York' },
|
||||
GB: { de: 'London', fr: 'Londres', es: 'Londres' },
|
||||
FR: { de: 'Paris', fr: 'Paris', es: 'París' },
|
||||
FI: { de: 'Helsinki', fr: 'Helsinki', es: 'Helsinki' },
|
||||
SE: { de: 'Stockholm', fr: 'Stockholm', es: 'Estocolmo' },
|
||||
NO: { de: 'Oslo', fr: 'Oslo', es: 'Oslo' },
|
||||
DK: { de: 'Kopenhagen', fr: 'Copenhague', es: 'Copenhague' },
|
||||
CH: { de: 'Zürich', fr: 'Zurich', es: 'Zúrich' },
|
||||
AT: { de: 'Wien', fr: 'Vienne', es: 'Viena' },
|
||||
ES: { de: 'Madrid', fr: 'Madrid', es: 'Madrid' },
|
||||
IT: { de: 'Mailand', fr: 'Milan', es: 'Milán' },
|
||||
PL: { de: 'Warschau', fr: 'Varsovie', es: 'Varsovia' },
|
||||
CZ: { de: 'Prag', fr: 'Prague', es: 'Praga' },
|
||||
RO: { de: 'Bukarest', fr: 'Bucarest', es: 'Bucarest' },
|
||||
LT: { de: 'Vilnius', fr: 'Vilnius', es: 'Vilna' },
|
||||
LV: { de: 'Riga', fr: 'Riga', es: 'Riga' },
|
||||
EE: { de: 'Tallinn', fr: 'Tallinn', es: 'Tallin' },
|
||||
TR: { de: 'Istanbul', fr: 'Istanbul', es: 'Estambul' },
|
||||
KZ: { de: 'Almaty', fr: 'Almaty', es: 'Almaty' },
|
||||
AM: { de: 'Jerewan', fr: 'Erevan', es: 'Ereván' },
|
||||
GE: { de: 'Tiflis', fr: 'Tbilissi', es: 'Tiflis' },
|
||||
AE: { de: 'Dubai', fr: 'Dubaï', es: 'Dubái' },
|
||||
SG: { de: 'Singapur', fr: 'Singapour', es: 'Singapur' },
|
||||
JP: { de: 'Tokio', fr: 'Tokyo', es: 'Tokio' },
|
||||
KR: { de: 'Seoul', fr: 'Séoul', es: 'Seúl' },
|
||||
CA: { de: 'Toronto', fr: 'Toronto', es: 'Toronto' },
|
||||
AU: { de: 'Sydney', fr: 'Sydney', es: 'Sídney' },
|
||||
BR: { de: 'São Paulo', fr: 'São Paulo', es: 'São Paulo' },
|
||||
}
|
||||
|
||||
/** Названия тарифов на de, fr, es по коду тарифа; en и ru — в описаниях тарифов */
|
||||
export const TARIFF_NAMES: Record<string, MoreNames> = {
|
||||
base: { de: 'Basis', fr: 'Base', es: 'Básico' },
|
||||
rare: { de: 'Seltene Standorte', fr: 'Emplacements rares', es: 'Ubicaciones poco comunes' },
|
||||
distant: { de: 'Ferne Standorte', fr: 'Emplacements lointains', es: 'Ubicaciones lejanas' },
|
||||
month: { de: 'Monatlich', fr: 'Mensuel', es: 'Mensual' },
|
||||
year: { de: 'Jährlich', fr: 'Annuel', es: 'Anual' },
|
||||
}
|
||||
+186
-5
@@ -1,29 +1,43 @@
|
||||
/**
|
||||
* Начальные данные: `pnpm seed` (dev) или `SEED_ENV=prod pnpm seed`.
|
||||
* Документы ищутся по естественному ключу: повторный запуск обновляет их, а не дублирует,
|
||||
* Справочники ищутся по естественному ключу: повторный запуск обновляет их, а не дублирует,
|
||||
* и возвращает значения seed, если их меняли в админке.
|
||||
* Контент (страницы и FAQ) создаётся, только если его ещё нет: правки из админки повторный
|
||||
* запуск не затирает. `SEED_CONTENT=reset` удаляет страницы и FAQ сайтов и создаёт заново.
|
||||
*/
|
||||
import config from '@payload-config'
|
||||
import { convertMarkdownToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
|
||||
import { type CollectionSlug, getPayload, type Where } from 'payload'
|
||||
|
||||
import type { SiteCode } from '@/core/sites'
|
||||
import { DEFAULT_LOCALE, type Locale, LOCALES } from '@/i18n/locales'
|
||||
import { SKIP_REVALIDATE } from '@/payload/hooks/revalidateSite'
|
||||
|
||||
import { contentLocales, keysUnder, resolveTree, t, text } from './content'
|
||||
|
||||
import {
|
||||
APPS,
|
||||
CITY_NAMES,
|
||||
CURRENCIES,
|
||||
DEDICATED_TARIFFS,
|
||||
LOCATIONS,
|
||||
MASS_TARIFFS,
|
||||
RUSSIA,
|
||||
SITES,
|
||||
TARIFF_NAMES,
|
||||
type TariffSeed,
|
||||
} from './data'
|
||||
import { legalMarkdown } from './legal'
|
||||
import { buildPages } from './pages'
|
||||
|
||||
const env = process.env.SEED_ENV === 'prod' ? 'prod' : 'dev'
|
||||
const hostPrefix = env === 'dev' ? 'dev.' : ''
|
||||
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// seed работает вне Next: пересобирать маркетинговые страницы нечего
|
||||
const context = { [SKIP_REVALIDATE]: true }
|
||||
|
||||
type Fields = Record<string, unknown>
|
||||
type Localized = Partial<Record<Locale, Fields>>
|
||||
|
||||
@@ -52,6 +66,7 @@ const upsert = async (
|
||||
locale: DEFAULT_LOCALE,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
: await payload.create({
|
||||
collection,
|
||||
@@ -59,6 +74,7 @@ const upsert = async (
|
||||
locale: DEFAULT_LOCALE,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
for (const [locale, fields] of Object.entries(otherLocales)) {
|
||||
await payload.update({
|
||||
@@ -68,11 +84,18 @@ const upsert = async (
|
||||
locale: locale as Locale,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
}
|
||||
return String(id)
|
||||
}
|
||||
|
||||
/** Локализованные поля из словарей контента: на каждом языке, где есть перевод */
|
||||
const fromContent = (tree: unknown): Localized =>
|
||||
Object.fromEntries(
|
||||
contentLocales().map((locale) => [locale, resolveTree(tree, locale) as Fields]),
|
||||
)
|
||||
|
||||
const capitalize = (value: string) => value.charAt(0).toLocaleUpperCase() + value.slice(1)
|
||||
|
||||
/** Названия из данных ICU для языков, которых нет в макете */
|
||||
@@ -126,12 +149,160 @@ for (const [index, item] of [...LOCATIONS, { ...RUSSIA, tier: null }].entries())
|
||||
)
|
||||
}
|
||||
|
||||
/** Название тарифа и, если есть в словарях, описание и плашка (у mass — «выгоднее») */
|
||||
const tariffNames = (site: SiteCode, tariff: TariffSeed): Localized => {
|
||||
const content = keysUnder(`tariffs.${site}.${tariff.code}`)
|
||||
const extra = fromContent(
|
||||
Object.fromEntries(
|
||||
content.map((field) => [field, t(`tariffs.${site}.${tariff.code}.${field}`)]),
|
||||
),
|
||||
)
|
||||
const more = Object.entries(TARIFF_NAMES[tariff.code] ?? {}).map(([locale, name]) => [
|
||||
locale,
|
||||
{ ...extra[locale as Locale], name },
|
||||
])
|
||||
return {
|
||||
...extra,
|
||||
...Object.fromEntries(more),
|
||||
en: { ...extra.en, name: tariff.name.en },
|
||||
ru: { ...extra.ru, name: tariff.name.ru },
|
||||
}
|
||||
}
|
||||
|
||||
const contentMode = process.env.SEED_CONTENT === 'reset' ? 'reset' : 'missing'
|
||||
const editorConfig = await editorConfigFactory.default({ config: payload.config })
|
||||
const markdown = (value: string) => convertMarkdownToLexical({ editorConfig, markdown: value })
|
||||
|
||||
/** Юридические тексты с данными сайта: название бренда на языке, домены этого окружения */
|
||||
const pages = buildPages((code, slug, locale) => {
|
||||
const site = SITES.find((item) => item.code === code)
|
||||
if (!site) throw new Error(`seed: нет сайта ${code}`)
|
||||
return markdown(
|
||||
legalMarkdown(code, slug, locale, {
|
||||
brand: locale === 'ru' ? site.name.ru : site.name.en,
|
||||
site: `${hostPrefix}${site.baseDomain}`,
|
||||
cabinet: `${hostPrefix}my.${site.baseDomain}`,
|
||||
email: `help@${site.baseDomain}`,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
/** Страницы из блоков и вопросы FAQ сайта: создаются, только если их ещё нет */
|
||||
const seedContent = async (siteId: string, code: SiteCode) => {
|
||||
const ofSite = { site: { equals: siteId } }
|
||||
if (contentMode === 'reset') {
|
||||
for (const collection of ['pages', 'faq'] as const) {
|
||||
await payload.delete({ collection, where: ofSite, overrideAccess: true, context })
|
||||
}
|
||||
}
|
||||
|
||||
for (const page of pages[code]) {
|
||||
const { totalDocs } = await payload.count({
|
||||
collection: 'pages',
|
||||
where: { and: [ofSite, { slug: { equals: page.slug } }] },
|
||||
overrideAccess: true,
|
||||
})
|
||||
if (totalDocs > 0) continue
|
||||
const tree = { title: page.title, seo: page.seo ?? {}, layout: page.layout }
|
||||
const created = await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
...(resolveTree(tree, DEFAULT_LOCALE) as Fields),
|
||||
site: siteId,
|
||||
slug: page.slug,
|
||||
_status: 'published',
|
||||
} as never,
|
||||
locale: DEFAULT_LOCALE,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
for (const locale of contentLocales().filter((code) => code !== DEFAULT_LOCALE)) {
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: created.id,
|
||||
data: { ...(resolveTree(tree, locale, created) as Fields), _status: 'published' } as never,
|
||||
locale,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let sort = 0
|
||||
for (const placement of keysUnder(`faq.${code}`)) {
|
||||
for (const key of keysUnder(`faq.${code}.${placement}`)) {
|
||||
sort += 10
|
||||
const question = t(`faq.${code}.${placement}.${key}.q`)
|
||||
const answer = t(`faq.${code}.${placement}.${key}.a`)
|
||||
const english = text(question, DEFAULT_LOCALE)
|
||||
// один и тот же вопрос на главной и в поддержке — разные записи (решение этапа 1b)
|
||||
const { totalDocs } = await payload.count({
|
||||
collection: 'faq',
|
||||
where: {
|
||||
and: [ofSite, { question: { equals: english } }, { placements: { in: [placement] } }],
|
||||
},
|
||||
locale: DEFAULT_LOCALE,
|
||||
overrideAccess: true,
|
||||
})
|
||||
if (totalDocs > 0) continue
|
||||
const created = await payload.create({
|
||||
collection: 'faq',
|
||||
data: {
|
||||
site: siteId,
|
||||
question: english,
|
||||
answer: markdown(text(answer, DEFAULT_LOCALE)),
|
||||
placements: [placement] as never,
|
||||
sort,
|
||||
active: true,
|
||||
},
|
||||
locale: DEFAULT_LOCALE,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
for (const locale of contentLocales().filter((code) => code !== DEFAULT_LOCALE)) {
|
||||
await payload.update({
|
||||
collection: 'faq',
|
||||
id: created.id,
|
||||
data: { question: text(question, locale), answer: markdown(text(answer, locale)) },
|
||||
locale,
|
||||
depth: 0,
|
||||
overrideAccess: true,
|
||||
context,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const site of SITES) {
|
||||
const siteContent = fromContent({
|
||||
supportHours: t(`site.${site.code}.hours`),
|
||||
seo: {
|
||||
titleTemplate: t(`site.${site.code}.titleTemplate`),
|
||||
description: t(`site.${site.code}.description`),
|
||||
},
|
||||
})
|
||||
const siteId = await upsert(
|
||||
'sites',
|
||||
{ code: { equals: site.code } },
|
||||
{ code: site.code, orderPrefix: site.orderPrefix, locales: [...LOCALES] },
|
||||
{ en: { name: site.name.en }, ru: { name: site.name.ru } },
|
||||
{
|
||||
code: site.code,
|
||||
orderPrefix: site.orderPrefix,
|
||||
locales: [...LOCALES],
|
||||
supportEmail: `help@${site.baseDomain}`,
|
||||
// Telegram поддержки — общий, пока нет ботов поддержки (этап 3e)
|
||||
supportBot: 'veilix_support',
|
||||
},
|
||||
// бренд пишется латиницей на всех языках, кроме русского
|
||||
Object.fromEntries(
|
||||
contentLocales().map((locale) => [
|
||||
locale,
|
||||
{ ...siteContent[locale], name: locale === 'ru' ? site.name.ru : site.name.en },
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
for (const { app, subdomain } of APPS) {
|
||||
@@ -172,7 +343,13 @@ for (const site of SITES) {
|
||||
visible: true,
|
||||
sort: sortOf(index),
|
||||
},
|
||||
{ en: { city: item.city.en }, ru: { city: item.city.ru } },
|
||||
{
|
||||
en: { city: item.city.en },
|
||||
ru: { city: item.city.ru },
|
||||
...Object.fromEntries(
|
||||
Object.entries(CITY_NAMES[item.iso2] ?? {}).map(([locale, city]) => [locale, { city }]),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -202,9 +379,11 @@ for (const site of SITES) {
|
||||
active: true,
|
||||
sort: sortOf(index),
|
||||
},
|
||||
{ en: { name: tariff.name.en }, ru: { name: tariff.name.ru } },
|
||||
tariffNames(site.code, tariff),
|
||||
)
|
||||
}
|
||||
|
||||
await seedContent(siteId, site.code)
|
||||
}
|
||||
|
||||
await upsert(
|
||||
@@ -236,6 +415,8 @@ const counts = await Promise.all(
|
||||
'locations',
|
||||
'tariffs',
|
||||
'payment-systems',
|
||||
'pages',
|
||||
'faq',
|
||||
] as const
|
||||
).map(
|
||||
async (collection) =>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Юридические черновики (01-product, раздел 13): Markdown в src/seed/legal/{язык}/{страница}.md.
|
||||
* Текст общий для брендов: блоки `<!-- dedicated -->…<!-- /dedicated -->` и `<!-- mass -->…`
|
||||
* остаются только у своего бренда, переменные {{brand}}, {{site}}, {{cabinet}}, {{email}}
|
||||
* заменяются значениями сайта. Нет файла на языке — берётся английский.
|
||||
*/
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import type { LegalSlug } from '@/core/pages'
|
||||
import { SITE_CODES, type SiteCode } from '@/core/sites'
|
||||
import { DEFAULT_LOCALE, type Locale } from '@/i18n/locales'
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export type LegalVars = { brand: string; site: string; cabinet: string; email: string }
|
||||
|
||||
const source = (slug: LegalSlug, locale: Locale): string => {
|
||||
const file = path.join(dirname, 'legal', locale, `${slug}.md`)
|
||||
const fallback = path.join(dirname, 'legal', DEFAULT_LOCALE, `${slug}.md`)
|
||||
return readFileSync(existsSync(file) ? file : fallback, 'utf8')
|
||||
}
|
||||
|
||||
export const legalMarkdown = (
|
||||
site: SiteCode,
|
||||
slug: LegalSlug,
|
||||
locale: Locale,
|
||||
vars: LegalVars,
|
||||
): string => {
|
||||
let text = source(slug, locale)
|
||||
for (const code of SITE_CODES) {
|
||||
const block = new RegExp(`<!-- ${code} -->\\n([\\s\\S]*?)\\n<!-- /${code} -->`, 'g')
|
||||
text = text.replace(block, (_match, content: string) => (code === site ? content : ''))
|
||||
}
|
||||
return (
|
||||
text
|
||||
// служебные комментарии (пометка «Черновик») на сайт не попадают
|
||||
.replace(/^<!--.*-->\s*$/gm, '')
|
||||
.replace(/\{\{(brand|site|cabinet|email)\}\}/g, (_match, name: keyof LegalVars) => vars[name])
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Gültig ab [Datum des Inkrafttretens].
|
||||
|
||||
Diese Richtlinie erklärt, welche Cookies der Dienst {{brand}} auf {{site}} und {{cabinet}} verwendet. Der Dienst wird von [Name der juristischen Person] bereitgestellt. Wie wir Daten im Allgemeinen verarbeiten, ist in der [Datenschutzerklärung](https://{{site}}/legal/privacy) beschrieben.
|
||||
|
||||
## 1. Was Cookies sind
|
||||
|
||||
Cookies sind kleine Datensätze, die eine Website in Ihrem Browser speichert, um sich zum Beispiel zu merken, dass Sie angemeldet sind oder welche Sprache Sie gewählt haben. Ähnlich funktioniert localStorage: ein Speicher in Ihrem Browser, dessen Inhalt nicht an den Server gesendet wird.
|
||||
|
||||
## 2. Welche Cookies wir verwenden
|
||||
|
||||
Zu jedem Cookie sind Zweck, Speicherdauer und Kategorie angegeben.
|
||||
|
||||
- `vx_session` – hält Sie in Ihrem Kundenkonto angemeldet, wird nur auf {{cabinet}} gesetzt – 30 Tage, verlängert sich, solange Sie Ihr Kundenkonto nutzen – unbedingt erforderlich.
|
||||
- `NEXT_LOCALE` – die im Umschalter gewählte Sprache – 1 Jahr – unbedingt erforderlich.
|
||||
- `vx_currency` – die für Preise gewählte Währung oder, wenn Sie angemeldet sind, Ihre Kontowährung – 1 Jahr – unbedingt erforderlich.
|
||||
- `vx_ref` – der Einladungscode, wenn Sie einem Empfehlungslink gefolgt sind, wird nur auf {{cabinet}} gesetzt – 60 Tage – unbedingt erforderlich.
|
||||
- `vx_consent` – Ihre Auswahl im Cookie-Banner – 1 Jahr – unbedingt erforderlich.
|
||||
- `cw_*` – Cookies des Support-Chats (Chatwoot): speichern Ihre Unterhaltung mit dem Support zwischen Besuchen – [Speicherdauer der Chat-Cookies] – funktional.
|
||||
|
||||
**Unbedingt erforderliche** Cookies werden benötigt, damit die Website und Ihr Kundenkonto funktionieren: Ohne sie können Sie sich nicht anmelden, und Ihre Auswahl von Sprache und Währung wird nicht gespeichert. Für diese Cookies holen wir keine Einwilligung ein.
|
||||
|
||||
**Funktionale** Cookies werden nur mit Ihrer Einwilligung gesetzt. Haben Sie Ihre Einwilligung nicht erteilt, wird das Chat-Widget nicht geladen; Sie erreichen uns weiterhin per E-Mail oder über Telegram.
|
||||
|
||||
## 3. Analyse ohne Cookies
|
||||
|
||||
Besuchsstatistiken erheben wir mit Umami auf unserem eigenen Server. Umami verwendet keine Cookies, und die Daten werden nicht an Dritte weitergegeben.
|
||||
|
||||
## 4. Werbe-Cookies
|
||||
|
||||
Wir verwenden keine Werbe-Cookies und keine Tracker.
|
||||
|
||||
## 5. localStorage
|
||||
|
||||
Das Design (hell, dunkel oder gemäß Systemeinstellung) wird nicht in einem Cookie, sondern im localStorage unter dem Schlüssel `vx_theme` gespeichert. Jede Domain speichert ihre eigene Einstellung.
|
||||
|
||||
## 6. Cookies anderer Dienste
|
||||
|
||||
- Die Schaltfläche für die Anmeldung über Telegram auf der Anmeldeseite wird von den Servern von Telegram geladen, und Telegram kann nach eigenen Regeln eigene Cookies setzen.
|
||||
- Im Zahlungsschritt kann die Seite oder das Widget des Zahlungsdienstleisters eigene Cookies setzen, die für die Abwicklung der Zahlung und die Betrugsprävention erforderlich sind.
|
||||
|
||||
## 7. Cookies verwalten
|
||||
|
||||
Beim ersten Besuch können Sie über ein Banner funktionale Cookies akzeptieren oder ablehnen. Um Ihre Auswahl zu ändern, löschen Sie in den Browsereinstellungen das Cookie `vx_consent` oder alle Cookies der Website – dann erscheint das Banner erneut.
|
||||
|
||||
In den Browsereinstellungen können Sie Cookies löschen oder blockieren. Ohne unbedingt erforderliche Cookies können Sie sich nicht in Ihrem Kundenkonto anmelden, und Ihre Auswahl von Sprache und Währung wird nicht gespeichert.
|
||||
|
||||
## 8. Kontakt
|
||||
|
||||
Fragen zu Cookies senden Sie bitte an [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 9. Sprache
|
||||
|
||||
Diese Richtlinie wird in mehreren Sprachen veröffentlicht. Bei Abweichungen zwischen den Sprachfassungen ist die englische Fassung maßgeblich.
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Gültig ab [Datum des Inkrafttretens].
|
||||
|
||||
## 1. Wer wir sind
|
||||
|
||||
Der Dienst {{brand}} (die Website {{site}}, Ihr Kundenkonto unter {{cabinet}}, die Telegram-Bots, die Telegram-Mini-App und die VPN-Server) wird von [Name der juristischen Person] bereitgestellt, eingetragen in [Land der Registrierung] unter der Nummer [Registernummer], mit eingetragener Anschrift [Eingetragene Anschrift] („wir“, „uns“). Wir sind der Verantwortliche für die Verarbeitung Ihrer personenbezogenen Daten.
|
||||
|
||||
Diese Datenschutzerklärung beschreibt, welche Daten wir erheben, zu welchem Zweck, wie lange wir sie speichern und an wen wir sie weitergeben. Bei Fragen zu Ihren Daten schreiben Sie an [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 2. Das Wichtigste in Kürze
|
||||
|
||||
- Die VPN-Server führen keine Logs: Weder Datenverkehr noch besuchte Websites, Verbindungszeiten oder IP-Adressen der Nutzer werden erfasst.
|
||||
- Wir speichern nur, was für Ihr Kundenkonto, Zahlungen, den Support und den Schutz vor Missbrauch erforderlich ist.
|
||||
- In unserer Datenbank werden IP-Adressen der Besucher der Website und des Kundenkontos nur als nicht umkehrbare Hashwerte gespeichert; Webserver-Logs werden nach 14 Tagen gelöscht.
|
||||
- Wir verkaufen keine Daten und verwenden keine Werbe-Tracker.
|
||||
|
||||
## 3. Die VPN-Server führen keine Logs
|
||||
|
||||
Unsere VPN-Server zeichnen nicht auf:
|
||||
|
||||
- Ihren Datenverkehr oder dessen Inhalt;
|
||||
- die Websites und Dienste, die Sie besuchen;
|
||||
- wann Sie eine Verbindung herstellen oder trennen;
|
||||
- die IP-Adressen, von denen aus Sie sich verbinden.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Ihre dedizierte IP-Adresse und Ihre RU-Adresse sind Ihrem Schlüssel zugewiesen, daher ist aus Ihren Kontodaten ersichtlich, welche Adressen dem Schlüssel zugeteilt wurden. Was über diese Adressen geschieht, zeichnen wir nicht auf.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
## 4. Welche Daten wir erheben und warum
|
||||
|
||||
**Kundenkonto.** Ihre E-Mail-Adresse; Ihre Telegram-ID, Ihr Benutzername und Ihr Name, wenn Sie sich über Telegram anmelden oder Telegram verknüpft haben; Ihre Sprache und Währung; das Datum der Registrierung und der letzten Anmeldung; Ihr Empfehlungscode und wer Sie eingeladen hat. Zweck: Ihr Kundenkonto anlegen, Ihnen die Anmeldung ermöglichen sowie Anmeldecodes und Mitteilungen senden. Rechtsgrundlage: Vertragserfüllung.
|
||||
|
||||
**Schlüssel und Zahlungen.** Ihre Schlüssel, deren Verbindungen, Tarif, Standorte, Laufzeiten und die Einstellung der automatischen Verlängerung; Bestellungen, Zahlungen und der Verlauf der Guthabenbewegungen. Wenn Sie eine Karte hinterlegt haben: ihre Kennung beim Zahlungsdienstleister, der Kartentyp, die letzten 4 Ziffern und das Ablaufdatum. Die vollständige Kartennummer und der Sicherheitscode werden ausschließlich beim Zahlungsdienstleister eingegeben und erreichen uns nie. Zweck: den Dienst erbringen, Zahlungen annehmen und die Buchhaltung führen. Rechtsgrundlage: Vertragserfüllung und rechtliche Verpflichtungen.
|
||||
|
||||
**Support und Anfragen.** Ihre Nachrichten an den Support per E-Mail, über Telegram und im Chat auf der Website sowie interne Notizen der Support-Mitarbeiter. Wenn Sie angemeldet sind, zeigt der Chat dem Support-Mitarbeiter Ihre E-Mail-Adresse, Ihre Konto-ID und die Anzahl Ihrer Schlüssel. Daten aus Formularen auf der Website: Name, E-Mail-Adresse, Telegram, WhatsApp, Thema und Text der Nachricht sowie die Seite, von der aus das Formular gesendet wurde. Zweck: Ihnen antworten und Ihr Anliegen klären. Rechtsgrundlage: Vertragserfüllung, Maßnahmen auf Ihre Anfrage oder unser berechtigtes Interesse.
|
||||
|
||||
**Sicherheit.** IP-Adressen der Besucher der Website und des Kundenkontos werden in unserer Datenbank nur als nicht umkehrbare Hashwerte (HMAC) gespeichert. Wir nutzen sie, um zu begrenzen, wie oft Anmeldecodes angefordert werden können, und um Selbsteinladungen zu erkennen. Für Sitzungen speichern wir Angaben zu Browser und Gerät sowie den Zeitpunkt der letzten Aktivität. Anmeldecodes werden nur als Hashwerte gespeichert. Die Prüfung „Ich bin kein Roboter“ funktioniert ohne Dienste Dritter. Rechtsgrundlage: unser berechtigtes Interesse am Schutz des Dienstes und der Kundenkonten.
|
||||
|
||||
**Technische Logs.** Webserver-Logs enthalten die IP-Adresse, den Zeitpunkt, die aufgerufene Adresse und Angaben zum Browser; sie werden nach 14 Tagen gelöscht. Das Versandlog für E-Mails (Empfängeradresse, Betreff und Zustellstatus) wird 30 Tage lang aufbewahrt. Rechtsgrundlage: unser berechtigtes Interesse am Betrieb und Schutz des Dienstes.
|
||||
|
||||
**Besuchsstatistik.** Wir nutzen Umami auf unserem eigenen Server, ohne Cookies; die Daten werden nicht an Dritte weitergegeben. Erfasst werden die aufgerufenen Seiten, die verweisende Quelle, Browser- und Gerätetyp sowie das Land. Rechtsgrundlage: unser berechtigtes Interesse an der Verbesserung der Website.
|
||||
|
||||
Die Standardsprache der Website wird anhand Ihres Landes gewählt, das wir mithilfe einer Datenbank auf unserem eigenen Server aus Ihrer IP-Adresse ermitteln. Die IP-Adresse wird dafür nirgendwohin übermittelt.
|
||||
|
||||
## 5. Cookies
|
||||
|
||||
Unbedingt erforderliche Cookies werden für die Anmeldung und zum Speichern Ihrer Sprache und Währung verwendet. Der Support-Chat wird nur mit Ihrer Einwilligung geladen. Werbe-Cookies gibt es nicht. Einzelheiten finden Sie in der [Cookie-Richtlinie](https://{{site}}/legal/cookies).
|
||||
|
||||
## 6. An wen wir Daten weitergeben
|
||||
|
||||
Wir verkaufen keine Daten und geben sie nicht an Werbenetzwerke weiter. Daten erhalten nur:
|
||||
|
||||
- Zahlungsdienstleister: die für die Abwicklung einer Zahlung erforderlichen Angaben;
|
||||
- E-Mail-Anbieter: für die Zustellung unserer E-Mails, sofern ein externer Versanddienst genutzt wird, und für den Betrieb der Support-Postfächer;
|
||||
- Hosting-Anbieter, auf deren Servern die Website, das Kundenkonto, die Datenbank und die Support-Dienste laufen.
|
||||
|
||||
Der Support-Chat (Chatwoot), die Verwaltung von Anfragen (Twenty CRM) und die Analyse (Umami) laufen auf unseren eigenen Servern, nicht bei Drittanbietern.
|
||||
|
||||
Wenn Sie sich über Telegram anmelden oder uns über Telegram schreiben, verarbeitet Telegram die Daten nach eigenen Regeln.
|
||||
|
||||
Wir geben Daten an Behörden nur heraus, wenn das Gesetz es verlangt, und nur Daten, die wir tatsächlich haben. Logs über VPN-Datenverkehr haben wir nicht.
|
||||
|
||||
## 7. Wo Daten gespeichert werden
|
||||
|
||||
Die Website, das Kundenkonto und die Datenbank werden auf Servern in [Land des Hostings] betrieben. Die VPN-Server befinden sich in den Ländern der Standorte: Ihr Datenverkehr läuft über sie, aber sie führen keine Logs. Zahlungsdienstleister können Daten nach ihren eigenen Regeln in anderen Ländern verarbeiten. Werden Daten in Länder außerhalb Ihres Wohnsitzlandes übermittelt, geschieht dies auf Grundlagen, die das anwendbare Recht zulässt.
|
||||
|
||||
## 8. Wie lange wir Daten speichern
|
||||
|
||||
- Kontodaten: solange das Kundenkonto besteht. Nach der Löschung des Kundenkontos löschen oder anonymisieren wir diese Daten, mit Ausnahme der Bestell- und Zahlungsunterlagen, die wir gesetzlich für Buchhaltungs- und Steuerzwecke aufbewahren müssen.
|
||||
- Support-Korrespondenz und Anfragen: solange sie für die Bearbeitung des Anliegens erforderlich sind, jedoch nicht länger als [Aufbewahrungsfrist für Support-Daten].
|
||||
- Webserver-Logs: 14 Tage; Versandlog für E-Mails: 30 Tage.
|
||||
- Sitzungen: 30 Tage ab der letzten Aktivität oder bis zur Abmeldung.
|
||||
- Anmeldecodes: 10 Minuten gültig.
|
||||
|
||||
## 9. Wie wir Daten schützen
|
||||
|
||||
- Daten zwischen Ihrem Browser und unseren Servern werden bei der Übertragung verschlüsselt (HTTPS).
|
||||
- Verbindungskonfigurationen und Zahlungskennungen werden verschlüsselt gespeichert.
|
||||
- Anmeldecodes, Sitzungstoken und IP-Adressen werden nur als Hashwerte gespeichert.
|
||||
- Zugriff auf Daten haben nur Mitarbeiter, die sie für ihre Arbeit benötigen, und der Zugriff ist nach Rollen beschränkt.
|
||||
|
||||
Kein System ist völlig frei von Risiken. Betrifft eine Datenschutzverletzung Ihre Daten, benachrichtigen wir Sie gemäß den Vorgaben des anwendbaren Rechts.
|
||||
|
||||
## 10. Ihre Rechte
|
||||
|
||||
Je nach anwendbarem Recht können Sie:
|
||||
|
||||
- erfahren, welche Daten wir über Sie gespeichert haben, und eine Kopie davon erhalten;
|
||||
- unrichtige Daten berichtigen lassen;
|
||||
- Ihr Kundenkonto und Ihre Daten löschen lassen, mit Ausnahme der Daten, die wir gesetzlich aufbewahren müssen;
|
||||
- die Verarbeitung einschränken lassen oder ihr widersprechen;
|
||||
- Ihre Daten in einem maschinenlesbaren Format erhalten;
|
||||
- Ihre Einwilligung in funktionale Cookies widerrufen;
|
||||
- Beschwerde bei einer Datenschutzaufsichtsbehörde einlegen.
|
||||
|
||||
In Ihrem Kundenkonto können Sie Ihre Karte selbst entfernen und sich auf allen Geräten abmelden. Für alles Weitere schreiben Sie an [{{email}}](mailto:{{email}}) von der E-Mail-Adresse, die mit Ihrem Kundenkonto verknüpft ist, oder über Ihr verknüpftes Telegram-Konto, damit wir bestätigen können, dass die Anfrage von Ihnen stammt. Wir antworten innerhalb der Fristen, die das anwendbare Recht vorsieht.
|
||||
|
||||
## 11. Kinder
|
||||
|
||||
Der Dienst richtet sich nicht an Personen unter [Mindestalter] Jahren, und wir erheben wissentlich keine Daten von ihnen. Wenn Sie glauben, dass ein Kind uns seine Daten übermittelt hat, kontaktieren Sie uns, und wir löschen sie.
|
||||
|
||||
## 12. Änderungen dieser Datenschutzerklärung
|
||||
|
||||
Wir können diese Datenschutzerklärung aktualisieren. Die neue Fassung wird mit ihrem Datum des Inkrafttretens auf dieser Seite veröffentlicht. Über wesentliche Änderungen informieren wir Sie per E-Mail oder in Ihrem Kundenkonto.
|
||||
|
||||
## 13. Kontakt
|
||||
|
||||
- E-Mail: [{{email}}](mailto:{{email}})
|
||||
- Postanschrift: [Eingetragene Anschrift]
|
||||
|
||||
## 14. Sprache
|
||||
|
||||
Diese Datenschutzerklärung wird in mehreren Sprachen veröffentlicht. Bei Abweichungen zwischen den Sprachfassungen ist die englische Fassung maßgeblich.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Gültig ab [Datum des Inkrafttretens].
|
||||
|
||||
Diese Richtlinie ergänzt die [Nutzungsbedingungen](https://{{site}}/legal/terms) des Dienstes {{brand}}, der von [Name der juristischen Person] bereitgestellt wird. Die Richtlinie erklärt, wann und wie wir Geld für bezahlte Bestellungen erstatten: neue Schlüssel und Verlängerungen.
|
||||
|
||||
## 1. Wann wir erstatten
|
||||
|
||||
**Der Dienst hat durch unser Verschulden nicht funktioniert.** Wir erstatten den vollen Bestellbetrag, wenn Sie sich innerhalb der ersten 7 Tage des bezahlten Zeitraums an uns wenden; der bezahlte Zeitraum beginnt mit der Ausgabe des Schlüssels. Das gilt zum Beispiel, wenn wir den Schlüssel nicht ausgeben konnten oder die Verbindungen wegen eines Problems auf unserer Seite nicht funktioniert haben.
|
||||
|
||||
**Sie haben es sich anders überlegt.** Wir erstatten den nicht genutzten Rest des bezahlten Zeitraums anteilig nach Tagen, berechnet zum Datum Ihrer Anfrage: Wir teilen den für den Zeitraum gezahlten Betrag durch die Anzahl seiner Tage und multiplizieren das Ergebnis mit der Anzahl der nicht genutzten Tage. Beträgt der Zeitraum zum Beispiel 30 Tage und sind 10 davon vergangen, erstatten wir den Preis für 20 Tage. Das gilt auch für Verlängerungen: Wurde ein Schlüssel automatisch verlängert und Sie benötigen ihn nicht mehr, schreiben Sie uns, und wir erstatten die nicht genutzten Tage.
|
||||
|
||||
Nach einer Erstattung wird der betreffende Schlüssel deaktiviert.
|
||||
|
||||
## 2. So beantragen Sie eine Erstattung
|
||||
|
||||
Schreiben Sie an [{{email}}](mailto:{{email}}) von der E-Mail-Adresse, die mit Ihrem Kundenkonto verknüpft ist, und geben Sie an:
|
||||
|
||||
- die Bestellnummer oder den Schlüsselcode – beides finden Sie in Ihrem Kundenkonto;
|
||||
- den Grund für die Erstattung;
|
||||
- wenn der Dienst nicht funktioniert hat: was genau nicht funktioniert hat und seit wann.
|
||||
|
||||
Wenn Ihr Kundenkonto keine E-Mail-Adresse hat und Sie sich über Telegram anmelden, schreiben Sie uns über Telegram: Den Link finden Sie auf der [Support-Seite](https://{{site}}/support). Um das Problem zu verstehen, bitten wir Sie möglicherweise um weitere Angaben.
|
||||
|
||||
## 3. Wohin erstattet wird
|
||||
|
||||
Erstattungen erfolgen auf das Guthaben Ihres Kundenkontos in dessen Währung oder, sofern der Zahlungsdienstleister dies zulässt, auf die ursprüngliche Zahlungsart. Eine Erstattung auf das Guthaben wird gutgeschrieben, sobald sie genehmigt ist, und wir informieren Sie per E-Mail über die Erstattung. Wie lange es dauert, bis eine Erstattung auf Ihrer Karte oder bei einer anderen Zahlungsart eingeht, hängt vom Zahlungsdienstleister und Ihrer Bank ab.
|
||||
|
||||
## 4. Bearbeitungsdauer
|
||||
|
||||
Wir prüfen Ihre Anfrage und teilen Ihnen unsere Entscheidung innerhalb von [Bearbeitungsfrist für Rückerstattungen] mit.
|
||||
|
||||
## 5. Was nicht erstattet wird
|
||||
|
||||
- Bonustage aus dem Empfehlungsprogramm und andere Tage, die einem Schlüssel ohne Zahlung hinzugefügt wurden: Diese Tage sind kein Geld und können weder erstattet noch in Geld umgetauscht werden.
|
||||
- Der bereits genutzte Teil des bezahlten Zeitraums, wenn Sie es sich anders überlegt haben.
|
||||
- Der verbleibende Zeitraum eines Schlüssels, der wegen eines Verstoßes gegen die [Nutzungsbedingungen](https://{{site}}/legal/terms) gesperrt wurde, sofern das anwendbare Recht nichts anderes vorschreibt.
|
||||
|
||||
## 6. Kontakt
|
||||
|
||||
- E-Mail: [{{email}}](mailto:{{email}})
|
||||
- Telegram: Link auf der [Support-Seite](https://{{site}}/support)
|
||||
|
||||
Der Support ist an Arbeitstagen von 10:00 bis 20:00 Uhr Moskauer Zeit erreichbar.
|
||||
|
||||
## 7. Sprache
|
||||
|
||||
Diese Richtlinie wird in mehreren Sprachen veröffentlicht. Bei Abweichungen zwischen den Sprachfassungen ist die englische Fassung maßgeblich.
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Gültig ab [Datum des Inkrafttretens].
|
||||
|
||||
## 1. Allgemeines
|
||||
|
||||
Diese Bedingungen sind eine Vereinbarung zwischen Ihnen und [Name der juristischen Person], eingetragen in [Land der Registrierung] unter der Nummer [Registernummer], mit eingetragener Anschrift [Eingetragene Anschrift] („wir“, „uns“). Diese Bedingungen regeln Ihre Nutzung des Dienstes {{brand}}: der Website {{site}}, Ihres Kundenkontos unter {{cabinet}}, der Telegram-Bots, der Telegram-Mini-App und der VPN-Server (zusammen der „Dienst“).
|
||||
|
||||
Mit der Nutzung des Dienstes akzeptieren Sie diese Bedingungen. Bei einer Bestellung bestätigen Sie Ihre Zustimmung gesondert. Zusätzlich gelten die [Datenschutzerklärung](https://{{site}}/legal/privacy), die [Rückerstattungsrichtlinie](https://{{site}}/legal/refund) und die [Cookie-Richtlinie](https://{{site}}/legal/cookies).
|
||||
|
||||
Um den Dienst zu nutzen, müssen Sie mindestens [Mindestalter] Jahre alt sein.
|
||||
|
||||
## 2. Der Dienst
|
||||
|
||||
Der Dienst {{brand}} stellt VPN-Zugang über Schlüssel bereit. Ein Schlüssel ist eine gekaufte Zugangseinheit mit einer Gültigkeitsdauer. Zu jedem Schlüssel gehören Verbindungen: Konfigurationen und Links für Apps, die Sie in Ihrem Kundenkonto erhalten.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Besonderheiten von {{brand}}:
|
||||
|
||||
- jeder Schlüssel hat eine eigene dedizierte IP-Adresse in dem Land, das Sie beim Kauf wählen. Die Adresse ist ausschließlich für Sie reserviert und läuft auf einem gemeinsam genutzten Server, nicht auf einem separaten;
|
||||
- das Land eines Schlüssels ändert sich nie. Für ein anderes Land benötigen Sie einen separaten Schlüssel an einem anderen Standort; die automatische Verlängerung des bisherigen Schlüssels können Sie in Ihrem Kundenkonto deaktivieren;
|
||||
- jeder Schlüssel enthält eine dedizierte RU-Adresse;
|
||||
- alle Geräte eines Schlüssels nutzen dieselbe Adresse;
|
||||
- Protokolle: WireGuard / AmneziaWG und Xray (VLESS, Hysteria2, XHTTP);
|
||||
- jeder Standort ist mit „Server verfügbar“ oder „In 24 Stunden bereit“ gekennzeichnet. Im zweiten Fall bereiten wir die Adresse nach der Zahlung innerhalb von 24 Stunden vor. Der bezahlte Zeitraum beginnt mit der Ausgabe des Schlüssels;
|
||||
- der erste Monat kostet mehr als eine Verlängerung: Er umfasst die Vorbereitung der Adresse sowie die Einrichtung der Protokolle und des Routings.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
<!-- mass -->
|
||||
|
||||
Besonderheiten von {{brand}}:
|
||||
|
||||
- die Server werden gemeinsam genutzt: Viele Kunden nutzen dieselbe Adresse;
|
||||
- ein Schlüssel (Abonnement) gewährt Zugang zu allen Ländern des Tarifs mit dem Status „Inklusive“. Standorte mit dem Status „In Kürze“ werden auf der Website angezeigt, funktionieren aber noch nicht;
|
||||
- Sie können das Land beliebig oft und kostenlos wechseln;
|
||||
- jeder Schlüssel enthält eine russische Adresse; sie zählt nicht zu den Ländern des Tarifs;
|
||||
- Protokolle: Xray und WireGuard;
|
||||
- Tarife gibt es monatlich oder jährlich; der erste Zeitraum kostet so viel wie eine Verlängerung.
|
||||
|
||||
<!-- /mass -->
|
||||
|
||||
Preise, Laufzeiten, Gerätelimits und Standortlisten sind auf {{site}} und in Ihrem Kundenkonto angegeben. Verlängerungspreis, Laufzeit und Gerätelimit eines Schlüssels werden beim Kauf festgelegt.
|
||||
|
||||
## 3. Kundenkonto und Anmeldung
|
||||
|
||||
Eine gesonderte Registrierung und ein Passwort gibt es nicht: Sie geben Ihre E-Mail-Adresse und einen Einmalcode aus unserer E-Mail ein, und Ihr Kundenkonto wird bei der ersten Anmeldung angelegt. Der Code ist 10 Minuten gültig. Sie können sich auch über Telegram anmelden.
|
||||
|
||||
Ihr Kundenkonto beim Dienst {{brand}} ist nicht mit Konten in unseren anderen Diensten verknüpft: Jedes hat sein eigenes Guthaben, eigene Schlüssel und einen eigenen Verlauf.
|
||||
|
||||
Sie sind für den Zugang zu Ihrem E-Mail-Postfach und zu Telegram verantwortlich und dürfen Anmeldecodes nicht an andere Personen weitergeben. Wenn Sie vermuten, dass jemand auf Ihr Kundenkonto zugegriffen hat, melden Sie sich in Ihrem Kundenkonto auf allen Geräten ab und kontaktieren Sie uns.
|
||||
|
||||
Verwenden Sie eine funktionierende E-Mail-Adresse: An sie senden wir Anmeldecodes und Mitteilungen zu Bestellungen, Zahlungen und Ablaufdaten von Schlüsseln.
|
||||
|
||||
## 4. Schlüssel, Geräte und Verbindungen
|
||||
|
||||
Sie dürfen einen Schlüssel auf so vielen Geräten nutzen, wie das Gerätelimit des Tarifs erlaubt, bis zu 5. Die Verbindungsdaten (Konfigurationen, Links und QR-Codes) sind nur für Ihre Geräte bestimmt: Veröffentlichen Sie sie nicht und geben Sie sie nicht an andere Personen weiter.
|
||||
|
||||
Für die Verbindung benötigen Sie Apps von Drittanbietern, etwa WireGuard- oder Xray-Clients. Für diese Apps gelten die Regeln ihrer Entwickler.
|
||||
|
||||
Die Auswahl an Protokollen und die Verbindungsdaten können aktualisiert werden, damit der Dienst weiter funktioniert. In Ihrem Kundenkonto finden Sie stets die aktuellen Daten.
|
||||
|
||||
Jeder Schlüssel wird unabhängig von den anderen verlängert und läuft unabhängig von ihnen ab.
|
||||
|
||||
## 5. Verfügbarkeit des Dienstes
|
||||
|
||||
Wir arbeiten an einem stabilen Betrieb des Dienstes, garantieren aber nicht, dass er jederzeit und ohne Unterbrechungen verfügbar ist. Es kann geplante Wartungsarbeiten, Störungen bei uns oder bei unseren Dienstleistern sowie Einschränkungen durch Netzbetreiber, Website-Betreiber und Behörden geben.
|
||||
|
||||
Wir garantieren keinen Zugang zu bestimmten Websites oder Diensten und sind nicht dafür verantwortlich, wie diese mit VPN-Verbindungen umgehen.
|
||||
|
||||
Geplante Wartungsarbeiten kündigen wir nach Möglichkeit im Voraus an. War der Dienst durch unser Verschulden längere Zeit nicht verfügbar, können wir Ihren Schlüssel um die Ausfallzeit verlängern.
|
||||
|
||||
Die Liste der Standorte, Protokolle und Tarife kann sich ändern.
|
||||
|
||||
## 6. Zahlung, Guthaben und automatische Verlängerung
|
||||
|
||||
Auf der Website werden Preise in der von Ihnen gewählten Währung angezeigt, in Ihrem Kundenkonto in Ihrer Kontowährung: russische Rubel oder US-Dollar. Die Kontowährung wird bei der Registrierung festgelegt. Sie können sie ändern, solange Ihr Guthaben null beträgt und Ihr Kundenkonto keine Schlüssel und keine unbezahlten Bestellungen hat; danach nur über den Support.
|
||||
|
||||
Ihr Kundenkonto hat ein Guthaben. Zahlungen werden dem Guthaben gutgeschrieben, und Bestellungen werden daraus bezahlt; reicht das Guthaben nicht aus, zahlen Sie nur die Differenz. Eine unbezahlte Bestellung ist 24 Stunden gültig. Geld, das nach der Stornierung einer Bestellung eingeht, verbleibt in Ihrem Guthaben.
|
||||
|
||||
Die Zahlungsarten werden bei der Bestellung angezeigt: Bankkarten, Kryptowährungen und weitere. Kartendaten werden ausschließlich beim Zahlungsdienstleister eingegeben und erreichen uns nie. Bei Zahlung in Kryptowährung wird der Betrag zu dem bei Ausstellung der Rechnung festgelegten Kurs umgerechnet und in Ihrer Kontowährung gutgeschrieben, sobald das Netzwerk die Transaktion bestätigt.
|
||||
|
||||
Die automatische Verlängerung wird für jeden Schlüssel einzeln eingestellt und ist bei neuen Schlüsseln aktiviert. Kurz vor Ablauf eines Schlüssels wird der Verlängerungspreis von Ihrem Guthaben abgebucht; reicht das Guthaben nicht aus, wird der Fehlbetrag Ihrer hinterlegten Karte belastet, sofern Sie eine hinterlegt haben. Die Karte ist mit Ihrem Kundenkonto verknüpft und wird für alle Schlüssel mit aktivierter automatischer Verlängerung verwendet. Mit dem Hinterlegen einer Karte ermächtigen Sie uns zu diesen Belastungen. Das Hinterlegen einer Karte ist nicht bei jeder Zahlungsart und Währung möglich. Schlägt eine Belastung fehl, informieren wir Sie per E-Mail und versuchen es mehrmals erneut, auch während der Kulanzfrist.
|
||||
|
||||
Wir senden 3 Tage vor Ablauf eines Schlüssels eine Erinnerung. Die automatische Verlängerung können Sie jederzeit in Ihrem Kundenkonto deaktivieren. Ihre Karte können Sie im Bereich „Guthaben & Zahlungen“ entfernen; danach werden Verlängerungen nur noch aus dem Guthaben bezahlt.
|
||||
|
||||
Sie können einen Schlüssel im Voraus verlängern: Der neue Zeitraum schließt an das aktuelle Ablaufdatum an.
|
||||
|
||||
Der Verlängerungspreis wird beim Kauf eines Schlüssels festgelegt und ist in Ihrem Kundenkonto angegeben. Ändern wir ihn, informieren wir Sie mindestens [Ankündigungsfrist], bevor wir den neuen Preis berechnen.
|
||||
|
||||
## 7. Kulanzfrist und Ablauf
|
||||
|
||||
Wird eine Verlängerung nicht bis zum Ablaufdatum bezahlt, beginnt eine Kulanzfrist von 3 Tagen. Während dieser Zeit funktioniert der Schlüssel weiter. Eine während der Kulanzfrist bezahlte Verlängerung läuft ab dem ursprünglichen Ablaufdatum, nicht ab dem Zahlungsdatum.
|
||||
|
||||
Mit dem Ende der Kulanzfrist läuft der Schlüssel ab, und seine Verbindungen werden deaktiviert. Diese Regel gilt gleichermaßen für Schlüssel mit und ohne automatische Verlängerung.
|
||||
|
||||
Ein abgelaufener Schlüssel kann nicht verlängert werden: Um den Dienst weiter zu nutzen, kaufen Sie einen neuen Schlüssel. Ein neuer Schlüssel erhält neue Verbindungsdaten; dass die bisherigen Adressen erhalten bleiben, ist nicht garantiert.
|
||||
|
||||
## 8. Rückerstattungen
|
||||
|
||||
Hat der Dienst durch unser Verschulden nicht funktioniert, erstatten wir den vollen Bestellbetrag, wenn Sie sich innerhalb der ersten 7 Tage des bezahlten Zeitraums an uns wenden. Haben Sie es sich anders überlegt, erstatten wir den nicht genutzten Rest des bezahlten Zeitraums anteilig nach Tagen. Erstattungen erfolgen auf Ihr Guthaben oder, sofern der Zahlungsdienstleister dies zulässt, auf die ursprüngliche Zahlungsart. Einzelheiten finden Sie in der [Rückerstattungsrichtlinie](https://{{site}}/legal/refund).
|
||||
|
||||
## 9. Empfehlungsprogramm
|
||||
|
||||
„Laden Sie einen Freund ein – Sie beide erhalten einen Monat“: Wenn ein Freund, der sich über Ihre Einladung registriert hat, seine erste Bestellung eines Schlüssels bezahlt, erhält jeder von Ihnen 30 zusätzliche Tage für einen Schlüssel.
|
||||
|
||||
- Ihr Freund erhält die Tage für den mit dieser Bestellung gekauften Schlüssel. Sie erhalten sie für Ihren zuletzt gekauften Schlüssel, der weder abgelaufen noch storniert ist; gibt es keinen solchen Schlüssel, werden die Tage Ihrem nächsten Schlüssel hinzugefügt.
|
||||
- Das Aufladen des Guthabens gilt nicht als Kauf.
|
||||
- Ein Einladungslink wird im Browser Ihres Freundes 60 Tage lang gespeichert.
|
||||
- Selbsteinladungen werden nicht berücksichtigt. Zweifelhafte Einladungen prüfen wir manuell und können sie ablehnen.
|
||||
- Es zählen nur Einladungen zum Dienst {{brand}}: Registrierungen und Käufe in unseren anderen Diensten werden nicht berücksichtigt.
|
||||
- Bonustage sind kein Geld: Ein Umtausch in Geld oder eine Erstattung ist nicht möglich.
|
||||
- Wir können das Programm ändern oder beenden. Bereits gutgeschriebene Tage bleiben erhalten.
|
||||
|
||||
## 10. Zulässige Nutzung
|
||||
|
||||
Sie dürfen den Dienst nicht nutzen für:
|
||||
|
||||
- rechtswidrige Tätigkeiten jeder Art;
|
||||
- Angriffe auf Netzwerke, Websites oder Geräte, Hacking oder unbefugten Zugriff auf fremde Systeme;
|
||||
- den Versand von Spam;
|
||||
- Betrug oder Phishing;
|
||||
- das Erstellen oder Verbreiten von Schadsoftware oder die Steuerung von Botnetzen;
|
||||
- das Erstellen, Speichern oder Verbreiten von Darstellungen sexuellen Missbrauchs von Kindern;
|
||||
- die Verletzung der Rechte anderer, einschließlich Urheberrechten und des Rechts auf Privatsphäre;
|
||||
- die Nutzung eines Schlüssels über sein Gerätelimit hinaus oder den Weiterverkauf oder die sonstige Weitergabe des Zugangs an Dritte ohne gesonderte Vereinbarung mit uns;
|
||||
- alles, was den Dienst oder andere Kunden stört oder die Beschränkungen des Dienstes umgeht.
|
||||
|
||||
Sie sind für alles verantwortlich, was über Ihren Schlüssel und Ihr Kundenkonto geschieht.
|
||||
|
||||
## 11. Aussetzung und Sperrung
|
||||
|
||||
Wenn Sie gegen diese Bedingungen verstoßen, können wir die Nutzung Ihrer Schlüssel aussetzen oder Ihr Kundenkonto sperren. In der Regel warnen wir Sie zuerst und bitten Sie, den Verstoß einzustellen. Bei schweren Verstößen (Angriffe, Betrug, Verbreitung von Schadsoftware oder von Darstellungen sexuellen Missbrauchs von Kindern) sperren wir sofort und ohne Vorwarnung.
|
||||
|
||||
Wird ein Schlüssel wegen eines Verstoßes gesperrt, wird der verbleibende bezahlte Zeitraum nicht erstattet, sofern das anwendbare Recht nichts anderes vorschreibt. Wenn Sie mit einer Sperrung nicht einverstanden sind, schreiben Sie an [{{email}}](mailto:{{email}}).
|
||||
|
||||
Sie können die Nutzung des Dienstes jederzeit beenden: Deaktivieren Sie dazu die automatische Verlängerung Ihrer Schlüssel. Um Ihr Kundenkonto zu löschen, kontaktieren Sie uns.
|
||||
|
||||
## 12. Haftungsbeschränkung
|
||||
|
||||
Der Dienst wird „wie besehen“ bereitgestellt. Soweit nach anwendbarem Recht zulässig, gilt:
|
||||
|
||||
- wir haften nicht für mittelbare Schäden, entgangenen Gewinn oder Datenverlust;
|
||||
- wir haften nicht für Unterbrechungen, die durch Netzbetreiber, Behörden, Website-Betreiber oder sonstige Dritte oder durch Störungen Ihrer Geräte und Apps verursacht werden;
|
||||
- unsere Haftung für jeden Anspruch ist auf den Betrag begrenzt, den Sie für den bezahlten Zeitraum des Schlüssels gezahlt haben, auf den sich der Anspruch bezieht.
|
||||
|
||||
Diese Beschränkungen berühren keine Rechte, die gesetzlich nicht beschränkt werden können, einschließlich der Verbraucherrechte.
|
||||
|
||||
## 13. Änderungen dieser Bedingungen
|
||||
|
||||
Wir können diese Bedingungen ändern. Die neue Fassung wird mit ihrem Datum des Inkrafttretens auf dieser Seite veröffentlicht. Über wesentliche Änderungen informieren wir Sie per E-Mail oder in Ihrem Kundenkonto mindestens [Ankündigungsfrist] vor ihrem Inkrafttreten. Wenn Sie mit der neuen Fassung nicht einverstanden sind, beenden Sie die Nutzung des Dienstes und deaktivieren Sie die automatische Verlängerung; den nicht genutzten Rest können Sie sich gemäß der [Rückerstattungsrichtlinie](https://{{site}}/legal/refund) erstatten lassen.
|
||||
|
||||
## 14. Anwendbares Recht und Streitigkeiten
|
||||
|
||||
Diese Bedingungen unterliegen [Anwendbares Recht]. Jede Streitigkeit versuchen wir zunächst im Schriftverkehr beizulegen: Schreiben Sie an [{{email}}](mailto:{{email}}). Kommt keine Einigung zustande, wird die Streitigkeit verwiesen an: [Ort der Streitbeilegung]. Dadurch verlieren Sie nicht den Schutz, den Ihnen zwingende Verbraucherschutzvorschriften gewähren, sofern diese auf Sie anwendbar sind.
|
||||
|
||||
## 15. Kontakt
|
||||
|
||||
- E-Mail: [{{email}}](mailto:{{email}})
|
||||
- Telegram: Link auf der [Support-Seite](https://{{site}}/support)
|
||||
- Postanschrift: [Eingetragene Anschrift]
|
||||
|
||||
Der Support ist an Arbeitstagen von 10:00 bis 20:00 Uhr Moskauer Zeit erreichbar.
|
||||
|
||||
## 16. Sprache
|
||||
|
||||
Diese Bedingungen werden in mehreren Sprachen veröffentlicht. Bei Abweichungen zwischen den Sprachfassungen ist die englische Fassung maßgeblich.
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Effective from [Effective date].
|
||||
|
||||
This policy explains which cookies the {{brand}} service uses on {{site}} and {{cabinet}}. The service is provided by [Legal entity name]. How we process data in general is described in the [Privacy policy](https://{{site}}/legal/privacy).
|
||||
|
||||
## 1. What cookies are
|
||||
|
||||
Cookies are small pieces of data that a website stores in your browser to remember things such as that you are signed in or which language you chose. localStorage works in a similar way: it is storage in your browser whose contents are not sent to the server.
|
||||
|
||||
## 2. Cookies we use
|
||||
|
||||
Each cookie is listed with its purpose, lifetime and category.
|
||||
|
||||
- `vx_session` — keeps you signed in to your account, set only on {{cabinet}} — 30 days, extended while you use your account — strictly necessary.
|
||||
- `NEXT_LOCALE` — the language chosen in the switcher — 1 year — strictly necessary.
|
||||
- `vx_currency` — the currency chosen for prices or, if you are signed in, your account currency — 1 year — strictly necessary.
|
||||
- `vx_ref` — the invitation code, if you followed a referral link, set only on {{cabinet}} — 60 days — strictly necessary.
|
||||
- `vx_consent` — your choice in the cookie banner — 1 year — strictly necessary.
|
||||
- `cw_*` — support chat cookies (Chatwoot): keep your conversation with support between visits — [Chat cookie lifetime] — functional.
|
||||
|
||||
**Strictly necessary** cookies are needed for the website and your account to work: without them you cannot sign in, and your language and currency choices are not saved. We do not ask for consent to them.
|
||||
|
||||
**Functional** cookies are set only with your consent. If you have not given it, the chat widget is not loaded; you can still reach us by email or in Telegram.
|
||||
|
||||
## 3. Analytics without cookies
|
||||
|
||||
We collect visit statistics with Umami on our own server. Umami does not use cookies, and the data is not passed to third parties.
|
||||
|
||||
## 4. Advertising cookies
|
||||
|
||||
We do not use advertising cookies or trackers.
|
||||
|
||||
## 5. localStorage
|
||||
|
||||
The theme (light, dark or matching the system) is stored not in a cookie but in localStorage under the key `vx_theme`. Each domain keeps its own setting.
|
||||
|
||||
## 6. Cookies of other services
|
||||
|
||||
- The Telegram sign-in button on the sign-in page is loaded from Telegram’s servers, and Telegram may set its own cookies under its own rules.
|
||||
- At the payment step, the payment provider’s page or widget may set its own cookies needed to process the payment and prevent fraud.
|
||||
|
||||
## 7. Managing cookies
|
||||
|
||||
On your first visit, a banner lets you accept or decline functional cookies. To change your choice, delete the `vx_consent` cookie or all cookies for the site in your browser settings, and the banner will appear again.
|
||||
|
||||
You can delete or block cookies in your browser settings. Without strictly necessary cookies, you cannot sign in to your account, and your language and currency choices are not saved.
|
||||
|
||||
## 8. Contacts
|
||||
|
||||
Send questions about cookies to [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 9. Language
|
||||
|
||||
This policy is published in several languages. If the language versions differ, the English version prevails.
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Effective from [Effective date].
|
||||
|
||||
## 1. Who we are
|
||||
|
||||
The {{brand}} service (the website {{site}}, your account at {{cabinet}}, the Telegram bots, the Telegram mini app and the VPN servers) is provided by [Legal entity name], registered in [Country of registration] under number [Registration number], with its registered address at [Registered address] (“we”, “us”). We are the controller of your personal data.
|
||||
|
||||
This policy explains what data we collect, why, how long we keep it and who we share it with. For any questions about your data, write to [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 2. In short
|
||||
|
||||
- The VPN servers keep no logs: no traffic, no visited sites, no connection times, no user IP addresses.
|
||||
- We keep only what is needed for your account, payments, support and protection against abuse.
|
||||
- In our database, IP addresses of website and account visitors are stored only as one-way hashes; web server logs are deleted after 14 days.
|
||||
- We do not sell data and do not use advertising trackers.
|
||||
|
||||
## 3. The VPN servers keep no logs
|
||||
|
||||
Our VPN servers do not record:
|
||||
|
||||
- your traffic or its content;
|
||||
- the websites and services you visit;
|
||||
- when you connect or disconnect;
|
||||
- the IP addresses you connect from.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Your dedicated IP and RU address are assigned to your key, so your account data shows which addresses have been issued to the key. We do not record what happens through these addresses.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
## 4. What we collect and why
|
||||
|
||||
**Account.** Your email; your Telegram ID, username and name, if you sign in with Telegram or have linked it; your language and currency; the dates of registration and last sign-in; your referral code and who invited you. Purpose: to create your account, let you sign in, and send sign-in codes and notices. Legal basis: performance of the contract.
|
||||
|
||||
**Keys and payments.** Your keys, their connections, plan, locations, dates and auto-renewal setting; orders, payments and the history of balance transactions. If you linked a card: its identifier at the payment provider, the card type, the last 4 digits and the expiry date. The full card number and security code are entered only on the provider’s side and never reach us. Purpose: to provide the service, accept payments and keep accounting records. Legal basis: performance of the contract and legal obligations.
|
||||
|
||||
**Support and requests.** Your messages to support by email, in Telegram and in the chat on the website, as well as internal notes made by support staff. If you are signed in, the chat shows the operator your email, account ID and number of keys. Data from forms on the website: name, email, Telegram, WhatsApp, the topic and text of the message, and the page the form was sent from. Purpose: to reply to you and resolve your request. Legal basis: performance of the contract, steps taken at your request, or our legitimate interest.
|
||||
|
||||
**Security.** IP addresses of website and account visitors are stored in our database only as one-way hashes (HMAC). We use them to limit how often sign-in codes can be requested and to detect self-invitations. For sessions, we store browser and device information and the time of last activity. Sign-in codes are stored only as hashes. The “I am not a robot” check works without third-party services. Legal basis: our legitimate interest in protecting the service and accounts.
|
||||
|
||||
**Technical logs.** Web server logs contain the IP address, time, requested address and browser information; they are deleted after 14 days. The email delivery log (recipient address, subject and delivery status) is kept for 30 days. Legal basis: our legitimate interest in running and protecting the service.
|
||||
|
||||
**Visit statistics.** We use Umami on our own server, without cookies; the data is not passed to third parties. It records the pages opened, the referring source, the browser and device type, and the country. Legal basis: our legitimate interest in improving the website.
|
||||
|
||||
The default website language is chosen based on your country, which we determine from your IP address using a database on our own server. The IP address is not sent anywhere for this.
|
||||
|
||||
## 5. Cookies
|
||||
|
||||
Strictly necessary cookies are used for sign-in and to remember your language and currency. The support chat loads only with your consent. There are no advertising cookies. Details are in the [Cookie policy](https://{{site}}/legal/cookies).
|
||||
|
||||
## 6. Who we share data with
|
||||
|
||||
We do not sell data or pass it to advertising networks. Data is received only by:
|
||||
|
||||
- payment providers: the information needed to process a payment;
|
||||
- email providers: to deliver our emails, if an external sending service is used, and to run the support mailboxes;
|
||||
- hosting providers whose servers run the website, the account area, the database and the support services.
|
||||
|
||||
The support chat (Chatwoot), request tracking (Twenty CRM) and analytics (Umami) run on our own servers, not with third-party vendors.
|
||||
|
||||
If you sign in with Telegram or write to us in Telegram, Telegram processes the data under its own rules.
|
||||
|
||||
We disclose data to public authorities only when the law requires it, and only data we actually have. We have no VPN traffic logs.
|
||||
|
||||
## 7. Where data is stored
|
||||
|
||||
The website, the account area and the database are hosted on servers in [Hosting country]. The VPN servers are located in the countries of the locations: your traffic passes through them, but they keep no logs. Payment providers may process data in other countries under their own rules. Where data is transferred outside your country of residence, we do so on grounds permitted by applicable law.
|
||||
|
||||
## 8. How long we keep data
|
||||
|
||||
- Account data: as long as the account exists. After the account is deleted, we delete or anonymize this data, except for order and payment records that the law requires us to keep for accounting and tax purposes.
|
||||
- Support conversations and requests: as long as needed to handle the request, but no longer than [Support data retention period].
|
||||
- Web server logs: 14 days; email delivery log: 30 days.
|
||||
- Sessions: 30 days from last activity, or until you sign out.
|
||||
- Sign-in codes: valid for 10 minutes.
|
||||
|
||||
## 9. How we protect data
|
||||
|
||||
- Data between your browser and our servers is encrypted in transit (HTTPS).
|
||||
- Connection configurations and payment identifiers are stored encrypted.
|
||||
- Sign-in codes, session tokens and IP addresses are stored only as hashes.
|
||||
- Only staff who need data for their work have access to it, and access is limited by role.
|
||||
|
||||
No system is completely free of risk. If a breach affects your data, we will notify you as required by applicable law.
|
||||
|
||||
## 10. Your rights
|
||||
|
||||
Depending on applicable law, you may:
|
||||
|
||||
- find out what data we hold about you and get a copy of it;
|
||||
- correct inaccurate data;
|
||||
- delete your account and data, except data we are required by law to keep;
|
||||
- restrict or object to processing;
|
||||
- receive your data in a machine-readable format;
|
||||
- withdraw your consent to functional cookies;
|
||||
- lodge a complaint with a data protection authority.
|
||||
|
||||
In your account, you can unlink your card and sign out on all devices yourself. For anything else, write to [{{email}}](mailto:{{email}}) from the email address linked to your account, or from your linked Telegram, so that we can confirm the request comes from you. We reply within the time limits set by applicable law.
|
||||
|
||||
## 11. Children
|
||||
|
||||
The service is not intended for anyone under the age of [Minimum age], and we do not knowingly collect their data. If you believe a child has given us their data, contact us and we will delete it.
|
||||
|
||||
## 12. Changes to this policy
|
||||
|
||||
We may update this policy. The new version is published on this page with its effective date. We notify you of material changes by email or in your account.
|
||||
|
||||
## 13. Contacts
|
||||
|
||||
- Email: [{{email}}](mailto:{{email}})
|
||||
- Postal address: [Registered address]
|
||||
|
||||
## 14. Language
|
||||
|
||||
This policy is published in several languages. If the language versions differ, the English version prevails.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Effective from [Effective date].
|
||||
|
||||
This policy supplements the [Terms of use](https://{{site}}/legal/terms) of the {{brand}} service provided by [Legal entity name]. It explains when and how we refund money for paid orders: new keys and renewals.
|
||||
|
||||
## 1. When we refund
|
||||
|
||||
**The service did not work through our fault.** We refund the full order amount if you contact us within the first 7 days of the paid period; the paid period starts when the key is issued. For example, if we could not issue the key, or the connections did not work because of a problem on our side.
|
||||
|
||||
**You changed your mind.** We refund the unused remainder of the paid period, pro rata by days, as of the date of your request: we divide the amount paid for the period by its number of days and multiply by the number of unused days. For example, if the period is 30 days and 10 have passed, we refund the cost of 20 days. This also applies to renewals: if a key was renewed automatically and you no longer need it, write to us and we will refund the unused days.
|
||||
|
||||
After a refund, the key it relates to is disabled.
|
||||
|
||||
## 2. How to request a refund
|
||||
|
||||
Write to [{{email}}](mailto:{{email}}) from the email address linked to your account and include:
|
||||
|
||||
- the order number or key code, both shown in your account;
|
||||
- the reason for the refund;
|
||||
- if the service did not work: what exactly did not work and since when.
|
||||
|
||||
If your account has no email and you sign in with Telegram, write to us in Telegram: the link is on the [support page](https://{{site}}/support). We may ask for more details to understand the problem.
|
||||
|
||||
## 3. Where refunds go
|
||||
|
||||
Refunds go to your account balance in its currency or, where the payment provider allows, to the original payment method. A refund to the balance is credited as soon as it is approved, and we notify you of the refund by email. How long a refund takes to reach your card or other payment method depends on the provider and your bank.
|
||||
|
||||
## 4. Processing time
|
||||
|
||||
We review your request and let you know our decision within [Refund processing time].
|
||||
|
||||
## 5. What is not refunded
|
||||
|
||||
- Referral bonus days and other days added to a key without payment: they are not money and cannot be refunded or exchanged for money.
|
||||
- The used part of the paid period, if you changed your mind.
|
||||
- The remaining period of a key blocked for a breach of the [Terms of use](https://{{site}}/legal/terms), unless applicable law requires otherwise.
|
||||
|
||||
## 6. Contacts
|
||||
|
||||
- Email: [{{email}}](mailto:{{email}})
|
||||
- Telegram: link on the [support page](https://{{site}}/support)
|
||||
|
||||
Support is available from 10:00 to 20:00 Moscow time on business days.
|
||||
|
||||
## 7. Language
|
||||
|
||||
This policy is published in several languages. If the language versions differ, the English version prevails.
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Effective from [Effective date].
|
||||
|
||||
## 1. General
|
||||
|
||||
These terms are an agreement between you and [Legal entity name], registered in [Country of registration] under number [Registration number], with its registered address at [Registered address] (“we”, “us”). They govern your use of the {{brand}} service: the website {{site}}, your account at {{cabinet}}, the Telegram bots, the Telegram mini app and the VPN servers (together, the “service”).
|
||||
|
||||
By using the service, you accept these terms. When you place an order, you confirm your acceptance separately. The [Privacy policy](https://{{site}}/legal/privacy), the [Refund policy](https://{{site}}/legal/refund) and the [Cookie policy](https://{{site}}/legal/cookies) also apply.
|
||||
|
||||
You must be at least [Minimum age] years old to use the service.
|
||||
|
||||
## 2. The service
|
||||
|
||||
The {{brand}} service provides VPN access through keys. A key is a purchased unit of access with a validity period. Each key includes connections: configurations and links for apps, which you get in your account.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
What is specific to {{brand}}:
|
||||
|
||||
- each key has its own dedicated IP address in the country you choose at purchase. The address is reserved for you alone and runs on a shared server, not on a separate one;
|
||||
- the country of a key never changes. For another country you need a separate key in another location; you can turn off auto-renewal of the previous key in your account;
|
||||
- every key includes a dedicated RU address;
|
||||
- all devices on a key use the same address;
|
||||
- protocols: WireGuard / AmneziaWG and Xray (VLESS, Hysteria2, XHTTP);
|
||||
- each location is marked “Servers available” or “Ready within 24 hours”. In the second case, we prepare the address after payment, within 24 hours. The paid period starts when the key is issued;
|
||||
- the first month costs more than a renewal: it includes preparing the address and setting up the protocols and routing.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
<!-- mass -->
|
||||
|
||||
What is specific to {{brand}}:
|
||||
|
||||
- servers are shared: many customers use the same address;
|
||||
- one key (subscription) gives access to all the plan’s countries marked “Included”. Locations marked “Coming soon” are shown on the website but do not work yet;
|
||||
- you can switch countries as often as you like, free of charge;
|
||||
- every key includes a Russian address; it is not counted as one of the plan’s countries;
|
||||
- protocols: Xray and WireGuard;
|
||||
- plans are monthly or yearly; the first period costs the same as a renewal.
|
||||
|
||||
<!-- /mass -->
|
||||
|
||||
Prices, periods, device limits and location lists are shown on {{site}} and in your account. The renewal price, period and device limit of a key are fixed at purchase.
|
||||
|
||||
## 3. Account and sign-in
|
||||
|
||||
There is no separate registration and no password: you enter your email and a one-time code from our email, and your account is created the first time you sign in. The code is valid for 10 minutes. You can also sign in with Telegram.
|
||||
|
||||
Your account in the {{brand}} service is not linked to accounts in our other services: each has its own balance, keys and history.
|
||||
|
||||
You are responsible for access to your email and Telegram, and you must not share sign-in codes with other people. If you suspect someone has accessed your account, sign out on all devices in your account and contact us.
|
||||
|
||||
Use a working email address: we send sign-in codes and notices about orders, payments and key expiry dates to it.
|
||||
|
||||
## 4. Keys, devices and connections
|
||||
|
||||
You may use a key on as many devices as the plan’s device limit allows, up to 5. Connection data (configurations, links and QR codes) is meant for your devices only: do not publish it or share it with other people.
|
||||
|
||||
To connect, you need third-party apps, such as WireGuard or Xray clients. They are governed by their developers’ rules.
|
||||
|
||||
The set of protocols and the connection data may be updated to keep the service working. Your account always has the current data.
|
||||
|
||||
Each key renews and expires independently of the others.
|
||||
|
||||
## 5. Availability of the service
|
||||
|
||||
We work to keep the service stable but do not guarantee that it will always be available without interruption. There may be scheduled maintenance, failures on our side or at our suppliers, and restrictions imposed by network operators, website owners and public authorities.
|
||||
|
||||
We do not guarantee access to any particular website or service, and we are not responsible for how they treat VPN connections.
|
||||
|
||||
Where possible, we announce scheduled maintenance in advance. If the service was down for a long time through our fault, we may extend your key to cover the downtime.
|
||||
|
||||
The list of locations, protocols and plans may change.
|
||||
|
||||
## 6. Payment, balance and auto-renewal
|
||||
|
||||
On the website, prices are shown in the currency you select; in your account, they are shown in your account currency: Russian rubles or US dollars. The account currency is chosen when you register. You can change it while your balance is zero and your account has no keys or unpaid orders; after that, only through support.
|
||||
|
||||
Your account has a balance. Payments are credited to the balance, and orders are paid from it; if the balance is not enough, you pay only the difference. An unpaid order is valid for 24 hours. Money received after an order is canceled stays on your balance.
|
||||
|
||||
Payment methods are shown at checkout: bank cards, cryptocurrency and others. Card details are entered only on the payment provider’s side and never reach us. When you pay in cryptocurrency, the amount is converted at the rate fixed when the invoice is issued and is credited in your account currency once the network confirms the transaction.
|
||||
|
||||
Auto-renewal is set for each key separately and is on for new keys. Shortly before a key expires, the renewal price is deducted from your balance; if the balance is not enough, the shortfall is charged to your linked card, if you have one. The card is linked to your account and used for all keys with auto-renewal on. By linking a card, you authorize us to make these charges. Linking a card is not available for every payment method and currency. If a charge fails, we let you know by email and retry several times, including during the grace period.
|
||||
|
||||
We send a reminder 3 days before a key expires. You can turn off auto-renewal in your account at any time. You can unlink your card in the Billing section; after that, renewals are paid from your balance only.
|
||||
|
||||
You can renew a key in advance: the new period is added to the current expiry date.
|
||||
|
||||
The renewal price is fixed when you buy a key and is shown in your account. If we change it, we will notify you at least [Notice period] before charging the new price.
|
||||
|
||||
## 7. Grace period and expiry
|
||||
|
||||
If a renewal is not paid by the expiry date, a 3-day grace period begins. The key keeps working throughout this time. A renewal paid during the grace period runs from the original expiry date, not from the payment date.
|
||||
|
||||
When the grace period ends, the key expires and its connections are disabled. The same rule applies to keys with and without auto-renewal.
|
||||
|
||||
An expired key cannot be renewed: to keep using the service, buy a new key. A new key comes with new connection data; keeping the previous addresses is not guaranteed.
|
||||
|
||||
## 8. Refunds
|
||||
|
||||
If the service did not work through our fault, we refund the full order amount if you contact us within the first 7 days of the paid period. If you changed your mind, we refund the unused remainder of the paid period, pro rata by days. Refunds go to your balance or, where the payment provider allows, to the original payment method. Details are in the [Refund policy](https://{{site}}/legal/refund).
|
||||
|
||||
## 9. Referral program
|
||||
|
||||
“Bring a friend — you both get a month”: when a friend who signed up through your invitation pays for their first key order, you each get 30 days added to a key.
|
||||
|
||||
- Your friend gets the days on the key bought with that order. You get them on your most recently bought key that has not expired or been canceled; if there is none, the days are added to your next key.
|
||||
- Topping up the balance does not count as a purchase.
|
||||
- An invitation link is remembered in your friend’s browser for 60 days.
|
||||
- Inviting yourself does not count. We review questionable invitations manually and may reject them.
|
||||
- Only invitations to the {{brand}} service count: sign-ups and purchases in our other services are not taken into account.
|
||||
- Bonus days are not money: they cannot be exchanged for money or refunded.
|
||||
- We may change or end the program. Days already credited remain.
|
||||
|
||||
## 10. Acceptable use
|
||||
|
||||
You must not use the service for:
|
||||
|
||||
- any illegal activity;
|
||||
- attacks on networks, websites or devices, hacking or unauthorized access to other people’s systems;
|
||||
- sending spam;
|
||||
- fraud or phishing;
|
||||
- creating or spreading malware, or controlling botnets;
|
||||
- creating, storing or distributing child sexual abuse material;
|
||||
- infringing other people’s rights, including copyright and the right to privacy;
|
||||
- using a key beyond its device limit, or reselling or otherwise passing access to third parties without a separate agreement with us;
|
||||
- anything that disrupts the service or other customers, or circumvents the service’s limits.
|
||||
|
||||
You are responsible for everything done using your key and your account.
|
||||
|
||||
## 11. Suspension and blocking
|
||||
|
||||
If you breach these terms, we may suspend your keys or block your account. Usually we warn you first and ask you to stop. For serious breaches (attacks, fraud, spreading malware or child sexual abuse material) we block immediately, without warning.
|
||||
|
||||
If a key is blocked because of a breach, the remaining paid period is not refunded, unless applicable law requires otherwise. If you disagree with a block, write to [{{email}}](mailto:{{email}}).
|
||||
|
||||
You can stop using the service at any time: to do so, turn off auto-renewal for your keys. To delete your account, contact us.
|
||||
|
||||
## 12. Limitation of liability
|
||||
|
||||
The service is provided “as is”. To the extent permitted by applicable law:
|
||||
|
||||
- we are not liable for indirect losses, lost profits or loss of data;
|
||||
- we are not liable for interruptions caused by network operators, public authorities, website owners or other third parties, or by faults in your devices and apps;
|
||||
- our liability for any claim is limited to the amount you paid for the paid period of the key that the claim relates to.
|
||||
|
||||
These limits do not affect rights that cannot be limited by law, including consumer rights.
|
||||
|
||||
## 13. Changes to these terms
|
||||
|
||||
We may change these terms. The new version is published on this page with its effective date. We notify you of material changes by email or in your account at least [Notice period] before they take effect. If you do not agree with the new version, stop using the service and turn off auto-renewal; you can get a refund of the unused remainder under the [Refund policy](https://{{site}}/legal/refund).
|
||||
|
||||
## 14. Governing law and disputes
|
||||
|
||||
These terms are governed by [Governing law]. We first try to resolve any dispute by correspondence: write to [{{email}}](mailto:{{email}}). If we cannot reach an agreement, the dispute is referred to: [Dispute venue]. This does not deprive you of the protection given by mandatory consumer protection rules, where they apply to you.
|
||||
|
||||
## 15. Contacts
|
||||
|
||||
- Email: [{{email}}](mailto:{{email}})
|
||||
- Telegram: link on the [support page](https://{{site}}/support)
|
||||
- Postal address: [Registered address]
|
||||
|
||||
Support is available from 10:00 to 20:00 Moscow time on business days.
|
||||
|
||||
## 16. Language
|
||||
|
||||
These terms are published in several languages. If the language versions differ, the English version prevails.
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigor desde el [Fecha de entrada en vigor].
|
||||
|
||||
Esta política explica qué cookies usa el servicio {{brand}} en {{site}} y {{cabinet}}. El servicio lo presta [Razón social]. La forma en que tratamos los datos en general se describe en la [Política de privacidad](https://{{site}}/legal/privacy).
|
||||
|
||||
## 1. Qué son las cookies
|
||||
|
||||
Las cookies son pequeños fragmentos de datos que un sitio web guarda en tu navegador para recordar, por ejemplo, que has iniciado sesión o qué idioma has elegido. localStorage funciona de forma parecida: es un almacenamiento de tu navegador cuyo contenido no se envía al servidor.
|
||||
|
||||
## 2. Cookies que usamos
|
||||
|
||||
Para cada cookie se indican su finalidad, su duración y su categoría.
|
||||
|
||||
- `vx_session` — mantiene tu sesión iniciada en el área de cliente; solo se instala en {{cabinet}} — 30 días, plazo que se prolonga mientras usas el área de cliente — estrictamente necesaria.
|
||||
- `NEXT_LOCALE` — el idioma elegido en el selector — 1 año — estrictamente necesaria.
|
||||
- `vx_currency` — la moneda elegida para los precios o, si has iniciado sesión, la moneda de tu cuenta — 1 año — estrictamente necesaria.
|
||||
- `vx_ref` — el código de invitación, si has accedido a través de un enlace de referido; solo se instala en {{cabinet}} — 60 días — estrictamente necesaria.
|
||||
- `vx_consent` — tu elección en el banner de cookies — 1 año — estrictamente necesaria.
|
||||
- `cw_*` — cookies del chat de soporte (Chatwoot): conservan tu conversación con el soporte entre visitas — [Duración de las cookies del chat] — funcionales.
|
||||
|
||||
Las cookies **estrictamente necesarias** son imprescindibles para que funcionen el sitio web y el área de cliente: sin ellas no puedes iniciar sesión y no se guardan tus preferencias de idioma y moneda. No pedimos tu consentimiento para ellas.
|
||||
|
||||
Las cookies **funcionales** solo se instalan con tu consentimiento. Si no lo has dado, el widget del chat no se carga; puedes seguir contactándonos por correo electrónico o por Telegram.
|
||||
|
||||
## 3. Analítica sin cookies
|
||||
|
||||
Recopilamos estadísticas de visitas con Umami en nuestro propio servidor. Umami no usa cookies, y los datos no se ceden a terceros.
|
||||
|
||||
## 4. Cookies publicitarias
|
||||
|
||||
No usamos cookies publicitarias ni rastreadores.
|
||||
|
||||
## 5. localStorage
|
||||
|
||||
El tema (claro, oscuro o según el sistema) no se guarda en una cookie, sino en localStorage con el nombre `vx_theme`. Cada dominio guarda su propia configuración.
|
||||
|
||||
## 6. Cookies de otros servicios
|
||||
|
||||
- El botón de inicio de sesión con Telegram de la página de inicio de sesión se carga desde los servidores de Telegram, y Telegram puede instalar sus propias cookies conforme a sus propias normas.
|
||||
- En el paso de pago, la página o el widget del proveedor de pagos puede instalar sus propias cookies, necesarias para procesar el pago y prevenir el fraude.
|
||||
|
||||
## 7. Cómo gestionar las cookies
|
||||
|
||||
En tu primera visita, un banner te permite aceptar o rechazar las cookies funcionales. Para cambiar tu elección, elimina la cookie `vx_consent` o todas las cookies del sitio en la configuración de tu navegador, y el banner volverá a aparecer.
|
||||
|
||||
Puedes eliminar o bloquear las cookies en la configuración de tu navegador. Sin las cookies estrictamente necesarias no puedes iniciar sesión en el área de cliente, y no se guardan tus preferencias de idioma y moneda.
|
||||
|
||||
## 8. Contacto
|
||||
|
||||
Envía tus preguntas sobre las cookies a [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 9. Idioma
|
||||
|
||||
Esta política se publica en varios idiomas. En caso de discrepancia entre las versiones lingüísticas, prevalece la versión en inglés.
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigor desde el [Fecha de entrada en vigor].
|
||||
|
||||
## 1. Quiénes somos
|
||||
|
||||
El servicio {{brand}} (el sitio web {{site}}, tu área de cliente en {{cabinet}}, los bots de Telegram, la miniaplicación de Telegram y los servidores VPN) lo presta [Razón social], con número de registro [Número de registro] en [País de registro] y domicilio social en [Domicilio social] (en adelante, «nosotros»). Somos el responsable del tratamiento de tus datos personales.
|
||||
|
||||
Esta política explica qué datos recopilamos, para qué, durante cuánto tiempo los conservamos y con quién los compartimos. Para cualquier pregunta sobre tus datos, escribe a [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 2. En resumen
|
||||
|
||||
- Los servidores VPN no guardan registros: ni del tráfico, ni de los sitios visitados, ni de las horas de conexión, ni de las direcciones IP de los usuarios.
|
||||
- Solo conservamos lo necesario para tu cuenta, los pagos, el soporte y la protección contra abusos.
|
||||
- En nuestra base de datos, las direcciones IP de los visitantes del sitio web y del área de cliente se guardan solo como hashes irreversibles; los registros del servidor web se eliminan a los 14 días.
|
||||
- No vendemos datos ni usamos rastreadores publicitarios.
|
||||
|
||||
## 3. Los servidores VPN no guardan registros
|
||||
|
||||
Nuestros servidores VPN no registran:
|
||||
|
||||
- tu tráfico ni su contenido;
|
||||
- los sitios web y servicios que visitas;
|
||||
- cuándo te conectas o te desconectas;
|
||||
- las direcciones IP desde las que te conectas.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Tu IP dedicada y tu dirección RU están asignadas a tu clave, por lo que en los datos de tu cuenta consta qué direcciones se han asignado a la clave. No registramos lo que ocurre a través de estas direcciones.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
## 4. Qué datos recopilamos y para qué
|
||||
|
||||
**Cuenta.** Tu correo electrónico; tu ID de Telegram, tu nombre de usuario y tu nombre, si inicias sesión con Telegram o lo has vinculado; tu idioma y tu moneda; las fechas de registro y del último inicio de sesión; tu código de referido y quién te invitó. Finalidad: crear tu cuenta, permitirte iniciar sesión y enviarte códigos de inicio de sesión y avisos. Base jurídica: ejecución del contrato.
|
||||
|
||||
**Claves y pagos.** Tus claves, sus conexiones, el plan, las ubicaciones, las fechas y la configuración de la renovación automática; los pedidos, los pagos y el historial de movimientos del saldo. Si has vinculado una tarjeta: su identificador en el proveedor de pagos, el tipo de tarjeta, los últimos 4 dígitos y la fecha de vencimiento. El número completo de la tarjeta y el código de seguridad se proporcionan únicamente en la plataforma del proveedor y nunca llegan a nosotros. Finalidad: prestar el servicio, aceptar pagos y llevar la contabilidad. Base jurídica: ejecución del contrato y cumplimiento de obligaciones legales.
|
||||
|
||||
**Soporte y solicitudes.** Tus mensajes al soporte por correo electrónico, por Telegram y en el chat del sitio web, así como las notas internas del personal de soporte. Si has iniciado sesión, el chat muestra al operador tu correo electrónico, el ID de tu cuenta y el número de claves. Datos de los formularios del sitio web: nombre, correo electrónico, Telegram, WhatsApp, el asunto y el texto del mensaje, y la página desde la que se envió el formulario. Finalidad: responderte y resolver tu solicitud. Base jurídica: ejecución del contrato, medidas adoptadas a petición tuya o nuestro interés legítimo.
|
||||
|
||||
**Seguridad.** Las direcciones IP de los visitantes del sitio web y del área de cliente se guardan en nuestra base de datos solo como hashes irreversibles (HMAC). Los usamos para limitar la frecuencia con la que se pueden solicitar códigos de inicio de sesión y para detectar autoinvitaciones. Para las sesiones, guardamos información sobre el navegador y el dispositivo, y la hora de la última actividad. Los códigos de inicio de sesión se guardan solo como hashes. La verificación «No soy un robot» funciona sin servicios de terceros. Base jurídica: nuestro interés legítimo en proteger el servicio y las cuentas.
|
||||
|
||||
**Registros técnicos.** Los registros del servidor web contienen la dirección IP, la hora, la dirección solicitada y la información del navegador; se eliminan a los 14 días. El registro de envío de correos electrónicos (dirección del destinatario, asunto y estado de entrega) se conserva durante 30 días. Base jurídica: nuestro interés legítimo en el funcionamiento y la protección del servicio.
|
||||
|
||||
**Estadísticas de visitas.** Usamos Umami en nuestro propio servidor, sin cookies; los datos no se ceden a terceros. Se registran las páginas abiertas, la fuente de procedencia, el tipo de navegador y de dispositivo, y el país. Base jurídica: nuestro interés legítimo en mejorar el sitio web.
|
||||
|
||||
El idioma predeterminado del sitio web se elige según tu país, que determinamos a partir de tu dirección IP mediante una base de datos alojada en nuestro propio servidor. Para ello, la dirección IP no se envía a ninguna parte.
|
||||
|
||||
## 5. Cookies
|
||||
|
||||
Las cookies estrictamente necesarias se usan para iniciar sesión y para recordar tu idioma y tu moneda. El chat de soporte solo se carga con tu consentimiento. No hay cookies publicitarias. Encontrarás los detalles en la [Política de cookies](https://{{site}}/legal/cookies).
|
||||
|
||||
## 6. Con quién compartimos los datos
|
||||
|
||||
No vendemos datos ni los cedemos a redes publicitarias. Solo reciben datos:
|
||||
|
||||
- los proveedores de pagos: la información necesaria para procesar un pago;
|
||||
- los proveedores de correo electrónico: para entregar nuestros correos, si se usa un servicio de envío externo, y para el funcionamiento de los buzones de soporte;
|
||||
- los proveedores de alojamiento en cuyos servidores funcionan el sitio web, el área de cliente, la base de datos y los servicios de soporte.
|
||||
|
||||
El chat de soporte (Chatwoot), la gestión de solicitudes (Twenty CRM) y la analítica (Umami) funcionan en nuestros propios servidores, no en los de proveedores externos.
|
||||
|
||||
Si inicias sesión con Telegram o nos escribes por Telegram, Telegram trata los datos conforme a sus propias normas.
|
||||
|
||||
Solo comunicamos datos a las autoridades públicas cuando la ley lo exige, y únicamente los datos que realmente tenemos. No tenemos registros del tráfico VPN.
|
||||
|
||||
## 7. Dónde se almacenan los datos
|
||||
|
||||
El sitio web, el área de cliente y la base de datos están alojados en servidores ubicados en [País de alojamiento]. Los servidores VPN se encuentran en los países de las ubicaciones: tu tráfico pasa por ellos, pero no guardan registros. Los proveedores de pagos pueden tratar los datos en otros países conforme a sus propias normas. Cuando los datos se transfieren fuera de tu país de residencia, lo hacemos sobre las bases que permite la legislación aplicable.
|
||||
|
||||
## 8. Durante cuánto tiempo conservamos los datos
|
||||
|
||||
- Datos de la cuenta: mientras exista la cuenta. Tras la eliminación de la cuenta, suprimimos o anonimizamos estos datos, salvo los registros de pedidos y pagos que la ley nos obliga a conservar a efectos contables y fiscales.
|
||||
- Conversaciones con el soporte y solicitudes: el tiempo necesario para gestionar la solicitud, pero no más de [Plazo de conservación de los datos de soporte].
|
||||
- Registros del servidor web: 14 días; registro de envío de correos electrónicos: 30 días.
|
||||
- Sesiones: 30 días desde la última actividad o hasta que cierres sesión.
|
||||
- Códigos de inicio de sesión: válidos durante 10 minutos.
|
||||
|
||||
## 9. Cómo protegemos los datos
|
||||
|
||||
- Los datos que circulan entre tu navegador y nuestros servidores se transmiten cifrados (HTTPS).
|
||||
- Las configuraciones de conexión y los identificadores de pago se almacenan cifrados.
|
||||
- Los códigos de inicio de sesión, los tokens de sesión y las direcciones IP se almacenan solo como hashes.
|
||||
- Solo tiene acceso a los datos el personal que los necesita para su trabajo, y el acceso está limitado según el rol.
|
||||
|
||||
Ningún sistema está completamente libre de riesgos. Si una brecha de seguridad afecta a tus datos, te lo notificaremos según lo exija la legislación aplicable.
|
||||
|
||||
## 10. Tus derechos
|
||||
|
||||
Según la legislación aplicable, puedes:
|
||||
|
||||
- saber qué datos tenemos sobre ti y obtener una copia de ellos;
|
||||
- rectificar los datos inexactos;
|
||||
- eliminar tu cuenta y tus datos, salvo los que la ley nos obliga a conservar;
|
||||
- limitar el tratamiento u oponerte a él;
|
||||
- recibir tus datos en un formato de lectura mecánica;
|
||||
- retirar tu consentimiento a las cookies funcionales;
|
||||
- presentar una reclamación ante una autoridad de protección de datos.
|
||||
|
||||
En tu área de cliente puedes desvincular directamente tu tarjeta y cerrar la sesión en todos los dispositivos. Para todo lo demás, escribe a [{{email}}](mailto:{{email}}) desde la dirección de correo electrónico vinculada a tu cuenta o desde tu Telegram vinculado, para que podamos confirmar que la solicitud procede de ti. Respondemos dentro de los plazos que establece la legislación aplicable.
|
||||
|
||||
## 11. Menores
|
||||
|
||||
El servicio no está dirigido a menores de [Edad mínima] años y no recopilamos conscientemente sus datos. Si crees que un menor nos ha proporcionado sus datos, ponte en contacto con nosotros y los eliminaremos.
|
||||
|
||||
## 12. Cambios en esta política
|
||||
|
||||
Podemos actualizar esta política. La nueva versión se publica en esta página junto con su fecha de entrada en vigor. Te notificamos los cambios sustanciales por correo electrónico o en tu área de cliente.
|
||||
|
||||
## 13. Contacto
|
||||
|
||||
- Correo electrónico: [{{email}}](mailto:{{email}})
|
||||
- Dirección postal: [Domicilio social]
|
||||
|
||||
## 14. Idioma
|
||||
|
||||
Esta política se publica en varios idiomas. En caso de discrepancia entre las versiones lingüísticas, prevalece la versión en inglés.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigor desde el [Fecha de entrada en vigor].
|
||||
|
||||
Esta política complementa las [Condiciones de uso](https://{{site}}/legal/terms) del servicio {{brand}}, prestado por [Razón social]. Explica cuándo y cómo reembolsamos el dinero de los pedidos pagados: claves nuevas y renovaciones.
|
||||
|
||||
## 1. Cuándo reembolsamos
|
||||
|
||||
**El servicio no ha funcionado por causas imputables a nosotros.** Reembolsamos el importe íntegro del pedido si te pones en contacto con nosotros dentro de los primeros 7 días del período pagado; el período pagado comienza en el momento en que se entrega la clave. Por ejemplo, si no hemos podido entregar la clave o si las conexiones no han funcionado por un problema de nuestra parte.
|
||||
|
||||
**Has cambiado de opinión.** Reembolsamos la parte no utilizada del período pagado, prorrateada por días, a la fecha de tu solicitud: dividimos el importe pagado por el período entre su número de días y lo multiplicamos por el número de días no utilizados. Por ejemplo, si el período es de 30 días y han transcurrido 10, reembolsamos el importe correspondiente a 20 días. Esto también se aplica a las renovaciones: si una clave se ha renovado automáticamente y ya no la necesitas, escríbenos y te reembolsaremos los días no utilizados.
|
||||
|
||||
Tras el reembolso, se desactiva la clave a la que corresponde.
|
||||
|
||||
## 2. Cómo solicitar un reembolso
|
||||
|
||||
Escribe a [{{email}}](mailto:{{email}}) desde la dirección de correo electrónico vinculada a tu cuenta e indica:
|
||||
|
||||
- el número de pedido o el código de la clave, que aparecen en tu área de cliente;
|
||||
- el motivo del reembolso;
|
||||
- si el servicio no ha funcionado: qué no ha funcionado exactamente y desde cuándo.
|
||||
|
||||
Si tu cuenta no tiene correo electrónico e inicias sesión con Telegram, escríbenos por Telegram: el enlace está en la [página de soporte](https://{{site}}/support). Es posible que te pidamos más detalles para entender el problema.
|
||||
|
||||
## 3. Destino de los reembolsos
|
||||
|
||||
Los reembolsos se hacen al saldo de tu cuenta, en su moneda, o, si el proveedor de pagos lo permite, al método de pago original. El reembolso al saldo se acredita en cuanto se aprueba, y te notificamos el reembolso por correo electrónico. El tiempo que tarda un reembolso en llegar a tu tarjeta o a otro método de pago depende del proveedor y de tu banco.
|
||||
|
||||
## 4. Plazo de tramitación
|
||||
|
||||
Revisamos tu solicitud y te comunicamos nuestra decisión en un plazo de [Plazo de tramitación del reembolso].
|
||||
|
||||
## 5. Qué no se reembolsa
|
||||
|
||||
- Los días de bonificación del programa de referidos y otros días añadidos a una clave sin pago: no son dinero y no se pueden reembolsar ni canjear por dinero.
|
||||
- La parte utilizada del período pagado, si has cambiado de opinión.
|
||||
- El período restante de una clave bloqueada por incumplir las [Condiciones de uso](https://{{site}}/legal/terms), salvo que la legislación aplicable exija otra cosa.
|
||||
|
||||
## 6. Contacto
|
||||
|
||||
- Correo electrónico: [{{email}}](mailto:{{email}})
|
||||
- Telegram: enlace en la [página de soporte](https://{{site}}/support)
|
||||
|
||||
El soporte atiende de 10:00 a 20:00, hora de Moscú, en días laborables.
|
||||
|
||||
## 7. Idioma
|
||||
|
||||
Esta política se publica en varios idiomas. En caso de discrepancia entre las versiones lingüísticas, prevalece la versión en inglés.
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigor desde el [Fecha de entrada en vigor].
|
||||
|
||||
## 1. Disposiciones generales
|
||||
|
||||
Estas condiciones constituyen un acuerdo entre tú y [Razón social], con número de registro [Número de registro] en [País de registro] y domicilio social en [Domicilio social] (en adelante, «nosotros»). Regulan el uso que haces del servicio {{brand}}: el sitio web {{site}}, tu área de cliente en {{cabinet}}, los bots de Telegram, la miniaplicación de Telegram y los servidores VPN (en conjunto, el «servicio»).
|
||||
|
||||
Al usar el servicio, aceptas estas condiciones. Al realizar un pedido, confirmas por separado que las aceptas. También se aplican la [Política de privacidad](https://{{site}}/legal/privacy), la [Política de reembolsos](https://{{site}}/legal/refund) y la [Política de cookies](https://{{site}}/legal/cookies).
|
||||
|
||||
Para usar el servicio debes tener al menos [Edad mínima] años.
|
||||
|
||||
## 2. El servicio
|
||||
|
||||
El servicio {{brand}} proporciona acceso a VPN mediante claves. Una clave es una unidad de acceso comprada con un período de validez. Cada clave incluye conexiones: configuraciones y enlaces para aplicaciones, que obtienes en tu área de cliente.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Particularidades del servicio {{brand}}:
|
||||
|
||||
- cada clave tiene su propia dirección IP dedicada en el país elegido al comprarla. La dirección está reservada solo para ti y funciona en un servidor compartido, no en uno independiente;
|
||||
- el país de una clave no cambia nunca. Para otro país necesitas una clave aparte en otra ubicación; puedes desactivar la renovación automática de la clave anterior en tu área de cliente;
|
||||
- cada clave incluye una dirección RU dedicada;
|
||||
- todos los dispositivos de una clave usan la misma dirección;
|
||||
- protocolos: WireGuard / AmneziaWG y Xray (VLESS, Hysteria2, XHTTP);
|
||||
- cada ubicación está marcada como «Servidores disponibles» o «Listo en 24 horas». En el segundo caso, preparamos la dirección después del pago, en un plazo de 24 horas. El período pagado comienza en el momento en que se entrega la clave;
|
||||
- el primer mes cuesta más que una renovación: incluye la preparación de la dirección y la configuración de los protocolos y del enrutamiento.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
<!-- mass -->
|
||||
|
||||
Particularidades del servicio {{brand}}:
|
||||
|
||||
- los servidores son compartidos: muchos clientes usan la misma dirección;
|
||||
- una clave (suscripción) da acceso a todos los países del plan marcados como «Incluido». Las ubicaciones marcadas como «Próximamente» se muestran en el sitio web, pero aún no funcionan;
|
||||
- puedes cambiar de país tantas veces como quieras, de forma gratuita;
|
||||
- cada clave incluye una dirección rusa; esta no se cuenta entre los países del plan;
|
||||
- protocolos: Xray y WireGuard;
|
||||
- los planes son mensuales o anuales; el primer período cuesta lo mismo que una renovación.
|
||||
|
||||
<!-- /mass -->
|
||||
|
||||
Los precios, los períodos, los límites de dispositivos y las listas de ubicaciones se indican en {{site}} y en tu área de cliente. El precio de renovación, el período y el límite de dispositivos de una clave se fijan en el momento de la compra.
|
||||
|
||||
## 3. Cuenta e inicio de sesión
|
||||
|
||||
No hay un registro aparte ni contraseña: indicas tu correo electrónico y un código de un solo uso que te enviamos por correo, y tu cuenta se crea la primera vez que inicias sesión. El código es válido durante 10 minutos. También puedes iniciar sesión con Telegram.
|
||||
|
||||
Tu cuenta en el servicio {{brand}} no está vinculada a las cuentas de nuestros otros servicios: cada una tiene su propio saldo, sus propias claves y su propio historial.
|
||||
|
||||
Eres responsable del acceso a tu correo electrónico y a tu Telegram, y no debes compartir los códigos de inicio de sesión con otras personas. Si sospechas que alguien ha accedido a tu cuenta, cierra la sesión en todos los dispositivos desde tu área de cliente y ponte en contacto con nosotros.
|
||||
|
||||
Usa una dirección de correo electrónico activa: a ella te enviamos los códigos de inicio de sesión y los avisos sobre pedidos, pagos y fechas de vencimiento de las claves.
|
||||
|
||||
## 4. Claves, dispositivos y conexiones
|
||||
|
||||
Puedes usar una clave en tantos dispositivos como permita el límite de dispositivos del plan, hasta 5. Los datos de conexión (configuraciones, enlaces y códigos QR) son solo para tus dispositivos: no los publiques ni los compartas con otras personas.
|
||||
|
||||
Para conectarte necesitas aplicaciones de terceros, como clientes de WireGuard o de Xray. Estas aplicaciones se rigen por las normas de sus desarrolladores.
|
||||
|
||||
El conjunto de protocolos y los datos de conexión pueden actualizarse para que el servicio siga funcionando. En tu área de cliente siempre tienes los datos actualizados.
|
||||
|
||||
Cada clave se renueva y vence de forma independiente de las demás.
|
||||
|
||||
## 5. Disponibilidad del servicio
|
||||
|
||||
Trabajamos para que el servicio funcione de forma estable, pero no garantizamos que esté siempre disponible y sin interrupciones. Puede haber trabajos de mantenimiento programados, fallos por nuestra parte o por parte de nuestros proveedores, así como restricciones impuestas por operadores de red, propietarios de sitios web y autoridades públicas.
|
||||
|
||||
No garantizamos el acceso a ningún sitio web o servicio concreto, ni somos responsables de cómo traten estos las conexiones VPN.
|
||||
|
||||
Siempre que sea posible, anunciamos con antelación los trabajos de mantenimiento programados. Si el servicio ha estado sin funcionar durante mucho tiempo por causas imputables a nosotros, podemos ampliar la vigencia de tu clave por el tiempo de inactividad.
|
||||
|
||||
La lista de ubicaciones, protocolos y planes puede cambiar.
|
||||
|
||||
## 6. Pago, saldo y renovación automática
|
||||
|
||||
En el sitio web, los precios se muestran en la moneda que selecciones; en el área de cliente, en la moneda de tu cuenta: rublos rusos o dólares estadounidenses. La moneda de la cuenta se elige al registrarte. Puedes cambiarla mientras tu saldo sea cero y tu cuenta no tenga claves ni pedidos sin pagar; después, solo a través del soporte.
|
||||
|
||||
Tu cuenta tiene un saldo. Los pagos se acreditan en el saldo y los pedidos se pagan con él; si el saldo no es suficiente, solo pagas la diferencia. Un pedido sin pagar es válido durante 24 horas. El dinero recibido después de cancelarse un pedido permanece en tu saldo.
|
||||
|
||||
Los métodos de pago se muestran al finalizar la compra: tarjetas bancarias, criptomonedas y otros. Los datos de la tarjeta se proporcionan únicamente en la plataforma del proveedor de pagos y nunca llegan a nosotros. Si pagas con criptomonedas, el importe se convierte al tipo de cambio fijado al emitir la factura y se acredita en la moneda de tu cuenta una vez que la red confirma la transacción.
|
||||
|
||||
La renovación automática se configura para cada clave por separado y está activada en las claves nuevas. Poco antes de que venza una clave, el precio de renovación se descuenta de tu saldo; si el saldo no es suficiente, la cantidad que falta se carga en tu tarjeta vinculada, si tienes una. La tarjeta se vincula a tu cuenta y se usa para todas las claves con la renovación automática activada. Al vincular una tarjeta, nos autorizas a realizar estos cargos. La vinculación de tarjetas no está disponible para todos los métodos de pago ni para todas las monedas. Si no se puede realizar un cargo, te avisamos por correo electrónico y lo intentamos de nuevo varias veces, también durante el período de gracia.
|
||||
|
||||
Te enviamos un recordatorio 3 días antes de que venza una clave. Puedes desactivar la renovación automática en tu área de cliente en cualquier momento. Puedes desvincular tu tarjeta en la sección «Saldo y pagos»; a partir de entonces, las renovaciones se pagan únicamente con tu saldo.
|
||||
|
||||
Puedes renovar una clave por adelantado: el nuevo período se suma a la fecha de vencimiento actual.
|
||||
|
||||
El precio de renovación se fija al comprar la clave y se muestra en tu área de cliente. Si lo modificamos, te lo notificaremos al menos [Plazo de preaviso] antes de cobrar el nuevo precio.
|
||||
|
||||
## 7. Período de gracia y vencimiento
|
||||
|
||||
Si al llegar la fecha de vencimiento la renovación no está pagada, comienza un período de gracia de 3 días. Durante todo ese tiempo, la clave sigue funcionando. Una renovación pagada durante el período de gracia se cuenta desde la fecha de vencimiento original, no desde la fecha de pago.
|
||||
|
||||
Cuando termina el período de gracia, la clave vence y sus conexiones se desactivan. La misma regla se aplica a las claves con y sin renovación automática.
|
||||
|
||||
Una clave vencida no se puede renovar: para seguir usando el servicio, compra una clave nueva. Una clave nueva tiene datos de conexión nuevos; no se garantiza que se conserven las direcciones anteriores.
|
||||
|
||||
## 8. Reembolsos
|
||||
|
||||
Si el servicio no ha funcionado por causas imputables a nosotros, reembolsamos el importe íntegro del pedido si te pones en contacto con nosotros dentro de los primeros 7 días del período pagado. Si has cambiado de opinión, reembolsamos la parte no utilizada del período pagado, prorrateada por días. Los reembolsos se hacen a tu saldo o, si el proveedor de pagos lo permite, al método de pago original. Encontrarás los detalles en la [Política de reembolsos](https://{{site}}/legal/refund).
|
||||
|
||||
## 9. Programa de referidos
|
||||
|
||||
«Invita a un amigo y cada uno recibirá un mes»: cuando un amigo que se haya registrado con tu invitación pague su primer pedido de una clave, cada uno de los dos recibirá 30 días adicionales en una clave.
|
||||
|
||||
- Tu amigo recibe los días en la clave comprada con ese pedido. Tú los recibes en la más reciente de tus claves compradas que no haya vencido ni se haya cancelado; si no hay ninguna, los días se añaden a tu próxima clave.
|
||||
- Recargar el saldo no cuenta como compra.
|
||||
- La invitación mediante enlace se guarda en el navegador de tu amigo durante 60 días.
|
||||
- Las autoinvitaciones no cuentan. Revisamos manualmente las invitaciones dudosas y podemos rechazarlas.
|
||||
- Solo cuentan las invitaciones al servicio {{brand}}: los registros y las compras en nuestros otros servicios no se tienen en cuenta.
|
||||
- Los días de bonificación no son dinero: no se pueden canjear por dinero ni reembolsar.
|
||||
- Podemos modificar el programa o ponerle fin. Los días ya acreditados se conservan.
|
||||
|
||||
## 10. Uso aceptable
|
||||
|
||||
No puedes usar el servicio para:
|
||||
|
||||
- llevar a cabo cualquier actividad ilegal;
|
||||
- atacar redes, sitios web o dispositivos, vulnerar la seguridad de sistemas ajenos o acceder a ellos sin autorización;
|
||||
- enviar spam;
|
||||
- cometer fraude o realizar phishing;
|
||||
- crear o difundir malware, o controlar botnets;
|
||||
- crear, almacenar o distribuir material de abuso sexual infantil;
|
||||
- vulnerar los derechos de terceros, incluidos los derechos de autor y el derecho a la privacidad;
|
||||
- superar el límite de dispositivos de una clave, o revender o ceder de cualquier otro modo el acceso a terceros sin un acuerdo específico con nosotros;
|
||||
- hacer cualquier cosa que perturbe el funcionamiento del servicio o a otros clientes, o que eluda los límites del servicio.
|
||||
|
||||
Eres responsable de todo lo que se haga con tu clave y tu cuenta.
|
||||
|
||||
## 11. Suspensión y bloqueo
|
||||
|
||||
Si incumples estas condiciones, podemos suspender tus claves o bloquear tu cuenta. Normalmente, primero te avisamos y te pedimos que pongas fin al incumplimiento. En caso de incumplimientos graves (ataques, fraude, difusión de malware o de material de abuso sexual infantil), bloqueamos de inmediato, sin previo aviso.
|
||||
|
||||
Si una clave se bloquea por un incumplimiento, el período pagado restante no se reembolsa, salvo que la legislación aplicable exija otra cosa. Si no estás de acuerdo con un bloqueo, escribe a [{{email}}](mailto:{{email}}).
|
||||
|
||||
Puedes dejar de usar el servicio en cualquier momento: para ello, desactiva la renovación automática de tus claves. Para eliminar tu cuenta, ponte en contacto con nosotros.
|
||||
|
||||
## 12. Limitación de responsabilidad
|
||||
|
||||
El servicio se presta «tal cual». En la medida en que lo permita la legislación aplicable:
|
||||
|
||||
- no somos responsables de daños indirectos, lucro cesante ni pérdida de datos;
|
||||
- no somos responsables de interrupciones causadas por operadores de red, autoridades públicas, propietarios de sitios web u otros terceros, ni por fallos de tus dispositivos y aplicaciones;
|
||||
- nuestra responsabilidad por cualquier reclamación se limita al importe que hayas pagado por el período pagado de la clave a la que se refiera la reclamación.
|
||||
|
||||
Estas limitaciones no afectan a los derechos que la ley no permite limitar, incluidos los derechos de los consumidores.
|
||||
|
||||
## 13. Cambios en estas condiciones
|
||||
|
||||
Podemos modificar estas condiciones. La nueva versión se publica en esta página junto con su fecha de entrada en vigor. Te notificamos los cambios sustanciales por correo electrónico o en tu área de cliente al menos [Plazo de preaviso] antes de que entren en vigor. Si no estás de acuerdo con la nueva versión, deja de usar el servicio y desactiva la renovación automática; puedes obtener el reembolso de la parte no utilizada conforme a la [Política de reembolsos](https://{{site}}/legal/refund).
|
||||
|
||||
## 14. Legislación aplicable y resolución de controversias
|
||||
|
||||
Estas condiciones se rigen por [Legislación aplicable]. Primero intentamos resolver cualquier controversia por correspondencia: escribe a [{{email}}](mailto:{{email}}). Si no llegamos a un acuerdo, la controversia se somete a: [Jurisdicción competente]. Esto no te priva de la protección que te otorgan las normas imperativas de protección de los consumidores, cuando te sean aplicables.
|
||||
|
||||
## 15. Contacto
|
||||
|
||||
- Correo electrónico: [{{email}}](mailto:{{email}})
|
||||
- Telegram: enlace en la [página de soporte](https://{{site}}/support)
|
||||
- Dirección postal: [Domicilio social]
|
||||
|
||||
El soporte atiende de 10:00 a 20:00, hora de Moscú, en días laborables.
|
||||
|
||||
## 16. Idioma
|
||||
|
||||
Estas condiciones se publican en varios idiomas. En caso de discrepancia entre las versiones lingüísticas, prevalece la versión en inglés.
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigueur à compter du [Date d’entrée en vigueur].
|
||||
|
||||
La présente politique explique quels cookies le service {{brand}} utilise sur {{site}} et {{cabinet}}. Le service est fourni par [Dénomination sociale]. La manière dont nous traitons les données en général est décrite dans la [Politique de confidentialité](https://{{site}}/legal/privacy).
|
||||
|
||||
## 1. Qu’est-ce qu’un cookie ?
|
||||
|
||||
Les cookies sont de petits fragments de données qu’un site web enregistre dans votre navigateur pour mémoriser, par exemple, que vous êtes connecté ou la langue que vous avez choisie. Le localStorage fonctionne de manière similaire : il s’agit d’un espace de stockage dans votre navigateur dont le contenu n’est pas envoyé au serveur.
|
||||
|
||||
## 2. Cookies que nous utilisons
|
||||
|
||||
Chaque cookie est indiqué avec sa finalité, sa durée de vie et sa catégorie.
|
||||
|
||||
- `vx_session` — maintient votre connexion à votre espace client, déposé uniquement sur {{cabinet}} — 30 jours, prolongés tant que vous utilisez votre espace client — strictement nécessaire.
|
||||
- `NEXT_LOCALE` — la langue choisie dans le sélecteur — 1 an — strictement nécessaire.
|
||||
- `vx_currency` — la devise choisie pour les prix ou, si vous êtes connecté, la devise de votre compte — 1 an — strictement nécessaire.
|
||||
- `vx_ref` — le code d’invitation, si vous avez suivi un lien de parrainage, déposé uniquement sur {{cabinet}} — 60 jours — strictement nécessaire.
|
||||
- `vx_consent` — votre choix dans le bandeau cookies — 1 an — strictement nécessaire.
|
||||
- `cw_*` — cookies du chat d’assistance (Chatwoot) : conservent votre conversation avec l’assistance d’une visite à l’autre — [Durée de vie des cookies du chat] — fonctionnels.
|
||||
|
||||
Les cookies **strictement nécessaires** sont indispensables au fonctionnement du site et de votre espace client : sans eux, vous ne pouvez pas vous connecter, et vos choix de langue et de devise ne sont pas enregistrés. Nous ne demandons pas de consentement pour ces cookies.
|
||||
|
||||
Les cookies **fonctionnels** ne sont déposés qu’avec votre consentement. Si vous ne l’avez pas donné, le widget de chat n’est pas chargé ; vous pouvez toujours nous contacter par e-mail ou sur Telegram.
|
||||
|
||||
## 3. Mesure d’audience sans cookies
|
||||
|
||||
Nous collectons des statistiques de visite avec Umami, sur notre propre serveur. Umami n’utilise pas de cookies, et les données ne sont pas transmises à des tiers.
|
||||
|
||||
## 4. Cookies publicitaires
|
||||
|
||||
Nous n’utilisons pas de cookies ni de traceurs publicitaires.
|
||||
|
||||
## 5. localStorage
|
||||
|
||||
Le thème (clair, sombre ou celui du système) est enregistré non pas dans un cookie, mais dans le localStorage, sous la clé `vx_theme`. Chaque domaine conserve son propre réglage.
|
||||
|
||||
## 6. Cookies d’autres services
|
||||
|
||||
- Le bouton de connexion via Telegram, sur la page de connexion, est chargé depuis les serveurs de Telegram, et Telegram peut déposer ses propres cookies selon ses propres règles.
|
||||
- À l’étape du paiement, la page ou le widget du prestataire de paiement peut déposer ses propres cookies, nécessaires au traitement du paiement et à la prévention de la fraude.
|
||||
|
||||
## 7. Gestion des cookies
|
||||
|
||||
Lors de votre première visite, un bandeau vous permet d’accepter ou de refuser les cookies fonctionnels. Pour modifier votre choix, supprimez le cookie `vx_consent` ou tous les cookies du site dans les paramètres de votre navigateur : le bandeau s’affichera à nouveau.
|
||||
|
||||
Vous pouvez supprimer ou bloquer les cookies dans les paramètres de votre navigateur. Sans les cookies strictement nécessaires, vous ne pouvez pas vous connecter à votre espace client, et vos choix de langue et de devise ne sont pas enregistrés.
|
||||
|
||||
## 8. Contacts
|
||||
|
||||
Envoyez vos questions sur les cookies à [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 9. Langue
|
||||
|
||||
La présente politique est publiée en plusieurs langues. En cas de divergence entre les versions linguistiques, la version anglaise prévaut.
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigueur à compter du [Date d’entrée en vigueur].
|
||||
|
||||
## 1. Qui sommes-nous ?
|
||||
|
||||
Le service {{brand}} (le site web {{site}}, votre espace client sur {{cabinet}}, les bots Telegram, la mini-application Telegram et les serveurs VPN) est fourni par [Dénomination sociale], immatriculée en [Pays d’immatriculation] sous le numéro [Numéro d’immatriculation], dont le siège social se situe à l’adresse suivante : [Adresse du siège social] (ci-après « nous »). Nous sommes le responsable du traitement de vos données personnelles.
|
||||
|
||||
La présente politique explique quelles données nous collectons, pourquoi, combien de temps nous les conservons et avec qui nous les partageons. Pour toute question concernant vos données, écrivez à [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 2. En bref
|
||||
|
||||
- Les serveurs VPN ne conservent aucun journal : ni trafic, ni sites visités, ni heures de connexion, ni adresses IP des utilisateurs.
|
||||
- Nous ne conservons que ce qui est nécessaire pour votre compte, les paiements, l’assistance et la protection contre les abus.
|
||||
- Dans notre base de données, les adresses IP des visiteurs du site et de l’espace client ne sont stockées que sous forme de hachages irréversibles ; les journaux du serveur web sont supprimés au bout de 14 jours.
|
||||
- Nous ne vendons pas de données et n’utilisons pas de traceurs publicitaires.
|
||||
|
||||
## 3. Les serveurs VPN ne conservent aucun journal
|
||||
|
||||
Nos serveurs VPN n’enregistrent pas :
|
||||
|
||||
- votre trafic ni son contenu ;
|
||||
- les sites web et les services que vous consultez ;
|
||||
- les heures auxquelles vous vous connectez ou vous déconnectez ;
|
||||
- les adresses IP à partir desquelles vous vous connectez.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Votre IP dédiée et votre adresse RU sont attribuées à votre clé ; les données de votre compte indiquent donc quelles adresses ont été délivrées à la clé. Nous n’enregistrons pas ce qui transite par ces adresses.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
## 4. Ce que nous collectons et pourquoi
|
||||
|
||||
**Compte.** Votre adresse e-mail ; votre identifiant Telegram, votre nom d’utilisateur et votre nom, si vous vous connectez via Telegram ou l’avez associé ; votre langue et votre devise ; les dates d’inscription et de dernière connexion ; votre code de parrainage et la personne qui vous a invité. Finalité : créer votre compte, vous permettre de vous connecter, et vous envoyer des codes de connexion et des notifications. Base légale : exécution du contrat.
|
||||
|
||||
**Clés et paiements.** Vos clés, leurs connexions, le forfait, les emplacements, les dates et le réglage du renouvellement automatique ; les commandes, les paiements et l’historique des mouvements du solde. Si vous avez associé une carte : son identifiant chez le prestataire de paiement, le type de carte, les 4 derniers chiffres et la date d’expiration. Le numéro complet de la carte et le code de sécurité sont saisis uniquement du côté du prestataire et ne nous parviennent jamais. Finalité : fournir le service, accepter les paiements et tenir la comptabilité. Base légale : exécution du contrat et obligations légales.
|
||||
|
||||
**Assistance et demandes.** Vos messages à l’assistance par e-mail, sur Telegram et dans le chat du site, ainsi que les notes internes rédigées par le personnel d’assistance. Si vous êtes connecté, le chat affiche à l’opérateur votre adresse e-mail, l’identifiant de votre compte et le nombre de vos clés. Données des formulaires du site : nom, adresse e-mail, Telegram, WhatsApp, objet et texte du message, et page depuis laquelle le formulaire a été envoyé. Finalité : vous répondre et traiter votre demande. Base légale : exécution du contrat, mesures prises à votre demande ou notre intérêt légitime.
|
||||
|
||||
**Sécurité.** Les adresses IP des visiteurs du site et de l’espace client ne sont stockées dans notre base de données que sous forme de hachages irréversibles (HMAC). Nous les utilisons pour limiter la fréquence des demandes de codes de connexion et pour détecter l’auto-parrainage. Pour les sessions, nous conservons des informations sur le navigateur et l’appareil ainsi que l’heure de la dernière activité. Les codes de connexion ne sont stockés que sous forme de hachages. La vérification « Je ne suis pas un robot » fonctionne sans services tiers. Base légale : notre intérêt légitime à protéger le service et les comptes.
|
||||
|
||||
**Journaux techniques.** Les journaux du serveur web contiennent l’adresse IP, l’heure, l’adresse demandée et des informations sur le navigateur ; ils sont supprimés au bout de 14 jours. Le journal d’envoi des e-mails (adresse du destinataire, objet et statut de distribution) est conservé 30 jours. Base légale : notre intérêt légitime à faire fonctionner et à protéger le service.
|
||||
|
||||
**Statistiques de visite.** Nous utilisons Umami sur notre propre serveur, sans cookies ; les données ne sont pas transmises à des tiers. Umami enregistre les pages ouvertes, la source de provenance, le type de navigateur et d’appareil, ainsi que le pays. Base légale : notre intérêt légitime à améliorer le site.
|
||||
|
||||
La langue par défaut du site est choisie en fonction de votre pays, que nous déterminons à partir de votre adresse IP à l’aide d’une base de données hébergée sur notre propre serveur. L’adresse IP n’est transmise nulle part à cette fin.
|
||||
|
||||
## 5. Cookies
|
||||
|
||||
Des cookies strictement nécessaires sont utilisés pour la connexion et pour mémoriser votre langue et votre devise. Le chat d’assistance ne se charge qu’avec votre consentement. Il n’y a pas de cookies publicitaires. Les détails figurent dans la [Politique relative aux cookies](https://{{site}}/legal/cookies).
|
||||
|
||||
## 6. Avec qui nous partageons les données
|
||||
|
||||
Nous ne vendons pas de données et ne les transmettons pas à des réseaux publicitaires. Seuls reçoivent des données :
|
||||
|
||||
- les prestataires de paiement : les informations nécessaires au traitement d’un paiement ;
|
||||
- les fournisseurs de messagerie : pour acheminer nos e-mails, si un service d’envoi externe est utilisé, et pour faire fonctionner les boîtes aux lettres de l’assistance ;
|
||||
- les hébergeurs dont les serveurs font fonctionner le site, l’espace client, la base de données et les services d’assistance.
|
||||
|
||||
Le chat d’assistance (Chatwoot), le suivi des demandes (Twenty CRM) et la mesure d’audience (Umami) fonctionnent sur nos propres serveurs, et non chez des fournisseurs tiers.
|
||||
|
||||
Si vous vous connectez via Telegram ou nous écrivez sur Telegram, Telegram traite les données selon ses propres règles.
|
||||
|
||||
Nous ne communiquons des données aux autorités publiques que lorsque la loi l’exige, et uniquement les données dont nous disposons réellement. Nous n’avons aucun journal du trafic VPN.
|
||||
|
||||
## 7. Où les données sont stockées
|
||||
|
||||
Le site, l’espace client et la base de données sont hébergés sur des serveurs situés en [Pays d’hébergement]. Les serveurs VPN se trouvent dans les pays des emplacements : votre trafic y transite, mais ils ne conservent aucun journal. Les prestataires de paiement peuvent traiter des données dans d’autres pays selon leurs propres règles. Lorsque des données sont transférées hors de votre pays de résidence, nous le faisons sur des fondements permis par le droit applicable.
|
||||
|
||||
## 8. Durée de conservation des données
|
||||
|
||||
- Données du compte : tant que le compte existe. Après la suppression du compte, nous supprimons ou anonymisons ces données, à l’exception des informations sur les commandes et les paiements que la loi nous oblige à conserver à des fins comptables et fiscales.
|
||||
- Échanges avec l’assistance et demandes : aussi longtemps que nécessaire pour traiter la demande, mais pas au-delà de [Durée de conservation des demandes d’assistance].
|
||||
- Journaux du serveur web : 14 jours ; journal d’envoi des e-mails : 30 jours.
|
||||
- Sessions : 30 jours à compter de la dernière activité, ou jusqu’à votre déconnexion.
|
||||
- Codes de connexion : valables 10 minutes.
|
||||
|
||||
## 9. Comment nous protégeons les données
|
||||
|
||||
- Les données échangées entre votre navigateur et nos serveurs sont chiffrées en transit (HTTPS).
|
||||
- Les configurations de connexion et les identifiants de paiement sont stockés sous forme chiffrée.
|
||||
- Les codes de connexion, les jetons de session et les adresses IP ne sont stockés que sous forme de hachages.
|
||||
- Seuls les membres du personnel qui en ont besoin pour leur travail ont accès aux données, et cet accès est limité selon les rôles.
|
||||
|
||||
Aucun système n’est totalement exempt de risques. Si une violation touche vos données, nous vous en informerons comme l’exige le droit applicable.
|
||||
|
||||
## 10. Vos droits
|
||||
|
||||
Selon le droit applicable, vous pouvez :
|
||||
|
||||
- savoir quelles données nous détenons à votre sujet et en obtenir une copie ;
|
||||
- rectifier des données inexactes ;
|
||||
- supprimer votre compte et vos données, à l’exception des données que la loi nous oblige à conserver ;
|
||||
- limiter le traitement ou vous y opposer ;
|
||||
- recevoir vos données dans un format lisible par machine ;
|
||||
- retirer votre consentement aux cookies fonctionnels ;
|
||||
- introduire une réclamation auprès d’une autorité de contrôle de la protection des données.
|
||||
|
||||
Dans votre espace client, vous pouvez vous-même dissocier votre carte et vous déconnecter de tous les appareils. Pour toute autre demande, écrivez à [{{email}}](mailto:{{email}}) depuis l’adresse e-mail associée à votre compte, ou depuis votre compte Telegram associé, afin que nous puissions vérifier que la demande émane bien de vous. Nous répondons dans les délais fixés par le droit applicable.
|
||||
|
||||
## 11. Enfants
|
||||
|
||||
Le service n’est pas destiné aux personnes de moins de [Âge minimum] ans, et nous ne collectons pas sciemment leurs données. Si vous pensez qu’un enfant nous a communiqué ses données, contactez-nous et nous les supprimerons.
|
||||
|
||||
## 12. Modification de la présente politique
|
||||
|
||||
Nous pouvons mettre à jour la présente politique. La nouvelle version est publiée sur cette page avec sa date d’entrée en vigueur. Nous vous informons des modifications substantielles par e-mail ou dans votre espace client.
|
||||
|
||||
## 13. Contacts
|
||||
|
||||
- E-mail : [{{email}}](mailto:{{email}})
|
||||
- Adresse postale : [Adresse du siège social]
|
||||
|
||||
## 14. Langue
|
||||
|
||||
La présente politique est publiée en plusieurs langues. En cas de divergence entre les versions linguistiques, la version anglaise prévaut.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigueur à compter du [Date d’entrée en vigueur].
|
||||
|
||||
La présente politique complète les [Conditions d’utilisation](https://{{site}}/legal/terms) du service {{brand}}, fourni par [Dénomination sociale]. Elle explique quand et comment nous remboursons les commandes payées : nouvelles clés et renouvellements.
|
||||
|
||||
## 1. Quand nous remboursons
|
||||
|
||||
**Le service n’a pas fonctionné par notre faute.** Nous remboursons l’intégralité du montant de la commande si vous nous contactez au cours des 7 premiers jours de la période payée ; la période payée commence à la délivrance de la clé. Par exemple, si nous n’avons pas pu délivrer la clé, ou si les connexions n’ont pas fonctionné en raison d’un problème de notre côté.
|
||||
|
||||
**Vous avez changé d’avis.** Nous remboursons la partie non utilisée de la période payée, au prorata du nombre de jours, à la date de votre demande : nous divisons le montant payé pour la période par son nombre de jours et le multiplions par le nombre de jours non utilisés. Par exemple, si la période est de 30 jours et que 10 jours se sont écoulés, nous remboursons le prix de 20 jours. Cela s’applique aussi aux renouvellements : si une clé a été renouvelée automatiquement et que vous n’en avez plus besoin, écrivez-nous et nous vous rembourserons les jours non utilisés.
|
||||
|
||||
Après un remboursement, la clé concernée est désactivée.
|
||||
|
||||
## 2. Comment demander un remboursement
|
||||
|
||||
Écrivez à [{{email}}](mailto:{{email}}) depuis l’adresse e-mail associée à votre compte en indiquant :
|
||||
|
||||
- le numéro de commande ou le code de la clé, tous deux affichés dans votre espace client ;
|
||||
- le motif du remboursement ;
|
||||
- si le service n’a pas fonctionné : ce qui n’a pas fonctionné exactement et depuis quand.
|
||||
|
||||
Si votre compte n’a pas d’adresse e-mail et que vous vous connectez via Telegram, écrivez-nous sur Telegram : le lien se trouve sur la [page d’assistance](https://{{site}}/support). Nous pouvons vous demander des précisions pour comprendre le problème.
|
||||
|
||||
## 3. Où sont versés les remboursements
|
||||
|
||||
Les remboursements sont versés sur le solde de votre compte, dans sa devise, ou, lorsque le prestataire de paiement le permet, sur le moyen de paiement d’origine. Un remboursement sur le solde est crédité dès qu’il est approuvé, et nous vous informons du remboursement par e-mail. Le délai de réception d’un remboursement sur votre carte ou un autre moyen de paiement dépend du prestataire et de votre banque.
|
||||
|
||||
## 4. Délai de traitement
|
||||
|
||||
Nous examinons votre demande et vous communiquons notre décision dans un délai de [Durée de traitement des remboursements].
|
||||
|
||||
## 5. Ce qui n’est pas remboursé
|
||||
|
||||
- Les jours bonus du programme de parrainage et les autres jours ajoutés à une clé sans paiement : ce n’est pas de l’argent, ils ne peuvent être ni remboursés ni échangés contre de l’argent.
|
||||
- La partie utilisée de la période payée, si vous avez changé d’avis.
|
||||
- La période restante d’une clé bloquée pour violation des [Conditions d’utilisation](https://{{site}}/legal/terms), sauf si le droit applicable en dispose autrement.
|
||||
|
||||
## 6. Contacts
|
||||
|
||||
- E-mail : [{{email}}](mailto:{{email}})
|
||||
- Telegram : lien sur la [page d’assistance](https://{{site}}/support)
|
||||
|
||||
L’assistance est disponible de 10 h à 20 h, heure de Moscou, les jours ouvrés.
|
||||
|
||||
## 7. Langue
|
||||
|
||||
La présente politique est publiée en plusieurs langues. En cas de divergence entre les versions linguistiques, la version anglaise prévaut.
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
En vigueur à compter du [Date d’entrée en vigueur].
|
||||
|
||||
## 1. Dispositions générales
|
||||
|
||||
Les présentes conditions constituent un contrat entre vous et [Dénomination sociale], immatriculée en [Pays d’immatriculation] sous le numéro [Numéro d’immatriculation], dont le siège social se situe à l’adresse suivante : [Adresse du siège social] (ci-après « nous »). Elles régissent votre utilisation du service {{brand}} : le site web {{site}}, votre espace client sur {{cabinet}}, les bots Telegram, la mini-application Telegram et les serveurs VPN (ensemble, le « service »).
|
||||
|
||||
En utilisant le service, vous acceptez les présentes conditions. Lorsque vous passez une commande, vous confirmez séparément votre acceptation. La [Politique de confidentialité](https://{{site}}/legal/privacy), la [Politique de remboursement](https://{{site}}/legal/refund) et la [Politique relative aux cookies](https://{{site}}/legal/cookies) s’appliquent également.
|
||||
|
||||
Vous devez avoir au moins [Âge minimum] ans pour utiliser le service.
|
||||
|
||||
## 2. Le service
|
||||
|
||||
Le service {{brand}} fournit un accès VPN au moyen de clés. Une clé est une unité d’accès achetée, assortie d’une durée de validité. Chaque clé comprend des connexions : des configurations et des liens pour les applications, que vous obtenez dans votre espace client.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Particularités de {{brand}} :
|
||||
|
||||
- chaque clé dispose de sa propre adresse IP dédiée dans le pays que vous choisissez lors de l’achat. L’adresse vous est réservée exclusivement et fonctionne sur un serveur partagé, et non sur un serveur distinct ;
|
||||
- le pays d’une clé ne change jamais. Pour un autre pays, il vous faut une clé distincte dans un autre emplacement ; vous pouvez désactiver le renouvellement automatique de la clé précédente dans votre espace client ;
|
||||
- chaque clé comprend une adresse RU dédiée ;
|
||||
- tous les appareils d’une clé utilisent la même adresse ;
|
||||
- protocoles : WireGuard / AmneziaWG et Xray (VLESS, Hysteria2, XHTTP) ;
|
||||
- chaque emplacement porte le statut « Serveurs disponibles » ou « Prêt sous 24 heures ». Dans le second cas, nous préparons l’adresse après le paiement, dans un délai de 24 heures. La période payée commence à la délivrance de la clé ;
|
||||
- le premier mois coûte plus cher qu’un renouvellement : il comprend la préparation de l’adresse ainsi que la configuration des protocoles et du routage.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
<!-- mass -->
|
||||
|
||||
Particularités de {{brand}} :
|
||||
|
||||
- les serveurs sont partagés : de nombreux clients utilisent la même adresse ;
|
||||
- une clé (abonnement) donne accès à tous les pays du forfait marqués « Inclus ». Les emplacements marqués « Bientôt » sont affichés sur le site, mais ne fonctionnent pas encore ;
|
||||
- vous pouvez changer de pays aussi souvent que vous le souhaitez, gratuitement ;
|
||||
- chaque clé comprend une adresse russe ; elle n’est pas comptée parmi les pays du forfait ;
|
||||
- protocoles : Xray et WireGuard ;
|
||||
- les forfaits sont mensuels ou annuels ; la première période coûte le même prix qu’un renouvellement.
|
||||
|
||||
<!-- /mass -->
|
||||
|
||||
Les prix, les périodes, les limites d’appareils et les listes d’emplacements sont indiqués sur {{site}} et dans votre espace client. Le prix de renouvellement, la période et la limite d’appareils d’une clé sont fixés au moment de l’achat.
|
||||
|
||||
## 3. Compte et connexion
|
||||
|
||||
Il n’y a ni inscription séparée ni mot de passe : vous saisissez votre adresse e-mail et un code à usage unique reçu dans notre e-mail, et votre compte est créé lors de votre première connexion. Le code est valable 10 minutes. Vous pouvez également vous connecter via Telegram.
|
||||
|
||||
Votre compte sur le service {{brand}} n’est pas lié aux comptes de nos autres services : chacun a son propre solde, ses propres clés et son propre historique.
|
||||
|
||||
Vous êtes responsable de l’accès à votre messagerie et à votre compte Telegram, et vous ne devez pas communiquer les codes de connexion à d’autres personnes. Si vous soupçonnez qu’une autre personne a accédé à votre compte, déconnectez-vous de tous les appareils depuis votre espace client et contactez-nous.
|
||||
|
||||
Utilisez une adresse e-mail active : nous y envoyons les codes de connexion et les notifications relatives aux commandes, aux paiements et aux dates d’expiration des clés.
|
||||
|
||||
## 4. Clés, appareils et connexions
|
||||
|
||||
Vous pouvez utiliser une clé sur autant d’appareils que le permet la limite d’appareils du forfait, soit 5 au maximum. Les données de connexion (configurations, liens et QR codes) sont destinées uniquement à vos appareils : ne les publiez pas et ne les communiquez pas à d’autres personnes.
|
||||
|
||||
Pour vous connecter, vous avez besoin d’applications tierces, par exemple des clients WireGuard ou Xray. Elles sont régies par les règles de leurs développeurs.
|
||||
|
||||
L’ensemble des protocoles et les données de connexion peuvent être mis à jour pour que le service continue de fonctionner. Votre espace client contient toujours les données à jour.
|
||||
|
||||
Chaque clé est renouvelée et expire indépendamment des autres.
|
||||
|
||||
## 5. Disponibilité du service
|
||||
|
||||
Nous nous efforçons d’assurer la stabilité du service, mais ne garantissons pas qu’il sera toujours disponible sans interruption. Des opérations de maintenance programmées, des pannes de notre côté ou chez nos fournisseurs, ainsi que des restrictions imposées par les opérateurs de réseau, les propriétaires de sites web et les autorités publiques peuvent survenir.
|
||||
|
||||
Nous ne garantissons pas l’accès à un site web ou à un service en particulier, et nous ne sommes pas responsables de la manière dont ils traitent les connexions VPN.
|
||||
|
||||
Dans la mesure du possible, nous annonçons à l’avance les opérations de maintenance programmées. Si le service a été indisponible pendant une longue période par notre faute, nous pouvons prolonger votre clé pour couvrir la durée de l’interruption.
|
||||
|
||||
La liste des emplacements, des protocoles et des forfaits peut changer.
|
||||
|
||||
## 6. Paiement, solde et renouvellement automatique
|
||||
|
||||
Sur le site, les prix sont affichés dans la devise que vous sélectionnez ; dans votre espace client, ils sont affichés dans la devise de votre compte : roubles russes ou dollars américains. La devise du compte est choisie lors de l’inscription. Vous pouvez la modifier tant que votre solde est nul et que votre compte n’a ni clé ni commande impayée ; par la suite, uniquement en passant par l’assistance.
|
||||
|
||||
Votre compte dispose d’un solde. Les paiements sont crédités sur le solde, et les commandes sont réglées à partir de celui-ci ; si le solde est insuffisant, vous ne payez que la différence. Une commande impayée est valable 24 heures. Les sommes reçues après l’annulation d’une commande restent sur votre solde.
|
||||
|
||||
Les moyens de paiement sont affichés lors de la commande : cartes bancaires, cryptomonnaies et autres. Les données de carte sont saisies uniquement du côté du prestataire de paiement et ne nous parviennent jamais. Lorsque vous payez en cryptomonnaie, le montant est converti au taux fixé lors de l’émission de la facture et crédité dans la devise de votre compte une fois la transaction confirmée par le réseau.
|
||||
|
||||
Le renouvellement automatique se règle séparément pour chaque clé et est activé pour les nouvelles clés. Peu avant l’expiration d’une clé, le prix du renouvellement est déduit de votre solde ; si le solde est insuffisant, votre carte associée, si vous en avez une, est débitée du montant manquant. La carte est associée à votre compte et utilisée pour toutes les clés dont le renouvellement automatique est activé. En associant une carte, vous nous autorisez à effectuer ces débits. L’association d’une carte n’est pas disponible pour tous les moyens de paiement et toutes les devises. Si un débit échoue, nous vous en informons par e-mail et réessayons plusieurs fois, y compris pendant le délai de grâce.
|
||||
|
||||
Nous envoyons un rappel 3 jours avant l’expiration d’une clé. Vous pouvez désactiver le renouvellement automatique dans votre espace client à tout moment. Vous pouvez dissocier votre carte dans la section « Facturation » ; les renouvellements sont alors payés uniquement à partir de votre solde.
|
||||
|
||||
Vous pouvez renouveler une clé à l’avance : la nouvelle période s’ajoute à la date d’expiration en cours.
|
||||
|
||||
Le prix du renouvellement est fixé lors de l’achat d’une clé et est indiqué dans votre espace client. Si nous le modifions, nous vous en informerons au moins [Délai de préavis] avant de facturer le nouveau prix.
|
||||
|
||||
## 7. Délai de grâce et expiration
|
||||
|
||||
Si un renouvellement n’est pas payé à la date d’expiration, un délai de grâce de 3 jours commence. La clé continue de fonctionner pendant toute cette durée. Un renouvellement payé pendant le délai de grâce court à partir de la date d’expiration initiale, et non de la date de paiement.
|
||||
|
||||
À la fin du délai de grâce, la clé expire et ses connexions sont désactivées. La même règle s’applique aux clés avec et sans renouvellement automatique.
|
||||
|
||||
Une clé expirée ne peut pas être renouvelée : pour continuer à utiliser le service, achetez une nouvelle clé. Une nouvelle clé est fournie avec de nouvelles données de connexion ; la conservation des adresses précédentes n’est pas garantie.
|
||||
|
||||
## 8. Remboursements
|
||||
|
||||
Si le service n’a pas fonctionné par notre faute, nous remboursons l’intégralité du montant de la commande si vous nous contactez au cours des 7 premiers jours de la période payée. Si vous avez changé d’avis, nous remboursons la partie non utilisée de la période payée, au prorata du nombre de jours. Les remboursements sont effectués sur votre solde ou, lorsque le prestataire de paiement le permet, sur le moyen de paiement d’origine. Les détails figurent dans la [Politique de remboursement](https://{{site}}/legal/refund).
|
||||
|
||||
## 9. Programme de parrainage
|
||||
|
||||
« Parrainez un ami — vous recevez chacun un mois » : lorsqu’un ami inscrit grâce à votre invitation paie sa première commande de clé, vous recevez chacun 30 jours supplémentaires sur une clé.
|
||||
|
||||
- Votre ami reçoit les jours sur la clé achetée avec cette commande. Vous les recevez sur la plus récente de vos clés qui n’a ni expiré ni été annulée ; s’il n’y en a pas, les jours sont ajoutés à votre prochaine clé.
|
||||
- La recharge du solde ne compte pas comme un achat.
|
||||
- Un lien d’invitation est mémorisé dans le navigateur de votre ami pendant 60 jours.
|
||||
- L’auto-parrainage n’est pas pris en compte. Nous examinons manuellement les invitations douteuses et pouvons les refuser.
|
||||
- Seules les invitations au service {{brand}} sont prises en compte : les inscriptions et les achats sur nos autres services ne sont pas comptabilisés.
|
||||
- Les jours bonus ne sont pas de l’argent : ils ne peuvent être ni échangés contre de l’argent ni remboursés.
|
||||
- Nous pouvons modifier ou arrêter le programme. Les jours déjà crédités sont conservés.
|
||||
|
||||
## 10. Règles d’utilisation
|
||||
|
||||
Vous ne devez pas utiliser le service pour :
|
||||
|
||||
- toute activité illégale ;
|
||||
- des attaques contre des réseaux, des sites web ou des appareils, le piratage ou l’accès non autorisé aux systèmes d’autrui ;
|
||||
- l’envoi de spam ;
|
||||
- la fraude ou l’hameçonnage ;
|
||||
- la création ou la diffusion de logiciels malveillants, ou le contrôle de botnets ;
|
||||
- la création, le stockage ou la diffusion de contenus d’abus sexuels sur enfants ;
|
||||
- la violation des droits d’autrui, notamment du droit d’auteur et du droit au respect de la vie privée ;
|
||||
- l’utilisation d’une clé au-delà de sa limite d’appareils, ou la revente ou toute autre cession de l’accès à des tiers sans accord distinct avec nous ;
|
||||
- toute action qui perturbe le service ou d’autres clients, ou qui contourne les limites du service.
|
||||
|
||||
Vous êtes responsable de tout ce qui est fait au moyen de votre clé et de votre compte.
|
||||
|
||||
## 11. Suspension et blocage
|
||||
|
||||
Si vous enfreignez les présentes conditions, nous pouvons suspendre vos clés ou bloquer votre compte. En général, nous vous avertissons d’abord et vous demandons de cesser. En cas d’infraction grave (attaques, fraude, diffusion de logiciels malveillants ou de contenus d’abus sexuels sur enfants), nous bloquons immédiatement, sans avertissement.
|
||||
|
||||
Si une clé est bloquée en raison d’une infraction, la période payée restante n’est pas remboursée, sauf si le droit applicable en dispose autrement. Si vous contestez un blocage, écrivez à [{{email}}](mailto:{{email}}).
|
||||
|
||||
Vous pouvez cesser d’utiliser le service à tout moment : pour cela, désactivez le renouvellement automatique de vos clés. Pour supprimer votre compte, contactez-nous.
|
||||
|
||||
## 12. Limitation de responsabilité
|
||||
|
||||
Le service est fourni « en l’état ». Dans la mesure permise par le droit applicable :
|
||||
|
||||
- nous ne sommes pas responsables des dommages indirects, du manque à gagner ou de la perte de données ;
|
||||
- nous ne sommes pas responsables des interruptions causées par les opérateurs de réseau, les autorités publiques, les propriétaires de sites web ou d’autres tiers, ni par des défaillances de vos appareils et applications ;
|
||||
- notre responsabilité au titre de toute réclamation est limitée au montant que vous avez payé pour la période payée de la clé concernée par la réclamation.
|
||||
|
||||
Ces limitations n’affectent pas les droits qui ne peuvent pas être limités par la loi, notamment les droits des consommateurs.
|
||||
|
||||
## 13. Modification des présentes conditions
|
||||
|
||||
Nous pouvons modifier les présentes conditions. La nouvelle version est publiée sur cette page avec sa date d’entrée en vigueur. Nous vous informons des modifications substantielles par e-mail ou dans votre espace client au moins [Délai de préavis] avant leur entrée en vigueur. Si vous n’acceptez pas la nouvelle version, cessez d’utiliser le service et désactivez le renouvellement automatique ; vous pouvez obtenir le remboursement de la partie non utilisée conformément à la [Politique de remboursement](https://{{site}}/legal/refund).
|
||||
|
||||
## 14. Droit applicable et litiges
|
||||
|
||||
Les présentes conditions sont régies par [Droit applicable]. Nous cherchons d’abord à résoudre tout litige par correspondance : écrivez à [{{email}}](mailto:{{email}}). Si nous ne parvenons pas à un accord, le litige est soumis à : [Lieu de règlement des litiges]. Cela ne vous prive pas de la protection que vous accordent les règles impératives de protection des consommateurs, lorsqu’elles s’appliquent à vous.
|
||||
|
||||
## 15. Contacts
|
||||
|
||||
- E-mail : [{{email}}](mailto:{{email}})
|
||||
- Telegram : lien sur la [page d’assistance](https://{{site}}/support)
|
||||
- Adresse postale : [Adresse du siège social]
|
||||
|
||||
L’assistance est disponible de 10 h à 20 h, heure de Moscou, les jours ouvrés.
|
||||
|
||||
## 16. Langue
|
||||
|
||||
Les présentes conditions sont publiées en plusieurs langues. En cas de divergence entre les versions linguistiques, la version anglaise prévaut.
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Действует с [Дата вступления в силу].
|
||||
|
||||
Эта политика объясняет, какие cookies использует сервис {{brand}} на {{site}} и {{cabinet}}. Сервис предоставляет [Наименование юрлица]. Как мы обрабатываем данные в целом, описано в [Политике конфиденциальности](https://{{site}}/legal/privacy).
|
||||
|
||||
## 1. Что такое cookies
|
||||
|
||||
Cookies — небольшие фрагменты данных, которые сайт сохраняет в браузере, чтобы запомнить, например, что вы вошли в аккаунт или какой язык выбрали. Похожим образом работает localStorage — хранилище в браузере, содержимое которого не отправляется на сервер.
|
||||
|
||||
## 2. Какие cookies мы используем
|
||||
|
||||
Для каждой cookie указаны назначение, срок хранения и категория.
|
||||
|
||||
- `vx_session` — сессия входа в кабинет, ставится только на {{cabinet}} — 30 дней, срок продлевается, пока вы пользуетесь кабинетом — обязательная.
|
||||
- `NEXT_LOCALE` — язык, выбранный в переключателе — 1 год — обязательная.
|
||||
- `vx_currency` — выбранная валюта цен, а если вы вошли — валюта аккаунта — 1 год — обязательная.
|
||||
- `vx_ref` — код приглашения, если вы перешли по реферальной ссылке, ставится только на {{cabinet}} — 60 дней — обязательная.
|
||||
- `vx_consent` — ваш выбор в баннере cookies — 1 год — обязательная.
|
||||
- `cw_*` — cookies чата поддержки (Chatwoot): сохраняют переписку с поддержкой между посещениями — [Срок хранения cookies чата] — функциональные.
|
||||
|
||||
**Обязательные** cookies нужны, чтобы сайт и кабинет работали: без них не получится войти в аккаунт, а выбор языка и валюты не сохранится. Согласие на них не запрашивается.
|
||||
|
||||
**Функциональные** cookies ставятся только с вашего согласия. Если вы его не дали, виджет чата не загружается; написать нам можно по почте или в Telegram.
|
||||
|
||||
## 3. Аналитика без cookies
|
||||
|
||||
Статистику посещений мы собираем с помощью Umami на собственном сервере. Umami не использует cookies, данные не передаются третьим лицам.
|
||||
|
||||
## 4. Рекламные cookies
|
||||
|
||||
Рекламных cookies и трекеров мы не используем.
|
||||
|
||||
## 5. localStorage
|
||||
|
||||
Тема оформления — светлая, тёмная или как в системе — хранится не в cookie, а в localStorage под ключом `vx_theme`. У каждого домена своя настройка.
|
||||
|
||||
## 6. Cookies других сервисов
|
||||
|
||||
- Кнопка входа через Telegram на странице входа загружается с серверов Telegram, и Telegram может ставить свои cookies по своим правилам.
|
||||
- На шаге оплаты страница или виджет платёжного провайдера может ставить свои cookies, нужные для проведения платежа и защиты от мошенничества.
|
||||
|
||||
## 7. Как управлять cookies
|
||||
|
||||
При первом посещении баннер предлагает принять или отклонить функциональные cookies. Чтобы изменить решение, удалите cookie `vx_consent` или все cookies сайта в настройках браузера — баннер появится снова.
|
||||
|
||||
В настройках браузера cookies можно удалить или заблокировать. Без обязательных cookies не получится войти в кабинет, а выбор языка и валюты не сохранится.
|
||||
|
||||
## 8. Контакты
|
||||
|
||||
Вопросы о cookies присылайте на [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 9. Язык документа
|
||||
|
||||
Политика опубликована на нескольких языках. При расхождениях между языковыми версиями действует английская.
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Действует с [Дата вступления в силу].
|
||||
|
||||
## 1. Кто мы
|
||||
|
||||
Сервис {{brand}} — сайт {{site}}, кабинет {{cabinet}}, Telegram-боты, мини-приложение в Telegram и VPN-серверы — предоставляет [Наименование юрлица], зарегистрированное в [Страна регистрации] под номером [Регистрационный номер], адрес: [Юридический адрес] (далее — «мы»). Мы отвечаем за обработку ваших персональных данных как оператор (контролёр).
|
||||
|
||||
Эта политика объясняет, какие данные мы собираем, зачем, сколько храним и кому передаём. По любым вопросам о данных пишите на [{{email}}](mailto:{{email}}).
|
||||
|
||||
## 2. Коротко
|
||||
|
||||
- VPN-серверы не ведут журналов: ни трафика, ни посещаемых сайтов, ни времени подключений, ни IP-адресов пользователей.
|
||||
- Мы храним только то, что нужно для аккаунта, оплаты, поддержки и защиты от злоупотреблений.
|
||||
- В базе данных IP-адреса посетителей сайта и кабинета хранятся только в виде необратимых хэшей, журналы веб-сервера удаляются через 14 дней.
|
||||
- Мы не продаём данные и не используем рекламные трекеры.
|
||||
|
||||
## 3. VPN-серверы не ведут журналов
|
||||
|
||||
Наши VPN-серверы не записывают:
|
||||
|
||||
- ваш трафик и его содержимое;
|
||||
- сайты и сервисы, которые вы посещаете;
|
||||
- время подключений и отключений;
|
||||
- IP-адреса, с которых вы подключаетесь.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Индивидуальный IP и RU-адрес закреплены за вашим ключом, поэтому в данных аккаунта указано, какие адреса выданы ключу. Что происходит через эти адреса, мы не записываем.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
## 4. Какие данные мы собираем и зачем
|
||||
|
||||
**Аккаунт.** Email; Telegram ID, имя пользователя и имя — если вы входите через Telegram или привязали его; язык и валюта; даты регистрации и последнего входа; реферальный код и то, кто вас пригласил. Цель: создать аккаунт, впустить вас в кабинет, отправлять коды входа и уведомления. Основание: исполнение договора.
|
||||
|
||||
**Ключи и оплата.** Ключи, их подключения, тариф, локации, сроки и настройка автопродления; заказы, платежи и история движений по балансу. Если вы привязали карту — её идентификатор у платёжного провайдера, тип карты, последние 4 цифры и срок действия. Номер карты целиком и код безопасности вводятся только на стороне провайдера и к нам не попадают. Цель: оказывать услугу, принимать оплату, вести учёт. Основание: исполнение договора и требования закона.
|
||||
|
||||
**Поддержка и заявки.** Переписка с поддержкой по почте, в Telegram и в чате на сайте, а также служебные заметки сотрудников поддержки. Если вы вошли в аккаунт, чат показывает оператору ваш email, идентификатор аккаунта и число ключей. Данные из форм на сайте: имя, email, Telegram, WhatsApp, тема и текст сообщения, страница, с которой отправлена форма. Цель: ответить вам и решить вопрос. Основание: исполнение договора, действия по вашему запросу или наш законный интерес.
|
||||
|
||||
**Безопасность.** IP-адреса посетителей сайта и кабинета хранятся в базе данных только как необратимые хэши (HMAC). По ним мы ограничиваем частоту запросов кода входа и выявляем приглашения самого себя. Для сессий мы храним сведения о браузере и устройстве и время последней активности. Коды входа хранятся только в виде хэшей. Проверка «я не робот» работает без сторонних сервисов. Основание: наш законный интерес — защита сервиса и аккаунтов.
|
||||
|
||||
**Технические журналы.** Журналы веб-сервера содержат IP-адрес, время, запрошенный адрес и сведения о браузере; они удаляются через 14 дней. Журнал отправки писем — адрес получателя, тема и статус доставки — хранится 30 дней. Основание: наш законный интерес — работа и защита сервиса.
|
||||
|
||||
**Статистика посещений.** Мы используем Umami на собственном сервере, без cookies; данные не передаются третьим лицам. Учитываются открытые страницы, источник перехода, тип браузера и устройства, страна. Основание: наш законный интерес — улучшение сайта.
|
||||
|
||||
Язык сайта по умолчанию подбирается по стране, которую мы определяем по IP-адресу с помощью базы на нашем сервере. IP-адрес при этом никуда не передаётся.
|
||||
|
||||
## 5. Cookies
|
||||
|
||||
Обязательные cookies нужны для входа, выбора языка и валюты. Чат поддержки загружается только с вашего согласия. Рекламных cookies нет. Подробно — в [Политике cookies](https://{{site}}/legal/cookies).
|
||||
|
||||
## 6. Кому мы передаём данные
|
||||
|
||||
Мы не продаём данные и не передаём их рекламным сетям. Данные получают только:
|
||||
|
||||
- платёжные провайдеры — сведения, нужные для проведения платежа;
|
||||
- почтовые провайдеры — для доставки наших писем, если используется внешний сервис отправки, и для работы ящиков поддержки;
|
||||
- хостинг-провайдеры, на серверах которых работают сайт, кабинет, база данных и сервисы поддержки.
|
||||
|
||||
Чат поддержки (Chatwoot), учёт заявок (Twenty CRM) и аналитика (Umami) работают на наших собственных серверах, а не у сторонних поставщиков.
|
||||
|
||||
Если вы входите через Telegram или пишете нам в Telegram, Telegram обрабатывает данные по своим правилам.
|
||||
|
||||
Мы раскрываем данные государственным органам, только когда этого требует закон, и только те, которые у нас есть. Журналов VPN-трафика у нас нет.
|
||||
|
||||
## 7. Где хранятся данные
|
||||
|
||||
Сайт, кабинет и база данных размещены на серверах в [Страна размещения серверов сайта]. VPN-серверы находятся в странах локаций: через них проходит ваш трафик, но журналов они не ведут. Платёжные провайдеры могут обрабатывать данные в других странах по своим правилам. Если данные передаются за пределы страны вашего проживания, мы делаем это на основаниях, которые допускает применимое право.
|
||||
|
||||
## 8. Сколько мы храним данные
|
||||
|
||||
- Данные аккаунта — пока аккаунт существует. После удаления аккаунта мы удаляем или обезличиваем их, кроме сведений о заказах и платежах, которые закон требует хранить для бухгалтерского и налогового учёта.
|
||||
- Переписку с поддержкой и заявки — пока они нужны для работы с обращением, но не дольше [Срок хранения обращений].
|
||||
- Журналы веб-сервера — 14 дней, журнал отправки писем — 30 дней.
|
||||
- Сессии — 30 дней с последней активности или до выхода из аккаунта.
|
||||
- Коды входа действуют 10 минут.
|
||||
|
||||
## 9. Как мы защищаем данные
|
||||
|
||||
- Данные между вашим браузером и нашими серверами передаются в зашифрованном виде (HTTPS).
|
||||
- Конфигурации подключений и платёжные идентификаторы хранятся в зашифрованном виде.
|
||||
- Коды входа, токены сессий и IP-адреса хранятся только в виде хэшей.
|
||||
- Доступ к данным есть только у сотрудников, которым он нужен для работы, и ограничен по ролям.
|
||||
|
||||
Полностью исключить риски нельзя. Если произойдёт утечка, затрагивающая ваши данные, мы сообщим об этом в порядке, который предусматривает применимое право.
|
||||
|
||||
## 10. Ваши права
|
||||
|
||||
В зависимости от применимого права вы можете:
|
||||
|
||||
- узнать, какие данные о вас мы храним, и получить их копию;
|
||||
- исправить неточные данные;
|
||||
- удалить аккаунт и данные, кроме тех, что мы обязаны хранить по закону;
|
||||
- ограничить обработку или возразить против неё;
|
||||
- получить свои данные в машиночитаемом формате;
|
||||
- отозвать согласие на функциональные cookies;
|
||||
- подать жалобу в надзорный орган по защите данных.
|
||||
|
||||
В кабинете вы можете сами отвязать карту и выйти из аккаунта на всех устройствах. Для остального напишите на [{{email}}](mailto:{{email}}) с адреса, привязанного к аккаунту, или из привязанного Telegram — так мы убедимся, что запрос отправили вы. Мы отвечаем в сроки, установленные применимым правом.
|
||||
|
||||
## 11. Дети
|
||||
|
||||
Сервис не предназначен для лиц младше [Минимальный возраст] лет, и мы сознательно не собираем их данные. Если вы считаете, что ребёнок передал нам свои данные, напишите нам — мы их удалим.
|
||||
|
||||
## 12. Изменения политики
|
||||
|
||||
Мы можем обновлять эту политику. Новая редакция публикуется на этой странице с датой вступления в силу. О существенных изменениях мы сообщаем по почте или в кабинете.
|
||||
|
||||
## 13. Контакты
|
||||
|
||||
- Email: [{{email}}](mailto:{{email}})
|
||||
- Почтовый адрес: [Юридический адрес]
|
||||
|
||||
## 14. Язык документа
|
||||
|
||||
Политика опубликована на нескольких языках. При расхождениях между языковыми версиями действует английская.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Действует с [Дата вступления в силу].
|
||||
|
||||
Эта политика дополняет [Условия использования](https://{{site}}/legal/terms) сервиса {{brand}}, который предоставляет [Наименование юрлица]. Она объясняет, когда и как мы возвращаем деньги за оплаченные заказы — новые ключи и продления.
|
||||
|
||||
## 1. Когда мы возвращаем деньги
|
||||
|
||||
**Сервис не работал по нашей вине.** Возвращаем полную стоимость заказа, если вы обратились в течение первых 7 дней оплаченного периода; оплаченный период начинается с момента выдачи ключа. Например, если мы не смогли выдать ключ или подключения не работали из-за неполадок на нашей стороне.
|
||||
|
||||
**Вы передумали.** Возвращаем стоимость неиспользованного остатка оплаченного периода — пропорционально дням, на дату запроса: сумму, уплаченную за период, делим на число его дней и умножаем на число неиспользованных. Например, если период — 30 дней и прошло 10, вернём стоимость 20 дней. Это правило действует и для продлений: если ключ продлился автоматически, а он вам больше не нужен, напишите нам — вернём стоимость неиспользованных дней.
|
||||
|
||||
После возврата ключ, за который вернули деньги, отключается.
|
||||
|
||||
## 2. Как запросить возврат
|
||||
|
||||
Напишите на [{{email}}](mailto:{{email}}) с адреса, привязанного к аккаунту, и укажите:
|
||||
|
||||
- номер заказа или код ключа — они есть в кабинете;
|
||||
- причину возврата;
|
||||
- если сервис не работал — что именно не работало и с какого времени.
|
||||
|
||||
Если у аккаунта нет почты и вы входите через Telegram, напишите нам в Telegram — ссылка на [странице поддержки](https://{{site}}/support). Чтобы разобраться в проблеме, мы можем попросить уточнить детали.
|
||||
|
||||
## 3. Куда возвращаются деньги
|
||||
|
||||
Деньги возвращаются на баланс аккаунта в его валюте или, если платёжный провайдер это позволяет, на исходный способ оплаты. На баланс деньги зачисляются сразу после одобрения возврата, о возврате мы сообщаем письмом. Срок поступления на карту или другой способ оплаты зависит от провайдера и банка.
|
||||
|
||||
## 4. Сроки рассмотрения
|
||||
|
||||
Мы рассматриваем запрос и сообщаем о решении в течение [Срок обработки возврата].
|
||||
|
||||
## 5. Что не возвращается
|
||||
|
||||
- Бонусные дни по реферальной программе и другие дни, добавленные к сроку ключа без оплаты: это не деньги, их нельзя вернуть или обменять на деньги.
|
||||
- Использованная часть оплаченного периода, если вы передумали.
|
||||
- Оставшийся период ключа, заблокированного за нарушение [Условий использования](https://{{site}}/legal/terms), если иное не требует применимое право.
|
||||
|
||||
## 6. Контакты
|
||||
|
||||
- Email: [{{email}}](mailto:{{email}})
|
||||
- Telegram: ссылка на [странице поддержки](https://{{site}}/support)
|
||||
|
||||
Поддержка работает с 10:00 до 20:00 по московскому времени в рабочие дни.
|
||||
|
||||
## 7. Язык документа
|
||||
|
||||
Политика опубликована на нескольких языках. При расхождениях между языковыми версиями действует английская.
|
||||
@@ -0,0 +1,168 @@
|
||||
<!-- Черновик: перед публичным запуском — проверка юристом (01-product, раздел 13) -->
|
||||
|
||||
Действует с [Дата вступления в силу].
|
||||
|
||||
## 1. Общие положения
|
||||
|
||||
Эти условия — договор между вами и [Наименование юрлица], зарегистрированным в [Страна регистрации] под номером [Регистрационный номер], адрес: [Юридический адрес] (далее — «мы»). Они определяют, как вы пользуетесь сервисом {{brand}}: сайтом {{site}}, кабинетом {{cabinet}}, Telegram-ботами, мини-приложением в Telegram и VPN-серверами (вместе — «сервис»).
|
||||
|
||||
Пользуясь сервисом, вы принимаете эти условия. При оформлении заказа вы подтверждаете согласие с ними отдельно. Кроме условий, действуют [Политика конфиденциальности](https://{{site}}/legal/privacy), [Политика возврата средств](https://{{site}}/legal/refund) и [Политика cookies](https://{{site}}/legal/cookies).
|
||||
|
||||
Пользоваться сервисом могут лица не моложе [Минимальный возраст] лет.
|
||||
|
||||
## 2. Сервис
|
||||
|
||||
Сервис {{brand}} предоставляет доступ к VPN по ключам. Ключ — купленная единица доступа со сроком действия. В ключ входят подключения: конфигурации и ссылки для приложений, которые вы получаете в кабинете.
|
||||
|
||||
<!-- dedicated -->
|
||||
|
||||
Особенности сервиса {{brand}}:
|
||||
|
||||
- у каждого ключа свой индивидуальный IP-адрес в стране, выбранной при покупке. Адрес закреплён только за вами и работает на общем сервере, а не на отдельном;
|
||||
- страна ключа не меняется никогда. Для другой страны нужен отдельный ключ в другой локации; автопродление прежнего ключа можно отключить в кабинете;
|
||||
- в каждый ключ входит выделенный RU-адрес;
|
||||
- все устройства ключа работают с одним и тем же адресом;
|
||||
- протоколы: WireGuard / AmneziaWG и Xray (VLESS, Hysteria2, XHTTP);
|
||||
- у локации статус «Серверы в наличии» или «Установим за 24 часа». Во втором случае мы готовим адрес после оплаты, в течение 24 часов. Оплаченный период начинается с момента выдачи ключа;
|
||||
- первый месяц стоит дороже продления: в него входит подготовка адреса, настройка протоколов и маршрутизации.
|
||||
|
||||
<!-- /dedicated -->
|
||||
|
||||
<!-- mass -->
|
||||
|
||||
Особенности сервиса {{brand}}:
|
||||
|
||||
- серверы общие: одним адресом пользуются многие клиенты;
|
||||
- один ключ (подписка) даёт доступ ко всем странам тарифа со статусом «Включено». Локации со статусом «Скоро» показаны на сайте, но пока не работают;
|
||||
- страну можно менять сколько угодно и бесплатно;
|
||||
- в каждый ключ входит российский адрес; в число стран тарифа он не входит;
|
||||
- протоколы: Xray и WireGuard;
|
||||
- тарифы — на месяц и на год; первый период стоит столько же, сколько продление.
|
||||
|
||||
<!-- /mass -->
|
||||
|
||||
Цены, сроки, лимиты устройств и списки локаций указаны на {{site}} и в кабинете. Цена продления, период и лимит устройств фиксируются за ключом при покупке.
|
||||
|
||||
## 3. Аккаунт и вход
|
||||
|
||||
Отдельной регистрации и паролей нет: вы вводите email и одноразовый код из письма, и аккаунт создаётся при первом входе. Код действует 10 минут. Войти можно также через Telegram.
|
||||
|
||||
Аккаунт в сервисе {{brand}} не связан с аккаунтами в других наших сервисах: баланс, ключи и история у них раздельные.
|
||||
|
||||
Вы отвечаете за доступ к своей почте и Telegram и не передаёте коды входа другим людям. Если вы подозреваете, что кто-то получил доступ к аккаунту, выйдите из него на всех устройствах в кабинете и напишите нам.
|
||||
|
||||
Указывайте действующий email: на него приходят коды входа и уведомления о заказах, платежах и сроках ключей.
|
||||
|
||||
## 4. Ключи, устройства и подключения
|
||||
|
||||
Ключ можно использовать на устройствах в пределах лимита, указанного в тарифе, — до 5. Данные подключений — конфигурации, ссылки и QR-коды — предназначены только для ваших устройств: не публикуйте их и не передавайте другим людям.
|
||||
|
||||
Для подключения нужны сторонние приложения, например клиенты WireGuard или Xray. Их работа регулируется правилами их разработчиков.
|
||||
|
||||
Набор протоколов и данные подключений могут обновляться, чтобы сервис продолжал работать. Актуальные данные всегда есть в кабинете.
|
||||
|
||||
Каждый ключ продлевается и истекает независимо от других.
|
||||
|
||||
## 5. Доступность сервиса
|
||||
|
||||
Мы стремимся к стабильной работе сервиса, но не гарантируем, что он будет доступен всегда и без перерывов. Возможны плановые работы, сбои у нас и у наших поставщиков, а также ограничения со стороны операторов связи, владельцев сайтов и государственных органов.
|
||||
|
||||
Мы не гарантируем доступ к конкретным сайтам и сервисам и не отвечаем за то, как они относятся к подключениям через VPN.
|
||||
|
||||
О плановых работах мы по возможности предупреждаем заранее. Если сервис долго не работал по нашей вине, мы можем продлить ключ на время простоя.
|
||||
|
||||
Список локаций, протоколов и тарифов может меняться.
|
||||
|
||||
## 6. Оплата, баланс и автопродление
|
||||
|
||||
На сайте цены показываются в выбранной валюте, в кабинете — в валюте аккаунта: рублях или долларах США. Валюта аккаунта выбирается при регистрации. Сменить её можно, пока на балансе нет денег, а у аккаунта нет ключей и неоплаченных заказов; позже — только через поддержку.
|
||||
|
||||
У аккаунта есть баланс. Платежи зачисляются на баланс, заказы оплачиваются с него; если баланса не хватает, к оплате выставляется только разница. Неоплаченный заказ действует 24 часа. Деньги, поступившие после отмены заказа, остаются на балансе.
|
||||
|
||||
Способы оплаты показываются при оформлении заказа: банковские карты, криптовалюта и другие. Данные карты вводятся только на стороне платёжного провайдера и к нам не попадают. При оплате криптовалютой сумма пересчитывается по курсу, зафиксированному при выставлении счёта, и зачисляется в валюте аккаунта после подтверждения в сети.
|
||||
|
||||
Автопродление настраивается для каждого ключа отдельно; у нового ключа оно включено. Незадолго до окончания срока стоимость продления списывается с баланса, а если его не хватает — недостающая сумма списывается с привязанной карты, если она есть. Карта привязывается к аккаунту и используется для всех ключей с автопродлением. Привязывая карту, вы разрешаете нам такие списания. Привязка карты доступна не для всех способов оплаты и валют. Если списание не прошло, мы сообщим об этом по почте и повторим попытку несколько раз, в том числе в льготный период.
|
||||
|
||||
За 3 дня до окончания срока ключа мы присылаем напоминание. Автопродление можно отключить в кабинете в любой момент. Карту можно отвязать в разделе «Счёт и платежи» — после этого продление оплачивается только с баланса.
|
||||
|
||||
Ключ можно продлить заранее: новый период добавляется к текущему сроку.
|
||||
|
||||
Цена продления фиксируется при покупке ключа и указана в кабинете. Если мы её изменим, то сообщим об этом не позднее чем за [Срок уведомления] до списания по новой цене.
|
||||
|
||||
## 7. Льготный период и окончание срока
|
||||
|
||||
Если продление не оплачено к окончанию срока, начинается льготный период — 3 дня. Всё это время ключ работает. Продление, оплаченное в льготный период, отсчитывается от даты окончания прежнего срока, а не от даты оплаты.
|
||||
|
||||
Когда льготный период заканчивается, ключ истекает и его подключения отключаются. Правило одинаковое для ключей с автопродлением и без него.
|
||||
|
||||
Истёкший ключ продлить нельзя: чтобы продолжить пользоваться сервисом, купите новый ключ. У нового ключа будут новые данные подключения; сохранение прежних адресов не гарантируется.
|
||||
|
||||
## 8. Возврат средств
|
||||
|
||||
Если сервис не работал по нашей вине, мы возвращаем полную стоимость заказа при обращении в течение первых 7 дней оплаченного периода. Если вы передумали, мы возвращаем стоимость неиспользованного остатка оплаченного периода пропорционально дням. Деньги возвращаются на баланс или, если платёжный провайдер это позволяет, на исходный способ оплаты. Подробности — в [Политике возврата средств](https://{{site}}/legal/refund).
|
||||
|
||||
## 9. Реферальная программа
|
||||
|
||||
«Приведите друга — вы оба получите по месяцу»: когда друг, зарегистрировавшийся по вашему приглашению, оплатит первый заказ на ключ, вы оба получите по 30 дней к сроку ключа.
|
||||
|
||||
- Другу дни добавляются к ключу, который он купил этим заказом. Вам — к последнему купленному ключу, который не истёк и не отменён; если такого нет, дни добавятся к вашему следующему ключу.
|
||||
- Пополнение баланса покупкой не считается.
|
||||
- Приглашение по ссылке запоминается в браузере друга на 60 дней.
|
||||
- Приглашение самого себя не засчитывается. Сомнительные приглашения мы проверяем вручную и можем отклонить.
|
||||
- Засчитываются только приглашения в сервис {{brand}}: регистрации и покупки в других наших сервисах не учитываются.
|
||||
- Бонусные дни — не деньги: их нельзя обменять на деньги или вернуть.
|
||||
- Мы можем изменить условия программы или завершить её. Уже начисленные дни сохраняются.
|
||||
|
||||
## 10. Правила использования
|
||||
|
||||
Сервис нельзя использовать для:
|
||||
|
||||
- любой незаконной деятельности;
|
||||
- атак на сети, сайты и устройства, взлома и несанкционированного доступа к чужим системам;
|
||||
- рассылки спама;
|
||||
- мошенничества и фишинга;
|
||||
- создания и распространения вредоносных программ, управления ботнетами;
|
||||
- создания, хранения и распространения материалов сексуального насилия над детьми;
|
||||
- нарушения прав других людей, в том числе авторских прав и права на частную жизнь;
|
||||
- использования ключа сверх лимита устройств, перепродажи или иной передачи доступа третьим лицам без отдельного договора с нами;
|
||||
- действий, которые мешают работе сервиса и других клиентов или обходят его ограничения.
|
||||
|
||||
Вы отвечаете за действия, совершённые с использованием вашего ключа и аккаунта.
|
||||
|
||||
## 11. Приостановка и блокировка
|
||||
|
||||
Если вы нарушаете эти условия, мы можем приостановить работу ваших ключей или заблокировать аккаунт. Обычно мы сначала предупреждаем и просим устранить нарушение. При серьёзных нарушениях — атаках, мошенничестве, распространении вредоносных программ или материалов сексуального насилия над детьми — блокируем сразу, без предупреждения.
|
||||
|
||||
Если ключ заблокирован из-за нарушения, оставшийся оплаченный период не возвращается, если иное не требует применимое право. Если вы не согласны с блокировкой, напишите на [{{email}}](mailto:{{email}}).
|
||||
|
||||
Вы можете прекратить пользоваться сервисом в любой момент: для этого отключите автопродление ключей. Чтобы удалить аккаунт, напишите нам.
|
||||
|
||||
## 12. Ограничение ответственности
|
||||
|
||||
Сервис предоставляется «как есть». В пределах, допустимых применимым правом:
|
||||
|
||||
- мы не отвечаем за косвенные убытки, упущенную выгоду и потерю данных;
|
||||
- мы не отвечаем за перебои, вызванные действиями операторов связи, государственных органов, владельцев сайтов и других третьих лиц, а также неисправностями ваших устройств и приложений;
|
||||
- наша ответственность по любой претензии ограничена суммой, которую вы заплатили за оплаченный период ключа, к которому относится претензия.
|
||||
|
||||
Эти ограничения не затрагивают права, которые нельзя ограничить по закону, в том числе права потребителей.
|
||||
|
||||
## 13. Изменение условий
|
||||
|
||||
Мы можем менять эти условия. Новая редакция публикуется на этой странице с датой вступления в силу. О существенных изменениях мы сообщаем по почте или в кабинете не позднее чем за [Срок уведомления] до их вступления в силу. Если вы не согласны с новой редакцией, прекратите пользоваться сервисом и отключите автопродление; стоимость неиспользованного остатка можно вернуть по [Политике возврата средств](https://{{site}}/legal/refund).
|
||||
|
||||
## 14. Применимое право и споры
|
||||
|
||||
К этим условиям применяется [Применимое право]. Сначала мы стараемся решить любой спор перепиской — напишите на [{{email}}](mailto:{{email}}). Если договориться не удалось, спор рассматривается: [Место рассмотрения споров]. Это не лишает вас защиты, которую дают обязательные нормы о защите прав потребителей, если они к вам применимы.
|
||||
|
||||
## 15. Контакты
|
||||
|
||||
- Email: [{{email}}](mailto:{{email}})
|
||||
- Telegram: ссылка на [странице поддержки](https://{{site}}/support)
|
||||
- Почтовый адрес: [Юридический адрес]
|
||||
|
||||
Поддержка работает с 10:00 до 20:00 по московскому времени в рабочие дни.
|
||||
|
||||
## 16. Язык документа
|
||||
|
||||
Условия опубликованы на нескольких языках. При расхождениях между языковыми версиями действует английская.
|
||||
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* Начальные страницы сайтов из блоков (D-029). Тексты — ссылки на словари content/*.json,
|
||||
* правки текстов макетов — по решениям при сверке плана этапа 1b.
|
||||
*/
|
||||
import { LEGAL_SLUGS, type LegalSlug, type PageSlug } from '@/core/pages'
|
||||
import type { SiteCode } from '@/core/sites'
|
||||
import type { Locale } from '@/i18n/locales'
|
||||
|
||||
import { local, t, type Text } from './content'
|
||||
|
||||
export type PageSeed = {
|
||||
slug: PageSlug
|
||||
title: Text
|
||||
seo?: { description?: Text }
|
||||
layout: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
const line = (text: Text) => ({ text })
|
||||
|
||||
/** Схема маршрутов: одинаковая у брендов, различаются подписи «вашу / выбранную локацию» */
|
||||
const routes = (k: (key: string) => Text) => ({
|
||||
title: k('advantages.routesTitle'),
|
||||
text: k('advantages.routesText'),
|
||||
schemes: [
|
||||
{
|
||||
title: k('advantages.inRussia'),
|
||||
rows: [
|
||||
{ label: k('advantages.ruSites'), mode: 'direct', text: k('advantages.viaRuDirect') },
|
||||
{
|
||||
label: k('advantages.restOfWorld'),
|
||||
mode: 'tunnel',
|
||||
text: k('advantages.tunnelToLocation'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: k('advantages.abroad'),
|
||||
rows: [
|
||||
{ label: k('advantages.globalSites'), mode: 'direct', text: k('advantages.fromLocation') },
|
||||
{ label: k('advantages.ruSites'), mode: 'tunnel', text: k('advantages.tunnelViaRu') },
|
||||
],
|
||||
note: k('advantages.abroadNote'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const hero = (k: (key: string) => Text, secondaryHref: string) => ({
|
||||
blockType: 'hero',
|
||||
badges: [
|
||||
{ text: k('hero.badge'), style: 'outline' },
|
||||
{ text: k('hero.since'), style: 'soft' },
|
||||
],
|
||||
title: k('hero.title'),
|
||||
text: k('hero.text'),
|
||||
primary: { label: k('hero.primary'), href: '#prices' },
|
||||
secondary: { label: k('hero.secondary'), href: secondaryHref },
|
||||
plate: {
|
||||
title: k('hero.plateTitle'),
|
||||
items: [line(k('hero.platePrice')), line(k('hero.plateDevices')), line(k('hero.plateRu'))],
|
||||
},
|
||||
})
|
||||
|
||||
const dedicatedHome = (): PageSeed => {
|
||||
const k = (key: string) => t(`dedicated.home.${key}`)
|
||||
return {
|
||||
slug: 'home',
|
||||
title: k('title'),
|
||||
seo: { description: k('description') },
|
||||
layout: [
|
||||
hero(k, '#locations'),
|
||||
{
|
||||
blockType: 'advantages',
|
||||
title: k('advantages.title'),
|
||||
main: {
|
||||
title: k('advantages.mainTitle'),
|
||||
text: k('advantages.mainText'),
|
||||
items: [
|
||||
{ title: k('advantages.aiTitle'), text: k('advantages.aiText') },
|
||||
{ title: k('advantages.sameTitle'), text: k('advantages.sameText') },
|
||||
{ title: k('advantages.captchaTitle'), text: k('advantages.captchaText') },
|
||||
{ title: k('advantages.encryptedTitle'), text: k('advantages.encryptedText') },
|
||||
],
|
||||
},
|
||||
cards: [
|
||||
{
|
||||
icon: 'devices',
|
||||
title: k('advantages.devicesTitle'),
|
||||
text: k('advantages.devicesText'),
|
||||
},
|
||||
{ icon: 'ru', title: k('advantages.ruTitle'), text: k('advantages.ruText') },
|
||||
{
|
||||
icon: 'protocols',
|
||||
title: k('advantages.protocolsTitle'),
|
||||
text: k('advantages.protocolsText'),
|
||||
chips: [line(k('advantages.chipWg')), line(k('advantages.chipXray'))],
|
||||
},
|
||||
],
|
||||
routes: routes(k),
|
||||
},
|
||||
{
|
||||
blockType: 'pricing',
|
||||
points: [
|
||||
line(k('pricing.ip')),
|
||||
line(k('pricing.ru')),
|
||||
line(k('pricing.devices')),
|
||||
{
|
||||
text: k('pricing.protocols'),
|
||||
sub: [line(k('advantages.chipWg')), line(k('advantages.chipXray'))],
|
||||
},
|
||||
],
|
||||
listTitle: k('pricing.listTitle'),
|
||||
buyLabel: k('pricing.buy'),
|
||||
lead: { buttonLabel: k('pricing.lead'), title: k('pricing.lead') },
|
||||
},
|
||||
{ blockType: 'faq', title: k('faqTitle'), groups: [{ placement: 'home' }] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const massHome = (): PageSeed => {
|
||||
const k = (key: string) => t(`mass.home.${key}`)
|
||||
return {
|
||||
slug: 'home',
|
||||
title: k('title'),
|
||||
seo: { description: k('description') },
|
||||
layout: [
|
||||
hero(k, '/locations'),
|
||||
{
|
||||
blockType: 'advantages',
|
||||
title: k('advantages.title'),
|
||||
main: {
|
||||
title: k('advantages.mainTitle'),
|
||||
text: k('advantages.mainText'),
|
||||
items: [
|
||||
{ title: k('advantages.countriesTitle'), text: k('advantages.countriesText') },
|
||||
{ title: k('advantages.devicesTitle'), text: k('advantages.devicesText') },
|
||||
{ title: k('advantages.ruSitesTitle'), text: k('advantages.ruSitesText') },
|
||||
{ title: k('advantages.encryptedTitle'), text: k('advantages.encryptedText') },
|
||||
],
|
||||
},
|
||||
cards: [
|
||||
{
|
||||
icon: 'devices',
|
||||
title: k('advantages.devicesCardTitle'),
|
||||
text: k('advantages.devicesCardText'),
|
||||
},
|
||||
{ icon: 'ru', title: k('advantages.ruTitle'), text: k('advantages.ruText') },
|
||||
{
|
||||
icon: 'protocols',
|
||||
title: k('advantages.protocolsTitle'),
|
||||
text: k('advantages.protocolsText'),
|
||||
chips: [
|
||||
line(t('chips.vless')),
|
||||
line(t('chips.hysteria2')),
|
||||
line(t('chips.xhttp')),
|
||||
line(t('chips.wireguard')),
|
||||
],
|
||||
},
|
||||
],
|
||||
routes: routes(k),
|
||||
},
|
||||
{
|
||||
blockType: 'pricing',
|
||||
title: k('pricing.title'),
|
||||
text: k('pricing.text'),
|
||||
points: [
|
||||
line(k('pricing.countries')),
|
||||
line(k('pricing.devices')),
|
||||
line(k('pricing.protocols')),
|
||||
],
|
||||
listTitle: k('pricing.listTitle'),
|
||||
buyLabel: k('pricing.buy'),
|
||||
note: k('pricing.note'),
|
||||
},
|
||||
{
|
||||
blockType: 'crossLink',
|
||||
title: k('crossLink.title'),
|
||||
text: k('crossLink.text'),
|
||||
buttonLabel: k('crossLink.button'),
|
||||
target: 'home',
|
||||
},
|
||||
{ blockType: 'faq', title: k('faqTitle'), groups: [{ placement: 'home' }] },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const dedicatedPricing = (): PageSeed => {
|
||||
const k = (key: string) => t(`dedicated.pricing.${key}`)
|
||||
return {
|
||||
slug: 'pricing',
|
||||
title: k('title'),
|
||||
seo: { description: k('description') },
|
||||
layout: [
|
||||
{
|
||||
blockType: 'pricingHero',
|
||||
subline: k('subline'),
|
||||
note: k('note'),
|
||||
includedTitle: k('includedTitle'),
|
||||
included: [1, 2, 3, 4, 5].map((n) => line(k(`included${n}`))),
|
||||
},
|
||||
{
|
||||
blockType: 'locationGrid',
|
||||
title: k('gridTitle'),
|
||||
paragraphs: [line(k('gridText1')), line(k('gridText2'))],
|
||||
},
|
||||
{
|
||||
blockType: 'cards',
|
||||
cards: [
|
||||
{ kind: 'info', title: k('severalTitle'), text: k('severalText') },
|
||||
{
|
||||
kind: 'lead',
|
||||
title: k('leadTitle'),
|
||||
text: k('leadText'),
|
||||
buttonLabel: k('leadButton'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
const massPricing = (): PageSeed => {
|
||||
const k = (key: string) => t(`mass.pricing.${key}`)
|
||||
return {
|
||||
slug: 'pricing',
|
||||
title: k('title'),
|
||||
seo: { description: k('description') },
|
||||
layout: [
|
||||
{
|
||||
blockType: 'pricingHero',
|
||||
subline: k('subline'),
|
||||
note: k('note'),
|
||||
buttonLabel: k('button'),
|
||||
includedTitle: k('includedTitle'),
|
||||
included: [1, 2, 3, 4].map((n) => line(k(`included${n}`))),
|
||||
},
|
||||
{ blockType: 'locationGrid', title: k('gridTitle'), paragraphs: [line(k('gridText1'))] },
|
||||
{
|
||||
blockType: 'cards',
|
||||
cards: [
|
||||
{
|
||||
kind: 'crossLink',
|
||||
title: k('crossTitle'),
|
||||
text: k('crossText'),
|
||||
buttonLabel: k('crossButton'),
|
||||
},
|
||||
{
|
||||
kind: 'lead',
|
||||
title: k('leadTitle'),
|
||||
text: k('leadText'),
|
||||
buttonLabel: k('leadButton'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/** Локации: у брендов одна структура, различаются тексты */
|
||||
const locations = (site: SiteCode): PageSeed => {
|
||||
const k = (key: string) => t(`${site}.locations.${key}`)
|
||||
return {
|
||||
slug: 'locations',
|
||||
title: k('title'),
|
||||
seo: { description: k('description') },
|
||||
layout: [
|
||||
{
|
||||
blockType: 'mapHero',
|
||||
title: k('title'),
|
||||
text: k('mapText'),
|
||||
hint: k('hint'),
|
||||
scrollHint: k('scrollHint'),
|
||||
ruLegend: k('ruLegend'),
|
||||
},
|
||||
{ blockType: 'locationGrid', title: k('gridTitle'), paragraphs: [line(k('gridText'))] },
|
||||
{
|
||||
blockType: 'cards',
|
||||
cards: [
|
||||
{ kind: 'info', title: k('ruTitle'), text: k('ruText') },
|
||||
{
|
||||
kind: 'lead',
|
||||
title: k('leadTitle'),
|
||||
text: k('leadText'),
|
||||
buttonLabel: k('leadButton'),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/** Поддержка: контакты из настроек сайта, FAQ тремя вкладками, форма связи */
|
||||
const support = (site: SiteCode): PageSeed => {
|
||||
const k = (key: string) => t(`${site}.support.${key}`)
|
||||
return {
|
||||
slug: 'support',
|
||||
title: k('title'),
|
||||
seo: { description: k('description') },
|
||||
layout: [
|
||||
{
|
||||
blockType: 'contacts',
|
||||
title: k('title'),
|
||||
telegramText: k('telegramText'),
|
||||
emailText: k('emailText'),
|
||||
},
|
||||
{
|
||||
blockType: 'faq',
|
||||
title: k('faqTitle'),
|
||||
groups: [
|
||||
{ placement: 'support_connection', label: k('tabConnection') },
|
||||
{ placement: 'support_billing', label: k('tabBilling') },
|
||||
{ placement: 'support_locations', label: k('tabLocations') },
|
||||
],
|
||||
},
|
||||
{
|
||||
blockType: 'contactForm',
|
||||
title: k('formTitle'),
|
||||
text: k('formText'),
|
||||
topics: [
|
||||
{ value: 'connection', label: k('topicConnection') },
|
||||
{ value: 'billing', label: k('topicBilling') },
|
||||
{ value: 'locations', label: k('topicLocations') },
|
||||
{ value: 'other', label: k('topicOther') },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/** Текст юридической страницы на языке: Markdown, переведённый в Lexical (seed/legal.ts) */
|
||||
export type RenderLegal = (site: SiteCode, slug: LegalSlug, locale: Locale) => unknown
|
||||
|
||||
/** Юридические страницы: заголовок и текст одним блоком */
|
||||
const legal = (site: SiteCode, slug: LegalSlug, render: RenderLegal): PageSeed => ({
|
||||
slug,
|
||||
title: t(`legal.${slug}.title`),
|
||||
seo: { description: t(`legal.${slug}.description`) },
|
||||
layout: [{ blockType: 'richText', content: local((locale) => render(site, slug, locale)) }],
|
||||
})
|
||||
|
||||
export const buildPages = (render: RenderLegal): Record<SiteCode, PageSeed[]> => ({
|
||||
dedicated: [
|
||||
dedicatedHome(),
|
||||
dedicatedPricing(),
|
||||
locations('dedicated'),
|
||||
support('dedicated'),
|
||||
...LEGAL_SLUGS.map((slug) => legal('dedicated', slug, render)),
|
||||
],
|
||||
mass: [
|
||||
massHome(),
|
||||
massPricing(),
|
||||
locations('mass'),
|
||||
support('mass'),
|
||||
...LEGAL_SLUGS.map((slug) => legal('mass', slug, render)),
|
||||
],
|
||||
})
|
||||
@@ -8,6 +8,44 @@
|
||||
--color-*: initial;
|
||||
/* 900px: шапка сайта и кабинет переходят в мобильный вид, как в макетах */
|
||||
--breakpoint-nav: 56.25rem;
|
||||
|
||||
/* «вуаль»: размытые пятна на первом экране и в карточках (Main.dc.html) */
|
||||
--animate-veil-drift: veilDrift 16s ease-in-out infinite;
|
||||
--animate-veil-drift-2: veilDrift2 19s ease-in-out infinite;
|
||||
/* пульс точек «в наличии» на карте локаций */
|
||||
--animate-veil-pulse: veilPulse 3.2s ease-out infinite;
|
||||
|
||||
@keyframes veilDrift {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate3d(2%, -3%, 0) scale(1.06);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes veilDrift2 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0) scale(1.04);
|
||||
}
|
||||
50% {
|
||||
transform: translate3d(-3%, 2%, 0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes veilPulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.55;
|
||||
}
|
||||
70%,
|
||||
100% {
|
||||
transform: scale(2.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -98,3 +136,13 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Текущий раздел в шапке — цветом текста, как в макетах; страница помечена data-page.
|
||||
* Вне слоёв: иначе утилита text-ink-2 из слоя utilities перебила бы это правило.
|
||||
*/
|
||||
html:has([data-page='pricing']) [data-nav='pricing'],
|
||||
html:has([data-page='locations']) [data-nav='locations'],
|
||||
html:has([data-page='support']) [data-nav='support'] {
|
||||
color: var(--ink);
|
||||
}
|
||||
+17
-6
@@ -2,8 +2,11 @@
|
||||
* Токены брендов из <style> прототипов design/handoff/project.
|
||||
* Бренд — атрибут data-site на <html>, тема — data-theme (ставит скрипт в <head> до отрисовки).
|
||||
* --sel — подсветка выбранного пункта: у Вейликса это --accent-soft, у ВейликсВПН свой цвет.
|
||||
* Цвета карты (--land, --land-line, --ocean) у Вейликса взяты из Locations: в Main глобус светлее,
|
||||
* какой вариант оставить — решаем на этапе 1b.
|
||||
* Цвета карты (--land, --land-line, --ocean) — плоская карта локаций (Locations). Глобус на главной
|
||||
* (--globe-*) у Вейликса светлее, как в Main: он лежит под стеклянными пластинами. У ВейликсВПН
|
||||
* значения в макетах одинаковые.
|
||||
* --ink-3 светлее или темнее, чем в макетах (60/63/60/65%): так вторичный текст даёт контраст
|
||||
* не ниже 4,5:1 (WCAG AA) на всех фонах темы, включая выделенные карточки (решение этапа 1b).
|
||||
*/
|
||||
|
||||
[data-site='dedicated'] {
|
||||
@@ -12,7 +15,7 @@
|
||||
--surface-2: oklch(97.5% 0.01 296);
|
||||
--ink: oklch(24% 0.04 296);
|
||||
--ink-2: oklch(45% 0.03 296);
|
||||
--ink-3: oklch(60% 0.025 296);
|
||||
--ink-3: oklch(53% 0.025 296);
|
||||
--border: oklch(92% 0.014 296);
|
||||
--accent: oklch(45% 0.17 296);
|
||||
--accent-hover: oklch(38% 0.17 296);
|
||||
@@ -28,6 +31,9 @@
|
||||
--land: oklch(93% 0.015 296);
|
||||
--land-line: oklch(87% 0.02 296);
|
||||
--ocean: oklch(97.5% 0.007 296);
|
||||
--globe-land: oklch(95% 0.012 296);
|
||||
--globe-line: oklch(90% 0.018 296);
|
||||
--globe-ocean: var(--ocean);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 296 / 0.05), 0 12px 32px -12px oklch(24% 0.04 296 / 0.14);
|
||||
}
|
||||
|
||||
@@ -37,7 +43,7 @@
|
||||
--surface-2: oklch(24% 0.03 296);
|
||||
--ink: oklch(96% 0.01 296);
|
||||
--ink-2: oklch(79% 0.02 296);
|
||||
--ink-3: oklch(63% 0.025 296);
|
||||
--ink-3: oklch(66% 0.025 296);
|
||||
--border: oklch(31% 0.032 296);
|
||||
--accent: oklch(74% 0.15 296);
|
||||
--accent-hover: oklch(80% 0.13 296);
|
||||
@@ -52,6 +58,8 @@
|
||||
--land: oklch(27% 0.03 296);
|
||||
--land-line: oklch(34% 0.035 296);
|
||||
--ocean: oklch(24% 0.025 296);
|
||||
--globe-land: oklch(27% 0.028 296);
|
||||
--globe-line: oklch(32% 0.032 296);
|
||||
--shadow: 0 1px 2px oklch(10% 0.02 296 / 0.4), 0 16px 40px -16px oklch(8% 0.02 296 / 0.7);
|
||||
}
|
||||
|
||||
@@ -61,7 +69,7 @@
|
||||
--surface-2: oklch(98.4% 0.003 215);
|
||||
--ink: oklch(24% 0.04 204);
|
||||
--ink-2: oklch(45% 0.03 204);
|
||||
--ink-3: oklch(60% 0.025 204);
|
||||
--ink-3: oklch(53% 0.025 204);
|
||||
--border: oklch(92% 0.014 200);
|
||||
--accent: oklch(48% 0.105 178);
|
||||
--accent-hover: oklch(41% 0.1 178);
|
||||
@@ -77,6 +85,9 @@
|
||||
--land: oklch(95% 0.012 196);
|
||||
--land-line: oklch(90% 0.018 196);
|
||||
--ocean: oklch(97.5% 0.007 196);
|
||||
--globe-land: var(--land);
|
||||
--globe-line: var(--land-line);
|
||||
--globe-ocean: var(--ocean);
|
||||
--shadow: 0 1px 2px oklch(24% 0.04 204 / 0.05), 0 12px 32px -12px oklch(24% 0.04 204 / 0.14);
|
||||
}
|
||||
|
||||
@@ -86,7 +97,7 @@
|
||||
--surface-2: oklch(30% 0.03 210);
|
||||
--ink: oklch(96% 0.012 196);
|
||||
--ink-2: oklch(80% 0.02 196);
|
||||
--ink-3: oklch(65% 0.025 196);
|
||||
--ink-3: oklch(69% 0.025 196);
|
||||
--border: oklch(36% 0.04 204);
|
||||
--accent: oklch(76% 0.105 190);
|
||||
--accent-hover: oklch(83% 0.09 190);
|
||||
|
||||
+11
-4
@@ -1,18 +1,25 @@
|
||||
import { Commissioner, Unbounded } from 'next/font/google'
|
||||
|
||||
/** Весь текст. Вариативный шрифт: один файл на набор символов вместо файла на каждое начертание */
|
||||
/**
|
||||
* Весь текст. Вариативный шрифт: один файл на набор символов вместо файла на каждое начертание.
|
||||
* `subsets` — только то, что предзагружается: латиница и кириллица есть на каждой странице.
|
||||
* Остальные наборы (latin-ext с «₽») браузер скачает сам, когда встретит их символы.
|
||||
*/
|
||||
export const commissioner = Commissioner({
|
||||
subsets: ['latin', 'latin-ext', 'cyrillic'],
|
||||
subsets: ['latin', 'cyrillic'],
|
||||
variable: '--font-commissioner',
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
/** Только надпись логотипа */
|
||||
/**
|
||||
* Только надпись логотипа: для первого экрана не критична, поэтому без предзагрузки —
|
||||
* браузер скачает один нужный набор символов вместо трёх.
|
||||
*/
|
||||
export const unbounded = Unbounded({
|
||||
subsets: ['latin', 'latin-ext', 'cyrillic'],
|
||||
weight: '600',
|
||||
variable: '--font-unbounded',
|
||||
display: 'swap',
|
||||
preload: false,
|
||||
})
|
||||
|
||||
export const fontVariables = `${commissioner.variable} ${unbounded.variable}`
|
||||
@@ -0,0 +1,198 @@
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
/**
|
||||
* Флаги стран, нарисованные фигурами, как в макетах (flagSpecs в Main.dc.html): полосы
|
||||
* по строкам или столбцам и детали поверх в пикселях рамки 46×32.
|
||||
*/
|
||||
type Mark = { l: number; t: number; w: number; h: number; bg: string; r?: string; clip?: string }
|
||||
type Spec = {
|
||||
rows?: string[]
|
||||
cols?: string[]
|
||||
sizes?: string
|
||||
bg?: string
|
||||
marks?: Mark[]
|
||||
}
|
||||
|
||||
const SPECS: Record<string, Spec> = {
|
||||
NL: { rows: ['#AE1C28', '#FFFFFF', '#21468B'] },
|
||||
DE: { rows: ['#000000', '#DD0000', '#FFCE00'] },
|
||||
US: {
|
||||
rows: ['#B22234', '#FFFFFF', '#B22234', '#FFFFFF', '#B22234', '#FFFFFF'],
|
||||
marks: [{ l: 0, t: 0, w: 22, h: 17, bg: '#3C3B6E' }],
|
||||
},
|
||||
GB: {
|
||||
bg: '#012169',
|
||||
marks: [
|
||||
{ l: 17, t: 0, w: 12, h: 32, bg: '#FFFFFF' },
|
||||
{ l: 0, t: 10, w: 46, h: 12, bg: '#FFFFFF' },
|
||||
{ l: 20, t: 0, w: 6, h: 32, bg: '#C8102E' },
|
||||
{ l: 0, t: 13, w: 46, h: 6, bg: '#C8102E' },
|
||||
],
|
||||
},
|
||||
FR: { cols: ['#002395', '#FFFFFF', '#ED2939'] },
|
||||
FI: {
|
||||
bg: '#FFFFFF',
|
||||
marks: [
|
||||
{ l: 13, t: 0, w: 8, h: 32, bg: '#003580' },
|
||||
{ l: 0, t: 12, w: 46, h: 8, bg: '#003580' },
|
||||
],
|
||||
},
|
||||
SE: {
|
||||
bg: '#006AA7',
|
||||
marks: [
|
||||
{ l: 13, t: 0, w: 6, h: 32, bg: '#FECC00' },
|
||||
{ l: 0, t: 13, w: 46, h: 6, bg: '#FECC00' },
|
||||
],
|
||||
},
|
||||
NO: {
|
||||
bg: '#BA0C2F',
|
||||
marks: [
|
||||
{ l: 12, t: 0, w: 10, h: 32, bg: '#FFFFFF' },
|
||||
{ l: 0, t: 11, w: 46, h: 10, bg: '#FFFFFF' },
|
||||
{ l: 15, t: 0, w: 4, h: 32, bg: '#00205B' },
|
||||
{ l: 0, t: 14, w: 46, h: 4, bg: '#00205B' },
|
||||
],
|
||||
},
|
||||
DK: {
|
||||
bg: '#C8102E',
|
||||
marks: [
|
||||
{ l: 13, t: 0, w: 6, h: 32, bg: '#FFFFFF' },
|
||||
{ l: 0, t: 13, w: 46, h: 6, bg: '#FFFFFF' },
|
||||
],
|
||||
},
|
||||
CH: {
|
||||
bg: '#D52B1E',
|
||||
marks: [
|
||||
{ l: 20, t: 8, w: 6, h: 16, bg: '#FFFFFF' },
|
||||
{ l: 15, t: 13, w: 16, h: 6, bg: '#FFFFFF' },
|
||||
],
|
||||
},
|
||||
AT: { rows: ['#ED2939', '#FFFFFF', '#ED2939'] },
|
||||
ES: { rows: ['#AA151B', '#F1BF00', '#AA151B'], sizes: '1fr 2fr 1fr' },
|
||||
IT: { cols: ['#008C45', '#F4F5F0', '#CD212A'] },
|
||||
PL: { rows: ['#FFFFFF', '#DC143C'] },
|
||||
CZ: {
|
||||
rows: ['#FFFFFF', '#D7141A'],
|
||||
marks: [{ l: 0, t: 0, w: 18, h: 32, bg: '#11457E', clip: 'polygon(0 0, 100% 50%, 0 100%)' }],
|
||||
},
|
||||
RO: { cols: ['#002B7F', '#FCD116', '#CE1126'] },
|
||||
LT: { rows: ['#FDB913', '#006A44', '#C1272D'] },
|
||||
LV: { rows: ['#9E3039', '#FFFFFF', '#9E3039'], sizes: '2fr 1fr 2fr' },
|
||||
EE: { rows: ['#0072CE', '#000000', '#FFFFFF'] },
|
||||
TR: {
|
||||
bg: '#E30A17',
|
||||
marks: [
|
||||
{ l: 11, t: 8, w: 16, h: 16, r: '50%', bg: '#FFFFFF' },
|
||||
{ l: 16, t: 8, w: 16, h: 16, r: '50%', bg: '#E30A17' },
|
||||
],
|
||||
},
|
||||
KZ: { bg: '#00AFCA', marks: [{ l: 16, t: 8, w: 15, h: 15, r: '50%', bg: '#FEC50C' }] },
|
||||
AM: { rows: ['#D90012', '#0033A0', '#F2A800'] },
|
||||
GE: {
|
||||
bg: '#FFFFFF',
|
||||
marks: [
|
||||
{ l: 20, t: 0, w: 6, h: 32, bg: '#FF0000' },
|
||||
{ l: 0, t: 13, w: 46, h: 6, bg: '#FF0000' },
|
||||
],
|
||||
},
|
||||
AE: {
|
||||
rows: ['#00732F', '#FFFFFF', '#000000'],
|
||||
marks: [{ l: 0, t: 0, w: 11, h: 32, bg: '#FF0000' }],
|
||||
},
|
||||
SG: {
|
||||
rows: ['#EF3340', '#FFFFFF'],
|
||||
marks: [
|
||||
{ l: 5, t: 3, w: 11, h: 11, r: '50%', bg: '#FFFFFF' },
|
||||
{ l: 9, t: 3, w: 11, h: 11, r: '50%', bg: '#EF3340' },
|
||||
],
|
||||
},
|
||||
JP: { bg: '#FFFFFF', marks: [{ l: 16, t: 9, w: 14, h: 14, r: '50%', bg: '#BC002D' }] },
|
||||
KR: {
|
||||
bg: '#FFFFFF',
|
||||
marks: [
|
||||
{ l: 16, t: 9, w: 14, h: 14, r: '50%', bg: '#CD2E3A' },
|
||||
{
|
||||
l: 16,
|
||||
t: 9,
|
||||
w: 14,
|
||||
h: 14,
|
||||
r: '50%',
|
||||
bg: '#0047A0',
|
||||
clip: 'polygon(0 50%, 100% 50%, 100% 100%, 0 100%)',
|
||||
},
|
||||
],
|
||||
},
|
||||
CA: {
|
||||
cols: ['#D80621', '#FFFFFF', '#D80621'],
|
||||
sizes: '1fr 2fr 1fr',
|
||||
marks: [{ l: 20, t: 9, w: 7, h: 13, bg: '#D80621', clip: 'polygon(50% 0, 100% 100%, 0 100%)' }],
|
||||
},
|
||||
AU: {
|
||||
bg: '#00247D',
|
||||
marks: [
|
||||
{ l: 9, t: 0, w: 4, h: 16, bg: '#FFFFFF' },
|
||||
{ l: 0, t: 6, w: 22, h: 4, bg: '#FFFFFF' },
|
||||
{ l: 30, t: 19, w: 7, h: 7, r: '50%', bg: '#FFFFFF' },
|
||||
],
|
||||
},
|
||||
BR: {
|
||||
bg: '#009739',
|
||||
marks: [
|
||||
{
|
||||
l: 9,
|
||||
t: 4,
|
||||
w: 28,
|
||||
h: 24,
|
||||
bg: '#FEDD00',
|
||||
clip: 'polygon(50% 0, 100% 50%, 50% 100%, 0 50%)',
|
||||
},
|
||||
{ l: 18, t: 11, w: 10, h: 10, r: '50%', bg: '#012169' },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
/** Флаг 46×32. Страна без описания — серая плашка с кодом: так её видно, пока нет рисунка */
|
||||
export function Flag({ code }: { code: string }) {
|
||||
const spec = SPECS[code]
|
||||
const bands = spec?.rows ?? spec?.cols ?? []
|
||||
const style: CSSProperties = spec?.rows
|
||||
? { gridTemplateRows: spec.sizes ?? `repeat(${bands.length}, 1fr)` }
|
||||
: spec?.cols
|
||||
? {
|
||||
gridAutoFlow: 'column',
|
||||
gridTemplateColumns: spec.sizes ?? `repeat(${bands.length}, 1fr)`,
|
||||
}
|
||||
: { background: spec?.bg ?? 'var(--surface-2)' }
|
||||
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className="relative grid h-8 w-[46px] flex-none overflow-hidden rounded-md border border-border"
|
||||
style={style}
|
||||
>
|
||||
{bands.map((color, index) => (
|
||||
<span key={index} className="block" style={{ background: color }} />
|
||||
))}
|
||||
{spec?.marks?.map((mark, index) => (
|
||||
<span
|
||||
key={`m${index}`}
|
||||
className="absolute block"
|
||||
style={{
|
||||
left: mark.l,
|
||||
top: mark.t,
|
||||
width: mark.w,
|
||||
height: mark.h,
|
||||
borderRadius: mark.r ?? 0,
|
||||
background: mark.bg,
|
||||
clipPath: mark.clip,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!spec && (
|
||||
<span className="absolute inset-0 grid place-items-center text-[11px] font-medium text-ink-3">
|
||||
{code}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* «Получить VPN» (07-frontend, раздел 7.6): на странице с блоком цен (#prices) — к нему,
|
||||
* на остальных — на /pricing. Без JavaScript ссылка всегда ведёт на /pricing.
|
||||
*/
|
||||
export function GetVpnLink({
|
||||
href,
|
||||
className,
|
||||
onNavigate,
|
||||
children,
|
||||
}: {
|
||||
href: string
|
||||
className: string
|
||||
onNavigate?: () => void
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className={className}
|
||||
onClick={(event) => {
|
||||
const prices = document.getElementById('prices')
|
||||
if (prices) {
|
||||
event.preventDefault()
|
||||
prices.scrollIntoView()
|
||||
history.replaceState(null, '', '#prices')
|
||||
}
|
||||
onNavigate?.()
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { LogoMark, LogoText } from '@/ui/Logo'
|
||||
import { CurrencyMenu } from '@/ui/switchers/CurrencySwitch'
|
||||
import { LanguageMenu } from '@/ui/switchers/LanguageSwitch'
|
||||
|
||||
import { GetVpnLink } from './GetVpnLink'
|
||||
import { MobileMenu } from './MobileMenu'
|
||||
import type { MarketingShell } from './types'
|
||||
|
||||
@@ -16,11 +17,11 @@ export async function Header({ shell }: { shell: MarketingShell }) {
|
||||
const brand = await getTranslations('brand')
|
||||
const href = (path: string) => localizedPath(path, locale, defaultLocale)
|
||||
const links = [
|
||||
{ href: href('/pricing'), label: t('pricing') },
|
||||
{ href: href('/locations'), label: t('locations') },
|
||||
{ href: href('/support'), label: t('support') },
|
||||
{ href: href('/pricing'), label: t('pricing'), page: 'pricing' },
|
||||
{ href: href('/locations'), label: t('locations'), page: 'locations' },
|
||||
{ href: href('/support'), label: t('support'), page: 'support' },
|
||||
]
|
||||
// кнопка «Получить VPN» ведёт к ценам; на главной — к блоку цен (появится на этапе 1b)
|
||||
// «Получить VPN» ведёт на /pricing, а на странице с блоком цен — к нему (GetVpnLink)
|
||||
const getVpn = href('/pricing')
|
||||
|
||||
return (
|
||||
@@ -39,7 +40,13 @@ export async function Header({ shell }: { shell: MarketingShell }) {
|
||||
className="hidden flex-wrap gap-x-7 gap-y-2.5 text-[15px] font-medium nav:flex"
|
||||
>
|
||||
{links.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="text-ink-2 hover:text-ink">
|
||||
// текущий раздел выделяет CSS по data-page страницы (globals.css)
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
data-nav={link.page}
|
||||
className="text-ink-2 hover:text-ink"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
@@ -62,12 +69,12 @@ export async function Header({ shell }: { shell: MarketingShell }) {
|
||||
{t('logIn')}
|
||||
</a>
|
||||
)}
|
||||
<Link
|
||||
<GetVpnLink
|
||||
href={getVpn}
|
||||
className="hidden h-[38px] min-w-[142px] flex-none items-center justify-center rounded-[10px] bg-accent px-3 text-[15px] font-medium text-on-accent shadow-[0_6px_18px_-8px_var(--accent)] hover:bg-accent-hover hover:text-on-accent nav:flex"
|
||||
>
|
||||
{t('getVpn')}
|
||||
</Link>
|
||||
</GetVpnLink>
|
||||
<MobileMenu shell={shell} links={links} getVpn={getVpn} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,11 +8,12 @@ import { BurgerIcon } from '@/ui/icons'
|
||||
import { CurrencySegments } from '@/ui/switchers/CurrencySwitch'
|
||||
import { LanguageSegments } from '@/ui/switchers/LanguageSwitch'
|
||||
|
||||
import { GetVpnLink } from './GetVpnLink'
|
||||
import type { MarketingShell } from './types'
|
||||
|
||||
type Props = {
|
||||
shell: MarketingShell
|
||||
links: { href: string; label: string }[]
|
||||
links: { href: string; label: string; page: string }[]
|
||||
getVpn: string
|
||||
}
|
||||
|
||||
@@ -89,13 +90,13 @@ export function MobileMenu({ shell, links, getVpn }: Props) {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Link
|
||||
<GetVpnLink
|
||||
href={getVpn}
|
||||
onClick={close}
|
||||
onNavigate={close}
|
||||
className="mt-3 flex h-[50px] items-center justify-center rounded-xl bg-accent text-base font-medium text-on-accent hover:bg-accent-hover hover:text-on-accent"
|
||||
>
|
||||
{t('nav.getVpn')}
|
||||
</Link>
|
||||
</GetVpnLink>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { LocationStatus } from '@/core/content/types'
|
||||
|
||||
/** Точка статуса локации: залитая — работает сейчас, кольцо — «24 часа» или «Скоро» */
|
||||
export function StatusDot({ status }: { status: LocationStatus }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={
|
||||
status === 'available'
|
||||
? 'block size-1.5 flex-none rounded-full bg-accent'
|
||||
: 'block size-1.5 flex-none rounded-full border-[1.5px] border-ink-3'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { AdvantagesBlock } from '@/payload-types'
|
||||
|
||||
import { type BlockContext, fill } from './context'
|
||||
|
||||
type Card = NonNullable<AdvantagesBlock['cards']>[number]
|
||||
|
||||
const CARD = 'rounded-[22px] border border-border bg-surface shadow-card'
|
||||
|
||||
/** «Что это даёт на практике» (Main, MassMain): большая карточка, малые и маршруты */
|
||||
export function Advantages({ block, context }: { block: AdvantagesBlock; context: BlockContext }) {
|
||||
const routes = block.routes
|
||||
return (
|
||||
<section id="advantages" className="pt-4 pb-2">
|
||||
{block.title && (
|
||||
<h2 className="mb-7 text-[clamp(28px,3vw,38px)] font-semibold tracking-[-0.03em]">
|
||||
{fill(context, block.title)}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(300px,100%),1fr))] gap-4">
|
||||
<div className={`relative isolate p-[clamp(20px,5vw,32px)] ${CARD}`}>
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[22px] [mask-image:radial-gradient(white,black)]">
|
||||
<div className="absolute -top-20 -right-20 size-[260px] rounded-full bg-glow-1 opacity-50 blur-[60px]" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="mb-[18px] flex items-center gap-3">
|
||||
<IpIcon />
|
||||
<h3 className="text-[24px] font-semibold tracking-[-0.025em]">
|
||||
{fill(context, block.main?.title)}
|
||||
</h3>
|
||||
</div>
|
||||
{block.main?.text && (
|
||||
<p className="mb-[26px] max-w-[34em] text-[16px] leading-[1.55] font-light text-ink-2">
|
||||
{fill(context, block.main.text)}
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(200px,100%),1fr))] gap-x-7 gap-y-5">
|
||||
{block.main?.items?.map((item) => (
|
||||
<div key={item.id}>
|
||||
<div className="mb-1.5 text-[15px] font-medium">{fill(context, item.title)}</div>
|
||||
<div className="text-[14px] leading-[1.5] font-light text-ink-2">
|
||||
{fill(context, item.text)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid content-start gap-4">
|
||||
{block.cards?.map((card) => (
|
||||
<SmallCard key={card.id} card={card} context={context} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{routes?.title && (
|
||||
<div className={`mt-4 p-[clamp(20px,5vw,32px)] ${CARD}`}>
|
||||
<h3 className="mb-1.5 text-[24px] font-semibold tracking-[-0.025em]">{routes.title}</h3>
|
||||
{routes.text && (
|
||||
<p className="mb-7 max-w-[46em] text-[16px] leading-[1.55] font-light text-ink-2">
|
||||
{routes.text}
|
||||
</p>
|
||||
)}
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(250px,100%),1fr))] gap-4">
|
||||
{routes.schemes?.map((scheme) => (
|
||||
<div
|
||||
key={scheme.id}
|
||||
className="rounded-2xl border border-border bg-surface-2 p-[22px]"
|
||||
>
|
||||
<div className="mb-4 text-[13px] text-ink-3">{scheme.title}</div>
|
||||
<div className="grid gap-3">
|
||||
{scheme.rows?.map((row) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className="flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[14px]"
|
||||
>
|
||||
<span className="w-[72px] flex-none font-medium text-ink-2">{row.label}</span>
|
||||
<span
|
||||
className={`block h-px flex-1 ${
|
||||
row.mode === 'tunnel'
|
||||
? 'bg-[repeating-linear-gradient(90deg,var(--accent)_0_6px,transparent_6px_12px)]'
|
||||
: 'bg-[linear-gradient(90deg,var(--accent),color-mix(in_oklab,var(--accent)_25%,transparent))]'
|
||||
}`}
|
||||
/>
|
||||
<span className="flex-none text-ink-2">{row.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{scheme.note && <div className="mt-4 text-[13px] text-ink-3">{scheme.note}</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function SmallCard({ card, context }: { card: Card; context: BlockContext }) {
|
||||
const chips = card.chips ?? []
|
||||
return (
|
||||
<div className={`p-[clamp(20px,4.6vw,26px)] ${CARD}`}>
|
||||
<div className="mb-4">
|
||||
<CardIcon icon={card.icon} />
|
||||
</div>
|
||||
<h3 className="mb-2 text-[19px] font-semibold tracking-[-0.02em]">
|
||||
{fill(context, card.title)}
|
||||
</h3>
|
||||
{card.text && (
|
||||
<p
|
||||
className={`text-[14px] leading-[1.5] font-light text-ink-2 ${chips.length > 0 ? 'mb-3' : ''}`}
|
||||
>
|
||||
{fill(context, card.text)}
|
||||
</p>
|
||||
)}
|
||||
{chips.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 text-[12px] text-ink-3 tabular-nums">
|
||||
{chips.map((chip) => (
|
||||
<span
|
||||
key={chip.id}
|
||||
className="rounded-[7px] border border-border bg-surface-2 px-[9px] py-1"
|
||||
>
|
||||
{chip.text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Значки нарисованы фигурами, как в макете: без иконок замков и щитов */
|
||||
function IpIcon() {
|
||||
return (
|
||||
<span aria-hidden className="relative block size-[34px] flex-none">
|
||||
<span className="absolute top-[3px] left-[11px] block size-3 rounded-full bg-accent" />
|
||||
<span className="absolute top-[19px] left-1 block h-3.5 w-[26px] rounded-[13px_13px_4px_4px] bg-accent opacity-35" />
|
||||
<span className="absolute top-[18px] left-[23px] block size-[11px] rounded-full border-2 border-accent bg-surface" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const BAR_MIX = [100, 75, 55, 35, 18]
|
||||
|
||||
function CardIcon({ icon }: { icon: Card['icon'] }) {
|
||||
if (icon === 'devices') {
|
||||
return (
|
||||
<span aria-hidden className="flex gap-1">
|
||||
{BAR_MIX.map((mix) => (
|
||||
<span
|
||||
key={mix}
|
||||
className="block h-[22px] w-[11px] rounded-[3px]"
|
||||
style={{ background: `color-mix(in oklab, var(--accent) ${mix}%, transparent)` }}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (icon === 'ru') {
|
||||
return (
|
||||
<span aria-hidden className="flex items-center gap-2">
|
||||
<span className="block size-[22px] rounded-full bg-accent" />
|
||||
<span className="block h-0.5 w-[26px] bg-[color-mix(in_oklab,var(--accent)_40%,transparent)]" />
|
||||
<span className="block size-[22px] rounded-full border-2 border-[color-mix(in_oklab,var(--accent)_55%,transparent)]" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span aria-hidden className="flex items-center gap-1.5">
|
||||
<span className="block size-[22px] rotate-45 rounded-[7px] bg-accent" />
|
||||
<span className="block size-[22px] rotate-45 rounded-[7px] border border-[color-mix(in_oklab,var(--accent)_40%,transparent)] bg-veil" />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { CardsBlock } from '@/payload-types'
|
||||
|
||||
import { type BlockContext, fill } from './context'
|
||||
import { LeadCardForm } from './LeadCard'
|
||||
|
||||
type Card = NonNullable<CardsBlock['cards']>[number]
|
||||
|
||||
const CARD = 'rounded-[22px] border border-border bg-surface shadow-card'
|
||||
const TITLE = 'mb-2.5 text-[22px] font-semibold tracking-[-0.025em]'
|
||||
const TEXT = 'text-[15px] leading-[1.55] font-light text-ink-2'
|
||||
|
||||
/** Две карточки внизу страниц цен и локаций: текст, ссылка на другой бренд, заявка */
|
||||
export function Cards({ block, context }: { block: CardsBlock; context: BlockContext }) {
|
||||
// на странице локаций отступы плотнее и у заявки нет свечения (Locations.dc.html)
|
||||
return (
|
||||
<section className={context.page === 'locations' ? 'pt-14' : 'pt-[72px]'}>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(300px,100%),1fr))] gap-4">
|
||||
{block.cards?.map((card) => (
|
||||
<CardView key={card.id} card={card} context={context} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function CardView({ card, context }: { card: Card; context: BlockContext }) {
|
||||
const compact = context.page === 'locations'
|
||||
const padding = compact ? 'p-[clamp(20px,5vw,32px)]' : 'p-[clamp(16px,4vw,32px)]'
|
||||
if (card.kind === 'lead') {
|
||||
return (
|
||||
<div className={`relative isolate ${padding} ${CARD}`}>
|
||||
{!compact && (
|
||||
<div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[22px] [mask-image:radial-gradient(white,black)]">
|
||||
<div
|
||||
className={`absolute -right-[70px] -bottom-[90px] size-[260px] rounded-full opacity-50 blur-[60px] ${
|
||||
context.site === 'mass' ? 'bg-glow-2' : 'bg-glow-1'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<h3 id="lead" className={`scroll-mt-[90px] ${TITLE}`}>
|
||||
{card.title}
|
||||
</h3>
|
||||
{card.text && (
|
||||
<p className={`${compact ? 'mb-[18px]' : 'mb-5'} ${TEXT}`}>
|
||||
{fill(context, card.text)}
|
||||
</p>
|
||||
)}
|
||||
<LeadCardForm
|
||||
buttonLabel={card.buttonLabel ?? card.title}
|
||||
privacyHref={context.href('/legal/privacy')}
|
||||
compact={compact}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const href = card.kind === 'crossLink' ? context.otherBrand.home : null
|
||||
return (
|
||||
<div className={`${padding} ${CARD}`}>
|
||||
<h3 className={TITLE}>{card.title}</h3>
|
||||
{card.text && <p className={`${href ? 'mb-5' : ''} ${TEXT}`}>{fill(context, card.text)}</p>}
|
||||
{href && card.buttonLabel && (
|
||||
<a
|
||||
href={href}
|
||||
className="inline-flex h-12 items-center rounded-xl border border-accent px-[22px] text-[15px] font-medium text-accent hover:bg-accent-soft hover:text-accent"
|
||||
>
|
||||
{card.buttonLabel}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ContactFormBlock } from '@/payload-types'
|
||||
|
||||
import type { BlockContext } from './context'
|
||||
import { ContactFormFields } from './ContactFormFields'
|
||||
|
||||
/** «Написать нам» (Support, #form): слева заголовок и пояснение, справа форма */
|
||||
export function ContactForm({
|
||||
block,
|
||||
context,
|
||||
}: {
|
||||
block: ContactFormBlock
|
||||
context: BlockContext
|
||||
}) {
|
||||
return (
|
||||
<section id="form" className="scroll-mt-[90px] pt-16">
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(280px,100%),1fr))] gap-10 rounded-3xl border border-border bg-surface p-[clamp(20px,5vw,40px)] shadow-card">
|
||||
<div>
|
||||
<h2 className="mb-2.5 text-[clamp(24px,2.6vw,30px)] font-semibold tracking-[-0.03em]">
|
||||
{block.title}
|
||||
</h2>
|
||||
{block.text && (
|
||||
<p className="max-w-[26em] text-[15px] leading-[1.55] font-light text-ink-2">
|
||||
{block.text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ContactFormFields
|
||||
topics={(block.topics ?? []).map((topic) => ({ value: topic.value, label: topic.label }))}
|
||||
privacyHref={context.href('/legal/privacy')}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { type FormEvent, useState } from 'react'
|
||||
|
||||
const FIELD =
|
||||
'w-full rounded-xl border border-border bg-bg text-ink placeholder:text-ink-3 disabled:opacity-60'
|
||||
|
||||
/**
|
||||
* Поля формы «Написать нам». Пока только интерфейс: проверка и состояние «отправлено»,
|
||||
* без отправки — обращения уйдут в Chatwoot на этапе 3e.
|
||||
*/
|
||||
export function ContactFormFields({
|
||||
topics,
|
||||
privacyHref,
|
||||
}: {
|
||||
topics: { value: string; label: string }[]
|
||||
privacyHref: string
|
||||
}) {
|
||||
const t = useTranslations('contact')
|
||||
const lead = useTranslations('lead')
|
||||
const [sent, setSent] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const submit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const data = new FormData(event.currentTarget)
|
||||
const email = String(data.get('email') ?? '').trim()
|
||||
const message = String(data.get('message') ?? '').trim()
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return setError(t('needEmail'))
|
||||
if (!message) return setError(t('needMessage'))
|
||||
setError(null)
|
||||
setSent(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<form noValidate onSubmit={submit} className="grid gap-2.5">
|
||||
<label className="block">
|
||||
<span className="sr-only">{t('email')}</span>
|
||||
<input
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder={t('email')}
|
||||
disabled={sent}
|
||||
className={`h-12 px-3.5 ${FIELD}`}
|
||||
/>
|
||||
</label>
|
||||
{topics.length > 0 && (
|
||||
<label className="block">
|
||||
<span className="sr-only">{t('topic')}</span>
|
||||
<select name="topic" disabled={sent} className={`h-12 px-3 ${FIELD}`}>
|
||||
{topics.map((topic) => (
|
||||
<option key={topic.value} value={topic.value}>
|
||||
{topic.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<label className="block">
|
||||
<span className="sr-only">{t('message')}</span>
|
||||
<textarea
|
||||
name="message"
|
||||
rows={5}
|
||||
placeholder={t('message')}
|
||||
disabled={sent}
|
||||
className={`resize-y px-3.5 py-3 ${FIELD}`}
|
||||
/>
|
||||
</label>
|
||||
{error && (
|
||||
<p role="alert" className="flex items-center gap-2 text-[13px] text-ink-2">
|
||||
<span aria-hidden className="block size-1.5 flex-none rounded-full bg-warn" />
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={sent}
|
||||
className="inline-flex h-[50px] items-center justify-center rounded-xl bg-accent text-[16px] font-medium text-on-accent hover:bg-accent-hover disabled:cursor-default disabled:hover:bg-accent"
|
||||
>
|
||||
{sent ? t('sent') : t('send')}
|
||||
</button>
|
||||
<p className="text-[12px] leading-[1.5] text-ink-3">
|
||||
{lead.rich('consent', {
|
||||
link: (chunks) => (
|
||||
<a href={privacyHref} className="underline underline-offset-2">
|
||||
{chunks}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getTranslations } from 'next-intl/server'
|
||||
|
||||
import type { ContactsBlock } from '@/payload-types'
|
||||
|
||||
import type { BlockContext } from './context'
|
||||
|
||||
const CARD =
|
||||
'grid content-start gap-2.5 rounded-[22px] border border-border bg-surface p-[clamp(20px,4.8vw,28px)] text-ink shadow-card hover:border-accent hover:text-ink'
|
||||
|
||||
/**
|
||||
* Верх страницы поддержки (Support): H1, карточки Telegram и почты, часы работы.
|
||||
* Контакты — из настроек сайта; пустое поле — карточки нет.
|
||||
*/
|
||||
export async function Contacts({
|
||||
block,
|
||||
context,
|
||||
}: {
|
||||
block: ContactsBlock
|
||||
context: BlockContext
|
||||
}) {
|
||||
const t = await getTranslations({ locale: context.locale, namespace: 'support' })
|
||||
const { supportBot, supportEmail, supportHours } = context.settings
|
||||
return (
|
||||
<section className="pt-14 pb-2">
|
||||
<h1 className="mb-[46px] text-[clamp(34px,4.4vw,52px)] font-semibold tracking-[-0.035em]">
|
||||
{block.title}
|
||||
</h1>
|
||||
<div className="grid grid-cols-[repeat(auto-fit,minmax(min(260px,100%),1fr))] gap-4">
|
||||
{supportBot && (
|
||||
<a href={`https://t.me/${supportBot}`} className={CARD}>
|
||||
<span className="text-[13px] text-ink-3">{t('telegram')}</span>
|
||||
<span className="text-[clamp(24px,3vw,32px)] font-semibold tracking-[-0.03em] break-words text-accent">
|
||||
@{supportBot}
|
||||
</span>
|
||||
{block.telegramText && (
|
||||
<span className="text-[14px] font-light text-ink-2">{block.telegramText}</span>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
{supportEmail && (
|
||||
<a href={`mailto:${supportEmail}`} className={CARD}>
|
||||
<span className="text-[13px] text-ink-3">{t('email')}</span>
|
||||
<span className="text-[clamp(22px,2.6vw,30px)] font-semibold tracking-[-0.03em] break-words text-accent">
|
||||
{supportEmail}
|
||||
</span>
|
||||
{block.emailText && (
|
||||
<span className="text-[14px] font-light text-ink-2">{block.emailText}</span>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{supportHours && <p className="mt-4 text-[14px] text-ink-3">{supportHours}</p>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { CrossLinkBlock } from '@/payload-types'
|
||||
|
||||
import { type BlockContext, fill } from './context'
|
||||
|
||||
/** «Нужен адрес, закреплённый только за вами» (MassMain): ссылка на сайт другого бренда */
|
||||
export function CrossLink({ block, context }: { block: CrossLinkBlock; context: BlockContext }) {
|
||||
const href = context.otherBrand[block.target]
|
||||
return (
|
||||
<section className="pt-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-[18px] rounded-[22px] border border-border bg-surface-2 p-[clamp(20px,5vw,32px)]">
|
||||
<div>
|
||||
<h3 className="mb-2 text-[20px] font-semibold tracking-[-0.025em]">{block.title}</h3>
|
||||
{block.text && (
|
||||
<p className="max-w-[46em] text-[15px] leading-[1.55] font-light text-ink-2">
|
||||
{fill(context, block.text)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{href && (
|
||||
<a
|
||||
href={href}
|
||||
className="flex h-12 items-center justify-center rounded-xl border border-accent px-[22px] text-[15px] font-medium whitespace-nowrap text-accent hover:bg-accent-soft hover:text-accent"
|
||||
>
|
||||
{block.buttonLabel}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Flag } from '../Flag'
|
||||
import { StatusDot } from '../StatusDot'
|
||||
import { LeadForm } from './LeadForm'
|
||||
import type { LocationOption, Point } from './pricing-types'
|
||||
import { Points } from './Points'
|
||||
|
||||
/**
|
||||
* Цены на главной Вейликса (Main, #prices): крупная цена выбранной локации, список локаций
|
||||
* радиокнопками, «Купить» и заявка на индивидуальные условия.
|
||||
*/
|
||||
export function DedicatedPricing({
|
||||
options,
|
||||
points,
|
||||
listTitle,
|
||||
buyLabel,
|
||||
lead,
|
||||
privacyHref,
|
||||
}: {
|
||||
options: LocationOption[]
|
||||
points: Point[]
|
||||
listTitle: string
|
||||
buyLabel: string
|
||||
lead: { buttonLabel: string; title: string } | null
|
||||
privacyHref: string
|
||||
}) {
|
||||
const t = useTranslations('pricing')
|
||||
const [selected, setSelected] = useState(options[0]?.iso2)
|
||||
const [leadOpen, setLeadOpen] = useState(false)
|
||||
const current = options.find((option) => option.iso2 === selected) ?? options[0]
|
||||
if (!current) return null
|
||||
|
||||
return (
|
||||
<div className="relative grid grid-cols-[repeat(auto-fit,minmax(min(280px,100%),1fr))] gap-[clamp(28px,5vw,40px)] p-[clamp(20px,5vw,40px)]">
|
||||
<div>
|
||||
<div className="flex items-baseline gap-2.5 whitespace-nowrap">
|
||||
<span className="text-[clamp(34px,8vw,68px)] leading-none font-semibold tracking-[-0.04em] tabular-nums">
|
||||
{current.first}
|
||||
</span>
|
||||
<span className="text-[clamp(14px,3.6vw,16px)] text-ink-2">{t('firstMonth')}</span>
|
||||
</div>
|
||||
<div className="mt-2.5 text-[clamp(15px,3.8vw,17px)] whitespace-nowrap text-ink-2 tabular-nums">
|
||||
{t('thenPerMonth', { price: current.renewal })}
|
||||
</div>
|
||||
<Points points={points} className="mt-6" />
|
||||
</div>
|
||||
|
||||
<div id="locations" className="scroll-mt-[90px]">
|
||||
<div className="mb-3.5 flex flex-wrap items-baseline justify-between gap-4">
|
||||
<div id="locations-title" className="text-[15px] font-medium">
|
||||
{listTitle}
|
||||
</div>
|
||||
<div aria-live="polite" className="text-[13px] text-ink-3">
|
||||
{current.statusLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby="locations-title"
|
||||
className="grid max-h-[420px] gap-2 overflow-y-auto pr-1"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.iso2}
|
||||
className="flex cursor-pointer flex-wrap items-center gap-x-3.5 gap-y-2.5 rounded-[14px] border border-border px-3.5 py-3 text-[15px] text-ink hover:border-ink-3 has-checked:border-accent has-checked:bg-sel has-focus-visible:outline-2 has-focus-visible:outline-accent"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="dedicated-location"
|
||||
value={option.iso2}
|
||||
checked={option.iso2 === current.iso2}
|
||||
onChange={() => setSelected(option.iso2)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<Flag code={option.iso2} />
|
||||
<span className="grid min-w-[110px] flex-1 gap-0.5">
|
||||
<span>{option.country}</span>
|
||||
<span className="flex items-center gap-1.5 text-[13px] text-ink-3">
|
||||
<StatusDot status={option.status} />
|
||||
{option.city}
|
||||
<span className="sr-only">, {option.statusLabel}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className="grid gap-0.5 text-right tabular-nums">
|
||||
<span className="text-[15px] font-medium">{option.first}</span>
|
||||
<span className="text-[13px] text-ink-3">
|
||||
{t('then')} {option.renewal}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 flex flex-wrap items-center gap-3">
|
||||
{current.href && (
|
||||
<a
|
||||
href={current.href}
|
||||
aria-label={t('buyIn', { country: current.country })}
|
||||
className="flex h-12 items-center rounded-xl bg-accent px-6 text-[16px] font-medium text-on-accent shadow-[0_10px_28px_-12px_var(--accent)] hover:bg-accent-hover hover:text-on-accent"
|
||||
>
|
||||
{buyLabel}
|
||||
</a>
|
||||
)}
|
||||
{lead && !leadOpen && (
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={false}
|
||||
onClick={() => setLeadOpen(true)}
|
||||
className="flex h-12 items-center rounded-xl border border-border bg-transparent px-5 text-[15px] text-ink hover:border-ink-3"
|
||||
>
|
||||
{lead.buttonLabel}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{lead && leadOpen && (
|
||||
<LeadForm
|
||||
title={lead.title}
|
||||
privacyHref={privacyHref}
|
||||
className="mt-5 max-w-[460px] scroll-mt-[90px]"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user