mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
feat(mobile): settings group their sections by what they reach: your account or this device
A tester could not tell which settings follow the account to every device and which stay on the phone, and looked for offline mode for a while. The section list and dropdown now stand under two headings, "Your account" and "This device". The server the app opens, the app lock and a client certificate move out of Account into a section of their own, "App", shown only in the app, next to Offline; About stands under TREK. Labels in all locales. TestFlight feedback, 5.1.3.
This commit is contained in:
@@ -6,6 +6,11 @@ export interface MSection {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
/**
|
||||
* The heading the section stands under. Consecutive sections with the same
|
||||
* group share one heading; a screen whose sections name none shows none.
|
||||
*/
|
||||
group?: string;
|
||||
}
|
||||
|
||||
interface MSectionRowProps {
|
||||
@@ -33,6 +38,50 @@ export function MSectionRow({ section, active, current = false, onPick }: MSecti
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The sections in order, each run of one group under its heading: the same list
|
||||
* in the dropdown of a phone and in the list beside the page.
|
||||
*/
|
||||
export function MSectionList({
|
||||
sections,
|
||||
activeId,
|
||||
markCurrent = false,
|
||||
onPick,
|
||||
}: {
|
||||
sections: MSection[];
|
||||
activeId: string;
|
||||
/** Marks the active row as the page on show (the list beside the page). */
|
||||
markCurrent?: boolean;
|
||||
onPick: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{sections.map((section, i) => {
|
||||
const heading = section.group && section.group !== sections[i - 1]?.group ? section.group : null;
|
||||
return (
|
||||
<div key={section.id} role="presentation">
|
||||
{heading && (
|
||||
<div
|
||||
className={`px-[10px] pb-1 font-geist text-[0.625rem] font-bold uppercase tracking-[.09em] text-m-faint ${
|
||||
i === 0 ? 'pt-1' : 'pt-3'
|
||||
}`}
|
||||
>
|
||||
{heading}
|
||||
</div>
|
||||
)}
|
||||
<MSectionRow
|
||||
section={section}
|
||||
active={section.id === activeId}
|
||||
current={markCurrent && section.id === activeId}
|
||||
onPick={() => onPick(section.id)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The frame of a settings-like screen (settings, administration), one rule for
|
||||
* both: the page column on a phone, a centred column on a wide window, and from
|
||||
@@ -88,15 +137,7 @@ export default function MSectionNav({
|
||||
<span className="min-w-0 flex-1 truncate text-subtitle font-bold text-m-ink">{title}</span>
|
||||
</div>
|
||||
{action && <div className="mb-1 px-[4px]">{action}</div>}
|
||||
{sections.map((section) => (
|
||||
<MSectionRow
|
||||
key={section.id}
|
||||
section={section}
|
||||
active={section.id === activeId}
|
||||
current={section.id === activeId}
|
||||
onPick={() => onPick(section.id)}
|
||||
/>
|
||||
))}
|
||||
<MSectionList sections={sections} activeId={activeId} markCurrent onPick={onPick} />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Puzzle,
|
||||
Settings2,
|
||||
SlidersHorizontal,
|
||||
Smartphone,
|
||||
User,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from '../../../i18n'
|
||||
@@ -26,8 +27,11 @@ import MSettingsIntegrations from './MSettingsIntegrations'
|
||||
import MSettingsAccount from './MSettingsAccount'
|
||||
import MSettingsAbout from './MSettingsAbout'
|
||||
import MSettingsServer from './MSettingsServer'
|
||||
import MAppLockCard from './MAppLockCard'
|
||||
import MClientCertCard from './MClientCertCard'
|
||||
import { isNativeApp } from '../../../native/platform'
|
||||
import { useIsSplit } from '../../../hooks/sizeClass'
|
||||
import MSectionNav, { MSectionRow, SECTION_SCREEN, type MSection } from '../../components/MSectionNav'
|
||||
import MSectionNav, { MSectionList, SECTION_SCREEN, type MSection } from '../../components/MSectionNav'
|
||||
|
||||
/**
|
||||
* Mobile settings screen: back + section-switcher pill on top, the section
|
||||
@@ -44,21 +48,33 @@ export default function MSettings() {
|
||||
// the page, the way Settings on an iPad does, instead of behind a dropdown.
|
||||
const split = useIsSplit()
|
||||
|
||||
// Two groups, so it is clear what a setting reaches: the account's settings
|
||||
// follow the user to every device; this device's stay on it (the server the
|
||||
// app opens, the app lock, a client certificate, the offline copy).
|
||||
const accountGroup = t('settings.groups.account')
|
||||
const deviceGroup = t('settings.groups.device')
|
||||
const app = isNativeApp()
|
||||
const tabs: MSection[] = [
|
||||
{ id: 'display', label: t('settings.tabs.display'), icon: SlidersHorizontal },
|
||||
{ id: 'appearance', label: t('settings.tabs.appearance'), icon: Palette },
|
||||
{ id: 'map', label: t('settings.tabs.map'), icon: Map },
|
||||
{ id: 'notifications', label: t('settings.tabs.notifications'), icon: Bell },
|
||||
...(hasIntegrations ? [{ id: 'integrations', label: t('settings.tabs.integrations'), icon: Plug }] : []),
|
||||
...(hasPlugins && !managed ? [{ id: 'plugins', label: t('settings.tabs.plugins'), icon: Puzzle }] : []),
|
||||
{ id: 'offline', label: t('settings.tabs.offline'), icon: CloudOff },
|
||||
{ id: 'account', label: t('settings.tabs.account'), icon: User },
|
||||
{ id: 'display', label: t('settings.tabs.display'), icon: SlidersHorizontal, group: accountGroup },
|
||||
{ id: 'appearance', label: t('settings.tabs.appearance'), icon: Palette, group: accountGroup },
|
||||
{ id: 'map', label: t('settings.tabs.map'), icon: Map, group: accountGroup },
|
||||
{ id: 'notifications', label: t('settings.tabs.notifications'), icon: Bell, group: accountGroup },
|
||||
...(hasIntegrations
|
||||
? [{ id: 'integrations', label: t('settings.tabs.integrations'), icon: Plug, group: accountGroup }]
|
||||
: []),
|
||||
...(hasPlugins && !managed
|
||||
? [{ id: 'plugins', label: t('settings.tabs.plugins'), icon: Puzzle, group: accountGroup }]
|
||||
: []),
|
||||
{ id: 'account', label: t('settings.tabs.account'), icon: User, group: accountGroup },
|
||||
...(app ? [{ id: 'device', label: t('settings.tabs.device'), icon: Smartphone, group: deviceGroup }] : []),
|
||||
{ id: 'offline', label: t('settings.tabs.offline'), icon: CloudOff, group: deviceGroup },
|
||||
// Same call as the desktop page: About is about the project — what TREK is,
|
||||
// where to file a bug, where to support it — and a customer of a hosted
|
||||
// instance is the audience for none of it. The version and the source link
|
||||
// move to the footer below, because AGPL §13 wants those offered wherever
|
||||
// people reach the software over a network.
|
||||
...(appVersion && !managed ? [{ id: 'about', label: t('settings.tabs.about'), icon: Info }] : []),
|
||||
// people reach the software over a network. Under the product's name, not
|
||||
// under either group.
|
||||
...(appVersion && !managed ? [{ id: 'about', label: t('settings.tabs.about'), icon: Info, group: 'TREK' }] : []),
|
||||
]
|
||||
|
||||
const active = tabs.find((tab) => tab.id === activeTab) || tabs[0]
|
||||
@@ -105,17 +121,14 @@ export default function MSettings() {
|
||||
{/* Section dropdown, in flow (pushes the content down like the demo) */}
|
||||
{dropOpen && !split && (
|
||||
<div className="m-pop-in -mt-[6px] mb-3 rounded-2xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-sheetop)] p-[6px] shadow-[0_16px_40px_-20px_rgba(0,0,0,.4)]">
|
||||
{tabs.map((tab) => (
|
||||
<MSectionRow
|
||||
key={tab.id}
|
||||
section={tab}
|
||||
active={tab.id === active.id}
|
||||
onPick={() => {
|
||||
setActiveTab(tab.id)
|
||||
setDropOpen(false)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<MSectionList
|
||||
sections={tabs}
|
||||
activeId={active.id}
|
||||
onPick={(id) => {
|
||||
setActiveTab(id)
|
||||
setDropOpen(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -126,8 +139,15 @@ export default function MSettings() {
|
||||
{active.id === 'integrations' && hasIntegrations && <MSettingsIntegrations />}
|
||||
{/* Per-plugin settings still reuse the existing responsive tab. */}
|
||||
{active.id === 'plugins' && hasPlugins && !managed && <MSettingsPlugins />}
|
||||
{/* Each card hides itself where it has nothing to offer; their own margins give way to one gap. */}
|
||||
{active.id === 'device' && app && (
|
||||
<div className="flex flex-col gap-3 [&>*]:m-0">
|
||||
<MSettingsServer />
|
||||
<MAppLockCard />
|
||||
<MClientCertCard />
|
||||
</div>
|
||||
)}
|
||||
{active.id === 'offline' && <MSettingsOffline />}
|
||||
{active.id === 'account' && <MSettingsServer />}
|
||||
{active.id === 'account' && <MSettingsAccount />}
|
||||
{active.id === 'about' && appVersion && <MSettingsAbout appVersion={appVersion} />}
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
import { useTranslation } from '../../../i18n'
|
||||
import { MSetCard, MSetEyebrow, MSetInput, MSetButton, MSetHint } from './MSettingsUi'
|
||||
import MConfirmSheet from './MConfirmSheet'
|
||||
import MAppLockCard from './MAppLockCard'
|
||||
import MClientCertCard from './MClientCertCard'
|
||||
import PasswordChecklist from '../../../components/shared/PasswordChecklist'
|
||||
import { fmtDate, usePasskeys } from '../../../components/Settings/usePasskeys'
|
||||
import { stripTrailingSlashes, useAccountSettings } from '../../../components/Settings/useAccountSettings'
|
||||
@@ -181,11 +179,7 @@ export default function MSettingsAccount() {
|
||||
)}
|
||||
|
||||
{/* ── Two-factor authentication ───────────────────────────── */}
|
||||
{/* The app lock, inside the app only (Face ID, Touch ID or the passcode). */}
|
||||
<MAppLockCard />
|
||||
{/* The client certificate for a server behind mutual TLS, once one is stored. */}
|
||||
<MClientCertCard />
|
||||
|
||||
{/* The app lock and the client certificate belong to this device: MSettings, "This device". */}
|
||||
<MSetCard title={t('settings.mfa.title')} icon={KeyRound} className="mt-3">
|
||||
{mfaRequiredByPolicy && (
|
||||
<div className="mb-3 flex gap-2 rounded-xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] p-3">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FE-MOB-SET-NAV-001 onwards
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from '../../../helpers/render';
|
||||
import { resetAllStores, seedStore } from '../../../helpers/store';
|
||||
@@ -7,6 +7,7 @@ import { buildSettings } from '../../../helpers/factories';
|
||||
import { useSettingsStore } from '../../../../src/store/settingsStore';
|
||||
import { usePluginStore } from '../../../../src/store/pluginStore';
|
||||
import MSettings from '../../../../src/mobile/screens/settings/MSettings';
|
||||
import * as platform from '../../../../src/native/platform';
|
||||
|
||||
describe('MSettings', () => {
|
||||
beforeEach(() => {
|
||||
@@ -35,4 +36,36 @@ describe('MSettings', () => {
|
||||
expect(screen.getByRole('button', { name: /Notifications/ })).toBeInTheDocument();
|
||||
expect(screen.queryByText('Language & region')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SET-NAV-003: the sections stand under "Your account" and "This device", with Offline on the device', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<MSettings />, { initialEntries: ['/settings'] });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /General/ }));
|
||||
const account = screen.getByText('Your account');
|
||||
const device = screen.getByText('This device');
|
||||
const offline = screen.getByRole('button', { name: 'Offline' });
|
||||
const accountRow = screen.getByRole('button', { name: 'Account' });
|
||||
// Account settings first, then what stays on this device.
|
||||
expect(account.compareDocumentPosition(accountRow) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(accountRow.compareDocumentPosition(device) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(device.compareDocumentPosition(offline) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
// The app's own section exists only in the app.
|
||||
expect(screen.queryByRole('button', { name: 'App' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-MOB-SET-NAV-004: in the app, "App" joins This device', async () => {
|
||||
const native = vi.spyOn(platform, 'isNativeApp').mockReturnValue(true);
|
||||
try {
|
||||
const user = userEvent.setup();
|
||||
render(<MSettings />, { initialEntries: ['/settings'] });
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /General/ }));
|
||||
const device = screen.getByText('This device');
|
||||
const app = screen.getByRole('button', { name: 'App' });
|
||||
expect(device.compareDocumentPosition(app) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
} finally {
|
||||
native.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -322,6 +322,9 @@ const settings: TranslationStrings = {
|
||||
'settings.mfa.toastEnabled': 'تم تفعيل المصادقة الثنائية',
|
||||
'settings.mfa.toastDisabled': 'تم تعطيل المصادقة الثنائية',
|
||||
'settings.tabs.offline': 'دون اتصال',
|
||||
'settings.tabs.device': 'التطبيق',
|
||||
'settings.groups.account': 'حسابك',
|
||||
'settings.groups.device': 'هذا الجهاز',
|
||||
'settings.notificationPreferences.email': 'البريد الإلكتروني',
|
||||
'settings.notificationPreferences.webhook': 'Webhook',
|
||||
'settings.notificationPreferences.inapp': 'داخل التطبيق',
|
||||
|
||||
@@ -31,6 +31,9 @@ const settings: TranslationStrings = {
|
||||
|
||||
'settings.tabs.account': 'Hesab',
|
||||
'settings.tabs.offline': 'Oflayn',
|
||||
'settings.tabs.device': 'Tətbiq',
|
||||
'settings.groups.account': 'Hesabınız',
|
||||
'settings.groups.device': 'Bu cihaz',
|
||||
'settings.tabs.about': 'Haqqında',
|
||||
|
||||
'settings.map': 'Xəritə',
|
||||
|
||||
@@ -20,6 +20,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" é obrigatório',
|
||||
'settings.tabs.account': 'Conta',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Sua conta',
|
||||
'settings.groups.device': 'Este dispositivo',
|
||||
'settings.tabs.about': 'Sobre',
|
||||
'settings.map': 'Mapa',
|
||||
'settings.mapTemplate': 'Modelo de mapa',
|
||||
|
||||
@@ -9,6 +9,9 @@ const settings: TranslationStrings = {
|
||||
'settings.tabs.integrations': 'Integracions',
|
||||
'settings.tabs.account': 'Compte',
|
||||
'settings.tabs.offline': 'Fora de línia',
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'El teu compte',
|
||||
'settings.groups.device': 'Aquest dispositiu',
|
||||
'settings.tabs.about': 'Quant a',
|
||||
'settings.map': 'Mapa',
|
||||
'settings.mapTemplate': 'Plantilla del mapa',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '„{field}" je povinné',
|
||||
'settings.tabs.account': 'Účet',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'Aplikace',
|
||||
'settings.groups.account': 'Tvůj účet',
|
||||
'settings.groups.device': 'Toto zařízení',
|
||||
'settings.tabs.about': 'O aplikaci',
|
||||
'settings.map': 'Mapy',
|
||||
'settings.mapTemplate': 'Šablona mapy',
|
||||
|
||||
@@ -20,6 +20,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" ist erforderlich',
|
||||
'settings.tabs.account': 'Konto',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Dein Konto',
|
||||
'settings.groups.device': 'Dieses Gerät',
|
||||
'settings.tabs.about': 'Über',
|
||||
'settings.map': 'Karte',
|
||||
'settings.mapTemplate': 'Karten-Vorlage',
|
||||
|
||||
@@ -28,6 +28,9 @@ const settings: TranslationStrings = {
|
||||
'settings.pluginActivity.columns.status': 'Result',
|
||||
'settings.tabs.account': 'Account',
|
||||
'settings.tabs.offline': 'Offline',
|
||||
'settings.tabs.device': 'App',
|
||||
'settings.groups.account': 'Your account',
|
||||
'settings.groups.device': 'This device',
|
||||
'settings.tabs.about': 'About',
|
||||
'settings.map': 'Map',
|
||||
'settings.mapTemplate': 'Map Template',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" es obligatorio',
|
||||
'settings.tabs.account': 'Cuenta',
|
||||
'settings.tabs.offline': 'Sin conexión',
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Tu cuenta',
|
||||
'settings.groups.device': 'Este dispositivo',
|
||||
'settings.tabs.about': 'Acerca de',
|
||||
'settings.map': 'Mapa',
|
||||
'settings.mapTemplate': 'Plantilla del mapa',
|
||||
|
||||
@@ -28,6 +28,9 @@ const settings: TranslationStrings = {
|
||||
'settings.pluginActivity.columns.status': 'Tulemus',
|
||||
'settings.tabs.account': 'Konto',
|
||||
'settings.tabs.offline': 'Võrguühenduseta',
|
||||
'settings.tabs.device': 'Rakendus',
|
||||
'settings.groups.account': 'Sinu konto',
|
||||
'settings.groups.device': 'See seade',
|
||||
'settings.tabs.about': 'Teave',
|
||||
'settings.map': 'Kaart',
|
||||
'settings.mapTemplate': 'Kaardimall',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '« {field} » est obligatoire',
|
||||
'settings.tabs.account': 'Compte',
|
||||
'settings.tabs.offline': 'Hors ligne',
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Ton compte',
|
||||
'settings.groups.device': 'Cet appareil',
|
||||
'settings.tabs.about': 'À propos',
|
||||
'settings.map': 'Carte',
|
||||
'settings.mapTemplate': 'Modèle de carte',
|
||||
|
||||
@@ -20,6 +20,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" είναι υποχρεωτικό',
|
||||
'settings.tabs.account': 'Λογαριασμός',
|
||||
'settings.tabs.offline': 'Εκτός σύνδεσης',
|
||||
'settings.tabs.device': 'Εφαρμογή',
|
||||
'settings.groups.account': 'Ο λογαριασμός σου',
|
||||
'settings.groups.device': 'Αυτή η συσκευή',
|
||||
'settings.tabs.about': 'Σχετικά',
|
||||
'settings.map': 'Χάρτης',
|
||||
'settings.mapTemplate': 'Πρότυπο Χάρτη',
|
||||
|
||||
@@ -28,6 +28,9 @@ const settings: TranslationStrings = {
|
||||
'settings.pluginActivity.columns.status': 'תוצאה',
|
||||
'settings.tabs.account': 'חשבון',
|
||||
'settings.tabs.offline': 'אופליין',
|
||||
'settings.tabs.device': 'אפליקציה',
|
||||
'settings.groups.account': 'החשבון שלך',
|
||||
'settings.groups.device': 'המכשיר הזה',
|
||||
'settings.tabs.about': 'אודות',
|
||||
'settings.map': 'מפה',
|
||||
'settings.mapTemplate': 'תבנית מפה',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" kötelező',
|
||||
'settings.tabs.account': 'Fiók',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'Alkalmazás',
|
||||
'settings.groups.account': 'Fiókod',
|
||||
'settings.groups.device': 'Ez az eszköz',
|
||||
'settings.tabs.about': 'Névjegy',
|
||||
'settings.map': 'Térkép',
|
||||
'settings.mapTemplate': 'Térkép sablon',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" wajib diisi',
|
||||
'settings.tabs.account': 'Akun',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'Aplikasi',
|
||||
'settings.groups.account': 'Akun Anda',
|
||||
'settings.groups.device': 'Perangkat ini',
|
||||
'settings.tabs.about': 'Tentang',
|
||||
'settings.map': 'Peta',
|
||||
'settings.mapTemplate': 'Template Peta',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" è obbligatorio',
|
||||
'settings.tabs.account': 'Account', // same-as-en
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Il tuo account',
|
||||
'settings.groups.device': 'Questo dispositivo',
|
||||
'settings.tabs.about': 'Informazioni',
|
||||
'settings.map': 'Mappa',
|
||||
'settings.mapTemplate': 'Modello Mappa',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '「{field}」は必須です',
|
||||
'settings.tabs.account': 'アカウント',
|
||||
'settings.tabs.offline': 'オフライン',
|
||||
'settings.tabs.device': 'アプリ',
|
||||
'settings.groups.account': 'あなたのアカウント',
|
||||
'settings.groups.device': 'このデバイス',
|
||||
'settings.tabs.about': '情報',
|
||||
'settings.map': '地図',
|
||||
'settings.mapTemplate': '地図テンプレート',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}"은(는) 필수입니다',
|
||||
'settings.tabs.account': '계정',
|
||||
'settings.tabs.offline': '오프라인',
|
||||
'settings.tabs.device': '앱',
|
||||
'settings.groups.account': '내 계정',
|
||||
'settings.groups.device': '이 기기',
|
||||
'settings.tabs.about': '정보',
|
||||
'settings.map': '지도',
|
||||
'settings.mapTemplate': '지도 템플릿',
|
||||
|
||||
@@ -20,6 +20,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" is vereist',
|
||||
'settings.tabs.account': 'Account', // same-as-en
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Je account',
|
||||
'settings.groups.device': 'Dit apparaat',
|
||||
'settings.tabs.about': 'Over',
|
||||
'settings.map': 'Kaart',
|
||||
'settings.mapTemplate': 'Kaartsjabloon',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" jest wymagane',
|
||||
'settings.tabs.account': 'Konto',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'Aplikacja',
|
||||
'settings.groups.account': 'Twoje konto',
|
||||
'settings.groups.device': 'To urządzenie',
|
||||
'settings.tabs.about': 'O aplikacji',
|
||||
'settings.map': 'Mapa',
|
||||
'settings.mapTemplate': 'Szablon mapy',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '«{field}» обязательно',
|
||||
'settings.tabs.account': 'Аккаунт',
|
||||
'settings.tabs.offline': 'Офлайн-режим',
|
||||
'settings.tabs.device': 'Приложение',
|
||||
'settings.groups.account': 'Ваш аккаунт',
|
||||
'settings.groups.device': 'Это устройство',
|
||||
'settings.tabs.about': 'О приложении',
|
||||
'settings.map': 'Карта',
|
||||
'settings.mapTemplate': 'Шаблон карты',
|
||||
|
||||
@@ -28,6 +28,9 @@ const settings: TranslationStrings = {
|
||||
'settings.pluginActivity.columns.status': 'Výsledok',
|
||||
'settings.tabs.account': 'Účet',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'Aplikácia',
|
||||
'settings.groups.account': 'Tvoj účet',
|
||||
'settings.groups.device': 'Toto zariadenie',
|
||||
'settings.tabs.about': 'O aplikácii',
|
||||
'settings.map': 'Mapa',
|
||||
'settings.mapTemplate': 'Šablóna mapy',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" krävs',
|
||||
'settings.tabs.account': 'Konto',
|
||||
'settings.tabs.offline': 'Offline', // same-as-en
|
||||
'settings.tabs.device': 'App', // same-as-en
|
||||
'settings.groups.account': 'Ditt konto',
|
||||
'settings.groups.device': 'Den här enheten',
|
||||
'settings.tabs.about': 'Om',
|
||||
'settings.map': 'Karta',
|
||||
'settings.mapTemplate': 'Kartmall',
|
||||
|
||||
@@ -28,6 +28,9 @@ const settings: TranslationStrings = {
|
||||
'settings.pluginActivity.columns.status': 'ผลลัพธ์',
|
||||
'settings.tabs.account': 'บัญชี',
|
||||
'settings.tabs.offline': 'ออฟไลน์',
|
||||
'settings.tabs.device': 'แอป',
|
||||
'settings.groups.account': 'บัญชีของคุณ',
|
||||
'settings.groups.device': 'อุปกรณ์นี้',
|
||||
'settings.tabs.about': 'เกี่ยวกับ',
|
||||
'settings.map': 'แผนที่',
|
||||
'settings.mapTemplate': 'แม่แบบแผนที่',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" gerekli',
|
||||
'settings.tabs.account': 'Hesap',
|
||||
'settings.tabs.offline': 'Çevrimdışı',
|
||||
'settings.tabs.device': 'Uygulama',
|
||||
'settings.groups.account': 'Hesabın',
|
||||
'settings.groups.device': 'Bu cihaz',
|
||||
'settings.tabs.about': 'Hakkında',
|
||||
'settings.map': 'Harita',
|
||||
'settings.mapTemplate': 'Harita Şablonu',
|
||||
|
||||
@@ -20,6 +20,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '«{field}» обов’язкове',
|
||||
'settings.tabs.account': 'Обліковий запис',
|
||||
'settings.tabs.offline': 'Офлайн',
|
||||
'settings.tabs.device': 'Застосунок',
|
||||
'settings.groups.account': 'Ваш обліковий запис',
|
||||
'settings.groups.device': 'Цей пристрій',
|
||||
'settings.tabs.about': 'Про застосунок',
|
||||
'settings.map': 'Карта',
|
||||
'settings.mapTemplate': 'Шаблон карти',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '"{field}" là bắt buộc',
|
||||
'settings.tabs.account': 'Tài khoản',
|
||||
'settings.tabs.offline': 'Ngoại tuyến',
|
||||
'settings.tabs.device': 'Ứng dụng',
|
||||
'settings.groups.account': 'Tài khoản của bạn',
|
||||
'settings.groups.device': 'Thiết bị này',
|
||||
'settings.tabs.about': 'Về',
|
||||
'settings.map': 'Bản đồ',
|
||||
'settings.mapTemplate': 'Mẫu bản đồ',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '「{field}」為必填項',
|
||||
'settings.tabs.account': '帳戶',
|
||||
'settings.tabs.offline': '離線',
|
||||
'settings.tabs.device': '應用程式',
|
||||
'settings.groups.account': '你的帳戶',
|
||||
'settings.groups.device': '此裝置',
|
||||
'settings.tabs.about': '關於',
|
||||
'settings.map': '地圖',
|
||||
'settings.mapTemplate': '地圖模板',
|
||||
|
||||
@@ -19,6 +19,9 @@ const settings: TranslationStrings = {
|
||||
'settings.plugins.requiredMissing': '「{field}」为必填项',
|
||||
'settings.tabs.account': '账户',
|
||||
'settings.tabs.offline': '离线',
|
||||
'settings.tabs.device': '应用',
|
||||
'settings.groups.account': '你的账户',
|
||||
'settings.groups.device': '此设备',
|
||||
'settings.tabs.about': '关于',
|
||||
'settings.map': '地图',
|
||||
'settings.mapTemplate': '地图模板',
|
||||
|
||||
Reference in New Issue
Block a user