+
{item.label}
@@ -506,7 +506,7 @@ function MonthQuickNav({ names, slot, onPick }: { names: string[]; slot: number;
data-active={i === slot}
aria-pressed={i === slot}
onClick={() => onPick(i)}
- className={`relative z-[1] whitespace-nowrap rounded-full py-[7px] text-center text-[0.71875rem] font-bold capitalize transition-colors duration-200 ${
+ className={`relative z-[1] whitespace-nowrap rounded-full py-[7px] text-center text-[length:calc(11.5px*var(--fs-scale-body,1))] font-bold capitalize transition-colors duration-200 ${
i === slot ? `text-m-actfg ${pill ? '' : 'bg-m-act'}` : 'text-m-ink active:bg-[color:var(--m-ic)]'
}`}
>
diff --git a/client/src/mobile/usePullToRefresh.test.tsx b/client/src/mobile/usePullToRefresh.test.tsx
new file mode 100644
index 000000000..1551becfb
--- /dev/null
+++ b/client/src/mobile/usePullToRefresh.test.tsx
@@ -0,0 +1,83 @@
+import { act, renderHook } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+
+vi.mock('../native/haptics', () => ({ tickHaptic: vi.fn() }));
+vi.mock('./overlayStack', () => ({ hasOpenOverlay: vi.fn(() => false) }));
+
+import { tickHaptic } from '../native/haptics';
+import { hasOpenOverlay } from './overlayStack';
+import { usePullToRefresh } from './usePullToRefresh';
+
+function touch(type: string, x = 0, y = 0) {
+ const event = new Event(type, { cancelable: true });
+ Object.defineProperty(event, 'touches', { value: type === 'touchend' ? [] : [{ clientX: x, clientY: y }] });
+ act(() => {
+ window.dispatchEvent(event);
+ });
+ return event;
+}
+
+function setup() {
+ const el = document.createElement('div');
+ const ref = { current: el };
+ const onRefresh = vi.fn(async () => {});
+ const hook = renderHook(() => usePullToRefresh(ref, onRefresh));
+ return { el, onRefresh, hook };
+}
+
+beforeEach(() => {
+ vi.useFakeTimers();
+ Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });
+ vi.mocked(tickHaptic).mockClear();
+ vi.mocked(hasOpenOverlay).mockReturnValue(false);
+});
+afterEach(() => vi.useRealTimers());
+
+describe('usePullToRefresh', () => {
+ it('springs back without refreshing after a short pull', () => {
+ const { el, onRefresh } = setup();
+ touch('touchstart', 100, 100);
+ const move = touch('touchmove', 100, 160);
+ expect(move.defaultPrevented).toBe(true);
+ expect(el.style.transform).toContain('translate3d');
+ touch('touchend');
+ expect(onRefresh).not.toHaveBeenCalled();
+ expect(el.style.transform).toBe('');
+ });
+
+ it('arms with a tick past the threshold and refreshes on release', async () => {
+ const { onRefresh, hook } = setup();
+ touch('touchstart', 100, 100);
+ touch('touchmove', 100, 400);
+ expect(tickHaptic).toHaveBeenCalledTimes(1);
+ expect(hook.result.current.armed).toBe(true);
+ touch('touchend');
+ expect(hook.result.current.refreshing).toBe(true);
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+ expect(onRefresh).toHaveBeenCalledTimes(1);
+ expect(hook.result.current.refreshing).toBe(false);
+ });
+
+ it('leaves sideways drags, scrolled pages and open sheets alone', () => {
+ const { el, onRefresh } = setup();
+ touch('touchstart', 100, 100);
+ expect(touch('touchmove', 200, 120).defaultPrevented).toBe(false);
+ touch('touchend');
+
+ Object.defineProperty(window, 'scrollY', { value: 40, configurable: true });
+ touch('touchstart', 100, 100);
+ touch('touchmove', 100, 400);
+ touch('touchend');
+
+ Object.defineProperty(window, 'scrollY', { value: 0, configurable: true });
+ vi.mocked(hasOpenOverlay).mockReturnValue(true);
+ touch('touchstart', 100, 100);
+ touch('touchmove', 100, 400);
+ touch('touchend');
+
+ expect(onRefresh).not.toHaveBeenCalled();
+ expect(el.style.transform).toBe('');
+ });
+});
diff --git a/client/src/mobile/usePullToRefresh.ts b/client/src/mobile/usePullToRefresh.ts
new file mode 100644
index 000000000..e58f94437
--- /dev/null
+++ b/client/src/mobile/usePullToRefresh.ts
@@ -0,0 +1,123 @@
+import { useEffect, useRef, useState, type RefObject } from 'react';
+import { tickHaptic } from '../native/haptics';
+import { hasOpenOverlay } from './overlayStack';
+
+/** How far the finger must pull (after resistance) before letting go refreshes. */
+export const PULL_THRESHOLD = 64;
+const MAX_PULL = 110;
+/** The indicator stays at least this long, so a fast answer does not just blink. */
+const MIN_SPIN_MS = 450;
+
+export type PullState = { distance: number; refreshing: boolean; armed: boolean };
+
+/**
+ * Pull-to-refresh on a page whose scroller is the window, as the mobile
+ * dashboard is. A downward drag that starts at the very top moves `contentRef`
+ * down with resistance; past the threshold a light tick tells the finger that
+ * letting go will refresh. Ignored while a sheet is open, for horizontal drags
+ * (chip rows, pagers) and when the page is not scrolled to the top.
+ *
+ * Writes the content's transform directly during the drag, so a pull costs no
+ * React render per frame; `state` only changes at the edges (armed, refreshing).
+ */
+export function usePullToRefresh(
+ contentRef: RefObject
,
+ onRefresh: () => Promise,
+ enabled = true
+): PullState {
+ const [state, setState] = useState({ distance: 0, refreshing: false, armed: false });
+ const refreshRef = useRef(onRefresh);
+ refreshRef.current = onRefresh;
+
+ useEffect(() => {
+ if (!enabled) return;
+ let start: { x: number; y: number } | null = null;
+ let pulling = false;
+ let armed = false;
+ let busy = false;
+ let distance = 0;
+
+ const place = (value: number, animate: boolean) => {
+ const el = contentRef.current;
+ if (!el) return;
+ el.style.transition = animate ? 'transform 0.32s cubic-bezier(0.32, 0.72, 0, 1)' : 'none';
+ el.style.transform = value > 0 ? `translate3d(0, ${value}px, 0)` : '';
+ };
+
+ const onStart = (e: TouchEvent) => {
+ if (busy || e.touches.length !== 1 || window.scrollY > 0 || hasOpenOverlay()) return;
+ start = { x: e.touches[0].clientX, y: e.touches[0].clientY };
+ pulling = false;
+ };
+
+ const onMove = (e: TouchEvent) => {
+ if (!start) return;
+ const dx = e.touches[0].clientX - start.x;
+ const dy = e.touches[0].clientY - start.y;
+ if (!pulling) {
+ // Decide once: a sideways or upward start belongs to whatever is under the finger.
+ if (Math.abs(dy) < 8 && Math.abs(dx) < 8) return;
+ if (dy <= 0 || Math.abs(dx) > Math.abs(dy) || window.scrollY > 0) {
+ start = null;
+ return;
+ }
+ pulling = true;
+ }
+ if (e.cancelable) e.preventDefault();
+ // Resistance grows with the pull, like a rubber band.
+ distance = Math.min(MAX_PULL, dy * 0.5 * (1 - Math.min(dy, 400) / 1000));
+ place(distance, false);
+ const nowArmed = distance >= PULL_THRESHOLD;
+ if (nowArmed !== armed) {
+ armed = nowArmed;
+ if (armed) tickHaptic();
+ setState({ distance, refreshing: false, armed });
+ } else {
+ setState((s) => (Math.abs(s.distance - distance) > 4 ? { ...s, distance } : s));
+ }
+ };
+
+ const onEnd = () => {
+ if (!start) return;
+ start = null;
+ if (!pulling) return;
+ pulling = false;
+ if (!armed) {
+ distance = 0;
+ place(0, true);
+ setState({ distance: 0, refreshing: false, armed: false });
+ return;
+ }
+ busy = true;
+ armed = false;
+ place(PULL_THRESHOLD * 0.75, true);
+ setState({ distance: PULL_THRESHOLD, refreshing: true, armed: false });
+ const began = Date.now();
+ void Promise.resolve()
+ .then(() => refreshRef.current())
+ .catch(() => {})
+ .then(() => new Promise((resolve) => setTimeout(resolve, Math.max(0, MIN_SPIN_MS - (Date.now() - began)))))
+ .then(() => {
+ busy = false;
+ distance = 0;
+ place(0, true);
+ setState({ distance: 0, refreshing: false, armed: false });
+ });
+ };
+
+ window.addEventListener('touchstart', onStart, { passive: true });
+ // Not passive: once a pull has started, the page must not also scroll or bounce.
+ window.addEventListener('touchmove', onMove, { passive: false });
+ window.addEventListener('touchend', onEnd, { passive: true });
+ window.addEventListener('touchcancel', onEnd, { passive: true });
+ return () => {
+ window.removeEventListener('touchstart', onStart);
+ window.removeEventListener('touchmove', onMove);
+ window.removeEventListener('touchend', onEnd);
+ window.removeEventListener('touchcancel', onEnd);
+ place(0, false);
+ };
+ }, [contentRef, enabled]);
+
+ return state;
+}
diff --git a/client/src/native/haptics.test.ts b/client/src/native/haptics.test.ts
index c5592544d..c8fa5938d 100644
--- a/client/src/native/haptics.test.ts
+++ b/client/src/native/haptics.test.ts
@@ -1,11 +1,11 @@
import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics';
import { beforeEach, describe, expect, it, vi } from 'vitest';
-import { tickHaptic, toastHaptic } from './haptics';
+import { pressHaptic, selectionHaptic, tickHaptic, toastHaptic } from './haptics';
import { isNativeApp } from './platform';
vi.mock('@capacitor/haptics', () => ({
- Haptics: { notification: vi.fn(), impact: vi.fn() },
- ImpactStyle: { Light: 'LIGHT' },
+ Haptics: { notification: vi.fn(), impact: vi.fn(), selectionChanged: vi.fn() },
+ ImpactStyle: { Light: 'LIGHT', Medium: 'MEDIUM' },
NotificationType: { Success: 'SUCCESS', Warning: 'WARNING', Error: 'ERROR' },
}));
vi.mock('./platform', () => ({ isNativeApp: vi.fn() }));
@@ -13,6 +13,7 @@ vi.mock('./platform', () => ({ isNativeApp: vi.fn() }));
beforeEach(() => {
vi.mocked(Haptics.notification).mockReset().mockResolvedValue();
vi.mocked(Haptics.impact).mockReset().mockResolvedValue();
+ vi.mocked(Haptics.selectionChanged).mockReset().mockResolvedValue();
vi.mocked(isNativeApp).mockReturnValue(true);
});
@@ -57,3 +58,17 @@ describe('tickHaptic', () => {
expect(Haptics.impact).not.toHaveBeenCalled();
});
});
+
+describe('selectionHaptic and pressHaptic', () => {
+ it('click on a changed value and bump on a long press, in the app only', () => {
+ selectionHaptic();
+ pressHaptic();
+ expect(Haptics.selectionChanged).toHaveBeenCalledTimes(1);
+ expect(Haptics.impact).toHaveBeenCalledWith({ style: ImpactStyle.Medium });
+ vi.mocked(isNativeApp).mockReturnValue(false);
+ selectionHaptic();
+ pressHaptic();
+ expect(Haptics.selectionChanged).toHaveBeenCalledTimes(1);
+ expect(Haptics.impact).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/client/src/native/haptics.ts b/client/src/native/haptics.ts
index da6cabb12..18d4a3ce8 100644
--- a/client/src/native/haptics.ts
+++ b/client/src/native/haptics.ts
@@ -27,3 +27,21 @@ export function tickHaptic(): void {
if (!isNativeApp()) return;
Haptics.impact({ style: ImpactStyle.Light }).catch(reportNativeFailure);
}
+
+/**
+ * The selection click a native switch, segmented control or picker gives when
+ * its value changes. Does nothing in a browser.
+ */
+export function selectionHaptic(): void {
+ if (!isNativeApp()) return;
+ Haptics.selectionChanged().catch(reportNativeFailure);
+}
+
+/**
+ * The firmer bump of a long press that opens a context menu, or of an item
+ * picked up to drag. Does nothing in a browser.
+ */
+export function pressHaptic(): void {
+ if (!isNativeApp()) return;
+ Haptics.impact({ style: ImpactStyle.Medium }).catch(reportNativeFailure);
+}
diff --git a/client/src/native/shareLink.test.ts b/client/src/native/shareLink.test.ts
new file mode 100644
index 000000000..c76c46420
--- /dev/null
+++ b/client/src/native/shareLink.test.ts
@@ -0,0 +1,40 @@
+import { Share } from '@capacitor/share';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { isNativeApp } from './platform';
+import { reportNativeFailure } from './reportFailure';
+import { canShareLink, shareLink } from './shareLink';
+
+vi.mock('@capacitor/share', () => ({ Share: { share: vi.fn() } }));
+vi.mock('./platform', () => ({ isNativeApp: vi.fn() }));
+vi.mock('./reportFailure', () => ({ reportNativeFailure: vi.fn() }));
+vi.mock('../utils/fileShare', () => ({ canShareText: vi.fn(() => false) }));
+
+beforeEach(() => {
+ vi.mocked(Share.share).mockReset().mockResolvedValue({ activityType: undefined });
+ vi.mocked(reportNativeFailure).mockClear();
+ vi.mocked(isNativeApp).mockReturnValue(true);
+});
+
+describe('shareLink', () => {
+ it('uses the native sheet inside the app', async () => {
+ expect(canShareLink()).toBe(true);
+ expect(await shareLink('https://trek.example/s/abc', 'Trip')).toBe(true);
+ expect(Share.share).toHaveBeenCalledWith({ url: 'https://trek.example/s/abc', title: 'Trip', dialogTitle: 'Trip' });
+ });
+
+ it('treats a closed sheet as no share, without a failure report', async () => {
+ vi.mocked(Share.share).mockRejectedValue(new Error('Share canceled'));
+ expect(await shareLink('https://trek.example/s/abc')).toBe(false);
+ expect(reportNativeFailure).not.toHaveBeenCalled();
+ });
+
+ it('falls back to the Web Share API in a browser, and offers nothing without one', async () => {
+ vi.mocked(isNativeApp).mockReturnValue(false);
+ expect(canShareLink()).toBe(false);
+ const share = vi.fn(async () => {});
+ vi.stubGlobal('navigator', { share });
+ expect(await shareLink('https://trek.example/s/abc')).toBe(true);
+ expect(share).toHaveBeenCalledWith({ url: 'https://trek.example/s/abc', title: undefined });
+ vi.unstubAllGlobals();
+ });
+});
diff --git a/client/src/native/shareLink.ts b/client/src/native/shareLink.ts
new file mode 100644
index 000000000..3b22326e9
--- /dev/null
+++ b/client/src/native/shareLink.ts
@@ -0,0 +1,33 @@
+import { Share } from '@capacitor/share';
+import { canShareText } from '../utils/fileShare';
+import { isNativeApp } from './platform';
+import { reportNativeFailure } from './reportFailure';
+
+/**
+ * Handing a link to the system share sheet, the way a native app offers
+ * "Share…" next to "Copy": Messages, Mail, AirDrop, WhatsApp. Inside the apps
+ * through Capacitor's Share, in a browser through the Web Share API where it
+ * exists. Where neither does, `canShareLink` is false and the caller shows
+ * only the copy button.
+ */
+export function canShareLink(): boolean {
+ return isNativeApp() || canShareText();
+}
+
+/** True when a target took the link, false when the sheet was closed or failed. */
+export async function shareLink(url: string, title?: string): Promise {
+ try {
+ if (isNativeApp()) {
+ await Share.share({ url, title, dialogTitle: title });
+ return true;
+ }
+ await navigator.share({ url, title });
+ return true;
+ } catch (err) {
+ // Closing the sheet rejects too; only a real failure is worth a report.
+ const name = (err as { name?: string; message?: string } | null)?.name ?? '';
+ const message = (err as { message?: string } | null)?.message ?? '';
+ if (name !== 'AbortError' && !/cancel/i.test(message)) reportNativeFailure(err);
+ return false;
+ }
+}
diff --git a/client/src/pages/dashboard/useDashboard.ts b/client/src/pages/dashboard/useDashboard.ts
index 95e191b08..524347e31 100644
--- a/client/src/pages/dashboard/useDashboard.ts
+++ b/client/src/pages/dashboard/useDashboard.ts
@@ -97,7 +97,7 @@ export function useDashboard() {
// client) is read back quietly, one read per burst: no loading state and no
// toast, since nobody here asked for it. trips:changed goes to the acting user
// only; trip:deleted goes to the trip's room, which this page is never in.
- useServerPing('trips:changed', true, async () => {
+ const refreshQuietly = async () => {
try {
const { trips, archivedTrips } = await tripRepo.list()
setTrips(sortTrips(trips))
@@ -106,7 +106,8 @@ export function useDashboard() {
} catch {
// The list on screen stays; the next visit reads it again.
}
- })
+ }
+ useServerPing('trips:changed', true, refreshQuietly)
// Re-run both the trip fetch and the auth check so a recovered backend clears
// the error banner (loadUser resets authCheckFailed on success). #1283
@@ -240,6 +241,8 @@ export function useDashboard() {
// data + derived
spotlight, heroBundle, stats, upcoming, gridTrips, isLoading,
loadError: loadError || authCheckFailed, retryLoad,
+ // Pull-to-refresh on mobile: the same quiet read, no loading state.
+ refreshQuietly,
// ui state
tripFilter, setTripFilter, viewMode, toggleViewMode,
showForm, setShowForm, editingTrip, setEditingTrip,
diff --git a/shared/src/i18n/ar/common.ts b/shared/src/i18n/ar/common.ts
index c85429084..5eacb211f 100644
--- a/shared/src/i18n/ar/common.ts
+++ b/shared/src/i18n/ar/common.ts
@@ -89,5 +89,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'تعذّر عرض هذه الإضافة',
'common.needsConnection': 'يتطلب هذا اتصالاً. حاول مرة أخرى عندما يعود TREK للاتصال.',
'common.errorDetails': 'التفاصيل',
+ 'common.share': 'مشاركة',
};
export default common;
diff --git a/shared/src/i18n/az/common.ts b/shared/src/i18n/az/common.ts
index 2314046fc..2231d5a73 100644
--- a/shared/src/i18n/az/common.ts
+++ b/shared/src/i18n/az/common.ts
@@ -81,5 +81,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Bu plagini göstərmək mümkün olmadı',
'common.needsConnection': 'Bunun üçün bağlantı lazımdır. TREK yenidən onlayn olduqda bir daha cəhd edin.',
'common.errorDetails': 'Təfərrüatlar',
+ 'common.share': 'Paylaş',
};
export default common;
diff --git a/shared/src/i18n/br/common.ts b/shared/src/i18n/br/common.ts
index f481d817a..c284f8d98 100644
--- a/shared/src/i18n/br/common.ts
+++ b/shared/src/i18n/br/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Não foi possível exibir este plugin',
'common.needsConnection': 'Isso precisa de conexão. Tente de novo quando o TREK voltar a ficar online.',
'common.errorDetails': 'Detalhes',
+ 'common.share': 'Compartilhar',
};
export default common;
diff --git a/shared/src/i18n/ca/common.ts b/shared/src/i18n/ca/common.ts
index 7ade04072..12da9dda4 100644
--- a/shared/src/i18n/ca/common.ts
+++ b/shared/src/i18n/ca/common.ts
@@ -83,5 +83,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': "No s'ha pogut mostrar aquest connector",
'common.needsConnection': 'Això necessita connexió. Torna-ho a provar quan TREK torni a estar en línia.',
'common.errorDetails': 'Detalls',
+ 'common.share': 'Comparteix',
};
export default common;
diff --git a/shared/src/i18n/cs/common.ts b/shared/src/i18n/cs/common.ts
index 692475251..0a86ac683 100644
--- a/shared/src/i18n/cs/common.ts
+++ b/shared/src/i18n/cs/common.ts
@@ -83,5 +83,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Tento plugin se nepodařilo zobrazit',
'common.needsConnection': 'Na tohle je potřeba připojení. Zkus to znovu, až bude TREK zase online.',
'common.errorDetails': 'Podrobnosti',
+ 'common.share': 'Sdílet',
};
export default common;
diff --git a/shared/src/i18n/de/common.ts b/shared/src/i18n/de/common.ts
index 870504e85..a116c7a9d 100644
--- a/shared/src/i18n/de/common.ts
+++ b/shared/src/i18n/de/common.ts
@@ -81,6 +81,7 @@ const common: TranslationStrings = {
'TREK wurde aktualisiert, während dieser Tab offen war. Lade neu, um die neue Version zu erhalten.',
'common.errorPluginTitle': 'Dieses Plugin konnte nicht angezeigt werden',
'common.needsConnection': 'Dafür braucht es eine Verbindung. Versuch es noch einmal, sobald TREK wieder online ist.',
- 'common.errorDetails': 'Details', // same-as-en
+ 'common.errorDetails': 'Details', // same-as-en,
+ 'common.share': 'Teilen',
};
export default common;
diff --git a/shared/src/i18n/en/common.ts b/shared/src/i18n/en/common.ts
index 0472d156f..ab1788736 100644
--- a/shared/src/i18n/en/common.ts
+++ b/shared/src/i18n/en/common.ts
@@ -81,5 +81,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'This plugin could not be shown',
'common.needsConnection': 'This needs a connection. Try again once TREK is back online.',
'common.errorDetails': 'Details',
+ 'common.share': 'Share',
};
export default common;
diff --git a/shared/src/i18n/es/common.ts b/shared/src/i18n/es/common.ts
index 82604fc62..4abbcdf77 100644
--- a/shared/src/i18n/es/common.ts
+++ b/shared/src/i18n/es/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'No se pudo mostrar este plugin',
'common.needsConnection': 'Esto necesita conexión. Vuelve a intentarlo cuando TREK esté de nuevo en línea.',
'common.errorDetails': 'Detalles',
+ 'common.share': 'Compartir',
};
export default common;
diff --git a/shared/src/i18n/et/common.ts b/shared/src/i18n/et/common.ts
index 3db16b6f4..21c7b5208 100644
--- a/shared/src/i18n/et/common.ts
+++ b/shared/src/i18n/et/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Seda pluginat ei saanud kuvada',
'common.needsConnection': 'Selleks on vaja ühendust. Proovi uuesti, kui TREK on taas võrgus.',
'common.errorDetails': 'Üksikasjad',
+ 'common.share': 'Jaga',
};
export default common;
diff --git a/shared/src/i18n/fr/common.ts b/shared/src/i18n/fr/common.ts
index 019e90a62..340cce31e 100644
--- a/shared/src/i18n/fr/common.ts
+++ b/shared/src/i18n/fr/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': "Ce plugin n'a pas pu être affiché",
'common.needsConnection': 'Cela nécessite une connexion. Réessaie dès que TREK sera de nouveau en ligne.',
'common.errorDetails': 'Détails',
+ 'common.share': 'Partager',
};
export default common;
diff --git a/shared/src/i18n/gr/common.ts b/shared/src/i18n/gr/common.ts
index c10fa5d15..4a7d0d18b 100644
--- a/shared/src/i18n/gr/common.ts
+++ b/shared/src/i18n/gr/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Δεν ήταν δυνατή η εμφάνιση αυτού του πρόσθετου',
'common.needsConnection': 'Αυτό χρειάζεται σύνδεση. Δοκίμασε ξανά μόλις το TREK συνδεθεί ξανά.',
'common.errorDetails': 'Λεπτομέρειες',
+ 'common.share': 'Κοινοποίηση',
};
export default common;
diff --git a/shared/src/i18n/he/common.ts b/shared/src/i18n/he/common.ts
index 887da8c75..61978f957 100644
--- a/shared/src/i18n/he/common.ts
+++ b/shared/src/i18n/he/common.ts
@@ -83,6 +83,7 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'לא ניתן להציג את התוסף הזה',
'common.needsConnection': 'הפעולה הזו דורשת חיבור. נסו שוב כש-TREK יחזור לרשת.',
'common.errorDetails': 'פרטים',
+ 'common.share': 'שיתוף',
};
export default common;
diff --git a/shared/src/i18n/hu/common.ts b/shared/src/i18n/hu/common.ts
index 6b2f1bc09..0264a4b47 100644
--- a/shared/src/i18n/hu/common.ts
+++ b/shared/src/i18n/hu/common.ts
@@ -81,5 +81,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Ezt a bővítményt nem sikerült megjeleníteni',
'common.needsConnection': 'Ehhez kapcsolat kell. Próbáld újra, amint a TREK újra online lesz.',
'common.errorDetails': 'Részletek',
+ 'common.share': 'Megosztás',
};
export default common;
diff --git a/shared/src/i18n/id/common.ts b/shared/src/i18n/id/common.ts
index 39471b441..e89a38d63 100644
--- a/shared/src/i18n/id/common.ts
+++ b/shared/src/i18n/id/common.ts
@@ -79,5 +79,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Plugin ini tidak dapat ditampilkan',
'common.needsConnection': 'Ini memerlukan koneksi. Coba lagi begitu TREK kembali online.',
'common.errorDetails': 'Detail',
+ 'common.share': 'Bagikan',
};
export default common;
diff --git a/shared/src/i18n/it/common.ts b/shared/src/i18n/it/common.ts
index ab8bc996e..e8f656f76 100644
--- a/shared/src/i18n/it/common.ts
+++ b/shared/src/i18n/it/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Impossibile mostrare questo plugin',
'common.needsConnection': 'Serve una connessione. Riprova quando TREK tornerà online.',
'common.errorDetails': 'Dettagli',
+ 'common.share': 'Condividi',
};
export default common;
diff --git a/shared/src/i18n/ja/common.ts b/shared/src/i18n/ja/common.ts
index 6c80a03a1..6beeef0f1 100644
--- a/shared/src/i18n/ja/common.ts
+++ b/shared/src/i18n/ja/common.ts
@@ -80,5 +80,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'このプラグインを表示できませんでした',
'common.needsConnection': 'この操作には接続が必要です。TREK がオンラインに戻ってから、もう一度お試しください。',
'common.errorDetails': '詳細',
+ 'common.share': '共有',
};
export default common;
diff --git a/shared/src/i18n/ko/common.ts b/shared/src/i18n/ko/common.ts
index bf1cadf28..125f1302b 100644
--- a/shared/src/i18n/ko/common.ts
+++ b/shared/src/i18n/ko/common.ts
@@ -79,5 +79,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': '이 플러그인을 표시할 수 없습니다',
'common.needsConnection': '연결이 필요한 작업입니다. TREK가 다시 온라인이 되면 다시 시도하세요.',
'common.errorDetails': '세부 정보',
+ 'common.share': '공유',
};
export default common;
diff --git a/shared/src/i18n/nl/common.ts b/shared/src/i18n/nl/common.ts
index 97e3de8d7..ee239f3de 100644
--- a/shared/src/i18n/nl/common.ts
+++ b/shared/src/i18n/nl/common.ts
@@ -81,6 +81,7 @@ const common: TranslationStrings = {
'TREK is bijgewerkt terwijl dit tabblad open stond. Herlaad om de nieuwe versie te krijgen.',
'common.errorPluginTitle': 'Deze plug-in kon niet worden getoond',
'common.needsConnection': 'Hiervoor is een verbinding nodig. Probeer het opnieuw zodra TREK weer online is.',
- 'common.errorDetails': 'Details', // same-as-en
+ 'common.errorDetails': 'Details', // same-as-en,
+ 'common.share': 'Delen',
};
export default common;
diff --git a/shared/src/i18n/pl/common.ts b/shared/src/i18n/pl/common.ts
index 3f7e57a28..f06839c02 100644
--- a/shared/src/i18n/pl/common.ts
+++ b/shared/src/i18n/pl/common.ts
@@ -85,5 +85,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Nie udało się wyświetlić tej wtyczki',
'common.needsConnection': 'To wymaga połączenia. Spróbuj ponownie, gdy TREK znów będzie online.',
'common.errorDetails': 'Szczegóły',
+ 'common.share': 'Udostępnij',
};
export default common;
diff --git a/shared/src/i18n/ru/common.ts b/shared/src/i18n/ru/common.ts
index c175439cb..22c3a3cd8 100644
--- a/shared/src/i18n/ru/common.ts
+++ b/shared/src/i18n/ru/common.ts
@@ -86,5 +86,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Не удалось показать этот плагин',
'common.needsConnection': 'Для этого нужно подключение. Попробуйте ещё раз, когда TREK снова будет онлайн.',
'common.errorDetails': 'Подробности',
+ 'common.share': 'Поделиться',
};
export default common;
diff --git a/shared/src/i18n/sk/common.ts b/shared/src/i18n/sk/common.ts
index b29ee14a1..f13884f53 100644
--- a/shared/src/i18n/sk/common.ts
+++ b/shared/src/i18n/sk/common.ts
@@ -84,5 +84,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Tento plugin sa nepodarilo zobraziť',
'common.needsConnection': 'Na toto je potrebné pripojenie. Skús to znova, keď bude TREK opäť online.',
'common.errorDetails': 'Podrobnosti',
+ 'common.share': 'Zdieľať',
};
export default common;
diff --git a/shared/src/i18n/sv/common.ts b/shared/src/i18n/sv/common.ts
index 76641a24d..1c3657499 100644
--- a/shared/src/i18n/sv/common.ts
+++ b/shared/src/i18n/sv/common.ts
@@ -82,5 +82,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Det här tillägget kunde inte visas',
'common.needsConnection': 'Det här kräver en anslutning. Försök igen när TREK är online igen.',
'common.errorDetails': 'Detaljer',
+ 'common.share': 'Dela',
};
export default common;
diff --git a/shared/src/i18n/th/common.ts b/shared/src/i18n/th/common.ts
index 041082dcd..49f191196 100644
--- a/shared/src/i18n/th/common.ts
+++ b/shared/src/i18n/th/common.ts
@@ -79,5 +79,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'ไม่สามารถแสดงปลั๊กอินนี้ได้',
'common.needsConnection': 'การทำงานนี้ต้องใช้การเชื่อมต่อ ลองอีกครั้งเมื่อ TREK กลับมาออนไลน์',
'common.errorDetails': 'รายละเอียด',
+ 'common.share': 'แชร์',
};
export default common;
diff --git a/shared/src/i18n/tr/common.ts b/shared/src/i18n/tr/common.ts
index 7123d1e96..28c0910fe 100644
--- a/shared/src/i18n/tr/common.ts
+++ b/shared/src/i18n/tr/common.ts
@@ -81,5 +81,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Bu eklenti gösterilemedi',
'common.needsConnection': 'Bunun için bağlantı gerekiyor. TREK yeniden çevrimiçi olduğunda tekrar dene.',
'common.errorDetails': 'Ayrıntılar',
+ 'common.share': 'Paylaş',
};
export default common;
diff --git a/shared/src/i18n/uk/common.ts b/shared/src/i18n/uk/common.ts
index efe5cf022..feeb0bb06 100644
--- a/shared/src/i18n/uk/common.ts
+++ b/shared/src/i18n/uk/common.ts
@@ -85,5 +85,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Не вдалося показати цей плагін',
'common.needsConnection': "Для цього потрібне з'єднання. Спробуйте ще раз, коли TREK знову буде онлайн.",
'common.errorDetails': 'Подробиці',
+ 'common.share': 'Поділитися',
};
export default common;
diff --git a/shared/src/i18n/vi/common.ts b/shared/src/i18n/vi/common.ts
index 2097e039c..9efbf808c 100644
--- a/shared/src/i18n/vi/common.ts
+++ b/shared/src/i18n/vi/common.ts
@@ -79,5 +79,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': 'Không thể hiển thị plugin này',
'common.needsConnection': 'Thao tác này cần kết nối. Hãy thử lại khi TREK trực tuyến trở lại.',
'common.errorDetails': 'Chi tiết',
+ 'common.share': 'Chia sẻ',
};
export default common;
diff --git a/shared/src/i18n/zh-TW/common.ts b/shared/src/i18n/zh-TW/common.ts
index f3d3009df..6f2a34ba9 100644
--- a/shared/src/i18n/zh-TW/common.ts
+++ b/shared/src/i18n/zh-TW/common.ts
@@ -79,5 +79,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': '無法顯示此外掛程式',
'common.needsConnection': '此操作需要網路連線。請在 TREK 恢復連線後再試一次。',
'common.errorDetails': '詳細資訊',
+ 'common.share': '分享',
};
export default common;
diff --git a/shared/src/i18n/zh/common.ts b/shared/src/i18n/zh/common.ts
index d17ede4b1..f382a91ce 100644
--- a/shared/src/i18n/zh/common.ts
+++ b/shared/src/i18n/zh/common.ts
@@ -79,5 +79,6 @@ const common: TranslationStrings = {
'common.errorPluginTitle': '无法显示此插件',
'common.needsConnection': '此操作需要网络连接。请在 TREK 恢复联网后重试。',
'common.errorDetails': '详细信息',
+ 'common.share': '分享',
};
export default common;