feat: translate app-store page
This commit is contained in:
parent
148391b9c0
commit
9df81d7989
6 changed files with 190 additions and 83 deletions
|
@ -19,25 +19,24 @@ import {
|
|||
import { AppCategory } from './types';
|
||||
|
||||
type AppCategoryEntry = {
|
||||
name: string;
|
||||
id: AppCategory;
|
||||
icon: Icon;
|
||||
};
|
||||
|
||||
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 },
|
||||
];
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"operator-not-found": "Operator user not found",
|
||||
"user-not-found": "User not found",
|
||||
"not-allowed-in-demo": "Not allowed in demo mode",
|
||||
"not-allowed-in-dev": "Not allowed in dev mode",
|
||||
"invalid-password": "Invalid password",
|
||||
"invalid-password-length": "Password must be at least 8 characters long",
|
||||
"invalid-locale": "Invalid locale",
|
||||
|
@ -29,7 +30,10 @@
|
|||
"invalid-config": "App {id} has an invalid config.json file",
|
||||
"app-not-exposable": "App {id} is not exposable",
|
||||
"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": {}
|
||||
},
|
||||
|
@ -116,7 +120,10 @@
|
|||
"empty-subtitle": "Install an app from the app store to get started",
|
||||
"empty-action": "Go to app store"
|
||||
},
|
||||
"app-store": {},
|
||||
"app-store": {
|
||||
"search-placeholder": "Search apps",
|
||||
"category-placeholder": "Select a category"
|
||||
},
|
||||
"app-details": {
|
||||
"install-success": "App installed successfully",
|
||||
"uninstall-success": "App uninstalled successfully",
|
||||
|
@ -125,6 +132,32 @@
|
|||
"start-success": "App started successfully",
|
||||
"update-config-success": "App config updated successfully. Restart the app to apply the changes",
|
||||
"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": {
|
||||
"start": "Start",
|
||||
"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": {
|
||||
"dashboard": "Dashboard",
|
||||
"my-apps": "My Apps",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import clsx from 'clsx';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { AppLogo } from '../../../../components/AppLogo/AppLogo';
|
||||
import { AppCategory } from '../../../../core/types';
|
||||
import { colorSchemeForCategory, limitText } from '../../helpers/table.helpers';
|
||||
|
@ -13,21 +14,25 @@ type App = {
|
|||
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>
|
||||
</Link>
|
||||
);
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppStoreTile;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import Select, { SingleValue, OptionProps, ControlProps, components } from 'react-select';
|
||||
import { Icon } from '@tabler/icons-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { APP_CATEGORIES } from '../../../../core/constants';
|
||||
import { AppCategory } from '../../../../core/types';
|
||||
import { useUIStore } from '../../../../state/uiStore';
|
||||
|
@ -47,10 +48,11 @@ const ControlComponent = (props: ControlProps<OptionsType>) => {
|
|||
};
|
||||
|
||||
const CategorySelector: React.FC<IProps> = ({ onSelect, className, initialValue }) => {
|
||||
const t = useTranslations('apps');
|
||||
const { darkMode } = useUIStore();
|
||||
const options: OptionsType[] = APP_CATEGORIES.map((category) => ({
|
||||
value: category.id,
|
||||
label: category.name,
|
||||
label: t(`app-details.categories.${category.id}`),
|
||||
icon: category.icon,
|
||||
}));
|
||||
|
||||
|
@ -106,7 +108,7 @@ const CategorySelector: React.FC<IProps> = ({ onSelect, className, initialValue
|
|||
defaultValue={[]}
|
||||
name="categories"
|
||||
options={options}
|
||||
placeholder="Category"
|
||||
placeholder={t('app-store.category-placeholder')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import type { NextPage } from 'next';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import styles from './AppStorePage.module.scss';
|
||||
import { useAppStoreState } from '../../state/appStoreState';
|
||||
import { Input } from '../../../../components/ui/Input';
|
||||
|
@ -13,12 +14,13 @@ import { ErrorPage } from '../../../../components/ui/ErrorPage';
|
|||
import { trpc } from '../../../../utils/trpc';
|
||||
|
||||
export const AppStorePage: NextPage = () => {
|
||||
const t = useTranslations('apps.app-store');
|
||||
const { data, isLoading, error } = trpc.app.listApps.useQuery();
|
||||
const { setCategory, setSearch, category, search, sort, sortDirection } = useAppStoreState();
|
||||
|
||||
const actions = (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { DataGrid, DataGridItem } from '../../../components/ui/DataGrid';
|
||||
import Markdown from '../../../components/Markdown/Markdown';
|
||||
import { AppInfo } from '../../../core/types';
|
||||
|
@ -9,53 +10,57 @@ interface IProps {
|
|||
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" />
|
||||
</a>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue