Merge pull request #2230 from bluewave-labs/fix/untranslated-menu-uptime

Fix/untranslated sidebar and uptime page
This commit is contained in:
Alexander Holliday
2025-05-09 19:06:45 -07:00
committed by GitHub
10 changed files with 303 additions and 80 deletions

View File

@@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({
isAdmin,
label = "Create new",
label,
isLoading = true,
path,
bulkPath,
@@ -14,6 +14,8 @@ const CreateMonitorHeader = ({
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
// Use the provided label or fall back to the translated default
if (!isAdmin) return null;
@@ -30,7 +32,7 @@ const CreateMonitorHeader = ({
color="accent"
onClick={() => navigate(path)}
>
{label}
{label || t("createNew")}
</Button>
{bulkPath && (
<Button

View File

@@ -47,46 +47,47 @@ import "./index.css";
import { useLocation, useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
const menu = [
{ name: "Uptime", path: "uptime", icon: <Monitors /> },
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: "Distributed uptime",
name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: <DistributedUptimeIcon />,
},
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
// { name: t("menu.integrations"), path: "integrations", icon: <Integrations /> },
{
name: "Settings",
name: t("menu.settings"),
icon: <Settings />,
path: "settings",
},
];
const otherMenuItems = [
{ name: "Support", path: "support", icon: <Support /> },
const getOtherMenuItems = (t) => [
{ name: t("menu.support"), path: "support", icon: <Support /> },
{
name: "Discussions",
name: t("menu.discussions"),
path: "discussions",
icon: <Discussions />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
];
const accountMenuItems = [
{ name: "Profile", path: "account/profile", icon: <UserSvg /> },
{ name: "Password", path: "account/password", icon: <LockSvg /> },
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
const getAccountMenuItems = (t) => [
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
];
/* TODO this could be a key in nested Path would be the link */
@@ -118,7 +119,12 @@ function Sidebar() {
const navigate = useNavigate();
const location = useLocation();
const dispatch = useDispatch();
const { t } = useTranslation();
const authState = useSelector((state) => state.auth);
const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false });
const [anchorEl, setAnchorEl] = useState(null);
@@ -726,7 +732,11 @@ function Sidebar() {
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role}
{authState.user?.role?.includes("superadmin") ? t("roles.superAdmin") :
authState.user?.role?.includes("admin") ? t("roles.admin") :
authState.user?.role?.includes("user") ? t("roles.teamMember") :
authState.user?.role?.includes("demo") ? t("roles.demoUser") :
authState.user?.role}
</Typography>
</Box>
<Stack

View File

@@ -2,11 +2,11 @@ import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import Select from "../../Inputs/Select";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
@@ -21,6 +21,7 @@ import { useGetInviteToken } from "../../../Hooks/inviteHooks";
const TeamPanel = () => {
const theme = useTheme();
const { t } = useTranslation();
const SPACING_GAP = theme.spacing(12);
const [toInvite, setToInvite] = useState({
@@ -39,7 +40,7 @@ const TeamPanel = () => {
const headers = [
{
id: "name",
content: "Name",
content: t("teamPanel.table.name"),
render: (row) => {
return (
<Stack>
@@ -47,16 +48,16 @@ const TeamPanel = () => {
{row.firstName + " " + row.lastName}
</Typography>
<Typography>
Created {new Date(row.createdAt).toLocaleDateString()}
{t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()}
</Typography>
</Stack>
);
},
},
{ id: "email", content: "Email", render: (row) => row.email },
{ id: "email", content: t("teamPanel.table.email"), render: (row) => row.email },
{
id: "role",
content: "Role",
content: t("teamPanel.table.role"),
render: (row) => row.role,
},
];
@@ -78,10 +79,10 @@ const TeamPanel = () => {
useEffect(() => {
const ROLE_MAP = {
superadmin: "Super admin",
admin: "Admin",
user: "Team member",
demo: "Demo User",
superadmin: t("roles.superAdmin"),
admin: t("roles.admin"),
user: t("roles.teamMember"),
demo: t("roles.demoUser"),
};
let team = members;
if (filter !== "all")
@@ -98,7 +99,7 @@ const TeamPanel = () => {
role: member.role.map((role) => ROLE_MAP[role]).join(","),
}));
setData(team);
}, [filter, members]);
}, [members, filter, t]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
@@ -197,7 +198,7 @@ const TeamPanel = () => {
spellCheck="false"
gap={SPACING_GAP}
>
<Typography component="h1">Team members</Typography>
<Typography component="h1">{t("teamPanel.teamMembers")}</Typography>
<Stack
direction="row"
justifyContent="space-between"
@@ -214,21 +215,21 @@ const TeamPanel = () => {
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
All
{t("teamPanel.filter.all")}
</Button>
<Button
variant="group"
filled={(filter === "admin").toString()}
onClick={() => setFilter("admin")}
>
Super admin
{t("roles.superAdmin")}
</Button>
<Button
variant="group"
filled={(filter === "user").toString()}
onClick={() => setFilter("user")}
>
Member
{t("teamPanel.filter.member")}
</Button>
</ButtonGroup>
</Stack>
@@ -237,22 +238,20 @@ const TeamPanel = () => {
color="accent"
onClick={() => setIsOpen(true)}
>
Invite a team member
{t("teamPanel.inviteTeamMember")}
</Button>
</Stack>
<DataTable
headers={headers}
data={data}
config={{ emptyView: "There are no team members with this role" }}
config={{ emptyView: t("teamPanel.noMembers") }}
/>
</Stack>
<GenericDialog
title={"Invite new team member"}
description={
"When you add a new team member, they will get access to all monitors."
}
title={t("teamPanel.inviteNewTeamMember")}
description={t("teamPanel.inviteDescription")}
open={isOpen}
onClose={closeInviteModal}
theme={theme}
@@ -261,7 +260,7 @@ const TeamPanel = () => {
marginBottom={SPACING_GAP}
type="email"
id="input-team-member"
placeholder="Email"
placeholder={t("teamPanel.email")}
value={toInvite.email}
onChange={handleChange}
error={errors.email ? true : false}
@@ -269,7 +268,7 @@ const TeamPanel = () => {
/>
<Select
id="team-member-role"
placeholder="Select role"
placeholder={t("teamPanel.selectRole")}
isHidden={true}
value={toInvite.role[0]}
onChange={(event) =>
@@ -279,11 +278,11 @@ const TeamPanel = () => {
}))
}
items={[
{ _id: "admin", name: "Admin" },
{ _id: "user", name: "User" },
{ _id: "admin", name: t("roles.admin") },
{ _id: "user", name: t("roles.teamMember") },
]}
/>
{token && <Typography>Invite link</Typography>}
{token && <Typography>{t("teamPanel.inviteLink")}</Typography>}
{token && (
<TextInput
id="invite-token"
@@ -302,7 +301,7 @@ const TeamPanel = () => {
color="error"
onClick={closeInviteModal}
>
Cancel
{t("teamPanel.cancel")}
</Button>
<Button
variant="contained"
@@ -311,7 +310,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
Get token
{t("teamPanel.getToken")}
</Button>
<Button
variant="contained"
@@ -320,7 +319,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
E-mail token
{t("teamPanel.emailToken")}
</Button>
</Stack>
</GenericDialog>

View File

@@ -27,22 +27,14 @@ import { useTranslation } from "react-i18next";
* @returns {JSX.Element} The rendered Filter component.
*/
const typeOptions = [
const getTypeOptions = () => [
{ value: "http", label: "HTTP(S)" },
{ value: "ping", label: "Ping" },
{ value: "docker", label: "Docker" },
{ value: "port", label: "Port" },
];
const statusOptions = [
{ value: "Up", label: "Up" },
{ value: "Down", label: "Down" },
];
const stateOptions = [
{ value: "Active", label: "Active" },
{ value: "Paused", label: "Paused" },
];
// These functions were moved inline to ensure translations are applied correctly
const Filter = ({
selectedTypes,
@@ -58,6 +50,18 @@ const Filter = ({
const theme = useTheme();
const { t } = useTranslation();
const typeOptions = getTypeOptions();
// Create status options with translations
const statusOptions = [
{ value: "Up", label: t("monitorStatus.up") },
{ value: "Down", label: t("monitorStatus.down") },
];
// Create state options with translations
const stateOptions = [
{ value: "Active", label: t("monitorState.active") },
{ value: "Paused", label: t("monitorState.paused") },
];
const handleTypeChange = (event) => {
const selectedValues = event.target.value;
setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined);

View File

@@ -2,10 +2,12 @@ import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./statusBox";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import SkeletonLayout from "./skeleton";
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) return <SkeletonLayout shouldRender={shouldRender} />;
return (
<Stack
@@ -14,15 +16,15 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
justifyContent="space-between"
>
<StatusBox
title="up"
title={t("monitorStatus.up").toUpperCase()}
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
title={t("monitorStatus.down").toUpperCase()}
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
title={t("monitorStatus.paused").toUpperCase()}
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
@@ -31,6 +33,7 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
StatusBoxes.propTypes = {
monitorsSummary: PropTypes.object,
shouldRender: PropTypes.bool,
};
export default StatusBoxes;

View File

@@ -31,11 +31,11 @@ import PropTypes from "prop-types";
import useFetchMonitorsWithSummary from "../../../Hooks/useFetchMonitorsWithSummary";
import useFetchMonitorsWithChecks from "../../../Hooks/useFetchMonitorsWithChecks";
import { useTranslation } from "react-i18next";
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
const TYPES = ["http", "ping", "docker", "port"];
const CreateMonitorButton = ({ shouldRender }) => {
// Utils
const navigate = useNavigate();
const { t } = useTranslation();
if (shouldRender === false) {
return;
}
@@ -49,7 +49,7 @@ const CreateMonitorButton = ({ shouldRender }) => {
navigate("/uptime/create");
}}
>
Create new
{t("createNew")}
</Button>
</Box>
);
@@ -78,10 +78,13 @@ const UptimeMonitors = () => {
// Utils
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const dispatch = useDispatch();
const { t } = useTranslation();
const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }];
// Handlers
const handleChangePage = (event, newPage) => {
setPage(newPage);

View File

@@ -3,6 +3,7 @@ import { useTheme } from "@emotion/react";
import { Box, Typography } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { setGreeting } from "../Features/UI/uiSlice";
const early = [
@@ -133,6 +134,7 @@ const evening = [
const Greeting = ({ type = "" }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const { firstName } = useSelector((state) => state.auth.user);
const index = useSelector((state) => state.ui.greeting.index);
const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate);
@@ -147,7 +149,7 @@ const Greeting = ({ type = "" }) => {
let random = Math.floor(Math.random() * 5);
dispatch(setGreeting({ index: random, lastUpdate: hour }));
}
}, [dispatch, hour]);
}, [dispatch, hour, lastUpdate]);
let greetingArray =
hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening;
@@ -165,7 +167,7 @@ const Greeting = ({ type = "" }) => {
fontSize="inherit"
color={theme.palette.primary.contrastTextTertiary}
>
{prepend},{" "}
{t("greeting.prepend", { defaultValue: prepend })}, {" "}
</Typography>
<Typography
component="span"
@@ -181,7 +183,7 @@ const Greeting = ({ type = "" }) => {
lineHeight={1}
color={theme.palette.primary.contrastTextTertiary}
>
{append} Heres an overview of your {type} monitors.
{t("greeting.append", { defaultValue: append })} {t("greeting.overview", { type: t(`menu.${type}`) })}
</Typography>
</Box>
);

View File

@@ -439,6 +439,69 @@
"pageSpeedApiKeyFieldLabel": "PageSpeed API key",
"pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.",
"pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.",
"reset": "Reset"
"reset": "Reset",
"createNew": "Create new",
"greeting": {
"prepend": "Hey there",
"append": "The afternoon is your playground—let's make it epic!",
"overview": "Here's an overview of your {{type}} monitors."
},
"monitorStatus": {
"up": "up",
"down": "down",
"paused": "paused"
},
"roles": {
"superAdmin": "Super admin",
"admin": "Admin",
"teamMember": "Team member",
"demoUser": "Demo user"
},
"teamPanel": {
"teamMembers": "Team members",
"filter": {
"all": "All",
"member": "Member"
},
"inviteTeamMember": "Invite a team member",
"inviteNewTeamMember": "Invite new team member",
"inviteDescription": "When you add a new team member, they will get access to all monitors.",
"email": "Email",
"selectRole": "Select role",
"inviteLink": "Invite link",
"cancel": "Cancel",
"noMembers": "There are no team members with this role",
"getToken": "Get token",
"emailToken": "E-mail token",
"table": {
"name": "Name",
"email": "Email",
"role": "Role",
"created": "Created"
}
},
"monitorState": {
"paused": "paused",
"resumed": "resumed",
"active": "active"
},
"menu": {
"uptime": "Uptime",
"pagespeed": "Pagespeed",
"infrastructure": "Infrastructure",
"distributedUptime": "Distributed uptime",
"incidents": "Incidents",
"statusPages": "Status pages",
"maintenance": "Maintenance",
"integrations": "Integrations",
"settings": "Settings",
"support": "Support",
"discussions": "Discussions",
"docs": "Docs",
"changelog": "Changelog",
"profile": "Profile",
"password": "Password",
"team": "Team"
}
}

View File

@@ -65,7 +65,18 @@
"authRegisterLastName": "Фамилия",
"authRegisterEmail": "Эл. почта",
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
"authRegisterEmailInvalid": "Пожалуйста, введите действительный адрес электронной почты",
"bulkImport": {
"title": "Массовый импорт",
"selectFileTips": "Выберите CSV-файл для загрузки",
"selectFileDescription": "Вы можете скачать наш <template>шаблон</template> или <sample>пример</sample>",
"selectFile": "Выбрать файл",
"parsingFailed": "Ошибка анализа",
"uploadSuccess": "Мониторы успешно созданы!",
"validationFailed": "Ошибка проверки",
"noFileSelected": "Файл не выбран",
"fallbackPage": "Импортируйте файл для загрузки списка серверов"
},
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
@@ -393,5 +404,68 @@
"maintenanceTableActionMenuDialogTitle": "",
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
"pageSpeedAddApiKey": "как добавить ваш ключ API."
"pageSpeedAddApiKey": "как добавить ваш ключ API.",
"createNew": "Создать новый",
"greeting": {
"prepend": "Привет",
"append": "День прекрасен для новых достижений!",
"overview": "Вот обзор ваших мониторов {{type}}."
},
"monitorStatus": {
"up": "работает",
"down": "не работает",
"paused": "приостановлен"
},
"roles": {
"superAdmin": "Суперадминистратор",
"admin": "Администратор",
"teamMember": "Член команды",
"demoUser": "Демо-пользователь"
},
"teamPanel": {
"teamMembers": "Члены команды",
"filter": {
"all": "Все",
"member": "Член"
},
"inviteTeamMember": "Пригласить члена команды",
"inviteNewTeamMember": "Пригласить нового члена команды",
"inviteDescription": "Когда вы добавляете нового члена команды, он получит доступ ко всем мониторам.",
"email": "Эл. почта",
"selectRole": "Выберите роль",
"inviteLink": "Ссылка для приглашения",
"cancel": "Отмена",
"noMembers": "Нет членов команды с этой ролью",
"getToken": "Получить токен",
"emailToken": "Отправить токен по эл. почте",
"table": {
"name": "Имя",
"email": "Эл. почта",
"role": "Роль",
"created": "Создан"
}
},
"monitorState": {
"paused": "приостановлен",
"resumed": "возобновлен",
"active": "активный"
},
"menu": {
"uptime": "Аптайм",
"pagespeed": "Скорость страницы",
"infrastructure": "Инфраструктура",
"distributedUptime": "Распределенный аптайм",
"incidents": "Инциденты",
"statusPages": "Страницы статуса",
"maintenance": "Обслуживание",
"integrations": "Интеграции",
"settings": "Настройки",
"support": "Поддержка",
"discussions": "Обсуждения",
"docs": "Документация",
"changelog": "История изменений",
"profile": "Профиль",
"password": "Пароль",
"team": "Команда"
}
}

View File

@@ -418,14 +418,77 @@
"authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
"authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin",
"bulkImport": {
"title": "",
"selectFileTips": "",
"selectFileDescription": "",
"selectFile": "",
"parsingFailed": "",
"uploadSuccess": "",
"validationFailed": "",
"noFileSelected": "",
"fallbackPage": ""
"title": "Toplu İçe Aktar",
"selectFileTips": "Yüklemek için CSV dosyası seçin",
"selectFileDescription": "<template>Şablonumuzu</template> veya <sample>örneğimizi</sample> indirebilirsiniz",
"selectFile": "Dosya Seç",
"parsingFailed": "Ayrıştırma başarısız oldu",
"uploadSuccess": "Monitörler başarıyla oluşturuldu!",
"validationFailed": "Doğrulama başarısız oldu",
"noFileSelected": "Dosya seçilmedi",
"fallbackPage": "Sunucuların listesini toplu olarak yüklemek için bir dosya içe aktarın"
},
"createNew": "Yeni oluştur",
"greeting": {
"prepend": "Merhaba",
"append": "Öğleden sonra senin oyun alanın—hadi onu muhteşem yapalım!",
"overview": "İşte {{type}} monitörlerinizin genel görünümü."
},
"monitorStatus": {
"up": "aktif",
"down": "devre dışı",
"paused": "duraklatıldı"
},
"roles": {
"superAdmin": "Süper yönetici",
"admin": "Yönetici",
"teamMember": "Takım üyesi",
"demoUser": "Demo kullanıcı"
},
"teamPanel": {
"teamMembers": "Takım üyeleri",
"filter": {
"all": "Tümü",
"member": "Üye"
},
"inviteTeamMember": "Takım üyesi davet et",
"inviteNewTeamMember": "Yeni takım üyesi davet et",
"inviteDescription": "Yeni bir takım üyesi eklediğinizde, tüm monitörlere erişim hakkı alacaktır.",
"email": "E-posta",
"selectRole": "Rol seçin",
"inviteLink": "Davet bağlantısı",
"cancel": "İptal",
"noMembers": "Bu role sahip takım üyesi bulunmamaktadır",
"getToken": "Token al",
"emailToken": "Token e-posta ile gönder",
"table": {
"name": "Ad",
"email": "E-posta",
"role": "Rol",
"created": "Oluşturuldu"
}
},
"monitorState": {
"paused": "duraklatıldı",
"resumed": "devam ettirildi",
"active": "aktif"
},
"menu": {
"uptime": "Çalışma Süresi",
"pagespeed": "Sayfa Hızı",
"infrastructure": "Altyapı",
"distributedUptime": "Dağıtılmış Çalışma Süresi",
"incidents": "Olaylar",
"statusPages": "Durum Sayfaları",
"maintenance": "Bakım",
"integrations": "Entegrasyonlar",
"settings": "Ayarlar",
"support": "Destek",
"discussions": "Tartışmalar",
"docs": "Belgeler",
"changelog": "Değişiklik Günlüğü",
"profile": "Profil",
"password": "Şifre",
"team": "Takım"
}
}