diff --git a/src/App.tsx b/src/App.tsx index 9c50822..f04ee20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,10 +13,21 @@ import { useAppDispatch, useAppSelector } from "./app/hooks"; import { push } from "redux-first-history"; import { ErrorSnackbar } from "./components/Error/ErrorSnackbar"; import I18n from "cozy-ui/transpiled/react/providers/I18n"; + +import { + enGB, + fr as frLocale, + ru as ruLocale, + vi as viLocale, +} from "date-fns/locale"; + import en from "./locales/en.json"; import fr from "./locales/fr.json"; +import ru from "./locales/ru.json"; +import vi from "./locales/vi.json"; -const locale = { en, fr }; +const locale = { en, fr, ru, vi }; +const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale }; function App() { const error = useAppSelector((state) => state.user.error); @@ -34,6 +45,7 @@ function App() { locale[lang]} lang={lang} + locales={dateLocales} > }> @@ -45,7 +57,6 @@ function App() { - diff --git a/src/components/Menubar/Menubar.tsx b/src/components/Menubar/Menubar.tsx index 9d92849..ecf32ab 100644 --- a/src/components/Menubar/Menubar.tsx +++ b/src/components/Menubar/Menubar.tsx @@ -22,6 +22,14 @@ import { push } from "redux-first-history"; import { CalendarApi } from "@fullcalendar/core"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; import { setLanguage } from "../../features/Settings/SettingsSlice"; +import { format } from "date-fns"; +import { + enGB, + fr as frLocale, + ru as ruLocale, + vi as viLocale, +} from "date-fns/locale"; +const dateLocales = { en: enGB, fr: frLocale, ru: ruLocale, vi: viLocale }; export type AppIconProps = { name: string; @@ -46,7 +54,7 @@ export function Menubar({ currentView, onViewChange, }: MenubarProps) { - const { t, f, lang } = useI18n(); + const { t, lang } = useI18n(); // deliberately NOT using f() const user = useAppSelector((state) => state.user.userData); const applist: AppIconProps[] = (window as any).appList ?? []; const [anchorEl, setAnchorEl] = useState(null); @@ -116,9 +124,15 @@ export function Menubar({ }; const handleLangClose = () => setLangAnchorEl(null); + const dateLabel = format(currentDate, "MMMM yyyy", { + locale: dateLocales[lang as keyof typeof dateLocales] || enGB, + }); + const availableLangs = [ { code: "en", label: "English" }, { code: "fr", label: "Français" }, + { code: "ru", label: "Русский" }, + { code: "vi", label: "Tiếng Việt" }, ]; return ( @@ -146,7 +160,7 @@ export function Menubar({
- {f(currentDate, "MMMM yyyy")} + {dateLabel}
diff --git a/src/locales/ru.json b/src/locales/ru.json new file mode 100644 index 0000000..2628d6d --- /dev/null +++ b/src/locales/ru.json @@ -0,0 +1,241 @@ +{ + "locale": "ru-ru", + "calendar": { + "personal": "Личные", + "delegated": "Делегированные", + "other": "Другие", + "caldav_access": "Доступ CalDAV", + "secretUrl": "Секретный URL", + "secretUrlDesc": "Используйте этот адрес для просмотра событий без авторизации. Можно сбросить и создать новый.", + "exportCalendar": "Экспорт календаря", + "exportDesc": "Экспорт в iCalendar (.ics) для импорта в другие приложения.", + "import_file_description": "Импорт событий из файла ICS в выбранный календарь.", + "ics_feed_url": "Адрес ICS-ленты", + "import_to": "Импортирт в", + "new_calendar": "Новый календарь", + "addDescription": "Добавить описание", + "color": "Цвет", + "newEventsVisibility": "Новые события будут видны:", + "browseOtherCalendars": "Просмотреть другие календари", + "noPublicCalendarsFor": "Нет общедоступных календарей для %{name}", + "noMoreCalendarsFor": "Других календарей для %{name} нет", + "delete": { + "title": "Удалить %{name}?", + "personalWarning": "Вы потеряете все события в этом календаре.", + "sharedWarning": "Вы потеряете доступ. Позже можно добавить снова." + } + }, + "actions": { + "modify": "Изменить", + "delete": "Удалить", + "remove": "Удалить", + "save": "Сохранить", + "create": "Создать", + "import": "Импортировать", + "add": "Добавить", + "reset": "Сбросить", + "export": "Экспортировать", + "exporting": "Экспорт..." + }, + "common": { + "cancel": "Отмена", + "ok": "Ок", + "link_copied": "Ссылка скопирована", + "import_file": "Файл", + "select_file": "Выбрать файл", + "import_url": "URL", + "name": "Имя", + "description": "Описание", + "all": "Все", + "you": "Вы", + "select_timezone": "Выберите часовой пояс", + "moreOptions": "Больше параметров" + }, + "calendarPopover": { + "tabs": { + "settings": "Настройки", + "addNew": "Добавить новый календарь", + "access": "Доступ", + "import": "Импорт" + } + }, + "colorPicker": { + "selectCustom": "Выбрать цвет", + "title": "Выберите цвет", + "subtitle": "Выберите цвет фона", + "hex": "Hex" + }, + "event": { + "createEvent": "Создать событие", + "updateEvent": "Обновить событие", + "organizer": "Организатор", + "untitled": "Без названия", + "repeat": { + "repeatEvery": "Повторять каждые", + "frequency": { + "days": "день(дней)", + "weeks": "неделю(ль)", + "months": "месяц(ев)", + "years": "год(лет)" + }, + "repeatOn": "Повторять по:", + "days": { + "monday": "ПН", + "tuesday": "ВТ", + "wednesday": "СР", + "thursday": "ЧТ", + "friday": "ПТ", + "saturday": "СБ", + "sunday": "ВС" + }, + "end": { + "label": "Окончание:", + "never": "Никогда", + "after": "После", + "on": "В", + "occurrences": "повторений" + } + }, + "form": { + "title": "Название", + "titlePlaceholder": "Добавить название", + "addDescription": "Добавить описание", + "description": "Описание", + "descriptionPlaceholder": "Добавить описание", + "dateTime": "Дата и время", + "start": "Начало", + "end": "Окончание", + "allDay": "Весь день", + "repeat": "Повторять", + "timezonePlaceholder": "Выберите часовой пояс", + "participants": "Участники", + "videoMeeting": "Видеовстреча", + "meetCopied": "Ссылка на встречу скопирована", + "addVisioConference": "Добавить видеоконференцию", + "joinVisioConference": "Присоединиться к видеоконференции", + "copyMeetingLink": "Скопировать ссылку на встречу", + "removeVideoConference": "Удалить видеоконференцию", + "location": "Место", + "locationPlaceholder": "Указать место", + "calendar": "Календарь", + "notification": "Уведомление", + "notifications": { + "": "Без уведомления", + "-PT1M": "За 1 минуту", + "-PT5M": "За 5 минут", + "-PT10M": "За 10 минут", + "-PT15M": "За 15 минут", + "-PT30M": "За 30 минут", + "-PT1H": "За 1 час", + "-PT2H": "За 2 часа", + "-PT5H": "За 5 часов", + "-PT12H": "За 12 часов", + "-PT1D": "За 1 день", + "-PT2D": "За 2 дня", + "-PT1W": "За 1 неделю", + "EMAIL": "Email" + }, + "showMeAs": "Показать как", + "free": "Свободен", + "busy": "Занят", + "visibleTo": "Видно для", + "visibleAll": "Всех", + "visibleParticipants": "Участников" + }, + "validation": { + "titleRequired": "Укажите название", + "startRequired": "Укажите время начала", + "invalidStart": "Неверное дата или время начала", + "endRequired": "Укажите время окончания", + "invalidEnd": "Неверное время окончания", + "endAfterStart": "Окончание должно быть позже начала" + } + }, + "peopleSearch": { + "label": "Поиск по имени или email", + "placeholder": "Введите имя или email", + "invalidEmail": "%{email} — некорректный адрес" + }, + "error": { + "title": "Что-то пошло не так", + "unknown": "Произошла неизвестная ошибка", + "retry": "Повторить", + "multipleEvents": "%{count} событий с ошибками" + }, + "editModeDialog": { + "updateRecurrentEvent": "Обновить повторяющееся событие", + "updateParticipationStatus": "Обновить статус участия", + "thisEvent": "Это событие", + "allEvents": "Все события" + }, + "eventDuplication": { + "duplicateEvent": "Дублировать" + }, + "menubar": { + "today": "Сегодня", + "refresh": "Обновить", + "viewSelector": "Выбор вида", + "languageSelector": "Язык", + "views": { + "month": "Месяц", + "week": "Неделя", + "day": "День" + }, + "apps": "Приложения", + "userProfile": "Профиль", + "logoAlt": "Календарь" + }, + "eventPreview": { + "emailAttendees": "Написать участникам", + "deleteEvent": "Удалить", + "privateEvent": { + "tooltipOwn": "Детали видны только вам и участникам", + "hiddenDetails": "Это приватное событие. Подробности скрыты." + }, + "free": { + "label": "Свободен", + "tooltip": "Другие видят вас свободным." + }, + "attendingQuestion": "Присоединитесь?", + "accept": "Да", + "maybe": "Возможно", + "decline": "Нет", + "showMore": "Показать больше", + "showLess": "Показать меньше", + "joinVideo": "Присоединиться к видеоконференции", + "guests": "%{count} гостей", + "yesCount": "%{count} да", + "noCount": "%{count} нет", + "recurrentEvent": "Повторяющееся событие", + "freq": { + "daily": "ежедневно", + "weekly": "еженедельно", + "monthly": "ежемесячно", + "yearly": "ежегодно" + }, + "onDays": { + "MO": "понедельник", + "TU": "вторник", + "WE": "среда", + "TH": "четверг", + "FR": "пятница", + "SA": "суббота", + "SU": "воскресенье" + }, + "recurrenceOnDays": "по %{days}", + "everyInterval": "каждые %{interval} %{unit}", + "forOccurrences": "в течение %{count} повторений", + "until": "до %{date}", + "days": "дни", + "months": "месяцы", + "years": "годы", + "alarmText": "%{trigger} через %{action}" + }, + "dateTimeFields": { + "date": "Дата", + "startDate": "Дата начала", + "startTime": "Время начала", + "endDate": "Дата окончания", + "endTime": "Время окончания" + } +} diff --git a/src/locales/vi.json b/src/locales/vi.json new file mode 100644 index 0000000..f31d5fa --- /dev/null +++ b/src/locales/vi.json @@ -0,0 +1,241 @@ +{ + "locale": "vi", + "calendar": { + "personal": "Lịch cá nhân", + "delegated": "Lịch được ủy quyền", + "other": "Lịch khác", + "caldav_access": "Truy cập CalDAV", + "secretUrl": "URL bí mật", + "secretUrlDesc": "Địa chỉ bí mật có thể được dùng để xem tất cả sự kiện trong lịch ngay cả khi không có quyền truy cập. Bạn có thể đặt lại địa chỉ bí mật và nhận địa chỉ mới.", + "exportCalendar": "Xuất lịch", + "exportDesc": "Xuất lịch của bạn thành tệp định dạng iCalendar (.ics) để nhập vào các ứng dụng khác.", + "import_file_description": "Nhập sự kiện từ tệp ICS vào một trong các lịch của bạn.", + "ics_feed_url": "URL nguồn ICS", + "import_to": "Nhập vào", + "new_calendar": "Lịch mới", + "addDescription": "Thêm mô tả", + "color": "Màu", + "newEventsVisibility": "Sự kiện mới tạo sẽ hiển thị với:", + "browseOtherCalendars": "Xem các lịch khác", + "noPublicCalendarsFor": "Không có lịch công khai cho %{name}", + "noMoreCalendarsFor": "Không còn lịch nào cho %{name}", + "delete": { + "title": "Xóa %{name}?", + "personalWarning": "Bạn sẽ mất tất cả sự kiện trong lịch này.", + "sharedWarning": "Bạn sẽ mất quyền truy cập các sự kiện của lịch. Bạn vẫn có thể thêm lại sau." + } + }, + "actions": { + "modify": "Chỉnh sửa", + "delete": "Xóa", + "remove": "Gỡ bỏ", + "save": "Lưu", + "create": "Tạo", + "import": "Nhập", + "add": "Thêm", + "reset": "Đặt lại", + "export": "Xuất", + "exporting": "Đang xuất..." + }, + "common": { + "cancel": "Hủy", + "ok": "OK", + "link_copied": "Đã sao chép liên kết!", + "import_file": "Tệp", + "select_file": "Chọn tệp", + "import_url": "URL", + "name": "Tên", + "description": "Mô tả", + "all": "Tất cả", + "you": "Bạn", + "select_timezone": "Chọn múi giờ", + "moreOptions": "Thêm tùy chọn" + }, + "calendarPopover": { + "tabs": { + "settings": "Cài đặt", + "addNew": "Thêm lịch mới", + "access": "Quyền truy cập", + "import": "Nhập" + } + }, + "colorPicker": { + "selectCustom": "Chọn màu tùy chỉnh", + "title": "Chọn màu tùy chỉnh", + "subtitle": "Chọn màu nền cho lịch này", + "hex": "Hex" + }, + "event": { + "createEvent": "Tạo sự kiện", + "updateEvent": "Cập nhật sự kiện", + "organizer": "Người tổ chức", + "untitled": "Không có tiêu đề", + "repeat": { + "repeatEvery": "Lặp lại mỗi", + "frequency": { + "days": "Ngày", + "weeks": "Tuần", + "months": "Tháng", + "years": "Năm" + }, + "repeatOn": "Lặp vào:", + "days": { + "monday": "Th 2", + "tuesday": "Th 3", + "wednesday": "Th 4", + "thursday": "Th 5", + "friday": "Th 6", + "saturday": "Th 7", + "sunday": "CN" + }, + "end": { + "label": "Kết thúc:", + "never": "Không bao giờ", + "after": "Sau", + "on": "Vào ngày", + "occurrences": "lần" + } + }, + "form": { + "title": "Tiêu đề", + "titlePlaceholder": "Thêm tiêu đề", + "addDescription": "Thêm mô tả", + "description": "Mô tả", + "descriptionPlaceholder": "Thêm mô tả", + "dateTime": "Ngày & Giờ", + "start": "Bắt đầu", + "end": "Kết thúc", + "allDay": "Cả ngày", + "repeat": "Lặp lại", + "timezonePlaceholder": "Chọn múi giờ", + "participants": "Người tham gia", + "videoMeeting": "Cuộc họp video", + "meetCopied": "Đã sao chép liên kết họp!", + "addVisioConference": "Thêm cuộc họp trực tuyến", + "joinVisioConference": "Tham gia cuộc họp", + "copyMeetingLink": "Sao chép liên kết họp", + "removeVideoConference": "Xóa cuộc họp", + "location": "Địa điểm", + "locationPlaceholder": "Thêm địa điểm", + "calendar": "Lịch", + "notification": "Thông báo", + "notifications": { + "": "Không thông báo", + "-PT1M": "1 phút trước", + "-PT5M": "5 phút trước", + "-PT10M": "10 phút trước", + "-PT15M": "15 phút trước", + "-PT30M": "30 phút trước", + "-PT1H": "1 giờ trước", + "-PT2H": "2 giờ trước", + "-PT5H": "5 giờ trước", + "-PT12H": "12 giờ trước", + "-PT1D": "1 ngày trước", + "-PT2D": "2 ngày trước", + "-PT1W": "1 tuần trước", + "EMAIL": "email" + }, + "showMeAs": "Hiển thị tôi là", + "free": "Rảnh", + "busy": "Bận", + "visibleTo": "Hiển thị với", + "visibleAll": "Tất cả", + "visibleParticipants": "Người tham gia" + }, + "validation": { + "titleRequired": "Tiêu đề là bắt buộc", + "startRequired": "Ngày/giờ bắt đầu là bắt buộc", + "invalidStart": "Ngày/giờ bắt đầu không hợp lệ", + "endRequired": "Ngày/giờ kết thúc là bắt buộc", + "invalidEnd": "Ngày/giờ kết thúc không hợp lệ", + "endAfterStart": "Thời gian kết thúc phải sau thời gian bắt đầu" + } + }, + "peopleSearch": { + "label": "Nhập tên hoặc email", + "placeholder": "Nhập tên hoặc email", + "invalidEmail": "%{email} không phải địa chỉ email hợp lệ" + }, + "error": { + "title": "Đã xảy ra lỗi", + "unknown": "Có lỗi không xác định", + "retry": "Thử lại", + "multipleEvents": "%{count} sự kiện gặp lỗi" + }, + "editModeDialog": { + "updateRecurrentEvent": "Cập nhật sự kiện lặp lại", + "updateParticipationStatus": "Cập nhật trạng thái tham gia", + "thisEvent": "Sự kiện này", + "allEvents": "Tất cả sự kiện" + }, + "eventDuplication": { + "duplicateEvent": "Nhân bản sự kiện" + }, + "menubar": { + "today": "Hôm nay", + "refresh": "Làm mới", + "viewSelector": "Chọn chế độ xem", + "languageSelector": "Chọn ngôn ngữ", + "views": { + "month": "Tháng", + "week": "Tuần", + "day": "Ngày" + }, + "apps": "Ứng dụng", + "userProfile": "Hồ sơ người dùng", + "logoAlt": "Lịch" + }, + "eventPreview": { + "emailAttendees": "Gửi email cho người tham gia", + "deleteEvent": "Xóa sự kiện", + "privateEvent": { + "tooltipOwn": "Chỉ bạn và người tham gia có thể xem chi tiết của sự kiện này.", + "hiddenDetails": "Đây là sự kiện riêng tư. Chi tiết bị ẩn." + }, + "free": { + "label": "Rảnh", + "tooltip": "Người khác sẽ thấy bạn rảnh trong khoảng thời gian này." + }, + "attendingQuestion": "Tham gia?", + "accept": "Chấp nhận", + "maybe": "Có thể", + "decline": "Từ chối", + "showMore": "Xem thêm", + "showLess": "Thu gọn", + "joinVideo": "Tham gia cuộc họp video", + "guests": "%{count} khách", + "yesCount": "%{count} đồng ý", + "noCount": "%{count} từ chối", + "recurrentEvent": "Sự kiện lặp lại", + "freq": { + "daily": "hàng ngày", + "weekly": "hàng tuần", + "monthly": "hàng tháng", + "yearly": "hàng năm" + }, + "onDays": { + "MO": "thứ hai", + "TU": "thứ ba", + "WE": "thứ tư", + "TH": "thứ năm", + "FR": "thứ sáu", + "SA": "thứ bảy", + "SU": "chủ nhật" + }, + "recurrenceOnDays": "vào %{days}", + "everyInterval": "mỗi %{interval} %{unit}", + "forOccurrences": "trong %{count} lần", + "until": "đến %{date}", + "days": "ngày", + "months": "tháng", + "years": "năm", + "alarmText": "%{trigger} bằng %{action}" + }, + "dateTimeFields": { + "date": "Ngày", + "startDate": "Ngày bắt đầu", + "startTime": "Giờ bắt đầu", + "endDate": "Ngày kết thúc", + "endTime": "Giờ kết thúc" + } +}