This commit is contained in:
@@ -28,6 +28,7 @@ describe("SettingsPage", () => {
|
|||||||
language: "en",
|
language: "en",
|
||||||
datetime: { timeZone: "UTC" },
|
datetime: { timeZone: "UTC" },
|
||||||
},
|
},
|
||||||
|
alarmEmailsEnabled: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
@@ -90,7 +91,7 @@ describe("SettingsPage", () => {
|
|||||||
fireEvent.click(notificationsTab);
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/settings.notifications.empty/i)
|
screen.getByText(/settings.notifications.deliveryMethod/i)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -253,7 +254,7 @@ describe("SettingsPage", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows empty state in Notifications tab", () => {
|
it("shows Email toggle in Notifications tab", () => {
|
||||||
renderWithProviders(<SettingsPage />, preloadedState);
|
renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
const notificationsTab = screen.getByRole("tab", {
|
const notificationsTab = screen.getByRole("tab", {
|
||||||
@@ -262,7 +263,10 @@ describe("SettingsPage", () => {
|
|||||||
fireEvent.click(notificationsTab);
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
screen.getByText("settings.notifications.empty")
|
screen.getByText(/settings.notifications.deliveryMethod/i)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByRole("switch", { name: /settings.notifications.email/i })
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -658,5 +662,181 @@ describe("SettingsPage", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Alarm Emails Settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays alarmEmails toggle with correct initial state", () => {
|
||||||
|
const stateWithAlarmEmailsEnabled = {
|
||||||
|
...preloadedState,
|
||||||
|
user: {
|
||||||
|
...preloadedState.user,
|
||||||
|
alarmEmailsEnabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<SettingsPage />, stateWithAlarmEmailsEnabled);
|
||||||
|
|
||||||
|
const notificationsTab = screen.getByRole("tab", {
|
||||||
|
name: /settings.notifications/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.notifications.email/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle).toBeInTheDocument();
|
||||||
|
expect(toggle.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays alarmEmails toggle as true when alarmEmailsEnabled is null", () => {
|
||||||
|
const stateWithNullAlarmEmails = {
|
||||||
|
...preloadedState,
|
||||||
|
user: {
|
||||||
|
...preloadedState.user,
|
||||||
|
alarmEmailsEnabled: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderWithProviders(<SettingsPage />, stateWithNullAlarmEmails);
|
||||||
|
|
||||||
|
const notificationsTab = screen.getByRole("tab", {
|
||||||
|
name: /settings.notifications/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.notifications.email/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle).toBeInTheDocument();
|
||||||
|
expect(toggle.checked).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates alarmEmails immediately (optimistic update) and calls API in background", async () => {
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
|
const notificationsTab = screen.getByRole("tab", {
|
||||||
|
name: /settings.notifications/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.notifications.email/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(false);
|
||||||
|
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
// AlarmEmails should be updated immediately (optimistic update)
|
||||||
|
await waitFor(() => {
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.user?.alarmEmailsEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// API should be called in background
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledWith(
|
||||||
|
"api/configurations?scope=user",
|
||||||
|
expect.objectContaining({
|
||||||
|
json: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "calendar",
|
||||||
|
configurations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "alarmEmails",
|
||||||
|
value: true,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rolls back alarmEmails change if API call fails", async () => {
|
||||||
|
(api.patch as jest.Mock).mockRejectedValue(new Error("API Error"));
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
|
||||||
|
|
||||||
|
const notificationsTab = screen.getByRole("tab", {
|
||||||
|
name: /settings.notifications/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.notifications.email/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(false);
|
||||||
|
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
// Wait for rollback - alarmEmails should be rolled back to false after error
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.user?.alarmEmailsEnabled).toBe(false);
|
||||||
|
},
|
||||||
|
{ timeout: 3000 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends false value when toggle is turned off", async () => {
|
||||||
|
(api.patch as jest.Mock).mockResolvedValue({ status: 204 });
|
||||||
|
|
||||||
|
const stateWithAlarmEmailsEnabled = {
|
||||||
|
...preloadedState,
|
||||||
|
user: {
|
||||||
|
...preloadedState.user,
|
||||||
|
alarmEmailsEnabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { store } = renderWithProviders(
|
||||||
|
<SettingsPage />,
|
||||||
|
stateWithAlarmEmailsEnabled
|
||||||
|
);
|
||||||
|
|
||||||
|
const notificationsTab = screen.getByRole("tab", {
|
||||||
|
name: /settings.notifications/i,
|
||||||
|
});
|
||||||
|
fireEvent.click(notificationsTab);
|
||||||
|
|
||||||
|
const toggle = screen.getByRole("switch", {
|
||||||
|
name: /settings.notifications.email/i,
|
||||||
|
}) as HTMLInputElement;
|
||||||
|
expect(toggle.checked).toBe(true);
|
||||||
|
|
||||||
|
fireEvent.click(toggle);
|
||||||
|
|
||||||
|
// AlarmEmails should be updated immediately (optimistic update)
|
||||||
|
await waitFor(() => {
|
||||||
|
const state = store.getState();
|
||||||
|
expect(state.user?.alarmEmailsEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// API should be called with false value
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.patch).toHaveBeenCalledWith(
|
||||||
|
"api/configurations?scope=user",
|
||||||
|
expect.objectContaining({
|
||||||
|
json: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "calendar",
|
||||||
|
configurations: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "alarmEmails",
|
||||||
|
value: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
Typography,
|
Typography,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
FormControlLabel,
|
|
||||||
Switch,
|
Switch,
|
||||||
|
FormControlLabel,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
||||||
import SettingsIcon from "@mui/icons-material/Settings";
|
import SettingsIcon from "@mui/icons-material/Settings";
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
updateUserConfigurationsAsync,
|
updateUserConfigurationsAsync,
|
||||||
setLanguage as setUserLanguage,
|
setLanguage as setUserLanguage,
|
||||||
setTimezone as setUserTimeZone,
|
setTimezone as setUserTimeZone,
|
||||||
|
setAlarmEmails,
|
||||||
} from "../User/userSlice";
|
} from "../User/userSlice";
|
||||||
import { AVAILABLE_LANGUAGES } from "./constants";
|
import { AVAILABLE_LANGUAGES } from "./constants";
|
||||||
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
|
||||||
@@ -64,13 +65,16 @@ export default function SettingsPage() {
|
|||||||
const isBrowserDefault = useAppSelector(
|
const isBrowserDefault = useAppSelector(
|
||||||
(state) => state.settings.isBrowserDefaultTimeZone
|
(state) => state.settings.isBrowserDefaultTimeZone
|
||||||
);
|
);
|
||||||
|
const alarmEmailsEnabled = useAppSelector(
|
||||||
|
(state) => state.user?.alarmEmailsEnabled ?? true
|
||||||
|
);
|
||||||
const [activeNavItem, setActiveNavItem] =
|
const [activeNavItem, setActiveNavItem] =
|
||||||
useState<SidebarNavItem>("settings");
|
useState<SidebarNavItem>("settings");
|
||||||
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
const [activeSettingsSubTab, setActiveSettingsSubTab] =
|
||||||
useState<SettingsSubTab>("settings");
|
useState<SettingsSubTab>("settings");
|
||||||
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
|
const [languageErrorOpen, setLanguageErrorOpen] = useState(false);
|
||||||
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
|
const [timeZoneErrorOpen, setTimeZoneErrorOpen] = useState(false);
|
||||||
|
const [alarmEmailsErrorOpen, setAlarmEmailsErrorOpen] = useState(false);
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
dispatch(setView("calendar"));
|
dispatch(setView("calendar"));
|
||||||
@@ -164,6 +168,30 @@ export default function SettingsPage() {
|
|||||||
setTimeZoneErrorOpen(false);
|
setTimeZoneErrorOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAlarmEmailsToggle = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const newValue = event.target.checked;
|
||||||
|
const previousValue = alarmEmailsEnabled;
|
||||||
|
|
||||||
|
// Optimistic update - update UI immediately
|
||||||
|
dispatch(setAlarmEmails(newValue));
|
||||||
|
|
||||||
|
// Call API in background, don't wait for it
|
||||||
|
dispatch(updateUserConfigurationsAsync({ alarmEmails: newValue }))
|
||||||
|
.unwrap()
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Failed to update alarm emails:", error);
|
||||||
|
// Rollback on error
|
||||||
|
dispatch(setAlarmEmails(previousValue));
|
||||||
|
setAlarmEmailsErrorOpen(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAlarmEmailsErrorClose = () => {
|
||||||
|
setAlarmEmailsErrorOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="main-layout settings-layout">
|
<main className="main-layout settings-layout">
|
||||||
<Box className="settings-sidebar">
|
<Box className="settings-sidebar">
|
||||||
@@ -291,10 +319,32 @@ export default function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
{activeSettingsSubTab === "notifications" && (
|
{activeSettingsSubTab === "notifications" && (
|
||||||
<Box className="settings-tab-content">
|
<Box className="settings-tab-content">
|
||||||
<Typography variant="body1" color="text.secondary">
|
<Typography
|
||||||
{t("settings.notifications.empty") ||
|
variant="body2"
|
||||||
"Notifications settings coming soon"}
|
color="text.secondary"
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
>
|
||||||
|
{t("settings.notifications.deliveryMethod") ||
|
||||||
|
"Delivery method"}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
checked={alarmEmailsEnabled}
|
||||||
|
onChange={handleAlarmEmailsToggle}
|
||||||
|
aria-label={
|
||||||
|
t("settings.notifications.email") || "Email"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t("settings.notifications.email") || "Email"}
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{
|
||||||
|
minWidth: 400,
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginLeft: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -322,6 +372,15 @@ export default function SettingsPage() {
|
|||||||
onClose={handleTimeZoneErrorClose}
|
onClose={handleTimeZoneErrorClose}
|
||||||
message={t("settings.timeZoneUpdateError")}
|
message={t("settings.timeZoneUpdateError")}
|
||||||
/>
|
/>
|
||||||
|
<Snackbar
|
||||||
|
open={alarmEmailsErrorOpen}
|
||||||
|
autoHideDuration={4000}
|
||||||
|
onClose={handleAlarmEmailsErrorClose}
|
||||||
|
message={
|
||||||
|
t("settings.alarmEmailsUpdateError") ||
|
||||||
|
"Failed to update email notifications setting"
|
||||||
|
}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ export interface UserConfigurationUpdates {
|
|||||||
notifications?: Record<string, unknown>;
|
notifications?: Record<string, unknown>;
|
||||||
timezone?: string | null;
|
timezone?: string | null;
|
||||||
previousConfig?: Record<string, any>;
|
previousConfig?: Record<string, any>;
|
||||||
|
alarmEmails?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUserConfigurations(
|
export async function updateUserConfigurations(
|
||||||
updates: UserConfigurationUpdates
|
updates: UserConfigurationUpdates
|
||||||
): Promise<Response | { status: number }> {
|
): Promise<Response | { status: number }> {
|
||||||
const coreConfigs: Array<{ name: string; value: any }> = [];
|
const coreConfigs: Array<{ name: string; value: any }> = [];
|
||||||
|
const calendarConfigs: Array<{ name: string; value: any }> = [];
|
||||||
|
|
||||||
if (updates.language !== undefined) {
|
if (updates.language !== undefined) {
|
||||||
coreConfigs.push({ name: "language", value: updates.language });
|
coreConfigs.push({ name: "language", value: updates.language });
|
||||||
@@ -61,17 +63,37 @@ export async function updateUserConfigurations(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (updates.alarmEmails !== undefined) {
|
||||||
|
calendarConfigs.push({
|
||||||
|
name: "alarmEmails",
|
||||||
|
value: updates.alarmEmails,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (coreConfigs.length === 0) {
|
const modules: Array<{
|
||||||
|
name: string;
|
||||||
|
configurations: Array<{ name: string; value: any }>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (coreConfigs.length > 0) {
|
||||||
|
modules.push({
|
||||||
|
name: "core",
|
||||||
|
configurations: coreConfigs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calendarConfigs.length > 0) {
|
||||||
|
modules.push({
|
||||||
|
name: "calendar",
|
||||||
|
configurations: calendarConfigs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modules.length === 0) {
|
||||||
return Promise.resolve({ status: 204 });
|
return Promise.resolve({ status: 204 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return await api.patch(`api/configurations?scope=user`, {
|
return await api.patch(`api/configurations?scope=user`, {
|
||||||
json: [
|
json: modules,
|
||||||
{
|
|
||||||
name: "core",
|
|
||||||
configurations: coreConfigs,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export const userSlice = createSlice({
|
|||||||
timeZone: null as string | null,
|
timeZone: null as string | null,
|
||||||
},
|
},
|
||||||
} as Record<string, any>,
|
} as Record<string, any>,
|
||||||
|
alarmEmailsEnabled: null as boolean | null,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null as unknown as string | null,
|
error: null as unknown as string | null,
|
||||||
},
|
},
|
||||||
@@ -82,6 +83,9 @@ export const userSlice = createSlice({
|
|||||||
state.userData.timezone = action.payload;
|
state.userData.timezone = action.payload;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setAlarmEmails: (state, action) => {
|
||||||
|
state.alarmEmailsEnabled = action.payload;
|
||||||
|
},
|
||||||
clearError: (state) => {
|
clearError: (state) => {
|
||||||
state.error = null;
|
state.error = null;
|
||||||
},
|
},
|
||||||
@@ -153,6 +157,19 @@ export const userSlice = createSlice({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract alarmEmails from configurations.modules
|
||||||
|
const calendarModule = action.payload.configurations.modules.find(
|
||||||
|
(module: any) => module.name === "calendar"
|
||||||
|
);
|
||||||
|
if (calendarModule?.configurations) {
|
||||||
|
const alarmEmailsConfig = calendarModule.configurations.find(
|
||||||
|
(config: any) => config.name === "alarmEmails"
|
||||||
|
);
|
||||||
|
if (alarmEmailsConfig) {
|
||||||
|
state.alarmEmailsEnabled = alarmEmailsConfig.value === true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
.addCase(getOpenPaasUserDataAsync.pending, (state) => {
|
||||||
@@ -181,6 +198,9 @@ export const userSlice = createSlice({
|
|||||||
state.userData.timezone = action.payload.timezone;
|
state.userData.timezone = action.payload.timezone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (action.payload.alarmEmails !== undefined) {
|
||||||
|
state.alarmEmailsEnabled = action.payload.alarmEmails === true;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.addCase(updateUserConfigurationsAsync.rejected, (state, action) => {
|
.addCase(updateUserConfigurationsAsync.rejected, (state, action) => {
|
||||||
if (action.payload?.status !== 401) {
|
if (action.payload?.status !== 401) {
|
||||||
@@ -192,7 +212,13 @@ export const userSlice = createSlice({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Action creators are generated for each case reducer function
|
// Action creators are generated for each case reducer function
|
||||||
export const { setUserData, setTokens, setLanguage, setTimezone, clearError } =
|
export const {
|
||||||
userSlice.actions;
|
setUserData,
|
||||||
|
setTokens,
|
||||||
|
setLanguage,
|
||||||
|
setTimezone,
|
||||||
|
setAlarmEmails,
|
||||||
|
clearError,
|
||||||
|
} = userSlice.actions;
|
||||||
|
|
||||||
export default userSlice.reducer;
|
export default userSlice.reducer;
|
||||||
|
|||||||
@@ -217,6 +217,9 @@
|
|||||||
"timeZoneBrowserDefault": "Detect time zone automatically",
|
"timeZoneBrowserDefault": "Detect time zone automatically",
|
||||||
"timeZoneUpdateError": "Failed to update time zone",
|
"timeZoneUpdateError": "Failed to update time zone",
|
||||||
"notifications.empty": "Notifications settings coming soon",
|
"notifications.empty": "Notifications settings coming soon",
|
||||||
|
"notifications.email": "Email",
|
||||||
|
"notifications.deliveryMethod": "Delivery method",
|
||||||
|
"alarmEmailsUpdateError": "Failed to update email notifications setting",
|
||||||
"sync.empty": "Sync settings coming soon",
|
"sync.empty": "Sync settings coming soon",
|
||||||
"back": "Back to calendar"
|
"back": "Back to calendar"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -217,6 +217,9 @@
|
|||||||
"timeZoneBrowserDefault": "Détection automatique du fuseau horaire",
|
"timeZoneBrowserDefault": "Détection automatique du fuseau horaire",
|
||||||
"timeZoneUpdateError": "Échec de la mise à jour du fuseau horaire",
|
"timeZoneUpdateError": "Échec de la mise à jour du fuseau horaire",
|
||||||
"notifications.empty": "Paramètres de notifications à venir",
|
"notifications.empty": "Paramètres de notifications à venir",
|
||||||
|
"notifications.email": "Email",
|
||||||
|
"notifications.deliveryMethod": "Mode de réception",
|
||||||
|
"alarmEmailsUpdateError": "Échec de la mise à jour du mode de notification par e-mail",
|
||||||
"sync.empty": "Paramètres de synchronisation à venir",
|
"sync.empty": "Paramètres de synchronisation à venir",
|
||||||
"back": "Retour au calendrier"
|
"back": "Retour au calendrier"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -217,6 +217,9 @@
|
|||||||
"timeZoneBrowserDefault": "Определять часовой пояс автоматически",
|
"timeZoneBrowserDefault": "Определять часовой пояс автоматически",
|
||||||
"timeZoneUpdateError": "Не удалось обновить часовой пояс",
|
"timeZoneUpdateError": "Не удалось обновить часовой пояс",
|
||||||
"notifications.empty": "Настройки уведомлений скоро появятся",
|
"notifications.empty": "Настройки уведомлений скоро появятся",
|
||||||
|
"notifications.email": "Email",
|
||||||
|
"notifications.deliveryMethod": "Способ доставки",
|
||||||
|
"alarmEmailsUpdateError": "Не удалось обновить настройки уведомлений по email",
|
||||||
"sync.empty": "Настройки синхронизации скоро появятся",
|
"sync.empty": "Настройки синхронизации скоро появятся",
|
||||||
"back": "Вернуться к календарю"
|
"back": "Вернуться к календарю"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -217,6 +217,9 @@
|
|||||||
"timeZoneBrowserDefault": "Tự động phát hiện múi giờ",
|
"timeZoneBrowserDefault": "Tự động phát hiện múi giờ",
|
||||||
"timeZoneUpdateError": "Không cập nhật được múi giờ",
|
"timeZoneUpdateError": "Không cập nhật được múi giờ",
|
||||||
"notifications.empty": "Cài đặt thông báo sắp có",
|
"notifications.empty": "Cài đặt thông báo sắp có",
|
||||||
|
"notifications.email": "Email",
|
||||||
|
"notifications.deliveryMethod": "Phương thức gửi",
|
||||||
|
"alarmEmailsUpdateError": "Cập nhật cài đặt thông báo email thất bại",
|
||||||
"sync.empty": "Cài đặt đồng bộ sắp có",
|
"sync.empty": "Cài đặt đồng bộ sắp có",
|
||||||
"back": "Quay lại lịch"
|
"back": "Quay lại lịch"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user