Add Scandinavian i18n (da/sv/nb) via DeepL pipeline.
Integrate new locale bundles, language cycling in the UI, SEO hreflang tags, and localized beta flyer HTML variants with scripts for batch translation and key validation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
OpenWeatherMapAPIKey=<owm_api_key>
|
OpenWeatherMapAPIKey=<owm_api_key>
|
||||||
|
|
||||||
|
# DeepL API (for scripts/translate-locales.mjs and scripts/translate-flyer.mjs)
|
||||||
|
# Free plan keys use api-free.deepl.com automatically (suffix :fx)
|
||||||
|
DeepLAPIKey=
|
||||||
|
|
||||||
# Passkey configuration (WebAuthn Relying Party ID and Origin)
|
# Passkey configuration (WebAuthn Relying Party ID and Origin)
|
||||||
# For local dev: localhost and http://localhost
|
# For local dev: localhost and http://localhost
|
||||||
# For production: e.g. kapteins-daagbok.eu and https://kapteins-daagbok.eu
|
# For production: e.g. kapteins-daagbok.eu and https://kapteins-daagbok.eu
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
<link rel="canonical" href="https://kapteins-daagbok.eu/" />
|
<link rel="canonical" href="https://kapteins-daagbok.eu/" />
|
||||||
<link rel="alternate" hreflang="de" href="https://kapteins-daagbok.eu/?lng=de" />
|
<link rel="alternate" hreflang="de" href="https://kapteins-daagbok.eu/?lng=de" />
|
||||||
<link rel="alternate" hreflang="en" href="https://kapteins-daagbok.eu/?lng=en" />
|
<link rel="alternate" hreflang="en" href="https://kapteins-daagbok.eu/?lng=en" />
|
||||||
|
<link rel="alternate" hreflang="da" href="https://kapteins-daagbok.eu/?lng=da" />
|
||||||
|
<link rel="alternate" hreflang="sv" href="https://kapteins-daagbok.eu/?lng=sv" />
|
||||||
|
<link rel="alternate" hreflang="nb" href="https://kapteins-daagbok.eu/?lng=nb" />
|
||||||
<link rel="alternate" hreflang="x-default" href="https://kapteins-daagbok.eu/" />
|
<link rel="alternate" hreflang="x-default" href="https://kapteins-daagbok.eu/" />
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
|||||||
+4
-1
@@ -11,7 +11,10 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"generate:flyer": "node ../scripts/generate-beta-flyer.mjs",
|
"generate:flyer": "node ../scripts/generate-beta-flyer.mjs",
|
||||||
"generate:flyer:png": "node ../scripts/generate-beta-flyer.mjs --png",
|
"generate:flyer:png": "node ../scripts/generate-beta-flyer.mjs --png",
|
||||||
"generate:flyer:setup": "playwright install chromium"
|
"generate:flyer:setup": "playwright install chromium",
|
||||||
|
"translate:locales": "node ../scripts/translate-locales.mjs",
|
||||||
|
"translate:flyer": "node ../scripts/translate-flyer.mjs",
|
||||||
|
"validate:i18n": "node ../scripts/validate-i18n-keys.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.3.0",
|
"@simplewebauthn/browser": "^13.3.0",
|
||||||
|
|||||||
+2
-2
@@ -45,6 +45,7 @@ import { Ship, LogOut, ChevronLeft, Users, FileText, Settings, Wifi, WifiOff, La
|
|||||||
import DisclaimerHeaderButton from './components/DisclaimerHeaderButton.tsx'
|
import DisclaimerHeaderButton from './components/DisclaimerHeaderButton.tsx'
|
||||||
import FeedbackHeaderButton from './components/FeedbackHeaderButton.tsx'
|
import FeedbackHeaderButton from './components/FeedbackHeaderButton.tsx'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getNextLanguage } from './utils/i18nLanguages.js'
|
||||||
import {
|
import {
|
||||||
resolveTourLogbookContext,
|
resolveTourLogbookContext,
|
||||||
seedDemoLogbookIfNeeded
|
seedDemoLogbookIfNeeded
|
||||||
@@ -496,8 +497,7 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const nextLang = i18n.language.startsWith('de') ? 'en' : 'de'
|
i18n.changeLanguage(getNextLanguage(i18n.language))
|
||||||
i18n.changeLanguage(nextLang)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExitDemo = () => {
|
const handleExitDemo = () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getNextLanguage } from '../utils/i18nLanguages.js'
|
||||||
import {
|
import {
|
||||||
registerUser,
|
registerUser,
|
||||||
loginUser,
|
loginUser,
|
||||||
@@ -209,8 +210,7 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const nextLang = i18n.language.startsWith('de') ? 'en' : 'de'
|
i18n.changeLanguage(getNextLanguage(i18n.language))
|
||||||
i18n.changeLanguage(nextLang)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
@@ -596,7 +596,7 @@ export default function AuthOnboarding({ onAuthenticated, onOpenDemo }: AuthOnbo
|
|||||||
<div className="auth-footer">
|
<div className="auth-footer">
|
||||||
<button type="button" className="btn-icon-text" onClick={toggleLanguage}>
|
<button type="button" className="btn-icon-text" onClick={toggleLanguage}>
|
||||||
<Languages size={18} />
|
<Languages size={18} />
|
||||||
{i18n.language.startsWith('de') ? 'English' : 'Deutsch'}
|
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getNextLanguage } from '../utils/i18nLanguages.js'
|
||||||
import VesselForm from './VesselForm.tsx'
|
import VesselForm from './VesselForm.tsx'
|
||||||
import CrewForm from './CrewForm.tsx'
|
import CrewForm from './CrewForm.tsx'
|
||||||
import LogEntriesList from './LogEntriesList.tsx'
|
import LogEntriesList from './LogEntriesList.tsx'
|
||||||
@@ -48,8 +49,7 @@ export default function DemoViewer({ onExit }: DemoViewerProps) {
|
|||||||
}, [registerNavigation, registerDemoTourContext, startTour, fixture.firstEntryId])
|
}, [registerNavigation, registerDemoTourContext, startTour, fixture.firstEntryId])
|
||||||
|
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const nextLang = i18n.language.startsWith('de') ? 'en' : 'de'
|
i18n.changeLanguage(getNextLanguage(i18n.language))
|
||||||
i18n.changeLanguage(nextLang)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, yacht, crews, entries, gpsTracks, photos, firstEntryId } = fixture
|
const { title, yacht, crews, entries, gpsTracks, photos, firstEntryId } = fixture
|
||||||
@@ -87,7 +87,7 @@ export default function DemoViewer({ onExit }: DemoViewerProps) {
|
|||||||
</button>
|
</button>
|
||||||
<button className="btn secondary" onClick={toggleLanguage} style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }}>
|
<button className="btn secondary" onClick={toggleLanguage} style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }}>
|
||||||
<Globe size={14} style={{ marginRight: '4px' }} />
|
<Globe size={14} style={{ marginRight: '4px' }} />
|
||||||
{i18n.language.startsWith('de') ? 'English' : 'Deutsch'}
|
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getNextLanguage } from '../utils/i18nLanguages.js'
|
||||||
import { Ship, LogIn, UserPlus, AlertTriangle, ShieldCheck, Languages, ArrowRight, KeyRound } from 'lucide-react'
|
import { Ship, LogIn, UserPlus, AlertTriangle, ShieldCheck, Languages, ArrowRight, KeyRound } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
getActiveMasterKey,
|
getActiveMasterKey,
|
||||||
@@ -308,7 +309,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
i18n.changeLanguage(i18n.language.startsWith('de') ? 'en' : 'de')
|
i18n.changeLanguage(getNextLanguage(i18n.language))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recoveryPhrase) {
|
if (recoveryPhrase) {
|
||||||
@@ -511,7 +512,7 @@ export default function InvitationAcceptance({ onAccepted, onCancel }: Invitatio
|
|||||||
<div className="auth-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '16px', marginTop: '24px' }}>
|
<div className="auth-footer" style={{ borderTop: '1px solid rgba(255,255,255,0.05)', paddingTop: '16px', marginTop: '24px' }}>
|
||||||
<button className="btn-icon-text" onClick={toggleLanguage}>
|
<button className="btn-icon-text" onClick={toggleLanguage}>
|
||||||
<Languages size={18} />
|
<Languages size={18} />
|
||||||
{i18n.language.startsWith('de') ? t('invitation.switch_language_en') : t('invitation.switch_language_de')}
|
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
import React, { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getNextLanguage } from '../utils/i18nLanguages.js'
|
||||||
import { useSyncIndicator } from '../hooks/useSyncIndicator.js'
|
import { useSyncIndicator } from '../hooks/useSyncIndicator.js'
|
||||||
import { fetchLogbooks, createLogbook, deleteLogbook, updateLogbookTitle, type DecryptedLogbook } from '../services/logbook.js'
|
import { fetchLogbooks, createLogbook, deleteLogbook, updateLogbookTitle, type DecryptedLogbook } from '../services/logbook.js'
|
||||||
import LogbookRoleBadge from './LogbookRoleBadge.tsx'
|
import LogbookRoleBadge from './LogbookRoleBadge.tsx'
|
||||||
@@ -193,8 +194,7 @@ export default function LogbookDashboard({ onSelectLogbook, onLogout, onOpenProf
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const nextLang = i18n.language.startsWith('de') ? 'en' : 'de'
|
i18n.changeLanguage(getNextLanguage(i18n.language))
|
||||||
i18n.changeLanguage(nextLang)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownedLogbooks = logbooks.filter((lb) => !lb.isShared)
|
const ownedLogbooks = logbooks.filter((lb) => !lb.isShared)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { getNextLanguage, isGermanLocale } from '../utils/i18nLanguages.js'
|
||||||
import { decryptJson } from '../services/crypto.js'
|
import { decryptJson } from '../services/crypto.js'
|
||||||
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js'
|
||||||
import VesselForm from './VesselForm.tsx'
|
import VesselForm from './VesselForm.tsx'
|
||||||
@@ -48,9 +49,9 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
|||||||
const res = await fetch(`/api/collaboration/share-pull?token=${token}`)
|
const res = await fetch(`/api/collaboration/share-pull?token=${token}`)
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 410) {
|
if (res.status === 410) {
|
||||||
throw new Error(i18n.language.startsWith('de') ? 'Dieser Freigabelink ist abgelaufen.' : 'This share link has expired.')
|
throw new Error(isGermanLocale(i18n.language) ? 'Dieser Freigabelink ist abgelaufen.' : 'This share link has expired.')
|
||||||
}
|
}
|
||||||
throw new Error(i18n.language.startsWith('de') ? 'Fehler beim Laden des freigegebenen Logbuchs.' : 'Failed to fetch shared logbook data.')
|
throw new Error(isGermanLocale(i18n.language) ? 'Fehler beim Laden des freigegebenen Logbuchs.' : 'Failed to fetch shared logbook data.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
@@ -136,15 +137,14 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const toggleLanguage = () => {
|
const toggleLanguage = () => {
|
||||||
const nextLang = i18n.language.startsWith('de') ? 'en' : 'de'
|
i18n.changeLanguage(getNextLanguage(i18n.language))
|
||||||
i18n.changeLanguage(nextLang)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="tab-placeholder" style={{ height: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
|
<div className="tab-placeholder" style={{ height: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<Ship className="header-logo spin" size={48} />
|
<Ship className="header-logo spin" size={48} />
|
||||||
<p>{i18n.language.startsWith('de') ? 'Lade freigegebenes Logbuch...' : 'Loading shared logbook...'}</p>
|
<p>{isGermanLocale(i18n.language) ? 'Lade freigegebenes Logbuch...' : 'Loading shared logbook...'}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -153,10 +153,10 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
|||||||
return (
|
return (
|
||||||
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: '20px', textAlign: 'center' }}>
|
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: '20px', textAlign: 'center' }}>
|
||||||
<AlertCircle size={48} style={{ color: '#ef4444', marginBottom: '16px' }} />
|
<AlertCircle size={48} style={{ color: '#ef4444', marginBottom: '16px' }} />
|
||||||
<h2 style={{ color: '#f1f5f9', marginBottom: '8px' }}>{i18n.language.startsWith('de') ? 'Verbindungsfehler' : 'Access Error'}</h2>
|
<h2 style={{ color: '#f1f5f9', marginBottom: '8px' }}>{isGermanLocale(i18n.language) ? 'Verbindungsfehler' : 'Access Error'}</h2>
|
||||||
<p style={{ color: '#94a3b8', maxWidth: '400px', marginBottom: '24px' }}>{error}</p>
|
<p style={{ color: '#94a3b8', maxWidth: '400px', marginBottom: '24px' }}>{error}</p>
|
||||||
<button className="btn primary" onClick={loadData} style={{ width: 'auto' }}>
|
<button className="btn primary" onClick={loadData} style={{ width: 'auto' }}>
|
||||||
{i18n.language.startsWith('de') ? 'Erneut versuchen' : 'Retry'}
|
{isGermanLocale(i18n.language) ? 'Erneut versuchen' : 'Retry'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -173,7 +173,7 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
|||||||
<h2>{logbookTitle}</h2>
|
<h2>{logbookTitle}</h2>
|
||||||
<p className="app-subtitle" style={{ color: '#10b981', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
<p className="app-subtitle" style={{ color: '#10b981', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||||
<Lock size={12} />
|
<Lock size={12} />
|
||||||
<span>{i18n.language.startsWith('de') ? 'Schreibgeschützte Ansicht (Ende-zu-Ende verschlüsselt)' : 'Read-Only View (End-to-End Encrypted)'}</span>
|
<span>{isGermanLocale(i18n.language) ? 'Schreibgeschützte Ansicht (Ende-zu-Ende verschlüsselt)' : 'Read-Only View (End-to-End Encrypted)'}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,7 +181,7 @@ export default function ReadOnlyViewer({ token, hexKey }: ReadOnlyViewerProps) {
|
|||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
<button className="btn secondary" onClick={toggleLanguage} style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }}>
|
<button className="btn secondary" onClick={toggleLanguage} style={{ width: 'auto', padding: '6px 12px', fontSize: '13px' }}>
|
||||||
<Globe size={14} style={{ marginRight: '4px' }} />
|
<Globe size={14} style={{ marginRight: '4px' }} />
|
||||||
{i18n.language.startsWith('de') ? 'English' : 'Deutsch'}
|
{t(`languages.${getNextLanguage(i18n.language)}`)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -3,12 +3,19 @@ import { initReactI18next } from 'react-i18next'
|
|||||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
import enJson from './locales/en.json'
|
import enJson from './locales/en.json'
|
||||||
import deJson from './locales/de.json'
|
import deJson from './locales/de.json'
|
||||||
|
import daJson from './locales/da.json'
|
||||||
|
import svJson from './locales/sv.json'
|
||||||
|
import nbJson from './locales/nb.json'
|
||||||
import { initSeo } from '../utils/seo.js'
|
import { initSeo } from '../utils/seo.js'
|
||||||
|
import { SUPPORTED_LANGUAGES } from '../utils/i18nLanguages.js'
|
||||||
|
|
||||||
/** JSON files wrap strings in `translation` — register that namespace explicitly. */
|
/** JSON files wrap strings in `translation` — register that namespace explicitly. */
|
||||||
const resources = {
|
const resources = {
|
||||||
en: { translation: enJson.translation },
|
en: { translation: enJson.translation },
|
||||||
de: { translation: deJson.translation }
|
de: { translation: deJson.translation },
|
||||||
|
da: { translation: daJson.translation },
|
||||||
|
sv: { translation: svJson.translation },
|
||||||
|
nb: { translation: nbJson.translation }
|
||||||
}
|
}
|
||||||
|
|
||||||
i18n
|
i18n
|
||||||
@@ -18,7 +25,7 @@ i18n
|
|||||||
resources,
|
resources,
|
||||||
defaultNS: 'translation',
|
defaultNS: 'translation',
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
supportedLngs: ['de', 'en'],
|
supportedLngs: [...SUPPORTED_LANGUAGES],
|
||||||
nonExplicitSupportedLngs: true,
|
nonExplicitSupportedLngs: true,
|
||||||
load: 'languageOnly',
|
load: 'languageOnly',
|
||||||
interpolation: {
|
interpolation: {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import deJson from '../i18n/locales/de.json'
|
||||||
|
import enJson from '../i18n/locales/en.json'
|
||||||
|
import daJson from '../i18n/locales/da.json'
|
||||||
|
import svJson from '../i18n/locales/sv.json'
|
||||||
|
import nbJson from '../i18n/locales/nb.json'
|
||||||
|
|
||||||
|
function collectKeys(obj: Record<string, unknown>, prefix = ''): string[] {
|
||||||
|
const keys: string[] = []
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const path = prefix ? `${prefix}.${key}` : key
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
keys.push(...collectKeys(value as Record<string, unknown>, path))
|
||||||
|
} else {
|
||||||
|
keys.push(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const bundles = {
|
||||||
|
de: deJson.translation,
|
||||||
|
en: enJson.translation,
|
||||||
|
da: daJson.translation,
|
||||||
|
sv: svJson.translation,
|
||||||
|
nb: nbJson.translation
|
||||||
|
} as const
|
||||||
|
|
||||||
|
describe('i18n locale key parity', () => {
|
||||||
|
const masterKeys = collectKeys(bundles.de)
|
||||||
|
|
||||||
|
it.each(Object.keys(bundles).filter((lang) => lang !== 'de'))(
|
||||||
|
'%s has the same keys as de',
|
||||||
|
(lang) => {
|
||||||
|
const keys = collectKeys(bundles[lang as keyof typeof bundles])
|
||||||
|
expect(keys).toEqual(masterKeys)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,735 @@
|
|||||||
|
{
|
||||||
|
"translation": {
|
||||||
|
"app": {
|
||||||
|
"name": "Kapteins Daagbok",
|
||||||
|
"tagline": "Privat yacht-logbog",
|
||||||
|
"beta": "Beta",
|
||||||
|
"beta_hint": "Betaversion - funktioner kan stadig ændres"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"da": "Dansk",
|
||||||
|
"sv": "Svenska",
|
||||||
|
"nb": "Norsk"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"unsaved_changes_title": "Ikke gemte ændringer",
|
||||||
|
"unsaved_changes_message": "Du har ændringer, der ikke er gemt. Vil du virkelig forlade siden? Dine ændringer vil gå tabt.",
|
||||||
|
"unsaved_changes_leave": "Forladelse",
|
||||||
|
"unsaved_changes_stay": "Bliv her"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"vessel": "Skibsdata",
|
||||||
|
"crew": "Besætningsliste",
|
||||||
|
"deviation": "Tabel over distraktioner",
|
||||||
|
"logs": "Indlæg i logbogen",
|
||||||
|
"stats": "Statistik",
|
||||||
|
"settings": "Indstillinger"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"welcome": "Velkommen til Kapteins Daagbok.",
|
||||||
|
"tagline": "Din sikre, E2E-krypterede maritime logbog.",
|
||||||
|
"register": "Registrer dig med Passkey.",
|
||||||
|
"login": "Log ind med Passkey.",
|
||||||
|
"login_as": "Log ind som {{name}}",
|
||||||
|
"quick_login": "Hurtigt login",
|
||||||
|
"forget_account": "Glemt konto på denne enhed",
|
||||||
|
"not_user": "Ikke {{name}}?",
|
||||||
|
"recovery_title": "Din genoprettelsesnøgle",
|
||||||
|
"recovery_warning": "VIGTIGT: Skriv disse 12 ord ned. Hvis du mister din Passkey og disse ord, kan dine data ikke gendannes.",
|
||||||
|
"confirm_recovery": "Jeg har skrevet ordene ned",
|
||||||
|
"status_logged_in": "Logget ind",
|
||||||
|
"status_logged_out": "Aflyst",
|
||||||
|
"copied": "Kopieret!",
|
||||||
|
"copy_phrase": "Kopieringstast",
|
||||||
|
"enter_recovery": "Indtast genoprettelsesnøgle",
|
||||||
|
"recovery_fallback_warning": "Din Passkey er blevet godkendt, men din enhed understøtter ikke hardwarebaseret nøgleafledning. Indtast din genoprettelsesnøgle på 12 ord for at dekryptere din logbog.",
|
||||||
|
"recovery_placeholder": "Indtast din genoprettelsesnøgle, som består af 12 ord adskilt af mellemrum...",
|
||||||
|
"back": "Tilbage",
|
||||||
|
"decrypting": "Dekryptering...",
|
||||||
|
"decrypt_logbook": "Afkodning af logbog",
|
||||||
|
"error_incorrect_recovery": "Forkert genoprettelsesnøgle. Dekryptering mislykkedes.",
|
||||||
|
"error_decryption_failed": "Dekryptering mislykkedes. Tjek venligst din genoprettelsesnøgle.",
|
||||||
|
"or_register": "eller registrer dig",
|
||||||
|
"explore_demo": "Udforsk demoen uden en konto",
|
||||||
|
"username_placeholder": "Brugernavn / skippernavn",
|
||||||
|
"processing": "Behandling...",
|
||||||
|
"help": "Hjælp",
|
||||||
|
"setup_pin_title": "Opsæt lokal PIN-kode (valgfrit)",
|
||||||
|
"setup_pin_warning": "Da din enhed ikke understøtter direkte Passkey-nøgleafledning, ville du ellers være nødt til at indtaste din 12-ordsnøgle, hver gang du logger ind på denne enhed. Opsæt en lokal PIN-kode for at undgå dette.",
|
||||||
|
"pin_placeholder": "E.G. 123456",
|
||||||
|
"pin_label": "Lokal PIN-kode (4-8 cifre)",
|
||||||
|
"save_pin": "Gem PIN-kode og fortsæt",
|
||||||
|
"skip_pin": "Spring over og brug gendannelse",
|
||||||
|
"enter_pin_title": "Afkodning med PIN-kode",
|
||||||
|
"enter_pin_warning": "Indtast din lokale PIN-kode for at låse op for dekrypteringsnøglen på denne enhed.",
|
||||||
|
"enter_pin_placeholder": "Indtast din pinkode...",
|
||||||
|
"decrypt_with_pin": "Afkodning",
|
||||||
|
"use_recovery_instead": "Brug genoprettelsesnøgler i stedet",
|
||||||
|
"error_incorrect_pin": "Forkert PIN-kode. Dekryptering mislykkedes."
|
||||||
|
},
|
||||||
|
"pwa": {
|
||||||
|
"title": "Installer app",
|
||||||
|
"generic_benefit": "Installer Kapteins Daagbok på din enhed for at få hurtigere adgang, offline-brug og permanent datalagring.",
|
||||||
|
"ios_instructions": "På iPad/iPhone: Føj appen til startskærmen, så dine logbogsdata forbliver beskyttet, og appen starter som en indbygget app.",
|
||||||
|
"ios_step_share": "Tryk på aktiesymbolet i Safari-linjen",
|
||||||
|
"ios_step_add": "Vælg \"Gå til startskærm\"",
|
||||||
|
"install_now": "Installer nu",
|
||||||
|
"installing": "Installation...",
|
||||||
|
"later": "Senere",
|
||||||
|
"never": "Vis ikke mere",
|
||||||
|
"platform_ios": "Installation via Safari.",
|
||||||
|
"platform_android": "Installation via browseren",
|
||||||
|
"platform_desktop": "Installation som desktop-app",
|
||||||
|
"settings_section": "Installation af app",
|
||||||
|
"update_title": "Opdatering tilgængelig",
|
||||||
|
"update_desc": "En ny version af Kapteins Daagbok er klar. Opdater venligst for at få de seneste ændringer.",
|
||||||
|
"update_now": "Opdater nu",
|
||||||
|
"update_reloading": "Indlæser..."
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"status_synced": "Synkroniseret",
|
||||||
|
"status_syncing": "Synkroniser...",
|
||||||
|
"status_offline": "Offline-cache",
|
||||||
|
"status_unsynced": "Usynkroniserede ændringer"
|
||||||
|
},
|
||||||
|
"vessel": {
|
||||||
|
"title": "Skibets stamdata",
|
||||||
|
"name": "Yacht-navn",
|
||||||
|
"type": "Yacht-type",
|
||||||
|
"type_unset": "- ikke specificeret -",
|
||||||
|
"type_sailing": "Sejlbåd",
|
||||||
|
"type_motor": "Motorbåd",
|
||||||
|
"length_m": "Længde (m)",
|
||||||
|
"draft_m": "Dybgang (m)",
|
||||||
|
"air_draft_m": "Højde (m)",
|
||||||
|
"invalid_metric": "Ugyldig numerisk værdi - indtast venligst meter som et decimaltal (f.eks. 12,5).",
|
||||||
|
"port": "Hjemmehavn",
|
||||||
|
"owner": "Ejer",
|
||||||
|
"charter": "Charterselskab",
|
||||||
|
"registration": "Nummerplade/registreringsnummer",
|
||||||
|
"callsign": "Radiokaldesignal",
|
||||||
|
"atis": "ATIS nr.",
|
||||||
|
"mmsi": "MMSI-nr.",
|
||||||
|
"save": "Gem skibsdata",
|
||||||
|
"saving": "Vil blive reddet...",
|
||||||
|
"saved": "Skibsdata er gemt med succes!",
|
||||||
|
"loading": "Skibsdata er indlæst...",
|
||||||
|
"sails_list": "Sejl (eksisterende sejl)",
|
||||||
|
"sails_help": "Indtast de sejl, der er tilgængelige på din båd her (f.eks. storsejl, genua, fok).",
|
||||||
|
"add_sail": "Tilføj sejl",
|
||||||
|
"sail_name_placeholder": "z. f.eks. storsejl",
|
||||||
|
"no_sails": "Ingen sejl opbevaret.",
|
||||||
|
"photo_add": "Tilføj foto",
|
||||||
|
"photo_change": "Skift foto",
|
||||||
|
"photo_delete": "Slet foto",
|
||||||
|
"tanks_section": "Tanke (kapacitet)",
|
||||||
|
"tanks_help": "Valgfrit i liter - muliggør slider i journalen for kendte tankstørrelser.",
|
||||||
|
"freshwater_capacity_l": "Drikkevand (liter)",
|
||||||
|
"fuel_capacity_l": "Brændstof (liter)",
|
||||||
|
"greywater_capacity_l": "Gråt vand (liter)",
|
||||||
|
"invalid_tank_liters": "Ugyldig numerisk værdi - indtast venligst liter som et tal (f.eks. 200)."
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "Logbogsdagbog",
|
||||||
|
"new_entry": "Ny rejsedag",
|
||||||
|
"travel_details": "Detaljer om rejsen",
|
||||||
|
"add_event": "Tilføj ny logbogspost",
|
||||||
|
"add_event_btn": "Tilføj begivenhed",
|
||||||
|
"edit_event": "Rediger begivenhed",
|
||||||
|
"save_event_btn": "Gem ændring",
|
||||||
|
"cancel_event_edit": "Annuller",
|
||||||
|
"delete_event": "Slet begivenhed",
|
||||||
|
"sign_cleared_skipper_re_sign_title": "Skippers underskrift fjernet",
|
||||||
|
"sign_cleared_skipper_re_sign": "Hændelsesloggen er blevet ændret. Skipperens underskrift er blevet fjernet. Godkend venligst igen.",
|
||||||
|
"date": "dato",
|
||||||
|
"day_of_travel": "Rejsedag / rejsedag",
|
||||||
|
"departure": "Starthavn (rejse fra)",
|
||||||
|
"destination": "Destinationsport (til)",
|
||||||
|
"route": "Rejse fra/til",
|
||||||
|
"freshwater": "Ferskvand (liter)",
|
||||||
|
"fuel": "Treibstoff / Brændstof (liter)",
|
||||||
|
"greywater": "Gråt vand (liter)",
|
||||||
|
"greywater_level": "Fyldningsniveau",
|
||||||
|
"tank_slider_of_max": "{{current}} / {{max}} L",
|
||||||
|
"tank_capacity_tooltip": "Hvis tankkapaciteten (liter) er gemt i skibsdataene, kan du indtaste fyldningsniveauerne her ved hjælp af skyderen.",
|
||||||
|
"morning": "Stå op om morgenen",
|
||||||
|
"refilled": "Genopfyldt",
|
||||||
|
"evening": "Stand om aftenen",
|
||||||
|
"consumption": "Dagligt forbrug",
|
||||||
|
"signatures": "Underskrifter / frigivelse",
|
||||||
|
"sign_skipper": "Skippers underskrift",
|
||||||
|
"sign_crew": "Crew-signatur",
|
||||||
|
"sign_hint": "Tegn med finger, pen eller mus",
|
||||||
|
"sign_clear": "Sletning",
|
||||||
|
"sign_export_image": "[Underskrift]",
|
||||||
|
"sign_with_passkey": "Frigør med Passkey.",
|
||||||
|
"sign_passkey_signing": "Der anmodes om Passkey...",
|
||||||
|
"sign_passkey_signed": "Udgivet af {{username}}",
|
||||||
|
"sign_passkey_export": "Passkey: {{username}} ({{date}})",
|
||||||
|
"sign_attribution_export": "{{username}} ({{date}})",
|
||||||
|
"sign_passkey_clear": "Fjern Passkey-frigivelse",
|
||||||
|
"sign_mode_passkey": "Passkey",
|
||||||
|
"sign_mode_classic": "Klassisk",
|
||||||
|
"sign_passkey_failed": "Passkey Frigivelse mislykkedes",
|
||||||
|
"sign_passkey_cancelled": "Passkey Frigivelse annulleret",
|
||||||
|
"sign_invalid": "Signatur ugyldig - indholdet er blevet ændret",
|
||||||
|
"sign_badge_skipper": "Skipper",
|
||||||
|
"sign_badge_skipper_invalid": "Ugyldig",
|
||||||
|
"sign_badge_skipper_title_valid": "Skipper har udgivet",
|
||||||
|
"sign_badge_skipper_title_invalid": "Skippers signatur er ugyldig - indholdet er blevet ændret",
|
||||||
|
"sign_classic_or_passkey": "Valgfrit: klassisk underskrift eller Passkey-frigivelse ovenfor",
|
||||||
|
"sign_crew_passkey_hint": "Besætningsmedlemmer med skriveadgang kan frigive via Passkey.",
|
||||||
|
"sign_offline_hint": "Passkey-Godkendelse kræver internet - klassisk underskrift mulig offline",
|
||||||
|
"sign_lock_notice": "Efter underskrivelsen kan der ikke foretages ændringer i logbogen (undtagen fotos), uden at skipper og besætning skal skrive under igen.",
|
||||||
|
"sign_lock_active": "Denne post er underskrevet. Ændringer i logbogen (undtagen fotos) fjerner automatisk skipperens og besætningens underskrifter.",
|
||||||
|
"sign_lock_warning_title": "Bekræft underskrift",
|
||||||
|
"sign_lock_warning": "Efter underskrivelsen er det ikke længere muligt at foretage ændringer i logbogen (undtagen fotos), uden at skipper og besætning skal skrive under igen.\n\nVil du gerne fortsætte?",
|
||||||
|
"sign_proceed": "Tegn",
|
||||||
|
"sign_cancel": "Annuller",
|
||||||
|
"sign_cleared_re_sign_title": "Underskrifter fjernet",
|
||||||
|
"sign_cleared_re_sign": "Logbogsoptegnelsen er blevet ændret. Skipperens og besætningens underskrifter er blevet fjernet. Underskriv venligst igen.",
|
||||||
|
"no_entries": "Ingen logbogsposter fundet for denne yacht. Opret din første rejsedag!",
|
||||||
|
"back_to_list": "Tilbage til tidsskriftslisten",
|
||||||
|
"save": "Gem logbogsside",
|
||||||
|
"saving": "Vil blive reddet...",
|
||||||
|
"saved": "Logbogsside gemt med succes!",
|
||||||
|
"loading": "Dagbogen er ved at blive indlæst.",
|
||||||
|
"delete_entry": "Slet tag",
|
||||||
|
"delete_confirm": "Er du sikker på, at du vil slette denne rejsedag permanent?",
|
||||||
|
"carry_over_tanks_title": "Overføre data fra den foregående dag?",
|
||||||
|
"carry_over_tanks_confirm": "Overtage starthavn, ferskvand, brændstof og gråvand fra den sidste dag på turen?\n\nStarthavn: {{departure}}\nFerskvand: {{fw}} L\nBrændstof: {{fuel}} L\nGråt vand: {{greywater}} L",
|
||||||
|
"carry_over_tanks_yes": "Tag over",
|
||||||
|
"carry_over_tanks_no": "Start med 0",
|
||||||
|
"event_title": "Kronologisk hændelseslog",
|
||||||
|
"no_events": "Der er endnu ikke indtastet nogen begivenheder for denne rejsedag.",
|
||||||
|
"event_time": "Tidspunkt på dagen",
|
||||||
|
"event_mgk": "MgK-kursus",
|
||||||
|
"event_rwk": "RwK-kursus",
|
||||||
|
"event_course_section": "Kursus",
|
||||||
|
"course_dial_hint": "Drej ringen eller indtast grader",
|
||||||
|
"course_dial_step_label": "Trinstørrelse",
|
||||||
|
"course_step_fine": "1°",
|
||||||
|
"course_step_medium": "5°",
|
||||||
|
"course_step_coarse": "10°",
|
||||||
|
"course_tab_mgk": "MgK",
|
||||||
|
"course_tab_rwk": "rwK",
|
||||||
|
"course_invalid": "Ugyldigt kursus (0-360)",
|
||||||
|
"course_placeholder_degrees": "z. B. 180",
|
||||||
|
"course_placeholder_cardinal": "z. E.G. NW",
|
||||||
|
"compass_n": "N",
|
||||||
|
"compass_e": "O",
|
||||||
|
"compass_s": "S",
|
||||||
|
"compass_w": "W",
|
||||||
|
"wind_mode_cardinal": "Kardinal",
|
||||||
|
"wind_mode_degrees": "Som grad",
|
||||||
|
"event_wind_direction": "Vindretning",
|
||||||
|
"event_wind_strength": "Vindstyrke",
|
||||||
|
"event_sea_state": "Havets tilstand",
|
||||||
|
"event_weather": "Vejret",
|
||||||
|
"event_log": "Log (sm)",
|
||||||
|
"event_gps": "GPS-position",
|
||||||
|
"event_location": "Sted/havn",
|
||||||
|
"event_location_placeholder": "z. f.eks. Kiel",
|
||||||
|
"event_remarks": "Bemærkninger / hændelser",
|
||||||
|
"gps_btn": "Hent GPS-koordinater",
|
||||||
|
"weather_btn": "OpenWeatherMap Kald vejret op",
|
||||||
|
"event_wind_pressure": "Lufttryk (hPa)",
|
||||||
|
"event_heel": "Krængning (°)",
|
||||||
|
"event_sails": "Sejlhåndtering/motor",
|
||||||
|
"motor_propulsion": "Kørsel med maskine",
|
||||||
|
"sails_picker_show_more": "Vis alle sejl",
|
||||||
|
"sails_picker_show_less": "Vis mindre",
|
||||||
|
"motor_hours": "Maskintimer (i alt)",
|
||||||
|
"fuel_per_motor_hour": "Forbrug pr. maskintime",
|
||||||
|
"event_distance": "Afstand (nm)",
|
||||||
|
"export_csv": "Download CSV.",
|
||||||
|
"share_csv": "CSV andel",
|
||||||
|
"export_pdf": "Download PDF.",
|
||||||
|
"exporting_pdf": "PDF er genereret...",
|
||||||
|
"photos_title": "Vedhæftede billeder (E2E-krypteret)",
|
||||||
|
"photo_caption_label": "Fotobeskrivelse/etiket (valgfrit)",
|
||||||
|
"photo_caption_placeholder": "f.eks. at sætte sejl nær indsejlingen til havnen",
|
||||||
|
"photo_btn": "Tag foto / upload",
|
||||||
|
"photo_processing": "Er ved at blive behandlet...",
|
||||||
|
"no_photos": "Der er endnu ingen billeder knyttet til denne rejsedag.",
|
||||||
|
"photo_delete_confirm": "Er du sikker på, at du vil slette dette foto permanent?",
|
||||||
|
"confirm_yes": "Ja",
|
||||||
|
"confirm_no": "Nej",
|
||||||
|
"track_upload_title": "GPS-spor (fil)",
|
||||||
|
"track_upload_points": "Point",
|
||||||
|
"gps_tracking_btn_gpx": "Download sporfilen",
|
||||||
|
"gps_track_upload_help": "Træk en GPX-, KML- eller GeoJSON-fil hertil, eller klik for at vælge",
|
||||||
|
"gps_track_upload_btn": "Upload GPS-spor",
|
||||||
|
"gps_track_delete": "Slet sporfilen",
|
||||||
|
"gps_track_delete_confirm": "Er du sikker på, at du vil slette denne sporfil permanent?",
|
||||||
|
"track_distance": "GPS-rute (sm)",
|
||||||
|
"track_speed_max": "Maks. Hastighed (kn)",
|
||||||
|
"track_speed_avg": "Ø Hastighed (kn)",
|
||||||
|
"track_map_title": "GPS-spor på OpenSeaMap",
|
||||||
|
"track_map_start": "Start",
|
||||||
|
"track_map_end": "Mål",
|
||||||
|
"track_map_speed_slow": "langsomt",
|
||||||
|
"track_map_speed_fast": "hurtigt",
|
||||||
|
"track_map_error": "Kortet kunne ikke indlæses.",
|
||||||
|
"exporting": "Eksport...",
|
||||||
|
"share_unsupported": "Deling understøttes ikke på denne enhed. Filen er blevet downloadet i stedet.",
|
||||||
|
"invite_crew": "Inviter besætningen",
|
||||||
|
"invite_link_copied": "Invitationslink kopieret til udklipsholderen!",
|
||||||
|
"invite_link_desc": "Del dette link med besætningsmedlemmer for at give dem skriveadgang til denne logbog.",
|
||||||
|
"collaborators_list": "Medlemmer / besætning",
|
||||||
|
"revoke": "Fjerne",
|
||||||
|
"revoke_confirm": "Er du sikker på, at du vil tilbagekalde dette besætningsmedlems adgang?",
|
||||||
|
"invite_role": "Rolle",
|
||||||
|
"invite_expires": "Linket er gyldigt i 48 timer"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dine logbøger",
|
||||||
|
"subtitle": "Vælg en logbog, eller opret en ny til at styre dine rejser.",
|
||||||
|
"create_btn": "Opret logbog",
|
||||||
|
"new_logbook_placeholder": "Navn på logbog eller yacht",
|
||||||
|
"logout": "Log ud",
|
||||||
|
"logged_in_as": "Logget ind som {{name}}",
|
||||||
|
"delete_confirm": "Er du sikker på, at du vil slette denne logbog permanent? Alle lokale data og serverkopier vil blive destrueret.\n\nTip: Lav en sikkerhedskopi (.daagbok.json) på forhånd under Indstillinger → Sikkerhedskopiering og gendannelse, hvis du vil beholde dataene senere.",
|
||||||
|
"no_logbooks": "Ingen logbøger fundet. Opret din første logbog for at komme i gang!",
|
||||||
|
"loading": "Logbøgerne er fyldt op...",
|
||||||
|
"status_synced": "Synkroniseret",
|
||||||
|
"status_local": "Kun lokal cache",
|
||||||
|
"delete_btn": "Slet logbog",
|
||||||
|
"section_owned": "Mine logbøger",
|
||||||
|
"section_shared": "Fælles logbøger",
|
||||||
|
"section_shared_hint": "Du er blevet inviteret som besætningsmedlem. Skipperprofil og indstillinger tilhører ejeren.",
|
||||||
|
"role_owner": "Egen logbog",
|
||||||
|
"role_owner_hint": "Du er ejer og skipper af denne logbog",
|
||||||
|
"role_crew": "Adgang for besætning",
|
||||||
|
"role_crew_hint": "Inviteret logbog - du kan arbejde som besætning og underskrive den",
|
||||||
|
"role_read": "Læs kun",
|
||||||
|
"role_read_hint": "Opdelt logbog - kun visning, ingen redigering",
|
||||||
|
"open_profile": "Åben profil af {{name}}",
|
||||||
|
"edit_title": "Omdøb logbog",
|
||||||
|
"edit_placeholder": "Nyt navn på logbogen",
|
||||||
|
"edit_success": "Logbog omdøbt med succes",
|
||||||
|
"edit_btn": "Omdøb",
|
||||||
|
"filter_label": "Filtrer logbøger",
|
||||||
|
"filter_placeholder": "Navn, årstal eller dato ...",
|
||||||
|
"filter_clear": "Nulstil filter",
|
||||||
|
"filter_results": "{{count}} Hits",
|
||||||
|
"filter_no_results": "Ingen logbøger matcher din søgning. Prøv med et andet navn eller et andet år.",
|
||||||
|
"sort_label": "Sortere",
|
||||||
|
"sort_by_label": "Sorter efter",
|
||||||
|
"sort_by_name": "Navn",
|
||||||
|
"sort_by_date": "dato",
|
||||||
|
"sort_dir_label": "Sekvens",
|
||||||
|
"sort_asc": "Stigende",
|
||||||
|
"sort_desc": "Nedadgående",
|
||||||
|
"sort_name_asc": "Navn A til Z",
|
||||||
|
"sort_name_desc": "Navn Z til A",
|
||||||
|
"sort_date_asc": "Ældste først",
|
||||||
|
"sort_date_desc": "Nyeste først"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Brugerprofil",
|
||||||
|
"subtitle": "Konto, Passkeys og statistik for {{name}}.",
|
||||||
|
"back": "Tilbage til instrumentbrættet",
|
||||||
|
"loading": "Profilen er ved at blive indlæst...",
|
||||||
|
"load_error": "Profilen kunne ikke indlæses.",
|
||||||
|
"copy_failed": "Kopiering mislykkedes.",
|
||||||
|
"processing": "Er ved at blive behandlet...",
|
||||||
|
"identity_title": "Konto-identitet",
|
||||||
|
"username": "Brugernavn",
|
||||||
|
"user_id": "Bruger-ID",
|
||||||
|
"copy_user_id": "Kopier bruger-ID",
|
||||||
|
"account_since": "Konto siden",
|
||||||
|
"prf_status": "Passkey nøgleafledning (PRF)",
|
||||||
|
"prf_active": "Aktiv",
|
||||||
|
"prf_inactive": "Ikke sat op",
|
||||||
|
"passkeys_title": "Passkeys",
|
||||||
|
"passkeys_desc": "Registrer en separat Passkey på hver enhed. På den måde kan du logge ind, selv når du har skiftet platform.",
|
||||||
|
"passkeys_empty": "Ingen Passkeys fundet.",
|
||||||
|
"add_passkey_btn": "Tilføj ny Passkey.",
|
||||||
|
"add_passkey_success": "Passkey tilføjet med succes.",
|
||||||
|
"add_passkey_failed": "Passkey kunne ikke tilføjes.",
|
||||||
|
"remove_passkey_btn": "Fjern Passkey",
|
||||||
|
"remove_passkey_last_title": "Sidste Passkey.",
|
||||||
|
"remove_passkey_last_desc": "Den eneste Passkey kan ikke fjernes uden at miste adgangen til din konto. Hvis du vil slette kontoen helt, skal du bruge farezonen nederst på denne side.",
|
||||||
|
"remove_passkey_failed": "Passkey kunne ikke fjernes.",
|
||||||
|
"remove_passkey_confirm_title": "Fjern Passkey?",
|
||||||
|
"remove_passkey_confirm_desc": "Denne enhed kan så ikke længere logge ind med denne Passkey.",
|
||||||
|
"remove_passkey_confirm_yes": "Fjerne",
|
||||||
|
"remove_passkey_confirm_no": "Annuller",
|
||||||
|
"pin_title": "Lokal PIN-kode",
|
||||||
|
"pin_status": "Status",
|
||||||
|
"pin_active": "Aktiv på denne enhed",
|
||||||
|
"pin_inactive": "Ikke sat op",
|
||||||
|
"pin_confirm_label": "Bekræft PIN-kode",
|
||||||
|
"pin_confirm_placeholder": "Indtast PIN-kode igen",
|
||||||
|
"pin_set_btn": "Opsæt PIN-kode",
|
||||||
|
"pin_change_btn": "Skift PIN-kode",
|
||||||
|
"pin_remove_btn": "Fjern PIN-kode",
|
||||||
|
"pin_saved": "PIN-kode gemt.",
|
||||||
|
"pin_save_failed": "PIN-koden kunne ikke gemmes.",
|
||||||
|
"pin_mismatch": "PIN-koderne stemmer ikke overens.",
|
||||||
|
"pin_length_error": "PIN-koden skal bestå af mindst 4 tegn.",
|
||||||
|
"pin_no_session": "Sessionen er udløbet - tilmeld dig venligst igen.",
|
||||||
|
"remove_pin_confirm_title": "Fjerne PIN-kode?",
|
||||||
|
"remove_pin_confirm_desc": "Du skal logge ind igen på denne enhed med Passkey eller genoprettelsesnøglen.",
|
||||||
|
"remove_pin_confirm_yes": "Fjern PIN-kode",
|
||||||
|
"remove_pin_confirm_no": "Annuller",
|
||||||
|
"security_title": "Tjekliste for sikkerhed",
|
||||||
|
"security_desc": "Oversigt over de vigtigste beskyttelsesmekanismer for din konto.",
|
||||||
|
"security_passkeys_ok": "Mindst én Passkey registreret",
|
||||||
|
"security_passkeys_missing": "Nej Passkey registreret",
|
||||||
|
"security_prf_ok": "PRF-nøgleafledning aktiv",
|
||||||
|
"security_prf_missing": "PRF ikke sat op",
|
||||||
|
"security_pin_ok": "Lokal PIN-kode på denne enhed",
|
||||||
|
"security_pin_missing": "Ingen lokal PIN-kode",
|
||||||
|
"security_recovery_ok": "Opsætning af genoprettelsesnøgle",
|
||||||
|
"security_recovery_hint": "De 12 ord blev vist under registreringen. Opbevar dem offline og adskilt fra enheden. Du kan oprette en ny nøgle nedenfor - den gamle bliver så ugyldig.",
|
||||||
|
"recovery_rotate_btn": "Opret en ny genoprettelsesnøgle",
|
||||||
|
"recovery_rotate_confirm_title": "Opret en ny genoprettelsesnøgle?",
|
||||||
|
"recovery_rotate_confirm_desc": "Den tidligere nøgle på 12 ord bliver ugyldig med det samme. Sørg for at opbevare den nye nøgle sikkert, før du fortsætter.",
|
||||||
|
"recovery_rotate_confirm_yes": "Opret ny nøgle",
|
||||||
|
"recovery_rotate_confirm_no": "Annuller",
|
||||||
|
"recovery_rotate_new_warning": "VIGTIGT: Skriv disse 12 ord ned, og opbevar dem offline. Den tidligere genoprettelsesnøgle er nu ugyldig.",
|
||||||
|
"recovery_rotate_failed": "Genoprettelsesnøglen kunne ikke oprettes.",
|
||||||
|
"recovery_rotate_no_session": "Krypteringssessionen er udløbet - log ud og log ind igen, og prøv så igen.",
|
||||||
|
"device_title": "Denne enhed",
|
||||||
|
"device_desc": "Lokal cache, synkroniseringsstatus og hurtig login i denne browser.",
|
||||||
|
"device_sync_pending": "{{count}} ventende synkroniseringsposter",
|
||||||
|
"device_sync_ok": "Alle lokale ændringer synkroniseres",
|
||||||
|
"device_remembered": "Konto til hurtigt login gemt på denne enhed",
|
||||||
|
"device_not_remembered": "Kontoen er ikke på listen over hurtige login",
|
||||||
|
"device_forget_btn": "Glemt konto på denne enhed",
|
||||||
|
"device_forget_confirm_title": "Fjerne hurtig login?",
|
||||||
|
"device_forget_confirm_desc": "Kontoen forsvinder fra listen over hurtige login på denne enhed. Din session og dine lokale logbøger bevares.",
|
||||||
|
"device_forget_confirm_yes": "Fjerne",
|
||||||
|
"device_forget_confirm_no": "Annuller",
|
||||||
|
"passkey_label": "Navn på ny Passkey (valgfrit)",
|
||||||
|
"passkey_label_placeholder": "z. f.eks. MacBook, iPhone.",
|
||||||
|
"passkey_rename_btn": "Gem navn",
|
||||||
|
"passkey_rename_success": "Passkey navn gemt.",
|
||||||
|
"passkey_rename_failed": "Passkey-Navnet kunne ikke gemmes.",
|
||||||
|
"passkey_unnamed": "Uden titel Passkey.",
|
||||||
|
"stats_title": "Statistik",
|
||||||
|
"stats_subtitle": "Om alle dine logbøger på denne enhed",
|
||||||
|
"stats_logbooks": "Logbøger",
|
||||||
|
"stats_account_since": "Konto siden",
|
||||||
|
"stats_shared_logbooks": "Fælles logbøger",
|
||||||
|
"appearance_title": "App og visualisering",
|
||||||
|
"appearance_desc": "Designet og farveskemaet gælder for hele appen på denne enhed.",
|
||||||
|
"theme_label": "Appens designstil",
|
||||||
|
"theme_auto": "Automatisk (OS-registrering)",
|
||||||
|
"theme_ocean": "Ocean (glasmorfisme)",
|
||||||
|
"theme_material": "Materiale (Android)",
|
||||||
|
"theme_cupertino": "Cupertino (iOS)",
|
||||||
|
"color_scheme_label": "Lys eller mørk tilstand",
|
||||||
|
"color_scheme_auto": "Automatisk (system)",
|
||||||
|
"color_scheme_light": "Lys",
|
||||||
|
"color_scheme_dark": "Mørk",
|
||||||
|
"integrations_title": "Integrationer",
|
||||||
|
"owm_key": "OpenWeatherMap API-nøgle",
|
||||||
|
"owm_help": "Valgfrit: egen OpenWeatherMap API-nøgle. Hvis der ikke er nogen indtastning, bruges nøglen på serversiden fra operatørkonfigurationen.",
|
||||||
|
"prefs_save": "Gemme",
|
||||||
|
"prefs_saving": "Vil blive reddet...",
|
||||||
|
"prefs_saved": "Gemt",
|
||||||
|
"tour_title": "App-tur",
|
||||||
|
"tour_desc": "Lad dig guide gennem de vigtigste områder i appen igen.",
|
||||||
|
"tour_restart": "Start turen igen",
|
||||||
|
"push_title": "Push-meddelelser",
|
||||||
|
"push_desc": "Som logbogsejer får du besked, når inviterede besætningsmedlemmer synkroniserer ændringer. Intet indhold overføres i ren tekst.",
|
||||||
|
"push_enable": "Giv os besked om ændringer i besætningen",
|
||||||
|
"push_active": "Push-meddelelser er aktive på denne enhed.",
|
||||||
|
"push_unsupported": "Push-meddelelser understøttes ikke i denne browser.",
|
||||||
|
"push_denied_hint": "Notifikationer er blokeret. Tillad dem i browserens eller enhedens indstillinger.",
|
||||||
|
"push_ios_install_hint": "På iPhone/iPad: Føj app til startskærmen (iOS 16.4+) for at bruge push.",
|
||||||
|
"push_error": "Push-meddelelser kunne ikke aktiveres."
|
||||||
|
},
|
||||||
|
"crew": {
|
||||||
|
"title": "Skipper- og besætningsprofiler",
|
||||||
|
"skipper_section": "Skipper-profil",
|
||||||
|
"skipper_read_only_hint": "Skipperprofilen kan kun redigeres af logbogens ejer.",
|
||||||
|
"crew_section": "Besætningsliste",
|
||||||
|
"add_crew": "Tilføj besætningsmedlem",
|
||||||
|
"edit_crew": "Rediger besætningsmedlem",
|
||||||
|
"no_crew": "Ingen besætningsmedlemmer tilføjet endnu.",
|
||||||
|
"max_crew": "Det maksimale antal på 5 besætningsmedlemmer er nået.",
|
||||||
|
"name": "Navn",
|
||||||
|
"address": "adresse",
|
||||||
|
"birthdate": "Fødselsdag",
|
||||||
|
"phone": "Telefonnummer",
|
||||||
|
"nationality": "Nationalitet",
|
||||||
|
"passport": "Pas/ID-nummer",
|
||||||
|
"bloodtype": "Blodgruppe",
|
||||||
|
"allergies": "Allergier",
|
||||||
|
"diseases": "Eksisterende tilstande/sygdomme",
|
||||||
|
"save": "Gem skipper-data",
|
||||||
|
"save_member": "Gem medlem",
|
||||||
|
"saved": "Skipperprofilen er blevet gemt!",
|
||||||
|
"loading": "Besætningsfilerne er indlæst.",
|
||||||
|
"delete_confirm": "Er du sikker på, at du vil fjerne dette besætningsmedlem?"
|
||||||
|
},
|
||||||
|
"deviation": {
|
||||||
|
"title": "Tabel over kompasafvigelser",
|
||||||
|
"subtitle": "Indtast den magnetiske kompasafbøjning (afbøjning) for kurser (MgK) fra 000° til 360° i trin på 10°.",
|
||||||
|
"heading": "MgK",
|
||||||
|
"deviation": "Distraktion",
|
||||||
|
"save": "Gem kalibreringsgitter",
|
||||||
|
"saving": "Vil blive reddet...",
|
||||||
|
"saved": "Kalibreringsgitteret er gemt med succes!",
|
||||||
|
"loading": "Kalibreringstabellen er indlæst..."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Indstillinger for logbog",
|
||||||
|
"subtitle": "Del, tag backup og samarbejd om denne logbog.",
|
||||||
|
"select_logbook_hint": "Vælg en logbog for at redigere dens indstillinger.",
|
||||||
|
"no_key": "Ingen OpenWeatherMap API-nøgle tilgængelig. Gem din egen nøgle i brugerprofilen, eller kontakt operatøren.",
|
||||||
|
"weather_success": "Vejrdata hentet med succes!",
|
||||||
|
"weather_error": "Hentning af vejrdata mislykkedes. Tjek API-nøglen og forbindelsen.",
|
||||||
|
"weather_date_mismatch": "Vejrdata kan kun hentes for i dag ({{today}}). Dette logbogsindlæg er dateret {{date}}.",
|
||||||
|
"gps_error": "Indtast en placering, eller find GPS-koordinaterne.",
|
||||||
|
"share_title": "Del logbog (skrivebeskyttet)",
|
||||||
|
"share_desc": "Aktivér denne mulighed for at oprette et offentligt, skrivebeskyttet link. Alle med linket kan se dine rejser, yachtprofiler og besætning. Krypteringsnøglerne overføres aldrig til serveren (de forbliver i hash-delen af URL'en).",
|
||||||
|
"share_privacy_warning": "Anbefaling: Del kun dette link privat (f.eks. via e-mail eller messenger), ikke på sociale medier.",
|
||||||
|
"share_enable": "Aktivér offentligt link",
|
||||||
|
"share_copied": "Link kopieret!",
|
||||||
|
"share_copy_btn": "Kopier link",
|
||||||
|
"danger_zone_title": "Farezone",
|
||||||
|
"danger_zone_desc": "Når du sletter din konto, slettes alle dine Passkey'er, logbøger, skibsdata, besætningsprofiler, rejseindlæg og E2E-nøgler uigenkaldeligt. Denne handling kan ikke fortrydes.",
|
||||||
|
"delete_account_btn": "Slet konto uigenkaldeligt",
|
||||||
|
"delete_account_confirm_title": "Slette konto?",
|
||||||
|
"delete_account_confirm_desc": "Er du helt sikker på, at du vil slette din konto uigenkaldeligt og alle tilknyttede logbøger og E2E-krypterede data?",
|
||||||
|
"delete_account_confirm_yes": "Ja, slet konto og alle data",
|
||||||
|
"delete_account_confirm_no": "Annuller",
|
||||||
|
"delete_account_failed": "Kontoen kunne ikke slettes. Prøv venligst igen.",
|
||||||
|
"delete_backup_hint": "Tip: Lav sikkerhedskopier af dine logbøger (.daagbok.json) i indstillingerne for hver logbog, før du sletter dem.",
|
||||||
|
"deleting_account": "Kontoen vil blive slettet...",
|
||||||
|
"invite_push_prompt_title": "Aktivere push-meddelelser?",
|
||||||
|
"invite_push_prompt_message": "Så snart inviterede besætningsmedlemmer synkroniserer ændringer, kan du blive informeret via push. Intet logbogsindhold sendes i almindelig tekst.",
|
||||||
|
"invite_push_prompt_ios_message": "Så snart besætningsmedlemmerne synkroniserer ændringer, kan du blive informeret via push. På iPhone/iPad: Føj appen til startskærmen (iOS 16.4+), og aktiver derefter push i brugerprofilen.",
|
||||||
|
"invite_push_prompt_enable": "Aktiver nu",
|
||||||
|
"invite_push_prompt_later": "Senere",
|
||||||
|
"invite_push_prompt_success": "Push-meddelelser er aktive på denne enhed.",
|
||||||
|
"backup_title": "Sikkerhedskopiering og gendannelse",
|
||||||
|
"backup_desc": "Komplet krypteret backup af denne logbog (poster, fotos, GPS-spor, besætning, skib). Beskyttet med backup-passphrase - til gendannelse til denne eller en ny konto.",
|
||||||
|
"backup_export_title": "Opret backup",
|
||||||
|
"backup_export_desc": "Downloader alle lokale data som .daagbok.json. Hold filen og adgangssætningen adskilt og sikker.",
|
||||||
|
"backup_restore_title": "Gendan sikkerhedskopi",
|
||||||
|
"backup_restore_desc": "Gendanner en sikkerhedskopi til din nuværende konto - selv efter registrering af en ny konto.",
|
||||||
|
"backup_passphrase": "Backup-passphrase",
|
||||||
|
"backup_passphrase_placeholder": "Mindst 8 tegn",
|
||||||
|
"backup_passphrase_confirm": "Bekræft adgangssætning",
|
||||||
|
"backup_passphrase_short": "Backup-passphrasen skal være mindst 8 tegn lang.",
|
||||||
|
"backup_passphrase_mismatch": "Passphrases matcher ikke.",
|
||||||
|
"backup_wrong_passphrase": "Passphrase forkert eller backup beskadiget.",
|
||||||
|
"backup_export_btn": "Download backup",
|
||||||
|
"backup_exporting": "Sikkerhedskopien er oprettet...",
|
||||||
|
"backup_export_success": "Backup oprettet ({{count}} rejsedage).",
|
||||||
|
"backup_file_label": "Backup-fil (.daagbok.json)",
|
||||||
|
"backup_preview_btn": "Tjek indhold",
|
||||||
|
"backup_previewing": "Tjek...",
|
||||||
|
"backup_restore_btn": "Gendan",
|
||||||
|
"backup_restoring": "Vil blive restaureret...",
|
||||||
|
"backup_restore_success": "Logbog \"{{title}}\" er blevet gendannet.",
|
||||||
|
"backup_restore_cancelled": "Genopretning aflyst.",
|
||||||
|
"backup_invalid_json": "Filen er ikke en gyldig JSON-fil.",
|
||||||
|
"backup_invalid_format": "Ukendt eller forældet backup-format.",
|
||||||
|
"backup_not_owner": "Kun logbogens ejer kan oprette sikkerhedskopier.",
|
||||||
|
"backup_not_authenticated": "Log ind for at gendanne en sikkerhedskopi.",
|
||||||
|
"backup_id_conflict": "Der findes allerede en logbog med dette ID.",
|
||||||
|
"backup_overwrite_confirm": "Den eksisterende logbog med samme ID erstattes. Fortsætter du?",
|
||||||
|
"backup_new_id_confirm": "Importere backup'en som en ny logbog med et nyt ID?",
|
||||||
|
"backup_stat_entries": "{{count}} Rejsedage",
|
||||||
|
"backup_stat_photos": "{{count}} Fotos",
|
||||||
|
"backup_stat_crew": "{{count}} Besætningens poster",
|
||||||
|
"backup_stat_tracks": "{{count}} GPS-spor",
|
||||||
|
"backup_exported_at": "Eksporteret: {{date}}"
|
||||||
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"title": "Vigtige bemærkninger",
|
||||||
|
"intro": "Læs venligst følgende instruktioner, før du bruger Kapteins Daagbok.",
|
||||||
|
"e2e_title": "Ende-til-ende-kryptering",
|
||||||
|
"e2e_body": "Dine logbogsdata er krypteret fra ende til anden. Kun du - eller personer med din nøgle - kan læse indholdet. Kun krypterede data gemmes på serveren.",
|
||||||
|
"pwa_title": "Progressiv web-app (PWA)",
|
||||||
|
"pwa_body": "Kapteins Daagbok kører som en progressiv webapp i din browser og kan installeres på din enhed - på samme måde som en native app, men uden en app store.",
|
||||||
|
"storage_title": "Lokal lagring og synkronisering",
|
||||||
|
"storage_body": "Dine data gemmes lokalt på din enhed (IndexedDB). Ændringer synkroniseres med serveren, når en internetforbindelse er aktiv. Du kan arbejde videre uden forbindelse; synkroniseringen finder sted senere.",
|
||||||
|
"free_title": "Gratis og uden reklamer",
|
||||||
|
"free_body": "Kapteins Daagbok er gratis og indeholder ingen reklamer.",
|
||||||
|
"liability_title": "Ansvarsfraskrivelse",
|
||||||
|
"liability_body": "Brug af appen sker på egen risiko. Vi påtager os intet ansvar for skader, der opstår som følge af brugen af appen - herunder forkerte eller ufuldstændige logbogsindførsler, tab af data eller tekniske fejl.",
|
||||||
|
"warranty_title": "Ingen garanti",
|
||||||
|
"warranty_body": "Der gives ingen garanti for tjenestens funktion, korrekthed eller tilgængelighed. Driften kan til enhver tid blive afbrudt, begrænset eller annulleret.",
|
||||||
|
"copyright": "© 2026 KnorrLabs, Markus F.J. Busche",
|
||||||
|
"accept": "Accepter og fortsæt",
|
||||||
|
"close": "Luk",
|
||||||
|
"button_title": "Noter og ansvarsfraskrivelse"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"button_title": "Send feedback",
|
||||||
|
"title": "Feedback",
|
||||||
|
"intro": "Del fejl, ideer eller generel feedback. Din besked vil blive sendt til projektteamet via en sikker meddelelseskanal.",
|
||||||
|
"category_label": "Kategori",
|
||||||
|
"category_general": "Generelt",
|
||||||
|
"category_bug": "Rapporter fejl",
|
||||||
|
"category_feature": "Anmodning om funktion",
|
||||||
|
"contact_label": "E-mail (valgfrit)",
|
||||||
|
"contact_placeholder": "deine@email.beispiel",
|
||||||
|
"message_label": "Besked",
|
||||||
|
"message_placeholder": "Beskriv din feedback...",
|
||||||
|
"send": "Send",
|
||||||
|
"sending": "Vil blive sendt...",
|
||||||
|
"cancel": "Annuller",
|
||||||
|
"success": "Tusind tak skal du have! Din feedback er blevet sendt.",
|
||||||
|
"error_send": "Feedback kunne ikke sendes. Prøv venligst igen senere.",
|
||||||
|
"error_invalid_email": "Indtast venligst en gyldig e-mailadresse.",
|
||||||
|
"error_not_configured": "Feedback er ikke tilgængelig på denne server.",
|
||||||
|
"error_rate_limited": "For mange tilbagemeldinger på kort tid. Vent venligst et par minutter.",
|
||||||
|
"error_spam": "Denne besked kunne ikke sendes. Vær venlig at omformulere den."
|
||||||
|
},
|
||||||
|
"demo": {
|
||||||
|
"logbook_title": "Demo-logbog Østersøen",
|
||||||
|
"badge": "Demo",
|
||||||
|
"public_banner": "Skrivebeskyttet demo-visning",
|
||||||
|
"cta_register": "Opret konto",
|
||||||
|
"back_to_login": "Til registreringen"
|
||||||
|
},
|
||||||
|
"invitation": {
|
||||||
|
"error_invalid_key": "Invitationslinket er kryptografisk ugyldigt (nøglen er forkert).",
|
||||||
|
"error_missing_key": "Invitationslinket indeholder ikke en dekrypteringsnøgle (#key=...). Brug venligst det fulde link fra ejeren.",
|
||||||
|
"error_expired": "Denne invitation er udløbet (gyldig i 48 timer).",
|
||||||
|
"error_invalid_token": "Invitationstokenet er ugyldigt.",
|
||||||
|
"error_load_failed": "Invitationsoplysningerne kunne ikke indlæses.",
|
||||||
|
"error_incomplete_session": "Session ufuldstændig - log venligst ind igen (bruger-ID mangler).",
|
||||||
|
"error_accept_failed": "Tiltrædelse mislykkedes.",
|
||||||
|
"error_login_failed": "Passkey Login mislykkedes.",
|
||||||
|
"error_username_missing": "Brugernavnet kunne ikke bestemmes - log venligst ind igen.",
|
||||||
|
"error_register_failed": "Registrering mislykkedes.",
|
||||||
|
"loading_joining": "At slutte sig til...",
|
||||||
|
"loading_checking": "Invitation vil blive tjekket...",
|
||||||
|
"loading_unlocking": "Logbogen er låst op og synkroniseret...",
|
||||||
|
"loading_retrieving_key": "Download krypteringsnøgle...",
|
||||||
|
"error_title": "Fejl i invitation",
|
||||||
|
"back_to_start": "Tilbage til start",
|
||||||
|
"title": "Invitation til logbog",
|
||||||
|
"invited_by": "Invitation fra",
|
||||||
|
"vessel_logbook": "Skib / Logbog",
|
||||||
|
"signed_in_preparing": "Registreret som {{username}}. Tilslutning er ved at blive forberedt...",
|
||||||
|
"join_again": "Deltag igen",
|
||||||
|
"login_or_register_hint": "Log ind eller opret en konto for at deltage i logbogen.",
|
||||||
|
"or_sign_up": "ELLER REGISTRER DIG IGEN",
|
||||||
|
"register_crew_account": "Opret en ny crew-konto",
|
||||||
|
"username_label": "Brugernavn",
|
||||||
|
"create_passkey": "Opret Passkey.",
|
||||||
|
"switch_language_en": "Engelsk",
|
||||||
|
"switch_language_de": "Tysk"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"title": "Statistik",
|
||||||
|
"subtitle": "Overblik over ruter, forbrug og kørselstype",
|
||||||
|
"scope_label": "Evalueringsområde",
|
||||||
|
"scope_logbook": "Denne logbog",
|
||||||
|
"scope_account": "Alle logbøger",
|
||||||
|
"loading": "Statistikkerne er beregnet...",
|
||||||
|
"no_data": "Ingen rejsedage tilgængelige endnu.",
|
||||||
|
"total_distance": "Samlet afstand",
|
||||||
|
"travel_days": "Rejsedage",
|
||||||
|
"sail_distance": "Under sejl",
|
||||||
|
"motor_distance": "Kørsel med maskine",
|
||||||
|
"motor_hours_total": "Samlet antal maskintimer",
|
||||||
|
"daily_motor_hours": "Maskintimer pr. rejsedag",
|
||||||
|
"avg_motor_hours": "Ø maskintimer pr. rejsedag",
|
||||||
|
"unknown_propulsion": "Ukendt",
|
||||||
|
"fuel_total": "Brændstof i alt",
|
||||||
|
"water_total": "Vand i alt",
|
||||||
|
"daily_etmal": "Daglige tider",
|
||||||
|
"daily_consumption": "Dagligt forbrug",
|
||||||
|
"route_overview": "Rute",
|
||||||
|
"route_map_title": "Oversigt over ruter",
|
||||||
|
"propulsion_title": "Sejl vs. maskine",
|
||||||
|
"propulsion_hint": "Opdelingen er baseret på logbogshændelser pr. rejsedag, ikke på GPS-segmenter.",
|
||||||
|
"avg_distance": "Ø pr. rejsedag",
|
||||||
|
"avg_fuel": "Ø Brændstof",
|
||||||
|
"avg_water": "Ø Vand",
|
||||||
|
"fuel_per_nm": "Brændstof pr. sm",
|
||||||
|
"fuel_per_motor_hour": "Brændstof pr. maskintime",
|
||||||
|
"daily_fuel_per_motor_hour": "Brændstofforbrug pr. maskintime pr. rejsedag",
|
||||||
|
"fuel_legend": "Brændstof",
|
||||||
|
"water_legend": "Vand",
|
||||||
|
"unit_nm": "sm",
|
||||||
|
"unit_h": "h",
|
||||||
|
"unit_l": "L",
|
||||||
|
"day_label": "Dag {{day}}",
|
||||||
|
"account_logbooks": "Et overblik over logbøger",
|
||||||
|
"col_logbook": "Logbog"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"skip": "Spring turen over",
|
||||||
|
"back": "Tilbage",
|
||||||
|
"next": "Yderligere",
|
||||||
|
"finish": "Klar",
|
||||||
|
"progress": "Trin {{current}} fra {{total}}.",
|
||||||
|
"steps": {
|
||||||
|
"welcome": {
|
||||||
|
"title": "Velkommen om bord!",
|
||||||
|
"body": "Vi har lavet en demo-logbog med tre dages rejse i Kielerfjorden til dig. Du kan til enhver tid slette prøveposterne, hvis du vil starte din egen logbog. Denne korte rundvisning viser dig de vigtigste funktioner."
|
||||||
|
},
|
||||||
|
"welcome_public": {
|
||||||
|
"title": "Velkommen om bord!",
|
||||||
|
"body": "Udforsk vores demo-logbog med tre dages rejse i Kielerfjorden - uden en konto. Denne korte tur viser dig skibsdata, besætning og logbogsposter."
|
||||||
|
},
|
||||||
|
"nav_logs": {
|
||||||
|
"title": "Indlæg i logbogen",
|
||||||
|
"body": "Det er her, du styrer dine rejsedage - afgang, destination, vejr, brændstofniveau og GPS-spor."
|
||||||
|
},
|
||||||
|
"entry_list": {
|
||||||
|
"title": "Dine rejsedage",
|
||||||
|
"body": "Hvert kort repræsenterer en rejsedag. Tryk på en post for at se eller redigere detaljer."
|
||||||
|
},
|
||||||
|
"entry_open": {
|
||||||
|
"title": "Åben rejsedag",
|
||||||
|
"body": "Sådan ser et udfyldt logbogsnotat ud - med begivenheder, tankniveauer og meget mere."
|
||||||
|
},
|
||||||
|
"entry_track": {
|
||||||
|
"title": "GPS-spor",
|
||||||
|
"body": "Upload GPX-filer, eller se allerede gemte ruter på kortet - inklusive afstand og hastighed."
|
||||||
|
},
|
||||||
|
"nav_vessel": {
|
||||||
|
"title": "Skibsdata",
|
||||||
|
"body": "Indtast navn, dimensioner og tekniske data for din yacht - udfyld én gang, tilgængelig for alle rejsedage."
|
||||||
|
},
|
||||||
|
"nav_crew": {
|
||||||
|
"title": "Besætningsliste",
|
||||||
|
"body": "Administrer besætningsmedlemmer og tildel dem rejsedage senere."
|
||||||
|
},
|
||||||
|
"nav_stats": {
|
||||||
|
"title": "Statistik-dashboard",
|
||||||
|
"body": "Her kan du se kørselsafstande, brændstofforbrug, rutekort og kørselsandele - automatisk beregnet ud fra dine logbogsnotater."
|
||||||
|
},
|
||||||
|
"nav_feedback": {
|
||||||
|
"title": "Send feedback",
|
||||||
|
"body": "Du kan bruge denne formular til at sende fejl, ideer eller generel feedback direkte til projektteamet - også efter rundvisningen, når som helst ved hjælp af ikonet øverst til højre."
|
||||||
|
},
|
||||||
|
"nav_profile": {
|
||||||
|
"title": "Din brugerprofil",
|
||||||
|
"body": "Du kan få adgang til din personlige profil via skipperknappen øverst - uanset den aktuelle logbog."
|
||||||
|
},
|
||||||
|
"profile_preferences": {
|
||||||
|
"title": "Regnskab og præsentation",
|
||||||
|
"body": "Her kan du administrere din kontoidentitet, tema og lys/mørk tilstand. Du kan til enhver tid genstarte app-turen. Passkeys og sikkerhedsindstillinger findes længere nede i profilen."
|
||||||
|
},
|
||||||
|
"finish": {
|
||||||
|
"title": "Okay!",
|
||||||
|
"body": "Du vil blive ført direkte til statistikdashboardet. Du kan til enhver tid genstarte turen i din brugerprofil. Hav en god tur!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"title": "Kapteins Daagbok - Gratis digital yachtlogbog (reklamefri)",
|
||||||
|
"description": "Gratis, reklamefri digital yachtlogbog med end-to-end-kryptering og Passkey-login. Dokumenter sikkert rejsedage, GPS-spor, besætnings- og skibsdata - også offline som PWA.",
|
||||||
|
"keywords": "Yachtlogbog, skibslogbog, logbog om bord, sejlads, Passkey, E2E-kryptering, GPS-spor, maritim logbog, gratis, reklamefri, gratis, uden reklame",
|
||||||
|
"ogImageAlt": "Kapteins Daagbok Logo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,13 @@
|
|||||||
"beta": "Beta",
|
"beta": "Beta",
|
||||||
"beta_hint": "Beta-Version — Funktionen können sich noch ändern"
|
"beta_hint": "Beta-Version — Funktionen können sich noch ändern"
|
||||||
},
|
},
|
||||||
|
"languages": {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"da": "Dansk",
|
||||||
|
"sv": "Svenska",
|
||||||
|
"nb": "Norsk"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"unsaved_changes_title": "Ungespeicherte Änderungen",
|
"unsaved_changes_title": "Ungespeicherte Änderungen",
|
||||||
"unsaved_changes_message": "Du hast ungespeicherte Änderungen. Möchtest du die Seite wirklich verlassen? Deine Änderungen gehen verloren.",
|
"unsaved_changes_message": "Du hast ungespeicherte Änderungen. Möchtest du die Seite wirklich verlassen? Deine Änderungen gehen verloren.",
|
||||||
|
|||||||
@@ -6,6 +6,13 @@
|
|||||||
"beta": "Beta",
|
"beta": "Beta",
|
||||||
"beta_hint": "Beta release — features may still change"
|
"beta_hint": "Beta release — features may still change"
|
||||||
},
|
},
|
||||||
|
"languages": {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"da": "Dansk",
|
||||||
|
"sv": "Svenska",
|
||||||
|
"nb": "Norsk"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"unsaved_changes_title": "Unsaved changes",
|
"unsaved_changes_title": "Unsaved changes",
|
||||||
"unsaved_changes_message": "You have unsaved changes. Leave this page anyway? Your changes will be lost.",
|
"unsaved_changes_message": "You have unsaved changes. Leave this page anyway? Your changes will be lost.",
|
||||||
|
|||||||
@@ -0,0 +1,735 @@
|
|||||||
|
{
|
||||||
|
"translation": {
|
||||||
|
"app": {
|
||||||
|
"name": "Kapteins Daagbok",
|
||||||
|
"tagline": "Loggbok for private båter",
|
||||||
|
"beta": "Beta",
|
||||||
|
"beta_hint": "Betaversjon - funksjoner kan fortsatt endres"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"da": "Dansk",
|
||||||
|
"sv": "Svenska",
|
||||||
|
"nb": "Norsk"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"unsaved_changes_title": "Ikke-lagrede endringer",
|
||||||
|
"unsaved_changes_message": "Du har endringer som ikke er lagret. Vil du virkelig forlate siden? Endringene dine vil gå tapt.",
|
||||||
|
"unsaved_changes_leave": "Oppgivelse",
|
||||||
|
"unsaved_changes_stay": "Bli"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "Dashbord",
|
||||||
|
"vessel": "Skipsdata",
|
||||||
|
"crew": "Mannskapsliste",
|
||||||
|
"deviation": "Tabell over distraksjoner",
|
||||||
|
"logs": "Loggbokoppføringer",
|
||||||
|
"stats": "Statistikk",
|
||||||
|
"settings": "Innstillinger"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"welcome": "Velkommen til Kapteins Daagbok",
|
||||||
|
"tagline": "Din sikre, E2E-krypterte maritime loggbok.",
|
||||||
|
"register": "Registrer deg med Passkey",
|
||||||
|
"login": "Logg inn med Passkey",
|
||||||
|
"login_as": "Logg inn som {{name}}",
|
||||||
|
"quick_login": "Rask innlogging",
|
||||||
|
"forget_account": "Glemt konto på denne enheten",
|
||||||
|
"not_user": "Ikke {{name}}?",
|
||||||
|
"recovery_title": "Gjenopprettingsnøkkelen din",
|
||||||
|
"recovery_warning": "VIKTIG: Skriv ned disse 12 ordene. Hvis du mister Passkey og disse ordene, kan du ikke gjenopprette dataene dine.",
|
||||||
|
"confirm_recovery": "Jeg har skrevet ned ordene",
|
||||||
|
"status_logged_in": "Innlogget",
|
||||||
|
"status_logged_out": "Avlyst",
|
||||||
|
"copied": "Oppfattet!",
|
||||||
|
"copy_phrase": "Kopieringstast",
|
||||||
|
"enter_recovery": "Skriv inn gjenopprettingsnøkkel",
|
||||||
|
"recovery_fallback_warning": "Din Passkey har blitt autentisert, men enheten din støtter ikke maskinvarebasert nøkkelderivering. Skriv inn gjenopprettingsnøkkelen på 12 ord for å dekryptere loggboken.",
|
||||||
|
"recovery_placeholder": "Skriv inn gjenopprettingsnøkkelen din, som består av 12 ord atskilt med mellomrom...",
|
||||||
|
"back": "Tilbake",
|
||||||
|
"decrypting": "Dekryptering...",
|
||||||
|
"decrypt_logbook": "Dekryptere loggbok",
|
||||||
|
"error_incorrect_recovery": "Feil gjenopprettingsnøkkel. Dekryptering mislyktes.",
|
||||||
|
"error_decryption_failed": "Dekryptering mislyktes. Vennligst sjekk gjenopprettingsnøkkelen din.",
|
||||||
|
"or_register": "eller registrer deg",
|
||||||
|
"explore_demo": "Utforsk demoen uten konto",
|
||||||
|
"username_placeholder": "Brukernavn / Skippernavn",
|
||||||
|
"processing": "Behandling...",
|
||||||
|
"help": "Hjelp",
|
||||||
|
"setup_pin_title": "Konfigurer lokal PIN-kode (valgfritt)",
|
||||||
|
"setup_pin_warning": "Siden enheten din ikke støtter direkte Passkey-nøkkelavledning, må du ellers skrive inn 12-ordsnøkkelen hver gang du logger deg på denne enheten. Konfigurer en lokal PIN-kode for å unngå dette.",
|
||||||
|
"pin_placeholder": "E.G. 123456",
|
||||||
|
"pin_label": "Lokal PIN-kode (4-8 sifre)",
|
||||||
|
"save_pin": "Lagre PIN-kode og fortsett",
|
||||||
|
"skip_pin": "Hopp over og bruk gjenoppretting",
|
||||||
|
"enter_pin_title": "Dekrypter med PIN-kode",
|
||||||
|
"enter_pin_warning": "Skriv inn din lokale PIN-kode for å låse opp dekrypteringsnøkkelen på denne enheten.",
|
||||||
|
"enter_pin_placeholder": "Tast inn PIN-koden din...",
|
||||||
|
"decrypt_with_pin": "Dekryptere",
|
||||||
|
"use_recovery_instead": "Bruk gjenopprettingsnøkler i stedet",
|
||||||
|
"error_incorrect_pin": "Feil PIN-kode. Dekryptering mislyktes."
|
||||||
|
},
|
||||||
|
"pwa": {
|
||||||
|
"title": "Installer app",
|
||||||
|
"generic_benefit": "Installer Kapteins Daagbok på enheten din for raskere tilgang, frakoblet bruk og permanent lagring av data.",
|
||||||
|
"ios_instructions": "På iPad/iPhone: Legg til appen på startskjermen, slik at loggbokdataene dine forblir beskyttet og appen starter som en vanlig app.",
|
||||||
|
"ios_step_share": "Trykk på aksjesymbolet i Safari-linjen",
|
||||||
|
"ios_step_add": "Velg \"Gå til startskjermen\"",
|
||||||
|
"install_now": "Installer nå",
|
||||||
|
"installing": "Installasjon...",
|
||||||
|
"later": "Senere",
|
||||||
|
"never": "Ikke vis mer",
|
||||||
|
"platform_ios": "Installasjon via Safari",
|
||||||
|
"platform_android": "Installasjon via nettleseren",
|
||||||
|
"platform_desktop": "Installasjon som en desktop-app",
|
||||||
|
"settings_section": "Installasjon av app",
|
||||||
|
"update_title": "Oppdatering tilgjengelig",
|
||||||
|
"update_desc": "En ny versjon av Kapteins Daagbok er klar. Oppdater for å få med de siste endringene.",
|
||||||
|
"update_now": "Oppdater nå",
|
||||||
|
"update_reloading": "Laster..."
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"status_synced": "Synkronisert",
|
||||||
|
"status_syncing": "Synkroniser...",
|
||||||
|
"status_offline": "Frakoblet hurtigbuffer",
|
||||||
|
"status_unsynced": "Usynkroniserte endringer"
|
||||||
|
},
|
||||||
|
"vessel": {
|
||||||
|
"title": "Stamdata for skip",
|
||||||
|
"name": "Båtens navn",
|
||||||
|
"type": "Båttype",
|
||||||
|
"type_unset": "- ikke spesifisert -",
|
||||||
|
"type_sailing": "Seilbåt",
|
||||||
|
"type_motor": "Motorbåt",
|
||||||
|
"length_m": "Lengde (m)",
|
||||||
|
"draft_m": "Trekkraft (m)",
|
||||||
|
"air_draft_m": "Høyde (m)",
|
||||||
|
"invalid_metric": "Ugyldig tallverdi - angi meter som desimaltall (f.eks. 12,5).",
|
||||||
|
"port": "Hjemmehavn",
|
||||||
|
"owner": "Eier",
|
||||||
|
"charter": "Charterselskap",
|
||||||
|
"registration": "Nummerskilt/registreringsnummer",
|
||||||
|
"callsign": "Radiokallesignal",
|
||||||
|
"atis": "ATIS nr.",
|
||||||
|
"mmsi": "MMSI-nr.",
|
||||||
|
"save": "Lagre skipsdata",
|
||||||
|
"saving": "...vil bli reddet...",
|
||||||
|
"saved": "Skipsdata vellykket lagret!",
|
||||||
|
"loading": "Skipsdata er lastet inn...",
|
||||||
|
"sails_list": "Seil (eksisterende seil)",
|
||||||
|
"sails_help": "Skriv inn seilene som er tilgjengelige på båten din her (f.eks. storseil, genua, fokk).",
|
||||||
|
"add_sail": "Legg til seil",
|
||||||
|
"sail_name_placeholder": "z. f.eks. storseil",
|
||||||
|
"no_sails": "Ingen seil lagret.",
|
||||||
|
"photo_add": "Legg til bilde",
|
||||||
|
"photo_change": "Endre bilde",
|
||||||
|
"photo_delete": "Slett bilde",
|
||||||
|
"tanks_section": "Tanker (kapasitet)",
|
||||||
|
"tanks_help": "Valgfritt i liter - muliggjør glidebryter i tidsskriftet for kjente tankstørrelser.",
|
||||||
|
"freshwater_capacity_l": "Drikkevann (liter)",
|
||||||
|
"fuel_capacity_l": "Drivstoff (liter)",
|
||||||
|
"greywater_capacity_l": "Gråvann (liter)",
|
||||||
|
"invalid_tank_liters": "Ugyldig tallverdi - skriv inn liter som et tall (f.eks. 200)."
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "Loggbokdagbok",
|
||||||
|
"new_entry": "Ny reisedag",
|
||||||
|
"travel_details": "Detaljer om reisen",
|
||||||
|
"add_event": "Legg til ny loggbokoppføring",
|
||||||
|
"add_event_btn": "Legg til hendelse",
|
||||||
|
"edit_event": "Rediger hendelse",
|
||||||
|
"save_event_btn": "Lagre endring",
|
||||||
|
"cancel_event_edit": "Avbryt",
|
||||||
|
"delete_event": "Slett hendelse",
|
||||||
|
"sign_cleared_skipper_re_sign_title": "Skippers signatur fjernet",
|
||||||
|
"sign_cleared_skipper_re_sign": "Hendelsesloggen har blitt endret. Skipperens signatur er fjernet. Vennligst godkjenn på nytt.",
|
||||||
|
"date": "dato",
|
||||||
|
"day_of_travel": "Reisens dag / reisedag",
|
||||||
|
"departure": "Starthavn (reise fra)",
|
||||||
|
"destination": "Destinasjonsport (til)",
|
||||||
|
"route": "Reise fra/til",
|
||||||
|
"freshwater": "Ferskvann (liter)",
|
||||||
|
"fuel": "Drivstoff / Drivstoff (liter)",
|
||||||
|
"greywater": "Gråvann (liter)",
|
||||||
|
"greywater_level": "Fyllingsnivå",
|
||||||
|
"tank_slider_of_max": "{{current}} / {{max}} L",
|
||||||
|
"tank_capacity_tooltip": "Hvis tankkapasiteten (liter) er lagret i skipsdataene, kan du angi fyllingsnivåene her ved hjelp av glidebryteren.",
|
||||||
|
"morning": "Stå opp om morgenen",
|
||||||
|
"refilled": "Påfyllt",
|
||||||
|
"evening": "Kveldsstand",
|
||||||
|
"consumption": "Daglig forbruk",
|
||||||
|
"signatures": "Underskrifter / frigivelse",
|
||||||
|
"sign_skipper": "Skippers signatur",
|
||||||
|
"sign_crew": "Mannskapets signatur",
|
||||||
|
"sign_hint": "Signer med finger, penn eller mus",
|
||||||
|
"sign_clear": "Slett",
|
||||||
|
"sign_export_image": "[Signatur]",
|
||||||
|
"sign_with_passkey": "Utgivelse med Passkey",
|
||||||
|
"sign_passkey_signing": "Passkey er forespurt...",
|
||||||
|
"sign_passkey_signed": "Utgitt av {{username}}",
|
||||||
|
"sign_passkey_export": "Passkey: {{username}} ({{date}})",
|
||||||
|
"sign_attribution_export": "{{username}} ({{date}})",
|
||||||
|
"sign_passkey_clear": "Fjern Passkey utgivelse",
|
||||||
|
"sign_mode_passkey": "Passkey",
|
||||||
|
"sign_mode_classic": "Klassisk",
|
||||||
|
"sign_passkey_failed": "Passkey Utgivelsen mislyktes",
|
||||||
|
"sign_passkey_cancelled": "Passkey Utgivelse kansellert",
|
||||||
|
"sign_invalid": "Signaturen er ugyldig - innholdet har blitt endret",
|
||||||
|
"sign_badge_skipper": "Skipper",
|
||||||
|
"sign_badge_skipper_invalid": "Ugyldig",
|
||||||
|
"sign_badge_skipper_title_valid": "Skipper har gitt ut",
|
||||||
|
"sign_badge_skipper_title_invalid": "Skippersignaturen er ugyldig - innholdet har blitt endret",
|
||||||
|
"sign_classic_or_passkey": "Valgfritt: klassisk signatur eller Passkey utgivelse ovenfor",
|
||||||
|
"sign_crew_passkey_hint": "Besetningsmedlemmer med skrivetilgang kan frigjøre via Passkey.",
|
||||||
|
"sign_offline_hint": "Passkey-Godkjenning krever Internett - klassisk signatur mulig offline",
|
||||||
|
"sign_lock_notice": "Etter signering er det ikke mulig å gjøre endringer i loggbokoppføringen (unntatt bilder) uten at skipper og mannskap må signere på nytt.",
|
||||||
|
"sign_lock_active": "Denne oppføringen er signert. Endringer i loggboken (unntatt bilder) fjerner automatisk skipperens og mannskapets signaturer.",
|
||||||
|
"sign_lock_warning_title": "Bekreft signatur",
|
||||||
|
"sign_lock_warning": "Etter signering er det ikke lenger mulig å gjøre endringer i loggbokoppføringen (unntatt bilder) uten at skipper og mannskap må signere på nytt.\n\nØnsker du å fortsette?",
|
||||||
|
"sign_proceed": "Skilt",
|
||||||
|
"sign_cancel": "Avbryt",
|
||||||
|
"sign_cleared_re_sign_title": "Signaturer fjernet",
|
||||||
|
"sign_cleared_re_sign": "Loggbokoppføringen har blitt endret. Skipperens og mannskapets signaturer er fjernet. Vennligst signer på nytt.",
|
||||||
|
"no_entries": "Ingen loggbokoppføringer funnet for denne båten. Lag din første seilasdag!",
|
||||||
|
"back_to_list": "Tilbake til tidsskriftlisten",
|
||||||
|
"save": "Lagre loggbokside",
|
||||||
|
"saving": "...vil bli reddet...",
|
||||||
|
"saved": "Loggboksiden er vellykket lagret!",
|
||||||
|
"loading": "Tidsskriftet lastes inn...",
|
||||||
|
"delete_entry": "Slett tagg",
|
||||||
|
"delete_confirm": "Er du sikker på at du vil slette denne reisedagen permanent?",
|
||||||
|
"carry_over_tanks_title": "Overføre data fra dagen før?",
|
||||||
|
"carry_over_tanks_confirm": "Overta starthavn, ferskvann, drivstoff og gråvann fra startnivåene fra siste dag på turen?\n\nStart havn: {{departure}}\nFerskvann: {{fw}} L\nDrivstoff: {{fuel}} L\nGråvann: {{greywater}} L",
|
||||||
|
"carry_over_tanks_yes": "Ta over",
|
||||||
|
"carry_over_tanks_no": "Begynn med 0",
|
||||||
|
"event_title": "Kronologisk hendelseslogg",
|
||||||
|
"no_events": "Ingen arrangementer lagt inn for denne reisedagen ennå.",
|
||||||
|
"event_time": "Tid på døgnet",
|
||||||
|
"event_mgk": "MgK-kurs",
|
||||||
|
"event_rwk": "RwK-kurs",
|
||||||
|
"event_course_section": "Kurs",
|
||||||
|
"course_dial_hint": "Vri ringen eller angi grader",
|
||||||
|
"course_dial_step_label": "Trinnstørrelse",
|
||||||
|
"course_step_fine": "1°",
|
||||||
|
"course_step_medium": "5°",
|
||||||
|
"course_step_coarse": "10°",
|
||||||
|
"course_tab_mgk": "MgK",
|
||||||
|
"course_tab_rwk": "rwK",
|
||||||
|
"course_invalid": "Ugyldig kurs (0-360)",
|
||||||
|
"course_placeholder_degrees": "z. B. 180",
|
||||||
|
"course_placeholder_cardinal": "z. E.G. NW",
|
||||||
|
"compass_n": "N",
|
||||||
|
"compass_e": "O",
|
||||||
|
"compass_s": "S",
|
||||||
|
"compass_w": "W",
|
||||||
|
"wind_mode_cardinal": "Kardinal",
|
||||||
|
"wind_mode_degrees": "Som grad",
|
||||||
|
"event_wind_direction": "Vindretning",
|
||||||
|
"event_wind_strength": "Vindstyrke",
|
||||||
|
"event_sea_state": "Havets tilstand",
|
||||||
|
"event_weather": "Været",
|
||||||
|
"event_log": "Logg (sm)",
|
||||||
|
"event_gps": "GPS-posisjon",
|
||||||
|
"event_location": "Sted / havn",
|
||||||
|
"event_location_placeholder": "z. f.eks. Kiel",
|
||||||
|
"event_remarks": "Merknader / hendelser",
|
||||||
|
"gps_btn": "Hent GPS-koordinater",
|
||||||
|
"weather_btn": "OpenWeatherMap Ring opp været",
|
||||||
|
"event_wind_pressure": "Lufttrykk (hPa)",
|
||||||
|
"event_heel": "Helning (°)",
|
||||||
|
"event_sails": "Seilhåndtering / motor",
|
||||||
|
"motor_propulsion": "Maskinreise",
|
||||||
|
"sails_picker_show_more": "Vis alle seil",
|
||||||
|
"sails_picker_show_less": "Vis mindre",
|
||||||
|
"motor_hours": "Maskintimer (totalt)",
|
||||||
|
"fuel_per_motor_hour": "Forbruk per maskintime",
|
||||||
|
"event_distance": "Avstand (sm)",
|
||||||
|
"export_csv": "Last ned CSV",
|
||||||
|
"share_csv": "CSV andel",
|
||||||
|
"export_pdf": "Last ned PDF",
|
||||||
|
"exporting_pdf": "PDF genereres...",
|
||||||
|
"photos_title": "Bildevedlegg (E2E-kryptert)",
|
||||||
|
"photo_caption_label": "Fotobeskrivelse/etikett (valgfritt)",
|
||||||
|
"photo_caption_placeholder": "f.eks. å sette seil nær innseilingen til havnen",
|
||||||
|
"photo_btn": "Ta bilde / last opp",
|
||||||
|
"photo_processing": "...blir behandlet...",
|
||||||
|
"no_photos": "Ingen bilder knyttet til denne reisedagen ennå.",
|
||||||
|
"photo_delete_confirm": "Er du sikker på at du vil slette dette bildet permanent?",
|
||||||
|
"confirm_yes": "Ja",
|
||||||
|
"confirm_no": "Nei",
|
||||||
|
"track_upload_title": "GPS-sporing (fil)",
|
||||||
|
"track_upload_points": "Poeng",
|
||||||
|
"gps_tracking_btn_gpx": "Last ned sporfil",
|
||||||
|
"gps_track_upload_help": "Dra en GPX-, KML- eller GeoJSON-fil hit, eller klikk for å velge",
|
||||||
|
"gps_track_upload_btn": "Last opp GPS-spor",
|
||||||
|
"gps_track_delete": "Slett sporfil",
|
||||||
|
"gps_track_delete_confirm": "Er du sikker på at du vil slette denne sporfilen permanent?",
|
||||||
|
"track_distance": "GPS-rute (sm)",
|
||||||
|
"track_speed_max": "Maks. Hastighet (kn)",
|
||||||
|
"track_speed_avg": "Ø Hastighet (kn)",
|
||||||
|
"track_map_title": "GPS-spor på OpenSeaMap",
|
||||||
|
"track_map_start": "Start",
|
||||||
|
"track_map_end": "Mål",
|
||||||
|
"track_map_speed_slow": "langsomt",
|
||||||
|
"track_map_speed_fast": "raskt",
|
||||||
|
"track_map_error": "Kartet kunne ikke lastes inn.",
|
||||||
|
"exporting": "Eksport...",
|
||||||
|
"share_unsupported": "Deling støttes ikke på denne enheten. Filen har blitt lastet ned i stedet.",
|
||||||
|
"invite_crew": "Inviter mannskapet",
|
||||||
|
"invite_link_copied": "Invitasjonslenke kopiert til utklippstavlen!",
|
||||||
|
"invite_link_desc": "Del denne lenken med besetningsmedlemmene for å gi dem skrivetilgang til loggboken.",
|
||||||
|
"collaborators_list": "Medlemmer / Besetning",
|
||||||
|
"revoke": "Fjern",
|
||||||
|
"revoke_confirm": "Er du sikker på at du vil oppheve dette besetningsmedlemmets tilgang?",
|
||||||
|
"invite_role": "Rolle",
|
||||||
|
"invite_expires": "Lenken er gyldig i 48 timer"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Loggbøkene dine",
|
||||||
|
"subtitle": "Velg en loggbok eller opprett en ny for å administrere reisene dine.",
|
||||||
|
"create_btn": "Opprett loggbok",
|
||||||
|
"new_logbook_placeholder": "Navn på loggboken eller båten",
|
||||||
|
"logout": "Logg ut",
|
||||||
|
"logged_in_as": "Innlogget som {{name}}",
|
||||||
|
"delete_confirm": "Er du sikker på at du vil slette denne loggboken permanent? Alle lokale data og serverkopier vil bli ødelagt.\n\nTips: Lag en sikkerhetskopi (.daagbok.json) på forhånd under Innstillinger → Sikkerhetskopiering og gjenoppretting hvis du ønsker å beholde dataene senere.",
|
||||||
|
"no_logbooks": "Ingen loggbøker funnet. Opprett din første loggbok for å komme i gang!",
|
||||||
|
"loading": "Loggbøker er lastet...",
|
||||||
|
"status_synced": "Synkronisert",
|
||||||
|
"status_local": "Kun lokal hurtigbuffer",
|
||||||
|
"delete_btn": "Slett loggbok",
|
||||||
|
"section_owned": "Loggbøkene mine",
|
||||||
|
"section_shared": "Felles loggbøker",
|
||||||
|
"section_shared_hint": "Du er invitert som besetningsmedlem. Skipperprofil og innstillinger tilhører eieren.",
|
||||||
|
"role_owner": "Egen loggbok",
|
||||||
|
"role_owner_hint": "Du er eier og skipper av denne loggboken",
|
||||||
|
"role_crew": "Tilgang for mannskapet",
|
||||||
|
"role_crew_hint": "Loggbok med invitasjon - du kan jobbe som mannskap og signere den",
|
||||||
|
"role_read": "Bare les",
|
||||||
|
"role_read_hint": "Delt loggbok - kun visning, ingen redigering",
|
||||||
|
"open_profile": "Åpne profilen til {{name}}",
|
||||||
|
"edit_title": "Endre navn på loggbok",
|
||||||
|
"edit_placeholder": "Nytt navn på loggboken",
|
||||||
|
"edit_success": "Loggboken har fått nytt navn",
|
||||||
|
"edit_btn": "Gi nytt navn",
|
||||||
|
"filter_label": "Filtrer loggbøker",
|
||||||
|
"filter_placeholder": "Navn, årstall eller dato ...",
|
||||||
|
"filter_clear": "Tilbakestill filter",
|
||||||
|
"filter_results": "{{count}} Treff",
|
||||||
|
"filter_no_results": "Ingen loggbøker samsvarer med søket ditt. Prøv et annet navn eller et annet år.",
|
||||||
|
"sort_label": "Sortere",
|
||||||
|
"sort_by_label": "Sorter etter",
|
||||||
|
"sort_by_name": "Navn",
|
||||||
|
"sort_by_date": "dato",
|
||||||
|
"sort_dir_label": "Sekvens",
|
||||||
|
"sort_asc": "Stigende",
|
||||||
|
"sort_desc": "Synkende",
|
||||||
|
"sort_name_asc": "Navn A til Å",
|
||||||
|
"sort_name_desc": "Navn Z til A",
|
||||||
|
"sort_date_asc": "Eldst først",
|
||||||
|
"sort_date_desc": "Nyeste først"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Brukerprofil",
|
||||||
|
"subtitle": "Regnskap, Passkeys og statistikk for {{name}}.",
|
||||||
|
"back": "Tilbake til dashbordet",
|
||||||
|
"loading": "Profilen lastes inn...",
|
||||||
|
"load_error": "Profilen kunne ikke lastes inn.",
|
||||||
|
"copy_failed": "Kopien mislyktes.",
|
||||||
|
"processing": "Blir behandlet...",
|
||||||
|
"identity_title": "Kontoidentitet",
|
||||||
|
"username": "Brukernavn",
|
||||||
|
"user_id": "Bruker-ID",
|
||||||
|
"copy_user_id": "Kopier bruker-ID",
|
||||||
|
"account_since": "Konto siden",
|
||||||
|
"prf_status": "Passkey nøkkelavledning (PRF)",
|
||||||
|
"prf_active": "Aktiv",
|
||||||
|
"prf_inactive": "Ikke satt opp",
|
||||||
|
"passkeys_title": "Passkeys",
|
||||||
|
"passkeys_desc": "Registrer en separat Passkey på hver enhet. Dette gjør at du kan logge på selv etter at du har byttet plattform.",
|
||||||
|
"passkeys_empty": "Ingen Passkeyer funnet.",
|
||||||
|
"add_passkey_btn": "Legg til ny Passkey",
|
||||||
|
"add_passkey_success": "Passkey vellykket lagt til.",
|
||||||
|
"add_passkey_failed": "Passkey kunne ikke legges til.",
|
||||||
|
"remove_passkey_btn": "Fjern Passkey",
|
||||||
|
"remove_passkey_last_title": "Sist Passkey",
|
||||||
|
"remove_passkey_last_desc": "Den eneste Passkey kan ikke fjernes uten at du mister tilgangen til kontoen din. For å slette kontoen helt, bruk faresonen nederst på denne siden.",
|
||||||
|
"remove_passkey_failed": "Passkey kunne ikke fjernes.",
|
||||||
|
"remove_passkey_confirm_title": "Fjern Passkey?",
|
||||||
|
"remove_passkey_confirm_desc": "Denne enheten kan da ikke lenger logge inn med denne Passkey.",
|
||||||
|
"remove_passkey_confirm_yes": "Fjern",
|
||||||
|
"remove_passkey_confirm_no": "Avbryt",
|
||||||
|
"pin_title": "Lokal PIN-kode",
|
||||||
|
"pin_status": "Status",
|
||||||
|
"pin_active": "Aktiv på denne enheten",
|
||||||
|
"pin_inactive": "Ikke satt opp",
|
||||||
|
"pin_confirm_label": "Bekreft PIN-kode",
|
||||||
|
"pin_confirm_placeholder": "Tast inn PIN-koden på nytt",
|
||||||
|
"pin_set_btn": "Konfigurer PIN-kode",
|
||||||
|
"pin_change_btn": "Endre PIN-kode",
|
||||||
|
"pin_remove_btn": "Fjern PIN-kode",
|
||||||
|
"pin_saved": "PIN-kode lagret.",
|
||||||
|
"pin_save_failed": "PIN-koden kunne ikke lagres.",
|
||||||
|
"pin_mismatch": "PIN-kodene stemmer ikke overens.",
|
||||||
|
"pin_length_error": "PIN-koden må bestå av minst 4 tegn.",
|
||||||
|
"pin_no_session": "Økten er utløpt - vennligst registrer deg på nytt.",
|
||||||
|
"remove_pin_confirm_title": "Fjerne PIN-kode?",
|
||||||
|
"remove_pin_confirm_desc": "Du må logge på igjen på denne enheten med Passkey eller gjenopprettingsnøkkel.",
|
||||||
|
"remove_pin_confirm_yes": "Fjern PIN-kode",
|
||||||
|
"remove_pin_confirm_no": "Avbryt",
|
||||||
|
"security_title": "Sjekkliste for sikkerhet",
|
||||||
|
"security_desc": "Oversikt over de viktigste beskyttelsesmekanismene for kontoen din.",
|
||||||
|
"security_passkeys_ok": "Minst én Passkey registrert",
|
||||||
|
"security_passkeys_missing": "Nei Passkey registrert",
|
||||||
|
"security_prf_ok": "PRF-nøkkelavledning aktiv",
|
||||||
|
"security_prf_missing": "PRF ikke satt opp",
|
||||||
|
"security_pin_ok": "Lokal PIN-kode på denne enheten",
|
||||||
|
"security_pin_missing": "Ingen lokal PIN-kode",
|
||||||
|
"security_recovery_ok": "Oppsett av gjenopprettingsnøkkel",
|
||||||
|
"security_recovery_hint": "De 12 ordene ble vist under registreringen. Oppbevar dem frakoblet og adskilt fra enheten. Du kan opprette en ny nøkkel nedenfor - den gamle blir da ugyldig.",
|
||||||
|
"recovery_rotate_btn": "Opprett en ny gjenopprettingsnøkkel",
|
||||||
|
"recovery_rotate_confirm_title": "Opprette en ny gjenopprettingsnøkkel?",
|
||||||
|
"recovery_rotate_confirm_desc": "Den forrige 12-ordsnøkkelen blir ugyldig umiddelbart. Sørg for at du oppbevarer den nye nøkkelen trygt før du fortsetter.",
|
||||||
|
"recovery_rotate_confirm_yes": "Opprett ny nøkkel",
|
||||||
|
"recovery_rotate_confirm_no": "Avbryt",
|
||||||
|
"recovery_rotate_new_warning": "VIKTIG: Skriv ned disse 12 ordene og oppbevar dem offline. Den forrige gjenopprettingsnøkkelen er nå ugyldig.",
|
||||||
|
"recovery_rotate_failed": "Gjenopprettingsnøkkel kunne ikke opprettes.",
|
||||||
|
"recovery_rotate_no_session": "Krypteringsøkten er utløpt - logg ut og logg inn igjen, og prøv deretter på nytt.",
|
||||||
|
"device_title": "Denne enheten",
|
||||||
|
"device_desc": "Lokal hurtigbuffer, synkroniseringsstatus og hurtigpålogging i denne nettleseren.",
|
||||||
|
"device_sync_pending": "{{count}} ventende synkroniseringsoppføringer",
|
||||||
|
"device_sync_ok": "Alle lokale endringer synkroniseres",
|
||||||
|
"device_remembered": "Konto for hurtiginnlogging lagret på denne enheten",
|
||||||
|
"device_not_remembered": "Kontoen er ikke i hurtiginnloggingslisten",
|
||||||
|
"device_forget_btn": "Glemt konto på denne enheten",
|
||||||
|
"device_forget_confirm_title": "Fjerne hurtiginnlogging?",
|
||||||
|
"device_forget_confirm_desc": "Kontoen forsvinner fra hurtiginnloggingslisten på denne enheten. Økten og de lokale loggbøkene beholdes.",
|
||||||
|
"device_forget_confirm_yes": "Fjern",
|
||||||
|
"device_forget_confirm_no": "Avbryt",
|
||||||
|
"passkey_label": "Navn på ny Passkey (valgfritt)",
|
||||||
|
"passkey_label_placeholder": "z. f.eks. MacBook, iPhone",
|
||||||
|
"passkey_rename_btn": "Lagre navn",
|
||||||
|
"passkey_rename_success": "Passkey navn lagret.",
|
||||||
|
"passkey_rename_failed": "Passkey-Navn kunne ikke lagres.",
|
||||||
|
"passkey_unnamed": "Uten tittel Passkey",
|
||||||
|
"stats_title": "Statistikk",
|
||||||
|
"stats_subtitle": "Om alle loggbøkene dine på denne enheten",
|
||||||
|
"stats_logbooks": "Loggbøker",
|
||||||
|
"stats_account_since": "Konto siden",
|
||||||
|
"stats_shared_logbooks": "Felles loggbøker",
|
||||||
|
"appearance_title": "App og visualisering",
|
||||||
|
"appearance_desc": "Designet og fargevalget gjelder for hele appen på denne enheten.",
|
||||||
|
"theme_label": "Appens designstil",
|
||||||
|
"theme_auto": "Automatisk (OS-deteksjon)",
|
||||||
|
"theme_ocean": "Ocean (glassmorfisme)",
|
||||||
|
"theme_material": "Materiale (Android)",
|
||||||
|
"theme_cupertino": "Cupertino (iOS)",
|
||||||
|
"color_scheme_label": "Lys eller mørk modus",
|
||||||
|
"color_scheme_auto": "Automatisk (system)",
|
||||||
|
"color_scheme_light": "Lys",
|
||||||
|
"color_scheme_dark": "Mørk",
|
||||||
|
"integrations_title": "Integrasjoner",
|
||||||
|
"owm_key": "OpenWeatherMap API-nøkkel",
|
||||||
|
"owm_help": "Valgfritt: egen OpenWeatherMap API-nøkkel. Hvis ingen oppføring er gjort, brukes serverside-nøkkelen fra operatørkonfigurasjonen.",
|
||||||
|
"prefs_save": "Spar",
|
||||||
|
"prefs_saving": "...vil bli reddet...",
|
||||||
|
"prefs_saved": "Reddet",
|
||||||
|
"tour_title": "App-tur",
|
||||||
|
"tour_desc": "La deg veilede gjennom de viktigste områdene i appen på nytt.",
|
||||||
|
"tour_restart": "Start turen på nytt",
|
||||||
|
"push_title": "Push-varsler",
|
||||||
|
"push_desc": "Som loggbokseier vil du bli varslet når inviterte besetningsmedlemmer synkroniserer endringer. Ingen innhold overføres i ren tekst.",
|
||||||
|
"push_enable": "Gi oss beskjed om endringer i mannskapet",
|
||||||
|
"push_active": "Push-varsler er aktive på denne enheten.",
|
||||||
|
"push_unsupported": "Push-varsler støttes ikke i denne nettleseren.",
|
||||||
|
"push_denied_hint": "Varsler er blokkert. Tillat dem i innstillingene i nettleseren eller på enheten.",
|
||||||
|
"push_ios_install_hint": "På iPhone/iPad: Legg til app på startskjermen (iOS 16.4+) for å bruke push.",
|
||||||
|
"push_error": "Push-varsler kunne ikke aktiveres."
|
||||||
|
},
|
||||||
|
"crew": {
|
||||||
|
"title": "Skipper- og mannskapsprofiler",
|
||||||
|
"skipper_section": "Skipperprofil",
|
||||||
|
"skipper_read_only_hint": "Skipperprofilen kan bare redigeres av eieren av loggboken.",
|
||||||
|
"crew_section": "Mannskapsliste",
|
||||||
|
"add_crew": "Legg til besetningsmedlem",
|
||||||
|
"edit_crew": "Rediger besetningsmedlem",
|
||||||
|
"no_crew": "Ingen besetningsmedlemmer er lagt til ennå.",
|
||||||
|
"max_crew": "Maksimalt antall på 5 besetningsmedlemmer er nådd.",
|
||||||
|
"name": "Navn",
|
||||||
|
"address": "adresse",
|
||||||
|
"birthdate": "Bursdag",
|
||||||
|
"phone": "Telefonnummer",
|
||||||
|
"nationality": "Nasjonalitet",
|
||||||
|
"passport": "Pass-/ID-nummer",
|
||||||
|
"bloodtype": "Blodgruppe",
|
||||||
|
"allergies": "Allergier",
|
||||||
|
"diseases": "Eksisterende tilstander/sykdommer",
|
||||||
|
"save": "Lagre skipperdata",
|
||||||
|
"save_member": "Lagre medlem",
|
||||||
|
"saved": "Skipperprofilen er vellykket lagret!",
|
||||||
|
"loading": "Mannskapsfilene er lastet inn...",
|
||||||
|
"delete_confirm": "Er du sikker på at du vil fjerne dette besetningsmedlemmet?"
|
||||||
|
},
|
||||||
|
"deviation": {
|
||||||
|
"title": "Tabell over kompassavvik",
|
||||||
|
"subtitle": "Angi den magnetiske kompassavbøyningen (avbøyning) for kurser (MgK) fra 000° til 360° i trinn på 10°.",
|
||||||
|
"heading": "MgK",
|
||||||
|
"deviation": "Distraksjon",
|
||||||
|
"save": "Lagre kalibreringsrutenettet",
|
||||||
|
"saving": "...vil bli reddet...",
|
||||||
|
"saved": "Kalibreringsrutenettet er vellykket lagret!",
|
||||||
|
"loading": "Kalibreringstabellen er lastet inn..."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Innstillinger for loggbok",
|
||||||
|
"subtitle": "Del, sikkerhetskopier og samarbeid for denne loggboken.",
|
||||||
|
"select_logbook_hint": "Velg en loggbok for å redigere innstillingene.",
|
||||||
|
"no_key": "Ingen OpenWeatherMap API-nøkkel tilgjengelig. Lagre din egen nøkkel i brukerprofilen, eller kontakt operatøren.",
|
||||||
|
"weather_success": "Værdata vellykket hentet!",
|
||||||
|
"weather_error": "Henting av værdata mislyktes. Kontroller API-nøkkelen og tilkoblingen.",
|
||||||
|
"weather_date_mismatch": "Værdata kan bare hentes ut for i dag ({{today}}). Denne loggbokoppføringen er datert {{date}}.",
|
||||||
|
"gps_error": "Vennligst skriv inn en posisjon eller finn GPS-koordinatene.",
|
||||||
|
"share_title": "Del loggbok (skrivebeskyttet)",
|
||||||
|
"share_desc": "Aktiver dette alternativet for å opprette en offentlig, skrivebeskyttet lenke. Alle som har denne lenken, kan se seilasene, båtprofilene og mannskapet ditt. Krypteringsnøklene overføres aldri til serveren (de forblir i hash-delen av URL-en).",
|
||||||
|
"share_privacy_warning": "Anbefaling: Del denne lenken kun privat (f.eks. via e-post eller messenger), ikke på sosiale medier.",
|
||||||
|
"share_enable": "Aktiver offentlig lenke",
|
||||||
|
"share_copied": "Linken er kopiert!",
|
||||||
|
"share_copy_btn": "Kopier lenke",
|
||||||
|
"danger_zone_title": "Faresone",
|
||||||
|
"danger_zone_desc": "Hvis du sletter kontoen din, slettes alle dine Passkeys, loggbøker, skipsdata, mannskapsprofiler, reiseoppføringer og E2E-nøkler ugjenkallelig. Denne handlingen kan ikke angres.",
|
||||||
|
"delete_account_btn": "Slett konto ugjenkallelig",
|
||||||
|
"delete_account_confirm_title": "Slett konto?",
|
||||||
|
"delete_account_confirm_desc": "Er du helt sikker på at du vil slette kontoen din og alle tilknyttede loggbøker og E2E-krypterte data ugjenkallelig?",
|
||||||
|
"delete_account_confirm_yes": "Ja, slett konto og alle data",
|
||||||
|
"delete_account_confirm_no": "Avbryt",
|
||||||
|
"delete_account_failed": "Kontoen kunne ikke slettes. Vennligst prøv igjen.",
|
||||||
|
"delete_backup_hint": "Tips: Lag sikkerhetskopier av loggbøkene dine (.daagbok.json) i innstillingene for hver loggbok før du sletter dem.",
|
||||||
|
"deleting_account": "Kontoen vil bli slettet...",
|
||||||
|
"invite_push_prompt_title": "Aktivere push-varsler?",
|
||||||
|
"invite_push_prompt_message": "Så snart inviterte besetningsmedlemmer synkroniserer endringer, kan du bli informert via push. Ingen loggbokinnhold sendes i ren tekst.",
|
||||||
|
"invite_push_prompt_ios_message": "Så snart besetningsmedlemmene synkroniserer endringer, kan du bli informert via push. På iPhone/iPad: Legg til appen på startskjermen (iOS 16.4+), og aktiver deretter push i brukerprofilen.",
|
||||||
|
"invite_push_prompt_enable": "Aktiver nå",
|
||||||
|
"invite_push_prompt_later": "Senere",
|
||||||
|
"invite_push_prompt_success": "Push-varsler er aktive på denne enheten.",
|
||||||
|
"backup_title": "Sikkerhetskopiering og gjenoppretting",
|
||||||
|
"backup_desc": "Fullstendig kryptert sikkerhetskopi av denne loggboken (oppføringer, bilder, GPS-spor, mannskap, skip). Beskyttet med sikkerhetskopieringspassord - for gjenoppretting til denne eller en ny konto.",
|
||||||
|
"backup_export_title": "Opprett sikkerhetskopi",
|
||||||
|
"backup_export_desc": "Laster ned alle lokale data som .daagbok.json. Hold filen og passordfrasen adskilt og sikker.",
|
||||||
|
"backup_restore_title": "Gjenopprett sikkerhetskopi",
|
||||||
|
"backup_restore_desc": "Gjenoppretter en sikkerhetskopi til din nåværende konto - selv etter at du har registrert en ny konto.",
|
||||||
|
"backup_passphrase": "Passord for sikkerhetskopiering",
|
||||||
|
"backup_passphrase_placeholder": "Minst 8 tegn",
|
||||||
|
"backup_passphrase_confirm": "Bekreft passordfrasen",
|
||||||
|
"backup_passphrase_short": "Passordfrasen for sikkerhetskopiering må være på minst 8 tegn.",
|
||||||
|
"backup_passphrase_mismatch": "Passordfraser stemmer ikke overens.",
|
||||||
|
"backup_wrong_passphrase": "Passordfrasen er feil eller sikkerhetskopien er ødelagt.",
|
||||||
|
"backup_export_btn": "Last ned sikkerhetskopi",
|
||||||
|
"backup_exporting": "Sikkerhetskopien er opprettet...",
|
||||||
|
"backup_export_success": "Sikkerhetskopi opprettet ({{count}} reisedager).",
|
||||||
|
"backup_file_label": "Sikkerhetskopifil (.daagbok.json)",
|
||||||
|
"backup_preview_btn": "Sjekk innhold",
|
||||||
|
"backup_previewing": "Sjekk...",
|
||||||
|
"backup_restore_btn": "Gjenopprett",
|
||||||
|
"backup_restoring": "Vil bli restaurert...",
|
||||||
|
"backup_restore_success": "Loggbok \"{{title}}\" er gjenopprettet.",
|
||||||
|
"backup_restore_cancelled": "Gjenoppretting avlyst.",
|
||||||
|
"backup_invalid_json": "Filen er ikke en gyldig JSON-fil.",
|
||||||
|
"backup_invalid_format": "Ukjent eller utdatert sikkerhetskopiformat.",
|
||||||
|
"backup_not_owner": "Det er bare eieren av loggboken som kan opprette sikkerhetskopier.",
|
||||||
|
"backup_not_authenticated": "Vennligst logg inn for å gjenopprette en sikkerhetskopi.",
|
||||||
|
"backup_id_conflict": "Det finnes allerede en loggbok med denne ID-en.",
|
||||||
|
"backup_overwrite_confirm": "Den eksisterende loggboken med samme ID erstattes. Fortsette?",
|
||||||
|
"backup_new_id_confirm": "Importere sikkerhetskopien som en ny loggbok med ny ID?",
|
||||||
|
"backup_stat_entries": "{{count}} Reisedager",
|
||||||
|
"backup_stat_photos": "{{count}} Bilder",
|
||||||
|
"backup_stat_crew": "{{count}} Mannskapsposter",
|
||||||
|
"backup_stat_tracks": "{{count}} GPS-spor",
|
||||||
|
"backup_exported_at": "Eksportert: {{date}}"
|
||||||
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"title": "Viktige merknader",
|
||||||
|
"intro": "Vennligst les følgende instruksjoner før du bruker Kapteins Daagbok.",
|
||||||
|
"e2e_title": "Ende-til-ende-kryptering",
|
||||||
|
"e2e_body": "Loggbokdataene dine er kryptert fra ende til ende. Bare du - eller personer med din nøkkel - kan lese innholdet. Kun krypterte data lagres på serveren.",
|
||||||
|
"pwa_title": "Progressiv webapp (PWA)",
|
||||||
|
"pwa_body": "Kapteins Daagbok kjører som en progressiv webapp i nettleseren din og kan installeres på enheten din - på samme måte som en native-app, men uten en appbutikk.",
|
||||||
|
"storage_title": "Lokal lagring og synkronisering",
|
||||||
|
"storage_body": "Dataene dine lagres lokalt på enheten din (IndexedDB). Endringer synkroniseres med serveren når en Internett-tilkobling er aktiv. Du kan fortsette å jobbe uten tilkobling, synkroniseringen skjer senere.",
|
||||||
|
"free_title": "Gratis og reklamefri",
|
||||||
|
"free_body": "Kapteins Daagbok er gratis og inneholder ingen reklame.",
|
||||||
|
"liability_title": "Ansvarsfraskrivelse",
|
||||||
|
"liability_body": "Bruk av appen skjer på eget ansvar. Vi fraskriver oss ethvert ansvar for skader som oppstår som følge av bruk av appen - inkludert feilaktige eller ufullstendige loggbokoppføringer, tap av data eller tekniske feil.",
|
||||||
|
"warranty_title": "Ingen garanti",
|
||||||
|
"warranty_body": "Det gis ingen garanti for tjenestens funksjon, korrekthet eller tilgjengelighet. Driften kan når som helst bli avbrutt, begrenset eller kansellert.",
|
||||||
|
"copyright": "© 2026 KnorrLabs, Markus F.J. Busche",
|
||||||
|
"accept": "Godta og fortsett",
|
||||||
|
"close": "Lukk",
|
||||||
|
"button_title": "Merknader og ansvarsfraskrivelse"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"button_title": "Send tilbakemelding",
|
||||||
|
"title": "Tilbakemeldinger",
|
||||||
|
"intro": "Del feil, ideer eller generelle tilbakemeldinger. Meldingen din vil bli sendt til prosjektteamet via en sikker varslingskanal.",
|
||||||
|
"category_label": "Kategori",
|
||||||
|
"category_general": "Generelt",
|
||||||
|
"category_bug": "Rapporter feil",
|
||||||
|
"category_feature": "Forespørsel om funksjonalitet",
|
||||||
|
"contact_label": "E-post (valgfritt)",
|
||||||
|
"contact_placeholder": "deine@email.beispiel",
|
||||||
|
"message_label": "Melding",
|
||||||
|
"message_placeholder": "Beskriv tilbakemeldingene dine...",
|
||||||
|
"send": "Send",
|
||||||
|
"sending": "Vil bli sendt...",
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"success": "Tusen takk skal du ha! Tilbakemeldingen din er sendt.",
|
||||||
|
"error_send": "Tilbakemelding kunne ikke sendes. Vennligst prøv igjen senere.",
|
||||||
|
"error_invalid_email": "Vennligst skriv inn en gyldig e-postadresse.",
|
||||||
|
"error_not_configured": "Tilbakemelding er ikke tilgjengelig på denne serveren.",
|
||||||
|
"error_rate_limited": "For mange tilbakemeldinger på kort tid. Vennligst vent noen minutter.",
|
||||||
|
"error_spam": "Denne meldingen kunne ikke sendes. Vennligst omformuler den."
|
||||||
|
},
|
||||||
|
"demo": {
|
||||||
|
"logbook_title": "Demologgbok Østersjøen",
|
||||||
|
"badge": "Demo",
|
||||||
|
"public_banner": "Skrivebeskyttet demovisning",
|
||||||
|
"cta_register": "Opprett konto",
|
||||||
|
"back_to_login": "Til registreringen"
|
||||||
|
},
|
||||||
|
"invitation": {
|
||||||
|
"error_invalid_key": "Invitasjonslenken er kryptografisk ugyldig (feil nøkkel).",
|
||||||
|
"error_missing_key": "Invitasjonslenken inneholder ikke en dekrypteringsnøkkel (#key=...). Vennligst bruk den fullstendige lenken fra eieren.",
|
||||||
|
"error_expired": "Denne invitasjonen har utløpt (gyldig i 48 timer).",
|
||||||
|
"error_invalid_token": "Invitasjonstokenet er ugyldig.",
|
||||||
|
"error_load_failed": "Invitasjonsdetaljer kunne ikke lastes inn.",
|
||||||
|
"error_incomplete_session": "Økten er ufullstendig - vennligst logg inn på nytt (bruker-ID mangler).",
|
||||||
|
"error_accept_failed": "Tiltredelse mislyktes.",
|
||||||
|
"error_login_failed": "Passkey Innlogging mislyktes.",
|
||||||
|
"error_username_missing": "Brukernavnet ble ikke funnet - vennligst logg inn på nytt.",
|
||||||
|
"error_register_failed": "Registrering mislyktes.",
|
||||||
|
"loading_joining": "Bli med...",
|
||||||
|
"loading_checking": "Invitasjonen vil bli sjekket...",
|
||||||
|
"loading_unlocking": "Loggboken er låst opp og synkronisert...",
|
||||||
|
"loading_retrieving_key": "Last ned krypteringsnøkkelen...",
|
||||||
|
"error_title": "Feil i invitasjonen",
|
||||||
|
"back_to_start": "Tilbake til start",
|
||||||
|
"title": "Invitasjon til loggbok",
|
||||||
|
"invited_by": "Invitasjon fra",
|
||||||
|
"vessel_logbook": "Skip / Loggbok",
|
||||||
|
"signed_in_preparing": "Registrert som {{username}}. Tilslutning er under forberedelse...",
|
||||||
|
"join_again": "Bli med igjen",
|
||||||
|
"login_or_register_hint": "Logg inn eller registrer en konto for å bli med i loggboken.",
|
||||||
|
"or_sign_up": "ELLER REGISTRER DEG PÅ NYTT",
|
||||||
|
"register_crew_account": "Opprett en ny crew-konto",
|
||||||
|
"username_label": "Brukernavn",
|
||||||
|
"create_passkey": "Opprett Passkey",
|
||||||
|
"switch_language_en": "Engelsk",
|
||||||
|
"switch_language_de": "Tysk"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"title": "Statistikk",
|
||||||
|
"subtitle": "Oversikt over ruter, forbruk og kjøretype",
|
||||||
|
"scope_label": "Evalueringsområde",
|
||||||
|
"scope_logbook": "Denne loggboken",
|
||||||
|
"scope_account": "Alle loggbøker",
|
||||||
|
"loading": "Statistikken er beregnet...",
|
||||||
|
"no_data": "Ingen reisedager tilgjengelig ennå.",
|
||||||
|
"total_distance": "Total avstand",
|
||||||
|
"travel_days": "Reisedager",
|
||||||
|
"sail_distance": "Under seil",
|
||||||
|
"motor_distance": "Maskinreise",
|
||||||
|
"motor_hours_total": "Totalt antall maskintimer",
|
||||||
|
"daily_motor_hours": "Maskintimer per reisedøgn",
|
||||||
|
"avg_motor_hours": "Ø maskintimer per reisedøgn",
|
||||||
|
"unknown_propulsion": "Ukjent",
|
||||||
|
"fuel_total": "Totalt drivstoff",
|
||||||
|
"water_total": "Totalt vann",
|
||||||
|
"daily_etmal": "Daglige tider",
|
||||||
|
"daily_consumption": "Daglig forbruk",
|
||||||
|
"route_overview": "Rute",
|
||||||
|
"route_map_title": "Oversikt over ruten",
|
||||||
|
"propulsion_title": "Seil vs. maskin",
|
||||||
|
"propulsion_hint": "Fordelingen er basert på loggbokhendelser per reisedag, ikke på GPS-segmenter.",
|
||||||
|
"avg_distance": "Ø per reisedag",
|
||||||
|
"avg_fuel": "Ø Drivstoff",
|
||||||
|
"avg_water": "Ø Vann",
|
||||||
|
"fuel_per_nm": "Drivstoff per sm",
|
||||||
|
"fuel_per_motor_hour": "Drivstoff per maskintime",
|
||||||
|
"daily_fuel_per_motor_hour": "Drivstofforbruk per maskintime per kjøredag",
|
||||||
|
"fuel_legend": "Drivstoff",
|
||||||
|
"water_legend": "Vann",
|
||||||
|
"unit_nm": "sm",
|
||||||
|
"unit_h": "h",
|
||||||
|
"unit_l": "L",
|
||||||
|
"day_label": "Dag {{day}}",
|
||||||
|
"account_logbooks": "Oversikt over loggbøker",
|
||||||
|
"col_logbook": "Loggbok"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"skip": "Hopp over turen",
|
||||||
|
"back": "Tilbake",
|
||||||
|
"next": "Videre",
|
||||||
|
"finish": "Ferdig",
|
||||||
|
"progress": "Trinn {{current}} fra {{total}}",
|
||||||
|
"steps": {
|
||||||
|
"welcome": {
|
||||||
|
"title": "Velkommen om bord!",
|
||||||
|
"body": "Vi har laget en demo-loggbok med tre dagers reise i Kielfjorden for deg. Du kan når som helst slette eksempeloppføringene hvis du vil starte din egen loggbok. Denne korte omvisningen viser deg de viktigste funksjonene."
|
||||||
|
},
|
||||||
|
"welcome_public": {
|
||||||
|
"title": "Velkommen om bord!",
|
||||||
|
"body": "Utforsk vår demologgbok med tre dagers reise i Kielfjorden - uten konto. Denne korte omvisningen viser deg skipsdata, mannskap og loggbokoppføringer."
|
||||||
|
},
|
||||||
|
"nav_logs": {
|
||||||
|
"title": "Loggbokoppføringer",
|
||||||
|
"body": "Her administrerer du reisedagene dine - avreise, destinasjon, vær, drivstoffnivå og GPS-spor."
|
||||||
|
},
|
||||||
|
"entry_list": {
|
||||||
|
"title": "Dine reisedager",
|
||||||
|
"body": "Hvert kort representerer en reisedag. Trykk på en oppføring for å vise eller redigere detaljer."
|
||||||
|
},
|
||||||
|
"entry_open": {
|
||||||
|
"title": "Åpen reisedag",
|
||||||
|
"body": "Slik ser en fullført loggbok ut - med hendelser, tanknivåer og mer."
|
||||||
|
},
|
||||||
|
"entry_track": {
|
||||||
|
"title": "GPS-sporing",
|
||||||
|
"body": "Last opp GPX-filer eller se allerede lagrede ruter på kartet - inkludert avstand og hastighet."
|
||||||
|
},
|
||||||
|
"nav_vessel": {
|
||||||
|
"title": "Skipsdata",
|
||||||
|
"body": "Skriv inn navn, dimensjoner og tekniske data for båten din - fyll inn én gang, tilgjengelig for alle reisedager."
|
||||||
|
},
|
||||||
|
"nav_crew": {
|
||||||
|
"title": "Mannskapsliste",
|
||||||
|
"body": "Administrer mannskapet og tilordne dem til reisedager senere."
|
||||||
|
},
|
||||||
|
"nav_stats": {
|
||||||
|
"title": "Dashbord for statistikk",
|
||||||
|
"body": "Her kan du se kjørelengder, drivstofforbruk, rutekart og kjøreandeler - automatisk beregnet ut fra loggbokoppføringene dine."
|
||||||
|
},
|
||||||
|
"nav_feedback": {
|
||||||
|
"title": "Send tilbakemelding",
|
||||||
|
"body": "Du kan bruke dette skjemaet til å sende feil, ideer eller generelle tilbakemeldinger direkte til prosjektteamet - også etter omvisningen, når som helst ved hjelp av ikonet øverst til høyre."
|
||||||
|
},
|
||||||
|
"nav_profile": {
|
||||||
|
"title": "Din brukerprofil",
|
||||||
|
"body": "Du får tilgang til din personlige profil via skipperknappen øverst - uavhengig av hvilken loggbok du bruker."
|
||||||
|
},
|
||||||
|
"profile_preferences": {
|
||||||
|
"title": "Regnskap og presentasjon",
|
||||||
|
"body": "Her kan du administrere kontoidentitet, tema og lys/mørk modus. Du kan når som helst starte appturen på nytt. Passkeys og sikkerhetsinnstillinger finner du lenger ned i profilen."
|
||||||
|
},
|
||||||
|
"finish": {
|
||||||
|
"title": "Greit!",
|
||||||
|
"body": "Du kommer rett til statistikkoversikten. Du kan når som helst starte turen på nytt i brukerprofilen din. Ha en riktig god tur!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"title": "Kapteins Daagbok - Gratis digital loggbok for fritidsbåter (uten reklame)",
|
||||||
|
"description": "Gratis, annonsefri digital loggbok med ende-til-ende-kryptering og Passkey-pålogging. Dokumenter seilingsdager, GPS-spor, mannskaps- og skipsdata på en sikker måte - også offline som PWA.",
|
||||||
|
"keywords": "Yachtloggbok, skipsloggbok, loggbok om bord, seiling, Passkey, E2E-kryptering, GPS-sporing, maritim loggbok, gratis, reklamefri, gratis, uten reklame",
|
||||||
|
"ogImageAlt": "Kapteins Daagbok Logo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,735 @@
|
|||||||
|
{
|
||||||
|
"translation": {
|
||||||
|
"app": {
|
||||||
|
"name": "Kapteins Daagbok",
|
||||||
|
"tagline": "Loggbok för privat yacht",
|
||||||
|
"beta": "Beta",
|
||||||
|
"beta_hint": "Betaversion - funktioner kan fortfarande ändras"
|
||||||
|
},
|
||||||
|
"languages": {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"da": "Dansk",
|
||||||
|
"sv": "Svenska",
|
||||||
|
"nb": "Norsk"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"unsaved_changes_title": "Osparade ändringar",
|
||||||
|
"unsaved_changes_message": "Du har ändringar som inte sparats. Vill du verkligen lämna sidan? Dina ändringar kommer att gå förlorade.",
|
||||||
|
"unsaved_changes_leave": "Övergivande",
|
||||||
|
"unsaved_changes_stay": "Stanna kvar"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "Instrumentpanel",
|
||||||
|
"vessel": "Fartygsdata",
|
||||||
|
"crew": "Besättningslista",
|
||||||
|
"deviation": "Distraktionsbord",
|
||||||
|
"logs": "Loggboksanteckningar",
|
||||||
|
"stats": "Statistik",
|
||||||
|
"settings": "Inställningar"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"welcome": "Välkommen till Kapteins Daagbok",
|
||||||
|
"tagline": "Din säkra, E2Ekrypterade loggbok för sjöfarten.",
|
||||||
|
"register": "Registrera dig med Passkey",
|
||||||
|
"login": "Logga in med Passkey",
|
||||||
|
"login_as": "Logga in som {{name}}",
|
||||||
|
"quick_login": "Snabb inloggning",
|
||||||
|
"forget_account": "Glömt konto på den här enheten",
|
||||||
|
"not_user": "Inte {{name}}?",
|
||||||
|
"recovery_title": "Din återställningsnyckel",
|
||||||
|
"recovery_warning": "VIKTIGT: Skriv ner dessa 12 ord. Om du förlorar din Passkey och dessa ord kan dina data inte återställas.",
|
||||||
|
"confirm_recovery": "Jag har skrivit ner orden",
|
||||||
|
"status_logged_in": "Inloggad",
|
||||||
|
"status_logged_out": "Avbruten",
|
||||||
|
"copied": "Kopierat!",
|
||||||
|
"copy_phrase": "Kopiera tangent",
|
||||||
|
"enter_recovery": "Ange återställningsnyckel",
|
||||||
|
"recovery_fallback_warning": "Din Passkey har autentiserats, men din enhet stöder inte maskinvarubaserad nyckelavledning. Ange din återställningsnyckel på 12 ord för att dekryptera din loggbok.",
|
||||||
|
"recovery_placeholder": "Ange din återställningsnyckel som består av 12 ord åtskilda av mellanslag...",
|
||||||
|
"back": "Tillbaka",
|
||||||
|
"decrypting": "Dekryptering...",
|
||||||
|
"decrypt_logbook": "Dekryptera loggbok",
|
||||||
|
"error_incorrect_recovery": "Felaktig återställningsnyckel. Dekryptering misslyckades.",
|
||||||
|
"error_decryption_failed": "Dekrypteringen misslyckades. Vänligen kontrollera din återställningsnyckel.",
|
||||||
|
"or_register": "eller registrera dig",
|
||||||
|
"explore_demo": "Utforska demoversionen utan konto",
|
||||||
|
"username_placeholder": "Användarnamn / Skepparnamn",
|
||||||
|
"processing": "Bearbetning...",
|
||||||
|
"help": "Hjälp",
|
||||||
|
"setup_pin_title": "Ange lokal PIN-kod (tillval)",
|
||||||
|
"setup_pin_warning": "Eftersom din enhet inte stöder direkt härledning av Passkey-nycklar måste du annars ange din nyckel på 12 ord varje gång du loggar in på den här enheten. Konfigurera en lokal PIN-kod för att undvika detta.",
|
||||||
|
"pin_placeholder": "E.G. 123456",
|
||||||
|
"pin_label": "Lokal PIN-kod (4-8 siffror)",
|
||||||
|
"save_pin": "Spara PIN-kod och fortsätt",
|
||||||
|
"skip_pin": "Skip & använd återvinning",
|
||||||
|
"enter_pin_title": "Dekryptera med PIN-kod",
|
||||||
|
"enter_pin_warning": "Ange din lokala PIN-kod för att låsa upp dekrypteringsnyckeln på den här enheten.",
|
||||||
|
"enter_pin_placeholder": "Ange din PIN-kod...",
|
||||||
|
"decrypt_with_pin": "Dekryptera",
|
||||||
|
"use_recovery_instead": "Använd återställningsnycklar istället",
|
||||||
|
"error_incorrect_pin": "Felaktig PIN-kod. Dekryptering misslyckades."
|
||||||
|
},
|
||||||
|
"pwa": {
|
||||||
|
"title": "Installera app",
|
||||||
|
"generic_benefit": "Installera Kapteins Daagbok på din enhet för snabbare åtkomst, offline-användning och permanent datalagring.",
|
||||||
|
"ios_instructions": "På iPad/iPhone: Lägg till appen på startskärmen så att dina loggboksdata förblir skyddade och appen startar som en inbyggd app.",
|
||||||
|
"ios_step_share": "Tryck på aktiesymbolen i fältet Safari.",
|
||||||
|
"ios_step_add": "Välj \"Gå till startskärmen\"",
|
||||||
|
"install_now": "Installera nu",
|
||||||
|
"installing": "Installation...",
|
||||||
|
"later": "Senare",
|
||||||
|
"never": "Visa inte mer",
|
||||||
|
"platform_ios": "Installation via Safari.",
|
||||||
|
"platform_android": "Installation via webbläsaren",
|
||||||
|
"platform_desktop": "Installation som en skrivbordsapp",
|
||||||
|
"settings_section": "Installation av app",
|
||||||
|
"update_title": "Uppdatering tillgänglig",
|
||||||
|
"update_desc": "En ny version av Kapteins Daagbok är klar. Uppdatera för att få de senaste ändringarna.",
|
||||||
|
"update_now": "Uppdatering nu",
|
||||||
|
"update_reloading": "Laddar..."
|
||||||
|
},
|
||||||
|
"sync": {
|
||||||
|
"status_synced": "Synkroniserad",
|
||||||
|
"status_syncing": "Synkronisera...",
|
||||||
|
"status_offline": "Offline-cache",
|
||||||
|
"status_unsynced": "Osynkroniserade förändringar"
|
||||||
|
},
|
||||||
|
"vessel": {
|
||||||
|
"title": "Masterdata för fartyg",
|
||||||
|
"name": "Yacht namn",
|
||||||
|
"type": "Typ av båt",
|
||||||
|
"type_unset": "- inte specificerad -",
|
||||||
|
"type_sailing": "Segelyacht",
|
||||||
|
"type_motor": "Motorbåt",
|
||||||
|
"length_m": "Längd (m)",
|
||||||
|
"draft_m": "Djupgående (m)",
|
||||||
|
"air_draft_m": "Höjd (m)",
|
||||||
|
"invalid_metric": "Ogiltigt numeriskt värde - ange meter som ett decimaltal (t.ex. 12,5).",
|
||||||
|
"port": "Hem hamn",
|
||||||
|
"owner": "Ägare",
|
||||||
|
"charter": "Charterbolag",
|
||||||
|
"registration": "Registreringsnummer/registreringsskylt",
|
||||||
|
"callsign": "Radioanropssignal",
|
||||||
|
"atis": "ATIS nr.",
|
||||||
|
"mmsi": "MMSI nr.",
|
||||||
|
"save": "Spara fartygsdata",
|
||||||
|
"saving": "Kommer att sparas...",
|
||||||
|
"saved": "Fartygsdata har sparats framgångsrikt!",
|
||||||
|
"loading": "Fartygsdata är inlästa...",
|
||||||
|
"sails_list": "Segel (befintliga segel)",
|
||||||
|
"sails_help": "Ange här de segel som finns tillgängliga på din båt (t.ex. storsegel, genua, fock).",
|
||||||
|
"add_sail": "Lägg till segel",
|
||||||
|
"sail_name_placeholder": "z. t.ex. storsegel",
|
||||||
|
"no_sails": "Inga segel lagrade.",
|
||||||
|
"photo_add": "Lägg till foto",
|
||||||
|
"photo_change": "Ändra foto",
|
||||||
|
"photo_delete": "Ta bort foto",
|
||||||
|
"tanks_section": "Tankar (kapacitet)",
|
||||||
|
"tanks_help": "Valfritt i liter - möjliggör slider i journalen för kända tankstorlekar.",
|
||||||
|
"freshwater_capacity_l": "Dricksvatten (liter)",
|
||||||
|
"fuel_capacity_l": "Bränsle (liter)",
|
||||||
|
"greywater_capacity_l": "Gråvatten (liter)",
|
||||||
|
"invalid_tank_liters": "Ogiltigt numeriskt värde - ange liter som ett tal (t.ex. 200)."
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"title": "Loggboksjournal",
|
||||||
|
"new_entry": "Ny resdag",
|
||||||
|
"travel_details": "Detaljer om resan",
|
||||||
|
"add_event": "Lägg till ny loggbokspost",
|
||||||
|
"add_event_btn": "Lägg till händelse",
|
||||||
|
"edit_event": "Redigera händelse",
|
||||||
|
"save_event_btn": "Spara ändring",
|
||||||
|
"cancel_event_edit": "Avbryt",
|
||||||
|
"delete_event": "Ta bort händelse",
|
||||||
|
"sign_cleared_skipper_re_sign_title": "Skippers signatur borttagen",
|
||||||
|
"sign_cleared_skipper_re_sign": "Händelseloggen har ändrats. Skepparens signatur har tagits bort. Vänligen godkänn igen.",
|
||||||
|
"date": "datum",
|
||||||
|
"day_of_travel": "Resedag / resedag",
|
||||||
|
"departure": "Starthamn (resa från)",
|
||||||
|
"destination": "Destinationsport (till)",
|
||||||
|
"route": "Resa från/till",
|
||||||
|
"freshwater": "Färskvatten (liter)",
|
||||||
|
"fuel": "Treibstoff / Bränsle (liter)",
|
||||||
|
"greywater": "Gråvatten (liter)",
|
||||||
|
"greywater_level": "Fyllnadsnivå",
|
||||||
|
"tank_slider_of_max": "{{current}} / {{max}} L",
|
||||||
|
"tank_capacity_tooltip": "Om tankens kapacitet (liter) finns lagrad i fartygets data kan du ange fyllnadsnivåerna här med hjälp av skjutreglaget.",
|
||||||
|
"morning": "Stå på morgonen",
|
||||||
|
"refilled": "Påfylld",
|
||||||
|
"evening": "Kvällsställ",
|
||||||
|
"consumption": "Daglig konsumtion",
|
||||||
|
"signatures": "Underskrifter / frisläppande",
|
||||||
|
"sign_skipper": "Skepparens signatur",
|
||||||
|
"sign_crew": "Besättningens signatur",
|
||||||
|
"sign_hint": "Signera med finger, penna eller mus",
|
||||||
|
"sign_clear": "Radera",
|
||||||
|
"sign_export_image": "[Signatur]",
|
||||||
|
"sign_with_passkey": "Frigör med Passkey",
|
||||||
|
"sign_passkey_signing": "Passkey begärs...",
|
||||||
|
"sign_passkey_signed": "Utgiven av {{username}}",
|
||||||
|
"sign_passkey_export": "Passkey: {{username}} ({{date}})",
|
||||||
|
"sign_attribution_export": "{{username}} ({{date}})",
|
||||||
|
"sign_passkey_clear": "Ta bort Passkey release",
|
||||||
|
"sign_mode_passkey": "Passkey",
|
||||||
|
"sign_mode_classic": "Klassisk",
|
||||||
|
"sign_passkey_failed": "Passkey Frigöring misslyckades",
|
||||||
|
"sign_passkey_cancelled": "Passkey Frigörandet inställt",
|
||||||
|
"sign_invalid": "Signaturen är ogiltig - innehållet har ändrats",
|
||||||
|
"sign_badge_skipper": "Skeppare",
|
||||||
|
"sign_badge_skipper_invalid": "Ogiltig",
|
||||||
|
"sign_badge_skipper_title_valid": "Skepparen har släppt",
|
||||||
|
"sign_badge_skipper_title_invalid": "Skippers signatur ogiltig - innehållet har ändrats",
|
||||||
|
"sign_classic_or_passkey": "Valfritt: klassisk signatur eller Passkey release ovan",
|
||||||
|
"sign_crew_passkey_hint": "Besättningsmedlemmar med skrivbehörighet kan frigöra via Passkey.",
|
||||||
|
"sign_offline_hint": "Passkey-Godkännande kräver Internet - klassisk signatur möjlig offline",
|
||||||
|
"sign_lock_notice": "Efter undertecknandet är det inte möjligt att göra ändringar i loggboksanteckningen (utom foton) utan att skepparen och besättningen måste underteckna på nytt.",
|
||||||
|
"sign_lock_active": "Denna post är signerad. Ändringar i loggboken (utom foton) tar automatiskt bort skepparens och besättningens signaturer.",
|
||||||
|
"sign_lock_warning_title": "Bekräfta underskrift",
|
||||||
|
"sign_lock_warning": "Efter undertecknandet är det inte längre möjligt att göra ändringar i loggboksanteckningen (utom foton) utan att skepparen och besättningen måste underteckna på nytt.\n\nVill du fortsätta?",
|
||||||
|
"sign_proceed": "Teckna",
|
||||||
|
"sign_cancel": "Avbryt",
|
||||||
|
"sign_cleared_re_sign_title": "Underskrifter borttagna",
|
||||||
|
"sign_cleared_re_sign": "Loggboksanteckningen har ändrats. Skepparens och besättningens namnteckningar har tagits bort. Vänligen underteckna igen.",
|
||||||
|
"no_entries": "Inga loggboksposter hittade för denna yacht. Skapa din första resedag!",
|
||||||
|
"back_to_list": "Tillbaka till tidskriftslistan",
|
||||||
|
"save": "Spara loggbokssida",
|
||||||
|
"saving": "Kommer att sparas...",
|
||||||
|
"saved": "Loggbokssidan har sparats framgångsrikt!",
|
||||||
|
"loading": "Journalen laddas...",
|
||||||
|
"delete_entry": "Ta bort tagg",
|
||||||
|
"delete_confirm": "Är du säker på att du vill radera den här resedagen permanent?",
|
||||||
|
"carry_over_tanks_title": "Överföra data från föregående dag?",
|
||||||
|
"carry_over_tanks_confirm": "Ta över starthamn, färskvatten, bränsle och gråvatten från startnivåerna från resans sista dag?\n\nStarthamn: {{departure}}\nFärskvatten: {{fw}} L\nBränsle: {{fuel}} L\nGråvatten: {{greywater}} L",
|
||||||
|
"carry_over_tanks_yes": "Ta över",
|
||||||
|
"carry_over_tanks_no": "Börja med 0",
|
||||||
|
"event_title": "Kronologisk händelselogg",
|
||||||
|
"no_events": "Inga händelser inlagda för denna resdag ännu.",
|
||||||
|
"event_time": "Tid på dygnet",
|
||||||
|
"event_mgk": "MgK-kurs",
|
||||||
|
"event_rwk": "RwK-kurs",
|
||||||
|
"event_course_section": "Kurs",
|
||||||
|
"course_dial_hint": "Vrid ringen eller gå in i grader",
|
||||||
|
"course_dial_step_label": "Stegstorlek",
|
||||||
|
"course_step_fine": "1°",
|
||||||
|
"course_step_medium": "5°",
|
||||||
|
"course_step_coarse": "10°",
|
||||||
|
"course_tab_mgk": "MgK",
|
||||||
|
"course_tab_rwk": "rwK",
|
||||||
|
"course_invalid": "Ogiltig kurs (0-360)",
|
||||||
|
"course_placeholder_degrees": "z. B. 180",
|
||||||
|
"course_placeholder_cardinal": "z. E.G. NW",
|
||||||
|
"compass_n": "N",
|
||||||
|
"compass_e": "O",
|
||||||
|
"compass_s": "S",
|
||||||
|
"compass_w": "W",
|
||||||
|
"wind_mode_cardinal": "Kardinal",
|
||||||
|
"wind_mode_degrees": "Som examen",
|
||||||
|
"event_wind_direction": "Vindriktning",
|
||||||
|
"event_wind_strength": "Vindstyrka",
|
||||||
|
"event_sea_state": "Havets tillstånd",
|
||||||
|
"event_weather": "Väder",
|
||||||
|
"event_log": "Log (sm)",
|
||||||
|
"event_gps": "GPS-position",
|
||||||
|
"event_location": "Plats / hamn",
|
||||||
|
"event_location_placeholder": "z. t.ex. Kiel",
|
||||||
|
"event_remarks": "Anmärkningar / incidenter",
|
||||||
|
"gps_btn": "Hämta GPS-koordinater",
|
||||||
|
"weather_btn": "OpenWeatherMap Ring upp väder",
|
||||||
|
"event_wind_pressure": "Lufttryck (hPa)",
|
||||||
|
"event_heel": "Krängning (°)",
|
||||||
|
"event_sails": "Segelhantering / motor",
|
||||||
|
"motor_propulsion": "Maskinens resa",
|
||||||
|
"sails_picker_show_more": "Visa alla segel",
|
||||||
|
"sails_picker_show_less": "Visa mindre",
|
||||||
|
"motor_hours": "Maskintimmar (totalt)",
|
||||||
|
"fuel_per_motor_hour": "Förbrukning per maskintimme",
|
||||||
|
"event_distance": "Avstånd (sm)",
|
||||||
|
"export_csv": "Hämta CSV.",
|
||||||
|
"share_csv": "Aktie",
|
||||||
|
"export_pdf": "Hämta PDF.",
|
||||||
|
"exporting_pdf": "PDF genereras...",
|
||||||
|
"photos_title": "Fotobilagor (E2E-krypterade)",
|
||||||
|
"photo_caption_label": "Fotobeskrivning/etikett (valfritt)",
|
||||||
|
"photo_caption_placeholder": "t.ex. sätta segel nära hamninloppet",
|
||||||
|
"photo_btn": "Ta foto / ladda upp",
|
||||||
|
"photo_processing": "Håller på att bearbetas...",
|
||||||
|
"no_photos": "Inga foton kopplade till denna resdag ännu.",
|
||||||
|
"photo_delete_confirm": "Är du säker på att du vill radera det här fotot permanent?",
|
||||||
|
"confirm_yes": "Ja",
|
||||||
|
"confirm_no": "Nej",
|
||||||
|
"track_upload_title": "GPS-spårning (fil)",
|
||||||
|
"track_upload_points": "Poäng",
|
||||||
|
"gps_tracking_btn_gpx": "Ladda ner spårfil",
|
||||||
|
"gps_track_upload_help": "Dra en GPX-, KML- eller GeoJSON-fil hit eller klicka för att välja",
|
||||||
|
"gps_track_upload_btn": "Ladda upp GPS-spår",
|
||||||
|
"gps_track_delete": "Ta bort spårfil",
|
||||||
|
"gps_track_delete_confirm": "Är du säker på att du vill radera den här spårfilen permanent?",
|
||||||
|
"track_distance": "GPS-rutt (sm)",
|
||||||
|
"track_speed_max": "Max. hastighet Hastighet (kn)",
|
||||||
|
"track_speed_avg": "Ø Hastighet (kn)",
|
||||||
|
"track_map_title": "GPS-spår på OpenSeaMap",
|
||||||
|
"track_map_start": "Start",
|
||||||
|
"track_map_end": "Mål",
|
||||||
|
"track_map_speed_slow": "långsamt",
|
||||||
|
"track_map_speed_fast": "snabb",
|
||||||
|
"track_map_error": "Kartan kunde inte läsas in.",
|
||||||
|
"exporting": "Export...",
|
||||||
|
"share_unsupported": "Delning stöds inte på den här enheten. Filen har laddats ner istället.",
|
||||||
|
"invite_crew": "Bjud in besättningen",
|
||||||
|
"invite_link_copied": "Länk till inbjudan kopierad till urklipp!",
|
||||||
|
"invite_link_desc": "Dela den här länken med besättningsmedlemmar för att ge dem skrivrättigheter till loggboken.",
|
||||||
|
"collaborators_list": "Medlemmar / Besättning",
|
||||||
|
"revoke": "Ta bort",
|
||||||
|
"revoke_confirm": "Är du säker på att du vill återkalla den här besättningsmedlemmens åtkomst?",
|
||||||
|
"invite_role": "Roll",
|
||||||
|
"invite_expires": "Länken är giltig i 48 timmar"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Dina loggböcker",
|
||||||
|
"subtitle": "Välj en loggbok eller skapa en ny för att hantera dina resor.",
|
||||||
|
"create_btn": "Skapa loggbok",
|
||||||
|
"new_logbook_placeholder": "Loggbokens eller båtens namn",
|
||||||
|
"logout": "Logga ut",
|
||||||
|
"logged_in_as": "Inloggad som {{name}}",
|
||||||
|
"delete_confirm": "Är du säker på att du vill radera den här loggboken permanent? Alla lokala data och serverkopior kommer att förstöras.\n\nTips: Skapa en säkerhetskopia (.daagbok.json) i förväg under Inställningar → Säkerhetskopiering och återställning om du vill behålla data senare.",
|
||||||
|
"no_logbooks": "Inga loggböcker hittades. Skapa din första loggbok för att komma igång!",
|
||||||
|
"loading": "Loggböckerna är fulla...",
|
||||||
|
"status_synced": "Synkroniserad",
|
||||||
|
"status_local": "Endast lokal cache",
|
||||||
|
"delete_btn": "Radera loggbok",
|
||||||
|
"section_owned": "Mina loggböcker",
|
||||||
|
"section_shared": "Delade loggböcker",
|
||||||
|
"section_shared_hint": "Du har blivit inbjuden som besättningsmedlem. Skepparens profil och inställningar tillhör ägaren.",
|
||||||
|
"role_owner": "Egen loggbok",
|
||||||
|
"role_owner_hint": "Du är ägare och skeppare till denna loggbok",
|
||||||
|
"role_crew": "Tillträde för besättningen",
|
||||||
|
"role_crew_hint": "Inbjuden loggbok - du kan arbeta som besättning och underteckna den",
|
||||||
|
"role_read": "Endast läsning",
|
||||||
|
"role_read_hint": "Delad loggbok - endast visning, ingen redigering",
|
||||||
|
"open_profile": "Öppna profil för {{name}}",
|
||||||
|
"edit_title": "Byt namn på loggbok",
|
||||||
|
"edit_placeholder": "Nytt namn på loggboken",
|
||||||
|
"edit_success": "Loggboken har framgångsrikt bytt namn",
|
||||||
|
"edit_btn": "Byt namn på",
|
||||||
|
"filter_label": "Filtrera loggböcker",
|
||||||
|
"filter_placeholder": "Namn, årtal eller datum ...",
|
||||||
|
"filter_clear": "Återställ filter",
|
||||||
|
"filter_results": "{{count}} Träffar",
|
||||||
|
"filter_no_results": "Inga loggböcker matchar din sökning. Försök med ett annat namn eller ett annat år.",
|
||||||
|
"sort_label": "Sortera",
|
||||||
|
"sort_by_label": "Sortera efter",
|
||||||
|
"sort_by_name": "Namn",
|
||||||
|
"sort_by_date": "datum",
|
||||||
|
"sort_dir_label": "Sekvens",
|
||||||
|
"sort_asc": "Stigande",
|
||||||
|
"sort_desc": "Nedåtgående",
|
||||||
|
"sort_name_asc": "Namn A till Ö",
|
||||||
|
"sort_name_desc": "Namn Z till A",
|
||||||
|
"sort_date_asc": "Äldst först",
|
||||||
|
"sort_date_desc": "Nyast först"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"title": "Användarprofil",
|
||||||
|
"subtitle": "Konto, Passkeys och statistik för {{name}}",
|
||||||
|
"back": "Tillbaka till instrumentpanelen",
|
||||||
|
"loading": "Profilen håller på att laddas...",
|
||||||
|
"load_error": "Profilen kunde inte laddas.",
|
||||||
|
"copy_failed": "Kopiering misslyckades.",
|
||||||
|
"processing": "Håller på att bearbetas...",
|
||||||
|
"identity_title": "Kontots identitet",
|
||||||
|
"username": "Användarens namn",
|
||||||
|
"user_id": "Användar-ID",
|
||||||
|
"copy_user_id": "Kopiera användar-ID",
|
||||||
|
"account_since": "Konto sedan",
|
||||||
|
"prf_status": "Passkey härledning av nyckel (PRF)",
|
||||||
|
"prf_active": "Aktiv",
|
||||||
|
"prf_inactive": "Inte konfigurerad",
|
||||||
|
"passkeys_title": "Passkeys",
|
||||||
|
"passkeys_desc": "Registrera en separat Passkey på varje enhet. Detta gör att du kan logga in även efter att du bytt plattform.",
|
||||||
|
"passkeys_empty": "Inga Passkeys hittades.",
|
||||||
|
"add_passkey_btn": "Lägg till ny Passkey",
|
||||||
|
"add_passkey_success": "Passkey har lagts till.",
|
||||||
|
"add_passkey_failed": "Passkey kunde inte läggas till.",
|
||||||
|
"remove_passkey_btn": "Ta bort Passkey.",
|
||||||
|
"remove_passkey_last_title": "Senaste Passkey.",
|
||||||
|
"remove_passkey_last_desc": "Den enda Passkey kan inte tas bort utan att du förlorar åtkomsten till ditt konto. Om du vill radera kontot helt använder du riskzonen längst ner på den här sidan.",
|
||||||
|
"remove_passkey_failed": "Passkey kunde inte tas bort.",
|
||||||
|
"remove_passkey_confirm_title": "Ta bort Passkey?",
|
||||||
|
"remove_passkey_confirm_desc": "Denna enhet kan sedan inte längre logga in med denna Passkey.",
|
||||||
|
"remove_passkey_confirm_yes": "Ta bort",
|
||||||
|
"remove_passkey_confirm_no": "Avbryt",
|
||||||
|
"pin_title": "Lokal PIN-kod",
|
||||||
|
"pin_status": "Status",
|
||||||
|
"pin_active": "Aktiv på den här enheten",
|
||||||
|
"pin_inactive": "Inte konfigurerad",
|
||||||
|
"pin_confirm_label": "Bekräfta PIN-kod",
|
||||||
|
"pin_confirm_placeholder": "Ange PIN-koden igen",
|
||||||
|
"pin_set_btn": "Ange PIN-kod",
|
||||||
|
"pin_change_btn": "Ändra PIN-kod",
|
||||||
|
"pin_remove_btn": "Ta bort PIN-koden",
|
||||||
|
"pin_saved": "PIN-koden sparad.",
|
||||||
|
"pin_save_failed": "PIN-koden kunde inte räddas.",
|
||||||
|
"pin_mismatch": "PIN-koderna stämmer inte överens.",
|
||||||
|
"pin_length_error": "PIN-koden måste innehålla minst 4 tecken.",
|
||||||
|
"pin_no_session": "Sessionen har löpt ut - vänligen registrera dig igen.",
|
||||||
|
"remove_pin_confirm_title": "Ta bort PIN-koden?",
|
||||||
|
"remove_pin_confirm_desc": "Du måste logga in igen på den här enheten med Passkey eller återställningsnyckel.",
|
||||||
|
"remove_pin_confirm_yes": "Ta bort PIN-koden",
|
||||||
|
"remove_pin_confirm_no": "Avbryt",
|
||||||
|
"security_title": "Checklista för säkerhet",
|
||||||
|
"security_desc": "Översikt över de viktigaste skyddsmekanismerna för ditt konto.",
|
||||||
|
"security_passkeys_ok": "Minst en Passkey registrerad",
|
||||||
|
"security_passkeys_missing": "Nej Passkey registrerad",
|
||||||
|
"security_prf_ok": "Avledning av PRF-nyckel aktiv",
|
||||||
|
"security_prf_missing": "PRF inte upprättad",
|
||||||
|
"security_pin_ok": "Lokal PIN-kod på den här enheten",
|
||||||
|
"security_pin_missing": "Ingen lokal PIN-kod",
|
||||||
|
"security_recovery_ok": "Uppsättning av återställningsnyckel",
|
||||||
|
"security_recovery_hint": "De 12 orden visades under registreringen. Håll dem offline och åtskilda från enheten. Du kan skapa en ny nyckel nedan - den gamla kommer då att bli ogiltig.",
|
||||||
|
"recovery_rotate_btn": "Skapa en ny återställningsnyckel",
|
||||||
|
"recovery_rotate_confirm_title": "Skapa en ny återställningsnyckel?",
|
||||||
|
"recovery_rotate_confirm_desc": "Den tidigare nyckeln på 12 ord blir ogiltig omedelbart. Se till att du förvarar den nya nyckeln säkert innan du fortsätter.",
|
||||||
|
"recovery_rotate_confirm_yes": "Skapa ny nyckel",
|
||||||
|
"recovery_rotate_confirm_no": "Avbryt",
|
||||||
|
"recovery_rotate_new_warning": "VIKTIGT: Skriv ner dessa 12 ord och förvara dem offline. Den tidigare återställningsnyckeln är nu ogiltig.",
|
||||||
|
"recovery_rotate_failed": "Återställningsnyckel kunde inte skapas.",
|
||||||
|
"recovery_rotate_no_session": "Krypteringssessionen har löpt ut - logga ut och logga in igen och försök sedan igen.",
|
||||||
|
"device_title": "Denna enhet",
|
||||||
|
"device_desc": "Lokal cache, synkroniseringsstatus och snabb inloggning i den här webbläsaren.",
|
||||||
|
"device_sync_pending": "{{count}} väntande synkroniseringsposter",
|
||||||
|
"device_sync_ok": "Alla lokala ändringar synkroniseras",
|
||||||
|
"device_remembered": "Konto för snabb inloggning sparat på den här enheten",
|
||||||
|
"device_not_remembered": "Kontot finns inte med i listan för snabb inloggning",
|
||||||
|
"device_forget_btn": "Glömt konto på den här enheten",
|
||||||
|
"device_forget_confirm_title": "Ta bort snabb inloggning?",
|
||||||
|
"device_forget_confirm_desc": "Kontot försvinner från snabbinloggningslistan på den här enheten. Din session och dina lokala loggböcker behålls.",
|
||||||
|
"device_forget_confirm_yes": "Ta bort",
|
||||||
|
"device_forget_confirm_no": "Avbryt",
|
||||||
|
"passkey_label": "Namn för ny Passkey (valfritt)",
|
||||||
|
"passkey_label_placeholder": "z. t.ex. MacBook, iPhone",
|
||||||
|
"passkey_rename_btn": "Spara namn",
|
||||||
|
"passkey_rename_success": "Passkey namn sparat.",
|
||||||
|
"passkey_rename_failed": "Passkey-Namnet kunde inte sparas.",
|
||||||
|
"passkey_unnamed": "Utan titel Passkey",
|
||||||
|
"stats_title": "Statistik",
|
||||||
|
"stats_subtitle": "Om alla dina loggböcker på den här enheten",
|
||||||
|
"stats_logbooks": "Loggböcker",
|
||||||
|
"stats_account_since": "Konto sedan",
|
||||||
|
"stats_shared_logbooks": "Delade loggböcker",
|
||||||
|
"appearance_title": "App & visualisering",
|
||||||
|
"appearance_desc": "Designen och färgschemat gäller för hela appen på den här enheten.",
|
||||||
|
"theme_label": "Appens designstil",
|
||||||
|
"theme_auto": "Automatisk (OS-detektering)",
|
||||||
|
"theme_ocean": "Ocean (glasmorfism)",
|
||||||
|
"theme_material": "Material (Android)",
|
||||||
|
"theme_cupertino": "Cupertino (iOS)",
|
||||||
|
"color_scheme_label": "Ljust eller mörkt läge",
|
||||||
|
"color_scheme_auto": "Automatisk (system)",
|
||||||
|
"color_scheme_light": "Ljus",
|
||||||
|
"color_scheme_dark": "Mörk",
|
||||||
|
"integrations_title": "Integrationer",
|
||||||
|
"owm_key": "OpenWeatherMap API-nyckel",
|
||||||
|
"owm_help": "Valfritt: egen OpenWeatherMap API-nyckel. Om inget anges används nyckeln på serversidan från operatörskonfigurationen.",
|
||||||
|
"prefs_save": "Spara",
|
||||||
|
"prefs_saving": "Kommer att sparas...",
|
||||||
|
"prefs_saved": "Sparade",
|
||||||
|
"tour_title": "App-turné",
|
||||||
|
"tour_desc": "Låt dig vägledas genom de viktigaste områdena i appen igen.",
|
||||||
|
"tour_restart": "Starta resan igen",
|
||||||
|
"push_title": "Push-meddelanden",
|
||||||
|
"push_desc": "Som loggboksägare får du ett meddelande när inbjudna besättningsmedlemmar synkroniserar ändringar. Inget innehåll överförs i klartext.",
|
||||||
|
"push_enable": "Meddela oss om förändringar i besättningen",
|
||||||
|
"push_active": "Push-meddelanden är aktiva på den här enheten.",
|
||||||
|
"push_unsupported": "Push-meddelanden stöds inte i den här webbläsaren.",
|
||||||
|
"push_denied_hint": "Meddelanden är blockerade. Tillåt dem i webbläsarens eller enhetens inställningar.",
|
||||||
|
"push_ios_install_hint": "På iPhone/iPad: Lägg till app på startskärmen (iOS 16.4+) för att använda push.",
|
||||||
|
"push_error": "Push-meddelanden kunde inte aktiveras."
|
||||||
|
},
|
||||||
|
"crew": {
|
||||||
|
"title": "Profiler för skeppare och besättning",
|
||||||
|
"skipper_section": "Skepparens profil",
|
||||||
|
"skipper_read_only_hint": "Skepparens profil kan endast redigeras av loggbokens ägare.",
|
||||||
|
"crew_section": "Besättningslista",
|
||||||
|
"add_crew": "Lägg till besättningsmedlem",
|
||||||
|
"edit_crew": "Redigera besättningsmedlem",
|
||||||
|
"no_crew": "Inga besättningsmedlemmar har lagts till ännu.",
|
||||||
|
"max_crew": "Maximalt antal på 5 besättningsmedlemmar uppnås.",
|
||||||
|
"name": "Namn",
|
||||||
|
"address": "adress",
|
||||||
|
"birthdate": "Födelsedag",
|
||||||
|
"phone": "Telefonnummer",
|
||||||
|
"nationality": "Nationalitet",
|
||||||
|
"passport": "Pass/ID-nummer",
|
||||||
|
"bloodtype": "Blodgrupp",
|
||||||
|
"allergies": "Allergier",
|
||||||
|
"diseases": "Redan existerande tillstånd/sjukdomar",
|
||||||
|
"save": "Spara skeppardata",
|
||||||
|
"save_member": "Spara medlem",
|
||||||
|
"saved": "Skepparens profil har sparats!",
|
||||||
|
"loading": "Besättningsfilerna är laddade...",
|
||||||
|
"delete_confirm": "Är du säker på att du vill ta bort den här besättningsmedlemmen?"
|
||||||
|
},
|
||||||
|
"deviation": {
|
||||||
|
"title": "Tabell för kompassavvikelse",
|
||||||
|
"subtitle": "Ange den magnetiska kompassdeflektionen (deflektion) för kurser (MgK) från 000° till 360° i steg om 10°.",
|
||||||
|
"heading": "MgK",
|
||||||
|
"deviation": "Distraktion",
|
||||||
|
"save": "Spara kalibreringsrutan",
|
||||||
|
"saving": "Kommer att sparas...",
|
||||||
|
"saved": "Kalibreringsnätet har sparats framgångsrikt!",
|
||||||
|
"loading": "Kalibreringsbordet är laddat..."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Inställningar för loggbok",
|
||||||
|
"subtitle": "Dela, säkerhetskopiera och samarbeta för den här loggboken.",
|
||||||
|
"select_logbook_hint": "Välj en loggbok för att redigera dess inställningar.",
|
||||||
|
"no_key": "Ingen OpenWeatherMap API-nyckel tillgänglig. Spara din egen nyckel i användarprofilen eller kontakta operatören.",
|
||||||
|
"weather_success": "Väderdata har hämtats framgångsrikt!",
|
||||||
|
"weather_error": "Hämtning av väderdata misslyckades. Kontrollera API-nyckeln och anslutningen.",
|
||||||
|
"weather_date_mismatch": "Väderdata kan endast hämtas för idag ({{today}}). Denna loggbokspost är daterad {{date}}.",
|
||||||
|
"gps_error": "Ange en plats eller bestäm GPS-koordinaterna.",
|
||||||
|
"share_title": "Aktieloggbok (skrivskyddad)",
|
||||||
|
"share_desc": "Aktivera det här alternativet för att skapa en publik, skrivskyddad länk. Alla som har länken kan se dina resor, båtprofiler och besättning. Krypteringsnycklarna överförs aldrig till servern (de finns kvar i hashdelen av URL:en).",
|
||||||
|
"share_privacy_warning": "Rekommendation: Dela endast den här länken privat (t.ex. via e-post eller messenger), inte på sociala medier.",
|
||||||
|
"share_enable": "Aktivera offentlig länk",
|
||||||
|
"share_copied": "Länk kopierad!",
|
||||||
|
"share_copy_btn": "Kopiera länk",
|
||||||
|
"danger_zone_title": "Farlig zon",
|
||||||
|
"danger_zone_desc": "Om du raderar ditt konto raderas oåterkalleligen alla dina Passkey, loggböcker, fartygsdata, besättningsprofiler, reseanteckningar och E2E-nycklar. Denna åtgärd kan inte ångras.",
|
||||||
|
"delete_account_btn": "Ta bort konto oåterkalleligt",
|
||||||
|
"delete_account_confirm_title": "Radera konto?",
|
||||||
|
"delete_account_confirm_desc": "Är du helt säker på att du oåterkalleligen vill radera ditt konto och alla tillhörande loggböcker och E2E-krypterade data?",
|
||||||
|
"delete_account_confirm_yes": "Ja, radera konto och all data",
|
||||||
|
"delete_account_confirm_no": "Avbryt",
|
||||||
|
"delete_account_failed": "Kontot kunde inte raderas. Vänligen försök igen.",
|
||||||
|
"delete_backup_hint": "Tips: Skapa säkerhetskopior av dina loggböcker (.daagbok.json) i inställningarna för varje loggbok innan du raderar dem.",
|
||||||
|
"deleting_account": "Kontot kommer att raderas...",
|
||||||
|
"invite_push_prompt_title": "Aktivera push-meddelanden?",
|
||||||
|
"invite_push_prompt_message": "Så snart inbjudna besättningsmedlemmar synkroniserar ändringar kan du bli informerad via push. Inget loggboksinnehåll skickas i klartext.",
|
||||||
|
"invite_push_prompt_ios_message": "Så snart besättningsmedlemmar synkroniserar ändringar kan du bli informerad via push. På iPhone/iPad: Lägg till appen på startskärmen (iOS 16.4+) och aktivera sedan push i användarprofilen.",
|
||||||
|
"invite_push_prompt_enable": "Aktivera nu",
|
||||||
|
"invite_push_prompt_later": "Senare",
|
||||||
|
"invite_push_prompt_success": "Push-meddelanden är aktiva på den här enheten.",
|
||||||
|
"backup_title": "Säkerhetskopiering och återställning",
|
||||||
|
"backup_desc": "Komplett krypterad säkerhetskopia av denna loggbok (poster, foton, GPS-spår, besättning, fartyg). Skyddad med lösenfras för säkerhetskopian - för återställning till detta eller ett nytt konto.",
|
||||||
|
"backup_export_title": "Skapa säkerhetskopia",
|
||||||
|
"backup_export_desc": "Laddar ner alla lokala data som .daagbok.json. Förvara filen och lösenfrasen separat och säkert.",
|
||||||
|
"backup_restore_title": "Återställ säkerhetskopian",
|
||||||
|
"backup_restore_desc": "Återställer en säkerhetskopia till ditt nuvarande konto - även efter att du har registrerat ett nytt konto.",
|
||||||
|
"backup_passphrase": "Lösenord för säkerhetskopiering",
|
||||||
|
"backup_passphrase_placeholder": "Minst 8 tecken",
|
||||||
|
"backup_passphrase_confirm": "Bekräfta lösenfras",
|
||||||
|
"backup_passphrase_short": "Säkerhetskopians lösenfras måste vara minst 8 tecken lång.",
|
||||||
|
"backup_passphrase_mismatch": "Lösenfraserna stämmer inte överens.",
|
||||||
|
"backup_wrong_passphrase": "Lösenordet är felaktigt eller säkerhetskopian är skadad.",
|
||||||
|
"backup_export_btn": "Ladda ner backup",
|
||||||
|
"backup_exporting": "Säkerhetskopian skapas...",
|
||||||
|
"backup_export_success": "Säkerhetskopia skapad ({{count}} resdagar).",
|
||||||
|
"backup_file_label": "Säkerhetskopieringsfil (.daagbok.json)",
|
||||||
|
"backup_preview_btn": "Kontrollera innehåll",
|
||||||
|
"backup_previewing": "Check...",
|
||||||
|
"backup_restore_btn": "Återställ",
|
||||||
|
"backup_restoring": "Kommer att återställas...",
|
||||||
|
"backup_restore_success": "Loggbok \"{{title}}\" har återställts.",
|
||||||
|
"backup_restore_cancelled": "Återhämtning avbruten.",
|
||||||
|
"backup_invalid_json": "Filen är inte en giltig JSON-fil.",
|
||||||
|
"backup_invalid_format": "Okänt eller föråldrat backupformat.",
|
||||||
|
"backup_not_owner": "Endast loggbokens ägare kan skapa säkerhetskopior.",
|
||||||
|
"backup_not_authenticated": "Logga in för att återställa en säkerhetskopia.",
|
||||||
|
"backup_id_conflict": "En loggbok med detta ID finns redan.",
|
||||||
|
"backup_overwrite_confirm": "Den befintliga loggboken med samma ID ersätts. Fortsätter du?",
|
||||||
|
"backup_new_id_confirm": "Importera säkerhetskopian som en ny loggbok med ett nytt ID?",
|
||||||
|
"backup_stat_entries": "{{count}} Resdagar",
|
||||||
|
"backup_stat_photos": "{{count}} Foton",
|
||||||
|
"backup_stat_crew": "{{count}} Besättningens uppgifter",
|
||||||
|
"backup_stat_tracks": "{{count}} GPS-spår",
|
||||||
|
"backup_exported_at": "Exporterad: {{date}}"
|
||||||
|
},
|
||||||
|
"disclaimer": {
|
||||||
|
"title": "Viktiga anmärkningar",
|
||||||
|
"intro": "Läs följande anvisningar innan du använder Kapteins Daagbok.",
|
||||||
|
"e2e_title": "End-to-end-kryptering",
|
||||||
|
"e2e_body": "Dina loggboksdata är krypterade från början till slut. Endast du - eller personer med din nyckel - kan läsa innehållet. Endast krypterade data lagras på servern.",
|
||||||
|
"pwa_title": "Progressiv webbapplikation (PWA)",
|
||||||
|
"pwa_body": "Kapteins Daagbok körs som en progressiv webbapp i din webbläsare och kan installeras på din enhet - på samma sätt som en native-app, utan en appbutik.",
|
||||||
|
"storage_title": "Lokal lagring och synkronisering",
|
||||||
|
"storage_body": "Dina data lagras lokalt på din enhet (IndexedDB). Ändringar synkroniseras med servern när en internetanslutning är aktiv. Du kan fortsätta att arbeta utan anslutning, synkroniseringen sker senare.",
|
||||||
|
"free_title": "Kostnadsfritt och reklamfritt",
|
||||||
|
"free_body": "Kapteins Daagbok är kostnadsfritt och innehåller ingen reklam.",
|
||||||
|
"liability_title": "Ansvarsfriskrivning",
|
||||||
|
"liability_body": "Användningen av appen sker på egen risk. Inget ansvar accepteras för skador som uppstår till följd av användningen av appen - inklusive felaktiga eller ofullständiga loggboksanteckningar, förlust av data eller tekniska fel.",
|
||||||
|
"warranty_title": "Ingen garanti",
|
||||||
|
"warranty_body": "Ingen garanti ges för tjänstens funktion, korrekthet eller tillgänglighet. Driften kan när som helst avbrytas, begränsas eller ställas in.",
|
||||||
|
"copyright": "© 2026 KnorrLabs, Markus F.J. Busche",
|
||||||
|
"accept": "Acceptera och fortsätt",
|
||||||
|
"close": "Nära",
|
||||||
|
"button_title": "Anmärkningar och ansvarsfriskrivning"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"button_title": "Skicka feedback",
|
||||||
|
"title": "Återkoppling",
|
||||||
|
"intro": "Dela med dig av buggar, idéer eller allmän feedback. Ditt meddelande kommer att skickas till projektgruppen via en säker meddelandekanal.",
|
||||||
|
"category_label": "Kategori",
|
||||||
|
"category_general": "Allmänt",
|
||||||
|
"category_bug": "Rapportera fel",
|
||||||
|
"category_feature": "Begäran om funktion",
|
||||||
|
"contact_label": "E-post (valfritt)",
|
||||||
|
"contact_placeholder": "deine@email.beispiel",
|
||||||
|
"message_label": "Meddelande",
|
||||||
|
"message_placeholder": "Beskriv din feedback...",
|
||||||
|
"send": "Skicka",
|
||||||
|
"sending": "Kommer att skickas...",
|
||||||
|
"cancel": "Avbryt",
|
||||||
|
"success": "Tack så mycket! Din feedback har skickats.",
|
||||||
|
"error_send": "Feedback kunde inte skickas. Vänligen försök igen senare.",
|
||||||
|
"error_invalid_email": "Vänligen ange en giltig e-postadress.",
|
||||||
|
"error_not_configured": "Feedback är inte tillgängligt på den här servern.",
|
||||||
|
"error_rate_limited": "För många feedbackmeddelanden på kort tid. Vänligen vänta några minuter.",
|
||||||
|
"error_spam": "Det här meddelandet kunde inte skickas. Vänligen omformulera det."
|
||||||
|
},
|
||||||
|
"demo": {
|
||||||
|
"logbook_title": "Demo loggbok Östersjön",
|
||||||
|
"badge": "Demo",
|
||||||
|
"public_banner": "Skrivskyddad demovy",
|
||||||
|
"cta_register": "Skapa konto",
|
||||||
|
"back_to_login": "Till registreringen"
|
||||||
|
},
|
||||||
|
"invitation": {
|
||||||
|
"error_invalid_key": "Länken till inbjudan är kryptografiskt ogiltig (nyckeln är felaktig).",
|
||||||
|
"error_missing_key": "Länken till inbjudan innehåller ingen dekrypteringsnyckel (#key=...). Vänligen använd den fullständiga länken från ägaren.",
|
||||||
|
"error_expired": "Denna inbjudan har löpt ut (giltig i 48 timmar).",
|
||||||
|
"error_invalid_token": "Inbjudan ogiltig.",
|
||||||
|
"error_load_failed": "Inbjudan kunde inte läsas in.",
|
||||||
|
"error_incomplete_session": "Sessionen är ofullständig - logga in igen (användar-ID saknas).",
|
||||||
|
"error_accept_failed": "Anslutningen misslyckades.",
|
||||||
|
"error_login_failed": "Passkey Inloggningen misslyckades.",
|
||||||
|
"error_username_missing": "Användarnamnet kunde inte fastställas - vänligen logga in igen.",
|
||||||
|
"error_register_failed": "Registreringen misslyckades.",
|
||||||
|
"loading_joining": "Ansluter sig...",
|
||||||
|
"loading_checking": "Inbjudan kommer att kontrolleras...",
|
||||||
|
"loading_unlocking": "Loggboken är upplåst och synkroniserad...",
|
||||||
|
"loading_retrieving_key": "Ladda ner krypteringsnyckel...",
|
||||||
|
"error_title": "Fel i inbjudan",
|
||||||
|
"back_to_start": "Tillbaka till början",
|
||||||
|
"title": "Inbjudan till loggbok",
|
||||||
|
"invited_by": "Inbjudan från",
|
||||||
|
"vessel_logbook": "Fartyg / Loggbok",
|
||||||
|
"signed_in_preparing": "Registrerad som {{username}}. Anslutning förbereds...",
|
||||||
|
"join_again": "Gå med igen",
|
||||||
|
"login_or_register_hint": "Logga in eller registrera ett konto för att gå med i loggboken.",
|
||||||
|
"or_sign_up": "ELLER REGISTRERA DIG IGEN",
|
||||||
|
"register_crew_account": "Skapa ett nytt konto för besättningen",
|
||||||
|
"username_label": "Användarens namn",
|
||||||
|
"create_passkey": "Skapa Passkey.",
|
||||||
|
"switch_language_en": "Engelska",
|
||||||
|
"switch_language_de": "Tysk"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"title": "Statistik",
|
||||||
|
"subtitle": "Översikt över rutter, förbrukning och typ av körning",
|
||||||
|
"scope_label": "Utvärderingsområde",
|
||||||
|
"scope_logbook": "Denna loggbok",
|
||||||
|
"scope_account": "Alla loggböcker",
|
||||||
|
"loading": "Statistiken är beräknad...",
|
||||||
|
"no_data": "Inga resdagar tillgängliga ännu.",
|
||||||
|
"total_distance": "Totalt avstånd",
|
||||||
|
"travel_days": "Resdagar",
|
||||||
|
"sail_distance": "Under segel",
|
||||||
|
"motor_distance": "Maskinens resa",
|
||||||
|
"motor_hours_total": "Totalt antal maskintimmar",
|
||||||
|
"daily_motor_hours": "Maskintimmar per resdag",
|
||||||
|
"avg_motor_hours": "Ø maskintimmar per resdag",
|
||||||
|
"unknown_propulsion": "Okänd",
|
||||||
|
"fuel_total": "Totalt bränsle",
|
||||||
|
"water_total": "Totalt vatten",
|
||||||
|
"daily_etmal": "Dagliga tider",
|
||||||
|
"daily_consumption": "Daglig konsumtion",
|
||||||
|
"route_overview": "Vägbeskrivning",
|
||||||
|
"route_map_title": "Översikt över rutten",
|
||||||
|
"propulsion_title": "Segel vs. maskin",
|
||||||
|
"propulsion_hint": "Fördelningen baseras på loggbokshändelser per resdag, inte på GPS-segment.",
|
||||||
|
"avg_distance": "Ø per resdag",
|
||||||
|
"avg_fuel": "Ø Bränsle",
|
||||||
|
"avg_water": "Ø Vatten",
|
||||||
|
"fuel_per_nm": "Bränsle per sm",
|
||||||
|
"fuel_per_motor_hour": "Bränsle per maskintimme",
|
||||||
|
"daily_fuel_per_motor_hour": "Bränsleförbrukning per maskintimme och resdag",
|
||||||
|
"fuel_legend": "Bränsle",
|
||||||
|
"water_legend": "Vatten",
|
||||||
|
"unit_nm": "sm",
|
||||||
|
"unit_h": "h",
|
||||||
|
"unit_l": "L",
|
||||||
|
"day_label": "Dag {{day}}__.",
|
||||||
|
"account_logbooks": "Loggböcker i en överblick",
|
||||||
|
"col_logbook": "Loggbok"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"skip": "Hoppa över turen",
|
||||||
|
"back": "Tillbaka",
|
||||||
|
"next": "Ytterligare",
|
||||||
|
"finish": "Färdig",
|
||||||
|
"progress": "Steg {{current}} från {{total}}.",
|
||||||
|
"steps": {
|
||||||
|
"welcome": {
|
||||||
|
"title": "Välkommen ombord!",
|
||||||
|
"body": "Vi har skapat en demo-loggbok med tre dagars resa i Kielfjorden åt dig. Du kan när som helst radera exempelposterna om du vill starta din egen loggbok. Den här korta rundturen visar dig de viktigaste funktionerna."
|
||||||
|
},
|
||||||
|
"welcome_public": {
|
||||||
|
"title": "Välkommen ombord!",
|
||||||
|
"body": "Utforska vår demologgbok med tre dagars resor i Kielfjorden - utan konto. Den här korta rundturen visar dig fartygsdata, besättning och loggboksanteckningar."
|
||||||
|
},
|
||||||
|
"nav_logs": {
|
||||||
|
"title": "Loggboksanteckningar",
|
||||||
|
"body": "Det är här du hanterar dina resdagar - avresa, destination, väder, bränslenivåer och GPS-spår."
|
||||||
|
},
|
||||||
|
"entry_list": {
|
||||||
|
"title": "Dina resdagar",
|
||||||
|
"body": "Varje kort representerar en resdag. Tryck på en post för att visa eller redigera detaljer."
|
||||||
|
},
|
||||||
|
"entry_open": {
|
||||||
|
"title": "Öppen resdag",
|
||||||
|
"body": "Så här ser en komplett loggboksanteckning ut - med händelser, tanknivåer och mycket mer."
|
||||||
|
},
|
||||||
|
"entry_track": {
|
||||||
|
"title": "GPS-spårning",
|
||||||
|
"body": "Ladda upp GPX-filer eller visa redan sparade rutter på kartan - inklusive avstånd och hastighet."
|
||||||
|
},
|
||||||
|
"nav_vessel": {
|
||||||
|
"title": "Fartygsdata",
|
||||||
|
"body": "Ange namn, dimensioner och tekniska data för din yacht - fyll i en gång, tillgänglig för alla resdagar."
|
||||||
|
},
|
||||||
|
"nav_crew": {
|
||||||
|
"title": "Besättningslista",
|
||||||
|
"body": "Hantera besättningsmedlemmar och tilldela dem resdagar senare."
|
||||||
|
},
|
||||||
|
"nav_stats": {
|
||||||
|
"title": "Kontrollpanel för statistik",
|
||||||
|
"body": "Här kan du se körsträckor, bränsleförbrukning, ruttkartor och körandelar - automatiskt beräknade från dina loggboksanteckningar."
|
||||||
|
},
|
||||||
|
"nav_feedback": {
|
||||||
|
"title": "Skicka feedback",
|
||||||
|
"body": "Du kan använda det här formuläret för att skicka fel, idéer eller allmän feedback direkt till projektgruppen - även efter rundturen när som helst med hjälp av ikonen längst upp till höger."
|
||||||
|
},
|
||||||
|
"nav_profile": {
|
||||||
|
"title": "Din användarprofil",
|
||||||
|
"body": "Du kommer åt din personliga profil via skipperknappen högst upp - oavsett vilken loggbok som är aktuell."
|
||||||
|
},
|
||||||
|
"profile_preferences": {
|
||||||
|
"title": "Redovisning & presentation",
|
||||||
|
"body": "Här kan du hantera din konto-identitet, ditt tema och ljus/mörker-läge. Du kan när som helst starta om appturen. Passkeys och säkerhetsinställningar hittar du längre ner i profilen."
|
||||||
|
},
|
||||||
|
"finish": {
|
||||||
|
"title": "Okej!",
|
||||||
|
"body": "Du kommer direkt till instrumentpanelen för statistik. Du kan när som helst starta om turen i din användarprofil. Ha en trevlig resa!"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"title": "Kapteins Daagbok - Gratis digital loggbok för båtar (reklamfri)",
|
||||||
|
"description": "Gratis, annonsfri digital loggbok för båtar med kryptering från början till slut och Passkey-inloggning. Dokumentera resdagar, GPS-spår, besättnings- och fartygsdata på ett säkert sätt - även offline som PWA.",
|
||||||
|
"keywords": "Yachtloggbok, skeppsdagbok, ombordloggbok, segling, Passkey, E2E kryptering, GPS-spår, sjöfartsloggbok, gratis, reklamfri, gratis, utan reklam",
|
||||||
|
"ogImageAlt": "Kapteins Daagbok Logotyp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { parseTrackFile } from './trackUpload.js'
|
import { parseTrackFile } from './trackUpload.js'
|
||||||
import { computeTrackStats } from '../utils/trackStats.js'
|
import { computeTrackStats } from '../utils/trackStats.js'
|
||||||
import i18n from '../i18n/index.js'
|
import i18n from '../i18n/index.js'
|
||||||
|
import { isGermanLocale } from '../utils/i18nLanguages.js'
|
||||||
|
|
||||||
import kielLaboeGpx from '../assets/demo/kiel-laboe.gpx?raw'
|
import kielLaboeGpx from '../assets/demo/kiel-laboe.gpx?raw'
|
||||||
import laboeDampGpx from '../assets/demo/laboe-damp.gpx?raw'
|
import laboeDampGpx from '../assets/demo/laboe-damp.gpx?raw'
|
||||||
@@ -59,7 +60,7 @@ export interface PublicDemoFixture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildDemoDays(): DemoDaySpec[] {
|
export function buildDemoDays(): DemoDaySpec[] {
|
||||||
const isDe = i18n.language.startsWith('de')
|
const isDe = isGermanLocale(i18n.language)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
date: '2026-05-29',
|
date: '2026-05-29',
|
||||||
@@ -165,7 +166,7 @@ export function buildDemoDays(): DemoDaySpec[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildDemoYachtData(): Record<string, unknown> {
|
export function buildDemoYachtData(): Record<string, unknown> {
|
||||||
const isDe = i18n.language.startsWith('de')
|
const isDe = isGermanLocale(i18n.language)
|
||||||
return {
|
return {
|
||||||
name: 'Seeadler',
|
name: 'Seeadler',
|
||||||
vesselType: isDe ? 'Segelyacht' : 'Sailing yacht',
|
vesselType: isDe ? 'Segelyacht' : 'Sailing yacht',
|
||||||
@@ -188,7 +189,7 @@ export function buildDemoYachtData(): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildDemoCrewRecords(): DemoCrewRecord[] {
|
export function buildDemoCrewRecords(): DemoCrewRecord[] {
|
||||||
const isDe = i18n.language.startsWith('de')
|
const isDe = isGermanLocale(i18n.language)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
payloadId: 'skipper',
|
payloadId: 'skipper',
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
|
import { normalizeAppLanguage } from './i18nLanguages.js'
|
||||||
|
|
||||||
|
const INTL_LOCALES: Record<string, string> = {
|
||||||
|
de: 'de-DE',
|
||||||
|
en: 'en-GB',
|
||||||
|
da: 'da-DK',
|
||||||
|
sv: 'sv-SE',
|
||||||
|
nb: 'nb-NO'
|
||||||
|
}
|
||||||
|
|
||||||
/** BCP 47 locales that use 24-hour clock for Intl formatting. */
|
/** BCP 47 locales that use 24-hour clock for Intl formatting. */
|
||||||
export function resolveIntlLocale(language?: string): string {
|
export function resolveIntlLocale(language?: string): string {
|
||||||
const lng = (language ?? 'en').toLowerCase()
|
const lng = normalizeAppLanguage(language)
|
||||||
return lng.startsWith('de') ? 'de-DE' : 'en-GB'
|
return INTL_LOCALES[lng] ?? 'en-GB'
|
||||||
}
|
}
|
||||||
|
|
||||||
const APP_DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
|
const APP_DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { getNextLanguage, normalizeAppLanguage, SUPPORTED_LANGUAGES } from './i18nLanguages.js'
|
||||||
|
|
||||||
|
describe('i18nLanguages', () => {
|
||||||
|
it('normalizes regional tags to supported base codes', () => {
|
||||||
|
expect(normalizeAppLanguage('de-DE')).toBe('de')
|
||||||
|
expect(normalizeAppLanguage('nb-NO')).toBe('nb')
|
||||||
|
expect(normalizeAppLanguage('xx')).toBe('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cycles through all supported languages', () => {
|
||||||
|
let current: string = 'de'
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (let i = 0; i < SUPPORTED_LANGUAGES.length; i++) {
|
||||||
|
seen.add(current)
|
||||||
|
current = getNextLanguage(current)
|
||||||
|
}
|
||||||
|
expect(seen.size).toBe(SUPPORTED_LANGUAGES.length)
|
||||||
|
expect(current).toBe('de')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/** Supported UI languages (ISO 639-1, language-only). */
|
||||||
|
export const SUPPORTED_LANGUAGES = ['de', 'en', 'da', 'sv', 'nb'] as const
|
||||||
|
|
||||||
|
export type AppLanguage = (typeof SUPPORTED_LANGUAGES)[number]
|
||||||
|
|
||||||
|
export function normalizeAppLanguage(language?: string): AppLanguage {
|
||||||
|
const base = (language ?? 'en').split('-')[0].toLowerCase()
|
||||||
|
if ((SUPPORTED_LANGUAGES as readonly string[]).includes(base)) {
|
||||||
|
return base as AppLanguage
|
||||||
|
}
|
||||||
|
return 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextLanguage(current?: string): AppLanguage {
|
||||||
|
const active = normalizeAppLanguage(current)
|
||||||
|
const index = SUPPORTED_LANGUAGES.indexOf(active)
|
||||||
|
return SUPPORTED_LANGUAGES[(index + 1) % SUPPORTED_LANGUAGES.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGermanLocale(language?: string): boolean {
|
||||||
|
return normalizeAppLanguage(language) === 'de'
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
import { resolveIntlLocale } from './dateTimeFormat.js'
|
import { resolveIntlLocale } from './dateTimeFormat.js'
|
||||||
import { initSeo, normalizeSeoLang, updatePageSeo } from './seo.js'
|
import { initSeo, normalizeSeoLang, updatePageSeo } from './seo.js'
|
||||||
|
|
||||||
const HTML_LANG = /^de|en$/
|
const SUPPORTED_HTML_LANG = /^de|en|da|sv|nb$/
|
||||||
|
|
||||||
function createMockI18n(language: string): I18nInstance {
|
function createMockI18n(language: string): I18nInstance {
|
||||||
return {
|
return {
|
||||||
@@ -20,7 +20,9 @@ describe('normalizeSeoLang', () => {
|
|||||||
['de-DE', 'de'],
|
['de-DE', 'de'],
|
||||||
['en', 'en'],
|
['en', 'en'],
|
||||||
['en-US', 'en'],
|
['en-US', 'en'],
|
||||||
['en-GB', 'en']
|
['da', 'da'],
|
||||||
|
['sv-SE', 'sv'],
|
||||||
|
['nb-NO', 'nb']
|
||||||
] as const)('maps %s to short code %s', (input, expected) => {
|
] as const)('maps %s to short code %s', (input, expected) => {
|
||||||
expect(normalizeSeoLang(input)).toBe(expected)
|
expect(normalizeSeoLang(input)).toBe(expected)
|
||||||
})
|
})
|
||||||
@@ -35,13 +37,15 @@ describe('updatePageSeo html lang', () => {
|
|||||||
it.each([
|
it.each([
|
||||||
['de', 'de'],
|
['de', 'de'],
|
||||||
['en', 'en'],
|
['en', 'en'],
|
||||||
['en-GB', 'en']
|
['da', 'da'],
|
||||||
|
['sv', 'sv'],
|
||||||
|
['nb', 'nb']
|
||||||
] as const)('sets html lang to %s when i18n language is %s', (i18nLanguage, expectedLang) => {
|
] as const)('sets html lang to %s when i18n language is %s', (i18nLanguage, expectedLang) => {
|
||||||
initSeo(createMockI18n(i18nLanguage))
|
initSeo(createMockI18n(i18nLanguage))
|
||||||
updatePageSeo()
|
updatePageSeo()
|
||||||
|
|
||||||
expect(document.documentElement.lang).toBe(expectedLang)
|
expect(document.documentElement.lang).toBe(expectedLang)
|
||||||
expect(document.documentElement.lang).toMatch(HTML_LANG)
|
expect(document.documentElement.lang).toMatch(SUPPORTED_HTML_LANG)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -49,14 +53,17 @@ describe('resolveIntlLocale', () => {
|
|||||||
it('uses full BCP 47 tags for Intl formatting only', () => {
|
it('uses full BCP 47 tags for Intl formatting only', () => {
|
||||||
expect(resolveIntlLocale('de')).toBe('de-DE')
|
expect(resolveIntlLocale('de')).toBe('de-DE')
|
||||||
expect(resolveIntlLocale('en')).toBe('en-GB')
|
expect(resolveIntlLocale('en')).toBe('en-GB')
|
||||||
|
expect(resolveIntlLocale('da')).toBe('da-DK')
|
||||||
|
expect(resolveIntlLocale('sv')).toBe('sv-SE')
|
||||||
|
expect(resolveIntlLocale('nb')).toBe('nb-NO')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not reuse Intl locale tags for html lang', () => {
|
it('does not reuse Intl locale tags for html lang', () => {
|
||||||
const intlLocale = resolveIntlLocale('en')
|
const intlLocale = resolveIntlLocale('nb')
|
||||||
const htmlLang = normalizeSeoLang('en')
|
const htmlLang = normalizeSeoLang('nb')
|
||||||
|
|
||||||
expect(intlLocale).toBe('en-GB')
|
expect(intlLocale).toBe('nb-NO')
|
||||||
expect(htmlLang).toBe('en')
|
expect(htmlLang).toBe('nb')
|
||||||
expect(htmlLang).not.toBe(intlLocale)
|
expect(htmlLang).not.toBe(intlLocale)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+12
-4
@@ -1,13 +1,22 @@
|
|||||||
import type { i18n as I18nInstance } from 'i18next'
|
import type { i18n as I18nInstance } from 'i18next'
|
||||||
|
import { normalizeAppLanguage, type AppLanguage } from './i18nLanguages.js'
|
||||||
|
|
||||||
const SITE_ORIGIN = 'https://kapteins-daagbok.eu'
|
const SITE_ORIGIN = 'https://kapteins-daagbok.eu'
|
||||||
|
|
||||||
export type SeoLang = 'de' | 'en'
|
export type SeoLang = AppLanguage
|
||||||
|
|
||||||
|
const OG_LOCALES: Record<SeoLang, string> = {
|
||||||
|
de: 'de_DE',
|
||||||
|
en: 'en_GB',
|
||||||
|
da: 'da_DK',
|
||||||
|
sv: 'sv_SE',
|
||||||
|
nb: 'nb_NO'
|
||||||
|
}
|
||||||
|
|
||||||
let i18nRef: I18nInstance | null = null
|
let i18nRef: I18nInstance | null = null
|
||||||
|
|
||||||
export function normalizeSeoLang(lng: string): SeoLang {
|
export function normalizeSeoLang(lng: string): SeoLang {
|
||||||
return lng.startsWith('de') ? 'de' : 'en'
|
return normalizeAppLanguage(lng)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMeta(attr: 'name' | 'property', key: string, content: string) {
|
function setMeta(attr: 'name' | 'property', key: string, content: string) {
|
||||||
@@ -47,8 +56,7 @@ export function updatePageSeo(lng?: string) {
|
|||||||
setMeta('name', 'keywords', keywords)
|
setMeta('name', 'keywords', keywords)
|
||||||
setMeta('property', 'og:title', title)
|
setMeta('property', 'og:title', title)
|
||||||
setMeta('property', 'og:description', description)
|
setMeta('property', 'og:description', description)
|
||||||
setMeta('property', 'og:locale', lang === 'de' ? 'de_DE' : 'en_US')
|
setMeta('property', 'og:locale', OG_LOCALES[lang])
|
||||||
setMeta('property', 'og:locale:alternate', lang === 'de' ? 'en_US' : 'de_DE')
|
|
||||||
setMeta('name', 'twitter:title', title)
|
setMeta('name', 'twitter:title', title)
|
||||||
setMeta('name', 'twitter:description', description)
|
setMeta('name', 'twitter:description', description)
|
||||||
setMeta('property', 'og:image:alt', imageAlt)
|
setMeta('property', 'og:image:alt', imageAlt)
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="da">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Kapteins Daagbok - Beta-flyer</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
størrelse: A4 stående;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
polstring: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
bredde: 210mm;
|
||||||
|
højde: 297 mm;
|
||||||
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
farve: #e2e8f0;
|
||||||
|
baggrund: #0f172a;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side {
|
||||||
|
bredde: 210mm;
|
||||||
|
højde: 297mm;
|
||||||
|
max-højde: 297mm;
|
||||||
|
polstring: 12mm 15mm 10mm;
|
||||||
|
display: flex;
|
||||||
|
flex-retning: kolonne;
|
||||||
|
mellemrum: 5 mm;
|
||||||
|
baggrund:
|
||||||
|
radial-gradient(ellipse 120% 80% ved 100% 0%, rgba(56, 189, 248, 0.12) 0%, transparent 55%),
|
||||||
|
radial-gradient(ellipse 90% 60% ved 0% 100%, rgba(134, 59, 255, 0.14) 0%, transparent 50%),
|
||||||
|
lineær-gradient(165deg, #0f172a 0%, #1e293b 45%, #0f172a 100%);
|
||||||
|
position: relative;
|
||||||
|
overløb: skjult;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before {
|
||||||
|
indhold: "";
|
||||||
|
position: absolut;
|
||||||
|
indsat: 8 mm;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 4mm;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
mellemrum: 5mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
bredde: 16mm;
|
||||||
|
højde: 16mm;
|
||||||
|
flex-krymp: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block h1 {
|
||||||
|
skriftstørrelse: 23pt;
|
||||||
|
font-vægt: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
farve: #f8fafc;
|
||||||
|
linjehøjde: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block p {
|
||||||
|
skriftstørrelse: 12pt;
|
||||||
|
farve: #94a3b8;
|
||||||
|
margin-top: 1,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
baggrund: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||||
|
farve: #1e293b;
|
||||||
|
skriftstørrelse: 11pt;
|
||||||
|
font-vægt: 800;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
polstring: 2,5 mm 4,5 mm;
|
||||||
|
border-radius: 2mm;
|
||||||
|
tekst-transformation: store bogstaver;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
skriftstørrelse: 12pt;
|
||||||
|
linjehøjde: 1,5;
|
||||||
|
farve: #cbd5e1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-bredde: 95%;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro strong {
|
||||||
|
farve: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshots {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
mellemrum: 3 mm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-kort {
|
||||||
|
border-radius: 2,5 mm;
|
||||||
|
overløb: skjult;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
baggrund: rgba(15, 23, 42, 0.55);
|
||||||
|
display: flex;
|
||||||
|
flex-retning: kolonne;
|
||||||
|
min-bredde: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-kort img {
|
||||||
|
bredde: 100%;
|
||||||
|
højde: 50mm;
|
||||||
|
object-fit: contain;
|
||||||
|
object-position: top center;
|
||||||
|
display: block;
|
||||||
|
baggrund: #0b1220;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-caption {
|
||||||
|
skriftstørrelse: 9pt;
|
||||||
|
farve: #94a3b8;
|
||||||
|
tekstjustering: center;
|
||||||
|
polstring: 1,5 mm 2 mm;
|
||||||
|
linjehøjde: 1,3;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funktioner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-kolonner: 1fr 1fr;
|
||||||
|
mellemrum: 1,8 mm 6 mm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
display: flex;
|
||||||
|
mellemrum: 2,5 mm;
|
||||||
|
align-items: flex-start;
|
||||||
|
skriftstørrelse: 10.5pt;
|
||||||
|
linjehøjde: 1,28;
|
||||||
|
farve: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
farve: #38bdf8;
|
||||||
|
font-vægt: 700;
|
||||||
|
flex-krymp: 0;
|
||||||
|
bredde: 4mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-liste {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
mellemrum: 1,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
mellemrum: 1,2 mm;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-flag {
|
||||||
|
display: inline-block;
|
||||||
|
bredde: 5 mm;
|
||||||
|
højde: 3,5 mm;
|
||||||
|
border-radius: 0,3 mm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 0 0 0.15mm rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-sep {
|
||||||
|
farve: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box {
|
||||||
|
baggrund: rgba(30, 41, 59, 0.85);
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||||
|
border-left: 3px solid #fbbf24;
|
||||||
|
border-radius: 3mm;
|
||||||
|
polstring: 5mm 6mm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box h2 {
|
||||||
|
skriftstørrelse: 12,5pt;
|
||||||
|
farve: #fbbf24;
|
||||||
|
margin-bund: 2mm;
|
||||||
|
font-vægt: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box p {
|
||||||
|
skriftstørrelse: 10,5pt;
|
||||||
|
linjehøjde: 1,5;
|
||||||
|
farve: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
mellemrum: 7mm;
|
||||||
|
baggrund: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 4mm;
|
||||||
|
polstring: 5 mm 6 mm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr {
|
||||||
|
bredde: 32mm;
|
||||||
|
højde: 32mm;
|
||||||
|
baggrund: #fff;
|
||||||
|
polstring: 2 mm;
|
||||||
|
border-radius: 2 mm;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr img {
|
||||||
|
width: 100%;
|
||||||
|
højde: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-tekst h3 {
|
||||||
|
skriftstørrelse: 14,5pt;
|
||||||
|
farve: #38bdf8;
|
||||||
|
font-vægt: 700;
|
||||||
|
margin-bund: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text p {
|
||||||
|
skriftstørrelse: 11pt;
|
||||||
|
farve: #94a3b8;
|
||||||
|
linjehøjde: 1,5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
mellemrum: 2mm;
|
||||||
|
margin-top: 3mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
skriftstørrelse: 9,5pt;
|
||||||
|
font-vægt: 600;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
tekst-transformation: store bogstaver;
|
||||||
|
farve: #64748b;
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.4);
|
||||||
|
border-radius: 1,5 mm;
|
||||||
|
polstring: 1 mm 2,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidefod {
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.15);
|
||||||
|
padding-top: 3mm;
|
||||||
|
margin-top: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
skriftstørrelse: 9,5pt;
|
||||||
|
linjehøjde: 1,5;
|
||||||
|
farve: #64748b;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidefod strong {
|
||||||
|
farve: #94a3b8;
|
||||||
|
font-vægt: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article class="page">
|
||||||
|
<header>
|
||||||
|
<img class="logo" src="../../client/public/logo.png" alt="Kapteins Daagbok" />
|
||||||
|
<div class="title-block">
|
||||||
|
<h1>Kapteins Daagbok</h1>
|
||||||
|
<p>Digital yachtlogbog - gratis og reklamefri</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge">Beta</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
Opbevar din logbog om bord digitalt: rejsedage, GPS-spor, besætnings- og skibsdata
|
||||||
|
<strong>End-to-end-krypteret</strong>kan installeres som en app og
|
||||||
|
<strong>også offline</strong> kan bruges til søs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="features" aria-label="Funktionen">
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Rejsedage i nautisk logbogsformat (havn, vejr, sejl, besætning, brændstofniveauer)</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Offline-kompatibel PWA - kører på enhver smartphone og tablet</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Simpelt login uden adgangskode Passkey.</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Ende-til-ende-kryptering</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Upload af GPS-spor (GPX/KML) med kortvisning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Rute-statistik</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Vedhæftede billeder pr. rejsedag</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Fotoavatarbilleder til skipper og besætning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Inviter besætningen - arbejd sammen om logbogen</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>PDF- & CSV-Export</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Krypteret sikkerhedskopiering og gendannelse</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Del logbog med venner</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Et vilkårligt antal skibe og logbøger</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span class="lang-list"><span class="lang-item"><svg class="feature-flag" viewBox="0 0 37 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="37" height="28" fill="#C8102E"/><rect x="12" width="4" height="28" fill="#fff"/><rect y="12" width="37" height="4" fill="#fff"/></svg>Dansk</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Engelsk</span></span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>3 temaer, hver med en lys og en mørk variant</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Fremstillet i Kiel.Sailing.City..</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screenshots" aria-label="App-Screenshots">
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-login.png" alt="Anmeldung mit Passkey und Demo" />
|
||||||
|
<figcaption class="screenshot-caption">Registrering & Passkey</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-logbook.png" alt="Logbuch-Journal mit Reisetagen" />
|
||||||
|
<figcaption class="screenshot-caption">Logbogsdagbog</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-vessel.png" alt="Schiffs-Stammdaten mit Yachtfoto" />
|
||||||
|
<figcaption class="screenshot-caption">Skibsdata</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="beta-box">
|
||||||
|
<h2>Betafase - din feedback tæller</h2>
|
||||||
|
<p>
|
||||||
|
Kapteins Daagbok er en<strong>Privat hobbyprojekt uden fortjeneste for øje</strong>.
|
||||||
|
Som betatester er du med til at forbedre appen for skippere og besætninger i hverdagen - feedback er meget velkommen.
|
||||||
|
Feedback er yderst velkommen.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cta">
|
||||||
|
<div class="qr">
|
||||||
|
<img src="assets/qr-kapteins-daagbok.eu.png" alt="QR-Code: kapteins-daagbok.eu" />
|
||||||
|
</div>
|
||||||
|
<div class="cta-text">
|
||||||
|
<h3>kapteins-daagbok.eu</h3>
|
||||||
|
<p>Åbn i browseren, eller tilføj som en app til startskærmen. Registrer dig hos Passkey - der kræves ingen app store.</p>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">Helt gratis</span>
|
||||||
|
<span class="tag">Gratis reklame</span>
|
||||||
|
<span class="tag">E2E-krypteret</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<strong>Påtryk</strong><br />
|
||||||
|
KnorrLabs - Markus F.J. Busche - Knorrstr. 16 · 24106 Kiel - elpatron+kd@mailbox.org.
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -328,7 +328,7 @@
|
|||||||
<div class="feature"><span class="feature-icon">✦</span><span>Verschlüsseltes Backup & Wiederherstellung</span></div>
|
<div class="feature"><span class="feature-icon">✦</span><span>Verschlüsseltes Backup & Wiederherstellung</span></div>
|
||||||
<div class="feature"><span class="feature-icon">✦</span><span>Logbuch mit Freunden teilen</span></div>
|
<div class="feature"><span class="feature-icon">✦</span><span>Logbuch mit Freunden teilen</span></div>
|
||||||
<div class="feature"><span class="feature-icon">✦</span><span>Beliebig viele Schiffe und Logbücher</span></div>
|
<div class="feature"><span class="feature-icon">✦</span><span>Beliebig viele Schiffe und Logbücher</span></div>
|
||||||
<div class="feature"><span class="feature-icon">✦</span><span class="lang-list"><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Englisch</span></span></div>
|
<div class="feature"><span class="feature-icon">✦</span><span class="lang-list"><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">·</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Englisch</span><span class="lang-sep">·</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 37 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="37" height="28" fill="#C8102E"/><rect x="12" width="4" height="28" fill="#fff"/><rect y="12" width="37" height="4" fill="#fff"/></svg>Dansk</span><span class="lang-sep">·</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 16 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="16" height="10" fill="#006AA7"/><rect x="5" width="2" height="10" fill="#FECC00"/><rect y="4" width="16" height="2" fill="#FECC00"/></svg>Svenska</span><span class="lang-sep">·</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 22 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="22" height="16" fill="#BA0C2F"/><rect x="6" width="4" height="16" fill="#fff"/><rect y="6" width="22" height="4" fill="#fff"/><rect x="7" width="2" height="16" fill="#00205B"/><rect y="7" width="22" height="2" fill="#00205B"/></svg>Norsk</span></span></div>
|
||||||
<div class="feature"><span class="feature-icon">✦</span><span>3 Themes, jeweils mit heller und dunkler Variante</span></div>
|
<div class="feature"><span class="feature-icon">✦</span><span>3 Themes, jeweils mit heller und dunkler Variante</span></div>
|
||||||
<div class="feature"><span class="feature-icon">✦</span><span>Crafted in Kiel.Sailing.City.</span></div>
|
<div class="feature"><span class="feature-icon">✦</span><span>Crafted in Kiel.Sailing.City.</span></div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nb">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Kapteins Daagbok - Beta-flygeblad</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
størrelse: A4 stående;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
polstring: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
bredde: 210mm;
|
||||||
|
høyde: 297 mm;
|
||||||
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
farge: #e2e8f0;
|
||||||
|
bakgrunn: #0f172a;
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
bredde: 210mm;
|
||||||
|
høyde: 297mm;
|
||||||
|
max-høyde: 297mm;
|
||||||
|
polstring: 12mm 15mm 10mm;
|
||||||
|
display: flex;
|
||||||
|
flex-retning: kolonne;
|
||||||
|
mellomrom: 5 mm
|
||||||
|
bakgrunn:
|
||||||
|
radial-gradient(ellipse 120% 80% ved 100% 0%, rgba(56, 189, 248, 0.12) 0%, transparent 55%),
|
||||||
|
radial-gradient(ellipse 90% 60% på 0% 100%, rgba(134, 59, 255, 0.14) 0%, gjennomsiktig 50%),
|
||||||
|
lineær-gradient(165deg, #0f172a 0%, #1e293b 45%, #0f172a 100%);
|
||||||
|
posisjon: relativ;
|
||||||
|
overflow: skjult;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before {
|
||||||
|
innhold: "";
|
||||||
|
posisjon: absolutt
|
||||||
|
inset: 8mm;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 4mm;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
mellomrom: 5 mm
|
||||||
|
flex-krymp: 0;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
bredde: 16mm;
|
||||||
|
høyde: 16mm;
|
||||||
|
flex-krymp: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block h1 {
|
||||||
|
skriftstørrelse: 23pt;
|
||||||
|
font-weight: 700;
|
||||||
|
bokstavavstand: -0.02em;
|
||||||
|
farge: #f8fafc;
|
||||||
|
linjehøyde: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block p {
|
||||||
|
skriftstørrelse: 12pt;
|
||||||
|
farge: #94a3b8;
|
||||||
|
margin-topp: 1,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
margin-left: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
bakgrunn: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||||
|
farge: #1e293b;
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: 800;
|
||||||
|
bokstavavstand: 0,12em;
|
||||||
|
polstring: 2,5 mm 4,5 mm;
|
||||||
|
border-radius: 2mm;
|
||||||
|
tekst-transformasjon: versaler;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
skriftstørrelse: 12pt;
|
||||||
|
linjehøyde: 1,5
|
||||||
|
farge: #cbd5e1;
|
||||||
|
flex-krymp: 0;
|
||||||
|
max-width: 95%;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro strong {
|
||||||
|
farge: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skjermbilder {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
mellomrom: 3 mm
|
||||||
|
flex-krymp: 0;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skjermbilde-kort {
|
||||||
|
border-radius: 2,5 mm;
|
||||||
|
overflow: skjult;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
bakgrunn: rgba(15, 23, 42, 0.55);
|
||||||
|
display: flex;
|
||||||
|
flex-retning: kolonne;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-card img {
|
||||||
|
bredde: 100 %;
|
||||||
|
høyde: 50mm;
|
||||||
|
object-fit: contain;
|
||||||
|
objekt-posisjon: øverst i midten;
|
||||||
|
display: block;
|
||||||
|
bakgrunn: #0b1220;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenshot-caption {
|
||||||
|
font-size: 9pt;
|
||||||
|
farge: #94a3b8;
|
||||||
|
tekstjustering: midt;
|
||||||
|
polstring: 1.5mm 2mm;
|
||||||
|
linjehøyde: 1.3
|
||||||
|
flex-krymping: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features {
|
||||||
|
display: grid;
|
||||||
|
grid-template-kolonner: 1fr 1fr;
|
||||||
|
gap: 1,8 mm 6 mm
|
||||||
|
flex-krymp: 0;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
display: flex;
|
||||||
|
mellomrom: 2,5 mm;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 10.5pt;
|
||||||
|
linjehøyde: 1,28
|
||||||
|
farge: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
farge: #38bdf8;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-krymp: 0;
|
||||||
|
bredde: 4mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-list {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
mellomrom: 1,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
mellomrom: 1,2 mm;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-flag {
|
||||||
|
display: inline-block;
|
||||||
|
bredde: 5 mm;
|
||||||
|
høyde: 3,5 mm;
|
||||||
|
border-radius: 0,3 mm
|
||||||
|
flex-krymping: 0;
|
||||||
|
box-shadow: 0 0 0 0 0,15mm rgba(0, 0, 0, 0, 0,25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-sep {
|
||||||
|
farge: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box {
|
||||||
|
bakgrunn: rgba(30, 41, 59, 0.85);
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||||
|
border-left: 3px solid #fbbf24;
|
||||||
|
border-radius: 3mm;
|
||||||
|
polstring: 5mm 6mm
|
||||||
|
flex-krymp: 0;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box h2 {
|
||||||
|
skriftstørrelse: 12,5pt;
|
||||||
|
farge: #fbbf24;
|
||||||
|
margin-bunn: 2 mm
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box p {
|
||||||
|
skriftstørrelse: 10,5pt;
|
||||||
|
linjehøyde: 1,5
|
||||||
|
farge: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
mellomrom: 7mm;
|
||||||
|
bakgrunn: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
border-radius: 4mm;
|
||||||
|
polstring: 5 mm 6 mm
|
||||||
|
flex-krymp: 0;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr {
|
||||||
|
bredde: 32mm;
|
||||||
|
høyde: 32mm;
|
||||||
|
bakgrunn: #fff;
|
||||||
|
polstring: 2mm;
|
||||||
|
border-radius: 2mm;
|
||||||
|
flex-krymp: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr img {
|
||||||
|
bredde: 100 %;
|
||||||
|
høyde: 100 %;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text h3 {
|
||||||
|
skriftstørrelse: 14,5pt;
|
||||||
|
farge: #38bdf8;
|
||||||
|
font-vekt: 700
|
||||||
|
margin-bunn: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text p {
|
||||||
|
skriftstørrelse: 11pt;
|
||||||
|
farge: #94a3b8;
|
||||||
|
linjehøyde: 1,5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
mellomrom: 2mm;
|
||||||
|
margin-top: 3mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
skriftstørrelse: 9,5pt;
|
||||||
|
font-weight: 600;
|
||||||
|
bokstavavstand: 0.04em;
|
||||||
|
tekst-transformasjon: store bokstaver;
|
||||||
|
farge: #64748b;
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.4);
|
||||||
|
border-radius: 1,5 mm
|
||||||
|
polstring: 1 mm 2,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
bunntekst {
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.15);
|
||||||
|
padding-top: 3mm;
|
||||||
|
margin-topp: auto;
|
||||||
|
flex-krymp: 0;
|
||||||
|
skriftstørrelse: 9,5pt;
|
||||||
|
linjehøyde: 1,5
|
||||||
|
farge: #64748b;
|
||||||
|
posisjon: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bunntekst strong {
|
||||||
|
farge: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article class="page">
|
||||||
|
<header>
|
||||||
|
<img class="logo" src="../../client/public/logo.png" alt="Kapteins Daagbok" />
|
||||||
|
<div class="title-block">
|
||||||
|
<h1>Kapteins Daagbok</h1>
|
||||||
|
<p>Digital loggbok for fritidsbåter - gratis og reklamefri</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge">Beta</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
Før loggboken om bord digitalt: reisedager, GPS-spor, mannskaps- og skipsdata
|
||||||
|
<strong>Ende-til-ende-kryptert</strong>kan installeres som en app og
|
||||||
|
<strong>også offline</strong> kan brukes til sjøs.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="features" aria-label="Funktionen">
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Reisedager i nautisk loggbokformat (havn, vær, seil, mannskap, drivstoffnivå)</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Offline-kompatibel PWA - kjører på alle smarttelefoner og nettbrett</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Enkel passordfri Passkey-pålogging</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Ende-til-ende-kryptering</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Opplasting av GPS-spor (GPX/KML) med kartvisning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Rutestatistikk</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Fotobilag per reisedag</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Avatarbilder for skipper og mannskap</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Inviter mannskapet - samarbeid om loggboken</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>PDF- & CSV-Export</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Kryptert sikkerhetskopiering og gjenoppretting</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Del loggboken med venner</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Et hvilket som helst antall skip og loggbøker</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span class="lang-list"><span class="lang-item"><svg class="feature-flag" viewBox="0 0 22 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="22" height="16" fill="#BA0C2F"/><rect x="6" width="4" height="16" fill="#fff"/><rect y="6" width="22" height="4" fill="#fff"/><rect x="7" width="2" height="16" fill="#00205B"/><rect y="7" width="22" height="2" fill="#00205B"/></svg>Norsk</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Engelsk</span></span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>3 temaer, hvert med en lys og en mørk variant</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Laget i Kiel.Sailing.City..</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screenshots" aria-label="App-Screenshots">
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-login.png" alt="Anmeldung mit Passkey und Demo" />
|
||||||
|
<figcaption class="screenshot-caption">Registrering & Passkey</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-logbook.png" alt="Logbuch-Journal mit Reisetagen" />
|
||||||
|
<figcaption class="screenshot-caption">Loggbokdagbok</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-vessel.png" alt="Schiffs-Stammdaten mit Yachtfoto" />
|
||||||
|
<figcaption class="screenshot-caption">Skipsdata</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="beta-box">
|
||||||
|
<h2>Betafasen - dine tilbakemeldinger teller</h2>
|
||||||
|
<p>
|
||||||
|
Kapteins Daagbok er en<strong>Privat hobbyprosjekt uten profitthensikt</strong>.
|
||||||
|
Som betatester bidrar du til å forbedre appen for skippere og mannskap i hverdagen - tilbakemeldinger er hjertelig velkomne.
|
||||||
|
Tilbakemeldinger er hjertelig velkomne.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cta">
|
||||||
|
<div class="qr">
|
||||||
|
<img src="assets/qr-kapteins-daagbok.eu.png" alt="QR-Code: kapteins-daagbok.eu" />
|
||||||
|
</div>
|
||||||
|
<div class="cta-text">
|
||||||
|
<h3>kapteins-daagbok.eu</h3>
|
||||||
|
<p>Åpne i nettleseren eller legg til som en app på startskjermen. Registrer deg med Passkey - ingen appbutikk er nødvendig.</p>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">Kostnadsfritt</span>
|
||||||
|
<span class="tag">Reklame gratis</span>
|
||||||
|
<span class="tag">E2E-kryptert</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<strong>Avtrykk</strong><br />
|
||||||
|
KnorrLabs - Markus F.J. Busche - Knorrstr. 16 · 24106 Kiel - elpatron+kd@mailbox.org - Knorrstr. 16 · 24106 Kiel - elpatron+kd@mailbox.org
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="sv">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Kapteins Daagbok - Beta-flygblad</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
storlek: A4 stående;
|
||||||
|
marginal: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-storlek: border-box;
|
||||||
|
marginal: 0;
|
||||||
|
stoppning: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, kropp {
|
||||||
|
bredd: 210 mm;
|
||||||
|
höjd: 297 mm;
|
||||||
|
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
färg: #e2e8f0;
|
||||||
|
bakgrund: #0f172a;
|
||||||
|
-webkit-print-color-adjust: exakt;
|
||||||
|
justering av utskriftsfärg: exakt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sida {
|
||||||
|
bredd: 210 mm;
|
||||||
|
höjd: 297 mm;
|
||||||
|
max-höjd: 297 mm;
|
||||||
|
stoppning: 12mm 15mm 10mm;
|
||||||
|
display: flex;
|
||||||
|
flex-riktning: kolumn;
|
||||||
|
mellanrum: 5mm;
|
||||||
|
bakgrund:
|
||||||
|
radial-gradient(ellips 120% 80% vid 100% 0%, rgba(56, 189, 248, 0.12) 0%, transparent 55%),
|
||||||
|
radial-gradient(ellips 90% 60% på 0% 100%, rgba(134, 59, 255, 0.14) 0%, transparent 50%),
|
||||||
|
linjär-gradient(165deg, #0f172a 0%, #1e293b 45%, #0f172a 100%);
|
||||||
|
position: relativ;
|
||||||
|
överflöd: dold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page::before {
|
||||||
|
innehåll: "";
|
||||||
|
position: absolut;
|
||||||
|
inskjutet: 8mm;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
gränsradie: 4mm;
|
||||||
|
pekare-händelser: ingen;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidhuvud {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
mellanrum: 5mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
bredd: 16mm;
|
||||||
|
höjd: 16 mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
objektanpassning: innehålla;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block h1 {
|
||||||
|
teckenstorlek: 23pt;
|
||||||
|
typsnittsvikt: 700;
|
||||||
|
bokstavsavstånd: -0,02em;
|
||||||
|
färg: #f8fafc;
|
||||||
|
Linjehöjd: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block p {
|
||||||
|
teckenstorlek: 12pt;
|
||||||
|
färg: #94a3b8;
|
||||||
|
marginal-topp: 1,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
marginal-vänster: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
bakgrund: linjär-gradient(135deg, #fbbf24 0%, #f59e0b 100%);
|
||||||
|
färg: #1e293b;
|
||||||
|
font-storlek: 11pt;
|
||||||
|
typsnittsvikt: 800;
|
||||||
|
bokstavsavstånd: 0.12em;
|
||||||
|
stoppning: 2,5 mm 4,5 mm;
|
||||||
|
gränsradie: 2mm;
|
||||||
|
text-transform: versaler;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
teckenstorlek: 12pt;
|
||||||
|
radhöjd: 1,5;
|
||||||
|
färg: #cbd5e1;
|
||||||
|
flex-krympning: 0;
|
||||||
|
max-bredd: 95%;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro strong {
|
||||||
|
färg: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skärmdumpar {
|
||||||
|
display: rutnät;
|
||||||
|
grid-template-kolumner: repeat(3, 1fr);
|
||||||
|
mellanrum: 3mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skärmdump-kort {
|
||||||
|
gränsradie: 2,5 mm;
|
||||||
|
överflöde: dold;
|
||||||
|
kant: 1px solid rgba(148, 163, 184, 0.22);
|
||||||
|
bakgrund: rgba(15, 23, 42, 0.55);
|
||||||
|
display: flex;
|
||||||
|
flex-riktning: kolumn;
|
||||||
|
min-bredd: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skärmdumpskort img {
|
||||||
|
bredd: 100%;
|
||||||
|
höjd: 50mm;
|
||||||
|
objektanpassning: innehålla;
|
||||||
|
objekt-position: övre mitten;
|
||||||
|
display: block;
|
||||||
|
bakgrund: #0b1220;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skärmdump-bildtext {
|
||||||
|
teckenstorlek: 9pt;
|
||||||
|
färg: #94a3b8;
|
||||||
|
text-align: center;
|
||||||
|
stoppning: 1,5 mm 2 mm;
|
||||||
|
Linjehöjd: 1,3;
|
||||||
|
flex-krympning: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funktioner {
|
||||||
|
display: rutnät;
|
||||||
|
grid-template-kolumner: 1fr 1fr;
|
||||||
|
mellanrum: 1,8mm 6mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
display: flex;
|
||||||
|
mellanrum: 2,5 mm;
|
||||||
|
linje-objekt: flex-start;
|
||||||
|
teckenstorlek: 10,5pt;
|
||||||
|
radhöjd: 1,28;
|
||||||
|
färg: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-ikon {
|
||||||
|
färg: #38bdf8;
|
||||||
|
typsnittsvikt: 700;
|
||||||
|
flex-krympning: 0;
|
||||||
|
bredd: 4 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-lista {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
linje-punkter: mitt;
|
||||||
|
mellanrum: 1,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-objekt {
|
||||||
|
display: inline-flex;
|
||||||
|
anpassa objekt: mitt;
|
||||||
|
mellanrum: 1,2 mm;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-flag {
|
||||||
|
display: inline-block;
|
||||||
|
bredd: 5mm;
|
||||||
|
höjd: 3,5 mm;
|
||||||
|
gränsradie: 0,3 mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
box-shadow: 0 0 0 0,15mm rgba(0, 0, 0, 0, 0,25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-sep {
|
||||||
|
färg: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box {
|
||||||
|
bakgrund: rgba(30, 41, 59, 0,85);
|
||||||
|
border: 1px solid rgba(251, 191, 36, 0.35);
|
||||||
|
gräns-vänster: 3px solid #fbbf24;
|
||||||
|
gränsradie: 3mm;
|
||||||
|
stoppning: 5mm 6mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box h2 {
|
||||||
|
teckenstorlek: 12,5pt;
|
||||||
|
färg: #fbbf24;
|
||||||
|
marginal-botten: 2mm;
|
||||||
|
typsnittsvikt: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta-box p {
|
||||||
|
teckenstorlek: 10,5pt;
|
||||||
|
Linjehöjd: 1,5;
|
||||||
|
färg: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta {
|
||||||
|
display: flex;
|
||||||
|
justera-objekt: mitt;
|
||||||
|
mellanrum: 7mm;
|
||||||
|
bakgrund: rgba(15, 23, 42, 0.6);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
gränsradie: 4mm;
|
||||||
|
stoppning: 5mm 6mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr {
|
||||||
|
bredd: 32mm;
|
||||||
|
höjd: 32 mm;
|
||||||
|
bakgrund: #fff;
|
||||||
|
stoppning: 2mm;
|
||||||
|
gräns-radie: 2mm;
|
||||||
|
flex-krympning: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr img {
|
||||||
|
bredd: 100%;
|
||||||
|
höjd: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text h3 {
|
||||||
|
teckenstorlek: 14,5pt;
|
||||||
|
färg: #38bdf8;
|
||||||
|
typsnittsvikt: 700;
|
||||||
|
marginal-botten: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text p {
|
||||||
|
teckenstorlek: 11pt;
|
||||||
|
färg: #94a3b8;
|
||||||
|
Linjehöjd: 1,5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
mellanrum: 2mm;
|
||||||
|
marginal-topp: 3mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
teckenstorlek: 9,5pt;
|
||||||
|
typsnittsvikt: 600;
|
||||||
|
bokstavsavstånd: 0.04em;
|
||||||
|
text-transform: versaler;
|
||||||
|
färg: #64748b;
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.4);
|
||||||
|
gränsradie: 1,5 mm;
|
||||||
|
stoppning: 1 mm 2,5 mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidfot {
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.15);
|
||||||
|
stoppning-topp: 3mm;
|
||||||
|
marginal-topp: auto;
|
||||||
|
flex-krympning: 0;
|
||||||
|
teckenstorlek: 9,5pt;
|
||||||
|
Linjehöjd: 1,5;
|
||||||
|
färg: #64748b;
|
||||||
|
position: relativ;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidfot strong {
|
||||||
|
färg: #94a3b8;
|
||||||
|
typsnittsvikt: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<article class="page">
|
||||||
|
<header>
|
||||||
|
<img class="logo" src="../../client/public/logo.png" alt="Kapteins Daagbok" />
|
||||||
|
<div class="title-block">
|
||||||
|
<h1>Kapteins Daagbok</h1>
|
||||||
|
<p>Digital loggbok för båtar - gratis & reklamfri</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge">Beta</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<p class="intro">
|
||||||
|
Förvara din loggbok ombord digitalt: resdagar, GPS-spår, besättnings- och fartygsdata
|
||||||
|
<strong>End-to-end-kryptering</strong>kan installeras som en app och
|
||||||
|
<strong>också offline</strong> användbar till sjöss.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="features" aria-label="Funktionen">
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Resdagar i nautiskt loggboksformat (hamn, väder, segel, besättning, bränslenivåer)</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Offline-kompatibel PWA - körs på alla smartphones och surfplattor</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Enkel lösenordsfri Passkey-inloggning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>End-to-end-kryptering</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Uppladdning av GPS-spår (GPX/KML) med kartvisning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Statistik över rutter</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Fotobilagor per resdag</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Fotoavatarbilder för skeppare och besättning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Bjud in besättningen - arbeta tillsammans med loggboken</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>PDF- & CSV-Export</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Krypterad säkerhetskopiering och återställning</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Dela loggbok med vänner</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Valfritt antal fartyg och loggböcker</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span class="lang-list"><span class="lang-item"><svg class="feature-flag" viewBox="0 0 16 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="16" height="10" fill="#006AA7"/><rect x="5" width="2" height="10" fill="#FECC00"/><rect y="4" width="16" height="2" fill="#FECC00"/></svg>Svenska</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Engelsk</span></span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>3 teman, vart och ett med en ljus och en mörk variant</span></div>
|
||||||
|
<div class="feature"><span class="feature-icon">✦</span><span>Tillverkad i Kiel.Sailing.City..</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="screenshots" aria-label="App-Screenshots">
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-login.png" alt="Anmeldung mit Passkey und Demo" />
|
||||||
|
<figcaption class="screenshot-caption">Registrering & Passkey</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-logbook.png" alt="Logbuch-Journal mit Reisetagen" />
|
||||||
|
<figcaption class="screenshot-caption">Loggboksjournal</figcaption>
|
||||||
|
</figure>
|
||||||
|
<figure class="screenshot-card">
|
||||||
|
<img src="assets/screenshot-vessel.png" alt="Schiffs-Stammdaten mit Yachtfoto" />
|
||||||
|
<figcaption class="screenshot-caption">Fartygsdata</figcaption>
|
||||||
|
</figure>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="beta-box">
|
||||||
|
<h2>Betafas - din feedback är viktig</h2>
|
||||||
|
<p>
|
||||||
|
Kapteins Daagbok är en<strong>Privat hobbyprojekt utan vinstsyfte</strong>.
|
||||||
|
Som betatestare hjälper du till att förbättra appen för skeppare och besättning i vardagen - feedback är uttryckligen välkommen.
|
||||||
|
Feedback är uttryckligen välkommen.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cta">
|
||||||
|
<div class="qr">
|
||||||
|
<img src="assets/qr-kapteins-daagbok.eu.png" alt="QR-Code: kapteins-daagbok.eu" />
|
||||||
|
</div>
|
||||||
|
<div class="cta-text">
|
||||||
|
<h3>kapteins-daagbok.eu</h3>
|
||||||
|
<p>Öppna i webbläsaren eller lägg till som en app på hemskärmen. Registrera dig med Passkey - ingen appbutik krävs.</p>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">Kostnadsfritt</span>
|
||||||
|
<span class="tag">Reklamfri</span>
|
||||||
|
<span class="tag">E2E-krypterad</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<strong>Avtryck</strong><br />
|
||||||
|
KnorrLabs - Markus F.J. Busche - Knorrstr. 16 · 24106 Kiel - elpatron+kd@mailbox.org
|
||||||
|
</footer>
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* Shared DeepL API helpers for batch translation scripts.
|
||||||
|
* @see https://developers.deepl.com/docs/getting-started/quickstart
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const repoRoot = resolve(__dirname, '../..')
|
||||||
|
|
||||||
|
/** Terms that must not be translated (product, tech, brands). */
|
||||||
|
export const NO_TRANSLATE_TERMS = [
|
||||||
|
'Kapteins Daagbok',
|
||||||
|
'Kiel.Sailing.City.',
|
||||||
|
'KnorrLabs',
|
||||||
|
'Markus F.J. Busche',
|
||||||
|
'kapteins-daagbok.eu',
|
||||||
|
'elpatron+kd@mailbox.org',
|
||||||
|
'GPX/KML',
|
||||||
|
'GPX',
|
||||||
|
'KML',
|
||||||
|
'PDF',
|
||||||
|
'CSV',
|
||||||
|
'PWA',
|
||||||
|
'E2E',
|
||||||
|
'Passkey',
|
||||||
|
'OpenWeatherMap',
|
||||||
|
'Safari',
|
||||||
|
'iPad',
|
||||||
|
'iPhone',
|
||||||
|
'Android',
|
||||||
|
'Knorrstr. 16 · 24106 Kiel'
|
||||||
|
]
|
||||||
|
|
||||||
|
const PLACEHOLDER_RE = /\{\{[^}]+\}\}/g
|
||||||
|
|
||||||
|
export function loadEnvKey() {
|
||||||
|
const fromEnv = process.env.DEEPL_API_KEY ?? process.env.DeepLAPIKey
|
||||||
|
if (fromEnv) return fromEnv.trim()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const envPath = resolve(repoRoot, '.env')
|
||||||
|
const content = readFileSync(envPath, 'utf8')
|
||||||
|
for (const line of content.split('\n')) {
|
||||||
|
const match = line.match(/^(?:DEEPL_API_KEY|DeepLAPIKey)\s*=\s*(.+)$/)
|
||||||
|
if (match) return match[1].trim()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// .env optional when key is exported in shell
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('DeepL API key missing. Set DEEPL_API_KEY or DeepLAPIKey in .env')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveApiUrl(apiKey) {
|
||||||
|
if (process.env.DEEPL_API_URL) return process.env.DEEPL_API_URL
|
||||||
|
return apiKey.endsWith(':fx')
|
||||||
|
? 'https://api-free.deepl.com/v2/translate'
|
||||||
|
: 'https://api.deepl.com/v2/translate'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function protectText(text) {
|
||||||
|
const segments = []
|
||||||
|
let protectedText = text.replace(PLACEHOLDER_RE, (match) => {
|
||||||
|
const id = segments.length
|
||||||
|
segments.push(match)
|
||||||
|
return `__X${id}__`
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const term of NO_TRANSLATE_TERMS) {
|
||||||
|
if (!protectedText.includes(term)) continue
|
||||||
|
const id = segments.length
|
||||||
|
segments.push(term)
|
||||||
|
protectedText = protectedText.split(term).join(`__X${id}__`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { text: protectedText, segments }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreText(text, segments) {
|
||||||
|
let restored = text
|
||||||
|
segments.forEach((value, id) => {
|
||||||
|
restored = restored.replaceAll(`__X${id}__`, value)
|
||||||
|
restored = restored.replaceAll(`__ X ${id} __`, value)
|
||||||
|
restored = restored.replaceAll(`__X ${id}__`, value)
|
||||||
|
})
|
||||||
|
return restored
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function translateBatch(texts, targetLang, { sourceLang = 'DE', apiKey, retries = 3 } = {}) {
|
||||||
|
if (texts.length === 0) return []
|
||||||
|
|
||||||
|
const key = apiKey ?? loadEnvKey()
|
||||||
|
const url = resolveApiUrl(key)
|
||||||
|
const protectedEntries = texts.map((text) => protectText(text))
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
text: protectedEntries.map((entry) => entry.text),
|
||||||
|
source_lang: sourceLang,
|
||||||
|
target_lang: targetLang
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < retries; attempt++) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `DeepL-Auth-Key ${key}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'kapteins-daagbok-translate/1.0'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 429 && attempt < retries - 1) {
|
||||||
|
const waitMs = 1500 * (attempt + 1)
|
||||||
|
console.warn(`Rate limited — waiting ${waitMs}ms…`)
|
||||||
|
await sleep(waitMs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const detail = await response.text()
|
||||||
|
throw new Error(`DeepL error ${response.status}: ${detail}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = await response.json()
|
||||||
|
return payload.translations.map((item, index) =>
|
||||||
|
restoreText(item.text, protectedEntries[index].segments)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('DeepL translation failed after retries')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function translateTexts(texts, targetLang, options = {}) {
|
||||||
|
const batchSize = options.batchSize ?? 40
|
||||||
|
const results = []
|
||||||
|
|
||||||
|
for (let i = 0; i < texts.length; i += batchSize) {
|
||||||
|
const batch = texts.slice(i, i + batchSize)
|
||||||
|
const translated = await translateBatch(batch, targetLang, options)
|
||||||
|
results.push(...translated)
|
||||||
|
if (i + batchSize < texts.length) {
|
||||||
|
process.stdout.write(` ${Math.min(i + batchSize, texts.length)}/${texts.length}\r`)
|
||||||
|
await sleep(300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(` ${texts.length}/${texts.length}\n`)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenTranslation(obj, prefix = '') {
|
||||||
|
const entries = []
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const path = prefix ? `${prefix}.${key}` : key
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
entries.push(...flattenTranslation(value, path))
|
||||||
|
} else if (typeof value === 'string') {
|
||||||
|
entries.push([path, value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unflattenTranslation(entries) {
|
||||||
|
const root = {}
|
||||||
|
for (const [path, value] of entries) {
|
||||||
|
const parts = path.split('.')
|
||||||
|
let node = root
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
node[parts[i]] ??= {}
|
||||||
|
node = node[parts[i]]
|
||||||
|
}
|
||||||
|
node[parts[parts.length - 1]] = value
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Generate localized beta flyer HTML files from the German master via DeepL.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/translate-flyer.mjs [--lang da,sv,nb]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import { resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { loadEnvKey, translateTexts } from './lib/deepl-translate.mjs'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const repoRoot = resolve(__dirname, '..')
|
||||||
|
const sourcePath = resolve(repoRoot, 'docs/marketing/beta-flyer.html')
|
||||||
|
|
||||||
|
const TARGETS = {
|
||||||
|
da: { code: 'DA', htmlLang: 'da', file: 'beta-flyer.da.html' },
|
||||||
|
sv: { code: 'SV', htmlLang: 'sv', file: 'beta-flyer.sv.html' },
|
||||||
|
nb: { code: 'NB', htmlLang: 'nb', file: 'beta-flyer.nb.html' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract translatable text segments from HTML (text nodes only). */
|
||||||
|
function extractSegments(html) {
|
||||||
|
const segments = []
|
||||||
|
const re = />([^<]+)</g
|
||||||
|
let match
|
||||||
|
while ((match = re.exec(html)) !== null) {
|
||||||
|
const text = match[1]
|
||||||
|
if (!text.trim()) continue
|
||||||
|
if (/^\s*$/.test(text)) continue
|
||||||
|
segments.push({ text, index: segments.length })
|
||||||
|
}
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSegments(html, originals, translated) {
|
||||||
|
let result = html
|
||||||
|
for (let i = 0; i < originals.length; i++) {
|
||||||
|
const from = originals[i].text
|
||||||
|
const to = translated[i]
|
||||||
|
if (from === to) continue
|
||||||
|
result = result.replace(`>${from}<`, `>${to}<`)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchLanguageFeature(html, lang) {
|
||||||
|
const langBlocks = {
|
||||||
|
da: `<span class="lang-item"><svg class="feature-flag" viewBox="0 0 37 28" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="37" height="28" fill="#C8102E"/><rect x="12" width="4" height="28" fill="#fff"/><rect y="12" width="37" height="4" fill="#fff"/></svg>Dansk</span>`,
|
||||||
|
sv: `<span class="lang-item"><svg class="feature-flag" viewBox="0 0 16 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="16" height="10" fill="#006AA7"/><rect x="5" width="2" height="10" fill="#FECC00"/><rect y="4" width="16" height="2" fill="#FECC00"/></svg>Svenska</span>`,
|
||||||
|
nb: `<span class="lang-item"><svg class="feature-flag" viewBox="0 0 22 16" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="22" height="16" fill="#BA0C2F"/><rect x="6" width="4" height="16" fill="#fff"/><rect y="6" width="22" height="4" fill="#fff"/><rect x="7" width="2" height="16" fill="#00205B"/><rect y="7" width="22" height="2" fill="#00205B"/></svg>Norsk</span>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const deEnBlock =
|
||||||
|
/<span class="lang-list">[\s\S]*?<\/span><\/span><\/div>/
|
||||||
|
const replacement = `<span class="lang-list">${langBlocks[lang]}<span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 5 3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="5" height="1" fill="#000"/><rect y="1" width="5" height="1" fill="#D00"/><rect y="2" width="5" height="1" fill="#FFCE00"/></svg>Deutsch</span><span class="lang-sep">&</span><span class="lang-item"><svg class="feature-flag" viewBox="0 0 60 30" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><clipPath id="gb-a"><path d="M0 0v30h60V0z"/></clipPath><clipPath id="gb-b"><path d="M30 15h30v15zv15H0z"/></clipPath><g clip-path="url(#gb-a)"><path d="M0 0v30h60V0z" fill="#012169"/><path d="M0 0l60 30m0-30L0 30" stroke="#fff" stroke-width="6"/><path d="M0 0l60 30m0-30L0 30" clip-path="url(#gb-b)" stroke="#C8102E" stroke-width="4"/><path d="M30 0v30M0 15h60" stroke="#fff" stroke-width="10"/><path d="M30 0v30M0 15h60" stroke="#C8102E" stroke-width="6"/></g></svg>Engelsk</span></span></div>`
|
||||||
|
|
||||||
|
return html.replace(deEnBlock, replacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
let langs = Object.keys(TARGETS)
|
||||||
|
for (let i = 2; i < argv.length; i++) {
|
||||||
|
if (argv[i] === '--lang' && argv[i + 1]) {
|
||||||
|
langs = argv[++i].split(',').map((l) => l.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return langs
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const langs = parseArgs(process.argv)
|
||||||
|
const apiKey = loadEnvKey()
|
||||||
|
const sourceHtml = await readFile(sourcePath, 'utf8')
|
||||||
|
const segments = extractSegments(sourceHtml)
|
||||||
|
const texts = segments.map((s) => s.text)
|
||||||
|
|
||||||
|
for (const lang of langs) {
|
||||||
|
const target = TARGETS[lang]
|
||||||
|
if (!target) {
|
||||||
|
console.error(`Unknown language: ${lang}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n→ ${target.file}`)
|
||||||
|
const translated = await translateTexts(texts, target.code, {
|
||||||
|
sourceLang: 'DE',
|
||||||
|
apiKey,
|
||||||
|
batchSize: 20
|
||||||
|
})
|
||||||
|
|
||||||
|
let html = replaceSegments(sourceHtml, segments, translated)
|
||||||
|
html = html.replace(/<html lang="de">/, `<html lang="${target.htmlLang}">`)
|
||||||
|
html = html.replace(
|
||||||
|
/<title>Kapteins Daagbok — Beta-Flyer<\/title>/,
|
||||||
|
`<title>Kapteins Daagbok — Beta-Flyer (${target.htmlLang.toUpperCase()})</title>`
|
||||||
|
)
|
||||||
|
html = patchLanguageFeature(html, lang)
|
||||||
|
|
||||||
|
const outPath = resolve(repoRoot, 'docs/marketing', target.file)
|
||||||
|
await writeFile(outPath, html, 'utf8')
|
||||||
|
console.log(`Wrote ${outPath}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err.message ?? err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Translate i18n locale JSON from German master via DeepL.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node scripts/translate-locales.mjs [--lang da,sv,nb] [--source client/src/i18n/locales/de.json]
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import { resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import {
|
||||||
|
flattenTranslation,
|
||||||
|
loadEnvKey,
|
||||||
|
translateTexts,
|
||||||
|
unflattenTranslation
|
||||||
|
} from './lib/deepl-translate.mjs'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const repoRoot = resolve(__dirname, '..')
|
||||||
|
const defaultSource = resolve(repoRoot, 'client/src/i18n/locales/de.json')
|
||||||
|
|
||||||
|
const TARGETS = {
|
||||||
|
da: 'DA',
|
||||||
|
sv: 'SV',
|
||||||
|
nb: 'NB'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keys whose values stay identical to source (language names, brand). */
|
||||||
|
const COPY_AS_IS_PREFIXES = ['languages.', 'app.name']
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
let langs = Object.keys(TARGETS)
|
||||||
|
let sourcePath = defaultSource
|
||||||
|
|
||||||
|
for (let i = 2; i < argv.length; i++) {
|
||||||
|
if (argv[i] === '--lang' && argv[i + 1]) {
|
||||||
|
langs = argv[++i].split(',').map((l) => l.trim())
|
||||||
|
} else if (argv[i] === '--source' && argv[i + 1]) {
|
||||||
|
sourcePath = resolve(repoRoot, argv[++i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { langs, sourcePath }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldCopyAsIs(path) {
|
||||||
|
return COPY_AS_IS_PREFIXES.some((prefix) => path === prefix || path.startsWith(prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function translateLocale(sourceJson, langCode, apiKey) {
|
||||||
|
const entries = flattenTranslation(sourceJson.translation)
|
||||||
|
const toTranslate = []
|
||||||
|
const indices = []
|
||||||
|
|
||||||
|
entries.forEach(([path, value], index) => {
|
||||||
|
if (shouldCopyAsIs(path)) return
|
||||||
|
toTranslate.push(value)
|
||||||
|
indices.push(index)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`Translating ${toTranslate.length} strings to ${langCode.toUpperCase()}…`)
|
||||||
|
const translated = await translateTexts(toTranslate, TARGETS[langCode], {
|
||||||
|
sourceLang: 'DE',
|
||||||
|
apiKey
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultEntries = [...entries]
|
||||||
|
indices.forEach((entryIndex, i) => {
|
||||||
|
resultEntries[entryIndex][1] = translated[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
return { translation: unflattenTranslation(resultEntries) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { langs, sourcePath } = parseArgs(process.argv)
|
||||||
|
const apiKey = loadEnvKey()
|
||||||
|
const sourceRaw = await readFile(sourcePath, 'utf8')
|
||||||
|
const sourceJson = JSON.parse(sourceRaw)
|
||||||
|
|
||||||
|
for (const lang of langs) {
|
||||||
|
if (!TARGETS[lang]) {
|
||||||
|
console.error(`Unknown language: ${lang}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const outPath = resolve(repoRoot, `client/src/i18n/locales/${lang}.json`)
|
||||||
|
console.log(`\n→ ${lang}.json`)
|
||||||
|
const translated = await translateLocale(sourceJson, lang, apiKey)
|
||||||
|
await writeFile(outPath, `${JSON.stringify(translated, null, 2)}\n`, 'utf8')
|
||||||
|
console.log(`Wrote ${outPath}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err.message ?? err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Verify all locale JSON files have identical key sets.
|
||||||
|
* Usage: node scripts/validate-i18n-keys.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFile } from 'node:fs/promises'
|
||||||
|
import { resolve, dirname } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { flattenTranslation } from './lib/deepl-translate.mjs'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const localesDir = resolve(__dirname, '../client/src/i18n/locales')
|
||||||
|
const localeFiles = ['de.json', 'en.json', 'da.json', 'sv.json', 'nb.json']
|
||||||
|
|
||||||
|
async function loadKeys(filename) {
|
||||||
|
const raw = await readFile(resolve(localesDir, filename), 'utf8')
|
||||||
|
const json = JSON.parse(raw)
|
||||||
|
return flattenTranslation(json.translation).map(([path]) => path).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const keySets = {}
|
||||||
|
for (const file of localeFiles) {
|
||||||
|
keySets[file] = await loadKeys(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const master = keySets['de.json']
|
||||||
|
let failed = false
|
||||||
|
|
||||||
|
for (const file of localeFiles) {
|
||||||
|
if (file === 'de.json') continue
|
||||||
|
const keys = keySets[file]
|
||||||
|
const missing = master.filter((k) => !keys.includes(k))
|
||||||
|
const extra = keys.filter((k) => !master.includes(k))
|
||||||
|
if (missing.length || extra.length) {
|
||||||
|
failed = true
|
||||||
|
console.error(`\n${file}:`)
|
||||||
|
if (missing.length) console.error(` missing (${missing.length}):`, missing.slice(0, 10).join(', '))
|
||||||
|
if (extra.length) console.error(` extra (${extra.length}):`, extra.slice(0, 10).join(', '))
|
||||||
|
} else {
|
||||||
|
console.log(`${file}: OK (${keys.length} keys)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed) process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user