diff --git a/docs/OBSOLETE_TRANSLATION_KEYS.md b/docs/OBSOLETE_TRANSLATION_KEYS.md new file mode 100644 index 000000000..dfae32df2 --- /dev/null +++ b/docs/OBSOLETE_TRANSLATION_KEYS.md @@ -0,0 +1,62 @@ +# Obsolete translation keys + +Remove in part 2 of the PR + +Mark these keys as obsolete in Weblate. Remove them from all catalogs after +release; older app versions may still use them. + +Keep the current `common` and `home` namespaces. + +## Namespace `common` + +| Obsolete key | Replacement | +| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `actors.you_accusativus` | Complete ICU messages | +| `actors.you_dativus` | Complete ICU messages | +| `expense_details.balance_list.are_settled_up` | `expense_details.balance_list.settled` | +| `expense_details.balance_list.is_settled_up` | `expense_details.balance_list.settled` | +| `ui.added_by` | `expense_metadata.added_by` | +| `ui.deleted_by` | `expense_metadata.deleted_by` | +| `ui.edited_by` | `expense_metadata.edited_by` | +| `ui.balance` | `balance_summary.more` (`count`) | +| `ui.balances` | `balance_summary.more` (`count`) | +| `ui.expense.for` | `activity.*_for_expense`, `expense_list.conversion_for_users` | +| `ui.expense.you.deleted` | `activity.user_deleted_expense` (`actor`) | +| `ui.expense.user.deleted` | `activity.user_deleted_expense` (`actor`) | +| `ui.expense.you.get` | `expense_participant.gets`, `balance_list.gets` | +| `ui.expense.user.get` | `expense_participant.gets`, `balance_list.gets` | +| `ui.expense.you.lent` | `expense_details.balance_list.lent`, `ui.expense.statements.you_lent*` | +| `ui.expense.user.lent` | `expense_details.balance_list.lent` (`actor`) | +| `ui.expense.you.owe` | `expense_details.balance_list.owes`, `expense_participant.owes`, `balance_summary.you_owe_friend`, `ui.expense.statements.you_owe*` | +| `ui.expense.user.owe` | `expense_details.balance_list.owes`, `expense_participant.owes`, `balance_summary.friend_owes_you` | +| `ui.expense.you.pay` | `balance_list.pays`, `ui.transfer`, `ui.expense.statements.you_pay_user` | +| `ui.expense.user.pay` | `balance_list.pays`, `ui.transfer`, `ui.expense.statements.user_pays_you`, `ui.expense.statements.user_pays_user` | +| `ui.expense.you.paid` | `expense_participant.paid` | +| `ui.expense.user.paid` | `expense_participant.paid` | +| `ui.expense.you.received` | `expense_participant.received` | +| `ui.expense.user.received` | `expense_participant.received` | + +## Namespace `home` + +| Obsolete key | Replacement | +| --------------------- | ----------------------------- | +| `hero.title_part1` | `hero.title_before_highlight` | +| `hero.subtitle_part1` | `hero.subtitle` | +| `hero.subtitle_link` | `hero.subtitle` | +| `hero.subtitle_part2` | `hero.subtitle` | + +## Keep + +All keys not listed above remain unchanged, including `actors.you`, +`ui.expense.{paid_by,received_by,from,to}`, +`ui.and`, `ui.on`, `account.messages.submit_*`, `errors.invalid_split`, +`errors.request_error`, and `errors.subscribe_error`. + +## Translation notes + +- Restored keys use English fallback until translated. +- Keep `errors.signin_error`; it now requires `{error}`. +- Use `{name}` for interpolation. Nested ICU braces such as + `other {{person} owes}` are valid; do not replace them globally. +- Participant messages must include `` and `` once per branch. + They render the existing clickable user and styled amount components. diff --git a/next-i18next.config.js b/next-i18next.config.js index b1c29aa5c..0a379061b 100644 --- a/next-i18next.config.js +++ b/next-i18next.config.js @@ -1,3 +1,12 @@ +// @ts-expect-error i18next-icu's CommonJS export has an invalid ambient module declaration. +import ICUModule from 'i18next-icu/cjs'; + +const ICU = ICUModule.default ?? ICUModule; + +/** @param {string} lng */ +const parseLngForICU = (lng) => + 'default' === lng ? 'en' : 'ca@valencia' === lng ? 'ca-ES-valencia' : lng; + /** @type {import('next-i18next').UserConfig} */ const config = { i18n: { @@ -22,7 +31,16 @@ const config = { ], localeDetection: false, }, + fallbackLng: 'en', localePath: './public/locales', + i18nFormat: { + // Keep the existing route and Weblate locale codes while ICU uses valid BCP 47 tags. + parseLngForICU, + }, + onPreInitI18next: (i18n) => { + i18n.use(ICU); + }, + serializeConfig: false, }; export default config; diff --git a/package.json b/package.json index 995ff0f52..ef4d4f0ac 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,9 @@ "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "i18next-http-backend": "^3.0.6", + "i18next-icu": "^2.4.1", "input-otp": "^1.2.3", + "intl-messageformat": "^10.7.18", "lucide-react": "^0.544.0", "nanoid": "^5.0.6", "next": "15.5.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b86928770..54d0cbe49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,9 +97,15 @@ importers: i18next-http-backend: specifier: ^3.0.6 version: 3.0.6 + i18next-icu: + specifier: ^2.4.1 + version: 2.4.4(intl-messageformat@10.7.18) input-otp: specifier: ^1.2.3 version: 1.4.2(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + intl-messageformat: + specifier: ^10.7.18 + version: 10.7.18 lucide-react: specifier: ^0.544.0 version: 0.544.0(react@19.2.1) @@ -664,6 +670,21 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@formatjs/ecma402-abstract@2.3.6': + resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + + '@formatjs/fast-memoize@2.2.7': + resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + + '@formatjs/icu-messageformat-parser@2.11.4': + resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + + '@formatjs/icu-skeleton-parser@1.8.16': + resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} + + '@formatjs/intl-localematcher@0.6.2': + resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} + '@heroicons/react@2.2.0': resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} peerDependencies: @@ -3026,6 +3047,11 @@ packages: i18next-http-backend@3.0.6: resolution: {integrity: sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==} + i18next-icu@2.4.4: + resolution: {integrity: sha512-yfYdGTftClNUnZAQG1cu0yHHLaMVsrqfcgQHHchGDufkunBLKHuw2wXTCDuE+8mWMMcdePl8UBR0EUUA/3+FDw==} + peerDependencies: + intl-messageformat: '>=10.3.3 <12.0.0' + i18next@25.10.10: resolution: {integrity: sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==} peerDependencies: @@ -3063,6 +3089,9 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -4779,6 +4808,32 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@formatjs/ecma402-abstract@2.3.6': + dependencies: + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/intl-localematcher': 0.6.2 + decimal.js: 10.5.0 + tslib: 2.8.1 + + '@formatjs/fast-memoize@2.2.7': + dependencies: + tslib: 2.8.1 + + '@formatjs/icu-messageformat-parser@2.11.4': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/icu-skeleton-parser': 1.8.16 + tslib: 2.8.1 + + '@formatjs/icu-skeleton-parser@1.8.16': + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + tslib: 2.8.1 + + '@formatjs/intl-localematcher@0.6.2': + dependencies: + tslib: 2.8.1 + '@heroicons/react@2.2.0(react@19.2.1)': dependencies: react: 19.2.1 @@ -7104,6 +7159,10 @@ snapshots: transitivePeerDependencies: - encoding + i18next-icu@2.4.4(intl-messageformat@10.7.18): + dependencies: + intl-messageformat: 10.7.18 + i18next@25.10.10(typescript@5.7.3): dependencies: '@babel/runtime': 7.29.2 @@ -7135,6 +7194,13 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + is-arrayish@0.2.1: {} is-fullwidth-code-point@3.0.0: {} diff --git a/prisma/seed-checksum.txt b/prisma/seed-checksum.txt index 119c95043..d184dc10a 100644 --- a/prisma/seed-checksum.txt +++ b/prisma/seed-checksum.txt @@ -1 +1 @@ -157cf91ec892d975f64aaf8ef567b06bdc5cfbd238a1ff3c59f92cdc4081d77f +0259523777d99a90e4fbda5a66a33f00033a1021a4eed5687f11d1899f77c125 diff --git a/public/locales/cs/common.json b/public/locales/cs/common.json index 176c777eb..9fcaa6417 100644 --- a/public/locales/cs/common.json +++ b/public/locales/cs/common.json @@ -97,7 +97,7 @@ "you_dativus": "Vám" }, "auth": { - "continue_with": "Pokračovat s {{provider}}", + "continue_with": "Pokračovat s {provider}", "email_placeholder": "Zadejte váš email", "no_providers_configured": "Není nastaven žádný poskytovatel přihlášení", "no_providers_instructions": "Abyste se mohli přihlásit, nastavte prosím alespoň jednoho poskytovatele přihlášení. Pro více informací se podívejte na", @@ -140,7 +140,7 @@ "group_name_update_failed": "Chyba při aktualizaci jména skupiny", "import_failed": "Chyba při importování souboru", "invalid_cron_expression": "Neplatný cron výraz", - "invalid_currency_code": "Neplatný kód měny {{code}}", + "invalid_currency_code": "Neplatný kód měny {code}", "language_change_failed": "Chyba při změně jazyka", "less_than": "Soubor by měl být menší než", "name_required": "Musí být zadáno jméno", @@ -153,7 +153,7 @@ "request_error": "Chyba při požadavku na notifikace", "saving_expense": "Chyba při ukládání výdaje", "setting_update_failed": "Nastavení se nepodařilo aktualizovat", - "signin_error": "Při přihlašování došlo k chybě: ", + "signin_error": "Při přihlašování došlo k chybě: {error}", "signup_disabled": "Registrace nových účtů je v této instanci zakázána", "something_went_wrong": "Něco se pokazilo", "subscribe_error": "Nelze se přihlásit k odběru oznámení", @@ -344,7 +344,7 @@ "edit_description": "Toto změní rozvrh pro budoucí výdaje. Existující výdaje upraveny nebudou.", "edit_title": "Upravit opakování", "empty": "Žádné výdaje zatím nejsou opakované", - "expense_for_the_amount_of": "Výdaj {{name}} za {{amount}} {{currency}}", + "expense_for_the_amount_of": "Výdaj {name} za {amount} {currency}", "last_day": "Poslední den", "months": "Měsíce", "never": "Není nastaven žádný plán", diff --git a/public/locales/de/common.json b/public/locales/de/common.json index b86f6d65b..53027a7fa 100644 --- a/public/locales/de/common.json +++ b/public/locales/de/common.json @@ -108,7 +108,7 @@ "you_dativus": "Dir" }, "auth": { - "continue_with": "Weiter mit {{provider}}", + "continue_with": "Weiter mit {provider}", "email_placeholder": "Mailadresse eingeben", "no_providers_configured": "Keine Authentifizierungsanbieter konfiguriert", "no_providers_instructions": "Bitte konfigurieren Sie mindestens einen Authentifizierungsanbieter, um sich anzumelden. Siehe die", @@ -165,9 +165,9 @@ "image_compression_failed": "Bildkomprimierung fehlgeschlagen, versuche Originaldatei hochzuladen", "import_failed": "Fehler beim Importieren der Datei", "invalid_cron_expression": "Ungültiger Cron-Ausdruck", - "invalid_currency_code": "Ungültiger Währungscode {{code}}", + "invalid_currency_code": "Ungültiger Währungscode {code}", "language_change_failed": "Fehler beim Ändern der Sprache", - "less_than": "Datei sollte kleiner als {{size}}MB sein", + "less_than": "Datei sollte kleiner als {size}MB sein", "name_required": "Name ist erforderlich", "no_providers": "Fehler beim Abrufen der Authentifizierungsanbieter. Bitte prüfen Sie die Backend-Logs.", "notification_not_supported": "Benachrichtigungen werden nicht unterstützt", @@ -178,7 +178,7 @@ "request_error": "Fehler bei der Anfrage der Benachrichtigung", "saving_expense": "Fehler beim Speichern der Ausgabe", "setting_update_failed": "Einstellung konnte nicht aktualisiert werden", - "signin_error": "Beim Anmelden ist ein Fehler aufgetreten: ", + "signin_error": "Beim Anmelden ist ein Fehler aufgetreten: {error}", "signup_disabled": "Neuanmeldung ist in dieser Instanz deaktiviert", "something_went_wrong": "Etwas ist schiefgelaufen", "subscribe_error": "Fehler beim Abonnieren der Benachrichtigung", @@ -377,7 +377,7 @@ "edit_description": "Dadurch ändert sich der Zeitplan für zukünftige Ausgaben. Bestehende Ausgaben werden nicht geändert.", "edit_title": "Wiederholung bearbeiten", "empty": "Noch keine wiederkehrenden Ausgaben", - "expense_for_the_amount_of": "Ausgabe {{name}} in Höhe von {{amount}} {{currency}}", + "expense_for_the_amount_of": "Ausgabe {name} in Höhe von {amount} {currency}", "last_day": "Letzter Tag", "months": "Monate", "never": "Kein Zeitplan festgelegt", diff --git a/public/locales/el/common.json b/public/locales/el/common.json index 58ec7fff8..ff90ba6fc 100644 --- a/public/locales/el/common.json +++ b/public/locales/el/common.json @@ -99,7 +99,7 @@ "you_dativus": "Εσύ" }, "auth": { - "continue_with": "Συνέχεια με {{provider}}" + "continue_with": "Συνέχεια με {provider}" }, "expense_details": { "add_expense": "Προσθήκη εξόδου", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 7b0717bca..ee0cab553 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -57,7 +57,11 @@ "submit_feedback": "Submit feedback", "submit_feedback_details": { "placeholder": "Enter your feedback", - "title": "Submit feedback" + "title": "Submit feedback", + "messages": { + "submit_error": "Failed to send feedback", + "submit_success": "Feedback sent" + } }, "support_us": "Sponsor us", "title": "Account", @@ -95,7 +99,8 @@ "settle_up": "Settle up", "submit": "Submit", "reconnect": "Reconnect", - "connect": "Connect" + "connect": "Connect", + "remind_later": "Remind me later" }, "actors": { "all": "All", @@ -108,7 +113,7 @@ "you_dativus": "You" }, "auth": { - "continue_with": "Continue with {{provider}}", + "continue_with": "Continue with {provider}", "email_placeholder": "Enter your email", "no_providers_configured": "No authentication providers configured", "no_providers_instructions": "Please configure at least one authentication provider to sign in. See the", @@ -125,7 +130,9 @@ "fetching_rate": "Fetching rate...", "rate": "Rate", "success_toast": "Currency conversion set successfully", - "title": "Currency conversion" + "title": "Currency conversion", + "amount_aria_label": "Amount", + "converted_amount_aria_label": "Converted amount" }, "errors": { "currency_conversion_error": "Error while setting currency conversion", @@ -137,10 +144,10 @@ "add_member_failed": "Failed to add member to the group", "group_name_update_failed": "Error in updating group name", "import_failed": "Error importing file", - "invalid_currency_code": "Invalid currency code {{code}}", + "invalid_currency_code": "Invalid currency code {code}", "invalid_split": "Adjust who owes this expense before saving.", "language_change_failed": "Error changing language", - "less_than": "File should be less than {{size}}MB", + "less_than": "File should be less than {size}MB", "image_compression_failed": "Image compression failed, trying to upload original file", "name_required": "Name is required", "notification_not_supported": "Notification is not supported", @@ -149,7 +156,7 @@ "request_error": "Error requesting notification", "saving_expense": "Error while saving expense", "setting_update_failed": "Failed to update setting", - "signin_error": "An error occurred while signing in: ", + "signin_error": "An error occurred while signing in: {error}", "signup_disabled": "Signup of new accounts is disabled on this instance", "something_went_wrong": "Something went wrong", "subscribe_error": "Cannot subscribe to notification", @@ -163,6 +170,12 @@ "currency_conversion_failed": "Failed to convert currencies", "recurrence_delete_failed": "Failed to delete recurrence", "recurrence_update_failed": "Failed to update recurrence", + "notification_request_failed": "Error requesting notification permission", + "notification_subscribe_failed": "Cannot subscribe to notifications", + "expense_submission_failed": "An unexpected error occurred while submitting the expense.", + "expense_deletion_failed": "An unexpected error occurred while deleting the expense.", + "friend_deletion_failed": "Failed to delete friend", + "signin_failed": "Sign-in failed. Please try again.", "invite_email_failed": "Failed to send invite email. Check the SMTP configuration." }, "bank_transactions": { @@ -189,14 +202,15 @@ "note": "Note: sending invite is disabled for now because of spam", "only_one_group_time": "You can have only one group at a time", "send_invite": "Send invite to user", - "warning": "Warning: Don't use send invite if it's invalid email. use add to Split Pro instead. Your account will be blocked if this feature is misused" + "warning": "Warning: Don't use send invite if it's invalid email. use add to Split Pro instead. Your account will be blocked if this feature is misused", + "empty_image_alt": "No users found" }, "split_type_section": { "direction": { "no_money_flow": "No one else owes anything", - "owes_payer": "{{debtor}} owes {{payer}}", - "owes_you": "{{debtor}} owes you", - "you_owe": "You owe {{payer}}" + "owes_payer": "{debtor} owes {payer}", + "owes_you": "{debtor} owes you", + "you_owe": "You owe {payer}" }, "split_equally": "split equally", "split_unequally": "split unequally", @@ -222,7 +236,8 @@ "title": "Share", "total_shares": "Total shares" } - } + }, + "split_equally_with_count": "split equally among {count, plural, one {# person} other {# people}}" }, "sponsor_us": "Sponsor us", "upload_file": { @@ -240,7 +255,10 @@ "balance_list": { "are_settled_up": "are settled up", "is_settled_up": "is settled up", - "press_balance_info": "Press on individual balance to initiate settlement" + "press_balance_info": "Press on individual balance to initiate settlement", + "settled": "{actor, select, you {You are settled up} other {{person} is settled up}}", + "lent": "{actor, select, you {You lent} other {{person} lent}}", + "owes": "{actor, select, you {You owe} other {{person} owes}}" }, "delete_expense_details": { "text": "This action cannot be undone. This will permanently delete your expense.", @@ -259,7 +277,13 @@ "submit_all": "Submit all", "pending": "Pending", "no_transactions_yet": "No transactions yet", - "already_added": "Already added" + "already_added": "Already added", + "receipt": { + "title": "Expense receipt", + "thumbnail_alt": "Expense receipt thumbnail", + "image_alt": "Expense receipt" + }, + "empty_state_image_alt": "No expenses" }, "group_details": { "add_members": "Add members", @@ -382,7 +406,7 @@ "never": "No schedule configured", "empty": "No recurring expenses yet", "recurring": "Recurring", - "expense_for_the_amount_of": "Expense {{name}} for the amount of {{amount}} {{currency}}", + "expense_for_the_amount_of": "Expense {name} for the amount of {amount} {currency}", "delete_title": "Delete Recurrence", "delete_description": "This will stop future expense creation. Existing expenses created by this recurrence will not be deleted.", "edit_title": "Edit Recurrence", @@ -400,7 +424,8 @@ "derived_edit_warning": "This expense is part of a recurrence. Changes will only affect this expense.", "template_delete_warning": "This is a recurring expense template. Deleting it will stop all future recurring expenses.", "derived_delete_warning": "This expense is part of a recurrence. Only this expense will be deleted. Future recurring expenses will continue.", - "view_recurring_page": "View Recurring page" + "view_recurring_page": "View Recurring page", + "toggle_options": "Toggle recurring expense options" }, "ui": { "added_by": "Added by", @@ -433,6 +458,21 @@ "paid": "paid", "pay": "pay", "received": "received" + }, + "payer": { + "current_user": "You" + }, + "recipient": { + "current_user": "You" + }, + "statements": { + "you_lent": "You lent", + "you_owe": "You owe", + "you_lent_amount": "You lent {amount}", + "you_owe_amount": "You owe {amount}", + "you_pay_user": "You pay {user}", + "user_pays_you": "{user} pays you", + "user_pays_user": "{payer} pays {receiver}" } }, "expense_details": "Expense details", @@ -451,6 +491,55 @@ "share_text": "Check out SplitPro. It's an open source free alternative for Splitwise", "show_all_currencies": "Show all currencies", "today": "Today", - "in_group": "In group" + "in_group": "In group", + "transfer": "{senderRole, select, you {You pay {receiver}} other {{receiverRole, select, you {{sender} pays you} other {{sender} pays {receiver}}}}}" + }, + "friend": { + "delete_confirmation": { + "title": "Are you absolutely sure?", + "description": "Do you really want to continue?", + "outstanding_balance": "You cannot remove a friend with outstanding balances. Settle up first." + } + }, + "activity": { + "you_paid_for_expense": "You paid {amount} for {expense}", + "you_received_for_expense": "You received {amount} for {expense}", + "user_paid_for_expense": "{user} paid {amount} for {expense}", + "user_received_for_expense": "{user} received {amount} for {expense}", + "user_deleted_expense": "{actor, select, you {You deleted {expense}} other {{user} deleted {expense}}}", + "you_paid_amount": "You paid {amount}", + "you_received_amount": "You received {amount}" + }, + "balance_summary": { + "friend_owes_you": "{friend} owes you {amount}", + "you_owe_friend": "You owe {friend} {amount}", + "more": "{count, plural, one {# more balance} other {# more balances}}" + }, + "balance_list": { + "pays": "{payerRole, select, you {You pay {amount} to {receiver}} other {{payer} pays {amount} to {receiverRole, select, you {you} other {{receiver}}}}}", + "gets": "{receiverRole, select, you {You get {amount} from {payer}} other {{receiver} gets {amount} from {payerRole, select, you {you} other {{payer}}}}}" + }, + "expense_participant": { + "gets": "{actor, select, you { get } other { gets }}", + "owes": "{actor, select, you { owe } other { owes }}", + "paid": " paid ", + "received": " received " + }, + "expense_metadata": { + "edited_by": "{actor, select, you {Edited by you on {date}} other {Edited by {user} on {date}}}", + "deleted_by": "{actor, select, you {Deleted by you on {date}} other {Deleted by {user} on {date}}}", + "added_by": "{actor, select, you {Added by you on {date}} other {Added by {user} on {date}}}" + }, + "expense_list": { + "user_paid": "{actor, select, you {You paid {amount}} other {{user} paid {amount}}}", + "user_received": "{actor, select, you {You received {amount}} other {{user} received {amount}}}", + "settlement": "{payerRole, select, you {You paid {amount} to {receiver}} other {{receiverRole, select, you {{payer} paid you {amount}} other {{payer} paid {amount} to {receiver}}}}}", + "conversion_for_users": "Currency conversion for {payerRole, select, you {you} other {{payer}}} and {receiverRole, select, you {you} other {{receiver}}}" + }, + "notifications": { + "enable_title": "Enable notifications", + "enable_description": "Don't miss important events. Subscribe to receive notifications when expenses are added or updated.", + "permission_granted": "You will receive notifications now", + "subscribe": "Subscribe" } } diff --git a/public/locales/en/home.json b/public/locales/en/home.json index b2d1b5e77..1232d3f14 100644 --- a/public/locales/en/home.json +++ b/public/locales/en/home.json @@ -64,7 +64,11 @@ "subtitle_part1": "An", "subtitle_part2": "alternative to SplitWise", "title_highlight": "free", - "title_part1": "Split Expenses with your friends for" + "title_part1": "Split Expenses with your friends for", + "image_alt": "SplitPro app preview", + "title_before_highlight": "Split expenses with your friends for", + "title_after_highlight": ".", + "subtitle": "An open source alternative to SplitWise." }, "meta": { "description": "SplitPro: Split Expenses with your friends for free", diff --git a/public/locales/es-AR/common.json b/public/locales/es-AR/common.json index acb3038fd..4479273f4 100644 --- a/public/locales/es-AR/common.json +++ b/public/locales/es-AR/common.json @@ -97,7 +97,7 @@ "you_dativus": "A vos" }, "auth": { - "continue_with": "Continuar con {{provider}}", + "continue_with": "Continuar con {provider}", "email_placeholder": "Ingresá tu correo electrónico", "no_providers_configured": "No hay proveedores de autenticación configurados", "no_providers_instructions": "Por favor, configurá al menos un proveedor de autenticación para iniciar sesión. Consultá las", @@ -141,7 +141,7 @@ "group_name_update_failed": "Error al actualizar el nombre del grupo", "import_failed": "Error al importar el archivo", "invalid_cron_expression": "Expresión cron inválida", - "invalid_currency_code": "Código de moneda inválido {{code}}", + "invalid_currency_code": "Código de moneda inválido {code}", "language_change_failed": "Error al cambiar el idioma", "less_than": "El archivo debe ser menor que", "name_required": "El nombre es requerido", @@ -154,7 +154,7 @@ "request_error": "Error al solicitar notificaciones", "saving_expense": "Error al guardar el gasto", "setting_update_failed": "Error al actualizar la configuración", - "signin_error": "Ocurrió un error al iniciar sesión: ", + "signin_error": "Ocurrió un error al iniciar sesión: {error}", "signup_disabled": "El registro de nuevas cuentas está deshabilitado en esta instancia", "something_went_wrong": "Algo salió mal", "subscribe_error": "No se puede suscribir a las notificaciones", @@ -348,7 +348,7 @@ "edit_description": "Esto cambiará el cronograma de los gastos futuros. Los gastos existentes no se modificarán.", "edit_title": "Editar recurrencia", "empty": "Sin gastos recurrentes aún", - "expense_for_the_amount_of": "Gasto {{name}} por el monto de {{amount}} {{currency}}", + "expense_for_the_amount_of": "Gasto {name} por el monto de {amount} {currency}", "last_day": "Último día", "months": "Meses", "never": "Sin agenda configurada", diff --git a/public/locales/es-MX/common.json b/public/locales/es-MX/common.json index f85f66b06..a29da7dfc 100644 --- a/public/locales/es-MX/common.json +++ b/public/locales/es-MX/common.json @@ -97,7 +97,7 @@ "you_dativus": "A ti" }, "auth": { - "continue_with": "Continuar con {{provider}}", + "continue_with": "Continuar con {provider}", "email_placeholder": "Introduce tu correo electrónico", "no_providers_configured": "No hay proveedores de autenticación configurados", "no_providers_instructions": "Por favor, configure al menos un proveedor de autenticación para iniciar sesión. Consulte las", @@ -140,7 +140,7 @@ "group_name_update_failed": "Error al actualizar el nombre del grupo", "import_failed": "Error al importar el archivo", "invalid_cron_expression": "Expresión cron inválida", - "invalid_currency_code": "Código de moneda inválido {{code}}", + "invalid_currency_code": "Código de moneda inválido {code}", "language_change_failed": "Error al cambiar el idioma", "less_than": "El archivo debe ser menor que", "name_required": "El nombre es requerido", @@ -153,7 +153,7 @@ "request_error": "Error al solicitar notificaciones", "saving_expense": "Error al guardar el gasto", "setting_update_failed": "Error al actualizar la configuración", - "signin_error": "Ocurrió un error al iniciar sesión: ", + "signin_error": "Ocurrió un error al iniciar sesión: {error}", "signup_disabled": "El registro de nuevas cuentas está deshabilitado en esta instancia", "something_went_wrong": "Algo salió mal", "submit_error": "Error al enviar los comentarios", @@ -347,7 +347,7 @@ "edit_description": "", "edit_title": "", "empty": "No hay gastos recurrentes aún", - "expense_for_the_amount_of": "Gasto {{name}} por la cantidad de {{amount}} {{currency}}", + "expense_for_the_amount_of": "Gasto {name} por la cantidad de {amount} {currency}", "last_day": "", "months": "Meses", "never": "No hay horario configurado", diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 720c3488f..4cba9fb7f 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -97,7 +97,7 @@ "you_dativus": "A ti" }, "auth": { - "continue_with": "Continuar con {{provider}}", + "continue_with": "Continuar con {provider}", "email_placeholder": "Introduce tu correo electrónico", "no_providers_configured": "No hay proveedores de autenticación configurados", "no_providers_instructions": "Configure al menos un provedor de autenticación para iniciar sesión. Consulte las", @@ -140,7 +140,7 @@ "group_name_update_failed": "Error al actualizar el nombre del grupo", "import_failed": "Error al importar el archivo", "invalid_cron_expression": "Expresión \"cron\" inválida", - "invalid_currency_code": "Código de moneda inválido {{code}}", + "invalid_currency_code": "Código de moneda inválido {code}", "language_change_failed": "Error al cambiar de idioma", "less_than": "El archivo debe ser menor que", "name_required": "El nombre es obligatorio", @@ -153,7 +153,7 @@ "request_error": "Error al solicitar notificaciones", "saving_expense": "Error al guardar el gasto", "setting_update_failed": "Error al actualizar los ajustes", - "signin_error": "Error al iniciar sesión: ", + "signin_error": "Error al iniciar sesión: {error}", "signup_disabled": "El registro de nuevas cuentas está deshabilitado", "something_went_wrong": "Algo salió mal", "subscribe_error": "No se pudo suscribir a la notificación", @@ -344,7 +344,7 @@ "edit_description": "Esto modificará la programación de futuros gastos. Los gastos existentes no serán modificados.", "edit_title": "Editar Recurrencia", "empty": "Sin gastos recurrentes", - "expense_for_the_amount_of": "Gasto {{name}} por la cantidad de {{amount}} {{currency}}", + "expense_for_the_amount_of": "Gasto {name} por la cantidad de {amount} {currency}", "last_day": "Último día", "months": "Meses", "never": "Sin recurrencia", diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index f99b42ee5..d415f4037 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -108,7 +108,7 @@ "you_dativus": "Toi" }, "auth": { - "continue_with": "Continuer avec {{provider}}", + "continue_with": "Continuer avec {provider}", "email_placeholder": "Ton adresse mail", "no_providers_configured": "Aucun fournisseur d'authentification configuré", "no_providers_instructions": "Veuillez configurer au moins un fournisseur d'authentification pour vous connecter. Voir les", @@ -163,10 +163,10 @@ "image_compression_failed": "Échec de compression de l'image, tentative d'envoi de l'image originale", "import_failed": "Une erreur est survenue lors de l'import du fichier", "invalid_cron_expression": "Expression cron invalide", - "invalid_currency_code": "Le code de devise {{code}} est invalide", + "invalid_currency_code": "Le code de devise {code} est invalide", "invalid_split": "Indique qui a payé cette dépense avant d'enregistrer.", "language_change_failed": "Le changement de langage a échoué", - "less_than": "Le poids du fichier doit être inférieur à {{size}}Mo", + "less_than": "Le poids du fichier doit être inférieur à {size}Mo", "name_required": "Un nom est requis", "no_providers": "Une erreur est survenue lors du chargement des fournisseurs d'authentification. Vérifiez les logs du backend sur le serveur.", "notification_not_supported": "Notification non supportée", @@ -177,7 +177,7 @@ "request_error": "Une erreur est survenue lors du chargement des notifications", "saving_expense": "Un erreur est survenue lors de l'enregistrement de la dépense", "setting_update_failed": "Une erreur est survenue lors de la mise à jour des paramètres", - "signin_error": "Une erreur est survenue lors de la connexion : ", + "signin_error": "Une erreur est survenue lors de la connexion : {error}", "signup_disabled": "L'inscription de nouveaux comptes est désactivée sur cette instance", "something_went_wrong": "Une erreur est survenue", "subscribe_error": "Une erreur est survenue lors de l'abonnement aux notifications", @@ -203,9 +203,9 @@ "split_type_section": { "direction": { "no_money_flow": "Personne d'autre ne doit d'argent", - "owes_payer": "{{debtor}} doit cette somme à {{payer}}", - "owes_you": "{{debtor}} te doit", - "you_owe": "Ta dette envers {{payer}}" + "owes_payer": "{debtor} doit cette somme à {payer}", + "owes_you": "{debtor} te doit", + "you_owe": "Ta dette envers {payer}" }, "split_equally": "partagé à parts égales", "split_unequally": "partagé inégalement", @@ -382,7 +382,7 @@ "edit_description": "Ceci changera la programmation des futures dépenses sur ce modèle, mais les dépenses déjà créées ne seront pas modifiées.", "edit_title": "Modifier la récurrence", "empty": "Aucune dépense récurrente pour l'instant", - "expense_for_the_amount_of": "Dépense {{name}} pour un montant de {{amount}}{{currency}}", + "expense_for_the_amount_of": "Dépense {name} pour un montant de {amount}{currency}", "last_day": "Dernier jour", "months": "Mois", "never": "Aucun programme planifié", diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json index 51a660bb7..491aac2c5 100644 --- a/public/locales/hu/common.json +++ b/public/locales/hu/common.json @@ -108,7 +108,7 @@ "you_dativus": "Neked" }, "auth": { - "continue_with": "Folytatás ezzel: {{provider}}", + "continue_with": "Folytatás ezzel: {provider}", "email_placeholder": "Add meg az e-mail címed", "no_providers_configured": "Nincsenek konfigurált hitelesítési szolgáltatók", "no_providers_instructions": "Kérjük, konfigurálj legalább egy hitelesítési szolgáltatót a bejelentkezéshez. Lásd a", @@ -163,10 +163,10 @@ "image_compression_failed": "A képtömörítés sikertelen, eredeti fájl feltöltése", "import_failed": "Hiba a fájl importálása során", "invalid_cron_expression": "Érvénytelen cron kifejezés", - "invalid_currency_code": "Érvénytelen pénznemkód {{code}}", + "invalid_currency_code": "Érvénytelen pénznemkód {code}", "invalid_split": "Mentés előtt állítsd be, hogy ki tartozik ezzel a költséggel.", "language_change_failed": "Hiba a nyelv módosításakor", - "less_than": "A fájl mérete nem haladhatja meg a {{size}} MB-ot", + "less_than": "A fájl mérete nem haladhatja meg a {size} MB-ot", "name_required": "Név megadása kötelező", "no_providers": "Hiba történt a hitelesítési szolgáltatók lekérésekor. Kérjük, ellenőrizd a háttérnaplókat.", "notification_not_supported": "Az értesítések nem támogatottak", @@ -177,7 +177,7 @@ "request_error": "Hiba az értesítés kérése során", "saving_expense": "Hiba a költség mentése közben", "setting_update_failed": "Nem sikerült frissíteni a beállítást", - "signin_error": "Hiba történt bejelentkezés közben: ", + "signin_error": "Hiba történt bejelentkezés közben: {error}", "signup_disabled": "Új fiókok regisztrációja ebben az esetben le van tiltva", "something_went_wrong": "Valami hiba történt", "subscribe_error": "Nem lehet feliratkozni az értesítésekre", @@ -203,9 +203,9 @@ "split_type_section": { "direction": { "no_money_flow": "Senki sem tartozik semmivel", - "owes_payer": "{{debtor}} tartozik neki: {{payer}}", - "owes_you": "{{debtor}} tartozik neked", - "you_owe": "Te tartozol neki: {{payer}}" + "owes_payer": "{debtor} tartozik neki: {payer}", + "owes_you": "{debtor} tartozik neked", + "you_owe": "Te tartozol neki: {payer}" }, "split_equally": "egyenlően elosztva", "split_unequally": "egyenlőtlenül oszlik meg", @@ -382,7 +382,7 @@ "edit_description": "Ez megváltoztatja a jövőbeli kiadások ütemezését. A meglévő kiadások nem módosulnak.", "edit_title": "Ismétlődés szerkesztése", "empty": "Még nincsenek ismétlődő költségek", - "expense_for_the_amount_of": "{{name}} kiadás {{amount}} {{currency}} összegben", + "expense_for_the_amount_of": "{name} kiadás {amount} {currency} összegben", "last_day": "Utolsó nap", "months": "Hónapok", "never": "Nincs konfigurálva ütemezés", diff --git a/public/locales/id/common.json b/public/locales/id/common.json index 54c49e08b..c752ff57b 100644 --- a/public/locales/id/common.json +++ b/public/locales/id/common.json @@ -108,7 +108,7 @@ "you_dativus": "Anda" }, "auth": { - "continue_with": "Lanjutkan dengan {{provider}}", + "continue_with": "Lanjutkan dengan {provider}", "email_placeholder": "Masukkan email Anda", "no_providers_configured": "Tidak ada penyedia autentikasi yang dikonfigurasi", "no_providers_instructions": "Harap konfigurasikan setidaknya satu penyedia autentikasi untuk masuk. Lihat", @@ -162,9 +162,9 @@ "image_compression_failed": "Kompresi gambar gagal, mencoba mengunggah berkas asli", "import_failed": "Gagal mengimpor berkas", "invalid_cron_expression": "Ekspresi cron tidak valid", - "invalid_currency_code": "Kode mata uang tidak valid {{code}}", + "invalid_currency_code": "Kode mata uang tidak valid {code}", "language_change_failed": "Gagal mengubah bahasa", - "less_than": "Ukuran berkas harus kurang dari {{size}}MB", + "less_than": "Ukuran berkas harus kurang dari {size}MB", "name_required": "Nama wajib diisi", "no_providers": "Gagal saat mengambil penyedia autentikasi. Harap periksa log backend.", "notification_not_supported": "Notifikasi tidak didukung", @@ -175,7 +175,7 @@ "request_error": "Gagal meminta notifikasi", "saving_expense": "Gagal saat menyimpan pengeluaran", "setting_update_failed": "Gagal memperbarui pengaturan", - "signin_error": "Terjadi kesalahan saat masuk: ", + "signin_error": "Terjadi kesalahan saat masuk: {error}", "signup_disabled": "Pendaftaran akun baru dinonaktifkan pada instansi ini", "something_went_wrong": "Ada yang salah", "subscribe_error": "Tidak dapat berlangganan notifikasi", @@ -374,7 +374,7 @@ "edit_description": "Ini akan mengubah jadwal untuk pengeluaran di masa mendatang. Pengeluaran yang ada tidak akan diubah.", "edit_title": "Edit Pengulangan", "empty": "Belum ada pengeluaran berulang", - "expense_for_the_amount_of": "Pengeluaran {{name}} sebesar {{amount}} {{currency}}", + "expense_for_the_amount_of": "Pengeluaran {name} sebesar {amount} {currency}", "last_day": "Hari terakhir", "months": "Bulan", "never": "Tidak ada jadwal yang dikonfigurasi", diff --git a/public/locales/it/common.json b/public/locales/it/common.json index 81c79e909..e9bd52d15 100644 --- a/public/locales/it/common.json +++ b/public/locales/it/common.json @@ -97,7 +97,7 @@ "you_dativus": "Te" }, "auth": { - "continue_with": "Continua con {{provider}}", + "continue_with": "Continua con {provider}", "email_placeholder": "Inserisci la tua email", "no_providers_configured": "Nessun provider di autenticazione configurato", "no_providers_instructions": "Si prega di configurare almeno un provider di autenticazione per accedere. Consulta le", @@ -142,7 +142,7 @@ "group_name_update_failed": "Errore nell'aggiornamento del nome del gruppo", "import_failed": "Errore durante l'importazione del file", "invalid_cron_expression": "Espressione cron non valida", - "invalid_currency_code": "Codice valuta {{code}} non valido", + "invalid_currency_code": "Codice valuta {code} non valido", "language_change_failed": "Errore nel cambiare lingua", "less_than": "Il file deve essere più piccolo di", "name_required": "Il nome è obbligatorio", @@ -155,7 +155,7 @@ "request_error": "Errore durante la richiesta delle notifiche", "saving_expense": "Errore durante il salvataggio della spesa", "setting_update_failed": "Errore nell'aggiornamento dell'impostazione", - "signin_error": "Si è verificato un errore durante l'accesso: ", + "signin_error": "Si è verificato un errore durante l'accesso: {error}", "signup_disabled": "La registrazione di nuovi account è disabilitata in questa istanza", "something_went_wrong": "Qualcosa è andato storto", "subscribe_error": "Impossibile attivare le notifiche", @@ -349,7 +349,7 @@ "edit_description": "Questo cambierà il programma delle spese future. Le spese già create non saranno modificate.", "edit_title": "Modifica ricorrenza", "empty": "Ancora nessuna spesa ricorrente", - "expense_for_the_amount_of": "Spesa {{name}} di {{amount}} {{currency}}", + "expense_for_the_amount_of": "Spesa {name} di {amount} {currency}", "last_day": "Ultimo giorno", "months": "Mesi", "never": "Nessuna pianificazione configurata", diff --git a/public/locales/ne/common.json b/public/locales/ne/common.json index 89a5bc85d..90378e6ff 100644 --- a/public/locales/ne/common.json +++ b/public/locales/ne/common.json @@ -106,7 +106,7 @@ "you_dativus": "तपाईंका लागि" }, "auth": { - "continue_with": "{{provider}} बाट अगाडि बढ्नुहोस्", + "continue_with": "{provider} बाट अगाडि बढ्नुहोस्", "email_placeholder": "इमेल ठेगाना", "no_providers_configured": "कुनै प्रदायक कन्फिगर गरिएको छैन", "no_providers_instructions": "कृपया लगइन प्रदायकहरू सेट अप गर्नुहोस्", @@ -160,9 +160,9 @@ "image_compression_failed": "छवि कम्प्रेसन असफल भयो", "import_failed": "आयात असफल भयो", "invalid_cron_expression": "अमान्य क्रोन अभिव्यक्ति", - "invalid_currency_code": "अमान्य मुद्रा कोड {{code}}", + "invalid_currency_code": "अमान्य मुद्रा कोड {code}", "language_change_failed": "भाषा परिवर्तन गर्न असफल", - "less_than": "File {{size}}MB भन्दा कम", + "less_than": "File {size}MB भन्दा कम", "name_required": "नाम आवश्यक छ", "no_providers": "प्रमाणीकरण प्रदायकहरू (authentication providers) प्राप्त गर्दा त्रुटि भयो। कृपया ब्याकइन्ड लगहरू जाँच गर्नुहोस्।", "notification_not_supported": "यो ब्राउजरमा सूचना समर्थन गर्दैन", @@ -173,7 +173,7 @@ "request_error": "अनुरोधमा त्रुटि", "saving_expense": "खर्च बचत गर्दा त्रुटि", "setting_update_failed": "सेटिङ अपडेट गर्न असफल", - "signin_error": "साइन इन गर्दा त्रुटि ", + "signin_error": "साइन इन गर्दा त्रुटि {error}", "signup_disabled": "साइन अप असक्षम गरिएको छ", "something_went_wrong": "केही गलत भयो", "subscribe_error": "सबस्क्राइब गर्दा त्रुटि", @@ -369,7 +369,7 @@ "edit_description": "यसले भविष्यका खर्चहरूको तालिका (schedule) परिवर्तन गर्नेछ। हाल भइरहेका खर्चहरूमा कुनै परिमार्जन हुने छैन।", "edit_title": "आवर्ती खर्च सम्पादन गर्नुहोस्", "empty": "कुनै आवर्ती खर्च छैन", - "expense_for_the_amount_of": "खर्च {{name}}, {{amount}} {{currency}} को रकमको लागि", + "expense_for_the_amount_of": "खर्च {name}, {amount} {currency} को रकमको लागि", "last_day": "अन्तिम दिन", "months": "महिनाहरू", "never": "कहिल्यै पनि", diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index 3701cf232..5bef79d58 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -108,7 +108,7 @@ "you_dativus": "Jou" }, "auth": { - "continue_with": "Doorgaan met {{provider}}", + "continue_with": "Doorgaan met {provider}", "email_placeholder": "Voer je e-mailadres in", "no_providers_configured": "Geen authenticatieproviders geconfigureerd", "no_providers_instructions": "Configureer ten minste één authenticatieprovider om in te loggen. Zie de", @@ -162,10 +162,10 @@ "image_compression_failed": "Afbeeldingscompressie mislukt, origineel bestand wordt geüpload", "import_failed": "Fout bij importeren bestand", "invalid_cron_expression": "Ongeldige cron-uitdrukking", - "invalid_currency_code": "Ongeldige valutacode {{code}}", + "invalid_currency_code": "Ongeldige valutacode {code}", "invalid_split": "Bepaal wie deze kosten moet betalen voordat je het opslaat.", "language_change_failed": "Fout bij taalwijziging", - "less_than": "Bestand moet minder dan {{size}} MB zijn", + "less_than": "Bestand moet minder dan {size} MB zijn", "name_required": "Naam is vereist", "no_providers": "Fout bij het ophalen van authenticatieproviders. Controleer de backend-logbestanden.", "notification_not_supported": "Notificaties worden niet ondersteund", @@ -176,7 +176,7 @@ "request_error": "Fout bij ophalen notificatie", "saving_expense": "Fout tijdens het opslaan van de uitgave", "setting_update_failed": "Instelling bijwerken mislukt", - "signin_error": "Een fout deed zich voor bij het inloggen: ", + "signin_error": "Een fout deed zich voor bij het inloggen: {error}", "signup_disabled": "Het registreren van nieuwe accounts is uitgeschakeld op deze instantie", "something_went_wrong": "Er is iets misgegaan", "subscribe_error": "Aanmelden voor meldingen niet mogelijk", @@ -202,9 +202,9 @@ "split_type_section": { "direction": { "no_money_flow": "Niemand anders is iets verschuldigd", - "owes_payer": "{{debtor}} is {{payer}} verschuldigd", - "owes_you": "{{debtor}} is je geld verschuldigd", - "you_owe": "Je bent {{payer}} geld verschuldigd" + "owes_payer": "{debtor} is {payer} verschuldigd", + "owes_you": "{debtor} is je geld verschuldigd", + "you_owe": "Je bent {payer} geld verschuldigd" }, "split_equally": "gelijk verdelen", "split_unequally": "ongelijk verdelen", @@ -381,7 +381,7 @@ "edit_description": "Dit zal de planning voor toekomstige uitgaven veranderen. Bestaande uitgaven blijven ongewijzigd.", "edit_title": "Herhaling bewerken", "empty": "Nog geen terugkerende uitgaven", - "expense_for_the_amount_of": "Uitgave {{name}} voor het bedrag van {{amount}} {{currency}}", + "expense_for_the_amount_of": "Uitgave {name} voor het bedrag van {amount} {currency}", "last_day": "Laatste dag", "months": "Maanden", "never": "Geen planning ingesteld", diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json index 99a648619..e059a8095 100644 --- a/public/locales/pl/common.json +++ b/public/locales/pl/common.json @@ -108,7 +108,7 @@ "you_dativus": "Ciebie" }, "auth": { - "continue_with": "Kontynuuj z {{provider}}", + "continue_with": "Kontynuuj z {provider}", "email_placeholder": "Wprowadź swój email", "no_providers_configured": "Brak skonfigurowanych dostawców uwierzytelniania", "no_providers_instructions": "Skonfiguruj przynajmniej jednego dostawcę uwierzytelniania, aby się zalogować. Zobacz", @@ -165,9 +165,9 @@ "image_compression_failed": "Kompresja obrazu nie powiodła się. Wczytuję oryginalny plik", "import_failed": "Błąd podczas importowania pliku", "invalid_cron_expression": "Błędne wyrażenie cron", - "invalid_currency_code": "Nieprawidłowy kod waluty {{code}}", + "invalid_currency_code": "Nieprawidłowy kod waluty {code}", "language_change_failed": "Błąd podczas zmiany języka", - "less_than": "Plik powinien być mniejszy niż {{size}}MB", + "less_than": "Plik powinien być mniejszy niż {size}MB", "name_required": "Nazwa jest wymagana", "no_providers": "Błąd podczas pobierania dostawców uwierzytelniania. Sprawdź logi.", "notification_not_supported": "Powiadomienia nie są obsługiwane", @@ -178,7 +178,7 @@ "request_error": "Błąd podczas żądania powiadomienia", "saving_expense": "Błąd podczas zapisywania wydatku", "setting_update_failed": "Nie udało się zaktualizować ustawienia", - "signin_error": "Wystąpił błąd podczas logowania: ", + "signin_error": "Wystąpił błąd podczas logowania: {error}", "signup_disabled": "Rejestracja nowych kont jest wyłączona w tej instancji", "something_went_wrong": "Coś poszło nie tak", "subscribe_error": "Nie można zasubskrybować powiadomień", @@ -386,7 +386,7 @@ "edit_description": "Spowoduje zmianę harmonogramu przyszłych wydatków. Istniejące wydatki nie zostaną zmienione.", "edit_title": "Edytuj cykliczność", "empty": "Brak wydatków cyklicznych", - "expense_for_the_amount_of": "Wydatek {{name}} w wysokości {{amount}} {{currency}}", + "expense_for_the_amount_of": "Wydatek {name} w wysokości {amount} {currency}", "last_day": "Ostatni dzień", "months": "Miesiące", "never": "Brak harmonogramu", diff --git a/public/locales/pt-BR/common.json b/public/locales/pt-BR/common.json index 8302ea679..34ae8bd17 100644 --- a/public/locales/pt-BR/common.json +++ b/public/locales/pt-BR/common.json @@ -108,7 +108,7 @@ "you_dativus": "Você" }, "auth": { - "continue_with": "Continuar com {{provider}}", + "continue_with": "Continuar com {provider}", "email_placeholder": "Insira seu e-mail", "no_providers_configured": "Nenhum provedor de autenticação configurado", "no_providers_instructions": "Por favor, configure pelo menos um provedor de autenticação para fazer login. Consulte as", @@ -162,9 +162,9 @@ "image_compression_failed": "A compressão da imagem falhou. Tentando enviar o arquivo original", "import_failed": "Erro ao importar arquivo", "invalid_cron_expression": "Expressão cron inválida", - "invalid_currency_code": "Moeda inválida com código {{code}}", + "invalid_currency_code": "Moeda inválida com código {code}", "language_change_failed": "Erro ao alterar idioma", - "less_than": "O arquivo deve ter menos de {{size}}MB", + "less_than": "O arquivo deve ter menos de {size}MB", "name_required": "O nome é obrigatório", "no_providers": "Erro ao buscar provedores de autenticação. Verifique os logs do backend.", "notification_not_supported": "Sem suporte a notificações", @@ -175,7 +175,7 @@ "request_error": "Erro ao solicitar notificação", "saving_expense": "Erro ao salvar despesa", "setting_update_failed": "Falha ao atualizar configuração", - "signin_error": "Erro ao fazer login: ", + "signin_error": "Erro ao fazer login: {error}", "signup_disabled": "Esta instância não permite criação de novas contas", "something_went_wrong": "Algo deu errado", "subscribe_error": "Não foi possível inscrever-se nas notificações", @@ -374,7 +374,7 @@ "edit_description": "Isso vai mudar a programação das despesas futuras. As despesas existentes não serão modificadas.", "edit_title": "Editar Recorrência", "empty": "Nenhuma despesa recorrente ainda", - "expense_for_the_amount_of": "Despesa {{name}} pelo valor de {{amount}} {{currency}}", + "expense_for_the_amount_of": "Despesa {name} pelo valor de {amount} {currency}", "last_day": "Último dia", "months": "Meses", "never": "Nenhum pagamento recorrente configurado", diff --git a/public/locales/pt-PT/common.json b/public/locales/pt-PT/common.json index 91d8f6b6d..4546a08c1 100644 --- a/public/locales/pt-PT/common.json +++ b/public/locales/pt-PT/common.json @@ -108,7 +108,7 @@ "you_dativus": "Tu" }, "auth": { - "continue_with": "Continuar com {{provider}}", + "continue_with": "Continuar com {provider}", "email_placeholder": "Inserir email", "no_providers_configured": "Nenhum fornecedor de autenticação configurado", "no_providers_instructions": "Por favor, configura pelo menos um fornecedor de autenticação para iniciar sessão. Consulta as", @@ -164,10 +164,10 @@ "image_compression_failed": "A compressão da imagem falhou. Tentar a enviar o ficheiro original", "import_failed": "Error ao importar ficheiro", "invalid_cron_expression": "Expressão \"cron\" inválida", - "invalid_currency_code": "Código {{code}} de moeda inválido", + "invalid_currency_code": "Código {code} de moeda inválido", "invalid_split": "Define quem deve esta despesa antes de guardar.", "language_change_failed": "Erro ao alterar idioma", - "less_than": "O ficheiro deve ter menos que {{size}}MB", + "less_than": "O ficheiro deve ter menos que {size}MB", "name_required": "O nome é obrigatório", "no_providers": "Ocorreu um erro ao transferir os fornecedores de autenticação. Por favor consulta os logs do backend.", "notification_not_supported": "Notificação não suportada", @@ -178,7 +178,7 @@ "request_error": "Erro requisitando notificação", "saving_expense": "Erro ao salvar despesa", "setting_update_failed": "Falha ao atualizar a configuração", - "signin_error": "Ocorreu um erro durante a autenticação: ", + "signin_error": "Ocorreu um erro durante a autenticação: {error}", "signup_disabled": "Criação de novas contas encontra-se desativo para esta instância", "something_went_wrong": "Algo deu errado", "subscribe_error": "Não foi possivel subscrever notificações", @@ -205,9 +205,9 @@ "split_type_section": { "direction": { "no_money_flow": "Mais ninguém deve nada", - "owes_payer": "{{debtor}} deve a {{payer}}", - "owes_you": "{{debtor}} deve-te", - "you_owe": "Tu deves a {{payer}}" + "owes_payer": "{debtor} deve a {payer}", + "owes_you": "{debtor} deve-te", + "you_owe": "Tu deves a {payer}" }, "split_equally": "dividir igualmente", "split_unequally": "dividir desigualmente", @@ -386,7 +386,7 @@ "edit_description": "Isto vai mudar a programação das despesas futuras. As despesas existentes não serão modificadas.", "edit_title": "Editar Recorrência", "empty": "Nenhuma despesa recorrente ainda", - "expense_for_the_amount_of": "Despesa {{name}} pelo valor de {{amount}} {{currency}}", + "expense_for_the_amount_of": "Despesa {name} pelo valor de {amount} {currency}", "last_day": "Último dia", "months": "Meses", "never": "Nenhum pagamento recorrente configurado", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 5d1926524..7250b1ec3 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -96,7 +96,7 @@ "you_dativus": "Вы" }, "auth": { - "continue_with": "Продолжить с {{provider}}", + "continue_with": "Продолжить с {provider}", "email_placeholder": "Введите свой имейл", "no_providers_configured": "Провайдеры аутентификации не настроены", "no_providers_instructions": "Пожалуйста, настройте хотя бы одного провайдера аутентификации для входа в систему. Смотрите", @@ -123,7 +123,7 @@ "feedback_required": "Требуется отзыв", "group_name_update_failed": "Ошибка при обновлении названия группы", "import_failed": "Ошибка при импорте файла", - "invalid_currency_code": "Неправильный код валюты {{code}}", + "invalid_currency_code": "Неправильный код валюты {code}", "language_change_failed": "Ошибка смены языка", "no_providers": "Ошибка при получении данных о провадерах аутентификации. Пожалуйста, проверьте логи бэкэнда." } diff --git a/public/locales/sk/common.json b/public/locales/sk/common.json index e775b9af5..a675e5199 100644 --- a/public/locales/sk/common.json +++ b/public/locales/sk/common.json @@ -97,7 +97,7 @@ "you_dativus": "Tebe" }, "auth": { - "continue_with": "Pokračuj s {{provider}}", + "continue_with": "Pokračuj s {provider}", "email_placeholder": "Zadaj svoj e-mail", "no_providers_configured": "Nie sú nakonfigurovaný žiadny poskytovatelia overovania", "no_providers_instructions": "Prosím nakonfigurujte si aspoň jedného poskytovateľa overovania na prihlásenie. Pozrite si", @@ -140,7 +140,7 @@ "group_name_update_failed": "Chyba pri zmene mena skupiny", "import_failed": "Chyba pri importe súboru", "invalid_cron_expression": "Neplatná cron výraz", - "invalid_currency_code": "Neplatný kód meny {{code}}", + "invalid_currency_code": "Neplatný kód meny {code}", "language_change_failed": "Chyba pri zmene jazyka", "less_than": "Súbor by mal byť menší ako", "name_required": "Meno je povinné", @@ -153,7 +153,7 @@ "request_error": "Chyba pri vyžiadaní upozornenia", "saving_expense": "Chyba pri ukladaní výdavku", "setting_update_failed": "Zlyhala zmena nastavení", - "signin_error": "Nastala chyba počas prihlasovania: ", + "signin_error": "Nastala chyba počas prihlasovania: {error}", "signup_disabled": "Registrácia nových uživateľov je zakázaná", "something_went_wrong": "Niečo sa pokazilo", "subscribe_error": "Nedá sa prihlásiť na odber upozornení", @@ -344,7 +344,7 @@ "edit_description": "Týmto sa zmení rozvrh budúcich výdavkov. Existujúce výdavky sa neupravia.", "edit_title": "Upraviť opakovanie", "empty": "Zatiaľ nie sú žiadne opakujúce sa výdavky", - "expense_for_the_amount_of": "Výdavok {{name}} vo výške {{amount}} {{currency}}", + "expense_for_the_amount_of": "Výdavok {name} vo výške {amount} {currency}", "last_day": "Posledný deň", "months": "Mesiace", "never": "Nie je nakonfigurovaný žiadny plán", diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json index 5799b83dd..cda94d42f 100644 --- a/public/locales/sv/common.json +++ b/public/locales/sv/common.json @@ -108,7 +108,7 @@ "you_dativus": "Dig" }, "auth": { - "continue_with": "Fortsätt med {{provider}}", + "continue_with": "Fortsätt med {provider}", "email_placeholder": "Ange din email", "no_providers_configured": "Inga autentiseringsleverantörer konfigurerade", "no_providers_instructions": "Vänligen konfigurera minst en autentiseringsleverantör för att logga in. Se", @@ -163,10 +163,10 @@ "image_compression_failed": "Bildkomprimering misslyckades, försöker ladda upp originalfilen", "import_failed": "Fel vid import", "invalid_cron_expression": "Ogiltigt cron‑uttryck", - "invalid_currency_code": "Ogiltig valutakod {{code}}", + "invalid_currency_code": "Ogiltig valutakod {code}", "invalid_split": "Justera vem som står för denna utgift innan du sparar.", "language_change_failed": "Fel vid ändring av språk", - "less_than": "Filen ska vara mindre än {{size}}MB", + "less_than": "Filen ska vara mindre än {size}MB", "name_required": "Namn krävs", "no_providers": "Fel vid hämtning av autentiseringsleverantörer. Kontrollera serverloggarna.", "notification_not_supported": "Notifikationer stöds inte", @@ -177,7 +177,7 @@ "request_error": "Fel vid begäran om notifikationer", "saving_expense": "Fel vid sparande av utgift", "setting_update_failed": "Misslyckades med att uppdatera inställning", - "signin_error": "Ett fel uppstod vid inloggning: ", + "signin_error": "Ett fel uppstod vid inloggning: {error}", "signup_disabled": "Registrering av nya konton är inaktiverat", "something_went_wrong": "Något gick fel", "subscribe_error": "Kan inte prenumerera på notifikationer", @@ -203,9 +203,9 @@ "split_type_section": { "direction": { "no_money_flow": "Ingen annan är skyldig något", - "owes_payer": "{{debtor}} är skyldig {{payer}}", - "owes_you": "{{debtor}} är skyldig dig", - "you_owe": "Du är skyldig {{payer}}" + "owes_payer": "{debtor} är skyldig {payer}", + "owes_you": "{debtor} är skyldig dig", + "you_owe": "Du är skyldig {payer}" }, "split_equally": "dela lika", "split_unequally": "dela ojämnt", @@ -382,7 +382,7 @@ "edit_description": "Detta kommer att ändra schemat för framtida utgifter. Befintliga utgifter kommer inte att ändras.", "edit_title": "Redigera återkommande händelse", "empty": "Inga återkommande utgifter", - "expense_for_the_amount_of": "Utgift {{name}} på beloppet {{amount}} {{currency}}", + "expense_for_the_amount_of": "Utgift {name} på beloppet {amount} {currency}", "last_day": "Sista dagen", "months": "Månader", "never": "Ingen schemaläggning är inställd", diff --git a/public/locales/zh-Hans/common.json b/public/locales/zh-Hans/common.json index 5d1558d13..f3e606f09 100644 --- a/public/locales/zh-Hans/common.json +++ b/public/locales/zh-Hans/common.json @@ -108,7 +108,7 @@ "you_dativus": "你" }, "auth": { - "continue_with": "使用 {{provider}} 继续", + "continue_with": "使用 {provider} 继续", "email_placeholder": "输入邮箱", "no_providers_configured": "尚未配置身份验证方式", "no_providers_instructions": "请至少配置一种身份验证方式后再登录。详见", @@ -163,10 +163,10 @@ "image_compression_failed": "图片压缩失败,正在尝试上传原始文件", "import_failed": "导入文件时出错", "invalid_cron_expression": "无效的 Cron 表达式", - "invalid_currency_code": "无效的货币代码 {{code}}", + "invalid_currency_code": "无效的货币代码 {code}", "invalid_split": "请调整这笔支出由谁承担后再保存。", "language_change_failed": "更改语言时出错", - "less_than": "文件大小应小于 {{size}}MB", + "less_than": "文件大小应小于 {size}MB", "name_required": "请输入姓名", "no_providers": "获取身份验证方式时出错,请检查后端日志。", "notification_not_supported": "当前不支持通知功能", @@ -177,7 +177,7 @@ "request_error": "请求通知权限时出错", "saving_expense": "保存支出时出错", "setting_update_failed": "更新设置失败", - "signin_error": "登录时发生错误: ", + "signin_error": "登录时发生错误: {error}", "signup_disabled": "此实例已关闭新账户注册", "something_went_wrong": "出了点问题", "subscribe_error": "无法订阅通知", @@ -203,9 +203,9 @@ "split_type_section": { "direction": { "no_money_flow": "其他人无需承担", - "owes_payer": "{{debtor}} 欠 {{payer}}", - "owes_you": "{{debtor}} 欠你", - "you_owe": "你欠 {{payer}}" + "owes_payer": "{debtor} 欠 {payer}", + "owes_you": "{debtor} 欠你", + "you_owe": "你欠 {payer}" }, "split_equally": "均分", "split_unequally": "自定义分摊", @@ -382,7 +382,7 @@ "edit_description": "修改后将更改未来支出的重复周期,已有支出不会受到影响。", "edit_title": "编辑定期设置", "empty": "暂无定期支出", - "expense_for_the_amount_of": "支出 {{name}}:{{amount}} {{currency}}", + "expense_for_the_amount_of": "支出 {name}:{amount} {currency}", "last_day": "最后一天", "months": "月份", "never": "未设置重复周期", diff --git a/public/locales/zh_Hans/currencies.json b/public/locales/zh-Hans/currencies.json similarity index 100% rename from public/locales/zh_Hans/currencies.json rename to public/locales/zh-Hans/currencies.json diff --git a/src/components/Account/SubmitFeedback.tsx b/src/components/Account/SubmitFeedback.tsx index abfe4c848..714d40113 100644 --- a/src/components/Account/SubmitFeedback.tsx +++ b/src/components/Account/SubmitFeedback.tsx @@ -36,10 +36,12 @@ export const SubmitFeedback: React.FC = () => { try { await submitFeedbackMutation.mutateAsync({ feedback: values.feedback }); feedbackForm.reset(); - toast.success(t('ui.messages.submit_success'), { duration: 1500 }); + toast.success(t('account.submit_feedback_details.messages.submit_success'), { + duration: 1500, + }); } catch (e) { console.info(e); - toast.error(t('ui.messages.submit_error')); + toast.error(t('account.submit_feedback_details.messages.submit_error')); } setFeedbackOpen(false); }, diff --git a/src/components/AddExpense/AddExpensePage.tsx b/src/components/AddExpense/AddExpensePage.tsx index 86eec7494..19ab15d4d 100644 --- a/src/components/AddExpense/AddExpensePage.tsx +++ b/src/components/AddExpense/AddExpensePage.tsx @@ -184,7 +184,7 @@ export const AddOrEditExpensePage: React.FC<{ if (error instanceof Error) { toast.error(error.message); } else { - toast.error('An unexpected error occurred while submitting the expense.'); + toast.error(t('errors.expense_submission_failed')); } } }, [ @@ -208,6 +208,7 @@ export const AddOrEditExpensePage: React.FC<{ cronExpression, multipleTransactions, setSingleTransaction, + t, update, ]); @@ -336,7 +337,9 @@ export const AddOrEditExpensePage: React.FC<{ className="text-primary h-8 max-w-full min-w-0 justify-start px-1.5 py-0 text-base sm:max-w-none" > - {displayName(paidBy, currentUser?.id, 'dativus')} + {paidBy?.id === currentUser?.id + ? t(`ui.expense.${isNegative ? 'recipient' : 'payer'}.current_user`) + : displayName(paidBy)} @@ -395,7 +398,7 @@ export const AddOrEditExpensePage: React.FC<{ 'size-6', )} /> - Toggle recurring expense options + {t('recurrence.toggle_options')} )} diff --git a/src/components/AddExpense/SelectUserOrGroup.tsx b/src/components/AddExpense/SelectUserOrGroup.tsx index 765dd9cd8..c81760909 100644 --- a/src/components/AddExpense/SelectUserOrGroup.tsx +++ b/src/components/AddExpense/SelectUserOrGroup.tsx @@ -250,7 +250,12 @@ export const SelectUserOrGroup: React.FC<{ {0 === filteredFriends?.length && 0 === filteredGroups?.length ? (
- empty user image + {t('expense_details.add_expense_details.select_user_or_group.empty_image_alt')}
) : null} diff --git a/src/components/Expense/BalanceList.tsx b/src/components/Expense/BalanceList.tsx index 311ab2992..a229a68eb 100644 --- a/src/components/Expense/BalanceList.tsx +++ b/src/components/Expense/BalanceList.tsx @@ -47,7 +47,7 @@ export const BalanceList: React.FC<{ total: {}, }; console.error('BalanceList: userId not found in users list', balance.userId); - toast.error(t('common:errors.group_balances_malformed')); + toast.error(t('errors.group_balances_malformed')); } if (!res[balance.userId]!.balances[balance.friendId]) { res[balance.userId]!.balances[balance.friendId] = {}; @@ -80,21 +80,20 @@ export const BalanceList: React.FC<{
- {displayName(user, userQuery.data?.id)} {Object.values(total).every((amount) => 0n === amount) ? ( - {' '} - {isCurrentUser - ? t('expense_details.balance_list.are_settled_up') - : t('expense_details.balance_list.is_settled_up')} + {t('expense_details.balance_list.settled', { + actor: isCurrentUser ? 'you' : 'other', + person: displayName(user), + })} ) : ( <> - {' '} - {t( - `ui.expense.${isCurrentUser ? 'you' : 'user'}.${0 < totalAmount[1] ? 'lent' : 'owe'}`, - )}{' '} + {t(`expense_details.balance_list.${0 < totalAmount[1] ? 'lent' : 'owes'}`, { + actor: isCurrentUser ? 'you' : 'other', + person: displayName(user), + })}{' '}
- {displayName(friend, userQuery.data?.id)} - - {' '} - {t( - `ui.expense.${friend.id === userQuery.data?.id ? 'you' : 'user'}.${0 > amount ? 'get' : 'pay'}`, - )}{' '} - - - {getCurrencyHelpersCached(currency).toUIString(BigMath.abs(amount))} - - - {' '} - {t(`ui.expense.${0 < amount ? 'to' : 'from'}`, { - ns: 'common', - })}{' '} - - - {displayName(user, userQuery.data?.id, 'accusativus')} - + {t(0 < amount ? 'balance_list.pays' : 'balance_list.gets', { + payerRole: + (0 < amount ? friend : user).id === userQuery.data?.id + ? 'you' + : 'other', + receiverRole: + (0 < amount ? user : friend).id === userQuery.data?.id + ? 'you' + : 'other', + payer: displayName(0 < amount ? friend : user), + receiver: displayName(0 < amount ? user : friend), + amount: getCurrencyHelpersCached(currency).toUIString( + BigMath.abs(amount), + ), + })}
diff --git a/src/components/Expense/ConvertibleBalance.tsx b/src/components/Expense/ConvertibleBalance.tsx index 666ecc921..c3c518bb5 100644 --- a/src/components/Expense/ConvertibleBalance.tsx +++ b/src/components/Expense/ConvertibleBalance.tsx @@ -281,7 +281,7 @@ const AmountDisplay: React.FC<{ className, )} > - {t('actors.you')} {t(`ui.expense.you.${isPositive ? 'lent' : 'owe'}`)} + {t(`ui.expense.statements.${isPositive ? 'you_lent' : 'you_owe'}`)}
)} diff --git a/src/components/Expense/CumulatedBalances.tsx b/src/components/Expense/CumulatedBalances.tsx index 3e3557594..3dd603d5c 100644 --- a/src/components/Expense/CumulatedBalances.tsx +++ b/src/components/Expense/CumulatedBalances.tsx @@ -39,7 +39,7 @@ export const CumulatedBalances: React.FC<{ ) : ( <> = ({ expenseId, recurre toast.error(`Error: ${error.message}`); } else { console.error('Unexpected error:', error); - toast.error('An unexpected error occurred while deleting the expense.'); + toast.error(t('errors.expense_deletion_failed')); } return; } router.back(); - }, [expenseId, deleteExpenseMutation, router]); + }, [expenseId, deleteExpenseMutation, router, t]); const description = useMemo(() => { if (!isPartOfRecurrence) { diff --git a/src/components/Expense/EditRecurrenceDialog.tsx b/src/components/Expense/EditRecurrenceDialog.tsx index ae6f7708e..3dfdd81cd 100644 --- a/src/components/Expense/EditRecurrenceDialog.tsx +++ b/src/components/Expense/EditRecurrenceDialog.tsx @@ -84,6 +84,7 @@ export const EditRecurrenceDialog: React.FC = ({ = ({ user, expense }) => { ) : null} {expense.updatedByUser ? (

- {t('ui.edited_by')} {displayName(expense.updatedByUser, user.id, 'dativus')}{' '} - {t('ui.on')} {toUIDate(expense.updatedAt, { year: true })} + {t('expense_metadata.edited_by', { + actor: expense.updatedByUser.id === user.id ? 'you' : 'other', + user: displayName(expense.updatedByUser), + date: toUIDate(expense.updatedAt, { year: true }), + })}

) : null} {expense.deletedByUser ? (

- {t('ui.deleted_by')} {displayName(expense.deletedByUser, user.id, 'dativus')}{' '} - {t('ui.on')} {toUIDate(expense.deletedAt ?? expense.createdAt, { year: true })} + {t('expense_metadata.deleted_by', { + actor: expense.deletedByUser.id === user.id ? 'you' : 'other', + user: displayName(expense.deletedByUser), + date: toUIDate(expense.deletedAt ?? expense.createdAt, { year: true }), + })}

) : (

- {t('ui.added_by')} {displayName(expense.addedByUser, user.id, 'dativus')}{' '} - {t('ui.on')} {toUIDate(expense.createdAt, { year: true })} + {t('expense_metadata.added_by', { + actor: expense.addedByUser.id === user.id ? 'you' : 'other', + user: displayName(expense.addedByUser), + date: toUIDate(expense.createdAt, { year: true }), + })}

)} {expense.recurrence ? ( @@ -117,24 +128,34 @@ const ExpenseDetails: React.FC = ({ user, expense }) => {
- expense.amount ? 'expense_participant.received' : 'expense_participant.paid' } - > - - - - {t( - `ui.expense.${expense.paidByUser.id === user.id ? 'you' : 'user'}.${expense.amount < 0 ? 'received' : 'paid'}`, - )} - - 0 ? 'text-negative' : 'text-positive'}> - {toUIString(expense.amount)} - + values={{ actor: expense.paidByUser.id === user.id ? 'you' : 'other' }} + components={{ + person: ( + + + + ), + amount: ( + + {toUIString(expense.amount)} + + ), + }} + />
{expense.expenseParticipants @@ -180,16 +201,22 @@ const ExpenseParticipantEntry: React.FC<{ return (
- - - - - {t(`ui.expense.${isCurrentUser ? 'you' : 'user'}.${isPositive ? 'get' : 'owe'}`)} - - {toUIString(participant.amount)} + + + + ), + amount: {toUIString(participant.amount)}, + }} + />
); }; @@ -261,6 +288,7 @@ export const EditCurrencyConversion: React.FC<{ expense: ExpenseDetailsOutput }> export const EditSettlement: React.FC<{ expense: ExpenseDetailsOutput }> = ({ expense }) => { const { displayName, t, getCurrencyHelpersCached } = useTranslationWithUtils(); + const { data: session } = useSession(); const sender = expense.paidByUser; const receiver = expense.expenseParticipants.find((p) => p.userId !== expense.paidBy)?.user; @@ -352,7 +380,12 @@ export const EditSettlement: React.FC<{ expense: ExpenseDetailsOutput }> = ({ ex

- {displayName(sender)} {t('ui.expense.user.pay')} {displayName(receiver)} + {t('ui.transfer', { + senderRole: sender.id === session?.user.id ? 'you' : 'other', + receiverRole: receiver.id === session?.user.id ? 'you' : 'other', + sender: displayName(sender), + receiver: displayName(receiver), + })}

{expense.group ? (

{expense.group.name}

diff --git a/src/components/Expense/ExpenseList.tsx b/src/components/Expense/ExpenseList.tsx index f54b6e0d3..3d755be56 100644 --- a/src/components/Expense/ExpenseList.tsx +++ b/src/components/Expense/ExpenseList.tsx @@ -2,6 +2,7 @@ import { SplitType } from '@prisma/client'; import { type inferRouterOutputs } from '@trpc/server'; import Image from 'next/image'; import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import React from 'react'; import { toast } from 'sonner'; @@ -111,8 +112,11 @@ const Expense: ExpenseComponent = ({ e, userId }) => {

{e.name}

- {displayName(e.paidByUser, userId)}{' '} - {t(`ui.expense.user.${e.amount < 0n ? 'received' : 'paid'}`)} {toUIString(e.amount)} + {t(e.amount < 0n ? 'expense_list.user_received' : 'expense_list.user_paid', { + actor: e.paidByUser.id === userId ? 'you' : 'other', + user: displayName(e.paidByUser), + amount: toUIString(e.amount), + })}

@@ -120,7 +124,7 @@ const Expense: ExpenseComponent = ({ e, userId }) => { {youPaid || 0n !== yourExpenseAmount ? ( <>
- {t('actors.you')} {t(`ui.expense.you.${youPaid ? 'lent' : 'owe'}`)} + {t(youPaid ? 'ui.expense.statements.you_lent' : 'ui.expense.statements.you_owe')}
{

- {displayName(e.paidByUser, userId)}{' '} - {t(`ui.expense.user.${e.amount < 0n ? 'received' : 'paid'}`)} {toUIString(e.amount)}{' '} - {t('ui.expense.to')} {displayName(userDetails.data, userId)} + {t('expense_list.settlement', { + payerRole: e.paidByUser.id === userId ? 'you' : 'other', + receiverRole: receiverId === userId ? 'you' : 'other', + payer: displayName(e.paidByUser), + amount: toUIString(e.amount), + receiver: displayName(userDetails.data), + })}

@@ -192,16 +200,30 @@ const CurrencyConversion: ExpenseComponent = ({ e, userId }) => { }

- {t('ui.expense.for')} {displayName(e.paidByUser, userId)} {t('ui.and')}{' '} - {displayName(userDetails.data, userId)} + {t('expense_list.conversion_for_users', { + payerRole: e.paidByUser.id === userId ? 'you' : 'other', + receiverRole: receiverId === userId ? 'you' : 'other', + payer: displayName(e.paidByUser), + receiver: displayName(userDetails.data), + })}

); }; -const NoExpenses = () => ( -
- Empty -
-); +const NoExpenses = () => { + const { t } = useTranslation(); + + return ( +
+ {t('expense_details.empty_state_image_alt')} +
+ ); +}; diff --git a/src/components/Expense/Receipt.tsx b/src/components/Expense/Receipt.tsx index c0c2cba3b..77c6c74d1 100644 --- a/src/components/Expense/Receipt.tsx +++ b/src/components/Expense/Receipt.tsx @@ -1,6 +1,9 @@ +import { useTranslation } from 'next-i18next'; + import { AppDrawer } from '../ui/drawer'; export const Receipt = ({ fileKey }: { fileKey: string }) => { + const { t } = useTranslation(); const thumbKey = fileKey.replace('.webp', '-thumb.webp'); const thumbUrl = `/api/files/${thumbKey}`; const fullUrl = `/api/files/${fileKey}`; @@ -11,7 +14,7 @@ export const Receipt = ({ fileKey }: { fileKey: string }) => { // oxlint-disable-next-line next/no-img-element Expense receipt thumbnail { className="h-14 w-14 rounded-md object-cover object-center data-[loaded=false]:animate-pulse data-[loaded=false]:bg-gray-100/10" /> } - leftAction="Close" - title="Expense Receipt" + leftAction={t('actions.close')} + title={t('expense_details.receipt.title')} className="h-[98vh]" >
@@ -29,7 +32,7 @@ export const Receipt = ({ fileKey }: { fileKey: string }) => { src={fullUrl} width={300} height={800} - alt="Expense receipt" + alt={t('expense_details.receipt.image_alt')} data-loaded="false" onLoad={setDataLoaded} className="h-full w-full rounded-2xl object-cover data-[loaded=false]:animate-pulse data-[loaded=false]:bg-gray-100/10" diff --git a/src/components/Friend/CurrencyConversion.tsx b/src/components/Friend/CurrencyConversion.tsx index b58974a3d..0b4e0503e 100644 --- a/src/components/Friend/CurrencyConversion.tsx +++ b/src/components/Friend/CurrencyConversion.tsx @@ -199,7 +199,7 @@ export const CurrencyConversion: React.FC<{
{t('currency_conversion.rate')}
= ({ friendId, disabled }) => { + const { t } = useTranslation(); const router = useRouter(); const deleteFriendMutation = api.user.deleteFriend.useMutation(); @@ -22,7 +24,7 @@ export const DeleteFriend: React.FC<{ await deleteFriendMutation.mutateAsync({ friendId }); } catch (e) { console.error('Failed to delete friend', e); - toast.error('Failed to delete user'); + toast.error(t('errors.friend_deletion_failed')); return; } utils.expense.getBalances.invalidate().catch(console.error); @@ -32,11 +34,11 @@ export const DeleteFriend: React.FC<{ return ( = ({ 'Settlement', ]; - const { getCurrencyHelpersCached } = useTranslationWithUtils('common'); + const { getCurrencyHelpersCached, t } = useTranslationWithUtils('common'); const exportToCSV = () => { const csvHeaders = headers.join(','); @@ -87,7 +87,7 @@ export const Export: React.FC = ({ return ( ); }; diff --git a/src/components/Friend/FriendBalance.tsx b/src/components/Friend/FriendBalance.tsx index aa462ab86..0d0ba4b13 100644 --- a/src/components/Friend/FriendBalance.tsx +++ b/src/components/Friend/FriendBalance.tsx @@ -27,7 +27,7 @@ export const FriendBalance: React.FC<{
- {t('actors.you')} {isPositive ? t('ui.expense.you.lent') : t('ui.expense.you.owe')} + {t(`ui.expense.statements.${isPositive ? 'you_lent' : 'you_owe'}`)}
{toUIString(balance.amount)} diff --git a/src/components/Friend/GroupSettleup.tsx b/src/components/Friend/GroupSettleup.tsx index 3e8a34d63..0fb3b1bdf 100644 --- a/src/components/Friend/GroupSettleup.tsx +++ b/src/components/Friend/GroupSettleup.tsx @@ -40,8 +40,20 @@ export const GroupSettleUp: React.FC<{ const addExpenseMutation = api.expense.addOrEditExpense.useMutation(); const utils = api.useUtils(); - const sender = 0 > _amount ? user : friend; - const receiver = 0 > _amount ? friend : user; + const isUserPaying = 0 > _amount; + const sender = isUserPaying ? user : friend; + const receiver = isUserPaying ? friend : user; + const payerRole = sender.id === data?.user.id ? 'you' : 'other'; + const receiverRole = receiver.id === data?.user.id ? 'you' : 'other'; + const paymentText = + payerRole === 'you' + ? t('ui.expense.statements.you_pay_user', { user: displayName(receiver) }) + : receiverRole === 'you' + ? t('ui.expense.statements.user_pays_you', { user: displayName(sender) }) + : t('ui.expense.statements.user_pays_user', { + payer: displayName(sender), + receiver: displayName(receiver), + }); const saveExpense = React.useCallback(() => { if (!amount) { @@ -98,11 +110,7 @@ export const GroupSettleUp: React.FC<{
-

- {displayName(sender, data?.user.id)}{' '} - {t(`ui.expense.${sender.id === data?.user.id ? 'you' : 'user'}.pay`)}{' '} - {displayName(receiver, data?.user.id)} -

+

{paymentText}

{isCurrentUserPaying - ? `${t('actors.you')} ${t('ui.expense.you.pay')} ${displayName(friend)}` - : `${displayName(friend)} ${t('ui.expense.user.pay')} ${t('actors.you')}`} + ? t('ui.expense.statements.you_pay_user', { user: displayName(friend) }) + : t('ui.expense.statements.user_pays_you', { user: displayName(friend) })}

{balanceToSettle.groupName ? (

{balanceToSettle.groupName}

diff --git a/src/components/NotificationModal.tsx b/src/components/NotificationModal.tsx index 09934d8c9..35331351a 100644 --- a/src/components/NotificationModal.tsx +++ b/src/components/NotificationModal.tsx @@ -1,5 +1,6 @@ import { Bell } from 'lucide-react'; import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'next-i18next'; import { toast } from 'sonner'; import { useAppStore } from '~/store/appStore'; @@ -33,6 +34,7 @@ const NOTIFICATION_DISMISSED_TIME = 'notification_dismissed_time'; const NOTIFICATION_DISMISSED_TIME_THRESHOLD = 1000 * 60 * 60 * 24 * 30; // 14 days export const NotificationModal: React.FC = () => { + const { t } = useTranslation(); const updatePushSubscription = api.user.updatePushNotification.useMutation(); const webPushPublicKey = useAppStore((s) => s.webPushPublicKey); @@ -40,7 +42,7 @@ export const NotificationModal: React.FC = () => { useEffect(() => { if ('undefined' !== typeof window && 'serviceWorker' in navigator) { - // run only in browser + // Run only in browser navigator.serviceWorker.ready .then((reg) => { reg.pushManager @@ -66,7 +68,7 @@ export const NotificationModal: React.FC = () => { try { const result = await Notification.requestPermission(); if ('granted' === result) { - toast.success('You will receive notifications now'); + toast.success(t('notifications.permission_granted')); navigator.serviceWorker.ready .then(async (reg) => { if (!webPushPublicKey) { @@ -80,13 +82,13 @@ export const NotificationModal: React.FC = () => { updatePushSubscription.mutate({ subscription: JSON.stringify(sub) }); }) .catch((e) => { - toast.error('Cannot subscribe to notification'); + toast.error(t('errors.notification_subscribe_failed')); console.error(e); }); setModalOpen(false); } } catch (e) { - toast.error('Error requesting notification'); + toast.error(t('errors.notification_request_failed')); console.error(e); } } @@ -104,16 +106,14 @@ export const NotificationModal: React.FC = () => { - Enable notifications - - Don't miss on important events. Subscribe to get notification for added expenses - + {t('notifications.enable_title')} + {t('notifications.enable_description')} - Remind later + {t('actions.remind_later')} - Subscribe + {t('notifications.subscribe')} diff --git a/src/components/SimpleConfirmationDialog.tsx b/src/components/SimpleConfirmationDialog.tsx index 16076a547..b28491e66 100644 --- a/src/components/SimpleConfirmationDialog.tsx +++ b/src/components/SimpleConfirmationDialog.tsx @@ -1,5 +1,5 @@ import { type VariantProps } from 'class-variance-authority'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { useTranslation } from 'next-i18next'; import { @@ -14,19 +14,21 @@ import { } from './ui/alert-dialog'; import { Button, type buttonVariants } from './ui/button'; -export const SimpleConfirmationDialog: React.FC< - { - title: string; - open?: boolean; - onOpenChange?: (open: boolean) => void; - onCancel?: () => void; - description: React.ReactNode; - hasPermission: boolean; - onConfirm: () => void | Promise; - loading: boolean; - children?: React.ReactNode; - } & VariantProps -> = ({ +type SimpleConfirmationDialogProps = { + title: string; + onCancel?: () => void; + description: React.ReactNode; + hasPermission: boolean; + onConfirm: () => void | Promise; + loading: boolean; + children?: React.ReactNode; +} & VariantProps & + ( + | { open: boolean; onOpenChange: (open: boolean) => void } + | { open?: never; onOpenChange?: never } + ); + +export const SimpleConfirmationDialog: React.FC = ({ open: controlledOpen, onOpenChange: controlledOnOpenChange, onCancel, @@ -41,8 +43,8 @@ export const SimpleConfirmationDialog: React.FC< const { t } = useTranslation(); const [internalOpen, setInternalOpen] = useState(false); const isControlled = typeof controlledOpen === 'boolean'; - const open = isControlled ? controlledOpen! : internalOpen; - const setOpen = isControlled ? controlledOnOpenChange! : setInternalOpen; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? controlledOnOpenChange : setInternalOpen; return ( diff --git a/src/components/group/GroupMyBalance.tsx b/src/components/group/GroupMyBalance.tsx index 7c1d29511..78824de58 100644 --- a/src/components/group/GroupMyBalance.tsx +++ b/src/components/group/GroupMyBalance.tsx @@ -22,47 +22,38 @@ const GroupMyBalance: React.FC = ({ const userMap = useMemo( () => - users.reduce( - (acc, user) => { - acc[user.id] = user; - return acc; - }, - {} as Record, - ), + users.reduce>((acc, user) => { + acc[user.id] = user; + return acc; + }, {}), [users], ); const friendBalances = useMemo( () => - groupBalances.reduce( - (acc, balance) => { - if (balance.userId === userId && 0 < BigMath.abs(balance.amount)) { - acc[balance.friendId] ??= {}; - const friendBalance = acc[balance.friendId]!; - friendBalance[balance.currency] = - (friendBalance[balance.currency] ?? 0n) + balance.amount; - } - return acc; - }, - {} as Record>, - ), + groupBalances.reduce>>((acc, balance) => { + if (balance.userId === userId && 0 < BigMath.abs(balance.amount)) { + acc[balance.friendId] ??= {}; + const friendBalance = acc[balance.friendId]!; + friendBalance[balance.currency] = + (friendBalance[balance.currency] ?? 0n) + balance.amount; + } + return acc; + }, {}), [groupBalances, userId], ); const cumulatedBalances = useMemo( () => Object.entries( - Object.values(friendBalances).reduce( - (acc, balances) => { - if (balances) { - Object.entries(balances).forEach(([currency, amount]) => { - acc[currency] = (acc[currency] ?? 0n) + amount; - }); - } - return acc; - }, - {} as Record, - ), + Object.values(friendBalances).reduce>((acc, balances) => { + if (balances) { + Object.entries(balances).forEach(([currency, amount]) => { + acc[currency] = (acc[currency] ?? 0n) + amount; + }); + } + return acc; + }, {}), ).map(([currency, amount]) => ({ currency, amount })), [friendBalances], ); @@ -80,10 +71,15 @@ const GroupMyBalance: React.FC = ({
{Object.entries(balances).map(([currency, amount]) => (
- {0 < amount - ? `${friend?.name} ${t('ui.expense.user.owe')} ${t('actors.you_dativus').toLowerCase()}` - : `${t('actors.you')} ${t('ui.expense.you.owe')} ${friend?.name}`}{' '} - {getCurrencyHelpersCached(currency).toUIString(amount)} + {t( + 0 < amount + ? 'balance_summary.friend_owes_you' + : 'balance_summary.you_owe_friend', + { + friend: friend?.name ?? '', + amount: getCurrencyHelpersCached(currency).toUIString(amount), + }, + )}
))}
@@ -92,8 +88,7 @@ const GroupMyBalance: React.FC = ({ {2 < Object.keys(friendBalances).length ? (
- +{Object.keys(friendBalances).length - 2}{' '} - {Object.keys(friendBalances).length === 3 ? t('ui.balance') : t('ui.balances')}... + {t('balance_summary.more', { count: Object.keys(friendBalances).length - 2 })}
) : null} diff --git a/src/dummies/splitGenerator.ts b/src/dummies/splitGenerator.ts index 3a26b5950..08ff0e281 100644 --- a/src/dummies/splitGenerator.ts +++ b/src/dummies/splitGenerator.ts @@ -50,11 +50,12 @@ const generatePercentageShares = (participants: ParticipantId[]): SplitShares => }; // 100% } - // Generate random percentages for first n-1 participants - const percentages = participants.slice(0, -1).map(() => faker.number.int({ min: 1, max: 99 })); - - const totalPercentage = percentages.reduce((sum, p) => sum + p, 0); - const lastParticipantPercentage = Math.max(0, 100 - totalPercentage); + let remainingPercentage = 100; + const percentages = participants.slice(0, -1).map(() => { + const percentage = faker.number.int({ min: 0, max: Math.min(99, remainingPercentage) }); + remainingPercentage -= percentage; + return percentage; + }); // Convert percentages to the format used (0-10000) const shares: SplitShares = {}; @@ -66,7 +67,7 @@ const generatePercentageShares = (participants: ParticipantId[]): SplitShares => }); shares[participants[participants.length - 1]!.id] = { ...initSplitShares(), - [SplitType.PERCENTAGE]: BigInt(lastParticipantPercentage * 100), + [SplitType.PERCENTAGE]: BigInt(remainingPercentage * 100), }; return shares; diff --git a/src/pages/account.tsx b/src/pages/account.tsx index 3f37ad261..5ce3ade8d 100644 --- a/src/pages/account.tsx +++ b/src/pages/account.tsx @@ -86,7 +86,10 @@ const AccountPage: NextPageWithUser<{ [updateDetailsMutation, utils.user.me, t], ); - const header = useMemo(() =>
Account
, []); + const header = useMemo( + () =>
{t('account.title')}
, + [t], + ); const onSignOut = useCallback(async () => { await signOut({ redirect: false }); diff --git a/src/pages/activity.tsx b/src/pages/activity.tsx index 62e66c648..10891d515 100644 --- a/src/pages/activity.tsx +++ b/src/pages/activity.tsx @@ -31,9 +31,9 @@ function getPaymentString( } else if (isSettlement) { return (
- {t('actors.you')}{' '} - {user.id === paidBy ? t('ui.expense.you.paid') : t('ui.expense.you.received')}{' '} - {toUIString(amount)} + {t(user.id === paidBy ? 'activity.you_paid_amount' : 'activity.you_received_amount', { + amount: toUIString(amount), + })}
); } else { @@ -41,9 +41,14 @@ function getPaymentString(
- {t(`actors.you`)}{' '} - {t(`ui.expense.you.${(user.id === paidBy) !== amount < 0n ? 'lent' : 'owe'}`)}{' '} - {toUIString(expenseUserAmt)} + {t( + (user.id === paidBy) !== amount < 0n + ? 'ui.expense.statements.you_lent_amount' + : 'ui.expense.statements.you_owe_amount', + { + amount: toUIString(expenseUserAmt), + }, + )}
); } @@ -93,24 +98,28 @@ const ActivityPage: NextPageWithUser = ({ user }) => {
{e.expense.deletedByUser ? (

- - {displayName(e.expense.deletedByUser, user.id)} - {' '} - {t( - `ui.expense.${e.expense.deletedByUser.id === user.id ? 'you' : 'user'}.deleted`, - )}{' '} - {e.expense.name} + {t('activity.user_deleted_expense', { + actor: e.expense.deletedByUser.id === user.id ? 'you' : 'other', + user: displayName(e.expense.deletedByUser), + expense: e.expense.name, + })}

) : (

- - {displayName(e.expense.paidByUser, user.id)} - {' '} {t( - `ui.expense.${e.expense.paidByUser.id === user.id ? 'you' : 'user'}.${e.expense.amount > 0n ? 'paid' : 'received'}`, - )}{' '} - {toUIString(e.expense.amount)} {t('ui.expense.for')}{' '} - {e.expense.name} + e.expense.paidByUser.id === user.id + ? e.expense.amount > 0n + ? 'activity.you_paid_for_expense' + : 'activity.you_received_for_expense' + : e.expense.amount > 0n + ? 'activity.user_paid_for_expense' + : 'activity.user_received_for_expense', + { + user: displayName(e.expense.paidByUser), + amount: toUIString(e.expense.amount), + expense: e.expense.name, + }, + )}

)} diff --git a/src/pages/auth/signin.tsx b/src/pages/auth/signin.tsx index 2e2cfe3d7..d99f02879 100644 --- a/src/pages/auth/signin.tsx +++ b/src/pages/auth/signin.tsx @@ -105,7 +105,7 @@ const Home: NextPage<{ } else if ('SessionRequired' === error) { return; } else { - toast.error(t('errors.signin_error') + error); + toast.error(t('errors.signin_error', { error })); console.error('Error during sign-in:', error); } } @@ -121,7 +121,7 @@ const Home: NextPage<{ const oauthProvider = oauthProviders[0]; if (shouldAutoRedirect && oauthProvider && !showVerificationStep && !isLoadingProviders) { void signIn(oauthProvider.id, { callbackUrl }).catch(() => { - toast.error(t('errors.signin_error')); + toast.error(t('errors.signin_failed')); }); } }, [ diff --git a/src/pages/balances.tsx b/src/pages/balances.tsx index 34b1a74c7..2ca9efb6c 100644 --- a/src/pages/balances.tsx +++ b/src/pages/balances.tsx @@ -86,12 +86,12 @@ const BalancePage: NextPageWithUser = ({ user }) => { ) : ( <> diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 224afd9e0..18eba273c 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -53,6 +53,10 @@ export default function Home() { return ( <> + + {t('meta.title')} + +