From 46b9df54bf7393f64891dec75b4192770e426d80 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 12 Nov 2026 10:04:31 +0100 Subject: [PATCH] feat(native): the TREK Cloud screens show the app and make the choice of a plan easy The cloud screen shows real screens of the app. The plans are a choice now: one tap marks a plan, the recommended one is marked, what every plan has is said once above them, storage says how many photos it holds, and the yearly switch shows what a year saves; one button at the foot carries the chosen plan on. The claim screen names the plan with a way to change it, shows the address as it will be once it is free, ticks off the password rules while typing, says in three words where the servers are, that it can be cancelled and that there are no ads, and puts the photo behind the form away. --- client/native-shell/img/preview.webp | Bin 0 -> 36946 bytes client/native-shell/main.ts | 234 ++++++++++++++++++++++---- client/native-shell/shell.css | 238 +++++++++++++++++++++++++-- client/src/native/priceValue.test.ts | 20 +++ client/src/native/priceValue.ts | 14 ++ shared/src/i18n/ar/native.ts | 14 +- shared/src/i18n/az/native.ts | 14 +- shared/src/i18n/br/native.ts | 14 +- shared/src/i18n/ca/native.ts | 14 +- shared/src/i18n/cs/native.ts | 14 +- shared/src/i18n/de/native.ts | 14 +- shared/src/i18n/en/native.ts | 14 +- shared/src/i18n/es/native.ts | 14 +- shared/src/i18n/et/native.ts | 14 +- shared/src/i18n/fr/native.ts | 14 +- shared/src/i18n/gr/native.ts | 14 +- shared/src/i18n/he/native.ts | 14 +- shared/src/i18n/hu/native.ts | 14 +- shared/src/i18n/id/native.ts | 14 +- shared/src/i18n/it/native.ts | 14 +- shared/src/i18n/ja/native.ts | 14 +- shared/src/i18n/ko/native.ts | 14 +- shared/src/i18n/nl/native.ts | 14 +- shared/src/i18n/pl/native.ts | 14 +- shared/src/i18n/ru/native.ts | 14 +- shared/src/i18n/sk/native.ts | 14 +- shared/src/i18n/sv/native.ts | 14 +- shared/src/i18n/th/native.ts | 14 +- shared/src/i18n/tr/native.ts | 14 +- shared/src/i18n/uk/native.ts | 14 +- shared/src/i18n/vi/native.ts | 14 +- shared/src/i18n/zh-TW/native.ts | 14 +- shared/src/i18n/zh/native.ts | 14 +- 33 files changed, 797 insertions(+), 101 deletions(-) create mode 100644 client/native-shell/img/preview.webp create mode 100644 client/src/native/priceValue.test.ts create mode 100644 client/src/native/priceValue.ts diff --git a/client/native-shell/img/preview.webp b/client/native-shell/img/preview.webp new file mode 100644 index 0000000000000000000000000000000000000000..b7ba646f113efd98f866fb0c173678235e19ee6f GIT binary patch literal 36946 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 literal 0 HcmV?d00001 diff --git a/client/native-shell/main.ts b/client/native-shell/main.ts index f00e23e36..503506fa8 100644 --- a/client/native-shell/main.ts +++ b/client/native-shell/main.ts @@ -6,9 +6,11 @@ import { StatusBar, Style } from '@capacitor/status-bar'; import logoUrl from '../public/icons/icon-white.svg'; import logoBlack from '../public/logo-dark.svg'; // #000 wordmark, for light backgrounds import logoWhite from '../public/logo-light.svg'; // #fff wordmark, for dark backgrounds +import previewImage from './img/preview.webp'; // real app screens, from docs/screenshots/showcase-mobile.webp import { AUTH_GATE_NAMES, canSignInInApp, type AuthGate } from '../src/native/authGate'; import { opensSoon, refundedBeforeOpening } from '../src/native/cloudOrders'; import { fakeIapClient, getIapClient, productIdFor, type Purchase } from '../src/native/iap'; +import { priceValue } from '../src/native/priceValue'; import { AUTH_PROXY_DOCS_URL, CLIENT_CERT_DOCS_URL, @@ -27,7 +29,7 @@ import { type ProbeFailure, type ProbeGet, } from '../src/native/serverAddress'; -import { passwordProblem } from '../../shared/src/auth/password-policy'; +import { PASSWORD_MIN_LENGTH, passwordProblem, passwordRuleStates } from '../../shared/src/auth/password-policy'; import { TrekShell, type ClientCertificateFailure } from '../src/native/trekShell'; import { directionOf, pickLocale, translator } from './i18n'; import './shell.css'; @@ -150,6 +152,7 @@ const I = { 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', + camera: 'M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3zM12 17a4 4 0 1 0 0-8 4 4 0 0 0 0 8z', }; async function tap(style: ImpactStyle = ImpactStyle.Light): Promise { @@ -188,6 +191,8 @@ function show(screen: HTMLElement, opts: { back?: boolean } = {}): void { } root.append(screen); current = screen; + // A form reads on a calm page: the photo steps back while it is on screen. + document.body.classList.toggle('quiet', screen.classList.contains('screen-quiet')); } function externalLink(text: string, href: string): HTMLAnchorElement { @@ -533,7 +538,8 @@ function showCloud(opts: { back?: boolean } = {}): void { showPlans(); }); - const have = el('button', { type: 'button', className: 'linkbtn' }, t('native.cloud.haveServer')); + // The way out for someone who already runs TREK: there, but quieter than the way in. + const have = el('button', { type: 'button', className: 'linkbtn linkbtn-quiet' }, t('native.cloud.haveServer')); have.addEventListener('click', () => { void tap(); showConnect(); @@ -557,6 +563,12 @@ function showCloud(opts: { back?: boolean } = {}): void { el('p', { className: 'sub' }, t('native.cloud.sub')), ], [ + // The app itself, as it runs on TREK Cloud: real screens, nothing drawn for the purpose. + el( + 'div', + { className: 'cloud-preview' }, + el('img', { src: previewImage, alt: '', loading: 'lazy', decoding: 'async' }) + ), el( 'div', { className: 'features' }, @@ -652,34 +664,58 @@ async function loadOffer(list: CloudPlans): Promise { return { canBuy, storePrices: await iap.prices(ids), opensSoon: soon }; } -function planCard(plan: CloudPlan, interval: 'month' | 'year', offer: Offer): HTMLElement { - const { label, buyable } = offerFor(offer, plan, interval); +/** The plan the picker starts on and marks: the smallest one with AI documents, the one most trips want. */ +function recommendedPlan(plans: CloudPlan[]): CloudPlan | undefined { + return plans.find((p) => p.aiDocuments > 0) ?? plans[0]; +} + +/** + * What a year saves against twelve months, in whole percent, rounded down so + * the badge never promises more than the price gives. From the store's own price + * strings where both parse (the buyer's currency), else from the plan list. + */ +function yearlySaving(plans: CloudPlan[], offer: Offer): number | null { + const savings = plans.map((plan) => { + const month = priceValue(offer.storePrices?.get(productIdFor(plan.id, 'month'))) ?? plan.monthlyCents; + const year = priceValue(offer.storePrices?.get(productIdFor(plan.id, 'year'))) ?? plan.yearlyCents; + if (!month || !year) return null; + return Math.floor((1 - year / (month * 12)) * 100); + }); + if (savings.some((v) => v === null)) return null; + const least = Math.min(...(savings as number[])); + return least >= 5 ? least : null; +} + +function planCard( + plan: CloudPlan, + interval: 'month' | 'year', + offer: Offer, + opts: { recommended: boolean; selected: boolean; onSelect?: () => void } +): HTMLElement { + const { label } = offerFor(offer, plan, interval); const per = interval === 'year' ? t('native.plans.perYear') : t('native.plans.perMonth'); - const feat = (text: string) => el('span', {}, icon(I.check, 16), text); + const feat = (text: string, extra?: Node) => el('span', {}, icon(I.check, 16), text, ...(extra ? [extra] : [])); // An absent feature (e.g. a plan with no AI documents): a muted dash, not a // green tick, so "none" never reads as a perk. const featOff = (text: string) => el('span', { className: 'off' }, icon('M5 12h14', 16), text); - let sub: HTMLElement | null = null; - if (buyable) { - const button = el('button', { type: 'button', className: 'btn' }, t('native.plans.subscribe')); - sub = button; - button.addEventListener('click', async () => { - void tap(ImpactStyle.Medium); - // A store account with a running subscription buys nothing more: say so before the form. - button.disabled = true; - const held = await iap.held(); - button.disabled = false; - if (held) showSubscribed(); - else showClaim(plan, interval, `${label} ${per}`); - }); - } - return el( - 'div', - { className: 'plan' }, + // About 3 MB a photo, as a phone takes them: a size people can picture, as a + // camera and a number, which needs no words in any language. + const photos = el( + 'small', + { className: 'plan-photos' }, + icon(I.camera, 13), + `≈ ${new Intl.NumberFormat(locale).format(plan.storageGb * 300)}` + ); + const body = [ el( 'div', { className: 'plan-top' }, - el('span', { className: 'plan-name' }, plan.name), + el( + 'span', + { className: 'plan-name' }, + plan.name, + ...(opts.recommended ? [el('span', { className: 'plan-badge' }, t('native.plans.recommended'))] : []) + ), // No price yet (orders not open): the plan shows without one rather than with an empty one. // The price is figures and a currency sign: one left-to-right run, so Arabic // keeps "5,99 €" whole, with the period standing apart beside it. @@ -688,14 +724,24 @@ function planCard(plan: CloudPlan, interval: 'month' | 'year', offer: Offer): HT el( 'div', { className: 'plan-feats' }, - feat(t('native.plans.unlimited')), - feat(t('native.plans.storage', { n: String(plan.storageGb) })), + feat(t('native.plans.storage', { n: String(plan.storageGb) }), photos), plan.aiDocuments > 0 ? feat(t('native.plans.aiDocs', { n: String(plan.aiDocuments) })) : featOff(t('native.plans.noAi')) ), - ...(sub ? [sub] : []) - ); + ]; + const className = `plan${opts.recommended ? ' featured' : ''}${opts.selected ? ' selected' : ''}`; + if (!opts.onSelect) return el('div', { className }, ...body); + // A choice among the plans, as radio buttons are: the one button below acts on it. + const card = el('button', { type: 'button', className }, el('span', { className: 'plan-radio' }), ...body); + card.setAttribute('role', 'radio'); + card.setAttribute('aria-checked', String(opts.selected)); + const select = opts.onSelect; + card.addEventListener('click', () => { + void tap(); + select(); + }); + return card; } /** @@ -784,23 +830,62 @@ function showPlans(opts: { back?: boolean } = {}): void { let interval: 'month' | 'year' = 'month'; let plans: CloudPlan[] | null = null; let offer: Offer = { canBuy: false, storePrices: null, opensSoon: false }; + let selectedId: string | null = null; const content = el('div', { className: 'loading' }, el('span', { className: 'spinner' })); const footer = el('div', { className: 'plans-foot' }); + // What every plan has, said once above them, so the cards show only what differs. + const everyPlan = el('p', { className: 'plans-every', hidden: true }, icon(I.check, 16), t('native.plans.everyPlan')); const monthBtn = el('button', { type: 'button' }, t('native.plans.monthly')); - const yearBtn = el('button', { type: 'button' }, t('native.plans.yearly')); + const saveBadge = el('span', { className: 'save', hidden: true }); + const yearBtn = el('button', { type: 'button' }, t('native.plans.yearly'), saveBadge); const seg = el('div', { className: 'segmented' }, monthBtn, yearBtn); + // The one way on, kept in reach at the foot of the screen while the plans scroll. + const ctaName = el('span', { className: 'btn-label' }); + const ctaPrice = el('small', {}); + const cta = el('button', { type: 'button', className: 'btn plans-go' }, el('span', { className: 'btn-row' }, el('b', {}, ctaName), ctaPrice), icon(I.chevron, 20, 'chev')); + const ctaBar = el('div', { className: 'plans-cta', hidden: true }, cta); + + const selected = (): CloudPlan | undefined => plans?.find((p) => p.id === selectedId); const paintSeg = () => { monthBtn.setAttribute('aria-pressed', String(interval === 'month')); yearBtn.setAttribute('aria-pressed', String(interval === 'year')); }; + const paintCta = () => { + const plan = selected(); + const buyable = plan ? offerFor(offer, plan, interval).buyable : false; + ctaBar.hidden = !buyable; + if (!plan || !buyable) return; + const { label } = offerFor(offer, plan, interval); + const per = interval === 'year' ? t('native.plans.perYear') : t('native.plans.perMonth'); + ctaName.textContent = t('native.plans.continueWith', { plan: plan.name }); + ctaPrice.textContent = `${label} ${per}`; + }; const renderList = () => { const list = plans; if (!list) return; + const recommended = recommendedPlan(list); content.className = 'plans'; - content.replaceChildren(...list.map((p) => planCard(p, interval, offer))); + content.setAttribute('role', 'radiogroup'); + content.setAttribute('aria-label', t('native.plans.title')); + content.replaceChildren( + ...list.map((p) => + planCard(p, interval, offer, { + recommended: p.id === recommended?.id, + selected: p.id === selectedId, + // Only a plan that can be bought here is a choice; a closed shop shows them as they are. + onSelect: offerFor(offer, p, interval).buyable + ? () => { + selectedId = p.id; + renderList(); + } + : undefined, + }) + ) + ); + paintCta(); }; const choose = (next: 'month' | 'year') => { if (interval === next) return; @@ -813,11 +898,35 @@ function showPlans(opts: { back?: boolean } = {}): void { yearBtn.addEventListener('click', () => choose('year')); paintSeg(); + cta.addEventListener('click', async () => { + const plan = selected(); + if (!plan) return; + void tap(ImpactStyle.Medium); + // A store account with a running subscription buys nothing more: say so before the form. + cta.disabled = true; + const held = await iap.held(); + cta.disabled = false; + if (held) { + showSubscribed(); + return; + } + const { label } = offerFor(offer, plan, interval); + const per = interval === 'year' ? t('native.plans.perYear') : t('native.plans.perMonth'); + showClaim(plan, interval, `${label} ${per}`); + }); + const load = async () => { const list = await fetchPlans(); if (list) { offer = await loadOffer(list); plans = list.plans; + selectedId = recommendedPlan(list.plans)?.id ?? null; + const saving = yearlySaving(list.plans, offer); + if (saving !== null) { + saveBadge.textContent = `−${new Intl.NumberFormat(locale, { style: 'percent' }).format(saving / 100)}`; + saveBadge.hidden = false; + } + everyPlan.hidden = false; renderList(); footer.replaceChildren(...planFooter(offer, list.plans)); return; @@ -844,7 +953,7 @@ function showPlans(opts: { back?: boolean } = {}): void { el('p', { className: 'sub' }, t('native.plans.sub')), seg, ], - [content, footer] + [everyPlan, content, ctaBar, footer] ) ), opts @@ -1002,7 +1111,31 @@ function showClaim( passwordInput.dir = 'ltr'; passwordInput.classList.add('ltr-value'); const passwordField = el('div', { className: 'field' }, passwordInput); - const passwordHint = el('p', { className: 'hint' }, t('native.claim.passwordHint')); + // Shown when the server's list knows the password, or on a send with a rule unmet. + const passwordHint = el('p', { className: 'hint', hidden: true }, t('native.claim.passwordHint')); + // The rules, each ticked off as it is met, rather than a paragraph to read. + const ruleLabels = { + // The number outside the words: it is always the same, and the word reads after it in every language. + length: `${PASSWORD_MIN_LENGTH}+ ${t('native.claim.ruleLength')}`, + upper: t('native.claim.ruleUpper'), + lower: t('native.claim.ruleLower'), + digit: t('native.claim.ruleDigit'), + special: t('native.claim.ruleSpecial'), + } as const; + const ruleItems = (Object.keys(ruleLabels) as (keyof typeof ruleLabels)[]).map((rule) => ({ + rule, + node: el('span', {}, icon(I.check, 13), ruleLabels[rule]), + })); + const passwordRules = el('div', { className: 'pw-rules' }, ...ruleItems.map((r) => r.node)); + const paintRules = () => { + const states = passwordRuleStates(passwordInput.value); + for (const { rule, node } of ruleItems) node.classList.toggle('met', states[rule]); + }; + passwordInput.addEventListener('input', () => { + paintRules(); + passwordHint.hidden = true; + passwordHint.className = 'hint'; + }); // The § 327h agreement, asked on its own before anything is bought. let consent: DeviationsConsent | null = null; @@ -1026,6 +1159,19 @@ function showClaim( void loadConsent(); const submit = el('button', { type: 'submit', className: 'btn' }, t('native.claim.subscribe', { price })); + // What is about to be bought, so nobody pays for a plan they did not mean. + const change = el('button', { type: 'button', className: 'linkbtn' }, t('native.claim.change')); + change.addEventListener('click', () => { + void tap(); + showPlans({ back: true }); + }); + const planSummary = el( + 'div', + { className: 'claim-plan' }, + el('span', {}, el('b', {}, `TREK Cloud ${plan.name}`), el('bdi', { dir: 'ltr' }, price)), + // A purchase already made is not a choice any more. + ...(bought ? [] : [change]) + ); const error = el('p', { className: 'error', role: 'alert' }); const form = el( 'form', @@ -1035,6 +1181,7 @@ function showClaim( emailField, el('p', { className: 'hint' }, t('native.claim.emailHint')), passwordField, + passwordRules, passwordHint, // Our terms govern the instance (terms § 5a): named before the purchase, not only in the confirmation mail. el( @@ -1045,6 +1192,13 @@ function showClaim( externalLink(t('native.plans.ourTerms'), CLOUD_TERMS_URL) ), consentField, + el( + 'div', + { className: 'trust' }, + el('span', {}, icon(I.globe, 15), t('native.claim.trustServers')), + el('span', {}, icon(I.check, 15), t('native.claim.trustCancel')), + el('span', {}, icon(I.shield, 15), t('native.claim.trustNoAds')) + ), submit, error ); @@ -1080,8 +1234,18 @@ function showClaim( const result = await slugStatus(value); if (slugInput.value.trim().toLowerCase() !== value) return; status = result; - slugNote.textContent = noteFor[result] ?? ''; slugNote.className = `hint ${result === 'available' ? 'ok' : result === 'error' ? '' : 'bad'}`; + if (result === 'available') { + // The address as it will be, the moment it is theirs to take. + slugNote.className = 'hint ok slug-free'; + slugNote.replaceChildren( + icon(I.check, 16), + el('bdi', { dir: 'ltr' }, `${value}${cloudSuffix()}`), + el('small', {}, noteFor.available ?? '') + ); + return; + } + slugNote.textContent = noteFor[result] ?? ''; }, 350); }); @@ -1110,10 +1274,12 @@ function showClaim( const password = passwordInput.value; if (passwordProblem(password) || password.length > 128) { shake(passwordField); + passwordHint.hidden = false; passwordHint.className = 'hint bad'; passwordInput.focus(); return; } + passwordHint.hidden = true; passwordHint.className = 'hint'; // Nothing is bought without the agreement on the text the server holds today. if (!consent && !(await loadConsent())) { @@ -1191,6 +1357,7 @@ function showClaim( } else if (redeemed.code === 'password-weak') { // A common password the list on the server knows: the purchase waits, unredeemed, for a better one. shake(passwordField); + passwordHint.hidden = false; passwordHint.className = 'hint bad'; } else { error.textContent = refundedBeforeOpening(redeemed.code, onPlay) @@ -1202,12 +1369,13 @@ function showClaim( show( el( 'div', - {}, + { className: 'screen-quiet' }, ...columns( [ topbar(() => showPlans({ back: true })), el('h1', { className: 'title-sm' }, t('native.claim.title')), el('p', { className: 'sub' }, t('native.claim.sub')), + planSummary, ], [form] ) diff --git a/client/native-shell/shell.css b/client/native-shell/shell.css index 18b2e2750..3520a47c1 100644 --- a/client/native-shell/shell.css +++ b/client/native-shell/shell.css @@ -236,8 +236,9 @@ body::after { background: linear-gradient(180deg, var(--veil-top) 0%, var(--veil-mid) 62%, var(--bg) 100%), url('./img/kyoto.webp') left 28% / cover no-repeat; - -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 40%, transparent 80%); - mask-image: linear-gradient(90deg, #000 0%, #000 40%, transparent 80%); + /* A long, soft hand-over to the grid: a short one read as an edge. */ + -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 22%, transparent 96%); + mask-image: linear-gradient(90deg, #000 0%, #000 22%, transparent 96%); pointer-events: none; z-index: 0; } @@ -246,8 +247,8 @@ body::after { background-position: 0 0, right 28%; - -webkit-mask-image: linear-gradient(270deg, #000 0%, #000 40%, transparent 80%); - mask-image: linear-gradient(270deg, #000 0%, #000 40%, transparent 80%); + -webkit-mask-image: linear-gradient(270deg, #000 0%, #000 22%, transparent 96%); + mask-image: linear-gradient(270deg, #000 0%, #000 22%, transparent 96%); } :root[dir='rtl'] body::before { -webkit-mask-image: radial-gradient(135% 95% at 0% 0%, #000 34%, transparent 80%); @@ -967,7 +968,9 @@ a.linkbtn { .plans { display: flex; flex-direction: column; - gap: 12px; + gap: 14px; + /* Room for the badge that sits on the first card's top edge. */ + padding-top: 4px; } .plan { border: 1px solid var(--edge); @@ -1036,15 +1039,94 @@ a.linkbtn { .plan-feats .off svg { color: var(--faint); } -.plan .btn { +/* A plan is a choice, as a radio button is: the whole card takes the tap. */ +button.plan { + font: inherit; + color: inherit; + text-align: start; width: 100%; - min-height: 48px; - padding: 13px; + cursor: pointer; + position: relative; + padding-inline-start: 48px; } -.plan:not(.featured) .btn { - background: var(--surface-2); - color: var(--ink); - border-color: var(--edge); +.plan-radio { + position: absolute; + inset-inline-start: 16px; + top: 19px; + width: 20px; + height: 20px; + border-radius: 50%; + border: 1.5px solid var(--edge-strong); + transition: + border-color 0.2s, + border-width 0.2s var(--ease); +} +.plan.selected { + border-color: var(--ink); + box-shadow: + 0 0 0 1px var(--ink), + 0 14px 34px -14px var(--wash-1-a85), + 0 2px 8px -3px rgba(0, 0, 0, 0.35); +} +.plan.selected .plan-radio { + border: 6px solid var(--ink); +} +/* Pinned to the card's top edge, so the name and the price keep their line. */ +.plan { + position: relative; +} +.plan-badge { + position: absolute; + top: -9px; + inset-inline-end: 16px; + padding: 2px 9px; + border-radius: 999px; + background: var(--accent); + color: var(--accent-ink); + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.02em; +} +.plan-feats small { + color: var(--faint); + font-size: 0.8rem; + margin-inline-start: 2px; +} +.plan-feats { + margin-bottom: 0; +} + +/* What every plan has, once above the cards. */ +.plans-every { + display: flex; + align-items: center; + gap: 8px; + margin: 0 2px; + color: var(--muted); + font-size: 0.88rem; +} +.plans-every svg { + flex: none; + color: var(--ok); +} + +/* The one button on, held at the foot of the screen over a fade while the plans scroll. */ +.plans-cta { + position: sticky; + bottom: 0; + z-index: 2; + margin: 0 -2px; + padding: 18px 2px calc(env(safe-area-inset-bottom, 0px) + 10px); + background: linear-gradient(to bottom, transparent, var(--bg) 38%); +} +.plans-cta .btn { + width: 100%; + justify-content: space-between; + border-radius: 22px; + padding-inline: 22px 18px; +} +.plans-cta .btn-row small { + opacity: 0.72; } .loading { @@ -1342,3 +1424,135 @@ a.linkbtn { border-color: var(--danger); animation: shake 0.35s; } + +/* ── Claim: what is bought, the address as it will be, the password rules ── */ +/* A form reads on a calm page: the photo steps back while one is on screen. */ +body::after { + transition: opacity 0.4s var(--ease); +} +body.quiet::after { + opacity: 0; +} +.claim-plan { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 4px; + padding: 12px 14px 12px 16px; + border: 1px solid var(--edge); + border-radius: var(--radius); + background: var(--surface); +} +.claim-plan > span { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; +} +.claim-plan b { + font-weight: 650; +} +.claim-plan bdi { + color: var(--muted); + font-size: 0.88rem; +} +.claim-plan .linkbtn { + flex: none; +} +.slug-free { + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; +} +.slug-free bdi { + color: var(--ink); + font-weight: 600; +} +.slug-free small { + font-size: inherit; +} +.pw-rules { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: -4px 0 0; +} +.pw-rules span { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 10px 4px 8px; + border-radius: 999px; + background: var(--surface); + border: 1px solid var(--edge); + color: var(--faint); + font-size: 0.78rem; + transition: + color 0.2s, + background 0.2s, + border-color 0.2s; +} +.pw-rules svg { + opacity: 0.35; + transition: opacity 0.2s; +} +.pw-rules .met { + color: var(--ok); + background: var(--ok-a16); + border-color: transparent; +} +.pw-rules .met svg { + opacity: 1; +} +/* Three short facts right above the purchase. */ +.trust { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 6px 16px; + color: var(--muted); + font-size: 0.8rem; +} +.trust span { + display: inline-flex; + align-items: center; + gap: 6px; +} +.trust svg { + flex: none; + color: var(--ok); +} + +/* ── Cloud: the app itself, and a quieter way out ── */ +.cloud-preview { + margin: 2px -8px 4px; + border-radius: 22px; + overflow: hidden; + aspect-ratio: 720 / 562; + background: var(--surface); + box-shadow: + 0 24px 50px -24px var(--wash-1-a85), + 0 2px 10px -4px rgba(0, 0, 0, 0.35), + inset 0 0 0 1px var(--ink-a4); +} +.cloud-preview img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} +.linkbtn-quiet { + font-size: 0.86rem; + color: var(--faint); + padding-top: 4px; +} +.plan-photos { + display: inline-flex; + align-items: center; + gap: 4px; +} +.plan-feats .plan-photos svg { + color: var(--faint); +} diff --git a/client/src/native/priceValue.test.ts b/client/src/native/priceValue.test.ts new file mode 100644 index 000000000..e2cd8d838 --- /dev/null +++ b/client/src/native/priceValue.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest'; +import { priceValue } from './priceValue'; + +describe('priceValue', () => { + it('reads the store price strings of the usual currencies, in hundredths', () => { + expect(priceValue('9,99 €')).toBe(999); + expect(priceValue('$9.99')).toBe(999); + expect(priceValue('59,99 €')).toBe(5999); + expect(priceValue('¥1,500')).toBe(150000); + expect(priceValue('1.234,50 €')).toBe(123450); + expect(priceValue('CHF 10')).toBe(1000); + }); + + it('has no value for nothing or for a string it cannot read with confidence', () => { + expect(priceValue(undefined)).toBeNull(); + expect(priceValue('')).toBeNull(); + expect(priceValue('free')).toBeNull(); + expect(priceValue('1.2.3')).toBeNull(); + }); +}); diff --git a/client/src/native/priceValue.ts b/client/src/native/priceValue.ts new file mode 100644 index 000000000..0af30b31f --- /dev/null +++ b/client/src/native/priceValue.ts @@ -0,0 +1,14 @@ +/** + * The amount in a store price string such as "9,99 €", "$9.99" or "¥1,500", in + * hundredths; null when it cannot be read with confidence. Only ever compared + * with another price of the same store, never shown. + */ +export function priceValue(label: string | undefined): number | null { + if (!label) return null; + const digits = label.replace(/[^\d.,]/g, ''); + const match = /^(\d{1,3}(?:[.,]\d{3})*|\d+)(?:[.,](\d{1,2}))?$/.exec(digits); + if (!match) return null; + const whole = Number(match[1].replace(/[.,]/g, '')); + const fraction = match[2] ? Number(match[2].padEnd(2, '0')) : 0; + return Number.isFinite(whole) ? whole * 100 + fraction : null; +} diff --git a/shared/src/i18n/ar/native.ts b/shared/src/i18n/ar/native.ts index 122cd820f..8854b2777 100644 --- a/shared/src/i18n/ar/native.ts +++ b/shared/src/i18n/ar/native.ts @@ -54,8 +54,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} مستند بالذكاء الاصطناعي شهريًا', 'native.plans.noAi': 'بلا مستندات ذكاء اصطناعي', 'native.plans.storage': '{n} غيغابايت تخزين', - 'native.plans.unlimited': 'أماكن ورحلات وأشخاص بلا حدود', - 'native.plans.subscribe': 'اشترك', 'native.plans.loadError': 'تعذّر تحميل الخطط. تحقق من اتصالك وحاول مرة أخرى.', 'native.plans.retry': 'حاول مرة أخرى', 'native.plans.soon': 'الاشتراك داخل التطبيق غير متاح بعد.', @@ -87,6 +85,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'يُخصم المبلغ من حساب Google الخاص بك عند التأكيد. يتجدد الاشتراك تلقائيًا ما لم تلغِه قبل نهاية الفترة. يمكنك إدارته أو إلغاؤه في Google Play من الملف الشخصي، الدفعات والاشتراكات، الاشتراكات.', 'native.plans.restoreNoneAndroid': 'لم يُعثر على اشتراك نشط في حساب Google هذا.', + 'native.plans.recommended': 'موصى به', + 'native.plans.everyPlan': 'في كل خطة: أماكن ورحلات وأشخاص بلا حدود', + 'native.plans.continueWith': 'المتابعة مع {plan}', + 'native.claim.change': 'تغيير', + 'native.claim.ruleLength': 'أحرف', + 'native.claim.ruleUpper': 'حرف كبير', + 'native.claim.ruleLower': 'حرف صغير', + 'native.claim.ruleDigit': 'رقم', + 'native.claim.ruleSpecial': 'رمز', + 'native.claim.trustServers': 'خوادم في ألمانيا', + 'native.claim.trustCancel': 'إلغاء في أي وقت', + 'native.claim.trustNoAds': 'بلا إعلانات', 'native.subscribed.title': 'لديك TREK Cloud بالفعل', 'native.subscribed.text': 'يوجد بالفعل اشتراك نشط في TREK Cloud على معرّف Apple هذا. يمكنك الاشتراك من جديد، وبعنوان جديد أيضًا، بعد انتهائه.', diff --git a/shared/src/i18n/az/native.ts b/shared/src/i18n/az/native.ts index 1d0d2b8af..9462f9c1b 100644 --- a/shared/src/i18n/az/native.ts +++ b/shared/src/i18n/az/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': 'ayda {n} süni intellekt sənədi', 'native.plans.noAi': 'Süni intellekt sənədi yoxdur', 'native.plans.storage': '{n} GB yaddaş', - 'native.plans.unlimited': 'Limitsiz yer, səyahət və insan', - 'native.plans.subscribe': 'Abunə ol', 'native.plans.loadError': 'Planları yükləmək mümkün olmadı. Bağlantınızı yoxlayın və yenidən cəhd edin.', 'native.plans.retry': 'Yenidən cəhd edin', 'native.plans.soon': 'Tətbiqdaxili abunə hələ mövcud deyil.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Ödəniş təsdiq etdikdə Google hesabınızdan tutulur. Abunəlik dövr bitməzdən əvvəl ləğv etməsəniz avtomatik yenilənir. Onu Google Play-də Profil, Ödənişlər və abunəliklər, Abunəliklər bölməsində idarə edə və ya ləğv edə bilərsiniz.', 'native.plans.restoreNoneAndroid': 'Bu Google hesabında aktiv abunəlik tapılmadı.', + 'native.plans.recommended': 'Tövsiyə olunur', + 'native.plans.everyPlan': 'Hər planda: limitsiz yerlər, səfərlər və insanlar', + 'native.plans.continueWith': '{plan} ilə davam et', + 'native.claim.change': 'Dəyiş', + 'native.claim.ruleLength': 'simvol', + 'native.claim.ruleUpper': 'Böyük hərf', + 'native.claim.ruleLower': 'Kiçik hərf', + 'native.claim.ruleDigit': 'Rəqəm', + 'native.claim.ruleSpecial': 'Xüsusi simvol', + 'native.claim.trustServers': 'Serverlər Almaniyada', + 'native.claim.trustCancel': 'İstənilən vaxt ləğv et', + 'native.claim.trustNoAds': 'Reklamsız', 'native.subscribed.title': 'Artıq TREK Cloud-unuz var', 'native.subscribed.text': 'Bu Apple ID-də artıq aktiv TREK Cloud abunəliyi var. O bitdikdən sonra yenisini, həm də yeni ünvanla, ala bilərsiniz.', diff --git a/shared/src/i18n/br/native.ts b/shared/src/i18n/br/native.ts index d40145a75..6c3e0b336 100644 --- a/shared/src/i18n/br/native.ts +++ b/shared/src/i18n/br/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} documentos com IA por mês', 'native.plans.noAi': 'Sem documentos com IA', 'native.plans.storage': '{n} GB de armazenamento', - 'native.plans.unlimited': 'Lugares, viagens e pessoas sem limite', - 'native.plans.subscribe': 'Assinar', 'native.plans.loadError': 'Não foi possível carregar os planos. Verifique sua conexão e tente novamente.', 'native.plans.retry': 'Tentar de novo', 'native.plans.soon': 'A assinatura no app ainda não está disponível.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'O pagamento é cobrado na sua conta Google ao confirmar. A assinatura é renovada automaticamente, a menos que você a cancele antes do fim do período. Gerencie ou cancele no Google Play em Perfil, Pagamentos e assinaturas, Assinaturas.', 'native.plans.restoreNoneAndroid': 'Nenhuma assinatura ativa encontrada nesta conta Google.', + 'native.plans.recommended': 'Recomendado', + 'native.plans.everyPlan': 'Em todo plano: lugares, viagens e pessoas ilimitados', + 'native.plans.continueWith': 'Continuar com {plan}', + 'native.claim.change': 'Alterar', + 'native.claim.ruleLength': 'caracteres', + 'native.claim.ruleUpper': 'Maiúscula', + 'native.claim.ruleLower': 'Minúscula', + 'native.claim.ruleDigit': 'Número', + 'native.claim.ruleSpecial': 'Símbolo', + 'native.claim.trustServers': 'Servidores na Alemanha', + 'native.claim.trustCancel': 'Cancele quando quiser', + 'native.claim.trustNoAds': 'Sem anúncios', 'native.subscribed.title': 'Você já tem o TREK Cloud', 'native.subscribed.text': 'Este ID Apple já tem uma assinatura ativa do TREK Cloud. Você pode fazer uma nova, inclusive com um novo endereço, quando ela terminar.', diff --git a/shared/src/i18n/ca/native.ts b/shared/src/i18n/ca/native.ts index 7d33cb522..de9c5cd7f 100644 --- a/shared/src/i18n/ca/native.ts +++ b/shared/src/i18n/ca/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} documents amb IA al mes', 'native.plans.noAi': 'Sense documents amb IA', 'native.plans.storage': '{n} GB d’emmagatzematge', - 'native.plans.unlimited': 'Llocs, viatges i persones sense límit', - 'native.plans.subscribe': 'Subscriu-te', 'native.plans.loadError': 'No s’han pogut carregar els plans. Comprova la connexió i torna-ho a provar.', 'native.plans.retry': 'Torna-ho a provar', 'native.plans.soon': 'La subscripció a l’app encara no està disponible.', @@ -91,6 +89,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'El pagament es carrega al teu compte de Google en confirmar. La subscripció es renova automàticament si no la cancel·les abans que acabi el període. Gestiona-la o cancel·la-la a Google Play, a Perfil, Pagaments i subscripcions, Subscripcions.', 'native.plans.restoreNoneAndroid': 'No s’ha trobat cap subscripció activa en aquest compte de Google.', + 'native.plans.recommended': 'Recomanat', + 'native.plans.everyPlan': 'En cada pla: llocs, viatges i persones il·limitats', + 'native.plans.continueWith': 'Continua amb {plan}', + 'native.claim.change': 'Canvia', + 'native.claim.ruleLength': 'caràcters', + 'native.claim.ruleUpper': 'Majúscula', + 'native.claim.ruleLower': 'Minúscula', + 'native.claim.ruleDigit': 'Número', + 'native.claim.ruleSpecial': 'Símbol', + 'native.claim.trustServers': 'Servidors a Alemanya', + 'native.claim.trustCancel': 'Cancel·la quan vulguis', + 'native.claim.trustNoAds': 'Sense anuncis', 'native.subscribed.title': 'Ja tens TREK Cloud', 'native.subscribed.text': 'Aquest ID d’Apple ja té una subscripció activa a TREK Cloud. En pots contractar una de nova, també amb una adreça nova, quan s’hagi acabat.', diff --git a/shared/src/i18n/cs/native.ts b/shared/src/i18n/cs/native.ts index e70312327..8c680e685 100644 --- a/shared/src/i18n/cs/native.ts +++ b/shared/src/i18n/cs/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} dokumentů s AI měsíčně', 'native.plans.noAi': 'Žádné dokumenty s AI', 'native.plans.storage': '{n} GB úložiště', - 'native.plans.unlimited': 'Neomezeně míst, cest a lidí', - 'native.plans.subscribe': 'Předplatit', 'native.plans.loadError': 'Tarify se nepodařilo načíst. Zkontrolujte připojení a zkuste to znovu.', 'native.plans.retry': 'Zkusit znovu', 'native.plans.soon': 'Předplatné v aplikaci zatím není k dispozici.', @@ -88,6 +86,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Platba se při potvrzení strhne z vašeho účtu Google. Předplatné se automaticky obnovuje, pokud je nezrušíte před koncem období. Spravovat nebo zrušit ho můžete v Google Play v části Profil, Platby a odběry, Odběry.', 'native.plans.restoreNoneAndroid': 'Pro tento účet Google nebylo nalezeno žádné aktivní předplatné.', + 'native.plans.recommended': 'Doporučeno', + 'native.plans.everyPlan': 'V každém tarifu: neomezeně míst, cest a lidí', + 'native.plans.continueWith': 'Pokračovat s {plan}', + 'native.claim.change': 'Změnit', + 'native.claim.ruleLength': 'znaků', + 'native.claim.ruleUpper': 'Velké písmeno', + 'native.claim.ruleLower': 'Malé písmeno', + 'native.claim.ruleDigit': 'Číslice', + 'native.claim.ruleSpecial': 'Speciální znak', + 'native.claim.trustServers': 'Servery v Německu', + 'native.claim.trustCancel': 'Kdykoli zrušitelné', + 'native.claim.trustNoAds': 'Bez reklam', 'native.subscribed.title': 'TREK Cloud už máte', 'native.subscribed.text': 'Toto Apple ID už má aktivní předplatné TREK Cloud. Nové, i s novou adresou, si můžete sjednat, až skončí.', diff --git a/shared/src/i18n/de/native.ts b/shared/src/i18n/de/native.ts index de7c0a809..d6c61ea35 100644 --- a/shared/src/i18n/de/native.ts +++ b/shared/src/i18n/de/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} KI-Dokumente im Monat', 'native.plans.noAi': 'Keine KI-Dokumente', 'native.plans.storage': '{n} GB Speicher', - 'native.plans.unlimited': 'Unbegrenzt Orte, Reisen & Personen', - 'native.plans.subscribe': 'Abonnieren', 'native.plans.loadError': 'Die Tarife konnten nicht geladen werden. Prüfe deine Verbindung und versuch es erneut.', 'native.plans.retry': 'Erneut versuchen', 'native.plans.soon': 'Abschließen in der App ist noch nicht verfügbar.', @@ -91,6 +89,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Die Zahlung wird bei Bestätigung über dein Google-Konto abgerechnet. Das Abo verlängert sich automatisch, wenn du es nicht vor Ablauf des Zeitraums kündigst. Verwalten und kündigen kannst du es in Google Play unter Profil, Zahlungen und Abos, Abos.', 'native.plans.restoreNoneAndroid': 'Für dieses Google-Konto wurde kein aktives Abo gefunden.', + 'native.plans.recommended': 'Empfohlen', + 'native.plans.everyPlan': 'In jedem Tarif: unbegrenzt Orte, Reisen und Personen', + 'native.plans.continueWith': 'Weiter mit {plan}', + 'native.claim.change': 'Ändern', + 'native.claim.ruleLength': 'Zeichen', + 'native.claim.ruleUpper': 'Großbuchstabe', + 'native.claim.ruleLower': 'Kleinbuchstabe', + 'native.claim.ruleDigit': 'Zahl', + 'native.claim.ruleSpecial': 'Sonderzeichen', + 'native.claim.trustServers': 'Server in Deutschland', + 'native.claim.trustCancel': 'Jederzeit kündbar', + 'native.claim.trustNoAds': 'Keine Werbung', 'native.subscribed.title': 'Du hast schon TREK Cloud', 'native.subscribed.text': 'Über diese Apple-ID läuft bereits ein TREK-Cloud-Abo. Ein neues kannst du abschließen, auch mit neuer Adresse, sobald es beendet ist.', diff --git a/shared/src/i18n/en/native.ts b/shared/src/i18n/en/native.ts index 0a0646e75..e0c356f8b 100644 --- a/shared/src/i18n/en/native.ts +++ b/shared/src/i18n/en/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} AI documents a month', 'native.plans.noAi': 'No AI documents', 'native.plans.storage': '{n} GB storage', - 'native.plans.unlimited': 'Unlimited places, trips & people', - 'native.plans.subscribe': 'Subscribe', 'native.plans.loadError': 'Could not load the plans. Check your connection and try again.', 'native.plans.retry': 'Try again', 'native.plans.soon': 'Subscribing in the app is not available yet.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Payment is charged to your Google account when you confirm. The subscription renews automatically unless you cancel it before the period ends. Manage or cancel it in Google Play under Profile, Payments and subscriptions, Subscriptions.', 'native.plans.restoreNoneAndroid': 'No active subscription found on this Google account.', + 'native.plans.recommended': 'Recommended', + 'native.plans.everyPlan': 'In every plan: unlimited places, trips and people', + 'native.plans.continueWith': 'Continue with {plan}', + 'native.claim.change': 'Change', + 'native.claim.ruleLength': 'characters', + 'native.claim.ruleUpper': 'Uppercase', + 'native.claim.ruleLower': 'Lowercase', + 'native.claim.ruleDigit': 'Number', + 'native.claim.ruleSpecial': 'Symbol', + 'native.claim.trustServers': 'Servers in Germany', + 'native.claim.trustCancel': 'Cancel anytime', + 'native.claim.trustNoAds': 'No ads', 'native.subscribed.title': 'You already have TREK Cloud', 'native.subscribed.text': 'This Apple ID already has an active TREK Cloud subscription. You can take out a new one, at a new address too, once it has ended.', diff --git a/shared/src/i18n/es/native.ts b/shared/src/i18n/es/native.ts index 2b69a5144..a7af561e0 100644 --- a/shared/src/i18n/es/native.ts +++ b/shared/src/i18n/es/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} documentos con IA al mes', 'native.plans.noAi': 'Sin documentos con IA', 'native.plans.storage': '{n} GB de almacenamiento', - 'native.plans.unlimited': 'Lugares, viajes y personas sin límite', - 'native.plans.subscribe': 'Suscribirse', 'native.plans.loadError': 'No se han podido cargar los planes. Revisa tu conexión e inténtalo de nuevo.', 'native.plans.retry': 'Reintentar', 'native.plans.soon': 'La suscripción dentro de la app aún no está disponible.', @@ -91,6 +89,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'El pago se carga a tu cuenta de Google al confirmar. La suscripción se renueva automáticamente salvo que la canceles antes de que termine el periodo. Gestiónala o cancélala en Google Play, en Perfil, Pagos y suscripciones, Suscripciones.', 'native.plans.restoreNoneAndroid': 'No se encontró ninguna suscripción activa en esta cuenta de Google.', + 'native.plans.recommended': 'Recomendado', + 'native.plans.everyPlan': 'En todos los planes: lugares, viajes y personas ilimitados', + 'native.plans.continueWith': 'Continuar con {plan}', + 'native.claim.change': 'Cambiar', + 'native.claim.ruleLength': 'caracteres', + 'native.claim.ruleUpper': 'Mayúscula', + 'native.claim.ruleLower': 'Minúscula', + 'native.claim.ruleDigit': 'Número', + 'native.claim.ruleSpecial': 'Símbolo', + 'native.claim.trustServers': 'Servidores en Alemania', + 'native.claim.trustCancel': 'Cancela cuando quieras', + 'native.claim.trustNoAds': 'Sin anuncios', 'native.subscribed.title': 'Ya tienes TREK Cloud', 'native.subscribed.text': 'Este ID de Apple ya tiene una suscripción activa a TREK Cloud. Podrás contratar una nueva, también con una dirección nueva, cuando haya terminado.', diff --git a/shared/src/i18n/et/native.ts b/shared/src/i18n/et/native.ts index af9188c51..f169c2823 100644 --- a/shared/src/i18n/et/native.ts +++ b/shared/src/i18n/et/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} AI-dokumenti kuus', 'native.plans.noAi': 'AI-dokumente pole', 'native.plans.storage': '{n} GB mahtu', - 'native.plans.unlimited': 'Piiramatult kohti, reise ja inimesi', - 'native.plans.subscribe': 'Telli', 'native.plans.loadError': 'Pakette ei õnnestunud laadida. Kontrolli ühendust ja proovi uuesti.', 'native.plans.retry': 'Proovi uuesti', 'native.plans.soon': 'Rakendusesisene tellimine pole veel saadaval.', @@ -89,6 +87,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': "Makse võetakse kinnitamisel sinu Google'i kontolt. Tellimus uueneb automaatselt, kui sa ei tühista seda enne perioodi lõppu. Halda või tühista seda Google Plays: Profiil, Maksed ja tellimused, Tellimused.", 'native.plans.restoreNoneAndroid': "Selle Google'i kontoga ei leitud aktiivset tellimust.", + 'native.plans.recommended': 'Soovitatud', + 'native.plans.everyPlan': 'Igas paketis: piiramatult kohti, reise ja inimesi', + 'native.plans.continueWith': 'Jätka paketiga {plan}', + 'native.claim.change': 'Muuda', + 'native.claim.ruleLength': 'märki', + 'native.claim.ruleUpper': 'Suurtäht', + 'native.claim.ruleLower': 'Väiketäht', + 'native.claim.ruleDigit': 'Arv', + 'native.claim.ruleSpecial': 'Erimärk', + 'native.claim.trustServers': 'Serverid Saksamaal', + 'native.claim.trustCancel': 'Tühista igal ajal', + 'native.claim.trustNoAds': 'Reklaamivaba', 'native.subscribed.title': 'Sul on juba TREK Cloud', 'native.subscribed.text': 'Sellel Apple ID-l on juba aktiivne TREK Cloudi tellimus. Uue, ka uue aadressiga, saad võtta pärast selle lõppemist.', diff --git a/shared/src/i18n/fr/native.ts b/shared/src/i18n/fr/native.ts index ba7dd0a69..c562380ad 100644 --- a/shared/src/i18n/fr/native.ts +++ b/shared/src/i18n/fr/native.ts @@ -57,8 +57,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} documents IA par mois', 'native.plans.noAi': 'Pas de documents IA', 'native.plans.storage': '{n} Go de stockage', - 'native.plans.unlimited': 'Lieux, voyages et personnes sans limite', - 'native.plans.subscribe': 'S’abonner', 'native.plans.loadError': 'Impossible de charger les forfaits. Vérifiez votre connexion et réessayez.', 'native.plans.retry': 'Réessayer', 'native.plans.soon': 'L’abonnement dans l’app n’est pas encore disponible.', @@ -92,6 +90,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Le paiement est débité de votre compte Google lors de la confirmation. L’abonnement se renouvelle automatiquement, sauf si vous le résiliez avant la fin de la période. Gérez-le ou résiliez-le dans Google Play sous Profil, Paiements et abonnements, Abonnements.', 'native.plans.restoreNoneAndroid': 'Aucun abonnement actif trouvé pour ce compte Google.', + 'native.plans.recommended': 'Recommandé', + 'native.plans.everyPlan': 'Dans chaque formule : lieux, voyages et personnes illimités', + 'native.plans.continueWith': 'Continuer avec {plan}', + 'native.claim.change': 'Modifier', + 'native.claim.ruleLength': 'caractères', + 'native.claim.ruleUpper': 'Majuscule', + 'native.claim.ruleLower': 'Minuscule', + 'native.claim.ruleDigit': 'Chiffre', + 'native.claim.ruleSpecial': 'Symbole', + 'native.claim.trustServers': 'Serveurs en Allemagne', + 'native.claim.trustCancel': 'Résiliable à tout moment', + 'native.claim.trustNoAds': 'Sans publicité', 'native.subscribed.title': 'Vous avez déjà TREK Cloud', 'native.subscribed.text': 'Cet identifiant Apple a déjà un abonnement TREK Cloud actif. Vous pourrez en souscrire un nouveau, avec une nouvelle adresse aussi, une fois celui-ci terminé.', diff --git a/shared/src/i18n/gr/native.ts b/shared/src/i18n/gr/native.ts index bfad16a9c..0c6dae33f 100644 --- a/shared/src/i18n/gr/native.ts +++ b/shared/src/i18n/gr/native.ts @@ -57,8 +57,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} έγγραφα AI τον μήνα', 'native.plans.noAi': 'Χωρίς έγγραφα AI', 'native.plans.storage': '{n} GB αποθηκευτικού χώρου', - 'native.plans.unlimited': 'Απεριόριστα μέρη, ταξίδια και άτομα', - 'native.plans.subscribe': 'Εγγραφή', 'native.plans.loadError': 'Δεν ήταν δυνατή η φόρτωση των πλάνων. Ελέγξτε τη σύνδεσή σας και δοκιμάστε ξανά.', 'native.plans.retry': 'Δοκιμάστε ξανά', 'native.plans.soon': 'Η συνδρομή μέσα από την εφαρμογή δεν είναι ακόμη διαθέσιμη.', @@ -92,6 +90,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Η πληρωμή χρεώνεται στον λογαριασμό σας Google κατά την επιβεβαίωση. Η συνδρομή ανανεώνεται αυτόματα, εκτός αν την ακυρώσετε πριν λήξει η περίοδος. Διαχειριστείτε ή ακυρώστε τη στο Google Play, στο Προφίλ, Πληρωμές και συνδρομές, Συνδρομές.', 'native.plans.restoreNoneAndroid': 'Δεν βρέθηκε ενεργή συνδρομή σε αυτόν τον λογαριασμό Google.', + 'native.plans.recommended': 'Προτείνεται', + 'native.plans.everyPlan': 'Σε κάθε πακέτο: απεριόριστα μέρη, ταξίδια και άτομα', + 'native.plans.continueWith': 'Συνέχεια με {plan}', + 'native.claim.change': 'Αλλαγή', + 'native.claim.ruleLength': 'χαρακτήρες', + 'native.claim.ruleUpper': 'Κεφαλαίο', + 'native.claim.ruleLower': 'Πεζό', + 'native.claim.ruleDigit': 'Αριθμός', + 'native.claim.ruleSpecial': 'Σύμβολο', + 'native.claim.trustServers': 'Διακομιστές στη Γερμανία', + 'native.claim.trustCancel': 'Ακύρωση οποτεδήποτε', + 'native.claim.trustNoAds': 'Χωρίς διαφημίσεις', 'native.subscribed.title': 'Έχετε ήδη TREK Cloud', 'native.subscribed.text': 'Αυτό το Apple ID έχει ήδη ενεργή συνδρομή στο TREK Cloud. Μπορείτε να κάνετε νέα, και με νέα διεύθυνση, όταν λήξει.', diff --git a/shared/src/i18n/he/native.ts b/shared/src/i18n/he/native.ts index 0945ac404..69718dd8c 100644 --- a/shared/src/i18n/he/native.ts +++ b/shared/src/i18n/he/native.ts @@ -54,8 +54,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} מסמכי AI בחודש', 'native.plans.noAi': 'בלי מסמכי AI', 'native.plans.storage': '{n} GB אחסון', - 'native.plans.unlimited': 'מקומות, טיולים ואנשים ללא הגבלה', - 'native.plans.subscribe': 'הרשמה', 'native.plans.loadError': 'לא ניתן לטעון את התוכניות. בדקו את החיבור ונסו שוב.', 'native.plans.retry': 'נסו שוב', 'native.plans.soon': 'הרשמה בתוך האפליקציה עדיין לא זמינה.', @@ -87,6 +85,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'התשלום יחויב בחשבון Google שלכם עם האישור. המינוי מתחדש אוטומטית אלא אם תבטלו אותו לפני סוף התקופה. אפשר לנהל או לבטל אותו ב-Google Play תחת פרופיל, תשלומים ומינויים, מינויים.', 'native.plans.restoreNoneAndroid': 'לא נמצא מינוי פעיל בחשבון Google הזה.', + 'native.plans.recommended': 'מומלץ', + 'native.plans.everyPlan': 'בכל מסלול: מקומות, טיולים ואנשים ללא הגבלה', + 'native.plans.continueWith': 'המשך עם {plan}', + 'native.claim.change': 'שינוי', + 'native.claim.ruleLength': 'תווים', + 'native.claim.ruleUpper': 'אות גדולה', + 'native.claim.ruleLower': 'אות קטנה', + 'native.claim.ruleDigit': 'ספרה', + 'native.claim.ruleSpecial': 'סימן מיוחד', + 'native.claim.trustServers': 'שרתים בגרמניה', + 'native.claim.trustCancel': 'ביטול בכל עת', + 'native.claim.trustNoAds': 'ללא פרסומות', 'native.subscribed.title': 'כבר יש לך TREK Cloud', 'native.subscribed.text': 'ל-Apple ID הזה כבר יש מינוי פעיל ל-TREK Cloud. אפשר להירשם למינוי חדש, גם בכתובת חדשה, אחרי שהוא יסתיים.', diff --git a/shared/src/i18n/hu/native.ts b/shared/src/i18n/hu/native.ts index 2d1c881b9..9687e732e 100644 --- a/shared/src/i18n/hu/native.ts +++ b/shared/src/i18n/hu/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': 'havi {n} MI-dokumentum', 'native.plans.noAi': 'Nincs MI-dokumentum', 'native.plans.storage': '{n} GB tárhely', - 'native.plans.unlimited': 'Korlátlan hely, utazás és személy', - 'native.plans.subscribe': 'Előfizetés', 'native.plans.loadError': 'A csomagokat nem sikerült betölteni. Ellenőrizd a kapcsolatot, és próbáld újra.', 'native.plans.retry': 'Újra', 'native.plans.soon': 'Az appon belüli előfizetés még nem érhető el.', @@ -91,6 +89,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'A díjat a megerősítéskor a Google-fiókodra terheljük. Az előfizetés automatikusan megújul, ha nem mondod le az időszak vége előtt. Kezelni vagy lemondani a Google Playben tudod: Profil, Fizetések és előfizetések, Előfizetések.', 'native.plans.restoreNoneAndroid': 'Ehhez a Google-fiókhoz nem található aktív előfizetés.', + 'native.plans.recommended': 'Ajánlott', + 'native.plans.everyPlan': 'Minden csomagban: korlátlan hely, utazás és résztvevő', + 'native.plans.continueWith': 'Tovább ezzel: {plan}', + 'native.claim.change': 'Módosítás', + 'native.claim.ruleLength': 'karakter', + 'native.claim.ruleUpper': 'Nagybetű', + 'native.claim.ruleLower': 'Kisbetű', + 'native.claim.ruleDigit': 'Szám', + 'native.claim.ruleSpecial': 'Speciális karakter', + 'native.claim.trustServers': 'Szerverek Németországban', + 'native.claim.trustCancel': 'Bármikor lemondható', + 'native.claim.trustNoAds': 'Reklámmentes', 'native.subscribed.title': 'Már van TREK Cloud-előfizetésed', 'native.subscribed.text': 'Ehhez az Apple ID-hoz már tartozik aktív TREK Cloud-előfizetés. Újat, akár új címmel is, akkor köthetsz, ha ez lejárt.', diff --git a/shared/src/i18n/id/native.ts b/shared/src/i18n/id/native.ts index 03437fc73..de869cef3 100644 --- a/shared/src/i18n/id/native.ts +++ b/shared/src/i18n/id/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} dokumen AI per bulan', 'native.plans.noAi': 'Tanpa dokumen AI', 'native.plans.storage': 'Penyimpanan {n} GB', - 'native.plans.unlimited': 'Tempat, perjalanan & orang tanpa batas', - 'native.plans.subscribe': 'Berlangganan', 'native.plans.loadError': 'Tidak dapat memuat paket. Periksa koneksi Anda dan coba lagi.', 'native.plans.retry': 'Coba lagi', 'native.plans.soon': 'Berlangganan di aplikasi belum tersedia.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Pembayaran ditagihkan ke akun Google Anda saat konfirmasi. Langganan diperpanjang otomatis kecuali Anda membatalkannya sebelum periode berakhir. Kelola atau batalkan di Google Play pada Profil, Pembayaran & langganan, Langganan.', 'native.plans.restoreNoneAndroid': 'Tidak ditemukan langganan aktif di akun Google ini.', + 'native.plans.recommended': 'Disarankan', + 'native.plans.everyPlan': 'Di setiap paket: tempat, perjalanan, dan orang tanpa batas', + 'native.plans.continueWith': 'Lanjut dengan {plan}', + 'native.claim.change': 'Ubah', + 'native.claim.ruleLength': 'karakter', + 'native.claim.ruleUpper': 'Huruf besar', + 'native.claim.ruleLower': 'Huruf kecil', + 'native.claim.ruleDigit': 'Angka', + 'native.claim.ruleSpecial': 'Simbol', + 'native.claim.trustServers': 'Server di Jerman', + 'native.claim.trustCancel': 'Batalkan kapan saja', + 'native.claim.trustNoAds': 'Tanpa iklan', 'native.subscribed.title': 'Kamu sudah punya TREK Cloud', 'native.subscribed.text': 'Apple ID ini sudah memiliki langganan TREK Cloud yang aktif. Kamu bisa berlangganan lagi, juga dengan alamat baru, setelah langganan ini berakhir.', diff --git a/shared/src/i18n/it/native.ts b/shared/src/i18n/it/native.ts index 0d00fb65d..375e07d62 100644 --- a/shared/src/i18n/it/native.ts +++ b/shared/src/i18n/it/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} documenti con IA al mese', 'native.plans.noAi': 'Nessun documento con IA', 'native.plans.storage': '{n} GB di spazio', - 'native.plans.unlimited': 'Luoghi, viaggi e persone illimitati', - 'native.plans.subscribe': 'Abbonati', 'native.plans.loadError': 'Impossibile caricare i piani. Controlla la connessione e riprova.', 'native.plans.retry': 'Riprova', 'native.plans.soon': 'L’abbonamento nell’app non è ancora disponibile.', @@ -91,6 +89,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Il pagamento viene addebitato sul tuo account Google alla conferma. L’abbonamento si rinnova automaticamente se non lo annulli prima della fine del periodo. Gestiscilo o annullalo in Google Play da Profilo, Pagamenti e abbonamenti, Abbonamenti.', 'native.plans.restoreNoneAndroid': 'Nessun abbonamento attivo trovato per questo account Google.', + 'native.plans.recommended': 'Consigliato', + 'native.plans.everyPlan': 'In ogni piano: luoghi, viaggi e persone illimitati', + 'native.plans.continueWith': 'Continua con {plan}', + 'native.claim.change': 'Cambia', + 'native.claim.ruleLength': 'caratteri', + 'native.claim.ruleUpper': 'Maiuscola', + 'native.claim.ruleLower': 'Minuscola', + 'native.claim.ruleDigit': 'Numero', + 'native.claim.ruleSpecial': 'Simbolo', + 'native.claim.trustServers': 'Server in Germania', + 'native.claim.trustCancel': 'Disdici quando vuoi', + 'native.claim.trustNoAds': 'Senza pubblicità', 'native.subscribed.title': 'Hai già TREK Cloud', 'native.subscribed.text': 'Questo ID Apple ha già un abbonamento TREK Cloud attivo. Potrai sottoscriverne uno nuovo, anche con un nuovo indirizzo, quando sarà terminato.', diff --git a/shared/src/i18n/ja/native.ts b/shared/src/i18n/ja/native.ts index 8080abd44..792bc0d5e 100644 --- a/shared/src/i18n/ja/native.ts +++ b/shared/src/i18n/ja/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '月に{n}件のAIドキュメント', 'native.plans.noAi': 'AIドキュメントなし', 'native.plans.storage': '{n} GB のストレージ', - 'native.plans.unlimited': '場所・旅行・人数は無制限', - 'native.plans.subscribe': '登録', 'native.plans.loadError': 'プランを読み込めませんでした。接続を確認してもう一度お試しください。', 'native.plans.retry': 'もう一度試す', 'native.plans.soon': 'アプリ内での登録はまだご利用いただけません。', @@ -89,6 +87,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': '確認時に Google アカウントに請求されます。期間終了前に解約しない限り、サブスクリプションは自動更新されます。Google Play の[プロフィール][お支払いと定期購入][定期購入]で管理または解約できます。', 'native.plans.restoreNoneAndroid': 'この Google アカウントには有効なサブスクリプションがありません。', + 'native.plans.recommended': 'おすすめ', + 'native.plans.everyPlan': 'すべてのプランで:場所・旅行・メンバーが無制限', + 'native.plans.continueWith': '{plan} で続ける', + 'native.claim.change': '変更', + 'native.claim.ruleLength': '文字', + 'native.claim.ruleUpper': '大文字', + 'native.claim.ruleLower': '小文字', + 'native.claim.ruleDigit': '数字', + 'native.claim.ruleSpecial': '記号', + 'native.claim.trustServers': 'ドイツのサーバー', + 'native.claim.trustCancel': 'いつでも解約可能', + 'native.claim.trustNoAds': '広告なし', 'native.subscribed.title': 'TREK Cloud はすでにご利用中です', 'native.subscribed.text': 'この Apple ID にはすでに有効な TREK Cloud のサブスクリプションがあります。終了後に、新しいアドレスでも新たに申し込めます。', diff --git a/shared/src/i18n/ko/native.ts b/shared/src/i18n/ko/native.ts index 0acf466a7..51e0b2cad 100644 --- a/shared/src/i18n/ko/native.ts +++ b/shared/src/i18n/ko/native.ts @@ -53,8 +53,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '월 {n}개의 AI 문서', 'native.plans.noAi': 'AI 문서 없음', 'native.plans.storage': '{n} GB 저장공간', - 'native.plans.unlimited': '장소, 여행, 인원 무제한', - 'native.plans.subscribe': '구독', 'native.plans.loadError': '플랜을 불러오지 못했습니다. 연결을 확인하고 다시 시도하세요.', 'native.plans.retry': '다시 시도', 'native.plans.soon': '앱 내 구독은 아직 이용할 수 없습니다.', @@ -86,6 +84,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': '확인 시 Google 계정으로 결제됩니다. 기간이 끝나기 전에 취소하지 않으면 구독이 자동으로 갱신됩니다. Google Play의 프로필, 결제 및 정기 결제, 정기 결제에서 관리하거나 취소할 수 있습니다.', 'native.plans.restoreNoneAndroid': '이 Google 계정에서 활성 구독을 찾을 수 없습니다.', + 'native.plans.recommended': '추천', + 'native.plans.everyPlan': '모든 요금제: 장소, 여행, 인원 무제한', + 'native.plans.continueWith': '{plan}(으)로 계속', + 'native.claim.change': '변경', + 'native.claim.ruleLength': '자', + 'native.claim.ruleUpper': '대문자', + 'native.claim.ruleLower': '소문자', + 'native.claim.ruleDigit': '숫자', + 'native.claim.ruleSpecial': '특수문자', + 'native.claim.trustServers': '독일 내 서버', + 'native.claim.trustCancel': '언제든 해지 가능', + 'native.claim.trustNoAds': '광고 없음', 'native.subscribed.title': '이미 TREK Cloud를 이용 중입니다', 'native.subscribed.text': '이 Apple ID에는 이미 활성 TREK Cloud 구독이 있습니다. 구독이 끝나면 새 주소로도 새로 구독할 수 있습니다.', diff --git a/shared/src/i18n/nl/native.ts b/shared/src/i18n/nl/native.ts index 6fd436949..a1b6f8d3f 100644 --- a/shared/src/i18n/nl/native.ts +++ b/shared/src/i18n/nl/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} AI-documenten per maand', 'native.plans.noAi': 'Geen AI-documenten', 'native.plans.storage': '{n} GB opslag', - 'native.plans.unlimited': 'Onbeperkt plekken, reizen & mensen', - 'native.plans.subscribe': 'Abonneren', 'native.plans.loadError': 'Kan de abonnementen niet laden. Controleer je verbinding en probeer het opnieuw.', 'native.plans.retry': 'Opnieuw proberen', 'native.plans.soon': 'Abonneren in de app is nog niet beschikbaar.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'De betaling wordt bij bevestiging via je Google-account in rekening gebracht. Het abonnement wordt automatisch verlengd, tenzij je het vóór het einde van de periode opzegt. Beheer of zeg het op in Google Play onder Profiel, Betalingen en abonnementen, Abonnementen.', 'native.plans.restoreNoneAndroid': 'Geen actief abonnement gevonden voor dit Google-account.', + 'native.plans.recommended': 'Aanbevolen', + 'native.plans.everyPlan': 'In elk abonnement: onbeperkt plekken, reizen en personen', + 'native.plans.continueWith': 'Doorgaan met {plan}', + 'native.claim.change': 'Wijzigen', + 'native.claim.ruleLength': 'tekens', + 'native.claim.ruleUpper': 'Hoofdletter', + 'native.claim.ruleLower': 'Kleine letter', + 'native.claim.ruleDigit': 'Cijfer', + 'native.claim.ruleSpecial': 'Symbool', + 'native.claim.trustServers': 'Servers in Duitsland', + 'native.claim.trustCancel': 'Altijd opzegbaar', + 'native.claim.trustNoAds': 'Geen advertenties', 'native.subscribed.title': 'Je hebt al TREK Cloud', 'native.subscribed.text': 'Deze Apple ID heeft al een actief TREK Cloud-abonnement. Je kunt een nieuw afsluiten, ook met een nieuw adres, zodra het is afgelopen.', diff --git a/shared/src/i18n/pl/native.ts b/shared/src/i18n/pl/native.ts index e8772d877..ea6f932d6 100644 --- a/shared/src/i18n/pl/native.ts +++ b/shared/src/i18n/pl/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} dokumentów AI miesięcznie', 'native.plans.noAi': 'Bez dokumentów AI', 'native.plans.storage': '{n} GB miejsca', - 'native.plans.unlimited': 'Bez limitu miejsc, podróży i osób', - 'native.plans.subscribe': 'Subskrybuj', 'native.plans.loadError': 'Nie udało się wczytać planów. Sprawdź połączenie i spróbuj ponownie.', 'native.plans.retry': 'Spróbuj ponownie', 'native.plans.soon': 'Subskrypcja w aplikacji nie jest jeszcze dostępna.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Płatność zostanie pobrana z Twojego konta Google po potwierdzeniu. Subskrypcja odnawia się automatycznie, jeśli nie anulujesz jej przed końcem okresu. Zarządzaj nią lub ją anuluj w Google Play w sekcji Profil, Płatności i subskrypcje, Subskrypcje.', 'native.plans.restoreNoneAndroid': 'Nie znaleziono aktywnej subskrypcji na tym koncie Google.', + 'native.plans.recommended': 'Polecany', + 'native.plans.everyPlan': 'W każdym planie: bez limitu miejsc, podróży i osób', + 'native.plans.continueWith': 'Dalej z planem {plan}', + 'native.claim.change': 'Zmień', + 'native.claim.ruleLength': 'znaków', + 'native.claim.ruleUpper': 'Wielka litera', + 'native.claim.ruleLower': 'Mała litera', + 'native.claim.ruleDigit': 'Cyfra', + 'native.claim.ruleSpecial': 'Znak specjalny', + 'native.claim.trustServers': 'Serwery w Niemczech', + 'native.claim.trustCancel': 'Anulujesz w każdej chwili', + 'native.claim.trustNoAds': 'Bez reklam', 'native.subscribed.title': 'Masz już TREK Cloud', 'native.subscribed.text': 'To Apple ID ma już aktywną subskrypcję TREK Cloud. Nową, także z nowym adresem, możesz wykupić, gdy ta się zakończy.', diff --git a/shared/src/i18n/ru/native.ts b/shared/src/i18n/ru/native.ts index 20ab6595f..52c6f911b 100644 --- a/shared/src/i18n/ru/native.ts +++ b/shared/src/i18n/ru/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} ИИ-документов в месяц', 'native.plans.noAi': 'Без ИИ-документов', 'native.plans.storage': '{n} ГБ хранилища', - 'native.plans.unlimited': 'Места, поездки и люди без лимита', - 'native.plans.subscribe': 'Подписаться', 'native.plans.loadError': 'Не удалось загрузить тарифы. Проверьте подключение и повторите попытку.', 'native.plans.retry': 'Повторить', 'native.plans.soon': 'Оформление подписки в приложении пока недоступно.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Оплата списывается с вашего аккаунта Google при подтверждении. Подписка продлевается автоматически, если вы не отмените её до конца периода. Управлять ею или отменить можно в Google Play: Профиль, Платежи и подписки, Подписки.', 'native.plans.restoreNoneAndroid': 'Для этого аккаунта Google активная подписка не найдена.', + 'native.plans.recommended': 'Рекомендуем', + 'native.plans.everyPlan': 'В каждом тарифе: без ограничений по местам, поездкам и людям', + 'native.plans.continueWith': 'Продолжить с {plan}', + 'native.claim.change': 'Изменить', + 'native.claim.ruleLength': 'символов', + 'native.claim.ruleUpper': 'Заглавная буква', + 'native.claim.ruleLower': 'Строчная буква', + 'native.claim.ruleDigit': 'Цифра', + 'native.claim.ruleSpecial': 'Спецсимвол', + 'native.claim.trustServers': 'Серверы в Германии', + 'native.claim.trustCancel': 'Отмена в любой момент', + 'native.claim.trustNoAds': 'Без рекламы', 'native.subscribed.title': 'У вас уже есть TREK Cloud', 'native.subscribed.text': 'На этом Apple ID уже есть активная подписка TREK Cloud. Оформить новую, в том числе с новым адресом, можно после её окончания.', diff --git a/shared/src/i18n/sk/native.ts b/shared/src/i18n/sk/native.ts index 4ee179053..266a96785 100644 --- a/shared/src/i18n/sk/native.ts +++ b/shared/src/i18n/sk/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} dokumentov s AI mesačne', 'native.plans.noAi': 'Žiadne dokumenty s AI', 'native.plans.storage': '{n} GB úložiska', - 'native.plans.unlimited': 'Neobmedzené miesta, cesty a ľudia', - 'native.plans.subscribe': 'Predplatiť', 'native.plans.loadError': 'Tarify sa nepodarilo načítať. Skontrolujte pripojenie a skúste to znova.', 'native.plans.retry': 'Skúsiť znova', 'native.plans.soon': 'Predplatné v aplikácii zatiaľ nie je dostupné.', @@ -89,6 +87,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Platba sa pri potvrdení strhne z vášho účtu Google. Predplatné sa automaticky obnovuje, ak ho nezrušíte pred koncom obdobia. Spravovať alebo zrušiť ho môžete v Google Play v časti Profil, Platby a odbery, Odbery.', 'native.plans.restoreNoneAndroid': 'Pre tento účet Google sa nenašlo žiadne aktívne predplatné.', + 'native.plans.recommended': 'Odporúčané', + 'native.plans.everyPlan': 'V každom balíku: neobmedzene miest, ciest a ľudí', + 'native.plans.continueWith': 'Pokračovať s {plan}', + 'native.claim.change': 'Zmeniť', + 'native.claim.ruleLength': 'znakov', + 'native.claim.ruleUpper': 'Veľké písmeno', + 'native.claim.ruleLower': 'Malé písmeno', + 'native.claim.ruleDigit': 'Číslica', + 'native.claim.ruleSpecial': 'Špeciálny znak', + 'native.claim.trustServers': 'Servery v Nemecku', + 'native.claim.trustCancel': 'Kedykoľvek zrušiteľné', + 'native.claim.trustNoAds': 'Bez reklám', 'native.subscribed.title': 'TREK Cloud už máte', 'native.subscribed.text': 'Toto Apple ID už má aktívne predplatné TREK Cloud. Nové, aj s novou adresou, si môžete objednať, keď skončí.', diff --git a/shared/src/i18n/sv/native.ts b/shared/src/i18n/sv/native.ts index f6ed7a98d..86eea38e2 100644 --- a/shared/src/i18n/sv/native.ts +++ b/shared/src/i18n/sv/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} AI-dokument i månaden', 'native.plans.noAi': 'Inga AI-dokument', 'native.plans.storage': '{n} GB lagring', - 'native.plans.unlimited': 'Obegränsat antal platser, resor & personer', - 'native.plans.subscribe': 'Prenumerera', 'native.plans.loadError': 'Det gick inte att läsa in abonnemangen. Kontrollera anslutningen och försök igen.', 'native.plans.retry': 'Försök igen', 'native.plans.soon': 'Prenumeration i appen är inte tillgänglig ännu.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Betalningen debiteras ditt Google-konto när du bekräftar. Prenumerationen förnyas automatiskt om du inte säger upp den innan perioden tar slut. Hantera eller säg upp den i Google Play under Profil, Betalningar och prenumerationer, Prenumerationer.', 'native.plans.restoreNoneAndroid': 'Ingen aktiv prenumeration hittades för det här Google-kontot.', + 'native.plans.recommended': 'Rekommenderas', + 'native.plans.everyPlan': 'I varje plan: obegränsat med platser, resor och personer', + 'native.plans.continueWith': 'Fortsätt med {plan}', + 'native.claim.change': 'Ändra', + 'native.claim.ruleLength': 'tecken', + 'native.claim.ruleUpper': 'Versal', + 'native.claim.ruleLower': 'Gemen', + 'native.claim.ruleDigit': 'Siffra', + 'native.claim.ruleSpecial': 'Specialtecken', + 'native.claim.trustServers': 'Servrar i Tyskland', + 'native.claim.trustCancel': 'Avsluta när du vill', + 'native.claim.trustNoAds': 'Ingen reklam', 'native.subscribed.title': 'Du har redan TREK Cloud', 'native.subscribed.text': 'Det här Apple-ID:t har redan en aktiv TREK Cloud-prenumeration. Du kan teckna en ny, även med en ny adress, när den har upphört.', diff --git a/shared/src/i18n/th/native.ts b/shared/src/i18n/th/native.ts index bc1bc5a1b..1d27aca34 100644 --- a/shared/src/i18n/th/native.ts +++ b/shared/src/i18n/th/native.ts @@ -54,8 +54,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': 'เอกสาร AI {n} ฉบับต่อเดือน', 'native.plans.noAi': 'ไม่มีเอกสาร AI', 'native.plans.storage': 'พื้นที่เก็บข้อมูล {n} GB', - 'native.plans.unlimited': 'สถานที่ ทริป และผู้คนไม่จำกัด', - 'native.plans.subscribe': 'สมัครสมาชิก', 'native.plans.loadError': 'โหลดแพ็กเกจไม่สำเร็จ ตรวจสอบการเชื่อมต่อแล้วลองอีกครั้ง', 'native.plans.retry': 'ลองอีกครั้ง', 'native.plans.soon': 'ยังไม่สามารถสมัครสมาชิกในแอปได้', @@ -87,6 +85,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'ระบบจะเรียกเก็บเงินจากบัญชี Google ของคุณเมื่อยืนยัน การสมัครสมาชิกจะต่ออายุอัตโนมัติ เว้นแต่คุณจะยกเลิกก่อนสิ้นสุดรอบ จัดการหรือยกเลิกได้ใน Google Play ที่ โปรไฟล์ การชำระเงินและการสมัครใช้บริการ การสมัครใช้บริการ', 'native.plans.restoreNoneAndroid': 'ไม่พบการสมัครสมาชิกที่ใช้งานอยู่ในบัญชี Google นี้', + 'native.plans.recommended': 'แนะนำ', + 'native.plans.everyPlan': 'ทุกแพ็กเกจ: สถานที่ ทริป และผู้ร่วมเดินทางไม่จำกัด', + 'native.plans.continueWith': 'ไปต่อกับ {plan}', + 'native.claim.change': 'เปลี่ยน', + 'native.claim.ruleLength': 'ตัวอักษร', + 'native.claim.ruleUpper': 'ตัวพิมพ์ใหญ่', + 'native.claim.ruleLower': 'ตัวพิมพ์เล็ก', + 'native.claim.ruleDigit': 'ตัวเลข', + 'native.claim.ruleSpecial': 'สัญลักษณ์', + 'native.claim.trustServers': 'เซิร์ฟเวอร์ในเยอรมนี', + 'native.claim.trustCancel': 'ยกเลิกได้ทุกเมื่อ', + 'native.claim.trustNoAds': 'ไม่มีโฆษณา', 'native.subscribed.title': 'คุณมี TREK Cloud อยู่แล้ว', 'native.subscribed.text': 'Apple ID นี้มีการสมัครสมาชิก TREK Cloud ที่ใช้งานอยู่แล้ว คุณสมัครใหม่ได้ รวมถึงใช้ที่อยู่ใหม่ หลังจากการสมัครนี้สิ้นสุดลง', diff --git a/shared/src/i18n/tr/native.ts b/shared/src/i18n/tr/native.ts index 5fed95828..d1948b2e1 100644 --- a/shared/src/i18n/tr/native.ts +++ b/shared/src/i18n/tr/native.ts @@ -56,8 +56,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': 'ayda {n} yapay zeka belgesi', 'native.plans.noAi': 'Yapay zeka belgesi yok', 'native.plans.storage': '{n} GB depolama', - 'native.plans.unlimited': 'Sınırsız yer, gezi ve kişi', - 'native.plans.subscribe': 'Abone ol', 'native.plans.loadError': 'Planlar yüklenemedi. Bağlantınızı kontrol edip tekrar deneyin.', 'native.plans.retry': 'Tekrar dene', 'native.plans.soon': 'Uygulama içi abonelik henüz kullanılamıyor.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': "Ödeme, onayladığınızda Google hesabınızdan tahsil edilir. Abonelik, dönem bitmeden iptal etmezseniz otomatik olarak yenilenir. Google Play'de Profil, Ödemeler ve abonelikler, Abonelikler bölümünden yönetebilir veya iptal edebilirsiniz.", 'native.plans.restoreNoneAndroid': 'Bu Google hesabında etkin abonelik bulunamadı.', + 'native.plans.recommended': 'Önerilen', + 'native.plans.everyPlan': 'Her planda: sınırsız yer, gezi ve kişi', + 'native.plans.continueWith': '{plan} ile devam et', + 'native.claim.change': 'Değiştir', + 'native.claim.ruleLength': 'karakter', + 'native.claim.ruleUpper': 'Büyük harf', + 'native.claim.ruleLower': 'Küçük harf', + 'native.claim.ruleDigit': 'Rakam', + 'native.claim.ruleSpecial': 'Sembol', + 'native.claim.trustServers': 'Sunucular Almanya’da', + 'native.claim.trustCancel': 'İstediğin zaman iptal et', + 'native.claim.trustNoAds': 'Reklamsız', 'native.subscribed.title': "Zaten TREK Cloud'unuz var", 'native.subscribed.text': 'Bu Apple Kimliğinde zaten etkin bir TREK Cloud aboneliği var. Sona erdikten sonra yeni bir abonelik, yeni bir adresle de, başlatabilirsiniz.', diff --git a/shared/src/i18n/uk/native.ts b/shared/src/i18n/uk/native.ts index 7d9cb486b..2460a3265 100644 --- a/shared/src/i18n/uk/native.ts +++ b/shared/src/i18n/uk/native.ts @@ -55,8 +55,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} ШІ-документів на місяць', 'native.plans.noAi': 'Без ШІ-документів', 'native.plans.storage': '{n} ГБ сховища', - 'native.plans.unlimited': 'Без обмежень: місця, подорожі, люди', - 'native.plans.subscribe': 'Підписатися', 'native.plans.loadError': 'Не вдалося завантажити тарифи. Перевірте з’єднання та повторіть спробу.', 'native.plans.retry': 'Повторити', 'native.plans.soon': 'Оформлення підписки в застосунку поки недоступне.', @@ -90,6 +88,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Оплата списується з вашого облікового запису Google під час підтвердження. Підписка подовжується автоматично, якщо ви не скасуєте її до кінця періоду. Керувати нею або скасувати можна в Google Play: Профіль, Платежі й підписки, Підписки.', 'native.plans.restoreNoneAndroid': 'Для цього облікового запису Google активної підписки не знайдено.', + 'native.plans.recommended': 'Рекомендуємо', + 'native.plans.everyPlan': 'У кожному тарифі: без обмежень місць, подорожей і людей', + 'native.plans.continueWith': 'Продовжити з {plan}', + 'native.claim.change': 'Змінити', + 'native.claim.ruleLength': 'символів', + 'native.claim.ruleUpper': 'Велика літера', + 'native.claim.ruleLower': 'Мала літера', + 'native.claim.ruleDigit': 'Цифра', + 'native.claim.ruleSpecial': 'Спецсимвол', + 'native.claim.trustServers': 'Сервери в Німеччині', + 'native.claim.trustCancel': 'Скасування будь-коли', + 'native.claim.trustNoAds': 'Без реклами', 'native.subscribed.title': 'У вас уже є TREK Cloud', 'native.subscribed.text': 'На цьому Apple ID уже є активна підписка TREK Cloud. Оформити нову, зокрема з новою адресою, можна після її завершення.', diff --git a/shared/src/i18n/vi/native.ts b/shared/src/i18n/vi/native.ts index 2b9da0abe..b82c8debb 100644 --- a/shared/src/i18n/vi/native.ts +++ b/shared/src/i18n/vi/native.ts @@ -57,8 +57,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '{n} tài liệu AI mỗi tháng', 'native.plans.noAi': 'Không có tài liệu AI', 'native.plans.storage': '{n} GB lưu trữ', - 'native.plans.unlimited': 'Không giới hạn địa điểm, chuyến đi, người', - 'native.plans.subscribe': 'Đăng ký', 'native.plans.loadError': 'Không thể tải các gói. Hãy kiểm tra kết nối và thử lại.', 'native.plans.retry': 'Thử lại', 'native.plans.soon': 'Chưa thể đăng ký trong ứng dụng.', @@ -91,6 +89,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': 'Khoản thanh toán được tính vào tài khoản Google của bạn khi xác nhận. Gói đăng ký tự động gia hạn trừ khi bạn hủy trước khi hết kỳ. Quản lý hoặc hủy trong Google Play tại Hồ sơ, Thanh toán và gói thuê bao, Gói thuê bao.', 'native.plans.restoreNoneAndroid': 'Không tìm thấy gói đăng ký đang hoạt động trên tài khoản Google này.', + 'native.plans.recommended': 'Đề xuất', + 'native.plans.everyPlan': 'Mọi gói: địa điểm, chuyến đi và người tham gia không giới hạn', + 'native.plans.continueWith': 'Tiếp tục với {plan}', + 'native.claim.change': 'Đổi', + 'native.claim.ruleLength': 'ký tự', + 'native.claim.ruleUpper': 'Chữ hoa', + 'native.claim.ruleLower': 'Chữ thường', + 'native.claim.ruleDigit': 'Chữ số', + 'native.claim.ruleSpecial': 'Ký hiệu', + 'native.claim.trustServers': 'Máy chủ tại Đức', + 'native.claim.trustCancel': 'Hủy bất cứ lúc nào', + 'native.claim.trustNoAds': 'Không quảng cáo', 'native.subscribed.title': 'Bạn đã có TREK Cloud', 'native.subscribed.text': 'Apple ID này đã có gói đăng ký TREK Cloud đang hoạt động. Bạn có thể đăng ký gói mới, kể cả với địa chỉ mới, sau khi gói này kết thúc.', diff --git a/shared/src/i18n/zh-TW/native.ts b/shared/src/i18n/zh-TW/native.ts index e10e8700e..4ba0be05c 100644 --- a/shared/src/i18n/zh-TW/native.ts +++ b/shared/src/i18n/zh-TW/native.ts @@ -51,8 +51,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '每月 {n} 份 AI 文件', 'native.plans.noAi': '無 AI 文件', 'native.plans.storage': '{n} GB 儲存空間', - 'native.plans.unlimited': '地點、行程和人數不限', - 'native.plans.subscribe': '訂閱', 'native.plans.loadError': '無法載入方案。請檢查網路連線後重試。', 'native.plans.retry': '重試', 'native.plans.soon': '目前尚不支援在 App 內訂閱。', @@ -84,6 +82,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': '確認後將從你的 Google 帳號扣款。除非你在期滿前取消,訂閱會自動續訂。可在 Google Play 的「個人資料」「付款和訂閱」「訂閱」中管理或取消。', 'native.plans.restoreNoneAndroid': '此 Google 帳號沒有有效的訂閱。', + 'native.plans.recommended': '推薦', + 'native.plans.everyPlan': '每個方案都包含:不限地點、行程和人數', + 'native.plans.continueWith': '繼續選擇 {plan}', + 'native.claim.change': '更改', + 'native.claim.ruleLength': '個字元', + 'native.claim.ruleUpper': '大寫字母', + 'native.claim.ruleLower': '小寫字母', + 'native.claim.ruleDigit': '數字', + 'native.claim.ruleSpecial': '符號', + 'native.claim.trustServers': '伺服器位於德國', + 'native.claim.trustCancel': '隨時取消', + 'native.claim.trustNoAds': '無廣告', 'native.subscribed.title': '你已經訂閱 TREK Cloud', 'native.subscribed.text': '此 Apple ID 已有一個有效的 TREK Cloud 訂閱。訂閱結束後,你可以重新訂閱,也可以使用新網址。', diff --git a/shared/src/i18n/zh/native.ts b/shared/src/i18n/zh/native.ts index 02ea969f5..adba91e23 100644 --- a/shared/src/i18n/zh/native.ts +++ b/shared/src/i18n/zh/native.ts @@ -50,8 +50,6 @@ const native: TranslationStrings = { 'native.plans.aiDocs': '每月 {n} 份 AI 文档', 'native.plans.noAi': '无 AI 文档', 'native.plans.storage': '{n} GB 存储空间', - 'native.plans.unlimited': '地点、行程和人数不限', - 'native.plans.subscribe': '订阅', 'native.plans.loadError': '无法加载方案。请检查网络连接后重试。', 'native.plans.retry': '重试', 'native.plans.soon': '暂不支持在应用内订阅。', @@ -83,6 +81,18 @@ const native: TranslationStrings = { 'native.plans.legalAndroid': '确认后将从你的 Google 账号扣款。除非你在期满前取消,订阅会自动续订。可在 Google Play 的“个人资料”“付款和订阅”“订阅”中管理或取消。', 'native.plans.restoreNoneAndroid': '此 Google 账号没有有效订阅。', + 'native.plans.recommended': '推荐', + 'native.plans.everyPlan': '每个方案都包含:不限地点、行程和人数', + 'native.plans.continueWith': '继续选择 {plan}', + 'native.claim.change': '更改', + 'native.claim.ruleLength': '个字符', + 'native.claim.ruleUpper': '大写字母', + 'native.claim.ruleLower': '小写字母', + 'native.claim.ruleDigit': '数字', + 'native.claim.ruleSpecial': '符号', + 'native.claim.trustServers': '服务器位于德国', + 'native.claim.trustCancel': '随时取消', + 'native.claim.trustNoAds': '无广告', 'native.subscribed.title': '你已经订阅了 TREK Cloud', 'native.subscribed.text': '此 Apple ID 已有一个有效的 TREK Cloud 订阅。订阅结束后,你可以重新订阅,也可以使用新地址。',