Browse Source

feat: translate app-store page

Nicolas Meienberger 2 years ago
parent
commit
9df81d7989

+ 15 - 16
src/client/core/constants.ts

@@ -19,25 +19,24 @@ import {
 import { AppCategory } from './types';
 import { AppCategory } from './types';
 
 
 type AppCategoryEntry = {
 type AppCategoryEntry = {
-  name: string;
   id: AppCategory;
   id: AppCategory;
   icon: Icon;
   icon: Icon;
 };
 };
 
 
 export const APP_CATEGORIES: AppCategoryEntry[] = [
 export const APP_CATEGORIES: AppCategoryEntry[] = [
-  { name: 'Network', id: 'network', icon: IconBroadcast },
-  { name: 'Media', id: 'media', icon: IconMovie },
-  { name: 'Development', id: 'development', icon: IconCode },
-  { name: 'Automation', id: 'automation', icon: IconRobot },
-  { name: 'Social', id: 'social', icon: IconUsers },
-  { name: 'Utilities', id: 'utilities', icon: IconTool },
-  { name: 'Photography', id: 'photography', icon: IconCamera },
-  { name: 'Security', id: 'security', icon: IconShieldLock },
-  { name: 'Featured', id: 'featured', icon: IconStar },
-  { name: 'Books', id: 'books', icon: IconBook },
-  { name: 'Data', id: 'data', icon: IconDatabase },
-  { name: 'Music', id: 'music', icon: IconMusic },
-  { name: 'Finance', id: 'finance', icon: IconPigMoney },
-  { name: 'Gaming', id: 'gaming', icon: IconDeviceGamepad2 },
-  { name: 'AI', id: 'ai', icon: IconBrain },
+  { id: 'network', icon: IconBroadcast },
+  { id: 'media', icon: IconMovie },
+  { id: 'development', icon: IconCode },
+  { id: 'automation', icon: IconRobot },
+  { id: 'social', icon: IconUsers },
+  { id: 'utilities', icon: IconTool },
+  { id: 'photography', icon: IconCamera },
+  { id: 'security', icon: IconShieldLock },
+  { id: 'featured', icon: IconStar },
+  { id: 'books', icon: IconBook },
+  { id: 'data', icon: IconDatabase },
+  { id: 'music', icon: IconMusic },
+  { id: 'finance', icon: IconPigMoney },
+  { id: 'gaming', icon: IconDeviceGamepad2 },
+  { id: 'ai', icon: IconBrain },
 ];
 ];

+ 96 - 2
src/client/messages/en.json

@@ -11,6 +11,7 @@
       "operator-not-found": "Operator user not found",
       "operator-not-found": "Operator user not found",
       "user-not-found": "User not found",
       "user-not-found": "User not found",
       "not-allowed-in-demo": "Not allowed in demo mode",
       "not-allowed-in-demo": "Not allowed in demo mode",
+      "not-allowed-in-dev": "Not allowed in dev mode",
       "invalid-password": "Invalid password",
       "invalid-password": "Invalid password",
       "invalid-password-length": "Password must be at least 8 characters long",
       "invalid-password-length": "Password must be at least 8 characters long",
       "invalid-locale": "Invalid locale",
       "invalid-locale": "Invalid locale",
@@ -29,7 +30,10 @@
       "invalid-config": "App {id} has an invalid config.json file",
       "invalid-config": "App {id} has an invalid config.json file",
       "app-not-exposable": "App {id} is not exposable",
       "app-not-exposable": "App {id} is not exposable",
       "app-force-exposed": "App {id} works only with exposed domain",
       "app-force-exposed": "App {id} works only with exposed domain",
-      "domain-already-in-use": "Domain {domain} is already in use by app {id}"
+      "domain-already-in-use": "Domain {domain} is already in use by app {id}",
+      "could-not-get-latest-version": "Could not get latest version",
+      "current-version-is-latest": "Current version is already up to date",
+      "major-version-update": "The major version has changed. Please update manually (instructions on GitHub)"
     },
     },
     "success": {}
     "success": {}
   },
   },
@@ -116,7 +120,10 @@
       "empty-subtitle": "Install an app from the app store to get started",
       "empty-subtitle": "Install an app from the app store to get started",
       "empty-action": "Go to app store"
       "empty-action": "Go to app store"
     },
     },
-    "app-store": {},
+    "app-store": {
+      "search-placeholder": "Search apps",
+      "category-placeholder": "Select a category"
+    },
     "app-details": {
     "app-details": {
       "install-success": "App installed successfully",
       "install-success": "App installed successfully",
       "uninstall-success": "App uninstalled successfully",
       "uninstall-success": "App uninstalled successfully",
@@ -125,6 +132,32 @@
       "start-success": "App started successfully",
       "start-success": "App started successfully",
       "update-config-success": "App config updated successfully. Restart the app to apply the changes",
       "update-config-success": "App config updated successfully. Restart the app to apply the changes",
       "version": "Version",
       "version": "Version",
+      "description": "Description",
+      "base-info": "Base info",
+      "source-code": "Source code",
+      "author": "Author",
+      "port": "Port",
+      "categories-title": "Categories",
+      "link": "Link",
+      "website": "Website",
+      "supported-arch": "Supported architectures",
+      "categories": {
+        "data": "Data",
+        "network": "Network",
+        "media": "Media",
+        "development": "Development",
+        "automation": "Automation",
+        "social": "Social",
+        "utilities": "Utilities",
+        "security": "Security",
+        "photography": "Photography",
+        "featured": "Featured",
+        "books": "Books",
+        "music": "Music",
+        "finance": "Finance",
+        "gaming": "Gaming",
+        "ai": "AI"
+      },
       "actions": {
       "actions": {
         "start": "Start",
         "start": "Start",
         "remove": "Remove",
         "remove": "Remove",
@@ -180,6 +213,67 @@
       }
       }
     }
     }
   },
   },
+  "settings": {
+    "title": "Settings",
+    "actions": {
+      "tab-title": "Actions",
+      "title": "Actions",
+      "current-version": "Current version: {version}",
+      "stay-up-to-date": "Stay up to date with the latest version of Tipi",
+      "new-version": "A new version ({version}) of Tipi is available",
+      "maintenance-title": "Maintenance",
+      "maintenance-subtitle": "Common actions to perform on your instance",
+      "restart": "Restart",
+      "update": "Update to {version}",
+      "already-latest": "Already up to date"
+    },
+    "settings": {
+      "tab-title": "Settings",
+      "title": "General Settings",
+      "subtitle": "This will update your settings.json file. Make sure you know what you are doing before updating these values.",
+      "settings-updated": "Settings updated. Restart your instance to apply new settings.",
+      "invalid-ip": "Invalid IP address",
+      "invalid-url": "Invalid URL",
+      "invalid-domain": "Invalid domain",
+      "domain-name": "Domain name",
+      "domain-name-hint": "Make sure this exact domain contains an A record pointing to your IP.",
+      "dns-ip": "DNS IP",
+      "internal-ip": "Internal IP",
+      "internal-ip-hint": "IP address your server is listening on.",
+      "apps-repo": "Apps repo URL",
+      "apps-repo-hint": "URL to the apps repository.",
+      "storage-path": "Storage path",
+      "storage-path-hint": "Path to the storage directory. Keep empty for default (runtipi/app-data). Make sure it is an absolute path and that it exists",
+      "submit": "Save"
+    },
+    "security": {
+      "tab-title": "Security",
+      "change-password-title": "Change password",
+      "change-password-subtitle": "Changing your password will log you out of all devices.",
+      "password-change-success": "Password changed successfully",
+      "2fa-title": "Two-factor authentication",
+      "2fa-subtitle": "Two-factor authentication (2FA) adds an additional layer of security to your account.",
+      "2fa-subtitle-2": "When enabled, you will be prompted to enter a code from your authenticator app when you log in.",
+      "2fa-enable-success": "Two-factor authentication enabled",
+      "2fa-disable-success": "Two-factor authentication disabled",
+      "scan-qr-code": "Scan this QR code with your authenticator app.",
+      "enter-key-manually": "Or enter this key manually.",
+      "enter-2fa-code": "Enter the 6-digit code from your authenticator app",
+      "enable-2fa": "Enable two-factor authentication",
+      "disable-2fa": "Disable two-factor authentication",
+      "password-needed": "Password needed",
+      "password-needed-hint": "Your password is required to change two-factor authentication settings.",
+      "form": {
+        "password-length": "Password must be at least 8 characters",
+        "password-match": "Passwords do not match",
+        "current-password": "Current password",
+        "new-password": "New password",
+        "confirm-password": "Confirm new password",
+        "change-password": "Change password",
+        "password": "Password"
+      }
+    }
+  },
   "header": {
   "header": {
     "dashboard": "Dashboard",
     "dashboard": "Dashboard",
     "my-apps": "My Apps",
     "my-apps": "My Apps",

+ 20 - 15
src/client/modules/AppStore/components/AppStoreTile/AppStoreTile.tsx

@@ -1,6 +1,7 @@
 import clsx from 'clsx';
 import clsx from 'clsx';
 import Link from 'next/link';
 import Link from 'next/link';
 import React from 'react';
 import React from 'react';
+import { useTranslations } from 'next-intl';
 import { AppLogo } from '../../../../components/AppLogo/AppLogo';
 import { AppLogo } from '../../../../components/AppLogo/AppLogo';
 import { AppCategory } from '../../../../core/types';
 import { AppCategory } from '../../../../core/types';
 import { colorSchemeForCategory, limitText } from '../../helpers/table.helpers';
 import { colorSchemeForCategory, limitText } from '../../helpers/table.helpers';
@@ -13,21 +14,25 @@ type App = {
   short_desc: string;
   short_desc: string;
 };
 };
 
 
-const AppStoreTile: React.FC<{ app: App }> = ({ app }) => (
-  <Link className={clsx('cursor-pointer col-sm-6 col-lg-4 p-2 mt-4', styles.appTile)} href={`/app-store/${app.id}`} passHref>
-    <div key={app.id} className="d-flex overflow-hidden align-items-center py-2 ps-2">
-      <AppLogo className={styles.logo} id={app.id} />
-      <div className="card-body">
-        <h3 className="text-bold h-3 mb-2">{limitText(app.name, 20)}</h3>
-        <p className="text-muted text-nowrap mb-2">{limitText(app.short_desc, 30)}</p>
-        {app.categories?.map((category) => (
-          <div className={`badge me-1 bg-${colorSchemeForCategory[category]}`} key={`${app.id}-${category}`}>
-            {category.toLocaleLowerCase()}
-          </div>
-        ))}
+const AppStoreTile: React.FC<{ app: App }> = ({ app }) => {
+  const t = useTranslations('apps.app-details');
+
+  return (
+    <Link className={clsx('cursor-pointer col-sm-6 col-lg-4 p-2 mt-4', styles.appTile)} href={`/app-store/${app.id}`} passHref>
+      <div key={app.id} className="d-flex overflow-hidden align-items-center py-2 ps-2">
+        <AppLogo className={styles.logo} id={app.id} />
+        <div className="card-body">
+          <h3 className="text-bold h-3 mb-2">{limitText(app.name, 20)}</h3>
+          <p className="text-muted text-nowrap mb-2">{limitText(app.short_desc, 30)}</p>
+          {app.categories?.map((category) => (
+            <div className={`badge me-1 bg-${colorSchemeForCategory[category]}`} key={`${app.id}-${category}`}>
+              {t(`categories.${category}`)}
+            </div>
+          ))}
+        </div>
       </div>
       </div>
-    </div>
-  </Link>
-);
+    </Link>
+  );
+};
 
 
 export default AppStoreTile;
 export default AppStoreTile;

+ 4 - 2
src/client/modules/AppStore/components/CategorySelector/CategorySelector.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import Select, { SingleValue, OptionProps, ControlProps, components } from 'react-select';
 import Select, { SingleValue, OptionProps, ControlProps, components } from 'react-select';
 import { Icon } from '@tabler/icons-react';
 import { Icon } from '@tabler/icons-react';
+import { useTranslations } from 'next-intl';
 import { APP_CATEGORIES } from '../../../../core/constants';
 import { APP_CATEGORIES } from '../../../../core/constants';
 import { AppCategory } from '../../../../core/types';
 import { AppCategory } from '../../../../core/types';
 import { useUIStore } from '../../../../state/uiStore';
 import { useUIStore } from '../../../../state/uiStore';
@@ -47,10 +48,11 @@ const ControlComponent = (props: ControlProps<OptionsType>) => {
 };
 };
 
 
 const CategorySelector: React.FC<IProps> = ({ onSelect, className, initialValue }) => {
 const CategorySelector: React.FC<IProps> = ({ onSelect, className, initialValue }) => {
+  const t = useTranslations('apps');
   const { darkMode } = useUIStore();
   const { darkMode } = useUIStore();
   const options: OptionsType[] = APP_CATEGORIES.map((category) => ({
   const options: OptionsType[] = APP_CATEGORIES.map((category) => ({
     value: category.id,
     value: category.id,
-    label: category.name,
+    label: t(`app-details.categories.${category.id}`),
     icon: category.icon,
     icon: category.icon,
   }));
   }));
 
 
@@ -106,7 +108,7 @@ const CategorySelector: React.FC<IProps> = ({ onSelect, className, initialValue
       defaultValue={[]}
       defaultValue={[]}
       name="categories"
       name="categories"
       options={options}
       options={options}
-      placeholder="Category"
+      placeholder={t('app-store.category-placeholder')}
     />
     />
   );
   );
 };
 };

+ 3 - 1
src/client/modules/AppStore/pages/AppStorePage/AppStorePage.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import type { NextPage } from 'next';
 import type { NextPage } from 'next';
 import clsx from 'clsx';
 import clsx from 'clsx';
+import { useTranslations } from 'next-intl';
 import styles from './AppStorePage.module.scss';
 import styles from './AppStorePage.module.scss';
 import { useAppStoreState } from '../../state/appStoreState';
 import { useAppStoreState } from '../../state/appStoreState';
 import { Input } from '../../../../components/ui/Input';
 import { Input } from '../../../../components/ui/Input';
@@ -13,12 +14,13 @@ import { ErrorPage } from '../../../../components/ui/ErrorPage';
 import { trpc } from '../../../../utils/trpc';
 import { trpc } from '../../../../utils/trpc';
 
 
 export const AppStorePage: NextPage = () => {
 export const AppStorePage: NextPage = () => {
+  const t = useTranslations('apps.app-store');
   const { data, isLoading, error } = trpc.app.listApps.useQuery();
   const { data, isLoading, error } = trpc.app.listApps.useQuery();
   const { setCategory, setSearch, category, search, sort, sortDirection } = useAppStoreState();
   const { setCategory, setSearch, category, search, sort, sortDirection } = useAppStoreState();
 
 
   const actions = (
   const actions = (
     <div className="d-flex align-items-stretch align-items-md-center flex-column flex-md-row justify-content-end">
     <div className="d-flex align-items-stretch align-items-md-center flex-column flex-md-row justify-content-end">
-      <Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search" className={clsx('flex-fill mt-2 mt-md-0 me-md-2', styles.selector)} />
+      <Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder={t('search-placeholder')} className={clsx('flex-fill mt-2 mt-md-0 me-md-2', styles.selector)} />
       <CategorySelector initialValue={category} className={clsx('flex-fill mt-2 mt-md-0', styles.selector)} onSelect={setCategory} />
       <CategorySelector initialValue={category} className={clsx('flex-fill mt-2 mt-md-0', styles.selector)} onSelect={setCategory} />
     </div>
     </div>
   );
   );

+ 52 - 47
src/client/modules/Apps/components/AppDetailsTabs.tsx

@@ -1,6 +1,7 @@
 import { IconExternalLink } from '@tabler/icons-react';
 import { IconExternalLink } from '@tabler/icons-react';
 import React from 'react';
 import React from 'react';
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
 import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
+import { useTranslations } from 'next-intl';
 import { DataGrid, DataGridItem } from '../../../components/ui/DataGrid';
 import { DataGrid, DataGridItem } from '../../../components/ui/DataGrid';
 import Markdown from '../../../components/Markdown/Markdown';
 import Markdown from '../../../components/Markdown/Markdown';
 import { AppInfo } from '../../../core/types';
 import { AppInfo } from '../../../core/types';
@@ -9,53 +10,57 @@ interface IProps {
   info: AppInfo;
   info: AppInfo;
 }
 }
 
 
-export const AppDetailsTabs: React.FC<IProps> = ({ info }) => (
-  <Tabs defaultValue="description" orientation="vertical" style={{ marginTop: -1 }}>
-    <TabsList>
-      <TabsTrigger value="description">Description</TabsTrigger>
-      <TabsTrigger value="info">Base Info</TabsTrigger>
-    </TabsList>
-    <TabsContent value="description">
-      <Markdown className="markdown">{info.description}</Markdown>
-    </TabsContent>
-    <TabsContent value="info">
-      <DataGrid>
-        <DataGridItem title="Source code">
-          <a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.source}>
-            Link
-            <IconExternalLink size={15} className="ms-1 mb-1" />
-          </a>
-        </DataGridItem>
-        <DataGridItem title="Author">{info.author}</DataGridItem>
-        <DataGridItem title="Port">
-          <b>{info.port}</b>
-        </DataGridItem>
-        <DataGridItem title="Categories">
-          {info.categories.map((c) => (
-            <div key={c} className="badge bg-green me-1">
-              {c.toLowerCase()}
-            </div>
-          ))}
-        </DataGridItem>
-        <DataGridItem title="Version">{info.version}</DataGridItem>
-        {info.supported_architectures && (
-          <DataGridItem title="Supported architectures">
-            {info.supported_architectures.map((a) => (
-              <div key={a} className="badge bg-red me-1">
-                {a.toLowerCase()}
-              </div>
-            ))}
-          </DataGridItem>
-        )}
-        {info.website && (
-          <DataGridItem title="Website">
-            <a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.website}>
-              {info.website}
+export const AppDetailsTabs: React.FC<IProps> = ({ info }) => {
+  const t = useTranslations('apps.app-details');
+
+  return (
+    <Tabs defaultValue="description" orientation="vertical" style={{ marginTop: -1 }}>
+      <TabsList>
+        <TabsTrigger value="description">{t('description')}</TabsTrigger>
+        <TabsTrigger value="info">{t('base-info')}</TabsTrigger>
+      </TabsList>
+      <TabsContent value="description">
+        <Markdown className="markdown">{info.description}</Markdown>
+      </TabsContent>
+      <TabsContent value="info">
+        <DataGrid>
+          <DataGridItem title={t('source-code')}>
+            <a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.source}>
+              {t('link')}
               <IconExternalLink size={15} className="ms-1 mb-1" />
               <IconExternalLink size={15} className="ms-1 mb-1" />
             </a>
             </a>
           </DataGridItem>
           </DataGridItem>
-        )}
-      </DataGrid>
-    </TabsContent>
-  </Tabs>
-);
+          <DataGridItem title={t('author')}>{info.author}</DataGridItem>
+          <DataGridItem title={t('port')}>
+            <b>{info.port}</b>
+          </DataGridItem>
+          <DataGridItem title={t('categories-title')}>
+            {info.categories.map((c) => (
+              <div key={c} className="badge bg-green me-1">
+                {t(`categories.${c}`)}
+              </div>
+            ))}
+          </DataGridItem>
+          <DataGridItem title={t('version')}>{info.version}</DataGridItem>
+          {info.supported_architectures && (
+            <DataGridItem title={t('supported-arch')}>
+              {info.supported_architectures.map((a) => (
+                <div key={a} className="badge bg-red me-1">
+                  {a.toLowerCase()}
+                </div>
+              ))}
+            </DataGridItem>
+          )}
+          {info.website && (
+            <DataGridItem title={t('website')}>
+              <a target="_blank" rel="noreferrer" className="text-blue-500 text-xs" href={info.website}>
+                {info.website}
+                <IconExternalLink size={15} className="ms-1 mb-1" />
+              </a>
+            </DataGridItem>
+          )}
+        </DataGrid>
+      </TabsContent>
+    </Tabs>
+  );
+};