W0 web foundation: Vite + React SPA under /app/, sync-pull ledger, auth, tokens, shared components
Build Kvitt images / build (push) Failing after 1h14m5s

web/ is the browser client's scaffold: Vite 8, React 19, TypeScript 5.9 strict, React Router 7 (basename /app, every
fixed route with a lazy placeholder page), TanStack Query 5 (refetch on focus and every 60 s), openapi-typescript
types generated from contracts/openapi.yaml with openapi-fetch. Reads go through GET /sync (no REST reads exist for
the ledger, SPEC §7): useLedger() pulls from cursor 0 and then incrementally, restarting on a new epoch or
cursor_ahead. Login pairs the browser as "Web · <browser>" with an invite code (E-3); every call sends the SPEC
§14.3.1 headers, a 401 signs out. kvitt-domain's split, balances, allocation and settlement rules are ported to
web/src/lib and pass every case of contracts/test-vectors. Design tokens from design-spec §2 in tokens.css
(light-dark()), Material Symbols Rounded (Apache-2.0) as generated inline SVG, sv/en catalogs generated from
docs/ux/strings.csv plus web-strings.csv. Gate: cd web && npm run check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-21 21:00:03 +02:00
parent 3fbd393082
commit 2ae65a5a9f
154 changed files with 15651 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
import { createContext, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
import { readStored, writeStored } from '../lib/storage';
import { translate, translatePlural, type FormatArg, type PluralKey, type StringKey } from './catalog';
import { languageOf, type Language } from './language';
export type LanguagePreference = 'auto' | Language;
export interface I18n {
language: Language;
preference: LanguagePreference;
setPreference: (preference: LanguagePreference) => void;
t: (key: StringKey, ...args: FormatArg[]) => string;
tp: (key: PluralKey, count: number, ...args: FormatArg[]) => string;
}
export const I18nContext = createContext<I18n | null>(null);
const STORAGE_KEY = 'kvitt.language';
function storedPreference(): LanguagePreference {
const value = readStored(STORAGE_KEY);
return value === 'sv' || value === 'en' ? value : 'auto';
}
function browserLanguage(): Language {
return languageOf(typeof navigator === 'undefined' ? undefined : navigator.language);
}
/** Language from `navigator.language`, overridden by the choice stored in localStorage (`kvitt.language`). */
export function I18nProvider({ children, initial }: { children: ReactNode; initial?: LanguagePreference }) {
const [preference, setPreferenceState] = useState<LanguagePreference>(() => initial ?? storedPreference());
const language: Language = preference === 'auto' ? browserLanguage() : preference;
useEffect(() => {
document.documentElement.lang = language;
}, [language]);
const setPreference = useCallback((next: LanguagePreference) => {
writeStored(STORAGE_KEY, next === 'auto' ? null : next);
setPreferenceState(next);
}, []);
const value = useMemo<I18n>(
() => ({
language,
preference,
setPreference,
t: (key, ...args) => translate(language, key, args),
tp: (key, count, ...args) => translatePlural(language, key, count, args.length > 0 ? args : undefined),
}),
[language, preference, setPreference],
);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
+59
View File
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';
import { featureCatalogs, fill, LOADED_FEATURE_CATALOGS, mergeCatalogs, translate, translatePlural } from './catalog';
import en from './en.json';
import { languageOf } from './language';
import sv from './sv.json';
describe('i18n catalogs', () => {
it('have the same keys in sv and en', () => {
expect(Object.keys(sv).sort()).toEqual(Object.keys(en).sort());
});
it('hold no Android placeholders after conversion', () => {
for (const value of [...Object.values(sv), ...Object.values(en)]) {
expect(value).not.toMatch(/%\d*\$?[sd]/);
}
});
it('fills positional placeholders', () => {
expect(fill('{1} och {2}', ['Jacob', 'Anna'])).toBe('Jacob och Anna');
expect(fill('{2} {1}', [1])).toBe('{2} 1');
expect(translate('sv', 'common_and', ['Jacob', 'Anna'])).toBe('Jacob och Anna');
expect(translate('en', 'balance_partner_owes_you', ['Anna'])).toBe('Anna owes you');
expect(translate('sv', 'common_percent', ['34,8'])).toBe('34,8 %');
});
it('chooses plural forms', () => {
expect(translatePlural('sv', 'sync_pending', 1)).toBe('1 ändring väntar på synk');
expect(translatePlural('sv', 'sync_pending', 2)).toBe('2 ändringar väntar på synk');
expect(translatePlural('en', 'ledger_settlement_accounts', 3)).toBe('3 accounts');
});
it('maps browser languages', () => {
expect(languageOf('sv-SE')).toBe('sv');
expect(languageOf('sv')).toBe('sv');
expect(languageOf('en-GB')).toBe('en');
expect(languageOf('de')).toBe('en');
expect(languageOf(undefined)).toBe('en');
});
});
describe('feature catalogs', () => {
it('merge web_ keys from features without replacing shared keys', () => {
const features = featureCatalogs({
'../features/settle/i18n/sv.json': { default: { web_settle_hint: 'Tips', common_save: 'Nej' } },
'../features/settle/i18n/en.json': { default: { web_settle_hint: 'Hint' } },
'../features/settle/i18n/notes.json': { default: { web_x: 'ignored' } },
});
expect(Object.keys(features)).toEqual(['settle']);
const merged = mergeCatalogs({ common_save: 'Spara' }, [features.settle?.sv]);
expect(merged).toEqual({ common_save: 'Spara', web_settle_hint: 'Tips' });
});
it('every loaded feature has the same web_ keys in sv and en, none shared', () => {
for (const [feature, catalogs] of Object.entries(LOADED_FEATURE_CATALOGS)) {
const svKeys = Object.keys(catalogs.sv ?? {}).sort();
expect(Object.keys(catalogs.en ?? {}).sort(), feature).toEqual(svKeys);
for (const key of svKeys) {
expect(key.startsWith('web_'), `${feature}: ${key}`).toBe(true);
expect(key in sv, `${feature}: ${key} is a shared key`).toBe(false);
}
}
});
});
+90
View File
@@ -0,0 +1,90 @@
/**
* String catalogs. The shared ones, `sv.json` and `en.json`, are generated from docs/ux/strings.csv and
* src/i18n/web-strings.csv by scripts/gen-i18n.mjs. A feature adds its own strings in
* `src/features/<name>/i18n/{sv,en}.json` (keys `web_<feature>_…`, never a shared key); they are merged here at build
* time, so a feature plan never edits the shared files. Placeholders are `{1}`, `{2}`, … (1-based, the numbers of
* Android's `%1$s`); plurals are `key:one` / `key:other`.
*/
import en from './en.json';
import type { Language } from './language';
import sv from './sv.json';
/** Keys of the shared catalogs (type-checked). */
export type SharedKey = keyof typeof sv;
/** Keys a feature catalog may add. */
export type FeatureKey = `web_${string}`;
export type StringKey = SharedKey | FeatureKey;
type PluralBaseOf<K> = K extends `${infer Base}:other` ? Base : never;
/** Shared keys that exist as `key:one` and `key:other`, or a feature's `web_` plural. */
export type PluralKey = PluralBaseOf<SharedKey> | FeatureKey;
export type FormatArg = string | number;
export type Catalog = Readonly<Record<string, string>>;
/** `{ '../features/<name>/i18n/<lang>.json': { default: Catalog } }` as `import.meta.glob` returns it. */
export type FeatureModules = Record<string, { default: Catalog }>;
/** Feature catalogs by feature name and language. */
export function featureCatalogs(modules: FeatureModules): Record<string, Partial<Record<Language, Catalog>>> {
const out: Record<string, Partial<Record<Language, Catalog>>> = {};
for (const [path, module] of Object.entries(modules)) {
const match = /features\/([^/]+)\/i18n\/(sv|en)\.json$/.exec(path);
if (match?.[1] === undefined || (match[2] !== 'sv' && match[2] !== 'en')) continue;
(out[match[1]] ??= {})[match[2]] = module.default;
}
return out;
}
/** The shared catalog plus every feature's `web_` keys; a shared key is never replaced. */
export function mergeCatalogs(base: Catalog, features: readonly (Catalog | undefined)[]): Catalog {
const merged: Record<string, string> = { ...base };
for (const catalog of features) {
if (catalog === undefined) continue;
for (const [key, value] of Object.entries(catalog)) {
if (key.startsWith('web_') && !(key in base)) merged[key] = value;
}
}
return merged;
}
const FEATURES = featureCatalogs(import.meta.glob<{ default: Catalog }>('../features/*/i18n/*.json', { eager: true }));
const CATALOGS: Record<Language, Catalog> = {
sv: mergeCatalogs(
sv,
Object.values(FEATURES).map((f) => f.sv),
),
en: mergeCatalogs(
en,
Object.values(FEATURES).map((f) => f.en),
),
};
/** Feature catalogs as loaded, for the parity test. */
export const LOADED_FEATURE_CATALOGS = FEATURES;
/** Replaces `{n}` with the n-th argument (1-based). A placeholder without an argument is left as written. */
export function fill(template: string, args: readonly FormatArg[]): string {
return template.replace(/\{(\d+)\}/g, (match, index: string) => {
const value = args[Number(index) - 1];
return value === undefined ? match : String(value);
});
}
/** The string, or the key itself when no catalog has it (visible in the page and caught by the feature tests). */
export function translate(language: Language, key: StringKey, args: readonly FormatArg[] = []): string {
return fill(CATALOGS[language][key] ?? key, args);
}
/**
* Plural form (Swedish and English both have one/other): `key:one` for exactly 1, else `key:other`.
* Without `args` the count fills `{1}`, like Android's `getQuantityString(id, count, count)`.
*/
export function translatePlural(
language: Language,
key: PluralKey,
count: number,
args?: readonly FormatArg[],
): string {
const form = count === 1 ? `${key}:one` : `${key}:other`;
return fill(CATALOGS[language][form] ?? form, args ?? [count]);
}
+603
View File
@@ -0,0 +1,603 @@
{
"account_archive": "Archive",
"account_archived_snackbar": "{1} was archived",
"account_default_split": "{1} · {2}",
"account_default_split_spoken": "Default split: {1}",
"account_delete": "Delete account",
"account_delete_blocked": "The account cannot be deleted while it has expenses, payments or recurring expenses",
"account_delete_confirm_body": "This cannot be undone.",
"account_delete_confirm_title": "Delete the account {1}?",
"account_edit": "Edit account",
"account_editor_default_split": "Default split",
"account_editor_edit": "Edit account",
"account_editor_name_taken": "There is already an account called {1}",
"account_editor_new": "New account",
"account_editor_offline_body": "Accounts are saved straight to the server. Connect the phone to the internet to create the account.",
"account_editor_offline_body_edit": "Accounts are saved straight to the server. Connect the phone to the internet to save the change.",
"account_editor_preview": "Of {1}: {2}",
"account_editor_split_hint": "Applies to new expenses in the account. Each expense can change its own split.",
"account_empty_body": "Add the first expense in {1}.",
"account_empty_title": "No expenses yet",
"account_settle_this": "Settle this account",
"account_unarchive": "Restore from archive",
"account_unarchived_snackbar": "{1} was restored",
"analytics_average": "Average",
"analytics_bar_spoken": "{1}: {2}",
"analytics_by_account": "By account",
"analytics_by_category": "By category",
"analytics_change_period": "Change period",
"analytics_drilldown_empty": "No expenses any more",
"analytics_drilldown_expenses:one": "{1} expense",
"analytics_drilldown_expenses:other": "{1} expenses",
"analytics_drilldown_title": "{1}, {2}",
"analytics_empty": "No expenses in this period",
"analytics_empty_body": "Choose another period, or add an expense dated within it.",
"analytics_granularity": "Interval",
"analytics_granularity_month": "Month",
"analytics_granularity_week": "Week",
"analytics_group_by": "Group by",
"analytics_group_by_account": "Account",
"analytics_group_by_category": "Category",
"analytics_measure": "Measure",
"analytics_measure_household": "Household",
"analytics_measure_my_share": "My share",
"analytics_moving_average": "Moving average ({1})",
"analytics_over_time": "Spending over time",
"analytics_period": "Period",
"analytics_period_spoken": "Period: {1}",
"analytics_range_all_time": "All time",
"analytics_range_apply": "Apply",
"analytics_range_custom": "Custom…",
"analytics_range_custom_dates": "Custom · {1}",
"analytics_range_last_12_months": "Last 12 months",
"analytics_range_last_3_months": "Last 3 months",
"analytics_range_last_6_months": "Last 6 months",
"analytics_range_this_month": "This month",
"analytics_range_this_year": "This year",
"analytics_summary_breakdown": "{1}: largest is {2} with {3} ({4}).",
"analytics_summary_line": "{1} over time, {2}: from {3} in {4} to {5} in {6}, {7} in total, average {8}.",
"analytics_summary_line_single": "{1} over time, {2}: {3} in total.",
"analytics_summary_over_time": "Spending over time, {1}: highest in {2} with {3}, lowest in {4} with {5}.",
"analytics_title": "Analytics",
"analytics_total": "Total",
"analytics_vs_average_above": "{1}: {2} above average",
"analytics_vs_average_below": "{1}: {2} below average",
"analytics_vs_average_on": "{1}: on average",
"analytics_week_label": "week {1}",
"analytics_weeks:one": "{1} week",
"analytics_weeks:other": "{1} weeks",
"app_name": "Kvitt",
"balance_account_settled": "Settled up",
"balance_all_settled": "All settled up",
"balance_all_settled_body": "Nobody owes anything right now.",
"balance_partner_owes_you": "{1} owes you",
"balance_partner_owes_you_amount": "{1} owes you {2}",
"balance_you_owe_partner": "You owe {1}",
"balance_you_owe_partner_amount": "You owe {1} {2}",
"category_default_children": "Children",
"category_default_entertainment": "Entertainment",
"category_default_fees": "Fees & taxes",
"category_default_groceries": "Groceries",
"category_default_health": "Health",
"category_default_home": "Home",
"category_default_housing": "Housing",
"category_default_insurance": "Insurance",
"category_default_other": "Other",
"category_default_restaurants": "Dining",
"category_default_shopping": "Shopping",
"category_default_transport": "Transport",
"category_default_travel": "Travel",
"category_default_utilities": "Utilities",
"color_amber": "Sand",
"color_blue": "Blue",
"color_brown": "Brown",
"color_green": "Green",
"color_grey": "Grey",
"color_indigo": "Indigo",
"color_lime": "Lime",
"color_orange": "Orange",
"color_pink": "Pink",
"color_purple": "Purple",
"color_red": "Red",
"color_slate": "Plum",
"color_teal": "Teal",
"color_yellow": "Yellow",
"common_amount_spoken": "{1} Swedish kronor",
"common_and": "{1} and {2}",
"common_archived": "Archived",
"common_back": "Back",
"common_cancel": "Cancel",
"common_close": "Close",
"common_continue": "Continue",
"common_could_not_reach_server": "Could not reach the server",
"common_date_spoken": "Date: {1}",
"common_delete": "Delete",
"common_discard": "Discard",
"common_discard_title": "Discard changes?",
"common_edit": "Edit",
"common_invite_code": "Invite code",
"common_invite_partner": "Invite partner",
"common_keep_editing": "Keep editing",
"common_kr_suffix": "SEK",
"common_more_options": "More options",
"common_name": "Name",
"common_name_required": "Enter a name",
"common_needs_connection": "Needs connection",
"common_offline": "Offline",
"common_ok": "OK",
"common_partner": "Partner",
"common_pending_sync": "Waiting to sync",
"common_percent": "{1} %",
"common_ratio": "{1}/{2}",
"common_retry": "Try again",
"common_save": "Save",
"common_separator_comma": ", ",
"common_separator_dot": " · ",
"common_tagline": "Shared expenses for two",
"common_today": "Today",
"common_too_much": "{1} too much",
"common_undo": "Undo",
"common_user_split": "{1} {2}",
"common_wordmark": "Kvitt",
"common_yesterday": "Yesterday",
"common_you": "You",
"common_you_lower": "you",
"editor_account": "Account",
"editor_account_spoken": "Account: {1}",
"editor_add_note": "Add note",
"editor_amount": "Amount",
"editor_category": "Category",
"editor_category_all": "All",
"editor_category_all_spoken": "All categories",
"editor_description": "Description",
"editor_keypad": "Number keys",
"editor_keypad_clear": "Clear",
"editor_keypad_decimal": "Decimal separator",
"editor_keypad_delete": "Delete",
"editor_notes": "Notes",
"editor_paid_by": "Paid by",
"editor_paid_by_toggle": "Paid by {1}. Double-tap to switch to {2}.",
"editor_percent_of": "{1} %",
"editor_percent_suffix": "%",
"editor_pick_account": "Choose account",
"editor_pick_category": "Choose category",
"editor_ratio_slider": "{1}'s share",
"editor_ratio_spoken": "{1} {2} percent, {3} {4} percent",
"editor_split": "Split",
"editor_split_collapse": "Collapse split",
"editor_split_exact_computed": "calculated",
"editor_split_exact_of": "{1} {2} of {3}",
"editor_split_full_on": "All on {1}",
"editor_split_full_on_you": "All on you",
"editor_split_full_question": "Who pays the whole amount?",
"editor_split_mode_default": "Account default {1}",
"editor_split_mode_exact": "Exact amounts",
"editor_split_mode_full": "All on one person",
"editor_split_mode_ratio": "Custom ratio",
"effect_borrowed": "You borrowed {1}",
"effect_lent": "You lent {1}",
"effect_not_involved": "Not involved",
"effect_paid_for_yourself": "Paid for yourself",
"error_amount_out_of_range": "The amount must be between {1} and {2}",
"error_amount_required": "Enter an amount",
"error_app_too_old": "Update Kvitt to continue",
"error_date_out_of_range": "The date is too far ahead or back",
"error_description_required": "Enter a description",
"error_exact_share_too_large": "The share is larger than the amount",
"error_forbidden": "You are not allowed to do this",
"error_generic_invalid": "This could not be saved. Check the fields.",
"error_household_full": "Your partner has already joined",
"error_household_incomplete": "Your partner needs to join first",
"error_not_found": "This no longer exists",
"error_rate_limited": "Too many attempts. Wait a while and try again.",
"error_ratio_invalid": "The parts must add up to 100 %",
"error_server": "Something went wrong on the server. Try again later.",
"error_too_long:one": "At most {1} character",
"error_too_long:other": "At most {1} characters",
"error_validation": "The server did not accept the values",
"expense_detail_date": "Date",
"expense_detail_delete_body": "It disappears for both of you.",
"expense_detail_delete_title": "Delete the expense?",
"expense_detail_deleted_banner": "{1} deleted this expense · {2}",
"expense_detail_deleted_body": "It no longer counts in balances or analytics.",
"expense_detail_missing": "This expense is not on the phone yet",
"expense_detail_missing_body": "It may have just been added on another phone. Sync to fetch it.",
"expense_detail_notes": "Notes",
"expense_detail_paid_by": "Paid by",
"expense_detail_recurring": "Recurring: {1}",
"expense_detail_split": "Split",
"expense_detail_sync_now": "Sync now",
"expense_editor_date": "Date",
"expense_editor_delete": "Delete expense",
"expense_editor_discard_title": "Discard the expense?",
"expense_editor_edit": "Edit expense",
"expense_editor_new": "New expense",
"fab_add_expense": "Add expense",
"home_account_expenses_month:one": "{1} expense in {2}",
"home_account_expenses_month:other": "{1} expenses in {2}",
"home_account_no_expenses_month": "No expenses in {1}",
"home_accounts": "Accounts",
"home_archived_collapsed": "collapsed",
"home_archived_expanded": "expanded",
"home_archived_section": "Archived ({1})",
"home_empty_body": "An account collects expenses that you share the same way, for example Everyday, The house or a trip. You can have as many as you like.",
"home_empty_subtitle": "The household is ready. Now you need an account.",
"home_empty_title": "Create your first account",
"home_empty_welcome": "You and {1} are in",
"home_loading": "Getting your expenses…",
"home_loading_body": "The first sync fetches everything on the server. It only takes a moment.",
"home_new_account": "New account",
"home_settle_up": "Settle up",
"home_sync_lost_body": "This is how it looked on your phone. It was not saved and cannot be changed here.",
"home_sync_lost_deleted": "You deleted this",
"home_sync_lost_show_current": "Show current version",
"home_sync_lost_title": "Your version",
"home_sync_reject_clock_skew": "Your phone's clock is wrong. Fix it and sync again",
"home_sync_reject_deleted": "This was deleted before your change arrived",
"home_sync_reject_reference": "Its account or category no longer exists",
"home_sync_reject_stale": "Your change was replaced by a newer change from {1}",
"home_sync_reject_stale_help": "{1}'s version applies. Open the expense if you want to make your change again.",
"home_sync_reject_stale_help_payment": "{1}'s version applies. Open the payment if you want to make your change again.",
"home_sync_reject_validation": "This change was not valid and was not saved",
"home_sync_status_all_synced": "Everything is synced",
"home_sync_status_clock_body": "The clock is more than 10 minutes ahead of the server. Your changes stay on the phone until it is fixed.",
"home_sync_status_clock_warning": "Your phone's clock is wrong. Fix it and sync again.",
"home_sync_status_dismiss": "Remove from list",
"home_sync_status_last_attempt_clock": "Last attempt refused: the clock",
"home_sync_status_last_success": "Last successful sync {1}",
"home_sync_status_open": "Open",
"home_sync_status_open_date_settings": "Date & time",
"home_sync_status_pending_title": "Pending changes ({1})",
"home_sync_status_rejected_title": "Changes that were not synced",
"home_sync_status_sync_now": "Sync now",
"home_sync_status_title": "Sync status",
"icon_account_balance": "Bank",
"icon_bolt": "Electricity",
"icon_build": "Repairs",
"icon_card_giftcard": "Gift",
"icon_category": "Other",
"icon_celebration": "Party",
"icon_chair": "Furniture",
"icon_checkroom": "Clothes",
"icon_child_care": "Children",
"icon_directions_car": "Car",
"icon_fitness_center": "Fitness",
"icon_flight": "Flight",
"icon_home": "Home",
"icon_hotel": "Hotel",
"icon_key": "Key",
"icon_local_cafe": "Café",
"icon_local_gas_station": "Fuel",
"icon_luggage": "Luggage",
"icon_medical_services": "Health care",
"icon_medication": "Medicine",
"icon_movie": "Film",
"icon_music_note": "Music",
"icon_pets": "Pets",
"icon_restaurant": "Restaurant",
"icon_school": "School",
"icon_shield": "Shield",
"icon_shopping_bag": "Shopping bag",
"icon_shopping_cart": "Shopping cart",
"icon_sports_esports": "Games",
"icon_subscriptions": "Subscriptions",
"icon_train": "Train",
"icon_water_drop": "Water",
"icon_wifi": "Internet",
"ledger_month_total": "{1} · {2}",
"ledger_settlement_accounts:one": "{1} account",
"ledger_settlement_accounts:other": "{1} accounts",
"ledger_settlement_label": "Payment",
"ledger_settlement_partner_paid_you": "{1} paid you {2}",
"ledger_settlement_you_paid_partner": "You paid {1} {2}",
"nav_analytics": "Analytics",
"nav_home": "Home",
"nav_label": "Navigation",
"nav_settings": "Settings",
"notif_channel_activity": "Activity",
"notif_expense_created_body": "{1} · {2} · {3}",
"notif_expense_created_title": "{1} added an expense",
"notif_expense_deleted_body": "{1} · {2}",
"notif_expense_deleted_title": "{1} deleted an expense",
"notif_expense_updated_body": "{1} · {2}",
"notif_expense_updated_title": "{1} changed an expense",
"notif_generic_body": "Open Kvitt to see it",
"notif_generic_title": "Kvitt",
"notif_generic_title_actor": "{1} changed something in Kvitt",
"notif_recurring_created_body": "{1} · {2} · {3}",
"notif_recurring_created_title": "Recurring expense added",
"notif_settlement_body": "{1} paid {2} {3}",
"notif_settlement_body_from_you": "You paid {1} {2}",
"notif_settlement_body_to_you": "{1} paid you {2}",
"notif_settlement_created_title": "{1} recorded a payment",
"notif_settlement_deleted_body": "{1}",
"notif_settlement_deleted_title": "{1} deleted a payment",
"notif_settlement_updated_title": "{1} changed a payment",
"notif_sync_batch_body": "Open Kvitt to see them",
"notif_sync_batch_title:one": "{1} made {2} change",
"notif_sync_batch_title:other": "{1} made {2} changes",
"onboarding_again_body": "Your expenses are still on this phone.",
"onboarding_again_title": "Pair this phone again",
"onboarding_code_help": "You get the code from {1}: from a link or from Settings › Invite partner.",
"onboarding_code_help_generic": "You get the code from a link, or it is printed when the server is set up.",
"onboarding_code_paste": "Paste",
"onboarding_code_placeholder": "XXXX-XXXX-XXXX",
"onboarding_error_code_length": "The code has 12 characters",
"onboarding_error_https": "The address must start with https://",
"onboarding_error_invite_invalid": "This code does not work. It may have expired or already been used. Ask for a new one.",
"onboarding_error_name_required": "Enter your name",
"onboarding_error_network": "Could not reach the server. Check the address and your connection.",
"onboarding_error_rate_limited": "Too many attempts. Wait a while and try again.",
"onboarding_name_label": "Your name",
"onboarding_pair": "Pair",
"onboarding_pairing": "Pairing…",
"onboarding_profile_hint": "This is how {1} sees you in Kvitt. You can change it later under Settings › Profile.",
"onboarding_profile_joining": "You are joining {1}'s household",
"onboarding_profile_title": "Your profile",
"onboarding_removed_body": "Access was removed and all Kvitt data on the phone has been deleted.",
"onboarding_removed_new_code": "To use Kvitt again you need a new invite code.",
"onboarding_removed_pair_again": "Pair again",
"onboarding_removed_title": "This phone has been removed",
"onboarding_server_label": "Server address",
"onboarding_waiting_body": "Kvitt needs both of you before you can start. Invite your partner with a link.",
"onboarding_waiting_footer": "Accounts, expenses and payments become available when your partner has joined.",
"onboarding_waiting_invite_expires": "Invite valid until {1}",
"onboarding_waiting_invite_sent": "Invite sent",
"onboarding_waiting_partner_missing": "Partner has not joined",
"onboarding_waiting_title": "Waiting for your partner",
"profile_add_picture": "Add picture",
"profile_change_picture": "Change picture",
"profile_picture_failed": "The picture could not be used",
"profile_picture_invalid": "The picture could not be read",
"profile_picture_too_large": "The picture is too large (max 5 MB)",
"profile_picture_unsupported": "Use a JPEG, PNG or WebP picture",
"profile_picture_uploading": "Uploading picture…",
"profile_remove_picture": "Remove picture",
"settings_about": "About Kvitt",
"settings_about_check_update": "Check for update",
"settings_about_server": "Server",
"settings_about_server_version": "Server version",
"settings_about_server_version_schema": "{1} (schema {2})",
"settings_about_server_version_unknown": "Unknown",
"settings_about_summary": "Version {1}",
"settings_about_title": "About Kvitt",
"settings_about_up_to_date": "You have the latest version",
"settings_about_version": "Kvitt {1} ({2})",
"settings_categories": "Categories",
"settings_categories_archived": "Archived ({1})",
"settings_categories_drag_handle": "Move {1}",
"settings_categories_move_down": "Move down",
"settings_categories_move_up": "Move up",
"settings_categories_new": "New category",
"settings_categories_reorder_offline": "Needs connection to change",
"settings_categories_summary:one": "{1} active",
"settings_categories_summary:other": "{1} active",
"settings_categories_title": "Categories",
"settings_category_archived_badge": "Archived",
"settings_category_archived_snackbar": "{1} was archived",
"settings_category_delete_blocked_default": "Default categories cannot be deleted, only archived",
"settings_category_delete_blocked_default_short": "Cannot be deleted: default category",
"settings_category_delete_blocked_in_use": "The category is used by expenses or recurring expenses. Archive it instead.",
"settings_category_delete_blocked_in_use_archived": "The category is used by expenses or recurring expenses and cannot be deleted.",
"settings_category_delete_body": "It disappears from the list for both of you. This cannot be undone.",
"settings_category_delete_title": "Delete the category {1}?",
"settings_category_editor_archive": "Archive",
"settings_category_editor_color": "Colour",
"settings_category_editor_default_name": "Default name: {1}",
"settings_category_editor_delete": "Delete category",
"settings_category_editor_edit": "Edit category",
"settings_category_editor_icon": "Icon",
"settings_category_editor_new": "New category",
"settings_category_editor_reset_name": "Reset name",
"settings_category_editor_unarchive": "Restore from archive",
"settings_category_unarchived_snackbar": "{1} was restored",
"settings_collapsed": "collapsed",
"settings_devices": "Devices",
"settings_devices_app_version": "Kvitt {1}",
"settings_devices_cannot_revoke_current": "You cannot remove access for the phone you are using right now.",
"settings_devices_last_seen": "Last seen {1}",
"settings_devices_loading": "Loading devices…",
"settings_devices_offline": "Needs connection to show devices",
"settings_devices_paired": "Paired {1}",
"settings_devices_revoke": "Remove access",
"settings_devices_revoke_body_own": "The phone is signed out and all Kvitt data on it is deleted. You can pair it again with a new invite code.",
"settings_devices_revoke_body_partner": "{1}'s phone is signed out and all Kvitt data on it is deleted. {1} can pair it again with a new invite code.",
"settings_devices_revoke_title": "Remove access for {1}?",
"settings_devices_this_device": "This device",
"settings_devices_title": "Devices",
"settings_expanded": "expanded",
"settings_invites_cancel": "Cancel invite",
"settings_invites_cancel_title": "Cancel the invite?",
"settings_invites_code_copied": "Code copied",
"settings_invites_code_hidden": "The code is only shown when it is created",
"settings_invites_copy_code": "Copy code",
"settings_invites_create_body": "Create a one-time code to pair another phone. The code is valid for 72 hours.",
"settings_invites_created": "Created {1}",
"settings_invites_for_me": "For me",
"settings_invites_for_partner": "For {1}",
"settings_invites_instructions": "Open the link on the new phone, or enter the code in Kvitt. The code works once.",
"settings_invites_kind_device": "Device · {1}",
"settings_invites_kind_partner": "Partner",
"settings_invites_new_device_for": "New device for {1}",
"settings_invites_new_partner": "Invite for your partner",
"settings_invites_none": "No pending invites",
"settings_invites_offline_banner": "Needs connection. Invites can only be created and shown online.",
"settings_invites_pending": "Pending invites",
"settings_invites_share": "Share",
"settings_invites_share_message": "Join our household in Kvitt: {1} The code is {2}.",
"settings_invites_title_device": "Pair a device",
"settings_invites_valid_until": "Valid until {1}",
"settings_pair_device": "Pair a device",
"settings_partner_missing": "Partner has not joined",
"settings_profile": "Profile",
"settings_profile_hint": "This is how {1} sees you in Kvitt.",
"settings_profile_offline_banner": "Needs connection. Your profile can only be changed online.",
"settings_profile_summary": "Name and picture",
"settings_recurring": "Recurring expenses",
"settings_recurring_created_at": "Created at 06:00 on day {1} every month",
"settings_recurring_day_clamp_hint": "In shorter months the last day is used",
"settings_recurring_day_of_month": "Day of month",
"settings_recurring_day_ordinal_a": "{1}",
"settings_recurring_day_ordinal_e": "{1}",
"settings_recurring_delete_body": "Expenses already created stay.",
"settings_recurring_delete_title": "Delete {1}?",
"settings_recurring_edit_hint": "The change applies to upcoming expenses.",
"settings_recurring_editor_edit": "Recurring expense",
"settings_recurring_editor_new": "New recurring expense",
"settings_recurring_empty_action": "New recurring expense",
"settings_recurring_empty_body": "Rent, electricity and streaming can be added automatically every month.",
"settings_recurring_empty_title": "No recurring expenses",
"settings_recurring_error_end_before_start": "The last month cannot be before the first",
"settings_recurring_error_start_too_early": "At most 12 months back",
"settings_recurring_every_month_on": "Every month on day {1}",
"settings_recurring_first_month": "First month",
"settings_recurring_last_month": "Last month (optional)",
"settings_recurring_new": "New recurring",
"settings_recurring_next": "Next {1}",
"settings_recurring_no_end": "No last month",
"settings_recurring_offline_banner": "Needs connection. Recurring expenses can only be changed online.",
"settings_recurring_paused": "Paused",
"settings_recurring_paused_switch": "Paused",
"settings_recurring_resume_hint": "Months that passed while paused are not created afterwards.",
"settings_recurring_row_partner_pays": "{1} · {2} pays",
"settings_recurring_row_you_pay": "{1} · You pay",
"settings_recurring_schedule": "Schedule",
"settings_recurring_summary_active:one": "{1} active",
"settings_recurring_summary_active:other": "{1} active",
"settings_recurring_summary_none": "None",
"settings_recurring_summary_paused:one": "{1} paused",
"settings_recurring_summary_paused:other": "{1} paused",
"settings_recurring_title": "Recurring expenses",
"settings_role_admin": "You · Admin",
"settings_role_admin_other": "Admin",
"settings_role_member": "You",
"settings_sync_status": "Sync status",
"settings_title": "Settings",
"settings_update_badge": "Update available",
"settle_add_account": "Add account",
"settle_allocation": "Allocation",
"settle_amount": "Amount",
"settle_confirm": "Confirm",
"settle_confirm_body_partner_pays": "{1} pays you {2} {3}.",
"settle_confirm_body_you_pay": "You pay {1} {2} {3}.",
"settle_confirm_date": "dated {1}",
"settle_confirm_date_today": "today, {1}",
"settle_confirm_dialog_title": "Confirm the payment",
"settle_date": "Date",
"settle_detail_delete": "Delete payment",
"settle_detail_delete_body": "Balances are recalculated for both of you.",
"settle_detail_delete_title": "Delete the payment?",
"settle_detail_deleted_banner": "{1} deleted this payment · {2}",
"settle_detail_deleted_body": "It no longer counts in the balances.",
"settle_detail_missing": "This payment is not on the phone yet",
"settle_detail_missing_body": "It may have just been added on another phone. Sync to fetch it.",
"settle_detail_partner_paid_you": "{1} paid you",
"settle_detail_sync_now": "Sync now",
"settle_detail_title": "Payment",
"settle_detail_you_paid_partner": "You paid {1}",
"settle_direction": "Direction",
"settle_empty_hint": "Enter an amount and Kvitt suggests an allocation.",
"settle_error_allocation_positive": "Each account needs at least 0,01",
"settle_error_allocation_sum": "The accounts must add up to the amount",
"settle_error_no_allocations": "Add at least one account",
"settle_error_too_many_allocations": "At most 50 accounts",
"settle_note": "Note",
"settle_note_hint": "For example Swish",
"settle_over_settle_warning": "This settles more than is owed in {1}",
"settle_partner_pays": "{1} pays you",
"settle_propose_again": "Suggest again",
"settle_remaining": "Left to allocate",
"settle_row_amount": "Amount for {1}",
"settle_row_debt": "Debt {1}",
"settle_row_remove": "Remove {1} from the allocation",
"settle_swap": "Swap direction",
"settle_title": "Settle up",
"settle_title_account": "Settle {1}",
"settle_title_edit": "Edit payment",
"settle_untouched_partner_owes": "{1} is left as it is, because {2} owes you there.",
"settle_untouched_you_owe": "{1} is left as it is, because you owe {2} there.",
"settle_you_pay": "You pay {1}",
"snackbar_expense_deleted": "Expense deleted",
"snackbar_expense_saved": "Expense saved",
"snackbar_expense_saved_offline": "Saved. Syncs when you're online.",
"snackbar_payment_deleted": "Payment deleted",
"snackbar_payment_saved": "Payment saved",
"sync_clock_wrong_short": "Your phone's clock is wrong",
"sync_failed_line": "Sync failed · trying again soon",
"sync_line_spoken": "Sync status: {1}",
"sync_pending:one": "{1} change waiting to sync",
"sync_pending:other": "{1} changes waiting to sync",
"sync_rejected:one": "{1} change was not synced",
"sync_rejected:other": "{1} changes were not synced",
"sync_synced_relative": "Synced {1}",
"sync_syncing": "Syncing…",
"time_hours_ago": "{1} h ago",
"time_just_now": "just now",
"time_minutes_ago": "{1} min ago",
"time_today_at": "today {1}",
"timeline_changed": "{1} changed {2} from {3} to {4}",
"timeline_changed_added": "{1} added {2}: {3}",
"timeline_changed_removed": "{1} removed {2}",
"timeline_created": "{1} created this",
"timeline_deleted": "{1} deleted this",
"timeline_field_account": "the account",
"timeline_field_allocations": "the allocation",
"timeline_field_amount": "the amount",
"timeline_field_category": "the category",
"timeline_field_date": "the date",
"timeline_field_description": "the description",
"timeline_field_from": "who paid",
"timeline_field_note": "the note",
"timeline_field_notes": "the notes",
"timeline_field_payer": "the payer",
"timeline_field_split_mode": "the split",
"timeline_field_splits": "the shares",
"timeline_field_to": "who received",
"timeline_import": "Imported from Splitwise",
"timeline_pending": "Your change is waiting to sync",
"timeline_recurring": "Created automatically (recurring)",
"timeline_time": "· {1}",
"timeline_title": "History",
"update_available": "Version {1} available",
"update_details": "New features and fixes · {1}",
"update_downloading": "Downloading… {1} %",
"update_downloading_spoken": "Downloading version {1}",
"update_failed": "Download failed, try again",
"update_install": "Update",
"update_later": "Later",
"update_permission_body": "Kvitt updates straight from your own server, not from the Play Store, so Android asks once whether Kvitt may Install unknown apps. Turn on Allow from this source and come back to Kvitt. The update continues by itself.",
"update_permission_title": "Allow installation",
"web_account_detail_title": "Account",
"web_expense_detail_title": "Expense",
"web_insights_title": "Insights",
"web_language": "Language",
"web_language_auto": "Browser language",
"web_language_en": "English",
"web_language_sv": "Svenska",
"web_login_body": "Enter an invite code from Settings › Pair a device on your phone.",
"web_login_name_needed": "This code adds a new person to the household. Enter your name.",
"web_login_server": "Server",
"web_login_title": "Sign in to Kvitt",
"web_logout": "Sign out",
"web_logout_body": "This browser forgets its access. Remove it under Devices on your phone to revoke it for good.",
"web_main_navigation": "Main navigation",
"web_nav_search": "Search",
"web_not_found_action": "Go to Home",
"web_not_found_title": "Page not found",
"web_search_title": "Search",
"web_shortcut_close": "Close dialog or go back",
"web_shortcut_help": "Show keyboard shortcuts",
"web_shortcut_new_expense": "New expense",
"web_shortcut_search": "Search",
"web_shortcuts_title": "Keyboard shortcuts",
"web_signed_out": "You were signed out",
"web_skip_to_content": "Skip to content",
"web_theme": "Theme",
"web_theme_dark": "Dark",
"web_theme_light": "Light",
"web_theme_system": "System",
"web_update_failed": "Could not update · trying again soon",
"web_update_now": "Update now",
"web_updated_relative": "Updated {1}",
"web_updating": "Updating…"
}
+10
View File
@@ -0,0 +1,10 @@
/** The two formatting languages of SPEC §9.8 and FR-19: Swedish, and every other locale (English forms). */
export type Language = 'sv' | 'en';
export const LANGUAGES: readonly Language[] = ['sv', 'en'];
/** Maps a BCP 47 tag (`sv-SE`, `en`, `sv_FI`) to a Language, as `KvittLanguage.of` does in the app. */
export function languageOf(tag: string | undefined): Language {
const primary = (tag ?? '').split(/[-_]/)[0]?.toLowerCase();
return primary === 'sv' ? 'sv' : 'en';
}
+603
View File
@@ -0,0 +1,603 @@
{
"account_archive": "Arkivera",
"account_archived_snackbar": "{1} arkiverades",
"account_default_split": "{1} · {2}",
"account_default_split_spoken": "Standarddelning: {1}",
"account_delete": "Ta bort konto",
"account_delete_blocked": "Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter",
"account_delete_confirm_body": "Det går inte att ångra.",
"account_delete_confirm_title": "Ta bort kontot {1}?",
"account_edit": "Ändra konto",
"account_editor_default_split": "Standarddelning",
"account_editor_edit": "Ändra konto",
"account_editor_name_taken": "Det finns redan ett konto som heter {1}",
"account_editor_new": "Nytt konto",
"account_editor_offline_body": "Konton sparas direkt på servern. Anslut telefonen till internet för att skapa kontot.",
"account_editor_offline_body_edit": "Konton sparas direkt på servern. Anslut telefonen till internet för att spara ändringen.",
"account_editor_preview": "Av {1}: {2}",
"account_editor_split_hint": "Gäller nya utgifter i kontot. Varje utgift kan ändra sin egen delning.",
"account_empty_body": "Lägg till den första utgiften i {1}.",
"account_empty_title": "Inga utgifter än",
"account_settle_this": "Gör upp det här kontot",
"account_unarchive": "Återställ från arkiv",
"account_unarchived_snackbar": "{1} återställdes",
"analytics_average": "Snitt",
"analytics_bar_spoken": "{1}: {2}",
"analytics_by_account": "Per konto",
"analytics_by_category": "Per kategori",
"analytics_change_period": "Byt period",
"analytics_drilldown_empty": "Inga utgifter längre",
"analytics_drilldown_expenses:one": "{1} utgift",
"analytics_drilldown_expenses:other": "{1} utgifter",
"analytics_drilldown_title": "{1}, {2}",
"analytics_empty": "Inga utgifter i den här perioden",
"analytics_empty_body": "Välj en annan period, eller lägg till en utgift med ett datum i perioden.",
"analytics_granularity": "Indelning",
"analytics_granularity_month": "Månad",
"analytics_granularity_week": "Vecka",
"analytics_group_by": "Gruppera efter",
"analytics_group_by_account": "Konto",
"analytics_group_by_category": "Kategori",
"analytics_measure": "Mått",
"analytics_measure_household": "Hushållet",
"analytics_measure_my_share": "Min del",
"analytics_moving_average": "Glidande snitt ({1})",
"analytics_over_time": "Utgifter över tid",
"analytics_period": "Period",
"analytics_period_spoken": "Period: {1}",
"analytics_range_all_time": "All tid",
"analytics_range_apply": "Använd",
"analytics_range_custom": "Anpassad…",
"analytics_range_custom_dates": "Anpassad · {1}",
"analytics_range_last_12_months": "Senaste 12 mån",
"analytics_range_last_3_months": "Senaste 3 mån",
"analytics_range_last_6_months": "Senaste 6 mån",
"analytics_range_this_month": "Denna månad",
"analytics_range_this_year": "I år",
"analytics_summary_breakdown": "{1}: störst är {2} med {3} ({4}).",
"analytics_summary_line": "{1} över tid, {2}: från {3} i {4} till {5} i {6}, totalt {7}, snitt {8}.",
"analytics_summary_line_single": "{1} över tid, {2}: totalt {3}.",
"analytics_summary_over_time": "Utgifter över tid, {1}: högst i {2} med {3}, lägst i {4} med {5}.",
"analytics_title": "Statistik",
"analytics_total": "Totalt",
"analytics_vs_average_above": "{1}: {2} över snittet",
"analytics_vs_average_below": "{1}: {2} under snittet",
"analytics_vs_average_on": "{1}: på snittet",
"analytics_week_label": "vecka {1}",
"analytics_weeks:one": "{1} vecka",
"analytics_weeks:other": "{1} veckor",
"app_name": "Kvitt",
"balance_account_settled": "Kvitt",
"balance_all_settled": "Ni är kvitt",
"balance_all_settled_body": "Ingen är skyldig något just nu.",
"balance_partner_owes_you": "{1} är skyldig dig",
"balance_partner_owes_you_amount": "{1} är skyldig dig {2}",
"balance_you_owe_partner": "Du är skyldig {1}",
"balance_you_owe_partner_amount": "Du är skyldig {1} {2}",
"category_default_children": "Barn",
"category_default_entertainment": "Nöje",
"category_default_fees": "Avgifter & skatter",
"category_default_groceries": "Mat & hushåll",
"category_default_health": "Hälsa",
"category_default_home": "Hem",
"category_default_housing": "Boende",
"category_default_insurance": "Försäkringar",
"category_default_other": "Övrigt",
"category_default_restaurants": "Restaurang & café",
"category_default_shopping": "Shopping",
"category_default_transport": "Transport",
"category_default_travel": "Resor",
"category_default_utilities": "Räkningar",
"color_amber": "Sand",
"color_blue": "Blå",
"color_brown": "Brun",
"color_green": "Grön",
"color_grey": "Grå",
"color_indigo": "Indigo",
"color_lime": "Lime",
"color_orange": "Orange",
"color_pink": "Rosa",
"color_purple": "Lila",
"color_red": "Röd",
"color_slate": "Plommon",
"color_teal": "Blågrön",
"color_yellow": "Gul",
"common_amount_spoken": "{1} kronor",
"common_and": "{1} och {2}",
"common_archived": "Arkiverat",
"common_back": "Tillbaka",
"common_cancel": "Avbryt",
"common_close": "Stäng",
"common_continue": "Fortsätt",
"common_could_not_reach_server": "Kunde inte nå servern",
"common_date_spoken": "Datum: {1}",
"common_delete": "Ta bort",
"common_discard": "Släng",
"common_discard_title": "Slänga ändringarna?",
"common_edit": "Ändra",
"common_invite_code": "Inbjudningskod",
"common_invite_partner": "Bjud in partner",
"common_keep_editing": "Fortsätt redigera",
"common_kr_suffix": "kr",
"common_more_options": "Fler alternativ",
"common_name": "Namn",
"common_name_required": "Skriv ett namn",
"common_needs_connection": "Kräver anslutning",
"common_offline": "Offline",
"common_ok": "OK",
"common_partner": "Partner",
"common_pending_sync": "Väntar på synk",
"common_percent": "{1} %",
"common_ratio": "{1}/{2}",
"common_retry": "Försök igen",
"common_save": "Spara",
"common_separator_comma": ", ",
"common_separator_dot": " · ",
"common_tagline": "Delade utgifter för två",
"common_today": "Idag",
"common_too_much": "{1} för mycket",
"common_undo": "Ångra",
"common_user_split": "{1} {2}",
"common_wordmark": "Kvitt",
"common_yesterday": "Igår",
"common_you": "Du",
"common_you_lower": "du",
"editor_account": "Konto",
"editor_account_spoken": "Konto: {1}",
"editor_add_note": "Lägg till anteckning",
"editor_amount": "Belopp",
"editor_category": "Kategori",
"editor_category_all": "Alla",
"editor_category_all_spoken": "Alla kategorier",
"editor_description": "Beskrivning",
"editor_keypad": "Sifferknappar",
"editor_keypad_clear": "Rensa",
"editor_keypad_decimal": "Decimaltecken",
"editor_keypad_delete": "Radera",
"editor_notes": "Anteckningar",
"editor_paid_by": "Betalt av",
"editor_paid_by_toggle": "Betalt av {1}. Dubbeltryck för att byta till {2}.",
"editor_percent_of": "{1} %",
"editor_percent_suffix": "%",
"editor_pick_account": "Välj konto",
"editor_pick_category": "Välj kategori",
"editor_ratio_slider": "{1}s andel",
"editor_ratio_spoken": "{1} {2} procent, {3} {4} procent",
"editor_split": "Delning",
"editor_split_collapse": "Fäll ihop delning",
"editor_split_exact_computed": "räknas ut",
"editor_split_exact_of": "{1} {2} av {3}",
"editor_split_full_on": "Allt på {1}",
"editor_split_full_on_you": "Allt på dig",
"editor_split_full_question": "Vem står för hela beloppet?",
"editor_split_mode_default": "Kontots standard {1}",
"editor_split_mode_exact": "Exakta belopp",
"editor_split_mode_full": "Allt på en person",
"editor_split_mode_ratio": "Egen fördelning",
"effect_borrowed": "Du lånade {1}",
"effect_lent": "Du lånade ut {1}",
"effect_not_involved": "Inte inblandad",
"effect_paid_for_yourself": "Betalade för dig själv",
"error_amount_out_of_range": "Beloppet måste vara mellan {1} och {2}",
"error_amount_required": "Skriv ett belopp",
"error_app_too_old": "Uppdatera Kvitt för att fortsätta",
"error_date_out_of_range": "Datumet ligger för långt fram eller bak",
"error_description_required": "Skriv en beskrivning",
"error_exact_share_too_large": "Andelen är större än beloppet",
"error_forbidden": "Du får inte göra det här",
"error_generic_invalid": "Det gick inte att spara. Kontrollera fälten.",
"error_household_full": "Din partner har redan gått med",
"error_household_incomplete": "Din partner behöver gå med först",
"error_not_found": "Det här finns inte längre",
"error_rate_limited": "För många försök. Vänta en stund och försök igen.",
"error_ratio_invalid": "Delarna måste bli 100 %",
"error_server": "Något gick fel på servern. Försök igen senare.",
"error_too_long:one": "Högst {1} tecken",
"error_too_long:other": "Högst {1} tecken",
"error_validation": "Servern godtog inte värdena",
"expense_detail_date": "Datum",
"expense_detail_delete_body": "Den försvinner för er båda.",
"expense_detail_delete_title": "Ta bort utgiften?",
"expense_detail_deleted_banner": "{1} tog bort den här utgiften · {2}",
"expense_detail_deleted_body": "Den räknas inte längre i saldon eller statistik.",
"expense_detail_missing": "Utgiften finns inte på telefonen än",
"expense_detail_missing_body": "Den kan ha lagts till nyss på en annan telefon. Synka för att hämta den.",
"expense_detail_notes": "Anteckning",
"expense_detail_paid_by": "Betalt av",
"expense_detail_recurring": "Återkommande: {1}",
"expense_detail_split": "Delning",
"expense_detail_sync_now": "Synka nu",
"expense_editor_date": "Datum",
"expense_editor_delete": "Ta bort utgift",
"expense_editor_discard_title": "Slänga utgiften?",
"expense_editor_edit": "Ändra utgift",
"expense_editor_new": "Ny utgift",
"fab_add_expense": "Lägg till utgift",
"home_account_expenses_month:one": "{1} utgift i {2}",
"home_account_expenses_month:other": "{1} utgifter i {2}",
"home_account_no_expenses_month": "Inga utgifter i {1}",
"home_accounts": "Konton",
"home_archived_collapsed": "hopfälld",
"home_archived_expanded": "utfälld",
"home_archived_section": "Arkiverade ({1})",
"home_empty_body": "Ett konto samlar utgifter som ni delar på samma sätt, till exempel Vardag, Huset eller en resa. Ni kan ha hur många ni vill.",
"home_empty_subtitle": "Hushållet är klart. Nu behövs ett konto.",
"home_empty_title": "Skapa ert första konto",
"home_empty_welcome": "Du och {1} är med",
"home_loading": "Hämtar era utgifter…",
"home_loading_body": "Första synken hämtar allt som finns på servern. Det tar bara en stund.",
"home_new_account": "Nytt konto",
"home_settle_up": "Gör upp",
"home_sync_lost_body": "Så här såg det ut på telefonen. Det sparades inte och kan inte ändras här.",
"home_sync_lost_deleted": "Du tog bort den här",
"home_sync_lost_show_current": "Visa nuvarande version",
"home_sync_lost_title": "Din version",
"home_sync_reject_clock_skew": "Telefonens klocka går fel. Rätta den och synka igen",
"home_sync_reject_deleted": "Den här togs bort innan din ändring kom fram",
"home_sync_reject_reference": "Dess konto eller kategori finns inte längre",
"home_sync_reject_stale": "Din ändring ersattes av en nyare ändring från {1}",
"home_sync_reject_stale_help": "{1}s version gäller. Öppna utgiften om du vill göra din ändring igen.",
"home_sync_reject_stale_help_payment": "{1}s version gäller. Öppna betalningen om du vill göra din ändring igen.",
"home_sync_reject_validation": "Ändringen var inte giltig och sparades inte",
"home_sync_status_all_synced": "Allt är synkat",
"home_sync_status_clock_body": "Klockan ligger mer än 10 minuter före servern. Dina ändringar sparas på telefonen tills dess.",
"home_sync_status_clock_warning": "Telefonens klocka går fel. Rätta den och synka igen.",
"home_sync_status_dismiss": "Ta bort från listan",
"home_sync_status_last_attempt_clock": "Senaste försöket nekades: klockan",
"home_sync_status_last_success": "Senast lyckade synk {1}",
"home_sync_status_open": "Öppna",
"home_sync_status_open_date_settings": "Datum och tid",
"home_sync_status_pending_title": "Väntande ändringar ({1})",
"home_sync_status_rejected_title": "Ändringar som inte synkades",
"home_sync_status_sync_now": "Synka nu",
"home_sync_status_title": "Synkstatus",
"icon_account_balance": "Bank",
"icon_bolt": "El",
"icon_build": "Reparationer",
"icon_card_giftcard": "Present",
"icon_category": "Övrigt",
"icon_celebration": "Fest",
"icon_chair": "Möbler",
"icon_checkroom": "Kläder",
"icon_child_care": "Barn",
"icon_directions_car": "Bil",
"icon_fitness_center": "Träning",
"icon_flight": "Flyg",
"icon_home": "Hem",
"icon_hotel": "Hotell",
"icon_key": "Nyckel",
"icon_local_cafe": "Kafé",
"icon_local_gas_station": "Bränsle",
"icon_luggage": "Bagage",
"icon_medical_services": "Vård",
"icon_medication": "Medicin",
"icon_movie": "Film",
"icon_music_note": "Musik",
"icon_pets": "Husdjur",
"icon_restaurant": "Restaurang",
"icon_school": "Skola",
"icon_shield": "Sköld",
"icon_shopping_bag": "Shoppingkasse",
"icon_shopping_cart": "Kundvagn",
"icon_sports_esports": "Spel",
"icon_subscriptions": "Prenumerationer",
"icon_train": "Tåg",
"icon_water_drop": "Vatten",
"icon_wifi": "Internet",
"ledger_month_total": "{1} · {2}",
"ledger_settlement_accounts:one": "{1} konto",
"ledger_settlement_accounts:other": "{1} konton",
"ledger_settlement_label": "Betalning",
"ledger_settlement_partner_paid_you": "{1} betalade dig {2}",
"ledger_settlement_you_paid_partner": "Du betalade {1} {2}",
"nav_analytics": "Statistik",
"nav_home": "Översikt",
"nav_label": "Navigering",
"nav_settings": "Inställningar",
"notif_channel_activity": "Aktivitet",
"notif_expense_created_body": "{1} · {2} · {3}",
"notif_expense_created_title": "{1} lade till en utgift",
"notif_expense_deleted_body": "{1} · {2}",
"notif_expense_deleted_title": "{1} tog bort en utgift",
"notif_expense_updated_body": "{1} · {2}",
"notif_expense_updated_title": "{1} ändrade en utgift",
"notif_generic_body": "Öppna Kvitt för att se det",
"notif_generic_title": "Kvitt",
"notif_generic_title_actor": "{1} ändrade något i Kvitt",
"notif_recurring_created_body": "{1} · {2} · {3}",
"notif_recurring_created_title": "Återkommande utgift tillagd",
"notif_settlement_body": "{1} betalade {2} {3}",
"notif_settlement_body_from_you": "Du betalade {1} {2}",
"notif_settlement_body_to_you": "{1} betalade dig {2}",
"notif_settlement_created_title": "{1} registrerade en betalning",
"notif_settlement_deleted_body": "{1}",
"notif_settlement_deleted_title": "{1} tog bort en betalning",
"notif_settlement_updated_title": "{1} ändrade en betalning",
"notif_sync_batch_body": "Öppna Kvitt för att se dem",
"notif_sync_batch_title:one": "{1} gjorde {2} ändring",
"notif_sync_batch_title:other": "{1} gjorde {2} ändringar",
"onboarding_again_body": "Dina utgifter finns kvar på telefonen.",
"onboarding_again_title": "Para ihop telefonen igen",
"onboarding_code_help": "Koden får du av {1}: från en länk eller från Inställningar › Bjud in partner.",
"onboarding_code_help_generic": "Koden får du från en länk, eller så skrivs den ut när servern sätts upp.",
"onboarding_code_paste": "Klistra in",
"onboarding_code_placeholder": "XXXX-XXXX-XXXX",
"onboarding_error_code_length": "Koden har 12 tecken",
"onboarding_error_https": "Adressen måste börja med https://",
"onboarding_error_invite_invalid": "Koden fungerar inte. Den kan ha gått ut eller redan använts. Be om en ny.",
"onboarding_error_name_required": "Skriv ditt namn",
"onboarding_error_network": "Kunde inte nå servern. Kontrollera adressen och anslutningen.",
"onboarding_error_rate_limited": "För många försök. Vänta en stund och försök igen.",
"onboarding_name_label": "Ditt namn",
"onboarding_pair": "Para ihop",
"onboarding_pairing": "Parar ihop…",
"onboarding_profile_hint": "Så här ser {1} dig i Kvitt. Du kan ändra det senare under Inställningar › Profil.",
"onboarding_profile_joining": "Du går med i {1}s hushåll",
"onboarding_profile_title": "Din profil",
"onboarding_removed_body": "Åtkomsten togs bort och allt Kvitt-data på telefonen har raderats.",
"onboarding_removed_new_code": "Vill du använda Kvitt igen behöver du en ny inbjudningskod.",
"onboarding_removed_pair_again": "Para ihop igen",
"onboarding_removed_title": "Den här telefonen har tagits bort",
"onboarding_server_label": "Serveradress",
"onboarding_waiting_body": "Kvitt behöver er båda innan ni kan börja. Bjud in din partner med en länk.",
"onboarding_waiting_footer": "Konton, utgifter och betalningar blir tillgängliga när din partner har gått med.",
"onboarding_waiting_invite_expires": "Inbjudan gäller till {1}",
"onboarding_waiting_invite_sent": "Inbjudan skickad",
"onboarding_waiting_partner_missing": "Partner saknas",
"onboarding_waiting_title": "Väntar på din partner",
"profile_add_picture": "Lägg till bild",
"profile_change_picture": "Byt bild",
"profile_picture_failed": "Bilden kunde inte användas",
"profile_picture_invalid": "Bilden kunde inte läsas",
"profile_picture_too_large": "Bilden är för stor (högst 5 MB)",
"profile_picture_unsupported": "Använd en bild i JPEG, PNG eller WebP",
"profile_picture_uploading": "Laddar upp bilden…",
"profile_remove_picture": "Ta bort bild",
"settings_about": "Om Kvitt",
"settings_about_check_update": "Sök efter uppdatering",
"settings_about_server": "Server",
"settings_about_server_version": "Serverversion",
"settings_about_server_version_schema": "{1} (schema {2})",
"settings_about_server_version_unknown": "Okänd",
"settings_about_summary": "Version {1}",
"settings_about_title": "Om Kvitt",
"settings_about_up_to_date": "Du har den senaste versionen",
"settings_about_version": "Kvitt {1} ({2})",
"settings_categories": "Kategorier",
"settings_categories_archived": "Arkiverade ({1})",
"settings_categories_drag_handle": "Flytta {1}",
"settings_categories_move_down": "Flytta ned",
"settings_categories_move_up": "Flytta upp",
"settings_categories_new": "Ny kategori",
"settings_categories_reorder_offline": "Kräver anslutning för att ändra",
"settings_categories_summary:one": "{1} aktiv",
"settings_categories_summary:other": "{1} aktiva",
"settings_categories_title": "Kategorier",
"settings_category_archived_badge": "Arkiverad",
"settings_category_archived_snackbar": "{1} arkiverades",
"settings_category_delete_blocked_default": "Standardkategorier kan inte tas bort, bara arkiveras",
"settings_category_delete_blocked_default_short": "Kan inte tas bort: standardkategori",
"settings_category_delete_blocked_in_use": "Kategorin används av utgifter eller återkommande utgifter. Arkivera den i stället.",
"settings_category_delete_blocked_in_use_archived": "Kategorin används av utgifter eller återkommande utgifter och kan inte tas bort.",
"settings_category_delete_body": "Den försvinner från listan för er båda. Det går inte att ångra.",
"settings_category_delete_title": "Ta bort kategorin {1}?",
"settings_category_editor_archive": "Arkivera",
"settings_category_editor_color": "Färg",
"settings_category_editor_default_name": "Standardnamn: {1}",
"settings_category_editor_delete": "Ta bort kategori",
"settings_category_editor_edit": "Ändra kategori",
"settings_category_editor_icon": "Ikon",
"settings_category_editor_new": "Ny kategori",
"settings_category_editor_reset_name": "Återställ namn",
"settings_category_editor_unarchive": "Återställ från arkiv",
"settings_category_unarchived_snackbar": "{1} återställdes",
"settings_collapsed": "hopfälld",
"settings_devices": "Enheter",
"settings_devices_app_version": "Kvitt {1}",
"settings_devices_cannot_revoke_current": "Du kan inte ta bort åtkomsten för den telefon du använder just nu.",
"settings_devices_last_seen": "Senast sedd {1}",
"settings_devices_loading": "Hämtar enheter…",
"settings_devices_offline": "Kräver anslutning för att visa enheter",
"settings_devices_paired": "Parad {1}",
"settings_devices_revoke": "Ta bort åtkomst",
"settings_devices_revoke_body_own": "Telefonen loggas ut och allt Kvitt-data på den raderas. Du kan para ihop den igen med en ny inbjudningskod.",
"settings_devices_revoke_body_partner": "{1}s telefon loggas ut och allt Kvitt-data på den raderas. {1} kan para ihop den igen med en ny inbjudningskod.",
"settings_devices_revoke_title": "Ta bort åtkomst för {1}?",
"settings_devices_this_device": "Den här enheten",
"settings_devices_title": "Enheter",
"settings_expanded": "utfälld",
"settings_invites_cancel": "Avbryt inbjudan",
"settings_invites_cancel_title": "Avbryta inbjudan?",
"settings_invites_code_copied": "Koden kopierades",
"settings_invites_code_hidden": "Koden visas bara när den skapas",
"settings_invites_copy_code": "Kopiera kod",
"settings_invites_create_body": "Skapa en engångskod för att para ihop en telefon till. Koden gäller i 72 timmar.",
"settings_invites_created": "Skapad {1}",
"settings_invites_for_me": "För mig",
"settings_invites_for_partner": "För {1}",
"settings_invites_instructions": "Öppna länken på den nya telefonen, eller skriv in koden i Kvitt. Koden fungerar en gång.",
"settings_invites_kind_device": "Enhet · {1}",
"settings_invites_kind_partner": "Partner",
"settings_invites_new_device_for": "Ny enhet för {1}",
"settings_invites_new_partner": "Inbjudan till din partner",
"settings_invites_none": "Inga väntande inbjudningar",
"settings_invites_offline_banner": "Kräver anslutning. Inbjudningar kan bara skapas och visas när telefonen är online.",
"settings_invites_pending": "Väntande inbjudningar",
"settings_invites_share": "Dela",
"settings_invites_share_message": "Gå med i vårt hushåll i Kvitt: {1} Koden är {2}.",
"settings_invites_title_device": "Para ihop en enhet",
"settings_invites_valid_until": "Gäller till {1}",
"settings_pair_device": "Para ihop en enhet",
"settings_partner_missing": "Partner saknas",
"settings_profile": "Profil",
"settings_profile_hint": "Så här ser {1} dig i Kvitt.",
"settings_profile_offline_banner": "Kräver anslutning. Profilen kan bara ändras när telefonen är online.",
"settings_profile_summary": "Namn och bild",
"settings_recurring": "Återkommande utgifter",
"settings_recurring_created_at": "Skapas kl. 06:00 den {1} varje månad",
"settings_recurring_day_clamp_hint": "I kortare månader används sista dagen",
"settings_recurring_day_of_month": "Dag i månaden",
"settings_recurring_day_ordinal_a": "{1}:a",
"settings_recurring_day_ordinal_e": "{1}:e",
"settings_recurring_delete_body": "Utgifter som redan skapats finns kvar.",
"settings_recurring_delete_title": "Ta bort {1}?",
"settings_recurring_edit_hint": "Ändringen gäller kommande utgifter.",
"settings_recurring_editor_edit": "Återkommande utgift",
"settings_recurring_editor_new": "Ny återkommande utgift",
"settings_recurring_empty_action": "Ny återkommande utgift",
"settings_recurring_empty_body": "Hyra, el och streaming kan läggas in automatiskt varje månad.",
"settings_recurring_empty_title": "Inga återkommande utgifter",
"settings_recurring_error_end_before_start": "Sista månaden kan inte vara före den första",
"settings_recurring_error_start_too_early": "Högst 12 månader bakåt",
"settings_recurring_every_month_on": "Varje månad den {1}",
"settings_recurring_first_month": "Första månad",
"settings_recurring_last_month": "Sista månad (valfritt)",
"settings_recurring_new": "Ny återkommande",
"settings_recurring_next": "Nästa {1}",
"settings_recurring_no_end": "Ingen slutmånad",
"settings_recurring_offline_banner": "Kräver anslutning. Återkommande utgifter kan bara ändras när telefonen är online.",
"settings_recurring_paused": "Pausad",
"settings_recurring_paused_switch": "Pausad",
"settings_recurring_resume_hint": "Månader som passerat under pausen skapas inte i efterhand.",
"settings_recurring_row_partner_pays": "{1} · {2} betalar",
"settings_recurring_row_you_pay": "{1} · Du betalar",
"settings_recurring_schedule": "Schema",
"settings_recurring_summary_active:one": "{1} aktiv",
"settings_recurring_summary_active:other": "{1} aktiva",
"settings_recurring_summary_none": "Inga",
"settings_recurring_summary_paused:one": "{1} pausad",
"settings_recurring_summary_paused:other": "{1} pausade",
"settings_recurring_title": "Återkommande utgifter",
"settings_role_admin": "Du · Administratör",
"settings_role_admin_other": "Admin",
"settings_role_member": "Du",
"settings_sync_status": "Synkstatus",
"settings_title": "Inställningar",
"settings_update_badge": "Uppdatering finns",
"settle_add_account": "Lägg till konto",
"settle_allocation": "Fördelning",
"settle_amount": "Belopp",
"settle_confirm": "Bekräfta",
"settle_confirm_body_partner_pays": "{1} betalar dig {2} {3}.",
"settle_confirm_body_you_pay": "Du betalar {1} {2} {3}.",
"settle_confirm_date": "med datum {1}",
"settle_confirm_date_today": "idag, {1}",
"settle_confirm_dialog_title": "Bekräfta betalningen",
"settle_date": "Datum",
"settle_detail_delete": "Ta bort betalning",
"settle_detail_delete_body": "Saldona räknas om för er båda.",
"settle_detail_delete_title": "Ta bort betalningen?",
"settle_detail_deleted_banner": "{1} tog bort den här betalningen · {2}",
"settle_detail_deleted_body": "Den räknas inte längre i saldona.",
"settle_detail_missing": "Betalningen finns inte på telefonen än",
"settle_detail_missing_body": "Den kan ha lagts till nyss på en annan telefon. Synka för att hämta den.",
"settle_detail_partner_paid_you": "{1} betalade dig",
"settle_detail_sync_now": "Synka nu",
"settle_detail_title": "Betalning",
"settle_detail_you_paid_partner": "Du betalade {1}",
"settle_direction": "Riktning",
"settle_empty_hint": "Skriv ett belopp så föreslår Kvitt en fördelning.",
"settle_error_allocation_positive": "Varje konto behöver minst 0,01 kr",
"settle_error_allocation_sum": "Kontona måste bli lika med beloppet",
"settle_error_no_allocations": "Lägg till minst ett konto",
"settle_error_too_many_allocations": "Högst 50 konton",
"settle_note": "Anteckning",
"settle_note_hint": "Till exempel Swish",
"settle_over_settle_warning": "Det här reglerar mer än som är skyldigt i {1}",
"settle_partner_pays": "{1} betalar dig",
"settle_propose_again": "Föreslå igen",
"settle_remaining": "Kvar att fördela",
"settle_row_amount": "Belopp för {1}",
"settle_row_debt": "Skuld {1}",
"settle_row_remove": "Ta bort {1} från fördelningen",
"settle_swap": "Byt riktning",
"settle_title": "Gör upp",
"settle_title_account": "Gör upp {1}",
"settle_title_edit": "Ändra betalning",
"settle_untouched_partner_owes": "{1} lämnas som det är, eftersom {2} är skyldig dig där.",
"settle_untouched_you_owe": "{1} lämnas som det är, eftersom du är skyldig {2} där.",
"settle_you_pay": "Du betalar {1}",
"snackbar_expense_deleted": "Utgiften togs bort",
"snackbar_expense_saved": "Utgiften sparades",
"snackbar_expense_saved_offline": "Sparad. Synkas när du är online.",
"snackbar_payment_deleted": "Betalningen togs bort",
"snackbar_payment_saved": "Betalningen sparades",
"sync_clock_wrong_short": "Telefonens klocka går fel",
"sync_failed_line": "Synk misslyckades · försöker igen snart",
"sync_line_spoken": "Synkstatus: {1}",
"sync_pending:one": "{1} ändring väntar på synk",
"sync_pending:other": "{1} ändringar väntar på synk",
"sync_rejected:one": "{1} ändring synkades inte",
"sync_rejected:other": "{1} ändringar synkades inte",
"sync_synced_relative": "Synkad {1}",
"sync_syncing": "Synkar…",
"time_hours_ago": "för {1} h sedan",
"time_just_now": "nyss",
"time_minutes_ago": "för {1} min sedan",
"time_today_at": "idag {1}",
"timeline_changed": "{1} ändrade {2} från {3} till {4}",
"timeline_changed_added": "{1} lade till {2}: {3}",
"timeline_changed_removed": "{1} tog bort {2}",
"timeline_created": "{1} skapade den här",
"timeline_deleted": "{1} tog bort den här",
"timeline_field_account": "kontot",
"timeline_field_allocations": "fördelningen",
"timeline_field_amount": "beloppet",
"timeline_field_category": "kategorin",
"timeline_field_date": "datumet",
"timeline_field_description": "beskrivningen",
"timeline_field_from": "vem som betalade",
"timeline_field_note": "anteckningen",
"timeline_field_notes": "anteckningen",
"timeline_field_payer": "betalaren",
"timeline_field_split_mode": "delningen",
"timeline_field_splits": "andelarna",
"timeline_field_to": "vem som fick",
"timeline_import": "Importerad från Splitwise",
"timeline_pending": "Din ändring väntar på synk",
"timeline_recurring": "Skapad automatiskt (återkommande)",
"timeline_time": "· {1}",
"timeline_title": "Historik",
"update_available": "Version {1} finns",
"update_details": "Nyheter och rättningar · {1}",
"update_downloading": "Laddar ner… {1} %",
"update_downloading_spoken": "Laddar ner version {1}",
"update_failed": "Nedladdningen misslyckades. Försök igen.",
"update_install": "Uppdatera",
"update_later": "Senare",
"update_permission_body": "Kvitt uppdateras direkt från er server, inte från Play Butik. Därför frågar Android en gång om Kvitt får Installera okända appar. Slå på Tillåt från den här källan och gå sedan tillbaka till Kvitt. Uppdateringen fortsätter av sig själv.",
"update_permission_title": "Tillåt installation",
"web_account_detail_title": "Konto",
"web_expense_detail_title": "Utgift",
"web_insights_title": "Insikter",
"web_language": "Språk",
"web_language_auto": "Webbläsarens språk",
"web_language_en": "English",
"web_language_sv": "Svenska",
"web_login_body": "Skriv in en inbjudningskod från Inställningar › Para ihop en enhet på telefonen.",
"web_login_name_needed": "Koden lägger till en ny person i hushållet. Skriv ditt namn.",
"web_login_server": "Server",
"web_login_title": "Logga in i Kvitt",
"web_logout": "Logga ut",
"web_logout_body": "Webbläsaren glömmer sin åtkomst. Ta bort den under Enheter på telefonen för att spärra den helt.",
"web_main_navigation": "Huvudnavigering",
"web_nav_search": "Sök",
"web_not_found_action": "Gå till Översikt",
"web_not_found_title": "Sidan finns inte",
"web_search_title": "Sök",
"web_shortcut_close": "Stäng dialog eller gå tillbaka",
"web_shortcut_help": "Visa kortkommandon",
"web_shortcut_new_expense": "Ny utgift",
"web_shortcut_search": "Sök",
"web_shortcuts_title": "Kortkommandon",
"web_signed_out": "Du loggades ut",
"web_skip_to_content": "Hoppa till innehållet",
"web_theme": "Tema",
"web_theme_dark": "Mörkt",
"web_theme_light": "Ljust",
"web_theme_system": "System",
"web_update_failed": "Kunde inte uppdatera · försöker igen snart",
"web_update_now": "Uppdatera nu",
"web_updated_relative": "Uppdaterad {1}",
"web_updating": "Uppdaterar…"
}
+9
View File
@@ -0,0 +1,9 @@
import { useContext } from 'react';
import { I18nContext, type I18n } from './I18nProvider';
/** The current language, `t(key, …args)` and `tp(pluralKey, count, …args)`. */
export function useI18n(): I18n {
const context = useContext(I18nContext);
if (context === null) throw new Error('useI18n outside I18nProvider');
return context;
}
+34
View File
@@ -0,0 +1,34 @@
key,en,sv,notes
web_nav_search,Search,Sök,module=web; sidebar entry and / shortcut
web_search_title,Search,Sök,module=web; /app/search title
web_insights_title,Insights,Insikter,module=web; /app/insights title
web_main_navigation,Main navigation,Huvudnavigering,module=web; label of the sidebar and the bottom bar
web_skip_to_content,Skip to content,Hoppa till innehållet,module=web; first focusable link
web_updated_relative,Updated %1$s,Uppdaterad %1$s,module=web; shell status line; %1$s = time_just_now / time_minutes_ago / time_hours_ago or a list date and time
web_updating,Updating…,Uppdaterar…,module=web; shell status line while the first read runs
web_update_failed,Could not update · trying again soon,Kunde inte uppdatera · försöker igen snart,module=web; shell status line after a failed read
web_update_now,Update now,Uppdatera nu,module=web; button on the status line
web_login_title,Sign in to Kvitt,Logga in i Kvitt,module=web; /app/login heading
web_login_body,Enter an invite code from Settings › Pair a device on your phone.,Skriv in en inbjudningskod från Inställningar › Para ihop en enhet på telefonen.,module=web; /app/login help line
web_login_server,Server,Server,module=web; label of the fixed server line on /app/login
web_login_name_needed,This code adds a new person to the household. Enter your name.,Koden lägger till en ny person i hushållet. Skriv ditt namn.,module=web; shown when the server asks for display_name
web_logout,Sign out,Logga ut,module=web; Settings and Devices
web_logout_body,This browser forgets its access. Remove it under Devices on your phone to revoke it for good.,Webbläsaren glömmer sin åtkomst. Ta bort den under Enheter på telefonen för att spärra den helt.,module=web; confirmation dialog body
web_signed_out,You were signed out,Du loggades ut,module=web; toast after a 401
web_shortcuts_title,Keyboard shortcuts,Kortkommandon,module=web; help dialog title
web_shortcut_new_expense,New expense,Ny utgift,module=web; shortcut N
web_shortcut_search,Search,Sök,module=web; shortcut /
web_shortcut_close,Close dialog or go back,Stäng dialog eller gå tillbaka,module=web; shortcut Esc
web_shortcut_help,Show keyboard shortcuts,Visa kortkommandon,module=web; shortcut ?
web_language,Language,Språk,module=web; language override
web_language_auto,Browser language,Webbläsarens språk,module=web
web_language_sv,Svenska,Svenska,module=web; translatable=false
web_language_en,English,English,module=web; translatable=false
web_theme,Theme,Tema,module=web; data-theme override
web_theme_system,System,System,module=web
web_theme_light,Light,Ljust,module=web
web_theme_dark,Dark,Mörkt,module=web
web_not_found_title,Page not found,Sidan finns inte,module=web; unknown route under /app/
web_not_found_action,Go to Home,Gå till Översikt,module=web
web_account_detail_title,Account,Konto,module=web; placeholder title of /app/accounts/:id until the page shows the account name
web_expense_detail_title,Expense,Utgift,module=web; placeholder title of /app/expenses/:id until the page shows the description
1 key en sv notes
2 web_nav_search Search Sök module=web; sidebar entry and / shortcut
3 web_search_title Search Sök module=web; /app/search title
4 web_insights_title Insights Insikter module=web; /app/insights title
5 web_main_navigation Main navigation Huvudnavigering module=web; label of the sidebar and the bottom bar
6 web_skip_to_content Skip to content Hoppa till innehållet module=web; first focusable link
7 web_updated_relative Updated %1$s Uppdaterad %1$s module=web; shell status line; %1$s = time_just_now / time_minutes_ago / time_hours_ago or a list date and time
8 web_updating Updating… Uppdaterar… module=web; shell status line while the first read runs
9 web_update_failed Could not update · trying again soon Kunde inte uppdatera · försöker igen snart module=web; shell status line after a failed read
10 web_update_now Update now Uppdatera nu module=web; button on the status line
11 web_login_title Sign in to Kvitt Logga in i Kvitt module=web; /app/login heading
12 web_login_body Enter an invite code from Settings › Pair a device on your phone. Skriv in en inbjudningskod från Inställningar › Para ihop en enhet på telefonen. module=web; /app/login help line
13 web_login_server Server Server module=web; label of the fixed server line on /app/login
14 web_login_name_needed This code adds a new person to the household. Enter your name. Koden lägger till en ny person i hushållet. Skriv ditt namn. module=web; shown when the server asks for display_name
15 web_logout Sign out Logga ut module=web; Settings and Devices
16 web_logout_body This browser forgets its access. Remove it under Devices on your phone to revoke it for good. Webbläsaren glömmer sin åtkomst. Ta bort den under Enheter på telefonen för att spärra den helt. module=web; confirmation dialog body
17 web_signed_out You were signed out Du loggades ut module=web; toast after a 401
18 web_shortcuts_title Keyboard shortcuts Kortkommandon module=web; help dialog title
19 web_shortcut_new_expense New expense Ny utgift module=web; shortcut N
20 web_shortcut_search Search Sök module=web; shortcut /
21 web_shortcut_close Close dialog or go back Stäng dialog eller gå tillbaka module=web; shortcut Esc
22 web_shortcut_help Show keyboard shortcuts Visa kortkommandon module=web; shortcut ?
23 web_language Language Språk module=web; language override
24 web_language_auto Browser language Webbläsarens språk module=web
25 web_language_sv Svenska Svenska module=web; translatable=false
26 web_language_en English English module=web; translatable=false
27 web_theme Theme Tema module=web; data-theme override
28 web_theme_system System System module=web
29 web_theme_light Light Ljust module=web
30 web_theme_dark Dark Mörkt module=web
31 web_not_found_title Page not found Sidan finns inte module=web; unknown route under /app/
32 web_not_found_action Go to Home Gå till Översikt module=web
33 web_account_detail_title Account Konto module=web; placeholder title of /app/accounts/:id until the page shows the account name
34 web_expense_detail_title Expense Utgift module=web; placeholder title of /app/expenses/:id until the page shows the description