feat: Ukrainian translation, UAH currency, shopping category i18n (closes #52)
- Add Ukrainian (uk) locale to SUPPORTED_LOCALES and locale picker - Add public/locales/uk.json (622 keys, full Ukrainian translation) - Add UAH (Ukrainian Hryvnia) to SUPPORTED_CURRENCIES and VALID_CURRENCIES - Add CATEGORY_I18N map and catLabel() in settings.js to translate default shopping category names in the settings panel; rename and delete dialogs now also use the translated name instead of the raw German DB string - Align server VALID_CURRENCIES with frontend: add missing AED, BRL, INR, SAR Co-Authored-By: baragoon <baragoon@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.20.9] - 2026-04-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Ukrainian (uk) translation (closes #52)
|
||||||
|
- Ukrainian Hryvnia (UAH) currency option in budget settings
|
||||||
|
- Shopping list category names are now translated in the settings panel; rename and delete dialogs also use the translated name
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Server-side `VALID_CURRENCIES` now matches the frontend list — `AED`, `BRL`, `INR`, and `SAR` were accepted by the UI but rejected by the API
|
||||||
|
|
||||||
## [0.20.8] - 2026-04-18
|
## [0.20.8] - 2026-04-18
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "oikos",
|
"name": "oikos",
|
||||||
"version": "0.20.8",
|
"version": "0.20.9",
|
||||||
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
"description": "Self-hosted family planner - calendar, tasks, shopping, meal planning, budget and more. Private, open-source, no subscription.",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const LOCALE_LABELS = {
|
|||||||
ar: 'العربية',
|
ar: 'العربية',
|
||||||
hi: 'हिन्दी',
|
hi: 'हिन्दी',
|
||||||
pt: 'Português',
|
pt: 'Português',
|
||||||
|
uk: 'Українська',
|
||||||
};
|
};
|
||||||
|
|
||||||
class OikosLocalePicker extends HTMLElement {
|
class OikosLocalePicker extends HTMLElement {
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
* Dependencies: none (vanilla JS, Fetch API, Intl API)
|
* Dependencies: none (vanilla JS, Fetch API, Intl API)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const SUPPORTED_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'sv', 'el', 'ru', 'tr', 'zh', 'ja', 'ar', 'hi', 'pt'];
|
const SUPPORTED_LOCALES = ['de', 'en', 'es', 'fr', 'it', 'sv', 'el', 'ru', 'tr', 'zh', 'ja', 'ar', 'hi', 'pt', 'uk'];
|
||||||
const DEFAULT_LOCALE = 'de';
|
const DEFAULT_LOCALE = 'de';
|
||||||
const STORAGE_KEY = 'oikos-locale';
|
const STORAGE_KEY = 'oikos-locale';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,623 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Зберегти",
|
||||||
|
"cancel": "Скасувати",
|
||||||
|
"delete": "Видалити",
|
||||||
|
"edit": "Редагувати",
|
||||||
|
"close": "Закрити",
|
||||||
|
"create": "Створити",
|
||||||
|
"add": "Додати",
|
||||||
|
"back": "Назад",
|
||||||
|
"next": "Далі",
|
||||||
|
"loading": "Завантаження…",
|
||||||
|
"saving": "Збереження…",
|
||||||
|
"required": "Це поле є обов'язковим.",
|
||||||
|
"error": "Помилка",
|
||||||
|
"allFieldsRequired": "Будь ласка, заповніть усі поля.",
|
||||||
|
"today": "Сьогодні",
|
||||||
|
"tomorrow": "Завтра",
|
||||||
|
"skipToContent": "Перейти до вмісту",
|
||||||
|
"reload": "Оновити",
|
||||||
|
"errorOccurred": "Щось пішло не так.",
|
||||||
|
"unexpectedError": "Сталася непередбачена помилка.",
|
||||||
|
"errorGeneric": "Сталася помилка.",
|
||||||
|
"updateAvailable": "Доступне оновлення — перезавантажте сторінку, щоб отримати останню версію.",
|
||||||
|
"titleRequired": "Заголовок є обов'язковим",
|
||||||
|
"nameRequired": "Ім'я є обов'язковим",
|
||||||
|
"contentRequired": "Вміст є обов'язковим",
|
||||||
|
"all": "Усі",
|
||||||
|
"unknownError": "Невідома помилка",
|
||||||
|
"confirm": "Підтвердити",
|
||||||
|
"undo": "Скасувати"
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"dashboard": "Огляд",
|
||||||
|
"tasks": "Завдання",
|
||||||
|
"calendar": "Календар",
|
||||||
|
"meals": "Харчування",
|
||||||
|
"shopping": "Покупки",
|
||||||
|
"notes": "Нотатки",
|
||||||
|
"contacts": "Контакти",
|
||||||
|
"budget": "Бюджет",
|
||||||
|
"settings": "Налаштування",
|
||||||
|
"main": "Головна навігація",
|
||||||
|
"navigation": "Навігація",
|
||||||
|
"quickActions": "Швидкі дії"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Огляд",
|
||||||
|
"greetingMorning": "Доброго ранку, {{name}}",
|
||||||
|
"greetingDay": "Доброго дня, {{name}}",
|
||||||
|
"greetingEvening": "Доброго вечора, {{name}}",
|
||||||
|
"allDone": "Усе зроблено",
|
||||||
|
"noEvents": "Немає подій",
|
||||||
|
"noPinnedNotes": "Немає закріплених нотаток",
|
||||||
|
"todayMeals": "Страви на сьогодні",
|
||||||
|
"allLink": "Усі",
|
||||||
|
"weekLink": "Тиждень",
|
||||||
|
"urgentTasksChip": "{{count}} термінове завдання",
|
||||||
|
"urgentTasksChipPlural": "{{count}} термінових завдань",
|
||||||
|
"eventsChip": "{{count}} подія сьогодні",
|
||||||
|
"eventsChipPlural": "{{count}} подій сьогодні",
|
||||||
|
"todayMealChip": "Сьогодні: {{title}}",
|
||||||
|
"loadError": "Огляд не вдалося повністю завантажити.",
|
||||||
|
"weatherRefresh": "Оновити погоду",
|
||||||
|
"weatherRefreshTitle": "Оновити",
|
||||||
|
"weatherUpdated": "Погоду оновлено",
|
||||||
|
"weatherFeelsLike": "Відчувається як {{temp}}° · {{humidity}}% · Вітер {{wind}} км/год",
|
||||||
|
"fabTaskLabel": "Додати завдання",
|
||||||
|
"fabCalendarLabel": "Додати подію",
|
||||||
|
"fabShoppingLabel": "Додати покупку",
|
||||||
|
"fabNoteLabel": "Додати нотатку",
|
||||||
|
"fabTask": "Завдання",
|
||||||
|
"fabCalendar": "Подія",
|
||||||
|
"fabShopping": "Покупка",
|
||||||
|
"fabNote": "Нотатка",
|
||||||
|
"overdue": "Прострочено",
|
||||||
|
"dueSoon": "Сьогодні",
|
||||||
|
"dueTomorrow": "Завтра",
|
||||||
|
"allDay": "Весь день",
|
||||||
|
"shoppingMore": "+{{count}} ще",
|
||||||
|
"weather": "Погода",
|
||||||
|
"customize": "Налаштувати",
|
||||||
|
"customizeTitle": "Налаштувати віджети",
|
||||||
|
"customizeReset": "Скинути",
|
||||||
|
"customizeSaved": "Огляд збережено",
|
||||||
|
"customizeMoveUp": "Перемістити вгору",
|
||||||
|
"customizeMoveDown": "Перемістити вниз"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"title": "Завдання",
|
||||||
|
"newTask": "Нове завдання",
|
||||||
|
"editTask": "Редагувати завдання",
|
||||||
|
"emptyTitle": "Завдань немає — все зроблено?",
|
||||||
|
"emptyDescription": "Створіть нові завдання кнопкою +.",
|
||||||
|
"titleLabel": "Заголовок *",
|
||||||
|
"titlePlaceholder": "Що потрібно зробити?",
|
||||||
|
"descriptionLabel": "Нотатка",
|
||||||
|
"descriptionPlaceholder": "Необов'язкові деталі…",
|
||||||
|
"priorityLabel": "Пріоритет",
|
||||||
|
"categoryLabel": "Категорія",
|
||||||
|
"dueDateLabel": "Термін виконання",
|
||||||
|
"dueTimeLabel": "Час",
|
||||||
|
"assignedLabel": "Призначено",
|
||||||
|
"assignedNobody": "- Нікому -",
|
||||||
|
"statusLabel": "Статус",
|
||||||
|
"priorityUrgent": "Терміново",
|
||||||
|
"priorityHigh": "Високий",
|
||||||
|
"priorityMedium": "Середній",
|
||||||
|
"priorityLow": "Низький",
|
||||||
|
"priorityNone": "Без пріоритету",
|
||||||
|
"statusOpen": "Відкрито",
|
||||||
|
"statusInProgress": "В процесі",
|
||||||
|
"statusDone": "Виконано",
|
||||||
|
"categoryHousehold": "Побут",
|
||||||
|
"categorySchool": "Навчання",
|
||||||
|
"categoryShopping": "Покупки",
|
||||||
|
"categoryRepair": "Ремонт",
|
||||||
|
"categoryHealth": "Здоров'я",
|
||||||
|
"categoryFinance": "Фінанси",
|
||||||
|
"categoryLeisure": "Дозвілля",
|
||||||
|
"categoryMisc": "Різне",
|
||||||
|
"overdue": "Прострочено",
|
||||||
|
"overdueDay": "Прострочено на {{count}} д.",
|
||||||
|
"dueToday": "Сьогодні",
|
||||||
|
"dueTomorrow": "Завтра",
|
||||||
|
"groupOverdue": "Прострочено",
|
||||||
|
"groupToday": "Сьогодні",
|
||||||
|
"groupThisWeek": "Цього тижня",
|
||||||
|
"groupNextWeek": "Наступного тижня",
|
||||||
|
"groupLater": "Пізніше",
|
||||||
|
"groupNoDate": "Без дати",
|
||||||
|
"markDone": "Позначити {{title}} як виконане",
|
||||||
|
"editButton": "Редагувати завдання",
|
||||||
|
"swipeOpen": "Відкрити знову",
|
||||||
|
"swipeDone": "Виконано",
|
||||||
|
"swipeEdit": "Редагувати",
|
||||||
|
"subtaskAdd": "+ Додати підзавдання",
|
||||||
|
"subtaskToggle": "Показати підзавдання",
|
||||||
|
"subtaskMarkDone": "Позначити {{title}} як виконане",
|
||||||
|
"deleteConfirm": "Видалити завдання та всі підзавдання?",
|
||||||
|
"savedToast": "Завдання збережено.",
|
||||||
|
"createdToast": "Завдання створено.",
|
||||||
|
"deletedToast": "Завдання видалено.",
|
||||||
|
"loadError": "Не вдалося завантажити завдання.",
|
||||||
|
"subtaskPrompt": "Підзавдання:",
|
||||||
|
"kanbanOpen": "Відкрито",
|
||||||
|
"kanbanInProgress": "В процесі",
|
||||||
|
"kanbanDone": "Виконано",
|
||||||
|
"kanbanMoveToInProgress": "Позначити як «в процесі»",
|
||||||
|
"kanbanMoveToDone": "Позначити як виконане",
|
||||||
|
"kanbanMoveToOpen": "Відкрити знову",
|
||||||
|
"recurring": "Повторюване",
|
||||||
|
"listView": "Список",
|
||||||
|
"kanbanView": "Канбан"
|
||||||
|
},
|
||||||
|
"shopping": {
|
||||||
|
"title": "Покупки",
|
||||||
|
"noLists": "Немає списків",
|
||||||
|
"noListsDescription": "Створіть список кнопкою +.",
|
||||||
|
"emptyList": "Список порожній",
|
||||||
|
"emptyListDescription": "Додайте товари через поле вводу вище.",
|
||||||
|
"newListPrompt": "Назва нового списку:",
|
||||||
|
"newListButton": "Створити новий список",
|
||||||
|
"renameListPrompt": "Нова назва списку:",
|
||||||
|
"deleteListConfirm": "Видалити список «{{name}}» та всі товари?",
|
||||||
|
"deletedListToast": "Список видалено.",
|
||||||
|
"itemDeletedToast": "«{{name}}» видалено.",
|
||||||
|
"itemsRemovedToast": "{{count}} товарів видалено.",
|
||||||
|
"clearChecked": "Видалити відмічені ({{count}})",
|
||||||
|
"itemNamePlaceholder": "Додати товар…",
|
||||||
|
"itemQtyPlaceholder": "Кількість",
|
||||||
|
"itemNameLabel": "Назва товару",
|
||||||
|
"itemQtyLabel": "Кількість",
|
||||||
|
"categoryLabel": "Категорія",
|
||||||
|
"addItemLabel": "Додати товар",
|
||||||
|
"renameListLabel": "Перейменувати список",
|
||||||
|
"deleteListLabel": "Видалити список",
|
||||||
|
"swipeBack": "Скасувати",
|
||||||
|
"swipeCheck": "Відмітити",
|
||||||
|
"swipeDelete": "Видалити",
|
||||||
|
"markDoneLabel": "Відмітити {{name}}",
|
||||||
|
"markUndoneLabel": "Зняти відмітку з {{name}}",
|
||||||
|
"deleteItemLabel": "Видалити {{name}}",
|
||||||
|
"listsLoadError": "Не вдалося завантажити списки.",
|
||||||
|
"itemsLoadError": "Не вдалося завантажити товари.",
|
||||||
|
"catFruitVeg": "Фрукти та овочі",
|
||||||
|
"catBakery": "Випічка",
|
||||||
|
"catDairy": "Молочні продукти",
|
||||||
|
"catMeatFish": "М'ясо та риба",
|
||||||
|
"catFrozen": "Заморожені продукти",
|
||||||
|
"catDrinks": "Напої",
|
||||||
|
"catHousehold": "Господарські товари",
|
||||||
|
"catDrugstore": "Аптека",
|
||||||
|
"catMisc": "Різне"
|
||||||
|
},
|
||||||
|
"meals": {
|
||||||
|
"title": "План харчування",
|
||||||
|
"noMealPlanned": "Страву не заплановано",
|
||||||
|
"addMeal": "Додати {{type}}",
|
||||||
|
"editMeal": "Редагувати страву",
|
||||||
|
"addMealTitle": "Додати страву",
|
||||||
|
"deleteMeal": "Видалити страву",
|
||||||
|
"transferToShoppingList": "Додати інгредієнти до списку покупок",
|
||||||
|
"today": "Сьогодні",
|
||||||
|
"prevWeek": "Попередній тиждень",
|
||||||
|
"nextWeek": "Наступний тиждень",
|
||||||
|
"loadError": "Не вдалося завантажити план харчування.",
|
||||||
|
"typeBreakfast": "Сніданок",
|
||||||
|
"typeLunch": "Обід",
|
||||||
|
"typeDinner": "Вечеря",
|
||||||
|
"typeSnack": "Перекус",
|
||||||
|
"dayMo": "Пн",
|
||||||
|
"dayDi": "Вт",
|
||||||
|
"dayMi": "Ср",
|
||||||
|
"dayDo": "Чт",
|
||||||
|
"dayFr": "Пт",
|
||||||
|
"daySa": "Сб",
|
||||||
|
"daySo": "Нд",
|
||||||
|
"dateLabel": "Дата",
|
||||||
|
"mealTypeLabel": "Прийом їжі",
|
||||||
|
"titleLabel": "Заголовок *",
|
||||||
|
"titlePlaceholder": "напр. Спагеті болоньєзе",
|
||||||
|
"notesLabel": "Нотатки",
|
||||||
|
"notesPlaceholder": "Необов'язково…",
|
||||||
|
"ingredientsLabel": "Інгредієнти",
|
||||||
|
"addIngredient": "Додати інгредієнт",
|
||||||
|
"ingredientNamePlaceholder": "Інгредієнт",
|
||||||
|
"ingredientQtyPlaceholder": "Кількість",
|
||||||
|
"ingredientCategoryLabel": "Категорія",
|
||||||
|
"ingredientCategoryDefault": "Різне",
|
||||||
|
"removeIngredient": "Видалити інгредієнт",
|
||||||
|
"transferLabel": "Перенести інгредієнти до списку покупок",
|
||||||
|
"transferNow": "Перенести зараз",
|
||||||
|
"noShoppingLists": "Немає доступних списків покупок",
|
||||||
|
"transferSuccess": "{{count}} інгредієнт перенесено",
|
||||||
|
"transferSuccessPlural": "{{count}} інгредієнтів перенесено",
|
||||||
|
"transferAlreadyDone": "Усі інгредієнти вже перенесено",
|
||||||
|
"ingredientCount": "{{count}} інгредієнт",
|
||||||
|
"ingredientCountPlural": "{{count}} інгредієнтів",
|
||||||
|
"titleRequired": "Заголовок є обов'язковим",
|
||||||
|
"loadingIndicator": "Завантаження…",
|
||||||
|
"recipeUrlLabel": "Посилання на рецепт (необов'язково)",
|
||||||
|
"recipeUrlPlaceholder": "https://…",
|
||||||
|
"openRecipe": "Відкрити рецепт"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"title": "Календар",
|
||||||
|
"newEvent": "Нова подія",
|
||||||
|
"editEvent": "Редагувати подію",
|
||||||
|
"addEvent": "Додати подію",
|
||||||
|
"deleteEvent": "Видалити подію",
|
||||||
|
"noEvents": "Немає подій у вибраному періоді.",
|
||||||
|
"today": "Сьогодні",
|
||||||
|
"back": "Назад",
|
||||||
|
"forward": "Вперед",
|
||||||
|
"viewMonth": "Місяць",
|
||||||
|
"viewWeek": "Тиждень",
|
||||||
|
"viewDay": "День",
|
||||||
|
"viewAgenda": "Порядок денний",
|
||||||
|
"allDay": "Весь день",
|
||||||
|
"allDayShort": "весь день",
|
||||||
|
"moreEvents": "+{{count}} ще",
|
||||||
|
"weekNumberLabel": "Т{{week}} · {{month}} {{year}}",
|
||||||
|
"agendaFrom": "З {{date}}",
|
||||||
|
"titleLabel": "Заголовок *",
|
||||||
|
"titlePlaceholder": "напр. Стоматолог",
|
||||||
|
"allDayToggle": "Весь день",
|
||||||
|
"startDateLabel": "Дата початку",
|
||||||
|
"startTimeLabel": "Час початку",
|
||||||
|
"endDateLabel": "Дата завершення",
|
||||||
|
"endTimeLabel": "Час завершення",
|
||||||
|
"fromLabel": "Від",
|
||||||
|
"toLabel": "До",
|
||||||
|
"locationLabel": "Місце",
|
||||||
|
"locationPlaceholder": "Необов'язково",
|
||||||
|
"assignedLabel": "Призначено",
|
||||||
|
"assignedNobody": "- Нікому -",
|
||||||
|
"colorLabel": "Колір {{color}}",
|
||||||
|
"descriptionLabel": "Опис",
|
||||||
|
"descriptionPlaceholder": "Необов'язково…",
|
||||||
|
"popupEdit": "Редагувати",
|
||||||
|
"deleteConfirm": "Справді видалити «{{title}}»?",
|
||||||
|
"createdToast": "Подію створено",
|
||||||
|
"savedToast": "Подію збережено",
|
||||||
|
"deletedToast": "Подію видалено",
|
||||||
|
"loadError": "Не вдалося завантажити події.",
|
||||||
|
"saveError": "Помилка збереження",
|
||||||
|
"deleteError": "Помилка видалення",
|
||||||
|
"titleRequired": "Заголовок є обов'язковим",
|
||||||
|
"monthJanuary": "Січень",
|
||||||
|
"monthFebruary": "Лютий",
|
||||||
|
"monthMarch": "Березень",
|
||||||
|
"monthApril": "Квітень",
|
||||||
|
"monthMay": "Травень",
|
||||||
|
"monthJune": "Червень",
|
||||||
|
"monthJuly": "Липень",
|
||||||
|
"monthAugust": "Серпень",
|
||||||
|
"monthSeptember": "Вересень",
|
||||||
|
"monthOctober": "Жовтень",
|
||||||
|
"monthNovember": "Листопад",
|
||||||
|
"monthDecember": "Грудень",
|
||||||
|
"dayShortSunday": "Нд",
|
||||||
|
"dayShortMonday": "Пн",
|
||||||
|
"dayShortTuesday": "Вт",
|
||||||
|
"dayShortWednesday": "Ср",
|
||||||
|
"dayShortThursday": "Чт",
|
||||||
|
"dayShortFriday": "Пт",
|
||||||
|
"dayShortSaturday": "Сб",
|
||||||
|
"dayLongSunday": "Неділя",
|
||||||
|
"dayLongMonday": "Понеділок",
|
||||||
|
"dayLongTuesday": "Вівторок",
|
||||||
|
"dayLongWednesday": "Середа",
|
||||||
|
"dayLongThursday": "Четвер",
|
||||||
|
"dayLongFriday": "П'ятниця",
|
||||||
|
"dayLongSaturday": "Субота",
|
||||||
|
"timeSuffix": ""
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"title": "Нотатки",
|
||||||
|
"newNote": "Нова нотатка",
|
||||||
|
"editNote": "Редагувати нотатку",
|
||||||
|
"addNoteLabel": "Нова нотатка",
|
||||||
|
"searchPlaceholder": "Пошук нотаток…",
|
||||||
|
"emptyTitle": "Нотаток поки немає",
|
||||||
|
"emptyDescription": "Створіть нову нотатку кнопкою +.",
|
||||||
|
"noResultsTitle": "Результатів немає",
|
||||||
|
"noResultsDescription": "Жодна нотатка не містить «{{query}}».",
|
||||||
|
"titleLabel": "Заголовок (необов'язково)",
|
||||||
|
"titlePlaceholder": "Без заголовку",
|
||||||
|
"contentLabel": "Вміст",
|
||||||
|
"contentMarkdownHint": "(підтримується форматування Markdown)",
|
||||||
|
"contentPlaceholder": "Введіть нотатку…",
|
||||||
|
"colorLabel": "Колір",
|
||||||
|
"pinnedLabel": "Закріпити (відображатиметься на огляді)",
|
||||||
|
"pinAction": "Закріпити",
|
||||||
|
"unpinAction": "Відкріпити",
|
||||||
|
"deleteLabel": "Видалити нотатку",
|
||||||
|
"deleteConfirm": "Справді видалити цю нотатку?",
|
||||||
|
"createdToast": "Нотатку створено",
|
||||||
|
"savedToast": "Нотатку збережено",
|
||||||
|
"deletedToast": "Нотатку видалено",
|
||||||
|
"loadError": "Не вдалося завантажити нотатки.",
|
||||||
|
"formatBold": "Жирний (Ctrl+B)",
|
||||||
|
"formatItalic": "Курсив (Ctrl+I)",
|
||||||
|
"formatUnderline": "Підкреслений (Ctrl+U)",
|
||||||
|
"formatStrikethrough": "Закреслений",
|
||||||
|
"formatHeading": "Заголовок",
|
||||||
|
"formatList": "Маркований список",
|
||||||
|
"formatOrderedList": "Нумерований список",
|
||||||
|
"formatChecklist": "Список завдань",
|
||||||
|
"formatLink": "Посилання",
|
||||||
|
"formatCode": "Код",
|
||||||
|
"formatQuote": "Цитата",
|
||||||
|
"formatDivider": "Розділювач"
|
||||||
|
},
|
||||||
|
"contacts": {
|
||||||
|
"title": "Контакти",
|
||||||
|
"newContact": "Новий контакт",
|
||||||
|
"editContact": "Редагувати контакт",
|
||||||
|
"addButton": "Новий",
|
||||||
|
"newContactLabel": "Новий контакт",
|
||||||
|
"searchPlaceholder": "Пошук за ім'ям, телефоном або email…",
|
||||||
|
"importButton": "Імпорт",
|
||||||
|
"importLabel": "Імпортувати контакт з vCard",
|
||||||
|
"importTooltip": "Імпортувати vCard",
|
||||||
|
"emptyTitle": "Контактів поки немає",
|
||||||
|
"emptyDescription": "Додайте нові контакти кнопкою +.",
|
||||||
|
"filterAll": "Усі",
|
||||||
|
"nameLabel": "Ім'я *",
|
||||||
|
"namePlaceholder": "Повне ім'я",
|
||||||
|
"categoryLabel": "Категорія",
|
||||||
|
"phoneLabel": "Телефон",
|
||||||
|
"phonePlaceholder": "+380 …",
|
||||||
|
"emailLabel": "Email",
|
||||||
|
"emailPlaceholder": "ім'я@приклад.com",
|
||||||
|
"addressLabel": "Адреса",
|
||||||
|
"addressPlaceholder": "Вулиця, індекс місто",
|
||||||
|
"notesLabel": "Нотатки",
|
||||||
|
"notesPlaceholder": "Необов'язково…",
|
||||||
|
"callLabel": "Зателефонувати",
|
||||||
|
"emailActionLabel": "Написати email",
|
||||||
|
"mapsLabel": "Відкрити на картах",
|
||||||
|
"exportLabel": "Експортувати як vCard",
|
||||||
|
"exportTooltip": "Експортувати vCard",
|
||||||
|
"deleteLabel": "Видалити контакт",
|
||||||
|
"deleteConfirm": "Справді видалити цей контакт?",
|
||||||
|
"deletePersonConfirm": "Справді видалити «{{name}}»?",
|
||||||
|
"savedToast": "Контакт збережено",
|
||||||
|
"updatedToast": "Контакт оновлено",
|
||||||
|
"deletedToast": "Контакт видалено",
|
||||||
|
"importedToast": "{{name}} імпортовано.",
|
||||||
|
"importError": "Помилка імпорту: {{error}}",
|
||||||
|
"vcardNoName": "vCard не містить імені.",
|
||||||
|
"catDoctor": "Лікар",
|
||||||
|
"catSchool": "Школа/Дитсадок",
|
||||||
|
"catAuthority": "Державний орган",
|
||||||
|
"catInsurance": "Страхування",
|
||||||
|
"catCraftsman": "Майстер",
|
||||||
|
"catEmergency": "Екстрена служба",
|
||||||
|
"catMisc": "Різне",
|
||||||
|
"categoryDoctor": "Лікар",
|
||||||
|
"categorySchool": "Школа/Дитсадок",
|
||||||
|
"categoryAuthority": "Державний орган",
|
||||||
|
"categoryInsurance": "Страхування",
|
||||||
|
"categoryCraftsman": "Майстер",
|
||||||
|
"categoryEmergency": "Екстрена служба",
|
||||||
|
"categoryOther": "Інше"
|
||||||
|
},
|
||||||
|
"budget": {
|
||||||
|
"title": "Бюджет",
|
||||||
|
"newEntry": "Новий запис",
|
||||||
|
"editEntry": "Редагувати запис",
|
||||||
|
"addEntryLabel": "Додати запис",
|
||||||
|
"newEntryFabLabel": "Новий запис",
|
||||||
|
"currentMonth": "Поточний",
|
||||||
|
"prevMonth": "Попередній місяць",
|
||||||
|
"nextMonth": "Наступний місяць",
|
||||||
|
"income": "Доходи",
|
||||||
|
"expenses": "Витрати",
|
||||||
|
"balance": "Баланс",
|
||||||
|
"byCategory": "За категоріями",
|
||||||
|
"transactions": "Транзакції",
|
||||||
|
"emptyTitle": "Записів у цьому місяці немає",
|
||||||
|
"emptyDescription": "Додайте записи бюджету кнопкою +.",
|
||||||
|
"csvExport": "CSV",
|
||||||
|
"typeExpense": "Витрата",
|
||||||
|
"typeIncome": "Дохід",
|
||||||
|
"titleLabel": "Заголовок *",
|
||||||
|
"titlePlaceholder": "напр. Супермаркет",
|
||||||
|
"amountLabel": "Сума *",
|
||||||
|
"amountPlaceholder": "0.00",
|
||||||
|
"categoryLabel": "Категорія",
|
||||||
|
"dateLabel": "Дата *",
|
||||||
|
"recurringLabel": "Повторюване",
|
||||||
|
"deleteLabel": "Видалити запис",
|
||||||
|
"deleteConfirm": "Справді видалити цей запис?",
|
||||||
|
"deletePersonConfirm": "Справді видалити «{{title}}»?",
|
||||||
|
"addedToast": "Запис додано",
|
||||||
|
"savedToast": "Запис збережено",
|
||||||
|
"deletedToast": "Запис видалено",
|
||||||
|
"loadError": "Не вдалося завантажити бюджет.",
|
||||||
|
"trendNeutral": "— так само, як у {{month}}",
|
||||||
|
"validAmountRequired": "Будь ласка, введіть коректну суму",
|
||||||
|
"dateRequired": "Дата є обов'язковою",
|
||||||
|
"catFood": "Продукти",
|
||||||
|
"catRent": "Оренда",
|
||||||
|
"catInsurance": "Страхування",
|
||||||
|
"catMobility": "Транспорт",
|
||||||
|
"catLeisure": "Дозвілля",
|
||||||
|
"catClothing": "Одяг",
|
||||||
|
"catHealth": "Здоров'я",
|
||||||
|
"catEducation": "Освіта",
|
||||||
|
"catMisc": "Різне",
|
||||||
|
"loadingIndicator": "Завантаження…"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Налаштування",
|
||||||
|
"tabGeneral": "Загальні",
|
||||||
|
"tabMeals": "Харчування",
|
||||||
|
"tabBudget": "Бюджет",
|
||||||
|
"tabShopping": "Покупки",
|
||||||
|
"tabCalendar": "Календар",
|
||||||
|
"tabAccount": "Обліковий запис",
|
||||||
|
"tabsAriaLabel": "Розділи налаштувань",
|
||||||
|
"sectionDesign": "Зовнішній вигляд",
|
||||||
|
"sectionShopping": "Покупки",
|
||||||
|
"shoppingCategoriesLabel": "Категорії покупок",
|
||||||
|
"shoppingCategoriesHint": "Додавайте, перейменовуйте, видаляйте або змінюйте порядок категорій.",
|
||||||
|
"shoppingCategoryPlaceholder": "Нова категорія…",
|
||||||
|
"shoppingCategoryRenameHint": "Натисніть, щоб перейменувати",
|
||||||
|
"shoppingCategoryRenamePrompt": "Нова назва категорії:",
|
||||||
|
"shoppingCategoryMoveUp": "Перемістити категорію вгору",
|
||||||
|
"shoppingCategoryMoveDown": "Перемістити категорію вниз",
|
||||||
|
"shoppingCategoryDelete": "Видалити категорію",
|
||||||
|
"shoppingCategoryDeleteConfirm": "Видалити категорію «{{name}}»? Наявні товари буде переміщено до наступної категорії.",
|
||||||
|
"shoppingCategoryAdded": "Категорію додано.",
|
||||||
|
"shoppingCategoryRenamed": "Категорію перейменовано.",
|
||||||
|
"shoppingCategoryDeleted": "Категорію видалено.",
|
||||||
|
"sectionAccount": "Мій обліковий запис",
|
||||||
|
"sectionCalendarSync": "Синхронізація календаря",
|
||||||
|
"sectionFamily": "Члени родини",
|
||||||
|
"cardAppearance": "Відображення",
|
||||||
|
"themeSystem": "Системна",
|
||||||
|
"themeSysLabel": "Використовувати системні налаштування",
|
||||||
|
"themeLight": "Світла",
|
||||||
|
"themeLightLabel": "Світла тема",
|
||||||
|
"themeDark": "Темна",
|
||||||
|
"themeDarkLabel": "Темна тема",
|
||||||
|
"changePassword": "Змінити пароль",
|
||||||
|
"currentPasswordLabel": "Поточний пароль",
|
||||||
|
"newPasswordLabel": "Новий пароль",
|
||||||
|
"confirmPasswordLabel": "Підтвердити новий пароль",
|
||||||
|
"savePassword": "Зберегти пароль",
|
||||||
|
"passwordMismatch": "Паролі не збігаються.",
|
||||||
|
"passwordSavedToast": "Пароль успішно змінено.",
|
||||||
|
"googleCalendar": "Google Календар",
|
||||||
|
"appleCalendar": "Apple Календар (iCloud)",
|
||||||
|
"syncNow": "Синхронізувати зараз",
|
||||||
|
"disconnect": "Від'єднати",
|
||||||
|
"connectGoogle": "Підключити через Google",
|
||||||
|
"connected": "Підключено",
|
||||||
|
"connectedLastSync": "Підключено · Остання: {{date}}",
|
||||||
|
"notConnected": "Не підключено",
|
||||||
|
"notConfigured": "Не налаштовано (відсутні змінні .env)",
|
||||||
|
"configured": "Налаштовано (через .env)",
|
||||||
|
"configuredLastSync": "Налаштовано (через .env) · Остання: {{date}}",
|
||||||
|
"syncSuccess": "{{provider}} синхронізовано.",
|
||||||
|
"disconnectedToast": "{{provider}} від'єднано.",
|
||||||
|
"googleOnlyAdmin": "Лише адміністратор може підключити Google Календар.",
|
||||||
|
"appleOnlyAdmin": "Лише адміністратор може підключити Apple Календар.",
|
||||||
|
"caldavUrlLabel": "URL CalDAV-сервера",
|
||||||
|
"caldavUrlPlaceholder": "https://caldav.icloud.com",
|
||||||
|
"appleIdLabel": "Apple ID (email)",
|
||||||
|
"applePasswordLabel": "Пароль програми",
|
||||||
|
"applePasswordHint": "Створіть пароль на <strong>appleid.apple.com → Безпека</strong>.",
|
||||||
|
"appleConnectBtn": "Підключити та перевірити",
|
||||||
|
"appleConnecting": "Підключення…",
|
||||||
|
"appleConnectedToast": "Apple Календар підключено.",
|
||||||
|
"syncSuccessGoogle": "Синхронізацію з Google Календарем успішно підключено.",
|
||||||
|
"syncSuccessApple": "Синхронізацію з Apple Календарем успішно підключено.",
|
||||||
|
"syncErrorGoogle": "Не вдалося підключитися до Google. Спробуйте ще раз.",
|
||||||
|
"syncErrorApple": "Не вдалося підключитися до Apple. Спробуйте ще раз.",
|
||||||
|
"addMember": "+ Додати члена",
|
||||||
|
"newMemberTitle": "Новий член родини",
|
||||||
|
"usernameLabel": "Ім'я користувача",
|
||||||
|
"displayNameLabel": "Відображуване ім'я",
|
||||||
|
"memberPasswordLabel": "Пароль",
|
||||||
|
"colorLabel": "Колір",
|
||||||
|
"roleLabel": "Роль",
|
||||||
|
"roleMember": "Учасник",
|
||||||
|
"roleAdmin": "Адміністратор",
|
||||||
|
"createMember": "Створити",
|
||||||
|
"cancelAddMember": "Скасувати",
|
||||||
|
"memberAddedToast": "{{name}} додано.",
|
||||||
|
"deleteMemberConfirm": "Справді видалити {{name}}?",
|
||||||
|
"memberDeletedToast": "{{name}} видалено.",
|
||||||
|
"deleteMemberLabel": "Видалити",
|
||||||
|
"logout": "Вийти",
|
||||||
|
"synchronizing": "Синхронізація…",
|
||||||
|
"googleDisconnectConfirm": "Від'єднати Google Календар?",
|
||||||
|
"appleDisconnectConfirm": "Від'єднати Apple Календар?",
|
||||||
|
"localeSystem": "Системна",
|
||||||
|
"localeLabel": "Мова",
|
||||||
|
"languageTitle": "Мова",
|
||||||
|
"sectionMeals": "План харчування",
|
||||||
|
"mealTypesLabel": "Видимі прийоми їжі",
|
||||||
|
"mealTypesHint": "У планувальнику харчування відображатимуться лише вибрані типи прийомів їжі.",
|
||||||
|
"mealTypesSaved": "Налаштування харчування збережено.",
|
||||||
|
"mealTypesMinOne": "Має бути активним принаймні один тип прийому їжі.",
|
||||||
|
"sectionBudget": "Бюджет",
|
||||||
|
"currencyLabel": "Валюта",
|
||||||
|
"currencyHint": "Встановлює валюту, що використовується в розділі бюджету.",
|
||||||
|
"currencySaved": "Валюту збережено."
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"tagline": "Планування для родини. Безпечно. Конфіденційно. Відкритий код.",
|
||||||
|
"usernameLabel": "Ім'я користувача",
|
||||||
|
"usernamePlaceholder": "ім'я користувача",
|
||||||
|
"passwordLabel": "Пароль",
|
||||||
|
"passwordPlaceholder": "••••••••",
|
||||||
|
"loginButton": "Увійти",
|
||||||
|
"loggingIn": "Вхід…",
|
||||||
|
"tooManyAttempts": "Забагато спроб. Будь ласка, зачекайте.",
|
||||||
|
"invalidCredentials": "Невірні облікові дані."
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"title": "Встановити Oikos",
|
||||||
|
"subtitle": "Додати на головний екран",
|
||||||
|
"iosTip1": "Натисніть ",
|
||||||
|
"iosTip2": " → «Додати на головний екран»",
|
||||||
|
"installButton": "Встановити",
|
||||||
|
"dismissLabel": "Закрити"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"closeLabel": "Закрити",
|
||||||
|
"overlayLabel": "Фон модального вікна"
|
||||||
|
},
|
||||||
|
"rrule": {
|
||||||
|
"freqNone": "Без повторення",
|
||||||
|
"freqDaily": "Щодня",
|
||||||
|
"freqWeekly": "Щотижня",
|
||||||
|
"freqMonthly": "Щомісяця",
|
||||||
|
"dayMo": "Пн",
|
||||||
|
"dayTu": "Вт",
|
||||||
|
"dayWe": "Ср",
|
||||||
|
"dayTh": "Чт",
|
||||||
|
"dayFr": "Пт",
|
||||||
|
"daySa": "Сб",
|
||||||
|
"daySu": "Нд",
|
||||||
|
"labelRepeat": "Повторення",
|
||||||
|
"labelEvery": "Кожні",
|
||||||
|
"labelOnDays": "У ці дні",
|
||||||
|
"labelUntil": "Закінчується (необов'язково)",
|
||||||
|
"unitDay": "день",
|
||||||
|
"unitDays": "днів",
|
||||||
|
"unitWeek": "тиждень",
|
||||||
|
"unitWeeks": "тижнів",
|
||||||
|
"unitMonth": "місяць",
|
||||||
|
"unitMonths": "місяців"
|
||||||
|
},
|
||||||
|
"reminders": {
|
||||||
|
"sectionTitle": "Нагадування",
|
||||||
|
"enableLabel": "Встановити нагадування",
|
||||||
|
"dateLabel": "Дата",
|
||||||
|
"timeLabel": "Час",
|
||||||
|
"offsetLabel": "Нагадати мені",
|
||||||
|
"offsetNone": "Без нагадування",
|
||||||
|
"offset15min": "За 15 хвилин",
|
||||||
|
"offset1hour": "За 1 годину",
|
||||||
|
"offset1day": "За 1 день",
|
||||||
|
"offsetAtTime": "У час події",
|
||||||
|
"toastTitle": "Нагадування",
|
||||||
|
"dismiss": "Закрити",
|
||||||
|
"notificationPermission": "Сповіщення браузера",
|
||||||
|
"notificationEnable": "Увімкнути сповіщення",
|
||||||
|
"notificationEnabled": "Сповіщення активні",
|
||||||
|
"notificationDenied": "Сповіщення заблоковано",
|
||||||
|
"notificationHint": "Отримуйте сповіщення, поки додаток відкрито.",
|
||||||
|
"pendingBadgeTitle": "{{count}} нагадування",
|
||||||
|
"pendingBadgeTitlePlural": "{{count}} нагадувань"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,9 +10,25 @@ import { t, formatDate, formatTime } from '/i18n.js';
|
|||||||
import { esc } from '/utils/html.js';
|
import { esc } from '/utils/html.js';
|
||||||
import '/components/oikos-locale-picker.js';
|
import '/components/oikos-locale-picker.js';
|
||||||
|
|
||||||
const SUPPORTED_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'INR', 'JPY', 'NOK', 'PLN', 'RUB', 'SAR', 'SEK', 'TRY', 'USD'];
|
const SUPPORTED_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'INR', 'JPY', 'NOK', 'PLN', 'RUB', 'SAR', 'SEK', 'TRY', 'UAH', 'USD'];
|
||||||
const SETTINGS_TAB_KEY = 'oikos:settings:tab';
|
const SETTINGS_TAB_KEY = 'oikos:settings:tab';
|
||||||
|
|
||||||
|
const CATEGORY_I18N = {
|
||||||
|
'Obst & Gemüse': 'shopping.catFruitVeg',
|
||||||
|
'Backwaren': 'shopping.catBakery',
|
||||||
|
'Milchprodukte': 'shopping.catDairy',
|
||||||
|
'Fleisch & Fisch': 'shopping.catMeatFish',
|
||||||
|
'Tiefkühl': 'shopping.catFrozen',
|
||||||
|
'Getränke': 'shopping.catDrinks',
|
||||||
|
'Haushalt': 'shopping.catHousehold',
|
||||||
|
'Drogerie': 'shopping.catDrugstore',
|
||||||
|
'Sonstiges': 'shopping.catMisc',
|
||||||
|
};
|
||||||
|
function catLabel(name) {
|
||||||
|
const key = CATEGORY_I18N[name];
|
||||||
|
return key ? t(key) : name;
|
||||||
|
}
|
||||||
|
|
||||||
function buildCurrencyOptions(selected) {
|
function buildCurrencyOptions(selected) {
|
||||||
const display = typeof Intl.DisplayNames !== 'undefined'
|
const display = typeof Intl.DisplayNames !== 'undefined'
|
||||||
? new Intl.DisplayNames([document.documentElement.lang || 'en'], { type: 'currency' })
|
? new Intl.DisplayNames([document.documentElement.lang || 'en'], { type: 'currency' })
|
||||||
@@ -671,7 +687,7 @@ function categoryRowHtml(cat, isFirst, isLast) {
|
|||||||
return `
|
return `
|
||||||
<li class="cat-row" data-cat-id="${cat.id}">
|
<li class="cat-row" data-cat-id="${cat.id}">
|
||||||
<i data-lucide="${esc(cat.icon)}" class="cat-row__icon" aria-hidden="true"></i>
|
<i data-lucide="${esc(cat.icon)}" class="cat-row__icon" aria-hidden="true"></i>
|
||||||
<span class="cat-row__name" data-action="rename-cat" title="${t('settings.shoppingCategoryRenameHint')}">${esc(cat.name)}</span>
|
<span class="cat-row__name" data-action="rename-cat" title="${t('settings.shoppingCategoryRenameHint')}">${esc(catLabel(cat.name))}</span>
|
||||||
<div class="cat-row__actions">
|
<div class="cat-row__actions">
|
||||||
<button class="btn btn--icon btn--ghost" data-action="move-cat-up" data-id="${cat.id}"
|
<button class="btn btn--icon btn--ghost" data-action="move-cat-up" data-id="${cat.id}"
|
||||||
aria-label="${t('settings.shoppingCategoryMoveUp')}"
|
aria-label="${t('settings.shoppingCategoryMoveUp')}"
|
||||||
@@ -746,7 +762,7 @@ function bindCategoryEvents(container) {
|
|||||||
const cat = cats.find((c) => c.id === id);
|
const cat = cats.find((c) => c.id === id);
|
||||||
if (!cat) return;
|
if (!cat) return;
|
||||||
const { promptModal } = await import('/components/modal.js');
|
const { promptModal } = await import('/components/modal.js');
|
||||||
const newName = await promptModal(t('settings.shoppingCategoryRenamePrompt'), cat.name);
|
const newName = await promptModal(t('settings.shoppingCategoryRenamePrompt'), catLabel(cat.name));
|
||||||
if (!newName || newName === cat.name) return;
|
if (!newName || newName === cat.name) return;
|
||||||
try {
|
try {
|
||||||
const res = await api.put(`/shopping/categories/${id}`, { name: newName });
|
const res = await api.put(`/shopping/categories/${id}`, { name: newName });
|
||||||
@@ -788,7 +804,7 @@ function bindCategoryEvents(container) {
|
|||||||
if (!cat) return;
|
if (!cat) return;
|
||||||
const { confirmModal: confirmDel } = await import('/components/modal.js');
|
const { confirmModal: confirmDel } = await import('/components/modal.js');
|
||||||
if (!await confirmDel(
|
if (!await confirmDel(
|
||||||
t('settings.shoppingCategoryDeleteConfirm', { name: cat.name }),
|
t('settings.shoppingCategoryDeleteConfirm', { name: catLabel(cat.name) }),
|
||||||
{ danger: true, confirmLabel: t('common.delete') }
|
{ danger: true, confirmLabel: t('common.delete') }
|
||||||
)) return;
|
)) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const router = express.Router();
|
|||||||
const VALID_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack'];
|
const VALID_MEAL_TYPES = ['breakfast', 'lunch', 'dinner', 'snack'];
|
||||||
const DEFAULT_MEAL_TYPES = VALID_MEAL_TYPES.join(',');
|
const DEFAULT_MEAL_TYPES = VALID_MEAL_TYPES.join(',');
|
||||||
|
|
||||||
const VALID_CURRENCIES = ['EUR', 'USD', 'GBP', 'SEK', 'NOK', 'DKK', 'CHF', 'CNY', 'PLN', 'CZK', 'HUF', 'JPY', 'AUD', 'CAD', 'TRY', 'RUB'];
|
const VALID_CURRENCIES = ['AED', 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'INR', 'JPY', 'NOK', 'PLN', 'RUB', 'SAR', 'SEK', 'TRY', 'UAH', 'USD'];
|
||||||
const DEFAULT_CURRENCY = 'EUR';
|
const DEFAULT_CURRENCY = 'EUR';
|
||||||
|
|
||||||
const VALID_WIDGET_IDS = ['weather', 'tasks', 'calendar', 'shopping', 'meals', 'notes'];
|
const VALID_WIDGET_IDS = ['weather', 'tasks', 'calendar', 'shopping', 'meals', 'notes'];
|
||||||
|
|||||||
Reference in New Issue
Block a user