-
Storage:
+
+ {t('modules.dashDot.card.graphs.storage.label')}
+
{((100 * totalUsed) / (totalSize || 1)).toFixed(1)}%{'\n'}
{bytePrettyPrint(totalUsed)} / {bytePrettyPrint(totalSize)}
@@ -204,10 +204,15 @@ export function DashdotComponent() {
)}
{networkEnabled && (
-
Network:
+
+ {t('modules.dashDot.card.graphs.network.label')}
+
- {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')}
)}
diff --git a/src/modules/docker/DockerModule.tsx b/src/modules/docker/DockerModule.tsx
index caca75306..91920c93a 100644
--- a/src/modules/docker/DockerModule.tsx
+++ b/src/modules/docker/DockerModule.tsx
@@ -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:
Docker integration failed,
+ title:
{t('layout.header.docker.errors.integrationFailed.title')},
color: 'red',
icon:
,
- 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) {
>
-
+
))}
{element.Ports.length > 3 && (
- {element.Ports.length - 3} more
+
+ {t('modules.docker.table.body.portCollapse', { ports: element.Ports.length - 3 })}
+
)}
@@ -94,7 +98,7 @@ export default function DockerTable({
return (
}
value={search}
@@ -111,10 +115,10 @@ export default function DockerTable({
transitionDuration={0}
/>
- Name |
- Image |
- Ports |
- State |
+ {t('modules.docker.table.header.name')} |
+ {t('modules.docker.table.header.image')} |
+ {t('modules.docker.table.header.ports')} |
+ {t('modules.docker.table.header.state')} |
{rows}
diff --git a/src/modules/downloads/DownloadsModule.tsx b/src/modules/downloads/DownloadsModule.tsx
index d81de9123..a8bdad466 100644
--- a/src/modules/downloads/DownloadsModule.tsx
+++ b/src/modules/downloads/DownloadsModule.tsx
@@ -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 = (
- | Name |
- Size |
- {width > 576 ? Down | : ''}
- {width > 576 ? Up | : ''}
- ETA |
- Progress |
+ {t('modules.downloads.card.table.header.name')} |
+ {t('modules.downloads.card.table.header.size')} |
+ {width > 576 ? {t('modules.downloads.card.table.header.download')} | : ''}
+ {width > 576 ? {t('modules.downloads.card.table.header.upload')} | : ''}
+ {t('modules.downloads.card.table.header.estimatedTimeOfArrival')} |
+ {t('modules.downloads.card.table.header.progress')} |
);
// Convert Seconds to readable format.
@@ -195,7 +196,7 @@ export default function DownloadComponent() {
) : (
- No torrents found
+ {t('modules.downloads.card.table.body.nothingFound')}
)}
diff --git a/src/modules/overseerr/RequestModal.tsx b/src/modules/overseerr/RequestModal.tsx
index ee23e2678..ffa06c5a6 100644
--- a/src/modules/overseerr/RequestModal.tsx
+++ b/src/modules/overseerr/RequestModal.tsx
@@ -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={
- Ask for {result.title}
+ {t('modules.overseerr.popup.item.buttons.askFor', { title: result.title })}
}
>
}
- 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')}
@@ -148,22 +149,24 @@ export function TvRequestModal({
title={
- Ask for {result.name ?? result.originalName ?? 'a TV show'}
+ {t('modules.overseerr.popup.item.buttons.askFor', {
+ title: result.name ?? result.originalName ?? 'a TV show',
+ })}
}
>
}
- 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')}
- Tick the seasons that you want to be downloaded
+ {t('modules.overseerr.popup.seasonSelector.caption')}
|
@@ -174,15 +177,15 @@ export function TvRequestModal({
transitionDuration={0}
/>
|
- Season |
- Number of episodes |
+ {t('modules.overseerr.popup.seasonSelector.table.header.season')} |
+ {t('modules.overseerr.popup.seasonSelector.table.header.numberOfEpisodes')} |
{rows}
diff --git a/src/modules/ping/PingModule.tsx b/src/modules/ping/PingModule.tsx
index 871b894d7..f8ab442fe 100644
--- a/src/modules/ping/PingModule.tsx
+++ b/src/modules/ping/PingModule.tsx
@@ -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 })
}
>
diff --git a/src/modules/weather/WeatherModule.tsx b/src/modules/weather/WeatherModule.tsx
index 947ed479d..4d3ce8ff7 100644
--- a/src/modules/weather/WeatherModule.tsx
+++ b/src/modules/weather/WeatherModule.tsx
@@ -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 (
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 776347495..2a0188c8f 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -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
-
+
+
+
diff --git a/src/providers/translation.provider.tsx b/src/providers/translation.provider.tsx
new file mode 100644
index 000000000..922c91115
--- /dev/null
+++ b/src/providers/translation.provider.tsx
@@ -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 (
+
+ {children}
+
+ );
+};
diff --git a/src/translations/i18n.ts b/src/translations/i18n.ts
new file mode 100644
index 000000000..ce3ad3996
--- /dev/null
+++ b/src/translations/i18n.ts
@@ -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';
+};