Starting point: Vue 3 + Vite frontend with a PHP 8.5.12 / Symfony 8.1 backend (FrankenPHP via Docker).
├── index.html, vite.config.ts, src/ # Vue frontend (root)
├── scripts/
│ ├── testing/ # Browser checks and frontend translation tests
│ ├── production/ # Production map and translation data generators
│ └── data/ # Cached downloads and intermediate data (git-ignored)
└── backend/ # Symfony backend (Docker Compose)
├── compose.yaml # php (FrankenPHP) + database (PostGIS)
├── Dockerfile # frankenphp_dev / frankenphp_prod targets
├── frankenphp/ # Caddyfile + PHP config
├── tag-translations/ # Shared XLIFF tag key/value catalogs
└── src/, config/, public/ # Symfony app
cd backend
docker compose up -d --wait- API: http://localhost:8080/api/hello
- Database: PostGIS on localhost:5433 (database
map; credentials frombackend/.env)
The first start runs composer install inside the container (Flex generates the
bundle configuration in config/packages/).
Tests:
docker compose exec php vendor/bin/phpunitTests use the configured database credentials and append _test to the database
name (map_test by default). Changing POSTGRES_PASSWORD in .env does not
update the password stored in an existing PostgreSQL volume; the database role's
password must also be updated.
npm install
npm run dev- App: http://localhost:5173
/apirequests are proxied to the backend on port 8080 (seevite.config.ts)
Production build: npm run build (type-checks with vue-tsc, then Vite build incl. PWA assets).
Run the commands in this section from the repository root.
scripts/production/ prepares map data and collects tag translation entries.
scripts/testing/ contains browser checks and frontend translation tests; Symfony
tests stay in backend/tests/. See Tag translations for the
complete extraction, translation and validation workflow.
Update translation files from both OSM node data and the backend with one command in your host terminal, from the repository root:
npm run translations:syncIt prepares the Flensburg OSM source if missing, extracts node tag keys and values, and syncs those together with stored backend tags and configured choices into both language catalogs. Existing translations are preserved. The backend Docker PHP service must be running. The command handles container file copies, PHP execution and cleanup automatically.
| Production command | Purpose |
|---|---|
npm run translations:sync |
Refresh translation entries from OSM nodes, ways, relations and backend tags in one run. Uses host Node.js, Python 3 and Osmium, plus the Docker PHP service. First-time OSM preparation also needs curl. |
bash scripts/production/update-map-and-translations.sh |
Rebuild map GeoJSON, then run the same combined translation sync. |
bash scripts/production/prepare-flensburg.sh |
Download and process OSM data into public/data/flensburg.geojson. Requires curl and osmium-tool. |
bash scripts/production/extract-osm-tags.sh [input.pbf] |
Extract tag frequency TSVs from nodes, ways and relations into scripts/data/. Requires osmium-tool and Python 3. |
bash scripts/production/extract-node-tags.sh [input.pbf] |
Extract node tag frequency TSVs into scripts/data/. Requires osmium-tool and Python 3. |
node scripts/production/generate-tag-key-catalog.mjs [keys.tsv] [options] |
The translation sync entry point used by npm; explicit inputs and options allow collecting selected sources. |
Use npm run translations:sync -- --local to run PHP on the host instead of
Docker. Source PBFs, generated TSVs and map GeoJSON are git-ignored; the XLIFF
catalogs are committed to Git. Sync collects entries for translators and does not
automatically translate them.
For browser checks, install dependencies with npm install and Chromium with
npx playwright install chromium, then start the dev server.
Testing scripts (under scripts/testing/) |
Requirements |
|---|---|
check-deep-link.mjs, check-node-creation.mjs, check-node-relation.mjs, check-node-selection.mjs, check-routing.mjs |
Dev server at http://localhost:5173 (override with APP_URL); use intercepted fixtures. |
check-osm-render.mjs, check-square-selection.mjs |
Dev server on port 5199 and generated public/data/flensburg.geojson. |
check-node-editing.mjs |
Dev server on port 5199, backend on port 8080, and generated GeoJSON. Writes test tags to the configured backend; use a development instance. Supports APP_URL and API_URL. |
Example: node scripts/testing/check-node-creation.mjs.
Frontend translation tests need Node.js and npm install, without a running
browser, backend or dev server:
node --test scripts/testing/tag-translations.test.mjsPress the map's Add node (+) button to enter edit mode, then click the map
to place nodes; each click adds another one and nothing is sent yet. A dialog
below the controls shows the placed count, a name input, the parent node
(the first placed node, the anchor of the pending relation) and Save/Abort.
The first click chooses the parent for the batch: clicking an OSM feature attaches
all placed custom nodes directly to it. On empty space, the first custom node
becomes the parent and every following node is its direct child. Clicking an
existing child uses its parent, so creation never adds another hierarchy level.
Saving commits the batch: a single node becomes a plain node — the entered
name becomes its name tag and its tag editor opens — while two or more nodes
become members of a new node relation named with the entered name, whose tag
editor then opens. Abort (or Escape) discards the placed nodes and leaves
edit mode. Failed saves keep the placed nodes; Retry re-sends exactly the
unsaved ones.
Apply database migrations after updating the backend:
cd backend
docker compose exec php php bin/console doctrine:migrations:migrate --no-interactionPOST /api/nodes accepts provider (OSM by default, or local),
providerNodeId, coordinates and tags. Local nodes use a generated UUID as their
provider ID; reusing that identity updates the node. An optional parent is
either { "id": 123 } for a saved node or an OSM reference containing
providerNodeId, latitude, and longitude. Each node stores one nullable
parent_id; only custom nodes can be children. A parent can have multiple direct
children, and children cannot have children. Ordinary tag saves preserve the
parent; attempts to add a different parent are rejected. Responses retain
provider, parentIds, and childIds; parentIds and parents contain at most
one entry for API compatibility. The parents and
children summaries include each related node's ID, provider, provider ID,
nullable saved name and saved custom tags. The edit panel displays these
relations above the tags, including OSM reference parents that do not appear as
user markers. Each parent shows read-only OSM and custom tags in separate
groups. Original OSM tags come from the shared map dataset, including buildings
and areas; unavailable OSM data is distinguished from a parent with no tags.
The hierarchy migration flattens existing descendants to their root. It stops for ambiguous root parents or cycles without a root, requiring those records to be corrected before retrying. Rolling back restores the relation table using the flattened links.
Run the deterministic creation browser check with the dev server running:
node scripts/testing/check-node-creation.mjs
node scripts/testing/check-node-selection.mjsIt uses intercepted API/OSM fixtures and does not change the development database.
The selection check covers overlapping children and highlighted OSM parents. When saved markers overlap, the most recently created node receives the click; editing an older node does not change that priority.
Logged-in editors can group multiple nodes into a node relation — for example the tagged part of a road. Relations are usually built in edit mode: place two or more nodes with the + button, optionally name the relation in the dialog, and Save creates the relation from them (see "Adding nodes").
Existing nodes and OSM features can be related too: press the Create a node relation (link) button, then click nodes or map features to add them (clicking a member again removes it). Once two or more nodes are selected, Create relation saves the relation and opens its tag editor. Press Escape to leave relation mode.
Relations are street parts: the area they cover is the corridor of a fixed
width (20 m around the member line, computed by PostGIS in
backend/src/Service/StreetPartArea.php) around the line through the
members in order. The map draws it in amber: a dashed preview connects the
members collected in relation mode, and every saved relation shows its
corridor outline. The lines are visual aids only — clicks pass through to
the nodes and features beneath them.
Routing uses these corridors as GraphHopper custom areas: when a logged-in
user has a route preference for a tag carried by a relation (see the
profile page), the corridor enters the request's custom model — avoid
levels block edges inside it, prefer levels penalize edges outside it
(Routing::customModelFor()), just like the circles around individually
tagged nodes.
The relation's tag editor works like the node editor: new tags are unapproved observations; approved ones become read-only. Selecting a member node lists its relations in the node panel under "Node relations", with an edit button that opens the relation's tag editor. Admins review unapproved relation tags in the admin UI under "Pending relation observations", mirroring the pending observations queue for nodes.
GET /api/node-groups lists relations with their name, ordered member
summaries, tags and observations (visible tags follow the same approval rules
as node tags). POST /api/node-groups (editors only) creates a relation from
members — {"id": 123} for a saved node or an OSM reference with
providerNodeId, latitude and longitude; unknown OSM references are
materialized — and requires at least two members; an optional name names
it. Re-posting with an id updates it: members are additive (already-present
nodes are skipped), a non-null name replaces the current one, and the
request tags replace the current user's own tags, as with nodes.
Run the deterministic relation browser check with the dev server running:
node scripts/testing/check-node-relation.mjsThe map renders a read-only OSM feature layer (buildings, parks, streets, POIs
with their tags) from public/data/flensburg.geojson. The file is generated by
scripts/production/prepare-flensburg.sh and git-ignored; rebuild it with:
brew install osmium-tool # once
scripts/production/prepare-flensburg.shThe script downloads the Geofabrik Schleswig-Holstein extract, selects a
bounding box around Flensburg (9.35,54.75,9.51,54.84 in west, south, east,
north order), keeps tagged features only (building, leisure, amenity, shop, tourism, natural, highway, name), and exports GeoJSON. The download is cached
in scripts/data/.
A quick render check (needs the dev server on port 5199):
npm run dev -- --port 5199 &
node scripts/testing/check-osm-render.mjsTag labels have one source of truth: the XLIFF 2.0 files in
backend/tag-translations/, one per language:
| File | Language |
|---|---|
tags.en.xlf |
English |
tags.de.xlf |
German |
OSM tags and tags created by users use the same catalogs. They supply labels for
node and relation tag tables, suggested observation keys and values, profile tag
priorities, and the admin tag list. General interface text, such as buttons and
validation messages, remains in src/i18n/locales/.
Translations affect display labels. Raw keys and values stay unchanged in the
database and tag APIs, and dropdown choices submit their raw values. Unknown or
untranslated keys replace underscores with spaces (tactile_paving becomes
tactile paving). Untranslated values remain verbatim, including names, URLs,
numbers and free text. There is no database override for translated labels.
Each <unit> represents either a key or a value in the context of its key:
Unit name |
Meaning |
|---|---|
key:wheelchair |
Label for the key wheelchair |
value:["wheelchair","yes"] |
Label for the value yes under wheelchair |
value:["name","yes"] |
Separate label for the value yes under name |
Generation also writes reciprocal references using the namespace
xmlns:tag="urn:accessapp:tag-references:1" on the XLIFF root:
| Attribute | Meaning |
|---|---|
tag:valueRefs on a key unit |
Space-separated IDs of its contextual value units. |
tag:keyRef on a value unit |
ID of its key unit. |
tag:category on key and value units |
osm for OSM inputs; custom for database inputs. |
tag:ignore on key and value units |
true or false; an admin review flag, defaulting to false. |
For example, a key unit with id="key_house" can have
tag:valueRefs="value_house_marktwache", and that value unit has
tag:keyRef="key_house". These refer to actual unit IDs in the same file.
Given a key, follow its tag:valueRefs; given a raw value, find its contextual
value units and follow their tag:keyRef. A value such as yes can belong to
several keys, so reverse lookup can return several keys. These links include
untranslated entries too.
OSM key/pair TSV inputs are categorized as osm. Tags stored in the database,
admin-configured observation choices and route preference keys are categorized
as custom unless that same unit is also known from OSM. OSM takes precedence
for overlapping entries, and a database-only sync preserves an existing osm
category. Categories apply per key or contextual value: a custom value can belong
to an OSM key. Entries absent from the inputs retain their existing category.
npm run translations:sync rebuilds the references from all retained catalog
pairs, including entries absent from the current inputs. Existing unit IDs,
translations, translator notes and review states are preserved. Redundant
generated Tag value: … notes are removed; their context is already stored in
the unit name and key/value references. New values are linked in
both directions, and repeated generation does not duplicate references. Target
edits in the admin preserve these attributes; newly approved pairs receive their
references on the next generation run. Preserve tag:keyRef, tag:valueRefs
and tag:category, plus tag:ignore, when editing catalogs in translation software.
Context prevents a translation for one tag from changing the same value under another tag. The contextual unit name and references identify its key without a repeated value note. For example, a German value entry looks like this:
<unit id="value_wheelchair_yes" name="value:["wheelchair","yes"]"
tag:category="osm" tag:ignore="false" tag:keyRef="key_wheelchair">
<segment state="translated">
<source>Yes</source>
<target>Ja</target>
</segment>
</unit>Open the .xlf files in translation software that supports XLIFF 2.0, or edit
<target> text directly. Preserve unit id and name attributes, source text,
and context notes. When editing XML by hand, escape characters such as & as
& and < as <.
New entries have empty targets and state="initial". Complete targets in each
language file, including English where a raw identifier needs a friendlier label.
The sync script discovers entries; it does not perform machine translation.
Setting a review state alone does not activate or hide a translation: the app uses
nonempty targets and falls back for empty ones.
Open Translations → Tag translations in the backend admin (or visit
http://localhost:8080/admin/translations). Only admins can access this page.
The editor groups each key and its contextual values in a bordered block, with a shaded key row and indented value rows. Spacing separates different keys. Each row has four columns: the raw Key / value, Translation German, Translation English, and the Ignore checkbox. Search matches tag keys, values, source text, notes and translations in either language. Untranslated only includes tags missing a translation in either language. Results show 25 matching tags per page, with a shared key row shown once above its values. Context notes are under Notes.
Use Category to select all entries, OSM, or Custom. Ignore selects all, ignored, or not ignored entries. Not ignored is the default. Both filters combine with search and Untranslated only, and stay selected across pagination and saves. A parent key can remain visible as context for a matching value even if that key does not match the metadata filters itself. Entries without a category appear as Uncategorized and are included under all categories.
Each key/value row has an Ignore checkbox. Save all changes writes its
tag:ignore flag to both German and English catalogs alongside translation
edits. Ignoring a key also hides all of its values from Not ignored, without
changing their own saved flags. All entries and Ignored show these values
with an Ignored via key marker, so the group can be reviewed and restored.
Unchecking the key restores values whose own flags are not ignored. Missing flags
mean false.
Generation adds tag:ignore="false" where missing and preserves existing flags,
including true. Ignore marks translation work for filtering; it does not
remove published labels from the app. Stale ignore edits reject the entire
batch and preserve the checkbox and text drafts.
Edit any fields on the displayed page and click Save all changes once to save both languages together. Save before changing filters or moving to another page. Clearing a field returns that label to its normal fallback. Untouched fields are not rewritten, and entries present in only one catalog can be translated into the other language from the same form.
Saving updates tags.de.xlf and tags.en.xlf directly and sets changed entries
to translated (or initial for empty targets). Source text, unit identities,
notes and generated key/value references are preserved. Both files are locked
and all edits are validated and staged before either file is replaced. A stale
edit rejects the entire save, preserving both catalogs and showing all submitted
drafts in the form. There is no separate translation database. The app uses the
changed labels after reloading the frontend page. Admin saves and generation
share the same file locks.
In development, changes are written to backend/tag-translations/ in your
checkout. Review and commit them as usual. The production Docker image makes
this directory writable by PHP, and Compose stores it in the persistent
tag_translation_data volume so edits survive container recreation. This volume
is initially seeded from the image's catalogs; later image deployments do not
overwrite its edited files. The production override explicitly mounts that
volume at /app/tag-translations, including when the development override is
also present. Its explicit Docker volume name defaults to
api_tag_translation_data, preserving the existing volume and remaining stable
when the deployment project name changes. Set TAG_TRANSLATION_VOLUME_NAME for
a separate installation and keep that value stable across its deployments.
Use the production Compose files explicitly when deploying, from backend/:
docker compose -f compose.yaml -f compose.prod.yaml up -d --build --waitThis recreates the application container while reusing the existing catalog
volume. The development bind mount stays in compose.override.yaml for editing
catalogs in the checkout. No startup script copies image files over an existing
catalog volume. If adopting the volume on an existing installation that stored
catalogs elsewhere, copy its current XLIFF files into the volume before directing
traffic to the new container.
Back up or export the volume's catalogs separately
from the database, and copy reviewed catalog updates into the volume explicitly
when deploying changes made elsewhere. For example, from backend/:
# Export a production catalog for review or backup.
docker compose -f compose.yaml -f compose.prod.yaml cp \
php:/app/tag-translations/tags.de.xlf /path/to/backup/tags.de.xlfCustom deployments need a persistent, writable catalog directory. If multiple
PHP instances share it, configure LOCK_DSN to use a shared Symfony Lock store;
the default flock store coordinates processes in one container.
Under Map data → Pending observations, open a node's edit/review page.
Each pending tag shows three rows: raw Key, Value and Observations;
German Key (DE) and Value (DE); then English Key (EN) and
Value (EN). Key, value and translation fields are editable, prefilled with
the proposed tag properties (or existing published labels). Save changes saves the admin's changes on the tag
without approving it or changing a catalog. Approve saves the reviewed
key, value and translations, and writes their targets to tags.<locale>.xlf, then approves the
observation. The relation review screen offers the same controls. Clearing a
translation removes its published label when approved. Other language catalogs
stay unchanged unless their translations are edited.
Key/value edits update the shared tag wherever it is referenced. A key/value pair that already belongs to another tag is rejected. Approval writes translations under the edited identifiers; existing catalog entries for old identifiers are preserved. Saving key/value changes alone does not publish translations.
Proposals are stored in the shared tag's translations JSON property, indexed
by language, with keyTranslation and valueTranslation fields. They are not
used as public labels before approval. Stale review forms and changed catalog
targets are rejected; the submitted draft remains visible. Already-published
labels can still be edited under Translations → Tag translations. Existing
catalog translations remain published after this update; the migration starts
existing tags with empty proposals and does not infer their authorship.
Apply the new migration before using these fields:
cd backend
docker compose exec php php bin/console doctrine:migrations:migrate --no-interactionRun this in your host terminal, from the repository root:
npm run translations:syncThe command performs the complete translation sync:
- If
scripts/data/flensburg.osm.pbfis missing, prepare the Flensburg OSM source usingprepare-flensburg.sh. This first-time step also builds map GeoJSON. - Extract current node tag keys and key/value pairs from that PBF, replacing
the intermediate frequency TSVs in
scripts/data/. - Collect all stored backend tag key/value pairs, default observation keys and their configured values, and route preference keys.
- Merge OSM and backend entries into
backend/tag-translations/tags.en.xlfandtags.de.xlfin the same sync operation.
Existing targets, custom entries, notes and review states are preserved. Missing
entries are appended with empty targets. Entries remain in the catalogs even if
a tag disappears from a source. Output such as de: 12 new translation units
counts new entries, not completed translations. Repeated syncs with unchanged
source tags report zero new units.
Saving a user tag without translation edits does not edit a catalog automatically. Rerun this command when you want to collect new tags for translation. It reads backend data without changing database tags.
The host needs Node.js, Python 3 and osmium-tool (brew install osmium-tool on
macOS). First-time OSM preparation also uses curl. Start the backend once:
# From the repository root, in your host terminal:
cd backend
docker compose up -d --wait
cd ..
npm run translations:syncThe command runs extraction on the host and PHP catalog sync inside the Docker
php service automatically. There is no need to enter a container shell or copy
TSV files yourself. In development, the container's /app/tag-translations/
is bind-mounted from backend/tag-translations/, so file changes appear in your
checkout immediately. The backend image mounts only backend/ and does not
include the full workflow's Node.js, Python and Osmium tools; run the npm command
on the host.
For a configured host PHP runtime with the backend's required version, extensions and Composer dependencies, use:
npm run translations:sync -- --localThis still collects both OSM tags and backend tags. --local selects where
PHP runs; it does not change which sources are included.
Normal translation sync reads the existing Flensburg PBF and always re-extracts tags from its nodes, ways and relations. It downloads and prepares OSM data only when that PBF is missing. To rebuild the map GeoJSON as well, run:
bash scripts/production/update-map-and-translations.shThis prepares the map, then calls the same combined OSM/backend translation
sync. It reuses the cached Schleswig-Holstein download when present. To fetch a
newer extract, remove scripts/data/schleswig-holstein-latest.osm.pbf before
running this map update command. The workflow updates map data files and
translation catalogs; it does not import map features as database nodes.
The extractor collects tags on nodes, ways and relations, including building
keys such as roof:levels that may only occur on ways. Its generated
inputs are scripts/data/flensburg-tag-keys.tsv (count, key) and
flensburg-tag-pairs.tsv (count, key, value). They use tab-separated fields
with CSV quoting for quotes, tabs and line breaks. They are intermediate data;
edit translations in XLIFF, not these TSVs.
The npm command and the direct Node.js entry point have the same behavior.
Without arguments, or with only --local, they refresh OSM inputs and include
backend tags automatically. Explicit input arguments or collection options select
only the sources you request:
| Argument or option | Effect |
|---|---|
| First positional argument | Collect keys from the supplied frequency TSV. |
--pairs path/to/pairs.tsv |
Collect contextual values and their keys from a pairs TSV. |
--database |
Collect stored backend tags and configured choices. |
--local |
Run PHP on the host instead of Docker; can be combined with source options. |
--import-legacy-labels |
Copy old database labels into the files, replacing conflicting targets; see migration below. |
To collect only backend tags, without extracting OSM data:
npm run translations:sync -- --databaseTo use another PBF instead of Flensburg and include backend tags:
bash scripts/production/extract-osm-tags.sh /path/to/town.osm.pbf
npm run translations:sync -- scripts/data/town-tag-keys.tsv \
--pairs scripts/data/town-tag-pairs.tsv --databaseExplicitly supplied missing or invalid TSV inputs fail before catalog edits. The
Node.js entry point delegates catalog editing to app:tags:sync-translations.
For example, the backend-only equivalent inside a container shell at /app is:
php bin/console app:tags:sync-translations --databaseAfter syncing, complete the new targets in translation software and save the returned XLIFF files to their original paths. Validate the catalogs:
cd backend
docker compose exec php php bin/console lint:xliff tag-translations
cd ..Review and commit the changes in backend/tag-translations/, then deploy those
files with the backend. Generated PBF, TSV and GeoJSON files are git-ignored and
are not the source of truth for translated labels. To check the result in the
app, choose the language on the profile page and reload the page after changing
a catalog, because the frontend caches the loaded language for the session.
The backend reads the files directly, so catalog target edits do not require a
frontend build or a Symfony translation-cache clear. The public
GET /api/i18n/tag-keys/{locale} endpoint (en or de) returns locale, labels
(raw key to label), and values (JSON-encoded key/value pair to label). Its ETag
hashes the complete response and changes when the returned translations change.
App\Service\TagKeyLabeler supplies both the admin UI and that endpoint. The
Vue tagKeyLabels store loads the maps at startup and on language changes,
keeping raw tag identifiers outside vue-i18n's message-path syntax. Catalogs live
outside Symfony's general translation directory; runtime lookups stream the XML
and keep only nonempty targets, and worker processes reset their maps between
requests.
For an existing installation, copy any old admin-managed labels into XLIFF once, before deploying the new label resolution:
node scripts/production/generate-tag-key-catalog.mjs --import-legacy-labelsUnlike normal sync, this explicit import replaces conflicting file targets
with the old database labels. Review and commit the result. The old
tag_key_labels table remains for data recovery, but runtime lookups do not read
it and the admin no longer offers a label editor. Future translation changes go
into XLIFF.
Create backend/tag-translations/tags.<locale>.xlf using the same unit identities,
with the matching XLIFF target language. Update
TagKeyLabeler::SUPPORTED_LOCALES, the frontend's AppLocale, and the profile
language choices; add the general UI messages for that language separately.
When changing fallback formatting, keep backend TagKeyLabeler::prettify() and
frontend prettifyTagKey() identical.
Admins choose the tag keys users are offered as ready-made observations, in
the backend admin UI under Map data → Default observation tags. Any key
works — OSM ones (e.g. wheelchair) and custom ones alike — and each key
can carry a list of default values; the admin list shows the readable
label, the raw key is stored. The keys and values are served by
GET /api/default-observation-tags and loaded once at startup by the
defaultObservationTags Pinia store. The node and relation editors offer
them in a searchable dropdown above the observation rows (options show the
readable label through tagKeyLabels; the search matches label and raw
key). Picking one adds a prefilled observation row whose raw key is fixed
(the relation editor shows its translated label with the raw key in a tooltip); when the key carries
default values, its value cell is a dropdown of those choices (clearable,
and editable so users can propose an own value) instead of the free-form
input. Keys already present on the node or relation are not offered. The
"Add observation" button for free-form keys stays, and its rows keep the
editable key and value inputs.
In the node editor, observation rows show Key translation and Value
translation, with the smaller raw key and value fields underneath. Translation
fields prefill from saved proposals or the current language catalog. Saving a
node stores translation edits as tag properties in the selected language, pending
admin review; the catalog changes only on approval. Raw tag identifiers stay
unchanged. Clearing a translation proposes returning to the normal label fallback. A language switch uses separate drafts, and
stale translations are rejected without discarding the form. The node response includes observationTranslations, keyed by raw tag key and
language, so saved proposals survive reopening the editor. The node save API
accepts optional translationLocale (en or de) and translations entries
with key, value, keyTranslation, valueTranslation, and the corresponding
previousKeyTranslation / previousValueTranslation for conflict checking.
Walking routes are served by GraphHopper 11.1 (graphhopper-web library jar,
run in its own container; walker profiles foot_shortest and
foot_wheelchair). The backend proxies it as
GET /api/route?fromLat=&fromLon=&toLat=&toLon= and returns the shortest
walking route as { distance (m), time (s), points ([lat, lon] pairs) };
errors are { "error": "no_route" } (422) or
{ "error": "routing_unavailable" } (503).
Start it once:
cd backend
docker compose --profile routing-data up # downloads the Geofabrik Schleswig-Holstein extract
docker compose --profile routing up -d # imports the graph on first start (takes a few minutes)The OSM extract is cached in backend/routing/data/, the prepared graph in
the graphhopper_graphs volume. Set GRAPHHOPPER_REBUILD_GRAPH=true to force
a re-import. The container exposes GraphHopper directly on
http://localhost:8989 (health: /info). The first start must finish the
graph import before routes resolve; watch
docker compose logs -f routing until the server announces its profiles.
The graph is memory-mapped (graph.dataaccess.default_type: MMAP) and the
heap defaults to 512 MB (GRAPHHOPPER_XMX), so the container runs in well
under 1 GB of RAM — the import over the full Schleswig-Holstein extract
included. Most of the container's resident memory is evictable page cache,
which the OS reclaims under memory pressure.
Frontend: press the route button (top-left), then click the map twice to set start and end. The shortest walking route is drawn with a distance/time summary; Escape leaves routing mode.