-
+
+ {t("landing.footer.imprint")}
+
+
{t("landing.footer.privacy")}
+
+ {t("landing.footer.terms")}
+
{t("landing.footer.source")}
diff --git a/openspec/changes/legal-disclaimers/tasks.md b/openspec/changes/legal-disclaimers/tasks.md
index a2007bb..f82543f 100644
--- a/openspec/changes/legal-disclaimers/tasks.md
+++ b/openspec/changes/legal-disclaimers/tasks.md
@@ -1,42 +1,42 @@
## 1. Operator Information
-- [ ] 1.1 Decide what operator address to use on the Impressum (private address vs. business address service vs. lawyer's address) — legal requirement, must be reachable
-- [ ] 1.2 Create `apps/journal/app/lib/operator.ts` exporting the operator details (name, address, email, responsible person)
+- [x] 1.1 Decide what operator address to use on the Impressum (private address vs. business address service vs. lawyer's address) — legal requirement, must be reachable
+- [x] 1.2 Create `apps/journal/app/lib/operator.ts` exporting the operator details (name, address, email, responsible person)
## 2. Legal Pages
-- [ ] 2.1 Create `apps/journal/app/routes/legal.imprint.tsx` rendering the Impressum from the operator config
-- [ ] 2.2 Create `apps/journal/app/routes/legal.terms.tsx` with Terms of Service covering alpha status, warranty disclaimer, DB reset disclosure, and data export responsibility
-- [ ] 2.3 Move existing `apps/journal/app/routes/privacy.tsx` to `apps/journal/app/routes/legal.privacy.tsx` and expand with GDPR-required sections (controller, legal basis, data subject rights, complaint right)
-- [ ] 2.4 Add a redirect from `/privacy` to `/legal/privacy` with 301 status
-- [ ] 2.5 Register the new routes in `apps/journal/app/routes.ts`
+- [x] 2.1 Create `apps/journal/app/routes/legal.imprint.tsx` rendering the Impressum from the operator config
+- [x] 2.2 Create `apps/journal/app/routes/legal.terms.tsx` with Terms of Service covering alpha status, warranty disclaimer, DB reset disclosure, and data export responsibility
+- [x] 2.3 Move existing `apps/journal/app/routes/privacy.tsx` to `apps/journal/app/routes/legal.privacy.tsx` and expand with GDPR-required sections (controller, legal basis, data subject rights, complaint right)
+- [x] 2.4 Add a redirect from `/privacy` to `/legal/privacy` with 301 status
+- [x] 2.5 Register the new routes in `apps/journal/app/routes.ts`
## 3. i18n
-- [ ] 3.1 Add German translations for all legal page copy (Impressum, Terms, Datenschutzerklärung) — DE is the authoritative version for German consumers
-- [ ] 3.2 Add English translations for the same pages
-- [ ] 3.3 Add translation keys for the alpha banner and signup acknowledgement checkbox
+- [x] 3.1 Add German translations for all legal page copy (Impressum, Terms, Datenschutzerklärung) — DE is the authoritative version for German consumers
+- [x] 3.2 Add English translations for the same pages
+- [x] 3.3 Add translation keys for the alpha banner and signup acknowledgement checkbox
## 4. Signup Acknowledgement
-- [ ] 4.1 Add `terms_accepted_at` column (timestamp, nullable initially for existing users) to the `users` table in `packages/db/src/schema/journal.ts`
-- [ ] 4.2 Run `pnpm db:push` to apply the schema
-- [ ] 4.3 Add a required checkbox to `apps/journal/app/routes/auth.register.tsx` linking to the Terms
-- [ ] 4.4 Client-side: block form submission if checkbox is unchecked; show validation message
-- [ ] 4.5 Server-side: reject registration requests in `api.auth.register.ts` that don't include `termsAccepted: true`
-- [ ] 4.6 On successful registration, set `users.terms_accepted_at = NOW()`
+- [x] 4.1 Add `terms_accepted_at` column (timestamp, nullable initially for existing users) to the `users` table in `packages/db/src/schema/journal.ts`
+- [x] 4.2 Run `pnpm db:push` to apply the schema
+- [x] 4.3 Add a required checkbox to `apps/journal/app/routes/auth.register.tsx` linking to the Terms
+- [x] 4.4 Client-side: block form submission if checkbox is unchecked; show validation message
+- [x] 4.5 Server-side: reject registration requests in `api.auth.register.ts` that don't include `termsAccepted: true`
+- [x] 4.6 On successful registration, set `users.terms_accepted_at = NOW()`
## 5. Alpha Banner
-- [ ] 5.1 Create `apps/journal/app/components/AlphaBanner.tsx` — thin banner with message and dismiss button, reads `sessionStorage.alphaBannerDismissed`
-- [ ] 5.2 Render `
` in the Journal root layout
-- [ ] 5.3 Add corresponding i18n strings
+- [x] 5.1 Create `apps/journal/app/components/AlphaBanner.tsx` — thin banner with message and dismiss button, reads `sessionStorage.alphaBannerDismissed`
+- [x] 5.2 Render `
` in the Journal root layout
+- [x] 5.3 Add corresponding i18n strings
## 6. Footer Links
-- [ ] 6.1 Create `apps/journal/app/components/Footer.tsx` with links to Impressum, Privacy, Terms
-- [ ] 6.2 Add the footer to the Journal root layout
-- [ ] 6.3 Add a footer to the Planner layout linking to the Journal's legal pages (absolute URLs, since Planner is stateless)
+- [x] 6.1 Create `apps/journal/app/components/Footer.tsx` with links to Impressum, Privacy, Terms
+- [x] 6.2 Add the footer to the Journal root layout
+- [x] 6.3 Add a footer to the Planner layout linking to the Journal's legal pages (absolute URLs, since Planner is stateless)
## 7. Review & Publish
diff --git a/packages/db/src/schema/journal.ts b/packages/db/src/schema/journal.ts
index 90d79a6..23365d3 100644
--- a/packages/db/src/schema/journal.ts
+++ b/packages/db/src/schema/journal.ts
@@ -29,6 +29,7 @@ export const users = journalSchema.table("users", {
displayName: text("display_name"),
bio: text("bio"),
domain: text("domain").notNull(),
+ termsAcceptedAt: timestamp("terms_accepted_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts
index 57c42f5..5db3adb 100644
--- a/packages/i18n/src/index.ts
+++ b/packages/i18n/src/index.ts
@@ -26,18 +26,34 @@ const commonOptions = {
/**
* Initialize i18next for server-side rendering.
* No language detector — language is determined from the request.
+ *
+ * Re-adds resource bundles on every call so dev-mode HMR edits to the
+ * locale files are reflected without a Node restart (the i18n singleton
+ * otherwise caches the resources from the first init call).
*/
export function initI18nServer(lng: SupportedLng = "en") {
if (i18n.isInitialized) {
+ syncResources();
i18n.changeLanguage(lng);
return;
}
i18n.use(initReactI18next).init({ ...commonOptions, lng });
}
+function syncResources() {
+ for (const [lang, namespaces] of Object.entries(resources)) {
+ for (const [ns, bundle] of Object.entries(namespaces)) {
+ i18n.addResourceBundle(lang, ns, bundle, true, true);
+ }
+ }
+}
+
/**
* Initialize i18next for the client.
* Uses LanguageDetector with htmlTag priority so hydration matches the server.
+ * No localStorage caching — we don't want to set non-essential storage
+ * without user consent. Language is resolved per-request from the server
+ * (Accept-Language → htmlTag) or the browser's navigator as a fallback.
*/
export function initI18nClient() {
if (i18n.isInitialized) return;
@@ -47,8 +63,8 @@ export function initI18nClient() {
.init({
...commonOptions,
detection: {
- order: ["htmlTag", "localStorage", "navigator"],
- caches: ["localStorage"],
+ order: ["htmlTag", "navigator"],
+ caches: [],
},
});
}
diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts
index dd6fabe..f17e32b 100644
--- a/packages/i18n/src/locales/de.ts
+++ b/packages/i18n/src/locales/de.ts
@@ -143,9 +143,12 @@ export default {
saveRoutesDesc: "Erstelle ein kostenloses Konto auf trails.cool, um Routen zu speichern, Aktivitäten zu verfolgen und mehr.",
goToJournal: "Zu trails.cool",
footer: {
+ imprint: "Impressum",
privacy: "Datenschutz",
+ terms: "Nutzungsbedingungen",
source: "Quellcode",
builtWith: "Erstellt mit BRouter und OpenStreetMap",
+ alpha: "Alpha-Version",
},
},
},
@@ -250,6 +253,16 @@ export default {
settings: "Einstellungen",
logout: "Abmelden",
},
+ alpha: {
+ message: "trails.cool befindet sich in früher Entwicklung — deine Daten können zurückgesetzt werden.",
+ },
+ footer: {
+ imprint: "Impressum",
+ privacy: "Datenschutz",
+ terms: "Nutzungsbedingungen",
+ source: "Quellcode",
+ alpha: "Alpha-Version",
+ },
auth: {
login: "Anmelden",
register: "Registrieren",
@@ -267,7 +280,14 @@ export default {
linkExpires: "Der Link ist 15 Minuten gültig.",
codeHelp: "Oder gib den 6-stelligen Code aus der E-Mail ein:",
verifyCode: "Code bestätigen",
+ termsBefore: "Ich habe die ",
+ termsLink: "Nutzungsbedingungen",
+ termsAfter: " gelesen und akzeptiere sie.",
+ termsRequired: "Du musst die Nutzungsbedingungen akzeptieren.",
noAccount: "Noch kein Konto?",
+ alreadyHaveAccount: "Bereits ein Konto?",
+ handleWillBe: "Dein Handle wird {{handle}} sein",
+ passkeyNotFound: "Für diese Seite wurde kein Passkey gefunden. Registriere ein neues Konto oder nutze stattdessen einen Magic Link.",
registerDescription: "Registriere dich mit einem Passkey — kein Passwort nötig.",
registerWithPasskey: "Mit Passkey registrieren",
creatingPasskey: "Erstelle Passkey...",
diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts
index b2dbf9c..a4c9a15 100644
--- a/packages/i18n/src/locales/en.ts
+++ b/packages/i18n/src/locales/en.ts
@@ -143,9 +143,12 @@ export default {
saveRoutesDesc: "Create a free account on trails.cool to keep your routes, track activities, and more.",
goToJournal: "Go to trails.cool",
footer: {
+ imprint: "Imprint",
privacy: "Privacy",
+ terms: "Terms",
source: "Source Code",
builtWith: "Built with BRouter and OpenStreetMap",
+ alpha: "Alpha version",
},
},
},
@@ -250,6 +253,16 @@ export default {
settings: "Settings",
logout: "Log Out",
},
+ alpha: {
+ message: "trails.cool is in early development — your data may be reset.",
+ },
+ footer: {
+ imprint: "Imprint",
+ privacy: "Privacy",
+ terms: "Terms",
+ source: "Source Code",
+ alpha: "Alpha version",
+ },
auth: {
login: "Sign In",
register: "Register",
@@ -267,7 +280,14 @@ export default {
linkExpires: "The link expires in 15 minutes.",
codeHelp: "Or enter the 6-digit code from the email:",
verifyCode: "Verify Code",
+ termsBefore: "I have read and accept the ",
+ termsLink: "Terms of Service",
+ termsAfter: ".",
+ termsRequired: "You must accept the Terms of Service to register.",
noAccount: "Don't have an account?",
+ alreadyHaveAccount: "Already have an account?",
+ handleWillBe: "Your handle will be {{handle}}",
+ passkeyNotFound: "No passkey found for this site. Register a new account or use a magic link instead.",
registerDescription: "Register with a passkey — no password needed.",
registerWithPasskey: "Register with Passkey",
creatingPasskey: "Creating passkey...",