mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-30 18:46:00 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72f9beffbe | |||
| 1ba8eecfbb |
+3
-1
@@ -65,4 +65,6 @@ coverage
|
||||
test-data
|
||||
|
||||
.run
|
||||
.full-review
|
||||
.full-review
|
||||
# Wiki offline snapshot is baked in at build, not committed (duplicates wiki/)
|
||||
server/assets/wiki/
|
||||
|
||||
@@ -13,6 +13,7 @@ import FilesPage from './pages/FilesPage'
|
||||
import AdminPage from './pages/AdminPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
import VacayPage from './pages/VacayPage'
|
||||
import HelpPage from './pages/HelpPage'
|
||||
import AtlasPage from './pages/AtlasPage'
|
||||
import JourneyPage from './pages/JourneyPage'
|
||||
import JourneyDetailPage from './pages/JourneyDetailPage'
|
||||
@@ -221,6 +222,22 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/help"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<HelpPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/help/:slug"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<HelpPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/trips/:id"
|
||||
element={
|
||||
|
||||
@@ -703,6 +703,17 @@ export const configApi = {
|
||||
apiClient.get('/config').then(r => r.data),
|
||||
}
|
||||
|
||||
export interface HelpNavItem { title: string; slug: string }
|
||||
export interface HelpNavSection { title: string; pages: HelpNavItem[] }
|
||||
export interface HelpPageData { slug: string; title: string; markdown: string }
|
||||
|
||||
export const helpApi = {
|
||||
index: (): Promise<{ sections: HelpNavSection[] }> =>
|
||||
apiClient.get('/help/index').then(r => r.data),
|
||||
page: (slug: string): Promise<HelpPageData> =>
|
||||
apiClient.get(`/help/page/${encodeURIComponent(slug)}`).then(r => r.data),
|
||||
}
|
||||
|
||||
export const settingsApi = {
|
||||
get: () => apiClient.get('/settings').then(r => r.data),
|
||||
set: (key: string, value: unknown) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAuthStore } from '../../store/authStore'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { useAddonStore } from '../../store/addonStore'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import { Plane, LogOut, Settings, ChevronDown, Shield, ArrowLeft, Users, Moon, Sun, Monitor, CalendarDays, Briefcase, Globe, Compass } from 'lucide-react'
|
||||
import { Plane, LogOut, Settings, ChevronDown, Shield, ArrowLeft, Users, Moon, Sun, Monitor, CalendarDays, Briefcase, Globe, Compass, BookOpen } from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import InAppNotificationBell from './InAppNotificationBell.tsx'
|
||||
|
||||
@@ -252,6 +252,14 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
|
||||
{t('nav.settings')}
|
||||
</Link>
|
||||
|
||||
<Link to="/help" onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm transition-colors text-content-secondary"
|
||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
{t('nav.help')}
|
||||
</Link>
|
||||
|
||||
{user.role === 'admin' && (
|
||||
<Link to="/admin" onClick={() => setUserMenuOpen(false)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm transition-colors text-content-secondary"
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Search, ChevronRight, Loader2, AlertCircle, BookOpen, PanelLeft, X } from 'lucide-react'
|
||||
import PageShell from '../components/Layout/PageShell'
|
||||
import { useTranslation } from '../i18n'
|
||||
import { useHelp } from './help/useHelp'
|
||||
|
||||
export default function HelpPage() {
|
||||
const { t } = useTranslation()
|
||||
const { page, loading, pageError, query, setQuery, navOpen, setNavOpen, contentRef, activeSlug, filtered } =
|
||||
useHelp()
|
||||
|
||||
const nav = (
|
||||
<nav className="flex flex-col gap-5">
|
||||
<div className="relative">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-content-faint" />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t('help.search')}
|
||||
className="w-full bg-surface-tertiary text-content rounded-lg pl-9 pr-3 py-2 text-[13px] outline-none border border-transparent focus:border-edge"
|
||||
/>
|
||||
</div>
|
||||
{filtered.map((section) => (
|
||||
<div key={section.title}>
|
||||
{section.title && (
|
||||
<h3 className="text-[10px] font-semibold tracking-[0.1em] uppercase text-content-faint mb-1.5 px-2">
|
||||
{section.title}
|
||||
</h3>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
{section.pages.map((p) => {
|
||||
const active = p.slug === activeSlug
|
||||
return (
|
||||
<Link
|
||||
key={p.slug}
|
||||
to={`/help/${p.slug}`}
|
||||
className={`flex items-center gap-1.5 px-2 py-1.5 rounded-lg text-[13px] transition-colors ${
|
||||
active
|
||||
? 'bg-accent-subtle text-accent font-semibold'
|
||||
: 'text-content-secondary hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{active && <ChevronRight size={13} className="shrink-0" />}
|
||||
<span className={active ? '' : 'pl-[18px]'}>{p.title}</span>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!filtered.length && <p className="text-[12px] text-content-faint px-2">{t('help.noResults')}</p>}
|
||||
</nav>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageShell className="bg-surface-secondary" navOffset="var(--nav-h, 56px)">
|
||||
<div className="max-w-[1600px] mx-auto px-4 lg:px-10 py-6 flex gap-10">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden lg:block w-[260px] shrink-0">
|
||||
<div className="sticky top-[calc(var(--nav-h,56px)+24px)] max-h-[calc(100vh-var(--nav-h,56px)-48px)] overflow-y-auto pr-1">
|
||||
<div className="flex items-center gap-2 mb-4 px-2">
|
||||
<BookOpen size={16} className="text-accent" />
|
||||
<span className="text-[14px] font-bold text-content">{t('help.title')}</span>
|
||||
</div>
|
||||
{nav}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Content */}
|
||||
<main className="flex-1 min-w-0" ref={contentRef}>
|
||||
{/* Mobile nav toggle */}
|
||||
<button
|
||||
onClick={() => setNavOpen(true)}
|
||||
className="lg:hidden inline-flex items-center gap-2 mb-4 px-3 py-2 rounded-lg bg-surface-card border border-edge text-[13px] font-medium text-content"
|
||||
>
|
||||
<PanelLeft size={15} /> {t('help.contents')}
|
||||
</button>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-24 text-content-faint">
|
||||
<Loader2 size={22} className="animate-spin" />
|
||||
</div>
|
||||
) : pageError ? (
|
||||
<div className="flex flex-col items-center justify-center gap-2 py-24 text-center">
|
||||
<AlertCircle size={28} className="text-content-faint" />
|
||||
<p className="text-[14px] font-semibold text-content">{t('help.errorTitle')}</p>
|
||||
<p className="text-[13px] text-content-faint max-w-sm">{t('help.errorBody')}</p>
|
||||
</div>
|
||||
) : page ? (
|
||||
<article className="wiki-prose max-w-[1040px]">
|
||||
<WikiContent markdown={page.markdown} />
|
||||
</article>
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile sidebar drawer */}
|
||||
{navOpen && (
|
||||
<div className="lg:hidden fixed inset-0 z-[120]" onClick={() => setNavOpen(false)}>
|
||||
<div className="absolute inset-0 bg-black/40" />
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-[280px] bg-surface-card p-5 overflow-y-auto shadow-xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-[14px] font-bold text-content flex items-center gap-2">
|
||||
<BookOpen size={16} className="text-accent" /> {t('help.title')}
|
||||
</span>
|
||||
<button onClick={() => setNavOpen(false)} className="text-content-faint">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{nav}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageShell>
|
||||
)
|
||||
}
|
||||
|
||||
/** Markdown renderer with TREK-styled elements and SPA-internal links. */
|
||||
function WikiContent({ markdown }: { markdown: string }) {
|
||||
return (
|
||||
<Markdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-[26px] font-bold text-content mt-1 mb-4 leading-tight">{children}</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-[19px] font-bold text-content mt-8 mb-3 pb-1.5 border-b border-edge-secondary">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => <h3 className="text-[15.5px] font-semibold text-content mt-6 mb-2">{children}</h3>,
|
||||
h4: ({ children }) => <h4 className="text-[14px] font-semibold text-content mt-5 mb-2">{children}</h4>,
|
||||
p: ({ children }) => <p className="text-[14px] text-content-secondary leading-[1.7] my-3">{children}</p>,
|
||||
ul: ({ children }) => <ul className="list-disc pl-5 my-3 space-y-1.5 text-[14px] text-content-secondary">{children}</ul>,
|
||||
ol: ({ children }) => <ol className="list-decimal pl-5 my-3 space-y-1.5 text-[14px] text-content-secondary">{children}</ol>,
|
||||
li: ({ children }) => <li className="leading-[1.6]">{children}</li>,
|
||||
a: ({ href, children }) => {
|
||||
const url = href ?? ''
|
||||
if (url.startsWith('#')) return <a href={url} className="text-accent hover:underline">{children}</a>
|
||||
if (url.startsWith('/')) return <Link to={url} className="text-accent hover:underline font-medium">{children}</Link>
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline font-medium">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
img: ({ src, alt }) => (
|
||||
<img src={typeof src === 'string' ? src : ''} alt={alt} loading="lazy" className="rounded-lg border border-edge my-4 max-w-full" />
|
||||
),
|
||||
code: ({ className, children }) => {
|
||||
const isBlock = (className ?? '').includes('language-')
|
||||
if (isBlock) return <code className={className}>{children}</code>
|
||||
return <code className="bg-surface-tertiary text-content rounded px-1.5 py-0.5 text-[12.5px] font-mono">{children}</code>
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-surface-tertiary text-content rounded-xl p-4 my-4 overflow-x-auto text-[12.5px] font-mono leading-relaxed border border-edge-secondary">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-3 border-accent bg-accent-subtle/40 rounded-r-lg px-4 py-1 my-4 text-content-secondary">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto my-4">
|
||||
<table className="w-full text-[13px] border-collapse">{children}</table>
|
||||
</div>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="text-left font-semibold text-content border border-edge-secondary px-3 py-2 bg-surface-tertiary">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => <td className="text-content-secondary border border-edge-secondary px-3 py-2">{children}</td>,
|
||||
hr: () => <hr className="my-6 border-edge-secondary" />,
|
||||
}}
|
||||
>
|
||||
{markdown}
|
||||
</Markdown>
|
||||
)
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export default function LoginPage(): React.ReactElement {
|
||||
navigate,
|
||||
mode, setMode,
|
||||
username, setUsername, email, setEmail, password, setPassword, rememberMe, setRememberMe, showPassword, setShowPassword,
|
||||
isLoading, error, setError, appConfig, inviteToken,
|
||||
isLoading, error, setError, insecureCookie, appConfig, inviteToken,
|
||||
langDropdownOpen, setLangDropdownOpen, setLanguageLocal,
|
||||
showTakeoff, mfaStep, setMfaStep, mfaToken, setMfaToken, mfaCode, setMfaCode,
|
||||
passwordChangeStep, newPassword, setNewPassword, confirmPassword, setConfirmPassword,
|
||||
@@ -447,6 +447,17 @@ export default function LoginPage(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{insecureCookie && (
|
||||
<div style={{ padding: '12px 14px', background: '#fffbeb', border: '1px solid #fde68a', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', color: '#92400e' }}>
|
||||
<div style={{ fontWeight: 700, marginBottom: 4 }}>{t('login.insecureCookie.title')}</div>
|
||||
<div style={{ lineHeight: 1.55 }}>{t('login.insecureCookie.body')}</div>
|
||||
<a href="https://github.com/mauriceboe/TREK/wiki/Troubleshooting" target="_blank" rel="noopener noreferrer"
|
||||
style={{ display: 'inline-block', marginTop: 6, fontWeight: 600, color: '#b45309', textDecoration: 'underline' }}>
|
||||
{t('login.insecureCookie.link')} ↗
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{passwordChangeStep && (
|
||||
<>
|
||||
<div style={{ padding: '10px 14px', background: '#fefce8', border: '1px solid #fde68a', borderRadius: 10, fontSize: 'calc(13px * var(--fs-scale-body, 1))', color: '#92400e' }}>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { helpApi, type HelpNavSection, type HelpPageData } from '../../api/client'
|
||||
|
||||
/** State + data loading for the in-app help wiki (see PATTERN.md). */
|
||||
export function useHelp() {
|
||||
const { slug } = useParams<{ slug: string }>()
|
||||
const [sections, setSections] = useState<HelpNavSection[]>([])
|
||||
const [page, setPage] = useState<HelpPageData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [pageError, setPageError] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [navOpen, setNavOpen] = useState(false)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
helpApi.index().then((d) => setSections(d.sections)).catch(() => setSections([]))
|
||||
}, [])
|
||||
|
||||
const homeSlug = sections[0]?.pages[0]?.slug ?? 'Home'
|
||||
const activeSlug = slug ?? homeSlug
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
setLoading(true)
|
||||
setPageError(false)
|
||||
helpApi
|
||||
.page(activeSlug)
|
||||
.then((p) => {
|
||||
if (!alive) return
|
||||
setPage(p)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!alive) return
|
||||
setPageError(true)
|
||||
setLoading(false)
|
||||
})
|
||||
contentRef.current?.scrollTo?.({ top: 0 })
|
||||
window.scrollTo?.({ top: 0 })
|
||||
setNavOpen(false)
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [activeSlug])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return sections
|
||||
return sections
|
||||
.map((s) => ({ ...s, pages: s.pages.filter((p) => p.title.toLowerCase().includes(q)) }))
|
||||
.filter((s) => s.pages.length > 0)
|
||||
}, [sections, query])
|
||||
|
||||
return { page, loading, pageError, query, setQuery, navOpen, setNavOpen, contentRef, activeSlug, filtered }
|
||||
}
|
||||
@@ -41,6 +41,9 @@ export function useLogin() {
|
||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string>('')
|
||||
// Set when the server signals it just issued a Secure cookie over plain HTTP —
|
||||
// the browser drops it, so we explain the fix instead of a bare 401 later.
|
||||
const [insecureCookie, setInsecureCookie] = useState(false)
|
||||
const [appConfig, setAppConfig] = useState<AppConfig | null>(null)
|
||||
const [inviteToken, setInviteToken] = useState<string>('')
|
||||
const [inviteValid, setInviteValid] = useState<boolean>(false)
|
||||
@@ -225,6 +228,7 @@ export function useLogin() {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>): Promise<void> => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setInsecureCookie(false)
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (passwordChangeStep) {
|
||||
@@ -260,6 +264,13 @@ export function useLogin() {
|
||||
await register(username, email, password, inviteToken || undefined)
|
||||
} else {
|
||||
const result = await login(email, password, rememberMe)
|
||||
if ((result as { insecureCookie?: boolean }).insecureCookie) {
|
||||
// Credentials were correct, but the secure cookie won't survive plain
|
||||
// HTTP — proceeding would just dead-end on "Access token required".
|
||||
setInsecureCookie(true)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if ('mfa_required' in result && result.mfa_required && 'mfa_token' in result) {
|
||||
setMfaToken(result.mfa_token)
|
||||
setMfaStep(true)
|
||||
@@ -291,7 +302,7 @@ export function useLogin() {
|
||||
navigate,
|
||||
mode, setMode,
|
||||
username, setUsername, email, setEmail, password, setPassword, rememberMe, setRememberMe, showPassword, setShowPassword,
|
||||
isLoading, error, setError, appConfig, inviteToken,
|
||||
isLoading, error, setError, insecureCookie, appConfig, inviteToken,
|
||||
langDropdownOpen, setLangDropdownOpen, setLanguageLocal,
|
||||
showTakeoff, mfaStep, setMfaStep, mfaToken, setMfaToken, mfaCode, setMfaCode,
|
||||
passwordChangeStep, newPassword, setNewPassword, confirmPassword, setConfirmPassword,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DatabaseModule } from './database/database.module';
|
||||
import { HealthController } from './health/health.controller';
|
||||
import { HealthService } from './health/health.service';
|
||||
import { WeatherModule } from './weather/weather.module';
|
||||
import { HelpModule } from './help/help.module';
|
||||
import { AirportsModule } from './airports/airports.module';
|
||||
import { ConfigModule } from './config/config.module';
|
||||
import { SystemNoticesModule } from './system-notices/system-notices.module';
|
||||
@@ -46,7 +47,7 @@ import { IdempotencyInterceptor } from './common/idempotency.interceptor';
|
||||
* migrated.
|
||||
*/
|
||||
@Module({
|
||||
imports: [DatabaseModule, WeatherModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, ShareModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, BookingImportModule],
|
||||
imports: [DatabaseModule, WeatherModule, HelpModule, AirportsModule, ConfigModule, SystemNoticesModule, MapsModule, CategoriesModule, TagsModule, NotificationsModule, AtlasModule, VacayModule, PackingModule, TodoModule, BudgetModule, ReservationsModule, DaysModule, AssignmentsModule, PlacesModule, TripsModule, CollabModule, FilesModule, PhotosModule, MemoriesModule, AirtrailModule, JourneyModule, ShareModule, SettingsModule, BackupModule, AuthModule, OidcModule, OauthModule, AdminModule, AddonsModule, BookingImportModule],
|
||||
controllers: [HealthController],
|
||||
providers: [
|
||||
HealthService,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AuthService } from './auth.service';
|
||||
import { RateLimitService } from './rate-limit.service';
|
||||
import { OptionalJwtGuard } from './optional-jwt.guard';
|
||||
import { writeAudit, getClientIp } from '../../services/auditLog';
|
||||
import { willDropSecureCookie } from '../../services/cookie';
|
||||
import type { User } from '../../types';
|
||||
|
||||
const WINDOW = 15 * 60 * 1000;
|
||||
@@ -88,7 +89,13 @@ export class AuthPublicController {
|
||||
return { mfa_required: true, mfa_token: result.mfa_token };
|
||||
}
|
||||
this.auth.setAuthCookie(res, result.token!, req, result.remember);
|
||||
return { token: result.token, user: result.user };
|
||||
return {
|
||||
token: result.token,
|
||||
user: result.user,
|
||||
// Surfaced so the client can explain the plain-HTTP cookie gotcha instead
|
||||
// of the user hitting a bare "Access token required" on the next request.
|
||||
...(willDropSecureCookie(req) ? { insecureCookie: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@Post('forgot-password')
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Controller, Get, Param, Req, Res } from '@nestjs/common';
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
getWikiIndex,
|
||||
getWikiPage,
|
||||
getWikiAsset,
|
||||
WikiNotFound,
|
||||
type WikiPage,
|
||||
type WikiNavSection,
|
||||
} from '../../services/wikiService';
|
||||
|
||||
/**
|
||||
* /api/help — embedded TREK wiki. Content is public docs (the wiki is public on
|
||||
* GitHub), so these endpoints are unauthenticated; that also lets <img> tags load
|
||||
* the proxied assets without sending credentials. All upstream calls go to the
|
||||
* fixed TREK wiki path and are cached server-side.
|
||||
*/
|
||||
@Controller('api/help')
|
||||
export class HelpController {
|
||||
@Get('index')
|
||||
index(): Promise<{ sections: WikiNavSection[] }> {
|
||||
return getWikiIndex();
|
||||
}
|
||||
|
||||
@Get('page/:slug')
|
||||
async page(@Param('slug') slug: string, @Res() res: Response): Promise<void> {
|
||||
try {
|
||||
const page: WikiPage = await getWikiPage(slug);
|
||||
res.json(page);
|
||||
} catch (err) {
|
||||
res.status(err instanceof WikiNotFound ? 404 : 502).json({ error: 'Help page unavailable' });
|
||||
}
|
||||
}
|
||||
|
||||
@Get('asset/*')
|
||||
async asset(@Req() req: Request, @Res() res: Response): Promise<void> {
|
||||
// Take everything after `/asset/` straight from the URL — the Express
|
||||
// wildcard param isn't reliably populated through the Nest adapter.
|
||||
const after = (req.originalUrl || req.url).split('/asset/')[1] ?? '';
|
||||
const assetPath = decodeURIComponent(after.split('?')[0]);
|
||||
try {
|
||||
const { buf, type } = await getWikiAsset(assetPath);
|
||||
res.setHeader('Content-Type', type);
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.end(buf);
|
||||
} catch {
|
||||
res.status(404).end();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HelpController } from './help.controller';
|
||||
|
||||
/** /api/help — embedded GitHub wiki (fetched + cached in wikiService). */
|
||||
@Module({
|
||||
controllers: [HelpController],
|
||||
})
|
||||
export class HelpModule {}
|
||||
@@ -54,6 +54,22 @@ function buildOptions(clear: boolean, secure: boolean, remember?: RememberOption
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* True when we are about to set a `Secure` session cookie but the request did
|
||||
* NOT arrive over HTTPS — the browser silently drops the cookie, so the next
|
||||
* request has no session and the server answers "Access token required". This is
|
||||
* the classic plain-HTTP install gotcha; callers surface it to the user with a
|
||||
* concrete fix (use HTTPS or set COOKIE_SECURE=false) instead of a bare 401.
|
||||
*/
|
||||
export function willDropSecureCookie(req?: Request): boolean {
|
||||
if (process.env.COOKIE_SECURE?.toLowerCase() === 'false') return false;
|
||||
if (req?.secure === true) return false;
|
||||
return (
|
||||
process.env.NODE_ENV?.toLowerCase() === 'production' ||
|
||||
process.env.FORCE_HTTPS?.toLowerCase() === 'true'
|
||||
);
|
||||
}
|
||||
|
||||
export function setAuthCookie(res: Response, token: string, req?: Request, remember?: RememberOption): void {
|
||||
res.cookie(COOKIE_NAME, token, cookieOptions(false, req, remember));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
/**
|
||||
* In-app Help/Wiki content, sourced from the TREK GitHub wiki (kept in the repo
|
||||
* under `wiki/**` and mirrored to the GitHub wiki on push to main). The server
|
||||
* fetches the markdown from GitHub and caches it, so the embedded help stays in
|
||||
* sync with wiki edits without a redeploy — and the client never talks to GitHub
|
||||
* directly (images are proxied too). A bundled snapshot under server/assets/wiki
|
||||
* is the cold-start / offline fallback.
|
||||
*/
|
||||
|
||||
const REPO = 'mauriceboe/TREK';
|
||||
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}/main/wiki`;
|
||||
const TTL_MS = 60 * 60 * 1000; // refresh from GitHub at most hourly
|
||||
const SNAPSHOT_DIR = path.join(__dirname, '..', '..', 'assets', 'wiki');
|
||||
const SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
||||
|
||||
export class WikiNotFound extends Error {
|
||||
status = 404;
|
||||
}
|
||||
|
||||
interface TextEntry {
|
||||
data: string;
|
||||
ts: number;
|
||||
}
|
||||
const textCache = new Map<string, TextEntry>();
|
||||
const assetCache = new Map<string, { buf: Buffer; type: string; ts: number }>();
|
||||
|
||||
const fresh = (ts: number): boolean => Date.now() - ts < TTL_MS;
|
||||
|
||||
async function readSnapshot(file: string): Promise<string | null> {
|
||||
try {
|
||||
return await fs.readFile(path.join(SNAPSHOT_DIR, file), 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fetch a wiki text file with cache → stale-cache → bundled-snapshot fallback. */
|
||||
async function fetchText(file: string): Promise<string> {
|
||||
const cached = textCache.get(file);
|
||||
if (cached && fresh(cached.ts)) return cached.data;
|
||||
try {
|
||||
const res = await fetch(`${RAW_BASE}/${encodeURIComponent(file)}`, {
|
||||
headers: { 'User-Agent': 'TREK-help', Accept: 'text/plain' },
|
||||
});
|
||||
if (res.ok) {
|
||||
const text = await res.text();
|
||||
textCache.set(file, { data: text, ts: Date.now() });
|
||||
return text;
|
||||
}
|
||||
if (res.status === 404) throw new WikiNotFound(file);
|
||||
} catch (err) {
|
||||
if (err instanceof WikiNotFound) throw err;
|
||||
// network/parse error — fall through to stale cache or snapshot
|
||||
}
|
||||
if (cached) return cached.data; // serve stale rather than fail
|
||||
const snap = await readSnapshot(file);
|
||||
if (snap != null) {
|
||||
textCache.set(file, { data: snap, ts: Date.now() });
|
||||
return snap;
|
||||
}
|
||||
throw new WikiNotFound(file);
|
||||
}
|
||||
|
||||
export interface WikiNavItem {
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
export interface WikiNavSection {
|
||||
title: string;
|
||||
pages: WikiNavItem[];
|
||||
}
|
||||
|
||||
/** Parse the wiki `_Sidebar.md` into ordered sections of `[[Title|Slug]]` links. */
|
||||
function parseSidebar(md: string): WikiNavSection[] {
|
||||
const sections: WikiNavSection[] = [];
|
||||
let current: WikiNavSection | null = null;
|
||||
for (const raw of md.split('\n')) {
|
||||
const heading = raw.match(/^#{1,4}\s+(.+?)\s*$/);
|
||||
if (heading) {
|
||||
current = { title: heading[1].replace(/[*_`]/g, '').trim(), pages: [] };
|
||||
sections.push(current);
|
||||
continue;
|
||||
}
|
||||
const link = raw.match(/^\s*[-*]\s*\[\[([^\]]+)\]\]/);
|
||||
if (link) {
|
||||
if (!current) {
|
||||
current = { title: '', pages: [] };
|
||||
sections.push(current);
|
||||
}
|
||||
const inner = link[1];
|
||||
const [title, slugRaw] = inner.includes('|') ? inner.split('|') : [inner, inner];
|
||||
const slug = slugRaw.trim().replace(/\s+/g, '-');
|
||||
if (SLUG_RE.test(slug)) current.pages.push({ title: title.trim(), slug });
|
||||
}
|
||||
}
|
||||
return sections.filter((s) => s.pages.length > 0);
|
||||
}
|
||||
|
||||
/** Rewrite GitHub-wiki `[[..]]` links to /help routes and proxy relative images. */
|
||||
function processMarkdown(md: string): string {
|
||||
// Strip HTML comments (e.g. `<!-- TODO: screenshot … -->` placeholders) — the
|
||||
// markdown renderer would otherwise surface them as raw text.
|
||||
let out = md.replace(/<!--[\s\S]*?-->/g, '');
|
||||
out = out.replace(/\[\[([^\]]+)\]\]/g, (_m, inner: string) => {
|
||||
const [title, slugRaw] = inner.includes('|') ? inner.split('|') : [inner, inner];
|
||||
const slug = slugRaw.trim().replace(/\s+/g, '-');
|
||||
return `[${title.trim()}](/help/${slug})`;
|
||||
});
|
||||
out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)([^)]*)\)/g, (m, alt: string, url: string) => {
|
||||
if (/^https?:\/\//i.test(url) || url.startsWith('/api/help/asset/')) return m;
|
||||
const clean = url.replace(/^\.?\//, '').replace(/^wiki\//, '');
|
||||
return ``;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractTitle(md: string, fallback: string): string {
|
||||
const h1 = md.match(/^#\s+(.+?)\s*$/m);
|
||||
return h1 ? h1[1].replace(/[*_`]/g, '').trim() : fallback.replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
export interface WikiPage {
|
||||
slug: string;
|
||||
title: string;
|
||||
markdown: string;
|
||||
}
|
||||
|
||||
export async function getWikiIndex(): Promise<{ sections: WikiNavSection[] }> {
|
||||
const md = await fetchText('_Sidebar.md');
|
||||
return { sections: parseSidebar(md) };
|
||||
}
|
||||
|
||||
export async function getWikiPage(slug: string): Promise<WikiPage> {
|
||||
if (!SLUG_RE.test(slug)) throw new WikiNotFound(slug);
|
||||
const md = await fetchText(`${slug}.md`);
|
||||
return { slug, title: extractTitle(md, slug), markdown: processMarkdown(md) };
|
||||
}
|
||||
|
||||
const ASSET_TYPES: Record<string, string> = {
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp',
|
||||
'.svg': 'image/svg+xml',
|
||||
};
|
||||
|
||||
/** Proxy a wiki image so the browser never calls GitHub directly. */
|
||||
export async function getWikiAsset(assetPath: string): Promise<{ buf: Buffer; type: string }> {
|
||||
// Defend against traversal; allow nested image folders.
|
||||
if (assetPath.includes('..') || !/^[A-Za-z0-9/._-]+$/.test(assetPath)) throw new WikiNotFound(assetPath);
|
||||
const ext = path.extname(assetPath).toLowerCase();
|
||||
const type = ASSET_TYPES[ext];
|
||||
if (!type) throw new WikiNotFound(assetPath);
|
||||
|
||||
const cached = assetCache.get(assetPath);
|
||||
if (cached && fresh(cached.ts)) return { buf: cached.buf, type: cached.type };
|
||||
try {
|
||||
const res = await fetch(`${RAW_BASE}/${assetPath.split('/').map(encodeURIComponent).join('/')}`, {
|
||||
headers: { 'User-Agent': 'TREK-help' },
|
||||
});
|
||||
if (res.ok) {
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
assetCache.set(assetPath, { buf, type, ts: Date.now() });
|
||||
return { buf, type };
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
if (cached) return { buf: cached.buf, type: cached.type };
|
||||
try {
|
||||
const buf = await fs.readFile(path.join(SNAPSHOT_DIR, assetPath));
|
||||
return { buf, type };
|
||||
} catch {
|
||||
throw new WikiNotFound(assetPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'ar' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.emailPlaceholder': 'your@email.com', // en-fallback
|
||||
'login.passkey.signIn': 'تسجيل الدخول باستخدام مفتاح المرور',
|
||||
'login.passkey.failed': 'فشل تسجيل الدخول بمفتاح المرور. يرجى المحاولة مرة أخرى.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'إعدادات المسؤول',
|
||||
'nav.bottomLogout': 'تسجيل الخروج',
|
||||
'nav.bottomAdminBadge': 'مسؤول',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'br' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -84,5 +84,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Falha na redefinição. O link pode ter expirado.',
|
||||
'login.passkey.signIn': 'Entrar com uma passkey',
|
||||
'login.passkey.failed': 'Falha ao entrar com passkey. Tente novamente.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Administração',
|
||||
'nav.bottomLogout': 'Sair',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'cs' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -84,5 +84,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Obnovení se nezdařilo. Odkaz mohl vypršet.',
|
||||
'login.passkey.signIn': 'Přihlásit se pomocí přístupového klíče',
|
||||
'login.passkey.failed': 'Přihlášení přístupovým klíčem se nezdařilo. Zkuste to prosím znovu.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Nastavení správce',
|
||||
'nav.bottomLogout': 'Odhlásit se',
|
||||
'nav.bottomAdminBadge': 'Správce',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const help: TranslationStrings = {
|
||||
'help.title': 'Hilfe & Doku',
|
||||
'help.search': 'Doku durchsuchen…',
|
||||
'help.contents': 'Inhalt',
|
||||
'help.noResults': 'Keine passenden Seiten.',
|
||||
'help.errorTitle': 'Seite konnte nicht geladen werden',
|
||||
'help.errorBody':
|
||||
'Die Hilfe-Inhalte kommen aus dem TREK-Wiki. Prüfe deine Verbindung und versuch es erneut.',
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -86,5 +86,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Zurücksetzen fehlgeschlagen. Der Link ist möglicherweise abgelaufen.',
|
||||
'login.passkey.signIn': 'Mit Passkey anmelden',
|
||||
'login.passkey.failed': 'Anmeldung mit Passkey fehlgeschlagen. Bitte erneut versuchen.',
|
||||
'login.insecureCookie.title': "Login hält über HTTP nicht",
|
||||
'login.insecureCookie.body': "Du verbindest dich über reines HTTP, daher verwirft dein Browser TREKs sicheren Session-Cookie — die nächste Anfrage scheitert mit „Access token required\". Lösung: HTTPS nutzen, oder für ein Heim-Setup COOKIE_SECURE=false setzen.",
|
||||
'login.insecureCookie.link': "Zur Troubleshooting-Anleitung",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Admin-Einstellungen',
|
||||
'nav.bottomLogout': 'Abmelden',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Hilfe',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
const help: TranslationStrings = {
|
||||
'help.title': 'Help & Docs',
|
||||
'help.search': 'Search docs…',
|
||||
'help.contents': 'Contents',
|
||||
'help.noResults': 'No matching pages.',
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody':
|
||||
'The help content is fetched from the TREK wiki. Check your connection and try again.',
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Reset failed. The link may have expired.',
|
||||
'login.passkey.signIn': 'Sign in with a passkey',
|
||||
'login.passkey.failed': 'Passkey sign-in failed. Please try again.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Admin Settings',
|
||||
'nav.bottomLogout': 'Logout',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'es' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -86,5 +86,8 @@ const login: TranslationStrings = {
|
||||
'login.oidcLoggedOut': 'Has cerrado sesión. Vuelve a iniciar sesión con tu proveedor SSO.',
|
||||
'login.passkey.signIn': 'Iniciar sesión con una passkey',
|
||||
'login.passkey.failed': 'Error al iniciar sesión con la passkey. Inténtalo de nuevo.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Administración',
|
||||
'nav.bottomLogout': 'Cerrar sesión',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'fr' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -88,5 +88,8 @@ const login: TranslationStrings = {
|
||||
'login.demoHint': 'Essayez la démo — aucune inscription nécessaire',
|
||||
'login.passkey.signIn': 'Se connecter avec une passkey',
|
||||
'login.passkey.failed': 'Échec de la connexion par passkey. Veuillez réessayer.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Administration',
|
||||
'nav.bottomLogout': 'Déconnexion',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'gr' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -88,5 +88,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Η επαναφορά απέτυχε. Ο σύνδεσμος μπορεί να έχει λήξει.',
|
||||
'login.passkey.signIn': 'Σύνδεση με passkey',
|
||||
'login.passkey.failed': 'Η σύνδεση με passkey απέτυχε. Παρακαλώ δοκιμάστε ξανά.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Ρυθμίσεις Διαχειριστή',
|
||||
'nav.bottomLogout': 'Αποσύνδεση',
|
||||
'nav.bottomAdminBadge': 'Διαχειριστής',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'hu' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -86,5 +86,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'A visszaállítás nem sikerült. A link lehet, hogy lejárt.',
|
||||
'login.passkey.signIn': 'Bejelentkezés passkey-jel',
|
||||
'login.passkey.failed': 'A passkey-bejelentkezés sikertelen. Kérjük, próbáld újra.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Adminisztráció',
|
||||
'nav.bottomLogout': 'Kijelentkezés',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'id' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -83,5 +83,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Reset gagal. Tautan mungkin sudah kedaluwarsa.',
|
||||
'login.passkey.signIn': 'Masuk dengan passkey',
|
||||
'login.passkey.failed': 'Masuk dengan passkey gagal. Silakan coba lagi.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Pengaturan Admin',
|
||||
'nav.bottomLogout': 'Keluar',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'it' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Reset non riuscito. Il link potrebbe essere scaduto.',
|
||||
'login.passkey.signIn': 'Accedi con una passkey',
|
||||
'login.passkey.failed': 'Accesso con passkey non riuscito. Riprova.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Amministrazione',
|
||||
'nav.bottomLogout': 'Disconnetti',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'ja' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'リセットに失敗しました。リンクの有効期限が切れている可能性があります。',
|
||||
'login.passkey.signIn': 'パスキーでサインイン',
|
||||
'login.passkey.failed': 'パスキーでのサインインに失敗しました。もう一度お試しください。',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': '管理者設定',
|
||||
'nav.bottomLogout': 'ログアウト',
|
||||
'nav.bottomAdminBadge': '管理者',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'ko' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -83,5 +83,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': '재설정 실패. 링크가 만료되었을 수 있습니다.',
|
||||
'login.passkey.signIn': '패스키로 로그인',
|
||||
'login.passkey.failed': '패스키 로그인에 실패했습니다. 다시 시도하세요.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': '관리자 설정',
|
||||
'nav.bottomLogout': '로그아웃',
|
||||
'nav.bottomAdminBadge': '관리자',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'nl' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.demoHint': 'Probeer de demo — geen registratie nodig',
|
||||
'login.passkey.signIn': 'Inloggen met een passkey',
|
||||
'login.passkey.failed': 'Inloggen met passkey mislukt. Probeer het opnieuw.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Beheerdersinstellingen',
|
||||
'nav.bottomLogout': 'Uitloggen',
|
||||
'nav.bottomAdminBadge': 'Beheerder',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'pl' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.setNewPasswordHint': 'Musisz zmienić hasło.',
|
||||
'login.passkey.signIn': 'Zaloguj się kluczem dostępu',
|
||||
'login.passkey.failed': 'Logowanie kluczem dostępu nie powiodło się. Spróbuj ponownie.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Ustawienia administratora',
|
||||
'nav.bottomLogout': 'Wyloguj się',
|
||||
'nav.bottomAdminBadge': 'Administrator',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'ru' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -86,5 +86,8 @@ const login: TranslationStrings = {
|
||||
'login.demoHint': 'Попробуйте демо — регистрация не требуется',
|
||||
'login.passkey.signIn': 'Войти с помощью passkey',
|
||||
'login.passkey.failed': 'Не удалось войти с помощью passkey. Попробуйте ещё раз.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Администрирование',
|
||||
'nav.bottomLogout': 'Выйти',
|
||||
'nav.bottomAdminBadge': 'Админ',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'sv' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Återställningen misslyckades. Länken kan ha gått ut.',
|
||||
'login.passkey.signIn': 'Logga in med en inloggningsnyckel',
|
||||
'login.passkey.failed': 'Inloggningsnyckel misslyckades. Försök igen.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Admin Inställningar',
|
||||
'nav.bottomLogout': 'Logga ut',
|
||||
'nav.bottomAdminBadge': 'Admin',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'tr' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -86,5 +86,8 @@ const login: TranslationStrings = {
|
||||
'login.resetPasswordFailed': 'Sıfırlama başarısız oldu. Bağlantının süresi dolmuş olabilir.',
|
||||
'login.passkey.signIn': 'Passkey ile oturum açın',
|
||||
'login.passkey.failed': 'Passkey ile oturum açma başarısız oldu. Lütfen tekrar deneyin.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Yönetici Ayarları',
|
||||
'nav.bottomLogout': 'Çıkış',
|
||||
'nav.bottomAdminBadge': 'Yönetici',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'uk' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -85,5 +85,8 @@ const login: TranslationStrings = {
|
||||
'login.demoHint': 'Спробуйте демо — реєстрація не потрібна',
|
||||
'login.passkey.signIn': 'Увійти за допомогою passkey',
|
||||
'login.passkey.failed': 'Не вдалося увійти за допомогою passkey. Спробуйте ще раз.',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': 'Адміністрування',
|
||||
'nav.bottomLogout': 'Вийти',
|
||||
'nav.bottomAdminBadge': 'Адмін',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'zh-TW' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -82,5 +82,8 @@ const login: TranslationStrings = {
|
||||
'login.demoHint': '試用演示——無需註冊',
|
||||
'login.passkey.signIn': '使用 Passkey 登入',
|
||||
'login.passkey.failed': 'Passkey 登入失敗,請重試。',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': '管理設定',
|
||||
'nav.bottomLogout': '退出登入',
|
||||
'nav.bottomAdminBadge': '管理員',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { TranslationStrings } from '../types';
|
||||
|
||||
// English fallback until 'zh' is translated.
|
||||
const help: TranslationStrings = {
|
||||
'help.title': "Help & Docs",
|
||||
'help.search': "Search docs…",
|
||||
'help.contents': "Contents",
|
||||
'help.noResults': "No matching pages.",
|
||||
'help.errorTitle': "Couldn't load this page",
|
||||
'help.errorBody': "The help content is fetched from the TREK wiki. Check your connection and try again.",
|
||||
};
|
||||
|
||||
export default help;
|
||||
@@ -39,6 +39,7 @@ import trip from './trip';
|
||||
import trips from './trips';
|
||||
import undo from './undo';
|
||||
import vacay from './vacay';
|
||||
import help from './help';
|
||||
|
||||
const locale = {
|
||||
...common,
|
||||
@@ -82,5 +83,6 @@ const locale = {
|
||||
...oauth,
|
||||
...system_notice,
|
||||
...transport,
|
||||
...help,
|
||||
};
|
||||
export default locale;
|
||||
|
||||
@@ -82,5 +82,8 @@ const login: TranslationStrings = {
|
||||
'login.demoHint': '试用演示——无需注册',
|
||||
'login.passkey.signIn': '使用通行密钥登录',
|
||||
'login.passkey.failed': '通行密钥登录失败,请重试。',
|
||||
'login.insecureCookie.title': "Login won't stick over HTTP",
|
||||
'login.insecureCookie.body': "You’re connecting over plain HTTP, so your browser drops TREK’s secure session cookie — the next request fails with \"Access token required\". Fix: use HTTPS, or for a home-lab set COOKIE_SECURE=false.",
|
||||
'login.insecureCookie.link': "Open the Troubleshooting guide",
|
||||
};
|
||||
export default login;
|
||||
|
||||
@@ -16,5 +16,6 @@ const nav: TranslationStrings = {
|
||||
'nav.bottomAdmin': '管理设置',
|
||||
'nav.bottomLogout': '退出登录',
|
||||
'nav.bottomAdminBadge': '管理员',
|
||||
'nav.help': 'Help',
|
||||
};
|
||||
export default nav;
|
||||
|
||||
Reference in New Issue
Block a user