From 607ca5a6c1724801f113f202e345f46196e561b0 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Sun, 15 Dec 2024 10:51:50 +0530 Subject: [PATCH 01/20] fix: adding language support package --- packages/i18n/.eslintignore | 3 ++ packages/i18n/.eslintrc.js | 9 +++++ packages/i18n/.prettierignore | 4 ++ packages/i18n/.prettierrc | 5 +++ packages/i18n/package.json | 20 ++++++++++ packages/i18n/src/config/index.ts | 16 ++++++++ packages/i18n/src/hooks/index.ts | 1 + packages/i18n/src/hooks/use-translation.ts | 39 +++++++++++++++++++ packages/i18n/src/index.ts | 2 + .../i18n/src/locales/en/translations.json | 35 +++++++++++++++++ .../i18n/src/locales/fr/translations.json | 38 ++++++++++++++++++ packages/i18n/tsconfig.json | 10 +++++ web/app/profile/page.tsx | 22 +++++++---- web/next.config.js | 1 + web/package.json | 1 + 15 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 packages/i18n/.eslintignore create mode 100644 packages/i18n/.eslintrc.js create mode 100644 packages/i18n/.prettierignore create mode 100644 packages/i18n/.prettierrc create mode 100644 packages/i18n/package.json create mode 100644 packages/i18n/src/config/index.ts create mode 100644 packages/i18n/src/hooks/index.ts create mode 100644 packages/i18n/src/hooks/use-translation.ts create mode 100644 packages/i18n/src/index.ts create mode 100644 packages/i18n/src/locales/en/translations.json create mode 100644 packages/i18n/src/locales/fr/translations.json create mode 100644 packages/i18n/tsconfig.json diff --git a/packages/i18n/.eslintignore b/packages/i18n/.eslintignore new file mode 100644 index 00000000000..6019047c3e5 --- /dev/null +++ b/packages/i18n/.eslintignore @@ -0,0 +1,3 @@ +build/* +dist/* +out/* \ No newline at end of file diff --git a/packages/i18n/.eslintrc.js b/packages/i18n/.eslintrc.js new file mode 100644 index 00000000000..558b8f76ed4 --- /dev/null +++ b/packages/i18n/.eslintrc.js @@ -0,0 +1,9 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@plane/eslint-config/library.js"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, +}; diff --git a/packages/i18n/.prettierignore b/packages/i18n/.prettierignore new file mode 100644 index 00000000000..d5be669c5e0 --- /dev/null +++ b/packages/i18n/.prettierignore @@ -0,0 +1,4 @@ +.turbo +out/ +dist/ +build/ \ No newline at end of file diff --git a/packages/i18n/.prettierrc b/packages/i18n/.prettierrc new file mode 100644 index 00000000000..87d988f1b26 --- /dev/null +++ b/packages/i18n/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/packages/i18n/package.json b/packages/i18n/package.json new file mode 100644 index 00000000000..0a4d0562797 --- /dev/null +++ b/packages/i18n/package.json @@ -0,0 +1,20 @@ +{ + "name": "@plane/i18n", + "version": "0.24.1", + "description": "I18n shared across multiple apps internally", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "lint": "eslint src --ext .ts,.tsx", + "lint:errors": "eslint src --ext .ts,.tsx --quiet" + }, + "dependencies": { + "@plane/utils": "*" + }, + "devDependencies": { + "@plane/eslint-config": "*", + "@types/node": "^22.5.4", + "typescript": "^5.3.3" + } +} diff --git a/packages/i18n/src/config/index.ts b/packages/i18n/src/config/index.ts new file mode 100644 index 00000000000..edd62071bd6 --- /dev/null +++ b/packages/i18n/src/config/index.ts @@ -0,0 +1,16 @@ +import en from "../locales/en/translations.json"; +import fr from "../locales/fr/translations.json"; + +export type Language = (typeof languages)[number]; +export type Translations = { + [key: string]: { + [key: string]: string; + }; +}; + +export const fallbackLng = "en"; +export const languages = ["en", "fr"] as const; +export const translations: Translations = { + en, + fr, +}; diff --git a/packages/i18n/src/hooks/index.ts b/packages/i18n/src/hooks/index.ts new file mode 100644 index 00000000000..fb4e297e216 --- /dev/null +++ b/packages/i18n/src/hooks/index.ts @@ -0,0 +1 @@ +export * from "./use-translation"; diff --git a/packages/i18n/src/hooks/use-translation.ts b/packages/i18n/src/hooks/use-translation.ts new file mode 100644 index 00000000000..6400b01c21f --- /dev/null +++ b/packages/i18n/src/hooks/use-translation.ts @@ -0,0 +1,39 @@ +import { useState, useEffect } from "react"; +import { translations, Language, fallbackLng, languages } from "../config"; + +export function useTranslation() { + const [currentLocale, setCurrentLocale] = useState(fallbackLng); + + useEffect(() => { + // Try to get language from localStorage + const savedLocale = localStorage.getItem("userLanguage") as Language; + if (savedLocale && languages.includes(savedLocale)) { + setCurrentLocale(savedLocale); + } else { + // Get browser language + const browserLang = navigator.language.split("-")[0] as Language; + const newLocale = languages.includes(browserLang) ? browserLang : fallbackLng; + localStorage.setItem("userLanguage", newLocale); + setCurrentLocale(newLocale); + } + }, []); + + const t = (key: string) => { + console.log("key", key); + const translatedValue = translations[currentLocale]?.[key] || translations[fallbackLng][key] || key; + console.log("translatedValue", translatedValue); + return translatedValue; + }; + + const changeLanguage = (lng: Language) => { + localStorage.setItem("userLanguage", lng); + setCurrentLocale(lng); + }; + + return { + t, + currentLocale, + changeLanguage, + languages, + }; +} diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts new file mode 100644 index 00000000000..b784e73e91a --- /dev/null +++ b/packages/i18n/src/index.ts @@ -0,0 +1,2 @@ +export * from "./config"; +export * from "./hooks"; diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json new file mode 100644 index 00000000000..df0640ea306 --- /dev/null +++ b/packages/i18n/src/locales/en/translations.json @@ -0,0 +1,35 @@ +{ + "submit": "Submit", + "cancel": "Cancel", + "loading": "Loading", + "error": "Error", + "success": "Success", + "warning": "Warning", + "info": "Info", + "close": "Close", + "yes": "Yes", + "no": "No", + "ok": "OK", + "name": "Name", + "description": "Description", + "search": "Search", + "add_member": "Add member", + "remove_member": "Remove member", + "add_members": "Add members", + "remove_members": "Remove members", + "add": "Add", + "remove": "Remove", + "add_new": "Add new", + "remove_selected": "Remove selected", + "first_name": "First name", + "last_name": "Last name", + "email": "Email", + "display_name": "Display name", + "role": "Role", + "timezone": "Timezone", + "avatar": "Avatar", + "cover_image": "Cover image", + "password": "Password", + "change_cover": "Change cover", + "language": "Language" +} diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json new file mode 100644 index 00000000000..73624ff72ee --- /dev/null +++ b/packages/i18n/src/locales/fr/translations.json @@ -0,0 +1,38 @@ +{ + "submit": "Soumettre", + "cancel": "Annuler", + "loading": "Chargement", + "error": "Erreur", + "success": "Succès", + "warning": "Avertissement", + "info": "Info", + "close": "Fermer", + "yes": "Oui", + "no": "Non", + "ok": "OK", + "name": "Nom", + "description": "Description", + "search": "Rechercher", + "add_member": "Ajouter un membre", + "remove_member": "Supprimer le membre", + "add_members": "Ajouter des membres", + "remove_members": "Supprimer des membres", + "add": "Ajouter", + "remove": "Supprimer", + "add_new": "Ajouter un nouveau", + "remove_selected": "Supprimer les sélectionnés", + "first_name": "Prénom", + "last_name": "Nom", + "email": "Email", + "display_name": "Nom d'affichage", + "role": "Rôle", + "timezone": "Fuseau horaire", + "avatar": "Avatar", + "member": "Membre", + "members": "Membres", + "invite": "Inviter", + "invite_members": "Inviter des membres", + "invite_new_members": "Inviter des nouveaux membres", + "change_cover": "Changer la couverture", + "language": "Langue" +} diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json new file mode 100644 index 00000000000..6599e6e82aa --- /dev/null +++ b/packages/i18n/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@plane/typescript-config/react-library.json", + "compilerOptions": { + "jsx": "react", + "lib": ["esnext", "dom"], + "resolveJsonModule": true + }, + "include": ["./src"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/web/app/profile/page.tsx b/web/app/profile/page.tsx index 1dd9702a36b..c8a5a8cfc55 100644 --- a/web/app/profile/page.tsx +++ b/web/app/profile/page.tsx @@ -5,6 +5,7 @@ import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { ChevronDown, CircleUserRound } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; +import { useTranslation } from "@plane/i18n"; import type { IUser } from "@plane/types"; import { Button, @@ -45,6 +46,8 @@ const ProfileSettingsPage = observer(() => { const [isLoading, setIsLoading] = useState(false); const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); const [deactivateAccountModal, setDeactivateAccountModal] = useState(false); + // language support + const { t } = useTranslation(); // form info const { handleSubmit, @@ -224,7 +227,8 @@ const ProfileSettingsPage = observer(() => {

- First name* + {t("first_name")} + *

{ {errors.first_name && {errors.first_name.message}}
-

Last name

+

{t("last_name")}

{

- Display name* + {t("display_name")} + *

{

- Email* + {t("email")} + *

{

- Role* + {t("role")} + *

{

- Timezone* + {t("timezone")} + *

{
-

Language

+

{t("language")}

{ // if (!isServer) { // // Ensure that all imports of 'yjs' resolve to the same instance diff --git a/web/package.json b/web/package.json index cd0a5ddcca5..d37ae8aaf97 100644 --- a/web/package.json +++ b/web/package.json @@ -31,6 +31,7 @@ "@plane/constants": "*", "@plane/editor": "*", "@plane/hooks": "*", + "@plane/i18n": "*", "@plane/types": "*", "@plane/ui": "*", "@plane/utils": "*", From 2dab6c0078143d2b3062b4456030d4cb36c9e979 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 16 Dec 2024 17:56:15 +0530 Subject: [PATCH 02/20] fix: language support implementation using mobx --- packages/i18n/src/components/index.tsx | 29 ++ packages/i18n/src/components/store.ts | 38 ++ packages/i18n/src/config/index.ts | 11 + packages/i18n/src/hooks/use-translation.ts | 44 +- packages/i18n/src/index.ts | 1 + .../i18n/src/locales/en/translations.json | 4 +- .../i18n/src/locales/fr/translations.json | 4 +- packages/types/src/users.d.ts | 2 +- web/app/profile/page.tsx | 445 +--------------- web/app/provider.tsx | 23 +- web/core/components/profile/form.tsx | 493 ++++++++++++++++++ web/core/components/profile/index.ts | 5 +- web/core/lib/wrappers/store-wrapper.tsx | 11 +- 13 files changed, 618 insertions(+), 492 deletions(-) create mode 100644 packages/i18n/src/components/index.tsx create mode 100644 packages/i18n/src/components/store.ts create mode 100644 web/core/components/profile/form.tsx diff --git a/packages/i18n/src/components/index.tsx b/packages/i18n/src/components/index.tsx new file mode 100644 index 00000000000..e94246d081a --- /dev/null +++ b/packages/i18n/src/components/index.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useEffect } from "react"; +import { observer } from "mobx-react"; +import { TranslationStore } from "./store"; +import { Language, languages } from "../config"; + +// Create the store instance +const translationStore = new TranslationStore(); + +// Create Context +export const TranslationContext = createContext(translationStore); + +export const TranslationProvider = observer(({ children }: any) => { + // Handle storage events for cross-tab synchronization + useEffect(() => { + const handleStorageChange = (event: StorageEvent) => { + if (event.key === "userLanguage" && event.newValue) { + const newLang = event.newValue as Language; + if (languages.includes(newLang)) { + translationStore.setLanguage(newLang); + } + } + }; + + window.addEventListener("storage", handleStorageChange); + return () => window.removeEventListener("storage", handleStorageChange); + }, []); + + return {children}; +}); diff --git a/packages/i18n/src/components/store.ts b/packages/i18n/src/components/store.ts new file mode 100644 index 00000000000..bf1934a6535 --- /dev/null +++ b/packages/i18n/src/components/store.ts @@ -0,0 +1,38 @@ +import { makeObservable, observable } from "mobx"; +import { Language, fallbackLng, languages, translations } from "../config"; + +export class TranslationStore { + currentLocale: Language = fallbackLng; + + constructor() { + makeObservable(this, { + currentLocale: observable.ref, + }); + this.initializeLanguage(); + } + + get availableLanguages() { + return languages; + } + + t(key: string) { + return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key; + } + + setLanguage(lng: Language) { + localStorage.setItem("userLanguage", lng); + this.currentLocale = lng; + } + + initializeLanguage() { + if (typeof window === "undefined") return; + const savedLocale = localStorage.getItem("userLanguage") as Language; + if (savedLocale && languages.includes(savedLocale)) { + this.setLanguage(savedLocale); + } else { + const browserLang = navigator.language.split("-")[0] as Language; + const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng; + this.setLanguage(newLocale); + } + } +} diff --git a/packages/i18n/src/config/index.ts b/packages/i18n/src/config/index.ts index edd62071bd6..a789825ec91 100644 --- a/packages/i18n/src/config/index.ts +++ b/packages/i18n/src/config/index.ts @@ -14,3 +14,14 @@ export const translations: Translations = { en, fr, }; + +export const SUPPORTED_LANGUAGES = [ + { + label: "English", + value: "en", + }, + { + label: "French", + value: "fr", + }, +]; diff --git a/packages/i18n/src/hooks/use-translation.ts b/packages/i18n/src/hooks/use-translation.ts index 6400b01c21f..f947d1d5eb5 100644 --- a/packages/i18n/src/hooks/use-translation.ts +++ b/packages/i18n/src/hooks/use-translation.ts @@ -1,39 +1,17 @@ -import { useState, useEffect } from "react"; -import { translations, Language, fallbackLng, languages } from "../config"; +import { useContext } from "react"; +import { TranslationContext } from "../components"; +import { Language } from "../config"; export function useTranslation() { - const [currentLocale, setCurrentLocale] = useState(fallbackLng); - - useEffect(() => { - // Try to get language from localStorage - const savedLocale = localStorage.getItem("userLanguage") as Language; - if (savedLocale && languages.includes(savedLocale)) { - setCurrentLocale(savedLocale); - } else { - // Get browser language - const browserLang = navigator.language.split("-")[0] as Language; - const newLocale = languages.includes(browserLang) ? browserLang : fallbackLng; - localStorage.setItem("userLanguage", newLocale); - setCurrentLocale(newLocale); - } - }, []); - - const t = (key: string) => { - console.log("key", key); - const translatedValue = translations[currentLocale]?.[key] || translations[fallbackLng][key] || key; - console.log("translatedValue", translatedValue); - return translatedValue; - }; - - const changeLanguage = (lng: Language) => { - localStorage.setItem("userLanguage", lng); - setCurrentLocale(lng); - }; + const store = useContext(TranslationContext); + if (!store) { + throw new Error("useTranslation must be used within a TranslationProvider"); + } return { - t, - currentLocale, - changeLanguage, - languages, + t: (key: string) => store.t(key), + currentLocale: store.currentLocale, + changeLanguage: (lng: Language) => store.setLanguage(lng), + languages: store.availableLanguages, }; } diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts index b784e73e91a..639ef4b59a4 100644 --- a/packages/i18n/src/index.ts +++ b/packages/i18n/src/index.ts @@ -1,2 +1,3 @@ export * from "./config"; +export * from "./components"; export * from "./hooks"; diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index df0640ea306..7eec69af328 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -31,5 +31,7 @@ "cover_image": "Cover image", "password": "Password", "change_cover": "Change cover", - "language": "Language" + "language": "Language", + "saving": "Saving...", + "save_changes": "Save changes" } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 73624ff72ee..683a429958a 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -34,5 +34,7 @@ "invite_members": "Inviter des membres", "invite_new_members": "Inviter des nouveaux membres", "change_cover": "Changer la couverture", - "language": "Langue" + "language": "Langue", + "saving": "Enregistrement...", + "save_changes": "Enregistrer les modifications" } diff --git a/packages/types/src/users.d.ts b/packages/types/src/users.d.ts index 452bc23c238..c562e7c246b 100644 --- a/packages/types/src/users.d.ts +++ b/packages/types/src/users.d.ts @@ -25,7 +25,6 @@ export interface IUser extends IUserLite { is_password_autoset: boolean; is_tour_completed: boolean; mobile_number: string | null; - role: string | null; last_workspace_id: string; user_timezone: string; username: string; @@ -62,6 +61,7 @@ export type TUserProfile = { billing_address_country: string | undefined; billing_address: string | undefined; has_billing_address: boolean; + language: string; created_at: Date | string; updated_at: Date | string; }; diff --git a/web/app/profile/page.tsx b/web/app/profile/page.tsx index c8a5a8cfc55..1a4470ea331 100644 --- a/web/app/profile/page.tsx +++ b/web/app/profile/page.tsx @@ -1,143 +1,16 @@ "use client"; -import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; -import { Controller, useForm } from "react-hook-form"; -import { ChevronDown, CircleUserRound } from "lucide-react"; -import { Disclosure, Transition } from "@headlessui/react"; -import { useTranslation } from "@plane/i18n"; -import type { IUser } from "@plane/types"; -import { - Button, - CustomSelect, - CustomSearchSelect, - Input, - TOAST_TYPE, - setPromiseToast, - setToast, - Tooltip, -} from "@plane/ui"; // components -import { DeactivateAccountModal } from "@/components/account"; import { LogoSpinner } from "@/components/common"; -import { ImagePickerPopover, UserImageUploadModal, PageHead } from "@/components/core"; -import { ProfileSettingContentWrapper } from "@/components/profile"; -// constants -import { TIME_ZONES, TTimezone } from "@/constants/timezones"; -import { USER_ROLES } from "@/constants/workspace"; -// helpers -import { getFileURL } from "@/helpers/file.helper"; +import { PageHead } from "@/components/core"; +import { ProfileSettingContentWrapper, ProfileForm } from "@/components/profile"; // hooks import { useUser } from "@/hooks/store"; -const defaultValues: Partial = { - avatar_url: "", - cover_image_url: "", - first_name: "", - last_name: "", - display_name: "", - email: "", - role: "Product / Project Manager", - user_timezone: "Asia/Kolkata", -}; - const ProfileSettingsPage = observer(() => { - // states - const [isLoading, setIsLoading] = useState(false); - const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); - const [deactivateAccountModal, setDeactivateAccountModal] = useState(false); - // language support - const { t } = useTranslation(); - // form info - const { - handleSubmit, - reset, - watch, - control, - setValue, - formState: { errors }, - } = useForm({ defaultValues }); - // derived values - const userAvatar = watch("avatar_url"); - const userCover = watch("cover_image_url"); // store hooks - const { data: currentUser, updateCurrentUser } = useUser(); - - useEffect(() => { - reset({ ...defaultValues, ...currentUser }); - }, [currentUser, reset]); - - const onSubmit = async (formData: IUser) => { - setIsLoading(true); - const payload: Partial = { - first_name: formData.first_name, - last_name: formData.last_name, - avatar_url: formData.avatar_url, - role: formData.role, - display_name: formData?.display_name, - user_timezone: formData.user_timezone, - }; - // if unsplash or a pre-defined image is uploaded, delete the old uploaded asset - if (formData.cover_image_url?.startsWith("http")) { - payload.cover_image = formData.cover_image_url; - payload.cover_image_asset = null; - } - - const updateCurrentUserDetail = updateCurrentUser(payload).finally(() => setIsLoading(false)); - setPromiseToast(updateCurrentUserDetail, { - loading: "Updating...", - success: { - title: "Success!", - message: () => `Profile updated successfully.`, - }, - error: { - title: "Error!", - message: () => `There was some error in updating your profile. Please try again.`, - }, - }); - }; - - const handleDelete = async (url: string | null | undefined) => { - if (!url) return; - - await updateCurrentUser({ - avatar_url: "", - }) - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Profile picture deleted successfully.", - }); - setValue("avatar_url", ""); - }) - .catch(() => { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "There was some error in deleting your profile picture. Please try again.", - }); - }) - .finally(() => { - setIsImageUploadModalOpen(false); - }); - }; - - const getTimeZoneLabel = (timezone: TTimezone | undefined) => { - if (!timezone) return undefined; - return ( -
- {timezone.gmtOffset} - {timezone.name} -
- ); - }; - - const timeZoneOptions = TIME_ZONES.map((timeZone) => ({ - value: timeZone.value, - query: timeZone.name + " " + timeZone.gmtOffset + " " + timeZone.value, - content: getTimeZoneLabel(timeZone), - })); + const { data: currentUser, userProfile } = useUser(); if (!currentUser) return ( @@ -150,317 +23,7 @@ const ProfileSettingsPage = observer(() => { <> - ( - setIsImageUploadModalOpen(false)} - handleRemove={async () => await handleDelete(currentUser?.avatar_url)} - onSuccess={(url) => { - onChange(url); - handleSubmit(onSubmit)(); - setIsImageUploadModalOpen(false); - }} - value={value && value.trim() !== "" ? value : null} - /> - )} - /> - setDeactivateAccountModal(false)} /> -
-
-
- {currentUser?.first_name -
-
-
- -
-
-
-
- ( - onChange(imageUrl)} - control={control} - value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"} - isProfileCover - /> - )} - /> -
-
-
-
-
- {`${watch("first_name")} ${watch("last_name")}`} -
- {watch("email")} -
-
-
-
-
-

- {t("first_name")} - * -

- ( - - )} - /> - {errors.first_name && {errors.first_name.message}} -
-
-

{t("last_name")}

- ( - - )} - /> -
-
-

- {t("display_name")} - * -

- { - if (value.trim().length < 1) return "Display name can't be empty."; - if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces."; - if (value.replace(/\s/g, "").length < 1) - return "Display name must be at least 1 character long."; - if (value.replace(/\s/g, "").length > 20) - return "Display name must be less than 20 characters long."; - return true; - }, - }} - render={({ field: { value, onChange, ref } }) => ( - - )} - /> - {errors?.display_name && ( - {errors?.display_name?.message} - )} -
-
-

- {t("email")} - * -

- ( - - )} - /> -
-
-

- {t("role")} - * -

- ( - - {USER_ROLES.map((item) => ( - - {item.label} - - ))} - - )} - /> - {errors.role && Please select a role} -
-
-
-
-
-
-

- {t("timezone")} - * -

- ( - t.value === value)) ?? value) - : "Select a timezone" - } - options={timeZoneOptions} - onChange={onChange} - buttonClassName={errors.user_timezone ? "border-red-500" : ""} - className="rounded-md border-[0.5px] !border-custom-border-200" - optionsClassName="w-72" - input - /> - )} - /> - {errors.user_timezone && {errors.user_timezone.message}} -
- -
-

{t("language")}

- {}} - className="rounded-md bg-custom-background-90" - input - disabled - /> -
-
-
-
- -
-
-
-
- - {({ open }) => ( - <> - - Deactivate account - - - - -
- - When deactivating an account, all of the data and resources within that account will be - permanently removed and cannot be recovered. - -
- -
-
-
-
- - )} -
+
); diff --git a/web/app/provider.tsx b/web/app/provider.tsx index dba975a6373..34526ffd14c 100644 --- a/web/app/provider.tsx +++ b/web/app/provider.tsx @@ -4,7 +4,8 @@ import { FC, ReactNode } from "react"; import dynamic from "next/dynamic"; import { useTheme, ThemeProvider } from "next-themes"; import { SWRConfig } from "swr"; -// ui +// Plane Imports +import { TranslationProvider } from "@plane/i18n"; import { Toast } from "@plane/ui"; // constants import { SWR_CONFIG } from "@/constants/swr-config"; @@ -41,15 +42,17 @@ export const AppProvider: FC = (props) => { - - - - - {children} - - - - + + + + + + {children} + + + + + diff --git a/web/core/components/profile/form.tsx b/web/core/components/profile/form.tsx new file mode 100644 index 00000000000..5b3fbcaf05e --- /dev/null +++ b/web/core/components/profile/form.tsx @@ -0,0 +1,493 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +import { Controller, useForm } from "react-hook-form"; +import { ChevronDown, CircleUserRound } from "lucide-react"; +import { Disclosure, Transition } from "@headlessui/react"; +import { useTranslation, SUPPORTED_LANGUAGES } from "@plane/i18n"; +import type { IUser, TUserProfile } from "@plane/types"; +import { + Button, + CustomSelect, + CustomSearchSelect, + Input, + TOAST_TYPE, + setPromiseToast, + setToast, + Tooltip, +} from "@plane/ui"; +// components +import { DeactivateAccountModal } from "@/components/account"; +import { ImagePickerPopover, UserImageUploadModal } from "@/components/core"; +// constants +import { TIME_ZONES, TTimezone } from "@/constants/timezones"; +import { USER_ROLES } from "@/constants/workspace"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useUser, useUserProfile } from "@/hooks/store"; + +type TUserProfileForm = { + avatar_url: string; + cover_image: string; + cover_image_asset: any; + cover_image_url: string; + first_name: string; + last_name: string; + display_name: string; + email: string; + role: string; + language: string; + user_timezone: string; +}; + +export type TProfileFormProps = { + user: IUser; + profile: TUserProfile; +}; + +export const ProfileForm = observer((props: TProfileFormProps) => { + const { user, profile } = props; + // states + const [isLoading, setIsLoading] = useState(false); + const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false); + const [deactivateAccountModal, setDeactivateAccountModal] = useState(false); + // language support + const { t } = useTranslation(); + // form info + const { + handleSubmit, + watch, + control, + setValue, + formState: { errors }, + } = useForm({ + defaultValues: { + avatar_url: user.avatar_url || "", + cover_image_asset: null, + cover_image_url: user.cover_image_url || "", + first_name: user.first_name || "", + last_name: user.last_name || "", + display_name: user.display_name || "", + email: user.email || "", + role: profile.role || "Product / Project Manager", + language: profile.language || "en", + user_timezone: "Asia/Kolkata", + }, + }); + // derived values + const userAvatar = watch("avatar_url"); + const userCover = watch("cover_image_url"); + // store hooks + const { data: currentUser, updateCurrentUser } = useUser(); + const { updateUserProfile } = useUserProfile(); + + const getLanguageLabel = (value: string) => { + const selectedLanguage = SUPPORTED_LANGUAGES.find((l) => l.value === value); + if (!selectedLanguage) return value; + return selectedLanguage.label; + }; + + const getTimeZoneLabel = (timezone: TTimezone | undefined) => { + if (!timezone) return undefined; + return ( +
+ {timezone.gmtOffset} + {timezone.name} +
+ ); + }; + + const timeZoneOptions = TIME_ZONES.map((timeZone) => ({ + value: timeZone.value, + query: timeZone.name + " " + timeZone.gmtOffset + " " + timeZone.value, + content: getTimeZoneLabel(timeZone), + })); + + const handleProfilePictureDelete = async (url: string | null | undefined) => { + if (!url) return; + await updateCurrentUser({ + avatar_url: "", + }) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Profile picture deleted successfully.", + }); + setValue("avatar_url", ""); + }) + .catch(() => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "There was some error in deleting your profile picture. Please try again.", + }); + }) + .finally(() => { + setIsImageUploadModalOpen(false); + }); + }; + + const onSubmit = async (formData: TUserProfileForm) => { + setIsLoading(true); + const userPayload: Partial = { + first_name: formData.first_name, + last_name: formData.last_name, + avatar_url: formData.avatar_url, + display_name: formData?.display_name, + user_timezone: formData.user_timezone, + }; + // if unsplash or a pre-defined image is uploaded, delete the old uploaded asset + if (formData.cover_image_url?.startsWith("http")) { + userPayload.cover_image = formData.cover_image_url; + userPayload.cover_image_asset = null; + } + + const profilePayload: Partial = { + role: formData.role, + language: formData.language, + }; + + const updateCurrentUserDetail = updateCurrentUser(userPayload).finally(() => setIsLoading(false)); + const updateCurrentUserProfile = updateUserProfile(profilePayload).finally(() => setIsLoading(false)); + + const promises = [updateCurrentUserDetail, updateCurrentUserProfile]; + const updateUserAndProfile = Promise.all(promises); + + setPromiseToast(updateUserAndProfile, { + loading: "Updating...", + success: { + title: "Success!", + message: () => `Profile updated successfully.`, + }, + error: { + title: "Error!", + message: () => `There was some error in updating your profile. Please try again.`, + }, + }); + }; + + return ( + <> + setDeactivateAccountModal(false)} /> + ( + setIsImageUploadModalOpen(false)} + handleRemove={async () => await handleProfilePictureDelete(currentUser?.avatar_url)} + onSuccess={(url) => { + onChange(url); + handleSubmit(onSubmit)(); + setIsImageUploadModalOpen(false); + }} + value={value && value.trim() !== "" ? value : null} + /> + )} + /> +
+
+
+ {currentUser?.first_name +
+
+
+ +
+
+
+
+ ( + onChange(imageUrl)} + control={control} + value={value ?? "https://images.unsplash.com/photo-1506383796573-caf02b4a79ab"} + isProfileCover + /> + )} + /> +
+
+
+
+
+ {`${watch("first_name")} ${watch("last_name")}`} +
+ {watch("email")} +
+
+
+
+
+

+ {t("first_name")}  + * +

+ ( + + )} + /> + {errors.first_name && {errors.first_name.message}} +
+
+

{t("last_name")}

+ ( + + )} + /> +
+
+

+ {t("display_name")}  + * +

+ { + if (value.trim().length < 1) return "Display name can't be empty."; + if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces."; + if (value.replace(/\s/g, "").length < 1) return "Display name must be at least 1 character long."; + if (value.replace(/\s/g, "").length > 20) + return "Display name must be less than 20 characters long."; + return true; + }, + }} + render={({ field: { value, onChange, ref } }) => ( + + )} + /> + {errors?.display_name && {errors?.display_name?.message}} +
+
+

+ {t("email")}  + * +

+ ( + + )} + /> +
+
+

+ {t("role")}  + * +

+ ( + + {USER_ROLES.map((item) => ( + + {item.label} + + ))} + + )} + /> + {errors.role && Please select a role} +
+
+
+
+
+
+

+ {t("timezone")}  + * +

+ ( + t.value === value)) ?? value) + : "Select a timezone" + } + options={timeZoneOptions} + onChange={onChange} + buttonClassName={errors.user_timezone ? "border-red-500" : ""} + className="rounded-md border-[0.5px] !border-custom-border-200" + optionsClassName="w-72" + input + /> + )} + /> + {errors.user_timezone && {errors.user_timezone.message}} +
+
+

{t("language")}

+ ( + + {SUPPORTED_LANGUAGES.map((item) => ( + + {item.label} + + ))} + + )} + /> +
+
+
+ +
+
+
+
+ + {({ open }) => ( + <> + + Deactivate account + + + + +
+ + When deactivating an account, all of the data and resources within that account will be permanently + removed and cannot be recovered. + +
+ +
+
+
+
+ + )} +
+ + ); +}); diff --git a/web/core/components/profile/index.ts b/web/core/components/profile/index.ts index d0d33af96ae..e5495aba1f5 100644 --- a/web/core/components/profile/index.ts +++ b/web/core/components/profile/index.ts @@ -3,5 +3,6 @@ export * from "./overview"; export * from "./profile-issues-filter"; export * from "./sidebar"; export * from "./time"; -export * from "./profile-setting-content-wrapper" -export * from "./profile-setting-content-header" \ No newline at end of file +export * from "./profile-setting-content-wrapper"; +export * from "./profile-setting-content-header"; +export * from "./form"; diff --git a/web/core/lib/wrappers/store-wrapper.tsx b/web/core/lib/wrappers/store-wrapper.tsx index fa60c354e3c..eb8f7325ba1 100644 --- a/web/core/lib/wrappers/store-wrapper.tsx +++ b/web/core/lib/wrappers/store-wrapper.tsx @@ -1,7 +1,8 @@ -import { ReactNode, useEffect, FC, useState } from "react"; +import { ReactNode, useEffect, FC } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; +import { useTranslation, Language } from "@plane/i18n"; // helpers import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; // hooks @@ -21,6 +22,7 @@ const StoreWrapper: FC = observer((props) => { const { setQuery } = useRouterParams(); const { sidebarCollapsed, toggleSidebar } = useAppTheme(); const { data: userProfile } = useUserProfile(); + const { changeLanguage } = useTranslation(); /** * Sidebar collapsed fetching from local storage @@ -28,7 +30,6 @@ const StoreWrapper: FC = observer((props) => { useEffect(() => { const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; - if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue); }, [sidebarCollapsed, setTheme, toggleSidebar]); @@ -37,7 +38,6 @@ const StoreWrapper: FC = observer((props) => { */ useEffect(() => { if (!userProfile?.theme?.theme) return; - const currentTheme = userProfile?.theme?.theme || "system"; const currentThemePalette = userProfile?.theme?.palette; if (currentTheme) { @@ -51,6 +51,11 @@ const StoreWrapper: FC = observer((props) => { } }, [userProfile?.theme?.theme, userProfile?.theme?.palette, setTheme]); + useEffect(() => { + if (!userProfile?.language) return; + changeLanguage(userProfile?.language as Language); + }, [userProfile?.language, changeLanguage]); + useEffect(() => { if (!params) return; setQuery(params); From f3112643976530bfeef53204ca3fc0e447c91575 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 16 Dec 2024 18:34:07 +0530 Subject: [PATCH 03/20] fix: adding more languages for support --- packages/i18n/src/config/index.ts | 14 ++++++- .../i18n/src/locales/en/translations.json | 4 +- .../i18n/src/locales/es/translations.json | 39 +++++++++++++++++++ .../i18n/src/locales/fr/translations.json | 27 +++++++------ .../i18n/src/locales/ja/translations.json | 39 +++++++++++++++++++ web/core/components/profile/form.tsx | 9 ++--- 6 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 packages/i18n/src/locales/es/translations.json create mode 100644 packages/i18n/src/locales/ja/translations.json diff --git a/packages/i18n/src/config/index.ts b/packages/i18n/src/config/index.ts index a789825ec91..3f55d8cf6f0 100644 --- a/packages/i18n/src/config/index.ts +++ b/packages/i18n/src/config/index.ts @@ -1,5 +1,7 @@ import en from "../locales/en/translations.json"; import fr from "../locales/fr/translations.json"; +import es from "../locales/es/translations.json"; +import ja from "../locales/ja/translations.json"; export type Language = (typeof languages)[number]; export type Translations = { @@ -9,10 +11,12 @@ export type Translations = { }; export const fallbackLng = "en"; -export const languages = ["en", "fr"] as const; +export const languages = ["en", "fr", "es", "ja"] as const; export const translations: Translations = { en, fr, + es, + ja, }; export const SUPPORTED_LANGUAGES = [ @@ -24,4 +28,12 @@ export const SUPPORTED_LANGUAGES = [ label: "French", value: "fr", }, + { + label: "Spanish", + value: "es", + }, + { + label: "Japanese", + value: "ja", + }, ]; diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 7eec69af328..8b798a6aa96 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -33,5 +33,7 @@ "change_cover": "Change cover", "language": "Language", "saving": "Saving...", - "save_changes": "Save changes" + "save_changes": "Save changes", + "deactivate_account": "Deactivate account", + "deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered." } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json new file mode 100644 index 00000000000..b49c1c060f5 --- /dev/null +++ b/packages/i18n/src/locales/es/translations.json @@ -0,0 +1,39 @@ +{ + "submit": "Enviar", + "cancel": "Cancelar", + "loading": "Cargando", + "error": "Error", + "success": "Éxito", + "warning": "Advertencia", + "info": "Información", + "close": "Cerrar", + "yes": "Sí", + "no": "No", + "ok": "OK", + "name": "Nombre", + "description": "Descripción", + "search": "Buscar", + "add_member": "Agregar miembro", + "remove_member": "Eliminar miembro", + "add_members": "Agregar miembros", + "remove_members": "Eliminar miembros", + "add": "Agregar", + "remove": "Eliminar", + "add_new": "Agregar nuevo", + "remove_selected": "Eliminar seleccionados", + "first_name": "Nombre", + "last_name": "Apellido", + "email": "Correo electrónico", + "display_name": "Nombre para mostrar", + "role": "Rol", + "timezone": "Zona horaria", + "avatar": "Avatar", + "cover_image": "Imagen de portada", + "password": "Contraseña", + "change_cover": "Cambiar portada", + "language": "Idioma", + "saving": "Guardando...", + "save_changes": "Guardar cambios", + "deactivate_account": "Desactivar cuenta", + "deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar." +} diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 683a429958a..14fc0bbdc01 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -3,7 +3,7 @@ "cancel": "Annuler", "loading": "Chargement", "error": "Erreur", - "success": "Succès", + "success": "Succès", "warning": "Avertissement", "info": "Info", "close": "Fermer", @@ -14,27 +14,26 @@ "description": "Description", "search": "Rechercher", "add_member": "Ajouter un membre", - "remove_member": "Supprimer le membre", + "remove_member": "Supprimer un membre", "add_members": "Ajouter des membres", "remove_members": "Supprimer des membres", "add": "Ajouter", "remove": "Supprimer", - "add_new": "Ajouter un nouveau", - "remove_selected": "Supprimer les sélectionnés", - "first_name": "Prénom", - "last_name": "Nom", + "add_new": "Ajouter nouveau", + "remove_selected": "Supprimer la sélection", + "first_name": "Prénom", + "last_name": "Nom de famille", "email": "Email", "display_name": "Nom d'affichage", - "role": "Rôle", + "role": "Rôle", "timezone": "Fuseau horaire", "avatar": "Avatar", - "member": "Membre", - "members": "Membres", - "invite": "Inviter", - "invite_members": "Inviter des membres", - "invite_new_members": "Inviter des nouveaux membres", - "change_cover": "Changer la couverture", + "cover_image": "Image de couverture", + "password": "Mot de passe", + "change_cover": "Modifier la couverture", "language": "Langue", "saving": "Enregistrement...", - "save_changes": "Enregistrer les modifications" + "save_changes": "Enregistrer les modifications", + "deactivate_account": "Désactiver le compte", + "deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées." } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json new file mode 100644 index 00000000000..ff3a6d12026 --- /dev/null +++ b/packages/i18n/src/locales/ja/translations.json @@ -0,0 +1,39 @@ +{ + "submit": "送信", + "cancel": "キャンセル", + "loading": "読み込み中", + "error": "エラー", + "success": "成功", + "warning": "警告", + "info": "情報", + "close": "閉じる", + "yes": "はい", + "no": "いいえ", + "ok": "OK", + "name": "名前", + "description": "説明", + "search": "検索", + "add_member": "メンバーを追加", + "remove_member": "メンバーを削除", + "add_members": "メンバーを追加", + "remove_members": "メンバーを削除", + "add": "追加", + "remove": "削除", + "add_new": "新規追加", + "remove_selected": "選択項目を削除", + "first_name": "名", + "last_name": "姓", + "email": "メールアドレス", + "display_name": "表示名", + "role": "役割", + "timezone": "タイムゾーン", + "avatar": "アバター", + "cover_image": "カバー画像", + "password": "パスワード", + "change_cover": "カバーを変更", + "language": "言語", + "saving": "保存中...", + "save_changes": "変更を保存", + "deactivate_account": "アカウントを無効化", + "deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。" +} diff --git a/web/core/components/profile/form.tsx b/web/core/components/profile/form.tsx index 5b3fbcaf05e..ae2679c6b77 100644 --- a/web/core/components/profile/form.tsx +++ b/web/core/components/profile/form.tsx @@ -459,7 +459,7 @@ export const ProfileForm = observer((props: TProfileFormProps) => { {({ open }) => ( <> - Deactivate account + {t("deactivate_account")} { >
- - When deactivating an account, all of the data and resources within that account will be permanently - removed and cannot be recovered. - + {t("deactivate_account_description")}
From 194003d3e6c54f280c81a033e328d874995b8c59 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 16 Dec 2024 18:52:23 +0530 Subject: [PATCH 04/20] fix: profile settings translations --- .../i18n/src/locales/en/translations.json | 12 ++++++++++- .../i18n/src/locales/es/translations.json | 12 ++++++++++- .../i18n/src/locales/fr/translations.json | 12 ++++++++++- .../i18n/src/locales/ja/translations.json | 12 ++++++++++- web/app/profile/sidebar.tsx | 21 ++++++++++--------- 5 files changed, 55 insertions(+), 14 deletions(-) diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 8b798a6aa96..98b5377a087 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -35,5 +35,15 @@ "saving": "Saving...", "save_changes": "Save changes", "deactivate_account": "Deactivate account", - "deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered." + "deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered.", + "profile_settings": "Profile settings", + "your_account": "Your account", + "profile": "Profile", + "security": "Security", + "activity": "Activity", + "appearance": "Appearance", + "notifications": "Notifications", + "workspaces": "Workspaces", + "create_workspace": "Create workspace", + "invitations": "Invitations" } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index b49c1c060f5..47d98d2d0a9 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -35,5 +35,15 @@ "saving": "Guardando...", "save_changes": "Guardar cambios", "deactivate_account": "Desactivar cuenta", - "deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar." + "deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar.", + "profile_settings": "Configuración de perfil", + "your_account": "Tu cuenta", + "profile": "Perfil", + "security": "Seguridad", + "activity": "Actividad", + "appearance": "Apariencia", + "notifications": "Notificaciones", + "workspaces": "Workspaces", + "create_workspace": "Crear workspace", + "invitations": "Invitaciones" } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 14fc0bbdc01..5acc0f457c9 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -35,5 +35,15 @@ "saving": "Enregistrement...", "save_changes": "Enregistrer les modifications", "deactivate_account": "Désactiver le compte", - "deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées." + "deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.", + "profile_settings": "Paramètres du profil", + "your_account": "Votre compte", + "profile": "Profil", + "security": " Sécurité", + "activity": "Activité", + "appearance": "Apparence", + "notifications": "Notifications", + "workspaces": "Workspaces", + "create_workspace": "Créer un workspace", + "invitations": "Invitations" } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index ff3a6d12026..8a12214d019 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -35,5 +35,15 @@ "saving": "保存中...", "save_changes": "変更を保存", "deactivate_account": "アカウントを無効化", - "deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。" + "deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。", + "profile_settings": "プロフィール設定", + "your_account": "アカウント", + "profile": "プロフィール", + "security": "セキュリティ", + "activity": "アクティビティ", + "appearance": "アピアンス", + "notifications": "通知", + "workspaces": "ワークスペース", + "create_workspace": "ワークスペースを作成", + "invitations": "招待" } diff --git a/web/app/profile/sidebar.tsx b/web/app/profile/sidebar.tsx index 479ef21f515..2f456415760 100644 --- a/web/app/profile/sidebar.tsx +++ b/web/app/profile/sidebar.tsx @@ -6,9 +6,9 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; // icons import { ChevronLeft, LogOut, MoveLeft, Plus, UserPlus } from "lucide-react"; -// plane helpers +// plane imports import { useOutsideClickDetector } from "@plane/hooks"; -// ui +import { useTranslation } from "@plane/i18n"; import { TOAST_TYPE, Tooltip, setToast } from "@plane/ui"; // components import { SidebarNavItem } from "@/components/sidebar"; @@ -23,7 +23,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; const WORKSPACE_ACTION_LINKS = [ { - key: "create-workspace", + key: "create_workspace", Icon: Plus, label: "Create workspace", href: "/create-workspace", @@ -47,6 +47,7 @@ export const ProfileLayoutSidebar = observer(() => { const { data: currentUserSettings } = useUserSettings(); const { workspaces } = useWorkspace(); const { isMobile } = usePlatformOS(); + const { t } = useTranslation(); const workspacesList = Object.values(workspaces ?? {}); @@ -117,13 +118,13 @@ export const ProfileLayoutSidebar = observer(() => { {!sidebarCollapsed && ( -

Profile settings

+

{t("profile_settings")}

)}
{!sidebarCollapsed && ( -
Your account
+
{t("your_account")}
)}
{PROFILE_ACTION_LINKS.map((link) => { @@ -132,7 +133,7 @@ export const ProfileLayoutSidebar = observer(() => { return ( { >
- {!sidebarCollapsed &&

{link.label}

} + {!sidebarCollapsed &&

{t(link.key)}

}
@@ -156,7 +157,7 @@ export const ProfileLayoutSidebar = observer(() => {
{!sidebarCollapsed && ( -
Workspaces
+
{t("workspaces")}
)} {workspacesList && workspacesList.length > 0 && (
{ {WORKSPACE_ACTION_LINKS.map((link) => ( { }`} > {} - {!sidebarCollapsed && link.label} + {!sidebarCollapsed && t(link.key)}
From 07c51478dee37ada14176ce46a0a435dd1f8f221 Mon Sep 17 00:00:00 2001 From: Vamsi krishna Date: Tue, 17 Dec 2024 17:41:40 +0530 Subject: [PATCH 05/20] feat: added language support for sidebar and user settings --- packages/constants/src/workspace.ts | 2 +- .../i18n/src/locales/en/translations.json | 224 +++++++++++++++++- .../i18n/src/locales/es/translations.json | 224 +++++++++++++++++- .../i18n/src/locales/fr/translations.json | 224 +++++++++++++++++- .../i18n/src/locales/ja/translations.json | 224 +++++++++++++++++- .../(projects)/active-cycles/header.tsx | 17 +- .../(projects)/analytics/header.tsx | 6 +- .../(projects)/profile/[userId]/layout.tsx | 4 +- .../(projects)/profile/[userId]/navbar.tsx | 5 +- web/app/create-workspace/page.tsx | 42 +++- web/app/invitations/page.tsx | 29 +-- web/app/profile/activity/page.tsx | 6 +- web/app/profile/appearance/page.tsx | 9 +- web/app/profile/notifications/page.tsx | 8 +- web/app/profile/page.tsx | 4 +- web/app/profile/security/page.tsx | 35 +-- web/app/profile/sidebar.tsx | 6 +- .../workspace-active-cycles-upgrade.tsx | 18 +- .../global/product-updates-header.tsx | 32 +-- web/ce/components/global/version-number.tsx | 6 +- .../components/projects/create/attributes.tsx | 6 +- web/ce/components/projects/create/root.tsx | 12 +- web/ce/components/workspace/edition-badge.tsx | 4 +- web/ce/constants/dashboard.ts | 23 +- .../constants/project/settings/features.tsx | 10 + .../account/password-strength-meter.tsx | 14 +- .../core/theme/custom-theme-selector.tsx | 71 +++--- .../components/core/theme/theme-switch.tsx | 9 +- .../global/product-updates/footer.tsx | 52 ++-- .../global/product-updates/modal.tsx | 11 +- web/core/components/profile/form.tsx | 2 +- .../notification/email-notification-form.tsx | 37 ++- .../project/create/common-attributes.tsx | 22 +- web/core/components/project/create/header.tsx | 6 +- .../project/create/project-create-buttons.tsx | 6 +- web/core/components/project/form.tsx | 6 +- .../project/project-feature-update.tsx | 10 +- .../project/settings/features-list.tsx | 10 +- .../workspace/create-workspace-form.tsx | 54 +++-- .../components/workspace/sidebar/dropdown.tsx | 59 ++--- .../sidebar/favorites/favorites-menu.tsx | 30 ++- .../sidebar/favorites/new-fav-folder.tsx | 38 +-- .../workspace/sidebar/help-section.tsx | 14 +- .../workspace/sidebar/project-navigation.tsx | 13 +- .../workspace/sidebar/projects-list-item.tsx | 38 +-- .../workspace/sidebar/projects-list.tsx | 23 +- .../workspace/sidebar/quick-actions.tsx | 4 +- .../workspace/sidebar/user-menu.tsx | 16 +- .../workspace/sidebar/workspace-menu.tsx | 25 +- web/core/constants/cycle.ts | 6 + web/core/constants/profile.ts | 4 + web/core/constants/themes.ts | 7 + web/core/store/user/profile.store.ts | 1 + 53 files changed, 1402 insertions(+), 366 deletions(-) diff --git a/packages/constants/src/workspace.ts b/packages/constants/src/workspace.ts index c17b5432ee8..d37decf3f2f 100644 --- a/packages/constants/src/workspace.ts +++ b/packages/constants/src/workspace.ts @@ -1,5 +1,5 @@ export const ORGANIZATION_SIZE = [ - "Just myself", + "Just myself", // TODO: translate "2-10", "11-50", "51-200", diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 98b5377a087..23edbfd14da 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -45,5 +45,227 @@ "notifications": "Notifications", "workspaces": "Workspaces", "create_workspace": "Create workspace", - "invitations": "Invitations" + "invitations": "Invitations", + "summary": "Summary", + "assigned": "Assigned", + "created": "Created", + "subscribed": "Subscribed", + "you_do_not_have_the_permission_to_access_this_page": "You do not have the permission to access this page.", + "failed_to_sign_out_please_try_again": "Failed to sign out. Please try again.", + "password_changed_successfully": "Password changed successfully.", + "something_went_wrong_please_try_again": "Something went wrong. Please try again.", + "change_password": "Change password", + "passwords_dont_match": "Passwords don't match", + "current_password": "Current password", + "new_password": "New password", + "confirm_password": "Confirm password", + "this_field_is_required": "This field is required", + "changing_password": "Changing password", + "please_enter_your_password": "Please enter your password.", + "password_length_should_me_more_than_8_characters": "Password length should me more than 8 characters.", + "password_is_weak": "Password is weak.", + "password_is_strong": "Password is strong.", + "load_more": "Load more", + "select_or_customize_your_interface_color_scheme": "Select or customize your interface color scheme.", + "theme": "Theme", + "system_preference": "System preference", + "light": "Light", + "dark": "Dark", + "light_contrast": "Light high contrast", + "dark_contrast": "Dark high contrast", + "custom": "Custom theme", + "select_your_theme": "Select your theme", + "customize_your_theme": "Customize your theme", + "background_color": "Background color", + "text_color": "Text color", + "primary_color": "Primary(Theme) color", + "sidebar_background_color": "Sidebar background color", + "sidebar_text_color": "Sidebar text color", + "set_theme": "Set theme", + "enter_a_valid_hex_code_of_6_characters": "Enter a valid hex code of 6 characters", + "background_color_is_required": "Background color is required", + "text_color_is_required": "Text color is required", + "primary_color_is_required": "Primary color is required", + "sidebar_background_color_is_required": "Sidebar background color is required", + "sidebar_text_color_is_required": "Sidebar text color is required", + "updating_theme": "Updating theme", + "theme_updated_successfully": "Theme updated successfully", + "failed_to_update_the_theme": "Failed to update the theme", + "email_notifications": "Email notifications", + "stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "Stay in the loop on Issues you are subscribed to. Enable this to get notified.", + "email_notification_setting_updated_successfully": "Email notification setting updated successfully", + "failed_to_update_email_notification_setting": "Failed to update email notification setting", + "notify_me_when": "Notify me when", + "property_changes": "Property changes", + "property_changes_description": "Notify me when issue's properties like assignees, priority, estimates or anything else changes.", + "state_change": "State change", + "state_change_description": "Notify me when the issues moves to a different state", + "issue_completed": "Issue completed", + "issue_completed_description": "Notify me only when an issue is completed", + "comments": "Comments", + "comments_description": "Notify me when someone leaves a comment on the issue", + "mentions": "Mentions", + "mentions_description": "Notify me only when someone mentions me in the comments or description", + "create_your_workspace": "Create your workspace", + "only_your_instance_admin_can_create_workspaces": "Only your instance admin can create workspaces", + "only_your_instance_admin_can_create_workspaces_description": "If you know your instance admin's email address, click the button below to get in touch with them.", + "go_back": "Go back", + "request_instance_admin": "Request instance admin", + "plane_logo": "Plane logo", + "workspace_creation_disabled": "Workspace creation disabled", + "workspace_request_subject": "Requesting a new workspace", + "workspace_request_body": "Hi instance admin(s),\n\nPlease create a new workspace with the URL [/workspace-name] for [purpose of creating the workspace].\n\nThanks,\n{{firstName}} {{lastName}}\n{{email}}", + "creating_workspace": "Creating workspace", + "workspace_created_successfully": "Workspace created successfully", + "create_workspace_page": "Create workspace page", + "workspace_could_not_be_created_please_try_again": "Workspace could not be created. Please try again.", + "workspace_could_not_be_created_please_try_again_description": "Some error occurred while creating workspace. Please try again.", + "this_is_a_required_field": "This is a required field.", + "name_your_workspace": "Name your workspace", + "workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "Workspaces names can contain only (' '), ('-'), ('_') and alphanumeric characters.", + "limit_your_name_to_80_characters": "Limit your name to 80 characters.", + "set_your_workspace_url": "Set your workspace's URL", + "limit_your_url_to_48_characters": "Limit your URL to 48 characters.", + "how_many_people_will_use_this_workspace": "How many people will use this workspace?", + "how_many_people_will_use_this_workspace_description": "This will help us to determine the number of seats you need to purchase.", + "select_a_range": "Select a range", + "urls_can_contain_only_dash_and_alphanumeric_characters": "URLs can contain only ('-') and alphanumeric characters.", + "something_familiar_and_recognizable_is_always_best": "Something familiar and recognizable is always best.", + "workspace_url_is_already_taken": "Workspace URL is already taken!", + "old_password": "Old password", + "general_settings": "General settings", + "sign_out": "Sign out", + "signing_out": "Signing out", + "active_cycles": "Active cycles", + "active_cycles_description": "Monitor cycles across projects, track high-priority issues, and zoom in cycles that need attention.", + "on_demand_snapshots_of_all_your_cycles": "On-demand snapshots of all your cycles", + "upgrade": "Upgrade", + "10000_feet_view": "10,000-feet view of all active cycles.", + "10000_feet_view_description": "Zoom out to see running cycles across all your projects at once instead of going from Cycle to Cycle in each project.", + "get_snapshot_of_each_active_cycle": "Get a snapshot of each active cycle.", + "get_snapshot_of_each_active_cycle_description": "Track high-level metrics for all active cycles, see their state of progress, and get a sense of scope against deadlines.", + "compare_burndowns": "Compare burndowns.", + "compare_burndowns_description": "Monitor how each of your teams are performing with a peek into each cycle's burndown report.", + "quickly_see_make_or_break_issues": "Quickly see make-or-break issues.", + "quickly_see_make_or_break_issues_description": "Preview high-priority issues for each cycle against due dates. See all of them per cycle in one click.", + "zoom_into_cycles_that_need_attention": "Zoom into cycles that need attention.", + "zoom_into_cycles_that_need_attention_description": "Investigate the state of any cycle that doesn't conform to expectations in one click.", + "stay_ahead_of_blockers": "Stay ahead of blockers.", + "stay_ahead_of_blockers_description": "Spot challenges from one project to another and see inter-cycle dependencies that aren't obvious from any other view.", + "analytics": "Analytics", + "workspace_invites": "Workspace invites", + "workspace_settings": "Workspace settings", + "enter_god_mode": "Enter god mode", + "workspace_logo": "Workspace logo", + "new_issue": "New issue", + "home": "Home", + "your_work": "Your work", + "drafts": "Drafts", + "projects": "Projects", + "views": "Views", + "workspace": "Workspace", + "archives": "Archives", + "settings": "Settings", + "failed_to_move_favorite": "Failed to move favorite", + "your_favorites": "Your favorites", + "no_favorites_yet": "No favorites yet", + "create_folder": "Create folder", + "new_folder": "New folder", + "favorite_updated_successfully": "Favorite updated successfully", + "favorite_created_successfully": "Favorite created successfully", + "folder_already_exists": "Folder already exists", + "folder_name_cannot_be_empty": "Folder name cannot be empty", + "something_went_wrong": "Something went wrong", + "failed_to_reorder_favorite": "Failed to reorder favorite", + "favorite_removed_successfully": "Favorite removed successfully", + "failed_to_create_favorite": "Failed to create favorite", + "failed_to_rename_favorite": "Failed to rename favorite", + "project_link_copied_to_clipboard": "Project link copied to clipboard", + "link_copied": "Link copied", + "your_projects": "Your projects", + "add_project": "Add project", + "create_project": "Create project", + "failed_to_remove_project_from_favorites": "Couldn't remove the project from favorites. Please try again.", + "project_created_successfully": "Project created successfully", + "project_created_successfully_description": "Project created successfully. You can now start adding issues to it.", + "project_cover_image_alt": "Project cover image", + "name_is_required": "Name is required", + "title_should_be_less_than_255_characters": "Title should be less than 255 characters", + "project_name": "Project name", + "project_id_must_be_at_least_1_character": "Project ID must at least be of 1 character", + "project_id_must_be_at_most_5_characters": "Project ID must at most be of 5 characters", + "project_id": "Project ID", + "project_id_tooltip_content": "Helps you identify issues in the project uniquely. Max 5 characters.", + "description_placeholder": "Description...", + "only_alphanumeric_non_latin_characters_allowed": "Only Alphanumeric & Non-latin characters are allowed.", + "project_id_is_required": "Project ID is required", + "select_network": "Select network", + "lead": "Lead", + "private": "Private", + "public": "Public", + "accessible_only_by_invite": "Accessible only by invite", + "anyone_in_the_workspace_except_guests_can_join": "Anyone in the workspace except Guests can join", + "creating": "Creating", + "creating_project": "Creating project", + "adding_project_to_favorites": "Adding project to favorites", + "project_added_to_favorites": "Project added to favorites", + "couldnt_add_the_project_to_favorites": "Couldn't add the project to favorites. Please try again.", + "removing_project_from_favorites": "Removing project from favorites", + "project_removed_from_favorites": "Project removed from favorites", + "couldnt_remove_the_project_from_favorites": "Couldn't remove the project from favorites. Please try again.", + "add_to_favorites": "Add to favorites", + "remove_from_favorites": "Remove from favorites", + "publish_settings": "Publish settings", + "publish": "Publish", + "copy_link": "Copy link", + "leave_project": "Leave project", + "join_the_project_to_rearrange": "Join the project to rearrange", + "drag_to_rearrange": "Drag to rearrange", + "congrats": "Congrats!", + "project": "Project", + "open_project": "Open project", + "issues": "Issues", + "cycles": "Cycles", + "modules": "Modules", + "pages": "Pages", + "intake": "Intake", + "time_tracking": "Time Tracking", + "work_management": "Work management", + "projects_and_issues": "Projects and issues", + "projects_and_issues_description": "Toggle these on or off this project.", + "cycles_description": "Timebox work as you see fit per project and change frequency from one period to the next.", + "modules_description": "Group work into sub-project-like set-ups with their own leads and assignees.", + "views_description": "Save sorts, filters, and display options for later or share them.", + "pages_description": "Write anything like you write anything.", + "intake_description": "Stay in the loop on Issues you are subscribed to. Enable this to get notified.", + "time_tracking_description": "Track time spent on issues and projects.", + "work_management_description": "Manage your work and projects with ease.", + "documentation": "Documentation", + "message_support": "Message support", + "contact_sales": "Contact sales", + "hyper_mode": "Hyper Mode", + "keyboard_shortcuts": "Keyboard shortcuts", + "whats_new": "What's new?", + "version": "Version", + "we_are_having_trouble_fetching_the_updates": "We are having trouble fetching the updates.", + "our_changelogs": "our changelogs", + "for_the_latest_updates": "for the latest updates.", + "please_visit": "Please visit", + "docs": "Docs", + "full_changelog": "Full changelog", + "support": "Support", + "discord": "Discord", + "powered_by_plane_pages": "Powered by Plane Pages", + "please_select_at_least_one_invitation": "Please select at least one invitation.", + "please_select_at_least_one_invitation_description": "Please select at least one invitation to join the workspace.", + "we_see_that_someone_has_invited_you_to_join_a_workspace": "We see that someone has invited you to join a workspace", + "join_a_workspace": "Join a workspace", + "we_see_that_someone_has_invited_you_to_join_a_workspace_description": "We see that someone has invited you to join a workspace", + "join_a_workspace_description": "Join a workspace", + "accept_and_join": "Accept & Join", + "go_home": "Go Home", + "no_pending_invites": "No pending invites", + "you_can_see_here_if_someone_invites_you_to_a_workspace": "You can see here if someone invites you to a workspace", + "back_to_home": "Back to home", + "workspace_name": "workspace-name" } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 47d98d2d0a9..895c5316e45 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -45,5 +45,227 @@ "notifications": "Notificaciones", "workspaces": "Workspaces", "create_workspace": "Crear workspace", - "invitations": "Invitaciones" + "invitations": "Invitaciones", + "summary": "Resumen", + "assigned": "Asignado", + "created": "Creado", + "subscribed": "Suscrito", + "you_do_not_have_the_permission_to_access_this_page": "No tienes permiso para acceder a esta página.", + "failed_to_sign_out_please_try_again": "Error al cerrar sesión. Por favor, inténtalo de nuevo.", + "password_changed_successfully": "Contraseña cambiada con éxito.", + "something_went_wrong_please_try_again": "Algo salió mal. Por favor, inténtalo de nuevo.", + "change_password": "Cambiar contraseña", + "passwords_dont_match": "Las contraseñas no coinciden", + "current_password": "Contraseña actual", + "new_password": "Nueva contraseña", + "confirm_password": "Confirmar contraseña", + "this_field_is_required": "Este campo es obligatorio", + "changing_password": "Cambiando contraseña", + "please_enter_your_password": "Por favor, introduce tu contraseña.", + "password_length_should_me_more_than_8_characters": "La longitud de la contraseña debe ser más de 8 caracteres.", + "password_is_weak": "La contraseña es débil.", + "password_is_strong": "La contraseña es fuerte.", + "load_more": "Cargar más", + "select_or_customize_your_interface_color_scheme": "Selecciona o personaliza el esquema de color de tu interfaz.", + "theme": "Tema", + "system_preference": "Preferencia del sistema", + "light": "Claro", + "dark": "Oscuro", + "light_contrast": "Alto contraste claro", + "dark_contrast": "Alto contraste oscuro", + "custom": "Tema personalizado", + "select_your_theme": "Selecciona tu tema", + "customize_your_theme": "Personaliza tu tema", + "background_color": "Color de fondo", + "text_color": "Color del texto", + "primary_color": "Color primario (Tema)", + "sidebar_background_color": "Color de fondo de la barra lateral", + "sidebar_text_color": "Color del texto de la barra lateral", + "set_theme": "Establecer tema", + "enter_a_valid_hex_code_of_6_characters": "Introduce un código hexadecimal válido de 6 caracteres", + "background_color_is_required": "El color de fondo es obligatorio", + "text_color_is_required": "El color del texto es obligatorio", + "primary_color_is_required": "El color primario es obligatorio", + "sidebar_background_color_is_required": "El color de fondo de la barra lateral es obligatorio", + "sidebar_text_color_is_required": "El color del texto de la barra lateral es obligatorio", + "updating_theme": "Actualizando tema", + "theme_updated_successfully": "Tema actualizado con éxito", + "failed_to_update_the_theme": "Error al actualizar el tema", + "email_notifications": "Notificaciones por correo electrónico", + "stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "Mantente al tanto de los problemas a los que estás suscrito. Activa esto para recibir notificaciones.", + "email_notification_setting_updated_successfully": "Configuración de notificaciones por correo electrónico actualizada con éxito", + "failed_to_update_email_notification_setting": "Error al actualizar la configuración de notificaciones por correo electrónico", + "notify_me_when": "Notifícame cuando", + "property_changes": "Cambios de propiedad", + "property_changes_description": "Notifícame cuando cambien las propiedades del problema como asignados, prioridad, estimaciones o cualquier otra cosa.", + "state_change": "Cambio de estado", + "state_change_description": "Notifícame cuando el problema se mueva a un estado diferente", + "issue_completed": "Problema completado", + "issue_completed_description": "Notifícame solo cuando un problema esté completado", + "comments": "Comentarios", + "comments_description": "Notifícame cuando alguien deje un comentario en el problema", + "mentions": "Menciones", + "mentions_description": "Notifícame solo cuando alguien me mencione en los comentarios o en la descripción", + "create_your_workspace": "Crea tu espacio de trabajo", + "only_your_instance_admin_can_create_workspaces": "Solo tu administrador de instancia puede crear espacios de trabajo", + "only_your_instance_admin_can_create_workspaces_description": "Si conoces el correo electrónico de tu administrador de instancia, haz clic en el botón de abajo para ponerte en contacto con él.", + "go_back": "Regresar", + "request_instance_admin": "Solicitar administrador de instancia", + "plane_logo": "Logo de Plane", + "workspace_creation_disabled": "Creación de espacio de trabajo deshabilitada", + "workspace_request_subject": "Solicitando un nuevo espacio de trabajo", + "workspace_request_body": "Hola administrador(es) de instancia,\n\nPor favor, crea un nuevo espacio de trabajo con la URL [/nombre-del-espacio-de-trabajo] para [propósito de crear el espacio de trabajo].\n\nGracias,\n{{firstName}} {{lastName}}\n{{email}}", + "creating_workspace": "Creando espacio de trabajo", + "workspace_created_successfully": "Espacio de trabajo creado con éxito", + "create_workspace_page": "Página de creación de espacio de trabajo", + "workspace_could_not_be_created_please_try_again": "No se pudo crear el espacio de trabajo. Por favor, inténtalo de nuevo.", + "workspace_could_not_be_created_please_try_again_description": "Ocurrió un error al crear el espacio de trabajo. Por favor, inténtalo de nuevo.", + "this_is_a_required_field": "Este es un campo obligatorio.", + "name_your_workspace": "Nombra tu espacio de trabajo", + "workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "Los nombres de los espacios de trabajo solo pueden contener (' '), ('-'), ('_') y caracteres alfanuméricos.", + "limit_your_name_to_80_characters": "Limita tu nombre a 80 caracteres.", + "set_your_workspace_url": "Establece la URL de tu espacio de trabajo", + "limit_your_url_to_48_characters": "Limita tu URL a 48 caracteres.", + "how_many_people_will_use_this_workspace": "¿Cuántas personas usarán este espacio de trabajo?", + "how_many_people_will_use_this_workspace_description": "Esto nos ayudará a determinar el número de asientos que necesitas comprar.", + "select_a_range": "Selecciona un rango", + "urls_can_contain_only_dash_and_alphanumeric_characters": "Las URLs solo pueden contener ('-') y caracteres alfanuméricos.", + "something_familiar_and_recognizable_is_always_best": "Algo familiar y reconocible siempre es mejor.", + "workspace_url_is_already_taken": "¡La URL del espacio de trabajo ya está tomada!", + "old_password": "Contraseña antigua", + "general_settings": "Configuración general", + "sign_out": "Cerrar sesión", + "signing_out": "Cerrando sesión", + "active_cycles": "Ciclos activos", + "active_cycles_description": "Monitorea ciclos a través de proyectos, rastrea problemas de alta prioridad y enfócate en ciclos que necesitan atención.", + "on_demand_snapshots_of_all_your_cycles": "Instantáneas bajo demanda de todos tus ciclos", + "upgrade": "Actualizar", + "10000_feet_view": "Vista de 10,000 pies de todos los ciclos activos.", + "10000_feet_view_description": "Amplía para ver ciclos en ejecución en todos tus proyectos a la vez en lugar de ir de ciclo en ciclo en cada proyecto.", + "get_snapshot_of_each_active_cycle": "Obtén una instantánea de cada ciclo activo.", + "get_snapshot_of_each_active_cycle_description": "Rastrea métricas de alto nivel para todos los ciclos activos, ve su estado de progreso y obtén una idea del alcance frente a los plazos.", + "compare_burndowns": "Compara burndowns.", + "compare_burndowns_description": "Monitorea cómo se están desempeñando cada uno de tus equipos con un vistazo al informe de burndown de cada ciclo.", + "quickly_see_make_or_break_issues": "Ve rápidamente problemas críticos.", + "quickly_see_make_or_break_issues_description": "Previsualiza problemas de alta prioridad para cada ciclo contra fechas de vencimiento. Vélos todos por ciclo en un clic.", + "zoom_into_cycles_that_need_attention": "Enfócate en ciclos que necesitan atención.", + "zoom_into_cycles_that_need_attention_description": "Investiga el estado de cualquier ciclo que no cumpla con las expectativas en un clic.", + "stay_ahead_of_blockers": "Anticípate a los bloqueadores.", + "stay_ahead_of_blockers_description": "Detecta desafíos de un proyecto a otro y ve dependencias entre ciclos que no son obvias desde ninguna otra vista.", + "analytics": "Analítica", + "workspace_invites": "Invitaciones al espacio de trabajo", + "workspace_settings": "Configuración del espacio de trabajo", + "enter_god_mode": "Entrar en modo dios", + "workspace_logo": "Logo del espacio de trabajo", + "new_issue": "Nuevo problema", + "home": "Inicio", + "your_work": "Tu trabajo", + "drafts": "Borradores", + "projects": "Proyectos", + "views": "Vistas", + "workspace": "Espacio de trabajo", + "archives": "Archivos", + "settings": "Configuración", + "failed_to_move_favorite": "Error al mover favorito", + "your_favorites": "Tus favoritos", + "no_favorites_yet": "Aún no hay favoritos", + "create_folder": "Crear carpeta", + "new_folder": "Nueva carpeta", + "favorite_updated_successfully": "Favorito actualizado con éxito", + "favorite_created_successfully": "Favorito creado con éxito", + "folder_already_exists": "La carpeta ya existe", + "folder_name_cannot_be_empty": "El nombre de la carpeta no puede estar vacío", + "something_went_wrong": "Algo salió mal", + "failed_to_reorder_favorite": "Error al reordenar favorito", + "favorite_removed_successfully": "Favorito eliminado con éxito", + "failed_to_create_favorite": "Error al crear favorito", + "failed_to_rename_favorite": "Error al renombrar favorito", + "project_link_copied_to_clipboard": "Enlace del proyecto copiado al portapapeles", + "link_copied": "Enlace copiado", + "your_projects": "Tus proyectos", + "add_project": "Agregar proyecto", + "create_project": "Crear proyecto", + "failed_to_remove_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.", + "project_created_successfully": "Proyecto creado con éxito", + "project_created_successfully_description": "Proyecto creado con éxito. Ahora puedes comenzar a agregar problemas a él.", + "project_cover_image_alt": "Imagen de portada del proyecto", + "name_is_required": "El nombre es obligatorio", + "title_should_be_less_than_255_characters": "El título debe tener menos de 255 caracteres", + "project_name": "Nombre del proyecto", + "project_id_must_be_at_least_1_character": "El ID del proyecto debe tener al menos 1 carácter", + "project_id_must_be_at_most_5_characters": "El ID del proyecto debe tener como máximo 5 caracteres", + "project_id": "ID del proyecto", + "project_id_tooltip_content": "Te ayuda a identificar problemas en el proyecto de manera única. Máximo 5 caracteres.", + "description_placeholder": "Descripción...", + "only_alphanumeric_non_latin_characters_allowed": "Solo se permiten caracteres alfanuméricos y no latinos.", + "project_id_is_required": "El ID del proyecto es obligatorio", + "select_network": "Seleccionar red", + "lead": "Líder", + "private": "Privado", + "public": "Público", + "accessible_only_by_invite": "Accesible solo por invitación", + "anyone_in_the_workspace_except_guests_can_join": "Cualquiera en el espacio de trabajo excepto invitados puede unirse", + "creating": "Creando", + "creating_project": "Creando proyecto", + "adding_project_to_favorites": "Agregando proyecto a favoritos", + "project_added_to_favorites": "Proyecto agregado a favoritos", + "couldnt_add_the_project_to_favorites": "No se pudo agregar el proyecto a favoritos. Por favor, inténtalo de nuevo.", + "removing_project_from_favorites": "Removing project from favorites", + "project_removed_from_favorites": "Project removed from favorites", + "couldnt_remove_the_project_from_favorites": "Couldn't remove the project from favorites. Please try again.", + "add_to_favorites": "Add to favorites", + "remove_from_favorites": "Remove from favorites", + "publish_settings": "Publish settings", + "publish": "Publish", + "copy_link": "Copy link", + "leave_project": "Leave project", + "join_the_project_to_rearrange": "Join the project to rearrange", + "drag_to_rearrange": "Drag to rearrange", + "congrats": "Congrats!", + "project": "Project", + "open_project": "Open project", + "issues": "Issues", + "cycles": "Cycles", + "modules": "Modules", + "pages": "Pages", + "intake": "Intake", + "time_tracking": "Time Tracking", + "work_management": "Work management", + "projects_and_issues": "Projects and issues", + "projects_and_issues_description": "Toggle these on or off this project.", + "cycles_description": "Timebox work as you see fit per project and change frequency from one period to the next.", + "modules_description": "Group work into sub-project-like set-ups with their own leads and assignees.", + "views_description": "Save sorts, filters, and display options for later or share them.", + "pages_description": "Write anything like you write anything.", + "intake_description": "Stay in the loop on Issues you are subscribed to. Enable this to get notified.", + "time_tracking_description": "Track time spent on issues and projects.", + "work_management_description": "Manage your work and projects with ease.", + "documentation": "Documentation", + "message_support": "Message support", + "contact_sales": "Contact sales", + "hyper_mode": "Hyper Mode", + "keyboard_shortcuts": "Keyboard shortcuts", + "whats_new": "What's new?", + "version": "Version", + "we_are_having_trouble_fetching_the_updates": "We are having trouble fetching the updates.", + "our_changelogs": "our changelogs", + "for_the_latest_updates": "for the latest updates.", + "please_visit": "Please visit", + "docs": "Docs", + "full_changelog": "Full changelog", + "support": "Support", + "discord": "Discord", + "powered_by_plane_pages": "Powered by Plane Pages", + "please_select_at_least_one_invitation": "Please select at least one invitation.", + "please_select_at_least_one_invitation_description": "Please select at least one invitation to join the workspace.", + "we_see_that_someone_has_invited_you_to_join_a_workspace": "We see that someone has invited you to join a workspace", + "join_a_workspace": "Join a workspace", + "we_see_that_someone_has_invited_you_to_join_a_workspace_description": "We see that someone has invited you to join a workspace", + "join_a_workspace_description": "Join a workspace", + "accept_and_join": "Accept & Join", + "go_home": "Go Home", + "no_pending_invites": "No pending invites", + "you_can_see_here_if_someone_invites_you_to_a_workspace": "You can see here if someone invites you to a workspace", + "back_to_home": "Back to home", + "workspace_name": "workspace-name" } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 5acc0f457c9..177be999bf7 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -45,5 +45,227 @@ "notifications": "Notifications", "workspaces": "Workspaces", "create_workspace": "Créer un workspace", - "invitations": "Invitations" + "invitations": "Invitations", + "summary": "Résumé", + "assigned": "Assigné", + "created": "Créé", + "subscribed": "Souscrit", + "you_do_not_have_the_permission_to_access_this_page": "Vous n'avez pas les permissions pour accéder à cette page.", + "failed_to_sign_out_please_try_again": "Impossible de se déconnecter. Veuillez réessayer.", + "password_changed_successfully": "Mot de passe changé avec succès.", + "something_went_wrong_please_try_again": "Quelque chose s'est mal passé. Veuillez réessayer.", + "change_password": "Changer le mot de passe", + "changing_password": "Changement de mot de passe", + "current_password": "Mot de passe actuel", + "new_password": "Nouveau mot de passe", + "confirm_password": "Confirmer le mot de passe", + "this_field_is_required": "Ce champ est requis", + "passwords_dont_match": "Les mots de passe ne correspondent pas", + "please_enter_your_password": "Veuillez entrer votre mot de passe.", + "password_length_should_me_more_than_8_characters": "La longueur du mot de passe doit être supérieure à 8 caractères.", + "password_is_weak": "Le mot de passe est faible.", + "password_is_strong": "Le mot de passe est fort.", + "load_more": "Charger plus", + "select_or_customize_your_interface_color_scheme": "Sélectionnez ou personnalisez votre schéma de couleurs de l'interface.", + "theme": "Thème", + "system_preference": "Préférence du système", + "light": "Clair", + "dark": "Foncé", + "light_contrast": "Clair de haut contraste", + "dark_contrast": "Foncé de haut contraste", + "custom": "Thème personnalisé", + "select_your_theme": "Sélectionnez votre thème", + "customize_your_theme": "Personnalisez votre thème", + "background_color": "Couleur de fond", + "text_color": "Couleur de texte", + "primary_color": "Couleur primaire (thème)", + "sidebar_background_color": "Couleur de fond du sidebar", + "sidebar_text_color": "Couleur de texte du sidebar", + "set_theme": "Définir le thème", + "enter_a_valid_hex_code_of_6_characters": "Entrez un code hexadécimal valide de 6 caractères", + "background_color_is_required": "La couleur de fond est requise", + "text_color_is_required": "La couleur de texte est requise", + "primary_color_is_required": "La couleur primaire est requise", + "sidebar_background_color_is_required": "La couleur de fond du sidebar est requise", + "sidebar_text_color_is_required": "La couleur de texte du sidebar est requise", + "updating_theme": "Mise à jour du thème", + "theme_updated_successfully": "Thème mis à jour avec succès", + "failed_to_update_the_theme": "Impossible de mettre à jour le thème", + "email_notifications": "Notifications par email", + "stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "Restez dans la boucle sur les problèmes auxquels vous êtes abonné. Activez cela pour être notifié.", + "email_notification_setting_updated_successfully": "Paramètres de notification par email mis à jour avec succès", + "failed_to_update_email_notification_setting": "Impossible de mettre à jour les paramètres de notification par email", + "notify_me_when": "Me notifier lorsque", + "property_changes": "Changements de propriété", + "property_changes_description": "Me notifier lorsque les propriétés du problème comme les assignés, la priorité, les estimations ou tout autre chose changent.", + "state_change": "Changement d'état", + "state_change_description": "Me notifier lorsque le problème passe à un autre état", + "issue_completed": "Problème terminé", + "issue_completed_description": "Me notifier uniquement lorsqu'un problème est terminé", + "comments": "Commentaires", + "comments_description": "Me notifier lorsqu'un utilisateur commente un problème", + "mentions": "Mention", + "mentions_description": "Me notifier uniquement lorsqu'un utilisateur mentionne un problème", + "create_your_workspace": "Créer votre workspace", + "only_your_instance_admin_can_create_workspaces": "Seuls les administrateurs de votre instance peuvent créer des workspaces", + "only_your_instance_admin_can_create_workspaces_description": "Si vous connaissez l'adresse email de votre administrateur d'instance, cliquez sur le bouton ci-dessous pour les contacter.", + "go_back": "Retour", + "request_instance_admin": "Demander à l'administrateur de l'instance", + "plane_logo": "Logo de Plane", + "workspace_creation_disabled": "Création d'espace de travail désactivée", + "workspace_request_subject": "Demande de création d'un espace de travail", + "workspace_request_body": "Bonjour administrateur(s) de l'instance,\n\nVeuillez créer un nouveau espace de travail avec l'URL [/workspace-name] pour [raison de la création de l'espace de travail].\n\nMerci,\n{{firstName}} {{lastName}}\n{{email}}", + "creating_workspace": "Création de l'espace de travail", + "workspace_created_successfully": "Espace de travail créé avec succès", + "create_workspace_page": "Page de création d'espace de travail", + "workspace_could_not_be_created_please_try_again": "L'espace de travail ne peut pas être créé. Veuillez réessayer.", + "workspace_could_not_be_created_please_try_again_description": "Une erreur est survenue lors de la création de l'espace de travail. Veuillez réessayer.", + "this_is_a_required_field": "Ce champ est requis.", + "name_your_workspace": "Nommez votre espace de travail", + "workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "Les noms des espaces de travail peuvent contenir uniquement des espaces, des tirets et des caractères alphanumériques.", + "limit_your_name_to_80_characters": "Limitez votre nom à 80 caractères.", + "set_your_workspace_url": "Définir l'URL de votre espace de travail", + "limit_your_url_to_48_characters": "Limitez votre URL à 48 caractères.", + "how_many_people_will_use_this_workspace": "Combien de personnes utiliseront cet espace de travail ?", + "how_many_people_will_use_this_workspace_description": "Cela nous aidera à déterminer le nombre de sièges que vous devez acheter.", + "select_a_range": "Sélectionner une plage", + "urls_can_contain_only_dash_and_alphanumeric_characters": "Les URLs peuvent contenir uniquement des tirets et des caractères alphanumériques.", + "something_familiar_and_recognizable_is_always_best": "Ce qui est familier et reconnaissable est toujours le meilleur.", + "workspace_url_is_already_taken": "L'URL de l'espace de travail est déjà utilisée !", + "old_password": "Mot de passe actuel", + "general_settings": "Paramètres généraux", + "sign_out": "Déconnexion", + "signing_out": "Déconnexion", + "active_cycles": "Cycles actifs", + "active_cycles_description": "Surveillez les cycles dans les projets, suivez les issues de haute priorité et zoomez sur les cycles qui nécessitent attention.", + "on_demand_snapshots_of_all_your_cycles": "Captures instantanées sur demande de tous vos cycles", + "upgrade": "Mettre à niveau", + "10000_feet_view": "Vue d'ensemble de tous les cycles actifs", + "10000_feet_view_description": "Prenez du recul pour voir les cycles en cours dans tous vos projets en même temps au lieu de passer d'un cycle à l'autre dans chaque projet.", + "get_snapshot_of_each_active_cycle": "Obtenez un aperçu de chaque cycle actif", + "get_snapshot_of_each_active_cycle_description": "Suivez les métriques de haut niveau pour tous les cycles actifs, observez leur état d'avancement et évaluez leur portée par rapport aux échéances.", + "compare_burndowns": "Comparez les graphiques d'avancement", + "compare_burndowns_description": "Surveillez les performances de chacune de vos équipes en consultant le rapport d'avancement de chaque cycle.", + "quickly_see_make_or_break_issues": "Identifiez rapidement les problèmes critiques", + "quickly_see_make_or_break_issues_description": "Visualisez les problèmes hautement prioritaires de chaque cycle par rapport aux dates d'échéance. Consultez-les tous par cycle en un seul clic.", + "zoom_into_cycles_that_need_attention": "Concentrez-vous sur les cycles nécessitant attention", + "zoom_into_cycles_that_need_attention_description": "Examinez en un clic l'état de tout cycle qui ne répond pas aux attentes.", + "stay_ahead_of_blockers": "Anticipez les blocages", + "stay_ahead_of_blockers_description": "Repérez les défis d'un projet à l'autre et identifiez les dépendances entre cycles qui ne sont pas évidentes depuis d'autres vues.", + "analytics": "Analyse", + "workspace_invites": "Invitations de l'espace de travail", + "workspace_settings": "Paramètres de l'espace de travail", + "enter_god_mode": "Entrer en mode dieu", + "workspace_logo": "Logo de l'espace de travail", + "new_issue": "Nouveau problème", + "home": "Accueil", + "your_work": "Votre travail", + "drafts": "Brouillons", + "projects": "Projets", + "views": "Vues", + "workspace": "Espace de travail", + "archives": "Archives", + "settings": "Paramètres", + "failed_to_move_favorite": "Impossible de déplacer le favori", + "your_favorites": "Vos favoris", + "no_favorites_yet": "Aucun favori pour le moment", + "create_folder": "Créer un dossier", + "new_folder": "Nouveau dossier", + "favorite_updated_successfully": "Favori mis à jour avec succès", + "favorite_created_successfully": "Favori créé avec succès", + "folder_already_exists": "Le dossier existe déjà", + "folder_name_cannot_be_empty": "Le nom du dossier ne peut pas être vide", + "something_went_wrong": "Quelque chose s'est mal passé", + "failed_to_reorder_favorite": "Impossible de réordonner le favori", + "favorite_removed_successfully": "Favori supprimé avec succès", + "failed_to_create_favorite": "Impossible de créer le favori", + "failed_to_rename_favorite": "Impossible de renommer le favori", + "project_link_copied_to_clipboard": "Lien du projet copié dans le presse-papiers", + "link_copied": "Lien copié", + "your_projects": "Vos projets", + "add_project": "Ajouter un projet", + "create_project": "Créer un projet", + "failed_to_remove_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.", + "project_created_successfully": "Projet créé avec succès", + "project_created_successfully_description": "Projet créé avec succès. Vous pouvez maintenant ajouter des issues à ce projet.", + "project_cover_image_alt": "Image de couverture du projet", + "name_is_required": "Le nom est requis", + "title_should_be_less_than_255_characters": "Le titre doit être inférieur à 255 caractères", + "project_name": "Nom du projet", + "project_id_must_be_at_least_1_character": "Le projet ID doit être au moins de 1 caractère", + "project_id_must_be_at_most_5_characters": "Le projet ID doit être au plus de 5 caractères", + "project_id": "ID du projet", + "project_id_tooltip_content": "Aide à identifier les issues du projet de manière unique. Max 5 caractères.", + "description_placeholder": "Description...", + "only_alphanumeric_non_latin_characters_allowed": "Seuls les caractères alphanumériques et non latins sont autorisés.", + "project_id_is_required": "Le projet ID est requis", + "select_network": "Sélectionner le réseau", + "lead": "Lead", + "private": "Privé", + "public": "Public", + "accessible_only_by_invite": "Accessible uniquement par invitation", + "anyone_in_the_workspace_except_guests_can_join": "Tout le monde dans l'espace de travail, sauf les invités, peut rejoindre", + "creating": "Création", + "creating_project": "Création du projet", + "adding_project_to_favorites": "Ajout du projet aux favoris", + "project_added_to_favorites": "Projet ajouté aux favoris", + "couldnt_add_the_project_to_favorites": "Impossible d'ajouter le projet aux favoris. Veuillez réessayer.", + "removing_project_from_favorites": "Suppression du projet des favoris", + "project_removed_from_favorites": "Projet supprimé des favoris", + "couldnt_remove_the_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.", + "add_to_favorites": "Ajouter aux favoris", + "remove_from_favorites": "Supprimer des favoris", + "publish_settings": "Paramètres de publication", + "publish": "Publier", + "copy_link": "Copier le lien", + "leave_project": "Quitter le projet", + "join_the_project_to_rearrange": "Rejoindre le projet pour réorganiser", + "drag_to_rearrange": "Glisser pour réorganiser", + "congrats": "Félicitations !", + "project": "Projet", + "open_project": "Ouvrir le projet", + "issues": "Problèmes", + "cycles": "Cycles", + "modules": "Modules", + "pages": "Pages", + "intake": "Intake", + "time_tracking": "Suivi du temps", + "work_management": "Gestion du travail", + "projects_and_issues": "Projets et problèmes", + "projects_and_issues_description": "Activer ou désactiver ces fonctionnalités pour ce projet.", + "cycles_description": "Organisez votre travail en périodes définies selon vos besoins par projet et modifiez la fréquence d'une période à l'autre.", + "modules_description": "Regroupez le travail en sous-projets avec leurs propres responsables et assignés.", + "views_description": "Enregistrez vos tris, filtres et options d'affichage pour plus tard ou partagez-les.", + "pages_description": "Rédigez tout type de contenu librement.", + "intake_description": "Restez informé des tickets auxquels vous êtes abonné. Activez cette option pour recevoir des notifications.", + "time_tracking_description": "Suivez le temps passé sur les tickets et les projets.", + "work_management_description": "Gérez votre travail et vos projets en toute simplicité.", + "documentation": "Documentation", + "message_support": "Contacter le support", + "contact_sales": "Contacter les ventes", + "hyper_mode": "Mode hyper", + "keyboard_shortcuts": "Raccourcis clavier", + "whats_new": "Nouveautés?", + "version": "Version", + "we_are_having_trouble_fetching_the_updates": "Nous avons des difficultés à récupérer les mises à jour.", + "our_changelogs": "nos changelogs", + "for_the_latest_updates": "pour les dernières mises à jour.", + "please_visit": "Veuillez visiter", + "docs": "Documentation", + "full_changelog": "Journal complet", + "support": "Support", + "discord": "Discord", + "powered_by_plane_pages": "Propulsé par Plane Pages", + "please_select_at_least_one_invitation": "Veuillez sélectionner au moins une invitation.", + "please_select_at_least_one_invitation_description": "Veuillez sélectionner au moins une invitation pour rejoindre l'espace de travail.", + "we_see_that_someone_has_invited_you_to_join_a_workspace": "Nous voyons que quelqu'un vous a invité à rejoindre un espace de travail", + "join_a_workspace": "Rejoindre un espace de travail", + "we_see_that_someone_has_invited_you_to_join_a_workspace_description": "Nous voyons que quelqu'un vous a invité à rejoindre un espace de travail", + "join_a_workspace_description": "Rejoindre un espace de travail", + "accept_and_join": "Accepter et rejoindre", + "go_home": "Retour à l'accueil", + "no_pending_invites": "Aucune invitation en attente", + "you_can_see_here_if_someone_invites_you_to_a_workspace": "Vous pouvez voir ici si quelqu'un vous invite à rejoindre un espace de travail", + "back_to_home": "Retour à l'accueil", + "workspace_name": "espace-de-travail" } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 8a12214d019..ceb9276d44a 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -45,5 +45,227 @@ "notifications": "通知", "workspaces": "ワークスペース", "create_workspace": "ワークスペースを作成", - "invitations": "招待" + "invitations": "招待", + "summary": "概要", + "assigned": "割り当て済み", + "created": "作成済み", + "subscribed": "購読済み", + "you_do_not_have_the_permission_to_access_this_page": "このページにアクセスする権限がありません。", + "failed_to_sign_out_please_try_again": "サインアウトに失敗しました。もう一度お試しください。", + "password_changed_successfully": "パスワードが正常に変更されました。", + "something_went_wrong_please_try_again": "何かがうまくいきませんでした。もう一度お試しください。", + "change_password": "パスワードを変更", + "passwords_dont_match": "パスワードが一致しません", + "current_password": "現在のパスワード", + "new_password": "新しいパスワード", + "confirm_password": "パスワードを確認", + "this_field_is_required": "このフィールドは必須です", + "changing_password": "パスワードを変更中", + "please_enter_your_password": "パスワードを入力してください。", + "password_length_should_me_more_than_8_characters": "パスワードの長さは8文字以上である必要があります。", + "password_is_weak": "パスワードが弱いです。", + "password_is_strong": "パスワードが強いです。", + "load_more": "もっと読み込む", + "select_or_customize_your_interface_color_scheme": "インターフェースのカラースキームを選択またはカスタマイズしてください。", + "theme": "テーマ", + "system_preference": "システム設定", + "light": "ライト", + "dark": "ダーク", + "light_contrast": "ライト高コントラスト", + "dark_contrast": "ダーク高コントラスト", + "custom": "カスタムテーマ", + "select_your_theme": "テーマを選択", + "customize_your_theme": "テーマをカスタマイズ", + "background_color": "背景色", + "text_color": "テキスト色", + "primary_color": "プライマリ(テーマ)色", + "sidebar_background_color": "サイドバー背景色", + "sidebar_text_color": "サイドバーテキスト色", + "set_theme": "テーマを設定", + "enter_a_valid_hex_code_of_6_characters": "6文字の有効な16進コードを入力してください", + "background_color_is_required": "背景色は必須です", + "text_color_is_required": "テキスト色は必須です", + "primary_color_is_required": "プライマリ色は必須です", + "sidebar_background_color_is_required": "サイドバー背景色は必須です", + "sidebar_text_color_is_required": "サイドバーテキスト色は必須です", + "updating_theme": "テーマを更新中", + "theme_updated_successfully": "テーマが正常に更新されました", + "failed_to_update_the_theme": "テーマの更新に失敗しました", + "email_notifications": "メール通知", + "stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "購読している問題についての通知を受け取るには、これを有効にしてください。", + "email_notification_setting_updated_successfully": "メール通知設定が正常に更新されました", + "failed_to_update_email_notification_setting": "メール通知設定の更新に失敗しました", + "notify_me_when": "通知する条件", + "property_changes": "プロパティの変更", + "property_changes_description": "担当者、優先度、見積もりなどのプロパティが変更されたときに通知します。", + "state_change": "状態の変更", + "state_change_description": "問題が別の状態に移動したときに通知します", + "issue_completed": "問題が完了", + "issue_completed_description": "問題が完了したときのみ通知します", + "comments": "コメント", + "comments_description": "誰かが問題にコメントを残したときに通知します", + "mentions": "メンション", + "mentions_description": "コメントや説明で誰かが自分をメンションしたときのみ通知します", + "create_your_workspace": "ワークスペースを作成", + "only_your_instance_admin_can_create_workspaces": "ワークスペースを作成できるのはインスタンス管理者のみです", + "only_your_instance_admin_can_create_workspaces_description": "インスタンス管理者のメールアドレスを知っている場合は、以下のボタンをクリックして連絡を取ってください。", + "go_back": "戻る", + "request_instance_admin": "インスタンス管理者にリクエスト", + "plane_logo": "プレーンロゴ", + "workspace_creation_disabled": "ワークスペースの作成が無効化されています", + "workspace_request_subject": "新しいワークスペースのリクエスト", + "workspace_request_body": "インスタンス管理者様\n\nURL [/workspace-name] で新しいワークスペースを作成してください。[ワークスペース作成の目的]\n\nありがとうございます。\n{{firstName}} {{lastName}}\n{{email}}", + "creating_workspace": "ワークスペースを作成中", + "workspace_created_successfully": "ワークスペースが正常に作成されました", + "create_workspace_page": "ワークスペース作成ページ", + "workspace_could_not_be_created_please_try_again": "ワークスペースを作成できませんでした。もう一度お試しください。", + "workspace_could_not_be_created_please_try_again_description": "ワークスペースの作成中にエラーが発生しました。もう一度お試しください。", + "this_is_a_required_field": "これは必須フィールドです。", + "name_your_workspace": "ワークスペースに名前を付ける", + "workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "ワークスペース名にはスペース、ダッシュ、アンダースコア、英数字のみを含めることができます。", + "limit_your_name_to_80_characters": "名前は80文字以内にしてください。", + "set_your_workspace_url": "ワークスペースのURLを設定", + "limit_your_url_to_48_characters": "URLは48文字以内にしてください。", + "how_many_people_will_use_this_workspace": "このワークスペースを使用する人数は?", + "how_many_people_will_use_this_workspace_description": "購入するシート数を決定するのに役立ちます。", + "select_a_range": "範囲を選択", + "urls_can_contain_only_dash_and_alphanumeric_characters": "URLにはダッシュと英数字のみを含めることができます。", + "something_familiar_and_recognizable_is_always_best": "親しみやすく認識しやすいものが常に最適です。", + "workspace_url_is_already_taken": "ワークスペースのURLは既に使用されています!", + "old_password": "古いパスワード", + "general_settings": "一般設定", + "sign_out": "サインアウト", + "signing_out": "サインアウト中", + "active_cycles": "アクティブサイクル", + "active_cycles_description": "プロジェクト全体のサイクルを監視し、高優先度の問題を追跡し、注意が必要なサイクルにズームインします。", + "on_demand_snapshots_of_all_your_cycles": "すべてのサイクルのオンデマンドスナップショット", + "upgrade": "アップグレード", + "10000_feet_view": "すべてのアクティブサイクルの10,000フィートビュー。", + "10000_feet_view_description": "各プロジェクトのサイクルを個別に見るのではなく、すべてのプロジェクトのサイクルを一度に見るためにズームアウトします。", + "get_snapshot_of_each_active_cycle": "各アクティブサイクルのスナップショットを取得します。", + "get_snapshot_of_each_active_cycle_description": "すべてのアクティブサイクルの高レベルのメトリクスを追跡し、進捗状況を確認し、期限に対するスコープの感覚を得ます。", + "compare_burndowns": "バーンダウンを比較します。", + "compare_burndowns_description": "各チームのパフォーマンスを監視し、各サイクルのバーンダウンレポートを覗き見します。", + "quickly_see_make_or_break_issues": "重要な問題をすばやく確認します。", + "quickly_see_make_or_break_issues_description": "各サイクルの期限に対する高優先度の問題をプレビューします。1クリックでサイクルごとにすべてを確認できます。", + "zoom_into_cycles_that_need_attention": "注意が必要なサイクルにズームインします。", + "zoom_into_cycles_that_need_attention_description": "期待に沿わないサイクルの状態を1クリックで調査します。", + "stay_ahead_of_blockers": "ブロッカーを先取りします。", + "stay_ahead_of_blockers_description": "プロジェクト間の課題を見つけ、他のビューからは明らかでないサイクル間の依存関係を確認します。", + "analytics": "分析", + "workspace_invites": "ワークスペースの招待", + "workspace_settings": "ワークスペース設定", + "enter_god_mode": "ゴッドモードに入る", + "workspace_logo": "ワークスペースロゴ", + "new_issue": "新しい問題", + "home": "ホーム", + "your_work": "あなたの作業", + "drafts": "下書き", + "projects": "プロジェクト", + "views": "ビュー", + "workspace": "ワークスペース", + "archives": "アーカイブ", + "settings": "設定", + "failed_to_move_favorite": "お気に入りの移動に失敗しました", + "your_favorites": "あなたのお気に入り", + "no_favorites_yet": "まだお気に入りはありません", + "create_folder": "フォルダーを作成", + "new_folder": "新しいフォルダー", + "favorite_updated_successfully": "お気に入りが正常に更新されました", + "favorite_created_successfully": "お気に入りが正常に作成されました", + "folder_already_exists": "フォルダーは既に存在します", + "folder_name_cannot_be_empty": "フォルダー名を空にすることはできません", + "something_went_wrong": "何かがうまくいきませんでした", + "failed_to_reorder_favorite": "お気に入りの並べ替えに失敗しました", + "favorite_removed_successfully": "お気に入りが正常に削除されました", + "failed_to_create_favorite": "お気に入りの作成に失敗しました", + "failed_to_rename_favorite": "お気に入りの名前変更に失敗しました", + "project_link_copied_to_clipboard": "プロジェクトリンクがクリップボードにコピーされました", + "link_copied": "リンクがコピーされました", + "your_projects": "あなたのプロジェクト", + "add_project": "プロジェクトを追加", + "create_project": "プロジェクトを作成", + "failed_to_remove_project_from_favorites": "お気に入りからプロジェクトを削除できませんでした。もう一度お試しください。", + "project_created_successfully": "プロジェクトが正常に作成されました", + "project_created_successfully_description": "プロジェクトが正常に作成されました。今すぐ問題を追加し始めることができます。", + "project_cover_image_alt": "プロジェクトカバー画像", + "name_is_required": "名前は必須です", + "title_should_be_less_than_255_characters": "タイトルは255文字未満である必要があります", + "project_name": "プロジェクト名", + "project_id_must_be_at_least_1_character": "プロジェクトIDは少なくとも1文字である必要があります", + "project_id_must_be_at_most_5_characters": "プロジェクトIDは最大5文字である必要があります", + "project_id": "プロジェクトID", + "project_id_tooltip_content": "プロジェクト内の問題を一意に識別するのに役立ちます。最大5文字。", + "description_placeholder": "説明...", + "only_alphanumeric_non_latin_characters_allowed": "英数字と非ラテン文字のみが許可されます。", + "project_id_is_required": "プロジェクトIDは必須です", + "select_network": "ネットワークを選択", + "lead": "リード", + "private": "プライベート", + "public": "パブリック", + "accessible_only_by_invite": "招待によってのみアクセス可能", + "anyone_in_the_workspace_except_guests_can_join": "ゲストを除くワークスペース内の誰でも参加できます", + "creating": "作成中", + "creating_project": "プロジェクトを作成中", + "adding_project_to_favorites": "プロジェクトをお気に入りに追加中", + "project_added_to_favorites": "プロジェクトがお気に入りに追加されました", + "couldnt_add_the_project_to_favorites": "プロジェクトをお気に入りに追加できませんでした。もう一度お試しください。", + "removing_project_from_favorites": "お気に入りからプロジェクトを削除中", + "project_removed_from_favorites": "プロジェクトがお気に入りから削除されました", + "couldnt_remove_the_project_from_favorites": "お気に入りからプロジェクトを削除できませんでした。もう一度お試しください。", + "add_to_favorites": "お気に入りに追加", + "remove_from_favorites": "お気に入りから削除", + "publish_settings": "公開設定", + "publish": "公開", + "copy_link": "リンクをコピー", + "leave_project": "プロジェクトを離れる", + "join_the_project_to_rearrange": "プロジェクトに参加して並べ替え", + "drag_to_rearrange": "ドラッグして並べ替え", + "congrats": "おめでとうございます!", + "project": "プロジェクト", + "open_project": "プロジェクトを開く", + "issues": "問題", + "cycles": "サイクル", + "modules": "モジュール", + "pages": "ページ", + "intake": "インテーク", + "time_tracking": "時間追跡", + "work_management": "作業管理", + "projects_and_issues": "プロジェクトと問題", + "projects_and_issues_description": "このプロジェクトでオンまたはオフに切り替えます。", + "cycles_description": "プロジェクトごとに作業をタイムボックス化し、期間ごとに頻度を変更します。", + "modules_description": "独自のリードと担当者を持つサブプロジェクトのようなセットアップに作業をグループ化します。", + "views_description": "後で使用するために、または共有するためにソート、フィルター、表示オプションを保存します。", + "pages_description": "何かを書くように何かを書く。", + "intake_description": "購読している問題についての通知を受け取るには、これを有効にしてください。", + "time_tracking_description": "問題とプロジェクトに費やした時間を追跡します。", + "work_management_description": "作業とプロジェクトを簡単に管理します。", + "documentation": "ドキュメント", + "message_support": "サポートにメッセージを送る", + "contact_sales": "営業に連絡", + "hyper_mode": "ハイパーモード", + "keyboard_shortcuts": "キーボードショートカット", + "whats_new": "新着情報", + "version": "バージョン", + "we_are_having_trouble_fetching_the_updates": "更新の取得に問題が発生しています。", + "our_changelogs": "私たちの変更履歴", + "for_the_latest_updates": "最新の更新情報については", + "please_visit": "訪問してください", + "docs": "ドキュメント", + "full_changelog": "完全な変更履歴", + "support": "サポート", + "discord": "ディスコード", + "powered_by_plane_pages": "Plane Pagesによって提供されています", + "please_select_at_least_one_invitation": "少なくとも1つの招待を選択してください。", + "please_select_at_least_one_invitation_description": "ワークスペースに参加するために少なくとも1つの招待を選択してください。", + "we_see_that_someone_has_invited_you_to_join_a_workspace": "誰かがワークスペースに参加するようにあなたを招待したことがわかります", + "join_a_workspace": "ワークスペースに参加", + "we_see_that_someone_has_invited_you_to_join_a_workspace_description": "誰かがワークスペースに参加するようにあなたを招待したことがわかります", + "join_a_workspace_description": "ワークスペースに参加", + "accept_and_join": "受け入れて参加", + "go_home": "ホームに戻る", + "no_pending_invites": "保留中の招待はありません", + "you_can_see_here_if_someone_invites_you_to_a_workspace": "誰かがワークスペースに招待した場合、ここで確認できます", + "back_to_home": "ホームに戻る", + "workspace_name": "ワークスペース名" } diff --git a/web/app/[workspaceSlug]/(projects)/active-cycles/header.tsx b/web/app/[workspaceSlug]/(projects)/active-cycles/header.tsx index 4edf41bbdba..6416aee125c 100644 --- a/web/app/[workspaceSlug]/(projects)/active-cycles/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/active-cycles/header.tsx @@ -1,6 +1,6 @@ "use client"; - import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; // ui import { Breadcrumbs, ContrastIcon, Header } from "@plane/ui"; // components @@ -8,15 +8,17 @@ import { BreadcrumbLink } from "@/components/common"; // plane web components import { UpgradeBadge } from "@/plane-web/components/workspace"; -export const WorkspaceActiveCycleHeader = observer(() => ( -
- - +export const WorkspaceActiveCycleHeader = observer(() => { + const { t } = useTranslation(); + return ( +
+ + } /> } @@ -25,4 +27,5 @@ export const WorkspaceActiveCycleHeader = observer(() => (
-)); + ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/analytics/header.tsx b/web/app/[workspaceSlug]/(projects)/analytics/header.tsx index 4aa66e2a433..fe55f8cbdc9 100644 --- a/web/app/[workspaceSlug]/(projects)/analytics/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/analytics/header.tsx @@ -3,8 +3,8 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; -// icons import { BarChart2, PanelRight } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; // ui import { Breadcrumbs, Header } from "@plane/ui"; // components @@ -13,8 +13,8 @@ import { BreadcrumbLink } from "@/components/common"; import { cn } from "@/helpers/common.helper"; // hooks import { useAppTheme } from "@/hooks/store"; - export const WorkspaceAnalyticsHeader = observer(() => { + const { t } = useTranslation(); const searchParams = useSearchParams(); const analytics_tab = searchParams.get("analytics_tab"); // store hooks @@ -41,7 +41,7 @@ export const WorkspaceAnalyticsHeader = observer(() => { } />} + link={} />} /> {analytics_tab === "custom" ? ( diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx index f31ff959dce..d6743e8f2ba 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/layout.tsx @@ -4,6 +4,7 @@ import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import useSWR from "swr"; // components +import { useTranslation } from "@plane/i18n"; import { AppHeader, ContentWrapper } from "@/components/core"; import { ProfileSidebar } from "@/components/profile"; // constants @@ -32,6 +33,7 @@ const UseProfileLayout: React.FC = observer((props) => { const pathname = usePathname(); // store hooks const { allowPermissions } = useUserPermissions(); + const { t } = useTranslation(); // derived values const isAuthorized = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], @@ -79,7 +81,7 @@ const UseProfileLayout: React.FC = observer((props) => {
{children}
) : (
- You do not have the permission to access this page. + {t("you_do_not_have_the_permission_to_access_this_page")}
)}
diff --git a/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx b/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx index 4acb93217f6..e002f8f6641 100644 --- a/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx +++ b/web/app/[workspaceSlug]/(projects)/profile/[userId]/navbar.tsx @@ -2,6 +2,7 @@ import React from "react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; +import { useTranslation } from "@plane/i18n"; // components // constants @@ -14,7 +15,7 @@ type Props = { export const ProfileNavbar: React.FC = (props) => { const { isAuthorized } = props; - + const { t } = useTranslation(); const { workspaceSlug, userId } = useParams(); const pathname = usePathname(); @@ -32,7 +33,7 @@ export const ProfileNavbar: React.FC = (props) => { : "border-transparent" }`} > - {tab.label} + {t(tab.label)} ))} diff --git a/web/app/create-workspace/page.tsx b/web/app/create-workspace/page.tsx index 36bc8978ad2..77b71492f3a 100644 --- a/web/app/create-workspace/page.tsx +++ b/web/app/create-workspace/page.tsx @@ -5,6 +5,7 @@ import { observer } from "mobx-react"; import Image from "next/image"; import Link from "next/link"; import { useTheme } from "next-themes"; +import { useTranslation } from "@plane/i18n"; import { IWorkspace } from "@plane/types"; // components import { Button, getButtonStyling } from "@plane/ui"; @@ -22,6 +23,7 @@ import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue import WorkspaceCreationDisabled from "@/public/workspace/workspace-creation-disabled.png"; const CreateWorkspacePage = observer(() => { + const { t } = useTranslation(); // router const router = useAppRouter(); // store hooks @@ -38,6 +40,17 @@ const CreateWorkspacePage = observer(() => { // derived values const isWorkspaceCreationDisabled = getIsWorkspaceCreationDisabled(); + // methods + const getMailtoHref = () => { + const subject = t("workspace_request_subject"); + const body = t("workspace_request_body") + .replace("{{firstName}}", currentUser?.first_name || "") + .replace("{{lastName}}", currentUser?.last_name || "") + .replace("{{email}}", currentUser?.email || ""); + + return `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + }; + const onSubmit = async (workspace: IWorkspace) => { await updateUserProfile({ last_workspace_id: workspace.id }).then(() => router.push(`/${workspace.slug}`)); }; @@ -54,7 +67,7 @@ const CreateWorkspacePage = observer(() => { href="/" >
- Plane logo + {t("plane_logo")}
@@ -64,27 +77,30 @@ const CreateWorkspacePage = observer(() => {
{isWorkspaceCreationDisabled ? (
- Workspace creation disabled -
Only your instance admin can create workspaces
-

- If you know your instance admin's email address,
click the button below to get in touch with - them. + {t("workspace_creation_disabled")} +

+ {t("only_your_instance_admin_can_create_workspaces")} +
+

+ {t("only_your_instance_admin_can_create_workspaces_description")}

- - Request instance admin + + {t("request_instance_admin")}
) : (
-

Create your workspace

+

{t("create_your_workspace")}

{ // router const router = useAppRouter(); // store hooks + const { t } = useTranslation(); const { captureEvent, joinWorkspaceMetricGroup } = useEventTracker(); const { data: currentUser } = useUser(); const { updateUserProfile } = useUserProfile(); @@ -72,8 +73,8 @@ const UserInvitationsPage = observer(() => { if (invitationsRespond.length === 0) { setToast({ type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Please select at least one invitation.", + title: t("error"), + message: t("please_select_at_least_one_invitation"), }); return; } @@ -107,8 +108,8 @@ const UserInvitationsPage = observer(() => { .catch(() => { setToast({ type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Something went wrong, Please try again.", + title: t("error"), + message: t("something_went_wrong_please_try_again"), }); setIsJoiningWorkspaces(false); }); @@ -122,8 +123,8 @@ const UserInvitationsPage = observer(() => { }); setToast({ type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Something went wrong, Please try again.", + title: t("error"), + message: t("something_went_wrong_please_try_again"), }); setIsJoiningWorkspaces(false); }); @@ -152,8 +153,8 @@ const UserInvitationsPage = observer(() => { invitations.length > 0 ? (
-
We see that someone has invited you to
-

Join a workspace

+
{t("we_see_that_someone_has_invited_you_to_join_a_workspace")}
+

{t("join_a_workspace")}

{invitations.map((invitation) => { const isSelected = invitationsRespond.includes(invitation.id); @@ -207,12 +208,12 @@ const UserInvitationsPage = observer(() => { disabled={isJoiningWorkspaces || invitationsRespond.length === 0} loading={isJoiningWorkspaces} > - Accept & Join + {t("accept_and_join")} @@ -222,11 +223,11 @@ const UserInvitationsPage = observer(() => { ) : (
router.push("/"), }} /> diff --git a/web/app/profile/activity/page.tsx b/web/app/profile/activity/page.tsx index afc9b29bf8d..a2a8cad851f 100644 --- a/web/app/profile/activity/page.tsx +++ b/web/app/profile/activity/page.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; // ui import { Button } from "@plane/ui"; // components @@ -18,6 +19,7 @@ import { EmptyStateType } from "@/constants/empty-state"; const PER_PAGE = 100; const ProfileActivityPage = observer(() => { + const { t } = useTranslation(); // states const [pageCount, setPageCount] = useState(1); const [totalPages, setTotalPages] = useState(0); @@ -55,12 +57,12 @@ const ProfileActivityPage = observer(() => { <> - + {activityPages} {isLoadMoreVisible && (
)} diff --git a/web/app/profile/appearance/page.tsx b/web/app/profile/appearance/page.tsx index 775ff637b04..5b1a96c5be1 100644 --- a/web/app/profile/appearance/page.tsx +++ b/web/app/profile/appearance/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useTheme } from "next-themes"; +import { useTranslation } from "@plane/i18n"; import { IUserTheme } from "@plane/types"; import { setPromiseToast } from "@plane/ui"; // components @@ -15,8 +16,8 @@ import { I_THEME_OPTION, THEME_OPTIONS } from "@/constants/themes"; import { applyTheme, unsetCustomCssVariables } from "@/helpers/theme.helper"; // hooks import { useUserProfile } from "@/hooks/store"; - const ProfileAppearancePage = observer(() => { + const { t } = useTranslation(); const { setTheme } = useTheme(); // states const [currentTheme, setCurrentTheme] = useState(null); @@ -62,11 +63,11 @@ const ProfileAppearancePage = observer(() => { {userProfile ? ( - +
-

Theme

-

Select or customize your interface color scheme.

+

{t("theme")}

+

{t("select_or_customize_your_interface_color_scheme")}

diff --git a/web/app/profile/notifications/page.tsx b/web/app/profile/notifications/page.tsx index b39563378b1..cbdcd147d73 100644 --- a/web/app/profile/notifications/page.tsx +++ b/web/app/profile/notifications/page.tsx @@ -2,6 +2,7 @@ import useSWR from "swr"; // components +import { useTranslation } from "@plane/i18n"; import { PageHead } from "@/components/core"; import { ProfileSettingContentHeader, ProfileSettingContentWrapper } from "@/components/profile"; import { EmailNotificationForm } from "@/components/profile/notification"; @@ -12,6 +13,7 @@ import { UserService } from "@/services/user.service"; const userService = new UserService(); export default function ProfileNotificationPage() { + const { t } = useTranslation(); // fetching user email notification settings const { data, isLoading } = useSWR("CURRENT_USER_EMAIL_NOTIFICATION_SETTINGS", () => userService.currentUserEmailNotificationSettings() @@ -23,11 +25,11 @@ export default function ProfileNotificationPage() { return ( <> - + diff --git a/web/app/profile/page.tsx b/web/app/profile/page.tsx index 1a4470ea331..22a0d4ba142 100644 --- a/web/app/profile/page.tsx +++ b/web/app/profile/page.tsx @@ -1,6 +1,7 @@ "use client"; import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; // components import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; @@ -9,6 +10,7 @@ import { ProfileSettingContentWrapper, ProfileForm } from "@/components/profile" import { useUser } from "@/hooks/store"; const ProfileSettingsPage = observer(() => { + const { t } = useTranslation(); // store hooks const { data: currentUser, userProfile } = useUser(); @@ -21,7 +23,7 @@ const ProfileSettingsPage = observer(() => { return ( <> - + diff --git a/web/app/profile/security/page.tsx b/web/app/profile/security/page.tsx index 594816cc165..48996de34f0 100644 --- a/web/app/profile/security/page.tsx +++ b/web/app/profile/security/page.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; import { Eye, EyeOff } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; // ui import { Button, Input, TOAST_TYPE, setToast } from "@plane/ui"; // components @@ -55,6 +56,8 @@ const SecurityPage = observer(() => { const oldPassword = watch("old_password"); const password = watch("new_password"); const confirmPassword = watch("confirm_password"); + // i18n + const { t } = useTranslation(); const isNewPasswordSameAsOldPassword = oldPassword !== "" && password !== "" && password === oldPassword; @@ -76,8 +79,8 @@ const SecurityPage = observer(() => { setShowPassword(defaultShowPassword); setToast({ type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: "Password changed successfully.", + title: t("success"), + message: t("password_changed_successfully"), }); } catch (err: any) { const errorInfo = authErrorHandler(err.error_code?.toString()); @@ -85,7 +88,7 @@ const SecurityPage = observer(() => { type: TOAST_TYPE.ERROR, title: errorInfo?.title ?? "Error!", message: - typeof errorInfo?.message === "string" ? errorInfo.message : "Something went wrong. Please try again 2.", + typeof errorInfo?.message === "string" ? errorInfo.message : t("something_went_wrong_please_try_again"), }); } }; @@ -109,17 +112,17 @@ const SecurityPage = observer(() => { <> - +
-

Current password

+

{t("current_password")}

( { type={showPassword?.oldPassword ? "text" : "password"} value={value} onChange={onChange} - placeholder="Old password" + placeholder={t("old_password")} className="w-full" hasError={Boolean(errors.old_password)} /> @@ -148,20 +151,20 @@ const SecurityPage = observer(() => { {errors.old_password && {errors.old_password.message}}
-

New password

+

{t("new_password")}

( {
{passwordSupport} {isNewPasswordSameAsOldPassword && !isPasswordInputFocused && ( - New password must be different from old password + {t("new_password_must_be_different_from_old_password")} )}
-

Confirm password

+

{t("confirm_password")}

( { )}
{!!confirmPassword && password !== confirmPassword && renderPasswordMatchError && ( - Passwords don{"'"}t match + {t("passwords_dont_match")} )}
diff --git a/web/app/profile/sidebar.tsx b/web/app/profile/sidebar.tsx index 2f456415760..d3b98642161 100644 --- a/web/app/profile/sidebar.tsx +++ b/web/app/profile/sidebar.tsx @@ -92,8 +92,8 @@ export const ProfileLayoutSidebar = observer(() => { .catch(() => setToast({ type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Failed to sign out. Please try again.", + title: t("error"), + message: t("failed_to_sign_out_please_try_again"), }) ) .finally(() => setIsSigningOut(false)); @@ -239,7 +239,7 @@ export const ProfileLayoutSidebar = observer(() => { disabled={isSigningOut} > - {!sidebarCollapsed && {isSigningOut ? "Signing out..." : "Sign out"}} + {!sidebarCollapsed && {isSigningOut ? `${t("signing_out")}...` : t("sign_out")}} diff --git a/web/ce/constants/dashboard.ts b/web/ce/constants/dashboard.ts index 0df2719a772..e2567d5804a 100644 --- a/web/ce/constants/dashboard.ts +++ b/web/ce/constants/dashboard.ts @@ -13,8 +13,9 @@ import { EUserPermissions } from "@/plane-web/constants/user-permissions"; import { TSidebarUserMenuItemKeys, TSidebarWorkspaceMenuItemKeys } from "@/plane-web/types/dashboard"; export type TSidebarMenuItems = { - key: T; + value: T; label: string; + key: string; href: string; access: EUserPermissions[]; highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => boolean; @@ -25,16 +26,18 @@ export type TSidebarUserMenuItems = TSidebarMenuItems; export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [ { - key: "home", + value: "home", label: "Home", + key: "home", href: ``, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`, Icon: Home, }, { - key: "your-work", + value: "your-work", label: "Your work", + key: "your_work", href: "/profile", access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string, options?: TLinkOptions) => @@ -42,16 +45,18 @@ export const SIDEBAR_USER_MENU_ITEMS: TSidebarUserMenuItems[] = [ Icon: UserActivityIcon, }, { - key: "notifications", + value: "notifications", label: "Inbox", + key: "notifications", href: `/notifications`, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`), Icon: Inbox, }, { - key: "drafts", + value: "drafts", label: "Drafts", + key: "drafts", href: `/drafts`, access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/drafts/`), @@ -63,6 +68,7 @@ export type TSidebarWorkspaceMenuItems = TSidebarMenuItems> = { projects: { + value: "projects", key: "projects", label: "Projects", href: `/projects`, @@ -71,7 +77,8 @@ export const SIDEBAR_WORKSPACE_MENU: Partial = (props) => { const { password, isFocused = false } = props; + const { t } = useTranslation(); // derived values const strength = useMemo(() => getPasswordStrength(password), [password]); const strengthBars = useMemo(() => { @@ -24,40 +26,40 @@ export const PasswordStrengthMeter: FC = (props) => { case E_PASSWORD_STRENGTH.EMPTY: { return { bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`], - text: "Please enter your password.", + text: t("please_enter_your_password"), textColor: "text-custom-text-100", }; } case E_PASSWORD_STRENGTH.LENGTH_NOT_VALID: { return { bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`], - text: "Password length should me more than 8 characters.", + text: t("password_length_should_me_more_than_8_characters"), textColor: "text-red-500", }; } case E_PASSWORD_STRENGTH.STRENGTH_NOT_VALID: { return { bars: [`bg-red-500`, `bg-custom-text-100`, `bg-custom-text-100`], - text: "Password is weak.", + text: t("password_is_weak"), textColor: "text-red-500", }; } case E_PASSWORD_STRENGTH.STRENGTH_VALID: { return { bars: [`bg-green-500`, `bg-green-500`, `bg-green-500`], - text: "Password is strong.", + text: t("password_is_strong"), textColor: "text-green-500", }; } default: { return { bars: [`bg-custom-text-100`, `bg-custom-text-100`, `bg-custom-text-100`], - text: "Please enter your password.", + text: t("please_enter_your_password"), textColor: "text-custom-text-100", }; } } - }, [strength]); + }, [strength,t]); const isPasswordMeterVisible = isFocused ? true : strength === E_PASSWORD_STRENGTH.STRENGTH_VALID ? false : true; diff --git a/web/core/components/core/theme/custom-theme-selector.tsx b/web/core/components/core/theme/custom-theme-selector.tsx index da903439307..d66fbe82f83 100644 --- a/web/core/components/core/theme/custom-theme-selector.tsx +++ b/web/core/components/core/theme/custom-theme-selector.tsx @@ -1,29 +1,16 @@ "use client"; +import { useMemo } from "react"; import { observer } from "mobx-react"; import { Controller, useForm } from "react-hook-form"; // types +import { useTranslation } from "@plane/i18n"; import { IUserTheme } from "@plane/types"; // ui import { Button, InputColorPicker, setPromiseToast } from "@plane/ui"; // hooks import { useUserProfile } from "@/hooks/store"; -const inputRules = { - minLength: { - value: 7, - message: "Enter a valid hex code of 6 characters", - }, - maxLength: { - value: 7, - message: "Enter a valid hex code of 6 characters", - }, - pattern: { - value: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, - message: "Enter a valid hex code of 6 characters", - }, -}; - type TCustomThemeSelector = { applyThemeChange: (theme: Partial) => void; }; @@ -32,7 +19,7 @@ export const CustomThemeSelector: React.FC = observer((pro const { applyThemeChange } = props; // hooks const { data: userProfile, updateUserTheme } = useUserProfile(); - + const { t } = useTranslation(); const { control, formState: { errors, isSubmitting }, @@ -51,6 +38,24 @@ export const CustomThemeSelector: React.FC = observer((pro }, }); + const inputRules = useMemo( + () => ({ + minLength: { + value: 7, + message: t("enter_a_valid_hex_code_of_6_characters"), + }, + maxLength: { + value: 7, + message: t("enter_a_valid_hex_code_of_6_characters"), + }, + pattern: { + value: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, + message: t("enter_a_valid_hex_code_of_6_characters"), + }, + }), + [t] // Empty dependency array since these rules never change + ); + const handleUpdateTheme = async (formData: Partial) => { const payload: IUserTheme = { background: formData.background, @@ -66,14 +71,14 @@ export const CustomThemeSelector: React.FC = observer((pro const updateCurrentUserThemePromise = updateUserTheme(payload); setPromiseToast(updateCurrentUserThemePromise, { - loading: "Updating theme...", + loading: t("updating_theme"), success: { - title: "Success!", - message: () => "Theme updated successfully!", + title: t("success"), + message: () => t("theme_updated_successfully"), }, error: { - title: "Error!", - message: () => "Failed to Update the theme", + title: t("error"), + message: () => t("failed_to_update_the_theme"), }, }); @@ -91,16 +96,16 @@ export const CustomThemeSelector: React.FC = observer((pro return (
-

Customize your theme

+

{t("customize_your_theme")}

-

Background color

+

{t("background_color")}

( = observer((pro
-

Text color

+

{t("text_color")}

( = observer((pro
-

Primary(Theme) color

+

{t("primary_color")}

( = observer((pro
-

Sidebar background color

+

{t("sidebar_background_color")}

( = observer((pro
-

Sidebar text color

+

{t("sidebar_text_color")}

( = observer((pro
diff --git a/web/core/components/core/theme/theme-switch.tsx b/web/core/components/core/theme/theme-switch.tsx index b79e2104eb2..7a188a48aa0 100644 --- a/web/core/components/core/theme/theme-switch.tsx +++ b/web/core/components/core/theme/theme-switch.tsx @@ -1,6 +1,7 @@ "use client"; import { FC } from "react"; +import { useTranslation } from "@plane/i18n"; // constants import { CustomSelect } from "@plane/ui"; import { THEME_OPTIONS, I_THEME_OPTION } from "@/constants/themes"; @@ -13,7 +14,7 @@ type Props = { export const ThemeSwitch: FC = (props) => { const { value, onChange } = props; - + const { t } = useTranslation(); return ( = (props) => { }} />
- {value.label} + {t(value.key)}
) : ( - "Select your theme" + t("select_your_theme") ) } onChange={onChange} @@ -72,7 +73,7 @@ export const ThemeSwitch: FC = (props) => { }} />
- {themeOption.label} + {t(themeOption.key)}
))} diff --git a/web/core/components/global/product-updates/footer.tsx b/web/core/components/global/product-updates/footer.tsx index 6dd2638332b..5d402e85eef 100644 --- a/web/core/components/global/product-updates/footer.tsx +++ b/web/core/components/global/product-updates/footer.tsx @@ -1,4 +1,5 @@ import Image from "next/image"; +import { useTranslation } from "@plane/i18n"; // ui import { getButtonStyling } from "@plane/ui"; // helpers @@ -6,38 +7,40 @@ import { cn } from "@/helpers/common.helper"; // assets import PlaneLogo from "@/public/plane-logos/blue-without-text.png"; -export const ProductUpdatesFooter = () => ( -
-
- { + const { t } = useTranslation(); + return ( +
+ -); + Plane + {t("powered_by_plane_pages")} + +
+ ); +}; diff --git a/web/core/components/global/product-updates/modal.tsx b/web/core/components/global/product-updates/modal.tsx index 4288d68ee35..c2700e18143 100644 --- a/web/core/components/global/product-updates/modal.tsx +++ b/web/core/components/global/product-updates/modal.tsx @@ -1,5 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; +import { useTranslation } from "@plane/i18n"; // ui import { EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // components @@ -16,7 +17,7 @@ export type ProductUpdatesModalProps = { export const ProductUpdatesModal: FC = observer((props) => { const { isOpen, handleClose } = props; - + const { t } = useTranslation(); const { config } = useInstance(); return ( @@ -27,17 +28,17 @@ export const ProductUpdatesModal: FC = observer((props