Tour content translations
Creating and structuring translated tour files
This guide covers how to create translated versions of your tour. We'll use Barcelona as an example, adding English, Spanish, and German versions.
File structure
Each language gets its own JSON file:
src/data/tour/
├── metadata.json # Shared settings (no translation needed)
├── en.json # English content
├── es.json # Spanish content
└── de.json # German content
The golden rules
Same tour ID
All language files must have the same id value as metadata.json
Same stop IDs
Stop IDs must match across all languages (stop "3" = stop "3")
Same number of stops
All languages should have the same stops in the same order
Unique Language Code
Each file needs a different language value
Step-by-step example
1. Start with English (en.json)
{
"id": "barcelona",
"language": "en",
"title": "Unlimited Barcelona",
"description": "Discover the rich history and hidden gems of Barcelona's Gothic Quarter.",
"totalDuration": "45 mins",
"totalStops": 3,
"stops": [
{
"id": "1",
"type": "audio",
"title": "Welcome to Barcelona",
"duration": "3 min audio",
"image": "https://images.unsplash.com/photo-xxx",
"audioFile": "https://storage.example.com/barcelona/en/01-welcome.mp3",
"transcription": "Welcome to Barcelona! Today we'll explore the Gothic Quarter..."
},
{
"id": "2",
"type": "audio",
"title": "The Cathedral",
"duration": "5 min audio",
"image": "https://images.unsplash.com/photo-yyy",
"audioFile": "https://storage.example.com/barcelona/en/02-cathedral.mp3"
},
{
"id": "3",
"type": "audio",
"title": "La Rambla",
"duration": "4 min audio",
"image": "https://images.unsplash.com/photo-zzz",
"audioFile": "https://storage.example.com/barcelona/en/03-rambla.mp3"
}
]
}2. Create Spanish (es.json)
{
"id": "barcelona",
"language": "es",
"title": "Barcelona Sin Límites",
"description": "Descubre la rica historia y los tesoros ocultos del Barrio Gótico de Barcelona.",
"totalDuration": "45 mins",
"totalStops": 3,
"stops": [
{
"id": "1",
"type": "audio",
"title": "Bienvenido a Barcelona",
"duration": "3 min audio",
"image": "https://images.unsplash.com/photo-xxx",
"audioFile": "https://storage.example.com/barcelona/es/01-welcome.mp3",
"transcription": "¡Bienvenido a Barcelona! Hoy exploraremos el Barrio Gótico..."
},
{
"id": "2",
"type": "audio",
"title": "La Catedral",
"duration": "5 min audio",
"image": "https://images.unsplash.com/photo-yyy",
"audioFile": "https://storage.example.com/barcelona/es/02-cathedral.mp3"
},
{
"id": "3",
"type": "audio",
"title": "La Rambla",
"duration": "4 min audio",
"image": "https://images.unsplash.com/photo-zzz",
"audioFile": "https://storage.example.com/barcelona/es/03-rambla.mp3"
}
]
}3. Create German (de.json)
{
"id": "barcelona",
"language": "de",
"title": "Barcelona Grenzenlos",
"description": "Entdecken Sie die reiche Geschichte und verborgenen Schätze des Gotischen Viertels von Barcelona.",
"totalDuration": "45 mins",
"totalStops": 3,
"stops": [
{
"id": "1",
"type": "audio",
"title": "Willkommen in Barcelona",
"duration": "3 min audio",
"image": "https://images.unsplash.com/photo-xxx",
"audioFile": "https://storage.example.com/barcelona/de/01-welcome.mp3",
"transcription": "Willkommen in Barcelona! Heute erkunden wir das Gotische Viertel..."
},
{
"id": "2",
"type": "audio",
"title": "Die Kathedrale",
"duration": "5 min audio",
"image": "https://images.unsplash.com/photo-yyy",
"audioFile": "https://storage.example.com/barcelona/de/02-cathedral.mp3"
},
{
"id": "3",
"type": "audio",
"title": "La Rambla",
"duration": "4 min audio",
"image": "https://images.unsplash.com/photo-zzz",
"audioFile": "https://storage.example.com/barcelona/de/03-rambla.mp3"
}
]
}What to translate
| Field | Translate? | Notes |
|---|---|---|
title | Yes | Tour title |
description | Yes | Tour description |
totalDuration | Maybe | "45 mins" vs "45 minutos" vs "45 Minuten" |
stops[].title | Yes | Stop titles |
stops[].duration | Maybe | "3 min audio" might need translation |
stops[].transcription | Yes | If using transcriptions |
stops[].audioFile | Different URL | Points to language-specific audio |
What NOT to translate
| Field | Keep Same | Why |
|---|---|---|
id | Same across all | Links language versions together |
stops[].id | Same across all | Enables position preservation |
stops[].type | Same across all | Must match |
stops[].image | Usually same | Images don't need translation |
Audio file organization
Organize your audio files by language:
your-storage.com/barcelona/
├── en/
│ ├── 01-welcome.mp3
│ ├── 02-cathedral.mp3
│ └── 03-rambla.mp3
├── es/
│ ├── 01-welcome.mp3
│ ├── 02-cathedral.mp3
│ └── 03-rambla.mp3
└── de/
├── 01-welcome.mp3
├── 02-cathedral.mp3
└── 03-rambla.mp3
This keeps things organized and makes the URL pattern predictable.
Per-language overrides
You can override metadata properties in individual language files:
{
"id": "barcelona",
"language": "es",
"title": "Barcelona Sin Límites",
"themeId": "warm-theme"
}This Spanish version would use "warm-theme" while other languages use whatever's in metadata.json.
This is useful for language-specific branding or when a different color scheme works better for certain markets.
Common mistakes
Different stop ids
// en.json
{ "id": "1", "title": "Welcome" }
// es.json - WRONG!
{ "id": "welcome", "title": "Bienvenido" }Fix: Use "id": "1" in both files.
Missing stops
// en.json has 5 stops
// es.json only has 4 stops - WRONG!Fix: All languages must have all stops translated.
Different tour ids
// en.json
{ "id": "barcelona-en" }
// es.json - WRONG!
{ "id": "barcelona-es" }Fix: Use "id": "barcelona" in all files.
Testing translations
- Run
bun run dev - Open the tour
- Click the language flag
- Switch between languages
- Verify:
- Content changes
- Position is preserved if mid-stop
- No console errors
- All images load