feat(collections): export and import a list as GPX

A list could leave the instance as a TREK file, which only another TREK
reads. Export now asks for a format, and GPX is the second one: the
places as waypoints, for OsmAnd, Organic Maps, a Garmin or gpx.studio.
Import takes a GPX in the same dialog as a list file. Asked for in
discussion #2301.

The GPX is written from the very list file the JSON export returns, so
what may leave the instance is still decided in one place. Name,
description and address, notes, website and category go into GPX's own
elements, where every app shows them. Labels, status, phone and the
rest travel in a TREK extension namespace, so a GPX made by TREK comes
back as the same list. A place without coordinates cannot be a
waypoint; it is left out and the export says how many were.

A GPX is read on the server, sent as text, with the XML library the
trip importer already uses. A document with a DOCTYPE is refused before
parsing, which closes both external entities and entity expansion. Size
is capped like the list file, and the element count is capped before a
tree is built. The reader only turns the GPX into a list file: the
browser previews it and imports it through the existing route, so one
contract, one transaction and one skipped counter cover both formats.
Waypoints and named route points become places; tracks and unnamed
route points are counted in the preview and not imported.

The two list-file routes get a body ceiling of twice the file limit.
The global 100 kB cap refused most real favourites files, and it
already refused list files well under the megabyte the contract allows.
This commit is contained in:
Maurice
2026-09-19 21:01:45 +02:00
parent bb6003684c
commit 80bb06c84f
57 changed files with 2506 additions and 109 deletions
@@ -0,0 +1,44 @@
import {
COLLECTION_GPX_PLACE_FIELDS,
MAX_COLLECTION_FILE_BYTES,
collectionFilePlaceSchema,
collectionGpxReadRequestSchema,
} from './collection-file.schema';
import { describe, expect, it } from 'vitest';
describe('COLLECTION_GPX_PLACE_FIELDS (#2301)', () => {
// The type already refuses a missing key; this is the same promise at runtime,
// for a field that reaches the schema through a spread the type cannot see.
it('gives every field of a place in a list file a home in a GPX waypoint', () => {
const fields = Object.keys(COLLECTION_GPX_PLACE_FIELDS).sort();
expect(fields).toEqual(Object.keys(collectionFilePlaceSchema.shape).sort());
});
it('keeps what GPX has an element for out of the extension', () => {
const native = Object.entries(COLLECTION_GPX_PLACE_FIELDS)
.filter(([, home]) => home === 'waypoint')
.map(([field]) => field);
expect(native.sort()).toEqual(['category', 'description', 'lat', 'lng', 'name', 'notes', 'website']);
});
});
describe('collectionGpxReadRequestSchema (#2301)', () => {
const accepts = (body: unknown) => collectionGpxReadRequestSchema.safeParse(body).success;
it('takes the document as text, with the file name optional', () => {
expect(collectionGpxReadRequestSchema.parse({ gpx: '<gpx/>' })).toEqual({ gpx: '<gpx/>' });
const named = collectionGpxReadRequestSchema.parse({ gpx: '<gpx/>', file_name: 'favourites.gpx' });
expect(named.file_name).toBe('favourites.gpx');
});
it('refuses an empty document and one past the size a list file may have', () => {
expect(accepts({ gpx: '' })).toBe(false);
expect(accepts({ gpx: 'x'.repeat(MAX_COLLECTION_FILE_BYTES + 1) })).toBe(false);
});
it('refuses anything that is not text', () => {
expect(accepts({ gpx: { wpt: [] } })).toBe(false);
expect(accepts({})).toBe(false);
});
});
@@ -48,8 +48,9 @@ import { z } from 'zod';
* ── What an imported file is trusted for ─────────────────────────────────
*
* Nothing. It is a stranger's JSON. It is read in the browser that chose it,
* never uploaded as a file, and what is sent to the server goes through this
* contract on both sides. Text is rendered as text; the two fields that would
* never uploaded as a file (a GPX is, as text, for the reasons given at the
* end), and what is sent to the server goes through this contract on both
* sides. Text is rendered as text; the two fields that would
* otherwise reach a browser API (`website` and a link's `url`) are pinned to
* http(s) by the schemas they borrow, so neither can be a `javascript:` value.
* The counts below are the other half: a file cannot ask the server for more
@@ -188,3 +189,91 @@ export interface CollectionImportResult {
/** Places the file carried that the contract refused. */
skipped: number;
}
/*
* ── The same list as GPX (#2301) ─────────────────────────────────────────
*
* GPX is what the apps a list is usually wanted in already read: OsmAnd,
* Organic Maps, a Garmin, gpx.studio. It is not a second file format with its
* own idea of what may leave. A GPX is written from the list file above, after
* the exporter has decided what travels, so it can only ever carry less.
*
* Each place becomes a waypoint, and a waypoint needs coordinates: a place
* without them is left out and counted, never guessed. What GPX 1.1 has an
* element for goes into that element, so every reader shows it. What it has
* none for travels in TREK's own extension namespace, so a GPX made by TREK
* comes back as the same place rather than a pin with a name.
*
* Reading goes the other way through the same contract: a GPX is turned into
* a list file by the server and imported like one, so nothing a GPX says gets
* past `collectionFilePlaceSchema`. The server reads it, sent as text, because
* XML is where the parser itself is the risk (entities, DTDs, sheer size), and
* the server has one parser, already used for trip GPX, whose limits are its
* own and tested, where a browser's differ by engine.
*/
/** TREK's extension namespace. Versioned in the path; nothing resolves it. */
export const COLLECTION_GPX_NAMESPACE = 'https://liketrek.com/xmlns/gpx/collection/1';
/**
* Where each field of a place goes in a waypoint.
*
* `waypoint` has a GPX element of its own (name, lat/lon, desc, cmt, link,
* type); `extension` travels in the namespace above, one element per field,
* named after the field. Keyed by the place contract, so a field added there
* does not build until somebody decides where it goes in a GPX.
*/
export const COLLECTION_GPX_PLACE_FIELDS = {
name: 'waypoint',
lat: 'waypoint',
lng: 'waypoint',
description: 'waypoint',
notes: 'waypoint',
website: 'waypoint',
category: 'waypoint',
address: 'extension',
phone: 'extension',
status: 'extension',
price: 'extension',
currency: 'extension',
image_url: 'extension',
google_place_id: 'extension',
google_ftid: 'extension',
osm_id: 'extension',
labels: 'extension',
links: 'extension',
} as const satisfies Record<keyof CollectionFilePlace, 'waypoint' | 'extension'>;
/**
* A GPX document to be read into a list file. Sent as text: the file is a
* stranger's XML, and it is parsed where the XML parser and its limits are.
*/
export const collectionGpxReadRequestSchema = z.object({
gpx: z.string().min(1).max(MAX_COLLECTION_FILE_BYTES),
/** The chosen file's name, which names the list when the document does not. */
file_name: z.string().max(255).optional(),
});
export type CollectionGpxReadRequest = z.infer<typeof collectionGpxReadRequestSchema>;
/** Why a GPX was not read, as the `code` of the 4xx that says so. */
export const COLLECTION_GPX_PROBLEMS = ['too-large', 'unreadable', 'not-gpx', 'too-many-places'] as const;
export type CollectionGpxProblem = (typeof COLLECTION_GPX_PROBLEMS)[number];
export interface CollectionGpxReadResult {
/** The list file the GPX amounts to, ready for the import above. */
file: CollectionFile;
/** Waypoints that could not become a place, mostly for want of coordinates. */
skipped: number;
/** Points of tracks and unnamed route points: lines, not places, so not imported. */
track_points: number;
}
export interface CollectionGpxExport {
/** The list's name, for the file name. */
name: string;
gpx: string;
/** Places written as waypoints. */
waypoints: number;
/** Places left out because they have no coordinates. */
omitted: number;
}
+13
View File
@@ -159,6 +159,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'هذا الملف أكبر من أن يكون قائمة.',
'collections.file.errorUnreadable': 'تعذّرت قراءة هذا الملف.',
'collections.file.errorNotACollection': 'هذا ليس ملف قائمة من TREK.',
'collections.file.formatTrek': 'قائمة TREK',
'collections.file.formatTrekHint': 'لنسخة TREK أخرى، مع التسميات والحالة',
'collections.file.formatGpxHint': 'نقاط طريق لتطبيقات OsmAnd وOrganic Maps وGarmin وغيرها من تطبيقات الخرائط',
'collections.file.gpxOmitted': 'لم يُضمَّن {count} من الأماكن في ملف GPX لأنها بلا إحداثيات',
'collections.file.gpxNothing': 'لا يملك أي من هذه الأماكن إحداثيات، فلا يوجد ما يُوضع في ملف GPX.',
'collections.file.reading': 'جارٍ قراءة الملف…',
'collections.file.gpxSkipped': 'سيُستبعد {count} من نقاط الطريق لعدم وجود إحداثيات صالحة لها.',
'collections.file.gpxTrack':
'يحتوي هذا الملف أيضًا على {count} من نقاط المسار. لا تُستورد المسارات، بل نقاط الطريق فقط.',
'collections.file.gpxEmpty': 'لا يحتوي ملف GPX هذا على نقاط طريق، فلا يوجد ما يُستورد.',
'collections.file.errorNotGpx': 'هذا ليس ملف GPX.',
'collections.file.errorTooManyPlaces':
'يحتوي هذا الملف على أكثر من {count} مكان. قسّمه واستورد الأجزاء واحدًا تلو الآخر.',
'collections.share.title': 'مشاركة القائمة',
'collections.share.titleNamed': 'مشاركة «{name}»',
+14
View File
@@ -160,6 +160,20 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Esse arquivo é grande demais para ser uma lista.',
'collections.file.errorUnreadable': 'Não foi possível ler esse arquivo.',
'collections.file.errorNotACollection': 'Esse não é um arquivo de lista do TREK.',
'collections.file.formatTrek': 'Lista do TREK',
'collections.file.formatTrekHint': 'Para outro TREK, com etiquetas e status',
'collections.file.formatGpxHint': 'Pontos de referência para OsmAnd, Organic Maps, Garmin e outros apps de mapa',
'collections.file.gpxOmitted': '{count} lugares não têm coordenadas e ficaram fora do arquivo GPX',
'collections.file.gpxNothing':
'Nenhum desses lugares tem coordenadas, então não há nada para colocar em um arquivo GPX.',
'collections.file.reading': 'Lendo o arquivo…',
'collections.file.gpxSkipped': '{count} pontos de referência sem coordenadas utilizáveis ficam de fora.',
'collections.file.gpxTrack':
'Este arquivo também tem {count} pontos de trilha. Trilhas não são importadas, apenas pontos de referência.',
'collections.file.gpxEmpty': 'Este arquivo GPX não tem pontos de referência, então não há nada para importar.',
'collections.file.errorNotGpx': 'Esse não é um arquivo GPX.',
'collections.file.errorTooManyPlaces':
'Esse arquivo tem mais de {count} lugares. Divida-o e importe as partes uma de cada vez.',
'collections.share.title': 'Compartilhar lista',
'collections.share.titleNamed': 'Compartilhar “{name}”',
+14
View File
@@ -151,6 +151,20 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Aquest fitxer és massa gran per ser una llista.',
'collections.file.errorUnreadable': 'No s’ha pogut llegir aquest fitxer.',
'collections.file.errorNotACollection': 'Això no és un fitxer de llista del TREK.',
'collections.file.formatTrek': 'Llista del TREK',
'collections.file.formatTrekHint': 'Per a un altre TREK, amb etiquetes i estat',
'collections.file.formatGpxHint': 'Punts de pas per a OsmAnd, Organic Maps, Garmin i altres aplicacions de mapes',
'collections.file.gpxOmitted': '{count} llocs no tenen coordenades i han quedat fora del fitxer GPX',
'collections.file.gpxNothing':
'Cap d’aquests llocs té coordenades, així que no hi ha res per posar en un fitxer GPX.',
'collections.file.reading': 'S’està llegint el fitxer…',
'collections.file.gpxSkipped': 'Es deixen fora {count} punts de pas sense coordenades vàlides.',
'collections.file.gpxTrack':
'Aquest fitxer també té {count} punts de traça. Les traces no s’importen, només els punts de pas.',
'collections.file.gpxEmpty': 'Aquest fitxer GPX no té punts de pas, així que no hi ha res per importar.',
'collections.file.errorNotGpx': 'Això no és un fitxer GPX.',
'collections.file.errorTooManyPlaces':
'Aquest fitxer té més de {count} llocs. Divideix-lo i importa’n les parts d’una en una.',
'collections.share.title': 'Compartir llista',
'collections.share.titleNamed': 'Compartir «{name}»',
+12
View File
@@ -159,6 +159,18 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Tento soubor je na seznam příliš velký.',
'collections.file.errorUnreadable': 'Tento soubor se nepodařilo přečíst.',
'collections.file.errorNotACollection': 'Toto není soubor se seznamem TREK.',
'collections.file.formatTrek': 'Seznam TREK',
'collections.file.formatTrekHint': 'Pro jiný TREK, se štítky a stavem',
'collections.file.formatGpxHint': 'Trasové body pro OsmAnd, Organic Maps, Garmin a další mapové aplikace',
'collections.file.gpxOmitted': 'Místa bez souřadnic vynechaná ze souboru GPX: {count}',
'collections.file.gpxNothing': 'Žádné z těchto míst nemá souřadnice, takže do souboru GPX není co uložit.',
'collections.file.reading': 'Načítání souboru…',
'collections.file.gpxSkipped': 'Trasové body bez použitelných souřadnic budou vynechány: {count}.',
'collections.file.gpxTrack': 'Soubor obsahuje také body stopy: {count}. Stopy se neimportují, jen trasové body.',
'collections.file.gpxEmpty': 'Tento soubor GPX neobsahuje žádné trasové body, takže není co importovat.',
'collections.file.errorNotGpx': 'Toto není soubor GPX.',
'collections.file.errorTooManyPlaces':
'Tento soubor obsahuje více než {count} míst. Rozdělte ho a importujte části postupně.',
'collections.share.title': 'Sdílet seznam',
'collections.share.titleNamed': 'Sdílet „{name}“',
+13
View File
@@ -161,6 +161,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Diese Datei ist zu groß für eine Liste.',
'collections.file.errorUnreadable': 'Diese Datei konnte nicht gelesen werden.',
'collections.file.errorNotACollection': 'Das ist keine TREK-Listendatei.',
'collections.file.formatTrek': 'TREK-Liste',
'collections.file.formatTrekHint': 'Für ein anderes TREK, mit Labels und Status',
'collections.file.formatGpxHint': 'Wegpunkte für OsmAnd, Organic Maps, Garmin und andere Karten-Apps',
'collections.file.gpxOmitted': '{count} Orte ohne Koordinaten fehlen in der GPX-Datei',
'collections.file.gpxNothing': 'Keiner dieser Orte hat Koordinaten, für eine GPX-Datei gibt es also nichts.',
'collections.file.reading': 'Datei wird gelesen…',
'collections.file.gpxSkipped': '{count} Wegpunkte ohne brauchbare Koordinaten bleiben weg.',
'collections.file.gpxTrack':
'Die Datei enthält außerdem {count} Trackpunkte. Tracks werden nicht importiert, nur Wegpunkte.',
'collections.file.gpxEmpty': 'Diese GPX-Datei enthält keine Wegpunkte, es gibt also nichts zu importieren.',
'collections.file.errorNotGpx': 'Das ist keine GPX-Datei.',
'collections.file.errorTooManyPlaces':
'Diese Datei enthält mehr als {count} Orte. Teile sie auf und importiere die Teile nacheinander.',
'collections.share.title': 'Liste teilen',
'collections.share.titleNamed': '„{name}“ teilen',
+12
View File
@@ -160,6 +160,18 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'That file is too large to be a list.',
'collections.file.errorUnreadable': 'That file could not be read.',
'collections.file.errorNotACollection': 'That is not a TREK list file.',
'collections.file.formatTrek': 'TREK list',
'collections.file.formatTrekHint': 'For another TREK, with labels and status',
'collections.file.formatGpxHint': 'Waypoints for OsmAnd, Organic Maps, Garmin and other map apps',
'collections.file.gpxOmitted': '{count} places have no coordinates and were left out of the GPX file',
'collections.file.gpxNothing': 'None of these places has coordinates, so there is nothing to put in a GPX file.',
'collections.file.reading': 'Reading the file…',
'collections.file.gpxSkipped': '{count} waypoints without usable coordinates are left out.',
'collections.file.gpxTrack': 'This file also has {count} track points. Tracks are not imported, only waypoints.',
'collections.file.gpxEmpty': 'This GPX file has no waypoints, so there is nothing to import.',
'collections.file.errorNotGpx': 'That is not a GPX file.',
'collections.file.errorTooManyPlaces':
'That file has more than {count} places. Split it and import the parts one at a time.',
'collections.share.title': 'Share list',
'collections.share.titleNamed': 'Share “{name}”',
+14
View File
@@ -160,6 +160,20 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Ese archivo es demasiado grande para ser una lista.',
'collections.file.errorUnreadable': 'No se ha podido leer ese archivo.',
'collections.file.errorNotACollection': 'Ese no es un archivo de lista de TREK.',
'collections.file.formatTrek': 'Lista de TREK',
'collections.file.formatTrekHint': 'Para otro TREK, con etiquetas y estado',
'collections.file.formatGpxHint': 'Puntos de ruta para OsmAnd, Organic Maps, Garmin y otras apps de mapas',
'collections.file.gpxOmitted': '{count} lugares no tienen coordenadas y se han quedado fuera del archivo GPX',
'collections.file.gpxNothing':
'Ninguno de estos lugares tiene coordenadas, así que no hay nada que poner en un archivo GPX.',
'collections.file.reading': 'Leyendo el archivo…',
'collections.file.gpxSkipped': 'Se dejan fuera {count} puntos de ruta sin coordenadas válidas.',
'collections.file.gpxTrack':
'Este archivo también tiene {count} puntos de traza. Las trazas no se importan, solo los puntos de ruta.',
'collections.file.gpxEmpty': 'Este archivo GPX no tiene puntos de ruta, así que no hay nada que importar.',
'collections.file.errorNotGpx': 'Ese no es un archivo GPX.',
'collections.file.errorTooManyPlaces':
'Ese archivo tiene más de {count} lugares. Divídelo e importa las partes de una en una.',
'collections.share.title': 'Compartir lista',
'collections.share.titleNamed': 'Compartir «{name}»',
+15
View File
@@ -160,6 +160,21 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Ce fichier est trop volumineux pour être une liste.',
'collections.file.errorUnreadable': 'Ce fichier n’a pas pu être lu.',
'collections.file.errorNotACollection': 'Ce n’est pas un fichier de liste TREK.',
'collections.file.formatTrek': 'Liste TREK',
'collections.file.formatTrekHint': 'Pour un autre TREK, avec étiquettes et statut',
'collections.file.formatGpxHint':
'Points de passage pour OsmAnd, Organic Maps, Garmin et d’autres applications cartographiques',
'collections.file.gpxOmitted': '{count} lieux sans coordonnées ont été laissés hors du fichier GPX',
'collections.file.gpxNothing':
'Aucun de ces lieux n’a de coordonnées, il n’y a donc rien à mettre dans un fichier GPX.',
'collections.file.reading': 'Lecture du fichier…',
'collections.file.gpxSkipped': '{count} points de passage sans coordonnées utilisables sont écartés.',
'collections.file.gpxTrack':
'Ce fichier contient aussi {count} points de trace. Les traces ne sont pas importées, seulement les points de passage.',
'collections.file.gpxEmpty': 'Ce fichier GPX ne contient aucun point de passage, il n’y a donc rien à importer.',
'collections.file.errorNotGpx': 'Ce n’est pas un fichier GPX.',
'collections.file.errorTooManyPlaces':
'Ce fichier contient plus de {count} lieux. Découpez-le et importez les parties une par une.',
'collections.share.title': 'Partager la liste',
'collections.share.titleNamed': 'Partager « {name} »',
+14
View File
@@ -160,6 +160,20 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Αυτό το αρχείο είναι πολύ μεγάλο για λίστα.',
'collections.file.errorUnreadable': 'Δεν ήταν δυνατή η ανάγνωση αυτού του αρχείου.',
'collections.file.errorNotACollection': 'Αυτό δεν είναι αρχείο λίστας του TREK.',
'collections.file.formatTrek': 'Λίστα TREK',
'collections.file.formatTrekHint': 'Για άλλο TREK, με ετικέτες και κατάσταση',
'collections.file.formatGpxHint': 'Σημεία διαδρομής για OsmAnd, Organic Maps, Garmin και άλλες εφαρμογές χαρτών',
'collections.file.gpxOmitted': '{count} μέρη χωρίς συντεταγμένες έμειναν εκτός του αρχείου GPX',
'collections.file.gpxNothing':
'Κανένα από αυτά τα μέρη δεν έχει συντεταγμένες, οπότε δεν υπάρχει τίποτα για αρχείο GPX.',
'collections.file.reading': 'Ανάγνωση αρχείου…',
'collections.file.gpxSkipped': '{count} σημεία διαδρομής χωρίς χρήσιμες συντεταγμένες μένουν εκτός.',
'collections.file.gpxTrack':
'Το αρχείο έχει επίσης {count} σημεία ίχνους. Τα ίχνη δεν εισάγονται, μόνο τα σημεία διαδρομής.',
'collections.file.gpxEmpty': 'Αυτό το αρχείο GPX δεν έχει σημεία διαδρομής, οπότε δεν υπάρχει τίποτα για εισαγωγή.',
'collections.file.errorNotGpx': 'Αυτό δεν είναι αρχείο GPX.',
'collections.file.errorTooManyPlaces':
'Αυτό το αρχείο έχει περισσότερα από {count} μέρη. Χωρίστε το και εισαγάγετε τα κομμάτια ένα ένα.',
'collections.share.title': 'Κοινή χρήση λίστας',
'collections.share.titleNamed': 'Κοινή χρήση «{name}»',
+13
View File
@@ -161,6 +161,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Ez a fájl túl nagy ahhoz, hogy lista legyen.',
'collections.file.errorUnreadable': 'Ezt a fájlt nem sikerült beolvasni.',
'collections.file.errorNotACollection': 'Ez nem TREK-listafájl.',
'collections.file.formatTrek': 'TREK-lista',
'collections.file.formatTrekHint': 'Másik TREK-hez, címkékkel és állapottal',
'collections.file.formatGpxHint': 'Útpontok OsmAndhez, Organic Mapshez, Garminhoz és más térképes alkalmazásokhoz',
'collections.file.gpxOmitted': '{count} hely koordináták nélkül kimaradt a GPX-fájlból',
'collections.file.gpxNothing': 'Egyik helynek sincs koordinátája, így nincs mit GPX-fájlba tenni.',
'collections.file.reading': 'Fájl beolvasása…',
'collections.file.gpxSkipped': '{count} használható koordináták nélküli útpont kimarad.',
'collections.file.gpxTrack':
'A fájlban {count} nyomvonalpont is van. A nyomvonalakat nem importáljuk, csak az útpontokat.',
'collections.file.gpxEmpty': 'Ebben a GPX-fájlban nincsenek útpontok, így nincs mit importálni.',
'collections.file.errorNotGpx': 'Ez nem GPX-fájl.',
'collections.file.errorTooManyPlaces':
'Ebben a fájlban több mint {count} hely van. Oszd fel, és importáld a részeket egyenként.',
'collections.share.title': 'Lista megosztása',
'collections.share.titleNamed': '„{name}” megosztása',
+13
View File
@@ -160,6 +160,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Berkas ini terlalu besar untuk sebuah daftar.',
'collections.file.errorUnreadable': 'Berkas ini tidak bisa dibaca.',
'collections.file.errorNotACollection': 'Ini bukan berkas daftar TREK.',
'collections.file.formatTrek': 'Daftar TREK',
'collections.file.formatTrekHint': 'Untuk TREK lain, lengkap dengan label dan status',
'collections.file.formatGpxHint': 'Titik lokasi untuk OsmAnd, Organic Maps, Garmin, dan aplikasi peta lain',
'collections.file.gpxOmitted': '{count} tempat tanpa koordinat tidak dimasukkan ke berkas GPX',
'collections.file.gpxNothing':
'Tidak ada tempat yang punya koordinat, jadi tidak ada yang bisa dimasukkan ke berkas GPX.',
'collections.file.reading': 'Membaca berkas…',
'collections.file.gpxSkipped': '{count} titik lokasi tanpa koordinat yang bisa dipakai tidak disertakan.',
'collections.file.gpxTrack': 'Berkas ini juga berisi {count} titik jejak. Jejak tidak diimpor, hanya titik lokasi.',
'collections.file.gpxEmpty': 'Berkas GPX ini tidak berisi titik lokasi, jadi tidak ada yang diimpor.',
'collections.file.errorNotGpx': 'Ini bukan berkas GPX.',
'collections.file.errorTooManyPlaces':
'Berkas ini berisi lebih dari {count} tempat. Pecah menjadi beberapa bagian dan impor satu per satu.',
'collections.share.title': 'Bagikan daftar',
'collections.share.titleNamed': 'Bagikan “{name}”',
+14
View File
@@ -160,6 +160,20 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Questo file è troppo grande per essere un elenco.',
'collections.file.errorUnreadable': 'Non è stato possibile leggere questo file.',
'collections.file.errorNotACollection': 'Questo non è un file di elenco TREK.',
'collections.file.formatTrek': 'Elenco TREK',
'collections.file.formatTrekHint': 'Per un altro TREK, con etichette e stato',
'collections.file.formatGpxHint': 'Punti di passaggio per OsmAnd, Organic Maps, Garmin e altre app di mappe',
'collections.file.gpxOmitted': '{count} luoghi senza coordinate sono rimasti fuori dal file GPX',
'collections.file.gpxNothing':
'Nessuno di questi luoghi ha coordinate, quindi non c’è niente da mettere in un file GPX.',
'collections.file.reading': 'Lettura del file…',
'collections.file.gpxSkipped': '{count} punti di passaggio senza coordinate utilizzabili restano fuori.',
'collections.file.gpxTrack':
'Questo file contiene anche {count} punti di traccia. Le tracce non vengono importate, solo i punti di passaggio.',
'collections.file.gpxEmpty': 'Questo file GPX non contiene punti di passaggio, quindi non c’è niente da importare.',
'collections.file.errorNotGpx': 'Questo non è un file GPX.',
'collections.file.errorTooManyPlaces':
'Questo file contiene più di {count} luoghi. Dividilo e importa le parti una alla volta.',
'collections.share.title': 'Condividi lista',
'collections.share.titleNamed': 'Condividi «{name}»',
+13
View File
@@ -159,6 +159,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'このファイルはリストとしては大きすぎます。',
'collections.file.errorUnreadable': 'このファイルを読み取れませんでした。',
'collections.file.errorNotACollection': 'これは TREK のリストファイルではありません。',
'collections.file.formatTrek': 'TREK リスト',
'collections.file.formatTrekHint': '別の TREK 向け。ラベルとステータスも含みます',
'collections.file.formatGpxHint': 'OsmAnd、Organic Maps、Garmin などの地図アプリ向けのウェイポイント',
'collections.file.gpxOmitted': '座標のない {count} 件の場所は GPX ファイルに含まれていません',
'collections.file.gpxNothing': 'どの場所にも座標がないため、GPX ファイルに入れるものがありません。',
'collections.file.reading': 'ファイルを読み込んでいます…',
'collections.file.gpxSkipped': '使える座標のないウェイポイント {count} 件は除外されます。',
'collections.file.gpxTrack':
'このファイルにはトラックポイントも {count} 件あります。トラックはインポートされず、ウェイポイントのみが対象です。',
'collections.file.gpxEmpty': 'この GPX ファイルにはウェイポイントがないため、インポートするものがありません。',
'collections.file.errorNotGpx': 'これは GPX ファイルではありません。',
'collections.file.errorTooManyPlaces':
'このファイルには {count} 件を超える場所があります。分割して、1 つずつインポートしてください。',
'collections.share.title': 'リストを共有',
'collections.share.titleNamed': '「{name}」を共有',
+12
View File
@@ -158,6 +158,18 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': '이 파일은 목록이라고 하기에 너무 큽니다.',
'collections.file.errorUnreadable': '이 파일을 읽을 수 없습니다.',
'collections.file.errorNotACollection': 'TREK 목록 파일이 아닙니다.',
'collections.file.formatTrek': 'TREK 목록',
'collections.file.formatTrekHint': '다른 TREK용, 라벨과 상태 포함',
'collections.file.formatGpxHint': 'OsmAnd, Organic Maps, Garmin 등 지도 앱용 웨이포인트',
'collections.file.gpxOmitted': '좌표가 없는 장소 {count}개는 GPX 파일에서 빠졌습니다',
'collections.file.gpxNothing': '좌표가 있는 장소가 없어 GPX 파일에 넣을 것이 없습니다.',
'collections.file.reading': '파일을 읽는 중…',
'collections.file.gpxSkipped': '쓸 수 있는 좌표가 없는 웨이포인트 {count}개는 제외됩니다.',
'collections.file.gpxTrack':
'이 파일에는 트랙 포인트 {count}개도 있습니다. 트랙은 가져오지 않고 웨이포인트만 가져옵니다.',
'collections.file.gpxEmpty': '이 GPX 파일에는 웨이포인트가 없어 가져올 것이 없습니다.',
'collections.file.errorNotGpx': 'GPX 파일이 아닙니다.',
'collections.file.errorTooManyPlaces': '이 파일에는 장소가 {count}개보다 많습니다. 나눠서 하나씩 가져오세요.',
'collections.share.title': '목록 공유',
'collections.share.titleNamed': '“{name}” 공유',
+14
View File
@@ -160,6 +160,20 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Dit bestand is te groot om een lijst te zijn.',
'collections.file.errorUnreadable': 'Dit bestand kon niet worden gelezen.',
'collections.file.errorNotACollection': 'Dit is geen TREK-lijstbestand.',
'collections.file.formatTrek': 'TREK-lijst',
'collections.file.formatTrekHint': 'Voor een andere TREK, met labels en status',
'collections.file.formatGpxHint': 'Waypoints voor OsmAnd, Organic Maps, Garmin en andere kaart-apps',
'collections.file.gpxOmitted': '{count} plekken zonder coördinaten zijn niet in het GPX-bestand opgenomen',
'collections.file.gpxNothing':
'Geen van deze plekken heeft coördinaten, dus er is niets om in een GPX-bestand te zetten.',
'collections.file.reading': 'Bestand wordt gelezen…',
'collections.file.gpxSkipped': '{count} waypoints zonder bruikbare coördinaten worden overgeslagen.',
'collections.file.gpxTrack':
'Dit bestand bevat ook {count} trackpunten. Tracks worden niet geïmporteerd, alleen waypoints.',
'collections.file.gpxEmpty': 'Dit GPX-bestand bevat geen waypoints, dus er valt niets te importeren.',
'collections.file.errorNotGpx': 'Dit is geen GPX-bestand.',
'collections.file.errorTooManyPlaces':
'Dit bestand bevat meer dan {count} plekken. Splits het op en importeer de delen een voor een.',
'collections.share.title': 'Lijst delen',
'collections.share.titleNamed': '“{name}” delen',
+12
View File
@@ -160,6 +160,18 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Ten plik jest za duży, aby był listą.',
'collections.file.errorUnreadable': 'Nie udało się odczytać tego pliku.',
'collections.file.errorNotACollection': 'To nie jest plik listy TREK.',
'collections.file.formatTrek': 'Lista TREK',
'collections.file.formatTrekHint': 'Dla innego TREK, z etykietami i statusem',
'collections.file.formatGpxHint': 'Punkty trasy dla OsmAnd, Organic Maps, Garmina i innych aplikacji z mapami',
'collections.file.gpxOmitted': 'Miejsca bez współrzędnych pominięte w pliku GPX: {count}',
'collections.file.gpxNothing': 'Żadne z tych miejsc nie ma współrzędnych, więc nie ma czego zapisać w pliku GPX.',
'collections.file.reading': 'Wczytywanie pliku…',
'collections.file.gpxSkipped': 'Punkty trasy bez użytecznych współrzędnych zostaną pominięte: {count}.',
'collections.file.gpxTrack': 'Plik zawiera też punkty śladu: {count}. Ślady nie są importowane, tylko punkty trasy.',
'collections.file.gpxEmpty': 'Ten plik GPX nie zawiera punktów trasy, więc nie ma czego importować.',
'collections.file.errorNotGpx': 'To nie jest plik GPX.',
'collections.file.errorTooManyPlaces':
'Ten plik zawiera więcej niż {count} miejsc. Podziel go i importuj części po kolei.',
'collections.share.title': 'Udostępnij listę',
'collections.share.titleNamed': 'Udostępnij „{name}”',
+13
View File
@@ -159,6 +159,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Этот файл слишком велик для списка.',
'collections.file.errorUnreadable': 'Не удалось прочитать этот файл.',
'collections.file.errorNotACollection': 'Это не файл списка TREK.',
'collections.file.formatTrek': 'Список TREK',
'collections.file.formatTrekHint': 'Для другого TREK, с метками и статусом',
'collections.file.formatGpxHint':
'Путевые точки для OsmAnd, Organic Maps, Garmin и других картографических приложений',
'collections.file.gpxOmitted': 'Мест без координат, не попавших в файл GPX: {count}',
'collections.file.gpxNothing': 'Ни у одного из этих мест нет координат, поэтому в файл GPX нечего записать.',
'collections.file.reading': 'Чтение файла…',
'collections.file.gpxSkipped': 'Путевые точки без пригодных координат будут пропущены: {count}.',
'collections.file.gpxTrack': 'В файле также есть точки трека: {count}. Треки не импортируются, только путевые точки.',
'collections.file.gpxEmpty': 'В этом файле GPX нет путевых точек, поэтому импортировать нечего.',
'collections.file.errorNotGpx': 'Это не файл GPX.',
'collections.file.errorTooManyPlaces':
'В этом файле больше {count} мест. Разделите его и импортируйте части по очереди.',
'collections.share.title': 'Поделиться списком',
'collections.share.titleNamed': 'Поделиться «{name}»',
+13
View File
@@ -159,6 +159,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Filen är för stor för att vara en lista.',
'collections.file.errorUnreadable': 'Filen gick inte att läsa.',
'collections.file.errorNotACollection': 'Det här är ingen TREK-listfil.',
'collections.file.formatTrek': 'TREK-lista',
'collections.file.formatTrekHint': 'För en annan TREK, med etiketter och status',
'collections.file.formatGpxHint': 'Vägpunkter för OsmAnd, Organic Maps, Garmin och andra kartappar',
'collections.file.gpxOmitted': '{count} platser utan koordinater kom inte med i GPX-filen',
'collections.file.gpxNothing':
'Ingen av de här platserna har koordinater, så det finns inget att lägga i en GPX-fil.',
'collections.file.reading': 'Läser filen…',
'collections.file.gpxSkipped': '{count} vägpunkter utan användbara koordinater lämnas utanför.',
'collections.file.gpxTrack': 'Filen innehåller också {count} spårpunkter. Spår importeras inte, bara vägpunkter.',
'collections.file.gpxEmpty': 'Den här GPX-filen har inga vägpunkter, så det finns inget att importera.',
'collections.file.errorNotGpx': 'Det här är ingen GPX-fil.',
'collections.file.errorTooManyPlaces':
'Filen har fler än {count} platser. Dela upp den och importera delarna en i taget.',
'collections.share.title': 'Dela lista',
'collections.share.titleNamed': 'Dela ”{name}”',
+13
View File
@@ -160,6 +160,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Bu dosya liste olamayacak kadar büyük.',
'collections.file.errorUnreadable': 'Bu dosya okunamadı.',
'collections.file.errorNotACollection': 'Bu bir TREK liste dosyası değil.',
'collections.file.formatTrek': 'TREK listesi',
'collections.file.formatTrekHint': 'Başka bir TREK için, etiketler ve durumla birlikte',
'collections.file.formatGpxHint': 'OsmAnd, Organic Maps, Garmin ve diğer harita uygulamaları için yol noktaları',
'collections.file.gpxOmitted': 'Koordinatı olmayan {count} yer GPX dosyasına eklenmedi',
'collections.file.gpxNothing': 'Bu yerlerin hiçbirinin koordinatı yok, bu yüzden GPX dosyasına konacak bir şey yok.',
'collections.file.reading': 'Dosya okunuyor…',
'collections.file.gpxSkipped': 'Kullanılabilir koordinatı olmayan {count} yol noktası dışarıda bırakılıyor.',
'collections.file.gpxTrack':
'Bu dosyada ayrıca {count} iz noktası var. İzler içe aktarılmaz, yalnızca yol noktaları.',
'collections.file.gpxEmpty': 'Bu GPX dosyasında yol noktası yok, bu yüzden içe aktarılacak bir şey yok.',
'collections.file.errorNotGpx': 'Bu bir GPX dosyası değil.',
'collections.file.errorTooManyPlaces':
'Bu dosyada {count} yerden fazlası var. Dosyayı bölüp parçaları tek tek içe aktar.',
'collections.share.title': 'Listeyi paylaş',
'collections.share.titleNamed': '“{name}” listesini paylaş',
+13
View File
@@ -159,6 +159,19 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Цей файл завеликий, щоб бути списком.',
'collections.file.errorUnreadable': 'Не вдалося прочитати цей файл.',
'collections.file.errorNotACollection': 'Це не файл списку TREK.',
'collections.file.formatTrek': 'Список TREK',
'collections.file.formatTrekHint': 'Для іншого TREK, з мітками й статусом',
'collections.file.formatGpxHint':
'Маршрутні точки для OsmAnd, Organic Maps, Garmin та інших картографічних застосунків',
'collections.file.gpxOmitted': 'Місць без координат, які не потрапили до файлу GPX: {count}',
'collections.file.gpxNothing': 'Жодне з цих місць не має координат, тож у файл GPX нічого записати.',
'collections.file.reading': 'Читання файлу…',
'collections.file.gpxSkipped': 'Маршрутні точки без придатних координат буде пропущено: {count}.',
'collections.file.gpxTrack': 'У файлі також є точки треку: {count}. Треки не імпортуються, лише маршрутні точки.',
'collections.file.gpxEmpty': 'У цьому файлі GPX немає маршрутних точок, тож імпортувати нічого.',
'collections.file.errorNotGpx': 'Це не файл GPX.',
'collections.file.errorTooManyPlaces':
'У цьому файлі понад {count} місць. Розділіть його та імпортуйте частини по черзі.',
'collections.share.title': 'Поділитися списком',
'collections.share.titleNamed': 'Поділитися «{name}»',
+12
View File
@@ -160,6 +160,18 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': 'Tệp này quá lớn để là một danh sách.',
'collections.file.errorUnreadable': 'Không đọc được tệp này.',
'collections.file.errorNotACollection': 'Đây không phải tệp danh sách của TREK.',
'collections.file.formatTrek': 'Danh sách TREK',
'collections.file.formatTrekHint': 'Cho một TREK khác, kèm nhãn và trạng thái',
'collections.file.formatGpxHint': 'Điểm tham chiếu cho OsmAnd, Organic Maps, Garmin và các ứng dụng bản đồ khác',
'collections.file.gpxOmitted': '{count} địa điểm không có tọa độ nên không có trong tệp GPX',
'collections.file.gpxNothing': 'Không địa điểm nào có tọa độ, nên không có gì để đưa vào tệp GPX.',
'collections.file.reading': 'Đang đọc tệp…',
'collections.file.gpxSkipped': '{count} điểm tham chiếu không có tọa độ dùng được sẽ bị bỏ qua.',
'collections.file.gpxTrack':
'Tệp này còn có {count} điểm của vết đường đi. Vết đường đi không được nhập, chỉ các điểm tham chiếu.',
'collections.file.gpxEmpty': 'Tệp GPX này không có điểm tham chiếu nào, nên không có gì để nhập.',
'collections.file.errorNotGpx': 'Đây không phải tệp GPX.',
'collections.file.errorTooManyPlaces': 'Tệp này có hơn {count} địa điểm. Hãy chia nhỏ và nhập từng phần một.',
'collections.share.title': 'Chia sẻ danh sách',
'collections.share.titleNamed': 'Chia sẻ “{name}”',
+11
View File
@@ -158,6 +158,17 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': '這個檔案太大,不像是一份清單。',
'collections.file.errorUnreadable': '無法讀取這個檔案。',
'collections.file.errorNotACollection': '這不是 TREK 清單檔案。',
'collections.file.formatTrek': 'TREK 清單',
'collections.file.formatTrekHint': '給其他 TREK 使用,包含標籤與狀態',
'collections.file.formatGpxHint': '供 OsmAnd、Organic Maps、Garmin 等地圖 App 使用的航點',
'collections.file.gpxOmitted': '{count} 個沒有座標的地點未寫入 GPX 檔案',
'collections.file.gpxNothing': '這些地點都沒有座標,沒有可寫入 GPX 檔案的內容。',
'collections.file.reading': '正在讀取檔案…',
'collections.file.gpxSkipped': '{count} 個沒有可用座標的航點將被略過。',
'collections.file.gpxTrack': '這個檔案還包含 {count} 個軌跡點。軌跡不會匯入,只匯入航點。',
'collections.file.gpxEmpty': '這個 GPX 檔案沒有航點,沒有可匯入的內容。',
'collections.file.errorNotGpx': '這不是 GPX 檔案。',
'collections.file.errorTooManyPlaces': '這個檔案包含超過 {count} 個地點。請分割後逐一匯入。',
'collections.share.title': '分享清單',
'collections.share.titleNamed': '分享「{name}」',
+11
View File
@@ -158,6 +158,17 @@ const collection: TranslationStrings = {
'collections.file.errorTooLarge': '该文件太大,不像是一个列表。',
'collections.file.errorUnreadable': '无法读取该文件。',
'collections.file.errorNotACollection': '这不是 TREK 列表文件。',
'collections.file.formatTrek': 'TREK 列表',
'collections.file.formatTrekHint': '用于其他 TREK,包含标签和状态',
'collections.file.formatGpxHint': '供 OsmAnd、Organic Maps、Garmin 等地图应用使用的航点',
'collections.file.gpxOmitted': '{count} 个没有坐标的地点未写入 GPX 文件',
'collections.file.gpxNothing': '这些地点都没有坐标,没有可写入 GPX 文件的内容。',
'collections.file.reading': '正在读取文件…',
'collections.file.gpxSkipped': '{count} 个没有可用坐标的航点将被略过。',
'collections.file.gpxTrack': '该文件还包含 {count} 个轨迹点。轨迹不会导入,只导入航点。',
'collections.file.gpxEmpty': '该 GPX 文件中没有航点,没有可导入的内容。',
'collections.file.errorNotGpx': '这不是 GPX 文件。',
'collections.file.errorTooManyPlaces': '该文件包含超过 {count} 个地点。请拆分后逐个导入。',
'collections.share.title': '共享列表',
'collections.share.titleNamed': '共享“{name}”',