mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
🚧 wip extract to translations file
This commit is contained in:
385
public/locales/de-de.json
Normal file
385
public/locales/de-de.json
Normal file
@@ -0,0 +1,385 @@
|
||||
{
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"tooltip": "Einstellungen",
|
||||
"tabs": {
|
||||
"common": {
|
||||
"title": "Allgemein",
|
||||
"settings": {
|
||||
"searchEngine": {
|
||||
"title": "Suchmaschine",
|
||||
"tips": {
|
||||
"generalTip": "Benutze die Prefixe !yt und !t vor deiner Suchanfrage um auf Youtube oder nach einem Torrent zu suchen.",
|
||||
"placeholderTip": "%s can be used as a placeholder for the query."
|
||||
},
|
||||
"customEngine": {
|
||||
"label": "Suchadresse",
|
||||
"placeholder": "Benutzerdefinierte Adresse"
|
||||
}
|
||||
},
|
||||
"colorScheme": {
|
||||
"label": "Wechseln zu {{scheme}} Modus"
|
||||
},
|
||||
"widgetsPositionSwitch": {
|
||||
"label": "Positionieren von Widgets auf linker Seite"
|
||||
},
|
||||
"moduleEnabler": {
|
||||
"title": "Modul Enabler"
|
||||
},
|
||||
"language": {
|
||||
"title": "Sprache"
|
||||
},
|
||||
"configChanger": {
|
||||
"configSelect": {
|
||||
"label": "Konfigurations Lader"
|
||||
},
|
||||
"modal": {
|
||||
"title": "Choose the name of your new config",
|
||||
"form": {
|
||||
"configName": {
|
||||
"label": "Config name",
|
||||
"placeholder": "Your new config name"
|
||||
},
|
||||
"buttons:": {
|
||||
"submit": "Confirm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"download": "Konfiguration herunterladen",
|
||||
"delete": {
|
||||
"text": "Konfiguration löschen",
|
||||
"notifications": {
|
||||
"deleted": {
|
||||
"title": "Config deleted",
|
||||
"message": "Config deleted"
|
||||
},
|
||||
"deleteFailed": {
|
||||
"title": "Config delete failed",
|
||||
"message": "Config delete failed"
|
||||
}
|
||||
}
|
||||
},
|
||||
"saveCopy": "Kopie speichern"
|
||||
}
|
||||
},
|
||||
"configTip": "Lade deine Konfiguration hoch, indem du sie per drag-and-drop auf die Seite ziehst."
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"title": "Anpassungen",
|
||||
"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": "Gemacht mit ❤️ von @"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
385
public/locales/en-us.json
Normal file
385
public/locales/en-us.json
Normal file
@@ -0,0 +1,385 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import { IconApps } from '@tabler/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { t } from 'i18next';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { tryMatchPort, ServiceTypeList, StatusCodes } from '../../tools/types';
|
||||
import Tip from '../layout/Tip';
|
||||
@@ -33,13 +34,13 @@ export function AddItemShelfButton(props: any) {
|
||||
<Modal
|
||||
size="xl"
|
||||
radius="md"
|
||||
title={<Title order={3}>Add service</Title>}
|
||||
title={<Title order={3}>{t('layout.header.addService.modal.title')}</Title>}
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
>
|
||||
<AddAppShelfItemForm setOpened={setOpened} />
|
||||
</Modal>
|
||||
<Tooltip withinPortal label="Add a service">
|
||||
<Tooltip withinPortal label={t('layout.header.addService.actionIcon.tooltip')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
@@ -120,13 +121,13 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
try {
|
||||
const _isValid = new URL(value);
|
||||
} catch (e) {
|
||||
return 'Please enter a valid URL';
|
||||
return t('layout.header.addService.modal.form.validation.invalidUrl');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
status: (value: string[]) => {
|
||||
if (!value.length) {
|
||||
return 'Please select a status code';
|
||||
return t('layout.header.addService.modal.form.validation.noStatusCodeSelected');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -203,48 +204,62 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
>
|
||||
<Tabs defaultValue="Options">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="Options">Options</Tabs.Tab>
|
||||
<Tabs.Tab value="Advanced Options">Advanced options</Tabs.Tab>
|
||||
<Tabs.Tab value="Options">
|
||||
{t('layout.header.addService.modal.tabs.options.title')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="Advanced Options">
|
||||
{t('layout.header.addService.modal.tabs.advancedOptions.title')}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel value="Options">
|
||||
<Stack>
|
||||
<TextInput
|
||||
required
|
||||
label="Service name"
|
||||
placeholder="Plex"
|
||||
label={t('layout.header.addService.modal.tabs.options.form.serviceName.label')}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.serviceName.placeholder'
|
||||
)}
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Icon URL"
|
||||
label={t('layout.header.addService.modal.tabs.options.form.iconUrl.label')}
|
||||
placeholder={DEFAULT_ICON}
|
||||
{...form.getInputProps('icon')}
|
||||
/>
|
||||
<TextInput
|
||||
required
|
||||
label="Service URL"
|
||||
label={t('layout.header.addService.modal.tabs.options.form.serviceUrl.label')}
|
||||
placeholder="http://localhost:7575"
|
||||
{...form.getInputProps('url')}
|
||||
/>
|
||||
<TextInput
|
||||
label="On Click URL"
|
||||
label={t('layout.header.addService.modal.tabs.options.form.onClickUrl.label')}
|
||||
placeholder="http://sonarr.example.com"
|
||||
{...form.getInputProps('openedUrl')}
|
||||
/>
|
||||
<Select
|
||||
label="Service type"
|
||||
defaultValue="Other"
|
||||
placeholder="Pick one"
|
||||
label={t('layout.header.addService.modal.tabs.options.form.serviceType.label')}
|
||||
defaultValue={t(
|
||||
'layout.header.addService.modal.tabs.options.form.serviceType.defaultValue'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.serviceType.placeholder'
|
||||
)}
|
||||
required
|
||||
searchable
|
||||
data={ServiceTypeList}
|
||||
{...form.getInputProps('type')}
|
||||
/>
|
||||
<Select
|
||||
label="Category"
|
||||
label={t('layout.header.addService.modal.tabs.options.form.category.label')}
|
||||
data={categories}
|
||||
placeholder="Select a category or create a new one"
|
||||
nothingFound="Nothing found"
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.category.placeholder'
|
||||
)}
|
||||
nothingFound={t(
|
||||
'layout.header.addService.modal.tabs.options.form.category.nothingFound'
|
||||
)}
|
||||
searchable
|
||||
clearable
|
||||
creatable
|
||||
@@ -253,7 +268,11 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
setCategories([...InitialCategories, query]);
|
||||
return item;
|
||||
}}
|
||||
getCreateLabel={(query) => `+ Create "${query}"`}
|
||||
getCreateLabel={(query) =>
|
||||
t('layout.header.addService.modal.tabs.options.form.category.createLabel', {
|
||||
query,
|
||||
})
|
||||
}
|
||||
{...form.getInputProps('category')}
|
||||
/>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
@@ -266,23 +285,36 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="API key"
|
||||
placeholder="Your API key"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.label'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.placeholder'
|
||||
)}
|
||||
value={form.values.apiKey}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('apiKey', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.apiKey && 'Invalid API key'}
|
||||
error={
|
||||
form.errors.apiKey &&
|
||||
t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.validation.noKey'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Tip>
|
||||
Get your API key{' '}
|
||||
{t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.tip.text'
|
||||
)}{' '}
|
||||
<Anchor
|
||||
target="_blank"
|
||||
weight="bold"
|
||||
style={{ fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
href={`${hostname}/settings/general`}
|
||||
>
|
||||
here.
|
||||
{t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.apiKey.tip.link'
|
||||
)}
|
||||
</Anchor>
|
||||
</Tip>
|
||||
</>
|
||||
@@ -291,79 +323,134 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
<>
|
||||
<TextInput
|
||||
required
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.username.label'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.username.placeholder'
|
||||
)}
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
error={
|
||||
form.errors.username &&
|
||||
t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.username.validation.invalidUsername'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PasswordInput
|
||||
required
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.password.label'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.password.placeholder'
|
||||
)}
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
error={
|
||||
form.errors.password &&
|
||||
t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.qBittorrent.password.validation.invalidPassword'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Deluge' && (
|
||||
<>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="password"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.deluge.password.label'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.deluge.password.placeholder'
|
||||
)}
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
error={
|
||||
form.errors.password &&
|
||||
t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.deluge.password.validation.invalidPassword'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{form.values.type === 'Transmission' && (
|
||||
<>
|
||||
<TextInput
|
||||
label="Username"
|
||||
placeholder="admin"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.transmission.username.label'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.transmission.username.placeholder'
|
||||
)}
|
||||
value={form.values.username}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('username', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.username && 'Invalid username'}
|
||||
error={
|
||||
form.errors.username &&
|
||||
t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.transmission.username.validation.invalidUsername'
|
||||
)
|
||||
}
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="adminadmin"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.transmission.password.label'
|
||||
)}
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.transmission.password.placeholder'
|
||||
)}
|
||||
value={form.values.password}
|
||||
onChange={(event) => {
|
||||
form.setFieldValue('password', event.currentTarget.value);
|
||||
}}
|
||||
error={form.errors.password && 'Invalid password'}
|
||||
error={
|
||||
form.errors.password &&
|
||||
t(
|
||||
'layout.header.addService.modal.tabs.options.form.integrations.transmission.password.validation.invalidPassword'
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="Advanced Options">
|
||||
<Tabs.Panel value={t('layout.header.addService.modal.tabs.advancedOptions.title')}>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
required
|
||||
label="HTTP Status Codes"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.label'
|
||||
)}
|
||||
data={StatusCodes}
|
||||
placeholder="Select valid status codes"
|
||||
clearButtonLabel="Clear selection"
|
||||
nothingFound="Nothing found"
|
||||
placeholder={t(
|
||||
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.placeholder'
|
||||
)}
|
||||
clearButtonLabel={t(
|
||||
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.clearButtonLabel'
|
||||
)}
|
||||
nothingFound={t(
|
||||
'layout.header.addService.modal.tabs.advancedOptions.form.httpStatusCodes.nothingFound'
|
||||
)}
|
||||
defaultValue={['200']}
|
||||
clearable
|
||||
searchable
|
||||
{...form.getInputProps('status')}
|
||||
/>
|
||||
<Switch
|
||||
label="Open service in new tab"
|
||||
label={t(
|
||||
'layout.header.addService.modal.tabs.advancedOptions.form.openServiceInNewTab.label'
|
||||
)}
|
||||
defaultChecked={form.values.newTab}
|
||||
{...form.getInputProps('newTab')}
|
||||
/>
|
||||
@@ -371,7 +458,10 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Group grow position="center" mt="xl">
|
||||
<Button type="submit">{props.message ?? 'Add service'}</Button>
|
||||
<Button type="submit">
|
||||
{props.message ??
|
||||
t('layout.header.addService.modal.tabs.advancedOptions.form.buttons.submit.content')}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { createStyles, Switch, Group, useMantineColorScheme, Kbd } from '@mantine/core';
|
||||
import { IconSun as Sun, IconMoonStars as MoonStars } from '@tabler/icons';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { t } from 'i18next';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
@@ -41,7 +42,9 @@ export function ColorSchemeSwitch() {
|
||||
<MoonStars className={cx(classes.icon, classes.iconDark)} size={18} />
|
||||
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
|
||||
</div>
|
||||
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
|
||||
{t('settings.tabs.common.settings.colorScheme.label', {
|
||||
scheme: colorScheme === 'dark' ? 'light' : 'dark',
|
||||
})}
|
||||
<Group spacing={2}>
|
||||
<Kbd>Ctrl</Kbd>+<Kbd>J</Kbd>
|
||||
</Group>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Center, Loader, Select, Tooltip } from '@mantine/core';
|
||||
import { setCookie } from 'cookies-next';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
@@ -23,7 +24,7 @@ export default function ConfigChanger() {
|
||||
// return <Select data={[{ value: '1', label: '1' },]} onChange={(e) => console.log(e)} value="1" />;
|
||||
return (
|
||||
<Select
|
||||
label="Config loader"
|
||||
label={t('settings.tabs.common.settings.configChanger.configSelect.label')}
|
||||
value={value}
|
||||
defaultValue={config.name}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
IconX as X,
|
||||
} from '@tabler/icons';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export default function SaveConfigComponent(props: any) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
@@ -32,7 +33,7 @@ export default function SaveConfigComponent(props: any) {
|
||||
radius="md"
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Choose the name of your new config"
|
||||
title={t('settings.tabs.common.settings.configChanger.modal.title')}
|
||||
>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
@@ -50,17 +51,21 @@ export default function SaveConfigComponent(props: any) {
|
||||
>
|
||||
<TextInput
|
||||
required
|
||||
label="Config name"
|
||||
placeholder="Your new config name"
|
||||
label={t('settings.tabs.common.settings.configChanger.modal.form.configName.label')}
|
||||
placeholder={t(
|
||||
'settings.tabs.common.settings.configChanger.modal.form.configName.placeholder'
|
||||
)}
|
||||
{...form.getInputProps('configName')}
|
||||
/>
|
||||
<Group position="right" mt="md">
|
||||
<Button type="submit">Confirm</Button>
|
||||
<Button type="submit">
|
||||
{t('settings.tabs.common.settings.configChanger.modal.form.buttons.submit')}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
<Button size="xs" leftIcon={<Download />} variant="outline" onClick={onClick}>
|
||||
Download config
|
||||
{t('settings.tabs.common.settings.configChanger.buttons.download')}
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -71,31 +76,39 @@ export default function SaveConfigComponent(props: any) {
|
||||
.delete(`/api/configs/${config.name}`)
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: 'Config deleted',
|
||||
title: t(
|
||||
'settings.tabs.common.settings.configChanger.buttons.delete.deleted.title'
|
||||
),
|
||||
icon: <Check />,
|
||||
color: 'green',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: 'Config deleted',
|
||||
message: t(
|
||||
'settings.tabs.common.settings.configChanger.buttons.delete.deleted.message'
|
||||
),
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showNotification({
|
||||
title: 'Config delete failed',
|
||||
title: t(
|
||||
'settings.tabs.common.settings.configChanger.buttons.delete.deleteFailed.title'
|
||||
),
|
||||
icon: <X />,
|
||||
color: 'red',
|
||||
autoClose: 1500,
|
||||
radius: 'md',
|
||||
message: 'Config delete failed',
|
||||
message: t(
|
||||
'settings.tabs.common.settings.configChanger.buttons.delete.deleteFailed.message'
|
||||
),
|
||||
});
|
||||
});
|
||||
setConfig({ ...config, name: 'default' });
|
||||
}}
|
||||
>
|
||||
Delete config
|
||||
{t('settings.tabs.common.settings.configChanger.buttons.delete.text')}
|
||||
</Button>
|
||||
<Button size="xs" leftIcon={<Plus />} variant="outline" onClick={() => setOpened(true)}>
|
||||
Save a copy
|
||||
{t('settings.tabs.common.settings.configChanger.buttons.saveCopy')}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ColorSelector } from './ColorSelector';
|
||||
import { OpacitySelector } from './OpacitySelector';
|
||||
import { AppCardWidthSelector } from './AppCardWidthSelector';
|
||||
import { ShadeSelector } from './ShadeSelector';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export default function TitleChanger() {
|
||||
const { config, setConfig } = useConfig();
|
||||
@@ -40,19 +41,27 @@ export default function TitleChanger() {
|
||||
<Stack mb="md" mr="sm" mt="xs">
|
||||
<form onSubmit={form.onSubmit((values) => saveChanges(values))}>
|
||||
<Stack>
|
||||
<TextInput label="Page title" placeholder="Homarr 🦞" {...form.getInputProps('title')} />
|
||||
<TextInput label="Logo" placeholder="/img/logo.png" {...form.getInputProps('logo')} />
|
||||
<TextInput
|
||||
label="Favicon"
|
||||
placeholder="/favicon.png"
|
||||
label={t('settings.tabs.customizations.settings.pageTitle.label')}
|
||||
placeholder={t('settings.tabs.customizations.settings.pageTitle.placeholder')}
|
||||
{...form.getInputProps('title')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('settings.tabs.customizations.settings.logo.label')}
|
||||
placeholder={t('settings.tabs.customizations.settings.logo.placeholder')}
|
||||
{...form.getInputProps('logo')}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('settings.tabs.customizations.settings.favicon.label')}
|
||||
placeholder={t('settings.tabs.customizations.settings.favicon.placeholder')}
|
||||
{...form.getInputProps('favicon')}
|
||||
/>
|
||||
<TextInput
|
||||
label="Background"
|
||||
placeholder="/img/background.png"
|
||||
label={t('settings.tabs.customizations.settings.background.label')}
|
||||
placeholder={t('settings.tabs.customizations.settings.background.placeholder')}
|
||||
{...form.getInputProps('background')}
|
||||
/>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit">{t('settings.tabs.customizations.settings.buttons.submit')}</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
<ColorSelector type="primary" />
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
|
||||
import { ColorSwatch, Grid, Group, Popover, Text, useMantineTheme } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { t } from 'i18next';
|
||||
|
||||
interface ColorControlProps {
|
||||
type: string;
|
||||
@@ -82,7 +83,11 @@ export function ColorSelector({ type }: ColorControlProps) {
|
||||
</Grid>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>{type[0].toUpperCase() + type.slice(1)} color</Text>
|
||||
<Text>
|
||||
{t('settings.tabs.customizations.settings.colorSelector.suffix', {
|
||||
color: type[0].toUpperCase() + type.slice(1),
|
||||
})}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import ConfigChanger from '../Config/ConfigChanger';
|
||||
import SaveConfigComponent from '../Config/SaveConfig';
|
||||
import ModuleEnabler from './ModuleEnabler';
|
||||
import Tip from '../layout/Tip';
|
||||
import { t } from 'i18next';
|
||||
import LanguageSwitch from './LanguageSwitch';
|
||||
|
||||
export default function CommonSettings(args: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
@@ -26,15 +28,12 @@ export default function CommonSettings(args: any) {
|
||||
return (
|
||||
<Stack mb="md" mr="sm">
|
||||
<Stack spacing={0} mt="xs">
|
||||
<Text>Search engine</Text>
|
||||
<Tip>
|
||||
Use the prefixes <b>!yt</b> and <b>!t</b> in front of your query to search on YouTube or
|
||||
for a Torrent respectively.
|
||||
</Tip>
|
||||
<Text>{t('settings.tabs.common.settings.searchEngine.title')}</Text>
|
||||
<Tip>{t('settings.tabs.common.settings.searchEngine.tips.generalTip')}</Tip>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
mb="sm"
|
||||
title="Search engine"
|
||||
title={t('settings.tabs.common.settings.searchEngine.title')}
|
||||
value={
|
||||
// Match config.settings.searchUrl with a key in the matches array
|
||||
searchUrl
|
||||
@@ -56,10 +55,10 @@ export default function CommonSettings(args: any) {
|
||||
/>
|
||||
{searchUrl === 'Custom' && (
|
||||
<>
|
||||
<Tip>%s can be used as a placeholder for the query.</Tip>
|
||||
<Tip>{t('settings.tabs.common.settings.searchEngine.tips.placeholderTip')}</Tip>
|
||||
<TextInput
|
||||
label="Query URL"
|
||||
placeholder="Custom query URL"
|
||||
label={t('settings.tabs.common.settings.searchEngine.customEngine.label')}
|
||||
placeholder={t('settings.tabs.common.settings.searchEngine.customEngine.placeholder')}
|
||||
value={customSearchUrl}
|
||||
onChange={(event) => {
|
||||
setCustomSearchUrl(event.currentTarget.value);
|
||||
@@ -78,9 +77,10 @@ export default function CommonSettings(args: any) {
|
||||
<ColorSchemeSwitch />
|
||||
<WidgetsPositionSwitch />
|
||||
<ModuleEnabler />
|
||||
<LanguageSwitch />
|
||||
<ConfigChanger />
|
||||
<SaveConfigComponent />
|
||||
<Tip>Upload your config file by dragging and dropping it onto the page!</Tip>
|
||||
<Tip>{t('settings.tabs.common.settings.configTip')}</Tip>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Group, ActionIcon, Anchor, Text } from '@mantine/core';
|
||||
import { IconBrandDiscord, IconBrandGithub } from '@tabler/icons';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { CURRENT_VERSION } from '../../../data/constants';
|
||||
|
||||
export default function Credits(props: any) {
|
||||
@@ -27,7 +29,7 @@ export default function Credits(props: any) {
|
||||
color: 'gray',
|
||||
}}
|
||||
>
|
||||
Made with ❤️ by @
|
||||
{t('settings.credits.madeWithLove')}
|
||||
<Anchor
|
||||
href="https://github.com/ajnart"
|
||||
style={{ color: 'inherit', fontStyle: 'inherit', fontSize: 'inherit' }}
|
||||
|
||||
78
src/components/Settings/LanguageSwitch.tsx
Normal file
78
src/components/Settings/LanguageSwitch.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Group, Image, Select, Stack, Text } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconLanguage } from '@tabler/icons';
|
||||
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { convertCodeToName } from '../../translations/i18n';
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { language, languages, changeLanguage } = i18n;
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string | null>(language);
|
||||
|
||||
const data = languages.map((language) => ({
|
||||
image: `https://countryflagsapi.com/png/${language.split('-').pop()}`,
|
||||
label: convertCodeToName(language),
|
||||
value: language,
|
||||
}));
|
||||
|
||||
const onChangeSelect = (value: string) => {
|
||||
setSelectedLanguage(value);
|
||||
|
||||
const languageName = convertCodeToName(value);
|
||||
|
||||
changeLanguage(value)
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: 'Language changed',
|
||||
message: `You changed the language to '${languageName}'`,
|
||||
color: 'green',
|
||||
autoClose: 5000,
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
showNotification({
|
||||
title: 'Failed to change language',
|
||||
message: `Failed to change to '${languageName}', Error:'${err}`,
|
||||
color: 'red',
|
||||
autoClose: 5000,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Select
|
||||
icon={<IconLanguage size={18} />}
|
||||
label={t('settings.tabs.common.settings.language.title')}
|
||||
data={data}
|
||||
itemComponent={SelectItem}
|
||||
nothingFound="Nothing found"
|
||||
onChange={onChangeSelect}
|
||||
value={selectedLanguage}
|
||||
defaultValue={language}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
image: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const SelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ image, label, ...others }: ItemProps, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group noWrap>
|
||||
<Image src={image} width={30} height={20} radius="xs" />
|
||||
|
||||
<div>
|
||||
<Text size="sm">{label}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Checkbox, SimpleGrid, Stack, Title } from '@mantine/core';
|
||||
import { t } from 'i18next';
|
||||
import * as Modules from '../../modules';
|
||||
import { useConfig } from '../../tools/state';
|
||||
|
||||
@@ -7,7 +8,7 @@ export default function ModuleEnabler(props: any) {
|
||||
const modules = Object.values(Modules).map((module) => module);
|
||||
return (
|
||||
<Stack>
|
||||
<Title order={4}>Module enabler</Title>
|
||||
<Title order={4}>{t('settings.tabs.common.settings.moduleEnabler.title')}</Title>
|
||||
<SimpleGrid cols={3} spacing="xs">
|
||||
{modules.map((module) => (
|
||||
<Checkbox
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Text, Slider, Stack } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export function OpacitySelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
@@ -30,7 +31,7 @@ export function OpacitySelector() {
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Text>App Opacity</Text>
|
||||
<Text>{t('settings.tabs.customizations.settings.opacitySelector.label')}</Text>
|
||||
<Slider
|
||||
defaultValue={config.settings.appOpacity || 100}
|
||||
step={10}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { ActionIcon, Title, Tooltip, Drawer, Tabs, ScrollArea } from '@mantine/c
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import { IconSettings } from '@tabler/icons';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import AdvancedSettings from './AdvancedSettings';
|
||||
import CommonSettings from './CommonSettings';
|
||||
import Credits from './Credits';
|
||||
@@ -10,8 +12,8 @@ function SettingsMenu(props: any) {
|
||||
return (
|
||||
<Tabs defaultValue="Common">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="Common">Common</Tabs.Tab>
|
||||
<Tabs.Tab value="Customizations">Customizations</Tabs.Tab>
|
||||
<Tabs.Tab value="Common">{t('settings.tabs.common.title')}</Tabs.Tab>
|
||||
<Tabs.Tab value="Customizations">{t('settings.tabs.customizations.title')}</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel data-autofocus value="Common">
|
||||
<ScrollArea style={{ height: '78vh' }} offsetScrollbars>
|
||||
@@ -37,14 +39,14 @@ export function SettingsMenuButton(props: any) {
|
||||
size="xl"
|
||||
padding="lg"
|
||||
position="right"
|
||||
title={<Title order={5}>Settings</Title>}
|
||||
title={<Title order={5}>{t('settings.title')}</Title>}
|
||||
opened={props.opened || opened}
|
||||
onClose={() => setOpened(false)}
|
||||
>
|
||||
<SettingsMenu />
|
||||
<Credits />
|
||||
</Drawer>
|
||||
<Tooltip label="Settings">
|
||||
<Tooltip label={t('settings.tooltip')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export function ShadeSelector() {
|
||||
const { config, setConfig } = useConfig();
|
||||
@@ -94,7 +95,7 @@ export function ShadeSelector() {
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
<Text>Shade</Text>
|
||||
<Text>{t('settings.tabs.customizations.settings.shadeSelector.label')}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { createStyles, Switch, Group } from '@mantine/core';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { t } from 'i18next';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
@@ -54,7 +55,7 @@ export function WidgetsPositionSwitch() {
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
Position widgets on left
|
||||
{t('settings.tabs.common.settings.widgetsPositionSwitch.label')}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,16 +23,16 @@ import {
|
||||
} from '../common';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const CalendarModule: IModule = {
|
||||
title: 'Calendar',
|
||||
description:
|
||||
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
|
||||
title: t('modules.calendar.title'),
|
||||
description: t('modules.calendar.description'),
|
||||
icon: CalendarIcon,
|
||||
component: CalendarComponent,
|
||||
options: {
|
||||
sundaystart: {
|
||||
name: 'Start the week on Sunday',
|
||||
name: t('modules.calendar.options.sundayStart'),
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Badge, Button, Group, Image, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons';
|
||||
import { t } from 'i18next';
|
||||
import { useState } from 'react';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { useConfig } from '../../tools/state';
|
||||
@@ -209,7 +210,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
size="sm"
|
||||
rightIcon={<IconPlayerPlay size={15} />}
|
||||
>
|
||||
Play
|
||||
{t('modules.common.mediaCard.buttons.play')}
|
||||
</Button>
|
||||
)}
|
||||
{media.imdbId && (
|
||||
@@ -249,7 +250,7 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
size="sm"
|
||||
rightIcon={<IconDownload size={15} />}
|
||||
>
|
||||
Request
|
||||
{t('modules.common.mediaCard.buttons.request')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createStyles, Stack, Title, useMantineColorScheme, useMantineTheme } from '@mantine/core';
|
||||
import { IconCalendar as CalendarIcon } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import { t } from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
@@ -143,30 +144,30 @@ export function DashdotComponent() {
|
||||
|
||||
const graphs = [
|
||||
{
|
||||
name: 'CPU',
|
||||
name: t('modules.dashDot.card.graphs.cpu.title'),
|
||||
enabled: cpuEnabled,
|
||||
params: {
|
||||
multiView: dashConfig?.cpuMultiView?.value ?? false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Storage',
|
||||
name: t('modules.dashDot.card.graphs.cpu.title'),
|
||||
enabled: storageEnabled && !isCompact,
|
||||
params: {
|
||||
multiView: dashConfig?.storageMultiView?.value ?? false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'RAM',
|
||||
name: t('modules.dashDot.card.graphs.memory.title'),
|
||||
enabled: ramEnabled,
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
name: t('modules.dashDot.card.graphs.network.title'),
|
||||
enabled: networkEnabled,
|
||||
spanTwo: true,
|
||||
},
|
||||
{
|
||||
name: 'GPU',
|
||||
name: t('modules.dashDot.card.graphs.gpu.title'),
|
||||
enabled: gpuEnabled,
|
||||
spanTwo: true,
|
||||
},
|
||||
@@ -175,27 +176,26 @@ export function DashdotComponent() {
|
||||
if (dashdotUrl === '') {
|
||||
return (
|
||||
<div>
|
||||
<h2 className={classes.heading}>Dash.</h2>
|
||||
<p>
|
||||
No dash. service found. Please add one to your Homarr dashboard or set a dashdot URL in
|
||||
the module options
|
||||
</p>
|
||||
<h2 className={classes.heading}>{t('modules.dashDot.card.title')}</h2>
|
||||
<p>{t('modules.dashDot.card.errors.noService')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className={classes.heading}>Dash.</h2>
|
||||
<h2 className={classes.heading}>{t('modules.dashDot.card.title')}</h2>
|
||||
|
||||
{!info ? (
|
||||
<p>Cannot acquire information from dash. - are you running the latest version?</p>
|
||||
<p>{t('modules.dashDot.card.errors.noInformation')}</p>
|
||||
) : (
|
||||
<div className={classes.graphsContainer}>
|
||||
<div className={classes.table}>
|
||||
{storageEnabled && isCompact && (
|
||||
<div className={classes.tableRow}>
|
||||
<p className={classes.tableLabel}>Storage:</p>
|
||||
<p className={classes.tableLabel}>
|
||||
{t('modules.dashDot.card.graphs.storage.label')}
|
||||
</p>
|
||||
<p className={classes.tableValue}>
|
||||
{((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'}
|
||||
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
|
||||
@@ -204,10 +204,15 @@ export function DashdotComponent() {
|
||||
)}
|
||||
{networkEnabled && (
|
||||
<div className={classes.tableRow}>
|
||||
<p className={classes.tableLabel}>Network:</p>
|
||||
<p className={classes.tableLabel}>
|
||||
{t('modules.dashDot.card.graphs.network.label')}
|
||||
</p>
|
||||
<p className={classes.tableValue}>
|
||||
{bpsPrettyPrint(info?.network?.speedUp)} Up{'\n'}
|
||||
{bpsPrettyPrint(info?.network?.speedDown)} Down
|
||||
{bpsPrettyPrint(info?.network?.speedUp)}{' '}
|
||||
{t('modules.dashDot.card.graphs.network.metrics.upload')}
|
||||
{'\n'}
|
||||
{bpsPrettyPrint(info?.network?.speedDown)}
|
||||
{t('modules.dashDot.card.graphs.network.metrics.download')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useEffect, useState } from 'react';
|
||||
import Docker from 'dockerode';
|
||||
import { IconBrandDocker, IconX } from '@tabler/icons';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import ContainerActionBar from './ContainerActionBar';
|
||||
import DockerTable from './DockerTable';
|
||||
import { useConfig } from '../../tools/state';
|
||||
@@ -42,10 +44,10 @@ export default function DockerMenuButton(props: any) {
|
||||
// Send an Error notification
|
||||
showNotification({
|
||||
autoClose: 1500,
|
||||
title: <Text>Docker integration failed</Text>,
|
||||
title: <Text>{t('layout.header.docker.errors.integrationFailed.title')}</Text>,
|
||||
color: 'red',
|
||||
icon: <IconX />,
|
||||
message: 'Did you forget to mount the docker socket ?',
|
||||
message: t('layout.header.docker.errors.integrationFailed.message'),
|
||||
})
|
||||
);
|
||||
}, 300);
|
||||
@@ -67,7 +69,7 @@ export default function DockerMenuButton(props: any) {
|
||||
>
|
||||
<DockerTable containers={containers} selection={selection} setSelection={setSelection} />
|
||||
</Drawer>
|
||||
<Tooltip label="Docker">
|
||||
<Tooltip label={t('layout.header.docker.actionIcon.tooltip')}>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Table, Checkbox, Group, Badge, createStyles, ScrollArea, TextInput } from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import Dockerode from 'dockerode';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ContainerState from './ContainerState';
|
||||
@@ -80,7 +82,9 @@ export default function DockerTable({
|
||||
</Badge>
|
||||
))}
|
||||
{element.Ports.length > 3 && (
|
||||
<Badge variant="filled">{element.Ports.length - 3} more</Badge>
|
||||
<Badge variant="filled">
|
||||
{t('modules.docker.table.body.portCollapse', { ports: element.Ports.length - 3 })}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</td>
|
||||
@@ -94,7 +98,7 @@ export default function DockerTable({
|
||||
return (
|
||||
<ScrollArea style={{ height: '80vh' }}>
|
||||
<TextInput
|
||||
placeholder="Search by container or image name"
|
||||
placeholder={t('modules.docker.search.placeholder')}
|
||||
mt="md"
|
||||
icon={<IconSearch size={14} />}
|
||||
value={search}
|
||||
@@ -111,10 +115,10 @@ export default function DockerTable({
|
||||
transitionDuration={0}
|
||||
/>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Image</th>
|
||||
<th>Ports</th>
|
||||
<th>State</th>
|
||||
<th>{t('modules.docker.table.header.name')}</th>
|
||||
<th>{t('modules.docker.table.header.image')}</th>
|
||||
<th>{t('modules.docker.table.header.ports')}</th>
|
||||
<th>{t('modules.docker.table.header.state')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useConfig } from '../../tools/state';
|
||||
import { AddItemShelfButton } from '../../components/AppShelf/AddAppShelfItem';
|
||||
import { useSetSafeInterval } from '../../tools/hooks/useSetSafeInterval';
|
||||
import { humanFileSize } from '../../tools/humanFileSize';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const DownloadsModule: IModule = {
|
||||
title: 'Torrent',
|
||||
@@ -105,12 +106,12 @@ export default function DownloadComponent() {
|
||||
const DEVICE_WIDTH = 576;
|
||||
const ths = (
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
{width > 576 ? <th>Down</th> : ''}
|
||||
{width > 576 ? <th>Up</th> : ''}
|
||||
<th>ETA</th>
|
||||
<th>Progress</th>
|
||||
<th>{t('modules.downloads.card.table.header.name')}</th>
|
||||
<th>{t('modules.downloads.card.table.header.size')}</th>
|
||||
{width > 576 ? <th>{t('modules.downloads.card.table.header.download')}</th> : ''}
|
||||
{width > 576 ? <th>{t('modules.downloads.card.table.header.upload')}</th> : ''}
|
||||
<th>{t('modules.downloads.card.table.header.estimatedTimeOfArrival')}</th>
|
||||
<th>{t('modules.downloads.card.table.header.progress')}</th>
|
||||
</tr>
|
||||
);
|
||||
// Convert Seconds to readable format.
|
||||
@@ -195,7 +196,7 @@ export default function DownloadComponent() {
|
||||
</Table>
|
||||
) : (
|
||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<Title order={3}>No torrents found</Title>
|
||||
<Title order={3}>{t('modules.downloads.card.table.body.nothingFound')}</Title>
|
||||
</Center>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
@@ -3,12 +3,13 @@ import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { IconAlertCircle, IconCheck, IconDownload } from '@tabler/icons';
|
||||
import axios from 'axios';
|
||||
import Consola from 'consola';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useColorTheme } from '../../tools/color';
|
||||
import { MovieResult } from './Movie.d';
|
||||
import { MediaType, Result } from './SearchResult.d';
|
||||
import { TvShowResult, TvShowResultSeason } from './TvShow.d';
|
||||
|
||||
interface RequestModalProps {
|
||||
base: Result;
|
||||
opened: boolean;
|
||||
@@ -67,23 +68,23 @@ export function MovieRequestModal({
|
||||
title={
|
||||
<Group>
|
||||
<IconDownload />
|
||||
Ask for {result.title}
|
||||
{t('modules.overseerr.popup.item.buttons.askFor', { title: result.title })}
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="Using API key"
|
||||
title={t('modules.overseerr.popup.item.alerts.automaticApproval.title')}
|
||||
color={secondaryColor}
|
||||
radius="md"
|
||||
variant="filled"
|
||||
>
|
||||
This request will be automatically approved
|
||||
{t('modules.overseerr.popup.item.alerts.automaticApproval.text')}
|
||||
</Alert>
|
||||
<Group>
|
||||
<Button variant="outline" color="gray" onClick={() => setOpened(false)}>
|
||||
Cancel
|
||||
{t('modules.overseerr.popup.item.buttons.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -91,7 +92,7 @@ export function MovieRequestModal({
|
||||
askForMedia(MediaType.Movie, result.id, result.title, []);
|
||||
}}
|
||||
>
|
||||
Request
|
||||
{t('modules.overseerr.popup.item.buttons.request')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
@@ -148,22 +149,24 @@ export function TvRequestModal({
|
||||
title={
|
||||
<Group>
|
||||
<IconDownload />
|
||||
Ask for {result.name ?? result.originalName ?? 'a TV show'}
|
||||
{t('modules.overseerr.popup.item.buttons.askFor', {
|
||||
title: result.name ?? result.originalName ?? 'a TV show',
|
||||
})}
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack>
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="Using API key"
|
||||
title={t('modules.overseerr.popup.item.alerts.automaticApproval.title')}
|
||||
color={secondaryColor}
|
||||
radius="md"
|
||||
variant="filled"
|
||||
>
|
||||
This request will be automatically approved
|
||||
{t('modules.overseerr.popup.item.alerts.automaticApproval.text')}
|
||||
</Alert>
|
||||
<Table captionSide="bottom" highlightOnHover>
|
||||
<caption>Tick the seasons that you want to be downloaded</caption>
|
||||
<caption>{t('modules.overseerr.popup.seasonSelector.caption')}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
@@ -174,15 +177,15 @@ export function TvRequestModal({
|
||||
transitionDuration={0}
|
||||
/>
|
||||
</th>
|
||||
<th>Season</th>
|
||||
<th>Number of episodes</th>
|
||||
<th>{t('modules.overseerr.popup.seasonSelector.table.header.season')}</th>
|
||||
<th>{t('modules.overseerr.popup.seasonSelector.table.header.numberOfEpisodes')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</Table>
|
||||
<Group>
|
||||
<Button variant="outline" color="gray" onClick={() => setOpened(false)}>
|
||||
Cancel
|
||||
{t('modules.overseerr.popup.item.buttons.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -196,7 +199,7 @@ export function TvRequestModal({
|
||||
);
|
||||
}}
|
||||
>
|
||||
Request
|
||||
{t('modules.overseerr.popup.item.buttons.request')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react';
|
||||
import { IconPlug as Plug } from '@tabler/icons';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const PingModule: IModule = {
|
||||
title: 'Ping Services',
|
||||
@@ -68,10 +69,10 @@ export default function PingComponent(props: any) {
|
||||
radius="lg"
|
||||
label={
|
||||
isOnline === 'loading'
|
||||
? 'Loading...'
|
||||
? t('modules.ping.states.loading')
|
||||
: isOnline === 'online'
|
||||
? `Online - ${response}`
|
||||
: `Offline - ${response}`
|
||||
? t('modules.ping.states.online', { response })
|
||||
: t('modules.ping.states.offline', { response })
|
||||
}
|
||||
>
|
||||
<Indicator
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
IconDownload as Download,
|
||||
IconMovie,
|
||||
} from '@tabler/icons';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import axios from 'axios';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useConfig } from '../../tools/state';
|
||||
@@ -175,7 +177,7 @@ export default function SearchBar(props: any) {
|
||||
radius="md"
|
||||
size="md"
|
||||
styles={{ rightSection: { pointerEvents: 'none' } }}
|
||||
placeholder="Search the web..."
|
||||
placeholder={t('layout.header.search.input.placeholder')}
|
||||
{...props}
|
||||
{...form.getInputProps('query')}
|
||||
/>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { IModule } from '../ModuleTypes';
|
||||
import { WeatherResponse } from './WeatherInterface';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const WeatherModule: IModule = {
|
||||
title: 'Weather',
|
||||
@@ -52,75 +53,81 @@ export function WeatherIcon(props: any) {
|
||||
let data: { icon: any; name: string };
|
||||
switch (code) {
|
||||
case 0: {
|
||||
data = { icon: Sun, name: 'Clear' };
|
||||
data = { icon: Sun, name: t('modules.weather.card.weatherDescriptions.clear') };
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
case 2:
|
||||
case 3: {
|
||||
data = { icon: Cloud, name: 'Mainly clear' };
|
||||
data = { icon: Cloud, name: t('modules.weather.card.weatherDescriptions.mainlyClear') };
|
||||
break;
|
||||
}
|
||||
case 45:
|
||||
case 48: {
|
||||
data = { icon: CloudFog, name: 'Fog' };
|
||||
data = { icon: CloudFog, name: t('modules.weather.card.weatherDescriptions.fog') };
|
||||
break;
|
||||
}
|
||||
case 51:
|
||||
case 53:
|
||||
case 55: {
|
||||
data = { icon: Cloud, name: 'Drizzle' };
|
||||
data = { icon: Cloud, name: t('modules.weather.card.weatherDescriptions.drizzle') };
|
||||
break;
|
||||
}
|
||||
case 56:
|
||||
case 57: {
|
||||
data = { icon: Snowflake, name: 'Freezing drizzle' };
|
||||
data = {
|
||||
icon: Snowflake,
|
||||
name: t('modules.weather.card.weatherDescriptions.freezingDrizzle'),
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 61:
|
||||
case 63:
|
||||
case 65: {
|
||||
data = { icon: CloudRain, name: 'Rain' };
|
||||
data = { icon: CloudRain, name: t('modules.weather.card.weatherDescriptions.rain') };
|
||||
break;
|
||||
}
|
||||
case 66:
|
||||
case 67: {
|
||||
data = { icon: CloudRain, name: 'Freezing rain' };
|
||||
data = { icon: CloudRain, name: t('modules.weather.card.weatherDescriptions.freezingRain') };
|
||||
break;
|
||||
}
|
||||
case 71:
|
||||
case 73:
|
||||
case 75: {
|
||||
data = { icon: CloudSnow, name: 'Snow fall' };
|
||||
data = { icon: CloudSnow, name: t('modules.weather.card.weatherDescriptions.snowFall') };
|
||||
break;
|
||||
}
|
||||
case 77: {
|
||||
data = { icon: CloudSnow, name: 'Snow grains' };
|
||||
data = { icon: CloudSnow, name: t('modules.weather.card.weatherDescriptions.snowGrains') };
|
||||
break;
|
||||
}
|
||||
case 80:
|
||||
case 81:
|
||||
case 82: {
|
||||
data = { icon: CloudRain, name: 'Rain showers' };
|
||||
data = { icon: CloudRain, name: t('modules.weather.card.weatherDescriptions.rainShowers') };
|
||||
|
||||
break;
|
||||
}
|
||||
case 85:
|
||||
case 86: {
|
||||
data = { icon: CloudSnow, name: 'Snow showers' };
|
||||
data = { icon: CloudSnow, name: t('modules.weather.card.weatherDescriptions.snowShowers') };
|
||||
break;
|
||||
}
|
||||
case 95: {
|
||||
data = { icon: CloudStorm, name: 'Thunderstorm' };
|
||||
data = { icon: CloudStorm, name: t('modules.weather.card.weatherDescriptions.thunderstorm') };
|
||||
break;
|
||||
}
|
||||
case 96:
|
||||
case 99: {
|
||||
data = { icon: CloudStorm, name: 'Thunderstorm with hail' };
|
||||
data = {
|
||||
icon: CloudStorm,
|
||||
name: t('modules.weather.card.weatherDescriptions.thunderstormWithHail'),
|
||||
};
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
data = { icon: QuestionMark, name: 'Unknown' };
|
||||
data = { icon: QuestionMark, name: t('modules.weather.card.weatherDescriptions.unknown') };
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,9 @@ import { ModalsProvider } from '@mantine/modals';
|
||||
import { ConfigProvider } from '../tools/state';
|
||||
import { theme } from '../tools/theme';
|
||||
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 }) {
|
||||
const { Component, pageProps } = props;
|
||||
@@ -69,7 +72,9 @@ export default function App(this: any, props: AppProps & { colorScheme: ColorSch
|
||||
<NotificationsProvider limit={4} position="bottom-left">
|
||||
<ModalsProvider>
|
||||
<ConfigProvider>
|
||||
<TranslationProvider>
|
||||
<Component {...pageProps} />
|
||||
</TranslationProvider>
|
||||
</ConfigProvider>
|
||||
</ModalsProvider>
|
||||
</NotificationsProvider>
|
||||
|
||||
16
src/providers/translation.provider.tsx
Normal file
16
src/providers/translation.provider.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
36
src/translations/i18n.ts
Normal file
36
src/translations/i18n.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
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';
|
||||
};
|
||||
Reference in New Issue
Block a user