Complete NextSteps roadmap: planning engine UI, calendar cluster, flexible wishes
Works through every numbered item in NextSteps260913.md section 3, plus the three known verification issues in section 4.1. Surface what the backend already computed: - 3.1 render surplus_advice in PlanResults, mirroring the shortfall remedies - 3.2 surface VacationBalance.is_assumed so assumed statutory years are visible - 3.3 age-4 expiry given the proposed plan, via a new Age4Projection reusing the existing _age4_projections arithmetic (Dashboard stat verified, not rebuilt) Calendar cluster (one pass over the holiday data): - new GET /api/calendar/days exposing roda dagar, klamdagar and day overrides, paginated four years at a time - 3.4 shade roda dagar/klamdagar, with R/K badges so colour is not the only signal - 3.6 click-to-override behind an explicit mode toggle, leaving drag multi-select intact - 3.7 click month/year to advance - 3.8 per-year red-days list in Settings, statutory days plus overrides i18n: - 3.5 five backend-generated Swedish surfaces converted to message_key + data, following the tips_service pattern; old string fields replaced, not kept Flexible wishes (3.9): - PLANNING_ENGINE_SPEC section 10: window_mode, candidate search, cost function, deterministic tie-break, two-pass ordering, test matrix rows 15-24 - backend: entity, migration 006, _FULL_SCHEMA sync, repository, allocator - frontend: season-preset toggle in WishEditor, unplaced outcome rendering Fixes (4.1): - pin typecheck to the local tsc rather than npx resolving a newer TypeScript - ignore generated coverage/ and dist/ in the ESLint flat config - fix the flaky usePlanApply test properly, with waitFor rather than a retry Tests: backend 361 -> 404, frontend 540 -> 704. Coverage 89.47% -> 96.52% lines. Not yet verified in a browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RXMhc9nT6sNz1SL4j8dznL
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import {
|
||||
LanguageProvider,
|
||||
useTranslation,
|
||||
detectBrowserLocale,
|
||||
readStoredLocale,
|
||||
resolveLocale,
|
||||
translate,
|
||||
} from './LanguageContext'
|
||||
|
||||
const STORAGE_KEY = 'language_preference'
|
||||
|
||||
// Node's own experimental `localStorage` global (active in newer Node
|
||||
// runtimes without a valid --localstorage-file) shadows jsdom's working
|
||||
// implementation and throws on every call. Stub a real in-memory Storage for
|
||||
// these tests, matching the idiom used in WorkspaceContext.test.tsx and
|
||||
// useCalendarPreferences.test.ts.
|
||||
function createStorageMock(): Storage {
|
||||
let store: Record<string, string> = {}
|
||||
return {
|
||||
getItem: (key: string) => (key in store ? store[key]! : null),
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key]
|
||||
},
|
||||
clear: () => {
|
||||
store = {}
|
||||
},
|
||||
key: (index: number) => Object.keys(store)[index] ?? null,
|
||||
get length() {
|
||||
return Object.keys(store).length
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function setNavigatorLanguage(lang: string) {
|
||||
Object.defineProperty(window.navigator, 'language', {
|
||||
value: lang,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
describe('LanguageContext', () => {
|
||||
const originalLanguage = window.navigator.language
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('localStorage', createStorageMock())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setNavigatorLanguage(originalLanguage)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('detectBrowserLocale', () => {
|
||||
it('detects Swedish from a Swedish browser locale', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
expect(detectBrowserLocale()).toBe('sv')
|
||||
})
|
||||
|
||||
it('detects Swedish from any sv* tag', () => {
|
||||
setNavigatorLanguage('sv-FI')
|
||||
expect(detectBrowserLocale()).toBe('sv')
|
||||
})
|
||||
|
||||
it('falls back to English for a non-Swedish locale', () => {
|
||||
setNavigatorLanguage('en-US')
|
||||
expect(detectBrowserLocale()).toBe('en')
|
||||
})
|
||||
|
||||
it('falls back to English for an unrelated locale', () => {
|
||||
setNavigatorLanguage('fr-FR')
|
||||
expect(detectBrowserLocale()).toBe('en')
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveLocale', () => {
|
||||
it('uses the explicit locale when set, ignoring the browser', () => {
|
||||
setNavigatorLanguage('en-US')
|
||||
expect(resolveLocale('sv')).toBe('sv')
|
||||
})
|
||||
|
||||
it('falls back to browser detection when no explicit locale is set', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
expect(resolveLocale(null)).toBe('sv')
|
||||
})
|
||||
})
|
||||
|
||||
describe('translate', () => {
|
||||
it('returns the Swedish string for a key', () => {
|
||||
expect(translate('sv', 'common.save')).toBe('Spara')
|
||||
})
|
||||
|
||||
it('returns the English string for the same key', () => {
|
||||
expect(translate('en', 'common.save')).toBe('Save')
|
||||
})
|
||||
|
||||
it('interpolates a single param', () => {
|
||||
expect(translate('sv', 'footer.copyright', { year: 2026 })).toBe(
|
||||
'© 2026 Föräldraledigplannerare. Alla rättigheter förbehållna.',
|
||||
)
|
||||
})
|
||||
|
||||
it('interpolates multiple params', () => {
|
||||
expect(
|
||||
translate('en', 'parentalLeaveBreakdown.reservedNote', { reserved: 90, transferable: 45 }),
|
||||
).toBe(
|
||||
'90 of your days are reserved and cannot be transferred to the other parent (up to 45 days can be transferred).',
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves an unmatched placeholder untouched', () => {
|
||||
expect(translate('sv', 'footer.copyright', {})).toBe(
|
||||
'© {year} Föräldraledigplannerare. Alla rättigheter förbehållna.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readStoredLocale', () => {
|
||||
it('returns null when nothing is stored', () => {
|
||||
expect(readStoredLocale()).toBeNull()
|
||||
})
|
||||
|
||||
it('returns the stored locale', () => {
|
||||
localStorage.setItem(STORAGE_KEY, 'en')
|
||||
expect(readStoredLocale()).toBe('en')
|
||||
})
|
||||
|
||||
it('ignores a garbage stored value', () => {
|
||||
localStorage.setItem(STORAGE_KEY, 'fr')
|
||||
expect(readStoredLocale()).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null when localStorage throws (private browsing etc.)', () => {
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: () => {
|
||||
throw new Error('storage disabled')
|
||||
},
|
||||
})
|
||||
expect(readStoredLocale()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
function Probe() {
|
||||
const { locale, explicitLocale, t, setLocale } = useTranslation()
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="locale">{locale}</span>
|
||||
<span data-testid="explicit">{explicitLocale ?? 'auto'}</span>
|
||||
<span data-testid="translated">{t('common.save')}</span>
|
||||
<span data-testid="interpolated">{t('footer.copyright', { year: 2026 })}</span>
|
||||
<button onClick={() => setLocale('en')}>set-en</button>
|
||||
<button onClick={() => setLocale('sv')}>set-sv</button>
|
||||
<button onClick={() => setLocale(null)}>set-auto</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('LanguageProvider + useTranslation', () => {
|
||||
it('defaults to Swedish when the browser reports a Swedish locale', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
expect(screen.getByTestId('locale')).toHaveTextContent('sv')
|
||||
expect(screen.getByTestId('translated')).toHaveTextContent('Spara')
|
||||
})
|
||||
|
||||
it('defaults to English when the browser reports a non-Swedish locale', () => {
|
||||
setNavigatorLanguage('en-US')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
expect(screen.getByTestId('locale')).toHaveTextContent('en')
|
||||
expect(screen.getByTestId('translated')).toHaveTextContent('Save')
|
||||
})
|
||||
|
||||
it('an explicit setting overrides browser detection', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
fireEvent.click(screen.getByText('set-en'))
|
||||
expect(screen.getByTestId('locale')).toHaveTextContent('en')
|
||||
expect(screen.getByTestId('translated')).toHaveTextContent('Save')
|
||||
})
|
||||
|
||||
it('persists the explicit setting to localStorage', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
fireEvent.click(screen.getByText('set-en'))
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBe('en')
|
||||
})
|
||||
|
||||
it('re-renders immediately when the locale is switched', () => {
|
||||
setNavigatorLanguage('en-US')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
expect(screen.getByTestId('translated')).toHaveTextContent('Save')
|
||||
fireEvent.click(screen.getByText('set-sv'))
|
||||
expect(screen.getByTestId('translated')).toHaveTextContent('Spara')
|
||||
})
|
||||
|
||||
it('going back to "auto" resumes following browser detection', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
fireEvent.click(screen.getByText('set-en'))
|
||||
expect(screen.getByTestId('locale')).toHaveTextContent('en')
|
||||
|
||||
fireEvent.click(screen.getByText('set-auto'))
|
||||
expect(screen.getByTestId('explicit')).toHaveTextContent('auto')
|
||||
expect(screen.getByTestId('locale')).toHaveTextContent('sv')
|
||||
})
|
||||
|
||||
it('interpolates params through the t() function', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
render(
|
||||
<LanguageProvider>
|
||||
<Probe />
|
||||
</LanguageProvider>,
|
||||
)
|
||||
expect(screen.getByTestId('interpolated')).toHaveTextContent(
|
||||
'© 2026 Föräldraledigplannerare. Alla rättigheter förbehållna.',
|
||||
)
|
||||
})
|
||||
|
||||
it('a component rendered without a provider still resolves a locale', () => {
|
||||
setNavigatorLanguage('sv-SE')
|
||||
render(<Probe />)
|
||||
expect(screen.getByTestId('locale')).toHaveTextContent('sv')
|
||||
expect(screen.getByTestId('translated')).toHaveTextContent('Spara')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Lightweight, in-house i18n: a React context holding the active locale plus
|
||||
* a `useTranslation()` hook exposing `t(key, params?)`. No external library —
|
||||
* this mirrors the rest of the codebase's preference for small custom hooks
|
||||
* over dependencies (see `useCalendarPreferences.ts` for the same
|
||||
* localStorage + custom-event pattern used here for persistence).
|
||||
*
|
||||
* Locale resolution order:
|
||||
* 1. An explicit user choice, persisted in localStorage.
|
||||
* 2. The browser's language (`navigator.language`): any `sv*` tag resolves
|
||||
* to Swedish, everything else falls back to English.
|
||||
*
|
||||
* `useTranslation()` works even for a component rendered without a
|
||||
* `<LanguageProvider>` ancestor (many existing component tests render in
|
||||
* isolation without wrapping every provider) — `createContext` is given a
|
||||
* real default value computed the same way the provider computes its own
|
||||
* state, so unwrapped components still get a sensible, correctly-detected
|
||||
* locale. Only locale *switching* requires the provider (mounted once, at
|
||||
* the app root, in `App.tsx`).
|
||||
*/
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from 'react'
|
||||
import { sv, type TranslationKey } from './sv'
|
||||
import { en } from './en'
|
||||
|
||||
export type Locale = 'sv' | 'en'
|
||||
|
||||
export type TranslationParams = Record<string, string | number>
|
||||
|
||||
export type { TranslationKey }
|
||||
|
||||
const STORAGE_KEY = 'language_preference'
|
||||
const STORAGE_EVENT_NAME = 'language:preference-changed'
|
||||
|
||||
const CATALOGUES: Record<Locale, Record<TranslationKey, string>> = { sv, en }
|
||||
|
||||
function isLocale(value: string | null): value is Locale {
|
||||
return value === 'sv' || value === 'en'
|
||||
}
|
||||
|
||||
/** `sv`, `sv-SE`, `sv-FI`, … resolve to Swedish; everything else to English. */
|
||||
export function detectBrowserLocale(): Locale {
|
||||
const lang = typeof navigator !== 'undefined' ? navigator.language : ''
|
||||
return (lang ?? '').toLowerCase().startsWith('sv') ? 'sv' : 'en'
|
||||
}
|
||||
|
||||
/** The user's persisted explicit choice, or `null` if following detection. */
|
||||
export function readStoredLocale(): Locale | null {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
return isLocale(stored) ? stored : null
|
||||
} catch {
|
||||
// localStorage unavailable (private browsing, disabled storage, ...) —
|
||||
// treat as "no explicit preference", same as useCalendarPreferences does.
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredLocale(locale: Locale | null): void {
|
||||
try {
|
||||
if (locale === null) {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
} else {
|
||||
localStorage.setItem(STORAGE_KEY, locale)
|
||||
}
|
||||
} catch {
|
||||
// Storage unavailable — the choice just won't survive a reload.
|
||||
}
|
||||
try {
|
||||
window.dispatchEvent(new Event(STORAGE_EVENT_NAME))
|
||||
} catch {
|
||||
// No window (non-browser environment) — nothing to notify.
|
||||
}
|
||||
}
|
||||
|
||||
/** Explicit preference wins; otherwise fall back to browser detection. */
|
||||
export function resolveLocale(explicit: Locale | null): Locale {
|
||||
return explicit ?? detectBrowserLocale()
|
||||
}
|
||||
|
||||
/** Narrow an arbitrary backend `data` bag (e.g. a `Tip`, `PlanWarning`,
|
||||
* `SurplusAdvice`, `ShortfallRemedy`, or `Rejection`'s `data`) down to the
|
||||
* string/number entries `TranslationParams` can interpolate — anything else
|
||||
* (a nested object, a boolean, `null`) is dropped rather than stringified,
|
||||
* since it can't be interpolated meaningfully. Shared so every `message_key`
|
||||
* + `data` surface (§3.5) renders through the same rule `TipCard` already
|
||||
* established.
|
||||
*/
|
||||
export function toTranslationParams(data: Record<string, unknown>): TranslationParams {
|
||||
const params: TranslationParams = {}
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
params[key] = value
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function interpolate(template: string, params?: TranslationParams): string {
|
||||
if (!params) return template
|
||||
return template.replace(/\{(\w+)\}/g, (match, key: string) => {
|
||||
const value = params[key]
|
||||
return value === undefined ? match : String(value)
|
||||
})
|
||||
}
|
||||
|
||||
export function translate(locale: Locale, key: TranslationKey, params?: TranslationParams): string {
|
||||
return interpolate(CATALOGUES[locale][key], params)
|
||||
}
|
||||
|
||||
export interface LanguageContextValue {
|
||||
/** The active, resolved locale — always concrete, never "auto". */
|
||||
locale: Locale
|
||||
/** The user's explicit override, or `null` when following browser detection. */
|
||||
explicitLocale: Locale | null
|
||||
/** Set an explicit locale, or `null` to go back to following detection. */
|
||||
setLocale: (locale: Locale | null) => void
|
||||
t: (key: TranslationKey, params?: TranslationParams) => string
|
||||
}
|
||||
|
||||
function buildContextValue(
|
||||
explicitLocale: Locale | null,
|
||||
setLocale: (locale: Locale | null) => void,
|
||||
): LanguageContextValue {
|
||||
const locale = resolveLocale(explicitLocale)
|
||||
return {
|
||||
locale,
|
||||
explicitLocale,
|
||||
setLocale,
|
||||
t: (key, params) => translate(locale, key, params),
|
||||
}
|
||||
}
|
||||
|
||||
// Real default value (not `undefined`) so a component rendered without a
|
||||
// wrapping <LanguageProvider> still resolves a correct locale instead of
|
||||
// throwing. It won't react to later locale changes — only the provider
|
||||
// (mounted once at the app root) subscribes to those.
|
||||
const defaultContextValue = buildContextValue(readStoredLocale(), () => {
|
||||
// No provider means nowhere to persist a change; this is a read-only
|
||||
// fallback view of whatever locale is already resolved.
|
||||
})
|
||||
|
||||
const LanguageContext = createContext<LanguageContextValue>(defaultContextValue)
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }): ReactNode {
|
||||
const [explicitLocale, setExplicitLocale] = useState<Locale | null>(() => readStoredLocale())
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => setExplicitLocale(readStoredLocale())
|
||||
window.addEventListener('storage', handleChange)
|
||||
window.addEventListener(STORAGE_EVENT_NAME, handleChange)
|
||||
return () => {
|
||||
window.removeEventListener('storage', handleChange)
|
||||
window.removeEventListener(STORAGE_EVENT_NAME, handleChange)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setLocale = useCallback((locale: Locale | null) => {
|
||||
writeStoredLocale(locale)
|
||||
setExplicitLocale(locale)
|
||||
}, [])
|
||||
|
||||
const value = useMemo(
|
||||
() => buildContextValue(explicitLocale, setLocale),
|
||||
[explicitLocale, setLocale],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.lang = value.locale
|
||||
}
|
||||
}, [value.locale])
|
||||
|
||||
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>
|
||||
}
|
||||
|
||||
export function useTranslation(): LanguageContextValue {
|
||||
return useContext(LanguageContext)
|
||||
}
|
||||
@@ -0,0 +1,602 @@
|
||||
import type { TranslationKey } from './sv'
|
||||
|
||||
/**
|
||||
* English message catalogue.
|
||||
*
|
||||
* Typed as `Record<TranslationKey, string>` against an object literal: if a
|
||||
* key present in `sv.ts` is missing here, TypeScript reports a "missing
|
||||
* property" error; if a key here doesn't exist in `sv.ts`, TypeScript's
|
||||
* excess-property check on the object literal reports it too. Either way,
|
||||
* `tsc --noEmit` fails before the drift ships.
|
||||
*
|
||||
* Domain terms are translated for meaning, not word-for-word — see
|
||||
* ARCHITECTURE notes / task handoff for the glossary (sjukpenningnivå is the
|
||||
* income-based rate, not sick leave; lägstanivå is the flat minimum rate;
|
||||
* dubbeldagar are "double days"; röd dag is a public holiday; klämdag is a
|
||||
* "bridge day").
|
||||
*/
|
||||
export const en: Record<TranslationKey, string> = {
|
||||
// ---------------------------------------------------------------------
|
||||
// App-wide
|
||||
// ---------------------------------------------------------------------
|
||||
'app.name': 'Föräldraledigplannerare',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Common
|
||||
// ---------------------------------------------------------------------
|
||||
'common.loadingEllipsis': 'Loading…',
|
||||
'common.save': 'Save',
|
||||
'common.saving': 'Saving…',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.close': 'Close',
|
||||
'common.delete': 'Delete',
|
||||
'common.deleting': 'Deleting…',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Navigation / Header / Footer
|
||||
// ---------------------------------------------------------------------
|
||||
'nav.overview': 'Overview',
|
||||
'nav.calendar': 'Calendar',
|
||||
'nav.plan': 'Plan',
|
||||
'nav.tips': 'Tips',
|
||||
'nav.settings': 'Settings',
|
||||
'nav.logout': 'Log out',
|
||||
'nav.selectChildAria': 'Select child',
|
||||
'nav.logoutAria': 'Log out',
|
||||
'footer.copyright': '© {year} Föräldraledigplannerare. All rights reserved.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Auth
|
||||
// ---------------------------------------------------------------------
|
||||
'auth.loadingEllipsis': 'Loading…',
|
||||
'auth.createAccountTitle': 'Create account',
|
||||
'auth.loginTitle': 'Log in',
|
||||
'auth.namePlaceholder': 'Name',
|
||||
'auth.emailPlaceholder': 'Email',
|
||||
'auth.passwordPlaceholder': 'Password',
|
||||
'auth.registerButton': 'Register',
|
||||
'auth.loginButton': 'Log in',
|
||||
'auth.haveAccountButton': 'Already have an account',
|
||||
'auth.createAccountButton': 'Create account',
|
||||
'auth.genericError': 'Something went wrong',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Dashboard
|
||||
// ---------------------------------------------------------------------
|
||||
'dashboard.loading': 'Loading...',
|
||||
'dashboard.addChildTitle': 'Add child',
|
||||
'dashboard.addChildIntro': 'You have no children registered yet. Add your first child to get started.',
|
||||
'dashboard.childNameLabel': "Child's name",
|
||||
'dashboard.namePlaceholder': 'Name',
|
||||
'dashboard.birthdayLabel': 'Birthday',
|
||||
'dashboard.addChildButton': 'Add child',
|
||||
'dashboard.addingChild': 'Adding...',
|
||||
'dashboard.fillAllFields': 'Fill in all fields',
|
||||
'dashboard.createChildError': 'Could not create child',
|
||||
'dashboard.fetchFromFk': 'Fetch from Försäkringskassan',
|
||||
'dashboard.enterManually': 'Enter details manually',
|
||||
'dashboard.overviewTitle': 'Overview',
|
||||
'dashboard.usedDays': 'Used days',
|
||||
'dashboard.daysLeft': 'Days left',
|
||||
'dashboard.expiringAtB4': 'Expires at age 4',
|
||||
'dashboard.plannedDaysTitle': 'Planned days',
|
||||
'dashboard.remainingUntilB4Title': 'Remaining until age 4',
|
||||
'dashboard.saveManualFetchError': 'Could not save the details',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------
|
||||
'settings.loading': 'Loading settings...',
|
||||
'settings.title': 'Settings',
|
||||
'settings.childSectionTitle': 'Child',
|
||||
'settings.selectedChildLabel': 'Selected child:',
|
||||
'settings.childBirthDateLabel': "Child's birth date",
|
||||
'settings.preschoolHelp':
|
||||
'Preschools usually close for about 6 days a year for planning. Enter how many days your preschool has, so they are reserved in the plan.',
|
||||
'settings.preschoolPlanningDaysLabel': 'Preschool planning days per year',
|
||||
'settings.saveChildButton': "Save child's details",
|
||||
'settings.noChildSelected': 'No child selected yet.',
|
||||
'settings.profileSectionTitle': 'Your profile',
|
||||
'settings.profileHelp': 'Your salary type and employment status are used to estimate your SGI and plan your leave.',
|
||||
'settings.saveProfileButton': 'Save your profile',
|
||||
'settings.planningSectionTitle': 'Planning',
|
||||
'settings.planningHelp':
|
||||
'Your planning goal, and whether you plan alone or together with a partner, control how the planner suggests distributing days.',
|
||||
'settings.savePlanningButton': 'Save planning',
|
||||
'settings.vacationSectionTitle': 'Vacation days',
|
||||
'settings.vacationHelp':
|
||||
'Enter vacation days per year. In the first year you are usually home more and have fewer days to spend on vacation, while later years have your full annual allotment. This controls how much time the planner has to work with.',
|
||||
'settings.calendarSectionTitle': 'Calendar',
|
||||
'settings.showWeekNumbers': 'Show week numbers',
|
||||
'settings.languageSectionTitle': 'Språk / Language',
|
||||
'settings.languageLabel': 'Välj språk / Select language',
|
||||
'settings.languageAutoOption': 'Automatic (browser language)',
|
||||
'settings.languageSwedishOption': 'Svenska',
|
||||
'settings.languageEnglishOption': 'English',
|
||||
'settings.partnerSectionTitle': 'Partner',
|
||||
'settings.partnerIntro': 'Add a partner to plan parental leave together.',
|
||||
'settings.partnerNamePlaceholder': "Partner's name",
|
||||
'settings.addPartnerButton': 'Add partner',
|
||||
'settings.addingPartner': 'Adding...',
|
||||
'settings.saveChildSuccess': "Child's details saved!",
|
||||
'settings.saveChildError': "Could not save the child's details",
|
||||
'settings.saveProfileSuccess': 'Your profile was saved!',
|
||||
'settings.saveProfileError': 'Could not save your profile',
|
||||
'settings.savePlanningSuccess': 'Planning settings saved!',
|
||||
'settings.savePlanningError': 'Could not save planning settings',
|
||||
'settings.saveVacationSuccess': 'Vacation days for {year} saved!',
|
||||
'settings.saveVacationError': 'Could not save vacation days',
|
||||
'settings.partnerNameRequired': 'Enter a name for the partner',
|
||||
'settings.addPartnerSuccess': 'Partner added! Reload the page to see the changes.',
|
||||
'settings.addPartnerError': 'Could not add partner',
|
||||
'settings.dayOverridesSectionTitle': 'Calendar exceptions',
|
||||
'settings.dayOverridesHelp':
|
||||
"The planner is based on the Swedish calendar (public holidays, bridge days, Christmas Eve, Midsummer Eve and Whitsun Eve are normally days off). Here you can enter exceptions for your workplace — if you have to work on Christmas Day, or are off on days that are otherwise working days.",
|
||||
'settings.redDaysSectionTitle': 'Public holidays',
|
||||
'settings.redDaysHelp':
|
||||
'A list of public holidays under the Swedish calendar, together with your own exceptions — four years at a time.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Day overrides input
|
||||
// ---------------------------------------------------------------------
|
||||
'dayOverridesInput.empty': 'You have no calendar exceptions yet.',
|
||||
'dayOverridesInput.workingStatus': "I'm working this day",
|
||||
'dayOverridesInput.offStatus': "I'm off this day",
|
||||
'dayOverridesInput.noteLabel': 'Note: {note}',
|
||||
'dayOverridesInput.addSectionTitle': 'Add exception',
|
||||
'dayOverridesInput.dateLabel': 'Date',
|
||||
'dayOverridesInput.statusLegend': 'Status',
|
||||
'dayOverridesInput.noteFieldLabel': 'Note (optional)',
|
||||
'dayOverridesInput.notePlaceholder': 'E.g. Worked on Christmas Day',
|
||||
'dayOverridesInput.charCount': '{count}/280',
|
||||
'dayOverridesInput.addButton': 'Add exception',
|
||||
'dayOverridesInput.addingButton': 'Adding...',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Red days list (Settings, §3.8)
|
||||
// ---------------------------------------------------------------------
|
||||
'redDaysList.prevWindow': 'Previous 4 years',
|
||||
'redDaysList.nextWindow': 'Next 4 years',
|
||||
'redDaysList.loading': 'Loading public holidays...',
|
||||
'redDaysList.emptyYear': 'No public holidays or exceptions this year.',
|
||||
'redDaysList.overrideWorking': 'Exception: working day',
|
||||
'redDaysList.overrideFree': 'Exception: day off',
|
||||
'redDaysList.effectiveFree': 'Off',
|
||||
'redDaysList.effectiveWorking': 'Working day',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Plan
|
||||
// ---------------------------------------------------------------------
|
||||
'plan.loading': 'Loading planning...',
|
||||
'plan.title': 'Planning',
|
||||
'plan.intro':
|
||||
'Describe how you want to be on leave — for example 6 weeks every summer and 4 weeks every Christmas — and get a suggested plan showing exactly which days are already free, which cost a vacation day, and which cost a parental-benefit day.',
|
||||
'plan.wishesTitle': 'Your wishes',
|
||||
'plan.proposeButton': 'Suggest plan',
|
||||
'plan.calculating': 'Calculating...',
|
||||
'plan.addAtLeastOneWish': 'Add at least one wish above to suggest a plan.',
|
||||
'plan.addWishError': 'Could not add wish',
|
||||
'plan.updateWishError': 'Could not save wish',
|
||||
'plan.removeWishError': 'Could not remove wish',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Tips
|
||||
// ---------------------------------------------------------------------
|
||||
'tips.loading': 'Loading tips...',
|
||||
'tips.title': 'Optimization Tips',
|
||||
'tips.empty': 'No tips yet. Great job planning your parental leave!',
|
||||
'tip.aria': 'Tip: {title}',
|
||||
'tip.dismissAria': 'Dismiss: {title}',
|
||||
|
||||
'tip.useVacationBeforeExpiry.title': 'Vacation days at risk of expiring',
|
||||
'tip.useVacationBeforeExpiry.body':
|
||||
'{parent_name} has {saved_days} saved vacation days from {year} that risk being lost if not used.',
|
||||
'tip.balanceBetweenParents.title': 'Imbalance between parents',
|
||||
'tip.balanceBetweenParents.body':
|
||||
'{leading_parent} has taken {gap_days} more days than {lagging_parent}. Consider evening out the split.',
|
||||
'tip.preserveForAge4.title': 'Days at risk of being lost at age 4',
|
||||
'tip.preserveForAge4.body':
|
||||
'{parent_name} risks losing {expiring_days} days when the child turns 4 ({fourth_birthday}). Only 96 days can be saved after that.',
|
||||
'tip.useDubbeldagarStrategically.title': 'Use a double day',
|
||||
'tip.useDubbeldagarStrategically.body':
|
||||
"You both have parental-benefit leave planned on {date} but it isn't marked as a double day (dubbeldag).",
|
||||
'tip.lagstaNivaCoverage.title': 'Cover the gap with lägstanivå',
|
||||
'tip.lagstaNivaCoverage.body':
|
||||
'{parent_name} has only {sjukpenning_left} days left at sjukpenningnivå (income-based) but {lagsta_left} days left at lägstanivå (minimum rate).',
|
||||
'tip.stretchTimeAtHome.title': 'Stretch out your time at home',
|
||||
'tip.stretchTimeAtHome.body':
|
||||
'{parent_name} is planning {weekly_intensity} days a week over the coming month, leaving {projected_left} days at age 4.',
|
||||
'tip.skipFpFirst12Months.title': 'Save parental benefit for later',
|
||||
'tip.skipFpFirst12Months.body':
|
||||
'{parent_name} has parental-benefit leave planned before the child turns 1. With the "most time at home" goal it can pay off to save those days for later.',
|
||||
'tip.sgiMinimumAfterAge1.title': 'Risk to SGI protection',
|
||||
'tip.sgiMinimumAfterAge1.body':
|
||||
'{parent_name} is planning fewer than {min_days_per_week} parental-benefit days a week over the coming month, which can affect SGI protection.',
|
||||
'tip.wrapWeekendWithVacation.title': 'Make the most of the weekend',
|
||||
'tip.wrapWeekendWithVacation.body':
|
||||
'{parent_name} has parental-benefit leave planned on Friday {friday} and Monday {monday}. A vacation day in between makes for a longer break.',
|
||||
'tip.wrapRedDaysWithVacation.title': 'Make the most of public holidays',
|
||||
'tip.wrapRedDaysWithVacation.body':
|
||||
'{parent_name} has parental-benefit leave planned near a holiday cluster ({cluster_start}–{cluster_end}). A vacation day around the public holidays can extend the break.',
|
||||
'tip.longBreakParentSwap.title': 'Consider alternating',
|
||||
'tip.longBreakParentSwap.body':
|
||||
'{parent_name} is covering less than half of "{break_name}" ({start_date}–{end_date}). Let {other_parent_name} take part of the period.',
|
||||
'tip.reserveForPreschoolPlanningDays.title': 'Reserve days for preschool planning days',
|
||||
'tip.reserveForPreschoolPlanningDays.body':
|
||||
"{parent_name} has {days_left} days left, which barely covers the preschool's {buffer_days} planning days. Reserve days ahead of time.",
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Calendar page
|
||||
// ---------------------------------------------------------------------
|
||||
'calendar.loading': 'Loading calendar...',
|
||||
'calendar.prevMonthAria': 'Previous month',
|
||||
'calendar.nextMonthAria': 'Next month',
|
||||
'calendar.advanceMonthAria': '{month} - click to advance month',
|
||||
'calendar.advanceYearAria': '{year} - click to advance year',
|
||||
'calendar.weekdayMon': 'Mon',
|
||||
'calendar.weekdayTue': 'Tue',
|
||||
'calendar.weekdayWed': 'Wed',
|
||||
'calendar.weekdayThu': 'Thu',
|
||||
'calendar.weekdayFri': 'Fri',
|
||||
'calendar.weekdaySat': 'Sat',
|
||||
'calendar.weekdaySun': 'Sun',
|
||||
'calendar.legendTitle': 'Legend',
|
||||
'calendar.legendVacation': 'Vacation',
|
||||
'calendar.legendIncomeBased': 'Income-based rate (sjukpenningnivå)',
|
||||
'calendar.legendMinimumRate': 'Minimum rate (lägstanivå)',
|
||||
'calendar.legendDoubleDay': 'Double day',
|
||||
'calendar.legendRedDay': 'Public holiday',
|
||||
'calendar.legendKlamdag': 'Bridge day',
|
||||
'calendar.legendOverride': 'Exception (working/day off)',
|
||||
'calendar.saveError': 'Failed to save the planned days',
|
||||
'calendar.deleteError': 'Failed to delete the planned day',
|
||||
'calendar.overrideError': 'Failed to update the exception',
|
||||
'calendar.overrideModeLabel': 'Mark working/day off',
|
||||
'calendar.overrideModeActiveLabel': 'Stop editing days',
|
||||
'calendar.overrideModeHint':
|
||||
'Click a day to mark it as a working day or a day off. Click a marked day again to reset it.',
|
||||
'calendar.you': 'You',
|
||||
'calendar.partner': 'Partner',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Calendar day / event (accessibility)
|
||||
// ---------------------------------------------------------------------
|
||||
'calendarDay.ariaWithEvents': '{date} with {count} event(s)',
|
||||
'calendarDay.klamdagAria': 'bridge day',
|
||||
'calendarDay.overrideWorkingAria': 'marked as a working day (exception)',
|
||||
'calendarDay.overrideFreeAria': 'marked as a day off (exception)',
|
||||
'calendarEvent.dismissAria': 'Dismiss {leaveType}',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Dashboard components
|
||||
// ---------------------------------------------------------------------
|
||||
'daysBreakdown.empty': 'No days registered yet',
|
||||
'daysBreakdown.typeHeader': 'Leave type',
|
||||
'daysBreakdown.daysHeader': 'Days',
|
||||
|
||||
'overviewMetrics.defaultTitle': 'Overview',
|
||||
|
||||
'parentalLeaveBreakdown.empty': 'No data fetched yet — press Fetch from Försäkringskassan.',
|
||||
'parentalLeaveBreakdown.typeHeader': 'Type',
|
||||
'parentalLeaveBreakdown.remainingHeader': 'Remaining',
|
||||
'parentalLeaveBreakdown.totalHeader': 'Total',
|
||||
'parentalLeaveBreakdown.transferableDays': 'Transferable days',
|
||||
'parentalLeaveBreakdown.dubbeldagarLeft': 'Double days left',
|
||||
'parentalLeaveBreakdown.sjukpenning': 'Sjukpenningnivå (income-based)',
|
||||
'parentalLeaveBreakdown.lagstaNiva': 'Minimum rate',
|
||||
'parentalLeaveBreakdown.reservedNote':
|
||||
'{reserved} of your days are reserved and cannot be transferred to the other parent (up to {transferable} days can be transferred).',
|
||||
'parentalLeaveBreakdown.sourceScraped': 'Fetched from Försäkringskassan',
|
||||
'parentalLeaveBreakdown.sourceManual': 'Manually entered',
|
||||
|
||||
'progressChart.title': 'Parental leave — progress',
|
||||
'progressChart.age': 'Age: {age}',
|
||||
'progressChart.weeksStat': '{current} / {target} weeks',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Inputs
|
||||
// ---------------------------------------------------------------------
|
||||
'childBirthdayInput.defaultLabel': "Child's birth date",
|
||||
'childBirthdayInput.birthDateLabel': 'Date of birth',
|
||||
'childBirthdayInput.preschoolStartLabel': 'Preschool start (optional)',
|
||||
|
||||
'parentProfileInput.salaryTypeLabel': 'Salary type',
|
||||
'parentProfileInput.selectSalaryType': 'Select salary type',
|
||||
'parentProfileInput.monthly': 'Monthly salary',
|
||||
'parentProfileInput.hourly': 'Hourly wage',
|
||||
'parentProfileInput.employmentStatusLabel': 'Employment status',
|
||||
'parentProfileInput.selectEmploymentStatus': 'Select employment status',
|
||||
'parentProfileInput.employed': 'Employed',
|
||||
'parentProfileInput.jobseeking': 'Job seeking',
|
||||
'parentProfileInput.none': 'None',
|
||||
|
||||
'planningGoalSelector.legend': 'Planning goal',
|
||||
'planningGoalSelector.maxTime': 'Most time at home',
|
||||
'planningGoalSelector.maxTimeHelp': 'Save days and extend your total leave time, with lower income now.',
|
||||
'planningGoalSelector.maxMoney': 'Most money',
|
||||
'planningGoalSelector.maxMoneyHelp': 'Protect your income, e.g. by placing vacation around weekends and public holidays.',
|
||||
'planningGoalSelector.balanced': 'Balanced',
|
||||
'planningGoalSelector.balancedHelp': 'A trade-off between time at home and income.',
|
||||
|
||||
'planningModeSelector.legend': 'Are you planning alone or together with a partner?',
|
||||
'planningModeSelector.solo': 'Just me',
|
||||
'planningModeSelector.couple': 'Me and my partner',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Modals
|
||||
// ---------------------------------------------------------------------
|
||||
'qrModal.title': 'Fetch from Försäkringskassan',
|
||||
'qrModal.closeAria': 'Close modal',
|
||||
'qrModal.statusLabel': 'Status:',
|
||||
'qrModal.qrInstruction': 'Open the BankID app and scan the QR code:',
|
||||
'qrModal.qrAlt': 'BankID QR code',
|
||||
'qrModal.authenticating': 'Authenticating…',
|
||||
'qrModal.fetching': 'Fetching data from FK…',
|
||||
'qrModal.success': 'Done! Data fetched from Försäkringskassan.',
|
||||
'qrModal.cancelled': 'The session was cancelled.',
|
||||
'qrModal.closeButton': 'Close',
|
||||
'qrModal.cancelButton': 'Cancel',
|
||||
'qrModal.errorLabel': 'Error:',
|
||||
|
||||
'manualFetchModal.title': 'Enter Parental Leave Data',
|
||||
'manualFetchModal.closeAria': 'Close',
|
||||
'manualFetchModal.description':
|
||||
'Enter your parental leave information from Försäkringskassan manually if automatic fetching is unavailable.',
|
||||
'manualFetchModal.totalDaysSectionTitle': 'Total Days',
|
||||
'manualFetchModal.totalDaysLabel': 'Total days (0-480)',
|
||||
'manualFetchModal.daysUsedLabel': 'Days used',
|
||||
'manualFetchModal.daysRemaining': 'Days remaining:',
|
||||
'manualFetchModal.remainingBreakdownTitle': 'Remaining Days Breakdown',
|
||||
'manualFetchModal.reservedDaysLabel': 'Reserved days left',
|
||||
'manualFetchModal.transferableDaysLabel': 'Transferable days left',
|
||||
'manualFetchModal.dubbeldagarLabel': 'Dubbeldagar left (0-60)',
|
||||
'manualFetchModal.tierBreakdownTitle': 'Tier Breakdown',
|
||||
'manualFetchModal.sjukpenningUsedLabel': 'Sjukpenningnivå (income-based, 390-day tier) used',
|
||||
'manualFetchModal.lagstaUsedLabel': 'Lägstanivå (minimum-rate, 90-day tier) used',
|
||||
'manualFetchModal.cancelButton': 'Cancel',
|
||||
'manualFetchModal.saveButton': 'Save',
|
||||
'manualFetchModal.savingButton': 'Saving…',
|
||||
'manualFetchModal.saveError': 'Failed to save manual data',
|
||||
|
||||
'plannedDayModal.editTitle': 'Edit Planned Day',
|
||||
'plannedDayModal.newTitle': 'Plan a Day',
|
||||
'plannedDayModal.rangeTitle': 'Plan {count} days',
|
||||
'plannedDayModal.closeAria': 'Close',
|
||||
'plannedDayModal.leaveTypeLabel': 'Leave Type',
|
||||
'plannedDayModal.vacationOption': 'Vacation',
|
||||
'plannedDayModal.sjukpenningOption': 'Sjukpenningnivå (income-based, 390-day tier)',
|
||||
'plannedDayModal.lagstaOption': 'Lägstanivå (minimum-rate, 90-day tier)',
|
||||
'plannedDayModal.parentLabel': 'Parent',
|
||||
'plannedDayModal.doubleDayLabel': 'Double day (both parents taking leave)',
|
||||
'plannedDayModal.doubleDayHint': 'Double days do not apply to vacation',
|
||||
'plannedDayModal.deleteButton': 'Delete',
|
||||
'plannedDayModal.deletingButton': 'Deleting…',
|
||||
'plannedDayModal.cancelButton': 'Cancel',
|
||||
'plannedDayModal.saveButton': 'Save',
|
||||
'plannedDayModal.savingButton': 'Saving…',
|
||||
'plannedDayModal.confirmDelete': 'Are you sure you want to delete this planned day?',
|
||||
'plannedDayModal.saveError': 'Failed to save planned day',
|
||||
'plannedDayModal.deleteError': 'Failed to delete planned day',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Vacation days
|
||||
// ---------------------------------------------------------------------
|
||||
'vacationDaysList.loading': 'Loading vacation days…',
|
||||
'vacationDaysList.empty': 'No vacation days registered.',
|
||||
'vacationDaysList.itemDays': '{count} days',
|
||||
'vacationDaysList.savedSuffix': '(+{count} saved)',
|
||||
|
||||
'vacationDaysTable.empty': 'No years to show yet.',
|
||||
'vacationDaysTable.yearHeader': 'Year',
|
||||
'vacationDaysTable.vacationDaysHeader': 'Vacation days',
|
||||
'vacationDaysTable.savedDaysHeader': 'Saved days carried in',
|
||||
'vacationDaysTable.saveButton': 'Save',
|
||||
'vacationDaysTable.assumedBadge': 'Assumed',
|
||||
'vacationDaysTable.assumedPrompt':
|
||||
'No value is recorded for this year — the statutory 25 days has been assumed. Confirm or edit below.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Planning: WishEditor
|
||||
// ---------------------------------------------------------------------
|
||||
'wishEditor.presetSommar': 'Summer',
|
||||
'wishEditor.presetJullov': 'Christmas break',
|
||||
'wishEditor.presetHostlov': 'Autumn break (höstlov)',
|
||||
'wishEditor.customLeaveButton': '+ Custom leave',
|
||||
'wishEditor.customLeaveName': 'Custom leave',
|
||||
'wishEditor.weekUnit': 'week',
|
||||
'wishEditor.weeksUnit': 'weeks',
|
||||
'wishEditor.emptyState': 'No leave wishes yet. Choose a suggestion above to get started.',
|
||||
'wishEditor.nameAria': 'Name for {name}',
|
||||
'wishEditor.weeksLabel': 'Weeks',
|
||||
'wishEditor.priorityLabel': 'Priority',
|
||||
'wishEditor.parentLabel': 'Parent',
|
||||
'wishEditor.eitherParent': 'Either parent',
|
||||
'wishEditor.bothParents': 'Both parents',
|
||||
'wishEditor.hideDates': 'Hide dates',
|
||||
'wishEditor.adjustDates': 'Adjust dates',
|
||||
'wishEditor.startsLabel': 'Starts',
|
||||
'wishEditor.endsLabel': 'Ends',
|
||||
'wishEditor.recurEveryYear': 'Repeats every year',
|
||||
'wishEditor.firstYearLabel': 'First year',
|
||||
'wishEditor.untilAge4': 'Until the child turns 4',
|
||||
'wishEditor.lastYearLabel': 'Last year',
|
||||
'wishEditor.saveButton': 'Save',
|
||||
'wishEditor.savingButton': 'Saving...',
|
||||
'wishEditor.removeButton': 'Remove',
|
||||
'wishEditor.month1': 'January',
|
||||
'wishEditor.month2': 'February',
|
||||
'wishEditor.month3': 'March',
|
||||
'wishEditor.month4': 'April',
|
||||
'wishEditor.month5': 'May',
|
||||
'wishEditor.month6': 'June',
|
||||
'wishEditor.month7': 'July',
|
||||
'wishEditor.month8': 'August',
|
||||
'wishEditor.month9': 'September',
|
||||
'wishEditor.month10': 'October',
|
||||
'wishEditor.month11': 'November',
|
||||
'wishEditor.month12': 'December',
|
||||
|
||||
// PLANNING_ENGINE_SPEC.md §10.7 — fixed vs "whenever it fits best" toggle.
|
||||
'wishEditor.modeSectionLabel': 'How should this wish be placed?',
|
||||
'wishEditor.modeFixed': 'Fixed dates',
|
||||
'wishEditor.modeFlexible': 'Whenever it fits best',
|
||||
'wishEditor.modeFlexibleExplainer':
|
||||
'The engine picks the best week inside the search range below by itself — it looks for the period with the most röda dagar (public holidays) and klämdagar (bridge days), since that costs the fewest semesterdagar (vacation days). A recurring wish is re-picked every year (Easter moves, for example), and the suggestion always needs confirming in the Plan tab before it is saved, just like any other plan.',
|
||||
'wishEditor.boundStartsLabel': 'Search range from',
|
||||
'wishEditor.boundEndsLabel': 'to',
|
||||
'wishEditor.adjustBound': 'Adjust search range',
|
||||
'wishEditor.boundTooShortError':
|
||||
'The search range is shorter than the {weeks} weeks wished — choose a longer range or fewer weeks.',
|
||||
'wishEditor.flexPresetsLabel': 'Quick picks — search within a season ("whenever it fits best"):',
|
||||
'wishEditor.seasonVar': 'Spring',
|
||||
'wishEditor.seasonSommar': 'Summer',
|
||||
'wishEditor.seasonHost': 'Autumn',
|
||||
'wishEditor.seasonJul': 'Christmas',
|
||||
'wishEditor.flexBadge': 'whenever it fits best',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Planning: PlanResults
|
||||
// ---------------------------------------------------------------------
|
||||
'planResults.dayTypeFree': 'free days (weekend/public holiday)',
|
||||
'planResults.dayTypeVacation': 'vacation days',
|
||||
'planResults.dayTypeSjukpenning': 'parental-benefit days (sjukpenningnivå, income-based)',
|
||||
'planResults.dayTypeLagsta': 'parental-benefit days (lägstanivå, minimum rate)',
|
||||
'planResults.strategyDubbeldag': 'Double days — both parents home at the same time',
|
||||
'planResults.strategyParentSwap': 'Parents alternate',
|
||||
'planResults.strategySolo': 'One parent home',
|
||||
'planResults.strategyUnplaced': 'Could not be placed — no free slot was found',
|
||||
'planResults.warningDaysLostAtAge4': 'Days at risk of being lost at age 4',
|
||||
'planResults.warningSgiRisk': 'Risk to SGI protection',
|
||||
'planResults.warningVacationExpiring': 'Vacation days at risk of being lost',
|
||||
'planResults.previewBanner':
|
||||
'This is a suggestion — nothing is saved automatically. You can adjust your wishes above and suggest a new plan as many times as you like.',
|
||||
'planResults.warningsTitle': 'Warnings',
|
||||
'planResults.yearOverviewTitle': 'Overview by year',
|
||||
'planResults.noWindowsInHorizon': 'No windows within the planning horizon.',
|
||||
'planResults.yearHeader': 'Year',
|
||||
'planResults.leavesHeader': 'Leave periods',
|
||||
'planResults.weeksUnit': 'weeks',
|
||||
'planResults.detailTitle': 'Leave periods in detail',
|
||||
'planResults.weekSingular': 'week',
|
||||
'planResults.wished': 'wished',
|
||||
'planResults.funded': 'funded',
|
||||
'planResults.strategyPrefix': 'Strategy:',
|
||||
'planResults.shortfallLabel': 'Shortfall:',
|
||||
'planResults.shortfallCouldNotBeFunded': '{days} {dayWord} could not be funded.',
|
||||
'planResults.daySingular': 'day',
|
||||
'planResults.daysPlural': 'days',
|
||||
'planResults.freeDaySingular': 'free day',
|
||||
'planResults.freeDaysPlural': 'free days',
|
||||
'planResults.fullyFunded': 'Fully funded',
|
||||
'planResults.applySavedPrefix': 'The plan has been saved!',
|
||||
'planResults.createdClause': '{count} new {dayWord} created',
|
||||
'planResults.replacedClause': ', {count} {dayWord} replaced',
|
||||
'planResults.skippedIdenticalClause': ', {count} {dayWord} skipped (already identical)',
|
||||
'planResults.skippedFreeClause': ', {count} {dayWord} skipped',
|
||||
'planResults.rejectionsClause': '. Rejected: {rejections}',
|
||||
'planResults.confirmOverwriteTitle': 'Conflict with existing days',
|
||||
'planResults.confirmOverwriteIntro': 'The following days are already planned and will be replaced:',
|
||||
'planResults.collisionDateHeader': 'Date',
|
||||
'planResults.collisionExistingHeader': 'Existing type',
|
||||
'planResults.collisionProposedHeader': 'Proposed type',
|
||||
'planResults.confirmOverwriteButton': 'Yes, replace the existing days',
|
||||
'planResults.applyButton': 'Use plan',
|
||||
'planResults.applyingButton': 'Saving...',
|
||||
'planResults.cancelButton': 'Cancel',
|
||||
'planResults.balancesTitle': 'Remaining balances after the plan',
|
||||
'planResults.parentalDaysTitle': 'Parental-benefit days',
|
||||
'planResults.parentHeader': 'Parent',
|
||||
'planResults.sjukpenningLeftHeader': 'Sjukpenningnivå days left',
|
||||
'planResults.lagstaLeftHeader': 'Lägstanivå days left',
|
||||
'planResults.dubbeldagarLeftHeader': 'Double days left',
|
||||
'planResults.vacationDaysTitle': 'Vacation days',
|
||||
'planResults.usedHeader': 'Used',
|
||||
'planResults.totalAvailableHeader': 'Total available',
|
||||
'planResults.vacationAssumedBadge': 'Assumed',
|
||||
'planResults.vacationAssumedPrompt':
|
||||
'No value is recorded for this year — the statutory 25 days has been assumed. Confirm or edit it in Settings.',
|
||||
|
||||
// §9.2 — surplus advice
|
||||
'planResults.surplusAdviceTitle': 'You have unused days',
|
||||
'planResults.surplusAdviceIntro':
|
||||
'This plan leaves days unused that would otherwise be lost at the child\'s 4th birthday. A few suggestions:',
|
||||
'planResults.surplusAdviceWishMoreLeave': 'Wish for more leave',
|
||||
'planResults.surplusAdviceShiftMixToFp': 'Spend parental-benefit days instead of vacation',
|
||||
'planResults.surplusAdviceFrontLoadEarlyYears': 'Take more leave while the child is small',
|
||||
'planResults.surplusAdviceUseLessVacation': 'Save fewer vacation days',
|
||||
|
||||
// §3.5 — message_key + data bodies for the five surfaces that used to
|
||||
// render backend-generated Swedish regardless of locale. Domain terms
|
||||
// (semesterdag, föräldrapenning, sjukpenningnivå/lägstanivå, dubbeldag,
|
||||
// SGI) are kept in Swedish, matching the rest of this catalogue.
|
||||
'planResults.explanationSummary':
|
||||
'{wish_name} {year} — {weeks_wished} weeks wished, {weeks_funded} weeks funded',
|
||||
'planResults.explanationShortfallSuffix': ' ({shortfall_days} days short)',
|
||||
'planResults.explanationDetailVacationAndFp':
|
||||
'you keep your salary for {vacation_days} days and save {fp_days} parental-benefit days',
|
||||
'planResults.explanationDetailVacationOnly': '{vacation_days} vacation days',
|
||||
'planResults.explanationDetailSjukpenning':
|
||||
'{sjukpenning_days} parental-benefit days (sjukpenningnivå, income-based)',
|
||||
'planResults.explanationDetailLagsta':
|
||||
'{lagsta_days} parental-benefit days (lägstanivå, minimum rate)',
|
||||
'planResults.explanationDetailFree': '{free_days} fully free days (weekend/public holiday)',
|
||||
|
||||
'planResults.remedyReducePreschoolReserve':
|
||||
'Reducing the preschool planning-day reserve ({reserved} reserved) frees up {reserved} days',
|
||||
'planResults.remedyCarryVacationForward':
|
||||
'{spare_total} vacation days from previous years can be carried forward',
|
||||
'planResults.remedyLowerPriorityWish':
|
||||
'Lowering {wish_name} from {from_weeks} to {to_weeks} weeks frees up {freed_days} days',
|
||||
'planResults.remedyShiftWindowIntoFreeDays':
|
||||
'Moving {wish_name} closer to a weekend frees up {shift_gain} days',
|
||||
|
||||
'planResults.warningBodyDaysLostAtAge4':
|
||||
"{parent_name} risks losing {expiring_days} days at the child's 4th birthday ({fourth_birthday}) unless more days are used before then.",
|
||||
'planResults.warningBodySgiRisk':
|
||||
'{parent_name} risks their SGI protection for the week starting {week_start} ({days_planned} day(s) planned, at least {min_days_per_week} required).',
|
||||
'planResults.warningBodyVacationExpiring':
|
||||
'{parent_name} has {saved_days_in} saved vacation day(s) from {year} at risk of being forfeited.',
|
||||
|
||||
'planResults.surplusAdviceBodyWishMoreLeave':
|
||||
"{parent_name} can afford {weeks} more week(s) of leave ({days_involved} parental-benefit days would otherwise be forfeited at the child's 4th birthday) — add a week in the summer or an autumn break.",
|
||||
'planResults.surplusAdviceBodyShiftMixToFp':
|
||||
'{parent_name} could swap {days_involved} vacation day(s) for parental-benefit days in this plan — the parental-benefit days would otherwise be forfeited at the 4th birthday, while the vacation days would not.',
|
||||
'planResults.surplusAdviceBodyFrontLoadEarlyYears':
|
||||
'{parent_name} has a large surplus ({days_involved} parental-benefit days) — take more days now while the child is still small and you are home anyway, rather than spreading them thinly until the 4th birthday.',
|
||||
'planResults.surplusAdviceBodyUseLessVacation':
|
||||
'{parent_name} could take {days_involved} fewer vacation day(s) ({years_text}) without going below the statutory minimum ({min_vacation_days_per_year} days/year) — save them instead to cover parental-benefit days that would otherwise be forfeited.',
|
||||
|
||||
'planResults.rejectionInvalidDayType': 'Day {index} ({date}): invalid day type "{day_type}"',
|
||||
'planResults.rejectionConflictingDuplicate':
|
||||
'Conflicting duplicate entries for {date} (child {child_id}, parent {parent_personal_data_uuid}) in the same batch',
|
||||
'planResults.rejectionCollisionsNeedReplace':
|
||||
'{count} day(s) already planned differently; pass replace=true to overwrite',
|
||||
'planResults.rejectionDubbeldagWindowExceeded':
|
||||
"Double days are only allowed within the child's first {months_limit} months (deadline {deadline})",
|
||||
'planResults.rejectionDubbeldagQuotaReached':
|
||||
'The double-days quota ({max_dubbeldagar} days) has already been reached',
|
||||
'planResults.rejectionDubbeldagInvalid': 'The double day for {date} could not be approved',
|
||||
'planResults.rejectionDubbeldagBatchQuotaExceeded':
|
||||
'This batch would result in {projected_total} double days, the max is {max_dubbeldagar}',
|
||||
'planResults.rejectionOutsideEligibleWindow':
|
||||
"{date}: outside the child's eligible window ({birth_date} – {hard_expiry})",
|
||||
'planResults.rejectionInsufficientBalance':
|
||||
'This batch needs {needed} more "{tier}" day(s) but only {remaining} remain',
|
||||
|
||||
// §3.3 part 2 — plan-aware age-4 projection
|
||||
'planResults.age4ProjectionsTitle': "At the child's 4th birthday, if this plan is followed",
|
||||
'planResults.age4ProjectionsIntro':
|
||||
"Here's what the 4th birthday looks like if this plan is taken as proposed — life may of course work out differently.",
|
||||
'planResults.fourthBirthdayHeader': '4th birthday',
|
||||
'planResults.projectedLeftHeader': 'Projected left',
|
||||
'planResults.savableAfterB4Header': 'Savable',
|
||||
'planResults.expiringAtB4Header': 'Expiring',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Leave type labels (shared lookup used by Dashboard + Calendar)
|
||||
// ---------------------------------------------------------------------
|
||||
'leaveType.vacation': 'Vacation',
|
||||
'leaveType.sjukpenning': 'Sjukpenningnivå (income-based)',
|
||||
'leaveType.lagsta': 'Lägstanivå (minimum rate)',
|
||||
'leaveType.unknown': 'Unknown',
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
LanguageProvider,
|
||||
useTranslation,
|
||||
detectBrowserLocale,
|
||||
readStoredLocale,
|
||||
resolveLocale,
|
||||
translate,
|
||||
toTranslationParams,
|
||||
} from './LanguageContext'
|
||||
export type { Locale, TranslationParams, LanguageContextValue } from './LanguageContext'
|
||||
export type { TranslationKey } from './sv'
|
||||
@@ -0,0 +1,602 @@
|
||||
/**
|
||||
* Swedish message catalogue — the single source of truth for translation keys.
|
||||
*
|
||||
* `en.ts` is typed against `keyof typeof sv`, so adding a key here without
|
||||
* adding it to `en.ts` (or vice versa) is a compile-time (`tsc --noEmit`)
|
||||
* error. This is deliberate: it makes locale drift impossible instead of
|
||||
* merely discouraged.
|
||||
*
|
||||
* These strings were extracted verbatim from the components that previously
|
||||
* hardcoded them — this file is the authoritative Swedish copy.
|
||||
*/
|
||||
export const sv = {
|
||||
// ---------------------------------------------------------------------
|
||||
// App-wide
|
||||
// ---------------------------------------------------------------------
|
||||
'app.name': 'Föräldraledigplannerare',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Common
|
||||
// ---------------------------------------------------------------------
|
||||
'common.loadingEllipsis': 'Laddar…',
|
||||
'common.save': 'Spara',
|
||||
'common.saving': 'Sparar…',
|
||||
'common.cancel': 'Avbryt',
|
||||
'common.close': 'Stäng',
|
||||
'common.delete': 'Ta bort',
|
||||
'common.deleting': 'Tar bort…',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Navigation / Header / Footer
|
||||
// ---------------------------------------------------------------------
|
||||
'nav.overview': 'Översikt',
|
||||
'nav.calendar': 'Kalender',
|
||||
'nav.plan': 'Planering',
|
||||
'nav.tips': 'Tips',
|
||||
'nav.settings': 'Inställningar',
|
||||
'nav.logout': 'Logga ut',
|
||||
'nav.selectChildAria': 'Välj barn',
|
||||
'nav.logoutAria': 'Logga ut',
|
||||
'footer.copyright': '© {year} Föräldraledigplannerare. Alla rättigheter förbehållna.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Auth
|
||||
// ---------------------------------------------------------------------
|
||||
'auth.loadingEllipsis': 'Laddar…',
|
||||
'auth.createAccountTitle': 'Skapa konto',
|
||||
'auth.loginTitle': 'Logga in',
|
||||
'auth.namePlaceholder': 'Namn',
|
||||
'auth.emailPlaceholder': 'E-post',
|
||||
'auth.passwordPlaceholder': 'Lösenord',
|
||||
'auth.registerButton': 'Registrera',
|
||||
'auth.loginButton': 'Logga in',
|
||||
'auth.haveAccountButton': 'Har redan konto',
|
||||
'auth.createAccountButton': 'Skapa konto',
|
||||
'auth.genericError': 'Något gick fel',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Dashboard
|
||||
// ---------------------------------------------------------------------
|
||||
'dashboard.loading': 'Laddar...',
|
||||
'dashboard.addChildTitle': 'Lägg till barn',
|
||||
'dashboard.addChildIntro': 'Du har inga barn registrerade ännu. Lägg till ditt första barn för att börja.',
|
||||
'dashboard.childNameLabel': 'Barnets namn',
|
||||
'dashboard.namePlaceholder': 'Namn',
|
||||
'dashboard.birthdayLabel': 'Födelsedag',
|
||||
'dashboard.addChildButton': 'Lägg till barn',
|
||||
'dashboard.addingChild': 'Lägger till...',
|
||||
'dashboard.fillAllFields': 'Fyll i alla fält',
|
||||
'dashboard.createChildError': 'Kunde inte skapa barn',
|
||||
'dashboard.fetchFromFk': 'Hämta från Försäkringskassan',
|
||||
'dashboard.enterManually': 'Ange uppgifter manuellt',
|
||||
'dashboard.overviewTitle': 'Översikt',
|
||||
'dashboard.usedDays': 'Använda dagar',
|
||||
'dashboard.daysLeft': 'Kvarvarande dagar',
|
||||
'dashboard.expiringAtB4': 'Förfaller vid 4 års ålder',
|
||||
'dashboard.plannedDaysTitle': 'Planerade dagar',
|
||||
'dashboard.remainingUntilB4Title': 'Återstående till 4 års ålder',
|
||||
'dashboard.saveManualFetchError': 'Kunde inte spara uppgifterna',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Settings
|
||||
// ---------------------------------------------------------------------
|
||||
'settings.loading': 'Laddar inställningar...',
|
||||
'settings.title': 'Inställningar',
|
||||
'settings.childSectionTitle': 'Barn',
|
||||
'settings.selectedChildLabel': 'Valt barn:',
|
||||
'settings.childBirthDateLabel': 'Barnets födelsedatum',
|
||||
'settings.preschoolHelp':
|
||||
'Förskolor stänger oftast ca 6 dagar per år för planering. Ange hur många dagar din förskola har, så reserveras de i planeringen.',
|
||||
'settings.preschoolPlanningDaysLabel': 'Förskolans planeringsdagar per år',
|
||||
'settings.saveChildButton': 'Spara barnuppgifter',
|
||||
'settings.noChildSelected': 'Inget barn valt ännu.',
|
||||
'settings.profileSectionTitle': 'Din profil',
|
||||
'settings.profileHelp': 'Din lönetyp och sysselsättning används för att uppskatta din SGI och planera din ledighet.',
|
||||
'settings.saveProfileButton': 'Spara din profil',
|
||||
'settings.planningSectionTitle': 'Planering',
|
||||
'settings.planningHelp':
|
||||
'Ditt planeringsmål och om du planerar själv eller tillsammans med en partner styr hur planeraren föreslår fördelning av dagar.',
|
||||
'settings.savePlanningButton': 'Spara planering',
|
||||
'settings.vacationSectionTitle': 'Semesterdagar',
|
||||
'settings.vacationHelp':
|
||||
'Ange semesterdagar per år. Det första året är du oftast hemma mer och har färre dagar att lägga på semester, medan kommande år har din fulla årliga tilldelning. Detta styr hur mycket tid planeraren har att arbeta med.',
|
||||
'settings.calendarSectionTitle': 'Kalender',
|
||||
'settings.showWeekNumbers': 'Visa veckonummer',
|
||||
'settings.languageSectionTitle': 'Språk / Language',
|
||||
'settings.languageLabel': 'Välj språk / Select language',
|
||||
'settings.languageAutoOption': 'Automatisk (webbläsarens språk)',
|
||||
'settings.languageSwedishOption': 'Svenska',
|
||||
'settings.languageEnglishOption': 'English',
|
||||
'settings.partnerSectionTitle': 'Partner',
|
||||
'settings.partnerIntro': 'Lägg till en partner för att planera föräldraledigheten tillsammans.',
|
||||
'settings.partnerNamePlaceholder': 'Partnerns namn',
|
||||
'settings.addPartnerButton': 'Lägg till partner',
|
||||
'settings.addingPartner': 'Lägger till...',
|
||||
'settings.saveChildSuccess': 'Barnets uppgifter sparade!',
|
||||
'settings.saveChildError': 'Kunde inte spara barnets uppgifter',
|
||||
'settings.saveProfileSuccess': 'Din profil sparades!',
|
||||
'settings.saveProfileError': 'Kunde inte spara din profil',
|
||||
'settings.savePlanningSuccess': 'Planeringsinställningar sparade!',
|
||||
'settings.savePlanningError': 'Kunde inte spara planeringsinställningar',
|
||||
'settings.saveVacationSuccess': 'Semesterdagar för {year} sparade!',
|
||||
'settings.saveVacationError': 'Kunde inte spara semesterdagar',
|
||||
'settings.partnerNameRequired': 'Ange ett namn för partnern',
|
||||
'settings.addPartnerSuccess': 'Partner tillagd! Ladda om sidan för att se ändringarna.',
|
||||
'settings.addPartnerError': 'Kunde inte lägga till partner',
|
||||
'settings.dayOverridesSectionTitle': 'Undantag i kalendern',
|
||||
'settings.dayOverridesHelp':
|
||||
'Planeraren utgår från den svenska kalendern (röda dagar, klämdagar, julafton, midsommarafton och pingstafton är normalt lediga). Här kan du ange undantag för din arbetsplats — om du måste jobba juldagen eller är ledig på annars arbetande dagar.',
|
||||
'settings.redDaysSectionTitle': 'Röda dagar',
|
||||
'settings.redDaysHelp':
|
||||
'En lista över röda dagar enligt den svenska kalendern, tillsammans med dina egna undantag — fyra år i taget.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Day overrides input
|
||||
// ---------------------------------------------------------------------
|
||||
'dayOverridesInput.empty': 'Du har inga undantag i kalendern ännu.',
|
||||
'dayOverridesInput.workingStatus': 'Jag jobbar den här dagen',
|
||||
'dayOverridesInput.offStatus': 'Jag är ledig den här dagen',
|
||||
'dayOverridesInput.noteLabel': 'Anteckning: {note}',
|
||||
'dayOverridesInput.addSectionTitle': 'Lägg till undantag',
|
||||
'dayOverridesInput.dateLabel': 'Datum',
|
||||
'dayOverridesInput.statusLegend': 'Status',
|
||||
'dayOverridesInput.noteFieldLabel': 'Anteckning (valfritt)',
|
||||
'dayOverridesInput.notePlaceholder': 'T.ex. Jobbade på juldagen',
|
||||
'dayOverridesInput.charCount': '{count}/280',
|
||||
'dayOverridesInput.addButton': 'Lägg till undantag',
|
||||
'dayOverridesInput.addingButton': 'Lägger till...',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Red days list (Settings, §3.8)
|
||||
// ---------------------------------------------------------------------
|
||||
'redDaysList.prevWindow': 'Föregående 4 år',
|
||||
'redDaysList.nextWindow': 'Nästa 4 år',
|
||||
'redDaysList.loading': 'Laddar röda dagar...',
|
||||
'redDaysList.emptyYear': 'Inga röda dagar eller undantag detta år.',
|
||||
'redDaysList.overrideWorking': 'Undantag: arbetsdag',
|
||||
'redDaysList.overrideFree': 'Undantag: ledig dag',
|
||||
'redDaysList.effectiveFree': 'Ledig',
|
||||
'redDaysList.effectiveWorking': 'Arbetsdag',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Plan
|
||||
// ---------------------------------------------------------------------
|
||||
'plan.loading': 'Laddar planering...',
|
||||
'plan.title': 'Planering',
|
||||
'plan.intro':
|
||||
'Beskriv hur du vill vara ledig — till exempel 6 veckor varje sommar och 4 veckor varje jul — och föreslå en plan som visar exakt vilka dagar som redan är lediga, vilka som kostar en semesterdag och vilka som kostar en föräldrapenningdag.',
|
||||
'plan.wishesTitle': 'Dina önskemål',
|
||||
'plan.proposeButton': 'Föreslå plan',
|
||||
'plan.calculating': 'Beräknar...',
|
||||
'plan.addAtLeastOneWish': 'Lägg till minst ett önskemål ovan för att föreslå en plan.',
|
||||
'plan.addWishError': 'Kunde inte lägga till önskemål',
|
||||
'plan.updateWishError': 'Kunde inte spara önskemål',
|
||||
'plan.removeWishError': 'Kunde inte ta bort önskemål',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Tips
|
||||
// ---------------------------------------------------------------------
|
||||
'tips.loading': 'Laddar tips...',
|
||||
'tips.title': 'Optimeringstips',
|
||||
'tips.empty': 'Inga tips än. Bra jobbat med planeringen av din föräldraledighet!',
|
||||
'tip.aria': 'Tips: {title}',
|
||||
'tip.dismissAria': 'Avfärda: {title}',
|
||||
|
||||
// Backend rule id -> title/body. `data` on the Tip carries the params
|
||||
// referenced below (see backend/app/services/tips_service.py) — the
|
||||
// backend's `message_key` is a genuine i18n key by design, so these are
|
||||
// translated client-side rather than left as backend-generated text.
|
||||
'tip.useVacationBeforeExpiry.title': 'Semesterdagar riskerar att gå förlorade',
|
||||
'tip.useVacationBeforeExpiry.body':
|
||||
'{parent_name} har {saved_days} sparade semesterdagar från {year} som riskerar att gå förlorade om de inte används.',
|
||||
'tip.balanceBetweenParents.title': 'Obalans mellan föräldrarna',
|
||||
'tip.balanceBetweenParents.body':
|
||||
'{leading_parent} har tagit ut {gap_days} fler dagar än {lagging_parent}. Fundera på att jämna ut fördelningen.',
|
||||
'tip.preserveForAge4.title': 'Dagar riskerar att gå förlorade vid 4 års ålder',
|
||||
'tip.preserveForAge4.body':
|
||||
'{parent_name} riskerar att förlora {expiring_days} dagar när barnet fyller 4 år ({fourth_birthday}). Endast 96 dagar kan sparas efter det.',
|
||||
'tip.useDubbeldagarStrategically.title': 'Använd en dubbeldag',
|
||||
'tip.useDubbeldagarStrategically.body':
|
||||
'Ni har båda föräldrapenning planerad {date} men den är inte markerad som dubbeldag.',
|
||||
'tip.lagstaNivaCoverage.title': 'Täck upp med lägstanivå',
|
||||
'tip.lagstaNivaCoverage.body':
|
||||
'{parent_name} har bara {sjukpenning_left} dagar kvar på sjukpenningnivå men {lagsta_left} dagar kvar på lägstanivå.',
|
||||
'tip.stretchTimeAtHome.title': 'Sträck ut tiden hemma',
|
||||
'tip.stretchTimeAtHome.body':
|
||||
'{parent_name} planerar {weekly_intensity} dagar per vecka den kommande månaden, vilket lämnar {projected_left} dagar kvar vid 4 års ålder.',
|
||||
'tip.skipFpFirst12Months.title': 'Spara föräldrapenning första året',
|
||||
'tip.skipFpFirst12Months.body':
|
||||
'{parent_name} har föräldrapenning planerad innan barnet fyller 1 år. Med målet "mest tid hemma" kan det löna sig att spara dagarna till senare.',
|
||||
'tip.sgiMinimumAfterAge1.title': 'Risk för SGI-skydd',
|
||||
'tip.sgiMinimumAfterAge1.body':
|
||||
'{parent_name} planerar färre än {min_days_per_week} föräldrapenningdagar per vecka den kommande månaden, vilket kan påverka SGI-skyddet.',
|
||||
'tip.wrapWeekendWithVacation.title': 'Utnyttja helgen',
|
||||
'tip.wrapWeekendWithVacation.body':
|
||||
'{parent_name} har föräldrapenning planerad fredagen {friday} och måndagen {monday}. En semesterdag mellan dem ger en längre ledighet.',
|
||||
'tip.wrapRedDaysWithVacation.title': 'Utnyttja röda dagar',
|
||||
'tip.wrapRedDaysWithVacation.body':
|
||||
'{parent_name} har föräldrapenning planerad nära en klämperiod ({cluster_start}–{cluster_end}). En semesterdag runt de röda dagarna kan ge en längre sammanhängande ledighet.',
|
||||
'tip.longBreakParentSwap.title': 'Överväg att växla av varandra',
|
||||
'tip.longBreakParentSwap.body':
|
||||
'{parent_name} täcker mindre än hälften av "{break_name}" ({start_date}–{end_date}). Låt {other_parent_name} ta en del av perioden.',
|
||||
'tip.reserveForPreschoolPlanningDays.title': 'Reservera dagar för förskolans planeringsdagar',
|
||||
'tip.reserveForPreschoolPlanningDays.body':
|
||||
'{parent_name} har {days_left} dagar kvar, vilket knappt räcker för förskolans {buffer_days} planeringsdagar. Reservera dagar i god tid.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Calendar page
|
||||
// ---------------------------------------------------------------------
|
||||
'calendar.loading': 'Laddar kalender...',
|
||||
'calendar.prevMonthAria': 'Föregående månad',
|
||||
'calendar.nextMonthAria': 'Nästa månad',
|
||||
'calendar.advanceMonthAria': '{month} - klicka för att byta månad',
|
||||
'calendar.advanceYearAria': '{year} - klicka för att byta år',
|
||||
'calendar.weekdayMon': 'Mån',
|
||||
'calendar.weekdayTue': 'Tis',
|
||||
'calendar.weekdayWed': 'Ons',
|
||||
'calendar.weekdayThu': 'Tor',
|
||||
'calendar.weekdayFri': 'Fre',
|
||||
'calendar.weekdaySat': 'Lör',
|
||||
'calendar.weekdaySun': 'Sön',
|
||||
'calendar.legendTitle': 'Teckenförklaring',
|
||||
'calendar.legendVacation': 'Semester',
|
||||
'calendar.legendIncomeBased': 'Sjukpenningnivå',
|
||||
'calendar.legendMinimumRate': 'Lägstanivå',
|
||||
'calendar.legendDoubleDay': 'Dubbeldag',
|
||||
'calendar.legendRedDay': 'Röd dag',
|
||||
'calendar.legendKlamdag': 'Klämdag',
|
||||
'calendar.legendOverride': 'Undantag (arbets-/ledig dag)',
|
||||
'calendar.saveError': 'Det gick inte att spara de planerade dagarna',
|
||||
'calendar.deleteError': 'Det gick inte att ta bort den planerade dagen',
|
||||
'calendar.overrideError': 'Det gick inte att uppdatera undantaget',
|
||||
'calendar.overrideModeLabel': 'Markera arbets-/ledig dag',
|
||||
'calendar.overrideModeActiveLabel': 'Avsluta redigering av dagar',
|
||||
'calendar.overrideModeHint':
|
||||
'Klicka på en dag för att markera den som arbetsdag eller ledig dag. Klicka igen på en markerad dag för att återställa den.',
|
||||
'calendar.you': 'Du',
|
||||
'calendar.partner': 'Partner',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Calendar day / event (accessibility)
|
||||
// ---------------------------------------------------------------------
|
||||
'calendarDay.ariaWithEvents': '{date} med {count} händelse(r)',
|
||||
'calendarDay.klamdagAria': 'klämdag',
|
||||
'calendarDay.overrideWorkingAria': 'markerad som arbetsdag (undantag)',
|
||||
'calendarDay.overrideFreeAria': 'markerad som ledig dag (undantag)',
|
||||
'calendarEvent.dismissAria': 'Avfärda {leaveType}',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Dashboard components
|
||||
// ---------------------------------------------------------------------
|
||||
'daysBreakdown.empty': 'Inga dagar registrerade än',
|
||||
'daysBreakdown.typeHeader': 'Typ av ledighet',
|
||||
'daysBreakdown.daysHeader': 'Dagar',
|
||||
|
||||
'overviewMetrics.defaultTitle': 'Översikt',
|
||||
|
||||
'parentalLeaveBreakdown.empty': 'Ingen data hämtad ännu — tryck på Hämta från Försäkringskassan.',
|
||||
'parentalLeaveBreakdown.typeHeader': 'Typ',
|
||||
'parentalLeaveBreakdown.remainingHeader': 'Återstående',
|
||||
'parentalLeaveBreakdown.totalHeader': 'Totalt',
|
||||
'parentalLeaveBreakdown.transferableDays': 'Överlåtbara dagar',
|
||||
'parentalLeaveBreakdown.dubbeldagarLeft': 'Dubbeldagar kvar',
|
||||
'parentalLeaveBreakdown.sjukpenning': 'Sjukpenning',
|
||||
'parentalLeaveBreakdown.lagstaNiva': 'Lägsta nivå',
|
||||
'parentalLeaveBreakdown.reservedNote':
|
||||
'{reserved} av dina dagar är reserverade och kan inte överlåtas till den andra föräldern (max {transferable} dagar kan överlåtas).',
|
||||
'parentalLeaveBreakdown.sourceScraped': 'Hämtad från Försäkringskassan',
|
||||
'parentalLeaveBreakdown.sourceManual': 'Manuellt angiven',
|
||||
|
||||
'progressChart.title': 'Föräldraledighet – förlopp',
|
||||
'progressChart.age': 'Ålder: {age}',
|
||||
'progressChart.weeksStat': '{current} / {target} veckor',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Inputs
|
||||
// ---------------------------------------------------------------------
|
||||
'childBirthdayInput.defaultLabel': 'Barnets födelsedatum',
|
||||
'childBirthdayInput.birthDateLabel': 'Födelsedatum',
|
||||
'childBirthdayInput.preschoolStartLabel': 'Förskolestart (valfritt)',
|
||||
|
||||
'parentProfileInput.salaryTypeLabel': 'Lönetyp',
|
||||
'parentProfileInput.selectSalaryType': 'Välj lönetyp',
|
||||
'parentProfileInput.monthly': 'Månadslön',
|
||||
'parentProfileInput.hourly': 'Timlön',
|
||||
'parentProfileInput.employmentStatusLabel': 'Sysselsättning',
|
||||
'parentProfileInput.selectEmploymentStatus': 'Välj sysselsättning',
|
||||
'parentProfileInput.employed': 'Anställd',
|
||||
'parentProfileInput.jobseeking': 'Arbetssökande',
|
||||
'parentProfileInput.none': 'Ingen',
|
||||
|
||||
'planningGoalSelector.legend': 'Planeringsmål',
|
||||
'planningGoalSelector.maxTime': 'Mest tid hemma',
|
||||
'planningGoalSelector.maxTimeHelp': 'Spara dagar och förläng din totala lediga tid, med lägre inkomst nu.',
|
||||
'planningGoalSelector.maxMoney': 'Mest pengar',
|
||||
'planningGoalSelector.maxMoneyHelp': 'Skydda din inkomst, t.ex. genom att lägga semester runt helger och röda dagar.',
|
||||
'planningGoalSelector.balanced': 'Balanserad',
|
||||
'planningGoalSelector.balancedHelp': 'En avvägning mellan tid hemma och inkomst.',
|
||||
|
||||
'planningModeSelector.legend': 'Planerar du själv eller tillsammans med en partner?',
|
||||
'planningModeSelector.solo': 'Bara jag',
|
||||
'planningModeSelector.couple': 'Jag och min partner',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Modals
|
||||
// ---------------------------------------------------------------------
|
||||
'qrModal.title': 'Hämta från Försäkringskassan',
|
||||
'qrModal.closeAria': 'Stäng dialogruta',
|
||||
'qrModal.statusLabel': 'Status:',
|
||||
'qrModal.qrInstruction': 'Öppna BankID-appen och skanna QR-koden:',
|
||||
'qrModal.qrAlt': 'BankID QR-kod',
|
||||
'qrModal.authenticating': 'Autentiserar…',
|
||||
'qrModal.fetching': 'Hämtar data från FK…',
|
||||
'qrModal.success': 'Klar! Data hämtad från Försäkringskassan.',
|
||||
'qrModal.cancelled': 'Sessionen avbröts.',
|
||||
'qrModal.closeButton': 'Stäng',
|
||||
'qrModal.cancelButton': 'Avbryt',
|
||||
'qrModal.errorLabel': 'Fel:',
|
||||
|
||||
'manualFetchModal.title': 'Ange föräldrapenninguppgifter',
|
||||
'manualFetchModal.closeAria': 'Stäng',
|
||||
'manualFetchModal.description':
|
||||
'Ange dina uppgifter om föräldrapenning från Försäkringskassan manuellt om automatisk hämtning inte är tillgänglig.',
|
||||
'manualFetchModal.totalDaysSectionTitle': 'Totalt antal dagar',
|
||||
'manualFetchModal.totalDaysLabel': 'Totalt antal dagar (0–480)',
|
||||
'manualFetchModal.daysUsedLabel': 'Använda dagar',
|
||||
'manualFetchModal.daysRemaining': 'Kvarvarande dagar:',
|
||||
'manualFetchModal.remainingBreakdownTitle': 'Fördelning av kvarvarande dagar',
|
||||
'manualFetchModal.reservedDaysLabel': 'Reserverade dagar kvar',
|
||||
'manualFetchModal.transferableDaysLabel': 'Överlåtbara dagar kvar',
|
||||
'manualFetchModal.dubbeldagarLabel': 'Dubbeldagar kvar (0–60)',
|
||||
'manualFetchModal.tierBreakdownTitle': 'Fördelning per nivå',
|
||||
'manualFetchModal.sjukpenningUsedLabel': 'Använda dagar på sjukpenningnivå (390-nivån)',
|
||||
'manualFetchModal.lagstaUsedLabel': 'Använda dagar på lägstanivå (90-nivån)',
|
||||
'manualFetchModal.cancelButton': 'Avbryt',
|
||||
'manualFetchModal.saveButton': 'Spara',
|
||||
'manualFetchModal.savingButton': 'Sparar…',
|
||||
'manualFetchModal.saveError': 'Det gick inte att spara de manuella uppgifterna',
|
||||
|
||||
'plannedDayModal.editTitle': 'Redigera planerad dag',
|
||||
'plannedDayModal.newTitle': 'Planera en dag',
|
||||
'plannedDayModal.rangeTitle': 'Planera {count} dagar',
|
||||
'plannedDayModal.closeAria': 'Stäng',
|
||||
'plannedDayModal.leaveTypeLabel': 'Typ av ledighet',
|
||||
'plannedDayModal.vacationOption': 'Semester',
|
||||
'plannedDayModal.sjukpenningOption': 'Sjukpenningnivå (390-nivån)',
|
||||
'plannedDayModal.lagstaOption': 'Lägstanivå (90-nivån)',
|
||||
'plannedDayModal.parentLabel': 'Förälder',
|
||||
'plannedDayModal.doubleDayLabel': 'Dubbeldag (båda föräldrarna tar ut ledighet)',
|
||||
'plannedDayModal.doubleDayHint': 'Dubbeldagar gäller inte för semester',
|
||||
'plannedDayModal.deleteButton': 'Ta bort',
|
||||
'plannedDayModal.deletingButton': 'Tar bort…',
|
||||
'plannedDayModal.cancelButton': 'Avbryt',
|
||||
'plannedDayModal.saveButton': 'Spara',
|
||||
'plannedDayModal.savingButton': 'Sparar…',
|
||||
'plannedDayModal.confirmDelete': 'Är du säker på att du vill ta bort den här planerade dagen?',
|
||||
'plannedDayModal.saveError': 'Det gick inte att spara den planerade dagen',
|
||||
'plannedDayModal.deleteError': 'Det gick inte att ta bort den planerade dagen',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Vacation days
|
||||
// ---------------------------------------------------------------------
|
||||
'vacationDaysList.loading': 'Laddar semesterdagar…',
|
||||
'vacationDaysList.empty': 'Inga semesterdagar registrerade.',
|
||||
'vacationDaysList.itemDays': '{count} dagar',
|
||||
'vacationDaysList.savedSuffix': '(+{count} sparade)',
|
||||
|
||||
'vacationDaysTable.empty': 'Inga år att visa ännu.',
|
||||
'vacationDaysTable.yearHeader': 'År',
|
||||
'vacationDaysTable.vacationDaysHeader': 'Semesterdagar',
|
||||
'vacationDaysTable.savedDaysHeader': 'Sparade dagar in',
|
||||
'vacationDaysTable.saveButton': 'Spara',
|
||||
'vacationDaysTable.assumedBadge': 'Antaget',
|
||||
'vacationDaysTable.assumedPrompt':
|
||||
'Inget värde är registrerat för detta år — 25 dagar (lagstadgat) har antagits. Bekräfta eller ändra nedan.',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Planning: WishEditor
|
||||
// ---------------------------------------------------------------------
|
||||
'wishEditor.presetSommar': 'Sommar',
|
||||
'wishEditor.presetJullov': 'Jullov',
|
||||
'wishEditor.presetHostlov': 'Höstlov',
|
||||
'wishEditor.customLeaveButton': '+ Egen ledighet',
|
||||
'wishEditor.customLeaveName': 'Egen ledighet',
|
||||
'wishEditor.weekUnit': 'vecka',
|
||||
'wishEditor.weeksUnit': 'veckor',
|
||||
'wishEditor.emptyState': 'Inga ledighetsönskemål ännu. Välj ett förslag ovan för att komma igång.',
|
||||
'wishEditor.nameAria': 'Namn för {name}',
|
||||
'wishEditor.weeksLabel': 'Veckor',
|
||||
'wishEditor.priorityLabel': 'Prioritet',
|
||||
'wishEditor.parentLabel': 'Förälder',
|
||||
'wishEditor.eitherParent': 'Endera förälder',
|
||||
'wishEditor.bothParents': 'Båda föräldrarna',
|
||||
'wishEditor.hideDates': 'Dölj datum',
|
||||
'wishEditor.adjustDates': 'Justera datum',
|
||||
'wishEditor.startsLabel': 'Startar',
|
||||
'wishEditor.endsLabel': 'Slutar',
|
||||
'wishEditor.recurEveryYear': 'Återkommer varje år',
|
||||
'wishEditor.firstYearLabel': 'Första året',
|
||||
'wishEditor.untilAge4': 'Tills barnet fyller 4 år',
|
||||
'wishEditor.lastYearLabel': 'Sista året',
|
||||
'wishEditor.saveButton': 'Spara',
|
||||
'wishEditor.savingButton': 'Sparar...',
|
||||
'wishEditor.removeButton': 'Ta bort',
|
||||
'wishEditor.month1': 'Januari',
|
||||
'wishEditor.month2': 'Februari',
|
||||
'wishEditor.month3': 'Mars',
|
||||
'wishEditor.month4': 'April',
|
||||
'wishEditor.month5': 'Maj',
|
||||
'wishEditor.month6': 'Juni',
|
||||
'wishEditor.month7': 'Juli',
|
||||
'wishEditor.month8': 'Augusti',
|
||||
'wishEditor.month9': 'September',
|
||||
'wishEditor.month10': 'Oktober',
|
||||
'wishEditor.month11': 'November',
|
||||
'wishEditor.month12': 'December',
|
||||
|
||||
// PLANNING_ENGINE_SPEC.md §10.7 — fixed vs "när det passar bäst" toggle.
|
||||
'wishEditor.modeSectionLabel': 'Hur ska önskemålet placeras?',
|
||||
'wishEditor.modeFixed': 'Bestämda datum',
|
||||
'wishEditor.modeFlexible': 'När det passar bäst',
|
||||
'wishEditor.modeFlexibleExplainer':
|
||||
'Motorn väljer själv den bästa veckan inom sökintervallet nedan — den letar efter perioden med flest röda dagar och klämdagar, eftersom det kostar minst antal semesterdagar. Vid ett återkommande önskemål väljs ett nytt fönster varje år (påsken flyttar sig till exempel), och förslaget måste alltid bekräftas i planeringsfliken innan det sparas, precis som alla andra planer.',
|
||||
'wishEditor.boundStartsLabel': 'Sökintervall från',
|
||||
'wishEditor.boundEndsLabel': 'till',
|
||||
'wishEditor.adjustBound': 'Justera sökintervall',
|
||||
'wishEditor.boundTooShortError':
|
||||
'Sökintervallet är kortare än de {weeks} veckor som önskas — välj ett längre intervall eller minska antalet veckor.',
|
||||
'wishEditor.flexPresetsLabel': 'Snabbval — sök inom en säsong ("när det passar bäst"):',
|
||||
'wishEditor.seasonVar': 'Vår',
|
||||
'wishEditor.seasonSommar': 'Sommar',
|
||||
'wishEditor.seasonHost': 'Höst',
|
||||
'wishEditor.seasonJul': 'Jul',
|
||||
'wishEditor.flexBadge': 'när det passar bäst',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Planning: PlanResults
|
||||
// ---------------------------------------------------------------------
|
||||
'planResults.dayTypeFree': 'fria dagar (helg/röd dag)',
|
||||
'planResults.dayTypeVacation': 'semesterdagar',
|
||||
'planResults.dayTypeSjukpenning': 'FP-dagar (sjukpenningnivå)',
|
||||
'planResults.dayTypeLagsta': 'FP-dagar (lägstanivå)',
|
||||
'planResults.strategyDubbeldag': 'Dubbeldagar — båda föräldrarna hemma samtidigt',
|
||||
'planResults.strategyParentSwap': 'Föräldrarna växlar av varandra',
|
||||
'planResults.strategySolo': 'En förälder hemma',
|
||||
'planResults.strategyUnplaced': 'Kunde inte placeras — ingen ledig plats hittades',
|
||||
'planResults.warningDaysLostAtAge4': 'Dagar som riskerar att gå förlorade vid 4 år',
|
||||
'planResults.warningSgiRisk': 'Risk för SGI-skydd',
|
||||
'planResults.warningVacationExpiring': 'Semesterdagar som riskerar att gå förlorade',
|
||||
'planResults.previewBanner':
|
||||
'Detta är ett förslag — inget sparas automatiskt. Du kan justera dina önskemål ovan och föreslå en ny plan så många gånger du vill.',
|
||||
'planResults.warningsTitle': 'Varningar',
|
||||
'planResults.yearOverviewTitle': 'Översikt över åren',
|
||||
'planResults.noWindowsInHorizon': 'Inga fönster inom planeringshorisonten.',
|
||||
'planResults.yearHeader': 'År',
|
||||
'planResults.leavesHeader': 'Ledigheter',
|
||||
'planResults.weeksUnit': 'veckor',
|
||||
'planResults.detailTitle': 'Ledigheter i detalj',
|
||||
'planResults.weekSingular': 'vecka',
|
||||
'planResults.wished': 'önskade',
|
||||
'planResults.funded': 'finansierade',
|
||||
'planResults.strategyPrefix': 'Strategi:',
|
||||
'planResults.shortfallLabel': 'Underskott:',
|
||||
'planResults.shortfallCouldNotBeFunded': '{days} {dayWord} kunde inte finansieras.',
|
||||
'planResults.daySingular': 'dag',
|
||||
'planResults.daysPlural': 'dagar',
|
||||
'planResults.freeDaySingular': 'fri dag',
|
||||
'planResults.freeDaysPlural': 'fria dagar',
|
||||
'planResults.fullyFunded': 'Fullt finansierad',
|
||||
'planResults.applySavedPrefix': 'Planen har sparats!',
|
||||
'planResults.createdClause': '{count} nya {dayWord} skapades',
|
||||
'planResults.replacedClause': ', {count} {dayWord} ersattes',
|
||||
'planResults.skippedIdenticalClause': ', {count} {dayWord} hoppades över (redan likadana)',
|
||||
'planResults.skippedFreeClause': ', {count} {dayWord} hoppades över',
|
||||
'planResults.rejectionsClause': '. Avslag: {rejections}',
|
||||
'planResults.confirmOverwriteTitle': 'Konflikt med befintliga dagar',
|
||||
'planResults.confirmOverwriteIntro': 'Följande dagar är redan planerade och kommer att ersättas:',
|
||||
'planResults.collisionDateHeader': 'Datum',
|
||||
'planResults.collisionExistingHeader': 'Befintlig typ',
|
||||
'planResults.collisionProposedHeader': 'Föreslagen typ',
|
||||
'planResults.confirmOverwriteButton': 'Ja, ersätt de befintliga dagarna',
|
||||
'planResults.applyButton': 'Använd plan',
|
||||
'planResults.applyingButton': 'Sparar...',
|
||||
'planResults.cancelButton': 'Avbryt',
|
||||
'planResults.balancesTitle': 'Kvarvarande saldon efter planen',
|
||||
'planResults.parentalDaysTitle': 'Föräldrapenningdagar',
|
||||
'planResults.parentHeader': 'Förälder',
|
||||
'planResults.sjukpenningLeftHeader': 'Sjukpenningdagar kvar',
|
||||
'planResults.lagstaLeftHeader': 'Lägstanivådagar kvar',
|
||||
'planResults.dubbeldagarLeftHeader': 'Dubbeldagar kvar',
|
||||
'planResults.vacationDaysTitle': 'Semesterdagar',
|
||||
'planResults.usedHeader': 'Använda',
|
||||
'planResults.totalAvailableHeader': 'Totalt tillgängliga',
|
||||
'planResults.vacationAssumedBadge': 'Antaget',
|
||||
'planResults.vacationAssumedPrompt':
|
||||
'Inget värde är registrerat för detta år — 25 dagar (lagstadgat) har antagits. Bekräfta eller ändra i Inställningar.',
|
||||
|
||||
// §9.2 — surplus advice
|
||||
'planResults.surplusAdviceTitle': 'Ni har outnyttjade dagar',
|
||||
'planResults.surplusAdviceIntro':
|
||||
'Planen lämnar dagar outnyttjade som annars går förlorade vid barnets 4-årsdag. Några förslag:',
|
||||
'planResults.surplusAdviceWishMoreLeave': 'Önska mer ledighet',
|
||||
'planResults.surplusAdviceShiftMixToFp': 'Använd föräldradagar istället för semester',
|
||||
'planResults.surplusAdviceFrontLoadEarlyYears': 'Ta ut mer ledighet medan barnet är litet',
|
||||
'planResults.surplusAdviceUseLessVacation': 'Spara färre semesterdagar',
|
||||
|
||||
// §3.5 — message_key + data bodies for the five surfaces that used to
|
||||
// render backend-generated Swedish regardless of locale (see
|
||||
// backend/app/services/allocation_service.py + planning_service.py, and
|
||||
// the lookup tables in PlanResults.tsx that map each opaque message_key
|
||||
// to the keys below — same pattern as TipCard.tsx's TIP_TRANSLATION_KEYS).
|
||||
'planResults.explanationSummary':
|
||||
'{wish_name} {year} — {weeks_wished} veckor önskade, {weeks_funded} veckor finansierade',
|
||||
'planResults.explanationShortfallSuffix': ' ({shortfall_days} dagar saknas)',
|
||||
'planResults.explanationDetailVacationAndFp':
|
||||
'du behåller lönen {vacation_days} dagar och sparar {fp_days} FP-dagar',
|
||||
'planResults.explanationDetailVacationOnly': '{vacation_days} semesterdagar',
|
||||
'planResults.explanationDetailSjukpenning':
|
||||
'{sjukpenning_days} föräldrapenningdagar (sjukpenningnivå)',
|
||||
'planResults.explanationDetailLagsta': '{lagsta_days} föräldrapenningdagar (lägstanivå)',
|
||||
'planResults.explanationDetailFree': '{free_days} helt fria dagar (helg/röd dag)',
|
||||
|
||||
'planResults.remedyReducePreschoolReserve':
|
||||
'Minska förskolans planeringsdagar ({reserved} reserverade) frigör {reserved} dagar',
|
||||
'planResults.remedyCarryVacationForward':
|
||||
'{spare_total} semesterdagar från tidigare år kan sparas framåt',
|
||||
'planResults.remedyLowerPriorityWish':
|
||||
'Sänk {wish_name} från {from_weeks} till {to_weeks} veckor frigör {freed_days} dagar',
|
||||
'planResults.remedyShiftWindowIntoFreeDays':
|
||||
'Flytta {wish_name} närmare en helg frigör {shift_gain} dagar',
|
||||
|
||||
'planResults.warningBodyDaysLostAtAge4':
|
||||
'{parent_name} riskerar att förlora {expiring_days} dagar vid barnets 4-årsdag ({fourth_birthday}) om inte fler dagar tas ut innan dess.',
|
||||
'planResults.warningBodySgiRisk':
|
||||
'{parent_name} riskerar SGI-skyddet veckan som börjar {week_start} ({days_planned} dagar planerade, minst {min_days_per_week} krävs).',
|
||||
'planResults.warningBodyVacationExpiring':
|
||||
'{parent_name} har {saved_days_in} sparade semesterdagar från {year} som riskerar att gå förlorade.',
|
||||
|
||||
'planResults.surplusAdviceBodyWishMoreLeave':
|
||||
'{parent_name} har råd med ytterligare {weeks} veckor ledigt ({days_involved} FP-dagar går annars förlorade vid barnets 4-årsdag) — lägg till en vecka på sommaren eller ett höstlov.',
|
||||
'planResults.surplusAdviceBodyShiftMixToFp':
|
||||
'{parent_name} kan byta {days_involved} semesterdagar mot föräldrapenningdagar under planen — FP-dagarna går annars förlorade vid 4-årsdagen, medan semestern inte gör det.',
|
||||
'planResults.surplusAdviceBodyFrontLoadEarlyYears':
|
||||
'{parent_name} har ett stort överskott ({days_involved} FP-dagar) — ta ut fler dagar nu medan barnet är litet och ni ändå är hemma, istället för att sprida ut dem tunt fram till 4-årsdagen.',
|
||||
'planResults.surplusAdviceBodyUseLessVacation':
|
||||
'{parent_name} kan ta ut {days_involved} färre semesterdagar ({years_text}) utan att gå under lägsta uttaget ({min_vacation_days_per_year} dagar/år) — spara dem istället för att täcka FP-dagar som annars går förlorade.',
|
||||
|
||||
'planResults.rejectionInvalidDayType': 'Dag {index} ({date}): ogiltig dagtyp "{day_type}"',
|
||||
'planResults.rejectionConflictingDuplicate':
|
||||
'Motstridiga dubbletter för {date} (barn {child_id}, förälder {parent_personal_data_uuid}) i samma omgång',
|
||||
'planResults.rejectionCollisionsNeedReplace':
|
||||
'{count} dag(ar) är redan planerade på annat sätt; skicka replace=true för att skriva över',
|
||||
'planResults.rejectionDubbeldagWindowExceeded':
|
||||
'Dubbeldagar är endast tillåtna inom barnets första {months_limit} månader (senast {deadline})',
|
||||
'planResults.rejectionDubbeldagQuotaReached':
|
||||
'Kvoten för dubbeldagar ({max_dubbeldagar} dagar) är redan nådd',
|
||||
'planResults.rejectionDubbeldagInvalid': 'Dubbeldag för {date} kunde inte godkännas',
|
||||
'planResults.rejectionDubbeldagBatchQuotaExceeded':
|
||||
'Omgången skulle resultera i {projected_total} dubbeldagar, max är {max_dubbeldagar}',
|
||||
'planResults.rejectionOutsideEligibleWindow':
|
||||
'{date}: utanför barnets tillåtna period ({birth_date} – {hard_expiry})',
|
||||
'planResults.rejectionInsufficientBalance':
|
||||
'Omgången behöver {needed} fler "{tier}"-dagar men endast {remaining} finns kvar',
|
||||
|
||||
// §3.3 part 2 — plan-aware age-4 projection
|
||||
'planResults.age4ProjectionsTitle': 'Vid barnets 4-årsdag, om planen genomförs',
|
||||
'planResults.age4ProjectionsIntro':
|
||||
'Så här ser det ut vid 4-årsdagen om den här planen tas ut som föreslaget — livet kan förstås bli annorlunda.',
|
||||
'planResults.fourthBirthdayHeader': '4-årsdag',
|
||||
'planResults.projectedLeftHeader': 'Beräknat kvar',
|
||||
'planResults.savableAfterB4Header': 'Kan sparas',
|
||||
'planResults.expiringAtB4Header': 'Förfaller',
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Leave type labels (shared lookup used by Dashboard + Calendar)
|
||||
// ---------------------------------------------------------------------
|
||||
'leaveType.vacation': 'Semester',
|
||||
'leaveType.sjukpenning': 'Sjukpenningnivå',
|
||||
'leaveType.lagsta': 'Lägstanivå',
|
||||
'leaveType.unknown': 'Okänd',
|
||||
} as const
|
||||
|
||||
export type TranslationKey = keyof typeof sv
|
||||
Reference in New Issue
Block a user