The platform legal pages (/privacy/, /terms/, /disclaimer/) each contain a visually distinct section with the chat-specific additions β no separate tool URLs needed.
Linking from the chat app:
Location
Target
/info β Privacy
xpulse.one/privacy/
/info β Terms of Service
xpulse.one/terms/
/info β Disclaimer
xpulse.one/disclaimer/
/info β Imprint
xpulse.one/imprint/
Page Overview
Route
Title
Type
Status
/
xPulse
Platform Landing
v2.0.0
/status/
Status & Versions
Platform
v2.0.0
/doc/
Documentation
Platform
v2.0.0
/privacy/
Privacy Policy (Platform)
Legal
v2.0.0
/terms/
Terms of Service (Platform)
Legal
v2.0.0
/disclaimer/
Disclaimer (Platform)
Legal
v2.0.0
/contact/
Contact
Platform
v2.0.0
/imprint/
Imprint
Legal
v2.0.0
/doc/tool/chat/
Chat Documentation
Docs
v2.0.0
/doc/component/*/
Component Documentation
Docs
v2.0.0
Repo Structure
1
xpulse-web/
2
src/
3
index.js
4
controllers/
5
index.js
6
privacy.js
7
terms.js
8
disclaimer.js
9
contact.js
10
imprint.js
11
status.js
12
templates/
13
shared/base.tpl.html
14
platform.tpl.html
15
privacy.tpl.html
16
terms.tpl.html
17
disclaimer.tpl.html
18
contact.tpl.html
19
imprint.tpl.html
20
status.tpl.html
21
translations/
22
de/ platform.*.json
23
en/ platform.*.json
24
themes/
25
web.css
26
public/
27
assets/
28
favicon.svg
29
status/
30
status.json β gitignored, server-managed
31
status.json.example
32
docs/
33
Dockerfile
34
docker-compose.yml
35
docker-compose.stage.yml
36
Makefile
37
package.json
38
xpulse.json
Infrastructure
Docker / Traefik
Node.js 22 Alpine behind Traefik, port 3000. status.json is mounted as a read-only volume.
1
make stage # rc-2-0-0.stage.web.xpulse.one (requires .env.stage)
No language slugs in URLs (/de/, /en/ are dropped).
Language is a user preference, not a route.
Default: de
Switch: without page reload, via JS
Storage: localStorage β key xpulse_lang
<html lang="xx"> is updated on every language switch
Screen Reader Compatibility
Text always exists as real content in the DOM β never empty.
The default text (German) is embedded directly in the HTML.
On language switch, JS replaces the textContent / innerHTML of elements.
Data attributes serve only as hooks, not as text sources.
1
<!-- Correct: real text in HTML, data-i18n only as hook -->
2
<pdata-i18n="chat.guide.pairing.intro">
3
Um mit jemandem zu chatten, mΓΌsst ihr euch einmalig pairen...
4
</p>
5
6
<!-- Wrong: empty element, text only via JS -->
7
<pdata-i18n="chat.guide.pairing.intro"></p>
File Structure
1
locales/
2
de/
3
platform.about.json
4
platform.privacy.json
5
platform.terms.json
6
platform.disclaimer.json
7
tool.chat.guide.json
8
tool.chat.security.json
9
tool.chat.changelog.json
10
tool.chat.privacy.json
11
tool.chat.terms.json
12
tool.chat.disclaimer.json
13
en/
14
platform.about.json
15
platform.privacy.json
16
...
File names mirror the URL structure: {scope}.{tool?}.{page}.json.
The locales/ directory is fully parsed on startup β
new files are detected automatically, no manual registration.
JSON Format
1
{
2
"chat.guide.pairing.intro":"Um mit jemandem zu chatten...",
3
"chat.guide.pairing.step1":"Eine Person ΓΆffnet βPeer hinzufΓΌgen"...",
4
"chat.guide.faq.storage": "AusschlieΓlich lokal in deinem Browser..."
5
}
Flat key-value structure. Keys follow the scheme {scope}.{page}.{block}.{element}.
No deeply nested JSON β easier to search and maintain.
JavaScript β i18n.js
1
// Responsibilities of i18n.js:
2
// 1. Read language from localStorage (fallback: 'de')
3
// 2. Load matching JSON for the current page
4
// 3. Populate all [data-i18n] elements with textContent/innerHTML
5
// 4. Set <html lang="xx">
6
// 5. Lang switch: load new language, update DOM, write to localStorage
7
8
constSTORAGE_KEY = 'xpulse_lang';
9
constDEFAULT_LANG = 'de';
Each page only loads the JSON it needs β
no monolithic translations bundle.