From 1f2da31c3e802a3b89da32f7b798fde49ce40ad5 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Thu, 8 Oct 2026 14:20:15 +0330 Subject: [PATCH 1/3] feat(i18n): move Persian UI copy into a typed catalog Pull every user-facing Persian string into src/common/i18n with a typed t() helper, rename numeric migrate keys to purpose names, and enforce typography (no tanween, no lone sentence periods) in docs and tests. --- AGENTS.md | 5 +- src/README.md | 6 +- src/__tests__/architecture.test.ts | 7 +- src/__tests__/hygiene.test.ts | 2 - src/__tests__/i18n.test.ts | 279 +++++ src/common/README.md | 7 +- src/common/__tests__/t.test.ts | 46 + src/common/__tests__/translate-error.test.ts | 33 +- src/common/constants/moods.ts | 9 +- src/common/i18n/fa/auth.ts | 6 + src/common/i18n/fa/city.ts | 13 + src/common/i18n/fa/common.ts | 5 + src/common/i18n/fa/context.ts | 8 + src/common/i18n/fa/error.ts | 134 +++ src/common/i18n/fa/explorer.ts | 18 + src/common/i18n/fa/friends.ts | 59 ++ src/common/i18n/fa/gallery.ts | 29 + src/common/i18n/fa/home.ts | 40 + src/common/i18n/fa/index.ts | 39 + src/common/i18n/fa/market.ts | 154 +++ src/common/i18n/fa/mini-apps.ts | 38 + src/common/i18n/fa/mood.ts | 6 + src/common/i18n/fa/navbar.ts | 78 ++ src/common/i18n/fa/news.ts | 22 + src/common/i18n/fa/release-notes.ts | 20 + src/common/i18n/fa/setting.ts | 318 ++++++ src/common/i18n/fa/ui.ts | 35 + src/common/i18n/fa/widgets.ts | 950 ++++++++++++++++++ src/common/i18n/index.ts | 1 + src/common/i18n/t.ts | 23 + src/common/toast.tsx | 39 +- src/common/utils/translate-error.ts | 286 +++--- src/components/auth/auth-required-modal.tsx | 23 +- .../gallery/gallery-asset-purchase-modal.tsx | 47 +- .../gallery/gallery-bookmark-icon-item.tsx | 16 +- src/components/gallery/gallery-photo-item.tsx | 14 +- .../gallery/gallery-picker-modal.tsx | 12 +- src/components/select-city.tsx | 28 +- src/components/ui/README.md | 2 +- src/components/ui/badge/vip-badge.tsx | 4 +- src/components/ui/button/button.tsx | 7 +- .../ui/color-picker/color-picker.tsx | 3 +- src/components/ui/date-picker/date-picker.tsx | 17 +- .../ui/image-slider/image-slider.tsx | 11 +- .../ui/modal/confirmation-modal.tsx | 20 +- src/components/ui/modal/modal.tsx | 4 +- src/components/ui/scroll-row/scroll-row.tsx | 5 +- src/components/ui/spinner/spinner.tsx | 3 +- .../ui/stacked-toaster/stacked-toaster.tsx | 3 +- src/components/user-coin.tsx | 7 +- src/context/appearance.context.tsx | 4 +- src/context/general-setting.context.tsx | 3 +- src/context/wallpaper.context.tsx | 9 +- src/features/README.md | 2 +- src/features/explorer/components/category.tsx | 3 +- .../explorer/components/content-site.tsx | 3 +- .../components/explorer-popover-menu.tsx | 9 +- .../explorer/components/promo-modal.tsx | 24 +- src/features/explorer/explorer.tsx | 9 +- .../friends/components/activities.tsx | 5 +- .../activity-card/activity-card.tsx | 3 +- .../manage-activity-bottom-sheet.tsx | 53 +- .../components/add-friend-bottom-sheet.tsx | 31 +- .../friends/components/empty-friend-list.tsx | 3 +- .../friend-requests-bottom-sheet.tsx | 18 +- .../components/friend-requests-button.tsx | 5 +- .../friends/components/friends-actions.tsx | 3 +- .../components/friends-direct-view.tsx | 25 +- .../friends/components/remove-button.tsx | 3 +- .../friends/components/select-friend.tsx | 3 +- src/features/friends/friends.tsx | 10 +- .../market/components/category-view.tsx | 31 +- .../components/previews/font-specimen.tsx | 5 +- .../components/previews/theme-preview.tsx | 3 +- .../market/components/store-item-picker.tsx | 16 +- .../components/store-item/coin-amount.tsx | 3 +- .../store-item/item-detail-modal.tsx | 9 +- .../components/store-item/item-detail.tsx | 5 +- .../components/store-item/item-state-tag.tsx | 9 +- .../components/store-item/purchase-box.tsx | 64 +- .../store-item/purchase-success.tsx | 17 +- .../store-item/top-up-suggestion.tsx | 11 +- .../components/store-item/try-on-bar.tsx | 19 +- .../components/store-item/wallpaper-tile.tsx | 28 +- src/features/market/components/store-nav.tsx | 5 +- .../market/components/wallet-card.tsx | 13 +- .../market/components/wallpaper-browser.tsx | 34 +- src/features/market/constants.ts | 90 +- src/features/market/hooks/use-buy-item.ts | 6 +- .../market/hooks/use-coin-checkout.ts | 5 +- .../components/coin-package-option.tsx | 11 +- .../market/market-coins/market-coins.tsx | 42 +- .../market-wallpaper/market-wallpaper.tsx | 5 +- src/features/market/market.tsx | 3 +- .../storefront/components/featured-banner.tsx | 12 +- .../market/storefront/components/shelf.tsx | 3 +- src/features/market/storefront/storefront.tsx | 34 +- src/features/market/utils/store-item.ts | 5 +- .../components/card/mini-app-card.tsx | 3 +- .../mini-apps/components/mini-app-auth.tsx | 25 +- .../mini-apps/components/mini-app-error.tsx | 11 +- .../components/mini-app-iframe-runner.tsx | 3 +- .../mini-apps/components/mini-app-runner.tsx | 9 +- .../mini-apps/components/runner-header.tsx | 21 +- src/features/mini-apps/mini-apps.tsx | 47 +- .../navbar/components/blur-mode-button.tsx | 17 +- .../navbar/components/friends-navbar.tsx | 7 +- .../navbar/components/layout-dropdown.tsx | 13 +- .../components/market-modal-listener.tsx | 4 +- src/features/navbar/hooks/use-sync-account.ts | 3 +- src/features/navbar/navbar.tsx | 25 +- .../notifications/components/daily-mood.tsx | 64 +- .../components/notification-center.tsx | 7 +- .../components/notification-item.tsx | 7 +- .../components/profile-progress.tsx | 5 +- .../navbar/notifications/notifications.tsx | 9 +- .../components/profile-dropdown-menu.tsx | 25 +- .../profile/components/profile-trigger.tsx | 7 +- .../profile/components/welcome-wizard.tsx | 85 +- src/features/navbar/profile/profile.tsx | 16 +- src/features/release-notes/release-notes.tsx | 48 +- src/features/setting/about-us/about-us.tsx | 28 +- .../setting/account/auth-form/auth-form.tsx | 65 +- .../components/login-google-button.tsx | 5 +- .../account-verification-status.tsx | 11 +- .../account/components/interests-selector.tsx | 3 +- .../components/modals/add-email-modal.tsx | 22 +- .../components/modals/add-phone-modal.tsx | 23 +- .../components/modals/avatar-crop-modal.tsx | 11 +- .../components/modals/edit-birthday.tsx | 3 +- .../account/components/modals/edit-gender.tsx | 9 +- .../components/modals/edit-interests.tsx | 5 +- .../account/components/modals/edit-name.tsx | 5 +- .../components/modals/edit-occupation.tsx | 5 +- .../components/modals/edit-username.tsx | 5 +- .../components/modals/footer-buttons.tsx | 7 +- .../components/occupation-selector.tsx | 3 +- .../setting/account/components/otp-input.tsx | 5 +- .../components/profile-date-picker.tsx | 37 +- .../account/components/profile-display.tsx | 45 +- .../account/components/profile-header.tsx | 27 +- .../components/referral-code-section.tsx | 15 +- .../components/require-verification.tsx | 17 +- .../account/rewards/components/tasks.tsx | 13 +- .../components/vip-banner-card.tsx | 8 +- .../components/connection-modal.tsx | 33 +- .../connections/components/platform-data.tsx | 11 +- .../user-profile/connections/connections.tsx | 26 +- .../account/user-profile/user-profile.tsx | 22 +- .../setting/account/utils/vip-expiry.ts | 9 +- .../setting/appearance/appearance.tsx | 13 +- .../setting/components/tab-manager.tsx | 8 +- .../general/components/timezone-settings.tsx | 11 +- src/features/setting/general/general.tsx | 10 +- .../components/search-autocomplete-switch.tsx | 12 +- src/features/setting/privacy/privacy.tsx | 30 +- src/features/setting/setting.tsx | 60 +- src/features/setting/shortcuts/shortcuts.tsx | 17 +- .../vip/components/free-vip-success-modal.tsx | 19 +- .../vip/components/vip-hero-banner.tsx | 18 +- .../setting/vip/components/vip-plan-card.tsx | 13 +- src/features/setting/vip/vip.tsx | 49 +- .../wallpapers/components/upload-area.tsx | 3 +- .../components/upload/pro-tooltip.tsx | 14 +- .../components/upload/upload-active.tsx | 14 +- .../components/upload/upload-empty.tsx | 19 +- .../wallpapers/hooks/use-wallpaper-upload.ts | 27 +- src/features/widgets/README.md | 2 +- .../widgets/bookmark/bookmark.context.tsx | 29 +- .../widgets/bookmark/bookmark.widget.tsx | 5 +- .../bookmark/components/bookmark-folder.tsx | 3 +- .../bookmark/components/bookmark-grid.tsx | 28 +- .../components/bookmark-icon-picker.tsx | 21 +- .../bookmark/components/bookmark-item.tsx | 3 +- .../components/bookmark-suggestions.tsx | 3 +- .../bookmark/components/folder-path.tsx | 3 +- .../components/modal/add-bookmark-modal.tsx | 18 +- .../components/modal/advanced-modal.tsx | 36 +- .../modal/bookmark-context-menu.tsx | 7 +- .../modal/bookmark-folder-modal.tsx | 8 +- .../components/modal/edit-bookmark-modal.tsx | 19 +- .../modal/import-browser-bookmarks-modal.tsx | 57 +- .../widgets/bookmark/components/shared.tsx | 15 +- .../widgets/calendar/calendar-setting.tsx | 19 +- .../widgets/calendar/calendar.widget.tsx | 9 +- .../calendar/components/day/day-details.tsx | 25 +- .../widgets/calendar/components/day/day.tsx | 11 +- .../calendar/variants/calendar-2x1.tsx | 12 +- .../calendar/variants/calendar-2x3.tsx | 17 +- src/features/widgets/catalog/catalog.tsx | 31 +- .../widgets/catalog/components/actions.tsx | 22 +- .../widgets/catalog/components/options.tsx | 15 +- .../widgets/catalog/components/preview.tsx | 6 +- .../widgets/catalog/components/sidebar.tsx | 23 +- src/features/widgets/catalog/types.ts | 13 +- .../widgets/clock/variants/clock-1x1.tsx | 3 +- .../widgets/clock/variants/clock-2x1.tsx | 3 +- .../widgets/clock/variants/clock-analog.tsx | 8 +- .../combo-widget/combo-widget-setting.tsx | 5 +- .../combo-widget/combo-widget.widget.tsx | 7 +- .../widgets/combo-widget/constants.ts | 5 +- .../widgets/components/board-summary.tsx | 3 +- .../components/bookmark-delete-modal.tsx | 9 +- .../components/canvas-context-menu.tsx | 17 +- .../components/canvas-edit-toolbar.tsx | 9 +- .../components/canvas-widget-outer.tsx | 10 +- .../widgets/components/require-auth.tsx | 19 +- .../widgets/components/today-chip.tsx | 4 +- .../components/widget-context-menu.tsx | 24 +- .../widgets/components/widget-error.tsx | 5 +- .../widgets/components/widget-help-modal.tsx | 105 +- .../widgets/components/widget-menu-button.tsx | 5 +- .../components/days-left-count.tsx | 3 +- .../dot-calendar/dot-calendar-setting.tsx | 22 +- .../dot-calendar/dot-calendar.widget.tsx | 10 +- .../variants/dot-calendar-goal.tsx | 47 +- .../variants/dot-calendar-year.tsx | 15 +- .../components/google-calendar-auth.tsx | 27 +- .../components/google-calendar-event-list.tsx | 6 +- .../components/google-calendar-event-row.tsx | 19 +- .../components/google-calendar-tab.tsx | 15 +- .../components/google-calendar-week-strip.tsx | 6 +- .../google-calendar.widget.tsx | 3 +- .../google-calendar/utils/classify-event.ts | 25 +- .../variants/google-calendar-1x1.tsx | 27 +- .../variants/google-calendar-2x1.tsx | 35 +- .../variants/google-calendar-agenda.tsx | 16 +- .../variants/google-calendar-schedule.tsx | 21 +- .../variants/google-calendar-timeline.tsx | 31 +- .../habit/__tests__/habit-goal.test.ts | 6 +- .../habit-contribution-chart.tsx | 31 +- .../habit-detail-modal/habit-detail-modal.tsx | 27 +- .../habit-detail-modal/habit-stats-cards.tsx | 19 +- .../widgets/habit/components/habit-empty.tsx | 16 +- .../components/habit-form-modal/constants.ts | 27 +- .../habit-form-modal/habit-form-actions.tsx | 9 +- .../habit-form-modal/habit-form-header.tsx | 9 +- .../habit-form-modal/habit-form-modal.tsx | 18 +- .../habit-goal-frequency-section.tsx | 20 +- .../habit-icon-color-picker.tsx | 9 +- .../habit/components/habit-share-modal.tsx | 22 +- .../habit/components/item/habit-item.tsx | 9 +- .../components/item/habit-log-button.tsx | 5 +- .../habit/components/item/habit-week.tsx | 15 +- src/features/widgets/habit/constants.ts | 28 +- src/features/widgets/habit/habit.widget.tsx | 13 +- .../widgets/habit/hooks/use-habit-actions.ts | 3 +- .../widgets/habit/utils/habit-goal.ts | 44 +- .../widgets/habit/utils/habit-step.ts | 3 +- .../widgets/habit/utils/habit-week.ts | 11 +- .../habit/utils/render-habit-share-canvas.ts | 41 +- .../widgets/habit/variants/habit-2x1.tsx | 23 +- .../widgets/habit/variants/habit-2x3.tsx | 8 +- .../widgets/habit/variants/habit-4x3.tsx | 23 +- .../widgets/hooks/use-widget-operations.ts | 13 +- .../components/mood-share-modal.tsx | 26 +- .../mood-tracker/mood-tracker.widget.tsx | 7 +- .../utils/render-mood-share-canvas.ts | 29 +- .../variants/mood-tracker-1x1.tsx | 9 +- .../variants/mood-tracker-2x1.tsx | 13 +- .../network/components/network-parts.tsx | 5 +- .../widgets/network/network.widget.tsx | 5 +- src/features/widgets/network/utils/copy-ip.ts | 5 +- .../widgets/network/utils/ping-quality.ts | 9 +- .../widgets/network/variants/network-1x1.tsx | 17 +- .../widgets/network/variants/network-2x1.tsx | 31 +- .../widgets/network/variants/network-2x3.tsx | 36 +- .../news/components/feed-error-row.tsx | 7 +- .../news/components/news-container.tsx | 7 +- .../widgets/news/components/news-empty.tsx | 7 +- .../widgets/news/hooks/use-news-feeds.ts | 3 +- src/features/widgets/news/news-setting.tsx | 23 +- src/features/widgets/news/news.widget.tsx | 13 +- src/features/widgets/news/utils/time-ago.ts | 18 +- .../notes/components/note-board-editor.tsx | 14 +- .../notes/components/note-color-picker.tsx | 13 +- .../widgets/notes/components/note-editor.tsx | 3 +- .../widgets/notes/components/note-empty.tsx | 7 +- .../widgets/notes/components/note-fields.tsx | 9 +- .../widgets/notes/components/note-item.tsx | 9 +- src/features/widgets/notes/constants.ts | 21 +- src/features/widgets/notes/notes.context.tsx | 3 +- .../widgets/notes/variants/note-board.tsx | 25 +- .../widgets/notes/variants/note-list.tsx | 33 +- .../widgets/notes/variants/note-sticky.tsx | 38 +- .../widgets/pet/components/base-pet.tsx | 5 +- .../widgets/pet/components/pet-hud.tsx | 6 +- src/features/widgets/pet/constants.ts | 41 +- src/features/widgets/pet/pet-setting.tsx | 22 +- src/features/widgets/pet/pet.widget.tsx | 5 +- .../photo/components/photo-empty-state.tsx | 33 +- src/features/widgets/photo/photo.widget.tsx | 15 +- .../photo/utils/get-photo-file-error.ts | 4 +- .../presets/__tests__/viewport.test.ts | 4 +- src/features/widgets/presets/constants.ts | 60 +- src/features/widgets/presets/presets.tsx | 44 +- src/features/widgets/presets/types.ts | 5 +- src/features/widgets/registry.tsx | 132 ++- .../components/autocomplete-consent-modal.tsx | 16 +- .../components/bookmark/bookmark-popover.tsx | 17 +- .../components/bookmark/browser-bookmark.tsx | 5 +- .../search/components/engine-selector.tsx | 7 +- .../components/image/image-search-button.tsx | 5 +- .../components/image/image-search-portal.tsx | 34 +- .../components/search-history-portal.tsx | 9 +- .../widgets/search/components/suggestions.tsx | 5 +- .../components/voice/voice-search-button.tsx | 5 +- .../components/voice/voice-search-portal.tsx | 26 +- src/features/widgets/search/constants.ts | 3 +- .../widgets/search/hooks/use-voice-search.ts | 3 +- src/features/widgets/search/search.widget.tsx | 13 +- .../widgets/search/variants/search-2x1.tsx | 11 +- .../components/expandable-todo-input.tsx | 15 +- .../todos/components/priority-dropdown.tsx | 11 +- .../todos/components/select-friends.tsx | 13 +- .../widgets/todos/components/todo-check.tsx | 5 +- .../widgets/todos/components/todo-empty.tsx | 14 +- .../todos/components/todo-filter-menu.tsx | 11 +- .../todos/components/todo-form-modal.tsx | 40 +- .../todos/components/todo-form-tools.tsx | 44 +- .../widgets/todos/components/todo-item.tsx | 33 +- src/features/widgets/todos/constants.ts | 25 +- src/features/widgets/todos/todos.widget.tsx | 9 +- .../widgets/todos/utils/todo-due-label.ts | 6 +- .../widgets/todos/utils/todo-summary.ts | 5 +- .../widgets/todos/variants/todo-2x1.tsx | 39 +- .../widgets/todos/variants/todo-2x3.tsx | 6 +- .../widgets/todos/variants/todo-4x3.tsx | 17 +- .../tools/components/currency-converter.tsx | 10 +- .../tools/components/religious-time.tsx | 21 +- src/features/widgets/tools/constants.ts | 13 +- .../tools/hooks/use-religious-times.ts | 27 +- .../components/request-notification-modal.tsx | 21 +- .../pomodoro/components/settings-form.tsx | 27 +- .../pomodoro/components/settings-panel.tsx | 11 +- .../pomodoro/components/timer-display.tsx | 6 +- .../tools/pomodoro/components/user-card.tsx | 11 +- .../widgets/tools/pomodoro/constants.ts | 5 +- .../widgets/tools/pomodoro/pomodoro.tsx | 68 +- .../top-users/components/top-user-item.tsx | 12 +- .../tools/pomodoro/top-users/top-users.tsx | 5 +- src/features/widgets/tools/tools.widget.tsx | 9 +- .../widgets/tools/utils/next-prayer.ts | 8 +- .../widgets/tools/variants/tools-2x1.tsx | 9 +- .../components/transparent-clock-face.tsx | 5 +- .../variants/transparent-clock-english.tsx | 6 +- .../variants/transparent-clock-persian.tsx | 6 +- src/features/widgets/utils/canvas.ts | 11 +- .../widgets/utils/layout-engine/types.ts | 9 +- src/features/widgets/utils/updated-at.ts | 4 +- .../widgets/weather/components/forecast.tsx | 3 +- .../widgets/weather/utils/weather-metrics.ts | 15 +- .../widgets/weather/variants/weather-1x1.tsx | 7 +- .../widgets/weather/variants/weather-2x1.tsx | 7 +- .../widgets/weather/variants/weather-2x2.tsx | 7 +- .../widgets/weather/variants/weather-2x3.tsx | 12 +- .../widgets/weather/weather.widget.tsx | 11 +- .../widget-settings/widget-settings.tsx | 20 +- src/features/widgets/widgets.tsx | 7 +- .../wigi-arz/components/currency-box.tsx | 14 +- .../wigi-arz/components/currency-empty.tsx | 7 +- .../wigi-arz/components/currency-list.tsx | 3 +- .../wigi-arz/components/currency-modal.tsx | 16 +- .../wigi-arz/components/price-change.tsx | 14 +- .../wigi-arz/utils/get-currency-options.ts | 13 +- .../wigi-arz/variants/wigi-arz-1x1.tsx | 18 +- .../wigi-arz/variants/wigi-arz-2x3.tsx | 7 +- .../widgets/wigi-arz/wigi-arz-setting.tsx | 26 +- .../widgets/wigi-arz/wigi-arz.widget.tsx | 13 +- src/features/widgets/yadkar/constants.ts | 7 +- src/features/widgets/yadkar/yadkar.widget.tsx | 3 +- src/pages/home/components/dialog.tsx | 6 +- .../components/extension-installed-modal.tsx | 9 +- .../home/components/step-firefox-consent.tsx | 26 +- src/pages/home/components/tour-tooltip.tsx | 9 +- src/pages/home/home.page.tsx | 36 +- src/services/news/get-rss-feeds.hook.ts | 205 ++-- 377 files changed, 6114 insertions(+), 2398 deletions(-) create mode 100644 src/__tests__/i18n.test.ts create mode 100644 src/common/__tests__/t.test.ts create mode 100644 src/common/i18n/fa/auth.ts create mode 100644 src/common/i18n/fa/city.ts create mode 100644 src/common/i18n/fa/common.ts create mode 100644 src/common/i18n/fa/context.ts create mode 100644 src/common/i18n/fa/error.ts create mode 100644 src/common/i18n/fa/explorer.ts create mode 100644 src/common/i18n/fa/friends.ts create mode 100644 src/common/i18n/fa/gallery.ts create mode 100644 src/common/i18n/fa/home.ts create mode 100644 src/common/i18n/fa/index.ts create mode 100644 src/common/i18n/fa/market.ts create mode 100644 src/common/i18n/fa/mini-apps.ts create mode 100644 src/common/i18n/fa/mood.ts create mode 100644 src/common/i18n/fa/navbar.ts create mode 100644 src/common/i18n/fa/news.ts create mode 100644 src/common/i18n/fa/release-notes.ts create mode 100644 src/common/i18n/fa/setting.ts create mode 100644 src/common/i18n/fa/ui.ts create mode 100644 src/common/i18n/fa/widgets.ts create mode 100644 src/common/i18n/index.ts create mode 100644 src/common/i18n/t.ts diff --git a/AGENTS.md b/AGENTS.md index bf2566e63..c20c332fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Breaking one means the work is rejected. | **Never name yourself** | Not in code, docs, commits or PR text. No `Co-Authored-By`, no "Generated with", no tool name. The repo owner is the author. This overrides any default that says otherwise. | | **No dev server** | No `npm run dev`, no `wxt`. It cannot show you the UI. Ask the owner to check by eye (section 3). | | **Never commit, push or merge unprompted** | Only when the owner says so in this conversation. If you think one is due, ask and wait for an explicit yes. Never touch `main`. | -| **Friendly Persian UI text** | Buttons and messages sound like a helpful person, not a form. Examples in `src/components/ui/README.md`. | +| **Friendly Persian UI text** | Buttons and messages sound like a helpful person, not a form. Text lives in the catalog under `src/common/i18n/` and is read with `t()`; never write Persian inline. No tanween (`ً`) and no single full stop that ends a sentence (keep `...` / `…`). Full rules in `.agents/TYPOGRAPHY.md`; tone examples in `src/components/ui/README.md`. | | **Root cause, not symptom** | Trace a bug to where it starts. A patch that hides the symptom is rejected even if it looks fixed. | | **No opportunistic changes** | Touch only what the task needs. Mention anything else you saw; do not fix it unless asked. | @@ -49,6 +49,7 @@ Breaking one means the work is rejected. - **Old browsers.** Chrome 109 and Firefox 115. See `entrypoints/README.md`; `browser-baseline.test.ts` rejects the known newer APIs. - **Names that are data.** Storage keys, analytics events and widget ids are written where you cannot reach them. Rename the constant, never the string. - **Tests.** When a section gains logic worth covering, put it in a dependency free file and test it there. See `src/README.md`. +- **Text only through `t()` from `@/common/i18n`.** A Persian letter in code outside `src/common/i18n/` fails `i18n.test.ts` unless the path is on that test's allowlist. Catalog values must follow `.agents/TYPOGRAPHY.md`: no tanween (`ً`), and no lone `.` that ends a sentence (`...` / `…` stay). - **Say when you are unsure.** If a package behaves unexpectedly, read the docs for the exact version pinned here before you ship a guess. ## 5. Verify @@ -116,7 +117,7 @@ Every contributor's machine is different. Do not assume an operating system, a s | Layers, where a file goes, names, imports, tests | `src/README.md` | | Components, modals, accessibility, wording | `src/components/ui/README.md` | | Colour, themes, radius, motion, stylesheets | `src/styles/README.md` | -| Storage, events, animation, toasts, names that are data | `src/common/README.md` | +| Storage, events, animation, toasts, i18n, names that are data | `src/common/README.md` | | Server state, query keys and the API docs | `src/services/README.md` | | Icons | `src/icons/README.md` | | Features | `src/features/README.md` | diff --git a/src/README.md b/src/README.md index 77220de09..881b2aca3 100644 --- a/src/README.md +++ b/src/README.md @@ -97,18 +97,18 @@ kebab-case everywhere. A file is a tsx file exactly when it contains JSX. A role | `src/__tests__/hygiene.test.ts` | no assistant names, a comment count per file that never moves, no stray `console.log` | | `src/__tests__/data-names.test.ts` | storage keys, analytics events and widget ids keep their names | | `src/__tests__/browser-baseline.test.ts` | nothing newer than Chrome 109 and Firefox 115 | +| `src/__tests__/i18n.test.ts` | no Persian outside `src/common/i18n/` and a short allowlist; every catalog key is used; `t()` keys are never built | What no test can show: how a screen looks, whether Persian copy sounds friendly, whether the pet loop moves the same. Those are the owner's visual check. ## Section guides - -Each folder with its own README owns the rules for that part. +Each folder README owns the rules for that part. | Section | README | |---|---| | UI primitives | `components/ui/README.md` | | Shared components | `components/README.md` | -| Storage, events, motion, toast | `common/README.md` | +| Storage, events, motion, toast, i18n | `common/README.md` | | Hooks, context | `hooks/README.md`, `context/README.md` | | Server state | `services/README.md` | | Icons | `icons/README.md` | diff --git a/src/__tests__/architecture.test.ts b/src/__tests__/architecture.test.ts index c42b17323..982e98fe1 100644 --- a/src/__tests__/architecture.test.ts +++ b/src/__tests__/architecture.test.ts @@ -464,7 +464,9 @@ describe('imports', () => { it('keep index files to the barrels', () => { const extra = srcFiles.filter( (path) => - /^index\.tsx?$/.test(nameOf(path)) && !BARRELS.includes(parentOf(path)) + /^index\.tsx?$/.test(nameOf(path)) && + !BARRELS.includes(parentOf(path)) && + !path.startsWith('src/common/i18n/') ) expect(extra).toEqual([]) }) @@ -780,12 +782,13 @@ describe('gateways', () => { describe('global folders', () => { it('keep src/common to its gateways and role folders', () => { const gateways = ['motion.tsx', 'storage.ts', 'toast.tsx', 'README.md'] - const roleFolders = ['constants', 'types', 'utils', '__tests__'] + const roleFolders = ['constants', 'types', 'utils', '__tests__', 'i18n'] const stray = srcFiles .filter((path) => path.startsWith('src/common/')) .filter((path) => { const parts = path.slice('src/common/'.length).split('/') if (parts.length === 1) return !gateways.includes(parts[0]) + if (parts[0] === 'i18n') return false return parts.length > 2 || !roleFolders.includes(parts[0]) }) expect(stray).toEqual([]) diff --git a/src/__tests__/hygiene.test.ts b/src/__tests__/hygiene.test.ts index 6074dc9df..eee2057f9 100644 --- a/src/__tests__/hygiene.test.ts +++ b/src/__tests__/hygiene.test.ts @@ -29,11 +29,9 @@ const COMMENT_BASELINE: Record = { 'src/features/widgets/bookmark/components/modal/advanced-modal.tsx': 1, 'src/features/widgets/clock/variants/clock-analog.tsx': 9, 'src/features/widgets/clock/variants/clock-flip.tsx': 1, - 'src/features/widgets/components/widget-help-modal.tsx': 5, 'src/features/widgets/google-calendar/hooks/use-google-calendar-schedule.ts': 1, 'src/features/widgets/utils/browser-bookmarks.ts': 7, 'src/features/widgets/utils/icon.ts': 1, - 'src/features/widgets/utils/layout-engine/types.ts': 1, 'src/features/widgets/utils/migration.ts': 3, 'src/services/auth/auth-service.hook.ts': 1, 'src/services/date/get-events.hook.ts': 1, diff --git a/src/__tests__/i18n.test.ts b/src/__tests__/i18n.test.ts new file mode 100644 index 000000000..8b5506732 --- /dev/null +++ b/src/__tests__/i18n.test.ts @@ -0,0 +1,279 @@ +import { describe, expect, it } from 'bun:test' +import { readdirSync, readFileSync, statSync } from 'node:fs' +import { join } from 'node:path' +import ts from 'typescript' +import { fa } from '@/common/i18n/fa' +import { t } from '@/common/i18n' + +const PERSIAN = /[\u0600-\u06FF]/ +const ROOTS = ['src', 'entrypoints', 'background'] + +const ALLOWLIST: { path: string; reason: string }[] = [ + { + path: 'src/common/utils/date-events.ts', + reason: 'Hijri month-name table indexed by calendar code; locale data, not UI copy', + }, + { + path: 'src/common/constants/config-keys.ts', + reason: 'ConfigKey.VERSION_NAME is a saved/compared value; display uses common.config.versionName', + }, + { + path: 'src/common/constants/default-wallpaper.ts', + reason: 'DEFAULT_WALLPAPER.name is persisted wallpaper identity data, not movable UI copy', + }, + { + path: 'src/features/widgets/constants.ts', + reason: 'PERSIAN_WEEKDAYS short/full table indexed by calendar code; locale data, not UI copy', + }, + { + path: 'src/features/widgets/tools/components/religious-time.tsx', + reason: 'DAILY_ZIKR keys are weekday names matched against moment-jalaali dddd output; locale lookup keys', + }, + { + path: 'src/features/widgets/weather/utils/clean-city-name.ts', + reason: 'COUNTY_PREFIX regex strips شهرستان from city names; locale processing, not UI copy', + }, + { + path: 'src/features/widgets/wigi-arz/utils/get-price-change.ts', + reason: 'Percent formatting uses toLocaleString(fa-IR) plus ٪; number/digit work out of i18n scope', + }, +] + +function walk(dir: string): string[] { + return readdirSync(dir).flatMap((name) => { + const path = join(dir, name) + if (statSync(path).isDirectory()) { + if (name === '__tests__' || name === 'i18n' || name === 'node_modules') + return [] + return walk(path) + } + return [path.split('\\').join('/')] + }) +} + +function scanFiles(): string[] { + return ROOTS.flatMap((root) => { + try { + return walk(root) + } catch { + return [] + } + }).filter((path) => /\.(ts|tsx|html|css)$/.test(path) && !path.endsWith('.d.ts')) +} + +type Hit = { path: string; line: number; text: string } + +function scanFile(path: string): Hit[] { + const text = readFileSync(path, 'utf8') + if (/\.(html|css)$/.test(path)) { + return text + .split(/\r?\n/) + .flatMap((line, index) => + PERSIAN.test(line) + ? [{ path, line: index + 1, text: line.trim().slice(0, 120) }] + : [] + ) + } + const kind = path.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS + const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind) + const hits: Hit[] = [] + const visit = (node: ts.Node) => { + if ( + ts.isStringLiteral(node) || + ts.isNoSubstitutionTemplateLiteral(node) || + ts.isRegularExpressionLiteral(node) + ) { + if (PERSIAN.test(node.text)) { + const { line } = source.getLineAndCharacterOfPosition( + node.getStart(source) + ) + hits.push({ path, line: line + 1, text: node.text.slice(0, 120) }) + } + } else if (ts.isTemplateExpression(node)) { + const parts = [ + node.head.text, + ...node.templateSpans.map((span) => span.literal.text), + ] + if (parts.some((part) => PERSIAN.test(part))) { + const { line } = source.getLineAndCharacterOfPosition( + node.getStart(source) + ) + hits.push({ + path, + line: line + 1, + text: parts.join('[…]').slice(0, 120), + }) + } + } else if (ts.isJsxText(node)) { + const value = node.getText(source) + if (PERSIAN.test(value)) { + const { line } = source.getLineAndCharacterOfPosition( + node.getStart(source) + ) + hits.push({ path, line: line + 1, text: value.trim().slice(0, 120) }) + } + } else if (ts.isIdentifier(node) && PERSIAN.test(node.text)) { + const { line } = source.getLineAndCharacterOfPosition(node.getStart(source)) + hits.push({ path, line: line + 1, text: node.text }) + } + ts.forEachChild(node, visit) + } + visit(source) + return hits +} + +function persianCommentCount(): number { + let count = 0 + for (const path of scanFiles().filter((p) => /\.tsx?$/.test(p))) { + const text = readFileSync(path, 'utf8') + if (!PERSIAN.test(text) || (!text.includes('//') && !text.includes('/*'))) + continue + const kind = path.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS + const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, kind) + const seen = new Map() + const visit = (node: ts.Node) => { + for (const range of ts.getLeadingCommentRanges(text, node.getFullStart()) ?? + []) { + seen.set(range.pos, range) + } + for (const range of ts.getTrailingCommentRanges(text, node.getEnd()) ?? []) { + seen.set(range.pos, range) + } + ts.forEachChild(node, visit) + } + visit(source) + for (const range of seen.values()) { + if (PERSIAN.test(text.slice(range.pos, range.end))) count += 1 + } + } + return count +} + +function collectHits(): Hit[] { + const allow = new Set(ALLOWLIST.map((entry) => entry.path)) + return scanFiles().flatMap((path) => (allow.has(path) ? [] : scanFile(path))) +} + +describe('i18n catalog', () => { + it('keeps no Persian text outside the catalog and the allowlist', () => { + const hits = collectHits() + const report = hits.map( + (hit) => + `${hit.path}:${hit.line}: ${JSON.stringify(hit.text)} — move this text to src/common/i18n/fa/.ts and read it with t()` + ) + expect(report).toEqual([]) + }) + + it('keeps every allowlist entry justified by a real hit', () => { + const stale = ALLOWLIST.filter((entry) => scanFile(entry.path).length === 0).map( + (entry) => + `${entry.path}: allowlisted (${entry.reason}) but has no Persian hit` + ) + expect(stale).toEqual([]) + }) + + it('reports how many Persian comments remain in code', () => { + expect(persianCommentCount()).toBeGreaterThanOrEqual(0) + }) + + it('references every catalog key from at least one t() call or MessageKey map', () => { + const keys = new Set(Object.keys(fa)) + const used = new Set() + const callPattern = /\bt\(\s*['"]([^'"]+)['"]/g + const mark = (text: string) => { + for (const match of text.matchAll(callPattern)) { + used.add(match[1]) + } + const kind = text.includes(' { + if (ts.isStringLiteral(node) && keys.has(node.text)) { + used.add(node.text) + } + ts.forEachChild(node, visit) + } + visit(source) + } + for (const path of scanFiles().filter((p) => /\.tsx?$/.test(p))) { + mark(readFileSync(path, 'utf8')) + } + for (const path of walk('src/common/__tests__').filter((p) => + p.endsWith('.ts') + )) { + mark(readFileSync(path, 'utf8')) + } + const dead = [...keys].filter((key) => !used.has(key)) + expect(dead).toEqual([]) + }) + + it('never builds a t() key from a template or concatenation', () => { + const bad: string[] = [] + for (const path of scanFiles().filter((p) => /\.tsx?$/.test(p))) { + const text = readFileSync(path, 'utf8') + const kind = path.endsWith('x') ? ts.ScriptKind.TSX : ts.ScriptKind.TS + const source = ts.createSourceFile( + path, + text, + ts.ScriptTarget.Latest, + true, + kind + ) + const visit = (node: ts.Node) => { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 't' && + node.arguments[0] + ) { + const arg = node.arguments[0] + const ok = + ts.isStringLiteral(arg) || + ts.isNoSubstitutionTemplateLiteral(arg) || + ts.isIdentifier(arg) || + ts.isPropertyAccessExpression(arg) || + ts.isElementAccessExpression(arg) + if ( + !ok || + ts.isTemplateExpression(arg) || + ts.isBinaryExpression(arg) + ) { + const { line } = source.getLineAndCharacterOfPosition( + node.getStart(source) + ) + bad.push(`${path}:${line + 1}`) + } + } + ts.forEachChild(node, visit) + } + visit(source) + } + expect(bad).toEqual([]) + }) + + it('exports a working t helper', () => { + expect(typeof t).toBe('function') + expect(t('common.coin.name')).toBe('ویج‌کوین') + }) + + it('keeps catalog values free of tanween and lone sentence periods', () => { + const bad = Object.entries(fa).flatMap(([key, value]) => { + const hits: string[] = [] + if (/\u064B/.test(value)) { + hits.push(`${key}: tanween (ً) — write فعلا/مثلا/دقیقا without it`) + } + if (/(?.ts` and is read with `t()` from `@/common/i18n`. Keys are flat (`area.scope.name`), typed as `MessageKey`, and never built from a template or concatenation. Name a key after its purpose (`market.coin.amountLabel`, `setting.auth.continue`), never after a file path and never with a numeric suffix. Parameters use `{name}` and are typed from the message text. A constant that needs a label stores a `MessageKey` (`labelKey`, `titleKey`, …) and the render site calls `t(...)`. Primitives take finished text from props; they do not import the catalog. Catalog wording follows `.agents/TYPOGRAPHY.md`: write `فعلا` / `مثلا` / `دقیقا` without tanween (`ً`), and do not end a UI sentence with a lone `.` (ellipsis `...` / `…` is fine; domains like `widgetify.ir` stay). Numbers and digits are not converted by i18n: a digit stays as written, and existing `toLocaleString('fa-IR')` / Jalali formatting is left alone. The short allowlist in `src/__tests__/i18n.test.ts` is only for locale data and saved/compared values, not for UI copy. A second locale later is a new `/` folder with the same keys plus a way to pick it; that is not built now. + ## Animation - Use `Motion` and `Presence` from `@/common/motion`, never `framer-motion`. The wrappers are what make optimisation mode work. diff --git a/src/common/__tests__/t.test.ts b/src/common/__tests__/t.test.ts new file mode 100644 index 000000000..2d6b6fd23 --- /dev/null +++ b/src/common/__tests__/t.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'bun:test' +import { t } from '@/common/i18n' + +describe('t', () => { + it('looks up a catalog string', () => { + expect(t('common.coin.name')).toBe('ویج‌کوین') + }) + + it('replaces named parameters', () => { + expect(t('ui.slider.slide', { n: 3 })).toBe('اسلاید 3') + }) + + it('leaves an unknown placeholder when a parameter is missing at runtime', () => { + const broken = t as ( + key: string, + params?: Record + ) => string + expect(broken('ui.slider.slide', {})).toBe('اسلاید {n}') + }) + + it('looks up confirmation copy', () => { + expect(t('ui.common.confirmAction')).toBe('این کار انجام بشه؟') + expect(t('ui.common.areYouSure')).toBe('مطمئنی؟') + expect(t('ui.common.confirm')).toBe('تایید') + expect(t('ui.common.cancel')).toBe('انصراف') + expect(t('ui.common.close')).toBe('بستن') + expect(t('ui.common.moment')).toBe('یه لحظه…') + expect(t('ui.common.loading')).toBe('در حال بارگذاری') + expect(t('ui.common.notifications')).toBe('اعلان‌ها') + expect(t('ui.common.pickColor')).toBe('انتخاب رنگ') + expect(t('ui.common.previous')).toBe('قبلی') + expect(t('ui.common.next')).toBe('بعدی') + expect(t('ui.vip.pro')).toBe('پرو') + expect(t('ui.slider.prevImage')).toBe('عکس قبلی') + expect(t('ui.slider.nextImage')).toBe('عکس بعدی') + expect(t('ui.slider.role')).toBe('اسلایدر') + expect(t('ui.slider.images')).toBe('تصاویر') + expect(t('ui.date.weekday.sat')).toBe('ش') + expect(t('ui.date.weekday.sun')).toBe('ی') + expect(t('ui.date.weekday.mon')).toBe('د') + expect(t('ui.date.weekday.tue')).toBe('س') + expect(t('ui.date.weekday.wed')).toBe('چ') + expect(t('ui.date.weekday.thu')).toBe('پ') + expect(t('ui.date.weekday.fri')).toBe('ج') + }) +}) diff --git a/src/common/__tests__/translate-error.test.ts b/src/common/__tests__/translate-error.test.ts index efa9e93f4..33107d742 100644 --- a/src/common/__tests__/translate-error.test.ts +++ b/src/common/__tests__/translate-error.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'bun:test' +import { error } from '../i18n/fa/error' import { translateError } from '../utils/translate-error' -const fallback = 'یه مشکلی پیش اومد، دوباره امتحان کن' +const fallback = error['error.default'] describe('translateError', () => { it('falls back to a friendly message when there is nothing to translate', () => { @@ -12,19 +13,19 @@ describe('translateError', () => { }) it('translates a known code, whether it is the error or the server message', () => { - expect(translateError('NOT_FOUND')).toBe('چیزی پیدا نکردیم') + expect(translateError('NOT_FOUND')).toBe(error['error.notFound']) expect( translateError({ response: { data: { message: 'USER_NOT_FOUND' } } }) - ).toBe('این کاربر رو پیدا نکردیم') - expect(translateError({ message: 'NOT_FOUND' })).toBe('چیزی پیدا نکردیم') + ).toBe(error['error.userNotFound']) + expect(translateError({ message: 'NOT_FOUND' })).toBe(error['error.notFound']) }) it('prefers the message the server sent over the error object message', () => { - const error = { + const err = { message: 'NOT_FOUND', response: { data: { message: 'USER_NOT_FOUND' } }, } - expect(translateError(error)).toBe('این کاربر رو پیدا نکردیم') + expect(translateError(err)).toBe(error['error.userNotFound']) }) it('shows an unknown message as it is instead of hiding it', () => { @@ -32,7 +33,7 @@ describe('translateError', () => { }) it('returns one message per field for form validation errors', () => { - const error = { + const err = { response: { data: { formValidation: [ @@ -42,13 +43,13 @@ describe('translateError', () => { }, }, } - const result = translateError(error) + const result = translateError(err) expect(typeof result).toBe('object') expect(Object.keys(result as Record)).toEqual(['email', 'name']) }) it('translates a known validation message and keeps an unknown one', () => { - const error = { + const err = { response: { data: { formValidation: [ @@ -58,14 +59,14 @@ describe('translateError', () => { }, }, } - expect(translateError(error)).toEqual({ - username: 'نام کاربری نمی‌تونه خالی باشه', + expect(translateError(err)).toEqual({ + username: error['error.validation.usernameEmpty'], bio: 'something the server added', }) }) it('returns the field map even when only one field failed', () => { - const error = { + const err = { response: { data: { formValidation: [ @@ -75,13 +76,15 @@ describe('translateError', () => { }, }, } - expect(translateError(error)).toEqual({ username: 'این نام کاربری وجود نداره' }) + expect(translateError(err)).toEqual({ + username: error['error.validation.usernameMissing'], + }) }) it('ignores an empty validation list and uses the message instead', () => { - const error = { + const err = { response: { data: { formValidation: [], message: 'NOT_FOUND' } }, } - expect(translateError(error)).toBe('چیزی پیدا نکردیم') + expect(translateError(err)).toBe(error['error.notFound']) }) }) diff --git a/src/common/constants/moods.ts b/src/common/constants/moods.ts index 506c7ff79..98ccc7df0 100644 --- a/src/common/constants/moods.ts +++ b/src/common/constants/moods.ts @@ -3,6 +3,7 @@ import excited from '@/assets/images/moods/excited.webp' import happy from '@/assets/images/moods/happy.webp' import normal from '@/assets/images/moods/normal.webp' import sad from '@/assets/images/moods/sad.webp' +import type { MessageKey } from '@/common/i18n' export const moodEmptyImage = empty @@ -10,28 +11,28 @@ export const moodOptions = [ { value: 'sad', image: sad, - label: 'ناراحتم', + labelKey: 'mood.label.sad' as const satisfies MessageKey, activeClass: 'bg-danger text-on-danger', borderClass: 'border-[rgba(var(--color-error-rgb),0.5)]', }, { value: 'normal', image: normal, - label: 'خستم', + labelKey: 'mood.label.normal' as const satisfies MessageKey, activeClass: 'bg-warning text-on-warning', borderClass: 'border-[rgba(var(--color-warning-rgb),0.5)]', }, { value: 'happy', image: happy, - label: 'اوکی‌ام', + labelKey: 'mood.label.happy' as const satisfies MessageKey, activeClass: 'bg-secondary text-on-secondary', borderClass: 'border-[rgba(var(--color-secondary-rgb),0.5)]', }, { value: 'excited', image: excited, - label: 'سرحالم', + labelKey: 'mood.label.excited' as const satisfies MessageKey, activeClass: 'bg-success text-on-success', borderClass: 'border-[rgba(var(--color-success-rgb),0.5)]', }, diff --git a/src/common/i18n/fa/auth.ts b/src/common/i18n/fa/auth.ts new file mode 100644 index 000000000..4b200d6ff --- /dev/null +++ b/src/common/i18n/fa/auth.ts @@ -0,0 +1,6 @@ +export const auth = { + 'auth.required.title': 'ورود به حساب کاربری', + 'auth.required.message': 'برای دسترسی به این بخش اول وارد حسابت شو', + 'auth.required.login': 'ورود به حساب', + 'auth.required.cancel': 'فعلا نه', +} as const diff --git a/src/common/i18n/fa/city.ts b/src/common/i18n/fa/city.ts new file mode 100644 index 000000000..2273b7024 --- /dev/null +++ b/src/common/i18n/fa/city.ts @@ -0,0 +1,13 @@ +export const city = { + 'city.select.title': 'انتخاب شهر', + 'city.select.placeholder': 'انتخاب شهر…', + 'city.select.loadFailedTitle': 'نتونستیم فهرست شهرها رو بیاریم', + 'city.select.loadFailedBody': 'اینترنتت رو چک کن و دوباره امتحان کن', + 'city.select.loginRequired': 'برای انتخاب شهر اول وارد حسابت شو', + 'city.select.searchPlaceholder': 'جستجوی شهر…', + 'city.select.loading': 'یه لحظه…', + 'city.select.noMatch': 'شهری با این اسم پیدا نکردیم', + 'city.select.emptyList': 'فعلا شهری توی فهرست نیست', + 'city.select.typeName': 'اسم شهرت رو بنویس', + 'city.select.missingHint': 'اگه شهرت توی فهرست نبود، بهمون بگو تا اضافه‌ش کنیم 🤝', +} as const diff --git a/src/common/i18n/fa/common.ts b/src/common/i18n/fa/common.ts new file mode 100644 index 000000000..1647f8381 --- /dev/null +++ b/src/common/i18n/fa/common.ts @@ -0,0 +1,5 @@ +export const common = { + 'common.coin.name': 'ویج‌کوین', + 'common.coin.zero': '۰', + 'common.config.versionName': 'پاییز', +} as const diff --git a/src/common/i18n/fa/context.ts b/src/common/i18n/fa/context.ts new file mode 100644 index 000000000..38f5388e4 --- /dev/null +++ b/src/common/i18n/fa/context.ts @@ -0,0 +1,8 @@ +export const context = { + 'context.appearance.loginRequired': 'برای استفاده از این حالت اول وارد حسابت شو', + 'context.general.timezoneTehran': 'آسیا / تهران', + 'context.wallpaper.customGradient': 'گرادیان سفارشی', + 'context.wallpaper.gradient': 'گرادیان', + 'context.wallpaper.loginRequired': 'برای انتخاب این تصویر زمینه اول وارد حسابت شو', + 'context.wallpaper.activated': 'هووورا! تصویر زمینه فعال شد 🎉', +} as const diff --git a/src/common/i18n/fa/error.ts b/src/common/i18n/fa/error.ts new file mode 100644 index 000000000..fd412e6e8 --- /dev/null +++ b/src/common/i18n/fa/error.ts @@ -0,0 +1,134 @@ +export const error = { + 'error.activityAlreadyExists': 'فعلا فقط یه نوشته می‌تونی داشته باشی', + 'error.maxSharedUsersExceeded': + 'این تسک رو با بیشتر از این تعداد نمی‌شه به اشتراک گذاشت', + 'error.activityNotFound': 'این نوشته رو پیدا نکردیم', + 'error.invalidPassMail': 'ایمیل یا رمز عبور درست نیست', + 'error.invalidCredentials': 'اطلاعات ورود درست نیست، یه بار دیگه چک کن', + 'error.emailAlreadyExists': 'این ایمیل از قبل ثبت شده', + 'error.userNotFound': 'این کاربر رو پیدا نکردیم', + 'error.tokenExpired': 'ورودت منقضی شده، دوباره وارد حسابت شو', + 'error.invalidToken': 'ورودت دیگه معتبر نیست، دوباره وارد حسابت شو', + 'error.unauthorized': 'به این بخش دسترسی نداری', + 'error.forbidden': 'دسترسی به این بخش محدود شده', + 'error.otpRateLimit': 'همین الان یه کد برات فرستادیم، یه کم صبر کن', + 'error.forgotPasswordRequestLimit': + 'زیادی درخواست بازیابی رمز دادی، یه کم بعد دوباره امتحان کن', + 'error.resetTokenExpired': 'لینک بازیابی رمز عبور منقضی شده، یه لینک جدید بگیر', + 'error.invalidResetToken': 'این لینک بازیابی رمز عبور درست نیست', + 'error.weakPassword': 'رمز عبور ضعیفه، از حروف، اعداد و نمادها استفاده کن', + 'error.passwordTooShort': 'رمز عبور باید حداقل ۸ کاراکتر باشه', + 'error.invalidEmailFormat': 'این ایمیل درست به نظر نمی‌رسه', + 'error.nameRequired': 'نام کاربری رو بنویس', + 'error.invalidInputs': 'یه چیزی توی اطلاعاتی که وارد کردی درست نیست', + 'error.internalServerError': 'سرور به مشکل خورده، یه کم بعد دوباره امتحان کن', + 'error.serviceUnavailable': 'سرویس الان در دسترس نیست، یه کم بعد دوباره امتحان کن', + 'error.tooManyRequests': 'یه کم تند رفتی، چند لحظه صبر کن و دوباره امتحان کن', + 'error.badRequest': 'این درخواست درست نبود، دوباره امتحان کن', + 'error.notFound': 'چیزی پیدا نکردیم', + 'error.activityUpdateRateLimitExceeded': 'وضعیتت رو زیادی عوض کردی، یه کم صبر کن', + 'error.cantRequestYourself': 'نمی‌تونی به خودت درخواست دوستی بفرستی', + 'error.friendRequestAlreadySent': 'قبلا براش درخواست دوستی فرستادی', + 'error.friendRequestAlreadyExists': 'از قبل یه درخواست دوستی بینتون هست', + 'error.failedToFetchFriends': 'نتونستیم فهرست دوستات رو بیاریم', + 'error.failedToSendRequest': 'نتونستیم درخواست دوستی رو بفرستیم', + 'error.failedToAcceptRequest': 'نتونستیم درخواست دوستی رو قبول کنیم', + 'error.failedToRemoveFriend': 'نتونستیم این دوست رو حذف کنیم', + 'error.friendRequestSent': 'درخواست دوستی فرستاده شد', + 'error.friendRequestNotFound': 'این درخواست دوستی رو پیدا نکردیم', + 'error.setUsernameFirst': 'اول نام کاربریت رو تنظیم کن', + 'error.sourceAndTargetLangMustBeDifferent': 'زبان مبدأ و مقصد نمی‌تونن یکسان باشن', + 'error.targetLangCannotBeAuto': 'زبان مقصد نمی‌تونه تشخیص خودکار باشه', + 'error.translationFailed': 'نتونستیم متن رو ترجمه کنیم', + 'error.failedToFetchLanguages': 'نتونستیم فهرست زبان‌ها رو بیاریم', + 'error.invalidLanguageCode': 'این زبان رو نمی‌شناسیم', + 'error.textTooLong': 'متن برای ترجمه خیلی طولانیه', + 'error.emptyText': 'متن برای ترجمه نمی‌تونه خالی باشه', + 'error.translationQuotaExceeded': 'سهمیه‌ی ترجمه‌ت تموم شده', + 'error.success': 'انجام شد', + 'error.widgetNotFound': 'این ویجت رو پیدا نکردیم', + 'error.widgetDeleted': 'ویجت حذف شد', + 'error.widgetDuplicated': 'ویجت تکرار شد', + 'error.widgetAlreadyExists': 'این ویجت از قبل به صفحه اضافه شده', + 'error.invalidWidgetPosition': 'موقعیت قرارگیری ویجت درست نیست', + 'error.storageQuotaExceeded': 'فضای ذخیره‌سازی عکس‌های ویجت پر شده', + 'error.widgetLimitExceeded': 'به سقف تعداد ویجت‌ها رسیدی', + 'error.maxWidgetsReached': + 'به سقف تعداد ویجت‌ها رسیدی، یکی از قبلی‌ها رو حذف کن تا جا باز بشه', + 'error.noSpaceForWidget': + 'روی صفحه جا نیست، چند تا ویجت رو جابه‌جا یا حذف کن تا جا باز بشه', + 'error.noSpaceForDuplicate': 'روی صفحه جا نیست، برای تکرار ویجت کمی فضا باز کن', + 'error.bookmarkDeleted': 'بوکمارک حذف شد', + 'error.bookmarkAdded': 'بوکمارک اضافه شد', + 'error.bookmarkUpdated': 'بوکمارک ویرایش شد', + 'error.bookmarkParentNotFound': 'این پوشه رو پیدا نکردیم', + 'error.fileSizeExceeded': 'این فایل زیادی بزرگه', + 'error.networkError': 'اینترنتت رو چک کن و دوباره امتحان کن', + 'error.connectionTimeout': 'جواب دیر رسید، دوباره امتحان کن', + 'error.connectionRefused': 'نتونستیم وصل بشیم، یه کم بعد دوباره امتحان کن', + 'error.firstVerifyYourAccount': 'اول حسابت رو تایید کن', + 'error.usernameAlreadyExists': 'این نام کاربری رو قبلا یکی برداشته', + 'error.invalidFileType': 'این نوع فایل پشتیبانی نمی‌شه', + 'error.notEnoughCoins': 'ویج‌کوین‌هات کافی نیست', + 'error.invalidReferralCode': 'این کد دعوت درست نیست', + 'error.itemAlreadyExists': 'این رو قبلا گرفتی و مال توئه، لازم نیست دوباره بخریش', + 'error.invalidId': 'یه چیزی درست نیست، دوباره امتحان کن', + 'error.dateOutOfRange': 'این تاریخ رو نمی‌شه انتخاب کرد', + 'error.itemNotFound': 'پیداش نکردیم', + 'error.todoNotFound': 'این تسک رو پیدا نکردیم', + 'error.invalidOtpCode': 'کد تایید اشتباهه، دوباره امتحان کن', + 'error.useEmailForOtp': 'فعلا کد تایید رو با ایمیل بگیر', + 'error.usePhoneForOtp': 'فعلا کد تایید رو با شماره موبایل بگیر', + 'error.invalidOccupationId': 'این شغل توی فهرست نیست، یکی دیگه انتخاب کن', + 'error.oneOrMoreInvalidInterestIds': + 'چند تا از علاقه‌مندی‌هایی که انتخاب کردی توی فهرست نیستن', + 'error.tooManyAttempts': 'زیادی امتحان کردی، یه کم صبر کن', + 'error.otpExpired': 'این کد منقضی شده، یه کد جدید بگیر', + 'error.invalidPhoneNumberFormat': 'این شماره درست به نظر نمی‌رسه', + 'error.cannotChangePhoneNumber': 'نمی‌تونی شماره موبایل رو تغییر بدی', + 'error.samePhoneNumberError': 'این همون شماره‌ی فعلیته', + 'error.phoneNumberAlreadyExists': 'این شماره موبایل از قبل ثبت شده', + 'error.invalidVerificationCode': 'کد تایید درست نیست', + 'error.cannotChangeEmail': 'نمی‌تونی ایمیل رو تغییر بدی', + 'error.sameEmailError': 'این همون ایمیل فعلیته', + 'error.firstSetEmail': 'هنوز ایمیل ثبت نکردی', + 'error.packageNotFound': 'این بسته رو پیدا نکردیم', + 'error.paymentFailed': 'پرداخت انجام نشد، دوباره امتحان کن', + 'error.paymentAlreadyProcessed': 'این پرداخت قبلا انجام شده', + 'error.paymentNotFound': 'این پرداخت رو پیدا نکردیم', + 'error.tryNextTime': 'یه مشکلی پیش اومد، یه کم بعد دوباره امتحان کن', + 'error.tooManyAttemptsHabit': 'بیشتر از این نمی‌تونی بسازی', + 'error.folderStructureTooDeep': 'پوشه‌ها زیادی تو در تو شدن', + 'error.bulkImportLimitExceeded': 'این تعداد رو یه‌جا نمی‌شه درون‌ریزی کرد', + 'error.noValidItemsToImport': 'چیزی برای درون‌ریزی پیدا نکردیم', + 'error.birthdateCannotBeChanged': + 'تازه تاریخ تولدت رو عوض کردی، فعلا نمی‌شه دوباره عوضش کرد', + 'error.vipRequired': 'این قابلیت مال نسخه‌ی پروئه', + 'error.uploadInProgress': 'داریم فایل رو آپلود می‌کنیم، یه کم صبر کن', + 'error.customWallpaperRemoved': 'تصویر پس‌زمینه حذف شد', + 'error.uploadFailed': 'نتونستیم فایل رو آپلود کنیم، دوباره امتحان کن', + 'error.validation.passwordMinLength': 'رمز عبور باید حداقل ۸ کاراکتر باشه', + 'error.validation.passwordUppercase': 'رمز عبور باید حداقل یه حرف بزرگ داشته باشه', + 'error.validation.passwordLowercase': 'رمز عبور باید حداقل یه حرف کوچک داشته باشه', + 'error.validation.passwordNumber': 'رمز عبور باید حداقل یه عدد داشته باشه', + 'error.validation.passwordSymbol': 'رمز عبور باید حداقل یه نماد مثل @#$% داشته باشه', + 'error.validation.passwordString': 'رمز عبور باید متن باشه', + 'error.validation.passwordEmpty': 'رمز عبور نمی‌تونه خالی باشه', + 'error.validation.emailFormat': 'این ایمیل درست به نظر نمی‌رسه', + 'error.validation.emailEmpty': 'ایمیل نمی‌تونه خالی باشه', + 'error.validation.emailString': 'ایمیل باید متن باشه', + 'error.validation.nameEmpty': 'نام کاربری نمی‌تونه خالی باشه', + 'error.validation.nameString': 'نام کاربری باید متن باشه', + 'error.validation.nameMinLength': 'نام کاربری باید حداقل ۳ کاراکتر باشه', + 'error.validation.nameMaxLength': 'نام کاربری باید حداکثر ۵۰ کاراکتر باشه', + 'error.validation.widgetTitleEmpty': 'عنوان ویجت نمی‌تونه خالی باشه', + 'error.validation.widgetPosition': 'جای ویجت درست نیست', + 'error.validation.widgetSize': 'اندازه‌ی ویجت درست نیست', + 'error.validation.usernameEmpty': 'نام کاربری نمی‌تونه خالی باشه', + 'error.validation.usernameMissing': 'این نام کاربری وجود نداره', + 'error.validation.friendSelf': 'نمی‌تونی به خودت درخواست دوستی بفرستی', + 'error.validation.friendAlreadySent': 'قبلا براش درخواست دوستی فرستادی', + 'error.validation.nameMin2': 'نام کاربری رو بنویس', + 'error.contentContainsProfanity': 'توی متنت کلمه‌ی نامناسب هست، یه کم عوضش کن', + 'error.default': 'یه مشکلی پیش اومد، دوباره امتحان کن', +} as const diff --git a/src/common/i18n/fa/explorer.ts b/src/common/i18n/fa/explorer.ts new file mode 100644 index 000000000..ea18720af --- /dev/null +++ b/src/common/i18n/fa/explorer.ts @@ -0,0 +1,18 @@ +export const explorer = { + 'explorer.category.updateExtension': 'افزونه رو به‌روز کن', + 'explorer.site.unnamed': 'بدون نام', + 'explorer.promo.discountCopied': 'کد تخفیف {code} کپی شد', + 'explorer.promo.discountCode': 'کد تخفیف', + 'explorer.promo.copy': 'کپی', + 'explorer.promo.viewAndEnter': 'مشاهده و ورود', + 'explorer.promo.sponsor': 'اسپانسر', + 'explorer.promo.copyFailed': 'نتونستیم کد رو کپی کنیم', + 'explorer.promo.copyAndEnter': 'کپی کد و ورود به سایت', + 'explorer.promo.exclusiveCode': 'کد تخفیف اختصاصی', + 'explorer.promo.copied': 'کپی شد', + 'explorer.promo.close': 'بستن', + 'explorer.search.placeholder': 'جستجو در تمام سایت‌ها و دسته‌ها...', + 'explorer.search.noResults': 'برای «{query}» چیزی پیدا نکردیم', + 'explorer.search.tryAgain': 'یه چیز دیگه رو جستجو کن یا فیلتر رو پاک کن', + 'explorer.search.clear': 'پاک کردن جستجو', +} as const diff --git a/src/common/i18n/fa/friends.ts b/src/common/i18n/fa/friends.ts new file mode 100644 index 000000000..6b25d625d --- /dev/null +++ b/src/common/i18n/fa/friends.ts @@ -0,0 +1,59 @@ +export const friends = { + 'friends.you': 'شما', + 'friends.activity.reactAria': 'واکنش به این فعالیت', + 'friends.activity.writeFirst': 'اول یه چیزی بنویس', + 'friends.activity.cannotPost': 'فعلا نمی‌تونی نوشته‌ی جدید بذاری', + 'friends.activity.deleted': 'نوشته‌ت حذف شد', + 'friends.activity.deleteFailed': 'نتونستیم نوشته رو حذف کنیم، دوباره امتحان کن', + 'friends.activity.current': 'نوشته‌ی فعلیت', + 'friends.activity.delete': 'حذف نوشته', + 'friends.activity.reactions': 'واکنش‌ها ({count})', + 'friends.activity.noReactions': 'فعلا واکنشی نداری 😶‍🌫️', + 'friends.activity.textLabel': 'متن نوشته', + 'friends.activity.friendsOnly': 'نوشته‌ت رو فقط دوستات می‌بینن', + 'friends.activity.duration1Day': '1 روز نمایش بده', + 'friends.activity.duration4Hours': '4 ساعت نمایش بده', + 'friends.activity.duration1Hour': '1 ساعت نمایش بده', + 'friends.activity.display': 'نمایش', + 'friends.activity.placeholder': 'یه چیزی بگو...', + 'friends.activity.publish': 'انتشار نوشته', + 'friends.activity.rulesTitle': 'قوانین', + 'friends.activity.rule1': 'متن توهین‌آمیز، سیاسی یا دینی ننویس', + 'friends.activity.rule2': 'لینک یا محتوای بیرونی نذار', + 'friends.activity.rule3': + 'اگه نوشته‌ای ازت گزارش بشه و نامناسب باشه، ممکنه حسابت محدود بشه', + 'friends.activity.rule4': 'کمکمون کن اینجا جای امن و دوستانه‌ای بمونه', + 'friends.activity.rule5': 'هر روز فقط یه نوشته‌ی جدید می‌تونی بذاری', + 'friends.activity.gotIt': 'متوجه شدم', + 'friends.add.setUsernameFirst': + 'برای فرستادن درخواست دوستی، اول توی پروفایلت یه نام کاربری انتخاب کن', + 'friends.add.sent': 'درخواست دوستی فرستاده شد', + 'friends.add.title': 'افزودن دوست جدید', + 'friends.add.prompt': 'نام کاربری دوستت رو بنویس', + 'friends.add.setUsernameFirstLong': + 'برای فرستادن درخواست دوستی، اول توی پروفایلت یه نام کاربری انتخاب\n\t\t\t\t\t\tکن', + 'friends.add.usernameLabel': 'نام کاربری', + 'friends.add.usernameExample': 'مثال: john_doe', + 'friends.add.friendUsername': 'نام کاربری دوست', + 'friends.add.send': 'فرستادن درخواست', + 'friends.empty.list': 'هنوز چیزی اینجا نیست', + 'friends.requests.nowFriends': 'حالا با هم دوستید', + 'friends.requests.error': 'یه مشکلی پیش اومد، دوباره امتحان کن', + 'friends.requests.accept': 'دوست شیم', + 'friends.requests.decline': 'رد کردن', + 'friends.requests.sent': 'ارسال شده', + 'friends.requests.title': 'درخواست‌های دوستی', + 'friends.requests.empty': 'درخواست دوستی تازه‌ای نداری', + 'friends.requests.button': 'درخواست‌ها', + 'friends.actions.add': 'افزودن دوست', + 'friends.direct.statuses': 'وضعیت‌ها', + 'friends.direct.friends': 'دوستان', + 'friends.direct.add': 'افزودن دوست', + 'friends.direct.empty': 'هنوز دوستی نداری', + 'friends.direct.addFirst': 'اولین دوستت رو اضافه کن', + 'friends.direct.remove': 'حذف دوست', + 'friends.direct.removeConfirm': '"{name}" از لیست دوستات حذف بشه؟', + 'friends.remove': 'حذف', + 'friends.select.empty': 'هنوز دوستی نداری', + 'friends.page.empty': 'هنوز دوستی اضافه نکردی', +} as const diff --git a/src/common/i18n/fa/gallery.ts b/src/common/i18n/fa/gallery.ts new file mode 100644 index 000000000..eb537661d --- /dev/null +++ b/src/common/i18n/fa/gallery.ts @@ -0,0 +1,29 @@ +export const gallery = { + 'gallery.purchase.owned': '{name} مال تو شد', + 'gallery.purchase.itemFallback': 'آیتم', + 'gallery.purchase.fetchFailed': 'نتونستیم آیتم رو بگیریم، دوباره امتحان کن', + 'gallery.purchase.imageAlt': 'تصویر گالری', + 'gallery.purchase.permanentPrice': 'قیمت خرید دائمی', + 'gallery.purchase.vipHint': + 'چون اشتراک پرو داری می‌تونی این تصویر رو رایگان فعال کنی یا با ویج‌کوین دائمی بخریش', + 'gallery.purchase.buyHint': 'این تصویر رو با ویج‌کوین باز کن و همیشه ازش استفاده کن', + 'gallery.purchase.buyCoins': 'خرید ویج‌کوین', + 'gallery.purchase.coinsShort': '{count} ویج‌کوین کم داری', + 'gallery.purchase.freeWithPro': 'استفاده رایگان با پرو', + 'gallery.purchase.buying': 'داریم می‌خریم…', + 'gallery.purchase.buyPermanent': 'خرید دائمی', + 'gallery.purchase.cancel': 'انصراف', + 'gallery.bookmark.fetchFailed': 'نتونستیم بیاریمش', + 'gallery.bookmark.iconAlt': 'آیکون بوکمارک', + 'gallery.bookmark.price': 'قیمت خرید', + 'gallery.bookmark.freeWithPro': 'رایگان با پرو', + 'gallery.bookmark.owned': 'مال توئه', + 'gallery.photo.fetchFailed': 'نتونستیم بیاریمش', + 'gallery.photo.price': 'قیمت خرید', + 'gallery.photo.freeWithPro': 'رایگان با پرو', + 'gallery.photo.owned': 'مال توئه', + 'gallery.picker.title': 'گالری تصاویر', + 'gallery.picker.loginRequired': 'برای استفاده از این مورد اول وارد حسابت شو', + 'gallery.picker.all': 'همه', + 'gallery.picker.empty': 'تصویری پیدا نکردیم', +} as const diff --git a/src/common/i18n/fa/home.ts b/src/common/i18n/fa/home.ts new file mode 100644 index 000000000..91760c777 --- /dev/null +++ b/src/common/i18n/fa/home.ts @@ -0,0 +1,40 @@ +export const home = { + 'home.dialog.view': 'مشاهده', + 'home.dialog.close': 'بستن', + 'home.installed.welcome': 'به ویجتیفای خوش اومدی!', + 'home.installed.howToEnable': 'نحوه فعالسازی افزونه', + 'home.installed.keepItWarning': '⚠️ برای فعال شدن افزونه، دکمه‌ی "Keep It" رو بزن', + 'home.installed.start': 'شروع کنیم', + 'home.firefox.uninstallConfirm': + 'برای کارکرد کامل افزونه به این دسترسی‌ها نیاز داریم، می‌خوای افزونه رو حذف کنی؟', + 'home.firefox.privacyTitle': 'حریم خصوصی', + 'home.firefox.privacyBody': + 'تمام تنظیمات در مرورگر خودت ذخیره می‌شن، انتخاب کن کدوم موارد فعال باشن', + 'home.firefox.faviconTitle': 'دریافت آیکون سایت‌ها (Google Favicon)', + 'home.firefox.faviconBody': + 'ارسال دامنه سایت به سرویس رسمی گوگل برای نمایش آیکون بوکمارک‌ها', + 'home.firefox.analyticsTitle': 'ارسال آمار فنی و کارایی (Google Analytics)', + 'home.firefox.analyticsBody': + 'ارسال داده‌های کاملا ناشناس و بدون اطلاعات هویتی برای رفع باگ‌ها', + 'home.firefox.changeLater': 'می‌تونی بعدا توی تنظیمات این موارد رو تغییر بدی', + 'home.firefox.privacyPolicy': 'سیاست حریم خصوصی', + 'home.firefox.removeExtension': 'حذف افزونه', + 'home.firefox.confirmContinue': 'تایید و ادامه', + 'home.tour.from': 'از', + 'home.tour.skip': 'رد کردن', + 'home.tour.prev': 'قبلی', + 'home.tour.finish': 'پایان', + 'home.tour.next': 'بعدی', + 'home.tour.close': 'بستن', + 'home.tour.step1.title': 'خلوت کردن فضای مرورگر', + 'home.tour.step1.bodyBefore': 'برای مخفی کردن این نوار، کافیه روش', + 'home.tour.step1.rightClick': 'راست‌کلیک', + 'home.tour.step1.bodyAfter': 'کنی و این\n\t\t\t\t\tگزینه رو بزنی:\n\t\t\t\t', + 'home.tour.step1.imageAlt': 'نحوه مخفی کردن نوار پایین مرورگر', + 'home.tour.step2.body': + 'برای تغییر اندازه، جابه‌جایی، تغییر استایل، کپی یا حذف هر ویجت، کافیه\n\t\t\t\t\tروش راست‌کلیک کنی تا منوش باز بشه\n\t\t\t\t', + 'home.tour.step3.body': + 'از این بخش می‌تونی ویجت جدید اضافه کنی، وارد حالت ویرایش بشی، ظاهر صفحه رو شخصی‌سازی کنی یا تصویر زمینه رو تغییر بدی', + 'home.tour.step4.body': + 'از این منو می‌تونی به پروفایل، تنظیمات کلی، فروشگاه، اشتراک پرو و مدیریت محیط کارها دسترسی داشته باشی', +} as const diff --git a/src/common/i18n/fa/index.ts b/src/common/i18n/fa/index.ts new file mode 100644 index 000000000..9caa34f6e --- /dev/null +++ b/src/common/i18n/fa/index.ts @@ -0,0 +1,39 @@ +import { auth } from './auth' +import { city } from './city' +import { common } from './common' +import { context } from './context' +import { error } from './error' +import { explorer } from './explorer' +import { friends } from './friends' +import { gallery } from './gallery' +import { home } from './home' +import { market } from './market' +import { miniApps } from './mini-apps' +import { mood } from './mood' +import { navbar } from './navbar' +import { news } from './news' +import { releaseNotes } from './release-notes' +import { setting } from './setting' +import { ui } from './ui' +import { widgets } from './widgets' + +export const fa = { + ...auth, + ...city, + ...common, + ...context, + ...error, + ...explorer, + ...friends, + ...gallery, + ...home, + ...market, + ...miniApps, + ...mood, + ...navbar, + ...news, + ...releaseNotes, + ...setting, + ...ui, + ...widgets, +} as const diff --git a/src/common/i18n/fa/market.ts b/src/common/i18n/fa/market.ts new file mode 100644 index 000000000..85f518ba7 --- /dev/null +++ b/src/common/i18n/fa/market.ts @@ -0,0 +1,154 @@ +export const market = { + 'market.category.petsTab': 'حیوون‌ها', + 'market.category.environmentsTab': 'محیط‌ها', + 'market.category.allFilter': 'همه', + 'market.category.buyableFilter': 'قابل خرید', + 'market.category.ownedFilter': 'مال من', + 'market.category.loadErrorTitle': 'آیتم‌ها نیومدن', + 'market.category.loadErrorHint': 'اینترنتت رو چک کن و دوباره امتحان کن', + 'market.category.retry': 'دوباره امتحان کن', + 'market.category.emptyTitle': 'هنوز چیزی اینجا نیست', + 'market.category.emptyOwnedTitle': 'هنوز چیزی از اینجا نخریدی', + 'market.category.allOwnedTitle': 'همه‌ش مال توئه', + 'market.category.emptyOwnedHint': 'هر چی بخری اینجا کنار هم می‌مونه', + 'market.category.emptyBuyableHint': 'فعلا آیتم تازه‌ای برای خرید نیست', + 'market.category.showAll': 'همه رو نشونم بده', + 'market.preview.font.samplePhrase': 'دریاچه‌ای از آرامش', + 'market.preview.font.alphabetSample': 'آ ب پ ت ث ۱۲۳۴۵۶', + 'market.preview.theme.clockSample': '۱۲:۴۵', + 'market.coin.amountLabel': 'ویج‌کوین', + 'market.itemDetail.closeAria': 'بستن جزئیات', + 'market.itemDetail.affectsLabel': 'عوض می‌کنه:', + 'market.itemState.inUse': 'فعاله', + 'market.itemState.owned': 'مال توئه', + 'market.itemState.free': 'رایگان', + 'market.purchase.tryOn': 'امتحانش کن', + 'market.purchase.versionMismatchHint': + 'این حیوون با نسخه‌ی فعلی ویجتیفای کار نمی‌کنه افزونه رو به‌روز کن تا بتونی بخریش', + 'market.purchase.inUseHint': 'الان داری ازش استفاده می‌کنی', + 'market.purchase.ownedPrefix': 'این آیتم مال توئه از «', + 'market.purchase.ownedSuffix': '» انتخابش کن', + 'market.purchase.switching': 'داریم عوضش می‌کنیم...', + 'market.purchase.use': 'استفاده کن', + 'market.purchase.freeSelectHint': 'رایگانه، فقط انتخابش کن', + 'market.purchase.ownedSwitchHint': 'این آیتم مال توئه و هر وقت بخوای عوضش می‌کنی', + 'market.purchase.loginClaim': 'وارد شو و بگیرش', + 'market.purchase.loginBuy': 'وارد شو و بخر', + 'market.purchase.claiming': 'داریم اضافه‌ش می‌کنیم...', + 'market.purchase.claimFree': 'رایگان بگیرش', + 'market.purchase.shortfallHint': '{p0} ویج‌کوین کم داری', + 'market.purchase.balanceLabel': 'موجودیت', + 'market.purchase.shortfallMiddle': 'ویج‌کوینه و این آیتم', + 'market.purchase.shortfallSuffix': 'ویج‌کوین می‌خواد', + 'market.purchase.priceLabel': 'قیمت', + 'market.purchase.balanceAfterLabel': 'موجودیت بعد از خرید', + 'market.purchase.buying': 'داریم می‌خریم...', + 'market.purchase.buyWithPrefix': 'خرید با', + 'market.purchase.foreverHint': 'یه بار می‌خری، برای همیشه مال توئه', + 'market.purchaseSuccess.ownedSuffix': '» مال تو شد', + 'market.purchaseSuccess.selectPrefix': 'هر وقت خواستی از «', + 'market.purchaseSuccess.selectSuffix': '» انتخابش کن', + 'market.purchaseSuccess.useNow': 'همین الان استفاده کن', + 'market.purchaseSuccess.later': 'بعدا', + 'market.purchaseSuccess.ok': 'باشه', + 'market.topUp.cheapestHint': 'ارزون‌ترین بسته‌ای که کسری رو پر می‌کنه', + 'market.topUp.redirecting': 'انتقال به درگاه...', + 'market.topUp.currencyLabel': 'تومان', + 'market.topUp.seeOtherPackages': 'بسته‌های دیگه رو ببین', + 'market.tryOn.ownedActivatedToast': '«{p0}» مال تو شد و فعالش کردیم', + 'market.tryOn.activatedToast': '«{p0}» فعال شد', + 'market.tryOn.previewAria': 'امتحان آیتم روی صفحه', + 'market.tryOn.tryingHint': 'داری امتحانش می‌کنی', + 'market.tryOn.loading': 'یه لحظه...', + 'market.tryOn.keep': 'نگهش دار', + 'market.tryOn.revert': 'برگرد', + 'market.wallpaperTile.priceAria': '{p0}، {p1} ویج‌کوین', + 'market.wallpaperTile.animatedBadge': 'متحرک', + 'market.wallpaperTile.currentBadge': 'تصویر زمینه‌ی فعلی', + 'market.wallpaperTile.tryOnAria': 'امتحان {p0} روی صفحه', + 'market.wallpaperTile.tryOn': 'امتحان', + 'market.picker.moreInStore': '{p0} {p1} دیگه توی فروشگاه', + 'market.picker.storeLink': 'فروشگاه', + 'market.picker.selectedBadge': 'انتخاب شده', + 'market.picker.freeTryHint': 'از فروشگاه، امتحانشون مجانیه', + 'market.picker.notOwnedHint': 'هنوز مال تو نیست', + 'market.nav.sectionsAria': 'بخش‌های فروشگاه', + 'market.nav.newBadge': 'تازه', + 'market.wallet.balanceTitle': 'موجودی ویج‌کوین', + 'market.wallet.loggedOutHint': 'هنوز وارد نشدی', + 'market.wallet.topUp': 'افزایش موجودی', + 'market.wallet.howItWorks': 'ببین چطور کار می‌کنه', + 'market.wallpaperBrowser.coinFilter': 'ویج‌کوینی', + 'market.wallpaperBrowser.ownedFilter': 'خریده‌هام', + 'market.wallpaperBrowser.staticFilter': 'ثابت', + 'market.wallpaperBrowser.loadErrorTitle': 'تصویر زمینه‌ها نیومدن', + 'market.wallpaperBrowser.emptyFilteredTitle': 'با این فیلترها چیزی پیدا نکردیم', + 'market.wallpaperBrowser.emptyFilteredHint': + 'یه پوشه‌ی دیگه رو امتحان کن یا فیلترها رو پاک کن', + 'market.wallpaperBrowser.clearFilters': 'پاک کردن فیلترها', + 'market.wallpaperBrowser.viewLabel': 'نمایش', + 'market.wallpaperBrowser.wallpaperLabel': 'تصویر زمینه', + 'market.product.wallpaperBlurb': 'پس‌زمینه‌ی کل صفحه', + 'market.product.wallpaperSettingsPath': 'تنظیمات › تصویر زمینه‌ها', + 'market.product.themeTitle': 'تم', + 'market.product.themeBlurb': 'رنگ ویجت‌ها، منوها و پنجره‌ها', + 'market.product.appearanceSettingsPath': 'تنظیمات › ظاهری', + 'market.product.fontTitle': 'فونت', + 'market.product.fontBlurb': 'نوشته‌های همه‌ی بخش‌های ویجتیفای', + 'market.product.petTitle': 'حیوان خانگی', + 'market.product.petBlurb': 'حیوونی که توی ویجت پت زندگی می‌کنه', + 'market.product.petSettingsPath': 'تنظیمات ویجت پت', + 'market.product.petEnvTitle': 'محیط حیوون', + 'market.product.petEnvBlurb': 'پس‌زمینه‌ی ویجت پت', + 'market.product.tabTitleTitle': 'عنوان تب', + 'market.product.tabTitleBlurb': 'اسمی که روی تب مرورگر می‌بینی', + 'market.product.showcaseTitle': 'ویترین', + 'market.product.themeShowcaseBody': + 'رنگ‌بندی کل ویجتیفای هر پیش‌نمایش روی تصویر زمینه‌ی خودت نشون داده می‌شه', + 'market.product.fontShowcaseBody': 'فونت همه‌ی نوشته‌های ویجتیفای، از ساعت تا منوها', + 'market.product.tabTitleShowcaseBody': + 'اسمی که روی تب مرورگرت می‌بینی، تا بین تب‌ها زود پیداش کنی', + 'market.product.petShowcaseBody': 'یه هم‌خونه برای ویجت پت، و محیطی که توش زندگی کنه', + 'market.product.themeDefault': 'پیش‌فرض', + 'market.product.themeLight': 'روشن', + 'market.product.themeDark': 'تیره', + 'market.product.themeGlass': 'شیشه‌ای', + 'market.product.themeIcy': 'یخی', + 'market.product.fontZarna': 'زرنا', + 'market.product.fontVazir': 'وزیر', + 'market.product.fontSamim': 'صمیم', + 'market.product.fontPofak': 'پفـک', + 'market.product.fontRoyin': 'رویین', + 'market.buy.error': 'خرید انجام نشد، دوباره امتحان کن', + 'market.checkout.redirecting': 'داریم می‌ریم درگاه پرداخت...', + 'market.checkout.openError': 'درگاه پرداخت باز نشد، دوباره امتحان کن', + 'market.coinPackage.bestValueBadge': 'به‌صرفه‌ترین', + 'market.coinPackage.perHundredLabel': 'هر ۱۰۰ سکه', + 'market.coins.introBody': + 'با ویج‌کوین هر آیتمی رو یه بار می‌خری و برای همیشه مال تو می‌مونه', + 'market.coins.balanceLabel': 'موجودی فعلی', + 'market.coins.loginToSee': 'برای دیدنش وارد شو', + 'market.coins.freeTitle': 'ویج‌کوین رایگان', + 'market.coins.freeHint': 'با انجام ماموریت‌ها و دعوت دوستات', + 'market.coins.loadErrorTitle': 'بسته‌ها نیومدن', + 'market.coins.emptyTitle': 'فعلا بسته‌ای برای خرید نیست', + 'market.coins.selectPackage': 'یه بسته انتخاب کن', + 'market.coins.priceSeparator': '،', + 'market.coins.checkoutHint': + 'به درگاه بانک می‌ری و سکه‌ها همون لحظه به حسابت اضافه می‌شن', + 'market.coins.pay': 'پرداخت', + 'market.coins.login': 'ورود', + 'market.coins.loginHint': 'برای خرید ویج‌کوین وارد حسابت شو', + 'market.wallpaperPage.introBody': + 'پس‌زمینه‌ی صفحه‌ت متحرک‌ها علامت پخش دارن و وقتی موس روشونه پخش می‌شن', + 'market.page.itemDetailTitle': 'جزئیات آیتم', + 'market.featured.latestAria': 'تازه‌ترین آیتم فروشگاه', + 'market.featured.justArrivedPrefix': 'تازه رسیده ·', + 'market.featured.seeIt': 'ببینش', + 'market.storefront.loadErrorTitle': 'فروشگاه باز نشد', + 'market.storefront.wallpapersSection': 'تصویر زمینه‌ها', + 'market.storefront.themesSection': 'تم‌ها', + 'market.storefront.petsSection': 'حیوون‌ها و محیط‌هاشون', + 'market.storefront.fontsSection': 'فونت‌ها', + 'market.storeItem.untitled': 'بدون نام', +} as const diff --git a/src/common/i18n/fa/mini-apps.ts b/src/common/i18n/fa/mini-apps.ts new file mode 100644 index 000000000..b24e4eb52 --- /dev/null +++ b/src/common/i18n/fa/mini-apps.ts @@ -0,0 +1,38 @@ +export const miniApps = { + 'miniApps.card.openInWindow': 'تو پنجره جدا بازه', + 'miniApps.auth.scope.phoneEmail': 'شماره همراه و ایمیل', + 'miniApps.auth.scope.profile': 'اطلاعات پروفایل (نام، نام کاربری، تصویر پروفایل)', + 'miniApps.auth.allowTitle': 'اجازه می‌دی؟', + 'miniApps.auth.allowBody': 'این برنامک برای ادامه به این اطلاعات دسترسی می‌خواد:', + 'miniApps.auth.confirm': 'تایید و ادامه', + 'miniApps.auth.loginTitle': 'اول وارد حسابت شو', + 'miniApps.auth.loginBody': 'برای باز کردن برنامک باید وارد حسابت بشی', + 'miniApps.auth.login': 'ورود به حساب', + 'miniApps.error.title': 'برنامک باز نشد', + 'miniApps.error.body': 'یه مشکلی پیش اومد، دوباره امتحان کن', + 'miniApps.error.retry': 'دوباره امتحان کن', + 'miniApps.error.back': 'بازگشت', + 'miniApps.iframe.fallbackName': 'برنامک', + 'miniApps.runner.running': 'در حال اجرای {name}...', + 'miniApps.runner.connecting': 'در حال اتصال به {name}...', + 'miniApps.header.exitFullscreen': 'خروج از تمام‌صفحه', + 'miniApps.header.fullscreen': 'تمام‌صفحه', + 'miniApps.header.back': 'بازگشت', + 'miniApps.header.reload': 'بارگذاری دوباره', + 'miniApps.header.more': 'گزینه‌های بیشتر', + 'miniApps.header.openWindow': 'باز کردن تو پنجره جدا', + 'miniApps.page.title': 'برنامک‌ها', + 'miniApps.page.aboutTitle': 'درباره‌ی برنامک‌ها', + 'miniApps.page.emptyTitle': 'هنوز برنامکی اینجا نیست', + 'miniApps.page.emptyBody': 'به‌زودی پر می‌شه', + 'miniApps.page.runningInWindow': 'برنامک تو پنجره جدا در حال اجراست', + 'miniApps.page.pickOne': 'یه برنامک انتخاب کن', + 'miniApps.page.bringForward': 'بیارش جلو:', + 'miniApps.about.p1': + 'برنامک‌ها برنامه‌های کوچیکی هستن که تو ویجتیفای اجرا می‌شن و راحت\n می‌تونی ازشون استفاده کنی، بدون اینکه مجبور باشی از اپ اصلی بری\n بیرون\n ', + 'miniApps.about.p2': + 'خیالت راحت! این برنامک‌ها به طور پیش‌فرض به هیچ اطلاعاتی ازت دسترسی\n ندارن و فقط و فقط با اجازه خودت می‌تونن به اطلاعاتت دسترسی پیدا\n کنن\n ', + 'miniApps.about.p3': + 'اگه دوست داری با ما برنامک بسازی، از بخش «درباره ما» بهمون پیام\n بده\n ', + 'miniApps.about.ok': 'باشه', +} as const diff --git a/src/common/i18n/fa/mood.ts b/src/common/i18n/fa/mood.ts new file mode 100644 index 000000000..fefe3ccc5 --- /dev/null +++ b/src/common/i18n/fa/mood.ts @@ -0,0 +1,6 @@ +export const mood = { + 'mood.label.sad': 'ناراحتم', + 'mood.label.normal': 'خستم', + 'mood.label.happy': 'اوکی‌ام', + 'mood.label.excited': 'سرحالم', +} as const diff --git a/src/common/i18n/fa/navbar.ts b/src/common/i18n/fa/navbar.ts new file mode 100644 index 000000000..180d00bb8 --- /dev/null +++ b/src/common/i18n/fa/navbar.ts @@ -0,0 +1,78 @@ +export const navbar = { + 'navbar.blur.title': 'حالت مخفی', + 'navbar.blur.confirmTitle': 'حالت مخفی فعال میشه!', + 'navbar.blur.confirmBody': + 'با فعال کردن «حالت مخفی»، اطلاعات حساس، وظایف و محتوای ویجت‌ها\n\t\t\t\t\t\tبه‌صورت خودکار تار میشن تا اگر کسی از کنار صفحه یا پشت سرت نگاه\n\t\t\t\t\t\tکرد، نتونه چیزی بخونه 🔒', + 'navbar.blur.confirm': 'بریم تو حالت مخفی', + 'navbar.blur.cancel': 'نه فعلا', + 'navbar.friends.newRequest': 'دوستان، درخواست دوستی جدید داری', + 'navbar.friends.label': 'دوستان', + 'navbar.layout.widgets': 'چیدمان ویجت‌ها', + 'navbar.layout.manage': 'مدیریت ویجت‌ها', + 'navbar.layout.endEdit': 'پایان ویرایش', + 'navbar.layout.editMode': 'حالت ویرایش', + 'navbar.layout.appearance': 'تنظیمات ظاهری', + 'navbar.layout.wallpapers': 'تصویر زمینه‌ها', + 'navbar.market.title': 'فروشگاه', + 'navbar.sync.unnamed': 'بدون نام', + 'navbar.brand': 'ویجتیفای', + 'navbar.explorer': 'کاوش', + 'navbar.miniApps': 'برنامک‌ها', + 'navbar.openBar': 'باز کردن نوار', + 'navbar.closeBar': 'بستن نوار', + 'navbar.birthday': '🎂 تولدت مبارک {name}', + 'navbar.greeting.hello': 'سلام', + 'navbar.greeting.morning': 'صبح بخیر', + 'navbar.greeting.noon': 'ظهر بخیر', + 'navbar.greeting.evening': 'عصر بخیر', + 'navbar.mood.cleared': 'حال امروزت پاک شد، هر وقت خواستی دوباره انتخابش کن', + 'navbar.mood.saved': 'حال امروزت ثبت شد', + 'navbar.mood.askSuffix': '، امروز حالت چطوره؟', + 'navbar.notifications.emptyTitle': 'اعلان جدیدی نداری', + 'navbar.notifications.emptyBody': 'همه چیز به‌روز و مرتبه', + 'navbar.notifications.showLess': 'نمایش کمتر', + 'navbar.notifications.showMore': 'مشاهده بیشتر', + 'navbar.notifications.close': 'بستن', + 'navbar.notifications.completeProfile': 'پروفایلت رو کامل کن و پاداش بگیر!', + 'navbar.notifications.new': 'اعلان‌ها، اعلان جدید داری', + 'navbar.notifications.label': 'اعلان‌ها', + 'navbar.profile.defaultName': 'کاربر ویجتیفای', + 'navbar.profile.view': 'مشاهده پروفایل', + 'navbar.profile.login': 'ورود یا ثبت‌نام', + 'navbar.profile.loginHint': 'همگام‌سازی و دسترسی به امکانات', + 'navbar.profile.settings': 'تنظیمات', + 'navbar.profile.market': 'فروشگاه', + 'navbar.profile.pro': 'ویجتیفای پرو', + 'navbar.profile.logout': 'خروج از حساب', + 'navbar.profile.triggerLogin': 'ورود یا ثبت‌نام', + 'navbar.profile.triggerProgress': 'پروفایل، {percent}٪ تکمیل شده', + 'navbar.profile.trigger': 'پروفایل', + 'navbar.wizard.oneStepLeft': 'یه مرحله جا مونده، همه رو کامل کن', + 'navbar.wizard.saveFailed': 'نتونستیم اطلاعاتت رو ثبت کنیم، دوباره امتحان کن', + 'navbar.wizard.welcomeTitle': 'خوش اومدی!', + 'navbar.wizard.welcomeBody': + 'خیلی خوشحالیم که اینجایی بیا با هم پروفایلت رو کامل\n کنیم تا تجربه بهتری داشته باشی', + 'navbar.wizard.letsGo': 'بزن بریم', + 'navbar.wizard.occupationTitle': 'چه کاره‌ای؟', + 'navbar.wizard.occupationBody': 'حرفه‌ات رو انتخاب کن', + 'navbar.wizard.moment': 'یه لحظه…', + 'navbar.wizard.confirmContinue': 'تایید و ادامه', + 'navbar.wizard.interestsTitle': 'به چی علاقه داری؟', + 'navbar.wizard.interestsBody': 'هر تعداد که دوست داری انتخاب کن', + 'navbar.wizard.continue': 'ادامه', + 'navbar.wizard.sourceTitle': 'مرحله ۴: از کجا شنیدی؟', + 'navbar.wizard.sourceBody': 'بگو از کجا با ویجتیفای آشنا شدی', + 'navbar.wizard.sourceSocial': 'شبکه‌های اجتماعی', + 'navbar.wizard.sourceYoutube': 'یوتیوب', + 'navbar.wizard.sourceFriends': 'دوستان', + 'navbar.wizard.sourceOther': 'جستجو یا سایر', + 'navbar.wizard.invitePlaceholder': 'کد دعوت رو بنویس', + 'navbar.wizard.readyTitle': 'همه چیز آماده‌ست! 🚀', + 'navbar.wizard.readyBody': + 'پروفایلت آماده‌ست حالا می‌تونی از همه‌ی امکانات استفاده\n کنی', + 'navbar.wizard.start': 'شروع کنیم', + 'navbar.logout.title': 'خروج از حساب', + 'navbar.logout.message': 'مطمئنی می‌خوای از حسابت خارج بشی؟', + 'navbar.logout.confirm': 'خروج', + 'navbar.logout.cancel': 'بی‌خیال', +} as const diff --git a/src/common/i18n/fa/news.ts b/src/common/i18n/fa/news.ts new file mode 100644 index 000000000..8297d54e1 --- /dev/null +++ b/src/common/i18n/fa/news.ts @@ -0,0 +1,22 @@ +export const news = { + 'news.feed.zoomit': 'زومیت', + 'news.feed.digiato': 'دیجیاتو', + 'news.feed.digikalaMag': 'دیجی‌کالا مگ', + 'news.feed.zoomg': 'زومجی', + 'news.feed.itresan': 'آی‌تی‌رسان', + 'news.feed.pedal': 'پدال', + 'news.feed.varzesh3': 'ورزش ۳', + 'news.feed.tejaratnews': 'تجارت‌نیوز', + 'news.feed.donyaEEqtesad': 'دنیای اقتصاد', + 'news.feed.khabaronline': 'خبرآنلاین', + 'news.feed.isna': 'ایسنا', + 'news.feed.asriran': 'عصر ایران', + 'news.feed.tabnak': 'تابناک', + 'news.feed.fararu': 'فرارو', + 'news.feed.entekhab': 'انتخاب', + 'news.feed.mehrnews': 'مهر', + 'news.category.tech': 'فناوری', + 'news.category.sport': 'ورزش', + 'news.category.economy': 'اقتصاد', + 'news.category.general': 'عمومی', +} as const diff --git a/src/common/i18n/fa/release-notes.ts b/src/common/i18n/fa/release-notes.ts new file mode 100644 index 000000000..59c5f90f5 --- /dev/null +++ b/src/common/i18n/fa/release-notes.ts @@ -0,0 +1,20 @@ +export const releaseNotes = { + 'releaseNotes.item.dotCalendar.title': 'ویجت تازه: تقویم نقطه‌ای', + 'releaseNotes.item.dotCalendar.body': 'روزهای سال یا روزشمار هدفت رو نقطه‌نقطه ببین', + 'releaseNotes.item.news.title': 'اخبار از منابع دست‌چین', + 'releaseNotes.item.news.body': 'از ورزش تا اقتصاد، دسته‌ی دلخواهت رو انتخاب کن', + 'releaseNotes.item.themes.title': 'تم‌ها خوش‌رنگ‌تر شدن', + 'releaseNotes.item.themes.body': 'رنگ همه‌ی تم‌ها بهتر شده', + 'releaseNotes.item.notes.title': 'یادداشت‌هات، هر شکلی که بخوای', + 'releaseNotes.item.notes.body': 'لیست یادداشت‌ها رو راحت به استیک نوت تبدیل کن', + 'releaseNotes.item.layout.title': 'چیدن ویجت‌ها راحت‌تر شد', + 'releaseNotes.item.layout.body': 'موقع چیدن، کل صفحه جلوی چشمته', + 'releaseNotes.hero.title': 'پاییز اومد، با کلی چیز تازه', + 'releaseNotes.pets.alt': 'جغدو، میشا و تیغو کنار کاراکتر ویجتیفای تو جنگل پاییزی', + 'releaseNotes.pets.badge': 'دوستای تازه', + 'releaseNotes.pets.names': 'جغدو، میشا و تیغو اومدن', + 'releaseNotes.pets.storeHint': 'تو فروشگاه منتظرتن، محیط تهران هم براشون اومده', + 'releaseNotes.pets.cta': 'بریم ببینیم', + 'releaseNotes.wait': 'یه لحظه صبر کن ({seconds})', + 'releaseNotes.go': 'بزن بریم', +} as const diff --git a/src/common/i18n/fa/setting.ts b/src/common/i18n/fa/setting.ts new file mode 100644 index 000000000..c4d2a388c --- /dev/null +++ b/src/common/i18n/fa/setting.ts @@ -0,0 +1,318 @@ +export const setting = { + 'setting.about.brandName': 'ویجتیفای', + 'setting.about.versionPrefix': 'نسخه "', + 'setting.about.description': + 'ویجتیفای یه افزونه برای مرورگرته که تب جدید رو با ابزارهای کاربردی و ظاهری قشنگ، به یه فضای کارآمد و مال خودت تبدیل می‌کنه', + 'setting.about.contactTitle': 'راه‌های ارتباط با ما', + 'setting.about.websiteLink': 'وب‌سایت رسمی', + 'setting.about.blogLink': 'وبلاگ رسمی', + 'setting.about.telegramLink': 'کانال تلگرام', + 'setting.about.baleLink': 'پیام‌رسان بله', + 'setting.about.madeWithPrefix': 'ساخته شده با', + 'setting.about.madeInIran': 'در ایران', + 'setting.about.copyright': '© ویجتیفای - تمامی حقوق محفوظ است', + 'setting.about.versionSuffix': '"', + 'setting.auth.identifierRequired': 'ایمیل یا شماره موبایلت رو وارد کن', + 'setting.auth.sendCodeError': 'نتونستیم کد رو بفرستیم، دوباره امتحان کن', + 'setting.auth.passwordRequired': 'رمز عبورت رو وارد کن', + 'setting.auth.passwordMinLength': 'رمز عبور باید حداقل ۶ کاراکتر باشه', + 'setting.auth.credentialsError': 'ایمیل یا رمز عبور اشتباهه', + 'setting.auth.otpRequired': 'کد ارسال شده رو وارد کن', + 'setting.auth.otpError': 'کد تایید اشتباهه، دوباره امتحان کن', + 'setting.auth.title': 'ورود یا ساخت حساب', + 'setting.auth.identifierPlaceholder': 'شماره موبایل یا ایمیل...', + 'setting.auth.checking': 'در حال بررسی…', + 'setting.auth.continue': 'ادامه', + 'setting.auth.orDivider': 'یا', + 'setting.auth.passwordLabel': 'رمز عبور', + 'setting.auth.change': 'تغییر', + 'setting.auth.passwordPlaceholder': 'رمز عبورت رو وارد کن...', + 'setting.auth.signingIn': 'در حال ورود…', + 'setting.auth.signIn': 'ورود به حساب', + 'setting.auth.sending': 'در حال ارسال…', + 'setting.auth.loginWithOtp': 'ورود با کد موقت', + 'setting.auth.otpLabel': 'کد تایید', + 'setting.auth.otpSentPrefix': 'کد ارسال شده به', + 'setting.auth.otpSentSuffix': 'رو وارد کن', + 'setting.auth.resendCountdown': 'تا امکان ارسال دوباره', + 'setting.auth.resendCode': 'ارسال دوباره کد', + 'setting.auth.loginWithPassword': 'ورود با رمز عبور', + 'setting.auth.changeNumber': 'تغییر شماره', + 'setting.auth.confirmAndSignIn': 'تایید و ورود', + 'setting.auth.googleButton': 'ورود با حساب گوگل', + 'setting.verification.title': 'وضعیت تایید حساب', + 'setting.verification.unverifiedHint': 'حسابت هنوز تایید نشده', + 'setting.verification.sending': 'در حال ارسال...', + 'setting.verification.sendEmail': 'ارسال ایمیل تایید', + 'setting.verification.checkInboxHint': 'ایمیلت رو چک کن یا یه ایمیل تایید دیگه بگیر', + 'setting.interests.loading': 'یه لحظه…', + 'setting.modal.email.required': 'ایمیلت رو بنویس', + 'setting.modal.email.otpRequired': 'کدی که برات فرستادیم رو بنویس', + 'setting.modal.email.successToast': 'ایمیلت اضافه شد', + 'setting.modal.email.title': 'اضافه کردن ایمیل', + 'setting.modal.email.body': + 'با ایمیل، اگه رمزت یادت رفت می‌تونی برگردونیش و حسابت هم امن‌تر می‌شه', + 'setting.modal.email.label': 'ایمیل', + 'setting.modal.email.confirm': 'تایید', + 'setting.modal.email.continue': 'ادامه', + 'setting.modal.email.otpLabel': 'کد تایید', + 'setting.modal.phone.required': 'شماره موبایلت رو بنویس', + 'setting.modal.phone.successToast': 'شماره موبایلت اضافه شد', + 'setting.modal.phone.title': 'اضافه کردن شماره موبایل', + 'setting.modal.phone.body': 'تا بتونی با شماره موبایلت هم وارد حسابت بشی', + 'setting.modal.phone.label': 'شماره موبایل', + 'setting.modal.phone.placeholder': 'شماره موبایلت رو بنویس…', + 'setting.modal.phone.otpRequired': 'کدی که برات فرستادیم رو بنویس', + 'setting.modal.phone.otpLabel': 'کد تایید', + 'setting.modal.phone.confirm': 'تایید', + 'setting.modal.phone.continue': 'ادامه', + 'setting.modal.avatarCrop.title': 'برش تصویر', + 'setting.modal.avatarCrop.zoomLabel': 'بزرگنمایی', + 'setting.modal.birthday.title': 'تاریخ تولدت؟', + 'setting.modal.gender.male': 'آقا هستم', + 'setting.modal.gender.female': 'خانم هستم', + 'setting.modal.gender.preferNot': 'بماند', + 'setting.modal.gender.title': 'جنسیت (کاملا اختیاری)', + 'setting.modal.interests.title': 'به چی علاقه داری؟', + 'setting.modal.interests.placeholder': 'انتخاب زمینه‌های مورد علاقه...', + 'setting.modal.name.label': 'نام کامل', + 'setting.modal.name.placeholder': 'مثلا: ایلان رضایی', + 'setting.modal.occupation.title': 'چه‌کاره‌ای؟', + 'setting.modal.occupation.placeholder': 'انتخاب شغل', + 'setting.modal.username.label': 'نام کاربری (یوزرنیم)', + 'setting.modal.username.placeholder': 'مثلا: i_rez...', + 'setting.modal.footer.saving': 'در حال ذخیره...', + 'setting.modal.footer.save': 'ذخیره', + 'setting.modal.footer.cancel': 'انصراف', + 'setting.otp.digitAria': 'کد تایید رقم {p0}', + 'setting.datePicker.monthFarvardin': 'فروردین', + 'setting.datePicker.monthOrdibehesht': 'اردیبهشت', + 'setting.datePicker.monthKhordad': 'خرداد', + 'setting.datePicker.monthTir': 'تیر', + 'setting.datePicker.monthMordad': 'مرداد', + 'setting.datePicker.monthShahrivar': 'شهریور', + 'setting.datePicker.monthMehr': 'مهر', + 'setting.datePicker.monthAban': 'آبان', + 'setting.datePicker.monthAzar': 'آذر', + 'setting.datePicker.monthDey': 'دی', + 'setting.datePicker.monthBahman': 'بهمن', + 'setting.datePicker.monthEsfand': 'اسفند', + 'setting.datePicker.title': 'انتخاب تاریخ', + 'setting.datePicker.dayLabel': 'روز', + 'setting.datePicker.monthLabel': 'ماه', + 'setting.datePicker.yearLabel': 'سال', + 'setting.datePicker.cancel': 'لغو', + 'setting.profile.unset': 'تنظیم نشده', + 'setting.profile.photoSizeError': 'عکس باید کمتر از ۲ مگابایت باشه', + 'setting.profile.photoTypeError': 'این نوع فایل پشتیبانی نمی‌شه، یه عکس انتخاب کن', + 'setting.profile.uploadError': 'نتونستیم عکس رو آپلود کنیم، دوباره امتحان کن', + 'setting.profile.avatarChangeError': 'نتونستیم آواتارت رو عوض کنیم، دوباره امتحان کن', + 'setting.profile.fullNameLabel': 'نام و نام خانوادگی', + 'setting.profile.addPhone': 'افزودن شماره موبایل', + 'setting.profile.genderLabel': 'جنسیت', + 'setting.profile.birthdayLabel': 'تاریخ تولد', + 'setting.profile.occupationLabel': 'شغل', + 'setting.profile.interestsLabel': 'علایق', + 'setting.profile.cityLabel': 'شهر', + 'setting.profile.offlineHint': + 'این اطلاعات مال آخرین باریه که آنلاین بودی اینترنتت رو چک کن', + 'setting.profile.avatarGallery': 'گالری آواتارها', + 'setting.profile.editField': 'ویرایش {p0}', + 'setting.profileHeader.validUntil': 'اعتبار تا {p0}', + 'setting.profileHeader.proSubscription': 'اشتراک پرو', + 'setting.profileHeader.proCreditLabel': 'اعتبار پرو:', + 'setting.profileHeader.joinedPrefix': 'شروعِ ماجرا از', + 'setting.profileHeader.changeAvatar': 'تغییر تصویر پروفایل', + 'setting.profileHeader.userFallback': 'کاربر', + 'setting.profileHeader.uploadFromDevice': 'بارگذاری از دستگاه', + 'setting.profileHeader.pickFromGallery': 'انتخاب از گالری', + 'setting.referral.copiedToast': 'کد دعوت کپی شد', + 'setting.referral.copyError': 'نتونستیم کد رو کپی کنیم', + 'setting.referral.yourCodeTitle': 'کد دعوتت', + 'setting.referral.codeLabel': 'کد دعوت', + 'setting.referral.copyButton': 'کپی کد', + 'setting.referral.shareHint': + 'با فرستادن این کد برای دوستات، هم خودت و هم دوستت ویج‌کوین می‌گیرید', + 'setting.rewards.verifyRequiredHint': 'برای دیدن این بخش اول حسابت رو تایید کن', + 'setting.rewards.verifyAccount': 'تأیید حساب', + 'setting.rewards.tasksTitle': 'ماموریت‌ها', + 'setting.rewards.coinLabel': 'ویج‌کوین', + 'setting.rewards.emptyTitle': 'فعلا ماموریتی نیست', + 'setting.rewards.emptyHint': 'به‌زودی ماموریت‌های تازه میان', + 'setting.vipBanner.title': 'فراتر از یک تب ساده؛ با نسخه پرو', + 'setting.vipBanner.body': + 'والپیپرهای ویدیویی، ابعاد و مدل‌های اختصاصی ویجت‌ها و امکانات ویژه گالری', + 'setting.vipBanner.upgradeCta': 'ارتقا به پرو', + 'setting.connections.modalTitle': 'مدیریت پلتفرم‌های متصل', + 'setting.connections.disconnectFromPrefix': 'قطع اتصال از', + 'setting.connections.connectToPrefix': 'اتصال به', + 'setting.connections.disconnectConfirmTitle': 'قطع بشه؟', + 'setting.connections.disconnectWarning': + '⚠️ با قطع اتصال، دیگه به داده‌ها و امکانات این پلتفرم دسترسی نداری', + 'setting.connections.featuresLabel': 'امکانات:', + 'setting.connections.permissionsLabel': 'مجوزهای مورد نیاز:', + 'setting.connections.disconnect': 'قطع اتصال', + 'setting.connections.confirmConnect': 'تایید و شروع اتصال', + 'setting.connections.googleCalendarName': 'تقویم گوگل', + 'setting.connections.googleCalendarDescription': + 'اتصال به خدمات گوگل برای دسترسی به تقویم و جلسات گوگل میت', + 'setting.connections.featureCalendarAccess': 'دسترسی مستقیم به تقویم گوگل', + 'setting.connections.featureSmartReminders': + 'نمایش و یادآوری هوشمند رویدادها و جلسات آینده', + 'setting.connections.permissionCalendarRead': 'مشاهده تقویم (سرویس تقویم گوگل)', + 'setting.connections.verifyRequired': 'اول حسابت رو تایید کن', + 'setting.connections.platformDisabled': 'این پلتفرم فعلا غیرفعاله', + 'setting.connections.platformNotReady': 'این پلتفرم هنوز آماده نیست', + 'setting.connections.disconnectedToast': 'اتصال به {p0} قطع شد', + 'setting.connections.connectError': 'نتونستیم به {p0} وصل بشیم، دوباره امتحان کن', + 'setting.connections.connectedStatus': 'وصله', + 'setting.connections.disconnectedStatus': 'وصل نیست', + 'setting.connections.disconnectAction': 'قطع', + 'setting.connections.connectAction': 'اتصال', + 'setting.userProfile.verificationEmailSent': + 'ایمیل تایید رو فرستادیم، صندوقت رو چک کن', + 'setting.userProfile.reLoginRequired': 'دوباره وارد حسابت شو', + 'setting.userProfile.loadError': 'نتونستیم پروفایلت رو بیاریم، دوباره امتحان کن', + 'setting.userProfile.logoutTitle': 'خروج از حساب کاربری', + 'setting.userProfile.logoutAction': 'خروج از حساب', + 'setting.userProfile.logoutHint': 'می‌خوای از حسابت بیرون بیای؟ دکمه‌ی پایین رو بزن', + 'setting.userProfile.logoutConfirm': 'مطمئنی می‌خوای از حسابت خارج بشی؟', + 'setting.userProfile.logoutConfirmButton': 'بله، خروج', + 'setting.vipExpiry.expired': 'منقضی‌شده', + 'setting.vipExpiry.daysLeft': '{p0} روز', + 'setting.vipExpiry.hoursLeft': '{p0} ساعت', + 'setting.vipExpiry.underOneHour': 'کمتر از ۱ ساعت', + 'setting.appearance.themeLabel': 'تم', + 'setting.appearance.themeHint': + 'رنگ‌بندی ویجت‌ها و پنجره‌ها هر کارت صفحه‌ی خودته با همون تم', + 'setting.appearance.fontLabel': 'فونت', + 'setting.appearance.fontHint': 'فونت همه‌ی نوشته‌های ویجتیفای', + 'setting.appearance.tabTitleLabel': 'عنوان تب', + 'setting.appearance.tabTitleHint': + 'اسمی که روی تب مرورگرت می‌بینی، تا بین تب‌ها زود پیداش کنی', + 'setting.tabManager.sectionsAria': 'بخش‌های تنظیمات', + 'setting.timezone.label': 'منطقه‌ی زمانی', + 'setting.timezone.hint': 'منطقه‌ی زمانی‌ت رو انتخاب کن', + 'setting.timezone.loadError': 'نتونستیم فهرست منطقه‌های زمانی رو بیاریم', + 'setting.timezone.placeholder': 'انتخاب منطقه زمانی...', + 'setting.general.liteModeLabel': 'حالت بهینه', + 'setting.general.newBadge': 'جدید', + 'setting.general.liteModeHint': + 'برای اینکه ویجتیفای سبک‌تر اجرا بشه، انیمیشن‌ها، حیوان خانگی، ثانیه‌شمار ساعت و بعضی افکت‌ها خاموش می‌شن', + 'setting.privacy.autocompleteLoginRequired': 'برای این کار اول وارد حسابت شو', + 'setting.privacy.autocompleteLabel': 'پیشنهادهای جستجو', + 'setting.privacy.autocompleteHint': + 'هنگام تایپ در نوار جستجو، پیشنهادها مستقیما از گوگل دریافت و تاریخچه در دستگاه خودت ذخیره می‌شه و به سرور افزونه ارسال نمی‌شن', + 'setting.privacy.analyticsTitle': 'آمار و عملکرد افزونه (Analytics)', + 'setting.privacy.analyticsHint': + 'آمار فنی و گزارش خطاهای ناشناس رو می‌فرستیم تا افزونه رو بهتر کنیم هیچ اطلاعات شخصی یا یادداشتی فرستاده نمی‌شه', + 'setting.privacy.analyticsLabel': 'آمار و عملکرد افزونه', + 'setting.privacy.bookmarkIconsLabel': 'نمایش آیکون‌های بوکمارک‌ها', + 'setting.privacy.bookmarkIconsHint': + 'آیکون بوکمارک‌ها رو از سرویس گوگل می‌گیریم و برای این کار فقط دامنه‌ی سایت فرستاده می‌شه', + 'setting.privacy.bookmarksAccessLabel': 'دسترسی به بوکمارک‌های مرورگر', + 'setting.privacy.bookmarksAccessHint': + 'بوکمارک‌های مرورگرت توی ویجت جستجو نشون داده می‌شن و جایی ذخیره یا فرستاده نمی‌شن', + 'setting.privacy.tabsAccessLabel': 'دسترسی به تب‌ها', + 'setting.privacy.tabsAccessHint': + 'تا بتونی همه‌ی بوکمارک‌های یه پوشه رو یه‌جا توی تب‌های مرورگر باز کنی', + 'setting.tab.accountGroup': 'حساب کاربری', + 'setting.tab.profile': 'پروفایل من', + 'setting.tab.profileHint': 'اطلاعات حسابت رو ببین و هر وقت خواستی عوضش کن', + 'setting.tab.vip': 'ویجتیفای پرو', + 'setting.tab.vipHint': 'تجربه‌ای سریع‌تر، زیباتر و بدون هیچ مرزی در چیدمان ابزارها', + 'setting.tab.platforms': 'پلتفرم‌ها', + 'setting.tab.platformsHint': + 'پلتفرم‌های دیگه رو به ویجتیفای وصل کن و هر وقت خواستی قطعشون کن', + 'setting.tab.rewards': 'ماموریت‌ها و پاداش', + 'setting.tab.rewardsHint': 'ماموریت‌ها رو انجام بده و ویج‌کوین جایزه بگیر', + 'setting.tab.friends': 'دوستان', + 'setting.tab.friendsHint': 'دوستات رو اضافه کن و درخواست‌هاشون رو جواب بده', + 'setting.tab.settingsGroup': 'تنظیمات', + 'setting.tab.general': 'عمومی', + 'setting.tab.generalHint': 'شهر، منطقه‌ی زمانی و سبکی ویجتیفای', + 'setting.tab.privacy': 'حریم خصوصی', + 'setting.tab.privacyHint': 'خودت انتخاب کن ویجتیفای به چی دسترسی داشته باشه', + 'setting.tab.appearance': 'ظاهری', + 'setting.tab.appearanceHint': 'تم، فونت و عنوان تب رو به سلیقه‌ی خودت عوض کن', + 'setting.tab.wallpapers': 'تصویر زمینه‌ها', + 'setting.tab.wallpapersHint': + 'یکی رو انتخاب کن تا همون لحظه پشت صفحه بشینه، یا عکس خودت رو بذار', + 'setting.tab.shortcuts': 'میانبرها', + 'setting.tab.shortcutsHint': 'با این میانبرها کارت توی ویجتیفای سریع‌تر پیش می‌ره', + 'setting.tab.about': 'درباره ما', + 'setting.tab.aboutHint': 'ویجتیفای رو بشناس و از راه‌های ارتباطی باهامون حرف بزن', + 'setting.tab.widgetsManage': 'مدیریت ویجت‌ها', + 'setting.tab.changelog': 'تغییرات اخیر', + 'setting.shortcuts.openBookmarkNewTab': 'باز کردن بوکمارک در تب جدید', + 'setting.shortcuts.bookmarksCategory': 'بوکمارک‌ها', + 'setting.shortcuts.openBookmarkMiddleClick': + 'باز کردن بوکمارک در تب جدید با دکمه اسکرول', + 'setting.shortcuts.openFolderBookmarks': 'باز کردن همه بوکمارک‌های یک پوشه', + 'setting.shortcuts.toggleTheme': 'تغییر تم', + 'setting.vipSuccess.title': 'مبارکه! دسترسی پرو باز شد', + 'setting.vipSuccess.freePlanLabel': 'پلن رایگان', + 'setting.vipSuccess.body': + 'روزه روی حسابت فعال شد برای اینکه همه‌ی قابلیت‌ها باز بشن، یه بار صفحه رو دوباره باز کن', + 'setting.vipSuccess.later': 'بعدا', + 'setting.vipSuccess.reload': 'بارگذاری صفحه', + 'setting.vipHero.layoutFreedomTitle': 'آزادی بیشتر در چیدمان', + 'setting.vipHero.layoutFreedomBody': + 'ویجت‌هات رو هرجور دوست داری بچین، حتی چندتا از یک ویجت', + 'setting.vipHero.widgetsAccessTitle': 'دسترسی کامل به ویجت‌ها', + 'setting.vipHero.widgetsAccessBody': + 'از بین طرح‌ها و اندازه‌های مختلف انتخاب کن و ویجت‌هات رو متناسب با چیدمانت تنظیم کن', + 'setting.vipHero.videoWallpaperTitle': 'والپیپر ویدیویی و متحرک', + 'setting.vipHero.videoWallpaperBody': + 'گذاشتن ویدیوهای دلخواه به عنوان پس‌زمینه و ذخیره دائمی روی حسابت', + 'setting.vipHero.galleryAccessTitle': 'دسترسی کامل به گالری', + 'setting.vipHero.galleryAccessBody': + 'به مجموعه کامل تم‌ها، والپیپرها و طرح‌های ویژه دسترسی داشته باش', + 'setting.vipPlan.used': 'استفاده شده', + 'setting.vipPlan.daysCredit': 'روز اعتبار', + 'setting.vipPlan.alreadyClaimed': 'قبلا گرفتی', + 'setting.vipPlan.free': 'رایگان', + 'setting.vipPlan.currencyToman': 'تومان', + 'setting.vip.proLabel': 'پرو', + 'setting.vip.selectPlanFirst': 'اول یه پلن انتخاب کن', + 'setting.vip.loginRequired': 'برای خرید اشتراک {p0} اول وارد حسابت شو', + 'setting.vip.redirectingToPayment': 'داریم می‌ریم درگاه پرداخت…', + 'setting.vip.freeAlreadyClaimed': 'این اشتراک رایگان رو قبلا گرفتی', + 'setting.vip.purchaseError': 'خرید اشتراک {p0} انجام نشد، دوباره امتحان کن', + 'setting.vip.selectPlanTitle': 'پلن مناسب خودت رو انتخاب کن', + 'setting.vip.emptyPlans': 'فعلا پلنی برای خرید نیست', + 'setting.vip.subscriptionLabel': 'اشتراک', + 'setting.vip.fullAccessHint': 'دسترسی کامل به تمام امکانات', + 'setting.vip.payableAmount': 'مبلغ قابل پرداخت', + 'setting.vip.zeroAmount': '۰', + 'setting.vip.transferring': 'در حال انتقال...', + 'setting.vip.received': 'دریافت شده', + 'setting.vip.activatePlan': 'فعال‌سازی {p0}', + 'setting.wallpaperUpload.proFeaturesTitle': 'امکانات نسخه پرو', + 'setting.wallpaperUpload.mediaUpToPrefix': 'عکس، گیف و ویدیو تا', + 'setting.wallpaperUpload.megabyteUnit': 'مگابایت', + 'setting.wallpaperUpload.cloudSaveHint': 'ذخیره ابری بدون اشغال حافظه مرورگر', + 'setting.wallpaperUpload.syncHint': 'همگام‌سازی خودکار روی تمام دستگاه‌ها', + 'setting.wallpaperUpload.customPhoto': 'عکس خودت', + 'setting.wallpaperUpload.customVideo': 'ویدیوی خودت', + 'setting.wallpaperUpload.replaceHint': '{p0}؛ برای عوض کردنش بزن', + 'setting.wallpaperUpload.remove': 'حذفش کن', + 'setting.wallpaperUpload.removeAria': 'حذف {p0}', + 'setting.wallpaperUpload.dropHere': 'رهاش کن', + 'setting.wallpaperUpload.uploading': 'داریم آپلودش می‌کنیم...', + 'setting.wallpaperUpload.customMediaTitle': 'عکس یا ویدیوی خودت', + 'setting.wallpaperUpload.sizeLimitHint': 'عکس، گیف یا ویدیو تا {p0} مگابایت', + 'setting.wallpaperUpload.localSizeHint': 'تا {p0} مگابایت، روی همین مرورگر', + 'setting.wallpaperUpload.videoRequiresProBadge': 'ویدیو با پرو', + 'setting.wallpaper.removedToast': 'تصویر زمینه حذف شد', + 'setting.wallpaperUpload.pickMediaFile': 'یه فایل عکس، گیف یا ویدیو انتخاب کن', + 'setting.wallpaperUpload.fileTooLarge': 'حجم فایل نباید بیشتر از {p0} مگابایت باشه', + 'setting.wallpaperUpload.videoSyncedToast': 'ویدیو ذخیره شد و با اکانتت همگام‌سازی شد', + 'setting.wallpaperUpload.photoSyncedToast': 'عکس ذخیره شد و با اکانتت همگام‌سازی شد', + 'setting.wallpaperUpload.videoRequiresPro': + 'برای گذاشتن ویدیو به عنوان پس‌زمینه اشتراک پرو لازمه', + 'setting.wallpaperUpload.pickPhoto': 'یه عکس انتخاب کن', + 'setting.wallpaperUpload.largeUploadRequiresPro': + 'برای آپلود فایل تا {p0} مگابایت اشتراک پرو لازمه', + 'setting.wallpaperUpload.customPhotoLabel': 'عکس دلخواه', +} as const diff --git a/src/common/i18n/fa/ui.ts b/src/common/i18n/fa/ui.ts new file mode 100644 index 000000000..752a4b317 --- /dev/null +++ b/src/common/i18n/fa/ui.ts @@ -0,0 +1,35 @@ +export const ui = { + 'ui.toast.infoTitle': 'نکته', + 'ui.toast.infoAction': 'متوجه شدم', + 'ui.toast.errorTitle': 'یه مشکلی پیش اومد', + 'ui.toast.errorAction': 'باشه', + 'ui.toast.successTitle': 'انجام شد', + 'ui.toast.successAction': 'باشه', + 'ui.toast.warningTitle': 'حواست باشه', + 'ui.toast.warningAction': 'متوجه شدم', + 'ui.common.moment': 'یه لحظه…', + 'ui.common.close': 'بستن', + 'ui.common.previous': 'قبلی', + 'ui.common.next': 'بعدی', + 'ui.common.confirm': 'تایید', + 'ui.common.cancel': 'انصراف', + 'ui.common.areYouSure': 'مطمئنی؟', + 'ui.common.confirmAction': 'این کار انجام بشه؟', + 'ui.common.loading': 'در حال بارگذاری', + 'ui.common.notifications': 'اعلان‌ها', + 'ui.common.pickColor': 'انتخاب رنگ', + 'ui.vip.pro': 'پرو', + 'ui.slider.slide': 'اسلاید {n}', + 'ui.slider.prevImage': 'عکس قبلی', + 'ui.slider.nextImage': 'عکس بعدی', + 'ui.slider.role': 'اسلایدر', + 'ui.slider.images': 'تصاویر', + 'ui.date.weekday.sat': 'ش', + 'ui.date.weekday.sun': 'ی', + 'ui.date.weekday.mon': 'د', + 'ui.date.weekday.tue': 'س', + 'ui.date.weekday.wed': 'چ', + 'ui.date.weekday.thu': 'پ', + 'ui.date.weekday.fri': 'ج', + 'ui.date.headingSep': '، ', +} as const diff --git a/src/common/i18n/fa/widgets.ts b/src/common/i18n/fa/widgets.ts new file mode 100644 index 000000000..6e577d619 --- /dev/null +++ b/src/common/i18n/fa/widgets.ts @@ -0,0 +1,950 @@ +export const widgets = { + 'widgets.notes.toast.deleted': 'یادداشت حذف شد', + 'widgets.notes.priority.high': 'اولویت مهم', + 'widgets.notes.priority.medium': 'اولویت متوسط', + 'widgets.notes.priority.low': 'اولویت کم‌اهمیت', + 'widgets.notes.color.default': 'رنگ پیش‌فرض', + 'widgets.notes.untitled': 'یادداشت بدون عنوان', + 'widgets.notes.untitledShort': 'بدون عنوان', + 'widgets.notes.saving': 'در حال ذخیره…', + 'widgets.notes.wordCount': '{words} کلمه · {editedAt}', + 'widgets.notes.deleteThis': 'حذف این یادداشت', + 'widgets.notes.save': 'ذخیره', + 'widgets.notes.refresh': 'به‌روز کن', + 'widgets.notes.loadError': 'نتونستیم یادداشت‌ها رو بیاریم', + 'widgets.notes.listAria': 'یادداشت‌ها', + 'widgets.notes.title': 'یادداشت‌ها', + 'widgets.notes.stickyTitle': 'یادداشت', + 'widgets.notes.count': '{count} یادداشت', + 'widgets.notes.new': 'یادداشت جدید', + 'widgets.notes.deleteConfirmTitle': 'این یادداشت حذف بشه؟', + 'widgets.notes.deleteConfirmMessage': 'دیگه نمی‌تونی برش گردونی', + 'widgets.notes.deleteConfirm': 'حذف', + 'widgets.notes.deleteCancel': 'نه', + 'widgets.notes.emptyCtaTitle': 'اولین یادداشتت رو بنویس', + 'widgets.notes.emptyCtaHint': 'همین‌جا کلیک کن و شروع کن', + 'widgets.notes.otherNotesAria': 'یادداشت‌های دیگه', + 'widgets.notes.prev': 'یادداشت قبلی', + 'widgets.notes.next': 'یادداشت بعدی', + 'widgets.notes.pager': '{current} از {total}', + 'widgets.notes.openAria': 'باز کردن یادداشت {title}', + 'widgets.notes.edit': 'ویرایش یادداشت', + 'widgets.notes.delete': 'حذف یادداشت', + 'widgets.notes.backToList': 'بازگشت به یادداشت‌ها', + 'widgets.notes.editTitle': 'ویرایش یادداشت', + 'widgets.notes.emptyTitle': 'هنوز یادداشتی نداری', + 'widgets.notes.emptyDescription': + 'ایده، لیست خرید یا یه جمله برای بعد؛ همین‌جا نگهش دار', + 'widgets.notes.field.titlePlaceholder': 'یه عنوان بنویس', + 'widgets.notes.field.titleAria': 'عنوان یادداشت', + 'widgets.notes.field.bodyPlaceholder': 'هرچی می‌خوای اینجا بنویس', + 'widgets.notes.field.bodyAria': 'متن یادداشت', + + 'widgets.pet.feed': 'غذا دادن', + 'widgets.pet.aria': 'حیوان خانگی', + 'widgets.pet.tip.feed': 'واسه غذا دادن، هر جای محیطش کلیک کن', + 'widgets.pet.tip.hoverName': 'اسمش رو ببینی؟ موس رو ببر روش', + 'widgets.pet.tip.maxFood': 'همزمان بیشتر از سه تا غذا نمی‌شه گذاشت', + 'widgets.pet.nameLabel': 'اسم حیوونت', + 'widgets.pet.namePlaceholder': 'یه اسم براش بذار…', + 'widgets.pet.section.pet': 'حیوان خانگی', + 'widgets.pet.morePets': 'حیوان خانگی بیشتر', + 'widgets.pet.section.background': 'محیط', + 'widgets.pet.moreBackgrounds': 'محیط‌های بیشتر', + 'widgets.pet.species.dog': 'سگ', + 'widgets.pet.species.chicken': 'مرغ', + 'widgets.pet.species.crab': 'خرچنگ', + 'widgets.pet.species.frog': 'قورباغه', + 'widgets.pet.species.cat': 'گربه', + 'widgets.pet.species.owl': 'جغد', + 'widgets.pet.species.sheep': 'گوسفند', + 'widgets.pet.species.hedgehog': 'جوجه‌تیغی', + 'widgets.pet.background.none': 'بدون محیط', + 'widgets.pet.background.forest': 'جنگل شب', + 'widgets.pet.background.autumn': 'پاییز', + 'widgets.pet.background.beach': 'ساحل', + 'widgets.pet.defaultName.dog': 'آکیتا', + 'widgets.pet.defaultName.chicken': 'قدقدپور', + 'widgets.pet.defaultName.crab': 'چنگولی', + 'widgets.pet.defaultName.cat': 'زردآلو', + 'widgets.pet.defaultName.frog': 'قوری', + 'widgets.pet.defaultName.owl': 'جغدو', + 'widgets.pet.defaultName.sheep': 'میشا', + 'widgets.pet.defaultName.hedgehog': 'تیغو', + 'widgets.pet.hungerAria': 'سیری: {filled} از {total}', + 'widgets.pet.feedAria': 'غذا دادن به {name}', + 'widgets.pet.hungryBubble': 'غذاااا بدهه', + + 'widgets.moodTracker.aria': 'حال روزانه', + 'widgets.moodTracker.todayTitle': 'حال امروز', + 'widgets.moodTracker.askToday': 'امروز چه حسی داری؟', + 'widgets.moodTracker.recorded': 'ثبت شد', + 'widgets.moodTracker.askShort': 'حس امروزت؟', + 'widgets.moodTracker.toast.cleared': 'حالت پاک شد', + 'widgets.moodTracker.toast.saved': 'حالت ثبت شد', + 'widgets.moodTracker.shareMonth': 'اشتراک‌گذاری ماه', + 'widgets.moodTracker.shareTitle': 'اشتراک‌گذاری حال این ماه', + 'widgets.moodTracker.shareNeedAuth': 'برای ساختن گزارش ماهانه اول وارد حسابت شو', + 'widgets.moodTracker.sharePreparing': 'دارم تصویر رو آماده می‌کنم…', + 'widgets.moodTracker.shareLoadError': 'نتونستیم گزارش این ماه رو بیاریم', + 'widgets.moodTracker.shareImageAria': 'تصویر حال این ماه', + 'widgets.moodTracker.copyImage': 'کپی تصویر', + 'widgets.moodTracker.downloadImage': 'دانلود تصویر', + 'widgets.moodTracker.monthFallback': 'ماه', + 'widgets.moodTracker.downloadFilename': 'گزارش-حال-{month}', + 'widgets.moodTracker.share.short.excited': 'سرحال', + 'widgets.moodTracker.share.short.happy': 'اوکی', + 'widgets.moodTracker.share.short.normal': 'خسته', + 'widgets.moodTracker.share.short.sad': 'ناراحت', + 'widgets.moodTracker.share.userFallback': 'کاربر عزیز', + 'widgets.moodTracker.share.monthSubtitle': 'حس و حال ثبت شده در ماه {month}', + 'widgets.moodTracker.share.badgeExcited': 'روی موج بودم 🌊', + 'widgets.moodTracker.share.badgeCalm': 'آروم بودم، خوب بودم 🌿', + 'widgets.moodTracker.share.insightDefault': + '{userName} جان این آرامشی که داشتی واقعیه! نه ساختگی نه اجباری، از دل میومد', + 'widgets.moodTracker.share.footer': 'افزونه مرورگر ویجتیفای - widgetify.ir', + + 'widgets.dotCalendar.summaryWithGoal': 'هدف: {title} · {end}', + 'widgets.dotCalendar.defaultGoalTitle': 'هدف من', + 'widgets.dotCalendar.noGoalYet': 'هنوز هدفی نداری', + 'widgets.dotCalendar.addWidgetFirst': + 'اول ویجت رو به صفحه اضافه کن، بعد از منوی خود ویجت تنظیمش کن', + 'widgets.dotCalendar.daysLeftUntil': '{days} روز مونده تا {date}', + 'widgets.dotCalendar.goalReachedPickNew': 'روز هدفت رسیده؛ یه روز تازه انتخاب کن', + 'widgets.dotCalendar.pickHint': 'از فردا تا یه سال بعد رو می‌تونی انتخاب کنی', + 'widgets.dotCalendar.goalNameLabel': 'اسم هدف', + 'widgets.dotCalendar.goalNamePlaceholder': 'مثلا کنکور، سفر یا تولد', + 'widgets.dotCalendar.goalDayLabel': 'روز هدف', + 'widgets.dotCalendar.setGoal': 'تعیین هدف', + 'widgets.dotCalendar.headerGoal': 'روزشمار هدف', + 'widgets.dotCalendar.emptyTitle': 'هنوز هدفی نداری', + 'widgets.dotCalendar.emptyDescription': 'یه هدف و روزش رو بده', + 'widgets.dotCalendar.untilPrefix': 'تا {date}', + 'widgets.dotCalendar.reachedMessage': 'رسیدی به روز هدفت', + 'widgets.dotCalendar.goalDayFallback': 'روز هدفت', + 'widgets.dotCalendar.daysLeftInfo': '{days} روز مونده', + 'widgets.dotCalendar.goalProgressAria': + '{passed} روز از {total} روز تا {title} گذشته', + 'widgets.dotCalendar.headerYear': 'روزهای سال', + 'widgets.dotCalendar.yearProgressAria': + '{passed} روز از {total} روز سال {year} گذشته', + 'widgets.dotCalendar.daysLeftUntilLabel': 'روز مونده تا {until}', + + 'widgets.photo.typeError': 'فقط عکس می‌تونی انتخاب کنی', + 'widgets.photo.toast.removed': 'عکس برداشته شد', + 'widgets.photo.fromDevice': 'عکس از دستگاه', + 'widgets.photo.fromGallery': 'انتخاب از گالری', + 'widgets.photo.remove': 'برداشتن عکس', + 'widgets.photo.uploading': 'دارم آپلودش می‌کنم…', + 'widgets.photo.galleryTitle': 'گالری قاب عکس', + 'widgets.photo.title': 'قاب عکس', + 'widgets.photo.failedTitle': 'عکس باز نشد', + 'widgets.photo.fromDeviceShort': 'از دستگاه', + 'widgets.photo.galleryShort': 'گالری', + 'widgets.photo.emptyPrompt': 'یه عکس بذار اینجا', + 'widgets.photo.failedHint': 'شاید پاک شده باشه، یکی دیگه انتخاب کن', + 'widgets.photo.emptyHint': 'عکس خودت یا یکی از عکس‌های گالری', + + 'widgets.combo.tab.currency': 'ارزها', + 'widgets.combo.tab.news': 'اخبار', + 'widgets.combo.settingsSummary': 'ارزها و منابع خبری', + 'widgets.combo.refresh': 'به‌روز کن', + 'widgets.combo.menuLabel': 'ارز و اخبار', + 'widgets.combo.settings.currency': 'تنظیمات ارزها', + 'widgets.combo.settings.news': 'تنظیمات اخبار', + + 'widgets.clock.title': 'ساعت', + 'widgets.clock.aria': 'ساعت {time}', + + 'widgets.transparentClock.readable': 'ساعت {hours}:{minutes}', + + 'widgets.yadkar.label': 'یادکار', + 'widgets.yadkar.tab.todos': 'تسک', + 'widgets.yadkar.tab.notes': 'یادداشت', + 'widgets.yadkar.tab.habits': 'عادت', + 'widgets.habit.detail.chart.periodLabel': '۶ ماه اخیر', + 'widgets.habit.detail.chart.legendLess': 'کمتر', + 'widgets.habit.detail.chart.legendMore': 'بیشتر', + 'widgets.habit.detail.chart.emptyDay': 'بدون ثبت', + 'widgets.habit.detail.chart.dayFormat': 'dddd، jD jMMMM', + 'widgets.habit.detail.chart.done': 'انجام شد', + 'widgets.habit.detail.chart.clickHint': 'برای ثبت، روی روزها کلیک کن', + 'widgets.habit.detail.loadError': 'نتونستیم جزئیات این عادت رو بیاریم', + 'widgets.habit.detail.notFound': 'این عادت پیدا نشد', + 'widgets.habit.detail.deleteConfirm': 'این عادت و سابقه‌اش برای همیشه حذف بشه؟', + 'widgets.habit.detail.deleteCancel': 'نه', + 'widgets.habit.detail.deleting': 'در حال حذف…', + 'widgets.habit.detail.delete': 'حذف', + 'widgets.habit.detail.edit': 'ویرایش', + 'widgets.habit.detail.shareImage': 'اشتراک‌گذاری تصویر', + 'widgets.habit.detail.stats.streak': 'پشت‌سرهم', + 'widgets.habit.detail.stats.dayUnit': 'روز', + 'widgets.habit.detail.stats.bestRecord': 'بهترین رکورد', + 'widgets.habit.detail.stats.successDays': 'روزهای موفق', + 'widgets.habit.empty.title': 'یه عادت خوب شروع کن', + 'widgets.habit.empty.hint': 'مثلا روزی ۸ لیوان چای، یا ۲۰ دقیقه مطالعه', + 'widgets.habit.empty.cta': 'عادت جدید', + 'widgets.habit.empty.authTitle': 'عادت‌هات توی حسابته', + 'widgets.habit.empty.authHint': 'برای دیدنشون وارد حسابت شو', + 'widgets.habit.empty.authCta': 'ورود', + 'widgets.habit.preset.waterGoal': '۸ لیوان آب', + 'widgets.habit.preset.waterTitle': 'نوشیدن آب', + 'widgets.habit.preset.walkGoal': '۳ کیلومتر پیاده‌روی', + 'widgets.habit.preset.walkTitle': 'پیاده‌روی', + 'widgets.habit.preset.walkUnit': 'کیلومتر', + 'widgets.habit.preset.bookGoal': '۲۰ صفحه کتاب', + 'widgets.habit.preset.bookTitle': 'مطالعه کتاب', + 'widgets.habit.preset.meditationTitle': 'مدیتیشن', + 'widgets.habit.preset.meditationDesc': 'مدیتیشن و تنفس', + 'widgets.habit.preset.categoryHealth': 'سلامتی', + 'widgets.habit.preset.categorySport': 'ورزش', + 'widgets.habit.preset.categoryStudy': 'مطالعه', + 'widgets.habit.preset.categoryLifestyle': 'سبک زندگی', + 'widgets.habit.form.saving': 'در حال ذخیره…', + 'widgets.habit.form.saveChanges': 'ذخیره تغییرات', + 'widgets.habit.form.add': 'افزودن عادت', + 'widgets.habit.form.editTitle': 'ویرایش عادت', + 'widgets.habit.form.createHint': 'از یه الگو شروع کن یا خودت بساز', + 'widgets.habit.form.titleRequired': 'یه عنوان برای عادت بنویس', + 'widgets.habit.form.customUnitRequired': 'اسم واحد دلخواهت رو بنویس', + 'widgets.habit.form.goalMustBePositive': 'هدف باید از صفر بیشتر باشه', + 'widgets.habit.form.savedToast': 'تغییرات ذخیره شد', + 'widgets.habit.form.addedToast': 'عادت جدید اضافه شد', + 'widgets.habit.form.titleLabel': 'عنوان', + 'widgets.habit.form.titlePlaceholder': 'مثلا نوشیدن آب', + 'widgets.habit.form.goalAtLeast': 'حداقل', + 'widgets.habit.form.frequency': 'تکرار', + 'widgets.habit.form.dailyGoal': 'هدف روزانه', + 'widgets.habit.form.customUnitLabel': 'اسم واحد دلخواه', + 'widgets.habit.form.customUnitPlaceholder': 'مثلا کیلومتر، ست یا فنجان', + 'widgets.habit.form.icon': 'شکلک', + 'widgets.habit.form.color': 'رنگ', + 'widgets.habit.share.filename': 'عادت-{p0}', + 'widgets.habit.share.title': 'اشتراک‌گذاری پیشرفت', + 'widgets.habit.share.imageAria': 'تصویر پیشرفت {p0}', + 'widgets.habit.share.copyImage': 'کپی تصویر', + 'widgets.habit.share.downloadImage': 'دانلود تصویر', + 'widgets.habit.item.detailsAria': 'جزئیات {p0}', + 'widgets.habit.item.pastDays': '{p0} روز گذشته', + 'widgets.habit.item.logProgressAria': 'ثبت پیشرفت {p0}', + 'widgets.habit.item.thisWeek': 'این هفته', + 'widgets.habit.item.notYet': 'هنوز نرسیده', + 'widgets.habit.item.progressOf': '{p0} از {p1} {p2}', + 'widgets.habit.item.unlogged': 'ثبت نشده', + 'widgets.habit.unit.times': 'دفعه', + 'widgets.habit.unit.minutes': 'دقیقه', + 'widgets.habit.unit.hours': 'ساعت', + 'widgets.habit.unit.pages': 'صفحه', + 'widgets.habit.unit.glasses': 'لیوان', + 'widgets.habit.unit.custom': 'دلخواه', + 'widgets.habit.unit.atMost': 'حداکثر', + 'widgets.habit.unit.exactly': 'دقیقا', + 'widgets.habit.unit.daily': 'روزانه', + 'widgets.habit.unit.weekly': 'هفتگی', + 'widgets.habit.unit.monthly': 'ماهانه', + 'widgets.habit.widget.refresh': 'به‌روز کن', + 'widgets.habit.widget.progressOf': '{p0} از {p1}{p2}', + 'widgets.habit.widget.todaySuffix': ' امروز', + 'widgets.habit.widget.title': 'عادت‌ها', + 'widgets.habit.toast.deleted': 'عادت حذف شد', + 'widgets.habit.goal.week': 'هفته', + 'widgets.habit.goal.month': 'ماه', + 'widgets.habit.goal.perDay': '{p0} در روز', + 'widgets.habit.goal.progressInPeriod': '{p0} · {p1} از {p2} بار در {p3}', + 'widgets.habit.goal.done': '{p0} · انجام شد', + 'widgets.habit.goal.notToday': 'امروز هنوز نه', + 'widgets.habit.goal.ceilingReached': 'به سقف هدفت ({p0}) رسیدی', + 'widgets.habit.share.canvas.brand': 'افزونه نیوتب مرورگر ویجتیفای', + 'widgets.habit.share.canvas.streakDays': 'روز پشت‌سرهم', + 'widgets.habit.share.canvas.myHabit': 'عادت من', + 'widgets.habit.share.canvas.recentActivity': 'فعالیت ۶ ماه اخیر', + 'widgets.habit.share.canvas.successDays': '{p0} روز موفق', + 'widgets.habit.share.canvas.bestRecord': 'بهترین رکورد {p0} روز', + 'widgets.habit.share.canvas.tagline': 'یه قدم کوچیک، هر روز', + 'widgets.habit.variant2x1.loadError': 'نتونستیم عادت‌ها رو بیاریم', + 'widgets.habit.variant2x1.emptyHint': 'مثلا روزی ۸ لیوان آب', + 'widgets.habit.variant2x1.of': 'از', + 'widgets.habit.variant2x1.prev': 'عادت قبلی', + 'widgets.habit.variant2x1.next': 'عادت بعدی', + 'widgets.habit.variant4x3.summaryTitle': 'خلاصه‌ی عادت‌های امروز', + 'widgets.habit.variant4x3.percentDone': '{p0} درصد عادت‌های امروز انجام شده', + 'widgets.habit.variant4x3.today': 'امروز', + 'widgets.habit.variant4x3.done': 'انجام‌شده', + 'widgets.habit.variant4x3.undone': 'انجام‌نشده', + 'widgets.tools.currency.amount': 'مقدار', + 'widgets.tools.currency.swap': 'جابه‌جایی دو ارز', + 'widgets.tools.currency.per': 'هر', + 'widgets.tools.currency.toman': 'تومان', + 'widgets.tools.religious.dhikrRabbilAlamin': 'یا رَبَّ الْعَالَمِینَ', + 'widgets.tools.religious.dhikrDhalJalali': 'یا ذَالْجَلَالِ وَالْإِکْرَامِ', + 'widgets.tools.religious.dhikrQadiAlHajat': 'یا قاضی الحاجات', + 'widgets.tools.religious.dhikrArhamAlRahimin': 'یا أَرْحَمَ الرَّاحِمِینَ', + 'widgets.tools.religious.dhikrHayyQayyum': 'یا حَیُّ یا قَیُّومُ', + 'widgets.tools.religious.dhikrLaIlaha': 'لا إِلَهَ إِلَّا اللَّهُ الْمَلِکُ الْحَقُّ الْمُبِینُ', + 'widgets.tools.religious.dhikrSalawat': 'اللَّهُمَّ صَلِّ عَلَى مُحَمَّدٍ وَ آلِ مُحَمَّدٍ', + 'widgets.tools.religious.loadError': 'نتونستیم اوقات شرعی رو بیاریم', + 'widgets.tools.religious.dhikrLabel': 'ذکر', + 'widgets.tools.tab.pomodoro': 'پومودورو', + 'widgets.tools.tab.religious': 'اوقات شرعی', + 'widgets.tools.tab.convert': 'تبدیل', + 'widgets.tools.tab.pomodoroTitle': 'تایمر پومودورو', + 'widgets.tools.tab.currencyTitle': 'تبدیل ارز', + 'widgets.tools.religious.cityTehran': 'تهران', + 'widgets.tools.religious.fajr': 'اذان صبح', + 'widgets.tools.religious.sunrise': 'طلوع آفتاب', + 'widgets.tools.religious.dhuhr': 'اذان ظهر', + 'widgets.tools.religious.sunset': 'غروب آفتاب', + 'widgets.tools.religious.maghrib': 'اذان مغرب', + 'widgets.tools.religious.midnight': 'نیمه‌شب شرعی', + 'widgets.pomodoro.notify.enabledToast': 'اعلان‌ها روشن شد', + 'widgets.pomodoro.notify.continueWithout': 'باشه، بدون اعلان ادامه می‌دیم', + 'widgets.pomodoro.notify.permissionError': 'نتونستیم اجازه‌ی اعلان رو بگیریم', + 'widgets.pomodoro.notify.title': 'اعلان‌های تایمر', + 'widgets.pomodoro.notify.sampleTitle': 'نمونه‌ی اعلان تایمر', + 'widgets.pomodoro.notify.sampleBody': 'آخر هر دور، یه همچین اعلانی می‌گیری', + 'widgets.pomodoro.notify.description': + 'اگه اعلان‌ها رو روشن کنی، وقتی کار یا استراحتت تموم شد خبرت می‌کنیم، حتی\n اگه این صفحه باز نباشه', + 'widgets.pomodoro.notify.notNow': 'فعلا نه', + 'widgets.pomodoro.notify.enable': 'روشن کردن اعلان‌ها', + 'widgets.pomodoro.settings.workLabel': 'زمان کار', + 'widgets.pomodoro.settings.break': 'استراحت', + 'widgets.pomodoro.settings.breakLabel': 'زمان استراحت', + 'widgets.pomodoro.settings.soundAlert': 'هشدار صوتی', + 'widgets.pomodoro.settings.soundHint': 'آخر هر دور کار یه صدا پخش می‌شه', + 'widgets.pomodoro.settings.increaseAria': 'بیشتر کردن {p0}', + 'widgets.pomodoro.settings.valueMinutes': '{p0}: {p1} دقیقه', + 'widgets.pomodoro.settings.minutesUnit': 'دقیقه', + 'widgets.pomodoro.settings.decreaseAria': 'کمتر کردن {p0}', + 'widgets.pomodoro.settings.panelTitle': 'تنظیمات تایمر', + 'widgets.pomodoro.settings.panelHint': 'هر تغییری همین الان روی تایمر می‌شینه', + 'widgets.pomodoro.timer.statusLine': '{p0}، {p1}', + 'widgets.pomodoro.userCard.requestSentToast': 'درخواست دوستی فرستاده شد', + 'widgets.pomodoro.userCard.sending': 'در حال ارسال…', + 'widgets.pomodoro.userCard.request': 'درخواست', + 'widgets.pomodoro.userCard.sent': 'ارسال شده', + 'widgets.pomodoro.mode.shortBreak': 'استراحت کوتاه', + 'widgets.pomodoro.notify.workDone': 'وقت کار تموم شد، یه استراحت کوتاه بکن', + 'widgets.pomodoro.notify.breakDone': 'استراحت تموم شد، آماده‌ای برگردی سر کار؟', + 'widgets.pomodoro.timer.sessionSummary': 'کار {p0} · استراحت {p1} دقیقه', + 'widgets.pomodoro.timer.untilWork': 'تا کار', + 'widgets.pomodoro.timer.untilBreak': 'تا استراحت', + 'widgets.pomodoro.timer.work': 'کار', + 'widgets.pomodoro.timer.reset': 'از اول', + 'widgets.pomodoro.timer.pause': 'مکث', + 'widgets.pomodoro.timer.start': 'شروع', + 'widgets.pomodoro.timer.goToWork': 'برو به کار', + 'widgets.pomodoro.timer.goToBreak': 'برو به استراحت', + 'widgets.pomodoro.leaderboard.open': 'جدول برترین‌ها', + 'widgets.pomodoro.leaderboard.back': 'بازگشت به تایمر', + 'widgets.pomodoro.leaderboard.today': 'امروز', + 'widgets.pomodoro.leaderboard.thisWeek': 'این هفته', + 'widgets.pomodoro.leaderboard.all': 'همه', + 'widgets.pomodoro.settings.open': 'تنظیمات', + 'widgets.pomodoro.leaderboard.hoursMinutes': '{p0} ساعت و {p1} دقیقه', + 'widgets.pomodoro.leaderboard.minutesOnly': '{p0} دقیقه', + 'widgets.pomodoro.leaderboard.loadError': 'نتونستیم فهرست برترین‌ها رو بیاریم', + 'widgets.pomodoro.leaderboard.empty': 'هنوز کسی توی این فهرست نیست', + 'widgets.tools.widget.title': 'ابزارها', + 'widgets.tools.religious.nextInMinutes': '{p0} دقیقه دیگه', + 'widgets.tools.religious.nextInClock': '{p0}:{p1} دیگه', + 'widgets.todos.input.dateFormat': 'dddd، jD jMMMM jYYYY', + 'widgets.todos.input.newTask': 'تسک جدید', + 'widgets.todos.input.titleAria': 'عنوان تسک جدید', + 'widgets.todos.input.notesPlaceholder': 'توضیح یا لینک، اگه لازمه', + 'widgets.todos.input.save': 'ذخیره', + 'widgets.todos.input.add': 'افزودن', + 'widgets.todos.priority.low': 'کم‌اهمیت', + 'widgets.todos.priority.medium': 'متوسط', + 'widgets.todos.priority.high': 'مهم', + 'widgets.todos.priority.label': 'اولویت', + 'widgets.todos.priority.none': 'بدون اولویت', + 'widgets.todos.friends.label': 'دوستان', + 'widgets.todos.friends.count': '{p0} دوست', + 'widgets.todos.friends.addAria': 'افزودن دوست به تسک', + 'widgets.todos.friends.saveClose': 'ذخیره و بستن', + 'widgets.todos.item.markUndoneAria': '{p0} رو برگردون به انجام‌نشده', + 'widgets.todos.item.markDoneAria': '{p0} رو انجام‌شده کن', + 'widgets.todos.empty.title': 'هنوز تسکی نداری', + 'widgets.todos.empty.hint': 'اولین کاری که باید انجام بدی رو بنویس', + 'widgets.todos.empty.hintBelow': 'اولین کاری که باید انجام بدی رو همین پایین بنویس', + 'widgets.todos.filter.time': 'زمان', + 'widgets.todos.filter.tag': 'برچسب', + 'widgets.todos.filter.sort': 'ترتیب', + 'widgets.todos.filter.label': 'فیلتر', + 'widgets.todos.filter.clearAria': 'برداشتن فیلتر {p0}', + 'widgets.todos.form.titleRequired': 'یه عنوان برای تسک بنویس', + 'widgets.todos.form.deletedToast': 'تسک حذف شد', + 'widgets.todos.form.editTitle': 'ویرایش تسک', + 'widgets.todos.form.friendOwnedHint': + 'این تسک رو یکی از دوستات ساخته؛ فقط خودش می‌تونه ویرایشش کنه', + 'widgets.todos.form.titleLabel': 'عنوان', + 'widgets.todos.form.titlePlaceholder': 'مثلا خرید نون', + 'widgets.todos.form.notesLabel': 'توضیح', + 'widgets.todos.form.deleteConfirm': 'این تسک برای همیشه حذف بشه؟', + 'widgets.todos.form.deleteCancel': 'نه', + 'widgets.todos.form.deleting': 'در حال حذف…', + 'widgets.todos.form.delete': 'حذف', + 'widgets.todos.form.saving': 'در حال ذخیره…', + 'widgets.todos.form.saveChanges': 'ذخیره تغییرات', + 'widgets.todos.form.add': 'افزودن تسک', + 'widgets.todos.form.date': 'تاریخ', + 'widgets.todos.form.tagRemovedToast': 'برچسب «{p0}» از {p1} تسک برداشته شد', + 'widgets.todos.form.tagRemoveConfirmPrefix': 'برچسب «', + 'widgets.todos.form.tagRemoveConfirmSuffix': + '» از همه‌ی تسک‌هات برداشته بشه؟ خود تسک‌ها سر\n جاشون می‌مونن', + 'widgets.todos.form.tagRemoving': 'در حال برداشتن…', + 'widgets.todos.form.tagRemove': 'برداشتن برچسب', + 'widgets.todos.form.tagSearchOrCreate': 'جستجو یا یه برچسب تازه', + 'widgets.todos.form.tagSearchOrBuild': 'جستجو یا ساختن برچسب', + 'widgets.todos.form.tagCreatePrefix': 'ساختن «', + 'widgets.todos.form.noTags': 'بدون برچسب', + 'widgets.todos.form.noTagsHint': 'هنوز برچسبی نداری؛ اسمش رو بالا بنویس', + 'widgets.todos.form.tagRemoveAria': 'برداشتن برچسب {p0}', + 'widgets.todos.item.notSavedYet': 'این تسک هنوز ذخیره نشده، یه لحظه صبر کن', + 'widgets.todos.item.deleteNeedAuth': 'برای حذفش اول وارد حسابت شو', + 'widgets.todos.item.editNeedAuth': 'برای ویرایشش اول وارد حسابت شو', + 'widgets.todos.item.shared': 'مشترک', + 'widgets.todos.item.delete': 'حذف تسک', + 'widgets.todos.item.deleteIrreversible': 'دیگه نمی‌تونی برش گردونی', + 'widgets.todos.item.deleteConfirm': 'این تسک حذف بشه؟', + 'widgets.todos.filter.all': 'همه', + 'widgets.todos.filter.today': 'امروز', + 'widgets.todos.filter.thisMonth': 'این ماه', + 'widgets.todos.filter.done': 'انجام‌شده', + 'widgets.todos.filter.undone': 'انجام‌نشده', + 'widgets.todos.filter.sortDefault': 'پیش‌فرض', + 'widgets.todos.filter.sortHighFirst': 'اول مهم‌ها', + 'widgets.todos.filter.sortMediumFirst': 'اول متوسط‌ها', + 'widgets.todos.filter.sortLowFirst': 'اول کم‌اهمیت‌ها', + 'widgets.todos.widget.refresh': 'به‌روز کن', + 'widgets.todos.widget.title': 'تسک‌ها', + 'widgets.todos.due.tomorrow': 'فردا', + 'widgets.todos.summary.count': '{p0} تسک', + 'widgets.todos.summary.doneOf': '{p0} از {p1} انجام شده', + 'widgets.todos.variant2x1.authTitle': 'تسک‌هات توی حسابته', + 'widgets.todos.variant2x1.authHint': 'برای دیدنشون وارد شو', + 'widgets.todos.variant2x1.authCta': 'ورود', + 'widgets.todos.variant2x1.loadError': 'نتونستیم تسک‌ها رو بیاریم', + 'widgets.todos.variant2x1.emptyHint': 'یه کار برای امروز بنویس', + 'widgets.todos.variant2x1.progressOf': '{p0} از {p1}', + 'widgets.todos.variant2x1.prev': 'تسک قبلی', + 'widgets.todos.variant2x1.next': 'تسک بعدی', + 'widgets.todos.variant4x3.summaryTitle': 'خلاصه‌ی تسک‌ها', + 'widgets.todos.variant4x3.percentDone': '{p0} درصد تسک‌ها انجام شده', + 'widgets.bookmark.context.iconTooLarge': 'آیکون باید کمتر از ۲۵۰ کیلوبایت باشه', + 'widgets.bookmark.context.addError': + 'نتونستیم بوکمارک رو اضافه کنیم، دوباره امتحان کن', + 'widgets.bookmark.context.importNeedAuth': + 'برای درون‌ریزی بوکمارک‌ها اول وارد حسابت شو', + 'widgets.bookmark.context.importNeedSync': + 'برای درون‌ریزی توی این پوشه، اول بوکمارک‌هات رو همگام‌سازی کن', + 'widgets.bookmark.context.editNeedSync': + 'برای ویرایش این بوکمارک، اول بوکمارک‌هات رو همگام‌سازی کن', + 'widgets.bookmark.context.deleteNeedSync': + 'برای حذف این بوکمارک، اول بوکمارک‌هات رو همگام‌سازی کن', + 'widgets.bookmark.widget.reorderError': 'نتونستیم ترتیب بوکمارک‌ها رو ذخیره کنیم', + 'widgets.bookmark.widget.addNeedAuth': 'برای افزودن بوکمارک جدید اول وارد حسابت شو', + 'widgets.bookmark.folder.optionsAria': 'گزینه‌های پوشه', + 'widgets.bookmark.grid.editNeedAuth': 'برای ویرایش بوکمارک اول وارد حسابت شو', + 'widgets.bookmark.grid.deleteBookmarkTitle': 'حذف بوکمارک؟', + 'widgets.bookmark.grid.deleteFolderPrefix': 'پوشه‌ی «', + 'widgets.bookmark.grid.deleteFolderSuffix': '» حذف بشه؟', + 'widgets.bookmark.grid.deleteFolderBody': + 'همه‌ی بوکمارک‌های داخلش هم برای همیشه حذف می‌شن و دیگه\n\t\t\t\t\t\t\t\tبرنمی‌گردن', + 'widgets.bookmark.grid.deleteBookmarkPrefix': 'بوکمارک «', + 'widgets.bookmark.grid.deleting': 'در حال حذف…', + 'widgets.bookmark.grid.delete': 'حذف', + 'widgets.bookmark.grid.deleteCancel': 'نه', + 'widgets.bookmark.icon.unsupportedType': + 'این نوع فایل پشتیبانی نمی‌شه، یه عکس انتخاب کن', + 'widgets.bookmark.icon.pickOrChange': 'انتخاب یا تغییر آیکون', + 'widgets.bookmark.icon.remove': 'حذف آیکون', + 'widgets.bookmark.icon.upload': 'بارگذاری از دستگاه', + 'widgets.bookmark.icon.pickFromGallery': 'انتخاب از گالری', + 'widgets.bookmark.icon.galleryTitle': 'گالری آیکون بوکمارک', + 'widgets.bookmark.item.optionsAria': 'گزینه‌های بوکمارک', + 'widgets.bookmark.suggestions.title': 'پیشنهاد ویجتیفای', + 'widgets.bookmark.folder.back': 'بازگشت', + 'widgets.bookmark.modal.add.folderTitle': 'پوشه جدید', + 'widgets.bookmark.modal.add.bookmarkTitle': 'بوکمارک جدید', + 'widgets.bookmark.modal.add.folderName': 'نام پوشه', + 'widgets.bookmark.modal.add.bookmarkName': 'عنوان بوکمارک', + 'widgets.bookmark.modal.add.importFromBrowser': 'درون‌ریزی از بوکمارک‌های مرورگر', + 'widgets.bookmark.modal.add.cancel': 'لغو', + 'widgets.bookmark.modal.add.save': 'ذخیره', + 'widgets.bookmark.modal.add.advanced': 'تنظیمات پیشرفته', + 'widgets.bookmark.modal.advanced.bgColor': 'رنگ پس زمینه (اختیاری)', + 'widgets.bookmark.modal.advanced.textColor': 'رنگ متن (اختیاری)', + 'widgets.bookmark.modal.advanced.pickSticker': 'انتخاب استیکر (اختیاری)', + 'widgets.bookmark.modal.advanced.changeSticker': 'تغییر استیکر', + 'widgets.bookmark.modal.advanced.selectSticker': 'انتخاب استیکر', + 'widgets.bookmark.modal.advanced.previewLabel': 'پیش‌نمایش:', + 'widgets.bookmark.modal.advanced.preview': 'پیش‌نمایش', + 'widgets.bookmark.menu.openInNewTab': 'در تب جدید', + 'widgets.bookmark.menu.edit': 'ویرایش', + 'widgets.bookmark.modal.folder.title': 'پوشه بوکمارک', + 'widgets.bookmark.modal.edit.title': 'ویرایش {p0}', + 'widgets.bookmark.modal.edit.folder': 'پوشه', + 'widgets.bookmark.modal.edit.bookmark': 'بوکمارک', + 'widgets.bookmark.modal.import.untitledFolder': 'پوشه بدون عنوان', + 'widgets.bookmark.modal.import.collapseFolder': 'بستن پوشه', + 'widgets.bookmark.modal.import.expandFolder': 'باز کردن پوشه', + 'widgets.bookmark.modal.import.untitled': 'بدون عنوان', + 'widgets.bookmark.modal.import.maxSelectToast': + 'هر بار تا {p0} مورد می‌تونی انتخاب کنی', + 'widgets.bookmark.modal.import.selectAtLeastOne': 'حداقل یه بوکمارک انتخاب کن', + 'widgets.bookmark.modal.import.importedToast': '{p0} مورد درون‌ریزی شد', + 'widgets.bookmark.modal.import.nothingFound': 'چیزی برای درون‌ریزی پیدا نکردیم', + 'widgets.bookmark.modal.import.permissionTitle': 'دسترسی به بوکمارک‌ها', + 'widgets.bookmark.modal.import.permissionBody': + 'برای درون‌ریزی بوکمارک‌های مرورگرت، اول بهمون اجازه‌ی دسترسی بده', + 'widgets.bookmark.modal.import.enableAccess': 'فعال‌سازی دسترسی', + 'widgets.bookmark.modal.import.selectedOf': '{p0} از {p1} مورد انتخاب شده', + 'widgets.bookmark.modal.import.maxSelectHint': 'تا {p0} مورد می‌تونی انتخاب کنی', + 'widgets.bookmark.modal.import.emptyBrowser': 'توی مرورگرت بوکمارکی پیدا نکردیم', + 'widgets.bookmark.modal.import.importing': 'در حال درون‌ریزی…', + 'widgets.bookmark.modal.import.submit': 'درون‌ریزی', + 'widgets.bookmark.shared.keepLink': 'یه لینک یا سایت رو نگه دار', + 'widgets.bookmark.shared.organize': 'دسته‌بندی و مرتب‌سازی بوکمارک‌ها', + 'widgets.bookmark.shared.fewerOptions': 'گزینه‌های کمتر', + 'widgets.bookmark.shared.moreOptions': 'گزینه‌های بیشتر', + 'widgets.calendar.holiday': 'تعطیل', + 'widgets.calendar.eventCount': '{count} مناسبت', + 'widgets.calendar.moodDay': 'حال روز: {mood}', + 'widgets.calendar.summary.eventsAndMoods': 'رویدادها و حال روز', + 'widgets.calendar.summary.eventsOnly': 'فقط رویدادها', + 'widgets.calendar.summary.moodsOnly': 'فقط حال روز', + 'widgets.calendar.summary.datesOnly': 'فقط تاریخ‌ها', + 'widgets.calendar.setting.addWidgetFirst': + 'اول ویجت رو به صفحه اضافه کن، بعد از منوی خود ویجت تنظیمش کن', + 'widgets.calendar.setting.intro': + 'انتخاب کن روی روزهای تقویم چی دیده بشه با کلیک روی هر روز، همه‌ی رویدادها و حالش رو می‌بینی', + 'widgets.calendar.setting.sizeNote': + 'این‌ها روی اندازه‌های ۲×۱ و ۲×۳ دیده می‌شن؛ ۱×۱ فقط امروز رو نشون می‌ده', + 'widgets.calendar.setting.eventsTitle': 'رویدادها', + 'widgets.calendar.setting.eventsDesc': + 'یه نقطه زیر روزی که مناسبت داره؛ قرمز اگه تعطیل رسمیه', + 'widgets.calendar.setting.moodsTitle': 'حال روز', + 'widgets.calendar.setting.moodsDesc': + 'دور روزهایی که حالت رو ثبت کردی، به رنگ همون حال', + 'widgets.calendar.setting.toggleLabel': 'نشون دادن {title} روی تقویم', + 'widgets.calendar.toast.needAuth': 'برای ثبت حالت اول وارد حسابت شو', + 'widgets.calendar.toast.moodRemoved': 'حال این روز برداشته شد', + 'widgets.calendar.toast.moodSaved': 'حالت ثبت شد', + 'widgets.calendar.moodTitle.today': 'حس و حال امروز', + 'widgets.calendar.moodTitle.day': 'حس و حال این روز', + 'widgets.calendar.hijriDate': 'تاریخ قمری', + 'widgets.calendar.gregorianDate': 'تاریخ میلادی', + 'widgets.calendar.tab.calendar': 'تقویم', + 'widgets.calendar.tab.google': 'تقویم گوگل', + 'widgets.calendar.goToToday': 'برو به امروز', + 'widgets.calendar.tabsLabel': 'تقویم', + 'widgets.calendar.prevMonth': 'ماه قبل', + 'widgets.calendar.nextMonth': 'ماه بعد', + 'widgets.calendar.aria': 'تقویم شمسی', + + 'widgets.googleCalendar.title': 'تقویم گوگل', + 'widgets.googleCalendar.status.notConnected': 'هنوز وصل نشده', + 'widgets.googleCalendar.status.notLoggedIn': 'وارد حسابت نشدی', + 'widgets.googleCalendar.action.connect': 'اتصال', + 'widgets.googleCalendar.action.login': 'ورود', + 'widgets.googleCalendar.connectPrompt': 'تقویم گوگلت رو وصل کن', + 'widgets.googleCalendar.loginPrompt': 'اول وارد حسابت شو', + 'widgets.googleCalendar.connectHint': + 'تقویم گوگلت رو وصل کن تا جلسه‌ها و برنامه‌های امروزت همین‌جا باشن', + 'widgets.googleCalendar.loginHint': 'برای دیدن برنامه‌هات اول وارد حسابت شو', + 'widgets.googleCalendar.connectAction': 'اتصال تقویم گوگل', + 'widgets.googleCalendar.loadError': 'نتونستیم برنامه‌هات رو بیاریم', + 'widgets.googleCalendar.untitled': 'بدون عنوان', + 'widgets.googleCalendar.nowRemaining': 'الان · {mins} دقیقه مونده', + 'widgets.googleCalendar.allDayAria': '{title}، تمام روز', + 'widgets.googleCalendar.rangeAria': '{title}، {start} تا {end}', + 'widgets.googleCalendar.refresh': 'به‌روز کن', + 'widgets.googleCalendar.emptyTodayTitle': 'امروز برنامه‌ای نداری', + 'widgets.googleCalendar.emptyDayTitle': 'این روز برنامه‌ای نداری', + 'widgets.googleCalendar.emptyDescription': 'فرصت خوبیه برای کارهای شخصی', + 'widgets.googleCalendar.eventCount': '{count} برنامه', + 'widgets.googleCalendar.minsRemaining': '{mins} دقیقه مونده', + 'widgets.googleCalendar.nextMeeting': 'جلسه‌ی بعدی', + 'widgets.googleCalendar.nowMeeting': 'جلسه‌ی الان', + 'widgets.googleCalendar.meetingAria': '{kind}: {title}، {start}', + 'widgets.googleCalendar.meetingRangeAria': '{kind}: {title}، {start} تا {end}', + 'widgets.googleCalendar.happeningNow': 'الان در جریانه', + 'widgets.googleCalendar.remainingUnit': 'مونده', + 'widgets.googleCalendar.untilStart': 'تا شروع', + 'widgets.googleCalendar.today': 'امروز', + 'widgets.googleCalendar.tomorrow': 'فردا، {weekday}', + 'widgets.googleCalendar.upcomingTitle': 'برنامه‌های پیش‌رو', + 'widgets.googleCalendar.upcomingEmptyTitle': 'برنامه‌ی پیش‌رویی نداری', + 'widgets.googleCalendar.upcomingEmptyDesc': 'هر چی توی تقویم گوگلت بذاری، اینجا میاد', + 'widgets.googleCalendar.prevWeek': 'هفته‌ی قبل', + 'widgets.googleCalendar.nextWeek': 'هفته‌ی بعد', + 'widgets.googleCalendar.todayWeekday': 'امروز، {weekday}', + 'widgets.googleCalendar.prevDay': 'روز قبل', + 'widgets.googleCalendar.nextDay': 'روز بعد', + 'widgets.googleCalendar.allDayEvent': 'رویداد تمام روز', + 'widgets.googleCalendar.timeRange': '{start} تا {end}', + 'widgets.googleCalendar.duration.hoursMins': '{hours} ساعت و {mins} دقیقه', + 'widgets.googleCalendar.duration.hours': '{hours} ساعت', + 'widgets.googleCalendar.duration.mins': '{mins} دقیقه', + 'widgets.googleCalendar.unit.minute': 'دقیقه', + 'widgets.googleCalendar.unit.hour': 'ساعت', + 'widgets.googleCalendar.allDay': 'تمام روز', + 'widgets.googleCalendar.noPlan': 'برنامه‌ای نداری', + 'widgets.googleCalendar.freeToday': 'امروز آزادی', + + 'widgets.news.title': 'اخبار', + 'widgets.news.refresh': 'به‌روز کن', + 'widgets.news.emptyTitle': 'فعلا خبر تازه‌ای نیست', + 'widgets.news.emptyDescription': 'یه کم دیگه دوباره سر بزن', + 'widgets.news.aria': 'اخبار', + 'widgets.news.noSourcesTitle': 'هیچ منبع خبری‌ای روشن نیست', + 'widgets.news.noSourcesDescription': 'اخبار پیش‌فرض یا یه منبع دیگه رو روشن کن', + 'widgets.news.noSourcesAction': 'تنظیمات اخبار', + 'widgets.news.defaultFeed': 'اخبار پیش‌فرض', + 'widgets.news.category.all': 'همه', + 'widgets.news.setting.general': 'تنظیمات کلی', + 'widgets.news.setting.defaultTitle': 'اخبار پیش‌فرض', + 'widgets.news.setting.defaultDesc': 'تیترهای روز از چند خبرگزاری معتبر', + 'widgets.news.setting.sources': 'منابع خبری', + 'widgets.news.sourcesOn': '{count} منبع روشنه', + 'widgets.news.sourcesOff': 'هیچ منبعی روشن نیست', + 'widgets.news.feedError': 'نتونستیم «{label}» رو بیاریم', + 'widgets.news.retry': 'دوباره امتحان کن', + 'widgets.news.time.justNow': 'همین الان', + 'widgets.news.time.minutesAgo': '{count} دقیقه پیش', + 'widgets.news.time.hoursAgo': '{count} ساعت پیش', + 'widgets.news.time.daysAgo': '{count} روز پیش', + 'widgets.news.updating': 'به‌روز می‌شه…', + + 'widgets.weather.title': 'آب و هوا', + 'widgets.weather.aria': 'آب و هوا', + 'widgets.weather.loadError': 'نتونستیم آب و هوا رو بیاریم', + 'widgets.weather.citySummary': 'شهر: {city}', + 'widgets.weather.hourlyForecast': 'پیش‌بینی ساعتی', + 'widgets.weather.metric.wind': 'باد', + 'widgets.weather.metric.humidity': 'رطوبت', + 'widgets.weather.metric.clouds': 'ابر', + 'widgets.weather.highLow': 'بیشینه {high} · کمینه {low}', + + 'widgets.search.engine.google': 'گوگل', + 'widgets.search.placeholderIn': 'جستجو در {engine}', + 'widgets.search.ariaIn': 'جستجو در {engine}', + 'widgets.search.clear': 'پاک کردن متن', + 'widgets.search.submit': 'جستجو', + 'widgets.search.placeholderShort': 'جستجو…', + 'widgets.search.aria': 'جستجو', + 'widgets.search.engineAria': 'موتور جستجو: {engine}', + 'widgets.search.enginePicker': 'انتخاب موتور جستجو', + 'widgets.search.removeHistory': 'حذف {text} از تاریخچه', + 'widgets.search.image.tooltip': 'جستجو با عکس', + 'widgets.search.image.aria': 'جستجو با عکس', + 'widgets.search.image.portalAria': 'جستجوی تصویر با گوگل', + 'widgets.search.image.close': 'بستن جستجوی تصویر', + 'widgets.search.image.previewAlt': 'پیش‌نمایش تصویر', + 'widgets.search.image.uploading': 'در حال فرستادن عکس…', + 'widgets.search.image.searching': 'در حال جستجو توی گوگل…', + 'widgets.search.image.urlPlaceholder': 'لینک عکس رو اینجا بذار…', + 'widgets.search.image.typeError': 'فقط عکس می‌تونی انتخاب کنی', + 'widgets.search.image.sizeError': 'عکس باید کمتر از ۱ مگابایت باشه', + 'widgets.search.image.dropHint': 'یه عکس رو بکش اینجا یا', + 'widgets.search.image.pick': 'انتخابش کن', + 'widgets.search.image.title': 'جستجوی تصویر با گوگل', + 'widgets.search.voice.tooltip': 'جستجوی صوتی', + 'widgets.search.voice.aria': 'جستجوی صوتی', + 'widgets.search.voice.close': 'بستن جستجوی صوتی', + 'widgets.search.voice.listening': 'دارم گوش می‌دم…', + 'widgets.search.voice.stop': 'توقف ضبط صدا', + 'widgets.search.voice.start': 'شروع ضبط صدا', + 'widgets.search.voice.lang.fa': 'فارسی', + 'widgets.search.voice.micDenied': + 'به میکروفون دسترسی نداریم از نوار آدرس مرورگر اجازه‌ی میکروفون رو بده و دوباره امتحان کن', + 'widgets.search.voice.unsupported': 'مرورگرت از جستجوی صوتی پشتیبانی نمی‌کنه', + 'widgets.search.voice.failed': 'جستجوی صوتی شروع نشد، دوباره امتحان کن', + 'widgets.search.consent.title': 'پیشنهادهای جستجو', + 'widgets.search.consent.body': + 'اگه روشنش کنی، موقع تایپ پیشنهادها مستقیم از گوگل میان ما چیزی ذخیره نمی‌کنیم', + 'widgets.search.consent.notNow': 'فعلا نه', + 'widgets.search.consent.enable': 'روشنش کن', + 'widgets.search.history.suggestions': 'پیشنهادهای جستجو', + 'widgets.search.history.suggestionsHint': 'اگه روشنش کنی، موقع تایپ پیشنهاد می‌گیری', + 'widgets.search.history.enable': 'روشنش کن', + 'widgets.search.history.empty': 'هنوز چیزی اینجا نیست', + 'widgets.search.bookmark.mine': 'بوکمارک‌های من', + 'widgets.search.bookmark.browser': 'بوکمارک‌های مرورگر', + 'widgets.search.bookmark.explorer': 'کاوش', + 'widgets.search.bookmark.permissionTitle': 'دسترسی به بوکمارک‌ها', + 'widgets.search.bookmark.permissionBody': + 'برای دیدن بوکمارک‌های مرورگرت اینجا، باید بهمون اجازه بدی', + 'widgets.search.bookmark.allow': 'اجازه بده', + 'widgets.search.bookmark.back': 'بازگشت', + 'widgets.search.bookmark.empty': 'این پوشه خالیه', + + 'widgets.network.title': 'شبکه', + 'widgets.network.aria': 'شبکه', + 'widgets.network.refresh': 'به‌روز کن', + 'widgets.network.copyIp': 'کپی آدرس IP', + 'widgets.network.copyIpValue': 'کپی آدرس {ip}', + 'widgets.network.status.online': 'متصل', + 'widgets.network.status.offline': 'قطع', + 'widgets.network.quality.unknown': 'معلوم نیست', + 'widgets.network.quality.good': 'عالی', + 'widgets.network.quality.fair': 'متوسط', + 'widgets.network.quality.poor': 'ضعیف', + 'widgets.network.toast.copied': 'آدرس IP کپی شد', + 'widgets.network.toast.copyFailed': 'نتونستیم آدرس IP رو کپی کنیم', + 'widgets.network.loadError': 'نتونستیم اطلاعات شبکه رو بیاریم', + 'widgets.network.authTitle': 'وارد حسابت نشدی', + 'widgets.network.authDescription': 'برای دیدن IP و پینگت وارد شو', + 'widgets.network.login': 'ورود', + 'widgets.network.offlineTitle': 'اینترنت قطعه', + 'widgets.network.offlineDescription': 'مودم یا وای‌فای رو چک کن', + 'widgets.network.offlineHint': + 'اتصال مودم یا وای‌فای رو چک کن؛ وصل که بشی خودش به‌روز می‌شه', + 'widgets.network.retry': 'دوباره', + 'widgets.network.retryFull': 'دوباره امتحان کن', + 'widgets.network.placeUnknown': 'مکان معلوم نیست', + 'widgets.network.ispUnknown': 'سرویس‌دهنده معلوم نیست', + 'widgets.network.noIp': 'آدرس IP رو نداریم', + 'widgets.network.ping': 'پینگ', + 'widgets.network.ipLabel': 'آدرس IP', + 'widgets.network.ms': 'میلی‌ثانیه', + + 'widgets.wigiArz.title': 'ویجی ارز', + 'widgets.wigiArz.refresh': 'به‌روز کن', + 'widgets.wigiArz.add': 'افزودن ارز', + 'widgets.wigiArz.emptyTitle': 'هنوز ارزی انتخاب نکردی', + 'widgets.wigiArz.emptyDescription': + 'دلار، طلا یا هر ارزی که می‌خوای رو اضافه کن تا قیمتش همین‌جا باشه', + 'widgets.wigiArz.listAria': 'لیست ارزها', + 'widgets.wigiArz.convertAria': 'تبدیل ارز', + 'widgets.wigiArz.amountPlaceholder': 'مبلغ', + 'widgets.wigiArz.change.up': 'افزایش', + 'widgets.wigiArz.change.down': 'کاهش', + 'widgets.wigiArz.group.crypto': '🪙 ارزهای دیجیتال', + 'widgets.wigiArz.group.currency': '💵 ارزها', + 'widgets.wigiArz.group.coin': '🥇 طلا و سکه', + 'widgets.wigiArz.filter.all': 'همه‌ی ارزها', + 'widgets.wigiArz.filter.crypto': 'ارزهای دیجیتال', + 'widgets.wigiArz.filter.currency': 'ارزها', + 'widgets.wigiArz.filter.coin': 'طلا و سکه', + 'widgets.wigiArz.searchPlaceholder': 'جستجو…', + 'widgets.wigiArz.settingTitle': 'انتخاب ارزها', + 'widgets.wigiArz.settingTitleCompact': 'انتخاب ارز برای ویجت', + 'widgets.wigiArz.summary.compact': 'ارز: {code}', + 'widgets.wigiArz.summary.count': '{count} ارز انتخاب شده', + 'widgets.wigiArz.summary.none': 'هنوز ارزی انتخاب نشده', + 'widgets.wigiArz.dollar': 'دلار', + 'widgets.wigiArz.toman': 'تومان', + 'widgets.wigiArz.dollarSuffix': ' · دلار', + 'widgets.wigiArz.partnerAlt': 'نماد همکار', + 'widgets.wigiArz.reorderAria': 'جابه‌جایی {code}', + 'widgets.wigiArz.priceAria': '{name}، {price}', + 'widgets.wigiArz.toast.partner': '🔗 در حال رفتن به سایت همکار…', + 'widgets.wigiArz.loadError': 'نتونستیم قیمت رو بیاریم', + 'widgets.wigiArz.retry': 'دوباره', + 'widgets.wigiArz.pickCurrency': 'انتخاب ارز', + 'widgets.wigiArz.updating': 'به‌روز می‌شه…', + 'widgets.registry.search.label': 'جستجو', + 'widgets.registry.bookmarks.label': 'بوکمارک‌ها', + 'widgets.registry.pet.label': 'پت (حیوان خانگی)', + 'widgets.registry.calendar.label': 'تقویم', + 'widgets.registry.googleCalendar.label': 'تقویم گوگل', + 'widgets.registry.weather.label': 'آب و هوا', + 'widgets.registry.comboWidget.label': 'ویجت ترکیبی (ارز و اخبار)', + 'widgets.registry.yadKar.label': 'یادکار (وظایف/یادداشت/عادت‌ها)', + 'widgets.registry.tools.label': 'ابزارها', + 'widgets.registry.arzLive.label': 'ویجی ارز', + 'widgets.registry.news.label': 'اخبار', + 'widgets.registry.network.label': 'شبکه', + 'widgets.registry.HabitTracker.label': 'عادت‌ها', + 'widgets.registry.todos.label': 'تسک‌ها', + 'widgets.registry.notes.label': 'یادداشت', + 'widgets.registry.transparentClock.label': 'ساعت شفاف', + 'widgets.registry.moodTracker.label': 'حال روزانه (Mood)', + 'widgets.registry.dotCalendar.label': 'تقویم نقطه‌ای', + 'widgets.registry.dotCalendar.menuLabel': 'روزشمار', + 'widgets.registry.variant.clock.digital': 'ساعت دیجیتال', + 'widgets.registry.variant.clock.flip': 'ساعت فیلیپ (ورقه‌ای)', + 'widgets.registry.variant.clock.digital-vertical': 'ساعت دیجیتال عمودی', + 'widgets.registry.variant.clock.analog': 'ساعت آنالوگ', + 'widgets.registry.variant.googleCalendar.schedule': 'تقویم هفتگی و روزانه', + 'widgets.registry.variant.googleCalendar.timeline': 'تایم‌لاین روزانه', + 'widgets.registry.variant.googleCalendar.agenda': 'برنامه‌های پیش‌رو', + 'widgets.registry.variant.googleCalendar.compact-2x1': 'نوار برنامه روزانه', + 'widgets.registry.variant.googleCalendar.compact-1x1': 'خلاصه سریع رویداد', + 'widgets.registry.variant.arzLive.list': 'لیست قیمت ارزها', + 'widgets.registry.variant.arzLive.compact': 'تک ارز', + 'widgets.registry.variant.notes.list': 'لیست یادداشت‌ها', + 'widgets.registry.variant.notes.sticky': 'استیک نوت', + 'widgets.registry.variant.notes.board': 'دفتر یادداشت', + 'widgets.registry.variant.transparentClock.persian': 'ساعت شفاف فارسی', + 'widgets.registry.variant.transparentClock.english': 'ساعت شفاف انگلیسی', + 'widgets.registry.variant.dotCalendar.year': 'روزهای سال', + 'widgets.registry.variant.dotCalendar.year-count': 'روزهای سال (عددی)', + 'widgets.registry.variant.dotCalendar.goal': 'روزشمار هدف', + 'widgets.registry.variant.dotCalendar.goal-count': 'روزشمار هدف (عددی)', + 'widgets.catalog.category.all': 'همه', + 'widgets.catalog.category.new': 'جدید', + 'widgets.catalog.category.time': 'زمان و تاریخ', + 'widgets.catalog.category.productivity': 'ابزار و تسک', + 'widgets.catalog.category.info': 'اطلاعات و رسانه', + 'widgets.catalog.category.lifestyle': 'سرگرمی', + 'widgets.catalog.title.edit': 'تغییر مدل و استایل ویجت', + 'widgets.catalog.title.add': 'مدیریت و افزودن ویجت‌ها', + 'widgets.catalog.help': 'راهنما', + 'widgets.catalog.badge.new': 'جدید', + 'widgets.catalog.duplicateHint': 'امکان افزودن چندین نمونه از این ویجت وجود دارد', + 'widgets.catalog.singleHint': 'ویجت تکی صفحه اصلی', + 'widgets.catalog.widgetSettings': 'تنظیمات ویجت', + 'widgets.catalog.emptyCategory': 'توی این دسته ویجتی پیدا نکردیم', + 'widgets.catalog.repeatable': 'قابل تکرار', + 'widgets.catalog.active': 'فعال', + 'widgets.catalog.selectVariant': 'انتخاب مدل و استایل:', + 'widgets.catalog.selectSize': 'انتخاب اندازه ویجت:', + 'widgets.catalog.defaultSize': '(پیش‌فرض)', + 'widgets.catalog.previewSize': 'پیش‌نمایش در اندازه {w}×{h}', + 'widgets.catalog.removeFromPage': 'حذف از صفحه', + 'widgets.catalog.upgradeToSave': 'ارتقا به پرو برای ذخیره این مدل', + 'widgets.catalog.upgradeToEnable': 'ارتقا به پرو برای فعال‌سازی', + 'widgets.catalog.saveChanges': 'ذخیره تغییرات', + 'widgets.catalog.duplicateProOnly': 'تکرار ویجت مخصوص کاربران پرو', + 'widgets.catalog.limitReached': 'تکمیل ظرفیت ویجت‌ها (ارتقا برای نامحدود)', + 'widgets.catalog.addWithSize': 'افزودن ویجت با اندازه {w}×{h}', + 'widgets.catalog.addToPage': 'افزودن به صفحه', + 'widgets.presets.default.title': 'چیدمان پیش‌فرض', + 'widgets.presets.default.description': + 'چیدمان استاندارد و کامل ویجتیفای با دسترسی به تمام ابزارها', + 'widgets.presets.simple-appearance.title': 'ظاهری ساده', + 'widgets.presets.simple-appearance.description': + 'جستجوی بالا و ابزارهای متمرکز در پایین صفحه', + 'widgets.presets.deep-work-flow.title': 'تمرکز کاری و تسک‌ها', + 'widgets.presets.deep-work-flow.description': + 'مدیریت وظایف روزانه، یادداشت‌ها، تقویم و ابزارها برای کار عمیق', + 'widgets.presets.student-study-hub.title': 'میز مطالعه و دانش‌آموز', + 'widgets.presets.student-study-hub.description': + 'برنامه درسی با تقویم گوگل، مدیریت تسک‌ها و قاب عکس انگیزشی', + 'widgets.presets.news-culture-magazine.title': 'مجله اخبار و فرهنگ', + 'widgets.presets.news-culture-magazine.description': + 'پیگیری داغ‌ترین خبرهای روز در کنار نرخ ارز و آب‌وهوا', + 'widgets.presets.cozy-lifestyle.title': 'گوشه دنج و خاطرات', + 'widgets.presets.cozy-lifestyle.description': + 'آلبوم قاب عکس، همراهی پت، پیگیری احوال و وضعیت آب‌وهوا', + 'widgets.presets.habit-builder-compact.title': 'توسعه فردی و عادت‌سازی', + 'widgets.presets.habit-builder-compact.description': + 'تمرکز ویژه بر ردیابی عادت‌ها، اهداف روزانه و پایش پیوسته حس‌وحال', + 'widgets.presets.crypto-finance-hub.title': 'مرکز ترید و ارز دیجیتال', + 'widgets.presets.crypto-finance-hub.description': + 'قیمت‌های زنده بازار، نرخ لحظه‌ای ارزها، وضعیت پینگ و ابزارها', + 'widgets.presets.developer-cockpit.title': 'میز کار برنامه‌نویس', + 'widgets.presets.developer-cockpit.description': + 'تودوهای عریض، پینگ سرور، یادداشت استیکی و بوکمارک‌های دم‌دست', + 'widgets.presets.habit-mindfulness.title': 'سلامت روان و عادات', + 'widgets.presets.habit-mindfulness.description': + 'ثبت حال روحی، پیگیری مستمر عادت‌ها و حیوان خانگی همراه', + 'widgets.presets.modalTitle': 'چیدمان‌های آماده', + 'widgets.presets.modalHint': + 'یکی از این قالب‌ها رو انتخاب کن تا ویجت‌های صفحه‌ی اصلی با همون چیدمان جایگزین بشن', + 'widgets.presets.free': 'رایگان', + 'widgets.presets.upgradeAria': 'ارتقا به پرو برای چیدمان {title}', + 'widgets.presets.upgrade': 'ارتقا به پرو', + 'widgets.presets.applyAria': 'اعمال چیدمان {title}', + 'widgets.presets.apply': 'اعمال چیدمان', + 'widgets.presets.confirmTitle': 'اعمال چیدمان جدید', + 'widgets.presets.confirmMessage': + 'با اعمال چیدمان «{title}»، ویجت‌ها و چیدمان فعلی جایگزین می‌شن مطمئنی؟', + 'widgets.presets.confirm': 'آره، اعمال کن', + 'widgets.canvas.editEnd': 'پایان ویرایش', + 'widgets.canvas.editWidgets': 'ویرایش ویجت‌ها', + 'widgets.canvas.addWidget': 'افزودن ویجت', + 'widgets.canvas.presets': 'چیدمان‌های آماده', + 'widgets.canvas.appearanceSettings': 'تنظیمات ظاهری', + 'widgets.canvas.wallpapers': 'تصویر‌ زمینه‌ها', + 'widgets.canvas.widgetsHelp': 'راهنمای ویجت‌ها', + 'widgets.canvas.editLayout': 'ویرایش چیدمان', + 'widgets.canvas.done': 'پایان', + 'widgets.canvas.deleteWidget': 'حذف ویجت', + 'widgets.canvas.upgradeProAria': 'ارتقا به اشتراک پرو', + 'widgets.canvas.upgradePro': 'ارتقا به اشتراک پرو', + 'widgets.canvas.widgetsAria': 'ویجت‌ها', + 'widgets.canvas.remove': 'حذف', + 'widgets.canvas.imageBuildFailed': 'نتونستیم تصویر رو بسازیم', + 'widgets.canvas.imageCopied': 'تصویر کپی شد', + 'widgets.canvas.imageCopyDenied': 'مرورگرت اجازه‌ی کپی نمی‌ده، تصویر رو دانلود کن', + 'widgets.canvas.imageDownloaded': 'تصویر دانلود شد', + 'widgets.canvas.imageDownloadFailed': 'نتونستیم تصویر رو دانلود کنیم', + 'widgets.menu.settingsOf': 'تنظیمات {label}', + 'widgets.menu.size': 'اندازه', + 'widgets.menu.editVariant': 'تغییر مدل و استایل', + 'widgets.menu.move': 'جابجایی', + 'widgets.menu.duplicate': 'تکرار ویجت', + 'widgets.menu.delete': 'حذف ویجت', + 'widgets.menu.options': 'گزینه‌های ویجت', + 'widgets.help.tab.move.label': 'جابه‌جایی آزاد', + 'widgets.help.tab.move.badge': 'چیدمان آزاد', + 'widgets.help.tab.move.title': 'جابجایی و درگ آزاد ویجت‌ها', + 'widgets.help.tab.move.description': + 'ویجت‌ها رو بدون محدودیت به هر نقطه از صفحه بکش و چیدمان دلخواهت رو خلق کن', + 'widgets.help.tab.move.tipEnterEdit': + 'روی فضای خالی صفحه کلیک راست کن و «ویرایش ویجت‌ها» رو بزن', + 'widgets.help.tab.move.tipDrag': 'ویجت‌ها رو با درگ کردن به موقعیت دلخواهت ببر', + 'widgets.help.tab.move.tipFinish': + 'در آخر دکمه «پایان» نوار پایین صفحه رو بزن تا چیدمان قفل و ذخیره بشه', + 'widgets.help.tab.styles.label': 'سایز و استایل', + 'widgets.help.tab.styles.badge': 'شخصی‌سازی', + 'widgets.help.tab.styles.title': 'تغییر ابعاد و استایل ظاهری', + 'widgets.help.tab.styles.description': + 'با کلیک راست روی هر ویجت، اندازه، ظاهر و نحوه نمایش اون رو به سلیقه خودت تغییر بده', + 'widgets.help.tab.styles.tipRightClick': 'روی ویجت موردنظرت کلیک راست کن', + 'widgets.help.tab.styles.tipPickSize': + 'از منوی باز شده اندازه دلخواه (کوچک، متوسط یا بزرگ) رو انتخاب کن', + 'widgets.help.tab.styles.tipChangeStyle': + 'برای ویجت‌های دارای استایل اختصاصی، از گزینه «تغییر مدل و استایل» استفاده کن', + 'widgets.help.tab.add.label': 'افزودن و تکرار', + 'widgets.help.tab.add.badge': 'تنوع بی‌نهایت', + 'widgets.help.tab.add.title': 'افزودن ویجت یا ساخت چند نسخه', + 'widgets.help.tab.add.description': + 'از منوی پایین صفحه ویجت جدید اضافه کن یا از یک ویجت چندین نمونه با کاربردهای جداگانه بساز', + 'widgets.help.tab.add.tipOpenAdd': + 'در حالت ویرایش، از نوار پایین گزینه «افزودن ویجت» رو بزن', + 'widgets.help.tab.add.tipPickWidget': 'ویجت دلخواهت رو به صفحه اضافه کن', + 'widgets.help.tab.add.tipMultiple': + 'می‌تونی چند ویجت مشابه (مثلا چند لیست کار یا یادداشت مجزا) در صفحه داشته باشی', + 'widgets.help.tab.presets.label': 'چیدمان‌های آماده', + 'widgets.help.tab.presets.badge': 'یک کلیک', + 'widgets.help.tab.presets.title': 'قالب‌ها و چیدمان‌های آماده', + 'widgets.help.tab.presets.description': + 'برای تغییر سریع ظاهر تب، از الگوهای حرفه‌ای و چیدمان‌های آماده پیش‌فرض استفاده کن', + 'widgets.help.tab.presets.tipOpenPresets': + 'در حالت ویرایش صفحه، گزینه «چیدمان‌های آماده» رو انتخاب کن', + 'widgets.help.tab.presets.tipPreview': 'پیش‌نمایش قالب‌های مختلف رو ببین', + 'widgets.help.tab.presets.tipApply': + 'با یک کلیک قالب مدنظرت رو اعمال کن و در صورت نیاز شخصی‌سازیش کن', + 'widgets.help.modalTitle': 'راهنمای مدیریت و چیدمان ویجت‌ها', + 'widgets.help.gotIt': 'متوجه شدم', + 'widgets.settings.pet.title': 'تنظیمات حیوان خانگی', + 'widgets.settings.weather.title': 'تنظیمات آب و هوا', + 'widgets.settings.wigiArz.title': 'تنظیمات ویجی ارز', + 'widgets.settings.news.title': 'تنظیمات اخبار', + 'widgets.settings.combo.title': 'تنظیمات ویجت ترکیبی', + 'widgets.settings.dotCalendar.title': 'تنظیمات روزشمار', + 'widgets.settings.calendar.title': 'تنظیمات تقویم', + 'widgets.auth.loginTitle': 'اول وارد حسابت شو', + 'widgets.auth.loginHint': 'بعدش این بخش برات باز می‌شه', + 'widgets.auth.login': 'ورود', + 'widgets.today.goToToday': 'برو به امروز', + 'widgets.error.loadFailed': 'نتونستیم بیاریمش', + 'widgets.error.retry': 'دوباره امتحان کن', + 'widgets.bookmarkDelete.title': 'حذف ویجت بوکمارک', + 'widgets.bookmarkDelete.message': + 'همه‌ی بوکمارک‌های داخلش هم حذف می‌شن ویجت بوکمارک حذف بشه؟', + 'widgets.bookmarkDelete.confirm': 'حذف ویجت', + 'widgets.bookmarkDelete.cancel': 'نه', + 'widgets.ops.noSpaceForSize': + 'برای این اندازه جا نیست! ویجت‌های کناری رو جابه‌جا کن تا جا باز بشه', + 'widgets.ops.noSpaceForVariant': + 'برای این مدل جا نیست! ویجت‌های کناری رو جابه‌جا کن یا مدل کوچک‌تری انتخاب کن', + 'widgets.ops.cannotDuplicate': 'امکان تکرار این ویجت وجود نداره', + 'widgets.updatedAt': 'به‌روز {time}', + 'widgets.shell.percent': '{percent}٪', +} as const diff --git a/src/common/i18n/index.ts b/src/common/i18n/index.ts new file mode 100644 index 000000000..bd8a88922 --- /dev/null +++ b/src/common/i18n/index.ts @@ -0,0 +1 @@ +export { t, type MessageKey } from './t' diff --git a/src/common/i18n/t.ts b/src/common/i18n/t.ts new file mode 100644 index 000000000..7b957757f --- /dev/null +++ b/src/common/i18n/t.ts @@ -0,0 +1,23 @@ +import { fa } from './fa' + +export type MessageKey = keyof typeof fa + +type ExtractParams = S extends `${string}{${infer P}}${infer Rest}` + ? P | ExtractParams + : never + +type ParamsFor = + ExtractParams<(typeof fa)[K]> extends never + ? never + : { [P in ExtractParams<(typeof fa)[K]>]: string | number } + +export function t(key: MessageKey): string +export function t(key: K, params: ParamsFor): string +export function t(key: MessageKey, params?: Record): string { + const template = fa[key] as string + if (!params) return template + return template.replace(/\{(\w+)\}/g, (_, name: string) => { + const value = params[name] + return value === undefined || value === null ? `{${name}}` : String(value) + }) +} diff --git a/src/common/toast.tsx b/src/common/toast.tsx index e159c0700..f04c42037 100644 --- a/src/common/toast.tsx +++ b/src/common/toast.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import toast from 'react-hot-toast' +import { t } from '@/common/i18n' import { translateError } from '@/common/utils/translate-error' import { Icon } from '@/icons' import { cn } from '@/common/utils/cn' @@ -99,8 +100,16 @@ const TOAST_THEMES: Record< { container: string icon: ReactNode - title: string - actionText: string + titleKey: + | 'ui.toast.infoTitle' + | 'ui.toast.errorTitle' + | 'ui.toast.successTitle' + | 'ui.toast.warningTitle' + actionKey: + | 'ui.toast.infoAction' + | 'ui.toast.errorAction' + | 'ui.toast.successAction' + | 'ui.toast.warningAction' messageClass: string } > = { @@ -111,8 +120,8 @@ const TOAST_THEMES: Record< ), - title: 'نکته', - actionText: 'متوجه شدم', + titleKey: 'ui.toast.infoTitle' as const, + actionKey: 'ui.toast.infoAction' as const, messageClass: 'text-[#d4d4d4]', }, error: { @@ -122,8 +131,8 @@ const TOAST_THEMES: Record< ), - title: 'یه مشکلی پیش اومد', - actionText: 'باشه', + titleKey: 'ui.toast.errorTitle' as const, + actionKey: 'ui.toast.errorAction' as const, messageClass: 'text-[#ffc9c9d9]', }, success: { @@ -133,8 +142,8 @@ const TOAST_THEMES: Record< ), - title: 'انجام شد', - actionText: 'باشه', + titleKey: 'ui.toast.successTitle' as const, + actionKey: 'ui.toast.successAction' as const, messageClass: 'text-[#a4f4cfd9]', }, warning: { @@ -144,8 +153,8 @@ const TOAST_THEMES: Record< ), - title: 'حواست باشه', - actionText: 'متوجه شدم', + titleKey: 'ui.toast.warningTitle' as const, + actionKey: 'ui.toast.warningAction' as const, messageClass: 'text-[#fee685d9]', }, } @@ -160,13 +169,13 @@ export function showToast( playNativeToastSound(type) return toast.custom( - (t) => ( + (toastItem) => (

- {theme.title} + {t(theme.titleKey)}

{message && (

toast.remove(t.id, t.toasterId)} + onClick={() => toast.remove(toastItem.id, toastItem.toasterId)} className="shrink-0 px-3.5 py-1.5 rounded-xl bg-[rgba(255,255,255,0.1)] hover:bg-image-fill active:scale-95 text-xs font-semibold text-white transition-ui cursor-pointer select-none" > - {theme.actionText} + {t(theme.actionKey)}

), diff --git a/src/common/utils/translate-error.ts b/src/common/utils/translate-error.ts index bc98bc133..93e91c4cb 100644 --- a/src/common/utils/translate-error.ts +++ b/src/common/utils/translate-error.ts @@ -1,185 +1,170 @@ -const errorTranslations: Record = { - ACTIVITY_ALREADY_EXISTS: 'فعلاً فقط یه نوشته می‌تونی داشته باشی', - MAX_SHARED_USERS_EXCEEDED: 'این تسک رو با بیشتر از این تعداد نمی‌شه به اشتراک گذاشت', - ACTIVITY_NOT_FOUND: 'این نوشته رو پیدا نکردیم', +import { t, type MessageKey } from '@/common/i18n' + +type ErrorKey = Extract + +const ERROR_KEYS: Record = { + ACTIVITY_ALREADY_EXISTS: 'error.activityAlreadyExists', + MAX_SHARED_USERS_EXCEEDED: 'error.maxSharedUsersExceeded', + ACTIVITY_NOT_FOUND: 'error.activityNotFound', // Authentication errors - INVALID_PASS_MAIL: 'ایمیل یا رمز عبور درست نیست', - INVALID_CREDENTIALS: 'اطلاعات ورود درست نیست، یه بار دیگه چک کن', - EMAIL_ALREADY_EXISTS: 'این ایمیل از قبل ثبت شده', - USER_NOT_FOUND: 'این کاربر رو پیدا نکردیم', - TOKEN_EXPIRED: 'ورودت منقضی شده، دوباره وارد حسابت شو', - INVALID_TOKEN: 'ورودت دیگه معتبر نیست، دوباره وارد حسابت شو', - UNAUTHORIZED: 'به این بخش دسترسی نداری', - FORBIDDEN: 'دسترسی به این بخش محدود شده', + INVALID_PASS_MAIL: 'error.invalidPassMail', + INVALID_CREDENTIALS: 'error.invalidCredentials', + EMAIL_ALREADY_EXISTS: 'error.emailAlreadyExists', + USER_NOT_FOUND: 'error.userNotFound', + TOKEN_EXPIRED: 'error.tokenExpired', + INVALID_TOKEN: 'error.invalidToken', + UNAUTHORIZED: 'error.unauthorized', + FORBIDDEN: 'error.forbidden', // Rate-limit & OTP errors - OTP_RATE_LIMIT: 'همین الان یه کد برات فرستادیم، یه کم صبر کن', - FORGOT_PASSWORD_REQUEST_LIMIT: - 'زیادی درخواست بازیابی رمز دادی، یه کم بعد دوباره امتحان کن', - RESET_TOKEN_EXPIRED: 'لینک بازیابی رمز عبور منقضی شده، یه لینک جدید بگیر', - INVALID_RESET_TOKEN: 'این لینک بازیابی رمز عبور درست نیست', + OTP_RATE_LIMIT: 'error.otpRateLimit', + FORGOT_PASSWORD_REQUEST_LIMIT: 'error.forgotPasswordRequestLimit', + RESET_TOKEN_EXPIRED: 'error.resetTokenExpired', + INVALID_RESET_TOKEN: 'error.invalidResetToken', // Validation errors - WEAK_PASSWORD: 'رمز عبور ضعیفه، از حروف، اعداد و نمادها استفاده کن', - PASSWORD_TOO_SHORT: 'رمز عبور باید حداقل ۸ کاراکتر باشه', - INVALID_EMAIL_FORMAT: 'این ایمیل درست به نظر نمی‌رسه', - NAME_REQUIRED: 'نام کاربری رو بنویس', - INVALID_INPUTS: 'یه چیزی توی اطلاعاتی که وارد کردی درست نیست', + WEAK_PASSWORD: 'error.weakPassword', + PASSWORD_TOO_SHORT: 'error.passwordTooShort', + INVALID_EMAIL_FORMAT: 'error.invalidEmailFormat', + NAME_REQUIRED: 'error.nameRequired', + INVALID_INPUTS: 'error.invalidInputs', // HTTP status errors - INTERNAL_SERVER_ERROR: 'سرور به مشکل خورده، یه کم بعد دوباره امتحان کن', - SERVICE_UNAVAILABLE: 'سرویس الان در دسترس نیست، یه کم بعد دوباره امتحان کن', - TOO_MANY_REQUESTS: 'یه کم تند رفتی، چند لحظه صبر کن و دوباره امتحان کن', - BAD_REQUEST: 'این درخواست درست نبود، دوباره امتحان کن', - NOT_FOUND: 'چیزی پیدا نکردیم', - ACTIVITY_UPDATE_RATE_LIMIT_EXCEEDED: 'وضعیتت رو زیادی عوض کردی، یه کم صبر کن', + INTERNAL_SERVER_ERROR: 'error.internalServerError', + SERVICE_UNAVAILABLE: 'error.serviceUnavailable', + TOO_MANY_REQUESTS: 'error.tooManyRequests', + BAD_REQUEST: 'error.badRequest', + NOT_FOUND: 'error.notFound', + ACTIVITY_UPDATE_RATE_LIMIT_EXCEEDED: 'error.activityUpdateRateLimitExceeded', // Friend-related errors - CANT_REQUEST_YOURSELF: 'نمی‌تونی به خودت درخواست دوستی بفرستی', - FRIEND_REQUEST_ALREADY_SENT: 'قبلاً براش درخواست دوستی فرستادی', - FRIEND_REQUEST_ALREADY_EXISTS: 'از قبل یه درخواست دوستی بینتون هست', - FAILED_TO_FETCH_FRIENDS: 'نتونستیم فهرست دوستات رو بیاریم', - FAILED_TO_SEND_REQUEST: 'نتونستیم درخواست دوستی رو بفرستیم', - FAILED_TO_ACCEPT_REQUEST: 'نتونستیم درخواست دوستی رو قبول کنیم', - FAILED_TO_REMOVE_FRIEND: 'نتونستیم این دوست رو حذف کنیم', - FRIEND_REQUEST_SENT: 'درخواست دوستی فرستاده شد', - FRIEND_REQUEST_NOT_FOUND: 'این درخواست دوستی رو پیدا نکردیم', - SET_USERNAME_FIRST: 'اول نام کاربریت رو تنظیم کن', + CANT_REQUEST_YOURSELF: 'error.cantRequestYourself', + FRIEND_REQUEST_ALREADY_SENT: 'error.friendRequestAlreadySent', + FRIEND_REQUEST_ALREADY_EXISTS: 'error.friendRequestAlreadyExists', + FAILED_TO_FETCH_FRIENDS: 'error.failedToFetchFriends', + FAILED_TO_SEND_REQUEST: 'error.failedToSendRequest', + FAILED_TO_ACCEPT_REQUEST: 'error.failedToAcceptRequest', + FAILED_TO_REMOVE_FRIEND: 'error.failedToRemoveFriend', + FRIEND_REQUEST_SENT: 'error.friendRequestSent', + FRIEND_REQUEST_NOT_FOUND: 'error.friendRequestNotFound', + SET_USERNAME_FIRST: 'error.setUsernameFirst', // Translate-related errors - SOURCE_AND_TARGET_LANG_MUST_BE_DIFFERENT: 'زبان مبدأ و مقصد نمی‌تونن یکسان باشن', - TARGET_LANG_CANNOT_BE_AUTO: 'زبان مقصد نمی‌تونه تشخیص خودکار باشه', - TRANSLATION_FAILED: 'نتونستیم متن رو ترجمه کنیم', - FAILED_TO_FETCH_LANGUAGES: 'نتونستیم فهرست زبان‌ها رو بیاریم', - INVALID_LANGUAGE_CODE: 'این زبان رو نمی‌شناسیم', - TEXT_TOO_LONG: 'متن برای ترجمه خیلی طولانیه', - EMPTY_TEXT: 'متن برای ترجمه نمی‌تونه خالی باشه', - TRANSLATION_QUOTA_EXCEEDED: 'سهمیه‌ی ترجمه‌ت تموم شده', + SOURCE_AND_TARGET_LANG_MUST_BE_DIFFERENT: 'error.sourceAndTargetLangMustBeDifferent', + TARGET_LANG_CANNOT_BE_AUTO: 'error.targetLangCannotBeAuto', + TRANSLATION_FAILED: 'error.translationFailed', + FAILED_TO_FETCH_LANGUAGES: 'error.failedToFetchLanguages', + INVALID_LANGUAGE_CODE: 'error.invalidLanguageCode', + TEXT_TOO_LONG: 'error.textTooLong', + EMPTY_TEXT: 'error.emptyText', + TRANSLATION_QUOTA_EXCEEDED: 'error.translationQuotaExceeded', // Success messages - SUCCESS: 'انجام شد', + SUCCESS: 'error.success', // Widget-related messages - WIDGET_NOT_FOUND: 'این ویجت رو پیدا نکردیم', - WIDGET_DELETED: 'ویجت حذف شد', - WIDGET_DUPLICATED: 'ویجت تکرار شد', - WIDGET_ALREADY_EXISTS: 'این ویجت از قبل به صفحه اضافه شده', - INVALID_WIDGET_POSITION: 'موقعیت قرارگیری ویجت درست نیست', - STORAGE_QUOTA_EXCEEDED: 'فضای ذخیره‌سازی عکس‌های ویجت پر شده', - WIDGET_LIMIT_EXCEEDED: 'به سقف تعداد ویجت‌ها رسیدی', - MAX_WIDGETS_REACHED: - 'به سقف تعداد ویجت‌ها رسیدی، یکی از قبلی‌ها رو حذف کن تا جا باز بشه', - NO_SPACE_FOR_WIDGET: - 'روی صفحه جا نیست، چند تا ویجت رو جابه‌جا یا حذف کن تا جا باز بشه', - NO_SPACE_FOR_DUPLICATE: 'روی صفحه جا نیست، برای تکرار ویجت کمی فضا باز کن', + WIDGET_NOT_FOUND: 'error.widgetNotFound', + WIDGET_DELETED: 'error.widgetDeleted', + WIDGET_DUPLICATED: 'error.widgetDuplicated', + WIDGET_ALREADY_EXISTS: 'error.widgetAlreadyExists', + INVALID_WIDGET_POSITION: 'error.invalidWidgetPosition', + STORAGE_QUOTA_EXCEEDED: 'error.storageQuotaExceeded', + WIDGET_LIMIT_EXCEEDED: 'error.widgetLimitExceeded', + MAX_WIDGETS_REACHED: 'error.maxWidgetsReached', + NO_SPACE_FOR_WIDGET: 'error.noSpaceForWidget', + NO_SPACE_FOR_DUPLICATE: 'error.noSpaceForDuplicate', // Bookmark-related messages - BOOKMARK_DELETED: 'بوکمارک حذف شد', - BOOKMARK_ADDED: 'بوکمارک اضافه شد', - BOOKMARK_UPDATED: 'بوکمارک ویرایش شد', - BOOKMARK_PARENT_NOT_FOUND: 'این پوشه رو پیدا نکردیم', - FILE_SIZE_EXCEEDED: 'این فایل زیادی بزرگه', + BOOKMARK_DELETED: 'error.bookmarkDeleted', + BOOKMARK_ADDED: 'error.bookmarkAdded', + BOOKMARK_UPDATED: 'error.bookmarkUpdated', + BOOKMARK_PARENT_NOT_FOUND: 'error.bookmarkParentNotFound', + FILE_SIZE_EXCEEDED: 'error.fileSizeExceeded', // Network errors - NETWORK_ERROR: 'اینترنتت رو چک کن و دوباره امتحان کن', - CONNECTION_TIMEOUT: 'جواب دیر رسید، دوباره امتحان کن', - CONNECTION_REFUSED: 'نتونستیم وصل بشیم، یه کم بعد دوباره امتحان کن', - - FIRST_VERIFY_YOUR_ACCOUNT: 'اول حسابت رو تایید کن', - USERNAME_ALREADY_EXISTS: 'این نام کاربری رو قبلاً یکی برداشته', - INVALID_FILE_TYPE: 'این نوع فایل پشتیبانی نمی‌شه', - NOT_ENOUGH_COINS: 'ویج‌کوین‌هات کافی نیست', - INVALID_REFERRAL_CODE: 'این کد دعوت درست نیست', - ITEM_ALREADY_EXISTS: 'این رو قبلاً گرفتی و مال توئه، لازم نیست دوباره بخریش', - - INVALID_ID: 'یه چیزی درست نیست، دوباره امتحان کن', - - DATE_OUT_OF_RANGE: 'این تاریخ رو نمی‌شه انتخاب کرد', - - ITEM_NOT_FOUND: 'پیداش نکردیم', - TODO_NOT_FOUND: 'این تسک رو پیدا نکردیم', - INVALID_OTP_CODE: 'کد تایید اشتباهه، دوباره امتحان کن', - USE_EMAIL_FOR_OTP: 'فعلاً کد تایید رو با ایمیل بگیر', - USE_PHONE_FOR_OTP: 'فعلاً کد تایید رو با شماره موبایل بگیر', - - INVALID_OCCUPATION_ID: 'این شغل توی فهرست نیست، یکی دیگه انتخاب کن', - ONE_OR_MORE_INVALID_INTEREST_IDS: - 'چند تا از علاقه‌مندی‌هایی که انتخاب کردی توی فهرست نیستن', - - TOO_MANY_ATTEMPTS: 'زیادی امتحان کردی، یه کم صبر کن', - OTP_EXPIRED: 'این کد منقضی شده، یه کد جدید بگیر', - INVALID_PHONE_NUMBER_FORMAT: 'این شماره درست به نظر نمی‌رسه', - CANNOT_CHANGE_PHONE_NUMBER: 'نمی‌تونی شماره موبایل رو تغییر بدی', - - SAME_PHONE_NUMBER_ERROR: 'این همون شماره‌ی فعلیته', - PHONE_NUMBER_ALREADY_EXISTS: 'این شماره موبایل از قبل ثبت شده', - INVALID_VERIFICATION_CODE: 'کد تایید درست نیست', - CANNOT_CHANGE_EMAIL: 'نمی‌تونی ایمیل رو تغییر بدی', - SAME_EMAIL_ERROR: 'این همون ایمیل فعلیته', - FIRST_SET_EMAIL: 'هنوز ایمیل ثبت نکردی', - - PACKAGE_NOT_FOUND: 'این بسته رو پیدا نکردیم', - PAYMENT_FAILED: 'پرداخت انجام نشد، دوباره امتحان کن', - PAYMENT_ALREADY_PROCESSED: 'این پرداخت قبلاً انجام شده', - PAYMENT_NOT_FOUND: 'این پرداخت رو پیدا نکردیم', - - TRY_NEXT_TIME: 'یه مشکلی پیش اومد، یه کم بعد دوباره امتحان کن', - - TOO_MANY_ATTEMPTS_HABIT: 'بیشتر از این نمی‌تونی بسازی', - - FOLDER_STRUCTURE_TOO_DEEP: 'پوشه‌ها زیادی تو در تو شدن', - BULK_IMPORT_LIMIT_EXCEEDED: 'این تعداد رو یه‌جا نمی‌شه درون‌ریزی کرد', - NO_VALID_ITEMS_TO_IMPORT: 'چیزی برای درون‌ریزی پیدا نکردیم', - BIRTHDATE_CANNOT_BE_CHANGED: - 'تازه تاریخ تولدت رو عوض کردی، فعلاً نمی‌شه دوباره عوضش کرد', - VIP_REQUIRED: 'این قابلیت مال نسخه‌ی پروئه', - UPLOAD_IN_PROGRESS: 'داریم فایل رو آپلود می‌کنیم، یه کم صبر کن', - CUSTOM_WALLPAPER_REMOVED: 'تصویر پس‌زمینه حذف شد', - UPLOAD_FAILED: 'نتونستیم فایل رو آپلود کنیم، دوباره امتحان کن', + NETWORK_ERROR: 'error.networkError', + CONNECTION_TIMEOUT: 'error.connectionTimeout', + CONNECTION_REFUSED: 'error.connectionRefused', + FIRST_VERIFY_YOUR_ACCOUNT: 'error.firstVerifyYourAccount', + USERNAME_ALREADY_EXISTS: 'error.usernameAlreadyExists', + INVALID_FILE_TYPE: 'error.invalidFileType', + NOT_ENOUGH_COINS: 'error.notEnoughCoins', + INVALID_REFERRAL_CODE: 'error.invalidReferralCode', + ITEM_ALREADY_EXISTS: 'error.itemAlreadyExists', + INVALID_ID: 'error.invalidId', + DATE_OUT_OF_RANGE: 'error.dateOutOfRange', + ITEM_NOT_FOUND: 'error.itemNotFound', + TODO_NOT_FOUND: 'error.todoNotFound', + INVALID_OTP_CODE: 'error.invalidOtpCode', + USE_EMAIL_FOR_OTP: 'error.useEmailForOtp', + USE_PHONE_FOR_OTP: 'error.usePhoneForOtp', + INVALID_OCCUPATION_ID: 'error.invalidOccupationId', + ONE_OR_MORE_INVALID_INTEREST_IDS: 'error.oneOrMoreInvalidInterestIds', + TOO_MANY_ATTEMPTS: 'error.tooManyAttempts', + OTP_EXPIRED: 'error.otpExpired', + INVALID_PHONE_NUMBER_FORMAT: 'error.invalidPhoneNumberFormat', + CANNOT_CHANGE_PHONE_NUMBER: 'error.cannotChangePhoneNumber', + SAME_PHONE_NUMBER_ERROR: 'error.samePhoneNumberError', + PHONE_NUMBER_ALREADY_EXISTS: 'error.phoneNumberAlreadyExists', + INVALID_VERIFICATION_CODE: 'error.invalidVerificationCode', + CANNOT_CHANGE_EMAIL: 'error.cannotChangeEmail', + SAME_EMAIL_ERROR: 'error.sameEmailError', + FIRST_SET_EMAIL: 'error.firstSetEmail', + PACKAGE_NOT_FOUND: 'error.packageNotFound', + PAYMENT_FAILED: 'error.paymentFailed', + PAYMENT_ALREADY_PROCESSED: 'error.paymentAlreadyProcessed', + PAYMENT_NOT_FOUND: 'error.paymentNotFound', + TRY_NEXT_TIME: 'error.tryNextTime', + TOO_MANY_ATTEMPTS_HABIT: 'error.tooManyAttemptsHabit', + FOLDER_STRUCTURE_TOO_DEEP: 'error.folderStructureTooDeep', + BULK_IMPORT_LIMIT_EXCEEDED: 'error.bulkImportLimitExceeded', + NO_VALID_ITEMS_TO_IMPORT: 'error.noValidItemsToImport', + BIRTHDATE_CANNOT_BE_CHANGED: 'error.birthdateCannotBeChanged', + VIP_REQUIRED: 'error.vipRequired', + UPLOAD_IN_PROGRESS: 'error.uploadInProgress', + CUSTOM_WALLPAPER_REMOVED: 'error.customWallpaperRemoved', + UPLOAD_FAILED: 'error.uploadFailed', } -const validationTranslations: Record = { +const VALIDATION_KEYS: Record = { 'password must be longer than or equal to 8 characters': - 'رمز عبور باید حداقل ۸ کاراکتر باشه', + 'error.validation.passwordMinLength', 'password must contain at least 1 uppercase letter': - 'رمز عبور باید حداقل یه حرف بزرگ داشته باشه', + 'error.validation.passwordUppercase', 'password must contain at least 1 lowercase letter': - 'رمز عبور باید حداقل یه حرف کوچک داشته باشه', - 'password must contain at least 1 number': 'رمز عبور باید حداقل یه عدد داشته باشه', - 'password must contain at least 1 symbol': - 'رمز عبور باید حداقل یه نماد مثل @#$% داشته باشه', - 'password must be a string': 'رمز عبور باید متن باشه', - 'password should not be empty': 'رمز عبور نمی‌تونه خالی باشه', - - 'email must be an email': 'این ایمیل درست به نظر نمی‌رسه', - 'email should not be empty': 'ایمیل نمی‌تونه خالی باشه', - 'email must be a string': 'ایمیل باید متن باشه', - - 'name should not be empty': 'نام کاربری نمی‌تونه خالی باشه', - 'name must be a string': 'نام کاربری باید متن باشه', - 'name must be longer than or equal to 3 characters': - 'نام کاربری باید حداقل ۳ کاراکتر باشه', + 'error.validation.passwordLowercase', + 'password must contain at least 1 number': 'error.validation.passwordNumber', + 'password must contain at least 1 symbol': 'error.validation.passwordSymbol', + 'password must be a string': 'error.validation.passwordString', + 'password should not be empty': 'error.validation.passwordEmpty', + 'email must be an email': 'error.validation.emailFormat', + 'email should not be empty': 'error.validation.emailEmpty', + 'email must be a string': 'error.validation.emailString', + 'name should not be empty': 'error.validation.nameEmpty', + 'name must be a string': 'error.validation.nameString', + 'name must be longer than or equal to 3 characters': 'error.validation.nameMinLength', 'name must be shorter than or equal to 50 characters': - 'نام کاربری باید حداکثر ۵۰ کاراکتر باشه', + 'error.validation.nameMaxLength', // Widget-specific validation messages - 'widget title should not be empty': 'عنوان ویجت نمی‌تونه خالی باشه', - 'widget position must be valid': 'جای ویجت درست نیست', - 'widget size must be valid': 'اندازه‌ی ویجت درست نیست', + 'widget title should not be empty': 'error.validation.widgetTitleEmpty', + 'widget position must be valid': 'error.validation.widgetPosition', + 'widget size must be valid': 'error.validation.widgetSize', // Friend-related validation messages - 'username should not be empty': 'نام کاربری نمی‌تونه خالی باشه', - 'username does not exist': 'این نام کاربری وجود نداره', - 'cannot send friend request to yourself': 'نمی‌تونی به خودت درخواست دوستی بفرستی', - 'friend request already sent': 'قبلاً براش درخواست دوستی فرستادی', - 'name must be longer than or equal to 2 characters': 'نام کاربری رو بنویس', - CONTENT_CONTAINS_PROFANITY: 'توی متنت کلمه‌ی نامناسب هست، یه کم عوضش کن', + 'username should not be empty': 'error.validation.usernameEmpty', + 'username does not exist': 'error.validation.usernameMissing', + 'cannot send friend request to yourself': 'error.validation.friendSelf', + 'friend request already sent': 'error.validation.friendAlreadySent', + 'name must be longer than or equal to 2 characters': 'error.validation.nameMin2', + CONTENT_CONTAINS_PROFANITY: 'error.contentContainsProfanity', } function translateValidationMessage(message: string): string { - return validationTranslations[message] || message + const key = VALIDATION_KEYS[message] + return key ? t(key) : message } export function translateError(error: any): string | Record { - const defaultMessage = 'یه مشکلی پیش اومد، دوباره امتحان کن' + const defaultMessage = t('error.default') if (!error) return defaultMessage @@ -212,5 +197,6 @@ export function translateError(error: any): string | Record { if (!errorMessage) return defaultMessage - return errorTranslations[errorMessage] || errorMessage || defaultMessage + const key = ERROR_KEYS[errorMessage] + return key ? t(key) : errorMessage || defaultMessage } diff --git a/src/components/auth/auth-required-modal.tsx b/src/components/auth/auth-required-modal.tsx index 06caa8a66..e2654f7ea 100644 --- a/src/components/auth/auth-required-modal.tsx +++ b/src/components/auth/auth-required-modal.tsx @@ -1,4 +1,5 @@ import { callEvent } from '@/common/utils/call-event' +import { t } from '@/common/i18n' import { Modal } from '@/components/ui' import { Button } from '@/components/ui' import { Icon } from '@/icons' @@ -15,11 +16,16 @@ interface AuthRequiredModalProps { export function AuthRequiredModal({ isOpen, onClose, - title = 'ورود به حساب کاربری', - message = 'برای دسترسی به این بخش اول وارد حسابت شو', - loginButtonText = 'ورود به حساب', - cancelButtonText = 'فعلا نه', + title, + message, + loginButtonText, + cancelButtonText, }: AuthRequiredModalProps) { + const resolvedTitle = title ?? t('auth.required.title') + const resolvedMessage = message ?? t('auth.required.message') + const resolvedLogin = loginButtonText ?? t('auth.required.login') + const resolvedCancel = cancelButtonText ?? t('auth.required.cancel') + function triggerAccountTabDisplay() { onClose() callEvent('openProfile') @@ -32,6 +38,7 @@ export function AuthRequiredModal({ onClose={onClose} closeOnBackdropClick={true} showCloseButton={true} + closeLabel={t('ui.common.close')} >
@@ -39,9 +46,9 @@ export function AuthRequiredModal({
-

{title}

+

{resolvedTitle}

- {message} + {resolvedMessage}

@@ -53,7 +60,7 @@ export function AuthRequiredModal({ className="flex-1 text-xs" rounded={'2xl'} > - {loginButtonText} + {resolvedLogin}
diff --git a/src/components/gallery/gallery-asset-purchase-modal.tsx b/src/components/gallery/gallery-asset-purchase-modal.tsx index aa62120b3..0e389aaf0 100644 --- a/src/components/gallery/gallery-asset-purchase-modal.tsx +++ b/src/components/gallery/gallery-asset-purchase-modal.tsx @@ -2,6 +2,7 @@ import { Alert, Button, Modal } from '@/components/ui' import { UserCoin } from '@/components/user-coin' import { callEvent } from '@/common/utils/call-event' import { showToast } from '@/common/toast' +import { t } from '@/common/i18n' import type { GalleryAsset } from '@/services/gallery/get-gallery-assets.hook' import { usePurchaseGalleryAsset } from '@/services/gallery/get-gallery-assets.hook' @@ -36,7 +37,12 @@ export function GalleryAssetPurchaseModal({ purchase(asset.id, { onSuccess: (response) => { - showToast(`${asset.title || 'آیتم'} مال تو شد`, 'success') + showToast( + t('gallery.purchase.owned', { + name: asset.title || t('gallery.purchase.itemFallback'), + }), + 'success' + ) const updatedAsset = response?.data?.asset || { ...asset, isOwned: true, @@ -45,7 +51,7 @@ export function GalleryAssetPurchaseModal({ onPurchaseSuccess(updatedAsset) }, onError: () => { - showToast('نتونستیم آیتم رو بگیریم، دوباره امتحان کن', 'error') + showToast(t('gallery.purchase.fetchFailed'), 'error') }, }) } @@ -66,7 +72,13 @@ export function GalleryAssetPurchaseModal({ } return ( - +

- {asset.title || 'تصویر گالری'} + {asset.title || t('gallery.purchase.imageAlt')}

{asset.price > 0 && ( - + )}

{isVipUnlocked - ? 'چون اشتراک پرو داری می‌تونی این تصویر رو رایگان فعال کنی یا با ویج‌کوین دائمی بخریش' - : 'این تصویر رو با ویج‌کوین باز کن و همیشه ازش استفاده کن'} + ? t('gallery.purchase.vipHint') + : t('gallery.purchase.buyHint')}

@@ -101,11 +116,13 @@ export function GalleryAssetPurchaseModal({ onClick={handleOpenCoins} className="font-medium underline cursor-pointer" > - خرید ویج‌کوین + {t('gallery.purchase.buyCoins')} } > - {asset.price - userCoins} ویج‌کوین کم داری + {t('gallery.purchase.coinsShort', { + count: asset.price - userCoins, + })} )} @@ -119,7 +136,7 @@ export function GalleryAssetPurchaseModal({ rounded="2xl" color="brand" > - استفاده رایگان با پرو + {t('gallery.purchase.freeWithPro')} {asset.price > 0 && ( )}
@@ -142,12 +159,12 @@ export function GalleryAssetPurchaseModal({ size="md" disabled={!canAfford || isPending} loading={isPending} - loadingText="داریم می‌خریم…" + loadingText={t('gallery.purchase.buying')} className="flex-1" rounded="2xl" color={canAfford ? 'brand' : 'base'} > - خرید دائمی + {t('gallery.purchase.buyPermanent')} )} diff --git a/src/components/gallery/gallery-bookmark-icon-item.tsx b/src/components/gallery/gallery-bookmark-icon-item.tsx index 802396e3d..71cc886c5 100644 --- a/src/components/gallery/gallery-bookmark-icon-item.tsx +++ b/src/components/gallery/gallery-bookmark-icon-item.tsx @@ -1,6 +1,7 @@ import { useState, useRef } from 'react' import { Icon } from '@/icons' import { UserCoin } from '@/components/user-coin' +import { t } from '@/common/i18n' import { useLazyLoad } from './use-lazy-load' import type { GalleryAsset } from '@/services/gallery/get-gallery-assets.hook' import { Spinner } from '@/components/ui' @@ -56,13 +57,15 @@ export function GalleryBookmarkIconItem({ {error && (
-

نتونستیم بیاریمش

+

+ {t('gallery.bookmark.fetchFailed')} +

)}
{asset.title { setLoaded(true) setError(false) @@ -86,7 +89,10 @@ export function GalleryBookmarkIconItem({ )} {asset.price > 0 && !asset.isOwned && (
- +
)} @@ -99,14 +105,14 @@ export function GalleryBookmarkIconItem({
- رایگان با پرو + {t('gallery.bookmark.freeWithPro')}
)} {asset.isOwned && !isSelected && (
- مال توئه + {t('gallery.bookmark.owned')}
)} diff --git a/src/components/gallery/gallery-photo-item.tsx b/src/components/gallery/gallery-photo-item.tsx index 22f89c87c..5233a1446 100644 --- a/src/components/gallery/gallery-photo-item.tsx +++ b/src/components/gallery/gallery-photo-item.tsx @@ -1,6 +1,7 @@ import { useState, useRef } from 'react' import { Icon } from '@/icons' import { UserCoin } from '@/components/user-coin' +import { t } from '@/common/i18n' import { useLazyLoad } from './use-lazy-load' import type { GalleryAsset } from '@/services/gallery/get-gallery-assets.hook' import { Spinner } from '@/components/ui' @@ -45,7 +46,9 @@ export function GalleryPhotoItem({ asset, isSelected, onClick }: GalleryPhotoIte {error && (
-

نتونستیم بیاریمش

+

+ {t('gallery.photo.fetchFailed')} +

)} @@ -73,7 +76,10 @@ export function GalleryPhotoItem({ asset, isSelected, onClick }: GalleryPhotoIte {asset.price > 0 && !asset.isOwned ? (
- +
) : null}
@@ -88,7 +94,7 @@ export function GalleryPhotoItem({ asset, isSelected, onClick }: GalleryPhotoIte
- رایگان با پرو + {t('gallery.photo.freeWithPro')}
)} @@ -96,7 +102,7 @@ export function GalleryPhotoItem({ asset, isSelected, onClick }: GalleryPhotoIte {asset.isOwned && !isSelected && (
- مال توئه + {t('gallery.photo.owned')}
)} diff --git a/src/components/gallery/gallery-picker-modal.tsx b/src/components/gallery/gallery-picker-modal.tsx index 188a70994..9d721beab 100644 --- a/src/components/gallery/gallery-picker-modal.tsx +++ b/src/components/gallery/gallery-picker-modal.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { Modal, Chip } from '@/components/ui' import { Icon } from '@/icons' import { playNativeToastSound, showToast } from '@/common/toast' +import { t } from '@/common/i18n' import { useAuth } from '@/context/auth.context' import { callEvent } from '@/common/utils/call-event' import { @@ -27,7 +28,7 @@ export function GalleryPickerModal({ isOpen, onClose, type, - title = 'گالری تصاویر', + title = t('gallery.picker.title'), onSelect, selectedAssetUrl, }: GalleryPickerModalProps) { @@ -58,7 +59,7 @@ export function GalleryPickerModal({ if (!user) { callEvent('openProfile') - showToast('برای استفاده از این مورد اول وارد حسابت شو', 'error') + showToast(t('gallery.picker.loginRequired'), 'error') return } @@ -91,6 +92,7 @@ export function GalleryPickerModal({ size="xl" className="" closeOnBackdropClick={true} + closeLabel={t('ui.common.close')} >
{categories.length > 0 && ( @@ -100,7 +102,7 @@ export function GalleryPickerModal({ onClick={() => setSelectedCategory('ALL')} className="text-xs" > - همه + {t('gallery.picker.all')} {categories.map((cat) => ( -

تصویری پیدا نکردیم

+

+ {t('gallery.picker.empty')} +

) : type === 'BOOKMARK_ICON' ? (
diff --git a/src/components/select-city.tsx b/src/components/select-city.tsx index 82c5eb2e7..d08f6f327 100644 --- a/src/components/select-city.tsx +++ b/src/components/select-city.tsx @@ -1,5 +1,6 @@ -import { useState, useMemo } from 'react' +import { useState, useMemo, useRef } from 'react' import Analytics from '@/analytics' +import { t } from '@/common/i18n' import { Alert, Spinner } from '@/components/ui' import { Modal } from '@/components/ui' import { SectionPanel } from '@/components/ui' @@ -81,7 +82,7 @@ export function SelectCity({ size }: Prop) { : null return ( - +
{error && ( - - اینترنتت رو چک کن و دوباره امتحان کن + + {t('city.select.loadFailedBody')} )}
setShowAuthModal(!showAuthModal)} - message="برای انتخاب شهر اول وارد حسابت شو" + message={t('city.select.loginRequired')} />
setSearchTerm(value)} @@ -142,7 +144,7 @@ export function SelectCity({ size }: Prop) { {isLoading ? (
) : filteredCities?.length > 0 ? ( filteredCities.map((city) => ( @@ -163,22 +165,22 @@ export function SelectCity({ size }: Prop) { )) ) : searchTerm ? (
- شهری با این اسم پیدا نکردیم + {t('city.select.noMatch')}
) : cities && cities.length === 0 ? (
- فعلاً شهری توی فهرست نیست + {t('city.select.emptyList')}
) : (
- اسم شهرت رو بنویس + {t('city.select.typeName')}
)}

- اگه شهرت توی فهرست نبود، بهمون بگو تا اضافه‌ش کنیم 🤝 + {t('city.select.missingHint')}

diff --git a/src/components/ui/README.md b/src/components/ui/README.md index 32f8cd10d..1212c3113 100644 --- a/src/components/ui/README.md +++ b/src/components/ui/README.md @@ -47,7 +47,7 @@ Look here before writing any UI. If a component other areas would reuse is missi ## Writing the words -Persian, friendly, short. Talk to the person, not at them. Say what happened and what to do next. +Persian, friendly, short. Talk to the person, not at them. Say what happened and what to do next. Primitives take their text from props and have no Persian defaults; the real wording lives in `src/common/i18n/` and callers pass `t(...)`. | Instead of | Write | |---|---| diff --git a/src/components/ui/badge/vip-badge.tsx b/src/components/ui/badge/vip-badge.tsx index d92b07bce..e7024d976 100644 --- a/src/components/ui/badge/vip-badge.tsx +++ b/src/components/ui/badge/vip-badge.tsx @@ -24,7 +24,7 @@ export function VipBadge({ size = 'sm', variant = 'solid', iconOnly = false, - text = 'پرو', + text, className, ...props }: VipBadgeProps) { @@ -44,7 +44,7 @@ export function VipBadge({ {...props} > - {!iconOnly && {text}} + {!iconOnly && text ? {text} : null} ) } diff --git a/src/components/ui/button/button.tsx b/src/components/ui/button/button.tsx index a0d686451..81e7cc67f 100644 --- a/src/components/ui/button/button.tsx +++ b/src/components/ui/button/button.tsx @@ -35,12 +35,7 @@ export function Button({ {...rest} > {loading ? ( - loadingText || ( - <> -
@@ -135,7 +136,7 @@ export function ExplorerPopoverMenu({ {menuItems.length === 0 && item.url && ( } onClick={() => handleItemClick(item.url)} /> diff --git a/src/features/explorer/components/promo-modal.tsx b/src/features/explorer/components/promo-modal.tsx index 4ae5854c4..f5ed5a7e7 100644 --- a/src/features/explorer/components/promo-modal.tsx +++ b/src/features/explorer/components/promo-modal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' import Analytics from '@/analytics' +import { t } from '@/common/i18n' import { showToast } from '@/common/toast' import { Badge, Button, ImageSlider, Modal } from '@/components/ui' import { Icon } from '@/icons' @@ -28,7 +29,9 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal const description = meta?.description || item.description const highlights: string[] = Array.isArray(meta?.highlights) ? meta.highlights : [] const isSponsor = - item.badge === 'اسپانسر' || item.badgeColor?.includes('amber') || meta?.isSponsor + item.badge === t('explorer.promo.sponsor') || + item.badgeColor?.includes('amber') || + meta?.isSponsor const handleCopy = async (e?: React.MouseEvent) => { if (e) e.stopPropagation() @@ -37,10 +40,10 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal try { await navigator.clipboard.writeText(promo.code) setIsCopied(true) - showToast(`کد تخفیف ${promo.code} کپی شد`, 'success') + showToast(t('explorer.promo.discountCopied', { code: promo.code }), 'success') setTimeout(() => setIsCopied(false), 2500) } catch { - showToast('نتونستیم کد رو کپی کنیم', 'error') + showToast(t('explorer.promo.copyFailed'), 'error') } } @@ -58,7 +61,9 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal } } - const ctaText = promo?.code ? 'کپی کد و ورود به سایت' : 'مشاهده و ورود' + const ctaText = promo?.code + ? t('explorer.promo.copyAndEnter') + : t('explorer.promo.viewAndEnter') return (
{/* Visual / Gallery Container */} @@ -103,7 +109,7 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal
{isSponsor && ( - اسپانسر + {t('explorer.promo.sponsor')} )} @@ -137,7 +143,7 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal
- {promo.title || 'کد تخفیف اختصاصی'} + {promo.title || t('explorer.promo.exclusiveCode')} {promo.discount && ( @@ -163,7 +169,9 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal size={12} className="ml-1" /> - {isCopied ? 'کپی شد' : 'کپی'} + {isCopied + ? t('explorer.promo.copied') + : t('explorer.promo.copy')}
)} @@ -178,7 +186,7 @@ export function ExplorerPromoModal({ isOpen, onClose, item }: ExplorerPromoModal rounded="2xl" className="text-xs px-4" > - بستن + {t('explorer.promo.close')}
) : ( diff --git a/src/features/friends/components/activities.tsx b/src/features/friends/components/activities.tsx index 881ca9416..ef2ceb28f 100644 --- a/src/features/friends/components/activities.tsx +++ b/src/features/friends/components/activities.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { useGetActivities } from '@/services/friends/friend-service.hook' import { useAuth } from '@/context/auth.context' +import { t } from '@/common/i18n' import { ActivityCard } from './activity-card/activity-card' import { ManageActivityBottomSheet } from './activity-card/manage-activity-bottom-sheet' import { EmptyActivityCard } from './activity-card/empty-activity-card' @@ -42,7 +43,7 @@ export const ActiveFriendsHorizontal = () => { trigger={ setIsBottomSheetOpen(true)} reactions={ @@ -76,7 +77,7 @@ export const ActiveFriendsHorizontal = () => { trigger={ setIsBottomSheetOpen(true)} /> } diff --git a/src/features/friends/components/activity-card/activity-card.tsx b/src/features/friends/components/activity-card/activity-card.tsx index 99c5acdbb..43419105b 100644 --- a/src/features/friends/components/activity-card/activity-card.tsx +++ b/src/features/friends/components/activity-card/activity-card.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { AvatarComponent } from '@/components/ui' import { Dropdown } from '@/components/ui' import { @@ -139,7 +140,7 @@ function ActivityReactionSelector({ reactions, activityId, index }: Prop) { trigger={ @@ -122,7 +125,9 @@ export function ManageActivityBottomSheet({

- واکنش‌ها ({fetchedReactions?.reactions?.length || 0}) + {t('friends.activity.reactions', { + count: fetchedReactions?.reactions?.length || 0, + })}

{isPending ? (
@@ -167,7 +172,7 @@ export function ManageActivityBottomSheet({
) : (
- فعلا واکنشی نداری 😶‍🌫️ + {t('friends.activity.noReactions')}
)}
@@ -182,8 +187,8 @@ export function ManageActivityBottomSheet({

- متن نوشته - + {t('friends.activity.textLabel')} + @@ -220,7 +225,7 @@ export function ManageActivityBottomSheet({ onChange={(e) => setActivity(e.target.value.slice(0, MAX_ACTIVITY_LENGTH)) } - placeholder="یه چیزی بگو..." + placeholder={t('friends.activity.placeholder')} className="w-full h-16 px-4 py-2 mt-1 text-base leading-relaxed transition-ui border-none outline-none resize-none max-h-16 bg-surface-2 text-fg-muted rounded-2xl placeholder:font-light focus:placeholder:text-fg-ghost" rows={4} dir={!activity ? 'rtl' : 'auto'} @@ -254,7 +259,7 @@ export function ManageActivityBottomSheet({ rounded={'2xl'} loading={isSubmitting} > - انتشار نوشته + {t('friends.activity.publish')}

@@ -264,20 +269,18 @@ export function ManageActivityBottomSheet({ onClose={() => setShowModal(false)} showCloseButton={false} className="px-4" - title="قوانین" + title={t('friends.activity.rulesTitle')} >
-

متن توهین‌آمیز، سیاسی یا دینی ننویس.

+

{t('friends.activity.rule1')}

-

لینک یا محتوای بیرونی نذار.

+

{t('friends.activity.rule2')}

-

- اگه نوشته‌ای ازت گزارش بشه و نامناسب باشه، ممکنه حسابت محدود بشه. -

+

{t('friends.activity.rule3')}

-

کمکمون کن اینجا جای امن و دوستانه‌ای بمونه.

+

{t('friends.activity.rule4')}

-

هر روز فقط یه نوشته‌ی جدید می‌تونی بذاری.

+

{t('friends.activity.rule5')}

diff --git a/src/features/friends/components/add-friend-bottom-sheet.tsx b/src/features/friends/components/add-friend-bottom-sheet.tsx index 94f62fcd2..f9f310a95 100644 --- a/src/features/friends/components/add-friend-bottom-sheet.tsx +++ b/src/features/friends/components/add-friend-bottom-sheet.tsx @@ -7,6 +7,7 @@ import { translateError } from '@/common/utils/translate-error' import { showToast } from '@/common/toast' import { Alert, Button, Modal } from '@/components/ui' import { Icon } from '@/icons' +import { t } from '@/common/i18n' interface AddFriendBottomSheetProps { isOpen: boolean @@ -22,10 +23,7 @@ export function AddFriendBottomSheet({ isOpen, onClose }: AddFriendBottomSheetPr const handleSendRequest = () => { if (!canSendRequest) { - showToast( - 'برای فرستادن درخواست دوستی، اول توی پروفایلت یه نام کاربری انتخاب کن', - 'error' - ) + showToast(t('friends.add.setUsernameFirst'), 'error') return } if (!username.trim()) return @@ -38,7 +36,7 @@ export function AddFriendBottomSheet({ isOpen, onClose }: AddFriendBottomSheetPr onSuccess: () => { Analytics.event('friends_request_sent') setUsername('') - showToast('درخواست دوستی فرستاده شد', 'success') + showToast(t('friends.add.sent'), 'success') setTranslatedError(null) // Close the bottom sheet after successful request setTimeout(() => { @@ -70,7 +68,13 @@ export function AddFriendBottomSheet({ isOpen, onClose }: AddFriendBottomSheetPr onClose() } return ( - +
@@ -83,15 +87,12 @@ export function AddFriendBottomSheet({ isOpen, onClose }: AddFriendBottomSheetPr

- نام کاربری دوستت رو بنویس + {t('friends.add.prompt')}

{!canSendRequest && ( - - برای فرستادن درخواست دوستی، اول توی پروفایلت یه نام کاربری انتخاب - کن - + {t('friends.add.setUsernameFirstLong')} )}
@@ -100,7 +101,7 @@ export function AddFriendBottomSheet({ isOpen, onClose }: AddFriendBottomSheetPr htmlFor="friend-username" className="block text-sm font-medium text-fg-strong" > - نام کاربری + {t('friends.add.usernameLabel')} @@ -133,7 +134,7 @@ export function AddFriendBottomSheet({ isOpen, onClose }: AddFriendBottomSheetPr fullWidth className="h-12 shadow-sm shadow-success-fill-2" > - فرستادن درخواست + {t('friends.add.send')}
diff --git a/src/features/friends/components/empty-friend-list.tsx b/src/features/friends/components/empty-friend-list.tsx index eb7c347c1..47526878f 100644 --- a/src/features/friends/components/empty-friend-list.tsx +++ b/src/features/friends/components/empty-friend-list.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { Icon } from '@/icons' interface Prop { @@ -19,7 +20,7 @@ export function FriendEmptyList({ emptyMessage }: Prop) { '' )} -

هنوز چیزی اینجا نیست

+

{t('friends.empty.list')}

) } diff --git a/src/features/friends/components/friend-requests-bottom-sheet.tsx b/src/features/friends/components/friend-requests-bottom-sheet.tsx index a8b762a47..4772aa419 100644 --- a/src/features/friends/components/friend-requests-bottom-sheet.tsx +++ b/src/features/friends/components/friend-requests-bottom-sheet.tsx @@ -3,6 +3,7 @@ import { type Friend, useHandleFriendRequest, } from '@/services/friends/friend-service.hook' +import { t } from '@/common/i18n' import { RemoveFriendButton } from './remove-button' import { FriendsList } from './friends-list' @@ -25,9 +26,9 @@ export const FriendRequestsBottomSheet = ({ isOpen, onClose }: Prop) => { state: 'accepted', }) Analytics.event('friends_request_accepted') - showToast('حالا با هم دوستید', 'success') + showToast(t('friends.requests.nowFriends'), 'success') } catch { - showToast('یه مشکلی پیش اومد، دوباره امتحان کن', 'error') + showToast(t('friends.requests.error'), 'error') } } @@ -53,18 +54,20 @@ export const FriendRequestsBottomSheet = ({ isOpen, onClose }: Prop) => { color="success" > - دوست شیم + + {t('friends.requests.accept')} + rejectFriend(friend.id)} disabled={isProcessing} - label="رد کردن" + label={t('friends.requests.decline')} /> ) : ( - ارسال شده + {t('friends.requests.sent')} )}
@@ -75,13 +78,14 @@ export const FriendRequestsBottomSheet = ({ isOpen, onClose }: Prop) => { isOpen={isOpen} onClose={() => onClose()} size="lg" - title="درخواست‌های دوستی" + title={t('friends.requests.title')} closeOnBackdropClick + closeLabel={t('ui.common.close')} >
diff --git a/src/features/friends/components/friend-requests-button.tsx b/src/features/friends/components/friend-requests-button.tsx index 185b98a7d..ef85797e6 100644 --- a/src/features/friends/components/friend-requests-button.tsx +++ b/src/features/friends/components/friend-requests-button.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { FriendRequestsBottomSheet } from './friend-requests-bottom-sheet' import { Icon } from '@/icons' +import { t } from '@/common/i18n' interface Prop { size: 'small' | 'large' @@ -18,7 +19,7 @@ export function FriendRequestsButton({ size, pendingCount }: Prop) { className="flex items-center relative gap-1.5 px-2.5 py-1 text-xs font-medium transition-ui rounded-lg text-fg hover:bg-fill-2 active:scale-95 cursor-pointer" > - درخواست‌ها + {t('friends.requests.button')} {pendingCount ? (
{pendingCount} @@ -30,7 +31,7 @@ export function FriendRequestsButton({ size, pendingCount }: Prop) { type="button" onClick={() => setIsRequestsOpen(true)} className="flex relative items-center justify-center w-8 h-8 transition-ui rounded-xl bg-fill hover:bg-fill-2 active:scale-90 cursor-pointer border border-line text-fg-muted hover:text-fg-strong" - aria-label="درخواست‌های دوستی" + aria-label={t('friends.requests.title')} > - افزودن دوست + {t('friends.actions.add')} {isAddFriendOpen && ( diff --git a/src/features/friends/components/friends-direct-view.tsx b/src/features/friends/components/friends-direct-view.tsx index 526cc5a1c..a626f121d 100644 --- a/src/features/friends/components/friends-direct-view.tsx +++ b/src/features/friends/components/friends-direct-view.tsx @@ -8,6 +8,7 @@ import { import { AvatarComponent, ConfirmationModal, Spinner } from '@/components/ui' import { useInfiniteScroll } from '@/hooks/use-infinite-scroll' import { Icon } from '@/icons' +import { t } from '@/common/i18n' import { translateError } from '@/common/utils/translate-error' import { showToast } from '@/common/toast' import { useAuth } from '@/context/auth.context' @@ -66,13 +67,18 @@ export function FriendsDirectView({ onSelectFriend }: FriendsDirectViewProps) { return (
-
+
- دوستان + + {t('friends.direct.friends')} +
@@ -84,7 +90,7 @@ export function FriendsDirectView({ onSelectFriend }: FriendsDirectViewProps) { type="button" onClick={() => setIsAddFriendOpen(true)} className="flex items-center justify-center w-8 h-8 transition-ui rounded-xl bg-fill hover:bg-fill-2 active:scale-90 cursor-pointer border border-line text-fg-muted hover:text-fg-strong" - aria-label="افزودن دوست" + aria-label={t('friends.direct.add')} > @@ -118,14 +124,14 @@ export function FriendsDirectView({ onSelectFriend }: FriendsDirectViewProps) {

- هنوز دوستی نداری + {t('friends.direct.empty')}

) : ( @@ -169,7 +175,7 @@ export function FriendsDirectView({ onSelectFriend }: FriendsDirectViewProps) { setSelectedUserToDelete(friend) }} className="p-1 text-fg-faint hover:text-danger hover:bg-danger-fill rounded-lg transition-colors cursor-pointer" - aria-label="حذف دوست" + aria-label={t('friends.direct.remove')} > @@ -200,7 +206,12 @@ export function FriendsDirectView({ onSelectFriend }: FriendsDirectViewProps) { isLoading={isRemoving} onClose={() => setSelectedUserToDelete(null)} onConfirm={() => handleRemoveFriend(selectedUserToDelete?.id || null)} - message={`"${selectedUserToDelete?.user.name}" از لیست دوستات حذف بشه؟`} + title={t('ui.common.areYouSure')} + message={t('friends.direct.removeConfirm', { + name: selectedUserToDelete?.user.name ?? '', + })} + confirmText={t('ui.common.confirm')} + cancelText={t('ui.common.cancel')} />
) diff --git a/src/features/friends/components/remove-button.tsx b/src/features/friends/components/remove-button.tsx index f48d2ee7f..fcd5283ea 100644 --- a/src/features/friends/components/remove-button.tsx +++ b/src/features/friends/components/remove-button.tsx @@ -1,6 +1,7 @@ import { Button } from '@/components/ui' import type { Friend } from '@/services/friends/friend-service.hook' import { Icon } from '@/icons' +import { t } from '@/common/i18n' type Props = { friend: Friend @@ -28,7 +29,7 @@ export function RemoveFriendButton({ friend, onClick, disabled, label }: Props) " > - {label || 'حذف'} + {label || t('friends.remove')} ) } diff --git a/src/features/friends/components/select-friend.tsx b/src/features/friends/components/select-friend.tsx index fc21e1669..be3fe183b 100644 --- a/src/features/friends/components/select-friend.tsx +++ b/src/features/friends/components/select-friend.tsx @@ -1,6 +1,7 @@ import { AvatarComponent, Spinner } from '@/components/ui' import { useGetFriends, type Friend } from '@/services/friends/friend-service.hook' import { FriendEmptyList } from './empty-friend-list' +import { t } from '@/common/i18n' interface SelectFriendBottomSheetProps { onChange: (friends: Friend[]) => void @@ -45,7 +46,7 @@ export function SelectFriendLayout({
) : friends.length === 0 ? ( - + ) : (
{friends.map((friend) => { diff --git a/src/features/friends/friends.tsx b/src/features/friends/friends.tsx index 5ef09da9a..00dc61753 100644 --- a/src/features/friends/friends.tsx +++ b/src/features/friends/friends.tsx @@ -5,6 +5,7 @@ import { translateError } from '@/common/utils/translate-error' import { showToast } from '@/common/toast' import { RemoveFriendButton } from './components/remove-button' import { FriendsList } from './components/friends-list' +import { t } from '@/common/i18n' import { ConfirmationModal } from '@/components/ui' export { FriendsActions } from './components/friends-actions' @@ -45,7 +46,7 @@ export const FriendsLayout = () => { status="ACCEPTED" renderFriendActions={renderFriendActions} itemsPerPage={8} - emptyMessage="هنوز دوستی اضافه نکردی" + emptyMessage={t('friends.page.empty')} caching={true} className="px-0 pb-0 mt-0" /> @@ -55,7 +56,12 @@ export const FriendsLayout = () => { isLoading={isRemoving} onClose={() => setSelectedUser(null)} onConfirm={() => handleRemoveFriend(selectedUser?.id || null)} - message={`"${selectedUser?.user.name}" از لیست دوستات حذف بشه؟`} + title={t('ui.common.areYouSure')} + message={t('friends.direct.removeConfirm', { + name: selectedUser?.user.name ?? '', + })} + confirmText={t('ui.common.confirm')} + cancelText={t('ui.common.cancel')} /> ) diff --git a/src/features/market/components/category-view.tsx b/src/features/market/components/category-view.tsx index 4259f023c..0dfa2f7b5 100644 --- a/src/features/market/components/category-view.tsx +++ b/src/features/market/components/category-view.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { useState } from 'react' import { Button, Chip, EmptyState, TabNavigation } from '@/components/ui' import { CATEGORY_COPY } from '../constants' @@ -19,8 +20,8 @@ const OWNERSHIP_STATES: Record = { } const PET_KINDS: { id: PetKind; label: string }[] = [ - { id: 'PET', label: 'حیوون‌ها' }, - { id: 'PET_BACKGROUND', label: 'محیط‌ها' }, + { id: 'PET', label: t('market.category.petsTab') }, + { id: 'PET_BACKGROUND', label: t('market.category.environmentsTab') }, ] interface CategoryViewProps { @@ -78,20 +79,20 @@ export function CategoryView({ className="w-44 me-2" /> )} - {filterChip('all', 'همه')} - {filterChip('buyable', 'قابل خرید')} - {filterChip('mine', 'مال من')} + {filterChip('all', t('market.category.allFilter'))} + {filterChip('buyable', t('market.category.buyableFilter'))} + {filterChip('mine', t('market.category.ownedFilter'))}
{isError ? ( refetch()}> - دوباره امتحان کن + {t('market.category.retry')} } /> @@ -127,20 +128,24 @@ function EmptyCategory({ onShowAll: () => void }) { if (ownership === 'all') { - return + return } return ( - همه رو نشونم بده + {t('market.category.showAll')} } /> diff --git a/src/features/market/components/previews/font-specimen.tsx b/src/features/market/components/previews/font-specimen.tsx index cf626dd3f..e883f701c 100644 --- a/src/features/market/components/previews/font-specimen.tsx +++ b/src/features/market/components/previews/font-specimen.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { cn } from '@/common/utils/cn' interface FontSpecimenProps { @@ -13,7 +14,7 @@ export function FontSpecimen({ family, size = 'sm' }: FontSpecimenProps) { style={{ fontFamily: `"${family}", Vazir` }} > - دریاچه‌ای از آرامش + {t('market.preview.font.samplePhrase')} - آ ب پ ت ث ۱۲۳۴۵۶ + {t('market.preview.font.alphabetSample')} ) diff --git a/src/features/market/components/previews/theme-preview.tsx b/src/features/market/components/previews/theme-preview.tsx index ab6afc8bc..d5a3c469f 100644 --- a/src/features/market/components/previews/theme-preview.tsx +++ b/src/features/market/components/previews/theme-preview.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { useMemo } from 'react' import type { StoredWallpaper, Wallpaper } from '@/common/types/wallpaper.interface' import { cn } from '@/common/utils/cn' @@ -51,7 +52,7 @@ export function ThemePreview({ theme }: { theme: string }) { - ۱۲:۴۵ + {t('market.preview.theme.clockSample')} diff --git a/src/features/market/components/store-item-picker.tsx b/src/features/market/components/store-item-picker.tsx index 0030ac3ec..7152d0ec9 100644 --- a/src/features/market/components/store-item-picker.tsx +++ b/src/features/market/components/store-item-picker.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { useState } from 'react' import { cn } from '@/common/utils/cn' import { SectionPanel, Tile } from '@/components/ui' @@ -76,8 +77,11 @@ export function StoreItemPicker({ > {inStore.length > 0 - ? `${faNumber(inStore.length)} ${title} دیگه توی فروشگاه` - : 'فروشگاه'} + ? t('market.picker.moreInStore', { + p0: faNumber(inStore.length), + p1: title, + }) + : t('market.picker.storeLink')} } > @@ -98,7 +102,7 @@ export function StoreItemPicker({ ) @@ -113,7 +117,7 @@ export function StoreItemPicker({ <>
- از فروشگاه، امتحانشون مجانیه + {t('market.picker.freeTryHint')}
@@ -129,7 +133,7 @@ export function StoreItemPicker({ ) : ( - رایگان + {t('market.itemState.free')} ) } @@ -138,7 +142,7 @@ export function StoreItemPicker({ } diff --git a/src/features/market/components/store-item/coin-amount.tsx b/src/features/market/components/store-item/coin-amount.tsx index a7f760423..3db85fcfa 100644 --- a/src/features/market/components/store-item/coin-amount.tsx +++ b/src/features/market/components/store-item/coin-amount.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { ConfigKey } from '@/common/constants/config-keys' import { cn } from '@/common/utils/cn' import { faNumber } from '../../utils/store-item' @@ -26,7 +27,7 @@ export function CoinAmount({ amount, size = 'sm', className }: CoinAmountProps) {faNumber(amount)} ویج‌کوین diff --git a/src/features/market/components/store-item/item-detail-modal.tsx b/src/features/market/components/store-item/item-detail-modal.tsx index 8d2dafa81..0f8bce3dd 100644 --- a/src/features/market/components/store-item/item-detail-modal.tsx +++ b/src/features/market/components/store-item/item-detail-modal.tsx @@ -3,6 +3,7 @@ import { Modal } from '@/components/ui' import { useStoreTryOn } from '../../store-try-on.context' import type { StoreItem } from '../../types' import { ItemDetail } from './item-detail' +import { t } from '@/common/i18n' interface ItemDetailModalProps { item: StoreItem | null @@ -19,7 +20,13 @@ export function ItemDetailModal({ item, isOpen, onClose }: ItemDetailModalProps) } return ( - + {item && ( @@ -59,7 +60,7 @@ export function ItemDetail({ )}

- عوض می‌کنه: {meta.changes} + {t('market.itemDetail.affectsLabel')} {meta.changes}

diff --git a/src/features/market/components/store-item/item-state-tag.tsx b/src/features/market/components/store-item/item-state-tag.tsx index a9c3daedb..42b3cda0b 100644 --- a/src/features/market/components/store-item/item-state-tag.tsx +++ b/src/features/market/components/store-item/item-state-tag.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { cn } from '@/common/utils/cn' import { Icon } from '@/icons' import type { ItemState } from '../../types' @@ -11,7 +12,7 @@ export function ItemStateTag({ state, price }: { state: ItemState; price: number return ( - فعاله + {t('market.itemState.inUse')} ) } @@ -19,9 +20,11 @@ export function ItemStateTag({ state, price }: { state: ItemState; price: number return ( - مال توئه + {t('market.itemState.owned')} ) } - return رایگان + return ( + {t('market.itemState.free')} + ) } diff --git a/src/features/market/components/store-item/purchase-box.tsx b/src/features/market/components/store-item/purchase-box.tsx index ec09471dc..86fbc2b12 100644 --- a/src/features/market/components/store-item/purchase-box.tsx +++ b/src/features/market/components/store-item/purchase-box.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { useState } from 'react' import Analytics from '@/analytics' import { callEvent } from '@/common/utils/call-event' @@ -64,17 +65,12 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr const tryButton = item.canTryOn && ( ) if (item.type === 'PET' && !KNOWN_PETS.has(item.value)) { - return ( - - این حیوون با نسخه‌ی فعلی ویجتیفای کار نمی‌کنه. افزونه رو به‌روز کن تا بتونی - بخریش. - - ) + return {t('market.purchase.versionMismatchHint')} } if (showSuccess) { @@ -93,7 +89,7 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr return (

- الان داری ازش استفاده می‌کنی + {t('market.purchase.inUseHint')}

) } @@ -102,8 +98,9 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr if (!canUseHere) { return (

- این آیتم مال توئه. از «{ITEM_TYPE_META[item.type].whereToChange}» - انتخابش کن. + {t('market.purchase.ownedPrefix')} + {ITEM_TYPE_META[item.type].whereToChange} + {t('market.purchase.ownedSuffix')}

) } @@ -114,15 +111,15 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr fullWidth onClick={use} loading={isApplying} - loadingText="داریم عوضش می‌کنیم..." + loadingText={t('market.purchase.switching')} icon={} > - استفاده کن + {t('market.purchase.use')}

{state === 'free' - ? 'رایگانه، فقط انتخابش کن' - : 'این آیتم مال توئه و هر وقت بخوای عوضش می‌کنی'} + ? t('market.purchase.freeSelectHint') + : t('market.purchase.ownedSwitchHint')}

) @@ -140,7 +137,9 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr onClick={signIn} icon={} > - {item.price === 0 ? 'وارد شو و بگیرش' : 'وارد شو و بخر'} + {item.price === 0 + ? t('market.purchase.loginClaim') + : t('market.purchase.loginBuy')} @@ -156,9 +155,9 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr className="flex-1" onClick={purchase} loading={isBuying} - loadingText="داریم اضافه‌ش می‌کنیم..." + loadingText={t('market.purchase.claiming')} > - رایگان بگیرش + {t('market.purchase.claimFree')} ) @@ -168,9 +167,15 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr if (shortfall > 0) { return (
- - موجودیت {faNumber(coins)} ویج‌کوینه و این آیتم {faNumber(item.price)}{' '} - ویج‌کوین می‌خواد. + + {t('market.purchase.balanceLabel')} {faNumber(coins)}{' '} + {t('market.purchase.shortfallMiddle')} {faNumber(item.price)}{' '} + {t('market.purchase.shortfallSuffix')} {tryButton &&
{tryButton}
} @@ -182,13 +187,15 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr
-
قیمت
+
{t('market.purchase.priceLabel')}
-
موجودیت بعد از خرید
+
+ {t('market.purchase.balanceAfterLabel')} +
{faNumber(coins - item.price)}
@@ -201,13 +208,14 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr className="flex-1" onClick={purchase} loading={isBuying} - loadingText="داریم می‌خریم..." + loadingText={t('market.purchase.buying')} > - خرید با {faNumber(item.price)} ویج‌کوین + {t('market.purchase.buyWithPrefix')} {faNumber(item.price)}{' '} + {t('market.coin.amountLabel')}

- یه بار می‌خری، برای همیشه مال توئه + {t('market.purchase.foreverHint')}

) @@ -216,9 +224,11 @@ export function PurchaseBox({ item, onSeeAllPackages, onApplied }: PurchaseBoxPr function PriceRow({ price }: { price: number }) { return (
- قیمت + {t('market.purchase.priceLabel')} {price === 0 ? ( - رایگان + + {t('market.itemState.free')} + ) : ( )} diff --git a/src/features/market/components/store-item/purchase-success.tsx b/src/features/market/components/store-item/purchase-success.tsx index 3a52e81b5..caa734f78 100644 --- a/src/features/market/components/store-item/purchase-success.tsx +++ b/src/features/market/components/store-item/purchase-success.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { Button } from '@/components/ui' import { Icon } from '@/icons' import { ITEM_TYPE_META } from '../../constants' @@ -25,11 +26,13 @@ export function PurchaseSuccess({

- «{item.name}» مال تو شد + «{item.name} + {t('market.purchaseSuccess.ownedSuffix')}

- هر وقت خواستی از «{ITEM_TYPE_META[item.type].whereToChange}» انتخابش - کن. + {t('market.purchaseSuccess.selectPrefix')} + {ITEM_TYPE_META[item.type].whereToChange} + {t('market.purchaseSuccess.selectSuffix')}

{canUseHere ? ( @@ -39,17 +42,17 @@ export function PurchaseSuccess({ className="flex-1" onClick={onUse} loading={isApplying} - loadingText="داریم عوضش می‌کنیم..." + loadingText={t('market.purchase.switching')} > - همین الان استفاده کن + {t('market.purchaseSuccess.useNow')}
) : ( )}
diff --git a/src/features/market/components/store-item/top-up-suggestion.tsx b/src/features/market/components/store-item/top-up-suggestion.tsx index f4d354413..edeecccdb 100644 --- a/src/features/market/components/store-item/top-up-suggestion.tsx +++ b/src/features/market/components/store-item/top-up-suggestion.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { ConfigKey } from '@/common/constants/config-keys' import { Button } from '@/components/ui' import { useGetCoinPackages } from '@/services/market/market-coins.hook' @@ -18,7 +19,7 @@ export function TopUpSuggestion({ shortfall, onSeeAll }: TopUpSuggestionProps) { return (

- ارزون‌ترین بسته‌ای که کسری رو پر می‌کنه + {t('market.topUp.cheapestHint')}

@@ -26,7 +27,7 @@ export function TopUpSuggestion({ shortfall, onSeeAll }: TopUpSuggestionProps) {

- {faNumber(pkg.coin)} ویج‌کوین + {faNumber(pkg.coin)} {t('market.coin.amountLabel')}

{pkg.title}

@@ -35,9 +36,9 @@ export function TopUpSuggestion({ shortfall, onSeeAll }: TopUpSuggestionProps) { size="sm" onClick={() => checkout(pkg)} loading={payingPackageId === pkg.id} - loadingText="انتقال به درگاه..." + loadingText={t('market.topUp.redirecting')} > - {faNumber(pkg.price)} تومان + {faNumber(pkg.price)} {t('market.topUp.currencyLabel')}
) diff --git a/src/features/market/components/store-item/try-on-bar.tsx b/src/features/market/components/store-item/try-on-bar.tsx index 1af184dfa..710ecda51 100644 --- a/src/features/market/components/store-item/try-on-bar.tsx +++ b/src/features/market/components/store-item/try-on-bar.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { useEffect, useRef, useState } from 'react' import { ConfigKey } from '@/common/constants/config-keys' import { showToast } from '@/common/toast' @@ -55,7 +56,9 @@ export function TryOnBar({ item, onKept, onBack }: TryOnBarProps) { setIsApplying(false) if (!applied) return showToast( - mustBuy ? `«${item.name}» مال تو شد و فعالش کردیم` : `«${item.name}» فعال شد`, + mustBuy + ? t('market.tryOn.ownedActivatedToast', { p0: item.name }) + : t('market.tryOn.activatedToast', { p0: item.name }), 'success' ) onKept() @@ -64,7 +67,7 @@ export function TryOnBar({ item, onKept, onBack }: TryOnBarProps) { return (
@@ -73,7 +76,7 @@ export function TryOnBar({ item, onKept, onBack }: TryOnBarProps) { - داری امتحانش می‌کنی + {t('market.tryOn.tryingHint')} {item.name} @@ -86,22 +89,22 @@ export function TryOnBar({ item, onKept, onBack }: TryOnBarProps) { className="shrink-0" onClick={keep} loading={isBuying || isApplying} - loadingText="یه لحظه..." + loadingText={t('market.tryOn.loading')} > {mustBuy && item.price > 0 ? ( <> - نگهش دار + {t('market.tryOn.keep')} {faNumber(item.price)} ویج‌کوین ) : ( - 'نگهش دار' + t('market.tryOn.keep') )}
diff --git a/src/features/market/components/store-item/wallpaper-tile.tsx b/src/features/market/components/store-item/wallpaper-tile.tsx index 6d888480a..c1e4d2db6 100644 --- a/src/features/market/components/store-item/wallpaper-tile.tsx +++ b/src/features/market/components/store-item/wallpaper-tile.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { ConfigKey } from '@/common/constants/config-keys' import { cn } from '@/common/utils/cn' import { Tile } from '@/components/ui' @@ -31,7 +32,14 @@ export function WallpaperTile({ bare selected={selected} onClick={onPick} - label={locked ? `${item.name}، ${faNumber(item.price)} ویج‌کوین` : item.name} + label={ + locked + ? t('market.wallpaperTile.priceAria', { + p0: item.name, + p1: faNumber(item.price), + }) + : item.name + } media={item.wallpaper && } overlay={ <> @@ -42,7 +50,11 @@ export function WallpaperTile({ canTry && 'group-hover:opacity-0' )} > - + )} {(caption || locked) && ( @@ -64,7 +76,11 @@ export function WallpaperTile({ )} {state === 'active' && ( - + )} @@ -74,11 +90,13 @@ export function WallpaperTile({ ) } diff --git a/src/features/market/components/store-nav.tsx b/src/features/market/components/store-nav.tsx index 3d8d00517..ed6e85e14 100644 --- a/src/features/market/components/store-nav.tsx +++ b/src/features/market/components/store-nav.tsx @@ -1,3 +1,4 @@ +import { t } from '@/common/i18n' import { cn } from '@/common/utils/cn' import { Icon } from '@/icons' import { STORE_NAV } from '../constants' @@ -13,7 +14,7 @@ interface StoreNavProps { export function StoreNav({ view, hasNew, onChange }: StoreNavProps) { return (