feat: translate app-store page

This commit is contained in:
Nicolas Meienberger 2023-05-15 08:05:58 +02:00 committed by Nicolas Meienberger
parent 148391b9c0
commit 9df81d7989
6 changed files with 190 additions and 83 deletions

View file

@ -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 },
];

View file

@ -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",

View file

@ -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;

View file

@ -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')}
/>
);
};

View file

@ -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>
);

View file

@ -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>
);
};