🚧 wip migrate to next-i18n

This commit is contained in:
Manuel Ruwe
2022-08-22 09:50:54 +02:00
parent ac4dc23e08
commit 6d0a31f79e
61 changed files with 817 additions and 753 deletions

9
next-i18next.config.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = {
// https://www.i18next.com/overview/configuration-options#logging
debug: process.env.NODE_ENV === 'development',
i18n: {
defaultLocale: 'en',
locales: ['en', 'de'],
},
reloadOnPrerender: process.env.NODE_ENV === 'development',
};

View File

@@ -1,5 +1,7 @@
const { env } = require('process'); const { env } = require('process');
const { i18n } = require('./next-i18next.config');
const withBundleAnalyzer = require('@next/bundle-analyzer')({ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true', enabled: process.env.ANALYZE === 'true',
}); });
@@ -13,4 +15,5 @@ module.exports = withBundleAnalyzer({
outputStandalone: true, outputStandalone: true,
}, },
output: 'standalone', output: 'standalone',
i18n,
}); });

View File

@@ -58,10 +58,10 @@
"i18next-http-backend": "^1.4.1", "i18next-http-backend": "^1.4.1",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"next": "12.1.6", "next": "12.1.6",
"next-i18next": "^11.3.0",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "^1.3.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^11.18.4",
"sharp": "^0.30.7", "sharp": "^0.30.7",
"systeminformation": "^5.12.1", "systeminformation": "^5.12.1",
"uuid": "^8.3.2", "uuid": "^8.3.2",

View File

@@ -108,14 +108,14 @@
"header": { "header": {
"search": { "search": {
"input": { "input": {
"placeholder": "Search the web..." "placeholder": "Das Internet durchsuchen..."
} }
}, },
"docker": { "docker": {
"errors": { "errors": {
"integrationFailed": { "integrationFailed": {
"title": "Docker integration failed", "title": "Docker Integration ist gescheitert",
"message": "Did you forget to mount the docker socket ?" "message": "Hast du vergessen, den Docker Socket zu verbinden?"
} }
}, },
"actionIcon": { "actionIcon": {
@@ -124,22 +124,22 @@
}, },
"addService": { "addService": {
"actionIcon": { "actionIcon": {
"tooltip": "Add a service" "tooltip": "Einen Service hinzufügen"
}, },
"modal": { "modal": {
"title": "Add service", "title": "Service hinzufügen",
"form": { "form": {
"validation": { "validation": {
"invalidUrl": "Please enter a valid URL", "invalidUrl": "Bitte gebe eine gültige Addresse ein",
"noStatusCodeSelected": "Please select a status code" "noStatusCodeSelected": "Bitte wähle einen gültigen Status Code aus"
} }
}, },
"tabs": { "tabs": {
"options": { "options": {
"title": "Options", "title": "Optionen",
"form": { "form": {
"serviceName": { "serviceName": {
"label": "Service name", "label": "Service Namen",
"placeholder": "Plex" "placeholder": "Plex"
}, },
"iconUrl": { "iconUrl": {
@@ -149,59 +149,59 @@
"label": "Service URL" "label": "Service URL"
}, },
"onClickUrl": { "onClickUrl": {
"label": "On Click URL" "label": "URL beim Klicken"
}, },
"serviceType": { "serviceType": {
"label": "Service type", "label": "Service Typ",
"defaultValue": "Other", "defaultValue": "Andere",
"placeholder": "Pick one" "placeholder": "Wähle einen aus..."
}, },
"category": { "category": {
"label": "Category", "label": "Kategorie",
"placeholder": "Select a category or create a new one", "placeholder": "Wähle eine Kategorie oder erstelle eine neue...",
"nothingFound": "Nothing found", "nothingFound": "Keine Übereinstimmungen gefunden",
"createLabel": "+ Create {{query}}" "createLabel": "+ Erstellen {{query}}"
}, },
"integrations": { "integrations": {
"apiKey": { "apiKey": {
"label": "API key", "label": "API Schlüssel",
"placeholder": "Your API key", "placeholder": "Dein API Schlüssel",
"validation": { "validation": {
"noKey": "Invalid Key" "noKey": "Invalider Schlüssel"
}, },
"tip": { "tip": {
"text": "Get your API key", "text": "Hole deinen API Schlüssel",
"link": "here." "link": "hier."
} }
}, },
"qBittorrent": { "qBittorrent": {
"username": { "username": {
"label": "Username", "label": "Benutzername",
"placeholder": "admin", "placeholder": "admin",
"validation": { "validation": {
"invalidUsername": "Invalid username" "invalidUsername": "Invalider Benutzername"
} }
}, },
"password": { "password": {
"label": "Password", "label": "Passwort",
"placeholder": "adminadmin", "placeholder": "adminadmin",
"validation": { "validation": {
"invalidPassword": "Invalid password" "invalidPassword": "Invalides Passwort"
} }
} }
}, },
"deluge": { "deluge": {
"password": { "password": {
"label": "Password", "label": "Passwort",
"placeholder": "password", "placeholder": "password",
"validation": { "validation": {
"invalidPassword": "Invalid password" "invalidPassword": "Invalides PassworT"
} }
} }
}, },
"transmission": { "transmission": {
"username": { "username": {
"label": "Username", "label": "Benutzername",
"placeholder": "admin", "placeholder": "admin",
"validation": { "validation": {
"invalidUsername": "Invalid username" "invalidUsername": "Invalid username"

View File

@@ -1,385 +0,0 @@
{
"settings": {
"title": "Settings",
"tooltip": "Settings",
"tabs": {
"common": {
"title": "Common",
"settings": {
"searchEngine": {
"title": "Search engine",
"tips": {
"generalTip": "Use the prefixes !yt and !t in front of your query to search on YouTube or for a Torrent respectively.",
"placeholderTip": "%s can be used as a placeholder for the query."
},
"customEngine": {
"label": "Query URL",
"placeholder": "Custom query URL"
}
},
"colorScheme": {
"label": "Switch to {{scheme}} mode"
},
"widgetsPositionSwitch": {
"label": "Position widgets on left"
},
"moduleEnabler": {
"title": "Module enabler"
},
"language": {
"title": "Language"
},
"configChanger": {
"configSelect": {
"label": "Config loader"
},
"modal": {
"title": "Choose the name of your new config",
"form": {
"configName": {
"label": "Config name",
"placeholder": "Your new config name"
},
"buttons:": {
"submit": "Confirm"
}
}
},
"buttons": {
"download": "Download config",
"delete": {
"text": "Delete config",
"notifications": {
"deleted": {
"title": "Config deleted",
"message": "Config deleted"
},
"deleteFailed": {
"title": "Config delete failed",
"message": "Config delete failed"
}
}
},
"saveCopy": "Save a copy"
}
},
"configTip": "Upload your config file by dragging and dropping it onto the page!"
}
},
"customizations": {
"title": "Customizations",
"settings": {
"opacitySelector": {
"label": "App Opacity"
},
"colorSelector": {
"suffix": "{{color}} color"
},
"shadeSelector": {
"label": "Shade"
},
"pageTitle": {
"label": "Page Title",
"placeholder": "Homarr 🦞"
},
"logo": {
"label": "Logo",
"placeholder": "/img/logo.png"
},
"favicon": {
"label": "Favicon",
"placeholder": "/favicon.png"
},
"background": {
"label": "Background",
"placeholder": "/img/background.png"
},
"buttons": {
"submit": "Save"
}
}
}
},
"credits": {
"madeWithLove": "Made with ❤️ by @"
}
},
"layout": {
"header": {
"search": {
"input": {
"placeholder": "Search the web..."
}
},
"docker": {
"errors": {
"integrationFailed": {
"title": "Docker integration failed",
"message": "Did you forget to mount the docker socket ?"
}
},
"actionIcon": {
"tooltip": "Docker"
}
},
"addService": {
"actionIcon": {
"tooltip": "Add a service"
},
"modal": {
"title": "Add service",
"form": {
"validation": {
"invalidUrl": "Please enter a valid URL",
"noStatusCodeSelected": "Please select a status code"
}
},
"tabs": {
"options": {
"title": "Options",
"form": {
"serviceName": {
"label": "Service name",
"placeholder": "Plex"
},
"iconUrl": {
"label": "Icon URL"
},
"serviceUrl": {
"label": "Service URL"
},
"onClickUrl": {
"label": "On Click URL"
},
"serviceType": {
"label": "Service type",
"defaultValue": "Other",
"placeholder": "Pick one"
},
"category": {
"label": "Category",
"placeholder": "Select a category or create a new one",
"nothingFound": "Nothing found",
"createLabel": "+ Create {{query}}"
},
"integrations": {
"apiKey": {
"label": "API key",
"placeholder": "Your API key",
"validation": {
"noKey": "Invalid Key"
},
"tip": {
"text": "Get your API key",
"link": "here."
}
},
"qBittorrent": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "adminadmin",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"deluge": {
"password": {
"label": "Password",
"placeholder": "password",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"transmission": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "adminadmin",
"validation": {
"invalidPassword": "Invalid password"
}
}
}
}
}
},
"advancedOptions": {
"title": "Advanced options",
"form": {
"httpStatusCodes": {
"label": "HTTP Status Codes",
"placeholder": "Select valid status codes",
"clearButtonLabel": "Clear selection",
"nothingFound": "Nothing found"
},
"openServiceInNewTab": {
"label": "Open service in new tab"
},
"buttons": {
"submit": {
"content": "Add service"
}
}
}
}
}
}
}
}
},
"modules": {
"common": {
"mediaCard": {
"buttons": {
"play": "Play",
"request": "Request"
}
}
},
"calendar": {
"title": "Calendar",
"description": "A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.",
"options": {
"sundayStart": "Start the week on Sunday"
}
},
"dashDot": {
"card": {
"title": "Dash.",
"errors": {
"noService": "No dash. service found. Please add one to your Homarr dashboard or set a dashdot URL in the module options",
"noInformation": "Cannot acquire information from dash. - are you running the latest version?"
},
"graphs": {
"storage": {
"title": "Storage",
"label": "Storage:"
},
"network": {
"title": "Network",
"label": "Network:",
"metrics": {
"download": "Down",
"upload": "Up"
}
},
"cpu": {
"title": "CPU"
},
"memory": {
"title": "RAM"
},
"gpu": {
"title": "GPU"
}
}
}
},
"torrent": {
"card": {
"title": "Your Downloads"
}
},
"downloads": {
"card": {
"table": {
"header": {
"name": "Name",
"size": "Size",
"download": "Down",
"upload": "Up",
"estimatedTimeOfArrival": "ETA",
"progress": "Progress"
},
"body": {
"nothingFound": "No torrents found"
}
}
}
},
"weather": {
"card": {
"weatherDescriptions": {
"clear": "Clear",
"mainlyClear": "Mainly clear",
"fog": "Fog",
"drizzle": "Drizzle",
"freezingDrizzle": "Freezing drizzle",
"rain": "Rain",
"freezingRain": "Freezing rain",
"snowFall": "Snow fall",
"snowGrains": "Snow grains",
"rainShowers": "Rain showers",
"snowShowers": "Snow showers",
"thunderstorm": "Thunderstorm",
"thunderstormWithHail": "Thunderstorm with hail",
"unknown": "Unknown"
}
}
},
"overseerr": {
"popup": {
"item": {
"buttons": {
"askFor": "Ask for {{title}}",
"cancel": "Cancel",
"request": "Request"
},
"alerts": {
"automaticApproval": {
"title": "Using API key",
"text": "This request will be automatically approved"
}
}
},
"seasonSelector": {
"caption": "Tick the seasons that you want to be downloaded",
"table": {
"header": {
"season": "Season",
"numberOfEpisodes": "Number of episodes"
}
}
}
}
},
"ping": {
"states": {
"online": "Online {{response}}",
"offline": "Offline {{response}}",
"loading": "Loading..."
}
},
"docker": {
"search": {
"placeholder": "Search by container or image name"
},
"table": {
"header": {
"name": "Name",
"image": "Image",
"ports": "Ports",
"state": "State"
},
"body": {
"portCollapse": "{{ports}} more"
}
}
}
}
}

View File

@@ -0,0 +1,5 @@
{
"actions": {
"save": "Save"
}
}

View File

@@ -0,0 +1,118 @@
{
"actionIcon": {
"tooltip": "Add a service"
},
"modal": {
"title": "Add service",
"form": {
"validation": {
"invalidUrl": "Please enter a valid URL",
"noStatusCodeSelected": "Please select a status code"
}
},
"tabs": {
"options": {
"title": "Options",
"form": {
"serviceName": {
"label": "Service name",
"placeholder": "Plex"
},
"iconUrl": {
"label": "Icon URL"
},
"serviceUrl": {
"label": "Service URL"
},
"onClickUrl": {
"label": "On Click URL"
},
"serviceType": {
"label": "Service type",
"defaultValue": "Other",
"placeholder": "Pick one"
},
"category": {
"label": "Category",
"placeholder": "Select a category or create a new one",
"nothingFound": "Nothing found",
"createLabel": "+ Create {{query}}"
},
"integrations": {
"apiKey": {
"label": "API key",
"placeholder": "Your API key",
"validation": {
"noKey": "Invalid Key"
},
"tip": {
"text": "Get your API key",
"link": "here."
}
},
"qBittorrent": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "adminadmin",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"deluge": {
"password": {
"label": "Password",
"placeholder": "password",
"validation": {
"invalidPassword": "Invalid password"
}
}
},
"transmission": {
"username": {
"label": "Username",
"placeholder": "admin",
"validation": {
"invalidUsername": "Invalid username"
}
},
"password": {
"label": "Password",
"placeholder": "adminadmin",
"validation": {
"invalidPassword": "Invalid password"
}
}
}
}
}
},
"advancedOptions": {
"title": "Advanced options",
"form": {
"httpStatusCodes": {
"label": "HTTP Status Codes",
"placeholder": "Select valid status codes",
"clearButtonLabel": "Clear selection",
"nothingFound": "Nothing found"
},
"openServiceInNewTab": {
"label": "Open service in new tab"
},
"buttons": {
"submit": {
"content": "Add service"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
{
"accordions": {
"downloads": {
"text": "Your downloads"
},
"others": {
"text": "Others"
}
}
}

View File

@@ -0,0 +1,6 @@
{
"buttons": {
"play": "Play",
"request": "Request"
}
}

View File

@@ -0,0 +1,32 @@
{
"card": {
"title": "Dash.",
"errors": {
"noService": "No dash. service found. Please add one to your Homarr dashboard or set a dashdot URL in the module options",
"noInformation": "Cannot acquire information from dash. - are you running the latest version?"
},
"graphs": {
"storage": {
"title": "Storage",
"label": "Storage:"
},
"network": {
"title": "Network",
"label": "Network:",
"metrics": {
"download": "Down",
"upload": "Up"
}
},
"cpu": {
"title": "CPU"
},
"memory": {
"title": "RAM"
},
"gpu": {
"title": "GPU"
}
}
}
}

View File

@@ -0,0 +1,65 @@
{
"search": {
"placeholder": "Search by container or image name"
},
"table": {
"header": {
"name": "Name",
"image": "Image",
"ports": "Ports",
"state": "State"
},
"body": {
"portCollapse": "{{ports}} more"
},
"states": {
"running": "Running",
"created": "Created",
"stopped": "Stopped",
"unknown": "Unknown"
}
},
"actionBar": {
"addService": {
"title": "Add service",
"message": "Add service to Homarr"
},
"restart": {
"title": "Restart"
},
"stop": {
"title": "Stop"
},
"start": {
"title": "Start"
},
"refreshData": "Refresh data",
"addToHomarr": {
"title": "Add to Homarr"
},
"remove": {
"title": "Remove"
}
},
"messages": {
"successfullyExecuted": {
"title": "Container {{containerName}} {{action}}ed",
"message": "Your container was successfully {{action}}ed"
}
},
"errors": {
"integrationFailed": {
"title": "Docker integration failed",
"message": "Did you forget to mount the docker socket ?"
},
"unknownError": {
"title": "There was an error"
},
"oneServiceAtATime": {
"title": "Please only add one service at a time!"
}
},
"actionIcon": {
"tooltip": "Docker"
}
}

View File

@@ -0,0 +1,17 @@
{
"card": {
"table": {
"header": {
"name": "Name",
"size": "Size",
"download": "Down",
"upload": "Up",
"estimatedTimeOfArrival": "ETA",
"progress": "Progress"
},
"body": {
"nothingFound": "No torrents found"
}
}
}
}

View File

@@ -0,0 +1,26 @@
{
"popup": {
"item": {
"buttons": {
"askFor": "Ask for {{title}}",
"cancel": "Cancel",
"request": "Request"
},
"alerts": {
"automaticApproval": {
"title": "Using API key",
"text": "This request will be automatically approved"
}
}
},
"seasonSelector": {
"caption": "Tick the seasons that you want to be downloaded",
"table": {
"header": {
"season": "Season",
"numberOfEpisodes": "Number of episodes"
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
{
"states": {
"online": "Online {{response}}",
"offline": "Offline {{response}}",
"loading": "Loading..."
}
}

View File

@@ -0,0 +1,5 @@
{
"input": {
"placeholder": "Search the web..."
}
}

View File

@@ -0,0 +1,20 @@
{
"card": {
"weatherDescriptions": {
"clear": "Clear",
"mainlyClear": "Mainly clear",
"fog": "Fog",
"drizzle": "Drizzle",
"freezingDrizzle": "Freezing drizzle",
"rain": "Rain",
"freezingRain": "Freezing rain",
"snowFall": "Snow fall",
"snowGrains": "Snow grains",
"rainShowers": "Rain showers",
"snowShowers": "Snow showers",
"thunderstorm": "Thunderstorm",
"thunderstormWithHail": "Thunderstorm with hail",
"unknown": "Unknown"
}
}
}

View File

@@ -0,0 +1,11 @@
{
"title": "Settings",
"tooltip": "Settings",
"tabs": {
"common": "Common",
"customizations": "Customizations"
},
"credits": {
"madeWithLove": "Made with ❤️ by @"
}
}

View File

@@ -0,0 +1,3 @@
{
"suffix": "{{color}} color"
}

View File

@@ -0,0 +1,3 @@
{
"label": "App Opacity"
}

View File

@@ -0,0 +1,18 @@
{
"pageTitle": {
"label": "Page Title",
"placeholder": "Homarr 🦞"
},
"logo": {
"label": "Logo",
"placeholder": "/img/logo.png"
},
"favicon": {
"label": "Favicon",
"placeholder": "/favicon.png"
},
"background": {
"label": "Background",
"placeholder": "/img/background.png"
}
}

View File

@@ -0,0 +1,3 @@
{
"label": "Shade"
}

View File

@@ -0,0 +1,3 @@
{
"label": "Switch to {{scheme}} mode"
}

View File

@@ -0,0 +1,35 @@
{
"configSelect": {
"label": "Config loader"
},
"modal": {
"title": "Choose the name of your new config",
"form": {
"configName": {
"label": "Config name",
"placeholder": "Your new config name"
},
"buttons:": {
"submit": "Confirm"
}
}
},
"buttons": {
"download": "Download config",
"delete": {
"text": "Delete config",
"notifications": {
"deleted": {
"title": "Config deleted",
"message": "Config deleted"
},
"deleteFailed": {
"title": "Config delete failed",
"message": "Config delete failed"
}
}
},
"saveCopy": "Save a copy"
},
"configTip": "Upload your config file by dragging and dropping it onto the page!"
}

View File

@@ -0,0 +1,3 @@
{
"label": "Language"
}

View File

@@ -0,0 +1,3 @@
{
"title": "Module enabler"
}

View File

@@ -0,0 +1,11 @@
{
"title": "Search engine",
"tips": {
"generalTip": "Use the prefixes !yt and !t in front of your query to search on YouTube or for a Torrent respectively.",
"placeholderTip": "%s can be used as a placeholder for the query."
},
"customEngine": {
"label": "Query URL",
"placeholder": "Custom query URL"
}
}

View File

@@ -0,0 +1,3 @@
{
"label": "Switch to {{theme}} mode"
}

View File

@@ -0,0 +1,3 @@
{
"label": "Position widgets on left"
}

View File

@@ -22,25 +22,26 @@ import { IconApps } from '@tabler/icons';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types'; import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
import Tip from '../layout/Tip'; import Tip from '../layout/Tip';
export function AddItemShelfButton(props: any) { export function AddItemShelfButton(props: any) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const { t } = useTranslation('layout/add-service-app-shelf');
return ( return (
<> <>
<Modal <Modal
size="xl" size="xl"
radius="md" radius="md"
title={<Title order={3}>{t('layout.header.addService.modal.title')}</Title>} title={<Title order={3}>{t('modal.title')}</Title>}
opened={props.opened || opened} opened={props.opened || opened}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
> >
<AddAppShelfItemForm setOpened={setOpened} /> <AddAppShelfItemForm setOpened={setOpened} />
</Modal> </Modal>
<Tooltip withinPortal label={t('layout.header.addService.actionIcon.tooltip')}> <Tooltip withinPortal label={t('actionIcon.tooltip')}>
<ActionIcon <ActionIcon
variant="default" variant="default"
radius="md" radius="md"
@@ -85,6 +86,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
const { setOpened } = props; const { setOpened } = props;
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const [isLoading, setLoading] = useState(false); const [isLoading, setLoading] = useState(false);
const { t } = useTranslation('layout/add-service-app-shelf');
// Extract all the categories from the services in config // Extract all the categories from the services in config
const InitialCategories = config.services.reduce((acc, cur) => { const InitialCategories = config.services.reduce((acc, cur) => {
@@ -121,13 +123,13 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
try { try {
const _isValid = new URL(value); const _isValid = new URL(value);
} catch (e) { } catch (e) {
return t('layout.header.addService.modal.form.validation.invalidUrl'); return t('modal.form.validation.invalidUrl');
} }
return null; return null;
}, },
status: (value: string[]) => { status: (value: string[]) => {
if (!value.length) { if (!value.length) {
return t('layout.header.addService.modal.form.validation.noStatusCodeSelected'); return t('modal.form.validation.noStatusCodeSelected');
} }
return null; return null;
}, },
@@ -204,62 +206,48 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
> >
<Tabs defaultValue="Options"> <Tabs defaultValue="Options">
<Tabs.List grow> <Tabs.List grow>
<Tabs.Tab value="Options"> <Tabs.Tab value="Options">{t('modal.tabs.options.title')}</Tabs.Tab>
{t('layout.header.addService.modal.tabs.options.title')} <Tabs.Tab value="Advanced Options">{t('modal.tabs.advancedOptions.title')}</Tabs.Tab>
</Tabs.Tab>
<Tabs.Tab value="Advanced Options">
{t('layout.header.addService.modal.tabs.advancedOptions.title')}
</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel value="Options"> <Tabs.Panel value="Options">
<Stack> <Stack>
<TextInput <TextInput
required required
label={t('layout.header.addService.modal.tabs.options.form.serviceName.label')} label={t('modal.tabs.options.form.serviceName.label')}
placeholder={t( placeholder={t('modal.tabs.options.form.serviceName.placeholder')}
'layout.header.addService.modal.tabs.options.form.serviceName.placeholder'
)}
{...form.getInputProps('name')} {...form.getInputProps('name')}
/> />
<TextInput <TextInput
required required
label={t('layout.header.addService.modal.tabs.options.form.iconUrl.label')} label={t('modal.tabs.options.form.iconUrl.label')}
placeholder={DEFAULT_ICON} placeholder={DEFAULT_ICON}
{...form.getInputProps('icon')} {...form.getInputProps('icon')}
/> />
<TextInput <TextInput
required required
label={t('layout.header.addService.modal.tabs.options.form.serviceUrl.label')} label={t('modal.tabs.options.form.serviceUrl.label')}
placeholder="http://localhost:7575" placeholder="http://localhost:7575"
{...form.getInputProps('url')} {...form.getInputProps('url')}
/> />
<TextInput <TextInput
label={t('layout.header.addService.modal.tabs.options.form.onClickUrl.label')} label={t('modal.tabs.options.form.onClickUrl.label')}
placeholder="http://sonarr.example.com" placeholder="http://sonarr.example.com"
{...form.getInputProps('openedUrl')} {...form.getInputProps('openedUrl')}
/> />
<Select <Select
label={t('layout.header.addService.modal.tabs.options.form.serviceType.label')} label={t('modal.tabs.options.form.serviceType.label')}
defaultValue={t( defaultValue={t('modal.tabs.options.form.serviceType.defaultValue')}
'layout.header.addService.modal.tabs.options.form.serviceType.defaultValue' placeholder={t('modal.tabs.options.form.serviceType.placeholder')}
)}
placeholder={t(
'layout.header.addService.modal.tabs.options.form.serviceType.placeholder'
)}
required required
searchable searchable
data={ServiceTypeList} data={ServiceTypeList}
{...form.getInputProps('type')} {...form.getInputProps('type')}
/> />
<Select <Select
label={t('layout.header.addService.modal.tabs.options.form.category.label')} label={t('modal.tabs.options.form.category.label')}
data={categories} data={categories}
placeholder={t( placeholder={t('modal.tabs.options.form.category.placeholder')}
'layout.header.addService.modal.tabs.options.form.category.placeholder' nothingFound={t('modal.tabs.options.form.category.nothingFound')}
)}
nothingFound={t(
'layout.header.addService.modal.tabs.options.form.category.nothingFound'
)}
searchable searchable
clearable clearable
creatable creatable
@@ -269,7 +257,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
return item; return item;
}} }}
getCreateLabel={(query) => getCreateLabel={(query) =>
t('layout.header.addService.modal.tabs.options.form.category.createLabel', { t('modal.tabs.options.form.category.createLabel', {
query, query,
}) })
} }
@@ -285,36 +273,26 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
<> <>
<TextInput <TextInput
required required
label={t( label={t('modal.tabs.options.form.integrations.apiKey.label')}
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.label' placeholder={t('modal.tabs.options.form.integrations.apiKey.placeholder')}
)}
placeholder={t(
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.placeholder'
)}
value={form.values.apiKey} value={form.values.apiKey}
onChange={(event) => { onChange={(event) => {
form.setFieldValue('apiKey', event.currentTarget.value); form.setFieldValue('apiKey', event.currentTarget.value);
}} }}
error={ error={
form.errors.apiKey && form.errors.apiKey &&
t( t('modal.tabs.options.form.integrations.apiKey.validation.noKey')
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.validation.noKey'
)
} }
/> />
<Tip> <Tip>
{t( {t('modal.tabs.options.form.integrations.apiKey.tip.text')}{' '}
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.tip.text'
)}{' '}
<Anchor <Anchor
target="_blank" target="_blank"
weight="bold" weight="bold"
style={{ fontStyle: 'inherit', fontSize: 'inherit' }} style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
href={`${hostname}/settings/general`} href={`${hostname}/settings/general`}
> >
{t( {t('modal.tabs.options.form.integrations.apiKey.tip.link')}
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.tip.link'
)}
</Anchor> </Anchor>
</Tip> </Tip>
</> </>
@@ -323,11 +301,9 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
<> <>
<TextInput <TextInput
required required
label={t( label={t('modal.tabs.options.form.integrations.qBittorrent.username.label')}
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.username.label'
)}
placeholder={t( placeholder={t(
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.username.placeholder' 'modal.tabs.options.form.integrations.qBittorrent.username.placeholder'
)} )}
value={form.values.username} value={form.values.username}
onChange={(event) => { onChange={(event) => {
@@ -336,17 +312,15 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
error={ error={
form.errors.username && form.errors.username &&
t( t(
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername' 'modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername'
) )
} }
/> />
<PasswordInput <PasswordInput
required required
label={t( label={t('modal.tabs.options.form.integrations.qBittorrent.password.label')}
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.password.label'
)}
placeholder={t( placeholder={t(
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.password.placeholder' 'modal.tabs.options.form.integrations.qBittorrent.password.placeholder'
)} )}
value={form.values.password} value={form.values.password}
onChange={(event) => { onChange={(event) => {
@@ -355,7 +329,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
error={ error={
form.errors.password && form.errors.password &&
t( t(
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword' 'modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword'
) )
} }
/> />
@@ -364,11 +338,9 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
{form.values.type === 'Deluge' && ( {form.values.type === 'Deluge' && (
<> <>
<PasswordInput <PasswordInput
label={t( label={t('modal.tabs.options.form.integrations.deluge.password.label')}
'layout.header.addService.modal.tabs.options.form.integrations.deluge.password.label'
)}
placeholder={t( placeholder={t(
'layout.header.addService.modal.tabs.options.form.integrations.deluge.password.placeholder' 'modal.tabs.options.form.integrations.deluge.password.placeholder'
)} )}
value={form.values.password} value={form.values.password}
onChange={(event) => { onChange={(event) => {
@@ -377,7 +349,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
error={ error={
form.errors.password && form.errors.password &&
t( t(
'layout.header.addService.modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword' 'modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword'
) )
} }
/> />
@@ -386,11 +358,9 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
{form.values.type === 'Transmission' && ( {form.values.type === 'Transmission' && (
<> <>
<TextInput <TextInput
label={t( label={t('modal.tabs.options.form.integrations.transmission.username.label')}
'layout.header.addService.modal.tabs.options.form.integrations.transmission.username.label'
)}
placeholder={t( placeholder={t(
'layout.header.addService.modal.tabs.options.form.integrations.transmission.username.placeholder' 'modal.tabs.options.form.integrations.transmission.username.placeholder'
)} )}
value={form.values.username} value={form.values.username}
onChange={(event) => { onChange={(event) => {
@@ -399,16 +369,14 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
error={ error={
form.errors.username && form.errors.username &&
t( t(
'layout.header.addService.modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername' 'modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername'
) )
} }
/> />
<PasswordInput <PasswordInput
label={t( label={t('modal.tabs.options.form.integrations.transmission.password.label')}
'layout.header.addService.modal.tabs.options.form.integrations.transmission.password.label'
)}
placeholder={t( placeholder={t(
'layout.header.addService.modal.tabs.options.form.integrations.transmission.password.placeholder' 'modal.tabs.options.form.integrations.transmission.password.placeholder'
)} )}
value={form.values.password} value={form.values.password}
onChange={(event) => { onChange={(event) => {
@@ -417,7 +385,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
error={ error={
form.errors.password && form.errors.password &&
t( t(
'layout.header.addService.modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword' 'modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword'
) )
} }
/> />
@@ -425,32 +393,24 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
)} )}
</Stack> </Stack>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value={t('layout.header.addService.modal.tabs.advancedOptions.title')}> <Tabs.Panel value={t('modal.tabs.advancedOptions.title')}>
<Stack> <Stack>
<MultiSelect <MultiSelect
required required
label={t( label={t('modal.tabs.advancedOptions.form.httpStatusCodes.label')}
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.label'
)}
data={StatusCodes} data={StatusCodes}
placeholder={t( placeholder={t('modal.tabs.advancedOptions.form.httpStatusCodes.placeholder')}
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.placeholder'
)}
clearButtonLabel={t( clearButtonLabel={t(
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.clearButtonLabel' 'modal.tabs.advancedOptions.form.httpStatusCodes.clearButtonLabel'
)}
nothingFound={t(
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.nothingFound'
)} )}
nothingFound={t('modal.tabs.advancedOptions.form.httpStatusCodes.nothingFound')}
defaultValue={['200']} defaultValue={['200']}
clearable clearable
searchable searchable
{...form.getInputProps('status')} {...form.getInputProps('status')}
/> />
<Switch <Switch
label={t( label={t('modal.tabs.advancedOptions.form.openServiceInNewTab.label')}
'layout.header.addService.modal.tabs.advancedOptions.form.openServiceInNewTab.label'
)}
defaultChecked={form.values.newTab} defaultChecked={form.values.newTab}
{...form.getInputProps('newTab')} {...form.getInputProps('newTab')}
/> />
@@ -459,8 +419,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
</Tabs> </Tabs>
<Group grow position="center" mt="xl"> <Group grow position="center" mt="xl">
<Button type="submit"> <Button type="submit">
{props.message ?? {props.message ?? t('modal.tabs.advancedOptions.form.buttons.submit.content')}
t('layout.header.addService.modal.tabs.advancedOptions.form.buttons.submit.content')}
</Button> </Button>
</Group> </Group>
</form> </form>

View File

@@ -11,6 +11,7 @@ import {
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import { arrayMove, SortableContext } from '@dnd-kit/sortable'; import { arrayMove, SortableContext } from '@dnd-kit/sortable';
import { useLocalStorage } from '@mantine/hooks'; import { useLocalStorage } from '@mantine/hooks';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem'; import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem';
@@ -36,6 +37,8 @@ const AppShelf = (props: any) => {
const [activeId, setActiveId] = useState(null); const [activeId, setActiveId] = useState(null);
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const { t } = useTranslation('layout/app-shelf');
const sensors = useSensors( const sensors = useSensors(
useSensor(TouchSensor, { useSensor(TouchSensor, {
activationConstraint: { activationConstraint: {
@@ -147,13 +150,13 @@ const AppShelf = (props: any) => {
{/* Return the item for all services without category */} {/* Return the item for all services without category */}
{noCategory && noCategory.length > 0 ? ( {noCategory && noCategory.length > 0 ? (
<Accordion.Item key="Other" value="Other"> <Accordion.Item key="Other" value="Other">
<Accordion.Control>Other</Accordion.Control> <Accordion.Control>{t('accordions.others.text')}</Accordion.Control>
<Accordion.Panel>{getItems()}</Accordion.Panel> <Accordion.Panel>{getItems()}</Accordion.Panel>
</Accordion.Item> </Accordion.Item>
) : null} ) : null}
{downloadEnabled ? ( {downloadEnabled ? (
<Accordion.Item key="Downloads" value="Your downloads"> <Accordion.Item key="Downloads" value="Your downloads">
<Accordion.Control>Your downloads</Accordion.Control> <Accordion.Control>{t('accordions.downloads.text')}</Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
<Paper <Paper
p="lg" p="lg"

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core'; import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core';
import { IconSun as Sun, IconMoonStars as MoonStars } from '@tabler/icons'; import { IconSun as Sun, IconMoonStars as MoonStars } from '@tabler/icons';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
root: { root: {
@@ -34,6 +34,7 @@ export function ColorSchemeSwitch() {
const { config } = useConfig(); const { config } = useConfig();
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const { t } = useTranslation('settings/general/theme-selector');
return ( return (
<Group> <Group>
@@ -42,8 +43,8 @@ export function ColorSchemeSwitch() {
<MoonStars className={cx(classes.icon, classes.iconDark)} size={18} /> <MoonStars className={cx(classes.icon, classes.iconDark)} size={18} />
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" /> <Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
</div> </div>
{t('settings.tabs.common.settings.colorScheme.label', { {t('label', {
scheme: colorScheme === 'dark' ? 'light' : 'dark', theme: colorScheme === 'dark' ? 'light' : 'dark',
})} })}
<Group spacing={2}> <Group spacing={2}>
<Kbd>Ctrl</Kbd>+<Kbd>J</Kbd> <Kbd>Ctrl</Kbd>+<Kbd>J</Kbd>

View File

@@ -1,6 +1,6 @@
import { Center, Loader, Select, Tooltip } from '@mantine/core'; import { Center, Loader, Select, Tooltip } from '@mantine/core';
import { setCookie } from 'cookies-next'; import { setCookie } from 'cookies-next';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
@@ -8,6 +8,8 @@ export default function ConfigChanger() {
const { config, loadConfig, setConfig, getConfigs } = useConfig(); const { config, loadConfig, setConfig, getConfigs } = useConfig();
const [configList, setConfigList] = useState<string[]>([]); const [configList, setConfigList] = useState<string[]>([]);
const [value, setValue] = useState(config.name); const [value, setValue] = useState(config.name);
const { t } = useTranslation('settings/general/config-changer');
useEffect(() => { useEffect(() => {
getConfigs().then((configs) => setConfigList(configs)); getConfigs().then((configs) => setConfigList(configs));
}, [config]); }, [config]);
@@ -24,7 +26,7 @@ export default function ConfigChanger() {
// return <Select data={[{ value: '1', label: '1' },]} onChange={(e) => console.log(e)} value="1" />; // return <Select data={[{ value: '1', label: '1' },]} onChange={(e) => console.log(e)} value="1" />;
return ( return (
<Select <Select
label={t('settings.tabs.common.settings.configChanger.configSelect.label')} label={t('configSelect.label')}
value={value} value={value}
defaultValue={config.name} defaultValue={config.name}
onChange={(e) => { onChange={(e) => {

View File

@@ -4,6 +4,7 @@ import { showNotification } from '@mantine/notifications';
import axios from 'axios'; import axios from 'axios';
import fileDownload from 'js-file-download'; import fileDownload from 'js-file-download';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { import {
IconCheck as Check, IconCheck as Check,
IconDownload as Download, IconDownload as Download,
@@ -12,11 +13,11 @@ import {
IconX as X, IconX as X,
} from '@tabler/icons'; } from '@tabler/icons';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { t } from 'i18next';
export default function SaveConfigComponent(props: any) { export default function SaveConfigComponent(props: any) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const { t } = useTranslation('settings/general/config-changer');
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
configName: config.name, configName: config.name,
@@ -29,12 +30,7 @@ export default function SaveConfigComponent(props: any) {
} }
return ( return (
<Group spacing="xs"> <Group spacing="xs">
<Modal <Modal radius="md" opened={opened} onClose={() => setOpened(false)} title={t('modal.title')}>
radius="md"
opened={opened}
onClose={() => setOpened(false)}
title={t('settings.tabs.common.settings.configChanger.modal.title')}
>
<form <form
onSubmit={form.onSubmit((values) => { onSubmit={form.onSubmit((values) => {
setConfig({ ...config, name: values.configName }); setConfig({ ...config, name: values.configName });
@@ -51,21 +47,17 @@ export default function SaveConfigComponent(props: any) {
> >
<TextInput <TextInput
required required
label={t('settings.tabs.common.settings.configChanger.modal.form.configName.label')} label={t('modal.form.configName.label')}
placeholder={t( placeholder={t('modal.form.configName.placeholder')}
'settings.tabs.common.settings.configChanger.modal.form.configName.placeholder'
)}
{...form.getInputProps('configName')} {...form.getInputProps('configName')}
/> />
<Group position="right" mt="md"> <Group position="right" mt="md">
<Button type="submit"> <Button type="submit">{t('modal.form.buttons.submit')}</Button>
{t('settings.tabs.common.settings.configChanger.modal.form.buttons.submit')}
</Button>
</Group> </Group>
</form> </form>
</Modal> </Modal>
<Button size="xs" leftIcon={<Download />} variant="outline" onClick={onClick}> <Button size="xs" leftIcon={<Download />} variant="outline" onClick={onClick}>
{t('settings.tabs.common.settings.configChanger.buttons.download')} {t('buttons.download')}
</Button> </Button>
<Button <Button
size="xs" size="xs"
@@ -76,39 +68,31 @@ export default function SaveConfigComponent(props: any) {
.delete(`/api/configs/${config.name}`) .delete(`/api/configs/${config.name}`)
.then(() => { .then(() => {
showNotification({ showNotification({
title: t( title: t('buttons.delete.deleted.title'),
'settings.tabs.common.settings.configChanger.buttons.delete.deleted.title'
),
icon: <Check />, icon: <Check />,
color: 'green', color: 'green',
autoClose: 1500, autoClose: 1500,
radius: 'md', radius: 'md',
message: t( message: t('buttons.delete.deleted.message'),
'settings.tabs.common.settings.configChanger.buttons.delete.deleted.message'
),
}); });
}) })
.catch(() => { .catch(() => {
showNotification({ showNotification({
title: t( title: t('buttons.delete.deleteFailed.title'),
'settings.tabs.common.settings.configChanger.buttons.delete.deleteFailed.title'
),
icon: <X />, icon: <X />,
color: 'red', color: 'red',
autoClose: 1500, autoClose: 1500,
radius: 'md', radius: 'md',
message: t( message: t('buttons.delete.deleteFailed.message'),
'settings.tabs.common.settings.configChanger.buttons.delete.deleteFailed.message'
),
}); });
}); });
setConfig({ ...config, name: 'default' }); setConfig({ ...config, name: 'default' });
}} }}
> >
{t('settings.tabs.common.settings.configChanger.buttons.delete.text')} {t('buttons.delete.text')}
</Button> </Button>
<Button size="xs" leftIcon={<Plus />} variant="outline" onClick={() => setOpened(true)}> <Button size="xs" leftIcon={<Plus />} variant="outline" onClick={() => setOpened(true)}>
{t('settings.tabs.common.settings.configChanger.buttons.saveCopy')} {t('buttons.saveCopy')}
</Button> </Button>
</Group> </Group>
); );

View File

@@ -1,14 +1,15 @@
import { TextInput, Button, Stack } from '@mantine/core'; import { TextInput, Button, Stack } from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ColorSelector } from './ColorSelector'; import { ColorSelector } from './ColorSelector';
import { OpacitySelector } from './OpacitySelector'; import { OpacitySelector } from './OpacitySelector';
import { AppCardWidthSelector } from './AppCardWidthSelector'; import { AppCardWidthSelector } from './AppCardWidthSelector';
import { ShadeSelector } from './ShadeSelector'; import { ShadeSelector } from './ShadeSelector';
import { t } from 'i18next';
export default function TitleChanger() { export default function TitleChanger() {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const { t } = useTranslation('settings/customization/page-appearance');
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
@@ -42,26 +43,26 @@ export default function TitleChanger() {
<form onSubmit={form.onSubmit((values) => saveChanges(values))}> <form onSubmit={form.onSubmit((values) => saveChanges(values))}>
<Stack> <Stack>
<TextInput <TextInput
label={t('settings.tabs.customizations.settings.pageTitle.label')} label={t('pageTitle.label')}
placeholder={t('settings.tabs.customizations.settings.pageTitle.placeholder')} placeholder={t('pageTitle.placeholder')}
{...form.getInputProps('title')} {...form.getInputProps('title')}
/> />
<TextInput <TextInput
label={t('settings.tabs.customizations.settings.logo.label')} label={t('logo.label')}
placeholder={t('settings.tabs.customizations.settings.logo.placeholder')} placeholder={t('logo.placeholder')}
{...form.getInputProps('logo')} {...form.getInputProps('logo')}
/> />
<TextInput <TextInput
label={t('settings.tabs.customizations.settings.favicon.label')} label={t('favicon.label')}
placeholder={t('settings.tabs.customizations.settings.favicon.placeholder')} placeholder={t('favicon.placeholder')}
{...form.getInputProps('favicon')} {...form.getInputProps('favicon')}
/> />
<TextInput <TextInput
label={t('settings.tabs.customizations.settings.background.label')} label={t('background.label')}
placeholder={t('settings.tabs.customizations.settings.background.placeholder')} placeholder={t('background.placeholder')}
{...form.getInputProps('background')} {...form.getInputProps('background')}
/> />
<Button type="submit">{t('settings.tabs.customizations.settings.buttons.submit')}</Button> <Button type="submit">{t('buttons.submit')}</Button>
</Stack> </Stack>
</form> </form>
<ColorSelector type="primary" /> <ColorSelector type="primary" />

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ColorSwatch, Grid, Group, Popover, Text, useMantineTheme } from '@mantine/core'; import { ColorSwatch, Grid, Group, Popover, Text, useMantineTheme } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { useColorTheme } from '../../tools/color'; import { useColorTheme } from '../../tools/color';
import { t } from 'i18next';
interface ColorControlProps { interface ColorControlProps {
type: string; type: string;
@@ -11,8 +11,8 @@ interface ColorControlProps {
export function ColorSelector({ type }: ColorControlProps) { export function ColorSelector({ type }: ColorControlProps) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const { primaryColor, secondaryColor, setPrimaryColor, setSecondaryColor } = useColorTheme(); const { primaryColor, secondaryColor, setPrimaryColor, setSecondaryColor } = useColorTheme();
const { t } = useTranslation('settings/customization/color-selector');
const theme = useMantineTheme(); const theme = useMantineTheme();
const colors = Object.keys(theme.colors).map((color) => ({ const colors = Object.keys(theme.colors).map((color) => ({
@@ -84,7 +84,7 @@ export function ColorSelector({ type }: ColorControlProps) {
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
<Text> <Text>
{t('settings.tabs.customizations.settings.colorSelector.suffix', { {t('suffix', {
color: type[0].toUpperCase() + type.slice(1), color: type[0].toUpperCase() + type.slice(1),
})} })}
</Text> </Text>

View File

@@ -1,5 +1,6 @@
import { Text, SegmentedControl, TextInput, Stack } from '@mantine/core'; import { Text, SegmentedControl, TextInput, Stack } from '@mantine/core';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch'; import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch'; import { WidgetsPositionSwitch } from '../WidgetsPositionSwitch/WidgetsPositionSwitch';
@@ -7,11 +8,14 @@ import ConfigChanger from '../Config/ConfigChanger';
import SaveConfigComponent from '../Config/SaveConfig'; import SaveConfigComponent from '../Config/SaveConfig';
import ModuleEnabler from './ModuleEnabler'; import ModuleEnabler from './ModuleEnabler';
import Tip from '../layout/Tip'; import Tip from '../layout/Tip';
import { t } from 'i18next';
import LanguageSwitch from './LanguageSwitch'; import LanguageSwitch from './LanguageSwitch';
export default function CommonSettings(args: any) { export default function CommonSettings(args: any) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const { t } = useTranslation([
'settings/general/search-engine',
'settings/general/config-changer',
]);
const matches = [ const matches = [
{ label: 'Google', value: 'https://google.com/search?q=' }, { label: 'Google', value: 'https://google.com/search?q=' },
@@ -28,12 +32,12 @@ export default function CommonSettings(args: any) {
return ( return (
<Stack mb="md" mr="sm"> <Stack mb="md" mr="sm">
<Stack spacing={0} mt="xs"> <Stack spacing={0} mt="xs">
<Text>{t('settings.tabs.common.settings.searchEngine.title')}</Text> <Text>{t('title')}</Text>
<Tip>{t('settings.tabs.common.settings.searchEngine.tips.generalTip')}</Tip> <Tip>{t('tips.generalTip')}</Tip>
<SegmentedControl <SegmentedControl
fullWidth fullWidth
mb="sm" mb="sm"
title={t('settings.tabs.common.settings.searchEngine.title')} title={t('title')}
value={ value={
// Match config.settings.searchUrl with a key in the matches array // Match config.settings.searchUrl with a key in the matches array
searchUrl searchUrl
@@ -55,10 +59,10 @@ export default function CommonSettings(args: any) {
/> />
{searchUrl === 'Custom' && ( {searchUrl === 'Custom' && (
<> <>
<Tip>{t('settings.tabs.common.settings.searchEngine.tips.placeholderTip')}</Tip> <Tip>{t('tips.placeholderTip')}</Tip>
<TextInput <TextInput
label={t('settings.tabs.common.settings.searchEngine.customEngine.label')} label={t('customEngine.label')}
placeholder={t('settings.tabs.common.settings.searchEngine.customEngine.placeholder')} placeholder={t('customEngine.placeholder')}
value={customSearchUrl} value={customSearchUrl}
onChange={(event) => { onChange={(event) => {
setCustomSearchUrl(event.currentTarget.value); setCustomSearchUrl(event.currentTarget.value);
@@ -80,7 +84,7 @@ export default function CommonSettings(args: any) {
<LanguageSwitch /> <LanguageSwitch />
<ConfigChanger /> <ConfigChanger />
<SaveConfigComponent /> <SaveConfigComponent />
<Tip>{t('settings.tabs.common.settings.configTip')}</Tip> <Tip>{t('configTip')}</Tip>
</Stack> </Stack>
); );
} }

View File

@@ -1,10 +1,12 @@
import { Group, ActionIcon, Anchor, Text } from '@mantine/core'; import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons'; import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import { CURRENT_VERSION } from '../../../data/constants'; import { CURRENT_VERSION } from '../../../data/constants';
export default function Credits(props: any) { export default function Credits(props: any) {
const { t } = useTranslation('settings/common');
return ( return (
<Group position="center" mt="xs"> <Group position="center" mt="xs">
<Group spacing={0}> <Group spacing={0}>
@@ -29,7 +31,7 @@ export default function Credits(props: any) {
color: 'gray', color: 'gray',
}} }}
> >
{t('settings.credits.madeWithLove')} {t('credits.madeWithLove')}
<Anchor <Anchor
href="https://github.com/ajnart" href="https://github.com/ajnart"
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }} style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}

View File

@@ -3,27 +3,26 @@ import { showNotification } from '@mantine/notifications';
import { IconLanguage } from '@tabler/icons'; import { IconLanguage } from '@tabler/icons';
import { forwardRef, useState } from 'react'; import { forwardRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'next-i18next';
import { convertCodeToName } from '../../translations/i18n';
export default function LanguageSwitch() { export default function LanguageSwitch() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation('settings/general/internationalization');
const { language, languages, changeLanguage } = i18n; /*const { language, languages, changeLanguage } = i18n;
const [selectedLanguage, setSelectedLanguage] = useState<string | null>(language); const [selectedLanguage, setSelectedLanguage] = useState<string | null>(language);
const data = languages.map((language) => ({ const data = languages.map((language) => ({
image: `https://countryflagsapi.com/png/${language.split('-').pop()}`, image: `https://countryflagsapi.com/png/${language.split('-').pop()}`,
label: convertCodeToName(language), label: 'JA',
value: language, value: language,
})); }));*/
const onChangeSelect = (value: string) => { const onChangeSelect = (value: string) => {
setSelectedLanguage(value); //setSelectedLanguage(value);
const languageName = convertCodeToName(value); const languageName = 'JA IS HALZ SCHEISSE NE';
changeLanguage(value) /*changeLanguage(value)
.then(() => { .then(() => {
showNotification({ showNotification({
title: 'Language changed', title: 'Language changed',
@@ -39,20 +38,27 @@ export default function LanguageSwitch() {
color: 'red', color: 'red',
autoClose: 5000, autoClose: 5000,
}); });
}); });*/
}; };
return ( return (
<Stack> <Stack>
<Select <Select
icon={<IconLanguage size={18} />} icon={<IconLanguage size={18} />}
label={t('settings.tabs.common.settings.language.title')} label={t('label')}
data={data} data={[
{
value: 'uwu',
label: 'asdf',
},
]}
itemComponent={SelectItem} itemComponent={SelectItem}
nothingFound="Nothing found" nothingFound="Nothing found"
onChange={onChangeSelect} onChange={onChangeSelect}
/*
value={selectedLanguage} value={selectedLanguage}
defaultValue={language} defaultValue={language}
*/
/> />
</Stack> </Stack>
); );

View File

@@ -1,14 +1,15 @@
import { Checkbox, SimpleGrid, Stack, Title } from '@mantine/core'; import { Checkbox, SimpleGrid, Stack, Title } from '@mantine/core';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import * as Modules from '../../modules'; import * as Modules from '../../modules';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
export default function ModuleEnabler(props: any) { export default function ModuleEnabler(props: any) {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const { t } = useTranslation('settings/general/module-enabler');
const modules = Object.values(Modules).map((module) => module); const modules = Object.values(Modules).map((module) => module);
return ( return (
<Stack> <Stack>
<Title order={4}>{t('settings.tabs.common.settings.moduleEnabler.title')}</Title> <Title order={4}>{t('title')}</Title>
<SimpleGrid cols={3} spacing="xs"> <SimpleGrid cols={3} spacing="xs">
{modules.map((module) => ( {modules.map((module) => (
<Checkbox <Checkbox

View File

@@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { Text, Slider, Stack } from '@mantine/core'; import { Text, Slider, Stack } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { t } from 'i18next';
export function OpacitySelector() { export function OpacitySelector() {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const { t } = useTranslation('settings/customization/shade-selector');
const MARKS = [ const MARKS = [
{ value: 10, label: '10' }, { value: 10, label: '10' },
@@ -31,7 +32,7 @@ export function OpacitySelector() {
return ( return (
<Stack spacing="xs"> <Stack spacing="xs">
<Text>{t('settings.tabs.customizations.settings.opacitySelector.label')}</Text> <Text>{t('label')}</Text>
<Slider <Slider
defaultValue={config.settings.appOpacity || 100} defaultValue={config.settings.appOpacity || 100}
step={10} step={10}

View File

@@ -2,18 +2,20 @@ import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/c
import { useHotkeys } from '@mantine/hooks'; import { useHotkeys } from '@mantine/hooks';
import { useState } from 'react'; import { useState } from 'react';
import { IconSettings } from '@tabler/icons'; import { IconSettings } from '@tabler/icons';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import AdvancedSettings from './AdvancedSettings'; import AdvancedSettings from './AdvancedSettings';
import CommonSettings from './CommonSettings'; import CommonSettings from './CommonSettings';
import Credits from './Credits'; import Credits from './Credits';
function SettingsMenu(props: any) { function SettingsMenu(props: any) {
const { t } = useTranslation('settings/common');
return ( return (
<Tabs defaultValue="Common"> <Tabs defaultValue="Common">
<Tabs.List grow> <Tabs.List grow>
<Tabs.Tab value="Common">{t('settings.tabs.common.title')}</Tabs.Tab> <Tabs.Tab value="Common">{t('tabs.common')}</Tabs.Tab>
<Tabs.Tab value="Customizations">{t('settings.tabs.customizations.title')}</Tabs.Tab> <Tabs.Tab value="Customizations">{t('tabs.customizations')}</Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel data-autofocus value="Common"> <Tabs.Panel data-autofocus value="Common">
<ScrollArea style={{ height: '78vh' }} offsetScrollbars> <ScrollArea style={{ height: '78vh' }} offsetScrollbars>
@@ -31,22 +33,24 @@ function SettingsMenu(props: any) {
export function SettingsMenuButton(props: any) { export function SettingsMenuButton(props: any) {
useHotkeys([['ctrl+L', () => setOpened(!opened)]]); useHotkeys([['ctrl+L', () => setOpened(!opened)]]);
const { t } = useTranslation('settings/common');
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
return ( return (
<> <>
<Drawer <Drawer
size="xl" size="xl"
padding="lg" padding="lg"
position="right" position="right"
title={<Title order={5}>{t('settings.title')}</Title>} title={<Title order={5}>{t('title')}</Title>}
opened={props.opened || opened} opened={props.opened || opened}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
> >
<SettingsMenu /> <SettingsMenu />
<Credits /> <Credits />
</Drawer> </Drawer>
<Tooltip label={t('settings.tooltip')}> <Tooltip label={t('tooltip')}>
<ActionIcon <ActionIcon
variant="default" variant="default"
radius="md" radius="md"

View File

@@ -9,13 +9,14 @@ import {
Stack, Stack,
Grid, Grid,
} from '@mantine/core'; } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { useColorTheme } from '../../tools/color'; import { useColorTheme } from '../../tools/color';
import { t } from 'i18next';
export function ShadeSelector() { export function ShadeSelector() {
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const { t } = useTranslation('settings/general/shade-selector');
const { primaryColor, secondaryColor, primaryShade, setPrimaryShade } = useColorTheme(); const { primaryColor, secondaryColor, primaryShade, setPrimaryShade } = useColorTheme();
@@ -95,7 +96,7 @@ export function ShadeSelector() {
</Stack> </Stack>
</Popover.Dropdown> </Popover.Dropdown>
</Popover> </Popover>
<Text>{t('settings.tabs.customizations.settings.shadeSelector.label')}</Text> <Text>{t('label')}</Text>
</Group> </Group>
); );
} }

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { createStyles, Switch, Group } from '@mantine/core'; import { createStyles, Switch, Group } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { t } from 'i18next';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
root: { root: {
@@ -34,6 +34,7 @@ export function WidgetsPositionSwitch() {
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const defaultPosition = config?.settings?.widgetPosition || 'right'; const defaultPosition = config?.settings?.widgetPosition || 'right';
const [widgetPosition, setWidgetPosition] = useState(defaultPosition); const [widgetPosition, setWidgetPosition] = useState(defaultPosition);
const { t } = useTranslation('settings/general/widget-positions');
const toggleWidgetPosition = () => { const toggleWidgetPosition = () => {
const position = widgetPosition === 'right' ? 'left' : 'right'; const position = widgetPosition === 'right' ? 'left' : 'right';
setWidgetPosition(position); setWidgetPosition(position);
@@ -55,7 +56,7 @@ export function WidgetsPositionSwitch() {
size="md" size="md"
/> />
</div> </div>
{t('settings.tabs.common.settings.widgetsPositionSwitch.label')} {t('label')}
</Group> </Group>
); );
} }

View File

@@ -23,16 +23,16 @@ import {
} from '../common'; } from '../common';
import { serviceItem } from '../../tools/types'; import { serviceItem } from '../../tools/types';
import { useColorTheme } from '../../tools/color'; import { useColorTheme } from '../../tools/color';
import { t } from 'i18next';
export const CalendarModule: IModule = { export const CalendarModule: IModule = {
title: t('modules.calendar.title'), title: 'Calendar',
description: t('modules.calendar.description'), description:
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
icon: CalendarIcon, icon: CalendarIcon,
component: CalendarComponent, component: CalendarComponent,
options: { options: {
sundaystart: { sundaystart: {
name: t('modules.calendar.options.sundayStart'), name: 'Start the week on Sunday',
value: false, value: false,
}, },
}, },

View File

@@ -1,6 +1,6 @@
import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core'; import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core';
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons'; import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { useColorTheme } from '../../tools/color'; import { useColorTheme } from '../../tools/color';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
@@ -160,6 +160,7 @@ export function SonarrMediaDisplay(props: any) {
export function MediaDisplay({ media }: { media: IMedia }) { export function MediaDisplay({ media }: { media: IMedia }) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const { secondaryColor } = useColorTheme(); const { secondaryColor } = useColorTheme();
const { t } = useTranslation('modules/common-media-cards-module');
return ( return (
<Group mr="xs" align="stretch" noWrap style={{ maxHeight: 200 }}> <Group mr="xs" align="stretch" noWrap style={{ maxHeight: 200 }}>
@@ -210,7 +211,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
size="sm" size="sm"
rightIcon={<IconPlayerPlay size={15} />} rightIcon={<IconPlayerPlay size={15} />}
> >
{t('modules.common.mediaCard.buttons.play')} {t('buttons.play')}
</Button> </Button>
)} )}
{media.imdbId && ( {media.imdbId && (
@@ -250,7 +251,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
size="sm" size="sm"
rightIcon={<IconDownload size={15} />} rightIcon={<IconDownload size={15} />}
> >
{t('modules.common.mediaCard.buttons.request')} {t('buttons.request')}
</Button> </Button>
</> </>
)} )}

View File

@@ -1,7 +1,7 @@
import { createStyles, Stack, Title, useMantineColorScheme, useMantineTheme } from '@mantine/core'; import { createStyles, Stack, Title, useMantineColorScheme, useMantineTheme } from '@mantine/core';
import { IconCalendar as CalendarIcon } from '@tabler/icons'; import { IconCalendar as CalendarIcon } from '@tabler/icons';
import axios from 'axios'; import axios from 'axios';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { serviceItem } from '../../tools/types'; import { serviceItem } from '../../tools/types';
@@ -142,32 +142,34 @@ export function DashdotComponent() {
const totalSize = const totalSize =
(info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0; (info?.storage?.layout as any[])?.reduce((acc, curr) => (curr.size ?? 0) + acc, 0) ?? 0;
const { t } = useTranslation('modules/dashdot-module');
const graphs = [ const graphs = [
{ {
name: t('modules.dashDot.card.graphs.cpu.title'), name: t('card.graphs.cpu.title'),
enabled: cpuEnabled, enabled: cpuEnabled,
params: { params: {
multiView: dashConfig?.cpuMultiView?.value ?? false, multiView: dashConfig?.cpuMultiView?.value ?? false,
}, },
}, },
{ {
name: t('modules.dashDot.card.graphs.cpu.title'), name: t('card.graphs.cpu.title'),
enabled: storageEnabled && !isCompact, enabled: storageEnabled && !isCompact,
params: { params: {
multiView: dashConfig?.storageMultiView?.value ?? false, multiView: dashConfig?.storageMultiView?.value ?? false,
}, },
}, },
{ {
name: t('modules.dashDot.card.graphs.memory.title'), name: t('card.graphs.memory.title'),
enabled: ramEnabled, enabled: ramEnabled,
}, },
{ {
name: t('modules.dashDot.card.graphs.network.title'), name: t('card.graphs.network.title'),
enabled: networkEnabled, enabled: networkEnabled,
spanTwo: true, spanTwo: true,
}, },
{ {
name: t('modules.dashDot.card.graphs.gpu.title'), name: t('card.graphs.gpu.title'),
enabled: gpuEnabled, enabled: gpuEnabled,
spanTwo: true, spanTwo: true,
}, },
@@ -176,26 +178,24 @@ export function DashdotComponent() {
if (dashdotUrl === '') { if (dashdotUrl === '') {
return ( return (
<div> <div>
<h2 className={classes.heading}>{t('modules.dashDot.card.title')}</h2> <h2 className={classes.heading}>{t('card.title')}</h2>
<p>{t('modules.dashDot.card.errors.noService')}</p> <p>{t('card.errors.noService')}</p>
</div> </div>
); );
} }
return ( return (
<div> <div>
<h2 className={classes.heading}>{t('modules.dashDot.card.title')}</h2> <h2 className={classes.heading}>{t('card.title')}</h2>
{!info ? ( {!info ? (
<p>{t('modules.dashDot.card.errors.noInformation')}</p> <p>{t('card.errors.noInformation')}</p>
) : ( ) : (
<div className={classes.graphsContainer}> <div className={classes.graphsContainer}>
<div className={classes.table}> <div className={classes.table}>
{storageEnabled && isCompact && ( {storageEnabled && isCompact && (
<div className={classes.tableRow}> <div className={classes.tableRow}>
<p className={classes.tableLabel}> <p className={classes.tableLabel}>{t('card.graphs.storage.label')}</p>
{t('modules.dashDot.card.graphs.storage.label')}
</p>
<p className={classes.tableValue}> <p className={classes.tableValue}>
{((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'} {((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'}
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)} {bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
@@ -204,15 +204,12 @@ export function DashdotComponent() {
)} )}
{networkEnabled && ( {networkEnabled && (
<div className={classes.tableRow}> <div className={classes.tableRow}>
<p className={classes.tableLabel}> <p className={classes.tableLabel}>{t('card.graphs.network.label')}</p>
{t('modules.dashDot.card.graphs.network.label')}
</p>
<p className={classes.tableValue}> <p className={classes.tableValue}>
{bpsPrettyPrint(info?.network?.speedUp)}{' '} {bpsPrettyPrint(info?.network?.speedUp)} {t('card.graphs.network.metrics.upload')}
{t('modules.dashDot.card.graphs.network.metrics.upload')}
{'\n'} {'\n'}
{bpsPrettyPrint(info?.network?.speedDown)} {bpsPrettyPrint(info?.network?.speedDown)}
{t('modules.dashDot.card.graphs.network.metrics.download')} {t('card.graphs.network.metrics.download')}
</p> </p>
</div> </div>
)} )}

View File

@@ -11,9 +11,10 @@ import {
} from '@tabler/icons'; } from '@tabler/icons';
import axios from 'axios'; import axios from 'axios';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { useState } from 'react';
import { useTranslation } from 'next-i18next';
import { tryMatchService } from '../../tools/addToHomarr'; import { tryMatchService } from '../../tools/addToHomarr';
import { AddAppShelfItemForm } from '../../components/AppShelf/AddAppShelfItem'; import { AddAppShelfItemForm } from '../../components/AppShelf/AddAppShelfItem';
import { useState } from 'react';
function sendDockerCommand( function sendDockerCommand(
action: string, action: string,
@@ -21,6 +22,8 @@ function sendDockerCommand(
containerName: string, containerName: string,
reload: () => void reload: () => void
) { ) {
const { t } = useTranslation('modules/docker-module');
showNotification({ showNotification({
id: containerId, id: containerId,
loading: true, loading: true,
@@ -34,8 +37,8 @@ function sendDockerCommand(
.then((res) => { .then((res) => {
updateNotification({ updateNotification({
id: containerId, id: containerId,
title: `Container ${containerName} ${action}ed`, title: t('messages.successfullyExecuted.message', { containerName, action }),
message: `Your container was successfully ${action}ed`, message: t('messages.successfullyExecuted.message', { action }),
icon: <IconCheck />, icon: <IconCheck />,
autoClose: 2000, autoClose: 2000,
}); });
@@ -44,7 +47,7 @@ function sendDockerCommand(
updateNotification({ updateNotification({
id: containerId, id: containerId,
color: 'red', color: 'red',
title: 'There was an error', title: t('errors.unknownError.title'),
message: err.response.data.reason, message: err.response.data.reason,
autoClose: 2000, autoClose: 2000,
}); });
@@ -61,6 +64,8 @@ export interface ContainerActionBarProps {
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) { export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
const [opened, setOpened] = useState<boolean>(false); const [opened, setOpened] = useState<boolean>(false);
const { t } = useTranslation('modules/docker-module');
return ( return (
<Group> <Group>
<Modal <Modal
@@ -68,12 +73,12 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
radius="md" radius="md"
opened={opened} opened={opened}
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
title="Add service" title={t('actionBar.addService.title')}
> >
<AddAppShelfItemForm <AddAppShelfItemForm
setOpened={setOpened} setOpened={setOpened}
{...tryMatchService(selected.at(0))} {...tryMatchService(selected.at(0))}
message="Add service to homarr" message={t('actionBar.addService.message')}
/> />
</Modal> </Modal>
<Button <Button
@@ -89,7 +94,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
color="orange" color="orange"
radius="md" radius="md"
> >
Restart {t('actionBar.restart.title')}
</Button> </Button>
<Button <Button
leftIcon={<IconPlayerStop />} leftIcon={<IconPlayerStop />}
@@ -104,7 +109,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
color="red" color="red"
radius="md" radius="md"
> >
Stop {t('actionBar.stop.title')}
</Button> </Button>
<Button <Button
leftIcon={<IconPlayerPlay />} leftIcon={<IconPlayerPlay />}
@@ -119,10 +124,10 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
color="green" color="green"
radius="md" radius="md"
> >
Start {t('actionBar.start.title')}
</Button> </Button>
<Button leftIcon={<IconRefresh />} onClick={() => reload()} variant="light" radius="md"> <Button leftIcon={<IconRefresh />} onClick={() => reload()} variant="light" radius="md">
Refresh data {t('actionBar.refreshData.title')}
</Button> </Button>
<Button <Button
leftIcon={<IconPlus />} leftIcon={<IconPlus />}
@@ -133,7 +138,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
if (selected.length !== 1) { if (selected.length !== 1) {
showNotification({ showNotification({
autoClose: 5000, autoClose: 5000,
title: <Title order={5}>Please only add one service at a time!</Title>, title: <Title order={5}>{t('errors.oneServiceAtATime')}</Title>,
color: 'red', color: 'red',
message: undefined, message: undefined,
}); });
@@ -142,7 +147,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
} }
}} }}
> >
Add to Homarr {t('actionBar.addToHomarr.title')}
</Button> </Button>
<Button <Button
leftIcon={<IconTrash />} leftIcon={<IconTrash />}
@@ -157,7 +162,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
) )
} }
> >
Remove {t('actionBar.remove.title')}
</Button> </Button>
</Group> </Group>
); );

View File

@@ -1,4 +1,5 @@
import { Badge, BadgeVariant, MantineSize } from '@mantine/core'; import { Badge, BadgeVariant, MantineSize } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
export interface ContainerStateProps { export interface ContainerStateProps {
@@ -7,6 +8,9 @@ export interface ContainerStateProps {
export default function ContainerState(props: ContainerStateProps) { export default function ContainerState(props: ContainerStateProps) {
const { state } = props; const { state } = props;
const { t } = useTranslation('modules/docker-module');
const options: { const options: {
size: MantineSize; size: MantineSize;
radius: MantineSize; radius: MantineSize;
@@ -20,28 +24,28 @@ export default function ContainerState(props: ContainerStateProps) {
case 'running': { case 'running': {
return ( return (
<Badge color="green" {...options}> <Badge color="green" {...options}>
Running {t('table.states.running')}
</Badge> </Badge>
); );
} }
case 'created': { case 'created': {
return ( return (
<Badge color="cyan" {...options}> <Badge color="cyan" {...options}>
Created {t('table.states.created')}
</Badge> </Badge>
); );
} }
case 'exited': { case 'exited': {
return ( return (
<Badge color="red" {...options}> <Badge color="red" {...options}>
Stopped {t('table.states.stopped')}
</Badge> </Badge>
); );
} }
default: { default: {
return ( return (
<Badge color="purple" {...options}> <Badge color="purple" {...options}>
Unknown {t('table.states.unknown')}
</Badge> </Badge>
); );
} }

View File

@@ -1,10 +1,10 @@
import { ActionIcon, Drawer, Group, LoadingOverlay, Text, Tooltip } from '@mantine/core'; import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
import axios from 'axios'; import axios from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Docker from 'dockerode'; import Docker from 'dockerode';
import { IconBrandDocker, IconX } from '@tabler/icons'; import { IconBrandDocker, IconX } from '@tabler/icons';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import ContainerActionBar from './ContainerActionBar'; import ContainerActionBar from './ContainerActionBar';
import DockerTable from './DockerTable'; import DockerTable from './DockerTable';
@@ -25,6 +25,8 @@ export default function DockerMenuButton(props: any) {
const { config } = useConfig(); const { config } = useConfig();
const moduleEnabled = config.modules?.[DockerModule.title]?.enabled ?? false; const moduleEnabled = config.modules?.[DockerModule.title]?.enabled ?? false;
const { t } = useTranslation('modules/docker-module');
useEffect(() => { useEffect(() => {
reload(); reload();
}, [config.modules]); }, [config.modules]);
@@ -44,10 +46,10 @@ export default function DockerMenuButton(props: any) {
// Send an Error notification // Send an Error notification
showNotification({ showNotification({
autoClose: 1500, autoClose: 1500,
title: <Text>{t('layout.header.docker.errors.integrationFailed.title')}</Text>, title: <Text>{t('errors.integrationFailed.title')}</Text>,
color: 'red', color: 'red',
icon: <IconX />, icon: <IconX />,
message: t('layout.header.docker.errors.integrationFailed.message'), message: t('errors.integrationFailed.message'),
}) })
); );
}, 300); }, 300);
@@ -69,7 +71,7 @@ export default function DockerMenuButton(props: any) {
> >
<DockerTable containers={containers} selection={selection} setSelection={setSelection} /> <DockerTable containers={containers} selection={selection} setSelection={setSelection} />
</Drawer> </Drawer>
<Tooltip label={t('layout.header.docker.actionIcon.tooltip')}> <Tooltip label={t('actionIcon.tooltip')}>
<ActionIcon <ActionIcon
variant="default" variant="default"
radius="md" radius="md"

View File

@@ -1,6 +1,6 @@
import { Table, Checkbox, Group, Badge, createStyles, ScrollArea, TextInput } from '@mantine/core'; import { Table, Checkbox, Group, Badge, createStyles, ScrollArea, TextInput } from '@mantine/core';
import { IconSearch } from '@tabler/icons'; import { IconSearch } from '@tabler/icons';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import Dockerode from 'dockerode'; import Dockerode from 'dockerode';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -28,6 +28,8 @@ export default function DockerTable({
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const { t } = useTranslation('modules/docker-module');
useEffect(() => { useEffect(() => {
setContainers(containers); setContainers(containers);
}, [containers]); }, [containers]);
@@ -83,7 +85,7 @@ export default function DockerTable({
))} ))}
{element.Ports.length > 3 && ( {element.Ports.length > 3 && (
<Badge variant="filled"> <Badge variant="filled">
{t('modules.docker.table.body.portCollapse', { ports: element.Ports.length - 3 })} {t('table.body.portCollapse', { ports: element.Ports.length - 3 })}
</Badge> </Badge>
)} )}
</Group> </Group>
@@ -98,7 +100,7 @@ export default function DockerTable({
return ( return (
<ScrollArea style={{ height: '80vh' }}> <ScrollArea style={{ height: '80vh' }}>
<TextInput <TextInput
placeholder={t('modules.docker.search.placeholder')} placeholder={t('search.placeholder')}
mt="md" mt="md"
icon={<IconSearch size={14} />} icon={<IconSearch size={14} />}
value={search} value={search}
@@ -115,10 +117,10 @@ export default function DockerTable({
transitionDuration={0} transitionDuration={0}
/> />
</th> </th>
<th>{t('modules.docker.table.header.name')}</th> <th>{t('table.header.name')}</th>
<th>{t('modules.docker.table.header.image')}</th> <th>{t('table.header.image')}</th>
<th>{t('modules.docker.table.header.ports')}</th> <th>{t('table.header.ports')}</th>
<th>{t('modules.docker.table.header.state')}</th> <th>{t('table.header.state')}</th>
</tr> </tr>
</thead> </thead>
<tbody>{rows}</tbody> <tbody>{rows}</tbody>

View File

@@ -20,7 +20,7 @@ import { useConfig } from '../../tools/state';
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem'; import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval'; import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
import { humanFileSize } from '../../tools/humanFileSize'; import { humanFileSize } from '../../tools/humanFileSize';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
export const DownloadsModule: IModule = { export const DownloadsModule: IModule = {
title: 'Torrent', title: 'Torrent',
@@ -50,6 +50,9 @@ export default function DownloadComponent() {
const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]); const [torrents, setTorrents] = useState<NormalizedTorrent[]>([]);
const setSafeInterval = useSetSafeInterval(); const setSafeInterval = useSetSafeInterval();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const { t } = useTranslation('modules/downloads-module');
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
if (downloadServices.length === 0) return; if (downloadServices.length === 0) return;
@@ -106,12 +109,12 @@ export default function DownloadComponent() {
const DEVICE_WIDTH = 576; const DEVICE_WIDTH = 576;
const ths = ( const ths = (
<tr> <tr>
<th>{t('modules.downloads.card.table.header.name')}</th> <th>{t('card.table.header.name')}</th>
<th>{t('modules.downloads.card.table.header.size')}</th> <th>{t('card.table.header.size')}</th>
{width > 576 ? <th>{t('modules.downloads.card.table.header.download')}</th> : ''} {width > 576 ? <th>{t('card.table.header.download')}</th> : ''}
{width > 576 ? <th>{t('modules.downloads.card.table.header.upload')}</th> : ''} {width > 576 ? <th>{t('card.table.header.upload')}</th> : ''}
<th>{t('modules.downloads.card.table.header.estimatedTimeOfArrival')}</th> <th>{t('card.table.header.estimatedTimeOfArrival')}</th>
<th>{t('modules.downloads.card.table.header.progress')}</th> <th>{t('card.table.header.progress')}</th>
</tr> </tr>
); );
// Convert Seconds to readable format. // Convert Seconds to readable format.
@@ -196,7 +199,7 @@ export default function DownloadComponent() {
</Table> </Table>
) : ( ) : (
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Title order={3}>{t('modules.downloads.card.table.body.nothingFound')}</Title> <Title order={3}>{t('card.table.body.nothingFound')}</Title>
</Center> </Center>
)} )}
</ScrollArea> </ScrollArea>

View File

@@ -3,7 +3,7 @@ import { showNotification, updateNotification } from '@mantine/notifications';
import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons'; import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons';
import axios from 'axios'; import axios from 'axios';
import Consola from 'consola'; import Consola from 'consola';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { useColorTheme } from '../../tools/color'; import { useColorTheme } from '../../tools/color';
@@ -28,6 +28,7 @@ const useStyles = createStyles((theme) => ({
export function RequestModal({ base, opened, setOpened }: RequestModalProps) { export function RequestModal({ base, opened, setOpened }: RequestModalProps) {
const [result, setResult] = useState<MovieResult | TvShowResult>(); const [result, setResult] = useState<MovieResult | TvShowResult>();
const { secondaryColor } = useColorTheme(); const { secondaryColor } = useColorTheme();
function getResults(base: Result) { function getResults(base: Result) {
axios.get(`/api/modules/overseerr/${base.id}?type=${base.mediaType}`).then((res) => { axios.get(`/api/modules/overseerr/${base.id}?type=${base.mediaType}`).then((res) => {
setResult(res.data); setResult(res.data);
@@ -56,6 +57,8 @@ export function MovieRequestModal({
setOpened: (opened: boolean) => void; setOpened: (opened: boolean) => void;
}) { }) {
const { secondaryColor } = useColorTheme(); const { secondaryColor } = useColorTheme();
const { t } = useTranslation('modules/overseerr-module');
return ( return (
<Modal <Modal
onClose={() => setOpened(false)} onClose={() => setOpened(false)}
@@ -68,23 +71,23 @@ export function MovieRequestModal({
title={ title={
<Group> <Group>
<IconDownload /> <IconDownload />
{t('modules.overseerr.popup.item.buttons.askFor', { title: result.title })} {t('popup.item.buttons.askFor', { title: result.title })}
</Group> </Group>
} }
> >
<Stack> <Stack>
<Alert <Alert
icon={<IconAlertCircle size={16} />} icon={<IconAlertCircle size={16} />}
title={t('modules.overseerr.popup.item.alerts.automaticApproval.title')} title={t('popup.item.alerts.automaticApproval.title')}
color={secondaryColor} color={secondaryColor}
radius="md" radius="md"
variant="filled" variant="filled"
> >
{t('modules.overseerr.popup.item.alerts.automaticApproval.text')} {t('popup.item.alerts.automaticApproval.text')}
</Alert> </Alert>
<Group> <Group>
<Button variant="outline" color="gray" onClick={() => setOpened(false)}> <Button variant="outline" color="gray" onClick={() => setOpened(false)}>
{t('modules.overseerr.popup.item.buttons.cancel')} {t('popup.item.buttons.cancel')}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -92,7 +95,7 @@ export function MovieRequestModal({
askForMedia(MediaType.Movie, result.id, result.title, []); askForMedia(MediaType.Movie, result.id, result.title, []);
}} }}
> >
{t('modules.overseerr.popup.item.buttons.request')} {t('popup.item.buttons.request')}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -111,6 +114,7 @@ export function TvRequestModal({
}) { }) {
const [selection, setSelection] = useState<TvShowResultSeason[]>(result.seasons); const [selection, setSelection] = useState<TvShowResultSeason[]>(result.seasons);
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const { t } = useTranslation('modules/overseerr-module');
const toggleRow = (container: TvShowResultSeason) => const toggleRow = (container: TvShowResultSeason) =>
setSelection((current: TvShowResultSeason[]) => setSelection((current: TvShowResultSeason[]) =>
@@ -149,7 +153,7 @@ export function TvRequestModal({
title={ title={
<Group> <Group>
<IconDownload /> <IconDownload />
{t('modules.overseerr.popup.item.buttons.askFor', { {t('popup.item.buttons.askFor', {
title: result.name ?? result.originalName ?? 'a TV show', title: result.name ?? result.originalName ?? 'a TV show',
})} })}
</Group> </Group>
@@ -158,15 +162,15 @@ export function TvRequestModal({
<Stack> <Stack>
<Alert <Alert
icon={<IconAlertCircle size={16} />} icon={<IconAlertCircle size={16} />}
title={t('modules.overseerr.popup.item.alerts.automaticApproval.title')} title={t('popup.item.alerts.automaticApproval.title')}
color={secondaryColor} color={secondaryColor}
radius="md" radius="md"
variant="filled" variant="filled"
> >
{t('modules.overseerr.popup.item.alerts.automaticApproval.text')} {t('popup.item.alerts.automaticApproval.text')}
</Alert> </Alert>
<Table captionSide="bottom" highlightOnHover> <Table captionSide="bottom" highlightOnHover>
<caption>{t('modules.overseerr.popup.seasonSelector.caption')}</caption> <caption>{t('popup.seasonSelector.caption')}</caption>
<thead> <thead>
<tr> <tr>
<th> <th>
@@ -177,15 +181,15 @@ export function TvRequestModal({
transitionDuration={0} transitionDuration={0}
/> />
</th> </th>
<th>{t('modules.overseerr.popup.seasonSelector.table.header.season')}</th> <th>{t('popup.seasonSelector.table.header.season')}</th>
<th>{t('modules.overseerr.popup.seasonSelector.table.header.numberOfEpisodes')}</th> <th>{t('popup.seasonSelector.table.header.numberOfEpisodes')}</th>
</tr> </tr>
</thead> </thead>
<tbody>{rows}</tbody> <tbody>{rows}</tbody>
</Table> </Table>
<Group> <Group>
<Button variant="outline" color="gray" onClick={() => setOpened(false)}> <Button variant="outline" color="gray" onClick={() => setOpened(false)}>
{t('modules.overseerr.popup.item.buttons.cancel')} {t('popup.item.buttons.cancel')}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -199,7 +203,7 @@ export function TvRequestModal({
); );
}} }}
> >
{t('modules.overseerr.popup.item.buttons.request')} {t('popup.item.buttons.request')}
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import { IconPlug as Plug } from '@tabler/icons'; import { IconPlug as Plug } from '@tabler/icons';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { IModule } from '../ModuleTypes'; import { IModule } from '../ModuleTypes';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
export const PingModule: IModule = { export const PingModule: IModule = {
title: 'Ping Services', title: 'Ping Services',
@@ -23,6 +23,8 @@ export default function PingComponent(props: any) {
const [response, setResponse] = useState(500); const [response, setResponse] = useState(500);
const exists = config.modules?.[PingModule.title]?.enabled ?? false; const exists = config.modules?.[PingModule.title]?.enabled ?? false;
const { t } = useTranslation('modules/ping-module');
function statusCheck(response: AxiosResponse) { function statusCheck(response: AxiosResponse) {
const { status }: { status: string[] } = props; const { status }: { status: string[] } = props;
//Default Status //Default Status
@@ -69,10 +71,10 @@ export default function PingComponent(props: any) {
radius="lg" radius="lg"
label={ label={
isOnline === 'loading' isOnline === 'loading'
? t('modules.ping.states.loading') ? t('states.loading')
: isOnline === 'online' : isOnline === 'online'
? t('modules.ping.states.online', { response }) ? t('states.online', { response })
: t('modules.ping.states.offline', { response }) : t('states.offline', { response })
} }
> >
<Indicator <Indicator

View File

@@ -8,8 +8,7 @@ import {
IconDownload as Download, IconDownload as Download,
IconMovie, IconMovie,
} from '@tabler/icons'; } from '@tabler/icons';
import { t } from 'i18next'; import { useTranslation } from 'next-i18next';
import axios from 'axios'; import axios from 'axios';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
@@ -61,6 +60,7 @@ export default function SearchBar(props: any) {
}, },
}); });
const [debounced, cancel] = useDebouncedValue(form.values.query, 250); const [debounced, cancel] = useDebouncedValue(form.values.query, 250);
const { t } = useTranslation('modules/search-module');
useEffect(() => { useEffect(() => {
if (OverseerrService === undefined && isOverseerrEnabled) { if (OverseerrService === undefined && isOverseerrEnabled) {
@@ -177,7 +177,7 @@ export default function SearchBar(props: any) {
radius="md" radius="md"
size="md" size="md"
styles={{ rightSection: { pointerEvents: 'none' } }} styles={{ rightSection: { pointerEvents: 'none' } }}
placeholder={t('layout.header.search.input.placeholder')} placeholder={t('input.placeholder')}
{...props} {...props}
{...form.getInputProps('query')} {...form.getInputProps('query')}
/> />

View File

@@ -13,10 +13,10 @@ import {
IconSnowflake as Snowflake, IconSnowflake as Snowflake,
IconSun as Sun, IconSun as Sun,
} from '@tabler/icons'; } from '@tabler/icons';
import { useTranslation } from 'next-i18next';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { IModule } from '../ModuleTypes'; import { IModule } from '../ModuleTypes';
import { WeatherResponse } from './WeatherInterface'; import { WeatherResponse } from './WeatherInterface';
import { t } from 'i18next';
export const WeatherModule: IModule = { export const WeatherModule: IModule = {
title: 'Weather', title: 'Weather',
@@ -49,85 +49,87 @@ export const WeatherModule: IModule = {
// 95 *Thunderstorm: Slight or moderate // 95 *Thunderstorm: Slight or moderate
// 96, 99 *Thunderstorm with slight and heavy hail // 96, 99 *Thunderstorm with slight and heavy hail
export function WeatherIcon(props: any) { export function WeatherIcon(props: any) {
const { t } = useTranslation('modules/weather-module');
const { code } = props; const { code } = props;
let data: { icon: any; name: string }; let data: { icon: any; name: string };
switch (code) { switch (code) {
case 0: { case 0: {
data = { icon: Sun, name: t('modules.weather.card.weatherDescriptions.clear') }; data = { icon: Sun, name: t('card.weatherDescriptions.clear') };
break; break;
} }
case 1: case 1:
case 2: case 2:
case 3: { case 3: {
data = { icon: Cloud, name: t('modules.weather.card.weatherDescriptions.mainlyClear') }; data = { icon: Cloud, name: t('card.weatherDescriptions.mainlyClear') };
break; break;
} }
case 45: case 45:
case 48: { case 48: {
data = { icon: CloudFog, name: t('modules.weather.card.weatherDescriptions.fog') }; data = { icon: CloudFog, name: t('card.weatherDescriptions.fog') };
break; break;
} }
case 51: case 51:
case 53: case 53:
case 55: { case 55: {
data = { icon: Cloud, name: t('modules.weather.card.weatherDescriptions.drizzle') }; data = { icon: Cloud, name: t('card.weatherDescriptions.drizzle') };
break; break;
} }
case 56: case 56:
case 57: { case 57: {
data = { data = {
icon: Snowflake, icon: Snowflake,
name: t('modules.weather.card.weatherDescriptions.freezingDrizzle'), name: t('card.weatherDescriptions.freezingDrizzle'),
}; };
break; break;
} }
case 61: case 61:
case 63: case 63:
case 65: { case 65: {
data = { icon: CloudRain, name: t('modules.weather.card.weatherDescriptions.rain') }; data = { icon: CloudRain, name: t('card.weatherDescriptions.rain') };
break; break;
} }
case 66: case 66:
case 67: { case 67: {
data = { icon: CloudRain, name: t('modules.weather.card.weatherDescriptions.freezingRain') }; data = { icon: CloudRain, name: t('card.weatherDescriptions.freezingRain') };
break; break;
} }
case 71: case 71:
case 73: case 73:
case 75: { case 75: {
data = { icon: CloudSnow, name: t('modules.weather.card.weatherDescriptions.snowFall') }; data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowFall') };
break; break;
} }
case 77: { case 77: {
data = { icon: CloudSnow, name: t('modules.weather.card.weatherDescriptions.snowGrains') }; data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowGrains') };
break; break;
} }
case 80: case 80:
case 81: case 81:
case 82: { case 82: {
data = { icon: CloudRain, name: t('modules.weather.card.weatherDescriptions.rainShowers') }; data = { icon: CloudRain, name: t('card.weatherDescriptions.rainShowers') };
break; break;
} }
case 85: case 85:
case 86: { case 86: {
data = { icon: CloudSnow, name: t('modules.weather.card.weatherDescriptions.snowShowers') }; data = { icon: CloudSnow, name: t('card.weatherDescriptions.snowShowers') };
break; break;
} }
case 95: { case 95: {
data = { icon: CloudStorm, name: t('modules.weather.card.weatherDescriptions.thunderstorm') }; data = { icon: CloudStorm, name: t('card.weatherDescriptions.thunderstorm') };
break; break;
} }
case 96: case 96:
case 99: { case 99: {
data = { data = {
icon: CloudStorm, icon: CloudStorm,
name: t('modules.weather.card.weatherDescriptions.thunderstormWithHail'), name: t('card.weatherDescriptions.thunderstormWithHail'),
}; };
break; break;
} }
default: { default: {
data = { icon: QuestionMark, name: t('modules.weather.card.weatherDescriptions.unknown') }; data = { icon: QuestionMark, name: t('card.weatherDescriptions.unknown') };
} }
} }
return ( return (

View File

@@ -4,17 +4,16 @@ import { AppProps } from 'next/app';
import { getCookie, setCookie } from 'cookies-next'; import { getCookie, setCookie } from 'cookies-next';
import Head from 'next/head'; import Head from 'next/head';
import { MantineProvider, ColorScheme, ColorSchemeProvider, MantineTheme } from '@mantine/core'; import { MantineProvider, ColorScheme, ColorSchemeProvider, MantineTheme } from '@mantine/core';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { NotificationsProvider } from '@mantine/notifications'; import { NotificationsProvider } from '@mantine/notifications';
import { useHotkeys } from '@mantine/hooks'; import { useHotkeys } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals'; import { ModalsProvider } from '@mantine/modals';
import { appWithTranslation } from 'next-i18next';
import { ConfigProvider } from '../tools/state'; import { ConfigProvider } from '../tools/state';
import { theme } from '../tools/theme'; import { theme } from '../tools/theme';
import { ColorTheme } from '../tools/color'; import { ColorTheme } from '../tools/color';
import { I18nextProvider } from 'react-i18next';
import { loadI18n } from '../translations/i18n';
import { TranslationProvider } from '../providers/translation.provider';
export default function App(this: any, props: AppProps & { colorScheme: ColorScheme }) { function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
const { Component, pageProps } = props; const { Component, pageProps } = props;
const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme); const [colorScheme, setColorScheme] = useState<ColorScheme>(props.colorScheme);
@@ -72,9 +71,7 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch
<NotificationsProvider limit={4} position="bottom-left"> <NotificationsProvider limit={4} position="bottom-left">
<ModalsProvider> <ModalsProvider>
<ConfigProvider> <ConfigProvider>
<TranslationProvider>
<Component {...pageProps} /> <Component {...pageProps} />
</TranslationProvider>
</ConfigProvider> </ConfigProvider>
</ModalsProvider> </ModalsProvider>
</NotificationsProvider> </NotificationsProvider>
@@ -88,3 +85,5 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch
App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => ({
colorScheme: getCookie('color-scheme', ctx) || 'light', colorScheme: getCookie('color-scheme', ctx) || 'light',
}); });
export default appWithTranslation(App);

View File

@@ -1,6 +1,8 @@
import { getCookie, setCookie } from 'cookies-next'; import { getCookie, setCookie } from 'cookies-next';
import { GetServerSidePropsContext } from 'next'; import { GetServerSidePropsContext } from 'next';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import AppShelf from '../components/AppShelf/AppShelf'; import AppShelf from '../components/AppShelf/AppShelf';
import LoadConfigComponent from '../components/Config/LoadConfig'; import LoadConfigComponent from '../components/Config/LoadConfig';
import { Config } from '../tools/types'; import { Config } from '../tools/types';
@@ -13,6 +15,7 @@ import Layout from '../components/layout/Layout';
export async function getServerSideProps({ export async function getServerSideProps({
req, req,
res, res,
locale,
}: GetServerSidePropsContext): Promise<{ props: { config: Config } }> { }: GetServerSidePropsContext): Promise<{ props: { config: Config } }> {
let cookie = getCookie('config-name', { req, res }); let cookie = getCookie('config-name', { req, res });
if (!cookie) { if (!cookie) {
@@ -24,7 +27,31 @@ export async function getServerSideProps({
}); });
cookie = 'default'; cookie = 'default';
} }
return getConfig(cookie as string);
const translations = await serverSideTranslations(locale as string, [
'common',
'layout/app-shelf',
'layout/add-service-app-shelf',
'settings/common',
'settings/general/theme-selector',
'settings/general/config-changer',
'settings/general/internationalization',
'settings/general/module-enabler',
'settings/general/search-engine',
'settings/general/widget-positions',
'settings/customization/color-selector',
'settings/customization/page-appearance',
'settings/customization/shade-selector',
'modules/search-module',
'modules/downloads-module',
'modules/weather-module',
'modules/ping-module',
'modules/docker-module',
'modules/dashdot-module',
'modules/overseerr-module',
'modules/common-media-cards-module',
]);
return getConfig(cookie as string, translations);
} }
export default function HomePage(props: any) { export default function HomePage(props: any) {

View File

@@ -1,16 +0,0 @@
import { ReactNode, Suspense } from 'react';
import { I18nextProvider } from 'react-i18next';
import { loadI18n } from '../translations/i18n';
interface TranslationProviderProps {
children: ReactNode;
}
export const TranslationProvider = ({ children }: TranslationProviderProps) => {
return (
<Suspense>
<I18nextProvider i18n={loadI18n()}>{children}</I18nextProvider>
</Suspense>
);
};

View File

@@ -1,7 +1,7 @@
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
export function getConfig(name: string) { export function getConfig(name: string, props: any = undefined) {
// Check if the config file exists // Check if the config file exists
const configPath = path.join(process.cwd(), 'data/configs', `${name}.json`); const configPath = path.join(process.cwd(), 'data/configs', `${name}.json`);
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
@@ -30,6 +30,7 @@ export function getConfig(name: string) {
props: { props: {
configName: name, configName: name,
config: JSON.parse(config), config: JSON.parse(config),
...props,
}, },
}; };
} }

View File

@@ -1,36 +0,0 @@
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
export const loadI18n = () => {
i18n
.use(LanguageDetector)
.use(Backend)
.init({
fallbackLng: ['de-de', 'en-us'],
supportedLngs: ['en-us', 'de-de'],
lowerCaseLng: true,
keySeparator: '.',
backend: {
loadPath: constructLoadPath,
},
debug: true,
});
return i18n;
};
export const convertCodeToName = (code: string) => {
switch (code) {
case 'en-us':
return 'English (US)';
case 'de-de':
return 'German';
default:
return `Unknown (${code})`;
}
};
const constructLoadPath = () => {
return '/locales/{{lng}}.json';
};

View File

@@ -364,7 +364,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": "@babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7":
version: 7.18.9 version: 7.18.9
resolution: "@babel/runtime@npm:7.18.9" resolution: "@babel/runtime@npm:7.18.9"
dependencies: dependencies:
@@ -2005,6 +2005,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/hoist-non-react-statics@npm:^3.3.1":
version: 3.3.1
resolution: "@types/hoist-non-react-statics@npm:3.3.1"
dependencies:
"@types/react": "*"
hoist-non-react-statics: ^3.3.0
checksum: 2c0778570d9a01d05afabc781b32163f28409bb98f7245c38d5eaf082416fdb73034003f5825eb5e21313044e8d2d9e1f3fe2831e345d3d1b1d20bcd12270719
languageName: node
linkType: hard
"@types/http-cache-semantics@npm:*": "@types/http-cache-semantics@npm:*":
version: 4.0.1 version: 4.0.1
resolution: "@types/http-cache-semantics@npm:4.0.1" resolution: "@types/http-cache-semantics@npm:4.0.1"
@@ -2109,6 +2119,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/react@npm:*":
version: 18.0.17
resolution: "@types/react@npm:18.0.17"
dependencies:
"@types/prop-types": "*"
"@types/scheduler": "*"
csstype: ^3.0.2
checksum: 18cae64f5bfd6bb58fbd8ee2ba52ec82de844f114254e26de7b513e4b86621f643f9b71d7066958cd571b0d78cb86cbceda449c5289f9349ca573df29ab69252
languageName: node
linkType: hard
"@types/react@npm:17.0.1": "@types/react@npm:17.0.1":
version: 17.0.1 version: 17.0.1
resolution: "@types/react@npm:17.0.1" resolution: "@types/react@npm:17.0.1"
@@ -2128,6 +2149,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/scheduler@npm:*":
version: 0.16.2
resolution: "@types/scheduler@npm:0.16.2"
checksum: b6b4dcfeae6deba2e06a70941860fb1435730576d3689225a421280b7742318d1548b3d22c1f66ab68e414f346a9542f29240bc955b6332c5b11e561077583bc
languageName: node
linkType: hard
"@types/ssh2@npm:*": "@types/ssh2@npm:*":
version: 1.11.5 version: 1.11.5
resolution: "@types/ssh2@npm:1.11.5" resolution: "@types/ssh2@npm:1.11.5"
@@ -3108,6 +3136,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"core-js@npm:^3":
version: 3.24.1
resolution: "core-js@npm:3.24.1"
checksum: 6fb5bf0fd9e9f3e69d95616dd03332fea6758a715d2628c108b5faf17b48b0f580e90c4febb0a523c4665b0991a810de16289f86187fe79d70cc722dbd3edf0e
languageName: node
linkType: hard
"core-util-is@npm:~1.0.0": "core-util-is@npm:~1.0.0":
version: 1.0.3 version: 1.0.3
resolution: "core-util-is@npm:1.0.3" resolution: "core-util-is@npm:1.0.3"
@@ -4679,7 +4714,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"hoist-non-react-statics@npm:^3.3.1": "hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2 version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2" resolution: "hoist-non-react-statics@npm:3.3.2"
dependencies: dependencies:
@@ -4747,11 +4782,11 @@ __metadata:
jest: ^28.1.3 jest: ^28.1.3
js-file-download: ^0.4.12 js-file-download: ^0.4.12
next: 12.1.6 next: 12.1.6
next-i18next: ^11.3.0
prettier: ^2.7.1 prettier: ^2.7.1
prism-react-renderer: ^1.3.5 prism-react-renderer: ^1.3.5
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
react-i18next: ^11.18.4
sharp: ^0.30.7 sharp: ^0.30.7
systeminformation: ^5.12.1 systeminformation: ^5.12.1
typescript: ^4.7.4 typescript: ^4.7.4
@@ -4890,6 +4925,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"i18next-fs-backend@npm:^1.1.4":
version: 1.1.5
resolution: "i18next-fs-backend@npm:1.1.5"
checksum: 71f6c4b0ff071676d69f1668675a68f2d72e1836dafcc8014123523bb584a78b0e4fccd16f83d7f37755b58d1dfcb4d6ad36c60b261833b509ccf20313419d9e
languageName: node
linkType: hard
"i18next-http-backend@npm:^1.4.1": "i18next-http-backend@npm:^1.4.1":
version: 1.4.1 version: 1.4.1
resolution: "i18next-http-backend@npm:1.4.1" resolution: "i18next-http-backend@npm:1.4.1"
@@ -4899,7 +4941,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"i18next@npm:^21.9.1": "i18next@npm:^21.8.13, i18next@npm:^21.9.1":
version: 21.9.1 version: 21.9.1
resolution: "i18next@npm:21.9.1" resolution: "i18next@npm:21.9.1"
dependencies: dependencies:
@@ -6246,6 +6288,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"next-i18next@npm:^11.3.0":
version: 11.3.0
resolution: "next-i18next@npm:11.3.0"
dependencies:
"@babel/runtime": ^7.18.6
"@types/hoist-non-react-statics": ^3.3.1
core-js: ^3
hoist-non-react-statics: ^3.3.2
i18next: ^21.8.13
i18next-fs-backend: ^1.1.4
react-i18next: ^11.18.0
peerDependencies:
next: ">= 10.0.0"
react: ">= 16.8.0"
checksum: fbce97a4fbf9ad846c08652471a833c7f173c3e7ddc7cafa1423625b4a684715bb85f76ae06fe9cbed3e70f12b8e78e2459e5bc1a3c3f5c517743f17648f8939
languageName: node
linkType: hard
"next@npm:12.1.6": "next@npm:12.1.6":
version: 12.1.6 version: 12.1.6
resolution: "next@npm:12.1.6" resolution: "next@npm:12.1.6"
@@ -6925,7 +6985,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-i18next@npm:^11.18.4": "react-i18next@npm:^11.18.0":
version: 11.18.4 version: 11.18.4
resolution: "react-i18next@npm:11.18.4" resolution: "react-i18next@npm:11.18.4"
dependencies: dependencies: