import { useSyncExternalStore } from "react"; export type Language = "ru" | "en"; const STORAGE_KEY = "aurum:language"; const ru = { "nav.dashboard": "Дашборд", "nav.netWorth": "Капитал", "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.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": "Всё", "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}}% годовых", "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.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": "Не удалось сохранить транзакцию. Проверьте данные и попробуйте снова.", "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": "Восстановление полностью заменяет текущие данные содержимым файла — это нельзя отменить.", "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.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.defaultCannotDelete": "Стандартные категории нельзя удалить", "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.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.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", "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", "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.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.", "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.", "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.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.defaultCannotDelete": "Default categories cannot be deleted", "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 }; }