mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
feat(native): a welcoming onboarding for the address screen
The app's own screen now opens on a welcome: the TREK mark, a line about what TREK is, and two clear paths — connect the TREK you already open in the browser, or get TREK Cloud. Both lead to polished screens: the address field with live validation, a shake and haptic on a bad address and a success tick before the server takes over; and a TREK Cloud intro with what it offers and a way in. Screens slide and stagger in, buttons give a tap, and it all respects the safe areas, light and dark, and reduced motion. Self-hosters are unaffected: cloud is its own path, never in the way. New native strings are added across all locales (English placeholders outside en/de, to translate).
This commit is contained in:
+268
-72
@@ -1,16 +1,18 @@
|
||||
import { CapacitorHttp } from '@capacitor/core'
|
||||
import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics'
|
||||
import { SplashScreen } from '@capacitor/splash-screen'
|
||||
import { TrekShell } from '../src/native/trekShell'
|
||||
import { probeDemo, probeServer, type ProbeFailure, type ProbeGet } from '../src/native/serverAddress'
|
||||
import { PRIVACY_URL, SERVER_DOCS_URL } from '../src/native/links'
|
||||
import { CLOUD_URL, PRIVACY_URL, SERVER_DOCS_URL } from '../src/native/links'
|
||||
import { pickLocale, translator } from './i18n'
|
||||
import logoUrl from '../public/icons/icon-white.svg'
|
||||
import './shell.css'
|
||||
|
||||
/**
|
||||
* The only page the app ships itself. It asks for a server on first start and
|
||||
* stands in when the remembered one cannot be reached; everything else is the
|
||||
* UI that server delivers.
|
||||
* The only page the app ships itself. It welcomes someone on first start, asks
|
||||
* for the TREK server they open in their browser, offers TREK Cloud to someone
|
||||
* without one, and stands in when a remembered server cannot be reached.
|
||||
* Everything past it is the UI that server delivers.
|
||||
*/
|
||||
|
||||
const locale = pickLocale(navigator.languages?.length ? navigator.languages : [navigator.language])
|
||||
@@ -19,7 +21,6 @@ document.documentElement.lang = locale
|
||||
document.documentElement.dir = locale === 'ar' ? 'rtl' : 'ltr'
|
||||
|
||||
const root = document.getElementById('shell')!
|
||||
|
||||
const PROBE_TIMEOUT_MS = 8000
|
||||
|
||||
// Native HTTP rather than fetch: the probe goes to a server of the user's
|
||||
@@ -42,115 +43,310 @@ function failureMessage(reason: ProbeFailure): string {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tiny DOM + haptics helpers ──────────────────────────────────────────────
|
||||
function el<K extends keyof HTMLElementTagNameMap>(
|
||||
tag: K,
|
||||
props: Partial<HTMLElementTagNameMap[K]> = {},
|
||||
props: Partial<HTMLElementTagNameMap[K]> & { dataset?: Record<string, string> } = {},
|
||||
...children: (Node | string)[]
|
||||
): HTMLElementTagNameMap[K] {
|
||||
// Typed as the plain element, not as Object.assign's intersection with the
|
||||
// props, which strict mode cannot represent for an <input>.
|
||||
const node: HTMLElementTagNameMap[K] = document.createElement(tag)
|
||||
Object.assign(node, props)
|
||||
node.append(...children)
|
||||
const { dataset, ...rest } = props
|
||||
const node = document.createElement(tag)
|
||||
Object.assign(node, rest)
|
||||
if (dataset) for (const [k, v] of Object.entries(dataset)) node.dataset[k] = v
|
||||
node.append(...children.filter((c) => c !== null && c !== undefined))
|
||||
return node
|
||||
}
|
||||
|
||||
function header(title: string, hint: string) {
|
||||
return [
|
||||
el('img', { src: logoUrl, alt: '', className: 'shell-logo' }),
|
||||
el('h1', { className: 'shell-title' }, title),
|
||||
el('p', { className: 'shell-hint' }, hint),
|
||||
]
|
||||
function icon(path: string, size = 22, className?: string): SVGSVGElement {
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('viewBox', '0 0 24 24')
|
||||
svg.setAttribute('width', String(size))
|
||||
svg.setAttribute('height', String(size))
|
||||
svg.setAttribute('fill', 'none')
|
||||
svg.setAttribute('stroke', 'currentColor')
|
||||
svg.setAttribute('stroke-width', '2')
|
||||
svg.setAttribute('stroke-linecap', 'round')
|
||||
svg.setAttribute('stroke-linejoin', 'round')
|
||||
if (className) svg.setAttribute('class', className)
|
||||
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
p.setAttribute('d', path)
|
||||
svg.appendChild(p)
|
||||
return svg
|
||||
}
|
||||
const I = {
|
||||
chevron: 'M9 18l6-6-6-6',
|
||||
cloud: 'M17.5 19a4.5 4.5 0 0 0 .5-9 6 6 0 0 0-11.6-1.6A4 4 0 0 0 6 19z',
|
||||
globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20',
|
||||
bolt: 'M13 2 4 14h7l-1 8 9-12h-7z',
|
||||
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10',
|
||||
check: 'M20 6 9 17l-5-5',
|
||||
}
|
||||
|
||||
/** A page outside the app; the WebView hands it to the system browser. */
|
||||
function externalLink(text: string, href: string) {
|
||||
return el('a', { href, target: '_blank', rel: 'noopener noreferrer', className: 'shell-link' }, text)
|
||||
async function tap(style: ImpactStyle = ImpactStyle.Light): Promise<void> {
|
||||
try {
|
||||
await Haptics.impact({ style })
|
||||
} catch {
|
||||
/* web / unavailable */
|
||||
}
|
||||
}
|
||||
async function notify(type: NotificationType): Promise<void> {
|
||||
try {
|
||||
await Haptics.notification({ type })
|
||||
} catch {
|
||||
/* web / unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
function showConnect(prefill = '') {
|
||||
// ── Screen transitions ──────────────────────────────────────────────────────
|
||||
let current: HTMLElement | null = null
|
||||
|
||||
function show(screen: HTMLElement, opts: { back?: boolean } = {}): void {
|
||||
screen.classList.add('screen', 'screen-enter')
|
||||
if (opts.back) screen.classList.add('screen-back')
|
||||
const previous = current
|
||||
if (previous) {
|
||||
previous.classList.remove('screen-enter')
|
||||
previous.classList.add('screen-leave')
|
||||
if (opts.back) previous.classList.add('screen-back')
|
||||
previous.addEventListener('animationend', () => previous.remove(), { once: true })
|
||||
setTimeout(() => previous.remove(), 400)
|
||||
}
|
||||
root.append(screen)
|
||||
current = screen
|
||||
}
|
||||
|
||||
function externalLink(text: string, href: string): HTMLAnchorElement {
|
||||
return el('a', { href, target: '_blank', rel: 'noopener noreferrer' }, text)
|
||||
}
|
||||
function footLinks(...extra: Node[]): HTMLElement {
|
||||
return el('div', { className: 'foot-links' }, ...extra, externalLink(t('native.privacy'), PRIVACY_URL))
|
||||
}
|
||||
function topbar(onBack: () => void): HTMLElement {
|
||||
const back = el('button', { type: 'button', className: 'back' }, icon('M15 18l-6-6 6-6', 18), t('native.back'))
|
||||
back.addEventListener('click', () => {
|
||||
void tap()
|
||||
onBack()
|
||||
})
|
||||
return el('div', { className: 'topbar' }, back)
|
||||
}
|
||||
|
||||
// ── Welcome ─────────────────────────────────────────────────────────────────
|
||||
function showWelcome(opts: { back?: boolean } = {}): void {
|
||||
const hero = el('img', { src: logoUrl, alt: '', className: 'hero-mark' })
|
||||
|
||||
const connect = el(
|
||||
'button',
|
||||
{ type: 'button', className: 'btn' },
|
||||
el('span', { className: 'btn-row' }, el('b', {}, t('native.welcome.connect')), el('small', {}, t('native.welcome.connectSub'))),
|
||||
icon(I.chevron, 20, 'chev'),
|
||||
)
|
||||
connect.addEventListener('click', () => {
|
||||
void tap()
|
||||
showConnect()
|
||||
})
|
||||
|
||||
const cloud = el(
|
||||
'button',
|
||||
{ type: 'button', className: 'btn btn-ghost' },
|
||||
el('span', { className: 'btn-row' }, el('b', {}, t('native.welcome.getCloud')), el('small', {}, t('native.welcome.getCloudSub'))),
|
||||
icon(I.chevron, 20, 'chev'),
|
||||
)
|
||||
cloud.addEventListener('click', () => {
|
||||
void tap()
|
||||
showCloud()
|
||||
})
|
||||
|
||||
const demo = el('button', { type: 'button', className: 'linkbtn' }, t('native.connect.tryDemo'))
|
||||
demo.addEventListener('click', () => runDemo(demo))
|
||||
|
||||
show(
|
||||
el(
|
||||
'div',
|
||||
{},
|
||||
hero,
|
||||
el('h1', { className: 'title' }, t('native.welcome.headline')),
|
||||
el('p', { className: 'sub' }, t('native.welcome.sub')),
|
||||
connect,
|
||||
cloud,
|
||||
demo,
|
||||
footLinks(externalLink(t('native.connect.whatIsServer'), SERVER_DOCS_URL)),
|
||||
),
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Connect (address) ───────────────────────────────────────────────────────
|
||||
function showConnect(prefill = '', opts: { back?: boolean } = {}): void {
|
||||
const input = el('input', {
|
||||
type: 'url',
|
||||
className: 'shell-input',
|
||||
placeholder: t('native.connect.placeholder'),
|
||||
value: prefill,
|
||||
autocapitalize: 'off',
|
||||
spellcheck: false,
|
||||
required: true,
|
||||
})
|
||||
input.setAttribute('inputmode', 'url')
|
||||
input.setAttribute('autocomplete', 'url')
|
||||
input.setAttribute('aria-label', t('native.connect.title'))
|
||||
const error = el('p', { className: 'shell-error', role: 'alert' })
|
||||
const submit = el('button', { type: 'submit', className: 'shell-button' }, t('native.connect.submit'))
|
||||
const form = el('form', { className: 'shell-form', noValidate: true }, input, submit, error)
|
||||
const field = el('div', { className: 'field' }, input)
|
||||
|
||||
const label = el('span', {}, t('native.connect.submit'))
|
||||
const submit = el('button', { type: 'submit', className: 'btn' }, label)
|
||||
const error = el('p', { className: 'error', role: 'alert' })
|
||||
const form = el('form', { className: 'form', noValidate: true }, field, submit, error)
|
||||
|
||||
const fail = (reason: ProbeFailure): void => {
|
||||
void notify(NotificationType.Error)
|
||||
field.classList.add('bad')
|
||||
setTimeout(() => field.classList.remove('bad'), 350)
|
||||
error.textContent = failureMessage(reason)
|
||||
input.disabled = false
|
||||
submit.disabled = false
|
||||
label.textContent = t('native.connect.submit')
|
||||
submit.replaceChildren(label)
|
||||
input.focus()
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault()
|
||||
void tap(ImpactStyle.Medium)
|
||||
error.textContent = ''
|
||||
input.disabled = true
|
||||
submit.disabled = true
|
||||
submit.textContent = t('native.connect.checking')
|
||||
submit.replaceChildren(el('span', { className: 'spinner' }), t('native.connect.checking'))
|
||||
const result = await probeServer(input.value, get)
|
||||
if (result.ok) {
|
||||
await TrekShell.setServer({ url: result.url })
|
||||
await connected(result.url)
|
||||
return
|
||||
}
|
||||
error.textContent = failureMessage(result.reason)
|
||||
input.disabled = false
|
||||
submit.disabled = false
|
||||
submit.textContent = t('native.connect.submit')
|
||||
input.focus()
|
||||
fail(result.reason)
|
||||
})
|
||||
|
||||
// For someone without a server of their own yet: the public demo, through the
|
||||
// same probe, so an outdated or unreachable demo says so like any server.
|
||||
const demo = el('button', { type: 'button', className: 'shell-link' }, t('native.connect.tryDemo'))
|
||||
demo.addEventListener('click', async () => {
|
||||
error.textContent = ''
|
||||
demo.disabled = true
|
||||
const result = await probeDemo(get)
|
||||
if (result.ok) {
|
||||
await TrekShell.setServer({ url: result.url })
|
||||
return
|
||||
}
|
||||
error.textContent = failureMessage(result.reason)
|
||||
demo.disabled = false
|
||||
})
|
||||
const demo = el('button', { type: 'button', className: 'linkbtn' }, t('native.connect.tryDemo'))
|
||||
demo.addEventListener('click', () => runDemo(demo, error))
|
||||
|
||||
root.replaceChildren(
|
||||
...header(t('native.connect.title'), t('native.connect.hint')),
|
||||
form,
|
||||
demo,
|
||||
show(
|
||||
el(
|
||||
'div',
|
||||
{ className: 'shell-links' },
|
||||
externalLink(t('native.connect.whatIsServer'), SERVER_DOCS_URL),
|
||||
externalLink(t('native.privacy'), PRIVACY_URL),
|
||||
{},
|
||||
topbar(() => showWelcome({ back: true })),
|
||||
el('h1', { className: 'title-sm' }, t('native.connect.title')),
|
||||
el('p', { className: 'sub' }, t('native.connect.hint')),
|
||||
form,
|
||||
demo,
|
||||
footLinks(externalLink(t('native.connect.whatIsServer'), SERVER_DOCS_URL)),
|
||||
),
|
||||
opts,
|
||||
)
|
||||
requestAnimationFrame(() => input.focus())
|
||||
}
|
||||
|
||||
// ── Get TREK Cloud ──────────────────────────────────────────────────────────
|
||||
function feature(path: string, text: string): HTMLElement {
|
||||
return el('div', { className: 'feature' }, el('span', { className: 'ic' }, icon(path, 18)), el('span', { className: 'tx' }, text))
|
||||
}
|
||||
|
||||
function showCloud(opts: { back?: boolean } = {}): void {
|
||||
const cta = el('a', { className: 'btn', href: CLOUD_URL, target: '_blank', rel: 'noopener noreferrer' }, t('native.cloud.cta'), icon(I.chevron, 20, 'chev'))
|
||||
cta.addEventListener('click', () => void tap())
|
||||
|
||||
const have = el('button', { type: 'button', className: 'linkbtn' }, t('native.cloud.haveServer'))
|
||||
have.addEventListener('click', () => {
|
||||
void tap()
|
||||
showConnect()
|
||||
})
|
||||
|
||||
show(
|
||||
el(
|
||||
'div',
|
||||
{},
|
||||
topbar(() => showWelcome({ back: true })),
|
||||
el('h1', { className: 'title-sm' }, t('native.cloud.title')),
|
||||
el('p', { className: 'sub' }, t('native.cloud.sub')),
|
||||
el('div', { className: 'features' }, feature(I.globe, t('native.cloud.f1')), feature(I.bolt, t('native.cloud.f2')), feature(I.shield, t('native.cloud.f3'))),
|
||||
cta,
|
||||
have,
|
||||
footLinks(),
|
||||
),
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Shared actions ──────────────────────────────────────────────────────────
|
||||
async function runDemo(button: HTMLButtonElement, error?: HTMLElement): Promise<void> {
|
||||
void tap()
|
||||
if (error) error.textContent = ''
|
||||
button.disabled = true
|
||||
const result = await probeDemo(get)
|
||||
if (result.ok) {
|
||||
await connected(result.url)
|
||||
return
|
||||
}
|
||||
button.disabled = false
|
||||
if (error) {
|
||||
error.textContent = failureMessage(result.reason)
|
||||
} else {
|
||||
void notify(NotificationType.Error)
|
||||
}
|
||||
}
|
||||
|
||||
/** The tick, then the server takes over. A beat so the success registers. */
|
||||
async function connected(url: string): Promise<void> {
|
||||
void notify(NotificationType.Success)
|
||||
const host = (() => {
|
||||
try {
|
||||
return new URL(url).host
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
})()
|
||||
const tick = el('div', { className: 'tick' })
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('viewBox', '0 0 24 24')
|
||||
svg.setAttribute('fill', 'none')
|
||||
svg.setAttribute('stroke', 'currentColor')
|
||||
svg.setAttribute('stroke-width', '2.5')
|
||||
svg.setAttribute('stroke-linecap', 'round')
|
||||
svg.setAttribute('stroke-linejoin', 'round')
|
||||
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
||||
p.setAttribute('d', I.check)
|
||||
svg.appendChild(p)
|
||||
tick.appendChild(svg)
|
||||
show(el('div', { className: 'success' }, tick, el('h1', { className: 'title-sm' }, t('native.connecting', { host }))))
|
||||
await new Promise((r) => setTimeout(r, 700))
|
||||
await TrekShell.setServer({ url })
|
||||
}
|
||||
|
||||
function showUnreachable(server: string): void {
|
||||
const retry = el('button', { type: 'button', className: 'btn' }, t('native.offline.retry'))
|
||||
retry.addEventListener('click', () => {
|
||||
void tap()
|
||||
retry.disabled = true
|
||||
void TrekShell.reload()
|
||||
})
|
||||
const change = el('button', { type: 'button', className: 'linkbtn' }, t('native.changeServer'))
|
||||
change.addEventListener('click', () => {
|
||||
void tap()
|
||||
void TrekShell.resetServer()
|
||||
})
|
||||
show(
|
||||
el(
|
||||
'div',
|
||||
{},
|
||||
el('img', { src: logoUrl, alt: '', className: 'shell-logo' }),
|
||||
el('h1', { className: 'title-sm' }, t('native.offline.title')),
|
||||
el('p', { className: 'sub' }, t('native.offline.hint', { server: new URL(server).host })),
|
||||
retry,
|
||||
change,
|
||||
footLinks(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
function showUnreachable(server: string) {
|
||||
const retry = el('button', { type: 'button', className: 'shell-button' }, t('native.offline.retry'))
|
||||
retry.addEventListener('click', () => {
|
||||
retry.disabled = true
|
||||
void TrekShell.reload()
|
||||
})
|
||||
const change = el('button', { type: 'button', className: 'shell-link' }, t('native.changeServer'))
|
||||
change.addEventListener('click', () => { void TrekShell.resetServer() })
|
||||
|
||||
root.replaceChildren(
|
||||
...header(t('native.offline.title'), t('native.offline.hint', { server: new URL(server).host })),
|
||||
el('div', { className: 'shell-form' }, retry, change),
|
||||
el('div', { className: 'shell-links' }, externalLink(t('native.privacy'), PRIVACY_URL)),
|
||||
)
|
||||
}
|
||||
|
||||
async function start() {
|
||||
// ── Start ───────────────────────────────────────────────────────────────────
|
||||
async function start(): Promise<void> {
|
||||
const { url } = await TrekShell.getServer()
|
||||
if (!url) {
|
||||
showConnect()
|
||||
showWelcome()
|
||||
} else {
|
||||
// Only reached when loading the server failed. On iOS the stored copy of
|
||||
// its start page brings the offline-capable UI up anyway.
|
||||
|
||||
+461
-41
@@ -1,99 +1,519 @@
|
||||
/* Mirrors the app's neutral default scheme (client/src/index.css), dark first:
|
||||
this page shares the launch screen's background and must not flash. */
|
||||
/*
|
||||
* The address screen the app ships. Dark first, mirroring the app's neutral
|
||||
* default scheme (client/src/index.css), so it shares the launch screen's
|
||||
* background and never flashes. Everything past this screen is the UI the
|
||||
* chosen server delivers.
|
||||
*/
|
||||
:root {
|
||||
--bg: #09090b;
|
||||
--bg-2: #101013;
|
||||
--surface: #18181b;
|
||||
--surface-2: #1f1f23;
|
||||
--edge: #27272a;
|
||||
--edge-strong: #3f3f46;
|
||||
--ink: #fafafa;
|
||||
--muted: #a1a1aa;
|
||||
--accent: #e4e4e7;
|
||||
--faint: #71717a;
|
||||
--accent: #fafafa;
|
||||
--accent-ink: #09090b;
|
||||
--danger: #f87171;
|
||||
--ok: #4ade80;
|
||||
--radius: 16px;
|
||||
--ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--bg-2: #fafafa;
|
||||
--surface: #f4f4f5;
|
||||
--surface-2: #ececef;
|
||||
--edge: #e4e4e7;
|
||||
--edge-strong: #d4d4d8;
|
||||
--ink: #111827;
|
||||
--muted: #6b7280;
|
||||
--muted: #52525b;
|
||||
--faint: #9ca3af;
|
||||
--accent: #111827;
|
||||
--accent-ink: #ffffff;
|
||||
--danger: #dc2626;
|
||||
--ok: #16a34a;
|
||||
color-scheme: light;
|
||||
}
|
||||
.shell-logo { filter: invert(1); }
|
||||
.shell-logo,
|
||||
.mark {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
html, body {
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, system-ui, sans-serif;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* An ambient glow behind the content, so the flat dark gains a little depth. */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(60% 50% at 50% -5%, color-mix(in srgb, var(--ink) 7%, transparent), transparent 70%),
|
||||
radial-gradient(50% 40% at 50% 110%, color-mix(in srgb, var(--ink) 4%, transparent), transparent 70%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
max-width: 420px;
|
||||
max-width: 440px;
|
||||
margin: 0 auto;
|
||||
padding: calc(env(safe-area-inset-top) + 24px) 24px calc(env(safe-area-inset-bottom) + 24px);
|
||||
padding: calc(env(safe-area-inset-top) + 28px) 24px calc(env(safe-area-inset-bottom) + 24px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shell-logo { width: 56px; height: 56px; margin-bottom: 8px; }
|
||||
.shell-title { font-size: 1.5rem; font-weight: 600; margin: 0; }
|
||||
.shell-hint { color: var(--muted); margin: 0 0 12px; line-height: 1.5; overflow-wrap: anywhere; }
|
||||
/* ── Screen transitions ──────────────────────────────────────────────────── */
|
||||
.screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.screen-enter {
|
||||
animation: screenIn 0.5s var(--ease) both;
|
||||
}
|
||||
.screen-leave {
|
||||
animation: screenOut 0.28s var(--ease-in) both;
|
||||
position: absolute;
|
||||
inset: calc(env(safe-area-inset-top) + 28px) 24px calc(env(safe-area-inset-bottom) + 24px);
|
||||
}
|
||||
.screen-back.screen-enter {
|
||||
animation-name: screenInBack;
|
||||
}
|
||||
.screen-back.screen-leave {
|
||||
animation-name: screenOutBack;
|
||||
}
|
||||
@keyframes screenIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(26px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@keyframes screenOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-22px);
|
||||
}
|
||||
}
|
||||
@keyframes screenInBack {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-26px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@keyframes screenOutBack {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(22px);
|
||||
}
|
||||
}
|
||||
|
||||
.shell-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
/* Children stagger in, so a screen assembles rather than snaps. */
|
||||
.screen-enter > * {
|
||||
animation: riseIn 0.55s var(--ease) both;
|
||||
}
|
||||
.screen-enter > *:nth-child(1) {
|
||||
animation-delay: 0.02s;
|
||||
}
|
||||
.screen-enter > *:nth-child(2) {
|
||||
animation-delay: 0.07s;
|
||||
}
|
||||
.screen-enter > *:nth-child(3) {
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
.screen-enter > *:nth-child(4) {
|
||||
animation-delay: 0.17s;
|
||||
}
|
||||
.screen-enter > *:nth-child(5) {
|
||||
animation-delay: 0.22s;
|
||||
}
|
||||
.screen-enter > *:nth-child(6) {
|
||||
animation-delay: 0.27s;
|
||||
}
|
||||
@keyframes riseIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
.shell-input {
|
||||
/* ── Brand ───────────────────────────────────────────────────────────────── */
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.brand .mark {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.brand .wordmark {
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.brand .tag {
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
color: var(--accent-ink);
|
||||
background: var(--accent);
|
||||
border-radius: 999px;
|
||||
padding: 2px 10px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.shell-logo {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* The big hero mark on the welcome screen, with a gentle breathing float. */
|
||||
.hero-mark {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
margin-bottom: 18px;
|
||||
animation: floaty 6s ease-in-out infinite;
|
||||
filter: drop-shadow(0 10px 30px color-mix(in srgb, var(--ink) 18%, transparent));
|
||||
}
|
||||
@keyframes floaty {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Type ────────────────────────────────────────────────────────────────── */
|
||||
.title {
|
||||
font-size: 1.9rem;
|
||||
line-height: 1.12;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0;
|
||||
}
|
||||
.title-sm {
|
||||
font-size: 1.45rem;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
margin: 0;
|
||||
}
|
||||
.sub {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
line-height: 1.55;
|
||||
font-size: 1.02rem;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
font: inherit;
|
||||
font-size: 1rem;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 1.02rem;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.14s var(--ease),
|
||||
opacity 0.2s,
|
||||
background 0.2s,
|
||||
border-color 0.2s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
min-height: 54px;
|
||||
}
|
||||
.btn:active {
|
||||
transform: scale(0.975);
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.btn-ghost {
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
border-color: var(--edge);
|
||||
}
|
||||
.btn-ghost:active {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
.btn-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
.btn-row b {
|
||||
font-weight: 650;
|
||||
}
|
||||
.btn-row small {
|
||||
font-weight: 500;
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.btn .chev {
|
||||
margin-left: auto;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.linkbtn {
|
||||
font: inherit;
|
||||
font-size: 0.95rem;
|
||||
padding: 12px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.linkbtn:active {
|
||||
color: var(--ink);
|
||||
}
|
||||
a.linkbtn {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.foot-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 2px 18px;
|
||||
margin-top: 6px;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
.foot-links a {
|
||||
color: var(--faint);
|
||||
text-decoration: none;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
|
||||
/* ── Address form ────────────────────────────────────────────────────────── */
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.field {
|
||||
position: relative;
|
||||
}
|
||||
.field input {
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
font-size: 1.05rem;
|
||||
padding: 16px 18px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--edge);
|
||||
background: var(--surface);
|
||||
color: var(--ink);
|
||||
outline: none;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
box-shadow 0.25s;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
.shell-input:focus { border-color: var(--accent); }
|
||||
|
||||
.shell-button {
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
padding: 14px 16px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
.field input::placeholder {
|
||||
color: var(--faint);
|
||||
}
|
||||
.field input:focus {
|
||||
border-color: var(--edge-strong);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--ink) 9%, transparent);
|
||||
}
|
||||
.field.bad input {
|
||||
border-color: var(--danger);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 16%, transparent);
|
||||
animation: shake 0.3s;
|
||||
}
|
||||
@keyframes shake {
|
||||
25% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(-3px);
|
||||
}
|
||||
}
|
||||
.shell-button:disabled { opacity: 0.6; }
|
||||
|
||||
.shell-link {
|
||||
.error {
|
||||
color: var(--danger);
|
||||
margin: 0;
|
||||
min-height: 1.4em;
|
||||
line-height: 1.45;
|
||||
font-size: 0.95rem;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
/* Spinner inside the connect button while probing. */
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid color-mix(in srgb, var(--accent-ink) 35%, transparent);
|
||||
border-top-color: var(--accent-ink);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Feature list (cloud screen) ─────────────────────────────────────────── */
|
||||
.features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 4px 0 2px;
|
||||
}
|
||||
.feature {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 13px;
|
||||
padding: 11px 0;
|
||||
border-top: 1px solid var(--edge);
|
||||
}
|
||||
.feature:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
.feature .ic {
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--edge);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink);
|
||||
}
|
||||
.feature .tx {
|
||||
line-height: 1.4;
|
||||
padding-top: 5px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
/* ── Success tick before the server loads ────────────────────────────────── */
|
||||
.success {
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.tick {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--ok) 16%, transparent);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-bottom: 8px;
|
||||
animation: pop 0.45s var(--ease) both;
|
||||
}
|
||||
.tick svg {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
color: var(--ok);
|
||||
}
|
||||
.tick path {
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: draw 0.45s 0.15s var(--ease) forwards;
|
||||
}
|
||||
@keyframes pop {
|
||||
from {
|
||||
transform: scale(0.4);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes draw {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 2px -8px;
|
||||
}
|
||||
.back {
|
||||
font: inherit;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: var(--muted);
|
||||
font-size: 0.98rem;
|
||||
padding: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.back:active {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.shell-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 4px 16px;
|
||||
font-size: 0.875rem;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before {
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
}
|
||||
}
|
||||
a.shell-link { text-decoration: underline; text-underline-offset: 2px; }
|
||||
|
||||
.shell-error { color: var(--danger); margin: 0; min-height: 1.5em; line-height: 1.5; }
|
||||
|
||||
@@ -11,3 +11,6 @@ export const SERVER_DOCS_URL = 'https://github.com/liketrek/TREK#readme';
|
||||
|
||||
/** The public demo the address screen offers to someone without a server. */
|
||||
export const DEMO_SERVER_URL = 'https://demo.liketrek.com';
|
||||
|
||||
/** TREK Cloud, for someone who wants a hosted TREK rather than running their own. */
|
||||
export const CLOUD_URL = 'https://liketrek.com';
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'سياسة الخصوصية',
|
||||
'native.settings.server': 'الخادم',
|
||||
'native.settings.serverHint': 'هذا التطبيق متصل بـ {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Məxfilik siyasəti',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Bu tətbiq {server} serverinə qoşulub.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Política de privacidade',
|
||||
'native.settings.server': 'Servidor',
|
||||
'native.settings.serverHint': 'Este app está conectado a {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Política de privadesa',
|
||||
'native.settings.server': 'Servidor',
|
||||
'native.settings.serverHint': 'Aquesta app està connectada a {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Zásady ochrany osobních údajů',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Tato aplikace je připojena k {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Datenschutzerklärung',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Diese App ist mit {server} verbunden.',
|
||||
'native.welcome.headline': 'Reisen gemeinsam planen.',
|
||||
'native.welcome.sub': 'TREK hält jeden Tag, Ort, jede Buchung und Kosten in einem gemeinsamen Plan, den eure ganze Gruppe live bearbeitet.',
|
||||
'native.welcome.connect': 'Mit deinem TREK verbinden',
|
||||
'native.welcome.connectSub': 'Die Adresse öffnen, die du schon nutzt',
|
||||
'native.welcome.getCloud': 'TREK Cloud holen',
|
||||
'native.welcome.getCloudSub': 'Für dich gehostet, ohne Einrichtung',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'Wir hosten TREK für dich, auf deiner eigenen Adresse. Nichts installieren, nichts warten — in Minuten bereit.',
|
||||
'native.cloud.f1': 'Eigene Adresse, auf Servern in Deutschland',
|
||||
'native.cloud.f2': 'In Minuten eingerichtet, Updates übernehmen wir',
|
||||
'native.cloud.f3': 'Deine Daten bleiben deine — keine Werbung, kein Tracking',
|
||||
'native.cloud.cta': 'Loslegen',
|
||||
'native.cloud.haveServer': 'Ich habe schon ein TREK',
|
||||
'native.connecting': 'Verbinde mit {host}',
|
||||
'native.back': 'Zurück',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Privacy policy',
|
||||
'native.settings.server': 'Server',
|
||||
'native.settings.serverHint': 'This app is connected to {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Política de privacidad',
|
||||
'native.settings.server': 'Servidor',
|
||||
'native.settings.serverHint': 'Esta app está conectada a {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Privaatsuspoliitika',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'See rakendus on ühendatud serveriga {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Politique de confidentialité',
|
||||
'native.settings.server': 'Serveur',
|
||||
'native.settings.serverHint': 'Cette application est connectée à {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -26,6 +26,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Πολιτική απορρήτου',
|
||||
'native.settings.server': 'Διακομιστής',
|
||||
'native.settings.serverHint': 'Αυτή η εφαρμογή είναι συνδεδεμένη στο {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Adatvédelmi irányelvek',
|
||||
'native.settings.server': 'Szerver',
|
||||
'native.settings.serverHint': 'Ez az alkalmazás ehhez csatlakozik: {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Kebijakan privasi',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Aplikasi ini terhubung ke {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Informativa sulla privacy',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Questa app è collegata a {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'プライバシーポリシー',
|
||||
'native.settings.server': 'サーバー',
|
||||
'native.settings.serverHint': 'このアプリは {server} に接続されています。',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': '개인정보 처리방침',
|
||||
'native.settings.server': '서버',
|
||||
'native.settings.serverHint': '이 앱은 {server}에 연결되어 있습니다.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Privacybeleid',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Deze app is verbonden met {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Polityka prywatności',
|
||||
'native.settings.server': 'Serwer',
|
||||
'native.settings.serverHint': 'Ta aplikacja jest połączona z {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Политика конфиденциальности',
|
||||
'native.settings.server': 'Сервер',
|
||||
'native.settings.serverHint': 'Приложение подключено к {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Zásady ochrany osobných údajov',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Táto aplikácia je pripojená k serveru {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Integritetspolicy',
|
||||
'native.settings.server': 'Server', // same-as-en
|
||||
'native.settings.serverHint': 'Den här appen är ansluten till {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'นโยบายความเป็นส่วนตัว',
|
||||
'native.settings.server': 'เซิร์ฟเวอร์',
|
||||
'native.settings.serverHint': 'แอปนี้เชื่อมต่อกับ {server} อยู่',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Gizlilik politikası',
|
||||
'native.settings.server': 'Sunucu',
|
||||
'native.settings.serverHint': 'Bu uygulama {server} adresine bağlı.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Політика конфіденційності',
|
||||
'native.settings.server': 'Сервер',
|
||||
'native.settings.serverHint': 'Застосунок підключено до {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -25,6 +25,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': 'Chính sách quyền riêng tư',
|
||||
'native.settings.server': 'Máy chủ',
|
||||
'native.settings.serverHint': 'Ứng dụng này đang kết nối với {server}.',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': '隱私權政策',
|
||||
'native.settings.server': '伺服器',
|
||||
'native.settings.serverHint': '此 App 已連線到 {server}。',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
@@ -24,6 +24,21 @@ const native: TranslationStrings = {
|
||||
'native.privacy': '隐私政策',
|
||||
'native.settings.server': '服务器',
|
||||
'native.settings.serverHint': '此应用已连接到 {server}。',
|
||||
'native.welcome.headline': 'Plan trips together.',
|
||||
'native.welcome.sub': 'TREK keeps every day, place, booking and cost in one live plan your whole group edits together.',
|
||||
'native.welcome.connect': 'Connect your TREK',
|
||||
'native.welcome.connectSub': 'Open the address you already use',
|
||||
'native.welcome.getCloud': 'Get TREK Cloud',
|
||||
'native.welcome.getCloudSub': 'Hosted for you, no setup',
|
||||
'native.cloud.title': 'TREK Cloud',
|
||||
'native.cloud.sub': 'We host TREK for you, on your own address. Nothing to install, nothing to maintain — ready in minutes.',
|
||||
'native.cloud.f1': 'Your own address, on servers in Germany',
|
||||
'native.cloud.f2': 'Set up in minutes, updates handled for you',
|
||||
'native.cloud.f3': 'Your data stays yours — no ads, no tracking',
|
||||
'native.cloud.cta': 'Get started',
|
||||
'native.cloud.haveServer': 'I already have a TREK',
|
||||
'native.connecting': 'Connecting to {host}',
|
||||
'native.back': 'Back',
|
||||
};
|
||||
|
||||
export default native;
|
||||
|
||||
Reference in New Issue
Block a user