mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
feat(ferratas): keep loaded via ferratas in a tile cache on server and device, group segments, own look and count badge
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
FERRATA_TILE_DEG,
|
||||
FERRATA_TILES_PER_REQUEST_SIDE,
|
||||
ferrataTileAt,
|
||||
ferrataTileCount,
|
||||
ferrataTileKey,
|
||||
ferrataTileRange,
|
||||
ferrataTileRangeBbox,
|
||||
ferrataTileBlockKey,
|
||||
ferrataTileRequests,
|
||||
ferrataTilesOf,
|
||||
growFerrataTileRange,
|
||||
parseFerrataTileKey,
|
||||
snapFerrataBbox,
|
||||
} from './ferrataGrid';
|
||||
import { ferrataBboxSpanOk } from './ferratas.schema';
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('ferrataGrid', () => {
|
||||
it('uses one 0.05 degree cell and two cells per request side', () => {
|
||||
expect(FERRATA_TILE_DEG).toBe(0.05);
|
||||
expect(FERRATA_TILES_PER_REQUEST_SIDE).toBe(2);
|
||||
});
|
||||
|
||||
it('snaps a box outwards to the grid and clamps it to the globe', () => {
|
||||
expect(snapFerrataBbox({ south: 46.51, west: 11.72, north: 46.58, east: 11.81 })).toEqual({
|
||||
south: 46.5,
|
||||
west: 11.7,
|
||||
north: 46.6,
|
||||
east: 11.85,
|
||||
});
|
||||
// An edge already on a grid line stays where it is, float noise or not.
|
||||
expect(snapFerrataBbox({ south: 46.15, west: 11.7, north: 46.2, east: 11.75 })).toEqual({
|
||||
south: 46.15,
|
||||
west: 11.7,
|
||||
north: 46.2,
|
||||
east: 11.75,
|
||||
});
|
||||
expect(snapFerrataBbox({ south: -90, west: -180, north: 90, east: 180 })).toEqual({
|
||||
south: -90,
|
||||
west: -180,
|
||||
north: 90,
|
||||
east: 180,
|
||||
});
|
||||
});
|
||||
|
||||
it('names the tile a point lies in, a point on a grid line in the tile north east of it', () => {
|
||||
expect(ferrataTileAt(46.52, 11.81)).toEqual({ row: 930, col: 236 });
|
||||
expect(ferrataTileAt(46.15, 11.7)).toEqual({ row: 923, col: 234 });
|
||||
expect(ferrataTileAt(-0.01, -0.01)).toEqual({ row: -1, col: -1 });
|
||||
// The poles and the antimeridian stay inside the grid.
|
||||
expect(ferrataTileAt(90, 180)).toEqual({ row: 1799, col: 3599 });
|
||||
});
|
||||
|
||||
it('round-trips a tile through its key and refuses anything else', () => {
|
||||
expect(parseFerrataTileKey(ferrataTileKey({ row: -12, col: 340 }))).toEqual({ row: -12, col: 340 });
|
||||
expect(parseFerrataTileKey('930:236')).toEqual({ row: 930, col: 236 });
|
||||
expect(parseFerrataTileKey('930,236')).toBeNull();
|
||||
expect(parseFerrataTileKey('99999:1')).toBeNull();
|
||||
expect(parseFerrataTileKey('')).toBeNull();
|
||||
});
|
||||
|
||||
it('covers a box with the tiles it touches, not the next row on a grid line', () => {
|
||||
const range = ferrataTileRange({ south: 46.5, west: 11.7, north: 46.6, east: 11.81 });
|
||||
expect(range).toEqual({ minRow: 930, maxRow: 931, minCol: 234, maxCol: 236 });
|
||||
expect(ferrataTileCount(range)).toBe(6);
|
||||
expect(ferrataTilesOf(range).map(ferrataTileKey)).toEqual([
|
||||
'930:234',
|
||||
'930:235',
|
||||
'930:236',
|
||||
'931:234',
|
||||
'931:235',
|
||||
'931:236',
|
||||
]);
|
||||
expect(ferrataTileRangeBbox(range)).toEqual({ south: 46.5, west: 11.7, north: 46.6, east: 11.85 });
|
||||
});
|
||||
|
||||
it('grows a range by a ring on every side', () => {
|
||||
const range = { minRow: 930, maxRow: 931, minCol: 234, maxCol: 236 };
|
||||
expect(growFerrataTileRange(range, 1)).toEqual({ minRow: 929, maxRow: 932, minCol: 233, maxCol: 237 });
|
||||
expect(growFerrataTileRange({ minRow: 1799, maxRow: 1799, minCol: 0, maxCol: 0 }, 1).maxRow).toBe(1799);
|
||||
});
|
||||
|
||||
it('asks for tiles of one grid-aligned block with one box around them', () => {
|
||||
const requests = ferrataTileRequests([
|
||||
{ row: 930, col: 234 },
|
||||
{ row: 931, col: 235 },
|
||||
]);
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
bbox: { south: 46.5, west: 11.7, north: 46.6, east: 11.8 },
|
||||
range: { minRow: 930, maxRow: 931, minCol: 234, maxCol: 235 },
|
||||
},
|
||||
]);
|
||||
expect(ferrataTileRequests([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('splits tiles that straddle a block line along it, so each request is one block of the server', () => {
|
||||
const requests = ferrataTileRequests([
|
||||
{ row: 931, col: 235 },
|
||||
{ row: 932, col: 236 },
|
||||
]);
|
||||
expect(requests.map(({ range }) => range)).toEqual([
|
||||
{ minRow: 931, maxRow: 931, minCol: 235, maxCol: 235 },
|
||||
{ minRow: 932, maxRow: 932, minCol: 236, maxCol: 236 },
|
||||
]);
|
||||
expect(ferrataTileBlockKey(931, 235)).toBe('465:117');
|
||||
expect(ferrataTileBlockKey(-1, -3)).toBe('-1:-2');
|
||||
});
|
||||
|
||||
it('cuts a wider set into boxes of at most 0.1 degree a side, each covering only tiles it holds', () => {
|
||||
const tiles = ferrataTilesOf({ minRow: 900, maxRow: 911, minCol: 200, maxCol: 202 });
|
||||
const requests = ferrataTileRequests(tiles);
|
||||
expect(requests).toHaveLength(12);
|
||||
for (const { bbox } of requests) {
|
||||
expect(bbox.north - bbox.south).toBeLessThanOrEqual(0.1 + 1e-9);
|
||||
expect(bbox.east - bbox.west).toBeLessThanOrEqual(0.1 + 1e-9);
|
||||
}
|
||||
for (const { bbox } of requests) expect(ferrataBboxSpanOk(bbox)).toBe(true);
|
||||
const covered = requests.flatMap(({ range }) => ferrataTilesOf(range).map(ferrataTileKey)).sort();
|
||||
expect(covered).toEqual(tiles.map(ferrataTileKey).sort());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import type { FerrataBbox } from './ferratas.schema';
|
||||
|
||||
/**
|
||||
* The tile grid of the via ferrata layer, shared by both ends.
|
||||
*
|
||||
* The server caches Overpass answers per box snapped outwards to this grid,
|
||||
* and the client loads, keeps and persists the layer per tile of it. One cell
|
||||
* size on both sides means a box the client asks for is always a whole
|
||||
* number of cells, which is exactly a cache entry on the server.
|
||||
*
|
||||
* A tile is named by its row and column: row = floor(lat / 0.05), column =
|
||||
* floor(lng / 0.05). Pure, no I/O.
|
||||
*/
|
||||
|
||||
/** The cell size, in degrees, on both axes. */
|
||||
export const FERRATA_TILE_DEG = 0.05;
|
||||
|
||||
/**
|
||||
* The most tiles one Overpass query covers per side: two, a box of 0.1 degree.
|
||||
* A box of that size answers in a second or two, where one of half a degree
|
||||
* often ran into the mirrors' timeout. The client asks in blocks of this size
|
||||
* and the server queries Overpass in the same blocks, both aligned to the
|
||||
* grid (a block starts at a row and a column divisible by it), so one request
|
||||
* of the client is exactly one query of the server.
|
||||
*/
|
||||
export const FERRATA_TILES_PER_REQUEST_SIDE = 2;
|
||||
|
||||
/** Room for float noise when an edge sits exactly on a grid line (46.15 / 0.05 is 922.9999999999999). */
|
||||
const GRID_EPSILON = 1e-9;
|
||||
|
||||
const MIN_ROW = Math.round(-90 / FERRATA_TILE_DEG);
|
||||
const MAX_ROW = Math.round(90 / FERRATA_TILE_DEG) - 1;
|
||||
const MIN_COL = Math.round(-180 / FERRATA_TILE_DEG);
|
||||
const MAX_COL = Math.round(180 / FERRATA_TILE_DEG) - 1;
|
||||
|
||||
export interface FerrataTile {
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
/** A rectangle of tiles, both ends inclusive. */
|
||||
export interface FerrataTileRange {
|
||||
minRow: number;
|
||||
maxRow: number;
|
||||
minCol: number;
|
||||
maxCol: number;
|
||||
}
|
||||
|
||||
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
||||
const snapDown = (v: number) => Math.floor(v / FERRATA_TILE_DEG + GRID_EPSILON) * FERRATA_TILE_DEG;
|
||||
const snapUp = (v: number) => Math.ceil(v / FERRATA_TILE_DEG - GRID_EPSILON) * FERRATA_TILE_DEG;
|
||||
/** Every grid line is a multiple of 0.05, so two decimals write it exactly. */
|
||||
const fixed = (v: number) => Number(v.toFixed(2));
|
||||
|
||||
/**
|
||||
* The box grown outwards to the grid and clamped to the globe. The server
|
||||
* queries Overpass for this box and caches the answer under it.
|
||||
*/
|
||||
export function snapFerrataBbox(bbox: FerrataBbox): FerrataBbox {
|
||||
return {
|
||||
south: fixed(Math.max(-90, snapDown(bbox.south))),
|
||||
west: fixed(Math.max(-180, snapDown(bbox.west))),
|
||||
north: fixed(Math.min(90, snapUp(bbox.north))),
|
||||
east: fixed(Math.min(180, snapUp(bbox.east))),
|
||||
};
|
||||
}
|
||||
|
||||
/** The tile a point lies in; a point on a grid line belongs to the tile north and east of it. */
|
||||
export function ferrataTileAt(lat: number, lng: number): FerrataTile {
|
||||
return {
|
||||
row: clamp(Math.floor(lat / FERRATA_TILE_DEG + GRID_EPSILON), MIN_ROW, MAX_ROW),
|
||||
col: clamp(Math.floor(lng / FERRATA_TILE_DEG + GRID_EPSILON), MIN_COL, MAX_COL),
|
||||
};
|
||||
}
|
||||
|
||||
/** The tile's stable name, as it is kept in memory and in IndexedDB. */
|
||||
export function ferrataTileKey(tile: FerrataTile): string {
|
||||
return `${tile.row}:${tile.col}`;
|
||||
}
|
||||
|
||||
/** The tile a key names, or null for anything that is not one. */
|
||||
export function parseFerrataTileKey(key: string): FerrataTile | null {
|
||||
const match = /^(-?\d+):(-?\d+)$/.exec(key);
|
||||
if (!match) return null;
|
||||
const row = Number(match[1]);
|
||||
const col = Number(match[2]);
|
||||
if (row < MIN_ROW || row > MAX_ROW || col < MIN_COL || col > MAX_COL) return null;
|
||||
return { row, col };
|
||||
}
|
||||
|
||||
/** The tiles a box touches, as a range. A box on a grid line does not reach into the next row or column. */
|
||||
export function ferrataTileRange(bbox: FerrataBbox): FerrataTileRange {
|
||||
const sw = ferrataTileAt(bbox.south, bbox.west);
|
||||
const lastRow = Math.ceil(bbox.north / FERRATA_TILE_DEG - GRID_EPSILON) - 1;
|
||||
const lastCol = Math.ceil(bbox.east / FERRATA_TILE_DEG - GRID_EPSILON) - 1;
|
||||
return {
|
||||
minRow: sw.row,
|
||||
maxRow: clamp(Math.max(sw.row, lastRow), MIN_ROW, MAX_ROW),
|
||||
minCol: sw.col,
|
||||
maxCol: clamp(Math.max(sw.col, lastCol), MIN_COL, MAX_COL),
|
||||
};
|
||||
}
|
||||
|
||||
/** The range grown by `rings` tiles on every side, inside the globe. */
|
||||
export function growFerrataTileRange(range: FerrataTileRange, rings: number): FerrataTileRange {
|
||||
return {
|
||||
minRow: clamp(range.minRow - rings, MIN_ROW, MAX_ROW),
|
||||
maxRow: clamp(range.maxRow + rings, MIN_ROW, MAX_ROW),
|
||||
minCol: clamp(range.minCol - rings, MIN_COL, MAX_COL),
|
||||
maxCol: clamp(range.maxCol + rings, MIN_COL, MAX_COL),
|
||||
};
|
||||
}
|
||||
|
||||
/** How many tiles a range holds. */
|
||||
export function ferrataTileCount(range: FerrataTileRange): number {
|
||||
return (range.maxRow - range.minRow + 1) * (range.maxCol - range.minCol + 1);
|
||||
}
|
||||
|
||||
/** Every tile of a range, row by row from the south west. */
|
||||
export function ferrataTilesOf(range: FerrataTileRange): FerrataTile[] {
|
||||
const tiles: FerrataTile[] = [];
|
||||
for (let row = range.minRow; row <= range.maxRow; row += 1) {
|
||||
for (let col = range.minCol; col <= range.maxCol; col += 1) tiles.push({ row, col });
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
/** The box a range of tiles covers, edges on the grid. */
|
||||
export function ferrataTileRangeBbox(range: FerrataTileRange): FerrataBbox {
|
||||
return {
|
||||
south: fixed(Math.max(-90, range.minRow * FERRATA_TILE_DEG)),
|
||||
west: fixed(Math.max(-180, range.minCol * FERRATA_TILE_DEG)),
|
||||
north: fixed(Math.min(90, (range.maxRow + 1) * FERRATA_TILE_DEG)),
|
||||
east: fixed(Math.min(180, (range.maxCol + 1) * FERRATA_TILE_DEG)),
|
||||
};
|
||||
}
|
||||
|
||||
/** The smallest range holding every tile, or null for none. */
|
||||
function enclosing(tiles: readonly FerrataTile[]): FerrataTileRange | null {
|
||||
if (tiles.length === 0) return null;
|
||||
let minRow = Infinity;
|
||||
let maxRow = -Infinity;
|
||||
let minCol = Infinity;
|
||||
let maxCol = -Infinity;
|
||||
for (const { row, col } of tiles) {
|
||||
minRow = Math.min(minRow, row);
|
||||
maxRow = Math.max(maxRow, row);
|
||||
minCol = Math.min(minCol, col);
|
||||
maxCol = Math.max(maxCol, col);
|
||||
}
|
||||
return { minRow, maxRow, minCol, maxCol };
|
||||
}
|
||||
|
||||
/** One request: the box to ask for and the range of tiles its answer completes. */
|
||||
export interface FerrataTileRequest {
|
||||
bbox: FerrataBbox;
|
||||
range: FerrataTileRange;
|
||||
}
|
||||
|
||||
/** The grid-aligned block of FERRATA_TILES_PER_REQUEST_SIDE tiles a side that a tile falls in. */
|
||||
export function ferrataTileBlockKey(row: number, col: number): string {
|
||||
const side = FERRATA_TILES_PER_REQUEST_SIDE;
|
||||
return `${Math.floor(row / side)}:${Math.floor(col / side)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The requests that load `tiles`: one per grid-aligned block of
|
||||
* FERRATA_TILES_PER_REQUEST_SIDE tiles a side that holds any of them, each box
|
||||
* shrunk to the tiles it holds. Every box is a whole number of tiles and well
|
||||
* within the server's span limit, so none is refused, each answer completes
|
||||
* every tile its box covers, and the server answers it with one query.
|
||||
*/
|
||||
export function ferrataTileRequests(tiles: readonly FerrataTile[]): FerrataTileRequest[] {
|
||||
const blocks = new Map<string, FerrataTile[]>();
|
||||
for (const tile of tiles) {
|
||||
const key = ferrataTileBlockKey(tile.row, tile.col);
|
||||
const block = blocks.get(key);
|
||||
if (block) block.push(tile);
|
||||
else blocks.set(key, [tile]);
|
||||
}
|
||||
const requests: FerrataTileRequest[] = [];
|
||||
for (const block of blocks.values()) {
|
||||
const range = enclosing(block);
|
||||
if (range) requests.push({ bbox: ferrataTileRangeBbox(range), range });
|
||||
}
|
||||
return requests;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'قرّب الخريطة لتحميل مسارات التسلق الحديدية',
|
||||
'ferratas.offline': 'تحتاج مسارات التسلق الحديدية إلى اتصال',
|
||||
'ferratas.unknownGrade': 'درجة الصعوبة غير معروفة',
|
||||
'ferratas.gradeRange': '{from} إلى {to}',
|
||||
'ferratas.unnamed': 'مسار تسلق حديدي بلا اسم',
|
||||
'ferratas.viewInOsm': 'عرض في OpenStreetMap',
|
||||
'ferratas.attribution': 'البيانات © مساهمو OpenStreetMap',
|
||||
|
||||
@@ -65,6 +65,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'اسم الجولة',
|
||||
'tours.planner.namePlaceholder': 'جولة مشي بلا اسم',
|
||||
'tours.planner.mapHint': 'انقر على الخريطة لإضافة نقاط.',
|
||||
'tours.planner.mapSearch': 'ابحث عن مكان أو عنوان',
|
||||
'tours.planner.waypoints': 'نقاط المرور',
|
||||
'tours.planner.waypointLabel': 'نقطة المرور {n}',
|
||||
'tours.durationMinutes': '{count} د',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Via ferrata marşrutlarını yükləmək üçün yaxınlaşdırın',
|
||||
'ferratas.offline': 'Via ferrata marşrutları üçün bağlantı lazımdır',
|
||||
'ferratas.unknownGrade': 'Çətinlik naməlumdur',
|
||||
'ferratas.gradeRange': '{from} - {to}',
|
||||
'ferratas.unnamed': 'Adsız via ferrata marşrutu',
|
||||
'ferratas.viewInOsm': 'OpenStreetMap-də bax',
|
||||
'ferratas.attribution': 'Məlumatlar © OpenStreetMap iştirakçıları',
|
||||
|
||||
@@ -63,6 +63,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Turun adı',
|
||||
'tours.planner.namePlaceholder': 'Adsız yürüş',
|
||||
'tours.planner.mapHint': 'Nöqtə əlavə etmək üçün xəritəyə klikləyin.',
|
||||
'tours.planner.mapSearch': 'Yer və ya ünvan axtar',
|
||||
'tours.planner.waypoints': 'Yol nöqtələri',
|
||||
'tours.planner.waypointLabel': 'Yol nöqtəsi {n}',
|
||||
'tours.durationMinutes': '{count} dəq.',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Aproxime o mapa para carregar vias ferratas',
|
||||
'ferratas.offline': 'As vias ferratas precisam de conexão',
|
||||
'ferratas.unknownGrade': 'Dificuldade desconhecida',
|
||||
'ferratas.gradeRange': '{from} a {to}',
|
||||
'ferratas.unnamed': 'Via ferrata sem nome',
|
||||
'ferratas.viewInOsm': 'Ver no OpenStreetMap',
|
||||
'ferratas.attribution': 'Dados © colaboradores do OpenStreetMap',
|
||||
|
||||
@@ -64,6 +64,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Nome do passeio',
|
||||
'tours.planner.namePlaceholder': 'Trilha sem título',
|
||||
'tours.planner.mapHint': 'Clique no mapa para adicionar pontos.',
|
||||
'tours.planner.mapSearch': 'Buscar lugar ou endereço',
|
||||
'tours.planner.waypoints': 'Pontos de passagem',
|
||||
'tours.planner.waypointLabel': 'Ponto de passagem {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Apropa el mapa per carregar vies ferrades',
|
||||
'ferratas.offline': 'Les vies ferrades necessiten connexió',
|
||||
'ferratas.unknownGrade': 'Dificultat desconeguda',
|
||||
'ferratas.gradeRange': '{from} a {to}',
|
||||
'ferratas.unnamed': 'Via ferrada sense nom',
|
||||
'ferratas.viewInOsm': 'Mostra a OpenStreetMap',
|
||||
'ferratas.attribution': "Dades © col·laboradors d'OpenStreetMap",
|
||||
|
||||
@@ -63,6 +63,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': "Nom de l'excursió",
|
||||
'tours.planner.namePlaceholder': 'Ruta de senderisme sense títol',
|
||||
'tours.planner.mapHint': 'Fes clic al mapa per afegir punts.',
|
||||
'tours.planner.mapSearch': 'Cerca un lloc o una adreça',
|
||||
'tours.planner.waypoints': 'Punts de pas',
|
||||
'tours.planner.waypointLabel': 'Punt de pas {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -10,6 +10,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Přibližte mapu pro načtení ferrat',
|
||||
'ferratas.offline': 'Ferraty vyžadují připojení',
|
||||
'ferratas.unknownGrade': 'Obtížnost neznámá',
|
||||
'ferratas.gradeRange': '{from} až {to}',
|
||||
'ferratas.unnamed': 'Ferrata bez názvu',
|
||||
'ferratas.viewInOsm': 'Zobrazit v OpenStreetMap',
|
||||
'ferratas.attribution': 'Data © přispěvatelé OpenStreetMap',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Název výletu',
|
||||
'tours.planner.namePlaceholder': 'Túra bez názvu',
|
||||
'tours.planner.mapHint': 'Kliknutím do mapy přidáte body.',
|
||||
'tours.planner.mapSearch': 'Hledat místo nebo adresu',
|
||||
'tours.planner.waypoints': 'Průjezdní body',
|
||||
'tours.planner.waypointLabel': 'Bod {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Für Klettersteige näher heranzoomen',
|
||||
'ferratas.offline': 'Klettersteige brauchen eine Verbindung',
|
||||
'ferratas.unknownGrade': 'Schwierigkeit unbekannt',
|
||||
'ferratas.gradeRange': '{from} bis {to}',
|
||||
'ferratas.unnamed': 'Klettersteig ohne Namen',
|
||||
'ferratas.viewInOsm': 'In OpenStreetMap ansehen',
|
||||
'ferratas.attribution': 'Daten © OpenStreetMap-Mitwirkende',
|
||||
|
||||
@@ -63,6 +63,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Name der Tour',
|
||||
'tours.planner.namePlaceholder': 'Wanderung ohne Titel',
|
||||
'tours.planner.mapHint': 'Klicke auf die Karte, um Punkte zu setzen.',
|
||||
'tours.planner.mapSearch': 'Ort oder Adresse suchen',
|
||||
'tours.planner.waypoints': 'Wegpunkte',
|
||||
'tours.planner.waypointLabel': 'Wegpunkt {n}',
|
||||
'tours.durationMinutes': '{count} Min.',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Zoom in to load via ferratas',
|
||||
'ferratas.offline': 'Via ferratas need a connection',
|
||||
'ferratas.unknownGrade': 'Grade unknown',
|
||||
'ferratas.gradeRange': '{from} to {to}',
|
||||
'ferratas.unnamed': 'Unnamed via ferrata',
|
||||
'ferratas.viewInOsm': 'View in OpenStreetMap',
|
||||
'ferratas.attribution': 'Data © OpenStreetMap contributors',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Tour name',
|
||||
'tours.planner.namePlaceholder': 'Untitled hike',
|
||||
'tours.planner.mapHint': 'Click the map to add points.',
|
||||
'tours.planner.mapSearch': 'Search place or address',
|
||||
'tours.planner.waypoints': 'Waypoints',
|
||||
'tours.planner.waypointLabel': 'Waypoint {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Acerca el mapa para cargar vías ferratas',
|
||||
'ferratas.offline': 'Las vías ferratas necesitan conexión',
|
||||
'ferratas.unknownGrade': 'Dificultad desconocida',
|
||||
'ferratas.gradeRange': '{from} a {to}',
|
||||
'ferratas.unnamed': 'Vía ferrata sin nombre',
|
||||
'ferratas.viewInOsm': 'Ver en OpenStreetMap',
|
||||
'ferratas.attribution': 'Datos © colaboradores de OpenStreetMap',
|
||||
|
||||
@@ -63,6 +63,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Nombre de la excursión',
|
||||
'tours.planner.namePlaceholder': 'Ruta de senderismo sin título',
|
||||
'tours.planner.mapHint': 'Haz clic en el mapa para añadir puntos.',
|
||||
'tours.planner.mapSearch': 'Buscar lugar o dirección',
|
||||
'tours.planner.waypoints': 'Puntos de paso',
|
||||
'tours.planner.waypointLabel': 'Punto de paso {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Via ferrata radade laadimiseks suumi lähemale',
|
||||
'ferratas.offline': 'Via ferrata rajad vajavad ühendust',
|
||||
'ferratas.unknownGrade': 'Raskusaste teadmata',
|
||||
'ferratas.gradeRange': '{from} kuni {to}',
|
||||
'ferratas.unnamed': 'Nimeta via ferrata',
|
||||
'ferratas.viewInOsm': 'Vaata OpenStreetMapis',
|
||||
'ferratas.attribution': 'Andmed © OpenStreetMapi kaastöötajad',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Retke nimi',
|
||||
'tours.planner.namePlaceholder': 'Nimetu matk',
|
||||
'tours.planner.mapHint': 'Klõpsa kaardil punktide lisamiseks.',
|
||||
'tours.planner.mapSearch': 'Otsi kohta või aadressi',
|
||||
'tours.planner.waypoints': 'Teekonnapunktid',
|
||||
'tours.planner.waypointLabel': 'Teekonnapunkt {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Zoomez pour charger les via ferrata',
|
||||
'ferratas.offline': 'Les via ferrata nécessitent une connexion',
|
||||
'ferratas.unknownGrade': 'Difficulté inconnue',
|
||||
'ferratas.gradeRange': '{from} à {to}',
|
||||
'ferratas.unnamed': 'Via ferrata sans nom',
|
||||
'ferratas.viewInOsm': 'Voir dans OpenStreetMap',
|
||||
'ferratas.attribution': 'Données © contributeurs OpenStreetMap',
|
||||
|
||||
@@ -65,6 +65,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Nom de la sortie',
|
||||
'tours.planner.namePlaceholder': 'Randonnée sans titre',
|
||||
'tours.planner.mapHint': 'Cliquez sur la carte pour ajouter des points.',
|
||||
'tours.planner.mapSearch': 'Rechercher un lieu ou une adresse',
|
||||
'tours.planner.waypoints': 'Points de passage',
|
||||
'tours.planner.waypointLabel': 'Point de passage {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Μεγεθύνετε για να φορτωθούν διαδρομές via ferrata',
|
||||
'ferratas.offline': 'Οι διαδρομές via ferrata χρειάζονται σύνδεση',
|
||||
'ferratas.unknownGrade': 'Άγνωστη δυσκολία',
|
||||
'ferratas.gradeRange': '{from} έως {to}',
|
||||
'ferratas.unnamed': 'Διαδρομή via ferrata χωρίς όνομα',
|
||||
'ferratas.viewInOsm': 'Προβολή στο OpenStreetMap',
|
||||
'ferratas.attribution': 'Δεδομένα © συντελεστές του OpenStreetMap',
|
||||
|
||||
@@ -65,6 +65,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Όνομα εκδρομής',
|
||||
'tours.planner.namePlaceholder': 'Πεζοπορία χωρίς όνομα',
|
||||
'tours.planner.mapHint': 'Κάντε κλικ στον χάρτη για να προσθέσετε σημεία.',
|
||||
'tours.planner.mapSearch': 'Αναζήτηση τόπου ή διεύθυνσης',
|
||||
'tours.planner.waypoints': 'Σημεία διέλευσης',
|
||||
'tours.planner.waypointLabel': 'Σημείο διέλευσης {n}',
|
||||
'tours.durationMinutes': '{count} λεπτά',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Nagyíts rá a vasalt utak betöltéséhez',
|
||||
'ferratas.offline': 'A vasalt utakhoz kapcsolat szükséges',
|
||||
'ferratas.unknownGrade': 'Ismeretlen nehézség',
|
||||
'ferratas.gradeRange': '{from}–{to}',
|
||||
'ferratas.unnamed': 'Névtelen vasalt út',
|
||||
'ferratas.viewInOsm': 'Megtekintés az OpenStreetMapen',
|
||||
'ferratas.attribution': 'Adatok © OpenStreetMap-közreműködők',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Túra neve',
|
||||
'tours.planner.namePlaceholder': 'Névtelen gyalogtúra',
|
||||
'tours.planner.mapHint': 'Kattints a térképre pontok hozzáadásához.',
|
||||
'tours.planner.mapSearch': 'Hely vagy cím keresése',
|
||||
'tours.planner.waypoints': 'Útvonalpontok',
|
||||
'tours.planner.waypointLabel': '{n}. útvonalpont',
|
||||
'tours.durationMinutes': '{count} perc',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Perbesar peta untuk memuat jalur via ferrata',
|
||||
'ferratas.offline': 'Jalur via ferrata memerlukan koneksi',
|
||||
'ferratas.unknownGrade': 'Tingkat kesulitan tidak diketahui',
|
||||
'ferratas.gradeRange': '{from} hingga {to}',
|
||||
'ferratas.unnamed': 'Via ferrata tanpa nama',
|
||||
'ferratas.viewInOsm': 'Lihat di OpenStreetMap',
|
||||
'ferratas.attribution': 'Data © kontributor OpenStreetMap',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Nama tur',
|
||||
'tours.planner.namePlaceholder': 'Pendakian tanpa judul',
|
||||
'tours.planner.mapHint': 'Klik peta untuk menambahkan titik.',
|
||||
'tours.planner.mapSearch': 'Cari tempat atau alamat',
|
||||
'tours.planner.waypoints': 'Titik lintasan',
|
||||
'tours.planner.waypointLabel': 'Titik lintasan {n}',
|
||||
'tours.durationMinutes': '{count} menit',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Ingrandisci per caricare le vie ferrate',
|
||||
'ferratas.offline': 'Le vie ferrate richiedono una connessione',
|
||||
'ferratas.unknownGrade': 'Difficoltà sconosciuta',
|
||||
'ferratas.gradeRange': 'da {from} a {to}',
|
||||
'ferratas.unnamed': 'Via ferrata senza nome',
|
||||
'ferratas.viewInOsm': 'Vedi su OpenStreetMap',
|
||||
'ferratas.attribution': 'Dati © contributori di OpenStreetMap',
|
||||
|
||||
@@ -64,6 +64,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': "Nome dell'escursione",
|
||||
'tours.planner.namePlaceholder': 'Escursione a piedi senza titolo',
|
||||
'tours.planner.mapHint': 'Fai clic sulla mappa per aggiungere punti.',
|
||||
'tours.planner.mapSearch': 'Cerca luogo o indirizzo',
|
||||
'tours.planner.waypoints': 'Punti di passaggio',
|
||||
'tours.planner.waypointLabel': 'Punto di passaggio {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'ヴィア・フェラータを読み込むには拡大してください',
|
||||
'ferratas.offline': 'ヴィア・フェラータの表示には接続が必要です',
|
||||
'ferratas.unknownGrade': '難易度不明',
|
||||
'ferratas.gradeRange': '{from}〜{to}',
|
||||
'ferratas.unnamed': '名称未設定のヴィア・フェラータ',
|
||||
'ferratas.viewInOsm': 'OpenStreetMap で表示',
|
||||
'ferratas.attribution': 'データ © OpenStreetMap 貢献者',
|
||||
|
||||
@@ -60,6 +60,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'ツアー名',
|
||||
'tours.planner.namePlaceholder': '名前のないハイキング',
|
||||
'tours.planner.mapHint': '地図をクリックして地点を追加します。',
|
||||
'tours.planner.mapSearch': '場所や住所を検索',
|
||||
'tours.planner.waypoints': 'ルート上の地点',
|
||||
'tours.planner.waypointLabel': '地点{n}',
|
||||
'tours.durationMinutes': '{count}分',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': '비아 페라타를 불러오려면 확대하세요',
|
||||
'ferratas.offline': '비아 페라타를 보려면 연결이 필요합니다',
|
||||
'ferratas.unknownGrade': '난이도 알 수 없음',
|
||||
'ferratas.gradeRange': '{from}~{to}',
|
||||
'ferratas.unnamed': '이름 없는 비아 페라타',
|
||||
'ferratas.viewInOsm': 'OpenStreetMap에서 보기',
|
||||
'ferratas.attribution': '데이터 © OpenStreetMap 기여자',
|
||||
|
||||
@@ -61,6 +61,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': '투어 이름',
|
||||
'tours.planner.namePlaceholder': '이름 없는 하이킹',
|
||||
'tours.planner.mapHint': '지도를 클릭해 지점을 추가하세요.',
|
||||
'tours.planner.mapSearch': '장소 또는 주소 검색',
|
||||
'tours.planner.waypoints': '경로 지점',
|
||||
'tours.planner.waypointLabel': '지점 {n}',
|
||||
'tours.durationMinutes': '{count}분',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': "Zoom in om via ferrata's te laden",
|
||||
'ferratas.offline': "Via ferrata's hebben een verbinding nodig",
|
||||
'ferratas.unknownGrade': 'Moeilijkheid onbekend',
|
||||
'ferratas.gradeRange': '{from} tot {to}',
|
||||
'ferratas.unnamed': 'Via ferrata zonder naam',
|
||||
'ferratas.viewInOsm': 'Bekijken in OpenStreetMap',
|
||||
'ferratas.attribution': 'Gegevens © OpenStreetMap-bijdragers',
|
||||
|
||||
@@ -63,6 +63,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Naam van de tocht',
|
||||
'tours.planner.namePlaceholder': 'Naamloze wandeling',
|
||||
'tours.planner.mapHint': 'Klik op de kaart om punten toe te voegen.',
|
||||
'tours.planner.mapSearch': 'Zoek plaats of adres',
|
||||
'tours.planner.waypoints': 'Routepunten',
|
||||
'tours.planner.waypointLabel': 'Routepunt {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -11,6 +11,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Przybliż mapę, aby wczytać ferraty',
|
||||
'ferratas.offline': 'Ferraty wymagają połączenia',
|
||||
'ferratas.unknownGrade': 'Nieznana trudność',
|
||||
'ferratas.gradeRange': 'od {from} do {to}',
|
||||
'ferratas.unnamed': 'Ferrata bez nazwy',
|
||||
'ferratas.viewInOsm': 'Pokaż w OpenStreetMap',
|
||||
'ferratas.attribution': 'Dane © współtwórcy OpenStreetMap',
|
||||
|
||||
@@ -65,6 +65,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Nazwa wycieczki',
|
||||
'tours.planner.namePlaceholder': 'Wędrówka bez nazwy',
|
||||
'tours.planner.mapHint': 'Kliknij mapę, aby dodać punkty.',
|
||||
'tours.planner.mapSearch': 'Szukaj miejsca lub adresu',
|
||||
'tours.planner.waypoints': 'Punkty trasy',
|
||||
'tours.planner.waypointLabel': 'Punkt trasy {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -11,6 +11,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Приблизьте карту, чтобы загрузить виа ферраты',
|
||||
'ferratas.offline': 'Для виа феррат нужно подключение',
|
||||
'ferratas.unknownGrade': 'Сложность неизвестна',
|
||||
'ferratas.gradeRange': 'от {from} до {to}',
|
||||
'ferratas.unnamed': 'Виа феррата без названия',
|
||||
'ferratas.viewInOsm': 'Открыть в OpenStreetMap',
|
||||
'ferratas.attribution': 'Данные © участники OpenStreetMap',
|
||||
|
||||
@@ -65,6 +65,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Название тура',
|
||||
'tours.planner.namePlaceholder': 'Поход без названия',
|
||||
'tours.planner.mapHint': 'Нажмите на карту, чтобы добавить точки.',
|
||||
'tours.planner.mapSearch': 'Поиск места или адреса',
|
||||
'tours.planner.waypoints': 'Путевые точки',
|
||||
'tours.planner.waypointLabel': 'Путевая точка {n}',
|
||||
'tours.durationMinutes': '{count} мин',
|
||||
|
||||
@@ -10,6 +10,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Priblížte mapu na načítanie ferrát',
|
||||
'ferratas.offline': 'Ferraty vyžadujú pripojenie',
|
||||
'ferratas.unknownGrade': 'Neznáma obťažnosť',
|
||||
'ferratas.gradeRange': '{from} až {to}',
|
||||
'ferratas.unnamed': 'Ferrata bez názvu',
|
||||
'ferratas.viewInOsm': 'Zobraziť v OpenStreetMap',
|
||||
'ferratas.attribution': 'Údaje © prispievatelia OpenStreetMap',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Názov túry',
|
||||
'tours.planner.namePlaceholder': 'Pešia túra bez názvu',
|
||||
'tours.planner.mapHint': 'Kliknutím do mapy pridáte body.',
|
||||
'tours.planner.mapSearch': 'Hľadať miesto alebo adresu',
|
||||
'tours.planner.waypoints': 'Body trasy',
|
||||
'tours.planner.waypointLabel': 'Bod trasy {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Zooma in för att läsa in via ferrata-leder',
|
||||
'ferratas.offline': 'Via ferrata-leder kräver en anslutning',
|
||||
'ferratas.unknownGrade': 'Okänd svårighetsgrad',
|
||||
'ferratas.gradeRange': '{from} till {to}',
|
||||
'ferratas.unnamed': 'Namnlös via ferrata',
|
||||
'ferratas.viewInOsm': 'Visa i OpenStreetMap',
|
||||
'ferratas.attribution': 'Data © OpenStreetMap-bidragsgivare',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Turens namn',
|
||||
'tours.planner.namePlaceholder': 'Vandring utan namn',
|
||||
'tours.planner.mapHint': 'Klicka på kartan för att lägga till punkter.',
|
||||
'tours.planner.mapSearch': 'Sök plats eller adress',
|
||||
'tours.planner.waypoints': 'Ruttpunkter',
|
||||
'tours.planner.waypointLabel': 'Ruttpunkt {n}',
|
||||
'tours.durationMinutes': '{count} min',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'ซูมเข้าเพื่อโหลดเส้นทางเวียเฟอร์ราตา',
|
||||
'ferratas.offline': 'เส้นทางเวียเฟอร์ราตาต้องใช้การเชื่อมต่อ',
|
||||
'ferratas.unknownGrade': 'ไม่ทราบระดับความยาก',
|
||||
'ferratas.gradeRange': '{from} ถึง {to}',
|
||||
'ferratas.unnamed': 'เส้นทางเวียเฟอร์ราตาไม่มีชื่อ',
|
||||
'ferratas.viewInOsm': 'ดูใน OpenStreetMap',
|
||||
'ferratas.attribution': 'ข้อมูล © ผู้ร่วมให้ข้อมูล OpenStreetMap',
|
||||
|
||||
@@ -61,6 +61,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'ชื่อทัวร์',
|
||||
'tours.planner.namePlaceholder': 'เส้นทางเดินป่าที่ยังไม่มีชื่อ',
|
||||
'tours.planner.mapHint': 'คลิกแผนที่เพื่อเพิ่มจุด',
|
||||
'tours.planner.mapSearch': 'ค้นหาสถานที่หรือที่อยู่',
|
||||
'tours.planner.waypoints': 'จุดแวะ',
|
||||
'tours.planner.waypointLabel': 'จุดแวะ {n}',
|
||||
'tours.durationMinutes': '{count} นาที',
|
||||
|
||||
@@ -20,7 +20,7 @@ const tokens = (value: string) => [...value.matchAll(/\{[^{}]+\}/g)].map((match)
|
||||
// placeholder spec; here only the general keys are.
|
||||
const groups = pluralGroups(Object.keys(english));
|
||||
const generalKeys = (keys: string[]) => keys.filter((key) => !pluralFormOf(key, groups));
|
||||
const GENERAL_KEY_COUNT = 147;
|
||||
const GENERAL_KEY_COUNT = 148;
|
||||
|
||||
function literalKeys(source: string): string[] {
|
||||
const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true);
|
||||
@@ -34,7 +34,7 @@ function literalKeys(source: string): string[] {
|
||||
}
|
||||
|
||||
describe('Tours locale contracts', () => {
|
||||
it('covers all 27 supported locales with 147 general source keys', () => {
|
||||
it('covers all 27 supported locales with 148 general source keys', () => {
|
||||
expect(Object.keys(domains)).toHaveLength(27);
|
||||
expect(Object.keys(domains).sort()).toEqual(SUPPORTED_LANGUAGE_CODES.map((code) => `./${code}/tours.ts`).sort());
|
||||
expect(generalKeys(Object.keys(english))).toHaveLength(GENERAL_KEY_COUNT);
|
||||
|
||||
@@ -9,6 +9,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Via ferrata rotalarını yüklemek için yakınlaştırın',
|
||||
'ferratas.offline': 'Via ferrata rotaları için bağlantı gerekiyor',
|
||||
'ferratas.unknownGrade': 'Zorluk bilinmiyor',
|
||||
'ferratas.gradeRange': '{from} ile {to} arası',
|
||||
'ferratas.unnamed': 'Adsız via ferrata',
|
||||
'ferratas.viewInOsm': "OpenStreetMap'te görüntüle",
|
||||
'ferratas.attribution': 'Veriler © OpenStreetMap katkıcıları',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Tur adı',
|
||||
'tours.planner.namePlaceholder': 'Adsız doğa yürüyüşü',
|
||||
'tours.planner.mapHint': 'Nokta eklemek için haritaya tıklayın.',
|
||||
'tours.planner.mapSearch': 'Yer veya adres ara',
|
||||
'tours.planner.waypoints': 'Rota noktaları',
|
||||
'tours.planner.waypointLabel': 'Rota noktası {n}',
|
||||
'tours.durationMinutes': '{count} dk',
|
||||
|
||||
@@ -11,6 +11,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Наблизьте мапу, щоб завантажити віа ферати',
|
||||
'ferratas.offline': 'Для віа ферат потрібне з’єднання',
|
||||
'ferratas.unknownGrade': 'Складність невідома',
|
||||
'ferratas.gradeRange': 'від {from} до {to}',
|
||||
'ferratas.unnamed': 'Віа ферата без назви',
|
||||
'ferratas.viewInOsm': 'Відкрити в OpenStreetMap',
|
||||
'ferratas.attribution': 'Дані © учасники OpenStreetMap',
|
||||
|
||||
@@ -64,6 +64,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Назва туру',
|
||||
'tours.planner.namePlaceholder': 'Похід без назви',
|
||||
'tours.planner.mapHint': 'Натисніть на карту, щоб додати точки.',
|
||||
'tours.planner.mapSearch': 'Пошук місця або адреси',
|
||||
'tours.planner.waypoints': 'Маршрутні точки',
|
||||
'tours.planner.waypointLabel': 'Маршрутна точка {n}',
|
||||
'tours.durationMinutes': '{count} хв',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': 'Phóng to để tải các tuyến via ferrata',
|
||||
'ferratas.offline': 'Các tuyến via ferrata cần kết nối mạng',
|
||||
'ferratas.unknownGrade': 'Không rõ độ khó',
|
||||
'ferratas.gradeRange': '{from} đến {to}',
|
||||
'ferratas.unnamed': 'Via ferrata chưa có tên',
|
||||
'ferratas.viewInOsm': 'Xem trên OpenStreetMap',
|
||||
'ferratas.attribution': 'Dữ liệu © những người đóng góp OpenStreetMap',
|
||||
|
||||
@@ -62,6 +62,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': 'Tên chuyến tham quan',
|
||||
'tours.planner.namePlaceholder': 'Chuyến đi bộ đường dài chưa có tên',
|
||||
'tours.planner.mapHint': 'Nhấp vào bản đồ để thêm điểm.',
|
||||
'tours.planner.mapSearch': 'Tìm địa điểm hoặc địa chỉ',
|
||||
'tours.planner.waypoints': 'Các điểm trên tuyến',
|
||||
'tours.planner.waypointLabel': 'Điểm {n}',
|
||||
'tours.durationMinutes': '{count} phút',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': '放大地圖以載入飛拉達',
|
||||
'ferratas.offline': '飛拉達需要網路連線',
|
||||
'ferratas.unknownGrade': '難度不明',
|
||||
'ferratas.gradeRange': '{from} 至 {to}',
|
||||
'ferratas.unnamed': '未命名的飛拉達',
|
||||
'ferratas.viewInOsm': '在 OpenStreetMap 中檢視',
|
||||
'ferratas.attribution': '資料 © OpenStreetMap 貢獻者',
|
||||
|
||||
@@ -58,6 +58,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': '遊程名稱',
|
||||
'tours.planner.namePlaceholder': '未命名的健行',
|
||||
'tours.planner.mapHint': '點選地圖以加入地點。',
|
||||
'tours.planner.mapSearch': '搜尋地點或地址',
|
||||
'tours.planner.waypoints': '路線點',
|
||||
'tours.planner.waypointLabel': '路線點 {n}',
|
||||
'tours.durationMinutes': '{count} 分鐘',
|
||||
|
||||
@@ -8,6 +8,7 @@ const ferratas: TranslationStrings = {
|
||||
'ferratas.zoomIn': '放大地图以加载飞拉达',
|
||||
'ferratas.offline': '飞拉达需要网络连接',
|
||||
'ferratas.unknownGrade': '难度未知',
|
||||
'ferratas.gradeRange': '{from} 至 {to}',
|
||||
'ferratas.unnamed': '未命名飞拉达',
|
||||
'ferratas.viewInOsm': '在 OpenStreetMap 中查看',
|
||||
'ferratas.attribution': '数据 © OpenStreetMap 贡献者',
|
||||
|
||||
@@ -58,6 +58,7 @@ const tours: TranslationStrings = {
|
||||
'tours.planner.name': '游览名称',
|
||||
'tours.planner.namePlaceholder': '未命名的徒步活动',
|
||||
'tours.planner.mapHint': '点击地图添加地点。',
|
||||
'tours.planner.mapSearch': '搜索地点或地址',
|
||||
'tours.planner.waypoints': '路线点',
|
||||
'tours.planner.waypointLabel': '路线点 {n}',
|
||||
'tours.durationMinutes': '{count} 分钟',
|
||||
|
||||
@@ -118,3 +118,5 @@ export * from './permissions/permissions';
|
||||
|
||||
// Via ferratas around a trip, from OpenStreetMap through Overpass (GET /api/trips/:tripId/ferratas).
|
||||
export * from './ferratas/ferratas.schema';
|
||||
// The 0.05 degree tile grid both ends load, cache and persist the layer on.
|
||||
export * from './ferratas/ferrataGrid';
|
||||
|
||||
Reference in New Issue
Block a user