import { useSyncExternalStore } from "react"; export type Language = "ru" | "en"; const STORAGE_KEY = "aurum:language"; const ru = { "nav.dashboard": "Дашборд", "nav.netWorth": "Капитал", "nav.crypto": "Криптовалюта", "nav.roi": "Доходность", "nav.transactions": "Транзакции", "nav.accounts": "Счета", "nav.categories": "Категории", "nav.cashFlow": "Движение денег", "nav.reports": "Отчёты", "nav.budget": "Бюджет", "nav.recurring": "Регулярные", "nav.goals": "Цели", "nav.advice": "Советы", "nav.settings": "Настройки", "nav.comingSoon": "скоро", "sidebar.collapse": "Свернуть", "sidebar.collapseMenu": "Свернуть меню", "sidebar.expandMenu": "Развернуть меню", "sidebar.closeMenu": "Закрыть меню", "topbar.openMenu": "Открыть меню", "common.loading": "Загрузка…", "common.expand": "Развернуть", "common.collapse": "Свернуть", "common.back": "Назад", "common.next": "Далее", "common.clear": "Очистить", "common.cancel": "Отмена", "common.save": "Сохранить", "common.saving": "Сохранение…", "common.edit": "Изменить", "common.delete": "Удалить", "common.close": "Закрыть", "common.add": "Добавить", "common.pageOf": "Стр. {{page}} из {{total}}", "common.totalCount": "{{count}} всего", "common.perMonth": "/мес", "common.allShort": "Всё", "auth.subtitle": "Личные финансы", "auth.usernameLabel": "Логин", "auth.passwordLabel": "Пароль", "auth.rememberMe": "Запомнить меня на 7 дней", "auth.sessionOnlyHint": "Вход сохраняется до закрытия браузера. Запоминание надолго требует HTTPS или localhost.", "auth.submitButton": "Войти", "auth.submitting": "Вход…", "auth.errorInvalidCredentials": "Неверный логин или пароль", "auth.errorUnreachable": "Не удалось подключиться к серверу. Проверьте соединение и попробуйте снова.", "auth.noAuthModalTitle": "Авторизация не настроена", "auth.noAuthModalBody": "Этот экземпляр Aurum не защищён паролем — все данные доступны любому, кто до него достучится. Задайте AURUM_BASIC_AUTH_USER и AURUM_BASIC_AUTH_PASSWORD в .env, чтобы включить вход.", "auth.noAuthModalAccept": "Я понимаю риски и хочу продолжить", "dashboard.errorLoading": "Не удалось загрузить данные дашборда. Проверьте, что backend запущен.", "dashboard.statRealIncomeLabel": "Реальный доход", "dashboard.statRealIncomeCaption": "зарплаты, возвраты", "dashboard.statSpentLabel": "Расходы", "dashboard.statSpentCaption": "без учёта переводов", "dashboard.statNetLabel": "Баланс", "dashboard.statNetCaption": "доход − расход", "dashboard.statSavingsRateLabel": "Норма сбережений", "dashboard.statSavingsRateCaption": "доля дохода, оставшаяся после расходов", "dashboard.spendingByCategoryTitle": "Расходы по категориям", "dashboard.noExpensesThisMonth": "За этот месяц пока нет расходов. Добавьте первую транзакцию на вкладке «Транзакции».", "dashboard.recentTransactionsTitle": "Последние транзакции", "dashboard.allTransactionsLink": "Все транзакции", "dashboard.noTransactionsYet": "Транзакций пока нет.", "netWorth.periodSuffix": "за период", "netWorth.noChartData": "Пока нет данных для графика.", "netWorth.assetsTitlePrefix": "Активы", "netWorth.myAssetsTitle": "Мои активы", "netWorth.confirmDeleteAsset": "Удалить актив «{{name}}»?", "netWorth.capitalByType": "Капитал по типу", "netWorth.assetClass.investments": "Инвестиции", "netWorth.assetClass.crypto": "Криптовалюта", "netWorth.assetClass.real_estate": "Недвижимость", "netWorth.assetClass.vehicles": "Транспорт", "netWorth.assetClass.precious_metals": "Драгметаллы", "netWorth.assetClass.other": "Другое", "netWorth.assetClass.cash": "Наличные", "netWorth.capitalRole.income": "Доходный", "netWorth.capitalRole.neutral": "Нейтральный", "netWorth.capitalRole.drain": "Убыточный", "netWorth.capitalRoleHint.income": "приносит деньги", "netWorth.capitalRoleHint.neutral": "просто есть, не тянет и не даёт", "netWorth.capitalRoleHint.drain": "тянет деньги каждый месяц", "netWorth.capitalRoleFormHint.income": "приносит деньги — сдаётся в аренду и т.п.", "netWorth.capitalRoleFormHint.neutral": "просто есть/используется — техника, мебель", "netWorth.capitalRoleFormHint.drain": "тянет деньги каждый месяц — машина и т.п.", "netWorth.riskAllocationTitle": "Капитал по уровню риска", "netWorth.riskAllocationOfCapital": "от капитала", "netWorth.riskLevel.low": "Низкий", "netWorth.riskLevel.medium": "Средний", "netWorth.riskLevel.high": "Высокий", "netWorth.riskLevelFormHint.low": "риск потери минимален — депозиты, наличные, гособлигации", "netWorth.riskLevelFormHint.medium": "умеренные колебания стоимости — недвижимость, диверсифицированные инвестиции", "netWorth.riskLevelFormHint.high": "может значительно потерять в цене — крипта, отдельные акции, стартапы", "netWorth.assetsTable.empty": "Активов пока нет. Добавьте инвестиции, крипту, недвижимость или другое имущество.", "netWorth.assetsTable.asOf": "на {{date}}", "netWorth.assetsTable.annualRoi": "{{percent}}% годовых", "crypto.addButton": "Добавить монету", "crypto.refreshButton": "Обновить цены", "crypto.refreshing": "Обновление…", "crypto.lastSynced": "Обновлено {{time}}", "crypto.neverSynced": "Цены ещё не загружались", "crypto.totalValueLabel": "Всего в криптовалюте", "crypto.empty": "Пока нет ни одной монеты. Добавьте первую, чтобы начать отслеживать.", "crypto.pendingPrice": "цена ещё не получена", "crypto.confirmDelete": "Убрать «{{name}}» из отслеживания?", "crypto.syncError.unreachable": "Не удалось подключиться к CoinGecko — показаны последние известные цены.", "crypto.search.placeholder": "Найти монету (например, Bitcoin или BTC)…", "crypto.search.hint": "Начните вводить название или тикер монеты.", "crypto.search.empty": "Ничего не найдено.", "crypto.search.noApiKey": "Не настроен ключ CoinGecko API — задайте AURUM_COINGECKO_API_KEY в .env (бесплатный ключ на coingecko.com/en/api/pricing).", "crypto.form.quantityLabel": "Количество", "crypto.form.pricePerUnitLabel": "Цена за штуку", "crypto.form.dateLabel": "Дата", "crypto.form.noteLabel": "Заметка (необязательно)", "crypto.form.networkLabel": "Сеть (необязательно)", "crypto.form.networkPlaceholder": "Например, Ethereum, Tron…", "crypto.form.addTitle": "Добавить {{name}}", "crypto.form.tradeTitle": "Сделка: {{name}}", "crypto.form.editTradeTitle": "Изменить сделку: {{name}}", "crypto.form.tradeButton": "Купить / продать", "crypto.form.buy": "Покупка", "crypto.form.sell": "Продажа", "crypto.form.errorSellTooMuch": "Нельзя продать больше, чем у вас есть.", "crypto.form.saveError": "Не удалось сохранить. Проверьте данные и попробуйте снова.", "crypto.table.name": "Название", "crypto.table.price": "Цена", "crypto.table.change1h": "1ч%", "crypto.table.change24h": "24ч%", "crypto.table.change7d": "7д%", "crypto.table.holdings": "Держите", "crypto.table.avgBuyPrice": "Ср. цена", "crypto.table.profitLoss": "Прибыль/убыток", "crypto.table.network": "Сеть", "crypto.table.addNetwork": "+ Сеть", "crypto.table.risk": "Риск", "crypto.table.actions": "Действия", "crypto.riskAllocationOfPortfolio": "от крипто-портфеля", "crypto.history.title": "История сделок: {{name}}", "crypto.history.empty": "Пока нет ни одной сделки.", "crypto.history.confirmDelete": "Удалить эту сделку? Количество и средняя цена будут пересчитаны.", "crypto.history.viewButton": "Смотреть историю", "crypto.holdingsTitle": "Активы", "crypto.chart.show": "Показать сумму", "crypto.chart.hide": "Скрыть сумму", "crypto.allocation.title": "Распределение по монетам", "crypto.overview.history": "История", "crypto.overview.allocation": "Аллокация", "crypto.overview.risk": "Уровни риска", "crypto.overview.networks": "Сети", "crypto.networkAllocation.unset": "Сеть не указана", "crypto.allocation.other": "Другое", "crypto.stats.allTimeProfit": "Прибыль за всё время", "crypto.stats.costBasis": "Вложено всего", "crypto.stats.costBasisHint": "сумма покупок по факту", "crypto.stats.bestPerformer": "Лучший рост", "crypto.stats.worstPerformer": "Худшее падение", "crypto.stats.changeLabel": "изменение цены за {{range}}", "crypto.portfolio.all": "Все портфели", "crypto.portfolio.addLabel": "Добавить портфель", "crypto.portfolio.archiveLabel": "Архивировать портфель", "crypto.portfolio.unarchiveLabel": "Вернуть из архива", "crypto.portfolio.confirmDelete": "Удалить портфель «{{name}}»?", "crypto.portfolio.deleteError": "Сначала переместите или удалите монеты из этого портфеля.", "crypto.portfolio.form.newTitle": "Новый портфель", "crypto.portfolio.form.editTitle": "Портфель", "crypto.portfolio.form.nameLabel": "Название портфеля", "crypto.portfolio.form.namePlaceholder": "Например, «Долгосрочные инвестиции»", "crypto.portfolio.form.saveError": "Не удалось сохранить. Проверьте данные и попробуйте снова.", "roi.calculator.title": "Калькулятор ROI", "roi.calculator.investmentLabel": "Сумма инвестиции", "roi.calculator.monthlyIncomeLabel": "Доход в месяц", "roi.calculator.annualRoiLabel": "Доходность в год", "roi.calculator.payback": "Окупится за {{years}} лет", "roi.calculator.noPayback": "Не окупается — доход не покрывает вложение", "roi.calculator.hint": "Впишите стоимость и ожидаемый доход в месяц, чтобы увидеть годовую доходность, срок окупаемости и прогноз на годы вперёд.", "roi.calculator.projectionTitle": "Прогноз со сложным процентом", "roi.calculator.projectionHint": "Если доходность {{percent}}% в год сохранится и весь доход будет реинвестироваться обратно в ту же инвестицию — расчёт только на основе введённых вами цифр, без данных из ваших активов.", "roi.calculator.chartCompoundLabel": "Со сложным процентом", "roi.calculator.chartSimpleLabel": "Без реинвестирования", "roi.calculator.tableTitle": "Сравнение по годам", "roi.calculator.colCompound": "Со сложным процентом", "roi.calculator.colSimple": "Без реинвестирования", "roi.calculator.colDifference": "Разница", "roi.calculator.yearOne": "Через 1 год", "roi.calculator.yearN": "Через {{years}} лет", "netWorth.form.editTitle": "Изменить актив", "netWorth.form.newTitle": "Новый актив", "netWorth.form.nameLabel": "Название", "netWorth.form.namePlaceholder": "Брокерский счёт, BTC, квартира…", "netWorth.form.classLabel": "Категория", "netWorth.form.currentValueLabel": "Текущая стоимость", "netWorth.form.valueLabel": "Стоимость", "netWorth.form.dateLabel": "Дата оценки", "netWorth.form.roleLabel": "Тип капитала", "netWorth.form.riskLevelLabel": "Уровень риска", "netWorth.form.cashFlowLabel": "Денежный поток в месяц (необязательно)", "netWorth.form.cashFlowPlaceholder": "800 для дохода, -150 для расхода", "netWorth.form.cashFlowHint": "Ориентировочно — реальная сумма из месяца в месяц меняется, это не строгий учёт.", "netWorth.form.notesLabel": "Заметки (необязательно)", "netWorth.form.saveError": "Не удалось сохранить актив. Проверьте данные и попробуйте снова.", "transactions.addButton": "Добавить", "transactions.importButton": "Импорт CSV", "transactions.import.title": "Импорт транзакций из CSV", "transactions.import.accountLabel": "Счёт для импорта", "transactions.import.accountHint": "Все транзакции из файла будут добавлены на этот счёт.", "transactions.import.uploadTitle": "Файл", "transactions.import.uploadHint": "Выгрузка транзакций из банка или другого приложения в формате CSV (с заголовками колонок в первой строке).", "transactions.import.chooseFile": "Выбрать файл", "transactions.import.pickAccountFirst": "Сначала выберите счёт.", "transactions.import.errorEmptyFile": "В файле нет ни одной строки с данными (кроме заголовка).", "transactions.import.mapTitle": "Сопоставьте колонки", "transactions.import.dateColumnLabel": "Колонка с датой", "transactions.import.dateFormatLabel": "Формат даты", "transactions.import.selectColumn": "Выберите колонку…", "transactions.import.amountColumnLabel": "Колонка с суммой", "transactions.import.amountHint": "Отрицательные суммы — расход, положительные — доход.", "transactions.import.descriptionColumnLabel": "Колонка с описанием", "transactions.import.notMapped": "— не сопоставлено —", "transactions.import.categoryHint": "Если текст в колонке точно совпадёт с названием существующей категории — она подставится автоматически.", "transactions.import.startOver": "Начать заново", "transactions.import.previewButton": "Показать превью", "transactions.import.previewTitle": "Превью импорта", "transactions.import.summary": "Готово к импорту: {{valid}}. Пропущено из-за ошибок: {{skipped}}.", "transactions.import.andMore": "…и ещё {{count}}", "transactions.import.skippedRow": "Строка {{row}}: {{reason}}", "transactions.import.importButton": "Импортировать {{count}}", "transactions.import.importSuccess": "Готово — добавлено транзакций: {{count}}.", "transactions.import.goToTransactions": "К транзакциям", "transactions.import.importError": "Не удалось импортировать. Проверьте данные и попробуйте снова.", "transactions.import.errorBadDate": "не удалось распознать дату «{{value}}»", "transactions.import.errorBadAmount": "не удалось распознать сумму «{{value}}»", "transactions.import.errorNoDescription": "нет описания", "transactions.import.encodingLabel": "Кодировка файла", "transactions.import.encodingHint": "Определяется автоматически; если вместо русских букв — кракозябры, попробуйте выбрать вручную.", "transactions.import.sampleValueLabel": "Пример из файла: {{value}}", "transactions.import.amountFormatLabel": "Разделитель дробной части", "transactions.import.amountFormat.auto": "Автоматически", "transactions.import.amountFormat.dot-decimal": "Точка (1234.56), запятая — тысячи", "transactions.import.amountFormat.comma-decimal": "Запятая (1234,56), точка — тысячи", "transactions.import.duplicateCheckLoading": "Проверяем, нет ли уже таких транзакций…", "transactions.import.duplicateCheckFailed": "Не удалось проверить, есть ли уже такие транзакции на счёте — при импорте возможны дубли.", "transactions.import.duplicatesFound": "Найдено похожих на уже существующие: {{count}}. По умолчанию они не импортируются.", "transactions.import.includeDuplicates": "Всё равно импортировать их", "transactions.allTypes": "Все типы", "transactions.expense": "Расходы", "transactions.income": "Доходы", "transactions.transfer": "Переводы", "transactions.allCategories": "Все категории", "transactions.allTags": "Все теги", "transactions.failedToLoad": "Не удалось загрузить транзакции.", "transactions.noneFound": "Транзакций не найдено.", "transactions.searchPlaceholder": "Поиск по описанию, продавцу или заметке…", "transactions.searchAcrossAllTime": "Поиск ведётся по всем периодам, а не только за выбранный месяц.", "transactions.searchNoneFound": "Ничего не найдено. Попробуйте другой запрос.", "transactions.jumpToMonth": "Перейти к этому месяцу", "transactions.confirmDelete": "Удалить транзакцию «{{description}}»?", "transactions.transferSuffix": "→ перевод", "transactions.sortDateDesc": "Сначала новые", "transactions.sortAmountDesc": "Сумма: сначала больше", "transactions.sortAmountAsc": "Сумма: сначала меньше", "transactions.form.editTitle": "Изменить транзакцию", "transactions.form.newTitle": "Новая транзакция", "transactions.form.typeLabel": "Тип", "transactions.form.typeExpense": "Расход", "transactions.form.typeIncome": "Доход", "transactions.form.typeTransfer": "Перевод", "transactions.form.amountLabel": "Сумма", "transactions.form.dateLabel": "Дата", "transactions.form.descriptionLabel": "Описание", "transactions.form.descriptionPlaceholder": "Продукты, зарплата, аренда…", "transactions.form.accountLabel": "Счёт", "transactions.form.selectAccount": "Выберите счёт", "transactions.form.transferAccountLabel": "Счёт назначения", "transactions.form.categoryLabel": "Категория", "transactions.form.noCategory": "Без категории", "transactions.form.merchantLabel": "Продавец (необязательно)", "transactions.form.notesLabel": "Заметка (необязательно)", "transactions.form.tagsLabel": "Теги (необязательно)", "transactions.form.tagsPlaceholder": "Начните вводить, чтобы найти или создать тег…", "transactions.form.removeTag": "Убрать тег «{{name}}»", "transactions.form.createTag": "Создать тег «{{name}}»", "transactions.viewNote": "Посмотреть заметку", "transactions.noteDialogTitle": "Заметка", "transactions.form.errorSelectAccount": "Выберите счёт", "transactions.form.errorSelectDestination": "Выберите счёт назначения для перевода", "transactions.form.errorSameAccount": "Счёт назначения должен отличаться от исходного счёта", "transactions.form.saveError": "Не удалось сохранить транзакцию. Проверьте данные и попробуйте снова.", "transactions.form.splitToggle": "Разбить на несколько категорий", "transactions.form.splitToggleOff": "Одна категория", "transactions.form.splitHint": "Одна покупка — несколько категорий, например часть чека — продукты, часть — бытовая химия.", "transactions.form.splitCategoryPlaceholder": "Выберите категорию", "transactions.form.splitNotePlaceholder": "Заметка (необязательно)", "transactions.form.splitAddRow": "Добавить категорию", "transactions.form.splitRemoveRow": "Убрать категорию", "transactions.form.splitRemainingLabel": "Осталось распределить: {{amount}}", "transactions.form.splitOverAllocatedLabel": "Превышение суммы на: {{amount}}", "transactions.form.splitFullyAllocatedLabel": "Вся сумма распределена", "transactions.form.errorSplitMinRows": "Нужно как минимум 2 категории для разбивки", "transactions.form.errorSplitIncomplete": "Выберите категорию и сумму для каждой строки разбивки", "transactions.form.errorSplitMismatch": "Сумма по категориям должна совпадать с суммой транзакции", "transactions.form.errorSplitNoBaseCategory": "Сначала выберите базовую категорию", "transactions.form.splitDirectOption": "— без конкретной подкатегории —", "transactions.form.splitNoChildren": "У этой категории пока нет подкатегорий — добавьте их на странице «Категории», чтобы разбивать покупки по ним.", "reports.directSpendLabel": "Без подкатегории", "reports.categoryBreakdownTotalLabel": "Итого", "reports.categoryLabel": "Категория", "reports.expenseGroup": "Расходы", "reports.incomeGroup": "Доходы", "reports.rangeAll": "Всё время", "reports.rangeThisYear": "Этот год", "reports.range5y": "5 лет", "reports.rangeCustom": "Диапазон лет", "reports.transactionsTitle": "Транзакции", "reports.defaultChartTitle": "Расходы по категории", "reports.averagePerMonth": "{{amount}}/мес в среднем", "reports.noDataForPeriod": "Нет данных за выбранный период.", "reports.categoryRankingTitle": "Топ категорий за период", "reports.noRankingData": "Нет расходов за выбранный период.", "settings.language": "Язык", "settings.languageRussian": "Русский", "settings.languageEnglish": "English", "settings.theme": "Тема оформления", "settings.themeLight": "Светлая", "settings.themeDark": "Тёмная", "settings.themeSystem": "Системная", "settings.currency": "Основная валюта", "settings.currencyHint": "Меняет только отображение сумм — уже введённые значения не пересчитываются и не конвертируются.", "settings.alertsTitle": "Проактивные предупреждения", "settings.alertsHint": "Через сколько месяцев подряд показывать предупреждение на Dashboard и «Капитал».", "settings.negativeCashFlowThresholdLabel": "Расходы превышают доходы, мес. подряд", "settings.netWorthDeclineThresholdLabel": "Капитал снижается, мес. подряд", "settings.riskyAllocationThresholdLabel": "Допустимая доля рискового капитала, %", "settings.idleCashThresholdAmountLabel": "Простаивающий кэш — сумма от", "settings.idleCashThresholdDaysLabel": "Простаивающий кэш — без движения, дней", "backup.title": "Резервное копирование", "backup.description": "Экспортируйте все свои данные (счета, категории, транзакции, активы) в один JSON-файл и храните его в надёжном месте. Позже этот файл можно загрузить обратно, чтобы полностью восстановить данные — например, после переустановки или переезда на другой сервер.", "backup.exportButton": "Экспортировать бэкап", "backup.exporting": "Экспорт…", "backup.importButton": "Восстановить из файла", "backup.importing": "Восстановление…", "backup.exportSuccess": "Бэкап скачан.", "backup.exportError": "Не удалось скачать бэкап.", "backup.importSuccess": "Данные восстановлены из бэкапа.", "backup.importError": "Не удалось восстановить бэкап.", "backup.invalidFile": "Файл повреждён или не является JSON-бэкапом Aurum.", "backup.confirmImport": "Восстановить данные из «{{filename}}»?\n\nЭто ПОЛНОСТЬЮ заменит все текущие счета, категории, транзакции и активы данными из файла. Отменить это действие нельзя — если хотите сохранить текущие данные, сначала сделайте экспорт.", "backup.footerWarning": "Восстановление полностью заменяет текущие данные содержимым файла — это нельзя отменить.", "settings.version": "Aurum v{{version}}", "insights.negativeCashFlow": "Расходы превышают доходы {{months}} мес. подряд (по последним завершённым месяцам)", "insights.netWorthDecline": "Капитал снижается {{months}} мес. подряд", "insights.budgetExceededOne": "Превышен бюджет в 1 категории в этом месяце", "insights.budgetExceeded": "Превышен бюджет в {{count}} категориях в этом месяце", "insights.riskyAllocationExceeded": "Рискованная часть капитала — {{percent}}%, превышает установленный лимит {{threshold}}%", "insights.idleCashOne": "На 1 счёте лежит крупная сумма без движения {{days}}+ дней — возможно, эти деньги стоит куда-то вложить", "insights.idleCash": "На {{count}} счетах лежит крупная сумма без движения {{days}}+ дней — возможно, эти деньги стоит куда-то вложить", "budget.empty": "Бюджетов пока нет. Задайте лимит для категории расходов, чтобы отслеживать её здесь.", "budget.remaining": "{{amount}} осталось", "budget.overBy": "Превышен на {{amount}}", "budget.confirmDelete": "Удалить бюджет для «{{name}}»?", "budget.form.newTitle": "Новый бюджет", "budget.form.editTitle": "Изменить бюджет", "budget.form.categoryLabel": "Категория", "budget.form.limitLabel": "Лимит в месяц", "budget.form.noCategoriesAvailable": "Все категории расходов уже добавлены.", "budget.form.saveError": "Не удалось сохранить бюджет. Проверьте данные и попробуйте снова.", "advice.empty": "Пока нет советов — недостаточно данных, или всё в порядке.", "advice.risingCategory": "Траты на {{category}} выросли на {{percent}}% по сравнению со средним за последние 3 месяца: {{current}} против {{average}}.", "advice.unbudgetedTopCategory": "На «{{category}}» уходит больше всего в этом месяце — {{amount}}. Задайте для неё бюджет на вкладке «Бюджет», чтобы отслеживать лимит.", "advice.savingsRateTrend": "Норма сбережений: {{rate}}% ({{diff}} п.п. к прошлому месяцу).", "goal.empty": "Целей пока нет. Добавьте цель и начните копить.", "goal.remaining": "{{amount}} осталось", "goal.remainingWithDate": "{{amount}} осталось · до {{date}}", "goal.reached": "Цель достигнута!", "goal.contributeLabel": "Добавить взнос", "goal.confirmDelete": "Удалить цель «{{name}}»?", "goal.form.newTitle": "Новая цель", "goal.form.editTitle": "Изменить цель", "goal.form.nameLabel": "Название", "goal.form.namePlaceholder": "Подушка безопасности, отпуск, новый ноутбук…", "goal.form.targetAmountLabel": "Целевая сумма", "goal.form.targetDateLabel": "Дата (необязательно)", "goal.form.saveError": "Не удалось сохранить цель. Проверьте данные и попробуйте снова.", "goal.contribution.title": "Взнос: {{name}}", "goal.contribution.amountLabel": "Сумма", "goal.contribution.amountPlaceholder": "100 для взноса, -50 для снятия", "goal.contribution.dateLabel": "Дата", "goal.contribution.hint": "Можно указать отрицательную сумму, если пришлось снять деньги с цели.", "goal.contribution.saveError": "Не удалось сохранить взнос. Проверьте данные и попробуйте снова.", "recurring.empty": "Регулярных платежей пока нет. Добавьте зарплату, аренду или подписку, чтобы не заводить их вручную каждый раз.", "recurring.postLabel": "Провести платёж", "recurring.confirmDelete": "Удалить регулярный платёж «{{name}}»?", "recurring.overdueDays": "Просрочено на {{days}} дн.", "recurring.dueToday": "Пора оплатить сегодня", "recurring.dueInDays": "Через {{days}} дн. · {{date}}", "recurring.frequency.weekly": "Еженедельно", "recurring.frequency.monthly": "Ежемесячно", "recurring.frequency.yearly": "Ежегодно", "recurring.form.newTitle": "Новый регулярный платёж", "recurring.form.editTitle": "Изменить регулярный платёж", "recurring.form.frequencyLabel": "Периодичность", "recurring.form.anchorDateLabel": "Дата начала", "recurring.form.saveError": "Не удалось сохранить регулярный платёж. Проверьте данные и попробуйте снова.", "account.empty": "Счетов пока нет. Добавьте первый — например, текущий счёт или наличные.", "account.showArchived": "Показывать архивные", "account.archivedBadge": "архив", "account.archiveLabel": "Архивировать", "account.unarchiveLabel": "Вернуть из архива", "account.confirmDelete": "Удалить счёт «{{name}}»?\n\nЭто безвозвратно удалит и ВСЕ транзакции по этому счёту. Если хотите сохранить историю, заархивируйте счёт вместо удаления.", "account.type.checking": "Текущий счёт", "account.type.debit_card": "Дебетовая карта", "account.type.savings": "Накопительный счёт", "account.type.credit_card": "Кредитная карта", "account.type.cash": "Наличные", "account.type.investment": "Инвестиционный счёт", "account.type.other": "Другое", "account.form.newTitle": "Новый счёт", "account.form.editTitle": "Изменить счёт", "account.form.nameLabel": "Название", "account.form.namePlaceholder": "Основной счёт, накопления, наличные…", "account.form.typeLabel": "Тип", "account.form.saveError": "Не удалось сохранить счёт. Проверьте данные и попробуйте снова.", "category.expenseSectionTitle": "Категории расходов", "category.incomeSectionTitle": "Категории доходов", "category.empty": "Категорий пока нет.", "category.defaultBadge": "стандартная", "category.defaultDeleteBlocked": "Стандартную категорию можно удалить только после того, как у неё не останется транзакций.", "category.confirmDelete": "Удалить категорию «{{name}}»?\n\nЕё транзакции останутся без категории.", "category.kind.expense": "Расход", "category.kind.income": "Доход", "category.form.newTitle": "Новая категория", "category.form.editTitle": "Изменить категорию", "category.form.nameLabel": "Название", "category.form.namePlaceholder": "Продукты, транспорт, подписки…", "category.form.kindLabel": "Тип", "category.form.parentLabel": "Родительская категория", "category.form.noParent": "— без родителя (верхний уровень) —", "category.form.hasChildrenHint": "У категории уже есть подкатегории — она не может сама стать подкатегорией.", "category.form.iconLabel": "Иконка", "category.form.colorLabel": "Цвет", "category.form.customColorLabel": "Свой цвет", "category.form.saveError": "Не удалось сохранить категорию. Проверьте данные и попробуйте снова.", "cashFlow.income": "Доход", "cashFlow.expense": "Расход", "cashFlow.net": "Баланс", "cashFlow.noData": "Нет данных за выбранный период.", // Default seeded categories (backend/app/db/seed.py) — translateCategoryName() // matches on the exact seeded English name and falls through unchanged // for anything else (a renamed default, or a custom category added via // the category-management UI). "category.housingUtilities": "Жильё и коммуналка", "category.groceries": "Продукты", "category.diningOut": "Кафе и рестораны", "category.transportation": "Транспорт", "category.healthFitness": "Здоровье и фитнес", "category.shopping": "Покупки", "category.entertainment": "Развлечения", "category.subscriptions": "Подписки", "category.salary": "Зарплата", "category.freelance": "Фриланс", "category.investments": "Инвестиции", "category.gifts": "Подарки", "category.businessIncome": "Доход от бизнеса", "category.rentalIncome": "Сдача недвижимости", "category.benefits": "Пособия и соцвыплаты", "category.itemSales": "Продажа личных вещей", "category.otherIncome": "Прочий доход", "months.jan": "Янв", "months.feb": "Фев", "months.mar": "Мар", "months.apr": "Апр", "months.may": "Май", "months.jun": "Июн", "months.jul": "Июл", "months.aug": "Авг", "months.sep": "Сен", "months.oct": "Окт", "months.nov": "Ноя", "months.dec": "Дек", }; const en: Record = { "nav.dashboard": "Dashboard", "nav.netWorth": "Net Worth", "nav.crypto": "Crypto", "nav.roi": "Returns", "nav.transactions": "Transactions", "nav.accounts": "Accounts", "nav.categories": "Categories", "nav.cashFlow": "Cash Flow", "nav.reports": "Reports", "nav.budget": "Budget", "nav.recurring": "Recurring", "nav.goals": "Goals", "nav.advice": "Advice", "nav.settings": "Settings", "nav.comingSoon": "soon", "sidebar.collapse": "Collapse", "sidebar.collapseMenu": "Collapse menu", "sidebar.expandMenu": "Expand menu", "sidebar.closeMenu": "Close menu", "topbar.openMenu": "Open menu", "common.loading": "Loading…", "common.expand": "Expand", "common.collapse": "Collapse", "common.back": "Back", "common.next": "Next", "common.clear": "Clear", "common.cancel": "Cancel", "common.save": "Save", "common.saving": "Saving…", "common.edit": "Edit", "common.delete": "Delete", "common.close": "Close", "common.add": "Add", "common.pageOf": "Page {{page}} of {{total}}", "common.totalCount": "{{count}} total", "common.perMonth": "/mo", "common.allShort": "All", "auth.subtitle": "Personal finance", "auth.usernameLabel": "Username", "auth.passwordLabel": "Password", "auth.rememberMe": "Remember me for 7 days", "auth.sessionOnlyHint": "You stay signed in until you close the browser. Staying signed in for longer needs HTTPS or localhost.", "auth.submitButton": "Sign in", "auth.submitting": "Signing in…", "auth.errorInvalidCredentials": "Incorrect username or password", "auth.errorUnreachable": "Couldn't reach the server. Check your connection and try again.", "auth.noAuthModalTitle": "Authentication not configured", "auth.noAuthModalBody": "This Aurum instance has no password protection — all data is accessible to anyone who can reach it. Set AURUM_BASIC_AUTH_USER and AURUM_BASIC_AUTH_PASSWORD in .env to enable login.", "auth.noAuthModalAccept": "I understand the risk and want to continue", "dashboard.errorLoading": "Failed to load dashboard data. Check that the backend is running.", "dashboard.statRealIncomeLabel": "Real income", "dashboard.statRealIncomeCaption": "paychecks, refunds", "dashboard.statSpentLabel": "Spent", "dashboard.statSpentCaption": "excludes transfers", "dashboard.statNetLabel": "Net", "dashboard.statNetCaption": "income − spend", "dashboard.statSavingsRateLabel": "Savings rate", "dashboard.statSavingsRateCaption": "share of income left after spending", "dashboard.spendingByCategoryTitle": "Spending by category", "dashboard.noExpensesThisMonth": "No expenses yet this month. Add your first transaction on the Transactions tab.", "dashboard.recentTransactionsTitle": "Recent transactions", "dashboard.allTransactionsLink": "All transactions", "dashboard.noTransactionsYet": "No transactions yet.", "netWorth.periodSuffix": "over period", "netWorth.noChartData": "No data for the chart yet.", "netWorth.assetsTitlePrefix": "Assets", "netWorth.myAssetsTitle": "My assets", "netWorth.confirmDeleteAsset": "Delete asset “{{name}}”?", "netWorth.capitalByType": "Capital by type", "netWorth.assetClass.investments": "Investments", "netWorth.assetClass.crypto": "Crypto", "netWorth.assetClass.real_estate": "Real estate", "netWorth.assetClass.vehicles": "Vehicles", "netWorth.assetClass.precious_metals": "Precious metals", "netWorth.assetClass.other": "Other", "netWorth.assetClass.cash": "Cash", "netWorth.capitalRole.income": "Income", "netWorth.capitalRole.neutral": "Neutral", "netWorth.capitalRole.drain": "Drain", "netWorth.capitalRoleHint.income": "brings in money", "netWorth.capitalRoleHint.neutral": "just sits there — no gain, no drain", "netWorth.capitalRoleHint.drain": "costs money every month", "netWorth.capitalRoleFormHint.income": "brings in money — rented out, etc.", "netWorth.capitalRoleFormHint.neutral": "just there / in use — electronics, furniture", "netWorth.capitalRoleFormHint.drain": "costs money every month — a car, etc.", "netWorth.riskAllocationTitle": "Capital by risk level", "netWorth.riskAllocationOfCapital": "of capital", "netWorth.riskLevel.low": "Low", "netWorth.riskLevel.medium": "Medium", "netWorth.riskLevel.high": "High", "netWorth.riskLevelFormHint.low": "minimal risk of loss — deposits, cash, government bonds", "netWorth.riskLevelFormHint.medium": "moderate value swings — real estate, diversified investments", "netWorth.riskLevelFormHint.high": "can lose significant value — crypto, individual stocks, startups", "netWorth.assetsTable.empty": "No assets yet. Add investments, crypto, real estate, or other property.", "netWorth.assetsTable.asOf": "as of {{date}}", "netWorth.assetsTable.annualRoi": "{{percent}}%/year", "crypto.addButton": "Add coin", "crypto.refreshButton": "Refresh prices", "crypto.refreshing": "Refreshing…", "crypto.lastSynced": "Updated {{time}}", "crypto.neverSynced": "Prices haven't loaded yet", "crypto.totalValueLabel": "Total in crypto", "crypto.empty": "No coins yet. Add your first one to start tracking.", "crypto.pendingPrice": "price not fetched yet", "crypto.confirmDelete": "Remove \"{{name}}\" from tracking?", "crypto.syncError.unreachable": "Could not reach CoinGecko — showing the last known prices.", "crypto.search.placeholder": "Find a coin (e.g. Bitcoin or BTC)…", "crypto.search.hint": "Start typing a coin's name or ticker.", "crypto.search.empty": "Nothing found.", "crypto.search.noApiKey": "CoinGecko API key not configured — set AURUM_COINGECKO_API_KEY in .env (free key at coingecko.com/en/api/pricing).", "crypto.form.quantityLabel": "Quantity", "crypto.form.pricePerUnitLabel": "Price per unit", "crypto.form.dateLabel": "Date", "crypto.form.noteLabel": "Note (optional)", "crypto.form.networkLabel": "Network (optional)", "crypto.form.networkPlaceholder": "e.g. Ethereum, Tron…", "crypto.form.addTitle": "Add {{name}}", "crypto.form.tradeTitle": "Trade: {{name}}", "crypto.form.editTradeTitle": "Edit trade: {{name}}", "crypto.form.tradeButton": "Buy / sell", "crypto.form.buy": "Buy", "crypto.form.sell": "Sell", "crypto.form.errorSellTooMuch": "You can't sell more than you currently hold.", "crypto.form.saveError": "Failed to save. Check the data and try again.", "crypto.table.name": "Name", "crypto.table.price": "Price", "crypto.table.change1h": "1h%", "crypto.table.change24h": "24h%", "crypto.table.change7d": "7d%", "crypto.table.holdings": "Holdings", "crypto.table.avgBuyPrice": "Avg. Price", "crypto.table.profitLoss": "Profit/Loss", "crypto.table.network": "Network", "crypto.table.addNetwork": "+ Network", "crypto.table.risk": "Risk", "crypto.table.actions": "Actions", "crypto.riskAllocationOfPortfolio": "of crypto portfolio", "crypto.history.title": "Trade history: {{name}}", "crypto.history.empty": "No trades yet.", "crypto.history.confirmDelete": "Delete this trade? Quantity and average price will be recalculated.", "crypto.history.viewButton": "View history", "crypto.holdingsTitle": "Holdings", "crypto.chart.show": "Show amount", "crypto.chart.hide": "Hide amount", "crypto.allocation.title": "Allocation by coin", "crypto.overview.history": "History", "crypto.overview.allocation": "Allocation", "crypto.overview.risk": "Risk levels", "crypto.overview.networks": "Networks", "crypto.networkAllocation.unset": "No network set", "crypto.allocation.other": "Other", "crypto.stats.allTimeProfit": "All-time profit", "crypto.stats.costBasis": "Total invested", "crypto.stats.costBasisHint": "actual amount spent buying", "crypto.stats.bestPerformer": "Best Performer", "crypto.stats.worstPerformer": "Worst Performer", "crypto.stats.changeLabel": "price change over {{range}}", "crypto.portfolio.all": "All portfolios", "crypto.portfolio.addLabel": "Add portfolio", "crypto.portfolio.archiveLabel": "Archive portfolio", "crypto.portfolio.unarchiveLabel": "Unarchive", "crypto.portfolio.confirmDelete": "Delete portfolio «{{name}}»?", "crypto.portfolio.deleteError": "Move or delete its coins before deleting a portfolio.", "crypto.portfolio.form.newTitle": "New portfolio", "crypto.portfolio.form.editTitle": "Portfolio", "crypto.portfolio.form.nameLabel": "Portfolio name", "crypto.portfolio.form.namePlaceholder": "e.g. \"Long-term investments\"", "crypto.portfolio.form.saveError": "Couldn't save. Check the details and try again.", "roi.calculator.title": "ROI Calculator", "roi.calculator.investmentLabel": "Investment amount", "roi.calculator.monthlyIncomeLabel": "Monthly income", "roi.calculator.annualRoiLabel": "Annual return", "roi.calculator.payback": "Pays back in {{years}} years", "roi.calculator.noPayback": "Doesn't pay back — income doesn't cover the investment", "roi.calculator.hint": "Enter the price and expected monthly income to see the annual return, payback period, and a multi-year projection.", "roi.calculator.projectionTitle": "Compound interest projection", "roi.calculator.projectionHint": "If the {{percent}}% annual return holds and all income is reinvested back into the same investment — based only on the numbers you entered, not your tracked assets.", "roi.calculator.chartCompoundLabel": "With compounding", "roi.calculator.chartSimpleLabel": "Without reinvesting", "roi.calculator.tableTitle": "Year-by-year comparison", "roi.calculator.colCompound": "With compounding", "roi.calculator.colSimple": "Without reinvesting", "roi.calculator.colDifference": "Difference", "roi.calculator.yearOne": "In 1 year", "roi.calculator.yearN": "In {{years}} years", "netWorth.form.editTitle": "Edit asset", "netWorth.form.newTitle": "New asset", "netWorth.form.nameLabel": "Name", "netWorth.form.namePlaceholder": "Brokerage account, BTC, apartment…", "netWorth.form.classLabel": "Category", "netWorth.form.currentValueLabel": "Current value", "netWorth.form.valueLabel": "Value", "netWorth.form.dateLabel": "Valuation date", "netWorth.form.roleLabel": "Capital type", "netWorth.form.riskLevelLabel": "Risk level", "netWorth.form.cashFlowLabel": "Monthly cash flow (optional)", "netWorth.form.cashFlowPlaceholder": "800 for income, -150 for expense", "netWorth.form.cashFlowHint": "Approximate — the real amount varies month to month, this isn't strict accounting.", "netWorth.form.notesLabel": "Notes (optional)", "netWorth.form.saveError": "Failed to save the asset. Check the data and try again.", "transactions.addButton": "Add", "transactions.importButton": "Import CSV", "transactions.import.title": "Import transactions from CSV", "transactions.import.accountLabel": "Account to import into", "transactions.import.accountHint": "Every transaction from the file will be added to this account.", "transactions.import.uploadTitle": "File", "transactions.import.uploadHint": "A transaction export from your bank or another app, in CSV format (with column headers in the first row).", "transactions.import.chooseFile": "Choose file", "transactions.import.pickAccountFirst": "Pick an account first.", "transactions.import.errorEmptyFile": "The file has no data rows (besides the header).", "transactions.import.mapTitle": "Map the columns", "transactions.import.dateColumnLabel": "Date column", "transactions.import.dateFormatLabel": "Date format", "transactions.import.selectColumn": "Select a column…", "transactions.import.amountColumnLabel": "Amount column", "transactions.import.amountHint": "Negative amounts are expenses, positive are income.", "transactions.import.descriptionColumnLabel": "Description column", "transactions.import.notMapped": "— not mapped —", "transactions.import.categoryHint": "If the column's text exactly matches an existing category name, it's filled in automatically.", "transactions.import.startOver": "Start over", "transactions.import.previewButton": "Show preview", "transactions.import.previewTitle": "Import preview", "transactions.import.summary": "Ready to import: {{valid}}. Skipped due to errors: {{skipped}}.", "transactions.import.andMore": "…and {{count}} more", "transactions.import.skippedRow": "Row {{row}}: {{reason}}", "transactions.import.importButton": "Import {{count}}", "transactions.import.importSuccess": "Done — {{count}} transactions added.", "transactions.import.goToTransactions": "Go to transactions", "transactions.import.importError": "Import failed. Check the data and try again.", "transactions.import.errorBadDate": "couldn't parse the date \"{{value}}\"", "transactions.import.errorBadAmount": "couldn't parse the amount \"{{value}}\"", "transactions.import.errorNoDescription": "no description", "transactions.import.encodingLabel": "File encoding", "transactions.import.encodingHint": "Auto-detected; if you see garbled characters instead of letters, try picking one manually.", "transactions.import.sampleValueLabel": "Example from file: {{value}}", "transactions.import.amountFormatLabel": "Decimal separator", "transactions.import.amountFormat.auto": "Automatic", "transactions.import.amountFormat.dot-decimal": "Dot (1234.56), comma is thousands", "transactions.import.amountFormat.comma-decimal": "Comma (1234,56), dot is thousands", "transactions.import.duplicateCheckLoading": "Checking for transactions already on this account…", "transactions.import.duplicateCheckFailed": "Couldn't check what's already on this account — importing may create duplicates.", "transactions.import.duplicatesFound": "Found {{count}} that look like duplicates of existing transactions. They're left out by default.", "transactions.import.includeDuplicates": "Import them anyway", "transactions.allTypes": "All types", "transactions.expense": "Expenses", "transactions.income": "Income", "transactions.transfer": "Transfers", "transactions.allCategories": "All categories", "transactions.allTags": "All tags", "transactions.failedToLoad": "Failed to load transactions.", "transactions.noneFound": "No transactions found.", "transactions.searchPlaceholder": "Search by description, merchant, or note…", "transactions.searchAcrossAllTime": "Searching across every period, not just the selected month.", "transactions.searchNoneFound": "Nothing found. Try a different search.", "transactions.jumpToMonth": "Jump to this month", "transactions.confirmDelete": "Delete transaction “{{description}}”?", "transactions.transferSuffix": "→ transfer", "transactions.sortDateDesc": "Newest first", "transactions.sortAmountDesc": "Amount: high to low", "transactions.sortAmountAsc": "Amount: low to high", "transactions.form.editTitle": "Edit transaction", "transactions.form.newTitle": "New transaction", "transactions.form.typeLabel": "Type", "transactions.form.typeExpense": "Expense", "transactions.form.typeIncome": "Income", "transactions.form.typeTransfer": "Transfer", "transactions.form.amountLabel": "Amount", "transactions.form.dateLabel": "Date", "transactions.form.descriptionLabel": "Description", "transactions.form.descriptionPlaceholder": "Groceries, salary, rent…", "transactions.form.accountLabel": "Account", "transactions.form.selectAccount": "Select account", "transactions.form.transferAccountLabel": "Destination account", "transactions.form.categoryLabel": "Category", "transactions.form.noCategory": "No category", "transactions.form.merchantLabel": "Merchant (optional)", "transactions.form.notesLabel": "Note (optional)", "transactions.form.tagsLabel": "Tags (optional)", "transactions.form.tagsPlaceholder": "Type to find or create a tag…", "transactions.form.removeTag": "Remove tag \"{{name}}\"", "transactions.form.createTag": "Create tag \"{{name}}\"", "transactions.viewNote": "View note", "transactions.noteDialogTitle": "Note", "transactions.form.errorSelectAccount": "Select an account", "transactions.form.errorSelectDestination": "Select a destination account for the transfer", "transactions.form.errorSameAccount": "The destination account must be different from the source account", "transactions.form.saveError": "Failed to save the transaction. Check the data and try again.", "transactions.form.splitToggle": "Split across multiple categories", "transactions.form.splitToggleOff": "Single category", "transactions.form.splitHint": "One purchase, several categories — e.g. part of a receipt is groceries, part is household chemicals.", "transactions.form.splitCategoryPlaceholder": "Select a category", "transactions.form.splitNotePlaceholder": "Note (optional)", "transactions.form.splitAddRow": "Add category", "transactions.form.splitRemoveRow": "Remove category", "transactions.form.splitRemainingLabel": "Remaining to allocate: {{amount}}", "transactions.form.splitOverAllocatedLabel": "Over-allocated by: {{amount}}", "transactions.form.splitFullyAllocatedLabel": "Fully allocated", "transactions.form.errorSplitMinRows": "Splitting needs at least 2 categories", "transactions.form.errorSplitIncomplete": "Pick a category and an amount for every split row", "transactions.form.errorSplitMismatch": "Split amounts must add up to the transaction amount", "transactions.form.errorSplitNoBaseCategory": "Pick a base category first", "transactions.form.splitDirectOption": "— no specific subcategory —", "transactions.form.splitNoChildren": "This category has no subcategories yet — add some on the Categories page to split purchases across them.", "reports.directSpendLabel": "No subcategory", "reports.categoryBreakdownTotalLabel": "Total", "reports.categoryLabel": "Category", "reports.expenseGroup": "Expenses", "reports.incomeGroup": "Income", "reports.rangeAll": "All time", "reports.rangeThisYear": "This year", "reports.range5y": "5 years", "reports.rangeCustom": "Year range", "reports.transactionsTitle": "Transactions", "reports.defaultChartTitle": "Spending by category", "reports.averagePerMonth": "{{amount}}/mo average", "reports.noDataForPeriod": "No data for the selected period.", "reports.categoryRankingTitle": "Top categories for period", "reports.noRankingData": "No expenses for the selected period.", "settings.language": "Language", "settings.languageRussian": "Русский", "settings.languageEnglish": "English", "settings.theme": "Theme", "settings.themeLight": "Light", "settings.themeDark": "Dark", "settings.themeSystem": "System", "settings.currency": "Primary currency", "settings.currencyHint": "Only changes how amounts are displayed — values you've already entered aren't recalculated or converted.", "settings.alertsTitle": "Proactive alerts", "settings.alertsHint": "How many consecutive months before showing a warning on Dashboard and Net Worth.", "settings.negativeCashFlowThresholdLabel": "Expenses exceed income, consecutive months", "settings.netWorthDeclineThresholdLabel": "Net worth declining, consecutive months", "settings.riskyAllocationThresholdLabel": "Max % of capital allowed at risk", "settings.idleCashThresholdAmountLabel": "Idle cash — balance at or above", "settings.idleCashThresholdDaysLabel": "Idle cash — no activity for, days", "backup.title": "Backup & Restore", "backup.description": "Export all your data (accounts, categories, transactions, assets) into a single JSON file and keep it somewhere safe. Later, you can load that file back in to fully restore your data — for example, after a reinstall or moving to a different server.", "backup.exportButton": "Export backup", "backup.exporting": "Exporting…", "backup.importButton": "Restore from file", "backup.importing": "Restoring…", "backup.exportSuccess": "Backup downloaded.", "backup.exportError": "Failed to download the backup.", "backup.importSuccess": "Data restored from backup.", "backup.importError": "Failed to restore the backup.", "backup.invalidFile": "The file is corrupted or is not an Aurum JSON backup.", "backup.confirmImport": "Restore data from “{{filename}}”?\n\nThis will COMPLETELY replace all current accounts, categories, transactions, and assets with the data from the file. This action cannot be undone — if you want to keep your current data, export it first.", "backup.footerWarning": "Restoring completely replaces your current data with the file's contents — this cannot be undone.", "settings.version": "Aurum v{{version}}", "insights.negativeCashFlow": "Expenses have exceeded income for {{months}} months in a row (based on the last completed months)", "insights.netWorthDecline": "Net worth has been declining for {{months}} months in a row", "insights.budgetExceededOne": "Budget exceeded in 1 category this month", "insights.budgetExceeded": "Budget exceeded in {{count}} categories this month", "insights.riskyAllocationExceeded": "Risky share of capital is {{percent}}%, above your {{threshold}}% limit", "insights.idleCashOne": "1 account is holding a large balance with no activity for {{days}}+ days — that money might be worth putting to work", "insights.idleCash": "{{count}} accounts are holding a large balance with no activity for {{days}}+ days — that money might be worth putting to work", "budget.empty": "No budgets yet. Set a limit for an expense category to start tracking it here.", "budget.remaining": "{{amount}} left", "budget.overBy": "Over by {{amount}}", "budget.confirmDelete": "Delete the budget for “{{name}}”?", "budget.form.newTitle": "New budget", "budget.form.editTitle": "Edit budget", "budget.form.categoryLabel": "Category", "budget.form.limitLabel": "Monthly limit", "budget.form.noCategoriesAvailable": "Every expense category already has a budget.", "budget.form.saveError": "Failed to save the budget. Check the data and try again.", "advice.empty": "No advice right now — not enough data, or everything looks fine.", "advice.risingCategory": "Spending on {{category}} is up {{percent}}% vs. the average of the last 3 months: {{current}} vs {{average}}.", "advice.unbudgetedTopCategory": "\"{{category}}\" is your biggest expense this month — {{amount}}. Set a budget for it on the Budget tab to track a limit.", "advice.savingsRateTrend": "Savings rate: {{rate}}% ({{diff}} pts vs last month).", "goal.empty": "No goals yet. Add one and start saving.", "goal.remaining": "{{amount}} left", "goal.remainingWithDate": "{{amount}} left · by {{date}}", "goal.reached": "Goal reached!", "goal.contributeLabel": "Add contribution", "goal.confirmDelete": "Delete the goal “{{name}}”?", "goal.form.newTitle": "New goal", "goal.form.editTitle": "Edit goal", "goal.form.nameLabel": "Name", "goal.form.namePlaceholder": "Emergency fund, vacation, new laptop…", "goal.form.targetAmountLabel": "Target amount", "goal.form.targetDateLabel": "Target date (optional)", "goal.form.saveError": "Failed to save the goal. Check the data and try again.", "goal.contribution.title": "Contribution: {{name}}", "goal.contribution.amountLabel": "Amount", "goal.contribution.amountPlaceholder": "100 to deposit, -50 to withdraw", "goal.contribution.dateLabel": "Date", "goal.contribution.hint": "You can enter a negative amount if you had to withdraw from the goal.", "goal.contribution.saveError": "Failed to save the contribution. Check the data and try again.", "recurring.empty": "No recurring transactions yet. Add your salary, rent, or a subscription so you don't have to enter it manually every time.", "recurring.postLabel": "Post now", "recurring.confirmDelete": "Delete the recurring transaction “{{name}}”?", "recurring.overdueDays": "{{days}} days overdue", "recurring.dueToday": "Due today", "recurring.dueInDays": "In {{days}} days · {{date}}", "recurring.frequency.weekly": "Weekly", "recurring.frequency.monthly": "Monthly", "recurring.frequency.yearly": "Yearly", "recurring.form.newTitle": "New recurring transaction", "recurring.form.editTitle": "Edit recurring transaction", "recurring.form.frequencyLabel": "Frequency", "recurring.form.anchorDateLabel": "Start date", "recurring.form.saveError": "Failed to save the recurring transaction. Check the data and try again.", "account.empty": "No accounts yet. Add your first one — a checking account or cash, for example.", "account.showArchived": "Show archived", "account.archivedBadge": "archived", "account.archiveLabel": "Archive", "account.unarchiveLabel": "Unarchive", "account.confirmDelete": "Delete the account “{{name}}”?\n\nThis will permanently delete ALL transactions on this account too. If you want to keep the history, archive the account instead of deleting it.", "account.type.checking": "Checking", "account.type.debit_card": "Debit card", "account.type.savings": "Savings", "account.type.credit_card": "Credit card", "account.type.cash": "Cash", "account.type.investment": "Investment", "account.type.other": "Other", "account.form.newTitle": "New account", "account.form.editTitle": "Edit account", "account.form.nameLabel": "Name", "account.form.namePlaceholder": "Main account, savings, cash…", "account.form.typeLabel": "Type", "account.form.saveError": "Failed to save the account. Check the data and try again.", "category.expenseSectionTitle": "Expense categories", "category.incomeSectionTitle": "Income categories", "category.empty": "No categories yet.", "category.defaultBadge": "default", "category.defaultDeleteBlocked": "A default category can only be deleted once it has no transactions left.", "category.confirmDelete": "Delete the category “{{name}}”?\n\nIts transactions will be left uncategorized.", "category.kind.expense": "Expense", "category.kind.income": "Income", "category.form.newTitle": "New category", "category.form.editTitle": "Edit category", "category.form.nameLabel": "Name", "category.form.namePlaceholder": "Groceries, transport, subscriptions…", "category.form.kindLabel": "Type", "category.form.parentLabel": "Parent category", "category.form.noParent": "— no parent (top level) —", "category.form.hasChildrenHint": "This category already has subcategories — it can't become a subcategory itself.", "category.form.iconLabel": "Icon", "category.form.colorLabel": "Color", "category.form.customColorLabel": "Custom color", "category.form.saveError": "Failed to save the category. Check the data and try again.", "cashFlow.income": "Income", "cashFlow.expense": "Expense", "cashFlow.net": "Net", "cashFlow.noData": "No data for the selected period.", "category.housingUtilities": "Housing & Utilities", "category.groceries": "Groceries", "category.diningOut": "Dining Out", "category.transportation": "Transportation", "category.healthFitness": "Health & Fitness", "category.shopping": "Shopping", "category.entertainment": "Entertainment", "category.subscriptions": "Subscriptions", "category.salary": "Salary", "category.freelance": "Freelance", "category.investments": "Investments", "category.gifts": "Gifts", "category.businessIncome": "Business Income", "category.rentalIncome": "Rental Income", "category.benefits": "Benefits", "category.itemSales": "Item Sales", "category.otherIncome": "Other Income", "months.jan": "Jan", "months.feb": "Feb", "months.mar": "Mar", "months.apr": "Apr", "months.may": "May", "months.jun": "Jun", "months.jul": "Jul", "months.aug": "Aug", "months.sep": "Sep", "months.oct": "Oct", "months.nov": "Nov", "months.dec": "Dec", }; const translations: Record> = { ru, en }; export type TranslationKey = keyof typeof ru; function readInitialLanguage(): Language { const stored = localStorage.getItem(STORAGE_KEY); if (stored === "ru" || stored === "en") return stored; return "ru"; } let currentLanguage: Language = readInitialLanguage(); // The primary display currency (ISO 4217 code, e.g. "USD"/"UAH"/"EUR"). Unlike // language this isn't client-only — it's persisted server-side (GET/PATCH // /api/settings) since it also drives what new accounts/assets are created // with. App.tsx syncs this from the server once on boot via setCurrency(); // until that resolves, formatCurrency() falls back to this default. let currentCurrency = "USD"; const listeners = new Set<() => void>(); export function getLanguage(): Language { return currentLanguage; } export function setLanguage(language: Language): void { if (language === currentLanguage) return; currentLanguage = language; localStorage.setItem(STORAGE_KEY, language); listeners.forEach((listener) => listener()); } export function getCurrency(): string { return currentCurrency; } /** Updates the reactive local mirror only — call this after the server * fetch/update succeeds, not in place of it. */ export function setCurrency(currency: string): void { if (currency === currentCurrency) return; currentCurrency = currency; listeners.forEach((listener) => listener()); } function subscribe(listener: () => void): () => void { listeners.add(listener); return () => listeners.delete(listener); } function interpolate(template: string, params?: Record): string { if (!params) return template; return template.replace(/\{\{(\w+)\}\}/g, (match, key) => { const value = params[key]; return value === undefined ? match : String(value); }); } /** Usable outside React (e.g. api/ modules) — reads the current language at call time. */ export function t(key: TranslationKey, params?: Record): string { return interpolate(translations[currentLanguage][key], params); } /** Subscribes the component to language AND currency changes so it re-renders * on either toggle — most components that call formatCurrency() already call * this hook for text, so that alone is enough to pick up a fresh currency on * their next render even without reading `currency` from the return value. */ export function useTranslation() { const language = useSyncExternalStore(subscribe, () => currentLanguage); const currency = useSyncExternalStore(subscribe, () => currentCurrency); return { t, language, setLanguage, currency, setCurrency }; }