This document describes how translations work in LIPAS to help LLM code assistants quickly understand and work with the translation system.
LIPAS uses the Tongue library for internationalization, supporting three languages:
- Finnish (
:fi) - Primary language/fallback - Swedish (
:se) - English (
:en)
LIPAS translations come from two main sources:
- Translation Files - Static translations for UI elements, labels, and messages
- Data-Driven Translations - Dynamic translations generated from data files
webapp/src/cljc/lipas/i18n/
├── core.cljc # Main i18n logic and translation function
├── utils.cljc # Macro for loading translations at compile time
├── fi.cljc # Finnish translations loader
├── se.cljc # Swedish translations loader
├── en.cljc # English translations loader
├── fi/ # Finnish translation files
│ ├── actions.edn
│ ├── general.edn
│ ├── route.edn # Route-specific translations
│ └── ...
├── se/ # Swedish translation files
│ └── ...
└── en/ # English translation files
└── ...
The translation system maps translation key namespaces directly to file names based on the top-level-keys list in utils.cljc.
- Translation key:
:route/editor-title - Namespace extracted:
:route(part before the/) - File lookup:
route.edn(namespace name +.edn) - Loading condition:
:routemust be intop-level-keyslist
;; Translation key → File location
:actions/save → fi/actions.edn, se/actions.edn, en/actions.edn
:route/editor-title → fi/route.edn, se/route.edn, en/route.edn
:map/zoom-in → fi/map.edn, se/map.edn, en/map.edn
:lipas.user/name → fi/lipas_user.edn, se/lipas_user.edn, en/lipas_user.edn- Each language has its own directory:
fi/,se/,en/ - Translation files are EDN files organized by feature/domain
- File naming convention:
namespace_name.edn(dots in namespace names are replaced with underscores) - File names must match entries in
top-level-keyslist
;; Example: webapp/src/cljc/lipas/i18n/fi/actions.edn
{:add "Lisää"
:back "Takaisin"
:cancel "Peruuta"
:save "Tallenna"
:delete "Poista"}Important: Keys in the file should NOT include the namespace prefix. The system automatically adds it when loading.
The system organizes translations into these top-level keys (defined in utils.cljc):
(def top-level-keys
[:accessibility
:actions ; → actions.edn files
:admin
:general ; → general.edn files
:route ; → route.edn files
:map ; → map.edn files
;; ... many more
])Some translations in LIPAS are automatically generated from data files rather than stored in translation files. These provide dynamic translations for domain-specific content.
- Sports site types from
lipas.data.types - Cities from
lipas.data.cities - Materials from
lipas.data.materials - Owners/administrators from respective data files
Data files contain multilingual information that gets transformed into translations at runtime:
;; Example from lipas.data.types
{:type-code 1234
:name {:fi "Uimahalli"
:se "Simhall"
:en "Swimming Hall"}
:description {:fi "Sisäuimala..."
:se "Inomhusbassäng..."
:en "Indoor swimming..."}}The core.cljc file provides special localization functions for working with data-driven content:
localize- Localizes sports site data based on locale (mutating)localize2- Non-mutating version that adds-localizedfields
;; Usage example
(localize sports-site-data :fi) ; Returns localized version
(localize2 sports-site-data :fi) ; Returns original + localized fields- Subscribe to translator function:
(ns lipas.ui.example.views
(:require [lipas.ui.utils :refer [<==]]))
(defn my-component []
(let [tr (<== [:lipas.ui.subs/translator])]
[:div
[:h1 (tr :general/welcome)] ; Loads from general.edn
[:button {:on-click #(...)}
(tr :actions/save)]])) ; Loads from actions.edn- Translation Key Format:
- Use namespaced keywords:
:namespace/key - Namespace must be in
top-level-keyslist - Examples:
:actions/save→actions.ednfiles:route/editor-title→route.ednfiles:map.tools/draw→map_tools.ednfiles (dots → underscores)
;; In translation file:
{:welcome "Welcome {1}!"}
;; In code:
(tr :general/welcome user-name)For data-driven content, use the localization functions:
;; Access localized sports site type names
(get-in (localize sports-site :fi) [:type :name])
;; Or use the non-mutating version
(get-in (localize2 sports-site :fi) [:type :name-localized])Step-by-step process:
-
Add to
top-level-keys:;; In utils.cljc (def top-level-keys [;; existing keys :your-new-namespace])
-
Create translation files:
;; fi/your_new_namespace.edn {:key1 "Finnish translation" :key2 "Another Finnish translation"} ;; se/your_new_namespace.edn {:key1 "Swedish translation" :key2 "Another Swedish translation"} ;; en/your_new_namespace.edn {:key1 "English translation" :key2 "Another English translation"}
-
Use in code:
(tr :your-new-namespace/key1) (tr :your-new-namespace/key2)
-
Recompilation: Shadow-cljs will automatically recompile when it detects the file changes
For domain-specific content that needs to be managed as data:
- Add to appropriate data namespace (e.g.,
lipas.data.types) - Use multilingual data structure:
{:name {:fi "Finnish name" :se "Swedish name" :en "English name"} :description {:fi "Finnish description" :se "Swedish description" :en "English description"}} - Use localization functions to access translations in components
Problem: You see {MISSING KEY :ROUTE/EDITOR-TITLE} in the UI
Causes:
- Missing namespace in
top-level-keys: The namespace:routeis not in thetop-level-keyslist - Missing translation files: No
route.ednfiles exist in language directories - Wrong file location: Translation is in wrong file based on namespace
Solutions:
- Add the namespace to
top-level-keysinutils.cljc - Create the missing translation files
- Move translations to the correct file based on their namespace
- Compile-time loading: Translations are loaded at compile time by the
deftranslationsmacro - Recompilation needed: Changes to translation files or
top-level-keysrequire recompilation (shadow-cljs handles this automatically when files change) - Fallback language: Finnish (
:fi) is the fallback when translations are missing - File naming: Use underscores for dots in namespace names (
:map.tools→map_tools.edn) - Key organization: Group related translations by logical namespace, not by UI location
- Dynamic loading: Data-driven translations are loaded at runtime from data files
To add new file-based translations:
- Determine the correct namespace based on functionality
- Check if namespace exists in
top-level-keys(add if missing) - Add/modify translations in appropriate
.ednfiles for all languages - Use
(tr :namespace/key)in ClojureScript code
To debug missing translations:
- Check if namespace is in
top-level-keys - Verify translation files exist for all languages
- Confirm translations are in correct files based on namespace
- Allow recompilation if files or
top-level-keyschanged
For data-driven content:
- Add multilingual data to appropriate data namespace
- Use
localizeorlocalize2functions to access localized versions - Consider whether content belongs in translation files or data files based on its nature