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:
Maurice
2026-11-11 21:31:14 +00:00
parent 948c9ef57b
commit 80c8d9c602
32 changed files with 212 additions and 40 deletions
+50 -9
View File
@@ -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();
}
});
});
+3
View File
@@ -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': 'داخل التطبيق',
+3
View File
@@ -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ə',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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': 'Πρότυπο Χάρτη',
+3
View File
@@ -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': 'תבנית מפה',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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': '地図テンプレート',
+3
View File
@@ -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': '지도 템플릿',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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': 'Шаблон карты',
+3
View File
@@ -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',
+3
View File
@@ -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',
+3
View File
@@ -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': 'แม่แบบแผนที่',
+3
View File
@@ -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',
+3
View File
@@ -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': 'Шаблон карти',
+3
View File
@@ -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 đồ',
+3
View File
@@ -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': '地圖模板',
+3
View File
@@ -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': '地图模板',