Replace the 3.0 upgrade notices with a thank-you / support modal

The 3.0 "what's new" notices have served their purpose, so swap them for a single thank-you notice that comes back once on every fresh install and version bump. It carries Buy Me a Coffee and Ko-fi buttons and only shows on desktop. Adds a per-version recurring mode (new dismissed_app_version column) plus external-link CTAs to support it; the 3.0.14 whitespace-collision admin notice stays active.
This commit is contained in:
Maurice
2026-06-27 19:18:01 +02:00
parent 4898b7ddcf
commit 18d12e5d7a
30 changed files with 420 additions and 207 deletions
@@ -62,16 +62,17 @@ function CTALink({
if (notice.cta.kind === 'nav') {
navigate(notice.cta.href);
if (notice.dismissible) onDismiss();
} else if (notice.cta.kind === 'link') {
window.open(notice.cta.href, '_blank', 'noopener,noreferrer');
} else {
runNoticeAction(notice.cta.actionId, { navigate });
const actionCta = notice.cta as { kind: 'action'; labelKey: string; actionId: string; dismissOnAction?: boolean };
if (actionCta.dismissOnAction !== false) onDismiss();
if (notice.cta.dismissOnAction !== false) onDismiss();
}
}
if (!notice.cta) return null;
if (notice.cta.kind === 'nav') {
if (notice.cta.kind === 'nav' || notice.cta.kind === 'link') {
return (
<a
href={notice.cta.href}
@@ -1,10 +1,26 @@
import React, { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useSystemNoticeStore } from '../../store/systemNoticeStore.js';
import { ModalRenderer } from './SystemNoticeModal.js';
import { BannerRenderer, ToastRenderer } from './SystemNoticeBanner.js';
// Mobile breakpoint matches the modal sheet's (max-width: 639px).
function useIsMobile() {
const [isMobile, setIsMobile] = useState(
() => typeof window !== 'undefined' && (window.matchMedia?.('(max-width: 639px)')?.matches ?? false)
);
useEffect(() => {
const mq = window.matchMedia?.('(max-width: 639px)');
if (!mq) return;
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, []);
return isMobile;
}
export function SystemNoticeHost() {
const { notices, loaded } = useSystemNoticeStore();
const isMobile = useIsMobile();
// Notices are fetched by authStore after login (see App.tsx / authStore modification).
// Cold-session fetch (page reload with valid session) is triggered here:
@@ -17,9 +33,12 @@ export function SystemNoticeHost() {
if (!loaded) return null;
const modals = notices.filter(n => n.display === 'modal');
const banners = notices.filter(n => n.display === 'banner');
const toasts = notices.filter(n => n.display === 'toast');
// desktopOnly notices (e.g. the thank-you/support modal) are hidden on mobile.
const visible = isMobile ? notices.filter(n => !n.desktopOnly) : notices;
const modals = visible.filter(n => n.display === 'modal');
const banners = visible.filter(n => n.display === 'banner');
const toasts = visible.filter(n => n.display === 'toast');
return (
<>
@@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from 'react';
import { flushSync } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { Info, AlertTriangle, AlertOctagon, X, ChevronLeft, ChevronRight } from 'lucide-react';
import { Info, AlertTriangle, AlertOctagon, X, ChevronLeft, ChevronRight, Coffee } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
import remarkGfm from 'remark-gfm';
import rehypeSanitize from 'rehype-sanitize';
@@ -36,6 +36,33 @@ const SEVERITY_ACCENT: Record<string, string> = {
critical: 'text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-950',
};
// Real brand marks (simple-icons single-path logos) for the support buttons, so the
// Buy Me a Coffee / Ko-fi buttons carry their actual logo instead of a generic
// lucide glyph. Tinted via currentColor.
const BRAND_ICON_PATHS: Record<string, string> = {
buymeacoffee:
'M20.216 6.415l-.132-.666c-.119-.598-.388-1.163-1.001-1.379-.197-.069-.42-.098-.57-.241-.152-.143-.196-.366-.231-.572-.065-.378-.125-.756-.192-1.133-.057-.325-.102-.69-.25-.987-.195-.4-.597-.634-.996-.788a5.723 5.723 0 00-.626-.194c-1-.263-2.05-.36-3.077-.416a25.834 25.834 0 00-3.7.062c-.915.083-1.88.184-2.75.5-.318.116-.646.256-.888.501-.297.302-.393.77-.177 1.146.154.267.415.456.692.58.36.162.737.284 1.123.366 1.075.238 2.189.331 3.287.37 1.218.05 2.437.01 3.65-.118.299-.033.598-.073.896-.119.352-.054.578-.513.474-.834-.124-.383-.457-.531-.834-.473-.466.074-.96.108-1.382.146-1.177.08-2.358.082-3.536.006a22.228 22.228 0 01-1.157-.107c-.086-.01-.18-.025-.258-.036-.243-.036-.484-.08-.724-.13-.111-.027-.111-.185 0-.212h.005c.277-.06.557-.108.838-.147h.002c.131-.009.263-.032.394-.048a25.076 25.076 0 013.426-.12c.674.019 1.347.067 2.017.144l.228.031c.267.04.533.088.798.145.392.085.895.113 1.07.542.055.137.08.288.111.431l.319 1.484a.237.237 0 01-.199.284h-.003c-.037.006-.075.01-.112.015a36.704 36.704 0 01-4.743.295 37.059 37.059 0 01-4.699-.304c-.14-.017-.293-.042-.417-.06-.326-.048-.649-.108-.973-.161-.393-.065-.768-.032-1.123.161-.29.16-.527.404-.675.701-.154.316-.199.66-.267 1-.069.34-.176.707-.135 1.056.087.753.613 1.365 1.37 1.502a39.69 39.69 0 0011.343.376.483.483 0 01.535.53l-.071.697-1.018 9.907c-.041.41-.047.832-.125 1.237-.122.637-.553 1.028-1.182 1.171-.577.131-1.165.2-1.756.205-.656.004-1.31-.025-1.966-.022-.699.004-1.556-.06-2.095-.58-.475-.458-.54-1.174-.605-1.793l-.731-7.013-.322-3.094c-.037-.351-.286-.695-.678-.678-.336.015-.718.3-.678.679l.228 2.185.949 9.112c.147 1.344 1.174 2.068 2.446 2.272.742.12 1.503.144 2.257.156.966.016 1.942.053 2.892-.122 1.408-.258 2.465-1.198 2.616-2.657.34-3.332.683-6.663 1.024-9.995l.215-2.087a.484.484 0 01.39-.426c.402-.078.787-.212 1.074-.518.455-.488.546-1.124.385-1.766zm-1.478.772c-.145.137-.363.201-.578.233-2.416.359-4.866.54-7.308.46-1.748-.06-3.477-.254-5.207-.498-.17-.024-.353-.055-.47-.18-.22-.236-.111-.71-.054-.995.052-.26.152-.609.463-.646.484-.057 1.046.148 1.526.22.577.088 1.156.159 1.737.212 2.48.226 5.002.19 7.472-.14.45-.06.899-.13 1.345-.21.399-.072.84-.206 1.08.206.166.281.188.657.162.974a.544.544 0 01-.169.364zm-6.159 3.9c-.862.37-1.84.788-3.109.788a5.884 5.884 0 01-1.569-.217l.877 9.004c.065.78.717 1.38 1.5 1.38 0 0 1.243.065 1.658.065.447 0 1.786-.065 1.786-.065.783 0 1.434-.6 1.499-1.38l.94-9.95a3.996 3.996 0 00-1.322-.238c-.826 0-1.491.284-2.26.613z',
kofi:
'M11.351 2.715c-2.7 0-4.986.025-6.83.26C2.078 3.285 0 5.154 0 8.61c0 3.506.182 6.13 1.585 8.493 1.584 2.701 4.233 4.182 7.662 4.182h.83c4.209 0 6.494-2.234 7.637-4a9.5 9.5 0 0 0 1.091-2.338C21.792 14.688 24 12.22 24 9.208v-.415c0-3.247-2.13-5.507-5.792-5.87-1.558-.156-2.65-.208-6.857-.208m0 1.947c4.208 0 5.09.052 6.571.182 2.624.311 4.13 1.584 4.13 4v.39c0 2.156-1.792 3.844-3.87 3.844h-.935l-.156.649c-.208 1.013-.597 1.818-1.039 2.546-.909 1.428-2.545 3.064-5.922 3.064h-.805c-2.571 0-4.831-.883-6.078-3.195-1.09-2-1.298-4.155-1.298-7.506 0-2.181.857-3.402 3.012-3.714 1.533-.233 3.559-.26 6.39-.26m6.547 2.287c-.416 0-.65.234-.65.546v2.935c0 .311.234.545.65.545 1.324 0 2.051-.754 2.051-2s-.727-2.026-2.052-2.026m-10.39.182c-1.818 0-3.013 1.48-3.013 3.142 0 1.533.858 2.857 1.949 3.897.727.701 1.87 1.429 2.649 1.896a1.47 1.47 0 0 0 1.507 0c.78-.467 1.922-1.195 2.623-1.896 1.117-1.039 1.974-2.364 1.974-3.897 0-1.662-1.247-3.142-3.039-3.142-1.065 0-1.792.545-2.338 1.298-.493-.753-1.246-1.298-2.312-1.298',
};
function brandForHref(href?: string): string | null {
if (!href) return null;
if (href.includes('buymeacoffee')) return 'buymeacoffee';
if (href.includes('ko-fi.com') || href.includes('kofi')) return 'kofi';
return null;
}
function BrandIcon({ brand, size = 18, className }: { brand: string; size?: number; className?: string }) {
const d = BRAND_ICON_PATHS[brand];
if (!d) return null;
return (
<svg viewBox="0 0 24 24" width={size} height={size} fill="currentColor" className={className} aria-hidden="true">
<path d={d} />
</svg>
);
}
interface Props {
notices: SystemNoticeDTO[];
}
@@ -46,12 +73,14 @@ interface ContentProps {
title: string;
body: string;
ctaLabel: string | null;
secondaryCtaLabel: string | null;
titleId: string;
bodyId: string;
isDark: boolean;
onDismiss: () => void;
onDismissAll: () => void;
onCTA: () => void;
onSecondaryCTA: () => void;
// Pager
total: number;
currentPage: number;
@@ -61,7 +90,7 @@ interface ContentProps {
onGoto: (i: number) => void;
}
function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark, onDismiss, onDismissAll, onCTA, total, currentPage, canPage, onPrev, onNext, onGoto }: ContentProps) {
function NoticeContent({ notice, title, body, ctaLabel, secondaryCtaLabel, titleId, bodyId, isDark, onDismiss, onDismissAll, onCTA, onSecondaryCTA, total, currentPage, canPage, onPrev, onNext, onGoto }: ContentProps) {
const { t } = useTranslation();
const isLastPage = total <= 1 || currentPage === total - 1;
@@ -70,6 +99,10 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
? ((LucideIcons as Record<string, unknown>)[notice.icon] as React.ElementType) ?? DefaultIcon
: DefaultIcon;
// Real brand logo for each support button, detected from the link target.
const primaryBrand = notice.cta?.kind === 'link' ? brandForHref(notice.cta.href) : null;
const secondaryBrand = notice.secondaryCta?.kind === 'link' ? brandForHref(notice.secondaryCta.href) : null;
return (
<div className="flex flex-col relative" style={{ flex: '1 1 0', minHeight: '100%' }}>
{/* Dismiss X button — only on last page so users read all notices */}
@@ -104,17 +137,9 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
{/* Special warm header for Heart icon (thank-you notice) */}
{notice.icon === 'Heart' && !notice.media && (
<div className="relative overflow-hidden bg-gradient-to-br from-rose-500 via-pink-500 to-indigo-500 px-8 py-5 text-center">
<div className="relative overflow-hidden bg-gradient-to-br from-rose-500 via-pink-500 to-indigo-500 px-8 py-6 text-center">
<div className="absolute inset-0 opacity-10" style={{ backgroundImage: 'radial-gradient(circle at 20% 50%, white 1px, transparent 1px), radial-gradient(circle at 80% 20%, white 1px, transparent 1px), radial-gradient(circle at 60% 80%, white 1px, transparent 1px)', backgroundSize: '60px 60px, 80px 80px, 40px 40px' }} />
<div className="relative flex items-center justify-center gap-3">
<div className="w-10 h-10 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center ring-2 ring-white/10">
<LucideIcon size={20} className="text-white" />
</div>
<div className="text-left">
<h2 id={titleId} className="text-lg font-bold text-white leading-tight">{title}</h2>
<p className="text-xs text-white/60 font-medium">TREK 3.0</p>
</div>
</div>
<h2 id={titleId} className="relative text-xl font-bold text-white leading-tight">{title}</h2>
</div>
)}
@@ -197,24 +222,27 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
</div>
)}
{/* Highlights */}
{/* Highlights — compact pills */}
{notice.highlights && notice.highlights.length > 0 && (
<ul className="mx-auto mb-4 space-y-2">
<div className="flex flex-wrap justify-center gap-2 mb-4">
{notice.highlights.map((h, i) => {
const HIcon: React.ElementType | null = h.iconName
? ((LucideIcons as Record<string, unknown>)[h.iconName] as React.ElementType) ?? null
: null;
return (
<li key={i} className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
<span
key={i}
className="inline-flex items-center gap-1.5 rounded-full bg-slate-100 dark:bg-slate-800 px-3 py-1 text-xs font-medium text-slate-700 dark:text-slate-300"
>
{HIcon
? <HIcon size={16} className="text-blue-500 shrink-0" />
: <span className="text-blue-500 shrink-0"></span>
? <HIcon size={13} className="text-indigo-500 dark:text-indigo-400 shrink-0" />
: <span className="text-indigo-500 shrink-0"></span>
}
{t(h.labelKey)}
</li>
</span>
);
})}
</ul>
</div>
)}
</div>
</div>
@@ -270,16 +298,37 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
</div>
)}
{/* CTA + dismiss link */}
{/* CTA(s) + dismiss link */}
<div className="flex flex-col items-center gap-3">
{ctaLabel && isLastPage ? (
<button
id={`notice-cta-${notice.id}`}
onClick={onCTA}
className="w-full h-11 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-medium transition-colors"
>
{ctaLabel}
</button>
<div className="flex w-full flex-col sm:flex-row gap-2.5">
<button
id={`notice-cta-${notice.id}`}
onClick={onCTA}
className={`flex-1 h-11 inline-flex items-center justify-center gap-2 rounded-lg font-semibold shadow-sm transition active:scale-[0.98] ${
notice.cta?.kind === 'link'
? 'bg-[#FFDD00] text-[#0D0C22] hover:brightness-95'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{primaryBrand ? <BrandIcon brand={primaryBrand} size={18} /> : (notice.cta?.kind === 'link' && <Coffee size={17} aria-hidden="true" />)}
{ctaLabel}
</button>
{secondaryCtaLabel && (
<button
id={`notice-cta2-${notice.id}`}
onClick={onSecondaryCTA}
className={`flex-1 h-11 inline-flex items-center justify-center gap-2 rounded-lg font-semibold shadow-sm transition active:scale-[0.98] ${
notice.secondaryCta?.kind === 'link'
? 'bg-[#FF5E5B] text-white hover:brightness-95'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{secondaryBrand ? <BrandIcon brand={secondaryBrand} size={18} /> : (notice.secondaryCta?.kind === 'link' && <Coffee size={17} aria-hidden="true" />)}
{secondaryCtaLabel}
</button>
)}
</div>
) : (notice.dismissible || isLastPage) && (
<button
id={`notice-cta-${notice.id}`}
@@ -289,14 +338,6 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark,
{t('common.ok')}
</button>
)}
{notice.dismissible && isLastPage && ctaLabel && (
<button
onClick={onDismiss}
className="text-sm text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 transition-colors"
>
Not now
</button>
)}
</div>
</div>
</div>
@@ -510,21 +551,22 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) {
notices.forEach(n => dismiss(n.id));
}
function handleCTA() {
if (!notice) return;
if (!notice.cta) {
handleDismissAll();
return;
}
if (notice.cta.kind === 'nav') {
navigate(notice.cta.href);
if (notice.dismissible !== false) handleDismissAll();
function runCta(cta: SystemNoticeDTO['cta']) {
if (!cta) { handleDismissAll(); return; }
if (cta.kind === 'nav') {
navigate(cta.href);
if (notice?.dismissible !== false) handleDismissAll();
} else if (cta.kind === 'link') {
// External link (e.g. Buy Me a Coffee / Ko-fi): open in a new tab and leave the
// notice open so the user can use the other button too.
window.open(cta.href, '_blank', 'noopener,noreferrer');
} else {
runNoticeAction(notice.cta.actionId, { navigate });
const actionCta = notice.cta as { kind: 'action'; labelKey: string; actionId: string; dismissOnAction?: boolean };
if (actionCta.dismissOnAction !== false) handleDismissAll();
runNoticeAction(cta.actionId, { navigate });
if (cta.dismissOnAction !== false) handleDismissAll();
}
}
function handleCTA() { runCta(notice?.cta); }
function handleSecondaryCTA() { runCta(notice?.secondaryCta); }
function animatedDismissAll() {
const sheet = sheetRef.current;
@@ -584,7 +626,7 @@ function useSystemNoticeModal(notices: SystemNoticeDTO[]) {
notice, canPage, isLastPage, language, t, dur, ease,
touchStartX, touchStartY, dragLockRef, scrollTopAtTouchStart, isPageNavRef,
stripRef, sheetRef, prevSlotRef, contentWrapperRef, nextSlotRef,
announceIndex, handleDismiss, handleDismissAll, handleCTA, animatedDismissAll,
announceIndex, handleDismiss, handleDismissAll, handleCTA, handleSecondaryCTA, animatedDismissAll,
handlePrev, handleNext, handleGoto,
};
}
@@ -593,7 +635,7 @@ type NoticeState = ReturnType<typeof useSystemNoticeModal>;
// Build the NoticeContent props for a given notice + pager slot index.
function makeContentProps(S: NoticeState, n: SystemNoticeDTO, slotIdx: number): ContentProps {
const { t, isDark, canPage, notices, handleDismiss, handleDismissAll, handleCTA, handlePrev, handleNext, handleGoto } = S;
const { t, isDark, canPage, notices, handleDismiss, handleDismissAll, handleCTA, handleSecondaryCTA, handlePrev, handleNext, handleGoto } = S;
const rawBody = t(n.bodyKey);
const body = n.bodyParams
? Object.entries(n.bodyParams).reduce(
@@ -606,12 +648,14 @@ function makeContentProps(S: NoticeState, n: SystemNoticeDTO, slotIdx: number):
title: t(n.titleKey),
body,
ctaLabel: n.cta ? t(n.cta.labelKey) : null,
secondaryCtaLabel: n.secondaryCta ? t(n.secondaryCta.labelKey) : null,
titleId: `notice-title-${n.id}`,
bodyId: `notice-body-${n.id}`,
isDark,
onDismiss: handleDismiss,
onDismissAll: handleDismissAll,
onCTA: handleCTA,
onSecondaryCTA: handleSecondaryCTA,
total: notices.length,
currentPage: slotIdx,
canPage,
+9
View File
@@ -3062,6 +3062,15 @@ function runMigrations(db: Database.Database): void {
if (!err.message?.includes('duplicate column name')) throw err;
}
},
// Remember the app version a notice was dismissed at, so per-version recurring
// notices (e.g. the thank-you) re-appear on the next install/upgrade.
() => {
try {
db.exec('ALTER TABLE user_notice_dismissals ADD COLUMN dismissed_app_version TEXT');
} catch (err: any) {
if (!err.message?.includes('duplicate column name')) throw err;
}
},
];
if (currentVersion < migrations.length) {
+45 -133
View File
@@ -11,128 +11,65 @@ registerPredicate('whitespace-collision-detected', () => {
* SYSTEM NOTICE REGISTRY
*
* Rules for authoring:
* - NEVER remove or renumber entries — dismissal tracking is keyed by `id`.
* - NEVER reuse a retired `id` — dismissal tracking is keyed by `id`. Retired ids are
* listed in RETIRED_NOTICE_IDS so they're never accidentally re-used.
* - `id` must be globally unique and stable across deployments.
* - Title: ≤40 chars, sentence case, no trailing punctuation.
* - Body: markdown (modal) or plain text (banner/toast). ≤400/140/80 chars.
* - CTA label: ≤20 chars, a verb.
* - CTA label: ≤20 chars.
* - Never hardcode version numbers/dates in translated strings — use bodyParams.
* - See plans/system-notices/00-overview.md for full authoring guidelines.
*/
/**
* Retired notices. Kept out of the active list but their ids stay reserved so a future
* notice never reuses one (dismissals are keyed by id). Do not re-add these ids.
*/
export const RETIRED_NOTICE_IDS = [
'v3-thankyou',
'v3-photos',
'v3-journey',
'v3-mcp',
'v3-features',
'welcome-v1',
] as const;
export const SYSTEM_NOTICES: SystemNotice[] = [
// ── 3.0.0 upgrade notices (shown as a multipage modal to pre-3.0 users) ─────
// ── Thank-you + support the project — shown once per install AND once per upgrade ──
// `recurring: 'per-version'` re-surfaces it whenever the app version moves up.
{
// Page 1 — breaking change first (warn → sorts before the two info notices)
id: 'v3-photos',
display: 'modal',
severity: 'warn',
icon: 'ImageOff',
titleKey: 'system_notice.v3_photos.title',
bodyKey: 'system_notice.v3_photos.body',
dismissible: true,
conditions: [{ kind: 'existingUserBeforeVersion', version: '3.0.0' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 90,
minVersion: '3.0.0',
maxVersion: '4.0.0',
},
{
// Page 2 — flagship feature (only when Journey addon is enabled)
id: 'v3-journey',
display: 'modal',
severity: 'info',
icon: 'BookOpen',
titleKey: 'system_notice.v3_journey.title',
bodyKey: 'system_notice.v3_journey.body',
highlights: [
{ labelKey: 'system_notice.v3_journey.highlight_timeline', iconName: 'CalendarDays' },
{ labelKey: 'system_notice.v3_journey.highlight_photos', iconName: 'Images' },
{ labelKey: 'system_notice.v3_journey.highlight_share', iconName: 'Globe' },
{ labelKey: 'system_notice.v3_journey.highlight_export', iconName: 'FileText' },
],
cta: {
kind: 'nav',
labelKey: 'system_notice.v3_journey.cta_label',
href: '/journey',
},
dismissible: true,
conditions: [
{ kind: 'existingUserBeforeVersion', version: '3.0.0' },
{ kind: 'addonEnabled', addonId: 'journey' },
],
publishedAt: '2026-04-16T00:00:00Z',
priority: 80,
minVersion: '3.0.0',
maxVersion: '4.0.0',
},
{
// Page 3 — MCP OAuth 2.1 upgrade (only when MCP addon is enabled)
id: 'v3-mcp',
display: 'modal',
severity: 'warn',
icon: 'Bot',
titleKey: 'system_notice.v3_mcp.title',
bodyKey: 'system_notice.v3_mcp.body',
highlights: [
{ labelKey: 'system_notice.v3_mcp.highlight_oauth', iconName: 'KeyRound' },
{ labelKey: 'system_notice.v3_mcp.highlight_scopes', iconName: 'ShieldCheck' },
{ labelKey: 'system_notice.v3_mcp.highlight_deprecated', iconName: 'AlertTriangle' },
{ labelKey: 'system_notice.v3_mcp.highlight_tools', iconName: 'Wrench' },
],
dismissible: true,
conditions: [
{ kind: 'existingUserBeforeVersion', version: '3.0.0' },
{ kind: 'addonEnabled', addonId: 'mcp' },
],
publishedAt: '2026-04-16T00:00:00Z',
priority: 75,
minVersion: '3.0.0',
maxVersion: '4.0.0',
},
{
// Page 4 — other highlights
id: 'v3-features',
display: 'modal',
severity: 'info',
icon: 'Sparkles',
titleKey: 'system_notice.v3_features.title',
bodyKey: 'system_notice.v3_features.body',
highlights: [
{ labelKey: 'system_notice.v3_features.highlight_dashboard', iconName: 'LayoutDashboard' },
{ labelKey: 'system_notice.v3_features.highlight_offline', iconName: 'WifiOff' },
{ labelKey: 'system_notice.v3_features.highlight_search', iconName: 'Search' },
{ labelKey: 'system_notice.v3_features.highlight_import', iconName: 'FileInput' },
],
dismissible: true,
conditions: [{ kind: 'existingUserBeforeVersion', version: '3.0.0' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 70,
minVersion: '3.0.0',
maxVersion: '4.0.0',
},
{
// Page 1 — personal thank-you from the creator (shown first)
id: 'v3-thankyou',
id: 'thank-you-support',
display: 'modal',
severity: 'info',
icon: 'Heart',
titleKey: 'system_notice.v3_thankyou.title',
bodyKey: 'system_notice.v3_thankyou.body',
titleKey: 'system_notice.thank_you_support.title',
bodyKey: 'system_notice.thank_you_support.body',
highlights: [
{ labelKey: 'system_notice.thank_you_support.highlight_opensource', iconName: 'Github' },
{ labelKey: 'system_notice.thank_you_support.highlight_free', iconName: 'Infinity' },
{ labelKey: 'system_notice.thank_you_support.highlight_community', iconName: 'Users' },
],
cta: {
kind: 'link',
labelKey: 'system_notice.thank_you_support.cta_bmc',
href: 'https://buymeacoffee.com/mauriceboe',
},
secondaryCta: {
kind: 'link',
labelKey: 'system_notice.thank_you_support.cta_kofi',
href: 'https://ko-fi.com/mauriceboe',
},
dismissible: true,
conditions: [{ kind: 'existingUserBeforeVersion', version: '3.0.0' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 95,
minVersion: '3.0.0',
maxVersion: '4.0.0',
// Desktop-only: the support modal is suppressed on small/mobile viewports.
desktopOnly: true,
conditions: [],
publishedAt: '2026-06-27T00:00:00Z',
priority: 100,
recurring: 'per-version',
},
// ── 3.0.14 admin notice — whitespace migration collision ───────────────────
// Operational alert (not promo): shown only to admins who upgraded across the
// 3.0.14 boundary AND only when the migration actually renamed colliding accounts.
{
id: 'v3014-whitespace-collision',
display: 'banner',
@@ -150,29 +87,4 @@ export const SYSTEM_NOTICES: SystemNotice[] = [
priority: 85,
minVersion: '3.0.14',
},
// ── Onboarding ─────────────────────────────────────────────────────────────
{
id: 'welcome-v1',
display: 'modal',
severity: 'info',
icon: 'Sparkles',
titleKey: 'system_notice.welcome_v1.title',
bodyKey: 'system_notice.welcome_v1.body',
highlights: [
{ labelKey: 'system_notice.welcome_v1.highlight_plan', iconName: 'Map' },
{ labelKey: 'system_notice.welcome_v1.highlight_share', iconName: 'Users' },
{ labelKey: 'system_notice.welcome_v1.highlight_offline', iconName: 'WifiOff' },
],
cta: {
kind: 'action',
labelKey: 'system_notice.welcome_v1.cta_label',
actionId: 'open:trip-create',
},
dismissible: true,
conditions: [{ kind: 'firstLogin' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 100,
},
];
+27 -9
View File
@@ -46,19 +46,32 @@ export function getActiveNoticesFor(userId: number): SystemNoticeDTO[] {
'SELECT COUNT(*) AS count FROM trips WHERE user_id = ?'
).get(userId) as { count: number };
const dismissedIds = new Set<string>(
(db.prepare('SELECT notice_id FROM user_notice_dismissals WHERE user_id = ?')
.all(userId) as Array<{ notice_id: string }>)
.map(r => r.notice_id)
// Dismissals mapped to the app version they were dismissed at (used by per-version notices).
const dismissals = new Map<string, string | null>(
(db.prepare('SELECT notice_id, dismissed_app_version FROM user_notice_dismissals WHERE user_id = ?')
.all(userId) as Array<{ notice_id: string; dismissed_app_version: string | null }>)
.map(r => [r.notice_id, r.dismissed_app_version])
);
const now = new Date();
const currentAppVersion = getCurrentAppVersion();
const ctx = { user: { ...user, noTrips: tripCount }, currentAppVersion, now };
const appVer = semver.coerce(currentAppVersion)?.version ?? '0.0.0';
const isStillDismissed = (n: SystemNotice): boolean => {
if (!dismissals.has(n.id)) return false;
if (n.recurring === 'per-version') {
// Re-show once the running app version moves past the version it was last dismissed at,
// so a per-version notice surfaces again on each install/upgrade.
const dismissedVer = semver.coerce(dismissals.get(n.id) ?? '0.0.0')?.version ?? '0.0.0';
return semver.gte(dismissedVer, appVer);
}
return true; // default: permanent one-time dismissal
};
return SYSTEM_NOTICES
.filter(n => {
if (dismissedIds.has(n.id)) return false;
if (isStillDismissed(n)) return false;
if (!isNoticeVersionActive(n, currentAppVersion)) return false;
return evaluate(n, ctx);
})
@@ -69,15 +82,20 @@ export function getActiveNoticesFor(userId: number): SystemNoticeDTO[] {
if (sw !== 0) return sw;
return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
})
.map(({ conditions: _c, publishedAt: _p, minVersion: _mn, maxVersion: _mx, priority: _pr, ...dto }) => dto);
.map(({ conditions: _c, publishedAt: _p, minVersion: _mn, maxVersion: _mx, priority: _pr, recurring: _rc, ...dto }) => dto);
}
export function dismissNotice(userId: number, noticeId: string): boolean {
const exists = SYSTEM_NOTICES.some(n => n.id === noticeId);
if (!exists) return false;
// Record the app version at dismissal so per-version notices can re-appear on the next
// upgrade. Upsert (not INSERT OR IGNORE) so re-dismissing after a bump refreshes the version.
db.prepare(`
INSERT OR IGNORE INTO user_notice_dismissals (user_id, notice_id, dismissed_at)
VALUES (?, ?, ?)
`).run(userId, noticeId, Date.now());
INSERT INTO user_notice_dismissals (user_id, notice_id, dismissed_at, dismissed_app_version)
VALUES (?, ?, ?, ?)
ON CONFLICT(user_id, notice_id) DO UPDATE SET
dismissed_at = excluded.dismissed_at,
dismissed_app_version = excluded.dismissed_app_version
`).run(userId, noticeId, Date.now(), getCurrentAppVersion());
return true;
}
+8 -1
View File
@@ -21,6 +21,7 @@ export interface NoticeMedia {
export type NoticeCta =
| { kind: 'nav'; labelKey: string; href: string }
| { kind: 'link'; labelKey: string; href: string } // external URL, opens in a new tab
| { kind: 'action'; labelKey: string; actionId: string; dismissOnAction?: boolean };
export interface SystemNotice {
@@ -34,13 +35,19 @@ export interface SystemNotice {
media?: NoticeMedia;
highlights?: Array<{ labelKey: string; iconName?: string }>;
cta?: NoticeCta;
secondaryCta?: NoticeCta;
// Hide this notice on small/mobile viewports (evaluated client-side).
desktopOnly?: boolean;
dismissible: boolean;
conditions: NoticeCondition[];
publishedAt: string;
minVersion?: string;
maxVersion?: string;
priority?: number;
// 'per-version': re-show on every app version bump (each install + upgrade) instead of
// the default permanent one-time dismissal.
recurring?: 'per-version';
}
// DTO sent to client (same shape minus the conditions — server evaluates those)
export type SystemNoticeDTO = Omit<SystemNotice, 'conditions' | 'publishedAt' | 'minVersion' | 'maxVersion' | 'priority'>;
export type SystemNoticeDTO = Omit<SystemNotice, 'conditions' | 'publishedAt' | 'minVersion' | 'maxVersion' | 'priority' | 'recurring'>;
+37 -7
View File
@@ -92,16 +92,17 @@ describe('GET /api/system-notices/active', () => {
expect(res.status).toBe(401);
});
it('returns empty array for non-first-login user with no applicable notices', async () => {
it('returns no login/version-gated notices for an established user', async () => {
const { user } = createUser(testDb);
// login_count > 1 means firstLogin condition does not match for any notice;
// first_seen_version >= 3.0.0 means existingUserBeforeVersion('3.0.0') also does not match
// login_count > 1 means firstLogin does not match; first_seen_version >= 3.0.0 means
// existingUserBeforeVersion('3.0.0') does not match either. The always-on thank-you
// notice (no conditions) may still apply, so only filter it out.
testDb.prepare('UPDATE users SET login_count = 5, first_seen_version = ? WHERE id = ?').run('3.0.0', user.id);
const res = await request(app)
.get('/api/system-notices/active')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
expect(res.body.filter((n: { id: string }) => n.id !== 'thank-you-support')).toEqual([]);
});
it('returns firstLogin notice for user with login_count <= 1', async () => {
@@ -115,7 +116,7 @@ describe('GET /api/system-notices/active', () => {
.get('/api/system-notices/active')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
// welcome-v1 is also in the registry and matches firstLogin, so at least TEST_NOTICE is present
// The always-on thank-you notice may also be present, so just assert TEST_NOTICE is there
const testNotice = res.body.find((n: { id: string }) => n.id === TEST_NOTICE.id);
expect(testNotice).toBeDefined();
// DTO should not expose conditions, publishedAt, minVersion, maxVersion, priority
@@ -139,7 +140,7 @@ describe('GET /api/system-notices/active', () => {
.get('/api/system-notices/active')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(res.body).toEqual([]);
expect(res.body.find((n: { id: string }) => n.id === TEST_NOTICE.id)).toBeUndefined();
} finally {
const idx = SYSTEM_NOTICES.indexOf(TEST_NOTICE);
if (idx !== -1) SYSTEM_NOTICES.splice(idx, 1);
@@ -161,7 +162,7 @@ describe('GET /api/system-notices/active', () => {
.get('/api/system-notices/active')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
// TEST_NOTICE should be filtered out; welcome-v1 may still appear
// TEST_NOTICE should be filtered out; the thank-you notice may still appear
const found = res.body.find((n: { id: string }) => n.id === TEST_NOTICE.id);
expect(found).toBeUndefined();
} finally {
@@ -169,6 +170,35 @@ describe('GET /api/system-notices/active', () => {
if (idx !== -1) SYSTEM_NOTICES.splice(idx, 1);
}
});
it('re-surfaces a per-version notice after an upgrade but hides it within the same version', async () => {
const TY = 'thank-you-support';
const { user } = createUser(testDb);
testDb.prepare('UPDATE users SET login_count = 5, first_seen_version = ? WHERE id = ?').run('3.0.0', user.id);
const shows = async () => {
const res = await request(app)
.get('/api/system-notices/active')
.set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
return res.body.some((n: { id: string }) => n.id === TY);
};
// Fresh user with no dismissal: the recurring thank-you shows.
expect(await shows()).toBe(true);
// Dismissed at an old version → it returns once the running version is newer.
testDb.prepare(
'INSERT INTO user_notice_dismissals (user_id, notice_id, dismissed_at, dismissed_app_version) VALUES (?, ?, ?, ?)'
).run(user.id, TY, Date.now(), '0.0.1');
expect(await shows()).toBe(true);
// Dismissed at a version >= the running one → stays hidden until the next upgrade.
testDb.prepare(
'UPDATE user_notice_dismissals SET dismissed_app_version = ? WHERE user_id = ? AND notice_id = ?'
).run('99.0.0', user.id, TY);
expect(await shows()).toBe(false);
});
});
// ─────────────────────────────────────────────────────────────────────────────
+8
View File
@@ -44,6 +44,14 @@ const system_notice: TranslationStrings = {
'system_notice.pager.position': 'الإشعار {current} من {total}',
'system_notice.dev_test_modal.title': '[Dev] Test notice', // en-fallback
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.', // en-fallback
'system_notice.thank_you_support.title': 'شكرًا لاستخدامك TREK',
'system_notice.thank_you_support.body':
'شكرًا سريعًا على تثبيتك TREK — هذا يعني لي الكثير حقًا.\n\nأنا مطوّر منفرد أبني TREK في وقت فراغي. بدأ كأداة صغيرة لرحلاتي الخاصة فحسب، وصدقًا أنا مندهش من الدعم والاهتمام اللذين أبداهما المجتمع منذ ذلك الحين. TREK مصنوع بكثير من الحب من جانبي — ولكن أيضًا بفضل العديد من المساهمين الخارجيين الرائعين الذين ساعدوا في تشكيله.\n\n**TREK مفتوح المصدر ومجاني تمامًا — وسيبقى كذلك إلى الأبد. لا باقات مدفوعة، لا اشتراكات، لا شروط خفية. أعدكم بذلك.**\n\nإذا كان TREK مفيدًا لك وأردت دعم تطويره، فإن فنجان قهوة صغيرًا يساعدني حقًا على مواصلة البناء — لا ضغط على الإطلاق، لكن كل فنجان يبقي الليالي المتأخرة مستمرة.\n\nشكرًا لوجودك هنا.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': 'مفتوح المصدر 100% على GitHub',
'system_notice.thank_you_support.highlight_free': 'مجاني للأبد — لا باقات مدفوعة أبدًا',
'system_notice.thank_you_support.highlight_community': 'مبني بالتعاون مع المجتمع',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'ادعمني على Ko-fi',
'system_notice.pager.counter': '{current} / {total}', // en-fallback
};
export default system_notice;
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Funciona offline no celular',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Obrigado por usar o TREK',
'system_notice.thank_you_support.body':
'Um obrigado rápido por instalar o TREK — isso significa muito para mim, de verdade.\n\nSou um desenvolvedor solo e construo o TREK no meu tempo livre. Tudo começou como uma ferramentinha só para as minhas próprias viagens, e confesso que fico maravilhado com o apoio e o interesse da comunidade desde então. O TREK é feito com muito carinho da minha parte — mas também graças aos muitos colaboradores externos incríveis que ajudaram a moldá-lo.\n\n**O TREK é open source e totalmente gratuito — e vai continuar assim para sempre. Sem planos pagos, sem assinaturas, sem pegadinhas. Eu prometo.**\n\nSe o TREK é útil para você e você quiser apoiar o seu desenvolvimento, um cafezinho ajuda muito a me manter construindo — sem nenhuma pressão, mas cada xícara mantém as noites longas em pé.\n\nObrigado por estar aqui.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% open source no GitHub',
'system_notice.thank_you_support.highlight_free': 'Gratuito para sempre — nunca planos pagos',
'system_notice.thank_you_support.highlight_community': 'Construído junto com a comunidade',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Apoiar no Ko-fi',
'system_notice.pager.prev': 'Aviso anterior',
'system_notice.pager.next': 'Próximo aviso',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Funguje offline na mobilu',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Děkuji, že používáte TREK',
'system_notice.thank_you_support.body':
'Rychlé poděkování za to, že jste si nainstalovali TREK — upřímně, znamená to pro mě hodně.\n\nJsem jediný vývojář a TREK tvořím ve svém volném čase. Začalo to jako malý nástroj jen pro mé vlastní cesty a od té doby mě podpora a zájem komunity naprosto dostávají. TREK dělám s velkým srdcem — ale také díky mnoha úžasným externím přispěvatelům, kteří ho pomohli utvářet.\n\n**TREK je open source a zcela zdarma — a tak to navždy zůstane. Žádné placené verze, žádná předplatná, žádné háčky. Slibuji.**\n\nPokud je pro vás TREK užitečný a chtěli byste podpořit jeho vývoj, malá káva mi opravdu pomáhá pokračovat ve tvoření — žádný tlak, ale každý šálek mi pomáhá přečkat pozdní noci.\n\nDěkuji, že jste tady.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% open source na GitHubu',
'system_notice.thank_you_support.highlight_free': 'Navždy zdarma — nikdy žádné placené verze',
'system_notice.thank_you_support.highlight_community': 'Tvořeno společně s komunitou',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Podpořit na Ko-fi',
'system_notice.pager.prev': 'Předchozí oznámení',
'system_notice.pager.next': 'Další oznámení',
'system_notice.pager.counter': '{current} / {total}',
+9
View File
@@ -11,6 +11,15 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Funktioniert offline auf dem Handy',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
// Dankeschön + Projektunterstützung (1x pro Installation und pro Update)
'system_notice.thank_you_support.title': 'Danke, dass du TREK nutzt',
'system_notice.thank_you_support.body':
'Ein kurzes Dankeschön, dass du TREK installiert hast — das bedeutet mir wirklich viel.\n\nIch bin Solo-Entwickler und baue TREK in meiner Freizeit. Angefangen hat alles als kleines Tool nur für meine eigenen Reisen, und ich bin ehrlich überwältigt von der Unterstützung und dem Interesse der Community seitdem. In TREK steckt viel Herzblut von meiner Seite — aber auch viele großartige externe Mitwirkende haben es mitgeprägt.\n\n**TREK ist Open Source und vollständig kostenlos — und das bleibt für immer so. Keine Paid Tiers, keine Abos, kein Haken. Versprochen.**\n\nWenn TREK dir nützt und du die Entwicklung unterstützen möchtest, hilft mir ein kleiner Kaffee wirklich beim Weitermachen — überhaupt kein Druck, aber jede Tasse trägt durch die langen Nächte.\n\nDanke, dass du dabei bist.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% Open Source auf GitHub',
'system_notice.thank_you_support.highlight_free': 'Für immer kostenlos keine Paid Tiers',
'system_notice.thank_you_support.highlight_community': 'Gemeinsam mit der Community gebaut',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Ko-fi unterstützen',
'system_notice.pager.prev': 'Vorherige Meldung',
'system_notice.pager.next': 'Nächste Meldung',
'system_notice.pager.counter': '{current} / {total}',
+9
View File
@@ -41,6 +41,15 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Works offline on mobile',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
// Thank-you + support the project (shown once per install and once per upgrade)
'system_notice.thank_you_support.title': 'Thank you for using TREK',
'system_notice.thank_you_support.body':
"A quick thank-you for installing TREK — it genuinely means a lot.\n\nI'm a solo developer and I build TREK in my spare time. It started as a little tool just for my own trips, and I'm honestly blown away by the support and interest from the community since then. TREK is made with a lot of heart on my side — but also thanks to the many amazing external contributors who've helped shape it.\n\n**TREK is open source and completely free — and it will stay that way forever. No paid tiers, no subscriptions, no catch. I promise.**\n\nIf TREK is useful to you and you'd like to support its development, a small coffee genuinely helps me keep building — no pressure at all, but every cup keeps the late nights going.\n\nThank you for being here.\n\n— Maurice",
'system_notice.thank_you_support.highlight_opensource': '100% open source on GitHub',
'system_notice.thank_you_support.highlight_free': 'Free forever — never any paid tiers',
'system_notice.thank_you_support.highlight_community': 'Built together with the community',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Support on Ko-fi',
'system_notice.pager.prev': 'Previous notice',
'system_notice.pager.next': 'Next notice',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Funciona sin conexión en móvil',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Gracias por usar TREK',
'system_notice.thank_you_support.body':
'Un pequeño agradecimiento por instalar TREK — de verdad significa mucho para mí.\n\nSoy un desarrollador independiente y construyo TREK en mi tiempo libre. Empezó como una pequeña herramienta solo para mis propios viajes, y sinceramente me deja sin palabras todo el apoyo y el interés de la comunidad desde entonces. TREK está hecho con mucho cariño de mi parte — pero también gracias a los muchos colaboradores externos increíbles que han ayudado a darle forma.\n\n**TREK es open source y completamente gratuito — y seguirá siéndolo para siempre. Sin planes de pago, sin suscripciones, sin trampa. Te lo prometo.**\n\nSi TREK te resulta útil y quieres apoyar su desarrollo, un pequeño café me ayuda de verdad a seguir construyendo — sin ninguna presión, pero cada taza mantiene vivas las noches en vela.\n\nGracias por estar aquí.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% open source en GitHub',
'system_notice.thank_you_support.highlight_free': 'Gratis para siempre — nunca habrá planes de pago',
'system_notice.thank_you_support.highlight_community': 'Construido junto a la comunidad',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Apóyame en Ko-fi',
'system_notice.pager.prev': 'Aviso anterior',
'system_notice.pager.next': 'Siguiente aviso',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Fonctionne hors ligne sur mobile',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': "Merci d'utiliser TREK",
'system_notice.thank_you_support.body':
"Un petit mot pour te remercier d'avoir installé TREK — ça compte vraiment beaucoup pour moi.\n\nJe suis développeur en solo et je construis TREK sur mon temps libre. Au départ, c'était juste un petit outil pour mes propres voyages, et je suis honnêtement bluffé par le soutien et l'intérêt de la communauté depuis. TREK est fait avec beaucoup de cœur de mon côté — mais aussi grâce aux nombreux et formidables contributeurs externes qui ont aidé à lui donner forme.\n\n**TREK est open source et entièrement gratuit — et le restera pour toujours. Pas de formules payantes, pas d'abonnements, aucun piège. Promis.**\n\nSi TREK t'est utile et que tu souhaites soutenir son développement, un petit café m'aide sincèrement à continuer — sans aucune pression, mais chaque tasse fait avancer les nuits blanches.\n\nMerci d'être là.\n\n— Maurice",
'system_notice.thank_you_support.highlight_opensource': '100 % open source sur GitHub',
'system_notice.thank_you_support.highlight_free': 'Gratuit pour toujours — jamais de formules payantes',
'system_notice.thank_you_support.highlight_community': 'Construit avec la communauté',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Soutenir sur Ko-fi',
'system_notice.pager.prev': 'Avis précédent',
'system_notice.pager.next': 'Avis suivant',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -43,6 +43,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Λειτουργεί εκτός σύνδεσης σε κινητά',
'system_notice.dev_test_modal.title': '[Dev] Δοκιμαστική ειδοποίηση',
'system_notice.dev_test_modal.body': 'Αυτή είναι μια δοκιμαστική ειδοποίηση μόνο για ανάπτυξη.',
'system_notice.thank_you_support.title': 'Ευχαριστώ που χρησιμοποιείτε το TREK',
'system_notice.thank_you_support.body':
'Ένα γρήγορο ευχαριστώ που εγκαταστήσατε το TREK — σημαίνει πραγματικά πολλά για μένα.\n\nΕίμαι ένας μόνος προγραμματιστής και φτιάχνω το TREK στον ελεύθερό μου χρόνο. Ξεκίνησε ως ένα μικρό εργαλείο μόνο για τα δικά μου ταξίδια, και ειλικρινά με συγκλονίζει η στήριξη και το ενδιαφέρον της κοινότητας από τότε. Το TREK φτιάχνεται με πολλή αγάπη από τη δική μου πλευρά — αλλά και χάρη στους πολλούς υπέροχους εξωτερικούς συνεισφέροντες που βοήθησαν να το διαμορφώσουν.\n\n**Το TREK είναι ανοιχτού κώδικα και εντελώς δωρεάν — και θα παραμείνει έτσι για πάντα. Καμία έκδοση επί πληρωμή, καμία συνδρομή, καμία παγίδα. Το υπόσχομαι.**\n\nΑν το TREK σάς είναι χρήσιμο και θέλετε να στηρίξετε την ανάπτυξή του, ένας μικρός καφές με βοηθά πραγματικά να συνεχίζω να φτιάχνω — καμία πίεση, αλλά κάθε φλιτζάνι κρατά ζωντανές τις ξενύχτιες.\n\nΣας ευχαριστώ που είστε εδώ.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% ανοιχτού κώδικα στο GitHub',
'system_notice.thank_you_support.highlight_free': 'Δωρεάν για πάντα — ποτέ επί πληρωμή',
'system_notice.thank_you_support.highlight_community': 'Φτιαγμένο μαζί με την κοινότητα',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Στηρίξτε στο Ko-fi',
'system_notice.pager.prev': 'Προηγούμενη ειδοποίηση',
'system_notice.pager.next': 'Επόμενη ειδοποίηση',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Mobilon offline is működik',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Köszönöm, hogy a TREK-et használod',
'system_notice.thank_you_support.body':
'Gyors köszönet, hogy telepítetted a TREK-et — őszintén sokat jelent.\n\nEgyedül fejlesztek, és a szabadidőmben építem a TREK-et. Egy kis eszközként indult, csak a saját utazásaimhoz, és azóta őszintén lenyűgöz a közösség támogatása és érdeklődése. A TREK sok szívvel készül a részemről — de annak a sok csodálatos külső közreműködőnek is köszönhetően, akik segítettek formálni.\n\n**A TREK nyílt forráskódú és teljesen ingyenes — és ez örökre így is marad. Nincsenek fizetős csomagok, nincsenek előfizetések, nincs semmi átverés. Ígérem.**\n\nHa a TREK hasznos számodra, és szeretnéd támogatni a fejlesztését, egy kis kávé őszintén segít, hogy tovább építhessem — semmi nyomás, de minden csésze átsegít a késő éjszakákon.\n\nKöszönöm, hogy itt vagy.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% nyílt forráskódú a GitHubon',
'system_notice.thank_you_support.highlight_free': 'Örökre ingyenes — soha semmi fizetős csomag',
'system_notice.thank_you_support.highlight_community': 'A közösséggel együtt épült',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Támogass a Ko-fi-n',
'system_notice.pager.prev': 'Előző értesítés',
'system_notice.pager.next': 'Következő értesítés',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Bekerja offline di ponsel',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Terima kasih telah memakai TREK',
'system_notice.thank_you_support.body':
'Sekadar ucapan terima kasih singkat karena telah memasang TREK — ini benar-benar berarti banyak bagi saya.\n\nSaya seorang developer solo dan membangun TREK di waktu luang. Awalnya hanya alat kecil untuk perjalanan saya sendiri, dan sejujurnya saya terharu dengan dukungan serta minat dari komunitas sejak saat itu. TREK dibuat dengan sepenuh hati dari saya — tetapi juga berkat banyak kontributor eksternal hebat yang telah membantu membentuknya.\n\n**TREK bersifat open source dan sepenuhnya gratis — dan akan selalu begitu, selamanya. Tanpa paket berbayar, tanpa langganan, tanpa syarat tersembunyi. Saya janji.**\n\nJika TREK bermanfaat bagimu dan kamu ingin mendukung pengembangannya, secangkir kopi kecil sungguh membantu saya untuk terus membangun — sama sekali tanpa paksaan, tapi setiap cangkir membuat malam-malam panjang ini tetap berjalan.\n\nTerima kasih telah berada di sini.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% open source di GitHub',
'system_notice.thank_you_support.highlight_free': 'Gratis selamanya — tanpa paket berbayar',
'system_notice.thank_you_support.highlight_community': 'Dibangun bersama komunitas',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Dukung di Ko-fi',
'system_notice.pager.prev': 'Pemberitahuan sebelumnya',
'system_notice.pager.next': 'Pemberitahuan berikutnya',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Funziona offline su mobile',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Grazie per usare TREK',
'system_notice.thank_you_support.body':
"Un piccolo grazie per aver installato TREK — significa davvero molto per me.\n\nSono uno sviluppatore indipendente e creo TREK nel mio tempo libero. È nato come un piccolo strumento solo per i miei viaggi, e sono sinceramente sbalordito dal supporto e dall'interesse che la community mi ha dimostrato da allora. TREK è fatto con tanto cuore da parte mia — ma anche grazie ai tanti fantastici collaboratori esterni che hanno contribuito a dargli forma.\n\n**TREK è open source e completamente gratuito — e resterà così per sempre. Nessun piano a pagamento, nessun abbonamento, nessuna fregatura. Te lo prometto.**\n\nSe TREK ti è utile e vuoi sostenerne lo sviluppo, un piccolo caffè mi aiuta davvero a continuare a costruirlo — nessuna pressione, ma ogni tazza tiene vive le notti tarde.\n\nGrazie per essere qui.\n\n— Maurice",
'system_notice.thank_you_support.highlight_opensource': '100% open source su GitHub',
'system_notice.thank_you_support.highlight_free': 'Gratis per sempre — mai un piano a pagamento',
'system_notice.thank_you_support.highlight_community': 'Creato insieme alla community',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Supporta su Ko-fi',
'system_notice.pager.prev': 'Avviso precedente',
'system_notice.pager.next': 'Avviso successivo',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -39,6 +39,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'モバイルでオフライン対応',
'system_notice.dev_test_modal.title': '[Dev] テスト通知',
'system_notice.dev_test_modal.body': 'これは開発用テスト通知です。',
'system_notice.thank_you_support.title': 'TREKを使ってくれてありがとう',
'system_notice.thank_you_support.body':
'TREKをインストールしてくれて、ちょっとお礼を言わせてください。本当に、心から嬉しいです。\n\n私は一人で開発をしていて、TREKは空いた時間に作っています。もともとは自分の旅のためだけの小さなツールでしたが、それ以来コミュニティから寄せられる応援や関心に、正直なところ圧倒されています。TREKは私自身がたくさんの思いを込めて作っていますが、それと同時に、形づくるのを手伝ってくれた多くの素晴らしい外部コントリビューターのおかげでもあります。\n\n**TREKはオープンソースで、完全に無料です。そしてこれからもずっと変わりません。有料プランも、サブスクリプションも、隠れた仕掛けも、一切ありません。約束します。**\n\nもしTREKがあなたの役に立っていて、開発を応援したいと思ってもらえたなら、ちょっとしたコーヒー一杯が、これからも作り続ける本当の支えになります。まったく無理はしないでください。でも一杯ごとに、夜なべの作業が続けられます。\n\nここにいてくれて、ありがとう。\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': 'GitHubで100%オープンソース',
'system_notice.thank_you_support.highlight_free': '永久に無料 — 有料プランは一切なし',
'system_notice.thank_you_support.highlight_community': 'コミュニティと一緒に作っています',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Ko-fiで応援する',
'system_notice.pager.prev': '前へ',
'system_notice.pager.next': '次へ',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -41,6 +41,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': '모바일에서 오프라인으로 작동',
'system_notice.dev_test_modal.title': '[Dev] 테스트 공지',
'system_notice.dev_test_modal.body': '개발 전용 테스트 공지입니다.',
'system_notice.thank_you_support.title': 'TREK을 사용해 주셔서 감사합니다',
'system_notice.thank_you_support.body':
'TREK을 설치해 주셔서 감사하다는 짧은 인사를 전하고 싶습니다 — 정말 큰 힘이 됩니다.\n\n저는 1인 개발자이고, 여가 시간에 TREK을 만들고 있습니다. 처음에는 그저 제 여행을 위한 작은 도구로 시작했는데, 그 이후로 커뮤니티에서 보내주신 응원과 관심에 솔직히 놀라움을 감추지 못하고 있습니다. TREK은 제 온 마음을 담아 만들었지만 — 이 프로젝트를 함께 다듬어 주신 많은 멋진 외부 기여자분들 덕분이기도 합니다.\n\n**TREK은 오픈 소스이며 완전히 무료입니다 — 그리고 앞으로도 영원히 그럴 것입니다. 유료 등급도, 구독도, 숨겨진 조건도 없습니다. 약속드릴게요.**\n\nTREK이 도움이 되셨고 개발을 응원하고 싶으시다면, 작은 커피 한 잔이 제가 계속 만들어 나가는 데 정말 큰 힘이 됩니다 — 전혀 부담 갖지 마세요. 하지만 한 잔 한 잔이 늦은 밤을 버티게 해줍니다.\n\n함께해 주셔서 감사합니다.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': 'GitHub에서 100% 오픈 소스',
'system_notice.thank_you_support.highlight_free': '영원히 무료 — 유료 등급 절대 없음',
'system_notice.thank_you_support.highlight_community': '커뮤니티와 함께 만들어 갑니다',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Ko-fi에서 후원하기',
'system_notice.pager.prev': '이전 공지',
'system_notice.pager.next': '다음 공지',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Werkt offline op mobiel',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Bedankt voor het gebruik van TREK',
'system_notice.thank_you_support.body':
"Even een kort bedankje dat je TREK hebt geïnstalleerd — het betekent echt veel voor me.\n\nIk ben een solo-ontwikkelaar en bouw TREK in mijn vrije tijd. Het begon als een klein hulpmiddel voor mijn eigen reizen, en ik ben oprecht overweldigd door de steun en de interesse vanuit de community sindsdien. TREK is met heel veel hart gemaakt aan mijn kant — maar ook dankzij de vele geweldige externe bijdragers die hebben geholpen het vorm te geven.\n\n**TREK is open source en volledig gratis — en dat zal het voor altijd blijven. Geen betaalde versies, geen abonnementen, geen addertjes onder het gras. Dat beloof ik.**\n\nAls TREK nuttig voor je is en je de ontwikkeling ervan wilt steunen, helpt een klein kopje koffie me oprecht om te blijven bouwen — absoluut geen druk, maar elk kopje houdt de late avonden gaande.\n\nBedankt dat je er bent.\n\n— Maurice",
'system_notice.thank_you_support.highlight_opensource': '100% open source op GitHub',
'system_notice.thank_you_support.highlight_free': 'Voor altijd gratis — nooit betaalde versies',
'system_notice.thank_you_support.highlight_community': 'Samen met de community gebouwd',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Steun op Ko-fi',
'system_notice.pager.prev': 'Vorige melding',
'system_notice.pager.next': 'Volgende melding',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Działa offline na telefonie',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Dziękuję, że korzystasz z TREK',
'system_notice.thank_you_support.body':
'Krótkie podziękowanie za zainstalowanie TREK — naprawdę wiele dla mnie znaczy.\n\nJestem samodzielnym programistą i tworzę TREK po godzinach. Wszystko zaczęło się jako małe narzędzie tylko na moje własne podróże, a wsparcie i zainteresowanie ze strony społeczności od tamtej pory szczerze mnie powaliły. TREK powstaje z wielkim sercem z mojej strony — ale także dzięki wielu wspaniałym zewnętrznym współtwórcom, którzy pomogli go ukształtować.\n\n**TREK jest open source i całkowicie darmowy — i tak już zostanie na zawsze. Bez płatnych wersji, bez subskrypcji, bez haczyków. Obiecuję.**\n\nJeśli TREK jest dla Ciebie przydatny i chciałbyś wesprzeć jego rozwój, mała kawa naprawdę pomaga mi tworzyć dalej — bez żadnej presji, ale każda filiżanka pozwala przetrwać kolejne nocne sesje przy kodzie.\n\nDziękuję, że tu jesteś.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% open source na GitHubie',
'system_notice.thank_you_support.highlight_free': 'Darmowy na zawsze — nigdy żadnych płatnych wersji',
'system_notice.thank_you_support.highlight_community': 'Tworzony wspólnie ze społecznością',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Wesprzyj na Ko-fi',
'system_notice.pager.prev': 'Poprzednie powiadomienie',
'system_notice.pager.next': 'Następne powiadomienie',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Работает офлайн на мобильном',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Спасибо, что выбрали TREK',
'system_notice.thank_you_support.body':
'Небольшое спасибо за то, что установили TREK — для меня это правда очень много значит.\n\nЯ разработчик-одиночка и делаю TREK в свободное время. Всё началось как маленький инструмент для моих собственных поездок, и я, честно говоря, поражён той поддержкой и интересом, которые проявило сообщество с тех пор. TREK создаётся с большой любовью с моей стороны — но также благодаря множеству замечательных внешних участников, которые помогли его сформировать.\n\n**TREK — это открытый исходный код и полностью бесплатно — и так будет всегда. Никаких платных тарифов, никаких подписок, никаких подвохов. Обещаю.**\n\nЕсли TREK вам полезен и вы хотите поддержать его развитие, маленький кофе по-настоящему помогает мне продолжать — без всякого давления, но каждая чашка даёт силы для поздних ночей.\n\nСпасибо, что вы здесь.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% открытый код на GitHub',
'system_notice.thank_you_support.highlight_free': 'Бесплатно навсегда — без платных тарифов',
'system_notice.thank_you_support.highlight_community': 'Создаётся вместе с сообществом',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Поддержать на Ko-fi',
'system_notice.pager.prev': 'Предыдущее уведомление',
'system_notice.pager.next': 'Следующее уведомление',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -41,6 +41,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Fungerar offline på mobilen',
'system_notice.dev_test_modal.title': '[Dev] Meddelande om test',
'system_notice.dev_test_modal.body': 'Detta är ett testmeddelande avsett endast för utvecklare.',
'system_notice.thank_you_support.title': 'Tack för att du använder TREK',
'system_notice.thank_you_support.body':
'Ett snabbt tack för att du installerade TREK det betyder verkligen mycket.\n\nJag är en ensam utvecklare och bygger TREK på min fritid. Det började som ett litet verktyg bara för mina egna resor, och jag är ärligt talat överväldigad av allt stöd och intresse från communityn sedan dess. TREK är skapat med mycket hjärta från min sida men också tack vare de många fantastiska externa bidragsgivare som har hjälpt till att forma det.\n\n**TREK är öppen källkod och helt gratis och kommer alltid att förbli så. Inga betalnivåer, inga prenumerationer, inga förbehåll. Jag lovar.**\n\nOm TREK är användbart för dig och du vill stödja utvecklingen, så hjälper en liten kaffe mig verkligen att fortsätta bygga ingen press alls, men varje kopp håller de sena nätterna igång.\n\nTack för att du är här.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100 % öppen källkod på GitHub',
'system_notice.thank_you_support.highlight_free': 'Gratis för alltid aldrig några betalnivåer',
'system_notice.thank_you_support.highlight_community': 'Byggt tillsammans med communityn',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Stöd på Ko-fi',
'system_notice.pager.prev': 'Tidigare meddelande',
'system_notice.pager.next': 'Nästa meddelande',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -41,6 +41,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Mobilde çevrimdışı çalışır',
'system_notice.dev_test_modal.title': '[Dev] Test bildirimi',
'system_notice.dev_test_modal.body': 'Bu yalnızca geliştirme ortamına özel bir test bildirimidir.',
'system_notice.thank_you_support.title': "TREK'i kullandığınız için teşekkürler",
'system_notice.thank_you_support.body':
"TREK'i yüklediğin için kısaca teşekkür etmek istiyorum — bu benim için gerçekten çok değerli.\n\nTek başına çalışan bir geliştiriciyim ve TREK'i boş zamanlarımda geliştiriyorum. Başlangıçta yalnızca kendi seyahatlerim için yaptığım küçük bir araçtı; o günden beri topluluktan gelen destek ve ilgi beni gerçekten hayrete düşürdü. TREK'i kendi adıma büyük bir sevgiyle hazırlıyorum — ama ona şekil vermeye yardım eden onca harika dış katkıcının da büyük payı var.\n\n**TREK açık kaynaklı ve tamamen ücretsiz — ve sonsuza dek böyle kalacak. Ücretli paketler yok, abonelikler yok, gizli bir şart yok. Söz veriyorum.**\n\nTREK işine yarıyorsa ve gelişimine destek olmak istersen, küçük bir kahve geliştirmeye devam etmeme cidden yardımcı oluyor — hiçbir baskı yok ama her fincan, o geç saatlere kadar süren çalışmaları ayakta tutuyor.\n\nBurada olduğun için teşekkür ederim.\n\n— Maurice",
'system_notice.thank_you_support.highlight_opensource': "GitHub'da %100 açık kaynak",
'system_notice.thank_you_support.highlight_free': 'Sonsuza dek ücretsiz — hiçbir ücretli plan yok',
'system_notice.thank_you_support.highlight_community': 'Toplulukla birlikte geliştirildi',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': "Ko-fi'de Destek Ol",
'system_notice.pager.prev': 'Önceki bildirim',
'system_notice.pager.next': 'Sonraki bildirim',
'system_notice.pager.counter': '{güncel} / {toplam}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': 'Працює офлайн на мобільному',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': 'Дякую, що користуєтесь TREK',
'system_notice.thank_you_support.body':
'Невелика подяка за те, що встановили TREK — для мене це справді багато значить.\n\nЯ розробник-одинак і створюю TREK у вільний час. Усе почалося як маленький інструмент для моїх власних поїздок, і відтоді я щиро вражений підтримкою та інтересом спільноти. TREK зроблено з великою любов’ю з мого боку — але також завдяки багатьом чудовим зовнішнім контриб’юторам, які допомогли його сформувати.\n\n**TREK має відкритий код і повністю безкоштовний — і таким залишиться назавжди. Жодних платних тарифів, жодних підписок, жодних підводних каменів. Обіцяю.**\n\nЯкщо TREK корисний для вас і ви хочете підтримати його розробку, невелика кава справді допомагає мені продовжувати — жодного тиску, але кожна чашка підтримує ці пізні ночі.\n\nДякую, що ви тут.\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '100% відкритий код на GitHub',
'system_notice.thank_you_support.highlight_free': 'Безкоштовно назавжди — жодних платних тарифів',
'system_notice.thank_you_support.highlight_community': 'Створено разом зі спільнотою',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': 'Підтримати на Ko-fi',
'system_notice.pager.prev': 'Попереднє повідомлення',
'system_notice.pager.next': 'Наступне повідомлення',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -11,6 +11,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': '行動裝置支援離線使用',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': '感謝你使用 TREK',
'system_notice.thank_you_support.body':
'想簡單地對你說聲謝謝——謝謝你安裝了 TREK,這對我來說真的意義重大。\n\n我是一名獨立開發者,利用業餘時間打造 TREK。它最初只是我為自己的旅行做的一個小工具,而自那以後社群給予的支持與關注,老實說讓我感到無比驚喜。TREK 是我傾注了許多心血做出來的——但也要感謝許多了不起的外部貢獻者,是他們一起塑造了它。\n\n**TREK 是開源且完全免費的——而且永遠都會如此。沒有付費方案,沒有訂閱,沒有任何附加條件。我保證。**\n\n如果 TREK 對你有幫助,而你願意支持它的開發,一杯小小的咖啡真的能幫助我繼續做下去——完全不必有任何壓力,但每一杯都讓那些熬夜的時光更有動力。\n\n謝謝你來到這裡。\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '在 GitHub 上 100% 開源',
'system_notice.thank_you_support.highlight_free': '永遠免費 — 絕無任何付費方案',
'system_notice.thank_you_support.highlight_community': '與社群一起攜手打造',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': '在 Ko-fi 上支持我',
'system_notice.pager.prev': '上一則通知',
'system_notice.pager.next': '下一則通知',
'system_notice.pager.counter': '{current} / {total}',
+8
View File
@@ -10,6 +10,14 @@ const system_notice: TranslationStrings = {
'system_notice.welcome_v1.highlight_offline': '移动端支持离线使用',
'system_notice.dev_test_modal.title': '[Dev] Test notice',
'system_notice.dev_test_modal.body': 'This is a dev-only test notice.',
'system_notice.thank_you_support.title': '感谢你使用 TREK',
'system_notice.thank_you_support.body':
'想跟你说声谢谢——谢谢你安装了 TREK,这对我来说真的意义非凡。\n\n我是一名独立开发者,TREK 是我利用业余时间打造的。它最初只是我为自己的旅行做的一个小工具,而社区一路以来给予的支持和关注,老实说让我感到无比惊喜。TREK 是我用满满的热爱做出来的——但也离不开许多了不起的外部贡献者,是他们一起塑造了今天的它。\n\n**TREK 是开源的,完全免费——而且永远都会如此。没有付费档位,没有订阅,没有任何套路。我保证。**\n\n如果 TREK 对你有帮助,并且你愿意支持它的开发,请我喝一杯小小的咖啡,真的能帮我把它继续做下去——完全没有任何压力,但每一杯都让那些挑灯夜战的夜晚有了坚持的动力。\n\n谢谢你来到这里。\n\n— Maurice',
'system_notice.thank_you_support.highlight_opensource': '在 GitHub 上 100% 开源',
'system_notice.thank_you_support.highlight_free': '永久免费——绝无付费档位',
'system_notice.thank_you_support.highlight_community': '与社区一起共建',
'system_notice.thank_you_support.cta_bmc': 'Buy Me a Coffee',
'system_notice.thank_you_support.cta_kofi': '在 Ko-fi 上支持',
'system_notice.pager.prev': '上一条通知',
'system_notice.pager.next': '下一条通知',
'system_notice.pager.counter': '{current} / {total}',
@@ -30,9 +30,10 @@ const noticeHighlightSchema = z.object({
iconName: z.string().optional(),
});
/** Call-to-action: either a navigation link or an in-app action. */
/** Call-to-action: an internal nav, an external link (new tab), or an in-app action. */
const noticeCtaSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('nav'), labelKey: z.string(), href: z.string() }),
z.object({ kind: z.literal('link'), labelKey: z.string(), href: z.string() }),
z.object({
kind: z.literal('action'),
labelKey: z.string(),
@@ -53,6 +54,8 @@ export const systemNoticeDtoSchema = z.object({
media: noticeMediaSchema.optional(),
highlights: z.array(noticeHighlightSchema).optional(),
cta: noticeCtaSchema.optional(),
secondaryCta: noticeCtaSchema.optional(),
desktopOnly: z.boolean().optional(),
dismissible: z.boolean(),
});
export type SystemNoticeDto = z.infer<typeof systemNoticeDtoSchema>;