}
- title={t('mobileTrip.exportCalendar')}
+ icon={
}
+ title={t('mobileTrip.export')}
onClose={shell.closeSheet}
closeLabel={t('common.close')}
/>
@@ -102,6 +129,12 @@ export default function MExportSheet({ planner, shell }: MTripSheetsProps) {
sub={`${planner.trip?.title || 'trip'}.ics`}
onClick={() => void downloadIcs()}
/>
+
void downloadGpx()}
+ />
{canManageShare && (
void }[] = [
{ id: 'members', icon: Share2, label: t('members.shareTrip'), go: () => shell.openSheet('members') },
- { id: 'export', icon: CalendarPlus, label: t('mobileTrip.exportCalendar'), go: () => shell.openSheet('export') },
+ { id: 'export', icon: FileDown, label: t('mobileTrip.export'), go: () => shell.openSheet('export') },
...(canEditTrip
? [{ id: 'tripedit', icon: Settings, label: t('dashboard.editTrip'), go: () => shell.openSheet('tripedit') }]
: []),
diff --git a/server/src/nest/places/gpx-export.helpers.ts b/server/src/nest/places/gpx-export.helpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..41b8788765a47549f4f926d5ac2f366fb4425763
GIT binary patch
literal 6030
zcmbVQ{g2zY5$(_UCpsyLvz^-VZhGhy&VJ-O=-qX*NK)W{y+uq*w5_g83M3ud-MIbN
z`(`M~vg2He2DtNzB8S77H*X%5FG}kI-P13B|M~ecFEZ_DOH*eT)R`(D2J1yJD3$Zt
zb)GzUFnsjr0X?EO*T2xp4|v&*1A&NK`Mw;7Am94~%3QRyALW}7%*poJ<+9(Vc^BBZl~
zUaAe;aax?VhJSei2Ys$QPG27`wM&B>AUuJl){WSvqD*Z%o_w&S=M3N=h~+}vX+ENM
zQ=NulLo;i$`cQpZ&GU55v0SlBgV@3*Spx$|Nrq%*9O|Lqfu2=?gKLZh
zy=FvVOQUrL4?;rE%qAW|Vq1qLA_yCC?1MvC61ExF!*ke}0zCAlT)z^V*Ni2)e?Vja
zp%ISg5SJ%-Mutw-FG#kv{1{V;H|{C4sKO(i8;};~1@o3rC0(h4a-vxJ4B5P#haCEx>~GK66fO
z&hGcoL9HA=;hfbQ(e~oW1Dy5ic>9|{A&8d%TKS>9J9`
zZ|?S1;Ok|-mDe+&V9$857VjE%wuTz4Jl9Nhl35IkY9Ht~oB
zK-)<3^kPSWLPm9prG|eHVqK^L^!V@7Q(7!q98#ihL?TgU11dFsxzLX3qyj0p&fDS>
zlP+L+r!|<0sbiWK_#gM4hDxV}1tA%mdo75XE=|g19vQlea*yTPYw&W^?u=bkX^U6~WaK4s!bFnD6Ym612wfKy2@+=in-fI4TMWsTg775w^4pEp
zdIwH$yx~Sw6XNnYMLXyb)Q#`8L1SV@sEkzG9@#}^_UQ9xlE0Edn;AOf89nZCZqd`0
zn*ArqbYQUIfK}*a$<4{FoUoaJW_B>+w4Naj#Kk!0>)b$ObYW5)c=*K&GsE$Zd4Z_D
z0+`PCoxY&Qbh+=_W0AoE^cUzIa0n}#(2oEMT9Q<@$e`SaNeH8b8Z|v1
zp0YMlAXvz~6(olVI3ZeqLm^i!eyU8G^8k1CiCynkZEVyN-Ie3D81kBX`>u#Tqkb|y
z6}Pw`Do{h{c;xg(Rwu2lf0>}B7u{YzvT{?llIsDF
z|3pN(oj!FK9E~fI!01Y?SIT
zCa09y6a;J_INk{_LP9c1S92%`MubNfh@@INn=Mm7p;p+aWnf4E28qFwQ#>8p2$<(>wjmbTU4}qy-!lqgKDnLOq1g?>xoU6+3;_QS7>7Axl0V4PVUFUIy*&Nj
z@RH<;!TPcz*~N`%RGqFFKge&LMOI!CAC68_{q>n!dJ6AYO~tarLsOJ7uoy`yX>n9P
zF<6j9BppcPOB&zBL5diZg54#Kd$*eC0>r{b_EUo2)@~C9>-nPoA~hV)lcnL*xh)3F%O{}4Tr1M3b&=w&YXfwNuUOYkQBqy;hsTRHCp@pVCCx1
ziFLE#z^$OvO4uQouF^d7$Pjm+GovX_#CHz
zTxi<*D?u#S=6+wtlZd&am2B<`cs%R_Jgb#zo`uI_vsxhGS$N!^C5hy_cXMf|BxWMN
zT>u5e1TNwW#A{YjXtbU+5-q_N{B7<9XdU8e@G|$M_4wL|RxsBJ@?1VZHWFQg7``2%
zdrW|aJJI^wGvZ{q7U0S0ROg?#5AnU+Z+y@Trll#`7bK!Wv)I#pX!ZEMitm@Zk9dt@
zLn3OQp)(FXrtCD?PK-?yt}?8`s=2~S31l_CIpqJ+-OknPpI&x)G7Rq29v7N0$a1eH
zg&r8(q6TusF?PV+&e>w%^h~d#9jB$ab%!^<8~px1fB5y+;poxj*~RDEKMiN7G|2vg
ydB6IDH*gHsjW`Q?r@DL6r@x#Wt|PHG=m-y17Y('SELECT title FROM trips WHERE id = ?', tripId);
+ if (!trip) return null;
+
+ const places = this.dbs.all(`
+ SELECT p.name, p.description, p.address, p.lat, p.lng, p.route_geometry, c.name AS category
+ FROM places p
+ LEFT JOIN categories c ON c.id = p.category_id
+ WHERE p.trip_id = ?
+ ORDER BY p.id
+ `, tripId);
+
+ // One row per stop, ordered the way the day plan draws it, then folded into days.
+ const stops = this.dbs.all<{
+ day_number: number; date: string | null; title: string | null;
+ name: string; lat: number; lng: number;
+ }>(`
+ SELECT d.day_number, d.date, d.title, p.name, p.lat, p.lng
+ FROM days d
+ JOIN day_assignments da ON da.day_id = d.id
+ JOIN places p ON p.id = da.place_id
+ WHERE d.trip_id = ? AND p.lat IS NOT NULL AND p.lng IS NOT NULL
+ ORDER BY d.day_number, da.order_index
+ `, tripId);
+
+ const days = new Map();
+ for (const stop of stops) {
+ let day = days.get(stop.day_number);
+ if (!day) {
+ day = { dayNumber: stop.day_number, date: stop.date, title: stop.title, points: [] };
+ days.set(stop.day_number, day);
+ }
+ day.points.push({ name: stop.name, lat: stop.lat, lng: stop.lng });
+ }
+
+ const gpx = buildGpx({ tripTitle: trip.title, places, days: [...days.values()] }, opts);
+ return gpx ? { gpx, filename: gpxFilename(trip.title) } : null;
+ }
+
private importGpxRows(tripId: string, fileBuffer: Buffer, opts: GpxImportOptions = {}): GpxImportResult | null {
const { importWaypoints = true, importRoutes = true, importTracks = true, defaultName } = opts;
diff --git a/server/tests/e2e/places.e2e.test.ts b/server/tests/e2e/places.e2e.test.ts
index 0978166f9..a85bb26ea 100644
--- a/server/tests/e2e/places.e2e.test.ts
+++ b/server/tests/e2e/places.e2e.test.ts
@@ -38,7 +38,9 @@ const { db } = vi.hoisted(() => {
tmp.exec(`CREATE TABLE place_ratings (place_id INTEGER NOT NULL, user_id INTEGER NOT NULL, rating INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(place_id, user_id));`);
// The assignment=unassigned/assigned filters join these.
- tmp.exec('CREATE TABLE days (id INTEGER PRIMARY KEY AUTOINCREMENT, trip_id INTEGER NOT NULL, day_number INTEGER, date TEXT);');
+ tmp.exec('CREATE TABLE days (id INTEGER PRIMARY KEY AUTOINCREMENT, trip_id INTEGER NOT NULL, day_number INTEGER, date TEXT, title TEXT);');
+ // The GPX export reads the trip title for and the filename.
+ tmp.exec('CREATE TABLE trips (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT);');
tmp.exec(`CREATE TABLE day_assignments (id INTEGER PRIMARY KEY AUTOINCREMENT, day_id INTEGER NOT NULL,
place_id INTEGER NOT NULL, order_index INTEGER DEFAULT 0);`);
// reclaimPlaceImage ref-counts an uploaded thumbnail across both tables.
@@ -106,7 +108,7 @@ describe('Places e2e (real auth guard + temp SQLite)', () => {
});
beforeEach(() => {
- db.exec('DELETE FROM places; DELETE FROM place_tags; DELETE FROM place_ratings; DELETE FROM day_assignments; DELETE FROM days;');
+ db.exec('DELETE FROM trips; DELETE FROM places; DELETE FROM place_tags; DELETE FROM place_ratings; DELETE FROM day_assignments; DELETE FROM days;');
canAccessTrip.mockReturnValue({ id: 5, user_id: 1 });
checkPermission.mockReturnValue(true);
});
@@ -292,6 +294,76 @@ describe('Places e2e (real auth guard + temp SQLite)', () => {
expect(del.body).toEqual({ error: 'Trip not found' });
});
+ // ── GPX export ────────────────────────────────────────────────────────────
+ describe('GET export.gpx (#1442)', () => {
+ const seedTrip = () => {
+ db.prepare("INSERT INTO trips (id, title) VALUES (5, 'Alpine week')").run();
+ db.prepare("INSERT INTO places (id, trip_id, name, lat, lng) VALUES (1, 5, 'Trailhead', 47.1, 11.2)").run();
+ db.prepare("INSERT INTO places (id, trip_id, name, lat, lng, route_geometry) VALUES (2, 5, 'Ridge', 47.2, 11.3, '[[47.2,11.3],[47.25,11.35]]')").run();
+ db.prepare("INSERT INTO days (id, trip_id, day_number, date, title) VALUES (1, 5, 1, '2026-05-01', 'Warm up')").run();
+ db.prepare('INSERT INTO day_assignments (day_id, place_id, order_index) VALUES (1, 1, 0), (1, 2, 1)').run();
+ };
+
+ it('serves the trip as an attachment named after it', async () => {
+ seedTrip();
+ const res = await request(server).get('/api/trips/5/places/export.gpx').set('Cookie', sessionCookie(1));
+
+ expect(res.status).toBe(200);
+ expect(res.headers['content-type']).toContain('application/gpx+xml');
+ expect(res.headers['content-disposition']).toBe('attachment; filename="Alpine-week.gpx"');
+ expect(res.text).toContain('Trailhead');
+ // Self-closing here: these points carry no elevation.
+ expect(res.text).toContain('1. Warm up');
+ });
+
+ it('narrows the document to the requested parts', async () => {
+ seedTrip();
+ const res = await request(server)
+ .get('/api/trips/5/places/export.gpx?waypoints=false&dayRoutes=false')
+ .set('Cookie', sessionCookie(1));
+
+ expect(res.status).toBe(200);
+ expect(res.text).toContain('');
+ expect(res.text).not.toContain('');
+ });
+
+ it('400s when every part was switched off', async () => {
+ seedTrip();
+ const res = await request(server)
+ .get('/api/trips/5/places/export.gpx?waypoints=false&tracks=false&dayRoutes=false')
+ .set('Cookie', sessionCookie(1));
+
+ expect(res.status).toBe(400);
+ expect(res.body).toEqual({ error: 'No export types selected' });
+ });
+
+ it('404s an empty trip rather than handing over a file that imports as nothing', async () => {
+ db.prepare("INSERT INTO trips (id, title) VALUES (5, 'Nothing here')").run();
+ const res = await request(server).get('/api/trips/5/places/export.gpx').set('Cookie', sessionCookie(1));
+ expect(res.status).toBe(404);
+ expect(res.body).toEqual({ error: 'Nothing to export' });
+ });
+
+ it('404s a trip the caller cannot reach, and 401s without a cookie', async () => {
+ seedTrip();
+ canAccessTrip.mockReturnValue(undefined);
+ const res = await request(server).get('/api/trips/5/places/export.gpx').set('Cookie', sessionCookie(1));
+ expect(res.status).toBe(404);
+ expect(res.body).toEqual({ error: 'Trip not found' });
+
+ expect((await request(server).get('/api/trips/5/places/export.gpx')).status).toBe(401);
+ });
+
+ it('is a read: no place_edit permission required', async () => {
+ seedTrip();
+ checkPermission.mockReturnValue(false);
+ const res = await request(server).get('/api/trips/5/places/export.gpx').set('Cookie', sessionCookie(1));
+ expect(res.status).toBe(200);
+ });
+ });
+
// The reason places keeps its inline checks on the write routes: a guard runs
// before the pipe, so guarding create would answer 404 where the suite above
// pins a 400. This is the non-regression pin for that decision.
diff --git a/server/tests/unit/nest/gpx-export.helpers.test.ts b/server/tests/unit/nest/gpx-export.helpers.test.ts
new file mode 100644
index 000000000..5becba73c
--- /dev/null
+++ b/server/tests/unit/nest/gpx-export.helpers.test.ts
@@ -0,0 +1,177 @@
+/**
+ * GPX writer unit tests. The builder is pure, so everything here runs without a
+ * DB or a Nest container. The last block parses its own output back with the very
+ * parser the importer uses, which is the closest thing to "a real reader can open
+ * this" that does not need a device.
+ */
+import { describe, it, expect } from 'vitest';
+import { XMLParser, XMLValidator } from 'fast-xml-parser';
+import { buildGpx, gpxFilename, type GpxExportPlace, type GpxExportDay } from '../../../src/nest/places/gpx-export.helpers';
+
+const place = (over: Partial = {}): GpxExportPlace => ({
+ name: 'Somewhere', description: null, address: null, lat: 48.8566, lng: 2.3522,
+ route_geometry: null, category: null, ...over,
+});
+
+const day = (over: Partial = {}): GpxExportDay => ({
+ dayNumber: 1, date: '2026-05-01', title: null, points: [], ...over,
+});
+
+const parser = new XMLParser({
+ ignoreAttributes: false,
+ attributeNamePrefix: '@_',
+ isArray: (name) => ['wpt', 'trkpt', 'rtept', 'trk', 'trkseg', 'rte'].includes(name),
+});
+
+describe('buildGpx', () => {
+ it('writes a place without geometry as a waypoint, with its category as ', () => {
+ const gpx = buildGpx({
+ tripTitle: 'Paris',
+ places: [place({ name: 'Louvre', description: 'Go early', address: 'Rue de Rivoli', category: 'Museum' })],
+ days: [],
+ })!;
+
+ expect(gpx).toContain('');
+ expect(gpx).toContain('Louvre');
+ expect(gpx).toContain('Museum');
+ // Description first, address after: a device usually shows only the first line.
+ expect(gpx).toContain('Go early, Rue de Rivoli');
+ });
+
+ it('writes a place carrying geometry as a track, elevation included', () => {
+ const gpx = buildGpx({
+ tripTitle: 'Hike',
+ places: [place({ name: 'Ridge', route_geometry: JSON.stringify([[47.1, 11.2, 1830], [47.11, 11.21, 1902]]) })],
+ days: [],
+ })!;
+
+ expect(gpx).toContain('');
+ expect(gpx).toContain('1830');
+ expect(gpx).toContain('1902');
+ });
+
+ it('does not also drop a waypoint on the start of a track', () => {
+ const gpx = buildGpx({
+ tripTitle: 'Hike',
+ places: [place({ name: 'Ridge', route_geometry: JSON.stringify([[47.1, 11.2], [47.11, 11.21]]) })],
+ days: [],
+ })!;
+
+ expect(gpx).not.toContain(' {
+ const gpx = buildGpx({
+ tripTitle: 'Rome',
+ places: [],
+ days: [day({
+ dayNumber: 2, date: '2026-05-02', title: 'Ancient city',
+ points: [
+ { name: 'Colosseum', lat: 41.89, lng: 12.49 },
+ { name: 'Forum', lat: 41.892, lng: 12.485 },
+ ],
+ })],
+ })!;
+
+ expect(gpx).toContain('2. Ancient city');
+ const order = [gpx.indexOf('Colosseum'), gpx.indexOf('Forum')];
+ expect(order[0]).toBeLessThan(order[1]);
+ });
+
+ it('falls back to the date, then the bare number, when a day has no title', () => {
+ const dated = buildGpx({ tripTitle: 'T', places: [], days: [day({ dayNumber: 3, points: [{ name: 'A', lat: 1, lng: 1 }, { name: 'B', lat: 2, lng: 2 }] })] })!;
+ expect(dated).toContain('3. 2026-05-01');
+
+ const bare = buildGpx({ tripTitle: 'T', places: [], days: [day({ dayNumber: 4, date: null, points: [{ name: 'A', lat: 1, lng: 1 }, { name: 'B', lat: 2, lng: 2 }] })] })!;
+ expect(bare).toContain('4');
+ });
+
+ it('skips a day with a single stop — one point is not a route', () => {
+ const gpx = buildGpx({
+ tripTitle: 'T',
+ places: [place()],
+ days: [day({ points: [{ name: 'Only stop', lat: 1, lng: 1 }] })],
+ })!;
+
+ expect(gpx).not.toContain('');
+ });
+
+ it('honours each flag on its own', () => {
+ const input = {
+ tripTitle: 'T',
+ places: [place({ name: 'Point' }), place({ name: 'Line', route_geometry: JSON.stringify([[1, 1], [2, 2]]) })],
+ days: [day({ points: [{ name: 'A', lat: 1, lng: 1 }, { name: 'B', lat: 2, lng: 2 }] })],
+ };
+
+ const noWpt = buildGpx(input, { waypoints: false })!;
+ expect(noWpt).not.toContain('');
+
+ const noTrk = buildGpx(input, { tracks: false })!;
+ expect(noTrk).not.toContain('');
+ // With tracks off, the geometry place still has coordinates, so it lands as a waypoint.
+ expect(noTrk).toContain('Line');
+
+ const noRoutes = buildGpx(input, { dayRoutes: false })!;
+ expect(noRoutes).not.toContain('');
+ });
+
+ it('returns null when the selection produced nothing', () => {
+ expect(buildGpx({ tripTitle: 'Empty', places: [], days: [] })).toBeNull();
+ expect(buildGpx({ tripTitle: 'T', places: [place({ lat: null, lng: null })], days: [] })).toBeNull();
+ expect(buildGpx({ tripTitle: 'T', places: [place()], days: [] }, { waypoints: false })).toBeNull();
+ });
+
+ it('ignores geometry that is not a usable coordinate list', () => {
+ const garbage = ['not json', '{}', '[]', JSON.stringify([['a', 'b']]), JSON.stringify([[1]])];
+ for (const route_geometry of garbage) {
+ // Falls through to the waypoint branch rather than emitting an empty .
+ const gpx = buildGpx({ tripTitle: 'T', places: [place({ route_geometry })], days: [] })!;
+ expect(gpx).not.toContain('');
+ expect(gpx).toContain(' {
+ const gpx = buildGpx({
+ tripTitle: 'Round trip',
+ places: [
+ place({ name: 'Waypoint', lat: 10.5, lng: -3.25 }),
+ place({ name: 'Track', route_geometry: JSON.stringify([[1.5, 2.5], [1.6, 2.6]]) }),
+ ],
+ days: [day({ points: [{ name: 'A', lat: 3, lng: 4 }, { name: 'B', lat: 5, lng: 6 }] })],
+ })!;
+
+ expect(XMLValidator.validate(gpx)).toBe(true);
+
+ const parsed = parser.parse(gpx).gpx;
+ expect(parsed['@_version']).toBe('1.1');
+ expect(parsed.metadata.name).toBe('Round trip');
+ expect(parsed.wpt).toHaveLength(1);
+ // Attributes come back as strings — the importer runs them through parseFloat too.
+ expect(parsed.wpt[0]['@_lat']).toBe('10.5');
+ expect(parsed.trk[0].trkseg[0].trkpt).toHaveLength(2);
+ expect(parsed.rte[0].rtept).toHaveLength(2);
+ });
+
+ it('escapes markup in names instead of breaking the document', () => {
+ const gpx = buildGpx({ tripTitle: 'T', places: [place({ name: 'Bar & Grill ' })], days: [] })!;
+ expect(XMLValidator.validate(gpx)).toBe(true);
+ expect(parser.parse(gpx).gpx.wpt[0].name).toBe('Bar & Grill ');
+ });
+});
+
+describe('gpxFilename', () => {
+ it('keeps a plain title readable', () => {
+ expect(gpxFilename('Berlin 2026')).toBe('Berlin-2026.gpx');
+ });
+
+ it('drops what a filesystem or a header would choke on', () => {
+ expect(gpxFilename('Trip: "Rome"/2026 ')).toBe('Trip-Rome-2026-draft.gpx');
+ });
+
+ it('falls back when a title leaves nothing behind', () => {
+ expect(gpxFilename('///')).toBe('trip.gpx');
+ expect(gpxFilename('')).toBe('trip.gpx');
+ });
+});
diff --git a/shared/src/i18n/ar/dayplan.ts b/shared/src/i18n/ar/dayplan.ts
index 41bc29e59..522ba5a26 100644
--- a/shared/src/i18n/ar/dayplan.ts
+++ b/shared/src/i18n/ar/dayplan.ts
@@ -58,6 +58,12 @@ const dayplan: TranslationStrings = {
'dayplan.confirmed': 'مؤكد',
'dayplan.pendingRes': 'قيد الانتظار',
'dayplan.pdfTooltip': 'تصدير خطة اليوم بصيغة PDF',
+ 'dayplan.gpxTooltip': 'تصدير كملف GPX للخرائط دون اتصال وأجهزة GPS',
+ 'dayplan.gpxAll': 'الرحلة كاملة',
+ 'dayplan.gpxPlaces': 'الأماكن فقط',
+ 'dayplan.gpxDays': 'الأيام كمسارات',
+ 'dayplan.gpxEmpty': 'لا يوجد شيء للتصدير بعد',
+ 'dayplan.gpxFailed': 'فشل تصدير GPX',
'dayplan.pdfError': 'فشل تصدير PDF',
'dayplan.expandAll': 'Expand all days', // en-fallback
'dayplan.collapseAll': 'Collapse all days', // en-fallback
diff --git a/shared/src/i18n/ar/mobileTrip.ts b/shared/src/i18n/ar/mobileTrip.ts
index 63ae892d9..0a8505e65 100644
--- a/shared/src/i18n/ar/mobileTrip.ts
+++ b/shared/src/i18n/ar/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'هذه الميزة معطّلة لهذه الرحلة',
'mobileTrip.compactView': 'عرض مضغوط',
'mobileTrip.dayTitlePlaceholder': 'عنوان اليوم',
- 'mobileTrip.exportCalendar': 'تصدير التقويم',
+ 'mobileTrip.export': 'تصدير',
'mobileTrip.filesEmpty': 'لا توجد ملفات بعد',
'mobileTrip.filesNext': 'الملف التالي',
'mobileTrip.filesPrev': 'الملف السابق',
diff --git a/shared/src/i18n/br/dayplan.ts b/shared/src/i18n/br/dayplan.ts
index 0672d3a3d..a532ac796 100644
--- a/shared/src/i18n/br/dayplan.ts
+++ b/shared/src/i18n/br/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Pendente',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exportar plano do dia em PDF',
+ 'dayplan.gpxTooltip': 'Exportar como GPX para mapas offline e aparelhos de GPS',
+ 'dayplan.gpxAll': 'Viagem inteira',
+ 'dayplan.gpxPlaces': 'Somente lugares',
+ 'dayplan.gpxDays': 'Dias como rotas',
+ 'dayplan.gpxEmpty': 'Ainda não há nada para exportar',
+ 'dayplan.gpxFailed': 'Falha ao exportar o GPX',
'dayplan.pdfError': 'Falha ao exportar PDF',
'dayplan.cannotReorderTransport': 'Reservas com horário fixo não podem ser reordenadas',
'dayplan.confirmRemoveTimeTitle': 'Remover horário?',
diff --git a/shared/src/i18n/br/mobileTrip.ts b/shared/src/i18n/br/mobileTrip.ts
index 9ca5118bc..c39491169 100644
--- a/shared/src/i18n/br/mobileTrip.ts
+++ b/shared/src/i18n/br/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Esse recurso está desativado para esta viagem',
'mobileTrip.compactView': 'Visualização compacta',
'mobileTrip.dayTitlePlaceholder': 'Título do dia',
- 'mobileTrip.exportCalendar': 'Exportar calendário',
+ 'mobileTrip.export': 'Exportar',
'mobileTrip.filesEmpty': 'Nenhum arquivo ainda',
'mobileTrip.filesNext': 'Próximo arquivo',
'mobileTrip.filesPrev': 'Arquivo anterior',
diff --git a/shared/src/i18n/ca/dayplan.ts b/shared/src/i18n/ca/dayplan.ts
index 309398a81..99ff78b63 100644
--- a/shared/src/i18n/ca/dayplan.ts
+++ b/shared/src/i18n/ca/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Pendent',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exporta el pla diari com a PDF',
+ 'dayplan.gpxTooltip': 'Exporta com a GPX per a mapes fora de línia i dispositius GPS',
+ 'dayplan.gpxAll': 'Tot el viatge',
+ 'dayplan.gpxPlaces': 'Només els llocs',
+ 'dayplan.gpxDays': 'Dies com a rutes',
+ 'dayplan.gpxEmpty': 'Encara no hi ha res per exportar',
+ 'dayplan.gpxFailed': 'L\'exportació GPX ha fallat',
'dayplan.pdfError': "No s'ha pogut exportar el PDF",
'dayplan.cannotReorderTransport': 'Les reserves amb hora fixa no es poden reordenar',
'dayplan.confirmRemoveTimeTitle': "Vols eliminar l'hora?",
diff --git a/shared/src/i18n/ca/mobileTrip.ts b/shared/src/i18n/ca/mobileTrip.ts
index da9ca2fea..0edad17b6 100644
--- a/shared/src/i18n/ca/mobileTrip.ts
+++ b/shared/src/i18n/ca/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Aquesta funció està desactivada per a aquest viatge',
'mobileTrip.compactView': 'Vista compacta',
'mobileTrip.dayTitlePlaceholder': 'Títol del dia',
- 'mobileTrip.exportCalendar': 'Exporta el calendari',
+ 'mobileTrip.export': 'Exporta',
'mobileTrip.filesEmpty': 'Encara no hi ha fitxers',
'mobileTrip.filesNext': 'Fitxer següent',
'mobileTrip.filesPrev': 'Fitxer anterior',
diff --git a/shared/src/i18n/cs/dayplan.ts b/shared/src/i18n/cs/dayplan.ts
index f590a2ef2..aaf1e931f 100644
--- a/shared/src/i18n/cs/dayplan.ts
+++ b/shared/src/i18n/cs/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Čeká na potvrzení',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exportovat denní plán do PDF',
+ 'dayplan.gpxTooltip': 'Exportovat jako GPX pro offline mapy a GPS zařízení',
+ 'dayplan.gpxAll': 'Celá cesta',
+ 'dayplan.gpxPlaces': 'Jen místa',
+ 'dayplan.gpxDays': 'Dny jako trasy',
+ 'dayplan.gpxEmpty': 'Zatím není co exportovat',
+ 'dayplan.gpxFailed': 'Export GPX selhal',
'dayplan.pdfError': 'Export do PDF se nezdařil',
'dayplan.cannotReorderTransport': 'Rezervace s pevným časem nelze přeuspořádat',
'dayplan.confirmRemoveTimeTitle': 'Odebrat čas?',
diff --git a/shared/src/i18n/cs/mobileTrip.ts b/shared/src/i18n/cs/mobileTrip.ts
index ab6021efb..26669f308 100644
--- a/shared/src/i18n/cs/mobileTrip.ts
+++ b/shared/src/i18n/cs/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Tato funkce je pro tento výlet vypnuta',
'mobileTrip.compactView': 'Kompaktní zobrazení',
'mobileTrip.dayTitlePlaceholder': 'Název dne',
- 'mobileTrip.exportCalendar': 'Export kalendáře',
+ 'mobileTrip.export': 'Exportovat',
'mobileTrip.filesEmpty': 'Zatím žádné soubory',
'mobileTrip.filesNext': 'Další soubor',
'mobileTrip.filesPrev': 'Předchozí soubor',
diff --git a/shared/src/i18n/de/dayplan.ts b/shared/src/i18n/de/dayplan.ts
index fa9667db1..14f461a5b 100644
--- a/shared/src/i18n/de/dayplan.ts
+++ b/shared/src/i18n/de/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Ausstehend',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Tagesplan als PDF exportieren',
+ 'dayplan.gpxTooltip': 'Als GPX für Offline-Karten und GPS-Geräte exportieren',
+ 'dayplan.gpxAll': 'Ganze Reise',
+ 'dayplan.gpxPlaces': 'Nur Orte',
+ 'dayplan.gpxDays': 'Tage als Routen',
+ 'dayplan.gpxEmpty': 'Es gibt noch nichts zu exportieren',
+ 'dayplan.gpxFailed': 'GPX-Export fehlgeschlagen',
'dayplan.pdfError': 'Fehler beim PDF-Export',
'dayplan.mobile.addPlace': 'Ort hinzufügen',
'dayplan.mobile.searchPlaces': 'Orte suchen...',
diff --git a/shared/src/i18n/de/mobileTrip.ts b/shared/src/i18n/de/mobileTrip.ts
index 40746694a..5f694c44b 100644
--- a/shared/src/i18n/de/mobileTrip.ts
+++ b/shared/src/i18n/de/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Diese Funktion ist für diese Reise deaktiviert',
'mobileTrip.compactView': 'Kompakte Ansicht',
'mobileTrip.dayTitlePlaceholder': 'Tagestitel',
- 'mobileTrip.exportCalendar': 'Kalender exportieren',
+ 'mobileTrip.export': 'Exportieren',
'mobileTrip.filesEmpty': 'Keine Dateien vorhanden',
'mobileTrip.filesNext': 'Nächste Datei',
'mobileTrip.filesPrev': 'Vorherige Datei',
diff --git a/shared/src/i18n/en/dayplan.ts b/shared/src/i18n/en/dayplan.ts
index 56176aac7..e2b1d5699 100644
--- a/shared/src/i18n/en/dayplan.ts
+++ b/shared/src/i18n/en/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Pending',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Export day plan as PDF',
+ 'dayplan.gpxTooltip': 'Export as GPX for offline maps and GPS devices',
+ 'dayplan.gpxAll': 'Whole trip',
+ 'dayplan.gpxPlaces': 'Places only',
+ 'dayplan.gpxDays': 'Days as routes',
+ 'dayplan.gpxEmpty': 'There is nothing to export yet',
+ 'dayplan.gpxFailed': 'GPX export failed',
'dayplan.pdfError': 'Failed to export PDF',
'dayplan.mobile.addPlace': 'Add Place',
'dayplan.mobile.searchPlaces': 'Search places...',
diff --git a/shared/src/i18n/en/mobileTrip.ts b/shared/src/i18n/en/mobileTrip.ts
index b7defc2e1..ad053c234 100644
--- a/shared/src/i18n/en/mobileTrip.ts
+++ b/shared/src/i18n/en/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'This feature is disabled for this trip',
'mobileTrip.compactView': 'Compact view',
'mobileTrip.dayTitlePlaceholder': 'Day title',
- 'mobileTrip.exportCalendar': 'Export Calendar',
+ 'mobileTrip.export': 'Export',
'mobileTrip.filesEmpty': 'No files yet',
'mobileTrip.filesNext': 'Next file',
'mobileTrip.filesPrev': 'Previous file',
diff --git a/shared/src/i18n/es/dayplan.ts b/shared/src/i18n/es/dayplan.ts
index f57377228..0e652d7a9 100644
--- a/shared/src/i18n/es/dayplan.ts
+++ b/shared/src/i18n/es/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Pendiente',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exportar plan diario como PDF',
+ 'dayplan.gpxTooltip': 'Exportar como GPX para mapas sin conexión y dispositivos GPS',
+ 'dayplan.gpxAll': 'Todo el viaje',
+ 'dayplan.gpxPlaces': 'Solo lugares',
+ 'dayplan.gpxDays': 'Días como rutas',
+ 'dayplan.gpxEmpty': 'Aún no hay nada que exportar',
+ 'dayplan.gpxFailed': 'Error al exportar el GPX',
'dayplan.pdfError': 'No se pudo exportar el PDF',
'dayplan.cannotReorderTransport': 'Las reservas con hora fija no se pueden reordenar',
'dayplan.confirmRemoveTimeTitle': '¿Eliminar hora?',
diff --git a/shared/src/i18n/es/mobileTrip.ts b/shared/src/i18n/es/mobileTrip.ts
index 85eda8b7f..3f1e31ed0 100644
--- a/shared/src/i18n/es/mobileTrip.ts
+++ b/shared/src/i18n/es/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Esta función está desactivada para este viaje',
'mobileTrip.compactView': 'Vista compacta',
'mobileTrip.dayTitlePlaceholder': 'Título del día',
- 'mobileTrip.exportCalendar': 'Exportar calendario',
+ 'mobileTrip.export': 'Exportar',
'mobileTrip.filesEmpty': 'Aún no hay archivos',
'mobileTrip.filesNext': 'Archivo siguiente',
'mobileTrip.filesPrev': 'Archivo anterior',
diff --git a/shared/src/i18n/fr/dayplan.ts b/shared/src/i18n/fr/dayplan.ts
index 51cdfaa6d..30961ac4b 100644
--- a/shared/src/i18n/fr/dayplan.ts
+++ b/shared/src/i18n/fr/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'En attente',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exporter le plan du jour en PDF',
+ 'dayplan.gpxTooltip': 'Exporter en GPX pour les cartes hors ligne et les GPS',
+ 'dayplan.gpxAll': 'Tout le voyage',
+ 'dayplan.gpxPlaces': 'Lieux uniquement',
+ 'dayplan.gpxDays': 'Journées en itinéraires',
+ 'dayplan.gpxEmpty': 'Il n\'y a encore rien à exporter',
+ 'dayplan.gpxFailed': 'Échec de l\'export GPX',
'dayplan.pdfError': "Échec de l'export PDF",
'dayplan.cannotReorderTransport': 'Les réservations avec une heure fixe ne peuvent pas être réorganisées',
'dayplan.confirmRemoveTimeTitle': "Supprimer l'heure ?",
diff --git a/shared/src/i18n/fr/mobileTrip.ts b/shared/src/i18n/fr/mobileTrip.ts
index e3e14c98d..7ff8e14ab 100644
--- a/shared/src/i18n/fr/mobileTrip.ts
+++ b/shared/src/i18n/fr/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Cette fonctionnalité est désactivée pour ce voyage',
'mobileTrip.compactView': 'Vue compacte',
'mobileTrip.dayTitlePlaceholder': 'Titre du jour',
- 'mobileTrip.exportCalendar': 'Exporter le calendrier',
+ 'mobileTrip.export': 'Exporter',
'mobileTrip.filesEmpty': 'Aucun fichier',
'mobileTrip.filesNext': 'Fichier suivant',
'mobileTrip.filesPrev': 'Fichier précédent',
diff --git a/shared/src/i18n/gr/dayplan.ts b/shared/src/i18n/gr/dayplan.ts
index 84b682f32..0a985ff0f 100644
--- a/shared/src/i18n/gr/dayplan.ts
+++ b/shared/src/i18n/gr/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Εκκρεμές',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Εξαγωγή ημερήσιου πλάνου ως PDF',
+ 'dayplan.gpxTooltip': 'Εξαγωγή ως GPX για χάρτες εκτός σύνδεσης και συσκευές GPS',
+ 'dayplan.gpxAll': 'Όλο το ταξίδι',
+ 'dayplan.gpxPlaces': 'Μόνο τα μέρη',
+ 'dayplan.gpxDays': 'Ημέρες ως διαδρομές',
+ 'dayplan.gpxEmpty': 'Δεν υπάρχει ακόμη κάτι για εξαγωγή',
+ 'dayplan.gpxFailed': 'Η εξαγωγή GPX απέτυχε',
'dayplan.pdfError': 'Αποτυχία εξαγωγής PDF',
'dayplan.mobile.addPlace': 'Προσθήκη Τοποθεσίας',
'dayplan.mobile.searchPlaces': 'Αναζήτηση τοποθεσιών...',
diff --git a/shared/src/i18n/gr/mobileTrip.ts b/shared/src/i18n/gr/mobileTrip.ts
index 2297ca6eb..1630ade89 100644
--- a/shared/src/i18n/gr/mobileTrip.ts
+++ b/shared/src/i18n/gr/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Αυτή η λειτουργία είναι απενεργοποιημένη για αυτό το ταξίδι',
'mobileTrip.compactView': 'Συμπαγής προβολή',
'mobileTrip.dayTitlePlaceholder': 'Τίτλος ημέρας',
- 'mobileTrip.exportCalendar': 'Εξαγωγή Ημερολογίου',
+ 'mobileTrip.export': 'Εξαγωγή',
'mobileTrip.filesEmpty': 'Δεν υπάρχουν αρχεία ακόμη',
'mobileTrip.filesNext': 'Επόμενο αρχείο',
'mobileTrip.filesPrev': 'Προηγούμενο αρχείο',
diff --git a/shared/src/i18n/hu/dayplan.ts b/shared/src/i18n/hu/dayplan.ts
index fef066cab..a3996e340 100644
--- a/shared/src/i18n/hu/dayplan.ts
+++ b/shared/src/i18n/hu/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Függőben',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Napi terv exportálása PDF-be',
+ 'dayplan.gpxTooltip': 'Exportálás GPX-ként offline térképekhez és GPS-eszközökhöz',
+ 'dayplan.gpxAll': 'A teljes utazás',
+ 'dayplan.gpxPlaces': 'Csak a helyek',
+ 'dayplan.gpxDays': 'Napok útvonalként',
+ 'dayplan.gpxEmpty': 'Még nincs mit exportálni',
+ 'dayplan.gpxFailed': 'A GPX-exportálás nem sikerült',
'dayplan.pdfError': 'Nem sikerült a PDF exportálás',
'dayplan.cannotReorderTransport': 'A rögzített időpontú foglalások nem rendezhetők át',
'dayplan.confirmRemoveTimeTitle': 'Időpont eltávolítása?',
diff --git a/shared/src/i18n/hu/mobileTrip.ts b/shared/src/i18n/hu/mobileTrip.ts
index f60900dff..97d1bdbf7 100644
--- a/shared/src/i18n/hu/mobileTrip.ts
+++ b/shared/src/i18n/hu/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Ez a funkció ki van kapcsolva ennél az utazásnál',
'mobileTrip.compactView': 'Kompakt nézet',
'mobileTrip.dayTitlePlaceholder': 'Nap címe',
- 'mobileTrip.exportCalendar': 'Naptár exportálása',
+ 'mobileTrip.export': 'Exportálás',
'mobileTrip.filesEmpty': 'Még nincsenek fájlok',
'mobileTrip.filesNext': 'Következő fájl',
'mobileTrip.filesPrev': 'Előző fájl',
diff --git a/shared/src/i18n/id/dayplan.ts b/shared/src/i18n/id/dayplan.ts
index 584fde494..ad6b3555e 100644
--- a/shared/src/i18n/id/dayplan.ts
+++ b/shared/src/i18n/id/dayplan.ts
@@ -60,6 +60,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Menunggu',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Ekspor rencana hari sebagai PDF',
+ 'dayplan.gpxTooltip': 'Ekspor sebagai GPX untuk peta offline dan perangkat GPS',
+ 'dayplan.gpxAll': 'Seluruh perjalanan',
+ 'dayplan.gpxPlaces': 'Hanya tempat',
+ 'dayplan.gpxDays': 'Hari sebagai rute',
+ 'dayplan.gpxEmpty': 'Belum ada yang bisa diekspor',
+ 'dayplan.gpxFailed': 'Ekspor GPX gagal',
'dayplan.pdfError': 'Gagal mengekspor PDF',
'dayplan.mobile.addPlace': 'Tambah Tempat',
'dayplan.mobile.searchPlaces': 'Cari tempat...',
diff --git a/shared/src/i18n/id/mobileTrip.ts b/shared/src/i18n/id/mobileTrip.ts
index 81d271e64..b3b34c80c 100644
--- a/shared/src/i18n/id/mobileTrip.ts
+++ b/shared/src/i18n/id/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Fitur ini dinonaktifkan untuk perjalanan ini',
'mobileTrip.compactView': 'Tampilan ringkas',
'mobileTrip.dayTitlePlaceholder': 'Judul hari',
- 'mobileTrip.exportCalendar': 'Ekspor Kalender',
+ 'mobileTrip.export': 'Ekspor',
'mobileTrip.filesEmpty': 'Belum ada file',
'mobileTrip.filesNext': 'File berikutnya',
'mobileTrip.filesPrev': 'File sebelumnya',
diff --git a/shared/src/i18n/it/dayplan.ts b/shared/src/i18n/it/dayplan.ts
index 3956f1778..dbde95b38 100644
--- a/shared/src/i18n/it/dayplan.ts
+++ b/shared/src/i18n/it/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'In attesa',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Esporta il programma del giorno come PDF',
+ 'dayplan.gpxTooltip': 'Esporta in GPX per mappe offline e dispositivi GPS',
+ 'dayplan.gpxAll': 'Tutto il viaggio',
+ 'dayplan.gpxPlaces': 'Solo i luoghi',
+ 'dayplan.gpxDays': 'Giorni come itinerari',
+ 'dayplan.gpxEmpty': 'Non c\'è ancora nulla da esportare',
+ 'dayplan.gpxFailed': 'Esportazione GPX non riuscita',
'dayplan.pdfError': 'Impossibile esportare il PDF',
'dayplan.cannotReorderTransport': 'Le prenotazioni con un orario fisso non possono essere riordinate',
'dayplan.confirmRemoveTimeTitle': "Rimuovere l'orario?",
diff --git a/shared/src/i18n/it/mobileTrip.ts b/shared/src/i18n/it/mobileTrip.ts
index 8078dcb58..b43cc4b1f 100644
--- a/shared/src/i18n/it/mobileTrip.ts
+++ b/shared/src/i18n/it/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Questa funzione è disattivata per questo viaggio',
'mobileTrip.compactView': 'Vista compatta',
'mobileTrip.dayTitlePlaceholder': 'Titolo del giorno',
- 'mobileTrip.exportCalendar': 'Esporta calendario',
+ 'mobileTrip.export': 'Esporta',
'mobileTrip.filesEmpty': 'Ancora nessun file',
'mobileTrip.filesNext': 'File successivo',
'mobileTrip.filesPrev': 'File precedente',
diff --git a/shared/src/i18n/ja/dayplan.ts b/shared/src/i18n/ja/dayplan.ts
index 7fe19637c..c1eea7d2f 100644
--- a/shared/src/i18n/ja/dayplan.ts
+++ b/shared/src/i18n/ja/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': '保留',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': '日別計画をPDFで書き出し',
+ 'dayplan.gpxTooltip': 'オフライン地図やGPS機器向けにGPXで書き出す',
+ 'dayplan.gpxAll': '旅行全体',
+ 'dayplan.gpxPlaces': '場所のみ',
+ 'dayplan.gpxDays': '日程をルートとして',
+ 'dayplan.gpxEmpty': 'まだ書き出せるものがありません',
+ 'dayplan.gpxFailed': 'GPXの書き出しに失敗しました',
'dayplan.pdfError': 'PDFの書き出しに失敗しました',
'dayplan.mobile.addPlace': '場所を追加',
'dayplan.mobile.searchPlaces': '場所を検索…',
diff --git a/shared/src/i18n/ja/mobileTrip.ts b/shared/src/i18n/ja/mobileTrip.ts
index 3625e983e..f8901bc06 100644
--- a/shared/src/i18n/ja/mobileTrip.ts
+++ b/shared/src/i18n/ja/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'この機能はこの旅行では無効になっています',
'mobileTrip.compactView': 'コンパクト表示',
'mobileTrip.dayTitlePlaceholder': '日のタイトル',
- 'mobileTrip.exportCalendar': 'カレンダーをエクスポート',
+ 'mobileTrip.export': '書き出し',
'mobileTrip.filesEmpty': 'ファイルはまだありません',
'mobileTrip.filesNext': '次のファイル',
'mobileTrip.filesPrev': '前のファイル',
diff --git a/shared/src/i18n/ko/dayplan.ts b/shared/src/i18n/ko/dayplan.ts
index d3009eaf3..598228e0e 100644
--- a/shared/src/i18n/ko/dayplan.ts
+++ b/shared/src/i18n/ko/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': '대기 중',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': '일별 계획을 PDF로 내보내기',
+ 'dayplan.gpxTooltip': '오프라인 지도와 GPS 기기용 GPX로 내보내기',
+ 'dayplan.gpxAll': '여행 전체',
+ 'dayplan.gpxPlaces': '장소만',
+ 'dayplan.gpxDays': '날짜를 경로로',
+ 'dayplan.gpxEmpty': '아직 내보낼 것이 없습니다',
+ 'dayplan.gpxFailed': 'GPX 내보내기에 실패했습니다',
'dayplan.pdfError': 'PDF 내보내기 실패',
'dayplan.mobile.addPlace': '장소 추가',
'dayplan.mobile.searchPlaces': '장소 검색...',
diff --git a/shared/src/i18n/ko/mobileTrip.ts b/shared/src/i18n/ko/mobileTrip.ts
index 0fd0b7114..01f941f9b 100644
--- a/shared/src/i18n/ko/mobileTrip.ts
+++ b/shared/src/i18n/ko/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': '이 여행에서는 이 기능이 비활성화되어 있습니다',
'mobileTrip.compactView': '간략히 보기',
'mobileTrip.dayTitlePlaceholder': '일차 제목',
- 'mobileTrip.exportCalendar': '캘린더 내보내기',
+ 'mobileTrip.export': '내보내기',
'mobileTrip.filesEmpty': '아직 파일이 없습니다',
'mobileTrip.filesNext': '다음 파일',
'mobileTrip.filesPrev': '이전 파일',
diff --git a/shared/src/i18n/nl/dayplan.ts b/shared/src/i18n/nl/dayplan.ts
index 57f1150b4..564928bd5 100644
--- a/shared/src/i18n/nl/dayplan.ts
+++ b/shared/src/i18n/nl/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'In behandeling',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Dagplan exporteren als PDF',
+ 'dayplan.gpxTooltip': 'Exporteren als GPX voor offline kaarten en gps-apparaten',
+ 'dayplan.gpxAll': 'Hele reis',
+ 'dayplan.gpxPlaces': 'Alleen plekken',
+ 'dayplan.gpxDays': 'Dagen als routes',
+ 'dayplan.gpxEmpty': 'Er valt nog niets te exporteren',
+ 'dayplan.gpxFailed': 'GPX-export mislukt',
'dayplan.pdfError': 'PDF-export mislukt',
'dayplan.cannotReorderTransport': 'Boekingen met een vast tijdstip kunnen niet worden verplaatst',
'dayplan.confirmRemoveTimeTitle': 'Tijd verwijderen?',
diff --git a/shared/src/i18n/nl/mobileTrip.ts b/shared/src/i18n/nl/mobileTrip.ts
index 44966c3b1..13c966287 100644
--- a/shared/src/i18n/nl/mobileTrip.ts
+++ b/shared/src/i18n/nl/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Deze functie is uitgeschakeld voor deze reis',
'mobileTrip.compactView': 'Compacte weergave',
'mobileTrip.dayTitlePlaceholder': 'Titel van de dag',
- 'mobileTrip.exportCalendar': 'Kalender exporteren',
+ 'mobileTrip.export': 'Exporteren',
'mobileTrip.filesEmpty': 'Nog geen bestanden',
'mobileTrip.filesNext': 'Volgend bestand',
'mobileTrip.filesPrev': 'Vorig bestand',
diff --git a/shared/src/i18n/pl/dayplan.ts b/shared/src/i18n/pl/dayplan.ts
index 34cd8f95e..cfb968522 100644
--- a/shared/src/i18n/pl/dayplan.ts
+++ b/shared/src/i18n/pl/dayplan.ts
@@ -61,6 +61,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Oczekujące',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Eksportuj plan dnia jako PDF',
+ 'dayplan.gpxTooltip': 'Eksportuj jako GPX do map offline i urządzeń GPS',
+ 'dayplan.gpxAll': 'Cała podróż',
+ 'dayplan.gpxPlaces': 'Tylko miejsca',
+ 'dayplan.gpxDays': 'Dni jako trasy',
+ 'dayplan.gpxEmpty': 'Nie ma jeszcze czego eksportować',
+ 'dayplan.gpxFailed': 'Eksport GPX nie powiódł się',
'dayplan.pdfError': 'Nie udało się wyeksportować pliku PDF',
'dayplan.mobile.addPlace': 'Dodaj miejsce',
'dayplan.mobile.searchPlaces': 'Szukaj miejsc...',
diff --git a/shared/src/i18n/pl/mobileTrip.ts b/shared/src/i18n/pl/mobileTrip.ts
index 25061c51f..edb335c26 100644
--- a/shared/src/i18n/pl/mobileTrip.ts
+++ b/shared/src/i18n/pl/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Ta funkcja jest wyłączona dla tej podróży',
'mobileTrip.compactView': 'Widok kompaktowy',
'mobileTrip.dayTitlePlaceholder': 'Tytuł dnia',
- 'mobileTrip.exportCalendar': 'Eksportuj kalendarz',
+ 'mobileTrip.export': 'Eksportuj',
'mobileTrip.filesEmpty': 'Brak plików',
'mobileTrip.filesNext': 'Następny plik',
'mobileTrip.filesPrev': 'Poprzedni plik',
diff --git a/shared/src/i18n/ru/dayplan.ts b/shared/src/i18n/ru/dayplan.ts
index 925106cf6..c59ad2ef3 100644
--- a/shared/src/i18n/ru/dayplan.ts
+++ b/shared/src/i18n/ru/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Ожидание',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Экспортировать план дня в PDF',
+ 'dayplan.gpxTooltip': 'Экспорт в GPX для офлайн-карт и GPS-устройств',
+ 'dayplan.gpxAll': 'Вся поездка',
+ 'dayplan.gpxPlaces': 'Только места',
+ 'dayplan.gpxDays': 'Дни как маршруты',
+ 'dayplan.gpxEmpty': 'Пока нечего экспортировать',
+ 'dayplan.gpxFailed': 'Не удалось экспортировать GPX',
'dayplan.pdfError': 'Ошибка экспорта PDF',
'dayplan.cannotReorderTransport': 'Бронирования с фиксированным временем нельзя перемещать',
'dayplan.confirmRemoveTimeTitle': 'Удалить время?',
diff --git a/shared/src/i18n/ru/mobileTrip.ts b/shared/src/i18n/ru/mobileTrip.ts
index ac921a028..0b0e5768b 100644
--- a/shared/src/i18n/ru/mobileTrip.ts
+++ b/shared/src/i18n/ru/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Эта функция отключена для этой поездки',
'mobileTrip.compactView': 'Компактный вид',
'mobileTrip.dayTitlePlaceholder': 'Название дня',
- 'mobileTrip.exportCalendar': 'Экспорт календаря',
+ 'mobileTrip.export': 'Экспорт',
'mobileTrip.filesEmpty': 'Файлов пока нет',
'mobileTrip.filesNext': 'Следующий файл',
'mobileTrip.filesPrev': 'Предыдущий файл',
diff --git a/shared/src/i18n/sv/dayplan.ts b/shared/src/i18n/sv/dayplan.ts
index b5673b439..5842c5aa4 100644
--- a/shared/src/i18n/sv/dayplan.ts
+++ b/shared/src/i18n/sv/dayplan.ts
@@ -63,6 +63,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Pendlande',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Exportera dagsplanen som PDF',
+ 'dayplan.gpxTooltip': 'Exportera som GPX för offlinekartor och GPS-enheter',
+ 'dayplan.gpxAll': 'Hela resan',
+ 'dayplan.gpxPlaces': 'Bara platser',
+ 'dayplan.gpxDays': 'Dagar som rutter',
+ 'dayplan.gpxEmpty': 'Det finns inget att exportera än',
+ 'dayplan.gpxFailed': 'GPX-exporten misslyckades',
'dayplan.pdfError': 'Kunde inte exportera PDF',
'dayplan.mobile.addPlace': 'Lägg itll plats',
'dayplan.mobile.searchPlaces': 'Sök platser...',
diff --git a/shared/src/i18n/sv/mobileTrip.ts b/shared/src/i18n/sv/mobileTrip.ts
index 4d376634c..f052a7922 100644
--- a/shared/src/i18n/sv/mobileTrip.ts
+++ b/shared/src/i18n/sv/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Den här funktionen är inaktiverad för den här resan',
'mobileTrip.compactView': 'Kompakt vy',
'mobileTrip.dayTitlePlaceholder': 'Dagens titel',
- 'mobileTrip.exportCalendar': 'Exportera kalender',
+ 'mobileTrip.export': 'Exportera',
'mobileTrip.filesEmpty': 'Inga filer ännu',
'mobileTrip.filesNext': 'Nästa fil',
'mobileTrip.filesPrev': 'Föregående fil',
diff --git a/shared/src/i18n/tr/dayplan.ts b/shared/src/i18n/tr/dayplan.ts
index 86b5d1b01..604b462a8 100644
--- a/shared/src/i18n/tr/dayplan.ts
+++ b/shared/src/i18n/tr/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Beklemede',
'dayplan.pdf': "PDF'ler",
'dayplan.pdfTooltip': 'Gün planını PDF olarak dışa aktar',
+ 'dayplan.gpxTooltip': 'Çevrimdışı haritalar ve GPS cihazları için GPX olarak dışa aktar',
+ 'dayplan.gpxAll': 'Tüm gezi',
+ 'dayplan.gpxPlaces': 'Yalnızca yerler',
+ 'dayplan.gpxDays': 'Günler rota olarak',
+ 'dayplan.gpxEmpty': 'Henüz dışa aktarılacak bir şey yok',
+ 'dayplan.gpxFailed': 'GPX dışa aktarma başarısız oldu',
'dayplan.pdfError': 'PDF dışa aktarılamadı',
'dayplan.mobile.addPlace': 'Yer Ekle',
'dayplan.mobile.searchPlaces': 'Yer ara...',
diff --git a/shared/src/i18n/tr/mobileTrip.ts b/shared/src/i18n/tr/mobileTrip.ts
index 45e22d8a4..a047e1543 100644
--- a/shared/src/i18n/tr/mobileTrip.ts
+++ b/shared/src/i18n/tr/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Bu özellik bu seyahat için devre dışı',
'mobileTrip.compactView': 'Kompakt görünüm',
'mobileTrip.dayTitlePlaceholder': 'Gün başlığı',
- 'mobileTrip.exportCalendar': 'Takvimi Dışa Aktar',
+ 'mobileTrip.export': 'Dışa aktar',
'mobileTrip.filesEmpty': 'Henüz dosya yok',
'mobileTrip.filesNext': 'Sonraki dosya',
'mobileTrip.filesPrev': 'Önceki dosya',
diff --git a/shared/src/i18n/uk/dayplan.ts b/shared/src/i18n/uk/dayplan.ts
index b8d0ddca6..f6af31419 100644
--- a/shared/src/i18n/uk/dayplan.ts
+++ b/shared/src/i18n/uk/dayplan.ts
@@ -53,6 +53,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Ожидание',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Експортувати план дня в PDF',
+ 'dayplan.gpxTooltip': 'Експорт у GPX для офлайн-карт і GPS-пристроїв',
+ 'dayplan.gpxAll': 'Уся подорож',
+ 'dayplan.gpxPlaces': 'Лише місця',
+ 'dayplan.gpxDays': 'Дні як маршрути',
+ 'dayplan.gpxEmpty': 'Поки немає чого експортувати',
+ 'dayplan.gpxFailed': 'Не вдалося експортувати GPX',
'dayplan.pdfError': 'Помилка експорту PDF',
'dayplan.cannotReorderTransport': 'Бронювання з фіксованим часом не можна переміщувати',
'dayplan.confirmRemoveTimeTitle': 'Видалити час?',
diff --git a/shared/src/i18n/uk/mobileTrip.ts b/shared/src/i18n/uk/mobileTrip.ts
index cfb559f01..cec203310 100644
--- a/shared/src/i18n/uk/mobileTrip.ts
+++ b/shared/src/i18n/uk/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Цю функцію вимкнено для цієї подорожі',
'mobileTrip.compactView': 'Компактний вигляд',
'mobileTrip.dayTitlePlaceholder': 'Назва дня',
- 'mobileTrip.exportCalendar': 'Експорт календаря',
+ 'mobileTrip.export': 'Експорт',
'mobileTrip.filesEmpty': 'Файлів поки немає',
'mobileTrip.filesNext': 'Наступний файл',
'mobileTrip.filesPrev': 'Попередній файл',
diff --git a/shared/src/i18n/vi/dayplan.ts b/shared/src/i18n/vi/dayplan.ts
index dbb16bb19..e0cc97c8f 100644
--- a/shared/src/i18n/vi/dayplan.ts
+++ b/shared/src/i18n/vi/dayplan.ts
@@ -62,6 +62,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': 'Chưa giải quyết',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': 'Kế hoạch ngày xuất dưới dạng PDF',
+ 'dayplan.gpxTooltip': 'Xuất ra GPX cho bản đồ ngoại tuyến và thiết bị GPS',
+ 'dayplan.gpxAll': 'Toàn bộ chuyến đi',
+ 'dayplan.gpxPlaces': 'Chỉ địa điểm',
+ 'dayplan.gpxDays': 'Các ngày dưới dạng lộ trình',
+ 'dayplan.gpxEmpty': 'Chưa có gì để xuất',
+ 'dayplan.gpxFailed': 'Xuất GPX thất bại',
'dayplan.pdfError': 'Không xuất được PDF',
'dayplan.mobile.addPlace': 'Thêm địa điểm',
'dayplan.mobile.searchPlaces': 'Tìm kiếm địa điểm...',
diff --git a/shared/src/i18n/vi/mobileTrip.ts b/shared/src/i18n/vi/mobileTrip.ts
index c7c27ca31..3b9e6b764 100644
--- a/shared/src/i18n/vi/mobileTrip.ts
+++ b/shared/src/i18n/vi/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': 'Tính năng này bị tắt cho chuyến đi này',
'mobileTrip.compactView': 'Chế độ xem gọn',
'mobileTrip.dayTitlePlaceholder': 'Tiêu đề ngày',
- 'mobileTrip.exportCalendar': 'Xuất lịch',
+ 'mobileTrip.export': 'Xuất',
'mobileTrip.filesEmpty': 'Chưa có tập tin nào',
'mobileTrip.filesNext': 'Tập tin tiếp theo',
'mobileTrip.filesPrev': 'Tập tin trước',
diff --git a/shared/src/i18n/zh-TW/dayplan.ts b/shared/src/i18n/zh-TW/dayplan.ts
index 745a0ddd8..12f6a0a35 100644
--- a/shared/src/i18n/zh-TW/dayplan.ts
+++ b/shared/src/i18n/zh-TW/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': '待確認',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': '匯出當天計劃為 PDF',
+ 'dayplan.gpxTooltip': '匯出為 GPX,用於離線地圖和 GPS 裝置',
+ 'dayplan.gpxAll': '整趟行程',
+ 'dayplan.gpxPlaces': '僅地點',
+ 'dayplan.gpxDays': '按天產生路線',
+ 'dayplan.gpxEmpty': '暫時沒有可匯出的內容',
+ 'dayplan.gpxFailed': 'GPX 匯出失敗',
'dayplan.pdfError': 'PDF 匯出失敗',
'dayplan.cannotReorderTransport': '有固定時間的預訂無法重新排序',
'dayplan.confirmRemoveTimeTitle': '移除時間?',
diff --git a/shared/src/i18n/zh-TW/mobileTrip.ts b/shared/src/i18n/zh-TW/mobileTrip.ts
index 5b6d03e7e..1b2add723 100644
--- a/shared/src/i18n/zh-TW/mobileTrip.ts
+++ b/shared/src/i18n/zh-TW/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': '此功能已針對本次行程停用',
'mobileTrip.compactView': '精簡檢視',
'mobileTrip.dayTitlePlaceholder': '天數標題',
- 'mobileTrip.exportCalendar': '匯出行事曆',
+ 'mobileTrip.export': '匯出',
'mobileTrip.filesEmpty': '暫無檔案',
'mobileTrip.filesNext': '下一個檔案',
'mobileTrip.filesPrev': '上一個檔案',
diff --git a/shared/src/i18n/zh/dayplan.ts b/shared/src/i18n/zh/dayplan.ts
index 005261ee0..30c456b84 100644
--- a/shared/src/i18n/zh/dayplan.ts
+++ b/shared/src/i18n/zh/dayplan.ts
@@ -51,6 +51,12 @@ const dayplan: TranslationStrings = {
'dayplan.pendingRes': '待确认',
'dayplan.pdf': 'PDF',
'dayplan.pdfTooltip': '导出当天计划为 PDF',
+ 'dayplan.gpxTooltip': '导出为 GPX,用于离线地图和 GPS 设备',
+ 'dayplan.gpxAll': '整趟行程',
+ 'dayplan.gpxPlaces': '仅地点',
+ 'dayplan.gpxDays': '按天生成路线',
+ 'dayplan.gpxEmpty': '暂时没有可导出的内容',
+ 'dayplan.gpxFailed': 'GPX 导出失败',
'dayplan.pdfError': 'PDF 导出失败',
'dayplan.cannotReorderTransport': '有固定时间的预订无法重新排序',
'dayplan.confirmRemoveTimeTitle': '移除时间?',
diff --git a/shared/src/i18n/zh/mobileTrip.ts b/shared/src/i18n/zh/mobileTrip.ts
index 010192fc9..77255d0c5 100644
--- a/shared/src/i18n/zh/mobileTrip.ts
+++ b/shared/src/i18n/zh/mobileTrip.ts
@@ -11,7 +11,7 @@ const mobileTrip: TranslationStrings = {
'mobileTrip.collabFeatureDisabled': '此功能已针对本次行程停用',
'mobileTrip.compactView': '紧凑视图',
'mobileTrip.dayTitlePlaceholder': '天数标题',
- 'mobileTrip.exportCalendar': '导出日历',
+ 'mobileTrip.export': '导出',
'mobileTrip.filesEmpty': '暂无文件',
'mobileTrip.filesNext': '下一个文件',
'mobileTrip.filesPrev': '上一个文件',
diff --git a/shared/src/place/place.schema.ts b/shared/src/place/place.schema.ts
index e2fe3fe7f..9c3fa5379 100644
--- a/shared/src/place/place.schema.ts
+++ b/shared/src/place/place.schema.ts
@@ -174,6 +174,18 @@ export const placeImportGpxRequestSchema = z.object({
});
export type PlaceImportGpxRequest = z.infer;
+/**
+ * GPX export (query string), the mirror of the import flags. Every field optional
+ * and string-typed because these arrive as query parameters; an omitted flag
+ * defaults to true server-side, so a bare `export.gpx` returns everything.
+ */
+export const placeExportGpxRequestSchema = z.object({
+ waypoints: z.string().optional(),
+ tracks: z.string().optional(),
+ dayRoutes: z.string().optional(),
+});
+export type PlaceExportGpxRequest = z.infer;
+
/** KML/KMZ import (multipart/form-data); same string-field contract as GPX. */
export const placeImportMapRequestSchema = z.object({
importPoints: z.string().optional(),
diff --git a/wiki/Map-Features.md b/wiki/Map-Features.md
index bac70754a..73b79e6e7 100644
--- a/wiki/Map-Features.md
+++ b/wiki/Map-Features.md
@@ -40,6 +40,18 @@ Any track that carries a colour — assigned at import or picked by you — is d
Clicking a line on the map selects that track and opens its details — useful when the start markers are still clustered together. In the places list, each track shows a short stroke in the colour it is drawn in, which is how you tell which line belongs to which entry.
+### Exporting a trip as GPX
+
+The **GPX** button in the day sidebar's toolbar, next to PDF and ICS, hands the trip back as a `.gpx` file for offline maps such as Organic Maps, for a handheld GPS, or for any other tool that reads the format. On a phone the same download sits in the trip's **Export** sheet, under "More".
+
+Three scopes:
+
+- **Whole trip** — every place as a waypoint, every imported track as a track, and every planned day as a route.
+- **Places only** — the same without the day routes, for when you just want the pins on an offline map.
+- **Days as routes** — only the planned days, each one a route through its stops in the order you arranged them. This is the one that puts a day's plan on a device you can follow.
+
+Places carry their description and address, and their category travels along as the GPX symbol, so devices that support it can show a different icon per kind of stop. Elevation is written back for tracks that were imported with it. Exporting is a read, so every trip member can do it, not only those who may edit.
+
## Route time pills
At zoom level 12 or higher, small pill-shaped labels appear between consecutive places and show the estimated **walking time** and **driving time** for each segment. Below zoom 12 they are hidden to keep the map clean.